mirror of
https://github.com/whyour/qinglong.git
synced 2026-08-11 19:05:20 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2370923a75 | ||
|
|
256e4e7a83 | ||
|
|
c0ec063333 | ||
|
|
576408de01 | ||
|
|
cf94ecfb11 | ||
|
|
118c92d9e5 | ||
|
|
280085668e | ||
|
|
bae4073a64 | ||
|
|
af3e358a6a | ||
|
|
64fcbff715 | ||
|
|
f9f78b4e05 | ||
|
|
fa83761d27 | ||
|
|
8173075b67 | ||
|
|
496918131f | ||
|
|
5d64aae452 | ||
|
|
952ea2859e | ||
|
|
25e8e8198d | ||
|
|
3ab5b0d86b | ||
|
|
6df651aa63 | ||
|
|
924dfd6a6f | ||
|
|
07f43538df | ||
|
|
4fa5fa2014 | ||
|
|
f9b9543a6d | ||
|
|
af97543918 | ||
|
|
a508522872 | ||
|
|
f1ca2134b7 | ||
|
|
a9755655b2 | ||
|
|
6a76e82f26 | ||
|
|
6775f5d123 | ||
|
|
ad6e08525c | ||
|
|
51ef4e7476 | ||
|
|
e5b35273f9 | ||
|
|
647ed3b66c | ||
|
|
4667af4ebe | ||
|
|
3f775a0e6c | ||
|
|
fa79de3f05 | ||
|
|
ffa8b25a66 | ||
|
|
05f8bbd26e | ||
|
|
7d43b14f81 | ||
|
|
cecc5aeb15 | ||
|
|
43d6ac2071 | ||
|
|
678e3e2dc6 | ||
|
|
75f91e1473 | ||
|
|
bdc45c538c | ||
|
|
955c7377d7 | ||
|
|
c71abd8c86 | ||
|
|
ab27a4c908 | ||
|
|
026640a757 | ||
|
|
a1501705c1 | ||
|
|
b321530dcf | ||
|
|
02c6ad8004 | ||
|
|
fff572869e | ||
|
|
b733937691 | ||
|
|
0d9eba4b6e | ||
|
|
20c6a1e8bf | ||
|
|
56bc2f0b1d | ||
|
|
ecc55883f0 | ||
|
|
185fd2ff91 | ||
|
|
3822c37fa0 | ||
|
|
4244502949 | ||
|
|
71dd82f74e | ||
|
|
6ea8d361fd | ||
|
|
3f71f9acdb | ||
|
|
418695c4aa |
@@ -20,10 +20,12 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: pixta-dev/repository-mirroring-action@v1
|
||||
- uses: wearerequired/git-mirror-action@v1
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.GITLAB_SSH_PK }}
|
||||
with:
|
||||
target_repo_url: git@gitlab.com:whyour/qinglong.git
|
||||
ssh_private_key: ${{ secrets.GITLAB_SSH_PK }}
|
||||
source-repo: https://github.com/whyour/qinglong.git
|
||||
destination-repo: git@gitlab.com:whyour/qinglong.git
|
||||
|
||||
to_gitee:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -31,10 +33,12 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: pixta-dev/repository-mirroring-action@v1
|
||||
- uses: wearerequired/git-mirror-action@v1
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.GITLAB_SSH_PK }}
|
||||
with:
|
||||
target_repo_url: git@gitee.com:whyour/qinglong.git
|
||||
ssh_private_key: ${{ secrets.GITLAB_SSH_PK }}
|
||||
source-repo: https://github.com/whyour/qinglong.git
|
||||
destination-repo: git@gitee.com:whyour/qinglong.git
|
||||
|
||||
build-static:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -27,3 +27,4 @@ __pycache__
|
||||
/shell/preload/env.*
|
||||
/shell/preload/notify.*
|
||||
/shell/preload/*-notify.json
|
||||
/shell/preload/__ql_notify__.*
|
||||
|
||||
+11
-11
@@ -2,22 +2,22 @@
|
||||
**/*.svg
|
||||
**/*.ejs
|
||||
**/*.html
|
||||
.umi
|
||||
.umi-production
|
||||
.umi-test
|
||||
.history
|
||||
.tmp
|
||||
node_modules
|
||||
/.umi
|
||||
/.umi-production
|
||||
/.umi-test
|
||||
/.history
|
||||
/.tmp
|
||||
/node_modules
|
||||
npm-debug.log*
|
||||
yarn-error.log
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
static
|
||||
data
|
||||
/static
|
||||
/data
|
||||
DS_Store
|
||||
src/.umi
|
||||
src/.umi-production
|
||||
src/.umi-test
|
||||
/src/.umi
|
||||
/src/.umi-production
|
||||
/src/.umi-test
|
||||
.env.local
|
||||
.env
|
||||
version.ts
|
||||
|
||||
@@ -43,19 +43,12 @@ export default defineConfig({
|
||||
}),
|
||||
);
|
||||
}) as any,
|
||||
externals: {
|
||||
react: 'window.React',
|
||||
'react-dom': 'window.ReactDOM',
|
||||
},
|
||||
headScripts: [
|
||||
`./api/env.js`,
|
||||
'https://gw.alipayobjects.com/os/lib/react/18.2.0/umd/react.production.min.js',
|
||||
'https://gw.alipayobjects.com/os/lib/react-dom/18.2.0/umd/react-dom.production.min.js',
|
||||
],
|
||||
headScripts: [`./api/env.js`],
|
||||
copy: [
|
||||
{
|
||||
from: 'node_modules/monaco-editor/min/vs',
|
||||
to: 'static/dist/monaco-editor/min/vs',
|
||||
},
|
||||
],
|
||||
npmClient: 'pnpm',
|
||||
});
|
||||
|
||||
+4
-127
@@ -56,138 +56,15 @@ npm i @whyour/qinglong
|
||||
|
||||
## Deployment
|
||||
|
||||
### Docker (Recommended)
|
||||
[View Documentation](https://qinglong.online/guide/getting-started/installation-guide)
|
||||
|
||||
```bash
|
||||
# curl -sSL get.docker.com | sh
|
||||
docker run -dit \
|
||||
-v $PWD/ql/data:/ql/data \
|
||||
# The 5700 after the colon is the default port, if QlPort is set, it needs to be the same as QlPort.
|
||||
-p 5700:5700 \
|
||||
# Deployment paths are not required, 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 \
|
||||
--hostname qinglong \
|
||||
--restart unless-stopped \
|
||||
whyour/qinglong:latest
|
||||
```
|
||||
## Built-in API
|
||||
|
||||
### BaoTa Panel one-click deployment (Recommended)
|
||||
|
||||
1. To install Pagoda Panel, go to the official website of [BaoTa Panel](https://www.bt.cn/u/EcDAFU), select the official version of the script to download and install.
|
||||
|
||||
2. After installation, login to Pagoda Panel, click `Docker` in the menu bar, the first time you enter, you will be prompted to install `Docker` service, click Install Now, follow the prompts to complete the installation.
|
||||
|
||||
3. After the installation is complete, find `Qinglong Panel` in the app shop, click Install, configure the domain name and other basic information to complete the installation.
|
||||
|
||||
### Docker-compose (Recommended)
|
||||
|
||||
```bash
|
||||
# curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
|
||||
mkdir qinglong
|
||||
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
|
||||
|
||||
# start
|
||||
docker-compose up -d
|
||||
# stop
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
### Podman (Recommended)
|
||||
|
||||
```bash
|
||||
# https://podman.io/getting-started/installation
|
||||
podman run -dit \
|
||||
--network bridge \
|
||||
-v $PWD/ql/data:/ql/data \
|
||||
# The 5700 after the colon is the default port, if QlPort is set, it needs to be the same as QlPort.
|
||||
-p 5700:5700 \
|
||||
# Deployment paths are not required, 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 \
|
||||
--hostname qinglong \
|
||||
docker.io/whyour/qinglong:latest
|
||||
```
|
||||
|
||||
### Npm (Local)
|
||||
|
||||
It is recommended to use a pure system installation to avoid losing the original system data, you need to install node/npm/python3/pip3/pnpm yourself
|
||||
|
||||
```bash
|
||||
# Debian/Ubuntu
|
||||
curl -sL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
||||
```
|
||||
|
||||
```bash
|
||||
npm install -g node-pre-gyp pnpm@8.3.1
|
||||
npm install -g @whyour/qinglong
|
||||
qinglong
|
||||
# Add the environment variables QL_DIR and QL_DATA_DIR when prompted
|
||||
export QL_DIR=""
|
||||
export QL_DATA_DIR=""
|
||||
# Run again
|
||||
qinglong
|
||||
```
|
||||
[View Documentation](https://qinglong.online/guide/user-guide/built-in-api)
|
||||
|
||||
## Built-in commands
|
||||
|
||||
- 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>
|
||||
# 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
|
||||
```
|
||||
|
||||
- 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
|
||||
```
|
||||
|
||||
| **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 |
|
||||
| extensions | Pull the branch of the repository |
|
||||
| branch | Number of days of logs to be kept |
|
||||
| days | File path for task execution |
|
||||
| file_path | The name of the environment variable that needs to be concurrent or specified at the time of task execution |
|
||||
[View Documentation](https://qinglong.online/guide/user-guide/basic-explanation)
|
||||
|
||||
## Development
|
||||
|
||||
|
||||
@@ -58,136 +58,15 @@ npm i @whyour/qinglong
|
||||
|
||||
## 部署
|
||||
|
||||
### docker (推荐)
|
||||
[查看文档](https://qinglong.online/guide/getting-started/installation-guide)
|
||||
|
||||
```bash
|
||||
# curl -sSL get.docker.com | sh
|
||||
docker run -dit \
|
||||
-v $PWD/ql/data:/ql/data \
|
||||
# 冒号后面的 5700 为默认端口,如果设置了 QlPort, 需要跟 QlPort 保持一致
|
||||
-p 5700:5700 \
|
||||
# 部署路径非必须,比如 /test
|
||||
-e QlBaseUrl="/" \
|
||||
# 部署端口非必须,当使用 host 模式时,可以设置服务启动后的端口,默认 5700
|
||||
-e QlPort="5700" \
|
||||
--name qinglong \
|
||||
--hostname qinglong \
|
||||
--restart unless-stopped \
|
||||
whyour/qinglong:latest
|
||||
```
|
||||
## 内置 API
|
||||
|
||||
### 宝塔面板一键部署(推荐)
|
||||
|
||||
1. 安装宝塔面板,前往 [宝塔面板](https://www.bt.cn/u/EcDAFU) 官网,选择正式版的脚本下载安装
|
||||
|
||||
2. 安装后登录宝塔面板,在菜单栏中点击 `Docker`,首次进入会提示安装`Docker`服务,点击立即安装,按提示完成安装
|
||||
|
||||
3. 安装完成后在应用商店中找到`青龙面板`,点击安装,配置域名等基本信息即可完成安装
|
||||
|
||||
### docker-compose (推荐)
|
||||
|
||||
```bash
|
||||
# curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
|
||||
mkdir qinglong
|
||||
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
|
||||
|
||||
# 启动
|
||||
docker-compose up -d
|
||||
# 停止
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
### podman (推荐)
|
||||
|
||||
```bash
|
||||
# https://podman.io/getting-started/installation
|
||||
podman run -dit \
|
||||
--network bridge \
|
||||
-v $PWD/ql/data:/ql/data \
|
||||
# 冒号后面的 5700 为默认端口,如果设置了 QlPort, 需要跟 QlPort 保持一致
|
||||
-p 5700:5700 \
|
||||
# 部署路径非必须,比如 /test
|
||||
-e QlBaseUrl="/" \
|
||||
# 部署端口非必须,当使用 host 模式时,可以设置服务启动后的端口,默认 5700
|
||||
-e QlPort="5700" \
|
||||
--name qinglong \
|
||||
--hostname qinglong \
|
||||
docker.io/whyour/qinglong:latest
|
||||
```
|
||||
|
||||
### npm (本地)
|
||||
|
||||
建议使用纯净系统安装,避免系统原有数据丢失,需要自己安装 node/npm/python3/pip3/pnpm
|
||||
|
||||
```bash
|
||||
# Debian/Ubuntu
|
||||
curl -sL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
||||
```
|
||||
|
||||
```bash
|
||||
npm install -g node-pre-gyp pnpm@8.3.1
|
||||
npm install -g @whyour/qinglong
|
||||
qinglong
|
||||
# 根据提示增加环境变量 QL_DIR 和 QL_DATA_DIR
|
||||
export QL_DIR=""
|
||||
export QL_DATA_DIR=""
|
||||
# 再次执行
|
||||
qinglong
|
||||
```
|
||||
[查看文档](https://qinglong.online/guide/user-guide/built-in-api)
|
||||
|
||||
## 内置命令
|
||||
|
||||
- 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>
|
||||
# 使用 -- 分割,-- 后面的参数会传给脚本,下面的例子,脚本就可接收到参数 -u whyour -p password
|
||||
task <file_path> -- -u whyour -p password
|
||||
```
|
||||
|
||||
- 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
|
||||
```
|
||||
|
||||
| **参数** | **说明** |
|
||||
|------------|---------------------------------------------------------------------------------------------|
|
||||
| file_url | 脚本地址 |
|
||||
| repo_url | 仓库地址 |
|
||||
| whitelist | 拉取仓库时的白名单,即就是需要拉取的脚本的路径包含的字符串,多个竖线分割 |
|
||||
| blacklist | 拉取仓库时的黑名单,即就是需要拉取的脚本的路径不包含的字符串,多个竖线分割 |
|
||||
| dependence | 拉取仓库需要的依赖文件,会直接从仓库拷贝到scripts下的仓库目录,不受黑名单影响,多个竖线分割 |
|
||||
| extensions | 拉取仓库的文件后缀,多个竖线分割 |
|
||||
| branch | 拉取仓库的分支 |
|
||||
| days | 需要保留的日志的天数 |
|
||||
| file_path | 任务执行时的文件路径 |
|
||||
[查看文档](https://qinglong.online/guide/user-guide/basic-explanation)
|
||||
|
||||
## 开发
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
## Reporting a Vulnerability
|
||||
|
||||
To report a vulnerability, please open a private vulnerability report at <https://github.com/whyour/qinglong/security>.
|
||||
|
||||
While the discovery of new vulnerabilities is rare, we also recommend always using the latest versions of Qinglong to ensure your application remains as secure as possible.
|
||||
@@ -0,0 +1,9 @@
|
||||
import 'express';
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
platform: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -7,6 +7,7 @@ import { celebrate, Joi } from 'celebrate';
|
||||
import { join } from 'path';
|
||||
import { SAMPLE_FILES } from '../config/const';
|
||||
import ConfigService from '../services/config';
|
||||
import { writeFileWithLock } from '../shared/utils';
|
||||
const route = Router();
|
||||
|
||||
export default (app: Router) => {
|
||||
@@ -77,7 +78,7 @@ export default (app: Router) => {
|
||||
if (name.startsWith('data/scripts/')) {
|
||||
path = join(config.rootPath, name);
|
||||
}
|
||||
await fs.writeFile(path, content);
|
||||
await writeFileWithLock(path, content);
|
||||
res.send({ code: 200, message: '保存成功' });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
|
||||
+11
-37
@@ -4,7 +4,8 @@ import { Logger } from 'winston';
|
||||
import CronService from '../services/cron';
|
||||
import CronViewService from '../services/cronView';
|
||||
import { celebrate, Joi } from 'celebrate';
|
||||
import cron_parser from 'cron-parser';
|
||||
import { commonCronSchema } from '../validation/schedule';
|
||||
|
||||
const route = Router();
|
||||
|
||||
export default (app: Router) => {
|
||||
@@ -170,27 +171,14 @@ export default (app: Router) => {
|
||||
route.post(
|
||||
'/',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
command: Joi.string().required(),
|
||||
schedule: Joi.string().required(),
|
||||
name: Joi.string().optional(),
|
||||
labels: Joi.array().optional(),
|
||||
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),
|
||||
}),
|
||||
body: Joi.object(commonCronSchema),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
if (cron_parser.parseExpression(req.body.schedule).hasNext()) {
|
||||
const cronService = Container.get(CronService);
|
||||
const data = await cronService.create(req.body);
|
||||
return res.send({ code: 200, data });
|
||||
} else {
|
||||
return res.send({ code: 400, message: 'param schedule error' });
|
||||
}
|
||||
const cronService = Container.get(CronService);
|
||||
const data = await cronService.create(req.body);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
@@ -331,30 +319,16 @@ export default (app: Router) => {
|
||||
'/',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
labels: Joi.array().optional().allow(null),
|
||||
command: Joi.string().required(),
|
||||
schedule: Joi.string().required(),
|
||||
name: Joi.string().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),
|
||||
...commonCronSchema,
|
||||
id: Joi.number().required(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
if (
|
||||
!req.body.schedule ||
|
||||
cron_parser.parseExpression(req.body.schedule).hasNext()
|
||||
) {
|
||||
const cronService = Container.get(CronService);
|
||||
const data = await cronService.update(req.body);
|
||||
return res.send({ code: 200, data });
|
||||
} else {
|
||||
return res.send({ code: 400, message: 'param schedule error' });
|
||||
}
|
||||
const cronService = Container.get(CronService);
|
||||
const data = await cronService.update(req.body);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
@@ -418,7 +392,7 @@ export default (app: Router) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cronService = Container.get(CronService);
|
||||
const data = await cronService.import_crontab();
|
||||
const data = await cronService.importCrontab();
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
|
||||
+68
-25
@@ -1,10 +1,15 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { celebrate, Joi } from 'celebrate';
|
||||
import { NextFunction, Request, Response, Router } from 'express';
|
||||
import { Container } from 'typedi';
|
||||
import { Logger } from 'winston';
|
||||
import config from '../config';
|
||||
import { getFileContentByName, readDirs, rmPath } from '../config/util';
|
||||
import { join, resolve } from 'path';
|
||||
import { celebrate, Joi } from 'celebrate';
|
||||
import {
|
||||
getFileContentByName,
|
||||
readDirs,
|
||||
removeAnsi,
|
||||
rmPath,
|
||||
} from '../config/util';
|
||||
import LogService from '../services/log';
|
||||
const route = Router();
|
||||
const blacklist = ['.tmp'];
|
||||
|
||||
@@ -29,20 +34,19 @@ export default (app: Router) => {
|
||||
'/detail',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const finalPath = resolve(
|
||||
config.logPath,
|
||||
const logService = Container.get(LogService);
|
||||
const finalPath = logService.checkFilePath(
|
||||
(req.query.path as string) || '',
|
||||
(req.query.file as string) || '',
|
||||
);
|
||||
|
||||
if (
|
||||
blacklist.includes(req.query.path as string) ||
|
||||
!finalPath.startsWith(config.logPath)
|
||||
) {
|
||||
return res.send({ code: 403, message: '暂无权限' });
|
||||
if (!finalPath || blacklist.includes(req.query.path as string)) {
|
||||
return res.send({
|
||||
code: 403,
|
||||
message: '暂无权限',
|
||||
});
|
||||
}
|
||||
const content = await getFileContentByName(finalPath);
|
||||
res.send({ code: 200, data: content });
|
||||
res.send({ code: 200, data: removeAnsi(content) });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
@@ -53,16 +57,16 @@ export default (app: Router) => {
|
||||
'/:file',
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const finalPath = resolve(
|
||||
config.logPath,
|
||||
const logService = Container.get(LogService);
|
||||
const finalPath = logService.checkFilePath(
|
||||
(req.query.path as string) || '',
|
||||
(req.params.file as string) || '',
|
||||
(req.query.file as string) || '',
|
||||
);
|
||||
if (
|
||||
blacklist.includes(req.path) ||
|
||||
!finalPath.startsWith(config.logPath)
|
||||
) {
|
||||
return res.send({ code: 403, message: '暂无权限' });
|
||||
if (!finalPath || blacklist.includes(req.query.path as string)) {
|
||||
return res.send({
|
||||
code: 403,
|
||||
message: '暂无权限',
|
||||
});
|
||||
}
|
||||
const content = await getFileContentByName(finalPath);
|
||||
res.send({ code: 200, data: content });
|
||||
@@ -83,17 +87,56 @@ export default (app: Router) => {
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
let { filename, path, type } = req.body as {
|
||||
let { filename, path } = req.body as {
|
||||
filename: string;
|
||||
path: string;
|
||||
type: string;
|
||||
};
|
||||
const filePath = join(config.logPath, path, filename);
|
||||
await rmPath(filePath);
|
||||
const logService = Container.get(LogService);
|
||||
const finalPath = logService.checkFilePath(filename, path);
|
||||
if (!finalPath || blacklist.includes(path)) {
|
||||
return res.send({
|
||||
code: 403,
|
||||
message: '暂无权限',
|
||||
});
|
||||
}
|
||||
await rmPath(finalPath);
|
||||
res.send({ code: 200 });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.post(
|
||||
'/download',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
filename: Joi.string().required(),
|
||||
path: Joi.string().allow(''),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
let { filename, path } = req.body as {
|
||||
filename: string;
|
||||
path: string;
|
||||
};
|
||||
const logService = Container.get(LogService);
|
||||
const filePath = logService.checkFilePath(path, filename);
|
||||
if (!filePath) {
|
||||
return res.send({
|
||||
code: 403,
|
||||
message: '暂无权限',
|
||||
});
|
||||
}
|
||||
return res.download(filePath, filename, (err) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
+39
-25
@@ -1,4 +1,4 @@
|
||||
import { fileExist, readDirs, readDir, rmPath } from '../config/util';
|
||||
import { fileExist, readDirs, readDir, rmPath, IFile } from '../config/util';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { Container } from 'typedi';
|
||||
import { Logger } from 'winston';
|
||||
@@ -8,6 +8,7 @@ import { celebrate, Joi } from 'celebrate';
|
||||
import path, { join, parse } from 'path';
|
||||
import ScriptService from '../services/script';
|
||||
import multer from 'multer';
|
||||
import { writeFileWithLock } from '../shared/utils';
|
||||
const route = Router();
|
||||
|
||||
const storage = multer.diskStorage({
|
||||
@@ -26,7 +27,7 @@ export default (app: Router) => {
|
||||
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
let result = [];
|
||||
let result: IFile[] = [];
|
||||
const blacklist = [
|
||||
'node_modules',
|
||||
'.git',
|
||||
@@ -101,7 +102,6 @@ export default (app: Router) => {
|
||||
'/',
|
||||
upload.single('file'),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
let { filename, path, content, originFilename, directory } =
|
||||
req.body as {
|
||||
@@ -123,8 +123,8 @@ export default (app: Router) => {
|
||||
}
|
||||
if (config.writePathList.every((x) => !path.startsWith(x))) {
|
||||
return res.send({
|
||||
code: 430,
|
||||
message: '文件路径禁止访问',
|
||||
code: 403,
|
||||
message: '暂无权限',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@ export default (app: Router) => {
|
||||
await rmPath(originFilePath);
|
||||
}
|
||||
}
|
||||
await fs.writeFile(filePath, content);
|
||||
await writeFileWithLock(filePath, content);
|
||||
return res.send({ code: 200 });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
@@ -174,15 +174,21 @@ export default (app: Router) => {
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
let { filename, content, path } = req.body as {
|
||||
filename: string;
|
||||
content: string;
|
||||
path: string;
|
||||
};
|
||||
const filePath = join(config.scriptPath, path, filename);
|
||||
await fs.writeFile(filePath, content);
|
||||
const scriptService = Container.get(ScriptService);
|
||||
const filePath = scriptService.checkFilePath(path, filename);
|
||||
if (!filePath) {
|
||||
return res.send({
|
||||
code: 403,
|
||||
message: '暂无权限',
|
||||
});
|
||||
}
|
||||
await writeFileWithLock(filePath, content);
|
||||
return res.send({ code: 200 });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
@@ -200,14 +206,19 @@ export default (app: Router) => {
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
let { filename, path, type } = req.body as {
|
||||
let { filename, path } = req.body as {
|
||||
filename: string;
|
||||
path: string;
|
||||
type: string;
|
||||
};
|
||||
const filePath = join(config.scriptPath, path, filename);
|
||||
const scriptService = Container.get(ScriptService);
|
||||
const filePath = scriptService.checkFilePath(path, filename);
|
||||
if (!filePath) {
|
||||
return res.send({
|
||||
code: 403,
|
||||
message: '暂无权限',
|
||||
});
|
||||
}
|
||||
await rmPath(filePath);
|
||||
res.send({ code: 200 });
|
||||
} catch (e) {
|
||||
@@ -221,24 +232,27 @@ export default (app: Router) => {
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
filename: Joi.string().required(),
|
||||
path: Joi.string().allow(''),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
let { filename } = req.body as {
|
||||
let { filename, path } = req.body as {
|
||||
filename: string;
|
||||
path: string;
|
||||
};
|
||||
const filePath = join(config.scriptPath, filename);
|
||||
// const stats = fs.statSync(filePath);
|
||||
// res.set({
|
||||
// 'Content-Type': 'application/octet-stream', //告诉浏览器这是一个二进制文件
|
||||
// 'Content-Disposition': 'attachment; filename=' + filename, //告诉浏览器这是一个需要下载的文件
|
||||
// 'Content-Length': stats.size //文件大小
|
||||
// });
|
||||
// fs.createReadStream(filePath).pipe(res);
|
||||
const scriptService = Container.get(ScriptService);
|
||||
const filePath = scriptService.checkFilePath(path, filename);
|
||||
if (!filePath) {
|
||||
return res.send({
|
||||
code: 403,
|
||||
message: '暂无权限',
|
||||
});
|
||||
}
|
||||
return res.download(filePath, filename, (err) => {
|
||||
return next(err);
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
@@ -261,7 +275,7 @@ export default (app: Router) => {
|
||||
let { filename, content, path } = req.body;
|
||||
const { name, ext } = parse(filename);
|
||||
const filePath = join(config.scriptPath, path, `${name}.swap${ext}`);
|
||||
await fs.writeFile(filePath, content || '', { encoding: 'utf8' });
|
||||
await writeFileWithLock(filePath, content || '');
|
||||
|
||||
const scriptService = Container.get(ScriptService);
|
||||
const result = await scriptService.runScript(filePath);
|
||||
|
||||
+39
-1
@@ -33,7 +33,7 @@ export default (app: Router) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const userService = Container.get(UserService);
|
||||
const authInfo = await userService.getUserInfo();
|
||||
const authInfo = await userService.getAuthInfo();
|
||||
const { version, changeLog, changeLogLink, publishTime } =
|
||||
await parseVersion(config.versionFile);
|
||||
|
||||
@@ -377,4 +377,42 @@ export default (app: Router) => {
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/auth/reset',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
retries: Joi.number().optional(),
|
||||
twoFactorActivated: Joi.boolean().optional(),
|
||||
password: Joi.string().optional(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const userService = Container.get(UserService);
|
||||
await userService.resetAuthInfo(req.body);
|
||||
res.send({ code: 200, message: '更新成功' });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/config/timezone',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
timezone: Joi.string().allow('').allow(null),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const systemService = Container.get(SystemService);
|
||||
const result = await systemService.updateTimezone(req.body);
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
+1
-1
@@ -90,7 +90,7 @@ export default (app: Router) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const userService = Container.get(UserService);
|
||||
const authInfo = await userService.getUserInfo();
|
||||
const authInfo = await userService.getAuthInfo();
|
||||
res.send({
|
||||
code: 200,
|
||||
data: {
|
||||
|
||||
+1
-1
@@ -1,4 +1,3 @@
|
||||
import './loaders/sentry'
|
||||
import 'reflect-metadata'; // We need this in order to use @Decorators
|
||||
import config from './config';
|
||||
import express from 'express';
|
||||
@@ -18,6 +17,7 @@ async function startServer() {
|
||||
Logger.debug(`✌️ 后端服务启动成功!`);
|
||||
console.debug(`✌️ 后端服务启动成功!`);
|
||||
process.send?.('ready');
|
||||
require('./loaders/bootAfter').default();
|
||||
})
|
||||
.on('error', (err) => {
|
||||
Logger.error(err);
|
||||
|
||||
@@ -42,8 +42,8 @@ const systemLogPath = path.join(dataPath, 'syslog/');
|
||||
const envFile = path.join(preloadPath, 'env.sh');
|
||||
const jsEnvFile = path.join(preloadPath, 'env.js');
|
||||
const pyEnvFile = path.join(preloadPath, 'env.py');
|
||||
const jsNotifyFile = path.join(preloadPath, 'notify.js');
|
||||
const pyNotifyFile = path.join(preloadPath, 'notify.py');
|
||||
const jsNotifyFile = path.join(preloadPath, '__ql_notify__.js');
|
||||
const pyNotifyFile = path.join(preloadPath, '__ql_notify__.py');
|
||||
const confFile = path.join(configPath, 'config.sh');
|
||||
const crontabFile = path.join(configPath, 'crontab.list');
|
||||
const authConfigFile = path.join(configPath, 'auth.json');
|
||||
@@ -51,7 +51,6 @@ const extraFile = path.join(configPath, 'extra.sh');
|
||||
const confBakDir = path.join(dataPath, 'config/bak/');
|
||||
const sampleFile = path.join(samplePath, 'config.sample.sh');
|
||||
const sqliteFile = path.join(samplePath, 'database.sqlite');
|
||||
const systemNotifyFile = path.join(preloadPath, 'system-notify.json');
|
||||
|
||||
const authError = '错误的用户名密码,请重试';
|
||||
const loginFaild = '请先登录!';
|
||||
@@ -135,5 +134,4 @@ export default {
|
||||
sqliteFile,
|
||||
sshdPath,
|
||||
systemLogPath,
|
||||
systemNotifyFile,
|
||||
};
|
||||
|
||||
+46
-13
@@ -10,6 +10,7 @@ import { load } from 'js-yaml';
|
||||
import config from './index';
|
||||
import { TASK_COMMAND } from './const';
|
||||
import Logger from '../loaders/logger';
|
||||
import { writeFileWithLock } from '../shared/utils';
|
||||
|
||||
export * from './share';
|
||||
|
||||
@@ -21,6 +22,10 @@ export async function getFileContentByName(fileName: string) {
|
||||
return '';
|
||||
}
|
||||
|
||||
export function removeAnsi(text: string) {
|
||||
return text.replace(/\x1b\[\d+m/g, '');
|
||||
}
|
||||
|
||||
export async function getLastModifyFilePath(dir: string) {
|
||||
let filePath = '';
|
||||
|
||||
@@ -145,12 +150,19 @@ export function getPlatform(userAgent: string): 'mobile' | 'desktop' {
|
||||
system = 'android'; // android系统
|
||||
} else if (testUa(/ios|iphone|ipad|ipod|iwatch/g)) {
|
||||
system = 'ios'; // ios系统
|
||||
} else if (testUa(/openharmony/g)) {
|
||||
system = 'openharmony'; // openharmony系统
|
||||
}
|
||||
|
||||
let platform = 'desktop';
|
||||
if (system === 'windows' || system === 'macos' || system === 'linux') {
|
||||
platform = 'desktop';
|
||||
} else if (system === 'android' || system === 'ios' || testUa(/mobile/g)) {
|
||||
} else if (
|
||||
system === 'android' ||
|
||||
system === 'ios' ||
|
||||
system === 'openharmony' ||
|
||||
testUa(/mobile/g)
|
||||
) {
|
||||
platform = 'mobile';
|
||||
}
|
||||
|
||||
@@ -168,7 +180,7 @@ export async function fileExist(file: any) {
|
||||
|
||||
export async function createFile(file: string, data: string = '') {
|
||||
await fs.mkdir(path.dirname(file), { recursive: true });
|
||||
await fs.writeFile(file, data);
|
||||
await writeFileWithLock(file, data);
|
||||
}
|
||||
|
||||
export async function handleLogPath(
|
||||
@@ -225,19 +237,19 @@ enum FileType {
|
||||
'file',
|
||||
}
|
||||
|
||||
interface IFile {
|
||||
export interface IFile {
|
||||
title: string;
|
||||
key: string;
|
||||
type: 'directory' | 'file';
|
||||
parent: string;
|
||||
mtime: number;
|
||||
createTime: number;
|
||||
size?: number;
|
||||
children?: IFile[];
|
||||
}
|
||||
|
||||
export function dirSort(a: IFile, b: IFile): number {
|
||||
if (a.type === 'file' && b.type === 'file') {
|
||||
return b.mtime - a.mtime;
|
||||
return b.createTime - a.createTime;
|
||||
} else if (a.type === 'directory' && b.type === 'directory') {
|
||||
return a.title.localeCompare(b.title);
|
||||
} else {
|
||||
@@ -271,7 +283,7 @@ export async function readDirs(
|
||||
key,
|
||||
type: 'directory',
|
||||
parent: relativePath,
|
||||
mtime: stats.mtime.getTime(),
|
||||
createTime: stats.birthtime.getTime(),
|
||||
children: children.sort(sort),
|
||||
});
|
||||
} else {
|
||||
@@ -281,7 +293,7 @@ export async function readDirs(
|
||||
key,
|
||||
parent: relativePath,
|
||||
size: stats.size,
|
||||
mtime: stats.mtime.getTime(),
|
||||
createTime: stats.birthtime.getTime(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -469,13 +481,18 @@ export async function getUniqPath(
|
||||
command: string,
|
||||
id: string,
|
||||
): Promise<string> {
|
||||
let suffix = '';
|
||||
if (/^\d+$/.test(id)) {
|
||||
id = `_${id}`;
|
||||
} else {
|
||||
id = '';
|
||||
suffix = `_${id}`;
|
||||
}
|
||||
|
||||
let items = command.split(/ +/);
|
||||
|
||||
const maxTimeCommandIndex = items.findIndex((x) => x === '-m');
|
||||
if (maxTimeCommandIndex !== -1) {
|
||||
items = items.slice(maxTimeCommandIndex + 2);
|
||||
}
|
||||
|
||||
const items = command.split(/ +/);
|
||||
let str = items[0];
|
||||
if (items[0] === TASK_COMMAND) {
|
||||
str = items[1];
|
||||
@@ -499,7 +516,7 @@ export async function getUniqPath(
|
||||
str = `${tempStr}_${str.slice(slashIndex + 1)}`;
|
||||
}
|
||||
|
||||
return `${str}${id}`;
|
||||
return `${str}${suffix}`;
|
||||
}
|
||||
|
||||
export function safeJSONParse(value?: string) {
|
||||
@@ -510,7 +527,7 @@ export function safeJSONParse(value?: string) {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch (error) {
|
||||
Logger.error('[JSON.parse失败]', error);
|
||||
Logger.error('[safeJSONParse失败]', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
@@ -525,3 +542,19 @@ export async function rmPath(path: string) {
|
||||
Logger.error('[rmPath失败]', error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function setSystemTimezone(timezone: string): Promise<boolean> {
|
||||
try {
|
||||
if (!(await fileExist(`/usr/share/zoneinfo/${timezone}`))) {
|
||||
throw new Error('Invalid timezone');
|
||||
}
|
||||
|
||||
await promiseExec(`ln -sf /usr/share/zoneinfo/${timezone} /etc/localtime`);
|
||||
await promiseExec(`echo "${timezone}" > /etc/timezone`);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
Logger.error('[setSystemTimezone失败]', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ export class Env {
|
||||
this.timestamp = new Date().toString();
|
||||
this.position = options.position;
|
||||
this.name = options.name;
|
||||
this.remarks = options.remarks;
|
||||
this.remarks = options.remarks || '';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+26
-7
@@ -20,6 +20,8 @@ export enum NotificationMode {
|
||||
'feishu' = 'feishu',
|
||||
'webhook' = 'webhook',
|
||||
'chronocat' = 'Chronocat',
|
||||
'ntfy' = 'ntfy',
|
||||
'wxPusherBot' = 'wxPusherBot',
|
||||
}
|
||||
|
||||
abstract class NotificationBaseInfo {
|
||||
@@ -47,9 +49,8 @@ export class PushDeerNotification extends NotificationBaseInfo {
|
||||
public pushDeerUrl = '';
|
||||
}
|
||||
|
||||
export class ChatNotification extends NotificationBaseInfo {
|
||||
public chatUrl = '';
|
||||
public chatToken = '';
|
||||
export class synologyChatNotification extends NotificationBaseInfo {
|
||||
public synologyChatUrl = '';
|
||||
}
|
||||
|
||||
export class BarkNotification extends NotificationBaseInfo {
|
||||
@@ -59,7 +60,7 @@ export class BarkNotification extends NotificationBaseInfo {
|
||||
public barkGroup = 'qinglong';
|
||||
public barkLevel = 'active';
|
||||
public barkUrl = '';
|
||||
public barkArchive=""
|
||||
public barkArchive = '';
|
||||
}
|
||||
|
||||
export class TelegramBotNotification extends NotificationBaseInfo {
|
||||
@@ -99,6 +100,11 @@ export class IGotNotification extends NotificationBaseInfo {
|
||||
export class PushPlusNotification extends NotificationBaseInfo {
|
||||
public pushPlusToken = '';
|
||||
public pushPlusUser = '';
|
||||
public pushPlusTemplate = '';
|
||||
public pushplusChannel = '';
|
||||
public pushplusWebhook = '';
|
||||
public pushplusCallbackUrl = '';
|
||||
public pushplusTo = '';
|
||||
}
|
||||
|
||||
export class WePlusBotNotification extends NotificationBaseInfo {
|
||||
@@ -139,12 +145,24 @@ export class LarkNotification extends NotificationBaseInfo {
|
||||
public larkKey = '';
|
||||
}
|
||||
|
||||
export class NtfyNotification extends NotificationBaseInfo {
|
||||
public ntfyUrl = '';
|
||||
public ntfyTopic = '';
|
||||
public ntfyPriority = '';
|
||||
}
|
||||
|
||||
export class WxPusherBotNotification extends NotificationBaseInfo {
|
||||
public wxPusherBotAppToken = '';
|
||||
public wxPusherBotTopicIds = '';
|
||||
public wxPusherBotUids = '';
|
||||
}
|
||||
|
||||
export interface NotificationInfo
|
||||
extends GoCqHttpBotNotification,
|
||||
GotifyNotification,
|
||||
ServerChanNotification,
|
||||
PushDeerNotification,
|
||||
ChatNotification,
|
||||
synologyChatNotification,
|
||||
BarkNotification,
|
||||
TelegramBotNotification,
|
||||
DingtalkBotNotification,
|
||||
@@ -158,5 +176,6 @@ export interface NotificationInfo
|
||||
PushMeNotification,
|
||||
WebhookNotification,
|
||||
ChronocatNotification,
|
||||
LarkNotification {}
|
||||
|
||||
LarkNotification,
|
||||
NtfyNotification,
|
||||
WxPusherBotNotification {}
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ export interface AppToken {
|
||||
expiration: number;
|
||||
}
|
||||
|
||||
export type AppScope = 'envs' | 'crons' | 'configs' | 'scripts' | 'logs';
|
||||
export type AppScope = 'envs' | 'crons' | 'configs' | 'scripts' | 'logs' | 'system';
|
||||
|
||||
export interface AppInstance extends Model<App, App>, App {}
|
||||
export const AppModel = sequelize.define<AppInstance>('App', {
|
||||
|
||||
+23
-2
@@ -27,6 +27,7 @@ export enum AuthDataType {
|
||||
'notification' = 'notification',
|
||||
'removeLogFrequency' = 'removeLogFrequency',
|
||||
'systemConfig' = 'systemConfig',
|
||||
'authConfig' = 'authConfig',
|
||||
}
|
||||
|
||||
export interface SystemConfigInfo {
|
||||
@@ -36,6 +37,7 @@ export interface SystemConfigInfo {
|
||||
nodeMirror?: string;
|
||||
pythonMirror?: string;
|
||||
linuxMirror?: string;
|
||||
timezone?: string;
|
||||
}
|
||||
|
||||
export interface LoginLogInfo {
|
||||
@@ -46,11 +48,30 @@ export interface LoginLogInfo {
|
||||
status?: LoginStatus;
|
||||
}
|
||||
|
||||
export interface AuthInfo {
|
||||
username: string;
|
||||
password: string;
|
||||
retries: number;
|
||||
lastlogon: number;
|
||||
lastip: string;
|
||||
lastaddr: string;
|
||||
platform: string;
|
||||
isTwoFactorChecking: boolean;
|
||||
token: string;
|
||||
tokens: Record<string, string>;
|
||||
twoFactorActivated: boolean;
|
||||
twoFactorSecret: string;
|
||||
avatar: string;
|
||||
}
|
||||
|
||||
export type SystemModelInfo = SystemConfigInfo &
|
||||
Partial<NotificationInfo> &
|
||||
LoginLogInfo;
|
||||
LoginLogInfo &
|
||||
Partial<AuthInfo>;
|
||||
|
||||
export interface SystemInstance extends Model<SystemInfo, SystemInfo>, SystemInfo { }
|
||||
export interface SystemInstance
|
||||
extends Model<SystemInfo, SystemInfo>,
|
||||
SystemInfo {}
|
||||
export const SystemModel = sequelize.define<SystemInstance>('Auth', {
|
||||
ip: DataTypes.STRING,
|
||||
type: DataTypes.STRING,
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
export enum ScheduleType {
|
||||
BOOT = '@boot',
|
||||
ONCE = '@once',
|
||||
}
|
||||
|
||||
export type ScheduleValidator = (schedule?: string) => boolean;
|
||||
export type CronSchedulerPayload = {
|
||||
name: string;
|
||||
id: string;
|
||||
schedule: string;
|
||||
command: string;
|
||||
extra_schedules: Array<{ schedule: string }>;
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import Container from 'typedi';
|
||||
import CronService from '../services/cron';
|
||||
|
||||
export default async () => {
|
||||
const cronService = Container.get(CronService);
|
||||
|
||||
await cronService.bootTask();
|
||||
};
|
||||
+7
-81
@@ -1,15 +1,11 @@
|
||||
import Logger from './logger';
|
||||
import path from 'path';
|
||||
import DataStore from 'nedb';
|
||||
import { EnvModel } from '../data/env';
|
||||
import { CrontabModel } from '../data/cron';
|
||||
import { DependenceModel } from '../data/dependence';
|
||||
import { AppModel } from '../data/open';
|
||||
import { SystemModel } from '../data/system';
|
||||
import { fileExist } from '../config/util';
|
||||
import { SubscriptionModel } from '../data/subscription';
|
||||
import { CrontabViewModel } from '../data/cronView';
|
||||
import config from '../config';
|
||||
import { sequelize } from '../data';
|
||||
|
||||
export default async () => {
|
||||
@@ -48,88 +44,18 @@ export default async () => {
|
||||
} catch (error) {}
|
||||
try {
|
||||
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) { }
|
||||
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) { }
|
||||
} catch (error) {}
|
||||
try {
|
||||
await sequelize.query('alter table Crontabs add column task_after TEXT');
|
||||
} catch (error) { }
|
||||
|
||||
// 2.10-2.11 升级
|
||||
const cronDbFile = path.join(config.rootPath, 'db/crontab.db');
|
||||
const envDbFile = path.join(config.rootPath, 'db/env.db');
|
||||
const appDbFile = path.join(config.rootPath, 'db/app.db');
|
||||
const authDbFile = path.join(config.rootPath, 'db/auth.db');
|
||||
const dependenceDbFile = path.join(config.rootPath, 'db/dependence.db');
|
||||
const crondbExist = await fileExist(cronDbFile);
|
||||
const dependenceDbExist = await fileExist(dependenceDbFile);
|
||||
const envDbExist = await fileExist(envDbFile);
|
||||
const appDbExist = await fileExist(appDbFile);
|
||||
const authDbExist = await fileExist(authDbFile);
|
||||
|
||||
const cronCount = await CrontabModel.count();
|
||||
const dependenceCount = await DependenceModel.count();
|
||||
const envCount = await EnvModel.count();
|
||||
const appCount = await AppModel.count();
|
||||
const authCount = await SystemModel.count();
|
||||
if (crondbExist && cronCount === 0) {
|
||||
const cronDb = new DataStore({
|
||||
filename: cronDbFile,
|
||||
autoload: true,
|
||||
});
|
||||
cronDb.persistence.compactDatafile();
|
||||
cronDb.find({}).exec(async (err, docs) => {
|
||||
await CrontabModel.bulkCreate(docs, { ignoreDuplicates: true });
|
||||
});
|
||||
}
|
||||
|
||||
if (dependenceDbExist && dependenceCount === 0) {
|
||||
const dependenceDb = new DataStore({
|
||||
filename: dependenceDbFile,
|
||||
autoload: true,
|
||||
});
|
||||
dependenceDb.persistence.compactDatafile();
|
||||
dependenceDb.find({}).exec(async (err, docs) => {
|
||||
await DependenceModel.bulkCreate(docs, { ignoreDuplicates: true });
|
||||
});
|
||||
}
|
||||
|
||||
if (envDbExist && envCount === 0) {
|
||||
const envDb = new DataStore({
|
||||
filename: envDbFile,
|
||||
autoload: true,
|
||||
});
|
||||
envDb.persistence.compactDatafile();
|
||||
envDb.find({}).exec(async (err, docs) => {
|
||||
await EnvModel.bulkCreate(docs, { ignoreDuplicates: true });
|
||||
});
|
||||
}
|
||||
|
||||
if (appDbExist && appCount === 0) {
|
||||
const appDb = new DataStore({
|
||||
filename: appDbFile,
|
||||
autoload: true,
|
||||
});
|
||||
appDb.persistence.compactDatafile();
|
||||
appDb.find({}).exec(async (err, docs) => {
|
||||
await AppModel.bulkCreate(docs, { ignoreDuplicates: true });
|
||||
});
|
||||
}
|
||||
|
||||
if (authDbExist && authCount === 0) {
|
||||
const authDb = new DataStore({
|
||||
filename: authDbFile,
|
||||
autoload: true,
|
||||
});
|
||||
authDb.persistence.compactDatafile();
|
||||
authDb.find({}).exec(async (err, docs) => {
|
||||
await SystemModel.bulkCreate(docs, { ignoreDuplicates: true });
|
||||
});
|
||||
}
|
||||
} catch (error) {}
|
||||
|
||||
console.log('✌️ DB loaded');
|
||||
Logger.info('✌️ DB loaded');
|
||||
|
||||
+16
-21
@@ -3,19 +3,14 @@ import bodyParser from 'body-parser';
|
||||
import cors from 'cors';
|
||||
import routes from '../api';
|
||||
import config from '../config';
|
||||
import jwt, { UnauthorizedError } from 'express-jwt';
|
||||
import fs from 'fs/promises';
|
||||
import { getPlatform, getToken, safeJSONParse } from '../config/util';
|
||||
import Container from 'typedi';
|
||||
import OpenService from '../services/open';
|
||||
import { UnauthorizedError, expressjwt } from 'express-jwt';
|
||||
import { getPlatform, getToken } from '../config/util';
|
||||
import rewrite from 'express-urlrewrite';
|
||||
import UserService from '../services/user';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { EnvModel } from '../data/env';
|
||||
import { errors } from 'celebrate';
|
||||
import { createProxyMiddleware } from 'http-proxy-middleware';
|
||||
import { serveEnv } from '../config/serverEnv';
|
||||
import Logger from './logger';
|
||||
import { IKeyvStore, shareStore } from '../shared/store';
|
||||
|
||||
export default ({ app }: { app: Application }) => {
|
||||
app.set('trust proxy', 'loopback');
|
||||
@@ -29,7 +24,7 @@ export default ({ app }: { app: Application }) => {
|
||||
target: `http://0.0.0.0:${config.publicPort}/api`,
|
||||
changeOrigin: true,
|
||||
pathRewrite: { '/api/public': '' },
|
||||
logProvider: () => Logger,
|
||||
logger: Logger,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -37,7 +32,7 @@ export default ({ app }: { app: Application }) => {
|
||||
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
|
||||
|
||||
app.use(
|
||||
jwt({
|
||||
expressjwt({
|
||||
secret: config.secret,
|
||||
algorithms: ['HS384'],
|
||||
}).unless({
|
||||
@@ -58,8 +53,10 @@ export default ({ app }: { app: Application }) => {
|
||||
app.use(async (req, res, next) => {
|
||||
const headerToken = getToken(req);
|
||||
if (req.path.startsWith('/open/')) {
|
||||
const openService = Container.get(OpenService);
|
||||
const doc = await openService.findTokenByValue(headerToken);
|
||||
const apps = await shareStore.getApps();
|
||||
const doc = apps?.filter((x) =>
|
||||
x.tokens?.find((y) => y.value === headerToken),
|
||||
)?.[0];
|
||||
if (doc && doc.tokens && doc.tokens.length > 0) {
|
||||
const currentToken = doc.tokens.find((x) => x.value === headerToken);
|
||||
const keyMatch = req.path.match(/\/open\/([a-z]+)\/*/);
|
||||
@@ -83,9 +80,9 @@ export default ({ app }: { app: Application }) => {
|
||||
return next();
|
||||
}
|
||||
|
||||
const data = await fs.readFile(config.authConfigFile, 'utf8');
|
||||
if (data && headerToken) {
|
||||
const { token = '', tokens = {} } = safeJSONParse(data);
|
||||
const authInfo = await shareStore.getAuthInfo();
|
||||
if (authInfo && headerToken) {
|
||||
const { token = '', tokens = {} } = authInfo;
|
||||
if (headerToken === token || tokens[req.platform] === headerToken) {
|
||||
return next();
|
||||
}
|
||||
@@ -103,8 +100,8 @@ export default ({ app }: { app: Application }) => {
|
||||
if (!['/api/user/init', '/api/user/notification/init'].includes(req.path)) {
|
||||
return next();
|
||||
}
|
||||
const userService = Container.get(UserService);
|
||||
const authInfo = await userService.getUserInfo();
|
||||
const authInfo =
|
||||
(await shareStore.getAuthInfo()) || ({} as IKeyvStore['authInfo']);
|
||||
|
||||
let isInitialized = true;
|
||||
if (
|
||||
@@ -133,8 +130,6 @@ export default ({ app }: { app: Application }) => {
|
||||
|
||||
app.use(errors());
|
||||
|
||||
Sentry.setupExpressErrorHandler(app);
|
||||
|
||||
app.use(
|
||||
(
|
||||
err: Error & { status: number },
|
||||
@@ -164,8 +159,8 @@ export default ({ app }: { app: Application }) => {
|
||||
.status(500)
|
||||
.send({
|
||||
code: 400,
|
||||
message: `${err.name} ${err.message}`,
|
||||
validation: err.errors,
|
||||
message: `${err.message}`,
|
||||
errors: err.errors,
|
||||
})
|
||||
.end();
|
||||
}
|
||||
|
||||
@@ -12,7 +12,12 @@ import { initPosition } from '../data/env';
|
||||
import { AuthDataType, SystemModel } from '../data/system';
|
||||
import SystemService from '../services/system';
|
||||
import UserService from '../services/user';
|
||||
import { writeFile } from 'fs/promises';
|
||||
import { writeFile, readFile } from 'fs/promises';
|
||||
import { createRandomString, safeJSONParse } from '../config/util';
|
||||
import OpenService from '../services/open';
|
||||
import { shareStore } from '../shared/store';
|
||||
import Logger from './logger';
|
||||
import { AppModel } from '../data/open';
|
||||
|
||||
export default async () => {
|
||||
const cronService = Container.get(CronService);
|
||||
@@ -20,14 +25,48 @@ export default async () => {
|
||||
const dependenceService = Container.get(DependenceService);
|
||||
const systemService = Container.get(SystemService);
|
||||
const userService = Container.get(UserService);
|
||||
const openService = Container.get(OpenService);
|
||||
|
||||
// 初始化增加系统配置
|
||||
await SystemModel.upsert({ type: AuthDataType.systemConfig });
|
||||
await SystemModel.upsert({ type: AuthDataType.notification });
|
||||
|
||||
// 初始化通知配置
|
||||
const notifyConfig = await userService.getNotificationMode();
|
||||
await writeFile(config.systemNotifyFile, JSON.stringify(notifyConfig));
|
||||
let systemApp = (
|
||||
await AppModel.findOne({
|
||||
where: { name: 'system' },
|
||||
})
|
||||
)?.get({ plain: true });
|
||||
if (!systemApp) {
|
||||
systemApp = await AppModel.create({
|
||||
name: 'system',
|
||||
scopes: ['crons', 'system'],
|
||||
client_id: createRandomString(12, 12),
|
||||
client_secret: createRandomString(24, 24),
|
||||
});
|
||||
}
|
||||
const [systemConfig] = await SystemModel.findOrCreate({
|
||||
where: { type: AuthDataType.systemConfig },
|
||||
});
|
||||
await SystemModel.findOrCreate({
|
||||
where: { type: AuthDataType.notification },
|
||||
});
|
||||
const [authConfig] = await SystemModel.findOrCreate({
|
||||
where: { type: AuthDataType.authConfig },
|
||||
});
|
||||
if (!authConfig?.info) {
|
||||
let authInfo = {
|
||||
username: 'admin',
|
||||
password: 'admin',
|
||||
};
|
||||
try {
|
||||
const content = await readFile(config.authConfigFile, 'utf8');
|
||||
authInfo = safeJSONParse(content);
|
||||
} catch (error) {
|
||||
Logger.warn('Failed to read auth config file, using default credentials');
|
||||
}
|
||||
await SystemModel.upsert({
|
||||
id: authConfig?.id,
|
||||
info: authInfo,
|
||||
type: AuthDataType.authConfig,
|
||||
});
|
||||
}
|
||||
|
||||
const installDependencies = () => {
|
||||
// 初始化时安装所有处于安装中,安装成功,安装失败的依赖
|
||||
@@ -50,7 +89,6 @@ export default async () => {
|
||||
};
|
||||
|
||||
// 初始化更新 linux/python/nodejs 镜像源配置
|
||||
const systemConfig = await systemService.getSystemConfig();
|
||||
if (systemConfig.info?.pythonMirror) {
|
||||
systemService.updatePythonMirror({
|
||||
pythonMirror: systemConfig.info?.pythonMirror,
|
||||
@@ -169,4 +207,11 @@ export default async () => {
|
||||
// 初始化保存一次ck和定时任务数据
|
||||
await cronService.autosave_crontab();
|
||||
await envService.set_envs();
|
||||
|
||||
const authInfo = await userService.getAuthInfo();
|
||||
const apps = await openService.findApps();
|
||||
await shareStore.updateAuthInfo(authInfo);
|
||||
if (apps?.length) {
|
||||
await shareStore.updateApps(apps);
|
||||
}
|
||||
};
|
||||
|
||||
+79
-105
@@ -3,6 +3,7 @@ import path from 'path';
|
||||
import os from 'os';
|
||||
import Logger from './logger';
|
||||
import { fileExist } from '../config/util';
|
||||
import { writeFileWithLock } from '../shared/utils';
|
||||
|
||||
const rootPath = process.env.QL_DIR as string;
|
||||
let dataPath = path.join(rootPath, 'data/');
|
||||
@@ -20,16 +21,14 @@ const bakPath = path.join(dataPath, 'bak/');
|
||||
const samplePath = path.join(rootPath, 'sample/');
|
||||
const tmpPath = path.join(logPath, '.tmp/');
|
||||
const confFile = path.join(configPath, 'config.sh');
|
||||
const authConfigFile = path.join(configPath, 'auth.json');
|
||||
const sampleConfigFile = path.join(samplePath, 'config.sample.sh');
|
||||
const sampleAuthFile = path.join(samplePath, 'auth.sample.json');
|
||||
const sampleTaskShellFile = path.join(samplePath, 'task.sample.sh');
|
||||
const sampleNotifyJsFile = path.join(samplePath, 'notify.js');
|
||||
const sampleNotifyPyFile = path.join(samplePath, 'notify.py');
|
||||
const scriptNotifyJsFile = path.join(scriptPath, 'sendNotify.js');
|
||||
const scriptNotifyPyFile = path.join(scriptPath, 'notify.py');
|
||||
const jsNotifyFile = path.join(preloadPath, 'notify.js');
|
||||
const pyNotifyFile = path.join(preloadPath, 'notify.py');
|
||||
const jsNotifyFile = path.join(preloadPath, '__ql_notify__.js');
|
||||
const pyNotifyFile = path.join(preloadPath, '__ql_notify__.py');
|
||||
const TaskBeforeFile = path.join(configPath, 'task_before.sh');
|
||||
const TaskBeforeJsFile = path.join(configPath, 'task_before.js');
|
||||
const TaskBeforePyFile = path.join(configPath, 'task_before.py');
|
||||
@@ -39,112 +38,87 @@ const sshPath = path.resolve(homedir, '.ssh');
|
||||
const sshdPath = path.join(dataPath, 'ssh.d');
|
||||
const systemLogPath = path.join(dataPath, 'syslog');
|
||||
|
||||
export default async () => {
|
||||
const authFileExist = await fileExist(authConfigFile);
|
||||
const confFileExist = await fileExist(confFile);
|
||||
const scriptDirExist = await fileExist(scriptPath);
|
||||
const preloadDirExist = await fileExist(preloadPath);
|
||||
const logDirExist = await fileExist(logPath);
|
||||
const configDirExist = await fileExist(configPath);
|
||||
const uploadDirExist = await fileExist(uploadPath);
|
||||
const sshDirExist = await fileExist(sshPath);
|
||||
const bakDirExist = await fileExist(bakPath);
|
||||
const sshdDirExist = await fileExist(sshdPath);
|
||||
const systemLogDirExist = await fileExist(systemLogPath);
|
||||
const tmpDirExist = await fileExist(tmpPath);
|
||||
const scriptNotifyJsFileExist = await fileExist(scriptNotifyJsFile);
|
||||
const scriptNotifyPyFileExist = await fileExist(scriptNotifyPyFile);
|
||||
const TaskBeforeFileExist = await fileExist(TaskBeforeFile);
|
||||
const TaskBeforeJsFileExist = await fileExist(TaskBeforeJsFile);
|
||||
const TaskBeforePyFileExist = await fileExist(TaskBeforePyFile);
|
||||
const TaskAfterFileExist = await fileExist(TaskAfterFile);
|
||||
const directories = [
|
||||
configPath,
|
||||
scriptPath,
|
||||
preloadPath,
|
||||
logPath,
|
||||
tmpPath,
|
||||
uploadPath,
|
||||
sshPath,
|
||||
bakPath,
|
||||
sshdPath,
|
||||
systemLogPath,
|
||||
];
|
||||
|
||||
if (!configDirExist) {
|
||||
await fs.mkdir(configPath);
|
||||
}
|
||||
|
||||
if (!scriptDirExist) {
|
||||
await fs.mkdir(scriptPath);
|
||||
}
|
||||
|
||||
if (!preloadDirExist) {
|
||||
await fs.mkdir(preloadPath);
|
||||
}
|
||||
|
||||
if (!logDirExist) {
|
||||
await fs.mkdir(logPath);
|
||||
}
|
||||
|
||||
if (!tmpDirExist) {
|
||||
await fs.mkdir(tmpPath);
|
||||
}
|
||||
|
||||
if (!uploadDirExist) {
|
||||
await fs.mkdir(uploadPath);
|
||||
}
|
||||
|
||||
if (!sshDirExist) {
|
||||
await fs.mkdir(sshPath);
|
||||
}
|
||||
|
||||
if (!bakDirExist) {
|
||||
await fs.mkdir(bakPath);
|
||||
}
|
||||
|
||||
if (!sshdDirExist) {
|
||||
await fs.mkdir(sshdPath);
|
||||
}
|
||||
|
||||
if (!systemLogDirExist) {
|
||||
await fs.mkdir(systemLogPath);
|
||||
}
|
||||
|
||||
// 初始化文件
|
||||
if (!authFileExist) {
|
||||
await fs.writeFile(authConfigFile, await fs.readFile(sampleAuthFile));
|
||||
}
|
||||
|
||||
if (!confFileExist) {
|
||||
await fs.writeFile(confFile, await fs.readFile(sampleConfigFile));
|
||||
}
|
||||
|
||||
await fs.writeFile(jsNotifyFile, await fs.readFile(sampleNotifyJsFile));
|
||||
await fs.writeFile(pyNotifyFile, await fs.readFile(sampleNotifyPyFile));
|
||||
|
||||
if (!scriptNotifyJsFileExist) {
|
||||
await fs.writeFile(
|
||||
scriptNotifyJsFile,
|
||||
await fs.readFile(sampleNotifyJsFile),
|
||||
);
|
||||
}
|
||||
|
||||
if (!scriptNotifyPyFileExist) {
|
||||
await fs.writeFile(
|
||||
scriptNotifyPyFile,
|
||||
await fs.readFile(sampleNotifyPyFile),
|
||||
);
|
||||
}
|
||||
|
||||
if (!TaskBeforeFileExist) {
|
||||
await fs.writeFile(TaskBeforeFile, await fs.readFile(sampleTaskShellFile));
|
||||
}
|
||||
|
||||
if (!TaskBeforeJsFileExist) {
|
||||
await fs.writeFile(
|
||||
TaskBeforeJsFile,
|
||||
const files = [
|
||||
{
|
||||
target: confFile,
|
||||
source: sampleConfigFile,
|
||||
checkExistence: true,
|
||||
},
|
||||
{
|
||||
target: jsNotifyFile,
|
||||
source: sampleNotifyJsFile,
|
||||
checkExistence: false,
|
||||
},
|
||||
{
|
||||
target: pyNotifyFile,
|
||||
source: sampleNotifyPyFile,
|
||||
checkExistence: false,
|
||||
},
|
||||
{
|
||||
target: scriptNotifyJsFile,
|
||||
source: sampleNotifyJsFile,
|
||||
checkExistence: true,
|
||||
},
|
||||
{
|
||||
target: scriptNotifyPyFile,
|
||||
source: sampleNotifyPyFile,
|
||||
checkExistence: true,
|
||||
},
|
||||
{
|
||||
target: TaskBeforeFile,
|
||||
source: sampleTaskShellFile,
|
||||
checkExistence: true,
|
||||
},
|
||||
{
|
||||
target: TaskBeforeJsFile,
|
||||
content:
|
||||
'// The JavaScript code that executes before the JavaScript task execution will execute.',
|
||||
);
|
||||
}
|
||||
|
||||
if (!TaskBeforePyFileExist) {
|
||||
await fs.writeFile(
|
||||
TaskBeforePyFile,
|
||||
checkExistence: true,
|
||||
},
|
||||
{
|
||||
target: TaskBeforePyFile,
|
||||
content:
|
||||
'# The Python code that executes before the Python task execution will execute.',
|
||||
);
|
||||
checkExistence: true,
|
||||
},
|
||||
{
|
||||
target: TaskAfterFile,
|
||||
source: sampleTaskShellFile,
|
||||
checkExistence: true,
|
||||
},
|
||||
];
|
||||
|
||||
export default async () => {
|
||||
for (const dirPath of directories) {
|
||||
if (!(await fileExist(dirPath))) {
|
||||
await fs.mkdir(dirPath);
|
||||
}
|
||||
}
|
||||
|
||||
if (!TaskAfterFileExist) {
|
||||
await fs.writeFile(TaskAfterFile, await fs.readFile(sampleTaskShellFile));
|
||||
for (const item of files) {
|
||||
const exists = await fileExist(item.target);
|
||||
if (!item.checkExistence || !exists) {
|
||||
if (!item.content && !item.source) {
|
||||
throw new Error(
|
||||
`Neither content nor source specified for ${item.target}`,
|
||||
);
|
||||
}
|
||||
const content = item.content || (await fs.readFile(item.source!));
|
||||
await writeFileWithLock(item.target, content);
|
||||
}
|
||||
}
|
||||
|
||||
Logger.info('✌️ Init file down');
|
||||
|
||||
+19
-15
@@ -38,21 +38,25 @@ export default async () => {
|
||||
|
||||
// 运行删除日志任务
|
||||
const data = await systemService.getSystemConfig();
|
||||
if (data && data.info && data.info.logRemoveFrequency) {
|
||||
const rmlogCron = {
|
||||
id: data.id as number,
|
||||
name: '删除日志',
|
||||
command: `ql rmlog ${data.info.logRemoveFrequency}`,
|
||||
runOrigin: 'system' as const,
|
||||
};
|
||||
await scheduleService.cancelIntervalTask(rmlogCron);
|
||||
scheduleService.createIntervalTask(
|
||||
rmlogCron,
|
||||
{
|
||||
days: data.info.logRemoveFrequency,
|
||||
},
|
||||
true,
|
||||
);
|
||||
if (data && data.info) {
|
||||
if (data.info.logRemoveFrequency) {
|
||||
const rmlogCron = {
|
||||
id: data.id as number,
|
||||
name: '删除日志',
|
||||
command: `ql rmlog ${data.info.logRemoveFrequency}`,
|
||||
runOrigin: 'system' as const,
|
||||
};
|
||||
await scheduleService.cancelIntervalTask(rmlogCron);
|
||||
scheduleService.createIntervalTask(
|
||||
rmlogCron,
|
||||
{
|
||||
days: data.info.logRemoveFrequency,
|
||||
},
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
systemService.updateTimezone(data.info);
|
||||
}
|
||||
|
||||
await subscriptionService.setSshConfig();
|
||||
|
||||
@@ -4,10 +4,10 @@ import config from '../config';
|
||||
import path from 'path';
|
||||
|
||||
const levelMap: Record<string, string> = {
|
||||
info: '🔵',
|
||||
warn: '🟡',
|
||||
error: '🔴',
|
||||
debug: '🔶'
|
||||
info: '\ue6f5',
|
||||
warn: '\ue880',
|
||||
error: '\ue602',
|
||||
debug: '\ue67f'
|
||||
}
|
||||
|
||||
const customFormat = winston.format.combine(
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import Logger from './logger';
|
||||
import fs from 'fs';
|
||||
import config from '../config';
|
||||
import { parseContentVersion } from '../config/util';
|
||||
|
||||
let version = '1.0.0';
|
||||
try {
|
||||
const content = fs.readFileSync(config.versionFile, 'utf-8');
|
||||
({ version } = parseContentVersion(content));
|
||||
} catch (error) {}
|
||||
|
||||
Sentry.init({
|
||||
ignoreErrors: [
|
||||
/SequelizeUniqueConstraintError/i,
|
||||
/Validation error/i,
|
||||
/UnauthorizedError/i,
|
||||
/celebrate request validation failed/i,
|
||||
],
|
||||
dsn: 'https://8b5c84cfef3e22541bc84de0ed00497b@o1098464.ingest.sentry.io/6122819',
|
||||
tracesSampleRate: 0.5,
|
||||
release: version,
|
||||
});
|
||||
|
||||
Logger.info('✌️ Sentry loaded');
|
||||
console.log('✌️ Sentry loaded');
|
||||
@@ -2,9 +2,8 @@ import sockJs from 'sockjs';
|
||||
import { Server } from 'http';
|
||||
import { Container } from 'typedi';
|
||||
import SockService from '../services/sock';
|
||||
import config from '../config/index';
|
||||
import fs from 'fs/promises';
|
||||
import { getPlatform, safeJSONParse } from '../config/util';
|
||||
import { getPlatform } from '../config/util';
|
||||
import { shareStore } from '../shared/store';
|
||||
|
||||
export default async ({ server }: { server: Server }) => {
|
||||
const echo = sockJs.createServer({ prefix: '/api/ws', log: () => {} });
|
||||
@@ -15,11 +14,11 @@ export default async ({ server }: { server: Server }) => {
|
||||
conn.close('404');
|
||||
}
|
||||
|
||||
const data = await fs.readFile(config.authConfigFile, 'utf8');
|
||||
const authInfo = await shareStore.getAuthInfo();
|
||||
const platform = getPlatform(conn.headers['user-agent'] || '') || 'desktop';
|
||||
const headerToken = conn.url.replace(`${conn.pathname}?token=`, '');
|
||||
if (data) {
|
||||
const { token = '', tokens = {} } = safeJSONParse(data);
|
||||
if (authInfo) {
|
||||
const { token = '', tokens = {} } = authInfo;
|
||||
if (headerToken === token || tokens[platform] === headerToken) {
|
||||
sockService.addClient(conn);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import bodyParser from 'body-parser';
|
||||
import { errors } from 'celebrate';
|
||||
import cors from 'cors';
|
||||
import { Application, NextFunction, Request, Response } from 'express';
|
||||
import jwt from 'express-jwt';
|
||||
import { expressjwt } from 'express-jwt';
|
||||
import Container from 'typedi';
|
||||
import config from '../config';
|
||||
import SystemService from '../services/system';
|
||||
@@ -16,7 +16,7 @@ export default ({ app }: { app: Application }) => {
|
||||
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
|
||||
|
||||
app.use(
|
||||
jwt({
|
||||
expressjwt({
|
||||
secret: config.secret,
|
||||
algorithms: ['HS384'],
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package com.ql.api;
|
||||
|
||||
message EnvItem {
|
||||
optional int32 id = 1;
|
||||
optional string name = 2;
|
||||
optional string value = 3;
|
||||
optional string remarks = 4;
|
||||
optional int32 status = 5;
|
||||
optional int64 position = 6;
|
||||
}
|
||||
|
||||
message GetEnvsRequest { string searchValue = 1; }
|
||||
|
||||
message CreateEnvRequest { repeated EnvItem envs = 1; }
|
||||
|
||||
message UpdateEnvRequest { EnvItem env = 1; }
|
||||
|
||||
message DeleteEnvsRequest { repeated int32 ids = 1; }
|
||||
|
||||
message MoveEnvRequest {
|
||||
int32 id = 1;
|
||||
int32 fromIndex = 2;
|
||||
int32 toIndex = 3;
|
||||
}
|
||||
|
||||
message DisableEnvsRequest { repeated int32 ids = 1; }
|
||||
|
||||
message EnableEnvsRequest { repeated int32 ids = 1; }
|
||||
|
||||
message UpdateEnvNamesRequest {
|
||||
repeated int32 ids = 1;
|
||||
string name = 2;
|
||||
}
|
||||
|
||||
message GetEnvByIdRequest { int32 id = 1; }
|
||||
|
||||
message EnvsResponse {
|
||||
int32 code = 1;
|
||||
repeated EnvItem data = 2;
|
||||
optional string message = 3;
|
||||
}
|
||||
|
||||
message EnvResponse {
|
||||
int32 code = 1;
|
||||
EnvItem data = 2;
|
||||
optional string message = 3;
|
||||
}
|
||||
|
||||
message Response {
|
||||
int32 code = 1;
|
||||
optional string message = 2;
|
||||
}
|
||||
|
||||
message SystemNotifyRequest {
|
||||
string title = 1;
|
||||
string content = 2;
|
||||
}
|
||||
|
||||
message ExtraScheduleItem {
|
||||
string schedule = 1;
|
||||
}
|
||||
|
||||
message CronItem {
|
||||
optional int32 id = 1;
|
||||
optional string command = 2;
|
||||
optional string schedule = 3;
|
||||
optional string name = 4;
|
||||
repeated string labels = 5;
|
||||
optional int32 sub_id = 6;
|
||||
repeated ExtraScheduleItem extra_schedules = 7;
|
||||
optional string task_before = 8;
|
||||
optional string task_after = 9;
|
||||
optional int32 status = 10;
|
||||
optional string log_path = 11;
|
||||
optional int32 pid = 12;
|
||||
optional int64 last_running_time = 13;
|
||||
optional int64 last_execution_time = 14;
|
||||
}
|
||||
|
||||
message CreateCronRequest {
|
||||
string command = 1;
|
||||
string schedule = 2;
|
||||
optional string name = 3;
|
||||
repeated string labels = 4;
|
||||
optional int32 sub_id = 5;
|
||||
repeated ExtraScheduleItem extra_schedules = 6;
|
||||
optional string task_before = 7;
|
||||
optional string task_after = 8;
|
||||
}
|
||||
|
||||
message UpdateCronRequest {
|
||||
int32 id = 1;
|
||||
optional string command = 2;
|
||||
optional string schedule = 3;
|
||||
optional string name = 4;
|
||||
repeated string labels = 5;
|
||||
optional int32 sub_id = 6;
|
||||
repeated ExtraScheduleItem extra_schedules = 7;
|
||||
optional string task_before = 8;
|
||||
optional string task_after = 9;
|
||||
}
|
||||
|
||||
message DeleteCronsRequest { repeated int32 ids = 1; }
|
||||
|
||||
message CronsResponse {
|
||||
int32 code = 1;
|
||||
repeated CronItem data = 2;
|
||||
optional string message = 3;
|
||||
}
|
||||
|
||||
message CronResponse {
|
||||
int32 code = 1;
|
||||
CronItem data = 2;
|
||||
optional string message = 3;
|
||||
}
|
||||
|
||||
message CronDetailRequest { string log_path = 1; }
|
||||
|
||||
message CronDetailResponse {
|
||||
int32 code = 1;
|
||||
CronItem data = 2;
|
||||
optional string message = 3;
|
||||
}
|
||||
|
||||
service Api {
|
||||
rpc GetEnvs(GetEnvsRequest) returns (EnvsResponse) {}
|
||||
rpc CreateEnv(CreateEnvRequest) returns (EnvsResponse) {}
|
||||
rpc UpdateEnv(UpdateEnvRequest) returns (EnvResponse) {}
|
||||
rpc DeleteEnvs(DeleteEnvsRequest) returns (Response) {}
|
||||
rpc MoveEnv(MoveEnvRequest) returns (EnvResponse) {}
|
||||
rpc DisableEnvs(DisableEnvsRequest) returns (Response) {}
|
||||
rpc EnableEnvs(EnableEnvsRequest) returns (Response) {}
|
||||
rpc UpdateEnvNames(UpdateEnvNamesRequest) returns (Response) {}
|
||||
rpc GetEnvById(GetEnvByIdRequest) returns (EnvResponse) {}
|
||||
rpc SystemNotify(SystemNotifyRequest) returns (Response) {}
|
||||
rpc GetCronDetail(CronDetailRequest) returns (CronDetailResponse) {}
|
||||
rpc CreateCron(CreateCronRequest) returns (CronResponse) {}
|
||||
rpc UpdateCron(UpdateCronRequest) returns (CronResponse) {}
|
||||
rpc DeleteCrons(DeleteCronsRequest) returns (Response) {}
|
||||
}
|
||||
+2801
File diff suppressed because it is too large
Load Diff
+114
-91
@@ -1,17 +1,23 @@
|
||||
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-ts_proto v2.6.1
|
||||
// protoc v3.17.3
|
||||
// source: back/protos/cron.proto
|
||||
|
||||
/* eslint-disable */
|
||||
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
|
||||
import {
|
||||
CallOptions,
|
||||
type CallOptions,
|
||||
ChannelCredentials,
|
||||
Client,
|
||||
ClientOptions,
|
||||
ClientUnaryCall,
|
||||
handleUnaryCall,
|
||||
type ClientOptions,
|
||||
type ClientUnaryCall,
|
||||
type handleUnaryCall,
|
||||
makeGenericClientConstructor,
|
||||
Metadata,
|
||||
ServiceError,
|
||||
UntypedServiceImplementation,
|
||||
type ServiceError,
|
||||
type UntypedServiceImplementation,
|
||||
} from "@grpc/grpc-js";
|
||||
import _m0 from "protobufjs/minimal";
|
||||
|
||||
export const protobufPackage = "com.ql.cron";
|
||||
|
||||
@@ -23,7 +29,7 @@ export interface ICron {
|
||||
id: string;
|
||||
schedule: string;
|
||||
command: string;
|
||||
extraSchedules: ISchedule[];
|
||||
extra_schedules: ISchedule[];
|
||||
name: string;
|
||||
}
|
||||
|
||||
@@ -45,51 +51,53 @@ function createBaseISchedule(): ISchedule {
|
||||
return { schedule: "" };
|
||||
}
|
||||
|
||||
export const ISchedule = {
|
||||
encode(message: ISchedule, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
||||
export const ISchedule: MessageFns<ISchedule> = {
|
||||
encode(message: ISchedule, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
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);
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): ISchedule {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(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:
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.schedule = reader.string();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skipType(tag & 7);
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): ISchedule {
|
||||
return { schedule: isSet(object.schedule) ? String(object.schedule) : "" };
|
||||
return { schedule: isSet(object.schedule) ? globalThis.String(object.schedule) : "" };
|
||||
},
|
||||
|
||||
toJSON(message: ISchedule): unknown {
|
||||
const obj: any = {};
|
||||
message.schedule !== undefined && (obj.schedule = message.schedule);
|
||||
if (message.schedule !== "") {
|
||||
obj.schedule = message.schedule;
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<ISchedule>, I>>(base?: I): ISchedule {
|
||||
return ISchedule.fromPartial(base ?? {});
|
||||
return ISchedule.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
|
||||
fromPartial<I extends Exact<DeepPartial<ISchedule>, I>>(object: I): ISchedule {
|
||||
const message = createBaseISchedule();
|
||||
message.schedule = object.schedule ?? "";
|
||||
@@ -98,11 +106,11 @@ export const ISchedule = {
|
||||
};
|
||||
|
||||
function createBaseICron(): ICron {
|
||||
return { id: "", schedule: "", command: "", extraSchedules: [], name: "" };
|
||||
return { id: "", schedule: "", command: "", extra_schedules: [], name: "" };
|
||||
}
|
||||
|
||||
export const ICron = {
|
||||
encode(message: ICron, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
||||
export const ICron: MessageFns<ICron> = {
|
||||
encode(message: ICron, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.id !== "") {
|
||||
writer.uint32(10).string(message.id);
|
||||
}
|
||||
@@ -112,8 +120,8 @@ export const ICron = {
|
||||
if (message.command !== "") {
|
||||
writer.uint32(26).string(message.command);
|
||||
}
|
||||
for (const v of message.extraSchedules) {
|
||||
ISchedule.encode(v!, writer.uint32(34).fork()).ldelim();
|
||||
for (const v of message.extra_schedules) {
|
||||
ISchedule.encode(v!, writer.uint32(34).fork()).join();
|
||||
}
|
||||
if (message.name !== "") {
|
||||
writer.uint32(42).string(message.name);
|
||||
@@ -121,93 +129,103 @@ export const ICron = {
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: _m0.Reader | Uint8Array, length?: number): ICron {
|
||||
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): ICron {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
let end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseICron();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1:
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.id = reader.string();
|
||||
continue;
|
||||
case 2:
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 18) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.schedule = reader.string();
|
||||
continue;
|
||||
case 3:
|
||||
}
|
||||
case 3: {
|
||||
if (tag !== 26) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.command = reader.string();
|
||||
continue;
|
||||
case 4:
|
||||
}
|
||||
case 4: {
|
||||
if (tag !== 34) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.extraSchedules.push(ISchedule.decode(reader, reader.uint32()));
|
||||
message.extra_schedules.push(ISchedule.decode(reader, reader.uint32()));
|
||||
continue;
|
||||
case 5:
|
||||
}
|
||||
case 5: {
|
||||
if (tag !== 42) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.name = reader.string();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skipType(tag & 7);
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): ICron {
|
||||
return {
|
||||
id: isSet(object.id) ? String(object.id) : "",
|
||||
schedule: isSet(object.schedule) ? String(object.schedule) : "",
|
||||
command: isSet(object.command) ? String(object.command) : "",
|
||||
extraSchedules: Array.isArray(object?.extraSchedules)
|
||||
? object.extraSchedules.map((e: any) => ISchedule.fromJSON(e))
|
||||
id: isSet(object.id) ? globalThis.String(object.id) : "",
|
||||
schedule: isSet(object.schedule) ? globalThis.String(object.schedule) : "",
|
||||
command: isSet(object.command) ? globalThis.String(object.command) : "",
|
||||
extra_schedules: globalThis.Array.isArray(object?.extra_schedules)
|
||||
? object.extra_schedules.map((e: any) => ISchedule.fromJSON(e))
|
||||
: [],
|
||||
name: isSet(object.name) ? String(object.name) : "",
|
||||
name: isSet(object.name) ? globalThis.String(object.name) : "",
|
||||
};
|
||||
},
|
||||
|
||||
toJSON(message: ICron): unknown {
|
||||
const obj: any = {};
|
||||
message.id !== undefined && (obj.id = message.id);
|
||||
message.schedule !== undefined && (obj.schedule = message.schedule);
|
||||
message.command !== undefined && (obj.command = message.command);
|
||||
if (message.extraSchedules) {
|
||||
obj.extraSchedules = message.extraSchedules.map((e) => e ? ISchedule.toJSON(e) : undefined);
|
||||
} else {
|
||||
obj.extraSchedules = [];
|
||||
if (message.id !== "") {
|
||||
obj.id = message.id;
|
||||
}
|
||||
if (message.schedule !== "") {
|
||||
obj.schedule = message.schedule;
|
||||
}
|
||||
if (message.command !== "") {
|
||||
obj.command = message.command;
|
||||
}
|
||||
if (message.extra_schedules?.length) {
|
||||
obj.extra_schedules = message.extra_schedules.map((e) => ISchedule.toJSON(e));
|
||||
}
|
||||
if (message.name !== "") {
|
||||
obj.name = message.name;
|
||||
}
|
||||
message.name !== undefined && (obj.name = message.name);
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<ICron>, I>>(base?: I): ICron {
|
||||
return ICron.fromPartial(base ?? {});
|
||||
return ICron.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
|
||||
fromPartial<I extends Exact<DeepPartial<ICron>, I>>(object: I): ICron {
|
||||
const message = createBaseICron();
|
||||
message.id = object.id ?? "";
|
||||
message.schedule = object.schedule ?? "";
|
||||
message.command = object.command ?? "";
|
||||
message.extraSchedules = object.extraSchedules?.map((e) => ISchedule.fromPartial(e)) || [];
|
||||
message.extra_schedules = object.extra_schedules?.map((e) => ISchedule.fromPartial(e)) || [];
|
||||
message.name = object.name ?? "";
|
||||
return message;
|
||||
},
|
||||
@@ -217,55 +235,53 @@ function createBaseAddCronRequest(): AddCronRequest {
|
||||
return { crons: [] };
|
||||
}
|
||||
|
||||
export const AddCronRequest = {
|
||||
encode(message: AddCronRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
||||
export const AddCronRequest: MessageFns<AddCronRequest> = {
|
||||
encode(message: AddCronRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
for (const v of message.crons) {
|
||||
ICron.encode(v!, writer.uint32(10).fork()).ldelim();
|
||||
ICron.encode(v!, writer.uint32(10).fork()).join();
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: _m0.Reader | Uint8Array, length?: number): AddCronRequest {
|
||||
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): AddCronRequest {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
let end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseAddCronRequest();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1:
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.crons.push(ICron.decode(reader, reader.uint32()));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skipType(tag & 7);
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): AddCronRequest {
|
||||
return { crons: Array.isArray(object?.crons) ? object.crons.map((e: any) => ICron.fromJSON(e)) : [] };
|
||||
return { crons: globalThis.Array.isArray(object?.crons) ? object.crons.map((e: any) => ICron.fromJSON(e)) : [] };
|
||||
},
|
||||
|
||||
toJSON(message: AddCronRequest): unknown {
|
||||
const obj: any = {};
|
||||
if (message.crons) {
|
||||
obj.crons = message.crons.map((e) => e ? ICron.toJSON(e) : undefined);
|
||||
} else {
|
||||
obj.crons = [];
|
||||
if (message.crons?.length) {
|
||||
obj.crons = message.crons.map((e) => ICron.toJSON(e));
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<AddCronRequest>, I>>(base?: I): AddCronRequest {
|
||||
return AddCronRequest.fromPartial(base ?? {});
|
||||
return AddCronRequest.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
|
||||
fromPartial<I extends Exact<DeepPartial<AddCronRequest>, I>>(object: I): AddCronRequest {
|
||||
const message = createBaseAddCronRequest();
|
||||
message.crons = object.crons?.map((e) => ICron.fromPartial(e)) || [];
|
||||
@@ -277,13 +293,13 @@ function createBaseAddCronResponse(): AddCronResponse {
|
||||
return {};
|
||||
}
|
||||
|
||||
export const AddCronResponse = {
|
||||
encode(_: AddCronResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
||||
export const AddCronResponse: MessageFns<AddCronResponse> = {
|
||||
encode(_: AddCronResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: _m0.Reader | Uint8Array, length?: number): AddCronResponse {
|
||||
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): AddCronResponse {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
let end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseAddCronResponse();
|
||||
while (reader.pos < end) {
|
||||
@@ -293,7 +309,7 @@ export const AddCronResponse = {
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skipType(tag & 7);
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
@@ -308,9 +324,8 @@ export const AddCronResponse = {
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<AddCronResponse>, I>>(base?: I): AddCronResponse {
|
||||
return AddCronResponse.fromPartial(base ?? {});
|
||||
return AddCronResponse.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
|
||||
fromPartial<I extends Exact<DeepPartial<AddCronResponse>, I>>(_: I): AddCronResponse {
|
||||
const message = createBaseAddCronResponse();
|
||||
return message;
|
||||
@@ -321,55 +336,53 @@ function createBaseDeleteCronRequest(): DeleteCronRequest {
|
||||
return { ids: [] };
|
||||
}
|
||||
|
||||
export const DeleteCronRequest = {
|
||||
encode(message: DeleteCronRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
||||
export const DeleteCronRequest: MessageFns<DeleteCronRequest> = {
|
||||
encode(message: DeleteCronRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
for (const v of message.ids) {
|
||||
writer.uint32(10).string(v!);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: _m0.Reader | Uint8Array, length?: number): DeleteCronRequest {
|
||||
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): DeleteCronRequest {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
let end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseDeleteCronRequest();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1:
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.ids.push(reader.string());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skipType(tag & 7);
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): DeleteCronRequest {
|
||||
return { ids: Array.isArray(object?.ids) ? object.ids.map((e: any) => String(e)) : [] };
|
||||
return { ids: globalThis.Array.isArray(object?.ids) ? object.ids.map((e: any) => globalThis.String(e)) : [] };
|
||||
},
|
||||
|
||||
toJSON(message: DeleteCronRequest): unknown {
|
||||
const obj: any = {};
|
||||
if (message.ids) {
|
||||
obj.ids = message.ids.map((e) => e);
|
||||
} else {
|
||||
obj.ids = [];
|
||||
if (message.ids?.length) {
|
||||
obj.ids = message.ids;
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(base?: I): DeleteCronRequest {
|
||||
return DeleteCronRequest.fromPartial(base ?? {});
|
||||
return DeleteCronRequest.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
|
||||
fromPartial<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(object: I): DeleteCronRequest {
|
||||
const message = createBaseDeleteCronRequest();
|
||||
message.ids = object.ids?.map((e) => e) || [];
|
||||
@@ -381,13 +394,13 @@ function createBaseDeleteCronResponse(): DeleteCronResponse {
|
||||
return {};
|
||||
}
|
||||
|
||||
export const DeleteCronResponse = {
|
||||
encode(_: DeleteCronResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
||||
export const DeleteCronResponse: MessageFns<DeleteCronResponse> = {
|
||||
encode(_: DeleteCronResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: _m0.Reader | Uint8Array, length?: number): DeleteCronResponse {
|
||||
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): DeleteCronResponse {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
let end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseDeleteCronResponse();
|
||||
while (reader.pos < end) {
|
||||
@@ -397,7 +410,7 @@ export const DeleteCronResponse = {
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skipType(tag & 7);
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
@@ -412,9 +425,8 @@ export const DeleteCronResponse = {
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(base?: I): DeleteCronResponse {
|
||||
return DeleteCronResponse.fromPartial(base ?? {});
|
||||
return DeleteCronResponse.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
|
||||
fromPartial<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(_: I): DeleteCronResponse {
|
||||
const message = createBaseDeleteCronResponse();
|
||||
return message;
|
||||
@@ -484,12 +496,14 @@ export interface CronClient extends Client {
|
||||
export const CronClient = makeGenericClientConstructor(CronService, "com.ql.cron.Cron") as unknown as {
|
||||
new (address: string, credentials: ChannelCredentials, options?: Partial<ClientOptions>): CronClient;
|
||||
service: typeof CronService;
|
||||
serviceName: string;
|
||||
};
|
||||
|
||||
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
|
||||
|
||||
export type DeepPartial<T> = T extends Builtin ? T
|
||||
: T extends Array<infer U> ? Array<DeepPartial<U>> : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
|
||||
: T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>
|
||||
: T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
|
||||
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||
: Partial<T>;
|
||||
|
||||
@@ -500,3 +514,12 @@ export type Exact<P, I extends P> = P extends Builtin ? P
|
||||
function isSet(value: any): boolean {
|
||||
return value !== null && value !== undefined;
|
||||
}
|
||||
|
||||
export interface MessageFns<T> {
|
||||
encode(message: T, writer?: BinaryWriter): BinaryWriter;
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): T;
|
||||
fromJSON(object: any): T;
|
||||
toJSON(message: T): unknown;
|
||||
create<I extends Exact<DeepPartial<T>, I>>(base?: I): T;
|
||||
fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T;
|
||||
}
|
||||
|
||||
+50
-29
@@ -1,19 +1,25 @@
|
||||
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-ts_proto v2.6.1
|
||||
// protoc v3.17.3
|
||||
// source: back/protos/health.proto
|
||||
|
||||
/* eslint-disable */
|
||||
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
|
||||
import {
|
||||
CallOptions,
|
||||
type CallOptions,
|
||||
ChannelCredentials,
|
||||
Client,
|
||||
ClientOptions,
|
||||
ClientReadableStream,
|
||||
ClientUnaryCall,
|
||||
handleServerStreamingCall,
|
||||
handleUnaryCall,
|
||||
type ClientOptions,
|
||||
type ClientReadableStream,
|
||||
type ClientUnaryCall,
|
||||
type handleServerStreamingCall,
|
||||
type handleUnaryCall,
|
||||
makeGenericClientConstructor,
|
||||
Metadata,
|
||||
ServiceError,
|
||||
UntypedServiceImplementation,
|
||||
type ServiceError,
|
||||
type UntypedServiceImplementation,
|
||||
} from "@grpc/grpc-js";
|
||||
import _m0 from "protobufjs/minimal";
|
||||
|
||||
export const protobufPackage = "com.ql.health";
|
||||
|
||||
@@ -74,51 +80,53 @@ function createBaseHealthCheckRequest(): HealthCheckRequest {
|
||||
return { service: "" };
|
||||
}
|
||||
|
||||
export const HealthCheckRequest = {
|
||||
encode(message: HealthCheckRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
||||
export const HealthCheckRequest: MessageFns<HealthCheckRequest> = {
|
||||
encode(message: HealthCheckRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.service !== "") {
|
||||
writer.uint32(10).string(message.service);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: _m0.Reader | Uint8Array, length?: number): HealthCheckRequest {
|
||||
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): HealthCheckRequest {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
let end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseHealthCheckRequest();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1:
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.service = reader.string();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skipType(tag & 7);
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
||||
fromJSON(object: any): HealthCheckRequest {
|
||||
return { service: isSet(object.service) ? String(object.service) : "" };
|
||||
return { service: isSet(object.service) ? globalThis.String(object.service) : "" };
|
||||
},
|
||||
|
||||
toJSON(message: HealthCheckRequest): unknown {
|
||||
const obj: any = {};
|
||||
message.service !== undefined && (obj.service = message.service);
|
||||
if (message.service !== "") {
|
||||
obj.service = message.service;
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<HealthCheckRequest>, I>>(base?: I): HealthCheckRequest {
|
||||
return HealthCheckRequest.fromPartial(base ?? {});
|
||||
return HealthCheckRequest.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
|
||||
fromPartial<I extends Exact<DeepPartial<HealthCheckRequest>, I>>(object: I): HealthCheckRequest {
|
||||
const message = createBaseHealthCheckRequest();
|
||||
message.service = object.service ?? "";
|
||||
@@ -130,33 +138,34 @@ function createBaseHealthCheckResponse(): HealthCheckResponse {
|
||||
return { status: 0 };
|
||||
}
|
||||
|
||||
export const HealthCheckResponse = {
|
||||
encode(message: HealthCheckResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
||||
export const HealthCheckResponse: MessageFns<HealthCheckResponse> = {
|
||||
encode(message: HealthCheckResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.status !== 0) {
|
||||
writer.uint32(8).int32(message.status);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: _m0.Reader | Uint8Array, length?: number): HealthCheckResponse {
|
||||
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): HealthCheckResponse {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
let end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseHealthCheckResponse();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1:
|
||||
case 1: {
|
||||
if (tag !== 8) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.status = reader.int32() as any;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skipType(tag & 7);
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
@@ -167,14 +176,15 @@ export const HealthCheckResponse = {
|
||||
|
||||
toJSON(message: HealthCheckResponse): unknown {
|
||||
const obj: any = {};
|
||||
message.status !== undefined && (obj.status = healthCheckResponse_ServingStatusToJSON(message.status));
|
||||
if (message.status !== 0) {
|
||||
obj.status = healthCheckResponse_ServingStatusToJSON(message.status);
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
create<I extends Exact<DeepPartial<HealthCheckResponse>, I>>(base?: I): HealthCheckResponse {
|
||||
return HealthCheckResponse.fromPartial(base ?? {});
|
||||
return HealthCheckResponse.fromPartial(base ?? ({} as any));
|
||||
},
|
||||
|
||||
fromPartial<I extends Exact<DeepPartial<HealthCheckResponse>, I>>(object: I): HealthCheckResponse {
|
||||
const message = createBaseHealthCheckResponse();
|
||||
message.status = object.status ?? 0;
|
||||
@@ -236,12 +246,14 @@ export interface HealthClient extends Client {
|
||||
export const HealthClient = makeGenericClientConstructor(HealthService, "com.ql.health.Health") as unknown as {
|
||||
new (address: string, credentials: ChannelCredentials, options?: Partial<ClientOptions>): HealthClient;
|
||||
service: typeof HealthService;
|
||||
serviceName: string;
|
||||
};
|
||||
|
||||
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
|
||||
|
||||
export type DeepPartial<T> = T extends Builtin ? T
|
||||
: T extends Array<infer U> ? Array<DeepPartial<U>> : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
|
||||
: T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>
|
||||
: T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
|
||||
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||
: Partial<T>;
|
||||
|
||||
@@ -252,3 +264,12 @@ export type Exact<P, I extends P> = P extends Builtin ? P
|
||||
function isSet(value: any): boolean {
|
||||
return value !== null && value !== undefined;
|
||||
}
|
||||
|
||||
export interface MessageFns<T> {
|
||||
encode(message: T, writer?: BinaryWriter): BinaryWriter;
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): T;
|
||||
fromJSON(object: any): T;
|
||||
toJSON(message: T): unknown;
|
||||
create<I extends Exact<DeepPartial<T>, I>>(base?: I): T;
|
||||
fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ const addCron = (
|
||||
callback: sendUnaryData<AddCronResponse>,
|
||||
) => {
|
||||
for (const item of call.request.crons) {
|
||||
const { id, schedule, command, extraSchedules, name } = item;
|
||||
const { id, schedule, command, extra_schedules, name } = item;
|
||||
if (scheduleStacks.has(id)) {
|
||||
scheduleStacks.get(id)?.forEach((x) => x.cancel());
|
||||
}
|
||||
@@ -23,8 +23,8 @@ const addCron = (
|
||||
command,
|
||||
);
|
||||
|
||||
if (extraSchedules?.length) {
|
||||
extraSchedules.forEach((x) => {
|
||||
if (extra_schedules?.length) {
|
||||
extra_schedules.forEach((x) => {
|
||||
Logger.info(
|
||||
'[schedule][创建定时任务], 任务ID: %s, 名称: %s, cron: %s, 执行命令: %s',
|
||||
id,
|
||||
@@ -40,8 +40,8 @@ const addCron = (
|
||||
Logger.info(`[schedule][准备运行任务] 命令: ${command}`);
|
||||
runCron(command, item);
|
||||
}),
|
||||
...(extraSchedules?.length
|
||||
? extraSchedules.map((x) =>
|
||||
...(extra_schedules?.length
|
||||
? extra_schedules.map((x) =>
|
||||
nodeSchedule.scheduleJob(id, x.schedule, async () => {
|
||||
Logger.info(`[schedule][准备运行任务] 命令: ${command}`);
|
||||
runCron(command, item);
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
import 'reflect-metadata';
|
||||
import { Container } from 'typedi';
|
||||
import EnvService from '../services/env';
|
||||
import { sendUnaryData, ServerUnaryCall } from '@grpc/grpc-js';
|
||||
import {
|
||||
CreateEnvRequest,
|
||||
CronItem,
|
||||
DeleteEnvsRequest,
|
||||
DisableEnvsRequest,
|
||||
EnableEnvsRequest,
|
||||
EnvItem,
|
||||
EnvResponse,
|
||||
EnvsResponse,
|
||||
GetEnvByIdRequest,
|
||||
GetEnvsRequest,
|
||||
MoveEnvRequest,
|
||||
Response,
|
||||
SystemNotifyRequest,
|
||||
UpdateEnvNamesRequest,
|
||||
UpdateEnvRequest,
|
||||
} from '../protos/api';
|
||||
import LoggerInstance from '../loaders/logger';
|
||||
import pick from 'lodash/pick';
|
||||
import SystemService from '../services/system';
|
||||
import CronService from '../services/cron';
|
||||
import {
|
||||
CronDetailRequest,
|
||||
CronDetailResponse,
|
||||
CreateCronRequest,
|
||||
UpdateCronRequest,
|
||||
DeleteCronsRequest,
|
||||
CronResponse,
|
||||
} from '../protos/api';
|
||||
|
||||
Container.set('logger', LoggerInstance);
|
||||
|
||||
export const getEnvs = async (
|
||||
call: ServerUnaryCall<GetEnvsRequest, EnvsResponse>,
|
||||
callback: sendUnaryData<EnvsResponse>,
|
||||
) => {
|
||||
try {
|
||||
if (!call.request.searchValue) {
|
||||
return callback(null, {
|
||||
code: 400,
|
||||
data: [],
|
||||
message: 'searchValue is required',
|
||||
});
|
||||
}
|
||||
const envService = Container.get(EnvService);
|
||||
const data = await envService.envs(call.request.searchValue);
|
||||
callback(null, {
|
||||
code: 200,
|
||||
data: data.map((x) => ({ ...x, remarks: x.remarks || '' })),
|
||||
});
|
||||
} catch (e: any) {
|
||||
callback(null, {
|
||||
code: 500,
|
||||
data: [],
|
||||
message: e.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const createEnv = async (
|
||||
call: ServerUnaryCall<CreateEnvRequest, EnvsResponse>,
|
||||
callback: sendUnaryData<EnvsResponse>,
|
||||
) => {
|
||||
try {
|
||||
const envService = Container.get(EnvService);
|
||||
const data = await envService.create(call.request.envs);
|
||||
callback(null, { code: 200, data });
|
||||
} catch (e: any) {
|
||||
callback(e);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateEnv = async (
|
||||
call: ServerUnaryCall<UpdateEnvRequest, EnvResponse>,
|
||||
callback: sendUnaryData<EnvResponse>,
|
||||
) => {
|
||||
try {
|
||||
const envService = Container.get(EnvService);
|
||||
const data = await envService.update(
|
||||
pick(call.request.env, ['id', 'name', 'value', 'remark']) as EnvItem,
|
||||
);
|
||||
callback(null, { code: 200, data });
|
||||
} catch (e: any) {
|
||||
callback(e);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteEnvs = async (
|
||||
call: ServerUnaryCall<DeleteEnvsRequest, Response>,
|
||||
callback: sendUnaryData<Response>,
|
||||
) => {
|
||||
try {
|
||||
const envService = Container.get(EnvService);
|
||||
await envService.remove(call.request.ids);
|
||||
callback(null, { code: 200 });
|
||||
} catch (e: any) {
|
||||
callback(e);
|
||||
}
|
||||
};
|
||||
|
||||
export const moveEnv = async (
|
||||
call: ServerUnaryCall<MoveEnvRequest, EnvResponse>,
|
||||
callback: sendUnaryData<EnvResponse>,
|
||||
) => {
|
||||
try {
|
||||
const envService = Container.get(EnvService);
|
||||
const data = await envService.move(call.request.id, {
|
||||
fromIndex: call.request.fromIndex,
|
||||
toIndex: call.request.toIndex,
|
||||
});
|
||||
callback(null, { code: 200, data });
|
||||
} catch (e: any) {
|
||||
callback(e);
|
||||
}
|
||||
};
|
||||
|
||||
export const disableEnvs = async (
|
||||
call: ServerUnaryCall<DisableEnvsRequest, Response>,
|
||||
callback: sendUnaryData<Response>,
|
||||
) => {
|
||||
try {
|
||||
const envService = Container.get(EnvService);
|
||||
await envService.disabled(call.request.ids);
|
||||
callback(null, { code: 200 });
|
||||
} catch (e: any) {
|
||||
callback(e);
|
||||
}
|
||||
};
|
||||
|
||||
export const enableEnvs = async (
|
||||
call: ServerUnaryCall<EnableEnvsRequest, Response>,
|
||||
callback: sendUnaryData<Response>,
|
||||
) => {
|
||||
try {
|
||||
const envService = Container.get(EnvService);
|
||||
await envService.enabled(call.request.ids);
|
||||
callback(null, { code: 200 });
|
||||
} catch (e: any) {
|
||||
callback(e);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateEnvNames = async (
|
||||
call: ServerUnaryCall<UpdateEnvNamesRequest, Response>,
|
||||
callback: sendUnaryData<Response>,
|
||||
) => {
|
||||
try {
|
||||
const envService = Container.get(EnvService);
|
||||
await envService.updateNames({
|
||||
ids: call.request.ids,
|
||||
name: call.request.name,
|
||||
});
|
||||
callback(null, { code: 200 });
|
||||
} catch (e: any) {
|
||||
callback(e);
|
||||
}
|
||||
};
|
||||
|
||||
export const getEnvById = async (
|
||||
call: ServerUnaryCall<GetEnvByIdRequest, EnvResponse>,
|
||||
callback: sendUnaryData<EnvResponse>,
|
||||
) => {
|
||||
try {
|
||||
const envService = Container.get(EnvService);
|
||||
const data = await envService.getDb({ id: call.request.id });
|
||||
callback(null, {
|
||||
code: 200,
|
||||
data: { ...data, remarks: data.remarks || '' },
|
||||
});
|
||||
} catch (e: any) {
|
||||
callback(e);
|
||||
}
|
||||
};
|
||||
|
||||
export const systemNotify = async (
|
||||
call: ServerUnaryCall<SystemNotifyRequest, Response>,
|
||||
callback: sendUnaryData<Response>,
|
||||
) => {
|
||||
try {
|
||||
const systemService = Container.get(SystemService);
|
||||
const data = await systemService.notify(call.request);
|
||||
callback(null, data);
|
||||
} catch (e: any) {
|
||||
callback(e);
|
||||
}
|
||||
};
|
||||
|
||||
const normalizeCronData = (data: CronItem | null): CronItem | undefined => {
|
||||
if (!data) return undefined;
|
||||
return {
|
||||
...data,
|
||||
sub_id: data.sub_id ?? undefined,
|
||||
extra_schedules: data.extra_schedules ?? undefined,
|
||||
pid: data.pid ?? undefined,
|
||||
task_before: data.task_before ?? undefined,
|
||||
task_after: data.task_after ?? undefined,
|
||||
};
|
||||
};
|
||||
|
||||
export const getCronDetail = async (
|
||||
call: ServerUnaryCall<CronDetailRequest, CronDetailResponse>,
|
||||
callback: sendUnaryData<CronDetailResponse>,
|
||||
) => {
|
||||
try {
|
||||
if (!call.request.log_path) {
|
||||
return callback(null, {
|
||||
code: 400,
|
||||
data: undefined,
|
||||
message: 'log_path is required',
|
||||
});
|
||||
}
|
||||
const cronService = Container.get(CronService);
|
||||
const data = (await cronService.find({
|
||||
log_path: call.request.log_path,
|
||||
})) as CronItem;
|
||||
callback(null, { code: 200, data: normalizeCronData(data) });
|
||||
} catch (e: any) {
|
||||
callback(e);
|
||||
}
|
||||
};
|
||||
|
||||
export const createCron = async (
|
||||
call: ServerUnaryCall<CreateCronRequest, CronResponse>,
|
||||
callback: sendUnaryData<CronResponse>,
|
||||
) => {
|
||||
try {
|
||||
const cronService = Container.get(CronService);
|
||||
const data = (await cronService.create(call.request)) as CronItem;
|
||||
callback(null, { code: 200, data: normalizeCronData(data) });
|
||||
} catch (e: any) {
|
||||
callback(e);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateCron = async (
|
||||
call: ServerUnaryCall<UpdateCronRequest, CronResponse>,
|
||||
callback: sendUnaryData<CronResponse>,
|
||||
) => {
|
||||
try {
|
||||
const cronService = Container.get(CronService);
|
||||
const { id, ...fields } = call.request;
|
||||
|
||||
const updateRequest = {
|
||||
id,
|
||||
...Object.entries(fields).reduce((acc: any, [key, value]) => {
|
||||
if (value !== undefined) {
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {}),
|
||||
} as UpdateCronRequest;
|
||||
|
||||
const data = (await cronService.update(updateRequest)) as CronItem;
|
||||
callback(null, { code: 200, data: normalizeCronData(data) });
|
||||
} catch (e: any) {
|
||||
callback(e);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteCrons = async (
|
||||
call: ServerUnaryCall<DeleteCronsRequest, Response>,
|
||||
callback: sendUnaryData<Response>,
|
||||
) => {
|
||||
try {
|
||||
const cronService = Container.get(CronService);
|
||||
await cronService.remove(call.request.ids);
|
||||
callback(null, { code: 200 });
|
||||
} catch (e: any) {
|
||||
callback(e);
|
||||
}
|
||||
};
|
||||
@@ -6,10 +6,13 @@ import { HealthService } from '../protos/health';
|
||||
import { check } from './health';
|
||||
import config from '../config';
|
||||
import Logger from '../loaders/logger';
|
||||
import { ApiService } from '../protos/api';
|
||||
import * as Api from './api';
|
||||
|
||||
const server = new Server({ 'grpc.enable_http_proxy': 0 });
|
||||
server.addService(HealthService, { check });
|
||||
server.addService(CronService, { addCron, delCron });
|
||||
server.addService(ApiService, Api);
|
||||
server.bindAsync(
|
||||
`0.0.0.0:${config.cronPort}`,
|
||||
ServerCredentials.createInsecure(),
|
||||
@@ -17,7 +20,6 @@ server.bindAsync(
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
server.start();
|
||||
Logger.debug(`✌️ 定时服务启动成功!`);
|
||||
console.debug(`✌️ 定时服务启动成功!`);
|
||||
process.send?.('ready');
|
||||
|
||||
+78
-29
@@ -21,6 +21,8 @@ import { spawn } from 'cross-spawn';
|
||||
import dayjs from 'dayjs';
|
||||
import pickBy from 'lodash/pickBy';
|
||||
import omit from 'lodash/omit';
|
||||
import { writeFileWithLock } from '../shared/utils';
|
||||
import { ScheduleType } from '../interface/schedule';
|
||||
|
||||
@Service()
|
||||
export default class CronService {
|
||||
@@ -34,22 +36,36 @@ export default class CronService {
|
||||
return false;
|
||||
}
|
||||
|
||||
private isOnceSchedule(schedule?: string) {
|
||||
return schedule?.startsWith(ScheduleType.ONCE);
|
||||
}
|
||||
|
||||
private isBootSchedule(schedule?: string) {
|
||||
return schedule?.startsWith(ScheduleType.BOOT);
|
||||
}
|
||||
|
||||
private isSpecialSchedule(schedule?: string) {
|
||||
return this.isOnceSchedule(schedule) || this.isBootSchedule(schedule);
|
||||
}
|
||||
|
||||
public async create(payload: Crontab): Promise<Crontab> {
|
||||
const tab = new Crontab(payload);
|
||||
tab.saved = false;
|
||||
const doc = await this.insert(tab);
|
||||
if (this.isNodeCron(doc)) {
|
||||
|
||||
if (this.isNodeCron(doc) && !this.isSpecialSchedule(doc.schedule)) {
|
||||
await cronClient.addCron([
|
||||
{
|
||||
name: doc.name || '',
|
||||
id: String(doc.id),
|
||||
schedule: doc.schedule!,
|
||||
command: this.makeCommand(doc),
|
||||
extraSchedules: doc.extra_schedules || [],
|
||||
extra_schedules: doc.extra_schedules || [],
|
||||
},
|
||||
]);
|
||||
}
|
||||
await this.set_crontab();
|
||||
|
||||
await this.setCrontab();
|
||||
return doc;
|
||||
}
|
||||
|
||||
@@ -57,29 +73,33 @@ export default class CronService {
|
||||
return await CrontabModel.create(payload, { returning: true });
|
||||
}
|
||||
|
||||
public async update(payload: Crontab): Promise<Crontab> {
|
||||
public async update(payload: Partial<Crontab>): Promise<Crontab> {
|
||||
const doc = await this.getDb({ id: payload.id });
|
||||
const tab = new Crontab({ ...doc, ...payload });
|
||||
tab.saved = false;
|
||||
const newDoc = await this.updateDb(tab);
|
||||
|
||||
if (doc.isDisabled === 1) {
|
||||
return newDoc;
|
||||
}
|
||||
|
||||
if (this.isNodeCron(doc)) {
|
||||
await cronClient.delCron([String(doc.id)]);
|
||||
}
|
||||
if (this.isNodeCron(newDoc)) {
|
||||
|
||||
if (this.isNodeCron(newDoc) && !this.isSpecialSchedule(newDoc.schedule)) {
|
||||
await cronClient.addCron([
|
||||
{
|
||||
name: doc.name || '',
|
||||
id: String(newDoc.id),
|
||||
schedule: newDoc.schedule!,
|
||||
command: this.makeCommand(newDoc),
|
||||
extraSchedules: newDoc.extra_schedules || [],
|
||||
extra_schedules: newDoc.extra_schedules || [],
|
||||
},
|
||||
]);
|
||||
}
|
||||
await this.set_crontab();
|
||||
|
||||
await this.setCrontab();
|
||||
return newDoc;
|
||||
}
|
||||
|
||||
@@ -134,7 +154,7 @@ export default class CronService {
|
||||
public async remove(ids: number[]) {
|
||||
await CrontabModel.destroy({ where: { id: ids } });
|
||||
await cronClient.delCron(ids.map(String));
|
||||
await this.set_crontab();
|
||||
await this.setCrontab();
|
||||
}
|
||||
|
||||
public async pin(ids: number[]) {
|
||||
@@ -178,8 +198,8 @@ export default class CronService {
|
||||
for (const col of viewQuery.filters) {
|
||||
const { property, value, operation } = col;
|
||||
let q: any = {};
|
||||
let operate2 = null;
|
||||
let operate = null;
|
||||
let operate2: any = null;
|
||||
let operate: any = null;
|
||||
switch (operation) {
|
||||
case 'Reg':
|
||||
operate = Op.like;
|
||||
@@ -202,9 +222,16 @@ export default class CronService {
|
||||
case 'Nin':
|
||||
q[Op.and] = [
|
||||
{
|
||||
[property]: {
|
||||
[Op.notIn]: Array.isArray(value) ? value : [value],
|
||||
},
|
||||
[Op.or]: [
|
||||
{
|
||||
[property]: {
|
||||
[Op.notIn]: Array.isArray(value) ? value : [value],
|
||||
},
|
||||
},
|
||||
{
|
||||
[property]: { [Op.is]: null },
|
||||
},
|
||||
],
|
||||
},
|
||||
property === 'status' && value.includes(2)
|
||||
? { isDisabled: { [Op.ne]: 1 } }
|
||||
@@ -319,10 +346,10 @@ export default class CronService {
|
||||
log_path,
|
||||
}: {
|
||||
log_path: string;
|
||||
}): Promise<Crontab | null> {
|
||||
}): Promise<Crontab | undefined> {
|
||||
try {
|
||||
const result = await CrontabModel.findOne({ where: { log_path } });
|
||||
return result;
|
||||
return result?.get({ plain: true });
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
@@ -373,7 +400,7 @@ export default class CronService {
|
||||
try {
|
||||
const result = await CrontabModel.findAll(condition);
|
||||
const count = await CrontabModel.count({ where: query });
|
||||
return { data: result, total: count };
|
||||
return { data: result.map((x) => x.get({ plain: true })), total: count };
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
@@ -423,7 +450,7 @@ export default class CronService {
|
||||
name: cron.name,
|
||||
command: cron.command,
|
||||
schedule: cron.schedule,
|
||||
extraSchedules: cron.extra_schedules,
|
||||
extra_schedules: cron.extra_schedules,
|
||||
};
|
||||
if (cron.status !== CrontabStatus.queued) {
|
||||
resolve(params);
|
||||
@@ -494,7 +521,7 @@ export default class CronService {
|
||||
public async disabled(ids: number[]) {
|
||||
await CrontabModel.update({ isDisabled: 1 }, { where: { id: ids } });
|
||||
await cronClient.delCron(ids.map(String));
|
||||
await this.set_crontab();
|
||||
await this.setCrontab();
|
||||
}
|
||||
|
||||
public async enabled(ids: number[]) {
|
||||
@@ -507,10 +534,10 @@ export default class CronService {
|
||||
id: String(doc.id),
|
||||
schedule: doc.schedule!,
|
||||
command: this.makeCommand(doc),
|
||||
extraSchedules: doc.extra_schedules || [],
|
||||
extra_schedules: doc.extra_schedules || [],
|
||||
}));
|
||||
await cronClient.addCron(sixCron);
|
||||
await this.set_crontab();
|
||||
await this.setCrontab();
|
||||
}
|
||||
|
||||
public async log(id: number) {
|
||||
@@ -578,7 +605,7 @@ export default class CronService {
|
||||
return crontab_job_string;
|
||||
}
|
||||
|
||||
private async set_crontab(data?: { data: Crontab[]; total: number }) {
|
||||
private async setCrontab(data?: { data: Crontab[]; total: number }) {
|
||||
const tabs = data ?? (await this.crontabs());
|
||||
var crontab_string = '';
|
||||
tabs.data.forEach((tab) => {
|
||||
@@ -586,7 +613,8 @@ export default class CronService {
|
||||
if (
|
||||
tab.isDisabled === 1 ||
|
||||
_schedule!.length !== 5 ||
|
||||
tab.extra_schedules?.length
|
||||
tab.extra_schedules?.length ||
|
||||
this.isSpecialSchedule(tab.schedule)
|
||||
) {
|
||||
crontab_string += '# ';
|
||||
crontab_string += tab.schedule;
|
||||
@@ -601,13 +629,13 @@ export default class CronService {
|
||||
}
|
||||
});
|
||||
|
||||
await fs.writeFile(config.crontabFile, crontab_string);
|
||||
await writeFileWithLock(config.crontabFile, crontab_string);
|
||||
|
||||
execSync(`crontab ${config.crontabFile}`);
|
||||
await CrontabModel.update({ saved: true }, { where: {} });
|
||||
}
|
||||
|
||||
public import_crontab() {
|
||||
public importCrontab() {
|
||||
exec('crontab -l', (error, stdout, stderr) => {
|
||||
const lines = stdout.split('\n');
|
||||
const namePrefix = new Date().getTime();
|
||||
@@ -643,17 +671,38 @@ export default class CronService {
|
||||
|
||||
public async autosave_crontab() {
|
||||
const tabs = await this.crontabs();
|
||||
this.set_crontab(tabs);
|
||||
this.setCrontab(tabs);
|
||||
|
||||
const sixCron = tabs.data
|
||||
.filter((x) => this.isNodeCron(x) && x.isDisabled !== 1)
|
||||
const regularCrons = tabs.data
|
||||
.filter(
|
||||
(x) =>
|
||||
this.isNodeCron(x) &&
|
||||
x.isDisabled !== 1 &&
|
||||
!this.isSpecialSchedule(x.schedule),
|
||||
)
|
||||
.map((doc) => ({
|
||||
name: doc.name || '',
|
||||
id: String(doc.id),
|
||||
schedule: doc.schedule!,
|
||||
command: this.makeCommand(doc),
|
||||
extraSchedules: doc.extra_schedules || [],
|
||||
extra_schedules: doc.extra_schedules || [],
|
||||
}));
|
||||
await cronClient.addCron(sixCron);
|
||||
await cronClient.addCron(regularCrons);
|
||||
}
|
||||
|
||||
public async bootTask() {
|
||||
const tabs = await this.crontabs();
|
||||
const bootTasks = tabs.data.filter(
|
||||
(x) => !x.isDisabled && this.isBootSchedule(x.schedule),
|
||||
);
|
||||
if (bootTasks.length > 0) {
|
||||
await CrontabModel.update(
|
||||
{ status: CrontabStatus.queued },
|
||||
{ where: { id: bootTasks.map((t) => t.id!) } },
|
||||
);
|
||||
for (const task of bootTasks) {
|
||||
await this.runSingle(task.id!);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,7 +353,7 @@ export default class DependenceService {
|
||||
});
|
||||
this.updateLog(depIds, message);
|
||||
|
||||
let status = null;
|
||||
let status: number;
|
||||
if (isSucceed) {
|
||||
status = isInstall
|
||||
? DependenceStatus.installed
|
||||
|
||||
+11
-10
@@ -13,6 +13,7 @@ import {
|
||||
} from '../data/env';
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import { FindOptions, Op } from 'sequelize';
|
||||
import { writeFileWithLock } from '../shared/utils';
|
||||
|
||||
@Service()
|
||||
export default class EnvService {
|
||||
@@ -40,7 +41,7 @@ export default class EnvService {
|
||||
}
|
||||
|
||||
public async insert(payloads: Env[]): Promise<Env[]> {
|
||||
const result = [];
|
||||
const result: Env[] = [];
|
||||
for (const env of payloads) {
|
||||
const doc = await EnvModel.create(env, { returning: true });
|
||||
result.push(doc);
|
||||
@@ -61,7 +62,7 @@ export default class EnvService {
|
||||
return await this.getDb({ id: payload.id });
|
||||
}
|
||||
|
||||
public async remove(ids: string[]) {
|
||||
public async remove(ids: number[]) {
|
||||
await EnvModel.destroy({ where: { id: ids } });
|
||||
await this.set_envs();
|
||||
}
|
||||
@@ -149,7 +150,7 @@ export default class EnvService {
|
||||
['position', 'DESC'],
|
||||
['createdAt', 'ASC'],
|
||||
]);
|
||||
return result as any;
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
@@ -160,7 +161,7 @@ export default class EnvService {
|
||||
where: { ...query },
|
||||
order: [...sort],
|
||||
});
|
||||
return docs;
|
||||
return docs.map((x) => x.get({ plain: true }));
|
||||
}
|
||||
|
||||
public async getDb(query: FindOptions<Env>['where']): Promise<Env> {
|
||||
@@ -171,7 +172,7 @@ export default class EnvService {
|
||||
return doc.get({ plain: true });
|
||||
}
|
||||
|
||||
public async disabled(ids: string[]) {
|
||||
public async disabled(ids: number[]) {
|
||||
await EnvModel.update(
|
||||
{ status: EnvStatus.disabled },
|
||||
{ where: { id: ids } },
|
||||
@@ -179,12 +180,12 @@ export default class EnvService {
|
||||
await this.set_envs();
|
||||
}
|
||||
|
||||
public async enabled(ids: string[]) {
|
||||
public async enabled(ids: number[]) {
|
||||
await EnvModel.update({ status: EnvStatus.normal }, { where: { id: ids } });
|
||||
await this.set_envs();
|
||||
}
|
||||
|
||||
public async updateNames({ ids, name }: { ids: string[]; name: string }) {
|
||||
public async updateNames({ ids, name }: { ids: number[]; name: string }) {
|
||||
await EnvModel.update({ name }, { where: { id: ids } });
|
||||
await this.set_envs();
|
||||
}
|
||||
@@ -225,8 +226,8 @@ export default class EnvService {
|
||||
}
|
||||
}
|
||||
}
|
||||
await fs.writeFile(config.envFile, env_string);
|
||||
await fs.writeFile(config.jsEnvFile, js_env_string);
|
||||
await fs.writeFile(config.pyEnvFile, py_env_string);
|
||||
await writeFileWithLock(config.envFile, env_string);
|
||||
await writeFileWithLock(config.jsEnvFile, js_env_string);
|
||||
await writeFileWithLock(config.pyEnvFile, py_env_string);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import path from 'path';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import winston from 'winston';
|
||||
import config from '../config';
|
||||
|
||||
@Service()
|
||||
export default class LogService {
|
||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||
|
||||
public checkFilePath(filePath: string, fileName: string) {
|
||||
const finalPath = path.resolve(config.logPath, filePath, fileName);
|
||||
return finalPath.startsWith(config.logPath) ? finalPath : '';
|
||||
}
|
||||
}
|
||||
+119
-45
@@ -3,12 +3,9 @@ import got from 'got';
|
||||
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
|
||||
import nodemailer from 'nodemailer';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import winston from 'winston';
|
||||
import { parseBody, parseHeaders, safeJSONParse } from '../config/util';
|
||||
import { parseBody, parseHeaders } from '../config/util';
|
||||
import { NotificationInfo } from '../data/notify';
|
||||
import UserService from './user';
|
||||
import { readFile } from 'fs/promises';
|
||||
import config from '../config';
|
||||
|
||||
@Service()
|
||||
export default class NotificationService {
|
||||
@@ -35,6 +32,8 @@ export default class NotificationService {
|
||||
['webhook', this.webhook],
|
||||
['lark', this.lark],
|
||||
['chronocat', this.chronocat],
|
||||
['ntfy', this.ntfy],
|
||||
['wxPusherBot', this.wxPusherBot],
|
||||
]);
|
||||
|
||||
private title = '';
|
||||
@@ -47,27 +46,6 @@ export default class NotificationService {
|
||||
|
||||
constructor() {}
|
||||
|
||||
public async externalNotify(
|
||||
title: string,
|
||||
content: string,
|
||||
): Promise<boolean | undefined> {
|
||||
const { type, ...rest } = safeJSONParse(
|
||||
await readFile(config.systemNotifyFile, 'utf-8'),
|
||||
);
|
||||
if (type) {
|
||||
this.title = title;
|
||||
this.content = content;
|
||||
this.params = rest;
|
||||
const notificationModeAction = this.modeMap.get(type);
|
||||
try {
|
||||
return await notificationModeAction?.call(this);
|
||||
} catch (error: any) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public async notify(
|
||||
title: string,
|
||||
content: string,
|
||||
@@ -151,14 +129,19 @@ export default class NotificationService {
|
||||
|
||||
private async serverChan() {
|
||||
const { serverChanKey } = this.params;
|
||||
const url = serverChanKey.startsWith('SCT')
|
||||
? `https://sctapi.ftqq.com/${serverChanKey}.send`
|
||||
: `https://sc.ftqq.com/${serverChanKey}.send`;
|
||||
const matchResult = serverChanKey.match(/^sctp(\d+)t/i);
|
||||
const url =
|
||||
matchResult && matchResult[1]
|
||||
? `https://${matchResult[1]}.push.ft07.com/send/${serverChanKey}.send`
|
||||
: `https://sctapi.ftqq.com/${serverChanKey}.send`;
|
||||
|
||||
try {
|
||||
const res: any = await got
|
||||
.post(url, {
|
||||
...this.gotOption,
|
||||
body: `title=${this.title}&desp=${this.content}`,
|
||||
body: `title=${encodeURIComponent(
|
||||
this.title,
|
||||
)}&desp=${encodeURIComponent(this.content)}`,
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
})
|
||||
.json();
|
||||
@@ -199,11 +182,10 @@ export default class NotificationService {
|
||||
}
|
||||
|
||||
private async chat() {
|
||||
const { chatUrl, chatToken } = this.params;
|
||||
const url = `${chatUrl}${chatToken}`;
|
||||
const { synologyChatUrl } = this.params;
|
||||
try {
|
||||
const res: any = await got
|
||||
.post(url, {
|
||||
.post(synologyChatUrl, {
|
||||
...this.gotOption,
|
||||
body: `payload={"text":"${this.title}\n${this.content}"}`,
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
@@ -519,20 +501,33 @@ export default class NotificationService {
|
||||
}
|
||||
|
||||
private async pushPlus() {
|
||||
const { pushPlusToken, pushPlusUser } = this.params;
|
||||
const {
|
||||
pushPlusToken,
|
||||
pushPlusUser,
|
||||
pushplusWebhook,
|
||||
pushPlusTemplate,
|
||||
pushplusChannel,
|
||||
pushplusCallbackUrl,
|
||||
pushplusTo,
|
||||
} = this.params;
|
||||
const url = `https://www.pushplus.plus/send`;
|
||||
try {
|
||||
const res: any = await got
|
||||
.post(url, {
|
||||
...this.gotOption,
|
||||
json: {
|
||||
token: `${pushPlusToken}`,
|
||||
title: `${this.title}`,
|
||||
content: `${this.content.replace(/[\n\r]/g, '<br>')}`,
|
||||
topic: `${pushPlusUser || ''}`,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
let body = {
|
||||
...this.gotOption,
|
||||
json: {
|
||||
token: `${pushPlusToken}`,
|
||||
title: `${this.title}`,
|
||||
content: `${this.content.replace(/[\n\r]/g, '<br>')}`,
|
||||
topic: `${pushPlusUser || ''}`,
|
||||
template: `${pushPlusTemplate || 'html'}`,
|
||||
channel: `${pushplusChannel || 'wechat'}`,
|
||||
webhook: `${pushplusWebhook || ''}`,
|
||||
callbackUrl: `${pushplusCallbackUrl || ''}`,
|
||||
to: `${pushplusTo || ''}`,
|
||||
},
|
||||
};
|
||||
|
||||
const res: any = await got.post(url, body).json();
|
||||
|
||||
if (res.code === 200) {
|
||||
return true;
|
||||
@@ -661,6 +656,85 @@ export default class NotificationService {
|
||||
}
|
||||
}
|
||||
|
||||
private async ntfy() {
|
||||
const { ntfyUrl, ntfyTopic, ntfyPriority } = this.params;
|
||||
// 编码函数
|
||||
const encodeRfc2047 = (text: string, charset: string = 'UTF-8'): string => {
|
||||
const encodedText = Buffer.from(text).toString('base64');
|
||||
return `=?${charset}?B?${encodedText}?=`;
|
||||
};
|
||||
try {
|
||||
const encodedTitle = encodeRfc2047(this.title);
|
||||
const res: any = await got.post(
|
||||
`${ntfyUrl || 'https://ntfy.sh'}/${ntfyTopic}`,
|
||||
{
|
||||
...this.gotOption,
|
||||
body: `${this.content}`,
|
||||
headers: { Title: encodedTitle, Priority: `${ntfyPriority || '3'}` },
|
||||
},
|
||||
);
|
||||
if (res.statusCode === 200) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(JSON.stringify(res));
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw new Error(error.response ? error.response.body : error);
|
||||
}
|
||||
}
|
||||
|
||||
private async wxPusherBot() {
|
||||
const { wxPusherBotAppToken, wxPusherBotTopicIds, wxPusherBotUids } =
|
||||
this.params;
|
||||
// 处理 topicIds,将分号分隔的字符串转为数组
|
||||
const topicIds = wxPusherBotTopicIds
|
||||
? wxPusherBotTopicIds
|
||||
.split(';')
|
||||
.map((id) => id.trim())
|
||||
.filter((id) => id)
|
||||
.map((id) => parseInt(id))
|
||||
: [];
|
||||
|
||||
// 处理 uids,将分号分隔的字符串转为数组
|
||||
const uids = wxPusherBotUids
|
||||
? wxPusherBotUids
|
||||
.split(';')
|
||||
.map((uid) => uid.trim())
|
||||
.filter((uid) => uid)
|
||||
: [];
|
||||
|
||||
// topic_ids 和 uids 至少要有一个
|
||||
if (!topicIds.length && !uids.length) {
|
||||
throw new Error('wxPusher 服务的 TopicIds 和 Uids 至少配置一个才行');
|
||||
}
|
||||
|
||||
const url = `https://wxpusher.zjiecode.com/api/send/message`;
|
||||
try {
|
||||
const res: any = await got
|
||||
.post(url, {
|
||||
...this.gotOption,
|
||||
json: {
|
||||
appToken: wxPusherBotAppToken,
|
||||
content: `<h1>${this.title}</h1><br/><div style='white-space: pre-wrap;'>${this.content}</div>`,
|
||||
summary: this.title,
|
||||
contentType: 2,
|
||||
topicIds: topicIds,
|
||||
uids: uids,
|
||||
verifyPayType: 0,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
|
||||
if (res.code === 1000) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(JSON.stringify(res));
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw new Error(error.response ? error.response.body : error);
|
||||
}
|
||||
}
|
||||
|
||||
private async chronocat() {
|
||||
const { chronocatURL, chronocatQQ, chronocatToken } = this.params;
|
||||
try {
|
||||
@@ -727,7 +801,7 @@ export default class NotificationService {
|
||||
webhookContentType,
|
||||
} = this.params;
|
||||
|
||||
if (!webhookUrl.includes('$title') && !webhookBody.includes('$title')) {
|
||||
if (!webhookUrl?.includes('$title') && !webhookBody?.includes('$title')) {
|
||||
throw new Error('Url 或者 Body 中必须包含 $title');
|
||||
}
|
||||
|
||||
|
||||
+28
-20
@@ -1,19 +1,18 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import winston from 'winston';
|
||||
import { createRandomString } from '../config/util';
|
||||
import config from '../config';
|
||||
import { App, AppModel } from '../data/open';
|
||||
import { v4 as uuidV4 } from 'uuid';
|
||||
import sequelize, { Op } from 'sequelize';
|
||||
import { shareStore } from '../shared/store';
|
||||
|
||||
@Service()
|
||||
export default class OpenService {
|
||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||
|
||||
public async findTokenByValue(token: string): Promise<App | null> {
|
||||
public async findApps(): Promise<App[] | null> {
|
||||
const docs = await this.find({});
|
||||
const doc = docs.filter((x) => x.tokens?.find((y) => y.value === token));
|
||||
return doc[0];
|
||||
return docs;
|
||||
}
|
||||
|
||||
public async create(payload: App): Promise<App> {
|
||||
@@ -21,6 +20,8 @@ export default class OpenService {
|
||||
tab.client_id = createRandomString(12, 12);
|
||||
tab.client_secret = createRandomString(24, 24);
|
||||
const doc = await this.insert(tab);
|
||||
const apps = await this.find({});
|
||||
await shareStore.updateApps(apps);
|
||||
return { ...doc, tokens: [] };
|
||||
}
|
||||
|
||||
@@ -34,17 +35,19 @@ export default class OpenService {
|
||||
name: payload.name,
|
||||
scopes: payload.scopes,
|
||||
id: payload.id,
|
||||
} as any);
|
||||
} as App);
|
||||
return { ...newDoc, tokens: [] };
|
||||
}
|
||||
|
||||
private async updateDb(payload: App): Promise<App> {
|
||||
private async updateDb(payload: Partial<App>): Promise<App> {
|
||||
await AppModel.update(payload, { where: { id: payload.id } });
|
||||
return await this.getDb({ id: payload.id });
|
||||
const apps = await this.find({});
|
||||
await shareStore.updateApps(apps);
|
||||
return apps?.find((x) => x.id === payload.id) as App;
|
||||
}
|
||||
|
||||
public async getDb(query: any): Promise<App> {
|
||||
const doc: any = await AppModel.findOne({ where: query });
|
||||
public async getDb(query: Record<string, any>): Promise<App> {
|
||||
const doc = await AppModel.findOne({ where: query });
|
||||
if (!doc) {
|
||||
throw new Error(`App ${JSON.stringify(query)} not found`);
|
||||
}
|
||||
@@ -53,10 +56,12 @@ export default class OpenService {
|
||||
|
||||
public async remove(ids: number[]) {
|
||||
await AppModel.destroy({ where: { id: ids } });
|
||||
const apps = await this.find({});
|
||||
await shareStore.updateApps(apps);
|
||||
}
|
||||
|
||||
public async resetSecret(id: number): Promise<App> {
|
||||
const tab: any = {
|
||||
const tab: Partial<App> = {
|
||||
client_secret: createRandomString(24, 24),
|
||||
tokens: [],
|
||||
id,
|
||||
@@ -74,7 +79,7 @@ export default class OpenService {
|
||||
public async list(
|
||||
searchText: string = '',
|
||||
sort: any = {},
|
||||
query: any = {},
|
||||
query: Record<string, any> = {},
|
||||
): Promise<App[]> {
|
||||
let condition = { ...query };
|
||||
if (searchText) {
|
||||
@@ -101,7 +106,7 @@ export default class OpenService {
|
||||
}
|
||||
}
|
||||
|
||||
private async find(query: any, sort?: any): Promise<App[]> {
|
||||
private async find(query: Record<string, any>, sort?: any): Promise<App[]> {
|
||||
const docs = await AppModel.findAll({ where: { ...query } });
|
||||
return docs.map((x) => x.get({ plain: true }));
|
||||
}
|
||||
@@ -135,6 +140,8 @@ export default class OpenService {
|
||||
{ tokens },
|
||||
{ where: { client_id, client_secret } },
|
||||
);
|
||||
const apps = await this.find({});
|
||||
await shareStore.updateApps(apps);
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
@@ -144,7 +151,7 @@ export default class OpenService {
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return { code: 400, message: 'client_id或client_seret有误' };
|
||||
return { code: 400, message: 'client_id 或 client_seret 有误' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,14 +159,15 @@ export default class OpenService {
|
||||
value: string;
|
||||
expiration: number;
|
||||
}> {
|
||||
let systemApp = (await AppModel.findOne({
|
||||
where: { name: 'system' },
|
||||
})) as App;
|
||||
const apps = await shareStore.getApps();
|
||||
const systemApp = apps?.find((x) => x.name === 'system');
|
||||
if (!systemApp) {
|
||||
systemApp = await this.create({
|
||||
name: 'system',
|
||||
scopes: ['crons', 'system'],
|
||||
} as App);
|
||||
throw new Error('system app not found');
|
||||
}
|
||||
const now = Math.round(Date.now() / 1000);
|
||||
const currentToken = systemApp.tokens?.find((x) => x.expiration > now);
|
||||
if (currentToken) {
|
||||
return currentToken;
|
||||
}
|
||||
const { data } = await this.authToken({
|
||||
client_id: systemApp.client_id,
|
||||
|
||||
@@ -214,7 +214,7 @@ export default class ScheduleService {
|
||||
const job = new LongIntervalJob(
|
||||
{ runImmediately: false, ...schedule },
|
||||
task,
|
||||
_id,
|
||||
{ id: _id },
|
||||
);
|
||||
|
||||
this.intervalSchedule.addIntervalJob(job);
|
||||
|
||||
@@ -64,10 +64,15 @@ export default class ScriptService {
|
||||
return { code: 200 };
|
||||
}
|
||||
|
||||
public async getFile(filePath: string, fileName: string) {
|
||||
public checkFilePath(filePath: string, fileName: string) {
|
||||
const finalPath = path.resolve(config.scriptPath, filePath, fileName);
|
||||
return finalPath.startsWith(config.scriptPath) ? finalPath : '';
|
||||
}
|
||||
|
||||
if (!finalPath.startsWith(config.scriptPath)) {
|
||||
public async getFile(filePath: string, fileName: string) {
|
||||
const finalPath = this.checkFilePath(filePath, fileName);
|
||||
|
||||
if (!finalPath) {
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
+17
-12
@@ -7,6 +7,7 @@ import { Subscription } from '../data/subscription';
|
||||
import { formatUrl } from '../config/subscription';
|
||||
import config from '../config';
|
||||
import { fileExist, rmPath } from '../config/util';
|
||||
import { writeFileWithLock } from '../shared/utils';
|
||||
|
||||
@Service()
|
||||
export default class SshKeyService {
|
||||
@@ -25,13 +26,12 @@ export default class SshKeyService {
|
||||
if (_exist) {
|
||||
config = await fs.readFile(this.sshConfigFilePath, { encoding: 'utf-8' });
|
||||
} else {
|
||||
await fs.writeFile(this.sshConfigFilePath, '');
|
||||
await writeFileWithLock(this.sshConfigFilePath, '');
|
||||
}
|
||||
if (!config.includes(this.sshConfigHeader)) {
|
||||
await fs.writeFile(
|
||||
await writeFileWithLock(
|
||||
this.sshConfigFilePath,
|
||||
`${this.sshConfigHeader}\n\n${config}`,
|
||||
{ encoding: 'utf-8' },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -41,10 +41,14 @@ export default class SshKeyService {
|
||||
key: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await fs.writeFile(path.join(this.sshPath, alias), `${key}${os.EOL}`, {
|
||||
encoding: 'utf8',
|
||||
mode: '400',
|
||||
});
|
||||
await writeFileWithLock(
|
||||
path.join(this.sshPath, alias),
|
||||
`${key}${os.EOL}`,
|
||||
{
|
||||
encoding: 'utf8',
|
||||
mode: '400',
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error('生成私钥文件失败', error);
|
||||
}
|
||||
@@ -74,12 +78,9 @@ export default class SshKeyService {
|
||||
this.sshPath,
|
||||
alias,
|
||||
)}\n StrictHostKeyChecking no\n${proxyStr}`;
|
||||
await fs.writeFile(
|
||||
await writeFileWithLock(
|
||||
`${path.join(this.sshPath, `${alias}.config`)}`,
|
||||
config,
|
||||
{
|
||||
encoding: 'utf8',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -102,7 +103,11 @@ export default class SshKeyService {
|
||||
await this.generateSingleSshConfig(alias, host, proxy);
|
||||
}
|
||||
|
||||
public async removeSSHKey(alias: string, host: string, proxy?: string): Promise<void> {
|
||||
public async removeSSHKey(
|
||||
alias: string,
|
||||
host: string,
|
||||
proxy?: string,
|
||||
): Promise<void> {
|
||||
await this.removePrivateKeyFile(alias);
|
||||
await this.removeSshConfig(alias);
|
||||
}
|
||||
|
||||
+29
-7
@@ -16,6 +16,7 @@ import {
|
||||
promiseExec,
|
||||
readDirs,
|
||||
rmPath,
|
||||
setSystemTimezone,
|
||||
} from '../config/util';
|
||||
import {
|
||||
DependenceModel,
|
||||
@@ -50,17 +51,21 @@ export default class SystemService {
|
||||
|
||||
public async getSystemConfig() {
|
||||
const doc = await this.getDb({ type: AuthDataType.systemConfig });
|
||||
return doc;
|
||||
return {
|
||||
...doc,
|
||||
info: { ...doc.info, timezone: doc.info?.timezone || 'Asia/Shanghai' },
|
||||
};
|
||||
}
|
||||
|
||||
private async updateAuthDb(payload: SystemInfo): Promise<SystemInfo> {
|
||||
await SystemModel.upsert({ ...payload });
|
||||
const doc = await this.getDb({ type: payload.type });
|
||||
const { id, ...others } = payload;
|
||||
await SystemModel.update(others, { where: { id } });
|
||||
const doc = await this.getDb({ id });
|
||||
return doc;
|
||||
}
|
||||
|
||||
public async getDb(query: any): Promise<SystemInfo> {
|
||||
const doc = await SystemModel.findOne({ where: { ...query } });
|
||||
const doc = await SystemModel.findOne({ where: query });
|
||||
if (!doc) {
|
||||
throw new Error(`System ${JSON.stringify(query)} not found`);
|
||||
}
|
||||
@@ -402,7 +407,7 @@ export default class SystemService {
|
||||
public async exportData(res: Response) {
|
||||
try {
|
||||
await promiseExec(
|
||||
`cd ${config.rootPath} && tar -zcvf ${config.dataTgzFile} data/`,
|
||||
`cd ${config.dataPath} && cd ../ && tar -zcvf ${config.dataTgzFile} data/`,
|
||||
);
|
||||
res.download(config.dataTgzFile);
|
||||
} catch (error: any) {
|
||||
@@ -414,7 +419,7 @@ export default class SystemService {
|
||||
try {
|
||||
await promiseExec(`rm -rf ${path.join(config.tmpPath, 'data')}`);
|
||||
const res = await promiseExec(
|
||||
`cd ${config.tmpPath} && tar -zxvf data.tgz`,
|
||||
`cd ${config.tmpPath} && tar -zxvf ${config.dataTgzFile}`,
|
||||
);
|
||||
return { code: 200, data: res };
|
||||
} catch (error: any) {
|
||||
@@ -439,7 +444,7 @@ export default class SystemService {
|
||||
const logs = result
|
||||
.reverse()
|
||||
.filter((x) => x.title.endsWith('.log'))
|
||||
.filter((x) => x.mtime >= startTime && x.mtime <= endTime);
|
||||
.filter((x) => x.createTime >= startTime && x.createTime <= endTime);
|
||||
|
||||
res.set({
|
||||
'Content-Length': sum(logs.map((x) => x.size)),
|
||||
@@ -470,4 +475,21 @@ export default class SystemService {
|
||||
await rmPath(path.join(config.systemLogPath, log.title));
|
||||
}
|
||||
}
|
||||
|
||||
public async updateTimezone(info: SystemModelInfo) {
|
||||
if (!info.timezone) {
|
||||
info.timezone = 'Asia/Shanghai';
|
||||
}
|
||||
const oDoc = await this.getSystemConfig();
|
||||
await this.updateAuthDb({
|
||||
...oDoc,
|
||||
info: { ...oDoc.info, ...info },
|
||||
});
|
||||
const success = await setSystemTimezone(info.timezone);
|
||||
if (success) {
|
||||
return { code: 200, data: info };
|
||||
} else {
|
||||
return { code: 400, message: '设置时区失败' };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+150
-192
@@ -1,15 +1,7 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import winston from 'winston';
|
||||
import {
|
||||
createFile,
|
||||
createRandomString,
|
||||
fileExist,
|
||||
getNetIp,
|
||||
getPlatform,
|
||||
safeJSONParse,
|
||||
} from '../config/util';
|
||||
import { createRandomString, getNetIp } from '../config/util';
|
||||
import config from '../config';
|
||||
import * as fs from 'fs/promises';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { authenticator } from '@otplib/preset-default';
|
||||
import {
|
||||
@@ -18,6 +10,7 @@ import {
|
||||
SystemModel,
|
||||
SystemModelInfo,
|
||||
LoginStatus,
|
||||
AuthInfo,
|
||||
} from '../data/system';
|
||||
import { NotificationInfo } from '../data/notify';
|
||||
import NotificationService from './notify';
|
||||
@@ -28,6 +21,7 @@ import dayjs from 'dayjs';
|
||||
import IP2Region from 'ip2region';
|
||||
import requestIp from 'request-ip';
|
||||
import uniq from 'lodash/uniq';
|
||||
import { shareStore } from '../shared/store';
|
||||
|
||||
@Service()
|
||||
export default class UserService {
|
||||
@@ -48,161 +42,138 @@ export default class UserService {
|
||||
req: Request,
|
||||
needTwoFactor = true,
|
||||
): Promise<any> {
|
||||
const _exist = await fileExist(config.authConfigFile);
|
||||
if (!_exist) {
|
||||
return this.initAuthInfo();
|
||||
}
|
||||
|
||||
let { username, password } = payloads;
|
||||
const content = await this.getAuthInfo();
|
||||
const timestamp = Date.now();
|
||||
if (content) {
|
||||
let {
|
||||
username: cUsername,
|
||||
password: cPassword,
|
||||
retries = 0,
|
||||
lastlogon,
|
||||
lastip,
|
||||
lastaddr,
|
||||
twoFactorActivated,
|
||||
twoFactorActived,
|
||||
tokens = {},
|
||||
platform,
|
||||
} = content;
|
||||
// patch old field
|
||||
twoFactorActivated = twoFactorActivated || twoFactorActived;
|
||||
let {
|
||||
username: cUsername,
|
||||
password: cPassword,
|
||||
retries = 0,
|
||||
lastlogon,
|
||||
lastip,
|
||||
lastaddr,
|
||||
twoFactorActivated,
|
||||
tokens = {},
|
||||
platform,
|
||||
} = content;
|
||||
const retriesTime = Math.pow(3, retries) * 1000;
|
||||
if (retries > 2 && timestamp - lastlogon < retriesTime) {
|
||||
const waitTime = Math.ceil(
|
||||
(retriesTime - (timestamp - lastlogon)) / 1000,
|
||||
);
|
||||
return {
|
||||
code: 410,
|
||||
message: `失败次数过多,请${waitTime}秒后重试`,
|
||||
data: waitTime,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
(cUsername === 'admin' && cPassword === 'admin') ||
|
||||
!cUsername ||
|
||||
!cPassword
|
||||
) {
|
||||
return this.initAuthInfo();
|
||||
}
|
||||
if (
|
||||
username === cUsername &&
|
||||
password === cPassword &&
|
||||
twoFactorActivated &&
|
||||
needTwoFactor
|
||||
) {
|
||||
await this.updateAuthInfo(content, {
|
||||
isTwoFactorChecking: true,
|
||||
});
|
||||
return {
|
||||
code: 420,
|
||||
message: '',
|
||||
};
|
||||
}
|
||||
|
||||
const retriesTime = Math.pow(3, retries) * 1000;
|
||||
if (retries > 2 && timestamp - lastlogon < retriesTime) {
|
||||
const waitTime = Math.ceil(
|
||||
(retriesTime - (timestamp - lastlogon)) / 1000,
|
||||
);
|
||||
const ip = requestIp.getClientIp(req) || '';
|
||||
const query = new IP2Region();
|
||||
const ipAddress = query.search(ip);
|
||||
let address = '';
|
||||
if (ipAddress) {
|
||||
const { country, province, city, isp } = ipAddress;
|
||||
address = uniq([country, province, city, isp]).filter(Boolean).join(' ');
|
||||
}
|
||||
if (username === cUsername && password === cPassword) {
|
||||
const data = createRandomString(50, 100);
|
||||
const expiration = twoFactorActivated ? 60 : 20;
|
||||
let token = jwt.sign({ data }, config.secret as any, {
|
||||
expiresIn: 60 * 60 * 24 * expiration,
|
||||
algorithm: 'HS384',
|
||||
});
|
||||
|
||||
await this.updateAuthInfo(content, {
|
||||
token,
|
||||
tokens: {
|
||||
...tokens,
|
||||
[req.platform]: token,
|
||||
},
|
||||
lastlogon: timestamp,
|
||||
retries: 0,
|
||||
lastip: ip,
|
||||
lastaddr: address,
|
||||
platform: req.platform,
|
||||
isTwoFactorChecking: false,
|
||||
});
|
||||
this.notificationService.notify(
|
||||
'登录通知',
|
||||
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}在 ${address} ${
|
||||
req.platform
|
||||
}端 登录成功,ip地址 ${ip}`,
|
||||
);
|
||||
await this.insertDb({
|
||||
type: AuthDataType.loginLog,
|
||||
info: {
|
||||
timestamp,
|
||||
address,
|
||||
ip,
|
||||
platform: req.platform,
|
||||
status: LoginStatus.success,
|
||||
},
|
||||
});
|
||||
this.getLoginLog();
|
||||
return {
|
||||
code: 200,
|
||||
data: { token, lastip, lastaddr, lastlogon, retries, platform },
|
||||
};
|
||||
} else {
|
||||
await this.updateAuthInfo(content, {
|
||||
retries: retries + 1,
|
||||
lastlogon: timestamp,
|
||||
lastip: ip,
|
||||
lastaddr: address,
|
||||
platform: req.platform,
|
||||
});
|
||||
this.notificationService.notify(
|
||||
'登录通知',
|
||||
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}在 ${address} ${
|
||||
req.platform
|
||||
}端 登录失败,ip地址 ${ip}`,
|
||||
);
|
||||
await this.insertDb({
|
||||
type: AuthDataType.loginLog,
|
||||
info: {
|
||||
timestamp,
|
||||
address,
|
||||
ip,
|
||||
platform: req.platform,
|
||||
status: LoginStatus.fail,
|
||||
},
|
||||
});
|
||||
this.getLoginLog();
|
||||
if (retries > 2) {
|
||||
const waitTime = Math.round(Math.pow(3, retries + 1));
|
||||
return {
|
||||
code: 410,
|
||||
message: `失败次数过多,请${waitTime}秒后重试`,
|
||||
data: waitTime,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
username === cUsername &&
|
||||
password === cPassword &&
|
||||
twoFactorActivated &&
|
||||
needTwoFactor
|
||||
) {
|
||||
this.updateAuthInfo(content, {
|
||||
isTwoFactorChecking: true,
|
||||
});
|
||||
return {
|
||||
code: 420,
|
||||
message: '',
|
||||
};
|
||||
}
|
||||
|
||||
const ip = requestIp.getClientIp(req) || '';
|
||||
const query = new IP2Region();
|
||||
const ipAddress = query.search(ip);
|
||||
let address = '';
|
||||
if (ipAddress) {
|
||||
const { country, province, city, isp } = ipAddress;
|
||||
address = uniq([country, province, city, isp])
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
}
|
||||
if (username === cUsername && password === cPassword) {
|
||||
const data = createRandomString(50, 100);
|
||||
const expiration = twoFactorActivated ? 60 : 20;
|
||||
let token = jwt.sign({ data }, config.secret as any, {
|
||||
expiresIn: 60 * 60 * 24 * expiration,
|
||||
algorithm: 'HS384',
|
||||
});
|
||||
|
||||
this.updateAuthInfo(content, {
|
||||
token,
|
||||
tokens: {
|
||||
...tokens,
|
||||
[req.platform]: token,
|
||||
},
|
||||
lastlogon: timestamp,
|
||||
retries: 0,
|
||||
lastip: ip,
|
||||
lastaddr: address,
|
||||
platform: req.platform,
|
||||
isTwoFactorChecking: false,
|
||||
});
|
||||
this.notificationService.notify(
|
||||
'登录通知',
|
||||
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}在 ${address} ${
|
||||
req.platform
|
||||
}端 登录成功,ip地址 ${ip}`,
|
||||
);
|
||||
await this.insertDb({
|
||||
type: AuthDataType.loginLog,
|
||||
info: {
|
||||
timestamp,
|
||||
address,
|
||||
ip,
|
||||
platform: req.platform,
|
||||
status: LoginStatus.success,
|
||||
},
|
||||
});
|
||||
this.getLoginLog();
|
||||
return {
|
||||
code: 200,
|
||||
data: { token, lastip, lastaddr, lastlogon, retries, platform },
|
||||
};
|
||||
} else {
|
||||
this.updateAuthInfo(content, {
|
||||
retries: retries + 1,
|
||||
lastlogon: timestamp,
|
||||
lastip: ip,
|
||||
lastaddr: address,
|
||||
platform: req.platform,
|
||||
});
|
||||
this.notificationService.notify(
|
||||
'登录通知',
|
||||
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}在 ${address} ${
|
||||
req.platform
|
||||
}端 登录失败,ip地址 ${ip}`,
|
||||
);
|
||||
await this.insertDb({
|
||||
type: AuthDataType.loginLog,
|
||||
info: {
|
||||
timestamp,
|
||||
address,
|
||||
ip,
|
||||
platform: req.platform,
|
||||
status: LoginStatus.fail,
|
||||
},
|
||||
});
|
||||
this.getLoginLog();
|
||||
if (retries > 2) {
|
||||
const waitTime = Math.round(Math.pow(3, retries + 1));
|
||||
return {
|
||||
code: 410,
|
||||
message: `失败次数过多,请${waitTime}秒后重试`,
|
||||
data: waitTime,
|
||||
};
|
||||
} else {
|
||||
return { code: 400, message: config.authError };
|
||||
}
|
||||
return { code: 400, message: config.authError };
|
||||
}
|
||||
} else {
|
||||
return this.initAuthInfo();
|
||||
}
|
||||
}
|
||||
|
||||
public async logout(platform: string): Promise<any> {
|
||||
const authInfo = await this.getAuthInfo();
|
||||
this.updateAuthInfo(authInfo, {
|
||||
await this.updateAuthInfo(authInfo, {
|
||||
token: '',
|
||||
tokens: { ...authInfo.tokens, [platform]: '' },
|
||||
});
|
||||
@@ -217,7 +188,7 @@ export default class UserService {
|
||||
(a, b) => b.info!.timestamp! - a.info!.timestamp!,
|
||||
);
|
||||
if (result.length > 100) {
|
||||
const ids = result.slice(0, result.length - 100).map((x) => x.id!);
|
||||
const ids = result.slice(100).map((x) => x.id!);
|
||||
await SystemModel.destroy({
|
||||
where: { id: ids },
|
||||
});
|
||||
@@ -232,20 +203,6 @@ export default class UserService {
|
||||
return doc;
|
||||
}
|
||||
|
||||
private async initAuthInfo() {
|
||||
await fs.writeFile(
|
||||
config.authConfigFile,
|
||||
JSON.stringify({
|
||||
username: 'admin',
|
||||
password: 'admin',
|
||||
}),
|
||||
);
|
||||
return {
|
||||
code: 100,
|
||||
message: '未找到认证文件,重新初始化',
|
||||
};
|
||||
}
|
||||
|
||||
public async updateUsernameAndPassword({
|
||||
username,
|
||||
password,
|
||||
@@ -257,35 +214,21 @@ export default class UserService {
|
||||
return { code: 400, message: '密码不能设置为admin' };
|
||||
}
|
||||
const authInfo = await this.getAuthInfo();
|
||||
this.updateAuthInfo(authInfo, { username, password });
|
||||
await this.updateAuthInfo(authInfo, { username, password });
|
||||
return { code: 200, message: '更新成功' };
|
||||
}
|
||||
|
||||
public async updateAvatar(avatar: string) {
|
||||
const authInfo = await this.getAuthInfo();
|
||||
this.updateAuthInfo(authInfo, { avatar });
|
||||
await this.updateAuthInfo(authInfo, { avatar });
|
||||
return { code: 200, data: avatar, message: '更新成功' };
|
||||
}
|
||||
|
||||
public async getUserInfo(): Promise<any> {
|
||||
const authFileExist = await fileExist(config.authConfigFile);
|
||||
if (!authFileExist) {
|
||||
await createFile(
|
||||
config.authConfigFile,
|
||||
JSON.stringify({
|
||||
username: 'admin',
|
||||
password: 'admin',
|
||||
}),
|
||||
);
|
||||
}
|
||||
return await this.getAuthInfo();
|
||||
}
|
||||
|
||||
public async initTwoFactor() {
|
||||
const secret = authenticator.generateSecret();
|
||||
const authInfo = await this.getAuthInfo();
|
||||
const otpauth = authenticator.keyuri(authInfo.username, 'qinglong', secret);
|
||||
this.updateAuthInfo(authInfo, { twoFactorSecret: secret });
|
||||
await this.updateAuthInfo(authInfo, { twoFactorSecret: secret });
|
||||
return { secret, url: otpauth };
|
||||
}
|
||||
|
||||
@@ -296,7 +239,7 @@ export default class UserService {
|
||||
secret: authInfo.twoFactorSecret,
|
||||
});
|
||||
if (isValid) {
|
||||
this.updateAuthInfo(authInfo, { twoFactorActivated: true });
|
||||
await this.updateAuthInfo(authInfo, { twoFactorActivated: true });
|
||||
}
|
||||
return isValid;
|
||||
}
|
||||
@@ -322,7 +265,7 @@ export default class UserService {
|
||||
return this.login({ username, password }, req, false);
|
||||
} else {
|
||||
const { ip, address } = await getNetIp(req);
|
||||
this.updateAuthInfo(authInfo, {
|
||||
await this.updateAuthInfo(authInfo, {
|
||||
lastip: ip,
|
||||
lastaddr: address,
|
||||
platform: req.platform,
|
||||
@@ -333,24 +276,29 @@ export default class UserService {
|
||||
|
||||
public async deactiveTwoFactor() {
|
||||
const authInfo = await this.getAuthInfo();
|
||||
this.updateAuthInfo(authInfo, {
|
||||
await this.updateAuthInfo(authInfo, {
|
||||
twoFactorActivated: false,
|
||||
twoFactorActived: false,
|
||||
twoFactorSecret: '',
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private async getAuthInfo() {
|
||||
const content = await fs.readFile(config.authConfigFile, 'utf8');
|
||||
return safeJSONParse(content);
|
||||
public async getAuthInfo() {
|
||||
const authInfo = await shareStore.getAuthInfo();
|
||||
if (authInfo) {
|
||||
return authInfo;
|
||||
}
|
||||
const doc = await this.getDb({ type: AuthDataType.authConfig });
|
||||
return (doc.info || {}) as AuthInfo;
|
||||
}
|
||||
|
||||
private async updateAuthInfo(authInfo: any, info: any) {
|
||||
await fs.writeFile(
|
||||
config.authConfigFile,
|
||||
JSON.stringify({ ...authInfo, ...info }),
|
||||
);
|
||||
private async updateAuthInfo(authInfo: AuthInfo, info: Partial<AuthInfo>) {
|
||||
const result = { ...authInfo, ...info };
|
||||
await shareStore.updateAuthInfo(result);
|
||||
await this.updateAuthDb({
|
||||
type: AuthDataType.authConfig,
|
||||
info: result,
|
||||
});
|
||||
}
|
||||
|
||||
public async getNotificationMode(): Promise<NotificationInfo> {
|
||||
@@ -359,7 +307,7 @@ export default class UserService {
|
||||
}
|
||||
|
||||
private async updateAuthDb(payload: SystemInfo): Promise<any> {
|
||||
let doc = await SystemModel.findOne({ type: payload.type });
|
||||
let doc = await SystemModel.findOne({ where: { type: payload.type } });
|
||||
if (doc) {
|
||||
const updateResult = await SystemModel.update(payload, {
|
||||
where: { id: doc.id },
|
||||
@@ -397,4 +345,14 @@ export default class UserService {
|
||||
return { code: 400, message: '通知发送失败,请检查参数' };
|
||||
}
|
||||
}
|
||||
|
||||
public async resetAuthInfo(info: Partial<AuthInfo>) {
|
||||
const { retries, twoFactorActivated, password } = info;
|
||||
const authInfo = await this.getAuthInfo();
|
||||
await this.updateAuthInfo(authInfo, {
|
||||
retries,
|
||||
twoFactorActivated,
|
||||
password,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+20
-3
@@ -11,6 +11,9 @@ import {
|
||||
IScheduleFn,
|
||||
TCron,
|
||||
} from './interface';
|
||||
import config from '../config';
|
||||
import { credentials } from '@grpc/grpc-js';
|
||||
import { ApiClient } from '../protos/api';
|
||||
|
||||
class TaskLimit {
|
||||
private dependenyLimit = new PQueue({ concurrency: 1 });
|
||||
@@ -33,6 +36,11 @@ class TaskLimit {
|
||||
private systemLimit = new PQueue({
|
||||
concurrency: Math.max(os.cpus().length, 4),
|
||||
});
|
||||
private client = new ApiClient(
|
||||
`0.0.0.0:${config.cronPort}`,
|
||||
credentials.createInsecure(),
|
||||
{ 'grpc.enable_http_proxy': 0 },
|
||||
);
|
||||
|
||||
get cronLimitActiveCount() {
|
||||
return this.cronLimit.pending;
|
||||
@@ -126,9 +134,18 @@ class TaskLimit {
|
||||
if (result?.length > 5) {
|
||||
if (repeatTimes < 3) {
|
||||
this.repeatCronNotifyMap.set(cron.id, repeatTimes + 1);
|
||||
this.notificationService.externalNotify(
|
||||
'任务重复运行',
|
||||
`任务:${cron.name},命令:${cron.command},定时:${cron.schedule},处于运行中的超过 5 个,请检查定时设置`,
|
||||
this.client.systemNotify(
|
||||
{
|
||||
title: '任务重复运行',
|
||||
content: `任务:${cron.name},命令:${cron.command},定时:${cron.schedule},处于运行中的超过 5 个,请检查定时设置`,
|
||||
},
|
||||
(err, res) => {
|
||||
if (err) {
|
||||
Logger.error(
|
||||
`[schedule][任务重复运行] 通知失败 ${JSON.stringify(err)}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
Logger.warn(`[schedule][任务重复运行] 参数 ${JSON.stringify(cron)}`);
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { AuthInfo } from '../data/system';
|
||||
import { App } from '../data/open';
|
||||
import Keyv from 'keyv';
|
||||
import KeyvSqlite from '@keyv/sqlite';
|
||||
import config from '../config';
|
||||
import path from 'path';
|
||||
|
||||
export enum EKeyv {
|
||||
'apps' = 'apps',
|
||||
'authInfo' = 'authInfo',
|
||||
}
|
||||
|
||||
export interface IKeyvStore {
|
||||
apps: App[];
|
||||
authInfo: AuthInfo;
|
||||
}
|
||||
|
||||
const keyvSqlite = new KeyvSqlite(path.join(config.dbPath, 'keyv.sqlite'));
|
||||
export const keyvStore = new Keyv<IKeyvStore>({ store: keyvSqlite });
|
||||
|
||||
export const shareStore = {
|
||||
getAuthInfo() {
|
||||
return keyvStore.get<IKeyvStore['authInfo']>(EKeyv.authInfo);
|
||||
},
|
||||
updateAuthInfo(value: IKeyvStore['authInfo']) {
|
||||
return keyvStore.set<IKeyvStore['authInfo']>(EKeyv.authInfo, value);
|
||||
},
|
||||
getApps() {
|
||||
return keyvStore.get<IKeyvStore['apps']>(EKeyv.apps);
|
||||
},
|
||||
updateApps(apps: App[]) {
|
||||
return keyvStore.set<IKeyvStore['apps']>(EKeyv.apps, apps);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import { lock } from 'proper-lockfile';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { writeFile, open, chmod } from 'fs/promises';
|
||||
import { fileExist } from '../config/util';
|
||||
|
||||
function getUniqueLockPath(filePath: string) {
|
||||
const sanitizedPath = filePath
|
||||
.replace(/[<>:"/\\|?*]/g, '_')
|
||||
.replace(/^_/, '');
|
||||
return path.join(os.tmpdir(), `${sanitizedPath}.ql_lock`);
|
||||
}
|
||||
|
||||
export async function writeFileWithLock(
|
||||
filePath: string,
|
||||
content: string | Buffer,
|
||||
options: Parameters<typeof writeFile>[2] = {},
|
||||
) {
|
||||
if (typeof options === 'string') {
|
||||
options = { encoding: options };
|
||||
}
|
||||
if (!(await fileExist(filePath))) {
|
||||
const fileHandle = await open(filePath, 'w');
|
||||
fileHandle.close();
|
||||
}
|
||||
const lockfilePath = getUniqueLockPath(filePath);
|
||||
|
||||
const release = await lock(filePath, {
|
||||
retries: {
|
||||
retries: 10,
|
||||
factor: 2,
|
||||
minTimeout: 100,
|
||||
maxTimeout: 3000,
|
||||
},
|
||||
lockfilePath,
|
||||
});
|
||||
await writeFile(filePath, content, { encoding: 'utf8', ...options });
|
||||
if (options?.mode) {
|
||||
await chmod(filePath, options.mode);
|
||||
}
|
||||
await release();
|
||||
}
|
||||
+2
-10
@@ -6,6 +6,7 @@ import fs from 'fs';
|
||||
import config from './config';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { writeFileWithLock } from './shared/utils';
|
||||
|
||||
const tokenFile = path.join(config.configPath, 'token.json');
|
||||
|
||||
@@ -25,16 +26,7 @@ async function getToken() {
|
||||
}
|
||||
|
||||
async function writeFile(data: any) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
fs.writeFile(
|
||||
tokenFile,
|
||||
`${JSON.stringify(data)}${os.EOL}`,
|
||||
{ encoding: 'utf8' },
|
||||
() => {
|
||||
resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
await writeFileWithLock(tokenFile, `${JSON.stringify(data)}${os.EOL}`);
|
||||
}
|
||||
|
||||
getToken();
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Joi } from 'celebrate';
|
||||
import cron_parser from 'cron-parser';
|
||||
import { ScheduleType } from '../interface/schedule';
|
||||
|
||||
const validateSchedule = (value: string, helpers: any) => {
|
||||
if (
|
||||
value.startsWith(ScheduleType.ONCE) ||
|
||||
value.startsWith(ScheduleType.BOOT)
|
||||
) {
|
||||
return value;
|
||||
}
|
||||
|
||||
try {
|
||||
if (cron_parser.parseExpression(value).hasNext()) {
|
||||
return value;
|
||||
}
|
||||
} catch (e) {
|
||||
return helpers.error('any.invalid');
|
||||
}
|
||||
return helpers.error('any.invalid');
|
||||
};
|
||||
|
||||
export const scheduleSchema = Joi.string()
|
||||
.required()
|
||||
.custom(validateSchedule)
|
||||
.messages({
|
||||
'any.invalid': '无效的定时规则',
|
||||
'string.empty': '定时规则不能为空',
|
||||
});
|
||||
|
||||
export const commonCronSchema = {
|
||||
name: Joi.string().optional(),
|
||||
command: Joi.string().required(),
|
||||
schedule: scheduleSchema,
|
||||
labels: Joi.array().optional(),
|
||||
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),
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
import { CommandModule } from 'yargs';
|
||||
export const updateCommand: CommandModule = {
|
||||
command: 'update',
|
||||
describe: 'Update and restart qinglong',
|
||||
builder: (yargs) => {
|
||||
return yargs.option('repositority', {
|
||||
type: 'string',
|
||||
alias: 'r',
|
||||
describe: `Specify the release warehouse address of the package`,
|
||||
});
|
||||
},
|
||||
handler: async (argv) => {},
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
import * as yargs from 'yargs';
|
||||
import { green, red } from 'chalk';
|
||||
import { updateCommand } from './commands/update';
|
||||
|
||||
yargs
|
||||
.usage('Usage: ql [command] <options>')
|
||||
.command(updateCommand)
|
||||
.fail((err) => {
|
||||
console.error(`${red(err)}`);
|
||||
})
|
||||
.alias('h', 'help')
|
||||
.showHelp()
|
||||
.recommendCommands().argv;
|
||||
@@ -1,15 +1,10 @@
|
||||
version: '2'
|
||||
services:
|
||||
web:
|
||||
# alpine 基础镜像版本
|
||||
image: whyour/qinglong:latest
|
||||
# debian-slim 基础镜像版本
|
||||
# image: whyour/qinglong:debian
|
||||
image: whyour/qinglong:latest # 基于 Debian 的版本:whyour/qinglong:debian
|
||||
volumes:
|
||||
- ./data:/ql/data
|
||||
ports:
|
||||
- "0.0.0.0:5700:5700"
|
||||
- "5700:5700"
|
||||
environment:
|
||||
# 部署路径非必须,以斜杠开头和结尾,比如 /test/
|
||||
QlBaseUrl: '/'
|
||||
QlBaseUrl: '/' # 部署路径非必须,以斜杠开头和结尾,比如 /test/
|
||||
restart: unless-stopped
|
||||
|
||||
+49
-45
@@ -13,7 +13,8 @@
|
||||
"schedule": "npm run build:back && node static/build/schedule/index.js",
|
||||
"public": "npm run build:back && node static/build/public.js",
|
||||
"update": "npm run build:back && node static/build/update.js",
|
||||
"gen:proto": "protoc --experimental_allow_proto3_optional --plugin=./node_modules/.bin/protoc-gen-ts_proto ./back/protos/*.proto --ts_proto_out=./ --ts_proto_opt=outputServices=grpc-js,env=node,esModuleInterop=true",
|
||||
"gen:proto": "protoc --experimental_allow_proto3_optional --plugin=./node_modules/.bin/protoc-gen-ts_proto ./back/protos/*.proto --ts_proto_out=./ --ts_proto_opt=outputServices=grpc-js,env=node,esModuleInterop=true,snakeToCamel=false",
|
||||
"gen:api": "python3 -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. ./back/protos/api.proto",
|
||||
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
|
||||
"postinstall": "max setup 2>/dev/null || true",
|
||||
"test": "umi-test",
|
||||
@@ -54,62 +55,66 @@
|
||||
"react-dom": "18",
|
||||
"dva-core": "2"
|
||||
}
|
||||
},
|
||||
"overrides": {
|
||||
"sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0.3"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@grpc/grpc-js": "^1.8.13",
|
||||
"@grpc/grpc-js": "^1.12.3",
|
||||
"@grpc/proto-loader": "^0.7.13",
|
||||
"@otplib/preset-default": "^12.0.1",
|
||||
"@sentry/node": "^8.26.0",
|
||||
"body-parser": "^1.19.2",
|
||||
"celebrate": "^15.0.1",
|
||||
"chokidar": "^3.5.3",
|
||||
"body-parser": "^1.20.3",
|
||||
"celebrate": "^15.0.3",
|
||||
"chokidar": "^4.0.1",
|
||||
"cors": "^2.8.5",
|
||||
"cron-parser": "^4.2.1",
|
||||
"cross-spawn": "^7.0.3",
|
||||
"dayjs": "^1.11.2",
|
||||
"dotenv": "^16.0.0",
|
||||
"express": "^4.17.3",
|
||||
"express-jwt": "^6.1.1",
|
||||
"express-rate-limit": "^7.0.0",
|
||||
"express-urlrewrite": "^1.4.0",
|
||||
"cron-parser": "^4.9.0",
|
||||
"cross-spawn": "^7.0.6",
|
||||
"dayjs": "^1.11.13",
|
||||
"dotenv": "^16.4.6",
|
||||
"express": "^4.21.2",
|
||||
"express-jwt": "^8.4.1",
|
||||
"express-rate-limit": "^7.4.1",
|
||||
"express-urlrewrite": "^2.0.3",
|
||||
"form-data": "^4.0.0",
|
||||
"got": "^11.8.2",
|
||||
"hpagent": "^1.2.0",
|
||||
"http-proxy-middleware": "^2.0.6",
|
||||
"http-proxy-middleware": "^3.0.3",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"multer": "1.4.5-lts.1",
|
||||
"nedb": "^1.8.0",
|
||||
"node-schedule": "^2.1.0",
|
||||
"nodemailer": "^6.7.2",
|
||||
"nodemailer": "^6.9.16",
|
||||
"p-queue-cjs": "7.3.4",
|
||||
"protobufjs": "^7.3.0",
|
||||
"@bufbuild/protobuf": "^2.2.3",
|
||||
"pstree.remy": "^1.1.8",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"sequelize": "^6.25.5",
|
||||
"serve-handler": "^6.1.3",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"sequelize": "^6.37.5",
|
||||
"serve-handler": "^6.1.6",
|
||||
"sockjs": "^0.3.24",
|
||||
"sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0.3",
|
||||
"toad-scheduler": "^1.6.0",
|
||||
"toad-scheduler": "^3.0.1",
|
||||
"typedi": "^0.10.0",
|
||||
"uuid": "^8.3.2",
|
||||
"winston": "^3.6.0",
|
||||
"winston-daily-rotate-file": "^4.7.1",
|
||||
"yargs": "^17.3.1",
|
||||
"tough-cookie": "^4.0.0",
|
||||
"uuid": "^11.0.3",
|
||||
"winston": "^3.17.0",
|
||||
"winston-daily-rotate-file": "^5.0.0",
|
||||
"request-ip": "3.3.0",
|
||||
"ip2region": "2.3.0"
|
||||
"ip2region": "2.3.0",
|
||||
"keyv": "^5.2.3",
|
||||
"@keyv/sqlite": "^4.0.1",
|
||||
"proper-lockfile": "^4.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"moment": "2.30.1",
|
||||
"@ant-design/icons": "^4.7.0",
|
||||
"@ant-design/icons": "^5.0.1",
|
||||
"@ant-design/pro-layout": "6.38.22",
|
||||
"@codemirror/view": "^6.34.1",
|
||||
"@codemirror/state": "^6.4.1",
|
||||
"@monaco-editor/react": "4.2.1",
|
||||
"@react-hook/resize-observer": "^1.2.6",
|
||||
"@react-hook/resize-observer": "^2.0.2",
|
||||
"react-router-dom": "6.26.1",
|
||||
"@sentry/react": "^8.26.0",
|
||||
"@types/body-parser": "^1.19.2",
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/cross-spawn": "^6.0.2",
|
||||
@@ -120,7 +125,6 @@
|
||||
"@types/jsonwebtoken": "^8.5.8",
|
||||
"@types/lodash": "^4.14.185",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/nedb": "^1.8.12",
|
||||
"@types/node": "^17.0.21",
|
||||
"@types/node-schedule": "^1.3.2",
|
||||
"@types/nodemailer": "^6.4.4",
|
||||
@@ -133,18 +137,19 @@
|
||||
"@types/sockjs-client": "^1.5.1",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@types/request-ip": "0.0.41",
|
||||
"@types/proper-lockfile": "^4.1.4",
|
||||
"@uiw/codemirror-extensions-langs": "^4.21.9",
|
||||
"@uiw/react-codemirror": "^4.21.9",
|
||||
"@umijs/max": "^4.0.72",
|
||||
"@umijs/max": "^4.4.4",
|
||||
"@umijs/ssr-darkreader": "^4.9.45",
|
||||
"ahooks": "^3.7.8",
|
||||
"ansi-to-react": "^6.1.6",
|
||||
"antd": "^4.24.8",
|
||||
"antd-img-crop": "^4.2.3",
|
||||
"antd": "^4.24.16",
|
||||
"antd-img-crop": "^4.23.0",
|
||||
"axios": "^1.4.0",
|
||||
"compression-webpack-plugin": "9.2.0",
|
||||
"concurrently": "^7.0.0",
|
||||
"react-hotkeys-hook": "^4.4.1",
|
||||
"react-hotkeys-hook": "^4.6.1",
|
||||
"file-saver": "2.0.2",
|
||||
"lint-staged": "^13.0.3",
|
||||
"monaco-editor": "0.33.0",
|
||||
@@ -155,23 +160,22 @@
|
||||
"qrcode.react": "^1.0.1",
|
||||
"query-string": "^7.1.1",
|
||||
"rc-tween-one": "^3.0.6",
|
||||
"rc-virtual-list": "3.5.3",
|
||||
"react": "18.2.0",
|
||||
"rc-virtual-list": "3.15.0",
|
||||
"react": "18.3.1",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
"react-diff-viewer": "^3.1.1",
|
||||
"react-dnd": "^14.0.2",
|
||||
"react-dnd-html5-backend": "^14.0.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-intl-universal": "^2.6.21",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-intl-universal": "^2.12.0",
|
||||
"react-split-pane": "^0.1.92",
|
||||
"sockjs-client": "^1.6.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"ts-proto": "^1.146.0",
|
||||
"ts-proto": "^2.6.1",
|
||||
"tslib": "^2.4.0",
|
||||
"typescript": "5.2.2",
|
||||
"vh-check": "^2.0.5",
|
||||
"virtualizedtableforantd4": "1.3.0",
|
||||
"webpack": "^5.70.0",
|
||||
"yorkie": "^2.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+4948
-6552
File diff suppressed because it is too large
Load Diff
+30
-1
@@ -117,6 +117,16 @@ export PUSH_PLUS_TOKEN=""
|
||||
## 下方填写您的一对多推送的 "群组编码" ,(一对多推送下面->您的群组(如无则新建)->群组编码)
|
||||
## 1. 需订阅者扫描二维码 2、如果您是创建群组所属人,也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送
|
||||
export PUSH_PLUS_USER=""
|
||||
## 发送模板,支持html,txt,json,markdown,cloudMonitor,jenkins,route,pay
|
||||
export PUSH_PLUS_TEMPLATE="html"
|
||||
## 发送渠道,支持wechat,webhook,cp,mail,sms
|
||||
export PUSH_PLUS_CHANNEL="wechat"
|
||||
## webhook编码,可在pushplus公众号上扩展配置出更多渠道
|
||||
export PUSH_PLUS_WEBHOOK=""
|
||||
## 发送结果回调地址,会把推送最终结果通知到这个地址上
|
||||
export PUSH_PLUS_CALLBACKURL=""
|
||||
## 好友令牌,微信公众号渠道填写好友令牌,企业微信渠道填写企业微信用户id
|
||||
export PUSH_PLUS_TO=""
|
||||
|
||||
## 9. 微加机器人
|
||||
## 官方网站:http://www.weplusbot.com
|
||||
@@ -211,7 +221,26 @@ export FSKEY=""
|
||||
export QMSG_KEY=""
|
||||
export QMSG_TYPE=""
|
||||
|
||||
## 20. 自定义通知
|
||||
## 20.Ntfy
|
||||
## 官方文档: https://docs.ntfy.sh
|
||||
## ntfy_url 填写ntfy地址,如https://ntfy.sh
|
||||
## ntfy_topic 填写ntfy的消息应用topic
|
||||
## ntfy_priority 填写推送消息优先级,默认为3
|
||||
export NTFY_URL=""
|
||||
export NTFY_TOPIC=""
|
||||
export NTFY_PRIORITY="3"
|
||||
|
||||
## 21. wxPusher
|
||||
## 官方文档: https://wxpusher.zjiecode.com/docs/
|
||||
## 管理后台: https://wxpusher.zjiecode.com/admin/
|
||||
## wxPusher 的 appToken
|
||||
export WXPUSHER_APP_TOKEN=""
|
||||
## wxPusher 的 topicIds,多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行
|
||||
export WXPUSHER_TOPIC_IDS=""
|
||||
## wxPusher 的 用户ID,多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行
|
||||
export WXPUSHER_UIDS=""
|
||||
|
||||
## 22. 自定义通知
|
||||
## 自定义通知 接收回调的URL
|
||||
export WEBHOOK_URL=""
|
||||
## WEBHOOK_BODY 和 WEBHOOK_HEADERS 多个参数时,直接换行或者使用 $'\n' 连接多行字符串,比如 export dd="line 1"$'\n'"line 2"
|
||||
|
||||
+165
-17
@@ -40,9 +40,14 @@ const push_config = {
|
||||
CHAT_URL: '', // synology chat url
|
||||
CHAT_TOKEN: '', // synology chat token
|
||||
|
||||
// 官方文档:http://www.pushplus.plus/
|
||||
PUSH_PLUS_TOKEN: '', // push+ 微信推送的用户令牌
|
||||
PUSH_PLUS_USER: '', // push+ 微信推送的群组编码
|
||||
// 官方文档:https://www.pushplus.plus/
|
||||
PUSH_PLUS_TOKEN: '', // pushplus 推送的用户令牌
|
||||
PUSH_PLUS_USER: '', // pushplus 推送的群组编码
|
||||
PUSH_PLUS_TEMPLATE: 'html', // pushplus 发送模板,支持html,txt,json,markdown,cloudMonitor,jenkins,route,pay
|
||||
PUSH_PLUS_CHANNEL: 'wechat', // pushplus 发送渠道,支持wechat,webhook,cp,mail,sms
|
||||
PUSH_PLUS_WEBHOOK: '', // pushplus webhook编码,可在pushplus公众号上扩展配置出更多渠道
|
||||
PUSH_PLUS_CALLBACKURL: '', // pushplus 发送结果回调地址,会把推送最终结果通知到这个地址上
|
||||
PUSH_PLUS_TO: '', // pushplus 好友令牌,微信公众号渠道填写好友令牌,企业微信渠道填写企业微信用户id
|
||||
|
||||
// 微加机器人,官方网站:https://www.weplusbot.com/
|
||||
WE_PLUS_BOT_TOKEN: '', // 微加机器人的用户令牌
|
||||
@@ -95,6 +100,16 @@ const push_config = {
|
||||
WEBHOOK_HEADERS: '', // 自定义通知 请求头
|
||||
WEBHOOK_METHOD: '', // 自定义通知 请求方法
|
||||
WEBHOOK_CONTENT_TYPE: '', // 自定义通知 content-type
|
||||
|
||||
NTFY_URL: '', // ntfy地址,如https://ntfy.sh,默认为https://ntfy.sh
|
||||
NTFY_TOPIC: '', // ntfy的消息应用topic
|
||||
NTFY_PRIORITY: '3', // 推送消息优先级,默认为3
|
||||
|
||||
// 官方文档: https://wxpusher.zjiecode.com/docs/
|
||||
// 管理后台: https://wxpusher.zjiecode.com/admin/
|
||||
WXPUSHER_APP_TOKEN: '', // wxpusher 的 appToken
|
||||
WXPUSHER_TOPIC_IDS: '', // wxpusher 的 主题ID,多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行
|
||||
WXPUSHER_UIDS: '', // wxpusher 的 用户ID,多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行
|
||||
};
|
||||
|
||||
for (const key in push_config) {
|
||||
@@ -224,11 +239,16 @@ function serverNotify(text, desp) {
|
||||
if (PUSH_KEY) {
|
||||
// 微信server酱推送通知一个\n不会换行,需要两个\n才能换行,故做此替换
|
||||
desp = desp.replace(/[\n\r]/g, '\n\n');
|
||||
|
||||
const matchResult = PUSH_KEY.match(/^sctp(\d+)t/i);
|
||||
const options = {
|
||||
url: PUSH_KEY.includes('SCT')
|
||||
? `https://sctapi.ftqq.com/${PUSH_KEY}.send`
|
||||
: `https://sc.ftqq.com/${PUSH_KEY}.send`,
|
||||
body: `text=${text}&desp=${desp}`,
|
||||
url:
|
||||
matchResult && matchResult[1]
|
||||
? `https://${matchResult[1]}.push.ft07.com/send/${PUSH_KEY}.send`
|
||||
: `https://sctapi.ftqq.com/${PUSH_KEY}.send`,
|
||||
body: `text=${encodeURIComponent(text)}&desp=${encodeURIComponent(
|
||||
desp,
|
||||
)}`,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
@@ -408,7 +428,7 @@ function tgBotNotify(text, desp) {
|
||||
TG_PROXY_AUTH,
|
||||
} = push_config;
|
||||
if (TG_BOT_TOKEN && TG_USER_ID) {
|
||||
const options = {
|
||||
let options = {
|
||||
url: `${TG_API_HOST}/bot${TG_BOT_TOKEN}/sendMessage`,
|
||||
json: {
|
||||
chat_id: `${TG_USER_ID}`,
|
||||
@@ -422,20 +442,20 @@ function tgBotNotify(text, desp) {
|
||||
};
|
||||
if (TG_PROXY_HOST && TG_PROXY_PORT) {
|
||||
const { HttpProxyAgent, HttpsProxyAgent } = require('hpagent');
|
||||
const options = {
|
||||
const _options = {
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 1000,
|
||||
maxSockets: 256,
|
||||
maxFreeSockets: 256,
|
||||
proxy: `http://${TG_PROXY_AUTH}${TG_PROXY_HOST}:${TG_PROXY_PORT}`,
|
||||
};
|
||||
const httpAgent = new HttpProxyAgent(options);
|
||||
const httpsAgent = new HttpsProxyAgent(options);
|
||||
const httpAgent = new HttpProxyAgent(_options);
|
||||
const httpsAgent = new HttpsProxyAgent(_options);
|
||||
const agent = {
|
||||
http: httpAgent,
|
||||
https: httpsAgent,
|
||||
};
|
||||
Object.assign(options, { agent });
|
||||
options.agent = agent;
|
||||
}
|
||||
$.post(options, (err, resp, data) => {
|
||||
try {
|
||||
@@ -759,7 +779,15 @@ function iGotNotify(text, desp, params = {}) {
|
||||
|
||||
function pushPlusNotify(text, desp) {
|
||||
return new Promise((resolve) => {
|
||||
const { PUSH_PLUS_TOKEN, PUSH_PLUS_USER } = push_config;
|
||||
const {
|
||||
PUSH_PLUS_TOKEN,
|
||||
PUSH_PLUS_USER,
|
||||
PUSH_PLUS_TEMPLATE,
|
||||
PUSH_PLUS_CHANNEL,
|
||||
PUSH_PLUS_WEBHOOK,
|
||||
PUSH_PLUS_CALLBACKURL,
|
||||
PUSH_PLUS_TO,
|
||||
} = push_config;
|
||||
if (PUSH_PLUS_TOKEN) {
|
||||
desp = desp.replace(/[\n\r]/g, '<br>'); // 默认为html, 不支持plaintext
|
||||
const body = {
|
||||
@@ -767,6 +795,11 @@ function pushPlusNotify(text, desp) {
|
||||
title: `${text}`,
|
||||
content: `${desp}`,
|
||||
topic: `${PUSH_PLUS_USER}`,
|
||||
template: `${PUSH_PLUS_TEMPLATE}`,
|
||||
channel: `${PUSH_PLUS_CHANNEL}`,
|
||||
webhook: `${PUSH_PLUS_WEBHOOK}`,
|
||||
callbackUrl: `${PUSH_PLUS_CALLBACKURL}`,
|
||||
to: `${PUSH_PLUS_TO}`,
|
||||
};
|
||||
const options = {
|
||||
url: `https://www.pushplus.plus/send`,
|
||||
@@ -780,7 +813,7 @@ function pushPlusNotify(text, desp) {
|
||||
try {
|
||||
if (err) {
|
||||
console.log(
|
||||
`Push+ 发送${
|
||||
`pushplus 发送${
|
||||
PUSH_PLUS_USER ? '一对多' : '一对一'
|
||||
}通知消息失败😞\n`,
|
||||
err,
|
||||
@@ -788,13 +821,15 @@ function pushPlusNotify(text, desp) {
|
||||
} else {
|
||||
if (data.code === 200) {
|
||||
console.log(
|
||||
`Push+ 发送${
|
||||
`pushplus 发送${
|
||||
PUSH_PLUS_USER ? '一对多' : '一对一'
|
||||
}通知消息完成🎉\n`,
|
||||
}通知请求成功🎉,可根据流水号查询推送结果:${
|
||||
data.data
|
||||
}\n注意:请求成功并不代表推送成功,如未收到消息,请到pushplus官网使用流水号查询推送最终结果`,
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
`Push+ 发送${
|
||||
`pushplus 发送${
|
||||
PUSH_PLUS_USER ? '一对多' : '一对一'
|
||||
}通知消息异常 ${data.msg}\n`,
|
||||
);
|
||||
@@ -1188,6 +1223,117 @@ function webhookNotify(text, desp) {
|
||||
});
|
||||
}
|
||||
|
||||
function ntfyNotify(text, desp) {
|
||||
function encodeRFC2047(text) {
|
||||
const encodedBase64 = Buffer.from(text).toString('base64');
|
||||
return `=?utf-8?B?${encodedBase64}?=`;
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const { NTFY_URL, NTFY_TOPIC, NTFY_PRIORITY } = push_config;
|
||||
if (NTFY_TOPIC) {
|
||||
const options = {
|
||||
url: `${NTFY_URL || 'https://ntfy.sh'}/${NTFY_TOPIC}`,
|
||||
body: `${desp}`,
|
||||
headers: {
|
||||
Title: `${encodeRFC2047(text)}`,
|
||||
Priority: NTFY_PRIORITY || '3',
|
||||
},
|
||||
timeout,
|
||||
};
|
||||
$.post(options, (err, resp, data) => {
|
||||
try {
|
||||
if (err) {
|
||||
console.log('Ntfy 通知调用API失败😞\n', err);
|
||||
} else {
|
||||
if (data.id) {
|
||||
console.log('Ntfy 发送通知消息成功🎉\n');
|
||||
} else {
|
||||
console.log(`Ntfy 发送通知消息异常 ${JSON.stringify(data)}`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
$.logErr(e, resp);
|
||||
} finally {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function wxPusherNotify(text, desp) {
|
||||
return new Promise((resolve) => {
|
||||
const { WXPUSHER_APP_TOKEN, WXPUSHER_TOPIC_IDS, WXPUSHER_UIDS } =
|
||||
push_config;
|
||||
if (WXPUSHER_APP_TOKEN) {
|
||||
// 处理topic_ids,将分号分隔的字符串转为数组
|
||||
const topicIds = WXPUSHER_TOPIC_IDS
|
||||
? WXPUSHER_TOPIC_IDS.split(';')
|
||||
.map((id) => id.trim())
|
||||
.filter((id) => id)
|
||||
.map((id) => parseInt(id))
|
||||
: [];
|
||||
|
||||
// 处理uids,将分号分隔的字符串转为数组
|
||||
const uids = WXPUSHER_UIDS
|
||||
? WXPUSHER_UIDS.split(';')
|
||||
.map((uid) => uid.trim())
|
||||
.filter((uid) => uid)
|
||||
: [];
|
||||
|
||||
// topic_ids uids 至少有一个
|
||||
if (!topicIds.length && !uids.length) {
|
||||
console.log(
|
||||
'wxpusher 服务的 WXPUSHER_TOPIC_IDS 和 WXPUSHER_UIDS 至少设置一个!!',
|
||||
);
|
||||
return resolve();
|
||||
}
|
||||
|
||||
const body = {
|
||||
appToken: WXPUSHER_APP_TOKEN,
|
||||
content: `<h1>${text}</h1><br/><div style='white-space: pre-wrap;'>${desp}</div>`,
|
||||
summary: text,
|
||||
contentType: 2,
|
||||
topicIds: topicIds,
|
||||
uids: uids,
|
||||
verifyPayType: 0,
|
||||
};
|
||||
|
||||
const options = {
|
||||
url: 'https://wxpusher.zjiecode.com/api/send/message',
|
||||
body: JSON.stringify(body),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
timeout,
|
||||
};
|
||||
|
||||
$.post(options, (err, resp, data) => {
|
||||
try {
|
||||
if (err) {
|
||||
console.log('wxpusher发送通知消息失败!\n', err);
|
||||
} else {
|
||||
if (data.code === 1000) {
|
||||
console.log('wxpusher发送通知消息完成!');
|
||||
} else {
|
||||
console.log(`wxpusher发送通知消息异常:${data.msg}`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
$.logErr(e, resp);
|
||||
} finally {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function parseString(input, valueFormatFn) {
|
||||
const regex = /(\w+):\s*((?:(?!\n\w+:).)*)/g;
|
||||
const matches = {};
|
||||
@@ -1316,6 +1462,8 @@ async function sendNotify(text, desp, params = {}) {
|
||||
chronocatNotify(text, desp), // Chronocat
|
||||
webhookNotify(text, desp), // 自定义通知
|
||||
qmsgNotify(text, desp), // 自定义通知
|
||||
ntfyNotify(text, desp), // Ntfy
|
||||
wxPusherNotify(text, desp), // wxpusher
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
+135
-36
@@ -72,8 +72,13 @@ push_config = {
|
||||
'CHAT_URL': '', # synology chat url
|
||||
'CHAT_TOKEN': '', # synology chat token
|
||||
|
||||
'PUSH_PLUS_TOKEN': '', # push+ 微信推送的用户令牌
|
||||
'PUSH_PLUS_USER': '', # push+ 微信推送的群组编码
|
||||
'PUSH_PLUS_TOKEN': '', # pushplus 推送的用户令牌
|
||||
'PUSH_PLUS_USER': '', # pushplus 推送的群组编码
|
||||
'PUSH_PLUS_TEMPLATE': 'html', # pushplus 发送模板,支持html,txt,json,markdown,cloudMonitor,jenkins,route,pay
|
||||
'PUSH_PLUS_CHANNEL': 'wechat', # pushplus 发送渠道,支持wechat,webhook,cp,mail,sms
|
||||
'PUSH_PLUS_WEBHOOK': '', # pushplus webhook编码,可在pushplus公众号上扩展配置出更多渠道
|
||||
'PUSH_PLUS_CALLBACKURL': '', # pushplus 发送结果回调地址,会把推送最终结果通知到这个地址上
|
||||
'PUSH_PLUS_TO': '', # pushplus 好友令牌,微信公众号渠道填写好友令牌,企业微信渠道填写企业微信用户id
|
||||
|
||||
'WE_PLUS_BOT_TOKEN': '', # 微加机器人的用户令牌
|
||||
'WE_PLUS_BOT_RECEIVER': '', # 微加机器人的消息接收者
|
||||
@@ -116,7 +121,15 @@ push_config = {
|
||||
'WEBHOOK_BODY': '', # 自定义通知 请求体
|
||||
'WEBHOOK_HEADERS': '', # 自定义通知 请求头
|
||||
'WEBHOOK_METHOD': '', # 自定义通知 请求方法
|
||||
'WEBHOOK_CONTENT_TYPE': '' # 自定义通知 content-type
|
||||
'WEBHOOK_CONTENT_TYPE': '', # 自定义通知 content-type
|
||||
|
||||
'NTFY_URL': '', # ntfy地址,如https://ntfy.sh
|
||||
'NTFY_TOPIC': '', # ntfy的消息应用topic
|
||||
'NTFY_PRIORITY':'3', # 推送消息优先级,默认为3
|
||||
|
||||
'WXPUSHER_APP_TOKEN': '', # wxpusher 的 appToken 官方文档: https://wxpusher.zjiecode.com/docs/ 管理后台: https://wxpusher.zjiecode.com/admin/
|
||||
'WXPUSHER_TOPIC_IDS': '', # wxpusher 的 主题ID,多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行
|
||||
'WXPUSHER_UIDS': '', # wxpusher 的 用户ID,多个用英文分号;分隔 topic_ids 与 uids 至少配置一个才行
|
||||
}
|
||||
# fmt: on
|
||||
|
||||
@@ -131,7 +144,6 @@ def bark(title: str, content: str) -> None:
|
||||
使用 bark 推送消息。
|
||||
"""
|
||||
if not push_config.get("BARK_PUSH"):
|
||||
print("bark 服务的 BARK_PUSH 未设置!!\n取消推送")
|
||||
return
|
||||
print("bark 服务启动")
|
||||
|
||||
@@ -183,7 +195,6 @@ def dingding_bot(title: str, content: str) -> None:
|
||||
使用 钉钉机器人 推送消息。
|
||||
"""
|
||||
if not push_config.get("DD_BOT_SECRET") or not push_config.get("DD_BOT_TOKEN"):
|
||||
print("钉钉机器人 服务的 DD_BOT_SECRET 或者 DD_BOT_TOKEN 未设置!!\n取消推送")
|
||||
return
|
||||
print("钉钉机器人 服务启动")
|
||||
|
||||
@@ -213,7 +224,6 @@ def feishu_bot(title: str, content: str) -> None:
|
||||
使用 飞书机器人 推送消息。
|
||||
"""
|
||||
if not push_config.get("FSKEY"):
|
||||
print("飞书 服务的 FSKEY 未设置!!\n取消推送")
|
||||
return
|
||||
print("飞书 服务启动")
|
||||
|
||||
@@ -232,7 +242,6 @@ def go_cqhttp(title: str, content: str) -> None:
|
||||
使用 go_cqhttp 推送消息。
|
||||
"""
|
||||
if not push_config.get("GOBOT_URL") or not push_config.get("GOBOT_QQ"):
|
||||
print("go-cqhttp 服务的 GOBOT_URL 或 GOBOT_QQ 未设置!!\n取消推送")
|
||||
return
|
||||
print("go-cqhttp 服务启动")
|
||||
|
||||
@@ -250,7 +259,6 @@ def gotify(title: str, content: str) -> None:
|
||||
使用 gotify 推送消息。
|
||||
"""
|
||||
if not push_config.get("GOTIFY_URL") or not push_config.get("GOTIFY_TOKEN"):
|
||||
print("gotify 服务的 GOTIFY_URL 或 GOTIFY_TOKEN 未设置!!\n取消推送")
|
||||
return
|
||||
print("gotify 服务启动")
|
||||
|
||||
@@ -273,7 +281,6 @@ def iGot(title: str, content: str) -> None:
|
||||
使用 iGot 推送消息。
|
||||
"""
|
||||
if not push_config.get("IGOT_PUSH_KEY"):
|
||||
print("iGot 服务的 IGOT_PUSH_KEY 未设置!!\n取消推送")
|
||||
return
|
||||
print("iGot 服务启动")
|
||||
|
||||
@@ -293,15 +300,18 @@ def serverJ(title: str, content: str) -> None:
|
||||
通过 serverJ 推送消息。
|
||||
"""
|
||||
if not push_config.get("PUSH_KEY"):
|
||||
print("serverJ 服务的 PUSH_KEY 未设置!!\n取消推送")
|
||||
return
|
||||
print("serverJ 服务启动")
|
||||
|
||||
data = {"text": title, "desp": content.replace("\n", "\n\n")}
|
||||
if push_config.get("PUSH_KEY").find("SCT") != -1:
|
||||
url = f'https://sctapi.ftqq.com/{push_config.get("PUSH_KEY")}.send'
|
||||
|
||||
match = re.match(r"sctp(\d+)t", push_config.get("PUSH_KEY"))
|
||||
if match:
|
||||
num = match.group(1)
|
||||
url = f'https://{num}.push.ft07.com/send/{push_config.get("PUSH_KEY")}.send'
|
||||
else:
|
||||
url = f'https://sc.ftqq.com/{push_config.get("PUSH_KEY")}.send'
|
||||
url = f'https://sctapi.ftqq.com/{push_config.get("PUSH_KEY")}.send'
|
||||
|
||||
response = requests.post(url, data=data).json()
|
||||
|
||||
if response.get("errno") == 0 or response.get("code") == 0:
|
||||
@@ -315,7 +325,6 @@ def pushdeer(title: str, content: str) -> None:
|
||||
通过PushDeer 推送消息
|
||||
"""
|
||||
if not push_config.get("DEER_KEY"):
|
||||
print("PushDeer 服务的 DEER_KEY 未设置!!\n取消推送")
|
||||
return
|
||||
print("PushDeer 服务启动")
|
||||
data = {
|
||||
@@ -341,7 +350,6 @@ def chat(title: str, content: str) -> None:
|
||||
通过Chat 推送消息
|
||||
"""
|
||||
if not push_config.get("CHAT_URL") or not push_config.get("CHAT_TOKEN"):
|
||||
print("chat 服务的 CHAT_URL或CHAT_TOKEN 未设置!!\n取消推送")
|
||||
return
|
||||
print("chat 服务启动")
|
||||
data = "payload=" + json.dumps({"text": title + "\n" + content})
|
||||
@@ -356,26 +364,36 @@ def chat(title: str, content: str) -> None:
|
||||
|
||||
def pushplus_bot(title: str, content: str) -> None:
|
||||
"""
|
||||
通过 push+ 推送消息。
|
||||
通过 pushplus 推送消息。
|
||||
"""
|
||||
if not push_config.get("PUSH_PLUS_TOKEN"):
|
||||
print("PUSHPLUS 服务的 PUSH_PLUS_TOKEN 未设置!!\n取消推送")
|
||||
return
|
||||
print("PUSHPLUS 服务启动")
|
||||
|
||||
url = "http://www.pushplus.plus/send"
|
||||
url = "https://www.pushplus.plus/send"
|
||||
data = {
|
||||
"token": push_config.get("PUSH_PLUS_TOKEN"),
|
||||
"title": title,
|
||||
"content": content,
|
||||
"topic": push_config.get("PUSH_PLUS_USER"),
|
||||
"template": push_config.get("PUSH_PLUS_TEMPLATE"),
|
||||
"channel": push_config.get("PUSH_PLUS_CHANNEL"),
|
||||
"webhook": push_config.get("PUSH_PLUS_WEBHOOK"),
|
||||
"callbackUrl": push_config.get("PUSH_PLUS_CALLBACKURL"),
|
||||
"to": push_config.get("PUSH_PLUS_TO"),
|
||||
}
|
||||
body = json.dumps(data).encode(encoding="utf-8")
|
||||
headers = {"Content-Type": "application/json"}
|
||||
response = requests.post(url=url, data=body, headers=headers).json()
|
||||
|
||||
if response["code"] == 200:
|
||||
print("PUSHPLUS 推送成功!")
|
||||
code = response["code"]
|
||||
if code == 200:
|
||||
print("PUSHPLUS 推送请求成功,可根据流水号查询推送结果:" + response["data"])
|
||||
print(
|
||||
"注意:请求成功并不代表推送成功,如未收到消息,请到pushplus官网使用流水号查询推送最终结果"
|
||||
)
|
||||
elif code == 900 or code == 903 or code == 905 or code == 999:
|
||||
print(response["msg"])
|
||||
|
||||
else:
|
||||
url_old = "http://pushplus.hxtrip.com/send"
|
||||
@@ -394,7 +412,6 @@ def weplus_bot(title: str, content: str) -> None:
|
||||
通过 微加机器人 推送消息。
|
||||
"""
|
||||
if not push_config.get("WE_PLUS_BOT_TOKEN"):
|
||||
print("微加机器人 服务的 WE_PLUS_BOT_TOKEN 未设置!!\n取消推送")
|
||||
return
|
||||
print("微加机器人 服务启动")
|
||||
|
||||
@@ -426,7 +443,6 @@ def qmsg_bot(title: str, content: str) -> None:
|
||||
使用 qmsg 推送消息。
|
||||
"""
|
||||
if not push_config.get("QMSG_KEY") or not push_config.get("QMSG_TYPE"):
|
||||
print("qmsg 的 QMSG_KEY 或者 QMSG_TYPE 未设置!!\n取消推送")
|
||||
return
|
||||
print("qmsg 服务启动")
|
||||
|
||||
@@ -445,11 +461,10 @@ def wecom_app(title: str, content: str) -> None:
|
||||
通过 企业微信 APP 推送消息。
|
||||
"""
|
||||
if not push_config.get("QYWX_AM"):
|
||||
print("QYWX_AM 未设置!!\n取消推送")
|
||||
return
|
||||
QYWX_AM_AY = re.split(",", push_config.get("QYWX_AM"))
|
||||
if 4 < len(QYWX_AM_AY) > 5:
|
||||
print("QYWX_AM 设置错误!!\n取消推送")
|
||||
print("QYWX_AM 设置错误!!")
|
||||
return
|
||||
print("企业微信 APP 服务启动")
|
||||
|
||||
@@ -542,7 +557,6 @@ def wecom_bot(title: str, content: str) -> None:
|
||||
通过 企业微信机器人 推送消息。
|
||||
"""
|
||||
if not push_config.get("QYWX_KEY"):
|
||||
print("企业微信机器人 服务的 QYWX_KEY 未设置!!\n取消推送")
|
||||
return
|
||||
print("企业微信机器人服务启动")
|
||||
|
||||
@@ -568,7 +582,6 @@ def telegram_bot(title: str, content: str) -> None:
|
||||
使用 telegram 机器人 推送消息。
|
||||
"""
|
||||
if not push_config.get("TG_BOT_TOKEN") or not push_config.get("TG_USER_ID"):
|
||||
print("tg 服务的 bot_token 或者 user_id 未设置!!\n取消推送")
|
||||
return
|
||||
print("tg 服务启动")
|
||||
|
||||
@@ -617,9 +630,6 @@ def aibotk(title: str, content: str) -> None:
|
||||
or not push_config.get("AIBOTK_TYPE")
|
||||
or not push_config.get("AIBOTK_NAME")
|
||||
):
|
||||
print(
|
||||
"智能微秘书 的 AIBOTK_KEY 或者 AIBOTK_TYPE 或者 AIBOTK_NAME 未设置!!\n取消推送"
|
||||
)
|
||||
return
|
||||
print("智能微秘书 服务启动")
|
||||
|
||||
@@ -658,9 +668,6 @@ def smtp(title: str, content: str) -> None:
|
||||
or not push_config.get("SMTP_PASSWORD")
|
||||
or not push_config.get("SMTP_NAME")
|
||||
):
|
||||
print(
|
||||
"SMTP 邮件 的 SMTP_SERVER 或者 SMTP_SSL 或者 SMTP_EMAIL 或者 SMTP_PASSWORD 或者 SMTP_NAME 未设置!!\n取消推送"
|
||||
)
|
||||
return
|
||||
print("SMTP 邮件 服务启动")
|
||||
|
||||
@@ -704,7 +711,6 @@ def pushme(title: str, content: str) -> None:
|
||||
使用 PushMe 推送消息。
|
||||
"""
|
||||
if not push_config.get("PUSHME_KEY"):
|
||||
print("PushMe 服务的 PUSHME_KEY 未设置!!\n取消推送")
|
||||
return
|
||||
print("PushMe 服务启动")
|
||||
|
||||
@@ -737,7 +743,6 @@ def chronocat(title: str, content: str) -> None:
|
||||
or not push_config.get("CHRONOCAT_QQ")
|
||||
or not push_config.get("CHRONOCAT_TOKEN")
|
||||
):
|
||||
print("CHRONOCAT 服务的 CHRONOCAT_URL 或 CHRONOCAT_QQ 未设置!!\n取消推送")
|
||||
return
|
||||
|
||||
print("CHRONOCAT 服务启动")
|
||||
@@ -777,6 +782,96 @@ def chronocat(title: str, content: str) -> None:
|
||||
print(f"QQ群消息:{ids}推送失败!")
|
||||
|
||||
|
||||
def ntfy(title: str, content: str) -> None:
|
||||
"""
|
||||
通过 Ntfy 推送消息
|
||||
"""
|
||||
|
||||
def encode_rfc2047(text: str) -> str:
|
||||
"""将文本编码为符合 RFC 2047 标准的格式"""
|
||||
encoded_bytes = base64.b64encode(text.encode("utf-8"))
|
||||
encoded_str = encoded_bytes.decode("utf-8")
|
||||
return f"=?utf-8?B?{encoded_str}?="
|
||||
|
||||
if not push_config.get("NTFY_TOPIC"):
|
||||
return
|
||||
print("ntfy 服务启动")
|
||||
priority = "3"
|
||||
if not push_config.get("NTFY_PRIORITY"):
|
||||
print("ntfy 服务的NTFY_PRIORITY 未设置!!默认设置为3")
|
||||
else:
|
||||
priority = push_config.get("NTFY_PRIORITY")
|
||||
|
||||
# 使用 RFC 2047 编码 title
|
||||
encoded_title = encode_rfc2047(title)
|
||||
|
||||
data = content.encode(encoding="utf-8")
|
||||
headers = {"Title": encoded_title, "Priority": priority} # 使用编码后的 title
|
||||
|
||||
url = push_config.get("NTFY_URL") + "/" + push_config.get("NTFY_TOPIC")
|
||||
response = requests.post(url, data=data, headers=headers)
|
||||
if response.status_code == 200: # 使用 response.status_code 进行检查
|
||||
print("Ntfy 推送成功!")
|
||||
else:
|
||||
print("Ntfy 推送失败!错误信息:", response.text)
|
||||
|
||||
|
||||
def wxpusher_bot(title: str, content: str) -> None:
|
||||
"""
|
||||
通过 wxpusher 推送消息。
|
||||
支持的环境变量:
|
||||
- WXPUSHER_APP_TOKEN: appToken
|
||||
- WXPUSHER_TOPIC_IDS: 主题ID, 多个用英文分号;分隔
|
||||
- WXPUSHER_UIDS: 用户ID, 多个用英文分号;分隔
|
||||
"""
|
||||
if not push_config.get("WXPUSHER_APP_TOKEN"):
|
||||
return
|
||||
|
||||
url = "https://wxpusher.zjiecode.com/api/send/message"
|
||||
|
||||
# 处理topic_ids和uids,将分号分隔的字符串转为数组
|
||||
topic_ids = []
|
||||
if push_config.get("WXPUSHER_TOPIC_IDS"):
|
||||
topic_ids = [
|
||||
int(id.strip())
|
||||
for id in push_config.get("WXPUSHER_TOPIC_IDS").split(";")
|
||||
if id.strip()
|
||||
]
|
||||
|
||||
uids = []
|
||||
if push_config.get("WXPUSHER_UIDS"):
|
||||
uids = [
|
||||
uid.strip()
|
||||
for uid in push_config.get("WXPUSHER_UIDS").split(";")
|
||||
if uid.strip()
|
||||
]
|
||||
|
||||
# topic_ids uids 至少有一个
|
||||
if not topic_ids and not uids:
|
||||
print("wxpusher 服务的 WXPUSHER_TOPIC_IDS 和 WXPUSHER_UIDS 至少设置一个!!")
|
||||
return
|
||||
|
||||
print("wxpusher 服务启动")
|
||||
|
||||
data = {
|
||||
"appToken": push_config.get("WXPUSHER_APP_TOKEN"),
|
||||
"content": f"<h1>{title}</h1><br/><div style='white-space: pre-wrap;'>{content}</div>",
|
||||
"summary": title,
|
||||
"contentType": 2,
|
||||
"topicIds": topic_ids,
|
||||
"uids": uids,
|
||||
"verifyPayType": 0,
|
||||
}
|
||||
|
||||
headers = {"Content-Type": "application/json"}
|
||||
response = requests.post(url=url, json=data, headers=headers).json()
|
||||
|
||||
if response.get("code") == 1000:
|
||||
print("wxpusher 推送成功!")
|
||||
else:
|
||||
print(f"wxpusher 推送失败!错误信息:{response.get('msg')}")
|
||||
|
||||
|
||||
def parse_headers(headers):
|
||||
if not headers:
|
||||
return {}
|
||||
@@ -833,7 +928,6 @@ 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("自定义通知服务启动")
|
||||
@@ -937,7 +1031,12 @@ def add_notify_function():
|
||||
notify_function.append(chronocat)
|
||||
if push_config.get("WEBHOOK_URL") and push_config.get("WEBHOOK_METHOD"):
|
||||
notify_function.append(custom_notify)
|
||||
|
||||
if push_config.get("NTFY_TOPIC"):
|
||||
notify_function.append(ntfy)
|
||||
if push_config.get("WXPUSHER_APP_TOKEN") and (
|
||||
push_config.get("WXPUSHER_TOPIC_IDS") or push_config.get("WXPUSHER_UIDS")
|
||||
):
|
||||
notify_function.append(wxpusher_bot)
|
||||
if not notify_function:
|
||||
print(f"无推送渠道,请检查通知变量是否正确")
|
||||
return notify_function
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,4 +6,10 @@
|
||||
*/
|
||||
console.log('test scripts');
|
||||
QLAPI.notify('test scripts', 'test desc');
|
||||
QLAPI.getEnvs({ searchValue: 'dddd' }).then((x) => {
|
||||
console.log('getEnvs', x);
|
||||
});
|
||||
QLAPI.systemNotify({ title: '123', content: '231' }).then((x) => {
|
||||
console.log('systemNotify', x);
|
||||
});
|
||||
console.log('test desc');
|
||||
|
||||
+6
-1
@@ -4,6 +4,11 @@ name: script name
|
||||
定时规则
|
||||
cron: 1 9 * * *
|
||||
"""
|
||||
|
||||
print("test script")
|
||||
QLAPI.notify('test script', 'test desc')
|
||||
print(QLAPI.notify("test script", "test desc"))
|
||||
print("test systemNotify")
|
||||
print(QLAPI.systemNotify({"title": "test script", "content": "dddd"}))
|
||||
print("test getEnvs")
|
||||
print(QLAPI.getEnvs({"searchValue": "1"}))
|
||||
print("test desc")
|
||||
|
||||
+30
-1
@@ -178,7 +178,10 @@ update_cron() {
|
||||
code=$(echo "$api" | jq -r .code)
|
||||
message=$(echo "$api" | jq -r .message)
|
||||
if [[ $code != 200 ]]; then
|
||||
echo -e "\n## 更新任务状态失败(${message})\n"
|
||||
if [[ ! $message ]]; then
|
||||
message="$api"
|
||||
fi
|
||||
echo -e "${message}"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -231,4 +234,30 @@ find_cron_api() {
|
||||
fi
|
||||
}
|
||||
|
||||
update_auth_config() {
|
||||
local body="$1"
|
||||
local tip="$2"
|
||||
local currentTimeStamp=$(date +%s)
|
||||
local api=$(
|
||||
curl -s --noproxy "*" "http://0.0.0.0:5600/open/system/auth/reset?t=$currentTimeStamp" \
|
||||
-X 'PUT' \
|
||||
-H "Accept: application/json" \
|
||||
-H "Authorization: Bearer ${__ql_token__}" \
|
||||
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
|
||||
-H "Content-Type: application/json;charset=UTF-8" \
|
||||
-H "Origin: http://0.0.0.0:5700" \
|
||||
-H "Referer: http://0.0.0.0:5700/crontab" \
|
||||
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
|
||||
--data-raw "{$body}" \
|
||||
--compressed
|
||||
)
|
||||
code=$(echo "$api" | jq -r .code)
|
||||
message=$(echo "$api" | jq -r .message)
|
||||
if [[ $code == 200 ]]; then
|
||||
echo -e "${tip}成功🎉"
|
||||
else
|
||||
echo -e "${tip}失败(${message})"
|
||||
fi
|
||||
}
|
||||
|
||||
get_token
|
||||
|
||||
+2
-1
@@ -125,7 +125,8 @@ run_concurrent() {
|
||||
fi
|
||||
|
||||
handle_env_split
|
||||
single_log_time=$(date "+%Y-%m-%d-%H-%M-%S.%3N")
|
||||
time=$(date "+$mtime_format")
|
||||
single_log_time=$(format_log_time "$mtime_format" "$time")
|
||||
|
||||
cd $dir_scripts
|
||||
local relative_path="${file_param%/*}"
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
const grpc = require('@grpc/grpc-js');
|
||||
const protoLoader = require('@grpc/proto-loader');
|
||||
const { join } = require('path');
|
||||
|
||||
class GrpcClient {
|
||||
static #config = {
|
||||
protoPath: join(process.env.QL_DIR, 'back/protos/api.proto'),
|
||||
serverAddress: '0.0.0.0:5500',
|
||||
protoOptions: {
|
||||
keepCase: true,
|
||||
longs: String,
|
||||
enums: String,
|
||||
defaults: true,
|
||||
},
|
||||
grpcOptions: {
|
||||
'grpc.enable_http_proxy': 0,
|
||||
},
|
||||
defaultTimeout: 30000,
|
||||
};
|
||||
|
||||
static #methods = [
|
||||
'getEnvs',
|
||||
'createEnv',
|
||||
'updateEnv',
|
||||
'deleteEnvs',
|
||||
'moveEnv',
|
||||
'disableEnvs',
|
||||
'enableEnvs',
|
||||
'updateEnvNames',
|
||||
'getEnvById',
|
||||
'systemNotify',
|
||||
'getCronDetail',
|
||||
'createCron',
|
||||
'updateCron',
|
||||
'deleteCrons',
|
||||
];
|
||||
|
||||
#client;
|
||||
#api = {};
|
||||
|
||||
constructor() {
|
||||
this.#initializeClient();
|
||||
this.#bindMethods();
|
||||
}
|
||||
|
||||
#initializeClient() {
|
||||
try {
|
||||
const { protoPath, protoOptions, serverAddress, grpcOptions } =
|
||||
GrpcClient.#config;
|
||||
|
||||
const packageDefinition = protoLoader.loadSync(protoPath, protoOptions);
|
||||
const apiProto = grpc.loadPackageDefinition(packageDefinition).com.ql.api;
|
||||
|
||||
this.#client = new apiProto.Api(
|
||||
serverAddress,
|
||||
grpc.credentials.createInsecure(),
|
||||
grpcOptions,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize gRPC client:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
#promisifyMethod(methodName) {
|
||||
const capitalizedMethod =
|
||||
methodName.charAt(0).toUpperCase() + methodName.slice(1);
|
||||
const method = this.#client[capitalizedMethod].bind(this.#client);
|
||||
|
||||
return async (params = {}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const metadata = new grpc.Metadata();
|
||||
const deadline = new Date(
|
||||
Date.now() + GrpcClient.#config.defaultTimeout,
|
||||
);
|
||||
|
||||
method(params, metadata, { deadline }, (error, response) => {
|
||||
if (error) {
|
||||
return reject(error);
|
||||
}
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
#bindMethods() {
|
||||
GrpcClient.#methods.forEach((method) => {
|
||||
this.#api[method] = this.#promisifyMethod(method);
|
||||
});
|
||||
}
|
||||
|
||||
getApi() {
|
||||
return {
|
||||
...this.#api,
|
||||
close: this.close.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.#client) {
|
||||
this.#client.close();
|
||||
this.#client = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const grpcClient = new GrpcClient();
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
grpcClient.close();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
grpcClient.close();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
if (reason instanceof Error) {
|
||||
if (reason.stack) {
|
||||
const relevantStack = reason.stack
|
||||
.split('\n')
|
||||
.filter((line) => {
|
||||
return (
|
||||
!line.includes('node:internal') &&
|
||||
!line.includes('node_modules/@grpc') &&
|
||||
!line.includes('processTicksAndRejections')
|
||||
);
|
||||
})
|
||||
.join('\n');
|
||||
console.error(relevantStack);
|
||||
}
|
||||
} else {
|
||||
console.error(reason);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = grpcClient.getApi();
|
||||
@@ -0,0 +1,283 @@
|
||||
import subprocess
|
||||
import json
|
||||
import tempfile
|
||||
import os
|
||||
from typing import Dict, List, TypedDict, Optional
|
||||
from functools import wraps
|
||||
|
||||
|
||||
def error_handler(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except TypeError as e:
|
||||
if "missing" in str(e):
|
||||
func_name = func.__name__
|
||||
annotations = func.__annotations__
|
||||
param_type = next(
|
||||
(t for name, t in annotations.items() if name != "return"), None
|
||||
)
|
||||
if param_type and hasattr(param_type, "__annotations__"):
|
||||
required_fields = {
|
||||
k: v
|
||||
for k, v in param_type.__annotations__.items()
|
||||
if not getattr(param_type, "__total__", True)
|
||||
or k in getattr(param_type, "__required_keys__", set())
|
||||
}
|
||||
fields_str = ", ".join(
|
||||
f'"{k}": {v.__name__}' for k, v in required_fields.items()
|
||||
)
|
||||
raise Exception(
|
||||
f"{func_name}() requires a dictionary with parameters: {{{fields_str}}}"
|
||||
) from None
|
||||
raise Exception(f"{str(e)}") from None
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
if "Error:" in error_msg:
|
||||
error_msg = error_msg.split("Error:")[-1].split("\n")[0].strip()
|
||||
raise Exception(f"{error_msg}") from None
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class EnvItem(TypedDict, total=False):
|
||||
id: Optional[int]
|
||||
name: Optional[str]
|
||||
value: Optional[str]
|
||||
remarks: Optional[str]
|
||||
status: Optional[int]
|
||||
position: Optional[int]
|
||||
|
||||
|
||||
class GetEnvsParams(TypedDict, total=False):
|
||||
searchValue: str
|
||||
|
||||
|
||||
class CreateEnvParams(TypedDict):
|
||||
envs: List[EnvItem]
|
||||
|
||||
|
||||
class UpdateEnvParams(TypedDict):
|
||||
env: EnvItem
|
||||
|
||||
|
||||
class DeleteEnvsParams(TypedDict):
|
||||
ids: List[int]
|
||||
|
||||
|
||||
class MoveEnvParams(TypedDict):
|
||||
id: int
|
||||
fromIndex: int
|
||||
toIndex: int
|
||||
|
||||
|
||||
class DisableEnvsParams(TypedDict):
|
||||
ids: List[int]
|
||||
|
||||
|
||||
class EnableEnvsParams(TypedDict):
|
||||
ids: List[int]
|
||||
|
||||
|
||||
class UpdateEnvNamesParams(TypedDict):
|
||||
ids: List[int]
|
||||
name: str
|
||||
|
||||
|
||||
class GetEnvByIdParams(TypedDict):
|
||||
id: int
|
||||
|
||||
|
||||
class SystemNotifyParams(TypedDict):
|
||||
title: str
|
||||
content: str
|
||||
|
||||
|
||||
class EnvsResponse(TypedDict):
|
||||
code: int
|
||||
data: List[EnvItem]
|
||||
message: Optional[str]
|
||||
|
||||
|
||||
class EnvResponse(TypedDict):
|
||||
code: int
|
||||
data: EnvItem
|
||||
message: Optional[str]
|
||||
|
||||
|
||||
class Response(TypedDict):
|
||||
code: int
|
||||
message: Optional[str]
|
||||
|
||||
|
||||
class ExtraScheduleItem(TypedDict, total=False):
|
||||
schedule: Optional[str]
|
||||
|
||||
|
||||
class CronItem(TypedDict, total=False):
|
||||
id: Optional[int]
|
||||
command: Optional[str]
|
||||
schedule: Optional[str]
|
||||
name: Optional[str]
|
||||
labels: List[str]
|
||||
sub_id: Optional[int]
|
||||
extra_schedules: List[ExtraScheduleItem]
|
||||
task_before: Optional[str]
|
||||
task_after: Optional[str]
|
||||
status: Optional[int]
|
||||
log_path: Optional[str]
|
||||
pid: Optional[int]
|
||||
last_running_time: Optional[int]
|
||||
last_execution_time: Optional[int]
|
||||
|
||||
|
||||
class CreateCronParams(TypedDict):
|
||||
command: str
|
||||
schedule: str
|
||||
name: Optional[str]
|
||||
labels: List[str]
|
||||
sub_id: Optional[int]
|
||||
extra_schedules: List[ExtraScheduleItem]
|
||||
task_before: Optional[str]
|
||||
task_after: Optional[str]
|
||||
|
||||
|
||||
class UpdateCronParams(TypedDict):
|
||||
id: int
|
||||
command: str
|
||||
schedule: str
|
||||
name: Optional[str]
|
||||
labels: List[str]
|
||||
sub_id: Optional[int]
|
||||
extra_schedules: List[ExtraScheduleItem]
|
||||
task_before: Optional[str]
|
||||
task_after: Optional[str]
|
||||
|
||||
|
||||
class DeleteCronsParams(TypedDict):
|
||||
ids: List[int]
|
||||
|
||||
|
||||
class CronDetailParams(TypedDict):
|
||||
log_path: str
|
||||
|
||||
|
||||
class CronsResponse(TypedDict):
|
||||
code: int
|
||||
data: List[CronItem]
|
||||
message: Optional[str]
|
||||
|
||||
|
||||
class CronResponse(TypedDict):
|
||||
code: int
|
||||
data: CronItem
|
||||
message: Optional[str]
|
||||
|
||||
|
||||
class Client:
|
||||
def __init__(self):
|
||||
self.temp_dir = tempfile.mkdtemp(prefix="node_client_")
|
||||
self.temp_script = os.path.join(self.temp_dir, "temp_script.js")
|
||||
|
||||
def __del__(self):
|
||||
try:
|
||||
if os.path.exists(self.temp_script):
|
||||
os.remove(self.temp_script)
|
||||
os.rmdir(self.temp_dir)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@error_handler
|
||||
def _execute_node(self, method: str, params: Dict = None) -> Dict:
|
||||
node_code = f"""
|
||||
const api = require('{os.getenv("QL_DIR")}/shell/preload/client.js');
|
||||
|
||||
(async () => {{
|
||||
try {{
|
||||
const result = await api.{method}({json.dumps(params) if params else ''});
|
||||
console.log(JSON.stringify(result));
|
||||
}} catch (error) {{
|
||||
console.error(JSON.stringify({{
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
name: error.name
|
||||
}}));
|
||||
process.exit(1);
|
||||
}}
|
||||
}})();
|
||||
"""
|
||||
|
||||
with open(self.temp_script, "w", encoding="utf-8") as f:
|
||||
f.write(node_code)
|
||||
|
||||
result = subprocess.run(
|
||||
["node", self.temp_script],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
error_data = json.loads(result.stderr)
|
||||
raise Exception(
|
||||
f"{error_data.get('name', 'Error')}: {error_data.get('stack')}"
|
||||
)
|
||||
|
||||
return json.loads(result.stdout)
|
||||
|
||||
@error_handler
|
||||
def getEnvs(self, params: GetEnvsParams = None) -> EnvsResponse:
|
||||
return self._execute_node("getEnvs", params)
|
||||
|
||||
@error_handler
|
||||
def createEnv(self, data: CreateEnvParams) -> EnvsResponse:
|
||||
return self._execute_node("createEnv", data)
|
||||
|
||||
@error_handler
|
||||
def updateEnv(self, data: UpdateEnvParams) -> EnvResponse:
|
||||
return self._execute_node("updateEnv", data)
|
||||
|
||||
@error_handler
|
||||
def deleteEnvs(self, data: DeleteEnvsParams) -> Response:
|
||||
return self._execute_node("deleteEnvs", data)
|
||||
|
||||
@error_handler
|
||||
def moveEnv(self, data: MoveEnvParams) -> EnvResponse:
|
||||
return self._execute_node("moveEnv", data)
|
||||
|
||||
@error_handler
|
||||
def disableEnvs(self, data: DisableEnvsParams) -> Response:
|
||||
return self._execute_node("disableEnvs", data)
|
||||
|
||||
@error_handler
|
||||
def enableEnvs(self, data: EnableEnvsParams) -> Response:
|
||||
return self._execute_node("enableEnvs", data)
|
||||
|
||||
@error_handler
|
||||
def updateEnvNames(self, data: UpdateEnvNamesParams) -> Response:
|
||||
return self._execute_node("updateEnvNames", data)
|
||||
|
||||
@error_handler
|
||||
def getEnvById(self, data: GetEnvByIdParams) -> EnvResponse:
|
||||
return self._execute_node("getEnvById", data)
|
||||
|
||||
@error_handler
|
||||
def systemNotify(self, data: SystemNotifyParams) -> Response:
|
||||
return self._execute_node("systemNotify", data)
|
||||
|
||||
@error_handler
|
||||
def getCronDetail(self, data: CronDetailParams) -> CronResponse:
|
||||
return self._execute_node("getCronDetail", data)
|
||||
|
||||
@error_handler
|
||||
def createCron(self, data: CreateCronParams) -> CronResponse:
|
||||
return self._execute_node("createCron", data)
|
||||
|
||||
@error_handler
|
||||
def updateCron(self, data: UpdateCronParams) -> CronResponse:
|
||||
return self._execute_node("updateCron", data)
|
||||
|
||||
@error_handler
|
||||
def deleteCrons(self, data: DeleteCronsParams) -> Response:
|
||||
return self._execute_node("deleteCrons", data)
|
||||
@@ -1,4 +1,5 @@
|
||||
const { execSync } = require('child_process');
|
||||
const client = require('./client.js');
|
||||
require(`./env.js`);
|
||||
|
||||
function expandRange(rangeStr, max) {
|
||||
@@ -29,7 +30,7 @@ function run() {
|
||||
file_task_before_js,
|
||||
dir_scripts,
|
||||
task_before,
|
||||
PREV_NODE_OPTIONS
|
||||
PREV_NODE_OPTIONS,
|
||||
} = process.env;
|
||||
|
||||
try {
|
||||
@@ -56,16 +57,18 @@ function run() {
|
||||
for (const key in newEnvObject) {
|
||||
process.env[key] = newEnvObject[key];
|
||||
}
|
||||
console.log(output);
|
||||
if (output) {
|
||||
console.log(output);
|
||||
}
|
||||
if (task_before) {
|
||||
console.log('执行前置命令结束\n');
|
||||
}
|
||||
} catch (error) {
|
||||
if (!error.message.includes('spawnSync /bin/sh E2BIG')) {
|
||||
console.log(`❌ run task before error: `, error);
|
||||
console.log(`\ue926 run task before error: `, error);
|
||||
} else {
|
||||
console.log(
|
||||
`❌ The environment variable is too large. It is recommended to use task_before.js instead of task_before.sh\n`,
|
||||
`\ue926 The environment variable is too large. It is recommended to use task_before.js instead of task_before.sh\n`,
|
||||
);
|
||||
}
|
||||
if (task_before) {
|
||||
@@ -89,11 +92,16 @@ try {
|
||||
return;
|
||||
}
|
||||
|
||||
process.on('SIGTERM', (code) => {
|
||||
process.exit(15);
|
||||
});
|
||||
|
||||
run();
|
||||
|
||||
const { sendNotify } = require('./notify.js');
|
||||
const { sendNotify } = require('./__ql_notify__.js');
|
||||
global.QLAPI = {
|
||||
notify: sendNotify,
|
||||
...client,
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(`run builtin code error: `, error, '\n');
|
||||
|
||||
@@ -5,6 +5,8 @@ import json
|
||||
import builtins
|
||||
import sys
|
||||
import env
|
||||
import signal
|
||||
from client import Client
|
||||
|
||||
|
||||
def try_parse_int(value):
|
||||
@@ -63,7 +65,8 @@ def run():
|
||||
for key, value in env_json.items():
|
||||
os.environ[key] = value
|
||||
|
||||
print(output)
|
||||
if len(output) > 0:
|
||||
print(output)
|
||||
if task_before:
|
||||
print("执行前置命令结束")
|
||||
|
||||
@@ -72,10 +75,10 @@ def run():
|
||||
except OSError as error:
|
||||
error_message = str(error)
|
||||
if "Argument list too long" not in error_message:
|
||||
print(f"❌ run task before error: {error}")
|
||||
print(f"\ue926 run task before error: {error}")
|
||||
else:
|
||||
print(
|
||||
"❌ The environment variable is too large. It is recommended to use task_before.py instead of task_before.sh\n"
|
||||
"\ue926 The environment variable is too large. It is recommended to use task_before.py instead of task_before.sh\n"
|
||||
)
|
||||
if task_before:
|
||||
print("执行前置命令结束")
|
||||
@@ -95,12 +98,18 @@ def run():
|
||||
os.environ[env_param] = env_str
|
||||
|
||||
|
||||
def handle_sigterm(signum, frame):
|
||||
sys.exit(15)
|
||||
|
||||
|
||||
try:
|
||||
signal.signal(signal.SIGTERM, handle_sigterm)
|
||||
|
||||
run()
|
||||
|
||||
from notify import send
|
||||
from __ql_notify__ import send
|
||||
|
||||
class BaseApi:
|
||||
class BaseApi(Client):
|
||||
def notify(self, *args, **kwargs):
|
||||
return send(*args, **kwargs)
|
||||
|
||||
|
||||
+20
-13
@@ -193,12 +193,6 @@ fix_config() {
|
||||
echo
|
||||
fi
|
||||
|
||||
if [[ ! -s $file_auth_user ]]; then
|
||||
echo -e "复制一份 $file_auth_sample 为 $file_auth_user\n"
|
||||
cp -fv $file_auth_sample $file_auth_user
|
||||
echo
|
||||
fi
|
||||
|
||||
if [[ ! -s $file_notify_py ]]; then
|
||||
echo -e "复制一份 $file_notify_py_sample 为 $file_notify_py\n"
|
||||
cp -fv $file_notify_py_sample $file_notify_py
|
||||
@@ -340,7 +334,7 @@ format_log_time() {
|
||||
local time="$2"
|
||||
|
||||
if [[ $is_macos -eq 1 ]]; then
|
||||
echo $(date -j -f "$format" "$time" "+%Y-%m-%d-%H-%M-%S-%3N")
|
||||
echo $(python3 -c 'from datetime import datetime; print(datetime.now().strftime("%Y-%m-%d-%H-%M-%S-%f")[:-3])')
|
||||
else
|
||||
echo $(date -d "$time" "+%Y-%m-%d-%H-%M-%S-%3N")
|
||||
fi
|
||||
@@ -444,8 +438,14 @@ clear_env() {
|
||||
}
|
||||
|
||||
handle_task_start() {
|
||||
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
|
||||
echo -e "## 开始执行... $begin_time\n"
|
||||
local error_message=""
|
||||
if [[ $ID ]]; then
|
||||
local error=$(update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp")
|
||||
if [[ $error ]]; then
|
||||
error_message=", 任务状态更新失败(${error})"
|
||||
fi
|
||||
fi
|
||||
echo -e "## 开始执行... ${begin_time}${error_message}\n"
|
||||
}
|
||||
|
||||
run_task_before() {
|
||||
@@ -453,7 +453,7 @@ run_task_before() {
|
||||
|
||||
if [[ ${task_before:=} ]]; then
|
||||
echo -e "执行前置命令\n"
|
||||
eval "${task_before%;}" "$@"
|
||||
eval "${task_before%;}"
|
||||
echo -e "\n执行前置命令结束\n"
|
||||
fi
|
||||
}
|
||||
@@ -463,7 +463,7 @@ run_task_after() {
|
||||
|
||||
if [[ ${task_after:=} ]]; then
|
||||
echo -e "\n执行后置命令\n"
|
||||
eval "${task_after%;}" "$@"
|
||||
eval "${task_after%;}"
|
||||
echo -e "\n执行后置命令结束"
|
||||
fi
|
||||
}
|
||||
@@ -473,11 +473,18 @@ handle_task_end() {
|
||||
local end_time=$(format_time "$time_format" "$etime")
|
||||
local end_timestamp=$(format_timestamp "$time_format" "$etime")
|
||||
local diff_time=$(($end_timestamp - $begin_timestamp))
|
||||
local suffix=""
|
||||
[[ "${MANUAL:=}" == "true" ]] && suffix="(手动停止)"
|
||||
|
||||
[[ "$diff_time" == 0 ]] && diff_time=1
|
||||
|
||||
echo -e "\n## 执行结束... $end_time 耗时 $diff_time 秒 "
|
||||
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
|
||||
if [[ $ID ]]; then
|
||||
local error=$(update_cron "\"$ID\"" "1" "$$" "$log_path" "$begin_timestamp" "$diff_time")
|
||||
if [[ $error ]]; then
|
||||
error_message=", 任务状态更新失败(${error})"
|
||||
fi
|
||||
fi
|
||||
echo -e "\n## 执行结束$suffix... $end_time 耗时 $diff_time 秒${error_message:=} "
|
||||
}
|
||||
|
||||
init_env
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ dir_shell=$QL_DIR/shell
|
||||
|
||||
trap "single_hanle" 2 3 20 15 14 19 1
|
||||
single_hanle() {
|
||||
eval handle_task_end "$@" "$cmd"
|
||||
eval MANUAL=true handle_task_end "$@" "$cmd"
|
||||
exit 1
|
||||
}
|
||||
|
||||
|
||||
+8
-8
@@ -489,7 +489,6 @@ main() {
|
||||
local time_format="%Y-%m-%d %H:%M:%S"
|
||||
local time=$(date "+$time_format")
|
||||
local begin_timestamp=$(format_timestamp "$time_format" "$time")
|
||||
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
|
||||
|
||||
local begin_time=$(format_time "$time_format" "$time")
|
||||
|
||||
@@ -497,6 +496,8 @@ main() {
|
||||
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
|
||||
fi
|
||||
|
||||
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
|
||||
|
||||
case $p1 in
|
||||
update)
|
||||
fix_config
|
||||
@@ -537,14 +538,13 @@ main() {
|
||||
eval . $dir_shell/check.sh $cmd
|
||||
;;
|
||||
resetlet)
|
||||
auth_value=$(cat $file_auth_user | jq '.retries =0' -c)
|
||||
echo "$auth_value" >$file_auth_user
|
||||
eval echo -e "重置登录错误次数成功" $cmd
|
||||
eval update_auth_config "\\\"retries\\\":0" "重置登录错误次数" $cmd
|
||||
;;
|
||||
resettfa)
|
||||
auth_value=$(cat $file_auth_user | jq '.twoFactorActivated =false' | jq '.twoFactorActived =false' -c)
|
||||
echo "$auth_value" >$file_auth_user
|
||||
eval echo -e "禁用两步验证成功" $cmd
|
||||
eval update_auth_config "\\\"twoFactorActivated\\\":false" "禁用两步验证" $cmd
|
||||
;;
|
||||
resetpwd)
|
||||
eval update_auth_config "\\\"password\\\":\\\"$p2\\\"" "重置密码" $cmd
|
||||
;;
|
||||
*)
|
||||
eval echo -e "命令输入错误...\\\n" $cmd
|
||||
@@ -556,7 +556,7 @@ main() {
|
||||
local end_time=$(format_time "$time_format" "$etime")
|
||||
local end_timestamp=$(format_timestamp "$time_format" "$etime")
|
||||
local diff_time=$(($end_timestamp - $begin_timestamp))
|
||||
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
|
||||
[[ $ID ]] && update_cron "\"$ID\"" "1" "$$" "$log_path" "$begin_timestamp" "$diff_time"
|
||||
|
||||
if [[ "$p1" != "repo" ]] && [[ "$p1" != "raw" ]]; then
|
||||
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time 秒 " $cmd
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -8,6 +8,13 @@
|
||||
url('../assets/fonts/SourceCodePro-Regular.ttf') format('truetype');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Log;
|
||||
src: url('../assets/fonts/log.woff2') format('woff2'),
|
||||
url('../assets/fonts/log.woff') format('woff'),
|
||||
url('../assets/fonts/log.ttf') format('truetype');
|
||||
}
|
||||
|
||||
body {
|
||||
// 禁止手机页面下拉刷新
|
||||
overflow: hidden;
|
||||
@@ -52,6 +59,11 @@ body {
|
||||
width: 100%;
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
code,
|
||||
span {
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo,
|
||||
Courier, monospace, Log;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +75,10 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
.ͼ1 .cm-scroller {
|
||||
font-family: monospace, Log;
|
||||
}
|
||||
|
||||
.monaco-editor:not(.rename-box) {
|
||||
height: calc(100vh - 128px) !important;
|
||||
height: calc(100vh - var(--vh-offset, 0px) - 128px) !important;
|
||||
|
||||
+25
-39
@@ -1,35 +1,21 @@
|
||||
import intl from 'react-intl-universal';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import ProLayout, { PageLoading } from '@ant-design/pro-layout';
|
||||
import * as DarkReader from '@umijs/ssr-darkreader';
|
||||
import defaultProps from './defaultProps';
|
||||
import { Link, history, Outlet, useLocation } from '@umijs/max';
|
||||
import config from '@/utils/config';
|
||||
import { useCtx, useTheme } from '@/utils/hooks';
|
||||
import { request } from '@/utils/http';
|
||||
import {
|
||||
LogoutOutlined,
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
UserOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import config from '@/utils/config';
|
||||
import { request } from '@/utils/http';
|
||||
import './index.less';
|
||||
import ProLayout, { PageLoading } from '@ant-design/pro-layout';
|
||||
import { history, Link, Outlet, useLocation } from '@umijs/max';
|
||||
import * as DarkReader from '@umijs/ssr-darkreader';
|
||||
import { Avatar, Badge, Dropdown, Image, MenuProps, Tooltip } from 'antd';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import vhCheck from 'vh-check';
|
||||
import { useCtx, useTheme } from '@/utils/hooks';
|
||||
import {
|
||||
message,
|
||||
Badge,
|
||||
Modal,
|
||||
Avatar,
|
||||
Dropdown,
|
||||
Menu,
|
||||
Image,
|
||||
Popover,
|
||||
Descriptions,
|
||||
Tooltip,
|
||||
MenuProps,
|
||||
} from 'antd';
|
||||
// @ts-ignore
|
||||
import * as Sentry from '@sentry/react';
|
||||
import defaultProps from './defaultProps';
|
||||
import './index.less';
|
||||
import { init } from '../utils/init';
|
||||
import WebSocketManager from '../utils/websocket';
|
||||
|
||||
@@ -178,9 +164,9 @@ export default function () {
|
||||
useEffect(() => {
|
||||
if (!user || !user.username) return;
|
||||
const ws = WebSocketManager.getInstance(
|
||||
`${window.location.origin}${config.apiPrefix}ws?token=${localStorage.getItem(
|
||||
config.authKey,
|
||||
)}`,
|
||||
`${window.location.origin}${
|
||||
config.apiPrefix
|
||||
}ws?token=${localStorage.getItem(config.authKey)}`,
|
||||
);
|
||||
|
||||
return () => {
|
||||
@@ -201,9 +187,6 @@ export default function () {
|
||||
console.log(
|
||||
`从开始至load总耗时: ${timing.loadEventEnd - timing.navigationStart}`,
|
||||
);
|
||||
Sentry.captureMessage(
|
||||
`白屏时间 ${timing.responseStart - timing.navigationStart}`,
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -254,18 +237,15 @@ export default function () {
|
||||
<ProLayout
|
||||
selectedKeys={[location.pathname]}
|
||||
loading={loading}
|
||||
ErrorBoundary={Sentry.ErrorBoundary}
|
||||
logo={
|
||||
<>
|
||||
<Image preview={false} src="https://qn.whyour.cn/logo.png" />
|
||||
<div className="title">
|
||||
<span className="title">{intl.get('青龙')}</span>
|
||||
<a
|
||||
href={systemInfo?.changeLogLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
<span
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
window.open(systemInfo?.changeLogLink, '_blank');
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
@@ -289,7 +269,7 @@ export default function () {
|
||||
</span>
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
@@ -320,7 +300,9 @@ export default function () {
|
||||
shape="square"
|
||||
size="small"
|
||||
icon={<UserOutlined />}
|
||||
src={user.avatar ? `${config.apiPrefix}static/${user.avatar}` : ''}
|
||||
src={
|
||||
user.avatar ? `${config.apiPrefix}static/${user.avatar}` : ''
|
||||
}
|
||||
/>
|
||||
<span style={{ marginLeft: 5 }}>{user.username}</span>
|
||||
</span>
|
||||
@@ -342,7 +324,11 @@ export default function () {
|
||||
shape="square"
|
||||
size="small"
|
||||
icon={<UserOutlined />}
|
||||
src={user.avatar ? `${config.apiPrefix}static/${user.avatar}` : ''}
|
||||
src={
|
||||
user.avatar
|
||||
? `${config.apiPrefix}static/${user.avatar}`
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
<span style={{ marginLeft: 5 }}>{user.username}</span>
|
||||
</span>
|
||||
|
||||
+21
-2
@@ -186,6 +186,7 @@
|
||||
"秒后重试": "Retry after seconds",
|
||||
"在您的设备上打开两步验证应用程序以查看您的身份验证代码并验证您的身份。": "Open the two-factor authentication application on your device to view your authentication code and verify your identity.",
|
||||
"请选择脚本文件": "Please select a script file",
|
||||
"当前文件不支持预览": "Current file type is not supported for preview",
|
||||
"清空日志": "Clear Logs",
|
||||
"设置": "Settings",
|
||||
"退出": "Exit",
|
||||
@@ -344,7 +345,7 @@
|
||||
"gotify的url地址,例如 https://push.example.de:8080": "gotify URL address, e.g., https://push.example.de:8080",
|
||||
"gotify的消息应用token码": "gotify message application token code",
|
||||
"推送消息的优先级": "Priority of Push Messages",
|
||||
"chat的url地址": "Chat URL address",
|
||||
"synologyChat的url地址": "Synology Chat Webhook URL address",
|
||||
"chat的token码": "Chat token code",
|
||||
"推送到个人QQ: http://127.0.0.1/send_private_msg,群:http://127.0.0.1/send_group_msg": "Push to personal QQ: http://127.0.0.1/send_private_msg, group: http://127.0.0.1/send_group_msg",
|
||||
"访问密钥": "Access Key",
|
||||
@@ -379,6 +380,11 @@
|
||||
"iGot的信息推送key,例如:https://push.hellyw.com/XXXXXXXX": "iGot information push key, e.g., https://push.hellyw.com/XXXXXXXX",
|
||||
"微信扫码登录后一对一推送或一对多推送下面的token(您的Token),不提供PUSH_PLUS_USER则默认为一对一推送,参考 https://www.pushplus.plus/": "After WeChat scan login, one-to-one or one-to-many push using the provided token (your Token). If PUSH_PLUS_USER is not provided, it defaults to one-to-one push. See reference at https://www.pushplus.plus/",
|
||||
"一对多推送的“群组编码”(一对多推送下面->您的群组(如无则创建)->群组编码,如果您是创建群组人。也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送)": "The 'group code' for one-to-many push (one-to-many push -> your group (if none, create one) -> group code). If you are the creator of the group, you need to click 'View QR code' to scan and bind, otherwise, you won't receive group messages.",
|
||||
"发送模板": "send template, can use type: 'html,txt,json,markdown,cloudMonitor,jenkins,route,pay'",
|
||||
"发送渠道": "send channel, can use type: 'wechat,webhook,cp,mail,sms'",
|
||||
"webhook编码": "webhook code",
|
||||
"发送结果回调地址": "send result callback url",
|
||||
"好友令牌": "friend token",
|
||||
"用户令牌,扫描登录后 我的—>设置->令牌 中获取,参考 https://www.weplusbot.com/": "Token, which can be obtained after scanning and logging in, is available under 'My Account' -> 'Settings' -> 'Tokens'. Please refer to the instructions for detailed steps: https://www.weplusbot.com/",
|
||||
"消息接收人": "message recipient",
|
||||
"调用版本;专业版填写pro,个人版填写personal,为空默认使用专业版": "Version, you can specify 'pro' for the Professional version and 'personal' for the Personal version. If left blank, it will default to the Professional version.",
|
||||
@@ -388,6 +394,11 @@
|
||||
"SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定": "The SMTP login password may also be a special passphrase, depending on the specific email service provider's instructions",
|
||||
"PushMe的Key,https://push.i-i.me/": "PushMe key, https://push.i-i.me/",
|
||||
"自建的PushMeServer消息接口地址,例如:http://127.0.0.1:3010,不填则使用官方消息接口": "The self built PushMeServer message interface address, for example: http://127.0.0.1:3010 If left blank, use the official message interface",
|
||||
"ntfy的url地址,例如 https://ntfy.sh": "The URL address of ntfy, for example, https://ntfy.sh.",
|
||||
"ntfy的消息应用topic": "The topic for ntfy's messaging application.",
|
||||
"wxPusherBot的appToken": "wxPusherBot's appToken, obtain according to docs https://wxpusher.zjiecode.com/docs/",
|
||||
"wxPusherBot的topicIds": "wxPusherBot's topicIds, at least one of topicIds or uids must be configured",
|
||||
"wxPusherBot的uids": "wxPusherBot's uids, at least one of topicIds or uids must be configured",
|
||||
"请求方法": "Request Method",
|
||||
"请求头Content-Type": "Request Header Content-Type",
|
||||
"请求链接以http或者https开头。url或者body中必须包含$title,$content可选,对应api内容的位置": "Request URL should start with http or https. URL or body must contain $title, $content is optional and corresponds to the API content position.",
|
||||
@@ -486,5 +497,13 @@
|
||||
"NPM 镜像源": "NPM Mirror Source",
|
||||
"PyPI 镜像源": "PyPI Mirror Source",
|
||||
"alpine linux 镜像源": "Alpine Linux Mirror Source",
|
||||
"如果恢复失败,可进入容器执行": "If recovery fails, you can enter the container and execute"
|
||||
"如果恢复失败,可进入容器执行": "If recovery fails, you can enter the container and execute",
|
||||
"常规定时": "Normal Timing",
|
||||
"手动运行": "Manual Run",
|
||||
"开机运行": "Boot Run",
|
||||
"时区": "Timezone",
|
||||
"强制打开": "Force Open",
|
||||
"强制打开可能会导致编辑器显示异常": "Force opening may cause display issues in the editor",
|
||||
"确认离开": "Confirm Leave",
|
||||
"当前文件未保存,确认离开吗": "Current file is not saved, are you sure to leave?"
|
||||
}
|
||||
|
||||
+23
-3
@@ -186,6 +186,7 @@
|
||||
"秒后重试": "秒后重试",
|
||||
"在您的设备上打开两步验证应用程序以查看您的身份验证代码并验证您的身份。": "在您的设备上打开两步验证应用程序以查看您的身份验证代码并验证您的身份。",
|
||||
"请选择脚本文件": "请选择脚本文件",
|
||||
"当前文件不支持预览": "当前文件不支持预览",
|
||||
"清空日志": "清空日志",
|
||||
"设置": "设置",
|
||||
"退出": "退出",
|
||||
@@ -344,7 +345,7 @@
|
||||
"gotify的url地址,例如 https://push.example.de:8080": "gotify的url地址,例如 https://push.example.de:8080",
|
||||
"gotify的消息应用token码": "gotify的消息应用token码",
|
||||
"推送消息的优先级": "推送消息的优先级",
|
||||
"chat的url地址": "chat的url地址",
|
||||
"synologyChat的url地址": "synologyChat的webhook url地址",
|
||||
"chat的token码": "chat的token码",
|
||||
"推送到个人QQ: http://127.0.0.1/send_private_msg,群:http://127.0.0.1/send_group_msg": "推送到个人QQ: http://127.0.0.1/send_private_msg,群:http://127.0.0.1/send_group_msg",
|
||||
"访问密钥": "访问密钥",
|
||||
@@ -377,8 +378,13 @@
|
||||
"好友": "好友",
|
||||
"要发送的用户昵称或群名,如果目标是群,需要填群名,如果目标是好友,需要填好友昵称": "要发送的用户昵称或群名,如果目标是群,需要填群名,如果目标是好友,需要填好友昵称",
|
||||
"iGot的信息推送key,例如:https://push.hellyw.com/XXXXXXXX": "iGot的信息推送key,例如:https://push.hellyw.com/XXXXXXXX",
|
||||
"微信扫码登录后一对一推送或一对多推送下面的token(您的Token),不提供PUSH_PLUS_USER则默认为一对一推送,参考 https://www.pushplus.plus/": "微信扫码登录后一对一推送或一对多推送下面的token(您的Token),不提供PUSH_PLUS_USER则默认为一对一推送,参考 https://www.pushplus.plus/",
|
||||
"微信扫码登录后一对一推送或一对多推送下面的token(您的Token),不提供PUSH_PLUS_USER则默认为一对一推送,参考 https://www.pushplus.plus/": "微信扫码登录后一对一推送或一对多推送下面的token(你的Token),不提供PUSH_PLUS_USER则默认为一对一推送,参考 https://www.pushplus.plus/",
|
||||
"一对多推送的“群组编码”(一对多推送下面->您的群组(如无则创建)->群组编码,如果您是创建群组人。也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送)": "一对多推送的“群组编码”(一对多推送下面->您的群组(如无则创建)->群组编码,如果您是创建群组人。也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送)",
|
||||
"发送模板": "发送模板,支持html,txt,json,markdown,cloudMonitor,jenkins,route,pay",
|
||||
"发送渠道": "发送渠道,支持wechat,webhook,cp,mail,sms",
|
||||
"webhook编码": "webhook编码,可在pushplus公众号上扩展配置出更多渠道",
|
||||
"发送结果回调地址": "发送结果回调地址,会把推送最终结果通知到这个地址上",
|
||||
"好友令牌": "好友令牌,微信公众号渠道填写好友令牌,企业微信渠道填写企业微信用户id",
|
||||
"用户令牌,扫描登录后 我的—>设置->令牌 中获取,参考 https://www.weplusbot.com/": "用户令牌,扫描登录后 我的—>设置->令牌 中获取,参考 https://www.weplusbot.com/",
|
||||
"消息接收人": "消息接收人",
|
||||
"调用版本;专业版填写pro,个人版填写personal,为空默认使用专业版": "调用版本;专业版填写pro,个人版填写personal,为空默认使用专业版",
|
||||
@@ -388,6 +394,11 @@
|
||||
"SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定": "SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定",
|
||||
"PushMe的Key,https://push.i-i.me/": "PushMe的Key,https://push.i-i.me/",
|
||||
"自建的PushMeServer消息接口地址,例如:http://127.0.0.1:3010,不填则使用官方消息接口": "自建的PushMeServer消息接口地址,例如:http://127.0.0.1:3010,不填则使用官方消息接口",
|
||||
"ntfy的url地址,例如 https://ntfy.sh": "ntfy的url地址,例如 https://ntfy.sh",
|
||||
"ntfy的消息应用topic": "ntfy的消息应用topic",
|
||||
"wxPusherBot的appToken": "wxPusherBot的appToken, 按照文档获取 https://wxpusher.zjiecode.com/docs/",
|
||||
"wxPusherBot的topicIds": "wxPusherBot的topicIds, topicIds 和 uids 至少配置一个才行",
|
||||
"wxPusherBot的uids": "wxPusherBot的uids, topicIds 和 uids 至少配置一个才行",
|
||||
"请求方法": "请求方法",
|
||||
"请求头Content-Type": "请求头Content-Type",
|
||||
"请求链接以http或者https开头。url或者body中必须包含$title,$content可选,对应api内容的位置": "请求链接以http或者https开头。url或者body中必须包含$title,$content可选,对应api内容的位置",
|
||||
@@ -486,5 +497,14 @@
|
||||
"NPM 镜像源": "NPM 镜像源",
|
||||
"PyPI 镜像源": "PyPI 镜像源",
|
||||
"alpine linux 镜像源": "alpine linux 镜像源",
|
||||
"如果恢复失败,可进入容器执行": "如果恢复失败,可进入容器执行"
|
||||
"如果恢复失败,可进入容器执行": "如果恢复失败,可进入容器执行",
|
||||
"常规定时": "常规定时",
|
||||
"手动运行": "手动运行",
|
||||
"开机运行": "开机运行",
|
||||
"时区": "时区",
|
||||
"强制打开": "强制打开",
|
||||
"强制打开可能会导致编辑器显示异常": "强制打开可能会导致编辑器显示异常",
|
||||
"确认离开": "确认离开",
|
||||
"当前文件未保存,确认离开吗": "当前文件未保存,确认离开吗"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ScheduleType } from './type';
|
||||
|
||||
export const scheduleTypeMap = {
|
||||
[ScheduleType.Normal]: '',
|
||||
[ScheduleType.Once]: '@once',
|
||||
[ScheduleType.Boot]: '@boot',
|
||||
};
|
||||
|
||||
export const getScheduleType = (schedule?: string): ScheduleType => {
|
||||
if (schedule?.startsWith('@once')) return ScheduleType.Once;
|
||||
if (schedule?.startsWith('@boot')) return ScheduleType.Boot;
|
||||
return ScheduleType.Normal;
|
||||
};
|
||||
@@ -202,9 +202,6 @@ const CronDetailModal = ({
|
||||
.catch((e) => reject(e));
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -232,9 +229,6 @@ const CronDetailModal = ({
|
||||
}
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -259,9 +253,6 @@ const CronDetailModal = ({
|
||||
}
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -298,9 +289,6 @@ const CronDetailModal = ({
|
||||
}
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -337,9 +325,6 @@ const CronDetailModal = ({
|
||||
}
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
+69
-75
@@ -1,66 +1,68 @@
|
||||
import intl from 'react-intl-universal';
|
||||
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
|
||||
import { SharedContext } from '@/layouts';
|
||||
import { getCommandScript, getCrontabsNextDate } from '@/utils';
|
||||
import config from '@/utils/config';
|
||||
import { diffTime } from '@/utils/date';
|
||||
import { request } from '@/utils/http';
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
CheckOutlined,
|
||||
ClockCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
CopyOutlined,
|
||||
DeleteOutlined,
|
||||
DownOutlined,
|
||||
EditOutlined,
|
||||
EllipsisOutlined,
|
||||
FieldTimeOutlined,
|
||||
Loading3QuartersOutlined,
|
||||
PlusOutlined,
|
||||
PushpinOutlined,
|
||||
SettingOutlined,
|
||||
StopOutlined,
|
||||
UnorderedListOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { PageContainer } from '@ant-design/pro-layout';
|
||||
import { history, useOutletContext } from '@umijs/max';
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
Input,
|
||||
MenuProps,
|
||||
message,
|
||||
Modal,
|
||||
Table,
|
||||
Tag,
|
||||
Space,
|
||||
Tooltip,
|
||||
Dropdown,
|
||||
Menu,
|
||||
Typography,
|
||||
Input,
|
||||
Popover,
|
||||
Tabs,
|
||||
Table,
|
||||
TablePaginationConfig,
|
||||
MenuProps,
|
||||
Tabs,
|
||||
Tag,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import {
|
||||
ClockCircleOutlined,
|
||||
Loading3QuartersOutlined,
|
||||
CloseCircleOutlined,
|
||||
FileTextOutlined,
|
||||
EllipsisOutlined,
|
||||
PlayCircleOutlined,
|
||||
CheckCircleOutlined,
|
||||
EditOutlined,
|
||||
StopOutlined,
|
||||
DeleteOutlined,
|
||||
PauseCircleOutlined,
|
||||
FieldTimeOutlined,
|
||||
PushpinOutlined,
|
||||
DownOutlined,
|
||||
SettingOutlined,
|
||||
PlusOutlined,
|
||||
UnorderedListOutlined,
|
||||
CheckOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import config from '@/utils/config';
|
||||
import { PageContainer } from '@ant-design/pro-layout';
|
||||
import { request } from '@/utils/http';
|
||||
import CronModal, { CronLabelModal } from './modal';
|
||||
import CronLogModal from './logModal';
|
||||
import { ColumnProps } from 'antd/lib/table';
|
||||
import { FilterValue, SorterResult } from 'antd/lib/table/interface';
|
||||
import dayjs from 'dayjs';
|
||||
import { noop, omit } from 'lodash';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import { useVT } from 'virtualizedtableforantd4';
|
||||
import { getScheduleType } from './const';
|
||||
import CronDetailModal from './detail';
|
||||
import { diffTime } from '@/utils/date';
|
||||
import { history, useOutletContext } from '@umijs/max';
|
||||
import './index.less';
|
||||
import CronLogModal from './logModal';
|
||||
import CronModal, { CronLabelModal } from './modal';
|
||||
import {
|
||||
CrontabStatus,
|
||||
ICrontab,
|
||||
OperationName,
|
||||
OperationPath,
|
||||
ScheduleType,
|
||||
} from './type';
|
||||
import ViewCreateModal from './viewCreateModal';
|
||||
import ViewManageModal from './viewManageModal';
|
||||
import { FilterValue, SorterResult } from 'antd/lib/table/interface';
|
||||
import { SharedContext } from '@/layouts';
|
||||
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
|
||||
import { getCommandScript, getCrontabsNextDate, parseCrontab } from '@/utils';
|
||||
import { ColumnProps } from 'antd/lib/table';
|
||||
import { useVT } from 'virtualizedtableforantd4';
|
||||
import { ICrontab, OperationName, OperationPath, CrontabStatus } from './type';
|
||||
import Name from '@/components/name';
|
||||
import dayjs from 'dayjs';
|
||||
import { noop } from 'lodash';
|
||||
|
||||
const { Text, Paragraph, Link } = Typography;
|
||||
const { Search } = Input;
|
||||
const SHOW_TAB_COUNT = 10;
|
||||
|
||||
const Crontab = () => {
|
||||
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
|
||||
@@ -261,7 +263,9 @@ const Crontab = () => {
|
||||
},
|
||||
},
|
||||
render: (text, record) => {
|
||||
return dayjs(record.nextRunTime).format('YYYY-MM-DD HH:mm:ss');
|
||||
return record.nextRunTime
|
||||
? dayjs(record.nextRunTime).format('YYYY-MM-DD HH:mm:ss')
|
||||
: '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -394,9 +398,14 @@ const Crontab = () => {
|
||||
|
||||
setValue(
|
||||
data.map((x) => {
|
||||
const scheduleType = getScheduleType(x.schedule);
|
||||
const nextRunTime =
|
||||
scheduleType === ScheduleType.Normal
|
||||
? getCrontabsNextDate(x.schedule, x.extra_schedules)
|
||||
: null;
|
||||
return {
|
||||
...x,
|
||||
nextRunTime: getCrontabsNextDate(x.schedule, x.extra_schedules),
|
||||
nextRunTime,
|
||||
subscription: subscriptionMap?.[x.sub_id],
|
||||
};
|
||||
}),
|
||||
@@ -444,9 +453,6 @@ const Crontab = () => {
|
||||
}
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -479,9 +485,6 @@ const Crontab = () => {
|
||||
}
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -515,9 +518,6 @@ const Crontab = () => {
|
||||
}
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -560,9 +560,6 @@ const Crontab = () => {
|
||||
}
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -605,9 +602,6 @@ const Crontab = () => {
|
||||
}
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -620,6 +614,7 @@ const Crontab = () => {
|
||||
icon:
|
||||
record.isDisabled === 1 ? <CheckCircleOutlined /> : <StopOutlined />,
|
||||
},
|
||||
{ label: intl.get('复制'), key: 'copy', icon: <CopyOutlined /> },
|
||||
{ label: intl.get('删除'), key: 'delete', icon: <DeleteOutlined /> },
|
||||
{
|
||||
label: record.isPinned === 1 ? intl.get('取消置顶') : intl.get('置顶'),
|
||||
@@ -655,6 +650,9 @@ const Crontab = () => {
|
||||
case 'edit':
|
||||
editCron(record, index);
|
||||
break;
|
||||
case 'copy':
|
||||
editCron(omit(record, 'id'), index);
|
||||
break;
|
||||
case 'enableOrDisable':
|
||||
enabledOrDisabledCron(record, index);
|
||||
break;
|
||||
@@ -725,9 +723,6 @@ const Crontab = () => {
|
||||
}
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -753,9 +748,6 @@ const Crontab = () => {
|
||||
}
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -800,7 +792,9 @@ const Crontab = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (viewConf && enabledCronViews && enabledCronViews.length > 0) {
|
||||
const view = enabledCronViews.slice(4).find((x) => x.id === viewConf.id);
|
||||
const view = enabledCronViews
|
||||
.slice(SHOW_TAB_COUNT)
|
||||
.find((x) => x.id === viewConf.id);
|
||||
setMoreMenuActive(!!view);
|
||||
}
|
||||
}, [viewConf, enabledCronViews]);
|
||||
@@ -830,7 +824,7 @@ const Crontab = () => {
|
||||
viewAction(key);
|
||||
},
|
||||
items: [
|
||||
...[...enabledCronViews].slice(4).map((x) => ({
|
||||
...[...enabledCronViews].slice(SHOW_TAB_COUNT).map((x) => ({
|
||||
label: (
|
||||
<Space style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span>{x.name}</span>
|
||||
@@ -950,7 +944,7 @@ const Crontab = () => {
|
||||
}
|
||||
onTabClick={tabClick}
|
||||
items={[
|
||||
...[...enabledCronViews].slice(0, 4).map((x) => ({
|
||||
...[...enabledCronViews].slice(0, SHOW_TAB_COUNT).map((x) => ({
|
||||
key: x.id,
|
||||
label: x.name,
|
||||
})),
|
||||
|
||||
+107
-66
@@ -1,11 +1,13 @@
|
||||
import intl from 'react-intl-universal';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Modal, message, Input, Form, Button, Space } from 'antd';
|
||||
import { request } from '@/utils/http';
|
||||
import config from '@/utils/config';
|
||||
import cronParse from 'cron-parser';
|
||||
import EditableTagGroup from '@/components/tag';
|
||||
import config from '@/utils/config';
|
||||
import { request } from '@/utils/http';
|
||||
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { Button, Form, Input, Modal, Select, Space, message } from 'antd';
|
||||
import cronParse from 'cron-parser';
|
||||
import { useEffect, useState } from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import { getScheduleType, scheduleTypeMap } from './const';
|
||||
import { ScheduleType } from './type';
|
||||
|
||||
const CronModal = ({
|
||||
cron,
|
||||
@@ -18,15 +20,26 @@ const CronModal = ({
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [scheduleType, setScheduleType] = useState<ScheduleType>(
|
||||
cron ? getScheduleType(cron.schedule) : ScheduleType.Normal,
|
||||
);
|
||||
|
||||
const handleOk = async (values: any) => {
|
||||
setLoading(true);
|
||||
const method = cron ? 'put' : 'post';
|
||||
const payload = { ...values };
|
||||
if (cron) {
|
||||
payload.id = cron.id;
|
||||
}
|
||||
try {
|
||||
const method = cron?.id ? 'put' : 'post';
|
||||
const payload = {
|
||||
...values,
|
||||
schedule:
|
||||
scheduleType !== ScheduleType.Normal
|
||||
? scheduleTypeMap[scheduleType]
|
||||
: values.schedule,
|
||||
};
|
||||
|
||||
if (cron?.id) {
|
||||
payload.id = cron.id;
|
||||
}
|
||||
|
||||
const { code, data } = await request[method](
|
||||
`${config.apiPrefix}crons`,
|
||||
payload,
|
||||
@@ -34,78 +47,61 @@ const CronModal = ({
|
||||
|
||||
if (code === 200) {
|
||||
message.success(
|
||||
cron ? intl.get('更新任务成功') : intl.get('创建任务成功'),
|
||||
cron?.id ? intl.get('更新任务成功') : intl.get('创建任务成功'),
|
||||
);
|
||||
handleCancel(data);
|
||||
}
|
||||
setLoading(false);
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
form.resetFields();
|
||||
setScheduleType(getScheduleType(cron?.schedule));
|
||||
}, [cron, visible]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={cron ? intl.get('编辑任务') : intl.get('创建任务')}
|
||||
open={visible}
|
||||
forceRender
|
||||
centered
|
||||
maskClosable={false}
|
||||
onOk={() => {
|
||||
form
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
handleOk(values);
|
||||
})
|
||||
.catch((info) => {
|
||||
console.log('Validate Failed:', info);
|
||||
});
|
||||
}}
|
||||
onCancel={() => handleCancel()}
|
||||
confirmLoading={loading}
|
||||
const handleScheduleTypeChange = (type: ScheduleType) => {
|
||||
setScheduleType(type);
|
||||
form.setFieldValue('schedule', '');
|
||||
};
|
||||
|
||||
const renderScheduleOptions = () => (
|
||||
<Select
|
||||
defaultValue={scheduleType}
|
||||
value={scheduleType}
|
||||
onChange={handleScheduleTypeChange}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
name="form_in_modal"
|
||||
initialValues={cron}
|
||||
>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={intl.get('名称')}
|
||||
rules={[{ required: true, whitespace: true }]}
|
||||
>
|
||||
<Input placeholder={intl.get('请输入任务名称')} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="command"
|
||||
label={intl.get('命令/脚本')}
|
||||
rules={[{ required: true, whitespace: true }]}
|
||||
>
|
||||
<Input.TextArea
|
||||
rows={4}
|
||||
autoSize={{ minRows: 1, maxRows: 5 }}
|
||||
placeholder={intl.get(
|
||||
'支持输入脚本路径/任意系统可执行命令/task 脚本路径',
|
||||
)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Select.Option value={ScheduleType.Normal}>
|
||||
{intl.get('常规定时')}
|
||||
</Select.Option>
|
||||
<Select.Option value={ScheduleType.Once}>
|
||||
{intl.get('手动运行')}
|
||||
</Select.Option>
|
||||
<Select.Option value={ScheduleType.Boot}>
|
||||
{intl.get('开机运行')}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
);
|
||||
|
||||
const renderScheduleFields = () => {
|
||||
if (scheduleType !== ScheduleType.Normal) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
name="schedule"
|
||||
label={intl.get('定时规则')}
|
||||
rules={[
|
||||
{ required: true },
|
||||
{
|
||||
validator: (rule, value) => {
|
||||
validator: (_, value) => {
|
||||
if (!value || cronParse.parseExpression(value).hasNext()) {
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
return Promise.reject(intl.get('Cron表达式格式有误'));
|
||||
}
|
||||
return Promise.reject(intl.get('Cron表达式格式有误'));
|
||||
},
|
||||
},
|
||||
]}
|
||||
@@ -136,14 +132,58 @@ const CronModal = ({
|
||||
))}
|
||||
<Form.Item>
|
||||
<a onClick={() => add({ schedule: '' })}>
|
||||
<PlusOutlined />
|
||||
{intl.get('新增定时规则')}
|
||||
<PlusOutlined /> {intl.get('新增定时规则')}
|
||||
</a>
|
||||
</Form.Item>
|
||||
<Form.ErrorList errors={errors} />
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={cron?.id ? intl.get('编辑任务') : intl.get('创建任务')}
|
||||
open={visible}
|
||||
forceRender
|
||||
centered
|
||||
maskClosable={false}
|
||||
onOk={() => form.validateFields().then(handleOk)}
|
||||
onCancel={() => handleCancel()}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
name="form_in_modal"
|
||||
initialValues={cron}
|
||||
>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={intl.get('名称')}
|
||||
rules={[{ required: true, whitespace: true }]}
|
||||
>
|
||||
<Input placeholder={intl.get('请输入任务名称')} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="command"
|
||||
label={intl.get('命令/脚本')}
|
||||
rules={[{ required: true, whitespace: true }]}
|
||||
>
|
||||
<Input.TextArea
|
||||
rows={4}
|
||||
autoSize={{ minRows: 1, maxRows: 5 }}
|
||||
placeholder={intl.get(
|
||||
'支持输入脚本路径/任意系统可执行命令/task 脚本路径',
|
||||
)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label={intl.get('定时类型')} required>
|
||||
{renderScheduleOptions()}
|
||||
</Form.Item>
|
||||
{renderScheduleFields()}
|
||||
<Form.Item name="labels" label={intl.get('标签')}>
|
||||
<EditableTagGroup />
|
||||
</Form.Item>
|
||||
@@ -155,7 +195,7 @@ const CronModal = ({
|
||||
)}
|
||||
rules={[
|
||||
{
|
||||
validator(rule, value) {
|
||||
validator(_, value) {
|
||||
if (
|
||||
value &&
|
||||
(value.includes(' task ') || value.startsWith('task '))
|
||||
@@ -183,7 +223,7 @@ const CronModal = ({
|
||||
)}
|
||||
rules={[
|
||||
{
|
||||
validator(rule, value) {
|
||||
validator(_, value) {
|
||||
if (
|
||||
value &&
|
||||
(value.includes(' task ') || value.startsWith('task '))
|
||||
@@ -284,4 +324,5 @@ const CronLabelModal = ({
|
||||
);
|
||||
};
|
||||
|
||||
export { CronModal as default, CronLabelModal };
|
||||
export { CronLabelModal, CronModal as default };
|
||||
|
||||
|
||||
@@ -36,5 +36,11 @@ export interface ICrontab {
|
||||
last_execution_time?: number;
|
||||
nextRunTime: Date;
|
||||
sub_id: number;
|
||||
extra_schedules?: Array<{ schedule: string; }>;
|
||||
extra_schedules?: Array<{ schedule: string }>;
|
||||
}
|
||||
|
||||
export enum ScheduleType {
|
||||
Normal = 'normal',
|
||||
Once = 'once',
|
||||
Boot = 'boot',
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ const ViewManageModal = ({
|
||||
title: intl.get('名称'),
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
render: (v) => (v === '全部任务' ? intl.get('全部任务') : v)
|
||||
render: (v) => (v === '全部任务' ? intl.get('全部任务') : v),
|
||||
},
|
||||
{
|
||||
title: intl.get('类型'),
|
||||
@@ -162,9 +162,6 @@ const ViewManageModal = ({
|
||||
}
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -315,9 +315,6 @@ const Dependence = () => {
|
||||
}
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -342,9 +339,6 @@ const Dependence = () => {
|
||||
}
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -367,9 +361,6 @@ const Dependence = () => {
|
||||
getDependencies();
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -419,9 +410,6 @@ const Dependence = () => {
|
||||
}
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -439,9 +427,6 @@ const Dependence = () => {
|
||||
}
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Vendored
-12
@@ -292,9 +292,6 @@ const Env = () => {
|
||||
}
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -337,9 +334,6 @@ const Env = () => {
|
||||
}
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -456,9 +450,6 @@ const Env = () => {
|
||||
}
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -484,9 +475,6 @@ const Env = () => {
|
||||
}
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
+43
-22
@@ -1,31 +1,32 @@
|
||||
import intl from 'react-intl-universal';
|
||||
import { useState, useEffect, useCallback, Key, useRef } from 'react';
|
||||
import useFilterTreeData from '@/hooks/useFilterTreeData';
|
||||
import { SharedContext } from '@/layouts';
|
||||
import { depthFirstSearch } from '@/utils';
|
||||
import config from '@/utils/config';
|
||||
import { request } from '@/utils/http';
|
||||
import { CloudDownloadOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import { PageContainer } from '@ant-design/pro-layout';
|
||||
import Editor from '@monaco-editor/react';
|
||||
import CodeMirror from '@uiw/react-codemirror';
|
||||
import { useOutletContext } from '@umijs/max';
|
||||
import {
|
||||
TreeSelect,
|
||||
Tree,
|
||||
Input,
|
||||
Empty,
|
||||
Button,
|
||||
Empty,
|
||||
Input,
|
||||
message,
|
||||
Modal,
|
||||
Tooltip,
|
||||
Tree,
|
||||
TreeSelect,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import config from '@/utils/config';
|
||||
import { PageContainer } from '@ant-design/pro-layout';
|
||||
import Editor from '@monaco-editor/react';
|
||||
import { request } from '@/utils/http';
|
||||
import styles from './index.module.less';
|
||||
import CodeMirror from '@uiw/react-codemirror';
|
||||
import SplitPane from 'react-split-pane';
|
||||
import { useOutletContext } from '@umijs/max';
|
||||
import { SharedContext } from '@/layouts';
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
import { depthFirstSearch } from '@/utils';
|
||||
import { saveAs } from 'file-saver';
|
||||
import debounce from 'lodash/debounce';
|
||||
import uniq from 'lodash/uniq';
|
||||
import useFilterTreeData from '@/hooks/useFilterTreeData';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import { Key, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import SplitPane from 'react-split-pane';
|
||||
import styles from './index.module.less';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
@@ -67,6 +68,21 @@ const Log = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const downloadLog = () => {
|
||||
request
|
||||
.post<Blob>(
|
||||
`${config.apiPrefix}logs/download`,
|
||||
{
|
||||
filename: currentNode.title,
|
||||
path: currentNode.parent || '',
|
||||
},
|
||||
{ responseType: 'blob' },
|
||||
)
|
||||
.then((res) => {
|
||||
saveAs(res, currentNode.title);
|
||||
});
|
||||
};
|
||||
|
||||
const onSelect = (value: any, node: any) => {
|
||||
if (node.key === select || !value) {
|
||||
return;
|
||||
@@ -159,9 +175,6 @@ const Log = () => {
|
||||
}
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -225,10 +238,18 @@ const Log = () => {
|
||||
/>,
|
||||
]
|
||||
: [
|
||||
<Tooltip title={intl.get('下载')}>
|
||||
<Button
|
||||
disabled={!currentNode || currentNode.type === 'directory'}
|
||||
type="primary"
|
||||
onClick={downloadLog}
|
||||
icon={<CloudDownloadOutlined />}
|
||||
/>
|
||||
</Tooltip>,
|
||||
<Tooltip title={intl.get('删除')}>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={!select}
|
||||
disabled={!currentNode}
|
||||
onClick={deleteFile}
|
||||
icon={<DeleteOutlined />}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
.container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--background-color);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.content {
|
||||
text-align: center;
|
||||
background: var(--card-background);
|
||||
padding: 24px;
|
||||
border-radius: 12px;
|
||||
max-width: 390px;
|
||||
width: 100%;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.iconWrapper {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
background: var(--background-color);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 32px;
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
|
||||
.message {
|
||||
font-size: 16px;
|
||||
color: var(--text-color);
|
||||
margin-bottom: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.actionArea {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button {
|
||||
min-width: 140px;
|
||||
height: 36px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.warning {
|
||||
font-size: 13px;
|
||||
color: var(--text-color-secondary);
|
||||
line-height: 1.5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.warningIcon {
|
||||
font-size: 14px;
|
||||
color: #faad14;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import { Button, Space } from 'antd';
|
||||
import { FileUnknownOutlined, WarningOutlined } from '@ant-design/icons';
|
||||
import intl from 'react-intl-universal';
|
||||
import styles from './index.module.less';
|
||||
|
||||
interface UnsupportedFilePreviewProps {
|
||||
onForceOpen: () => void;
|
||||
}
|
||||
|
||||
const UnsupportedFilePreview: React.FC<UnsupportedFilePreviewProps> = ({
|
||||
onForceOpen,
|
||||
}) => {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.iconWrapper}>
|
||||
<FileUnknownOutlined className={styles.icon} />
|
||||
</div>
|
||||
<div className={styles.message}>
|
||||
{intl.get('当前文件不支持预览')}
|
||||
</div>
|
||||
<Space direction="vertical" size={8} className={styles.actionArea}>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={onForceOpen}
|
||||
className={styles.button}
|
||||
>
|
||||
{intl.get('强制打开')}
|
||||
</Button>
|
||||
<div className={styles.warning}>
|
||||
<WarningOutlined className={styles.warningIcon} />
|
||||
{intl.get('强制打开可能会导致编辑器显示异常')}
|
||||
</div>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UnsupportedFilePreview;
|
||||
+139
-81
@@ -1,51 +1,49 @@
|
||||
import intl from 'react-intl-universal';
|
||||
import { useState, useEffect, useCallback, Key, useRef } from 'react';
|
||||
import {
|
||||
TreeSelect,
|
||||
Tree,
|
||||
Input,
|
||||
Button,
|
||||
Modal,
|
||||
message,
|
||||
Typography,
|
||||
Tooltip,
|
||||
Dropdown,
|
||||
Menu,
|
||||
Empty,
|
||||
MenuProps,
|
||||
} from 'antd';
|
||||
import IconFont from '@/components/iconfont';
|
||||
import useFilterTreeData from '@/hooks/useFilterTreeData';
|
||||
import { SharedContext } from '@/layouts';
|
||||
import { depthFirstSearch, findNode, getEditorMode } from '@/utils';
|
||||
import config from '@/utils/config';
|
||||
import { PageContainer } from '@ant-design/pro-layout';
|
||||
import Editor from '@monaco-editor/react';
|
||||
import { request } from '@/utils/http';
|
||||
import styles from './index.module.less';
|
||||
import EditModal from './editModal';
|
||||
import CodeMirror from '@uiw/react-codemirror';
|
||||
import SplitPane from 'react-split-pane';
|
||||
import { canPreviewInMonaco } from '@/utils/monaco';
|
||||
import {
|
||||
CloudDownloadOutlined,
|
||||
DeleteOutlined,
|
||||
DownloadOutlined,
|
||||
EditOutlined,
|
||||
EllipsisOutlined,
|
||||
FormOutlined,
|
||||
PlusOutlined,
|
||||
PlusSquareOutlined,
|
||||
SearchOutlined,
|
||||
UserOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import EditScriptNameModal from './editNameModal';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { history, useOutletContext, useLocation } from '@umijs/max';
|
||||
import { parse } from 'query-string';
|
||||
import { depthFirstSearch, findNode, getEditorMode } from '@/utils';
|
||||
import { SharedContext } from '@/layouts';
|
||||
import useFilterTreeData from '@/hooks/useFilterTreeData';
|
||||
import uniq from 'lodash/uniq';
|
||||
import IconFont from '@/components/iconfont';
|
||||
import RenameModal from './renameModal';
|
||||
import { PageContainer } from '@ant-design/pro-layout';
|
||||
import Editor from '@monaco-editor/react';
|
||||
import { langs } from '@uiw/codemirror-extensions-langs';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import CodeMirror from '@uiw/react-codemirror';
|
||||
import { history, useOutletContext } from '@umijs/max';
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
Empty,
|
||||
Input,
|
||||
MenuProps,
|
||||
message,
|
||||
Modal,
|
||||
Tooltip,
|
||||
Tree,
|
||||
TreeSelect,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { saveAs } from 'file-saver';
|
||||
import debounce from 'lodash/debounce';
|
||||
import uniq from 'lodash/uniq';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import { parse } from 'query-string';
|
||||
import { Key, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import intl from 'react-intl-universal';
|
||||
import SplitPane from 'react-split-pane';
|
||||
import EditModal from './editModal';
|
||||
import EditScriptNameModal from './editNameModal';
|
||||
import styles from './index.module.less';
|
||||
import RenameModal from './renameModal';
|
||||
import UnsupportedFilePreview from './components/UnsupportedFilePreview';
|
||||
const { Text } = Typography;
|
||||
|
||||
const Script = () => {
|
||||
@@ -66,6 +64,11 @@ const Script = () => {
|
||||
useState(false);
|
||||
const [currentNode, setCurrentNode] = useState<any>();
|
||||
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
|
||||
const [showMonaco, setShowMonaco] = useState(true);
|
||||
|
||||
const handleIsEditing = (filename: string, value: boolean) => {
|
||||
setIsEditing(value && canPreviewInMonaco(filename));
|
||||
};
|
||||
|
||||
const getScripts = (needLoading: boolean = true) => {
|
||||
needLoading && setLoading(true);
|
||||
@@ -81,7 +84,7 @@ const Script = () => {
|
||||
.finally(() => needLoading && setLoading(false));
|
||||
};
|
||||
|
||||
const getDetail = (node: any) => {
|
||||
const getDetail = (node: any, options: any = {}) => {
|
||||
request
|
||||
.get(
|
||||
`${config.apiPrefix}scripts/detail?file=${encodeURIComponent(
|
||||
@@ -91,10 +94,28 @@ const Script = () => {
|
||||
.then(({ code, data }) => {
|
||||
if (code === 200) {
|
||||
setValue(data);
|
||||
if (options.callback) {
|
||||
options.callback();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const downloadScript = () => {
|
||||
request
|
||||
.post<Blob>(
|
||||
`${config.apiPrefix}scripts/download`,
|
||||
{
|
||||
filename: currentNode.title,
|
||||
path: currentNode.parent || '',
|
||||
},
|
||||
{ responseType: 'blob' },
|
||||
)
|
||||
.then((res) => {
|
||||
saveAs(res, currentNode.title);
|
||||
});
|
||||
};
|
||||
|
||||
const initGetScript = (_data: any) => {
|
||||
const { p, s } = parse(history.location.search);
|
||||
if (s) {
|
||||
@@ -125,13 +146,27 @@ const Script = () => {
|
||||
|
||||
if (node.type === 'directory') {
|
||||
setValue(intl.get('请选择脚本文件'));
|
||||
setShowMonaco(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!canPreviewInMonaco(node.title)) {
|
||||
setShowMonaco(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setShowMonaco(true);
|
||||
const newMode = getEditorMode(value);
|
||||
setMode(isPhone && newMode === 'typescript' ? 'javascript' : newMode);
|
||||
setValue(intl.get('加载中...'));
|
||||
getDetail(node);
|
||||
|
||||
getDetail(node, {
|
||||
callback: () => {
|
||||
if (isEditing) {
|
||||
setIsEditing(true);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const onTreeSelect = useCallback(
|
||||
@@ -140,23 +175,23 @@ const Script = () => {
|
||||
if (node.key === select && isEditing) {
|
||||
return;
|
||||
}
|
||||
const content = editorRef.current
|
||||
|
||||
const currentContent = editorRef.current
|
||||
? editorRef.current.getValue().replace(/\r\n/g, '\n')
|
||||
: value;
|
||||
if (content !== value) {
|
||||
const originalContent = value.replace(/\r\n/g, '\n');
|
||||
|
||||
if (currentContent !== originalContent && isEditing) {
|
||||
Modal.confirm({
|
||||
title: `确认离开`,
|
||||
content: <>{intl.get('当前修改未保存,确定离开吗')}</>,
|
||||
title: intl.get('确认离开'),
|
||||
content: <>{intl.get('当前文件未保存,确认离开吗')}</>,
|
||||
onOk() {
|
||||
onSelect(keys[0], e.node);
|
||||
setIsEditing(false);
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
handleIsEditing(e.node.title, false);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
setIsEditing(false);
|
||||
handleIsEditing(e.node.title, false);
|
||||
onSelect(keys[0], e.node);
|
||||
}
|
||||
},
|
||||
@@ -196,18 +231,18 @@ const Script = () => {
|
||||
if (node.type === 'file') {
|
||||
setSelect(node.key);
|
||||
setCurrentNode(node);
|
||||
setIsEditing(true);
|
||||
handleIsEditing(node.title, true);
|
||||
}
|
||||
};
|
||||
|
||||
const editFile = () => {
|
||||
setTimeout(() => {
|
||||
setIsEditing(true);
|
||||
handleIsEditing(currentNode.title, true);
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const cancelEdit = () => {
|
||||
setIsEditing(false);
|
||||
handleIsEditing(currentNode.title, false);
|
||||
setValue(intl.get('加载中...'));
|
||||
getDetail(currentNode);
|
||||
};
|
||||
@@ -240,16 +275,13 @@ const Script = () => {
|
||||
if (code === 200) {
|
||||
message.success(`保存成功`);
|
||||
setValue(content);
|
||||
setIsEditing(false);
|
||||
handleIsEditing(currentNode.title, false);
|
||||
}
|
||||
resolve(null);
|
||||
})
|
||||
.catch((e) => reject(e));
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -299,9 +331,6 @@ const Script = () => {
|
||||
}
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -342,7 +371,7 @@ const Script = () => {
|
||||
}
|
||||
setData(newData);
|
||||
onSelect(_file.title, _file);
|
||||
setIsEditing(true);
|
||||
handleIsEditing(_file.title, true);
|
||||
}
|
||||
setIsAddFileModalVisible(false);
|
||||
};
|
||||
@@ -472,19 +501,19 @@ const Script = () => {
|
||||
label: intl.get('编辑'),
|
||||
key: 'edit',
|
||||
icon: <EditOutlined />,
|
||||
disabled: !select,
|
||||
disabled: !currentNode,
|
||||
},
|
||||
{
|
||||
label: intl.get('重命名'),
|
||||
key: 'rename',
|
||||
icon: <IconFont type="ql-icon-rename" />,
|
||||
disabled: !select,
|
||||
disabled: !currentNode,
|
||||
},
|
||||
{
|
||||
label: intl.get('删除'),
|
||||
key: 'delete',
|
||||
icon: <DeleteOutlined />,
|
||||
disabled: !select,
|
||||
disabled: !currentNode,
|
||||
},
|
||||
],
|
||||
onClick: ({ key, domEvent }) => {
|
||||
@@ -493,6 +522,20 @@ const Script = () => {
|
||||
},
|
||||
};
|
||||
|
||||
const handleForceOpen = () => {
|
||||
if (!currentNode) return;
|
||||
|
||||
setMode('plaintext');
|
||||
setValue(intl.get('加载中...'));
|
||||
setShowMonaco(true);
|
||||
|
||||
getDetail(currentNode, {
|
||||
callback: () => {
|
||||
setIsEditing(true);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer
|
||||
className="ql-container-wrapper log-wrapper"
|
||||
@@ -554,7 +597,7 @@ const Script = () => {
|
||||
</Tooltip>,
|
||||
<Tooltip title={intl.get('编辑')}>
|
||||
<Button
|
||||
disabled={!select}
|
||||
disabled={!currentNode}
|
||||
type="primary"
|
||||
onClick={editFile}
|
||||
icon={<EditOutlined />}
|
||||
@@ -562,16 +605,24 @@ const Script = () => {
|
||||
</Tooltip>,
|
||||
<Tooltip title={intl.get('重命名')}>
|
||||
<Button
|
||||
disabled={!select}
|
||||
disabled={!currentNode}
|
||||
type="primary"
|
||||
onClick={renameFile}
|
||||
icon={<IconFont type="ql-icon-rename" />}
|
||||
/>
|
||||
</Tooltip>,
|
||||
<Tooltip title={intl.get('下载')}>
|
||||
<Button
|
||||
disabled={!currentNode || currentNode.type === 'directory'}
|
||||
type="primary"
|
||||
onClick={downloadScript}
|
||||
icon={<CloudDownloadOutlined />}
|
||||
/>
|
||||
</Tooltip>,
|
||||
<Tooltip title={intl.get('删除')}>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={!select}
|
||||
disabled={!currentNode}
|
||||
onClick={deleteFile}
|
||||
icon={<DeleteOutlined />}
|
||||
/>
|
||||
@@ -635,21 +686,28 @@ const Script = () => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Editor
|
||||
language={mode}
|
||||
value={value}
|
||||
theme={theme}
|
||||
options={{
|
||||
readOnly: !isEditing,
|
||||
fontSize: 12,
|
||||
lineNumbersMinChars: 3,
|
||||
glyphMargin: false,
|
||||
accessibilitySupport: 'off',
|
||||
}}
|
||||
onMount={(editor) => {
|
||||
editorRef.current = editor;
|
||||
}}
|
||||
/>
|
||||
{showMonaco ? (
|
||||
<Editor
|
||||
language={mode}
|
||||
value={value}
|
||||
theme={theme}
|
||||
options={{
|
||||
readOnly: !isEditing,
|
||||
fontSize: 12,
|
||||
lineNumbersMinChars: 3,
|
||||
glyphMargin: false,
|
||||
accessibilitySupport: 'off',
|
||||
}}
|
||||
onMount={(editor) => {
|
||||
editorRef.current = editor;
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<UnsupportedFilePreview
|
||||
filename={currentNode?.title || ''}
|
||||
onForceOpen={handleForceOpen}
|
||||
/>
|
||||
)}
|
||||
</SplitPane>
|
||||
)}
|
||||
{isPhone && (
|
||||
|
||||
@@ -32,3 +32,13 @@
|
||||
display: flex;
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
.ql-container-wrapper.ql-setting-container {
|
||||
.ant-tabs-tabpane > div {
|
||||
padding-left: 2px;
|
||||
}
|
||||
|
||||
.ant-tabs-tabpane > .ant-form {
|
||||
padding-left: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,9 +181,6 @@ const Setting = () => {
|
||||
}
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -213,9 +210,6 @@ const Setting = () => {
|
||||
}
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -23,10 +23,12 @@ import Countdown from 'antd/lib/statistic/Countdown';
|
||||
import useProgress from './progress';
|
||||
import pick from 'lodash/pick';
|
||||
import { disableBody } from '@/utils';
|
||||
import { TIMEZONES } from '@/utils/const';
|
||||
|
||||
const dataMap = {
|
||||
'log-remove-frequency': 'logRemoveFrequency',
|
||||
'cron-concurrency': 'cronConcurrency',
|
||||
timezone: 'timezone',
|
||||
};
|
||||
|
||||
const Other = ({
|
||||
@@ -37,6 +39,7 @@ const Other = ({
|
||||
const [systemConfig, setSystemConfig] = useState<{
|
||||
logRemoveFrequency?: number | null;
|
||||
cronConcurrency?: number | null;
|
||||
timezone?: string | null;
|
||||
}>();
|
||||
const [form] = Form.useForm();
|
||||
const [exportLoading, setExportLoading] = useState(false);
|
||||
@@ -252,6 +255,34 @@ const Other = ({
|
||||
</Button>
|
||||
</Input.Group>
|
||||
</Form.Item>
|
||||
<Form.Item label={intl.get('时区')} name="timezone">
|
||||
<Input.Group compact>
|
||||
<Select
|
||||
value={systemConfig?.timezone}
|
||||
style={{ width: 180 }}
|
||||
onChange={(value) => {
|
||||
setSystemConfig({ ...systemConfig, timezone: value });
|
||||
}}
|
||||
options={TIMEZONES.map((timezone) => ({
|
||||
value: timezone,
|
||||
label: timezone,
|
||||
}))}
|
||||
showSearch
|
||||
filterOption={(input, option) =>
|
||||
option?.value?.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
updateSystemConfig('timezone');
|
||||
}}
|
||||
style={{ width: 84 }}
|
||||
>
|
||||
{intl.get('确认')}
|
||||
</Button>
|
||||
</Input.Group>
|
||||
</Form.Item>
|
||||
<Form.Item label={intl.get('语言')} name="lang">
|
||||
<Select
|
||||
defaultValue={localStorage.getItem('lang') || ''}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import intl from 'react-intl-universal';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import CodeMirror from '@uiw/react-codemirror';
|
||||
import { Button, DatePicker, Empty, message, Spin } from 'antd';
|
||||
@@ -9,6 +10,13 @@ import { request } from '@/utils/http';
|
||||
import config from '@/utils/config';
|
||||
import { useRequest } from 'ahooks';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
systemLogDebugHighlightPlugin,
|
||||
systemLogErrorHighlightPlugin,
|
||||
systemLogInfoHighlightPlugin,
|
||||
systemLogTheme,
|
||||
systemLogWarnHighlightPlugin,
|
||||
} from '@/utils/codemirror/systemLog';
|
||||
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
@@ -83,7 +91,7 @@ const SystemLog = ({ height, theme }: any) => {
|
||||
deleteLog();
|
||||
}}
|
||||
>
|
||||
清空日志
|
||||
{intl.get('清空日志')}
|
||||
</Button>
|
||||
</div>
|
||||
{systemLogData ? (
|
||||
@@ -94,6 +102,13 @@ const SystemLog = ({ height, theme }: any) => {
|
||||
onCreateEditor={(view) => {
|
||||
editorRef.current = view;
|
||||
}}
|
||||
extensions={[
|
||||
systemLogDebugHighlightPlugin,
|
||||
systemLogErrorHighlightPlugin,
|
||||
systemLogInfoHighlightPlugin,
|
||||
systemLogWarnHighlightPlugin,
|
||||
systemLogTheme,
|
||||
]}
|
||||
readOnly={true}
|
||||
theme={theme.includes('dark') ? 'dark' : 'light'}
|
||||
/>
|
||||
|
||||
@@ -266,9 +266,6 @@ const Subscription = () => {
|
||||
}
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -302,9 +299,6 @@ const Subscription = () => {
|
||||
}
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -370,9 +364,6 @@ const Subscription = () => {
|
||||
}
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -415,9 +406,6 @@ const Subscription = () => {
|
||||
}
|
||||
});
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
import {
|
||||
Decoration,
|
||||
EditorView,
|
||||
ViewPlugin,
|
||||
ViewUpdate,
|
||||
} from '@codemirror/view';
|
||||
import { RangeSet, RangeSetBuilder } from '@codemirror/state';
|
||||
|
||||
const infoWord = /\[\ue6f5info\]/g;
|
||||
const debugWord = /\[\ue67fdebug\]/g;
|
||||
const warnWord = /\[\ue880warn\]/g;
|
||||
const errorWord = /\[\ue602error\]/g;
|
||||
|
||||
const customWordClassMap = {
|
||||
info: 'system-log-info',
|
||||
warn: 'system-warn-info',
|
||||
error: 'system-error-info',
|
||||
debug: 'system-debug-info',
|
||||
};
|
||||
|
||||
export const systemLogInfoHighlightPlugin = ViewPlugin.fromClass(
|
||||
class {
|
||||
decorations: RangeSet<Decoration>;
|
||||
|
||||
constructor(view: EditorView) {
|
||||
this.decorations = this.getDecorations(view);
|
||||
}
|
||||
|
||||
update(update: ViewUpdate) {
|
||||
if (update.docChanged) {
|
||||
this.decorations = this.getDecorations(update.view);
|
||||
}
|
||||
}
|
||||
|
||||
getDecorations(view: EditorView) {
|
||||
const builder = new RangeSetBuilder<Decoration>();
|
||||
const doc = view.state.doc.toString();
|
||||
let match;
|
||||
|
||||
while ((match = infoWord.exec(doc)) !== null) {
|
||||
const deco = Decoration.mark({
|
||||
class: customWordClassMap.info,
|
||||
});
|
||||
|
||||
builder.add(match.index, match.index + match[0].length, deco);
|
||||
}
|
||||
|
||||
return builder.finish();
|
||||
}
|
||||
},
|
||||
{
|
||||
decorations: (v) => v.decorations,
|
||||
},
|
||||
);
|
||||
|
||||
export const systemLogWarnHighlightPlugin = ViewPlugin.fromClass(
|
||||
class {
|
||||
decorations: RangeSet<Decoration>;
|
||||
|
||||
constructor(view: EditorView) {
|
||||
this.decorations = this.getDecorations(view);
|
||||
}
|
||||
|
||||
update(update: ViewUpdate) {
|
||||
if (update.docChanged) {
|
||||
this.decorations = this.getDecorations(update.view);
|
||||
}
|
||||
}
|
||||
|
||||
getDecorations(view: EditorView) {
|
||||
const builder = new RangeSetBuilder<Decoration>();
|
||||
const doc = view.state.doc.toString();
|
||||
let match;
|
||||
|
||||
while ((match = warnWord.exec(doc)) !== null) {
|
||||
const deco = Decoration.mark({
|
||||
class: customWordClassMap.warn,
|
||||
});
|
||||
|
||||
builder.add(match.index, match.index + match[0].length, deco);
|
||||
}
|
||||
|
||||
return builder.finish();
|
||||
}
|
||||
},
|
||||
{
|
||||
decorations: (v) => v.decorations,
|
||||
},
|
||||
);
|
||||
|
||||
export const systemLogDebugHighlightPlugin = ViewPlugin.fromClass(
|
||||
class {
|
||||
decorations: RangeSet<Decoration>;
|
||||
|
||||
constructor(view: EditorView) {
|
||||
this.decorations = this.getDecorations(view);
|
||||
}
|
||||
|
||||
update(update: ViewUpdate) {
|
||||
if (update.docChanged) {
|
||||
this.decorations = this.getDecorations(update.view);
|
||||
}
|
||||
}
|
||||
|
||||
getDecorations(view: EditorView) {
|
||||
const builder = new RangeSetBuilder<Decoration>();
|
||||
const doc = view.state.doc.toString();
|
||||
let match;
|
||||
|
||||
while ((match = debugWord.exec(doc)) !== null) {
|
||||
const deco = Decoration.mark({
|
||||
class: customWordClassMap.debug,
|
||||
});
|
||||
|
||||
builder.add(match.index, match.index + match[0].length, deco);
|
||||
}
|
||||
|
||||
return builder.finish();
|
||||
}
|
||||
},
|
||||
{
|
||||
decorations: (v) => v.decorations,
|
||||
},
|
||||
);
|
||||
|
||||
export const systemLogErrorHighlightPlugin = ViewPlugin.fromClass(
|
||||
class {
|
||||
decorations: RangeSet<Decoration>;
|
||||
|
||||
constructor(view: EditorView) {
|
||||
this.decorations = this.getDecorations(view);
|
||||
}
|
||||
|
||||
update(update: ViewUpdate) {
|
||||
if (update.docChanged) {
|
||||
this.decorations = this.getDecorations(update.view);
|
||||
}
|
||||
}
|
||||
|
||||
getDecorations(view: EditorView) {
|
||||
const builder = new RangeSetBuilder<Decoration>();
|
||||
const doc = view.state.doc.toString();
|
||||
let match;
|
||||
|
||||
while ((match = errorWord.exec(doc)) !== null) {
|
||||
const deco = Decoration.mark({
|
||||
class: customWordClassMap.error,
|
||||
});
|
||||
|
||||
builder.add(match.index, match.index + match[0].length, deco);
|
||||
}
|
||||
|
||||
return builder.finish();
|
||||
}
|
||||
},
|
||||
{
|
||||
decorations: (v) => v.decorations,
|
||||
},
|
||||
);
|
||||
|
||||
export const systemLogTheme = EditorView.baseTheme({
|
||||
'.system-log-info': {
|
||||
color: '#2196F3',
|
||||
},
|
||||
'.system-warn-info': {
|
||||
color: '#FFB827',
|
||||
},
|
||||
'.system-error-info': {
|
||||
color: '#FA5151',
|
||||
},
|
||||
'.system-debug-info': {
|
||||
color: '#009A29',
|
||||
},
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user