Compare commits

..
2 Commits
Author SHA1 Message Date
whyour f4337008eb 修复文件校验 2024-07-07 21:26:11 +08:00
whyour ca5cb88eca 测试 cli 2024-07-07 20:17:36 +08:00
122 changed files with 9143 additions and 13354 deletions
+11 -14
View File
@@ -7,6 +7,7 @@ on:
branches: branches:
- "master" - "master"
- "develop" - "develop"
- "test-cli"
tags: tags:
- "v*" - "v*"
schedule: schedule:
@@ -20,12 +21,10 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: wearerequired/git-mirror-action@v1 - uses: pixta-dev/repository-mirroring-action@v1
env:
SSH_PRIVATE_KEY: ${{ secrets.GITLAB_SSH_PK }}
with: with:
source-repo: https://github.com/whyour/qinglong.git target_repo_url: git@gitlab.com:whyour/qinglong.git
destination-repo: git@gitlab.com:whyour/qinglong.git ssh_private_key: ${{ secrets.GITLAB_SSH_PK }}
to_gitee: to_gitee:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -33,12 +32,10 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: wearerequired/git-mirror-action@v1 - uses: pixta-dev/repository-mirroring-action@v1
env:
SSH_PRIVATE_KEY: ${{ secrets.GITLAB_SSH_PK }}
with: with:
source-repo: https://github.com/whyour/qinglong.git target_repo_url: git@gitee.com:whyour/qinglong.git
destination-repo: git@gitee.com:whyour/qinglong.git ssh_private_key: ${{ secrets.GITLAB_SSH_PK }}
build-static: build-static:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -107,7 +104,7 @@ jobs:
cache: "pnpm" cache: "pnpm"
- name: Setup timezone - name: Setup timezone
uses: szenius/set-timezone@v2.0 uses: szenius/set-timezone@v1.2
with: with:
timezoneLinux: Asia/Shanghai timezoneLinux: Asia/Shanghai
@@ -153,7 +150,7 @@ jobs:
- name: Build and push - name: Build and push
id: docker_build id: docker_build
uses: docker/build-push-action@v6 uses: docker/build-push-action@v5
with: with:
build-args: | build-args: |
MAINTAINER=${{ github.repository_owner }} MAINTAINER=${{ github.repository_owner }}
@@ -194,7 +191,7 @@ jobs:
cache: "pnpm" cache: "pnpm"
- name: Setup timezone - name: Setup timezone
uses: szenius/set-timezone@v2.0 uses: szenius/set-timezone@v1.2
with: with:
timezoneLinux: Asia/Shanghai timezoneLinux: Asia/Shanghai
@@ -219,7 +216,7 @@ jobs:
- name: Build and push python3.10 - name: Build and push python3.10
id: docker_build_310 id: docker_build_310
uses: docker/build-push-action@v6 uses: docker/build-push-action@v5
with: with:
build-args: | build-args: |
MAINTAINER=${{ github.repository_owner }} MAINTAINER=${{ github.repository_owner }}
+1 -6
View File
@@ -22,9 +22,4 @@
.env .env
.history .history
.version.ts .version.ts
/.tmp /.tmp
__pycache__
/shell/preload/env.*
/shell/preload/notify.*
/shell/preload/*-notify.json
/shell/preload/__ql_notify__.*
+12 -11
View File
@@ -2,22 +2,23 @@
**/*.svg **/*.svg
**/*.ejs **/*.ejs
**/*.html **/*.html
/.umi package.json
/.umi-production .umi
/.umi-test .umi-production
/.history .umi-test
/.tmp .history
/node_modules .tmp
node_modules
npm-debug.log* npm-debug.log*
yarn-error.log yarn-error.log
yarn.lock yarn.lock
package-lock.json package-lock.json
/static static
/data data
DS_Store DS_Store
/src/.umi src/.umi
/src/.umi-production src/.umi-production
/src/.umi-test src/.umi-test
.env.local .env.local
.env .env
version.ts version.ts
+9 -2
View File
@@ -43,12 +43,19 @@ export default defineConfig({
}), }),
); );
}) as any, }) as any,
headScripts: [`./api/env.js`], 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',
],
copy: [ copy: [
{ {
from: 'node_modules/monaco-editor/min/vs', from: 'node_modules/monaco-editor/min/vs',
to: 'static/dist/monaco-editor/min/vs', to: 'static/dist/monaco-editor/min/vs',
}, },
], ],
npmClient: 'pnpm',
}); });
+121 -6
View File
@@ -18,9 +18,9 @@ Timed task management platform supporting Python3, JavaScript, Shell, Typescript
[docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat [docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong [docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
[Demo](http://demo.ninesix.cc:4433/) / [Issues](https://github.com/whyour/qinglong/issues) / [Telegram Channel](https://t.me/jiao_long) / [Buy Me a Coffee](https://www.buymeacoffee.com/qinglong) [Demo](http://demo.dlww.cc:4433/) / [Issues](https://github.com/whyour/qinglong/issues) / [Telegram Channel](https://t.me/jiao_long) / [Buy Me a Coffee](https://www.buymeacoffee.com/qinglong)
[演示](http://demo.ninesix.cc:4433/) / [反馈](https://github.com/whyour/qinglong/issues) / [Telegram 频道](https://t.me/jiao_long) / [打赏开发者](https://user-images.githubusercontent.com/22700758/244744295-29cd0cd1-c8bb-4ea1-adf6-29bd390ad4dd.jpg) [演示](http://demo.dlww.cc:4433/) / [反馈](https://github.com/whyour/qinglong/issues) / [Telegram 频道](https://t.me/jiao_long) / [打赏开发者](https://user-images.githubusercontent.com/22700758/244744295-29cd0cd1-c8bb-4ea1-adf6-29bd390ad4dd.jpg)
</div> </div>
![cover](https://user-images.githubusercontent.com/22700758/244847235-8dc1ca21-e03f-4606-9458-0541fab60413.png) ![cover](https://user-images.githubusercontent.com/22700758/244847235-8dc1ca21-e03f-4606-9458-0541fab60413.png)
@@ -56,15 +56,130 @@ npm i @whyour/qinglong
## Deployment ## Deployment
[View Documentation](https://qinglong.online/guide/getting-started/installation-guide) ### Docker (Recommended)
## Built-in API ```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
```
[View Documentation](https://qinglong.online/guide/user-guide/built-in-api) ### 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
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
```
## Built-in commands ## Built-in commands
[View Documentation](https://qinglong.online/guide/user-guide/basic-explanation) - 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 |
## Development ## Development
+119 -6
View File
@@ -20,9 +20,9 @@ Timed task management platform supporting Python3, JavaScript, Shell, Typescript
[docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat [docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong [docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
[Demo](http://demo.ninesix.cc:4433/) / [Issues](https://github.com/whyour/qinglong/issues) / [Telegram Channel](https://t.me/jiao_long) / [Buy Me a Coffee](https://www.buymeacoffee.com/qinglong) [Demo](http://demo.dlww.cc:4433/) / [Issues](https://github.com/whyour/qinglong/issues) / [Telegram Channel](https://t.me/jiao_long) / [Buy Me a Coffee](https://www.buymeacoffee.com/qinglong)
[演示](http://demo.ninesix.cc:4433/) / [反馈](https://github.com/whyour/qinglong/issues) / [Telegram 频道](https://t.me/jiao_long) / [打赏开发者](https://user-images.githubusercontent.com/22700758/244744295-29cd0cd1-c8bb-4ea1-adf6-29bd390ad4dd.jpg) [演示](http://demo.dlww.cc:4433/) / [反馈](https://github.com/whyour/qinglong/issues) / [Telegram 频道](https://t.me/jiao_long) / [打赏开发者](https://user-images.githubusercontent.com/22700758/244744295-29cd0cd1-c8bb-4ea1-adf6-29bd390ad4dd.jpg)
</div> </div>
![cover](https://user-images.githubusercontent.com/22700758/244847235-8dc1ca21-e03f-4606-9458-0541fab60413.png) ![cover](https://user-images.githubusercontent.com/22700758/244847235-8dc1ca21-e03f-4606-9458-0541fab60413.png)
@@ -58,15 +58,128 @@ npm i @whyour/qinglong
## 部署 ## 部署
[查看文档](https://qinglong.online/guide/getting-started/installation-guide) ### docker (推荐)
## 内置 API ```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
```
[查看文档](https://qinglong.online/guide/user-guide/built-in-api) ### 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/basic-explanation) - 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 | 任务执行时的文件路径 |
## 开发 ## 开发
-5
View File
@@ -1,5 +0,0 @@
## 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.
Vendored
-9
View File
@@ -1,9 +0,0 @@
import 'express';
declare global {
namespace Express {
interface Request {
platform: string;
}
}
}
+2 -2
View File
@@ -1,3 +1,4 @@
import { getFileContentByName, getLastModifyFilePath } from '../config/util';
import { Router, Request, Response, NextFunction } from 'express'; import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi'; import { Container } from 'typedi';
import { Logger } from 'winston'; import { Logger } from 'winston';
@@ -7,7 +8,6 @@ import { celebrate, Joi } from 'celebrate';
import { join } from 'path'; import { join } from 'path';
import { SAMPLE_FILES } from '../config/const'; import { SAMPLE_FILES } from '../config/const';
import ConfigService from '../services/config'; import ConfigService from '../services/config';
import { writeFileWithLock } from '../shared/utils';
const route = Router(); const route = Router();
export default (app: Router) => { export default (app: Router) => {
@@ -78,7 +78,7 @@ export default (app: Router) => {
if (name.startsWith('data/scripts/')) { if (name.startsWith('data/scripts/')) {
path = join(config.rootPath, name); path = join(config.rootPath, name);
} }
await writeFileWithLock(path, content); await fs.writeFile(path, content);
res.send({ code: 200, message: '保存成功' }); res.send({ code: 200, message: '保存成功' });
} catch (e) { } catch (e) {
return next(e); return next(e);
+37 -11
View File
@@ -4,8 +4,7 @@ import { Logger } from 'winston';
import CronService from '../services/cron'; import CronService from '../services/cron';
import CronViewService from '../services/cronView'; import CronViewService from '../services/cronView';
import { celebrate, Joi } from 'celebrate'; import { celebrate, Joi } from 'celebrate';
import { commonCronSchema } from '../validation/schedule'; import cron_parser from 'cron-parser';
const route = Router(); const route = Router();
export default (app: Router) => { export default (app: Router) => {
@@ -171,14 +170,27 @@ export default (app: Router) => {
route.post( route.post(
'/', '/',
celebrate({ celebrate({
body: Joi.object(commonCronSchema), 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),
}),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const cronService = Container.get(CronService); if (cron_parser.parseExpression(req.body.schedule).hasNext()) {
const data = await cronService.create(req.body); const cronService = Container.get(CronService);
return res.send({ code: 200, data }); const data = await cronService.create(req.body);
return res.send({ code: 200, data });
} else {
return res.send({ code: 400, message: 'param schedule error' });
}
} catch (e) { } catch (e) {
return next(e); return next(e);
} }
@@ -319,16 +331,30 @@ export default (app: Router) => {
'/', '/',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
...commonCronSchema, 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),
id: Joi.number().required(), id: Joi.number().required(),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const cronService = Container.get(CronService); if (
const data = await cronService.update(req.body); !req.body.schedule ||
return res.send({ code: 200, data }); 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' });
}
} catch (e) { } catch (e) {
return next(e); return next(e);
} }
@@ -392,7 +418,7 @@ export default (app: Router) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const cronService = Container.get(CronService); const cronService = Container.get(CronService);
const data = await cronService.importCrontab(); const data = await cronService.import_crontab();
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
return next(e); return next(e);
+18 -24
View File
@@ -1,9 +1,10 @@
import { Router, Request, Response, NextFunction } from 'express'; import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi'; import { Container } from 'typedi';
import { Logger } from 'winston'; import { Logger } from 'winston';
import * as fs from 'fs';
import config from '../config'; import config from '../config';
import { getFileContentByName, readDirs, removeAnsi, rmPath } from '../config/util'; import { getFileContentByName, readDirs, rmPath } from '../config/util';
import { join, resolve } from 'path'; import { join } from 'path';
import { celebrate, Joi } from 'celebrate'; import { celebrate, Joi } from 'celebrate';
const route = Router(); const route = Router();
const blacklist = ['.tmp']; const blacklist = ['.tmp'];
@@ -29,20 +30,16 @@ export default (app: Router) => {
'/detail', '/detail',
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
try { try {
const finalPath = resolve( if (blacklist.includes(req.path)) {
config.logPath,
(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: '暂无权限' }); return res.send({ code: 403, message: '暂无权限' });
} }
const content = await getFileContentByName(finalPath); const filePath = join(
res.send({ code: 200, data: removeAnsi(content) }); config.logPath,
(req.query.path || '') as string,
req.query.file as string,
);
const content = await getFileContentByName(filePath);
res.send({ code: 200, data: content });
} catch (e) { } catch (e) {
return next(e); return next(e);
} }
@@ -53,18 +50,15 @@ export default (app: Router) => {
'/:file', '/:file',
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
try { try {
const finalPath = resolve( if (blacklist.includes(req.path)) {
config.logPath,
(req.query.path as string) || '',
(req.params.file as string) || '',
);
if (
blacklist.includes(req.path) ||
!finalPath.startsWith(config.logPath)
) {
return res.send({ code: 403, message: '暂无权限' }); return res.send({ code: 403, message: '暂无权限' });
} }
const content = await getFileContentByName(finalPath); const filePath = join(
config.logPath,
(req.query.path || '') as string,
req.params.file,
);
const content = await getFileContentByName(filePath);
res.send({ code: 200, data: content }); res.send({ code: 200, data: content });
} catch (e) { } catch (e) {
return next(e); return next(e);
+11 -5
View File
@@ -1,4 +1,11 @@
import { fileExist, readDirs, readDir, rmPath } from '../config/util'; import {
fileExist,
getFileContentByName,
readDirs,
getLastModifyFilePath,
readDir,
rmPath,
} from '../config/util';
import { Router, Request, Response, NextFunction } from 'express'; import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi'; import { Container } from 'typedi';
import { Logger } from 'winston'; import { Logger } from 'winston';
@@ -8,7 +15,6 @@ import { celebrate, Joi } from 'celebrate';
import path, { join, parse } from 'path'; import path, { join, parse } from 'path';
import ScriptService from '../services/script'; import ScriptService from '../services/script';
import multer from 'multer'; import multer from 'multer';
import { writeFileWithLock } from '../shared/utils';
const route = Router(); const route = Router();
const storage = multer.diskStorage({ const storage = multer.diskStorage({
@@ -157,7 +163,7 @@ export default (app: Router) => {
await rmPath(originFilePath); await rmPath(originFilePath);
} }
} }
await writeFileWithLock(filePath, content); await fs.writeFile(filePath, content);
return res.send({ code: 200 }); return res.send({ code: 200 });
} catch (e) { } catch (e) {
return next(e); return next(e);
@@ -183,7 +189,7 @@ export default (app: Router) => {
path: string; path: string;
}; };
const filePath = join(config.scriptPath, path, filename); const filePath = join(config.scriptPath, path, filename);
await writeFileWithLock(filePath, content); await fs.writeFile(filePath, content);
return res.send({ code: 200 }); return res.send({ code: 200 });
} catch (e) { } catch (e) {
return next(e); return next(e);
@@ -262,7 +268,7 @@ export default (app: Router) => {
let { filename, content, path } = req.body; let { filename, content, path } = req.body;
const { name, ext } = parse(filename); const { name, ext } = parse(filename);
const filePath = join(config.scriptPath, path, `${name}.swap${ext}`); const filePath = join(config.scriptPath, path, `${name}.swap${ext}`);
await writeFileWithLock(filePath, content || ''); await fs.writeFile(filePath, content || '', { encoding: 'utf8' });
const scriptService = Container.get(ScriptService); const scriptService = Container.get(ScriptService);
const result = await scriptService.runScript(filePath); const result = await scriptService.runScript(filePath);
+2 -3
View File
@@ -15,7 +15,6 @@ export default (app: Router) => {
const subscriptionService = Container.get(SubscriptionService); const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.list( const data = await subscriptionService.list(
req.query.searchValue as string, req.query.searchValue as string,
req.query.ids as string,
); );
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
@@ -213,8 +212,8 @@ export default (app: Router) => {
body: Joi.array().items(Joi.number().required()), body: Joi.array().items(Joi.number().required()),
query: Joi.object({ query: Joi.object({
force: Joi.boolean().optional(), force: Joi.boolean().optional(),
t: Joi.number(), t: Joi.number()
}), })
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
+9 -76
View File
@@ -33,7 +33,7 @@ export default (app: Router) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const userService = Container.get(UserService); const userService = Container.get(UserService);
const authInfo = await userService.getAuthInfo(); const authInfo = await userService.getUserInfo();
const { version, changeLog, changeLogLink, publishTime } = const { version, changeLog, changeLogLink, publishTime } =
await parseVersion(config.versionFile); await parseVersion(config.versionFile);
@@ -340,79 +340,12 @@ export default (app: Router) => {
}, },
); );
route.get( route.get('/log', async (req: Request, res: Response, next: NextFunction) => {
'/log', try {
celebrate({ const systemService = Container.get(SystemService);
query: { await systemService.getSystemLog(res);
startTime: Joi.string().allow('').optional(), } catch (e) {
endTime: Joi.string().allow('').optional(), return next(e);
t: Joi.string().optional(), }
}, });
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
await systemService.getSystemLog(
res,
req.query as {
startTime?: string;
endTime?: string;
},
);
} catch (e) {
return next(e);
}
},
);
route.delete(
'/log',
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
await systemService.deleteSystemLog();
res.send({ code: 200 });
} catch (e) {
return next(e);
}
},
);
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
View File
@@ -90,7 +90,7 @@ export default (app: Router) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const userService = Container.get(UserService); const userService = Container.get(UserService);
const authInfo = await userService.getAuthInfo(); const authInfo = await userService.getUserInfo();
res.send({ res.send({
code: 200, code: 200,
data: { data: {
+2 -1
View File
@@ -10,6 +10,8 @@ async function startServer() {
await require('./loaders/initFile').default(); await require('./loaders/initFile').default();
await require('./loaders/sentry').default({ expressApp: app });
await require('./loaders/app').default({ expressApp: app }); await require('./loaders/app').default({ expressApp: app });
const server = app const server = app
@@ -17,7 +19,6 @@ async function startServer() {
Logger.debug(`✌️ 后端服务启动成功!`); Logger.debug(`✌️ 后端服务启动成功!`);
console.debug(`✌️ 后端服务启动成功!`); console.debug(`✌️ 后端服务启动成功!`);
process.send?.('ready'); process.send?.('ready');
require('./loaders/bootAfter').default();
}) })
.on('error', (err) => { .on('error', (err) => {
Logger.error(err); Logger.error(err);
+3 -10
View File
@@ -26,12 +26,10 @@ if (process.env.QL_DATA_DIR) {
} }
const shellPath = path.join(rootPath, 'shell/'); const shellPath = path.join(rootPath, 'shell/');
const preloadPath = path.join(shellPath, 'preload/');
const tmpPath = path.join(rootPath, '.tmp/'); const tmpPath = path.join(rootPath, '.tmp/');
const samplePath = path.join(rootPath, 'sample/'); const samplePath = path.join(rootPath, 'sample/');
const configPath = path.join(dataPath, 'config/'); const configPath = path.join(dataPath, 'config/');
const scriptPath = path.join(dataPath, 'scripts/'); const scriptPath = path.join(dataPath, 'scripts/');
const repoPath = path.join(dataPath, 'repo/');
const bakPath = path.join(dataPath, 'bak/'); const bakPath = path.join(dataPath, 'bak/');
const logPath = path.join(dataPath, 'log/'); const logPath = path.join(dataPath, 'log/');
const dbPath = path.join(dataPath, 'db/'); const dbPath = path.join(dataPath, 'db/');
@@ -39,11 +37,9 @@ const uploadPath = path.join(dataPath, 'upload/');
const sshdPath = path.join(dataPath, 'ssh.d/'); const sshdPath = path.join(dataPath, 'ssh.d/');
const systemLogPath = path.join(dataPath, 'syslog/'); const systemLogPath = path.join(dataPath, 'syslog/');
const envFile = path.join(preloadPath, 'env.sh'); const envFile = path.join(configPath, 'env.sh');
const jsEnvFile = path.join(preloadPath, 'env.js'); const jsEnvFile = path.join(configPath, 'env.js');
const pyEnvFile = path.join(preloadPath, 'env.py'); const pyEnvFile = path.join(configPath, 'env.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 confFile = path.join(configPath, 'config.sh');
const crontabFile = path.join(configPath, 'crontab.list'); const crontabFile = path.join(configPath, 'crontab.list');
const authConfigFile = path.join(configPath, 'auth.json'); const authConfigFile = path.join(configPath, 'auth.json');
@@ -95,13 +91,10 @@ export default {
envFile, envFile,
jsEnvFile, jsEnvFile,
pyEnvFile, pyEnvFile,
jsNotifyFile,
pyNotifyFile,
dbPath, dbPath,
uploadPath, uploadPath,
configPath, configPath,
scriptPath, scriptPath,
repoPath,
samplePath, samplePath,
blackFileList: [ blackFileList: [
'auth.json', 'auth.json',
+3 -3
View File
@@ -5,13 +5,13 @@ let pickedEnv: Record<string, string>;
function getPickedEnv() { function getPickedEnv() {
if (pickedEnv) return pickedEnv; if (pickedEnv) return pickedEnv;
const picked = pick(process.env, ['QlBaseUrl', 'DeployEnv', 'QL_DIR']); const picked = pick(process.env, ['QlBaseUrl', 'DeployEnv']);
if (picked.QlBaseUrl) { if (picked.QlBaseUrl) {
if (!picked.QlBaseUrl.startsWith('/')) { if (!picked.QlBaseUrl.startsWith('/')) {
picked.QlBaseUrl = `/${picked.QlBaseUrl}`; picked.QlBaseUrl = `/${picked.QlBaseUrl}`
} }
if (!picked.QlBaseUrl.endsWith('/')) { if (!picked.QlBaseUrl.endsWith('/')) {
picked.QlBaseUrl = `${picked.QlBaseUrl}/`; picked.QlBaseUrl = `${picked.QlBaseUrl}/`
} }
} }
pickedEnv = picked as Record<string, string>; pickedEnv = picked as Record<string, string>;
+13 -46
View File
@@ -10,7 +10,6 @@ import { load } from 'js-yaml';
import config from './index'; import config from './index';
import { TASK_COMMAND } from './const'; import { TASK_COMMAND } from './const';
import Logger from '../loaders/logger'; import Logger from '../loaders/logger';
import { writeFileWithLock } from '../shared/utils';
export * from './share'; export * from './share';
@@ -22,10 +21,6 @@ export async function getFileContentByName(fileName: string) {
return ''; return '';
} }
export function removeAnsi(text: string) {
return text.replace(/\x1b\[\d+m/g, '');
}
export async function getLastModifyFilePath(dir: string) { export async function getLastModifyFilePath(dir: string) {
let filePath = ''; let filePath = '';
@@ -150,19 +145,12 @@ export function getPlatform(userAgent: string): 'mobile' | 'desktop' {
system = 'android'; // android系统 system = 'android'; // android系统
} else if (testUa(/ios|iphone|ipad|ipod|iwatch/g)) { } else if (testUa(/ios|iphone|ipad|ipod|iwatch/g)) {
system = 'ios'; // ios系统 system = 'ios'; // ios系统
} else if (testUa(/openharmony/g)) {
system = 'openharmony'; // openharmony系统
} }
let platform = 'desktop'; let platform = 'desktop';
if (system === 'windows' || system === 'macos' || system === 'linux') { if (system === 'windows' || system === 'macos' || system === 'linux') {
platform = 'desktop'; platform = 'desktop';
} else if ( } else if (system === 'android' || system === 'ios' || testUa(/mobile/g)) {
system === 'android' ||
system === 'ios' ||
system === 'openharmony' ||
testUa(/mobile/g)
) {
platform = 'mobile'; platform = 'mobile';
} }
@@ -180,7 +168,7 @@ export async function fileExist(file: any) {
export async function createFile(file: string, data: string = '') { export async function createFile(file: string, data: string = '') {
await fs.mkdir(path.dirname(file), { recursive: true }); await fs.mkdir(path.dirname(file), { recursive: true });
await writeFileWithLock(file, data); await fs.writeFile(file, data);
} }
export async function handleLogPath( export async function handleLogPath(
@@ -242,14 +230,14 @@ interface IFile {
key: string; key: string;
type: 'directory' | 'file'; type: 'directory' | 'file';
parent: string; parent: string;
createTime: number; mtime: number;
size?: number; size?: number;
children?: IFile[]; children?: IFile[];
} }
export function dirSort(a: IFile, b: IFile): number { export function dirSort(a: IFile, b: IFile): number {
if (a.type === 'file' && b.type === 'file') { if (a.type === 'file' && b.type === 'file') {
return b.createTime - a.createTime; return b.mtime - a.mtime;
} else if (a.type === 'directory' && b.type === 'directory') { } else if (a.type === 'directory' && b.type === 'directory') {
return a.title.localeCompare(b.title); return a.title.localeCompare(b.title);
} else { } else {
@@ -283,7 +271,7 @@ export async function readDirs(
key, key,
type: 'directory', type: 'directory',
parent: relativePath, parent: relativePath,
createTime: stats.birthtime.getTime(), mtime: stats.mtime.getTime(),
children: children.sort(sort), children: children.sort(sort),
}); });
} else { } else {
@@ -293,7 +281,7 @@ export async function readDirs(
key, key,
parent: relativePath, parent: relativePath,
size: stats.size, size: stats.size,
createTime: stats.birthtime.getTime(), mtime: stats.mtime.getTime(),
}); });
} }
} }
@@ -473,7 +461,7 @@ export async function parseVersion(path: string): Promise<IVersion> {
return load(await fs.readFile(path, 'utf8')) as IVersion; return load(await fs.readFile(path, 'utf8')) as IVersion;
} }
export function parseContentVersion(content: string): IVersion { export async function parseContentVersion(content: string): Promise<IVersion> {
return load(content) as IVersion; return load(content) as IVersion;
} }
@@ -481,18 +469,13 @@ export async function getUniqPath(
command: string, command: string,
id: string, id: string,
): Promise<string> { ): Promise<string> {
let suffix = '';
if (/^\d+$/.test(id)) { if (/^\d+$/.test(id)) {
suffix = `_${id}`; id = `_${id}`;
} } else {
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]; let str = items[0];
if (items[0] === TASK_COMMAND) { if (items[0] === TASK_COMMAND) {
str = items[1]; str = items[1];
@@ -516,7 +499,7 @@ export async function getUniqPath(
str = `${tempStr}_${str.slice(slashIndex + 1)}`; str = `${tempStr}_${str.slice(slashIndex + 1)}`;
} }
return `${str}${suffix}`; return `${str}${id}`;
} }
export function safeJSONParse(value?: string) { export function safeJSONParse(value?: string) {
@@ -527,7 +510,7 @@ export function safeJSONParse(value?: string) {
try { try {
return JSON.parse(value); return JSON.parse(value);
} catch (error) { } catch (error) {
Logger.error('[safeJSONParse失败]', error); Logger.error('[JSON.parse失败]', error);
return {}; return {};
} }
} }
@@ -542,19 +525,3 @@ export async function rmPath(path: string) {
Logger.error('[rmPath失败]', error); 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
View File
@@ -20,7 +20,7 @@ export class Env {
this.timestamp = new Date().toString(); this.timestamp = new Date().toString();
this.position = options.position; this.position = options.position;
this.name = options.name; this.name = options.name;
this.remarks = options.remarks || ''; this.remarks = options.remarks;
} }
} }
+7 -26
View File
@@ -20,8 +20,6 @@ export enum NotificationMode {
'feishu' = 'feishu', 'feishu' = 'feishu',
'webhook' = 'webhook', 'webhook' = 'webhook',
'chronocat' = 'Chronocat', 'chronocat' = 'Chronocat',
'ntfy' = 'ntfy',
'wxPusherBot' = 'wxPusherBot',
} }
abstract class NotificationBaseInfo { abstract class NotificationBaseInfo {
@@ -49,8 +47,9 @@ export class PushDeerNotification extends NotificationBaseInfo {
public pushDeerUrl = ''; public pushDeerUrl = '';
} }
export class synologyChatNotification extends NotificationBaseInfo { export class ChatNotification extends NotificationBaseInfo {
public synologyChatUrl = ''; public chatUrl = '';
public chatToken = '';
} }
export class BarkNotification extends NotificationBaseInfo { export class BarkNotification extends NotificationBaseInfo {
@@ -60,7 +59,7 @@ export class BarkNotification extends NotificationBaseInfo {
public barkGroup = 'qinglong'; public barkGroup = 'qinglong';
public barkLevel = 'active'; public barkLevel = 'active';
public barkUrl = ''; public barkUrl = '';
public barkArchive = ''; public barkArchive=""
} }
export class TelegramBotNotification extends NotificationBaseInfo { export class TelegramBotNotification extends NotificationBaseInfo {
@@ -100,11 +99,6 @@ export class IGotNotification extends NotificationBaseInfo {
export class PushPlusNotification extends NotificationBaseInfo { export class PushPlusNotification extends NotificationBaseInfo {
public pushPlusToken = ''; public pushPlusToken = '';
public pushPlusUser = ''; public pushPlusUser = '';
public pushPlusTemplate = '';
public pushplusChannel = '';
public pushplusWebhook = '';
public pushplusCallbackUrl = '';
public pushplusTo = '';
} }
export class WePlusBotNotification extends NotificationBaseInfo { export class WePlusBotNotification extends NotificationBaseInfo {
@@ -145,24 +139,12 @@ export class LarkNotification extends NotificationBaseInfo {
public larkKey = ''; 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 export interface NotificationInfo
extends GoCqHttpBotNotification, extends GoCqHttpBotNotification,
GotifyNotification, GotifyNotification,
ServerChanNotification, ServerChanNotification,
PushDeerNotification, PushDeerNotification,
synologyChatNotification, ChatNotification,
BarkNotification, BarkNotification,
TelegramBotNotification, TelegramBotNotification,
DingtalkBotNotification, DingtalkBotNotification,
@@ -176,6 +158,5 @@ export interface NotificationInfo
PushMeNotification, PushMeNotification,
WebhookNotification, WebhookNotification,
ChronocatNotification, ChronocatNotification,
LarkNotification, LarkNotification {}
NtfyNotification,
WxPusherBotNotification {}
+1 -1
View File
@@ -24,7 +24,7 @@ export interface AppToken {
expiration: number; expiration: number;
} }
export type AppScope = 'envs' | 'crons' | 'configs' | 'scripts' | 'logs' | 'system'; export type AppScope = 'envs' | 'crons' | 'configs' | 'scripts' | 'logs';
export interface AppInstance extends Model<App, App>, App {} export interface AppInstance extends Model<App, App>, App {}
export const AppModel = sequelize.define<AppInstance>('App', { export const AppModel = sequelize.define<AppInstance>('App', {
+2 -23
View File
@@ -27,7 +27,6 @@ export enum AuthDataType {
'notification' = 'notification', 'notification' = 'notification',
'removeLogFrequency' = 'removeLogFrequency', 'removeLogFrequency' = 'removeLogFrequency',
'systemConfig' = 'systemConfig', 'systemConfig' = 'systemConfig',
'authConfig' = 'authConfig',
} }
export interface SystemConfigInfo { export interface SystemConfigInfo {
@@ -37,7 +36,6 @@ export interface SystemConfigInfo {
nodeMirror?: string; nodeMirror?: string;
pythonMirror?: string; pythonMirror?: string;
linuxMirror?: string; linuxMirror?: string;
timezone?: string;
} }
export interface LoginLogInfo { export interface LoginLogInfo {
@@ -48,30 +46,11 @@ export interface LoginLogInfo {
status?: LoginStatus; 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 & export type SystemModelInfo = SystemConfigInfo &
Partial<NotificationInfo> & Partial<NotificationInfo> &
LoginLogInfo & LoginLogInfo;
Partial<AuthInfo>;
export interface SystemInstance export interface SystemInstance extends Model<SystemInfo, SystemInfo>, SystemInfo { }
extends Model<SystemInfo, SystemInfo>,
SystemInfo {}
export const SystemModel = sequelize.define<SystemInstance>('Auth', { export const SystemModel = sequelize.define<SystemInstance>('Auth', {
ip: DataTypes.STRING, ip: DataTypes.STRING,
type: DataTypes.STRING, type: DataTypes.STRING,
-13
View File
@@ -1,13 +0,0 @@
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 }>;
};
-8
View File
@@ -1,8 +0,0 @@
import Container from 'typedi';
import CronService from '../services/cron';
export default async () => {
const cronService = Container.get(CronService);
await cronService.bootTask();
};
+81 -7
View File
@@ -1,11 +1,15 @@
import Logger from './logger'; import Logger from './logger';
import path from 'path';
import DataStore from 'nedb';
import { EnvModel } from '../data/env'; import { EnvModel } from '../data/env';
import { CrontabModel } from '../data/cron'; import { CrontabModel } from '../data/cron';
import { DependenceModel } from '../data/dependence'; import { DependenceModel } from '../data/dependence';
import { AppModel } from '../data/open'; import { AppModel } from '../data/open';
import { SystemModel } from '../data/system'; import { SystemModel } from '../data/system';
import { fileExist } from '../config/util';
import { SubscriptionModel } from '../data/subscription'; import { SubscriptionModel } from '../data/subscription';
import { CrontabViewModel } from '../data/cronView'; import { CrontabViewModel } from '../data/cronView';
import config from '../config';
import { sequelize } from '../data'; import { sequelize } from '../data';
export default async () => { export default async () => {
@@ -44,18 +48,88 @@ export default async () => {
} catch (error) {} } catch (error) {}
try { try {
await sequelize.query('alter table Crontabs add column sub_id NUMBER'); await sequelize.query('alter table Crontabs add column sub_id NUMBER');
} catch (error) {} } catch (error) { }
try { try {
await sequelize.query( await sequelize.query('alter table Crontabs add column extra_schedules JSON');
'alter table Crontabs add column extra_schedules JSON', } catch (error) { }
);
} catch (error) {}
try { try {
await sequelize.query('alter table Crontabs add column task_before TEXT'); await sequelize.query('alter table Crontabs add column task_before TEXT');
} catch (error) {} } catch (error) { }
try { try {
await sequelize.query('alter table Crontabs add column task_after TEXT'); await sequelize.query('alter table Crontabs add column task_after TEXT');
} catch (error) {} } 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 });
});
}
console.log('✌️ DB loaded'); console.log('✌️ DB loaded');
Logger.info('✌️ DB loaded'); Logger.info('✌️ DB loaded');
+16 -7
View File
@@ -19,26 +19,35 @@ async function linkToNodeModule(src: string, dst?: string) {
async function linkCommand() { async function linkCommand() {
const commandPath = await promiseExec('which node'); const commandPath = await promiseExec('which node');
const commandDir = path.dirname(commandPath); const commandDir = path.dirname(commandPath);
const linkShell = [ const oldLinkShell = [
{ {
src: 'update.sh', src: 'update.sh',
dest: 'ql', dest: 'ql',
tmp: 'ql_tmp', tmp: 'ql_tmp',
}, },
{
src: 'task.sh',
dest: 'task',
tmp: 'task_tmp',
},
]; ];
// const newLinkShell = [
// {
// src: 'task.mjs',
// dest: 'task',
// tmp: 'task_tmp',
// },
// ];
for (const link of linkShell) { for (const link of oldLinkShell) {
const source = path.join(config.rootPath, 'shell', link.src); const source = path.join(config.rootPath, 'shell', link.src);
const target = path.join(commandDir, link.dest); const target = path.join(commandDir, link.dest);
const tmpTarget = path.join(commandDir, link.tmp); const tmpTarget = path.join(commandDir, link.tmp);
await fs.symlink(source, tmpTarget); await fs.symlink(source, tmpTarget);
await fs.rename(tmpTarget, target); await fs.rename(tmpTarget, target);
} }
// for (const link of newLinkShell) {
// const source = path.join(config.rootPath, 'cli', link.src);
// const target = path.join(commandDir, link.dest);
// const tmpTarget = path.join(commandDir, link.tmp);
// await fs.symlink(source, tmpTarget);
// await fs.rename(tmpTarget, target);
// }
} }
export default async (src: string = 'deps') => { export default async (src: string = 'deps') => {
+21 -16
View File
@@ -3,14 +3,19 @@ import bodyParser from 'body-parser';
import cors from 'cors'; import cors from 'cors';
import routes from '../api'; import routes from '../api';
import config from '../config'; import config from '../config';
import { UnauthorizedError, expressjwt } from 'express-jwt'; import jwt, { UnauthorizedError } from 'express-jwt';
import { getPlatform, getToken } from '../config/util'; import fs from 'fs/promises';
import { getPlatform, getToken, safeJSONParse } from '../config/util';
import Container from 'typedi';
import OpenService from '../services/open';
import rewrite from 'express-urlrewrite'; 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 { errors } from 'celebrate';
import { createProxyMiddleware } from 'http-proxy-middleware'; import { createProxyMiddleware } from 'http-proxy-middleware';
import { serveEnv } from '../config/serverEnv'; import { serveEnv } from '../config/serverEnv';
import Logger from './logger'; import Logger from './logger';
import { IKeyvStore, shareStore } from '../shared/store';
export default ({ app }: { app: Application }) => { export default ({ app }: { app: Application }) => {
app.set('trust proxy', 'loopback'); app.set('trust proxy', 'loopback');
@@ -24,7 +29,7 @@ export default ({ app }: { app: Application }) => {
target: `http://0.0.0.0:${config.publicPort}/api`, target: `http://0.0.0.0:${config.publicPort}/api`,
changeOrigin: true, changeOrigin: true,
pathRewrite: { '/api/public': '' }, pathRewrite: { '/api/public': '' },
logger: Logger, logProvider: () => Logger,
}), }),
); );
@@ -32,7 +37,7 @@ export default ({ app }: { app: Application }) => {
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true })); app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
app.use( app.use(
expressjwt({ jwt({
secret: config.secret, secret: config.secret,
algorithms: ['HS384'], algorithms: ['HS384'],
}).unless({ }).unless({
@@ -53,10 +58,8 @@ export default ({ app }: { app: Application }) => {
app.use(async (req, res, next) => { app.use(async (req, res, next) => {
const headerToken = getToken(req); const headerToken = getToken(req);
if (req.path.startsWith('/open/')) { if (req.path.startsWith('/open/')) {
const apps = await shareStore.getApps(); const openService = Container.get(OpenService);
const doc = apps?.filter((x) => const doc = await openService.findTokenByValue(headerToken);
x.tokens?.find((y) => y.value === headerToken),
)?.[0];
if (doc && doc.tokens && doc.tokens.length > 0) { if (doc && doc.tokens && doc.tokens.length > 0) {
const currentToken = doc.tokens.find((x) => x.value === headerToken); const currentToken = doc.tokens.find((x) => x.value === headerToken);
const keyMatch = req.path.match(/\/open\/([a-z]+)\/*/); const keyMatch = req.path.match(/\/open\/([a-z]+)\/*/);
@@ -80,9 +83,9 @@ export default ({ app }: { app: Application }) => {
return next(); return next();
} }
const authInfo = await shareStore.getAuthInfo(); const data = await fs.readFile(config.authConfigFile, 'utf8');
if (authInfo && headerToken) { if (data && headerToken) {
const { token = '', tokens = {} } = authInfo; const { token = '', tokens = {} } = safeJSONParse(data);
if (headerToken === token || tokens[req.platform] === headerToken) { if (headerToken === token || tokens[req.platform] === headerToken) {
return next(); return next();
} }
@@ -100,8 +103,8 @@ export default ({ app }: { app: Application }) => {
if (!['/api/user/init', '/api/user/notification/init'].includes(req.path)) { if (!['/api/user/init', '/api/user/notification/init'].includes(req.path)) {
return next(); return next();
} }
const authInfo = const userService = Container.get(UserService);
(await shareStore.getAuthInfo()) || ({} as IKeyvStore['authInfo']); const authInfo = await userService.getUserInfo();
let isInitialized = true; let isInitialized = true;
if ( if (
@@ -159,8 +162,8 @@ export default ({ app }: { app: Application }) => {
.status(500) .status(500)
.send({ .send({
code: 400, code: 400,
message: `${err.message}`, message: `${err.name} ${err.message}`,
errors: err.errors, validation: err.errors,
}) })
.end(); .end();
} }
@@ -175,6 +178,8 @@ export default ({ app }: { app: Application }) => {
res: Response, res: Response,
next: NextFunction, next: NextFunction,
) => { ) => {
Sentry.captureException(err);
res.status(err.status || 500); res.status(err.status || 500);
res.json({ res.json({
code: err.status || 500, code: err.status || 500,
+3 -55
View File
@@ -11,62 +11,16 @@ import { CrontabViewModel, CronViewType } from '../data/cronView';
import { initPosition } from '../data/env'; import { initPosition } from '../data/env';
import { AuthDataType, SystemModel } from '../data/system'; import { AuthDataType, SystemModel } from '../data/system';
import SystemService from '../services/system'; import SystemService from '../services/system';
import UserService from '../services/user';
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 () => { export default async () => {
const cronService = Container.get(CronService); const cronService = Container.get(CronService);
const envService = Container.get(EnvService); const envService = Container.get(EnvService);
const dependenceService = Container.get(DependenceService); const dependenceService = Container.get(DependenceService);
const systemService = Container.get(SystemService); const systemService = Container.get(SystemService);
const userService = Container.get(UserService);
const openService = Container.get(OpenService);
// 初始化增加系统配置 // 初始化增加系统配置
let systemApp = ( await SystemModel.upsert({ type: AuthDataType.systemConfig });
await AppModel.findOne({ await SystemModel.upsert({ type: AuthDataType.notification });
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 = () => { const installDependencies = () => {
// 初始化时安装所有处于安装中,安装成功,安装失败的依赖 // 初始化时安装所有处于安装中,安装成功,安装失败的依赖
@@ -89,6 +43,7 @@ export default async () => {
}; };
// 初始化更新 linux/python/nodejs 镜像源配置 // 初始化更新 linux/python/nodejs 镜像源配置
const systemConfig = await systemService.getSystemConfig();
if (systemConfig.info?.pythonMirror) { if (systemConfig.info?.pythonMirror) {
systemService.updatePythonMirror({ systemService.updatePythonMirror({
pythonMirror: systemConfig.info?.pythonMirror, pythonMirror: systemConfig.info?.pythonMirror,
@@ -207,11 +162,4 @@ export default async () => {
// 初始化保存一次ck和定时任务数据 // 初始化保存一次ck和定时任务数据
await cronService.autosave_crontab(); await cronService.autosave_crontab();
await envService.set_envs(); 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);
}
}; };
+81 -84
View File
@@ -3,7 +3,6 @@ import path from 'path';
import os from 'os'; import os from 'os';
import Logger from './logger'; import Logger from './logger';
import { fileExist } from '../config/util'; import { fileExist } from '../config/util';
import { writeFileWithLock } from '../shared/utils';
const rootPath = process.env.QL_DIR as string; const rootPath = process.env.QL_DIR as string;
let dataPath = path.join(rootPath, 'data/'); let dataPath = path.join(rootPath, 'data/');
@@ -12,7 +11,6 @@ if (process.env.QL_DATA_DIR) {
dataPath = process.env.QL_DATA_DIR.replace(/\/$/g, ''); dataPath = process.env.QL_DATA_DIR.replace(/\/$/g, '');
} }
const preloadPath = path.join(rootPath, 'shell/preload/');
const configPath = path.join(dataPath, 'config/'); const configPath = path.join(dataPath, 'config/');
const scriptPath = path.join(dataPath, 'scripts/'); const scriptPath = path.join(dataPath, 'scripts/');
const logPath = path.join(dataPath, 'log/'); const logPath = path.join(dataPath, 'log/');
@@ -21,104 +19,103 @@ const bakPath = path.join(dataPath, 'bak/');
const samplePath = path.join(rootPath, 'sample/'); const samplePath = path.join(rootPath, 'sample/');
const tmpPath = path.join(logPath, '.tmp/'); const tmpPath = path.join(logPath, '.tmp/');
const confFile = path.join(configPath, 'config.sh'); const confFile = path.join(configPath, 'config.sh');
const authConfigFile = path.join(configPath, 'auth.json');
const sampleConfigFile = path.join(samplePath, 'config.sample.sh'); 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 sampleTaskShellFile = path.join(samplePath, 'task.sample.sh');
const sampleNotifyJsFile = path.join(samplePath, 'notify.js'); const sampleNotifyJsFile = path.join(samplePath, 'notify.js');
const sampleNotifyPyFile = path.join(samplePath, 'notify.py'); const sampleNotifyPyFile = path.join(samplePath, 'notify.py');
const scriptNotifyJsFile = path.join(scriptPath, 'sendNotify.js'); const scriptNotifyJsFile = path.join(scriptPath, 'sendNotify.js');
const scriptNotifyPyFile = path.join(scriptPath, 'notify.py'); const scriptNotifyPyFile = path.join(scriptPath, '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 TaskBeforeFile = path.join(configPath, 'task_before.sh');
const TaskBeforeJsFile = path.join(configPath, 'task_before.js');
const TaskBeforePyFile = path.join(configPath, 'task_before.py');
const TaskAfterFile = path.join(configPath, 'task_after.sh'); const TaskAfterFile = path.join(configPath, 'task_after.sh');
const homedir = os.homedir(); const homedir = os.homedir();
const sshPath = path.resolve(homedir, '.ssh'); const sshPath = path.resolve(homedir, '.ssh');
const sshdPath = path.join(dataPath, 'ssh.d'); const sshdPath = path.join(dataPath, 'ssh.d');
const systemLogPath = path.join(dataPath, 'syslog'); const systemLogPath = path.join(dataPath, 'syslog');
const directories = [
configPath,
scriptPath,
preloadPath,
logPath,
tmpPath,
uploadPath,
sshPath,
bakPath,
sshdPath,
systemLogPath,
];
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.',
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 () => { export default async () => {
for (const dirPath of directories) { const authFileExist = await fileExist(authConfigFile);
if (!(await fileExist(dirPath))) { const confFileExist = await fileExist(confFile);
await fs.mkdir(dirPath); const scriptDirExist = await fileExist(scriptPath);
} 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 TaskAfterFileExist = await fileExist(TaskAfterFile);
if (!configDirExist) {
await fs.mkdir(configPath);
} }
for (const item of files) { if (!scriptDirExist) {
const exists = await fileExist(item.target); await fs.mkdir(scriptPath);
if (!item.checkExistence || !exists) { }
if (!item.content && !item.source) {
throw new Error( if (!logDirExist) {
`Neither content nor source specified for ${item.target}`, await fs.mkdir(logPath);
); }
}
const content = item.content || (await fs.readFile(item.source!)); if (!tmpDirExist) {
await writeFileWithLock(item.target, content); 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));
}
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 (!TaskAfterFileExist) {
await fs.writeFile(TaskAfterFile, await fs.readFile(sampleTaskShellFile));
} }
Logger.info('✌️ Init file down'); Logger.info('✌️ Init file down');
+16 -32
View File
@@ -12,10 +12,7 @@ export default async () => {
const subscriptionService = Container.get(SubscriptionService); const subscriptionService = Container.get(SubscriptionService);
// 生成内置token // 生成内置token
let tokenCommand = `ts-node-transpile-only ${join( let tokenCommand = `tsx ${join(config.rootPath, 'back/token.ts')}`;
config.rootPath,
'back/token.ts',
)}`;
const tokenFile = join(config.rootPath, 'static/build/token.js'); const tokenFile = join(config.rootPath, 'static/build/token.js');
if (await fileExist(tokenFile)) { if (await fileExist(tokenFile)) {
@@ -25,43 +22,30 @@ export default async () => {
id: NaN, id: NaN,
name: '生成token', name: '生成token',
command: tokenCommand, command: tokenCommand,
runOrigin: 'system',
} as ScheduleTaskType; } as ScheduleTaskType;
await scheduleService.cancelIntervalTask(cron); await scheduleService.cancelIntervalTask(cron);
scheduleService.createIntervalTask( scheduleService.createIntervalTask(cron, {
cron, days: 28,
{ });
days: 28,
},
true,
);
// 运行删除日志任务 // 运行删除日志任务
const data = await systemService.getSystemConfig(); const data = await systemService.getSystemConfig();
if (data && data.info) { if (data && data.info && data.info.logRemoveFrequency) {
if (data.info.logRemoveFrequency) { const rmlogCron = {
const rmlogCron = { id: data.id as number,
id: data.id as number, name: '删除日志',
name: '删除日志', command: `ql rmlog ${data.info.logRemoveFrequency}`,
command: `ql rmlog ${data.info.logRemoveFrequency}`, };
runOrigin: 'system' as const, await scheduleService.cancelIntervalTask(rmlogCron);
}; scheduleService.createIntervalTask(rmlogCron, {
await scheduleService.cancelIntervalTask(rmlogCron); days: data.info.logRemoveFrequency,
scheduleService.createIntervalTask( });
rmlogCron,
{
days: data.info.logRemoveFrequency,
},
true,
);
}
systemService.updateTimezone(data.info);
} }
// 运行所有订阅
await subscriptionService.setSshConfig(); await subscriptionService.setSshConfig();
const subs = await subscriptionService.list(); const subs = await subscriptionService.list();
for (const sub of subs) { for (const sub of subs) {
subscriptionService.handleTask(sub.get({ plain: true }), !sub.is_disabled); subscriptionService.handleTask(sub, !sub.is_disabled, !sub.is_disabled);
} }
}; };
+4 -4
View File
@@ -4,10 +4,10 @@ import config from '../config';
import path from 'path'; import path from 'path';
const levelMap: Record<string, string> = { const levelMap: Record<string, string> = {
info: '\ue6f5', info: '🔵',
warn: '\ue880', warn: '🟡',
error: '\ue602', error: '🔴',
debug: '\ue67f' debug: '🔶'
} }
const customFormat = winston.format.combine( const customFormat = winston.format.combine(
+32
View File
@@ -0,0 +1,32 @@
import { Application } from 'express';
import * as Sentry from '@sentry/node';
import Logger from './logger';
import config from '../config';
import fs from 'fs';
import { parseVersion } from '../config/util';
export default async ({ expressApp }: { expressApp: Application }) => {
const { version } = await parseVersion(config.versionFile);
Sentry.init({
ignoreErrors: [
/SequelizeUniqueConstraintError/i,
/Validation error/i,
/UnauthorizedError/i,
/celebrate request validation failed/i,
],
dsn: 'https://8b5c84cfef3e22541bc84de0ed00497b@o1098464.ingest.sentry.io/6122819',
integrations: [
new Sentry.Integrations.Http({ tracing: true }),
new Sentry.Integrations.Express({ app: expressApp }),
],
tracesSampleRate: 0.8,
release: version,
});
expressApp.use(Sentry.Handlers.requestHandler());
expressApp.use(Sentry.Handlers.tracingHandler());
Logger.info('✌️ Sentry loaded');
console.log('✌️ Sentry loaded');
};
+9 -5
View File
@@ -2,8 +2,9 @@ import sockJs from 'sockjs';
import { Server } from 'http'; import { Server } from 'http';
import { Container } from 'typedi'; import { Container } from 'typedi';
import SockService from '../services/sock'; import SockService from '../services/sock';
import { getPlatform } from '../config/util'; import config from '../config/index';
import { shareStore } from '../shared/store'; import fs from 'fs/promises';
import { getPlatform, safeJSONParse } from '../config/util';
export default async ({ server }: { server: Server }) => { export default async ({ server }: { server: Server }) => {
const echo = sockJs.createServer({ prefix: '/api/ws', log: () => {} }); const echo = sockJs.createServer({ prefix: '/api/ws', log: () => {} });
@@ -14,12 +15,13 @@ export default async ({ server }: { server: Server }) => {
conn.close('404'); conn.close('404');
} }
const authInfo = await shareStore.getAuthInfo(); const data = await fs.readFile(config.authConfigFile, 'utf8');
const platform = getPlatform(conn.headers['user-agent'] || '') || 'desktop'; const platform = getPlatform(conn.headers['user-agent'] || '') || 'desktop';
const headerToken = conn.url.replace(`${conn.pathname}?token=`, ''); const headerToken = conn.url.replace(`${conn.pathname}?token=`, '');
if (authInfo) { if (data) {
const { token = '', tokens = {} } = authInfo; const { token = '', tokens = {} } = safeJSONParse(data);
if (headerToken === token || tokens[platform] === headerToken) { if (headerToken === token || tokens[platform] === headerToken) {
conn.write(JSON.stringify({ type: 'ping', message: 'hanhh' }));
sockService.addClient(conn); sockService.addClient(conn);
conn.on('data', (message) => { conn.on('data', (message) => {
@@ -31,6 +33,8 @@ export default async ({ server }: { server: Server }) => {
}); });
return; return;
} else {
conn.write(JSON.stringify({ type: 'ping', message: 'whyour' }));
} }
} }
+2 -2
View File
@@ -2,7 +2,7 @@ import bodyParser from 'body-parser';
import { errors } from 'celebrate'; import { errors } from 'celebrate';
import cors from 'cors'; import cors from 'cors';
import { Application, NextFunction, Request, Response } from 'express'; import { Application, NextFunction, Request, Response } from 'express';
import { expressjwt } from 'express-jwt'; import jwt from 'express-jwt';
import Container from 'typedi'; import Container from 'typedi';
import config from '../config'; import config from '../config';
import SystemService from '../services/system'; import SystemService from '../services/system';
@@ -16,7 +16,7 @@ export default ({ app }: { app: Application }) => {
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true })); app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
app.use( app.use(
expressjwt({ jwt({
secret: config.secret, secret: config.secret,
algorithms: ['HS384'], algorithms: ['HS384'],
}), }),
-142
View File
@@ -1,142 +0,0 @@
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
View File
File diff suppressed because it is too large Load Diff
+91 -114
View File
@@ -1,23 +1,17 @@
// 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 */ /* eslint-disable */
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
import { import {
type CallOptions, CallOptions,
ChannelCredentials, ChannelCredentials,
Client, Client,
type ClientOptions, ClientOptions,
type ClientUnaryCall, ClientUnaryCall,
type handleUnaryCall, handleUnaryCall,
makeGenericClientConstructor, makeGenericClientConstructor,
Metadata, Metadata,
type ServiceError, ServiceError,
type UntypedServiceImplementation, UntypedServiceImplementation,
} from "@grpc/grpc-js"; } from "@grpc/grpc-js";
import _m0 from "protobufjs/minimal";
export const protobufPackage = "com.ql.cron"; export const protobufPackage = "com.ql.cron";
@@ -29,7 +23,7 @@ export interface ICron {
id: string; id: string;
schedule: string; schedule: string;
command: string; command: string;
extra_schedules: ISchedule[]; extraSchedules: ISchedule[];
name: string; name: string;
} }
@@ -51,53 +45,51 @@ function createBaseISchedule(): ISchedule {
return { schedule: "" }; return { schedule: "" };
} }
export const ISchedule: MessageFns<ISchedule> = { export const ISchedule = {
encode(message: ISchedule, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { encode(message: ISchedule, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.schedule !== "") { if (message.schedule !== "") {
writer.uint32(10).string(message.schedule); writer.uint32(10).string(message.schedule);
} }
return writer; return writer;
}, },
decode(input: BinaryReader | Uint8Array, length?: number): ISchedule { decode(input: _m0.Reader | Uint8Array, length?: number): ISchedule {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length; let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseISchedule(); const message = createBaseISchedule();
while (reader.pos < end) { while (reader.pos < end) {
const tag = reader.uint32(); const tag = reader.uint32();
switch (tag >>> 3) { switch (tag >>> 3) {
case 1: { case 1:
if (tag !== 10) { if (tag !== 10) {
break; break;
} }
message.schedule = reader.string(); message.schedule = reader.string();
continue; continue;
}
} }
if ((tag & 7) === 4 || tag === 0) { if ((tag & 7) === 4 || tag === 0) {
break; break;
} }
reader.skip(tag & 7); reader.skipType(tag & 7);
} }
return message; return message;
}, },
fromJSON(object: any): ISchedule { fromJSON(object: any): ISchedule {
return { schedule: isSet(object.schedule) ? globalThis.String(object.schedule) : "" }; return { schedule: isSet(object.schedule) ? String(object.schedule) : "" };
}, },
toJSON(message: ISchedule): unknown { toJSON(message: ISchedule): unknown {
const obj: any = {}; const obj: any = {};
if (message.schedule !== "") { message.schedule !== undefined && (obj.schedule = message.schedule);
obj.schedule = message.schedule;
}
return obj; return obj;
}, },
create<I extends Exact<DeepPartial<ISchedule>, I>>(base?: I): ISchedule { create<I extends Exact<DeepPartial<ISchedule>, I>>(base?: I): ISchedule {
return ISchedule.fromPartial(base ?? ({} as any)); return ISchedule.fromPartial(base ?? {});
}, },
fromPartial<I extends Exact<DeepPartial<ISchedule>, I>>(object: I): ISchedule { fromPartial<I extends Exact<DeepPartial<ISchedule>, I>>(object: I): ISchedule {
const message = createBaseISchedule(); const message = createBaseISchedule();
message.schedule = object.schedule ?? ""; message.schedule = object.schedule ?? "";
@@ -106,11 +98,11 @@ export const ISchedule: MessageFns<ISchedule> = {
}; };
function createBaseICron(): ICron { function createBaseICron(): ICron {
return { id: "", schedule: "", command: "", extra_schedules: [], name: "" }; return { id: "", schedule: "", command: "", extraSchedules: [], name: "" };
} }
export const ICron: MessageFns<ICron> = { export const ICron = {
encode(message: ICron, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { encode(message: ICron, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.id !== "") { if (message.id !== "") {
writer.uint32(10).string(message.id); writer.uint32(10).string(message.id);
} }
@@ -120,8 +112,8 @@ export const ICron: MessageFns<ICron> = {
if (message.command !== "") { if (message.command !== "") {
writer.uint32(26).string(message.command); writer.uint32(26).string(message.command);
} }
for (const v of message.extra_schedules) { for (const v of message.extraSchedules) {
ISchedule.encode(v!, writer.uint32(34).fork()).join(); ISchedule.encode(v!, writer.uint32(34).fork()).ldelim();
} }
if (message.name !== "") { if (message.name !== "") {
writer.uint32(42).string(message.name); writer.uint32(42).string(message.name);
@@ -129,103 +121,93 @@ export const ICron: MessageFns<ICron> = {
return writer; return writer;
}, },
decode(input: BinaryReader | Uint8Array, length?: number): ICron { decode(input: _m0.Reader | Uint8Array, length?: number): ICron {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length; let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseICron(); const message = createBaseICron();
while (reader.pos < end) { while (reader.pos < end) {
const tag = reader.uint32(); const tag = reader.uint32();
switch (tag >>> 3) { switch (tag >>> 3) {
case 1: { case 1:
if (tag !== 10) { if (tag !== 10) {
break; break;
} }
message.id = reader.string(); message.id = reader.string();
continue; continue;
} case 2:
case 2: {
if (tag !== 18) { if (tag !== 18) {
break; break;
} }
message.schedule = reader.string(); message.schedule = reader.string();
continue; continue;
} case 3:
case 3: {
if (tag !== 26) { if (tag !== 26) {
break; break;
} }
message.command = reader.string(); message.command = reader.string();
continue; continue;
} case 4:
case 4: {
if (tag !== 34) { if (tag !== 34) {
break; break;
} }
message.extra_schedules.push(ISchedule.decode(reader, reader.uint32())); message.extraSchedules.push(ISchedule.decode(reader, reader.uint32()));
continue; continue;
} case 5:
case 5: {
if (tag !== 42) { if (tag !== 42) {
break; break;
} }
message.name = reader.string(); message.name = reader.string();
continue; continue;
}
} }
if ((tag & 7) === 4 || tag === 0) { if ((tag & 7) === 4 || tag === 0) {
break; break;
} }
reader.skip(tag & 7); reader.skipType(tag & 7);
} }
return message; return message;
}, },
fromJSON(object: any): ICron { fromJSON(object: any): ICron {
return { return {
id: isSet(object.id) ? globalThis.String(object.id) : "", id: isSet(object.id) ? String(object.id) : "",
schedule: isSet(object.schedule) ? globalThis.String(object.schedule) : "", schedule: isSet(object.schedule) ? String(object.schedule) : "",
command: isSet(object.command) ? globalThis.String(object.command) : "", command: isSet(object.command) ? String(object.command) : "",
extra_schedules: globalThis.Array.isArray(object?.extra_schedules) extraSchedules: Array.isArray(object?.extraSchedules)
? object.extra_schedules.map((e: any) => ISchedule.fromJSON(e)) ? object.extraSchedules.map((e: any) => ISchedule.fromJSON(e))
: [], : [],
name: isSet(object.name) ? globalThis.String(object.name) : "", name: isSet(object.name) ? String(object.name) : "",
}; };
}, },
toJSON(message: ICron): unknown { toJSON(message: ICron): unknown {
const obj: any = {}; const obj: any = {};
if (message.id !== "") { message.id !== undefined && (obj.id = message.id);
obj.id = message.id; message.schedule !== undefined && (obj.schedule = message.schedule);
} message.command !== undefined && (obj.command = message.command);
if (message.schedule !== "") { if (message.extraSchedules) {
obj.schedule = message.schedule; obj.extraSchedules = message.extraSchedules.map((e) => e ? ISchedule.toJSON(e) : undefined);
} } else {
if (message.command !== "") { obj.extraSchedules = [];
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; return obj;
}, },
create<I extends Exact<DeepPartial<ICron>, I>>(base?: I): ICron { create<I extends Exact<DeepPartial<ICron>, I>>(base?: I): ICron {
return ICron.fromPartial(base ?? ({} as any)); return ICron.fromPartial(base ?? {});
}, },
fromPartial<I extends Exact<DeepPartial<ICron>, I>>(object: I): ICron { fromPartial<I extends Exact<DeepPartial<ICron>, I>>(object: I): ICron {
const message = createBaseICron(); const message = createBaseICron();
message.id = object.id ?? ""; message.id = object.id ?? "";
message.schedule = object.schedule ?? ""; message.schedule = object.schedule ?? "";
message.command = object.command ?? ""; message.command = object.command ?? "";
message.extra_schedules = object.extra_schedules?.map((e) => ISchedule.fromPartial(e)) || []; message.extraSchedules = object.extraSchedules?.map((e) => ISchedule.fromPartial(e)) || [];
message.name = object.name ?? ""; message.name = object.name ?? "";
return message; return message;
}, },
@@ -235,53 +217,55 @@ function createBaseAddCronRequest(): AddCronRequest {
return { crons: [] }; return { crons: [] };
} }
export const AddCronRequest: MessageFns<AddCronRequest> = { export const AddCronRequest = {
encode(message: AddCronRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { encode(message: AddCronRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
for (const v of message.crons) { for (const v of message.crons) {
ICron.encode(v!, writer.uint32(10).fork()).join(); ICron.encode(v!, writer.uint32(10).fork()).ldelim();
} }
return writer; return writer;
}, },
decode(input: BinaryReader | Uint8Array, length?: number): AddCronRequest { decode(input: _m0.Reader | Uint8Array, length?: number): AddCronRequest {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length; let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseAddCronRequest(); const message = createBaseAddCronRequest();
while (reader.pos < end) { while (reader.pos < end) {
const tag = reader.uint32(); const tag = reader.uint32();
switch (tag >>> 3) { switch (tag >>> 3) {
case 1: { case 1:
if (tag !== 10) { if (tag !== 10) {
break; break;
} }
message.crons.push(ICron.decode(reader, reader.uint32())); message.crons.push(ICron.decode(reader, reader.uint32()));
continue; continue;
}
} }
if ((tag & 7) === 4 || tag === 0) { if ((tag & 7) === 4 || tag === 0) {
break; break;
} }
reader.skip(tag & 7); reader.skipType(tag & 7);
} }
return message; return message;
}, },
fromJSON(object: any): AddCronRequest { fromJSON(object: any): AddCronRequest {
return { crons: globalThis.Array.isArray(object?.crons) ? object.crons.map((e: any) => ICron.fromJSON(e)) : [] }; return { crons: Array.isArray(object?.crons) ? object.crons.map((e: any) => ICron.fromJSON(e)) : [] };
}, },
toJSON(message: AddCronRequest): unknown { toJSON(message: AddCronRequest): unknown {
const obj: any = {}; const obj: any = {};
if (message.crons?.length) { if (message.crons) {
obj.crons = message.crons.map((e) => ICron.toJSON(e)); obj.crons = message.crons.map((e) => e ? ICron.toJSON(e) : undefined);
} else {
obj.crons = [];
} }
return obj; return obj;
}, },
create<I extends Exact<DeepPartial<AddCronRequest>, I>>(base?: I): AddCronRequest { create<I extends Exact<DeepPartial<AddCronRequest>, I>>(base?: I): AddCronRequest {
return AddCronRequest.fromPartial(base ?? ({} as any)); return AddCronRequest.fromPartial(base ?? {});
}, },
fromPartial<I extends Exact<DeepPartial<AddCronRequest>, I>>(object: I): AddCronRequest { fromPartial<I extends Exact<DeepPartial<AddCronRequest>, I>>(object: I): AddCronRequest {
const message = createBaseAddCronRequest(); const message = createBaseAddCronRequest();
message.crons = object.crons?.map((e) => ICron.fromPartial(e)) || []; message.crons = object.crons?.map((e) => ICron.fromPartial(e)) || [];
@@ -293,13 +277,13 @@ function createBaseAddCronResponse(): AddCronResponse {
return {}; return {};
} }
export const AddCronResponse: MessageFns<AddCronResponse> = { export const AddCronResponse = {
encode(_: AddCronResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { encode(_: AddCronResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
return writer; return writer;
}, },
decode(input: BinaryReader | Uint8Array, length?: number): AddCronResponse { decode(input: _m0.Reader | Uint8Array, length?: number): AddCronResponse {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length; let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseAddCronResponse(); const message = createBaseAddCronResponse();
while (reader.pos < end) { while (reader.pos < end) {
@@ -309,7 +293,7 @@ export const AddCronResponse: MessageFns<AddCronResponse> = {
if ((tag & 7) === 4 || tag === 0) { if ((tag & 7) === 4 || tag === 0) {
break; break;
} }
reader.skip(tag & 7); reader.skipType(tag & 7);
} }
return message; return message;
}, },
@@ -324,8 +308,9 @@ export const AddCronResponse: MessageFns<AddCronResponse> = {
}, },
create<I extends Exact<DeepPartial<AddCronResponse>, I>>(base?: I): AddCronResponse { create<I extends Exact<DeepPartial<AddCronResponse>, I>>(base?: I): AddCronResponse {
return AddCronResponse.fromPartial(base ?? ({} as any)); return AddCronResponse.fromPartial(base ?? {});
}, },
fromPartial<I extends Exact<DeepPartial<AddCronResponse>, I>>(_: I): AddCronResponse { fromPartial<I extends Exact<DeepPartial<AddCronResponse>, I>>(_: I): AddCronResponse {
const message = createBaseAddCronResponse(); const message = createBaseAddCronResponse();
return message; return message;
@@ -336,53 +321,55 @@ function createBaseDeleteCronRequest(): DeleteCronRequest {
return { ids: [] }; return { ids: [] };
} }
export const DeleteCronRequest: MessageFns<DeleteCronRequest> = { export const DeleteCronRequest = {
encode(message: DeleteCronRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { encode(message: DeleteCronRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
for (const v of message.ids) { for (const v of message.ids) {
writer.uint32(10).string(v!); writer.uint32(10).string(v!);
} }
return writer; return writer;
}, },
decode(input: BinaryReader | Uint8Array, length?: number): DeleteCronRequest { decode(input: _m0.Reader | Uint8Array, length?: number): DeleteCronRequest {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length; let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseDeleteCronRequest(); const message = createBaseDeleteCronRequest();
while (reader.pos < end) { while (reader.pos < end) {
const tag = reader.uint32(); const tag = reader.uint32();
switch (tag >>> 3) { switch (tag >>> 3) {
case 1: { case 1:
if (tag !== 10) { if (tag !== 10) {
break; break;
} }
message.ids.push(reader.string()); message.ids.push(reader.string());
continue; continue;
}
} }
if ((tag & 7) === 4 || tag === 0) { if ((tag & 7) === 4 || tag === 0) {
break; break;
} }
reader.skip(tag & 7); reader.skipType(tag & 7);
} }
return message; return message;
}, },
fromJSON(object: any): DeleteCronRequest { fromJSON(object: any): DeleteCronRequest {
return { ids: globalThis.Array.isArray(object?.ids) ? object.ids.map((e: any) => globalThis.String(e)) : [] }; return { ids: Array.isArray(object?.ids) ? object.ids.map((e: any) => String(e)) : [] };
}, },
toJSON(message: DeleteCronRequest): unknown { toJSON(message: DeleteCronRequest): unknown {
const obj: any = {}; const obj: any = {};
if (message.ids?.length) { if (message.ids) {
obj.ids = message.ids; obj.ids = message.ids.map((e) => e);
} else {
obj.ids = [];
} }
return obj; return obj;
}, },
create<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(base?: I): DeleteCronRequest { create<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(base?: I): DeleteCronRequest {
return DeleteCronRequest.fromPartial(base ?? ({} as any)); return DeleteCronRequest.fromPartial(base ?? {});
}, },
fromPartial<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(object: I): DeleteCronRequest { fromPartial<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(object: I): DeleteCronRequest {
const message = createBaseDeleteCronRequest(); const message = createBaseDeleteCronRequest();
message.ids = object.ids?.map((e) => e) || []; message.ids = object.ids?.map((e) => e) || [];
@@ -394,13 +381,13 @@ function createBaseDeleteCronResponse(): DeleteCronResponse {
return {}; return {};
} }
export const DeleteCronResponse: MessageFns<DeleteCronResponse> = { export const DeleteCronResponse = {
encode(_: DeleteCronResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { encode(_: DeleteCronResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
return writer; return writer;
}, },
decode(input: BinaryReader | Uint8Array, length?: number): DeleteCronResponse { decode(input: _m0.Reader | Uint8Array, length?: number): DeleteCronResponse {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length; let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseDeleteCronResponse(); const message = createBaseDeleteCronResponse();
while (reader.pos < end) { while (reader.pos < end) {
@@ -410,7 +397,7 @@ export const DeleteCronResponse: MessageFns<DeleteCronResponse> = {
if ((tag & 7) === 4 || tag === 0) { if ((tag & 7) === 4 || tag === 0) {
break; break;
} }
reader.skip(tag & 7); reader.skipType(tag & 7);
} }
return message; return message;
}, },
@@ -425,8 +412,9 @@ export const DeleteCronResponse: MessageFns<DeleteCronResponse> = {
}, },
create<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(base?: I): DeleteCronResponse { create<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(base?: I): DeleteCronResponse {
return DeleteCronResponse.fromPartial(base ?? ({} as any)); return DeleteCronResponse.fromPartial(base ?? {});
}, },
fromPartial<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(_: I): DeleteCronResponse { fromPartial<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(_: I): DeleteCronResponse {
const message = createBaseDeleteCronResponse(); const message = createBaseDeleteCronResponse();
return message; return message;
@@ -496,14 +484,12 @@ export interface CronClient extends Client {
export const CronClient = makeGenericClientConstructor(CronService, "com.ql.cron.Cron") as unknown as { export const CronClient = makeGenericClientConstructor(CronService, "com.ql.cron.Cron") as unknown as {
new (address: string, credentials: ChannelCredentials, options?: Partial<ClientOptions>): CronClient; new (address: string, credentials: ChannelCredentials, options?: Partial<ClientOptions>): CronClient;
service: typeof CronService; service: typeof CronService;
serviceName: string;
}; };
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
export type DeepPartial<T> = T extends Builtin ? T export type DeepPartial<T> = T extends Builtin ? T
: T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>> : T extends Array<infer U> ? Array<DeepPartial<U>> : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
: T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> } : T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>; : Partial<T>;
@@ -514,12 +500,3 @@ export type Exact<P, I extends P> = P extends Builtin ? P
function isSet(value: any): boolean { function isSet(value: any): boolean {
return value !== null && value !== undefined; 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;
}
+29 -50
View File
@@ -1,25 +1,19 @@
// 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 */ /* eslint-disable */
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
import { import {
type CallOptions, CallOptions,
ChannelCredentials, ChannelCredentials,
Client, Client,
type ClientOptions, ClientOptions,
type ClientReadableStream, ClientReadableStream,
type ClientUnaryCall, ClientUnaryCall,
type handleServerStreamingCall, handleServerStreamingCall,
type handleUnaryCall, handleUnaryCall,
makeGenericClientConstructor, makeGenericClientConstructor,
Metadata, Metadata,
type ServiceError, ServiceError,
type UntypedServiceImplementation, UntypedServiceImplementation,
} from "@grpc/grpc-js"; } from "@grpc/grpc-js";
import _m0 from "protobufjs/minimal";
export const protobufPackage = "com.ql.health"; export const protobufPackage = "com.ql.health";
@@ -80,53 +74,51 @@ function createBaseHealthCheckRequest(): HealthCheckRequest {
return { service: "" }; return { service: "" };
} }
export const HealthCheckRequest: MessageFns<HealthCheckRequest> = { export const HealthCheckRequest = {
encode(message: HealthCheckRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { encode(message: HealthCheckRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.service !== "") { if (message.service !== "") {
writer.uint32(10).string(message.service); writer.uint32(10).string(message.service);
} }
return writer; return writer;
}, },
decode(input: BinaryReader | Uint8Array, length?: number): HealthCheckRequest { decode(input: _m0.Reader | Uint8Array, length?: number): HealthCheckRequest {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length; let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseHealthCheckRequest(); const message = createBaseHealthCheckRequest();
while (reader.pos < end) { while (reader.pos < end) {
const tag = reader.uint32(); const tag = reader.uint32();
switch (tag >>> 3) { switch (tag >>> 3) {
case 1: { case 1:
if (tag !== 10) { if (tag !== 10) {
break; break;
} }
message.service = reader.string(); message.service = reader.string();
continue; continue;
}
} }
if ((tag & 7) === 4 || tag === 0) { if ((tag & 7) === 4 || tag === 0) {
break; break;
} }
reader.skip(tag & 7); reader.skipType(tag & 7);
} }
return message; return message;
}, },
fromJSON(object: any): HealthCheckRequest { fromJSON(object: any): HealthCheckRequest {
return { service: isSet(object.service) ? globalThis.String(object.service) : "" }; return { service: isSet(object.service) ? String(object.service) : "" };
}, },
toJSON(message: HealthCheckRequest): unknown { toJSON(message: HealthCheckRequest): unknown {
const obj: any = {}; const obj: any = {};
if (message.service !== "") { message.service !== undefined && (obj.service = message.service);
obj.service = message.service;
}
return obj; return obj;
}, },
create<I extends Exact<DeepPartial<HealthCheckRequest>, I>>(base?: I): HealthCheckRequest { create<I extends Exact<DeepPartial<HealthCheckRequest>, I>>(base?: I): HealthCheckRequest {
return HealthCheckRequest.fromPartial(base ?? ({} as any)); return HealthCheckRequest.fromPartial(base ?? {});
}, },
fromPartial<I extends Exact<DeepPartial<HealthCheckRequest>, I>>(object: I): HealthCheckRequest { fromPartial<I extends Exact<DeepPartial<HealthCheckRequest>, I>>(object: I): HealthCheckRequest {
const message = createBaseHealthCheckRequest(); const message = createBaseHealthCheckRequest();
message.service = object.service ?? ""; message.service = object.service ?? "";
@@ -138,34 +130,33 @@ function createBaseHealthCheckResponse(): HealthCheckResponse {
return { status: 0 }; return { status: 0 };
} }
export const HealthCheckResponse: MessageFns<HealthCheckResponse> = { export const HealthCheckResponse = {
encode(message: HealthCheckResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { encode(message: HealthCheckResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.status !== 0) { if (message.status !== 0) {
writer.uint32(8).int32(message.status); writer.uint32(8).int32(message.status);
} }
return writer; return writer;
}, },
decode(input: BinaryReader | Uint8Array, length?: number): HealthCheckResponse { decode(input: _m0.Reader | Uint8Array, length?: number): HealthCheckResponse {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length; let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseHealthCheckResponse(); const message = createBaseHealthCheckResponse();
while (reader.pos < end) { while (reader.pos < end) {
const tag = reader.uint32(); const tag = reader.uint32();
switch (tag >>> 3) { switch (tag >>> 3) {
case 1: { case 1:
if (tag !== 8) { if (tag !== 8) {
break; break;
} }
message.status = reader.int32() as any; message.status = reader.int32() as any;
continue; continue;
}
} }
if ((tag & 7) === 4 || tag === 0) { if ((tag & 7) === 4 || tag === 0) {
break; break;
} }
reader.skip(tag & 7); reader.skipType(tag & 7);
} }
return message; return message;
}, },
@@ -176,15 +167,14 @@ export const HealthCheckResponse: MessageFns<HealthCheckResponse> = {
toJSON(message: HealthCheckResponse): unknown { toJSON(message: HealthCheckResponse): unknown {
const obj: any = {}; const obj: any = {};
if (message.status !== 0) { message.status !== undefined && (obj.status = healthCheckResponse_ServingStatusToJSON(message.status));
obj.status = healthCheckResponse_ServingStatusToJSON(message.status);
}
return obj; return obj;
}, },
create<I extends Exact<DeepPartial<HealthCheckResponse>, I>>(base?: I): HealthCheckResponse { create<I extends Exact<DeepPartial<HealthCheckResponse>, I>>(base?: I): HealthCheckResponse {
return HealthCheckResponse.fromPartial(base ?? ({} as any)); return HealthCheckResponse.fromPartial(base ?? {});
}, },
fromPartial<I extends Exact<DeepPartial<HealthCheckResponse>, I>>(object: I): HealthCheckResponse { fromPartial<I extends Exact<DeepPartial<HealthCheckResponse>, I>>(object: I): HealthCheckResponse {
const message = createBaseHealthCheckResponse(); const message = createBaseHealthCheckResponse();
message.status = object.status ?? 0; message.status = object.status ?? 0;
@@ -246,14 +236,12 @@ export interface HealthClient extends Client {
export const HealthClient = makeGenericClientConstructor(HealthService, "com.ql.health.Health") as unknown as { export const HealthClient = makeGenericClientConstructor(HealthService, "com.ql.health.Health") as unknown as {
new (address: string, credentials: ChannelCredentials, options?: Partial<ClientOptions>): HealthClient; new (address: string, credentials: ChannelCredentials, options?: Partial<ClientOptions>): HealthClient;
service: typeof HealthService; service: typeof HealthService;
serviceName: string;
}; };
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
export type DeepPartial<T> = T extends Builtin ? T export type DeepPartial<T> = T extends Builtin ? T
: T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>> : T extends Array<infer U> ? Array<DeepPartial<U>> : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
: T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> } : T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>; : Partial<T>;
@@ -264,12 +252,3 @@ export type Exact<P, I extends P> = P extends Builtin ? P
function isSet(value: any): boolean { function isSet(value: any): boolean {
return value !== null && value !== undefined; 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;
}
+1
View File
@@ -22,6 +22,7 @@ app.get('/api/health', (req, res) => {
app app
.listen(config.publicPort, '0.0.0.0', async () => { .listen(config.publicPort, '0.0.0.0', async () => {
await require('./loaders/sentry').default({ expressApp: app });
await require('./loaders/db').default(); await require('./loaders/db').default();
Logger.debug(`✌️ 公共服务启动成功!`); Logger.debug(`✌️ 公共服务启动成功!`);
+12 -12
View File
@@ -10,7 +10,7 @@ const addCron = (
callback: sendUnaryData<AddCronResponse>, callback: sendUnaryData<AddCronResponse>,
) => { ) => {
for (const item of call.request.crons) { for (const item of call.request.crons) {
const { id, schedule, command, extra_schedules, name } = item; const { id, schedule, command, extraSchedules, name } = item;
if (scheduleStacks.has(id)) { if (scheduleStacks.has(id)) {
scheduleStacks.get(id)?.forEach((x) => x.cancel()); scheduleStacks.get(id)?.forEach((x) => x.cancel());
} }
@@ -23,8 +23,8 @@ const addCron = (
command, command,
); );
if (extra_schedules?.length) { if (extraSchedules?.length) {
extra_schedules.forEach((x) => { extraSchedules.forEach(x => {
Logger.info( Logger.info(
'[schedule][创建定时任务], 任务ID: %s, 名称: %s, cron: %s, 执行命令: %s', '[schedule][创建定时任务], 任务ID: %s, 名称: %s, cron: %s, 执行命令: %s',
id, id,
@@ -32,21 +32,21 @@ const addCron = (
x.schedule, x.schedule,
command, command,
); );
}); })
} }
scheduleStacks.set(id, [ scheduleStacks.set(id, [
nodeSchedule.scheduleJob(id, schedule, async () => { nodeSchedule.scheduleJob(id, schedule, async () => {
Logger.info(`[schedule][准备运行任务] 命令: ${command}`); Logger.info(`[schedule][准备运行任务] 命令: ${command}`);
runCron(command, item); runCron(command, { name, schedule, extraSchedules });
}), }),
...(extra_schedules?.length ...(extraSchedules?.length
? extra_schedules.map((x) => ? extraSchedules.map((x) =>
nodeSchedule.scheduleJob(id, x.schedule, async () => { nodeSchedule.scheduleJob(id, x.schedule, async () => {
Logger.info(`[schedule][准备运行任务] 命令: ${command}`); Logger.info(`[schedule][准备运行任务] 命令: ${command}`);
runCron(command, item); runCron(command, { name, schedule, extraSchedules });
}), }),
) )
: []), : []),
]); ]);
} }
-275
View File
@@ -1,275 +0,0 @@
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);
}
};
+1 -3
View File
@@ -6,13 +6,10 @@ import { HealthService } from '../protos/health';
import { check } from './health'; import { check } from './health';
import config from '../config'; import config from '../config';
import Logger from '../loaders/logger'; import Logger from '../loaders/logger';
import { ApiService } from '../protos/api';
import * as Api from './api';
const server = new Server({ 'grpc.enable_http_proxy': 0 }); const server = new Server({ 'grpc.enable_http_proxy': 0 });
server.addService(HealthService, { check }); server.addService(HealthService, { check });
server.addService(CronService, { addCron, delCron }); server.addService(CronService, { addCron, delCron });
server.addService(ApiService, Api);
server.bindAsync( server.bindAsync(
`0.0.0.0:${config.cronPort}`, `0.0.0.0:${config.cronPort}`,
ServerCredentials.createInsecure(), ServerCredentials.createInsecure(),
@@ -20,6 +17,7 @@ server.bindAsync(
if (err) { if (err) {
throw err; throw err;
} }
server.start();
Logger.debug(`✌️ 定时服务启动成功!`); Logger.debug(`✌️ 定时服务启动成功!`);
console.debug(`✌️ 定时服务启动成功!`); console.debug(`✌️ 定时服务启动成功!`);
process.send?.('ready'); process.send?.('ready');
+2 -14
View File
@@ -11,21 +11,9 @@ export default class ConfigService {
public async getFile(filePath: string, res: Response) { public async getFile(filePath: string, res: Response) {
let content = ''; let content = '';
const avaliablePath = [config.rootPath, config.configPath].map((x) => if (config.blackFileList.includes(filePath) || !filePath) {
path.resolve(x, filePath), res.send({ code: 403, message: '文件无法访问' });
);
if (
config.blackFileList.includes(filePath) ||
avaliablePath.every(
(x) =>
!x.startsWith(config.scriptPath) && !x.startsWith(config.configPath),
) ||
!filePath
) {
return res.send({ code: 403, message: '文件无法访问' });
} }
if (filePath.startsWith('sample/')) { if (filePath.startsWith('sample/')) {
const res = await got.get( const res = await got.get(
`https://gitlab.com/whyour/qinglong/-/raw/master/${filePath}`, `https://gitlab.com/whyour/qinglong/-/raw/master/${filePath}`,
+33 -99
View File
@@ -12,7 +12,7 @@ import {
getUniqPath, getUniqPath,
safeJSONParse, safeJSONParse,
} from '../config/util'; } from '../config/util';
import { Op, where, col as colFn, FindOptions, fn, Order } from 'sequelize'; import { Op, where, col as colFn, FindOptions, fn } from 'sequelize';
import path from 'path'; import path from 'path';
import { TASK_PREFIX, QL_PREFIX } from '../config/const'; import { TASK_PREFIX, QL_PREFIX } from '../config/const';
import cronClient from '../schedule/client'; import cronClient from '../schedule/client';
@@ -21,8 +21,6 @@ import { spawn } from 'cross-spawn';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import pickBy from 'lodash/pickBy'; import pickBy from 'lodash/pickBy';
import omit from 'lodash/omit'; import omit from 'lodash/omit';
import { writeFileWithLock } from '../shared/utils';
import { ScheduleType } from '../interface/schedule';
@Service() @Service()
export default class CronService { export default class CronService {
@@ -36,36 +34,22 @@ export default class CronService {
return false; 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> { public async create(payload: Crontab): Promise<Crontab> {
const tab = new Crontab(payload); const tab = new Crontab(payload);
tab.saved = false; tab.saved = false;
const doc = await this.insert(tab); const doc = await this.insert(tab);
if (this.isNodeCron(doc)) {
if (this.isNodeCron(doc) && !this.isSpecialSchedule(doc.schedule)) {
await cronClient.addCron([ await cronClient.addCron([
{ {
name: doc.name || '', name: doc.name || '',
id: String(doc.id), id: String(doc.id),
schedule: doc.schedule!, schedule: doc.schedule!,
command: this.makeCommand(doc), command: this.makeCommand(doc),
extra_schedules: doc.extra_schedules || [], extraSchedules: doc.extra_schedules || [],
}, },
]); ]);
} }
await this.set_crontab();
await this.setCrontab();
return doc; return doc;
} }
@@ -73,33 +57,29 @@ export default class CronService {
return await CrontabModel.create(payload, { returning: true }); return await CrontabModel.create(payload, { returning: true });
} }
public async update(payload: Partial<Crontab>): Promise<Crontab> { public async update(payload: Crontab): Promise<Crontab> {
const doc = await this.getDb({ id: payload.id }); const doc = await this.getDb({ id: payload.id });
const tab = new Crontab({ ...doc, ...payload }); const tab = new Crontab({ ...doc, ...payload });
tab.saved = false; tab.saved = false;
const newDoc = await this.updateDb(tab); const newDoc = await this.updateDb(tab);
if (doc.isDisabled === 1) { if (doc.isDisabled === 1) {
return newDoc; return newDoc;
} }
if (this.isNodeCron(doc)) { if (this.isNodeCron(doc)) {
await cronClient.delCron([String(doc.id)]); await cronClient.delCron([String(doc.id)]);
} }
if (this.isNodeCron(newDoc)) {
if (this.isNodeCron(newDoc) && !this.isSpecialSchedule(newDoc.schedule)) {
await cronClient.addCron([ await cronClient.addCron([
{ {
name: doc.name || '', name: doc.name || '',
id: String(newDoc.id), id: String(newDoc.id),
schedule: newDoc.schedule!, schedule: newDoc.schedule!,
command: this.makeCommand(newDoc), command: this.makeCommand(newDoc),
extra_schedules: newDoc.extra_schedules || [], extraSchedules: newDoc.extra_schedules || [],
}, },
]); ]);
} }
await this.set_crontab();
await this.setCrontab();
return newDoc; return newDoc;
} }
@@ -154,7 +134,7 @@ export default class CronService {
public async remove(ids: number[]) { public async remove(ids: number[]) {
await CrontabModel.destroy({ where: { id: ids } }); await CrontabModel.destroy({ where: { id: ids } });
await cronClient.delCron(ids.map(String)); await cronClient.delCron(ids.map(String));
await this.setCrontab(); await this.set_crontab();
} }
public async pin(ids: number[]) { public async pin(ids: number[]) {
@@ -198,8 +178,8 @@ export default class CronService {
for (const col of viewQuery.filters) { for (const col of viewQuery.filters) {
const { property, value, operation } = col; const { property, value, operation } = col;
let q: any = {}; let q: any = {};
let operate2: any = null; let operate2 = null;
let operate: any = null; let operate = null;
switch (operation) { switch (operation) {
case 'Reg': case 'Reg':
operate = Op.like; operate = Op.like;
@@ -222,16 +202,9 @@ export default class CronService {
case 'Nin': case 'Nin':
q[Op.and] = [ q[Op.and] = [
{ {
[Op.or]: [ [property]: {
{ [Op.notIn]: Array.isArray(value) ? value : [value],
[property]: { },
[Op.notIn]: Array.isArray(value) ? value : [value],
},
},
{
[property]: { [Op.is]: null },
},
],
}, },
property === 'status' && value.includes(2) property === 'status' && value.includes(2)
? { isDisabled: { [Op.ne]: 1 } } ? { isDisabled: { [Op.ne]: 1 } }
@@ -346,10 +319,10 @@ export default class CronService {
log_path, log_path,
}: { }: {
log_path: string; log_path: string;
}): Promise<Crontab | undefined> { }): Promise<Crontab | null> {
try { try {
const result = await CrontabModel.findOne({ where: { log_path } }); const result = await CrontabModel.findOne({ where: { log_path } });
return result?.get({ plain: true }); return result;
} catch (error) { } catch (error) {
throw error; throw error;
} }
@@ -389,9 +362,9 @@ export default class CronService {
order.unshift([field, type]); order.unshift([field, type]);
} }
} }
let condition: FindOptions<Crontab> = { let condition: any = {
where: query, where: query,
order: order as Order, order: order,
}; };
if (page && size) { if (page && size) {
condition.offset = (page - 1) * size; condition.offset = (page - 1) * size;
@@ -400,7 +373,7 @@ export default class CronService {
try { try {
const result = await CrontabModel.findAll(condition); const result = await CrontabModel.findAll(condition);
const count = await CrontabModel.count({ where: query }); const count = await CrontabModel.count({ where: query });
return { data: result.map((x) => x.get({ plain: true })), total: count }; return { data: result, total: count };
} catch (error) { } catch (error) {
throw error; throw error;
} }
@@ -450,7 +423,7 @@ export default class CronService {
name: cron.name, name: cron.name,
command: cron.command, command: cron.command,
schedule: cron.schedule, schedule: cron.schedule,
extra_schedules: cron.extra_schedules, extraSchedules: cron.extra_schedules,
}; };
if (cron.status !== CrontabStatus.queued) { if (cron.status !== CrontabStatus.queued) {
resolve(params); resolve(params);
@@ -458,7 +431,7 @@ export default class CronService {
} }
this.logger.info( this.logger.info(
`[panel][开始执行任务] 参数: ${JSON.stringify(params)}`, `[panel][开始执行任务] 参数 ${JSON.stringify(params)}`,
); );
let { id, command, log_path } = cron; let { id, command, log_path } = cron;
@@ -486,28 +459,13 @@ export default class CronService {
await fs.appendFile(absolutePath, data.toString()); await fs.appendFile(absolutePath, data.toString());
}); });
cp.stderr.on('data', async (data) => { cp.stderr.on('data', async (data) => {
this.logger.info(
'[panel][执行任务失败] 命令: %s, 错误信息: %j',
command,
data.toString(),
);
await fs.appendFile(absolutePath, data.toString()); await fs.appendFile(absolutePath, data.toString());
}); });
cp.on('error', async (err) => { cp.on('error', async (err) => {
this.logger.error(
'[panel][创建任务失败] 命令: %s, 错误信息: %j',
command,
err,
);
await fs.appendFile(absolutePath, JSON.stringify(err)); await fs.appendFile(absolutePath, JSON.stringify(err));
}); });
cp.on('exit', async (code) => { cp.on('exit', async (code) => {
this.logger.info(
'[panel][执行任务结束] 参数: %s, 退出码: %j',
JSON.stringify(params),
code,
);
await CrontabModel.update( await CrontabModel.update(
{ status: CrontabStatus.idle, pid: undefined }, { status: CrontabStatus.idle, pid: undefined },
{ where: { id } }, { where: { id } },
@@ -521,7 +479,7 @@ export default class CronService {
public async disabled(ids: number[]) { public async disabled(ids: number[]) {
await CrontabModel.update({ isDisabled: 1 }, { where: { id: ids } }); await CrontabModel.update({ isDisabled: 1 }, { where: { id: ids } });
await cronClient.delCron(ids.map(String)); await cronClient.delCron(ids.map(String));
await this.setCrontab(); await this.set_crontab();
} }
public async enabled(ids: number[]) { public async enabled(ids: number[]) {
@@ -534,10 +492,10 @@ export default class CronService {
id: String(doc.id), id: String(doc.id),
schedule: doc.schedule!, schedule: doc.schedule!,
command: this.makeCommand(doc), command: this.makeCommand(doc),
extra_schedules: doc.extra_schedules || [], extraSchedules: doc.extra_schedules || [],
})); }));
await cronClient.addCron(sixCron); await cronClient.addCron(sixCron);
await this.setCrontab(); await this.set_crontab();
} }
public async log(id: number) { public async log(id: number) {
@@ -591,13 +549,11 @@ export default class CronService {
if (tab.task_before) { if (tab.task_before) {
commandVariable += `task_before='${tab.task_before commandVariable += `task_before='${tab.task_before
.replace(/'/g, "'\\''") .replace(/'/g, "'\\''")
.replace(/;? *\n/g, ';')
.trim()}' `; .trim()}' `;
} }
if (tab.task_after) { if (tab.task_after) {
commandVariable += `task_after='${tab.task_after commandVariable += `task_after='${tab.task_after
.replace(/'/g, "'\\''") .replace(/'/g, "'\\''")
.replace(/;? *\n/g, ';')
.trim()}' `; .trim()}' `;
} }
@@ -605,7 +561,7 @@ export default class CronService {
return crontab_job_string; return crontab_job_string;
} }
private async setCrontab(data?: { data: Crontab[]; total: number }) { private async set_crontab(data?: { data: Crontab[]; total: number }) {
const tabs = data ?? (await this.crontabs()); const tabs = data ?? (await this.crontabs());
var crontab_string = ''; var crontab_string = '';
tabs.data.forEach((tab) => { tabs.data.forEach((tab) => {
@@ -613,8 +569,7 @@ export default class CronService {
if ( if (
tab.isDisabled === 1 || tab.isDisabled === 1 ||
_schedule!.length !== 5 || _schedule!.length !== 5 ||
tab.extra_schedules?.length || tab.extra_schedules?.length
this.isSpecialSchedule(tab.schedule)
) { ) {
crontab_string += '# '; crontab_string += '# ';
crontab_string += tab.schedule; crontab_string += tab.schedule;
@@ -629,13 +584,13 @@ export default class CronService {
} }
}); });
await writeFileWithLock(config.crontabFile, crontab_string); await fs.writeFile(config.crontabFile, crontab_string);
execSync(`crontab ${config.crontabFile}`); execSync(`crontab ${config.crontabFile}`);
await CrontabModel.update({ saved: true }, { where: {} }); await CrontabModel.update({ saved: true }, { where: {} });
} }
public importCrontab() { public import_crontab() {
exec('crontab -l', (error, stdout, stderr) => { exec('crontab -l', (error, stdout, stderr) => {
const lines = stdout.split('\n'); const lines = stdout.split('\n');
const namePrefix = new Date().getTime(); const namePrefix = new Date().getTime();
@@ -671,38 +626,17 @@ export default class CronService {
public async autosave_crontab() { public async autosave_crontab() {
const tabs = await this.crontabs(); const tabs = await this.crontabs();
this.setCrontab(tabs); this.set_crontab(tabs);
const regularCrons = tabs.data const sixCron = tabs.data
.filter( .filter((x) => this.isNodeCron(x) && x.isDisabled !== 1)
(x) =>
this.isNodeCron(x) &&
x.isDisabled !== 1 &&
!this.isSpecialSchedule(x.schedule),
)
.map((doc) => ({ .map((doc) => ({
name: doc.name || '', name: doc.name || '',
id: String(doc.id), id: String(doc.id),
schedule: doc.schedule!, schedule: doc.schedule!,
command: this.makeCommand(doc), command: this.makeCommand(doc),
extra_schedules: doc.extra_schedules || [], extraSchedules: doc.extra_schedules || [],
})); }));
await cronClient.addCron(regularCrons); await cronClient.addCron(sixCron);
}
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!);
}
}
} }
} }
+1 -1
View File
@@ -353,7 +353,7 @@ export default class DependenceService {
}); });
this.updateLog(depIds, message); this.updateLog(depIds, message);
let status: number; let status = null;
if (isSucceed) { if (isSucceed) {
status = isInstall status = isInstall
? DependenceStatus.installed ? DependenceStatus.installed
+15 -21
View File
@@ -13,7 +13,6 @@ import {
} from '../data/env'; } from '../data/env';
import groupBy from 'lodash/groupBy'; import groupBy from 'lodash/groupBy';
import { FindOptions, Op } from 'sequelize'; import { FindOptions, Op } from 'sequelize';
import { writeFileWithLock } from '../shared/utils';
@Service() @Service()
export default class EnvService { export default class EnvService {
@@ -41,7 +40,7 @@ export default class EnvService {
} }
public async insert(payloads: Env[]): Promise<Env[]> { public async insert(payloads: Env[]): Promise<Env[]> {
const result: Env[] = []; const result = [];
for (const env of payloads) { for (const env of payloads) {
const doc = await EnvModel.create(env, { returning: true }); const doc = await EnvModel.create(env, { returning: true });
result.push(doc); result.push(doc);
@@ -62,7 +61,7 @@ export default class EnvService {
return await this.getDb({ id: payload.id }); return await this.getDb({ id: payload.id });
} }
public async remove(ids: number[]) { public async remove(ids: string[]) {
await EnvModel.destroy({ where: { id: ids } }); await EnvModel.destroy({ where: { id: ids } });
await this.set_envs(); await this.set_envs();
} }
@@ -150,7 +149,7 @@ export default class EnvService {
['position', 'DESC'], ['position', 'DESC'],
['createdAt', 'ASC'], ['createdAt', 'ASC'],
]); ]);
return result; return result as any;
} catch (error) { } catch (error) {
throw error; throw error;
} }
@@ -161,7 +160,7 @@ export default class EnvService {
where: { ...query }, where: { ...query },
order: [...sort], order: [...sort],
}); });
return docs.map((x) => x.get({ plain: true })); return docs;
} }
public async getDb(query: FindOptions<Env>['where']): Promise<Env> { public async getDb(query: FindOptions<Env>['where']): Promise<Env> {
@@ -172,7 +171,7 @@ export default class EnvService {
return doc.get({ plain: true }); return doc.get({ plain: true });
} }
public async disabled(ids: number[]) { public async disabled(ids: string[]) {
await EnvModel.update( await EnvModel.update(
{ status: EnvStatus.disabled }, { status: EnvStatus.disabled },
{ where: { id: ids } }, { where: { id: ids } },
@@ -180,12 +179,12 @@ export default class EnvService {
await this.set_envs(); await this.set_envs();
} }
public async enabled(ids: number[]) { public async enabled(ids: string[]) {
await EnvModel.update({ status: EnvStatus.normal }, { where: { id: ids } }); await EnvModel.update({ status: EnvStatus.normal }, { where: { id: ids } });
await this.set_envs(); await this.set_envs();
} }
public async updateNames({ ids, name }: { ids: number[]; name: string }) { public async updateNames({ ids, name }: { ids: string[]; name: string }) {
await EnvModel.update({ name }, { where: { id: ids } }); await EnvModel.update({ name }, { where: { id: ids } });
await this.set_envs(); await this.set_envs();
} }
@@ -211,23 +210,18 @@ export default class EnvService {
.replace(/'/g, "'\\''") .replace(/'/g, "'\\''")
.trim(); .trim();
env_string += `export ${key}='${value}'\n`; env_string += `export ${key}='${value}'\n`;
const _env_value = `${group const _env_value = `'${group
.map((x) => x.value) .map((x) => x.value)
.join('&') .join('&')
.replace(/\\/g, '\\\\')}`; .replace(/\\/g, '\\\\')
js_env_string += `process.env.${key}=\`${_env_value.replace( .replace(/'/g, "\\'")}'`;
/\`/g, js_env_string += `process.env.${key}=${_env_value};\n`;
'\\`', py_env_string += `os.environ['${key}']=${_env_value}\n`;
)}\`;\n`;
py_env_string += `os.environ['${key}']='''${_env_value.replace(
/\'/g,
"\\'",
)}'''\n`;
} }
} }
} }
await writeFileWithLock(config.envFile, env_string); await fs.writeFile(config.envFile, env_string);
await writeFileWithLock(config.jsEnvFile, js_env_string); await fs.writeFile(config.jsEnvFile, js_env_string);
await writeFileWithLock(config.pyEnvFile, py_env_string); await fs.writeFile(config.pyEnvFile, py_env_string);
} }
} }
+22 -119
View File
@@ -3,6 +3,7 @@ import got from 'got';
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
import nodemailer from 'nodemailer'; import nodemailer from 'nodemailer';
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import winston from 'winston';
import { parseBody, parseHeaders } from '../config/util'; import { parseBody, parseHeaders } from '../config/util';
import { NotificationInfo } from '../data/notify'; import { NotificationInfo } from '../data/notify';
import UserService from './user'; import UserService from './user';
@@ -32,8 +33,6 @@ export default class NotificationService {
['webhook', this.webhook], ['webhook', this.webhook],
['lark', this.lark], ['lark', this.lark],
['chronocat', this.chronocat], ['chronocat', this.chronocat],
['ntfy', this.ntfy],
['wxPusherBot', this.wxPusherBot],
]); ]);
private title = ''; private title = '';
@@ -44,7 +43,7 @@ export default class NotificationService {
retry: 1, retry: 1,
}; };
constructor() {} constructor(@Inject('logger') private logger: winston.Logger) {}
public async notify( public async notify(
title: string, title: string,
@@ -129,19 +128,14 @@ export default class NotificationService {
private async serverChan() { private async serverChan() {
const { serverChanKey } = this.params; const { serverChanKey } = this.params;
const matchResult = serverChanKey.match(/^sctp(\d+)t/i); const url = serverChanKey.startsWith('SCT')
const url = ? `https://sctapi.ftqq.com/${serverChanKey}.send`
matchResult && matchResult[1] : `https://sc.ftqq.com/${serverChanKey}.send`;
? `https://${matchResult[1]}.push.ft07.com/send/${serverChanKey}.send`
: `https://sctapi.ftqq.com/${serverChanKey}.send`;
try { try {
const res: any = await got const res: any = await got
.post(url, { .post(url, {
...this.gotOption, ...this.gotOption,
body: `title=${encodeURIComponent( body: `title=${this.title}&desp=${this.content}`,
this.title,
)}&desp=${encodeURIComponent(this.content)}`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
}) })
.json(); .json();
@@ -182,10 +176,11 @@ export default class NotificationService {
} }
private async chat() { private async chat() {
const { synologyChatUrl } = this.params; const { chatUrl, chatToken } = this.params;
const url = `${chatUrl}${chatToken}`;
try { try {
const res: any = await got const res: any = await got
.post(synologyChatUrl, { .post(url, {
...this.gotOption, ...this.gotOption,
body: `payload={"text":"${this.title}\n${this.content}"}`, body: `payload={"text":"${this.title}\n${this.content}"}`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
@@ -501,33 +496,20 @@ export default class NotificationService {
} }
private async pushPlus() { private async pushPlus() {
const { const { pushPlusToken, pushPlusUser } = this.params;
pushPlusToken,
pushPlusUser,
pushplusWebhook,
pushPlusTemplate,
pushplusChannel,
pushplusCallbackUrl,
pushplusTo,
} = this.params;
const url = `https://www.pushplus.plus/send`; const url = `https://www.pushplus.plus/send`;
try { try {
let body = { const res: any = await got
...this.gotOption, .post(url, {
json: { ...this.gotOption,
token: `${pushPlusToken}`, json: {
title: `${this.title}`, token: `${pushPlusToken}`,
content: `${this.content.replace(/[\n\r]/g, '<br>')}`, title: `${this.title}`,
topic: `${pushPlusUser || ''}`, content: `${this.content.replace(/[\n\r]/g, '<br>')}`,
template: `${pushPlusTemplate || 'html'}`, topic: `${pushPlusUser || ''}`,
channel: `${pushplusChannel || 'wechat'}`, },
webhook: `${pushplusWebhook || ''}`, })
callbackUrl: `${pushplusCallbackUrl || ''}`, .json();
to: `${pushplusTo || ''}`,
},
};
const res: any = await got.post(url, body).json();
if (res.code === 200) { if (res.code === 200) {
return true; return true;
@@ -656,85 +638,6 @@ 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() { private async chronocat() {
const { chronocatURL, chronocatQQ, chronocatToken } = this.params; const { chronocatURL, chronocatQQ, chronocatToken } = this.params;
try { try {
@@ -801,7 +704,7 @@ export default class NotificationService {
webhookContentType, webhookContentType,
} = this.params; } = this.params;
if (!webhookUrl?.includes('$title') && !webhookBody?.includes('$title')) { if (!webhookUrl.includes('$title') && !webhookBody.includes('$title')) {
throw new Error('Url 或者 Body 中必须包含 $title'); throw new Error('Url 或者 Body 中必须包含 $title');
} }
+20 -28
View File
@@ -1,18 +1,19 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import winston from 'winston'; import winston from 'winston';
import { createRandomString } from '../config/util'; import { createRandomString } from '../config/util';
import config from '../config';
import { App, AppModel } from '../data/open'; import { App, AppModel } from '../data/open';
import { v4 as uuidV4 } from 'uuid'; import { v4 as uuidV4 } from 'uuid';
import sequelize, { Op } from 'sequelize'; import sequelize, { Op } from 'sequelize';
import { shareStore } from '../shared/store';
@Service() @Service()
export default class OpenService { export default class OpenService {
constructor(@Inject('logger') private logger: winston.Logger) {} constructor(@Inject('logger') private logger: winston.Logger) {}
public async findApps(): Promise<App[] | null> { public async findTokenByValue(token: string): Promise<App | null> {
const docs = await this.find({}); const docs = await this.find({});
return docs; const doc = docs.filter((x) => x.tokens?.find((y) => y.value === token));
return doc[0];
} }
public async create(payload: App): Promise<App> { public async create(payload: App): Promise<App> {
@@ -20,8 +21,6 @@ export default class OpenService {
tab.client_id = createRandomString(12, 12); tab.client_id = createRandomString(12, 12);
tab.client_secret = createRandomString(24, 24); tab.client_secret = createRandomString(24, 24);
const doc = await this.insert(tab); const doc = await this.insert(tab);
const apps = await this.find({});
await shareStore.updateApps(apps);
return { ...doc, tokens: [] }; return { ...doc, tokens: [] };
} }
@@ -35,19 +34,17 @@ export default class OpenService {
name: payload.name, name: payload.name,
scopes: payload.scopes, scopes: payload.scopes,
id: payload.id, id: payload.id,
} as App); } as any);
return { ...newDoc, tokens: [] }; return { ...newDoc, tokens: [] };
} }
private async updateDb(payload: Partial<App>): Promise<App> { private async updateDb(payload: App): Promise<App> {
await AppModel.update(payload, { where: { id: payload.id } }); await AppModel.update(payload, { where: { id: payload.id } });
const apps = await this.find({}); return await this.getDb({ id: payload.id });
await shareStore.updateApps(apps);
return apps?.find((x) => x.id === payload.id) as App;
} }
public async getDb(query: Record<string, any>): Promise<App> { public async getDb(query: any): Promise<App> {
const doc = await AppModel.findOne({ where: query }); const doc: any = await AppModel.findOne({ where: query });
if (!doc) { if (!doc) {
throw new Error(`App ${JSON.stringify(query)} not found`); throw new Error(`App ${JSON.stringify(query)} not found`);
} }
@@ -56,12 +53,10 @@ export default class OpenService {
public async remove(ids: number[]) { public async remove(ids: number[]) {
await AppModel.destroy({ where: { id: ids } }); await AppModel.destroy({ where: { id: ids } });
const apps = await this.find({});
await shareStore.updateApps(apps);
} }
public async resetSecret(id: number): Promise<App> { public async resetSecret(id: number): Promise<App> {
const tab: Partial<App> = { const tab: any = {
client_secret: createRandomString(24, 24), client_secret: createRandomString(24, 24),
tokens: [], tokens: [],
id, id,
@@ -79,7 +74,7 @@ export default class OpenService {
public async list( public async list(
searchText: string = '', searchText: string = '',
sort: any = {}, sort: any = {},
query: Record<string, any> = {}, query: any = {},
): Promise<App[]> { ): Promise<App[]> {
let condition = { ...query }; let condition = { ...query };
if (searchText) { if (searchText) {
@@ -106,7 +101,7 @@ export default class OpenService {
} }
} }
private async find(query: Record<string, any>, sort?: any): Promise<App[]> { private async find(query: any, sort?: any): Promise<App[]> {
const docs = await AppModel.findAll({ where: { ...query } }); const docs = await AppModel.findAll({ where: { ...query } });
return docs.map((x) => x.get({ plain: true })); return docs.map((x) => x.get({ plain: true }));
} }
@@ -140,8 +135,6 @@ export default class OpenService {
{ tokens }, { tokens },
{ where: { client_id, client_secret } }, { where: { client_id, client_secret } },
); );
const apps = await this.find({});
await shareStore.updateApps(apps);
return { return {
code: 200, code: 200,
data: { data: {
@@ -151,7 +144,7 @@ export default class OpenService {
}, },
}; };
} else { } else {
return { code: 400, message: 'client_idclient_seret 有误' }; return { code: 400, message: 'client_idclient_seret有误' };
} }
} }
@@ -159,15 +152,14 @@ export default class OpenService {
value: string; value: string;
expiration: number; expiration: number;
}> { }> {
const apps = await shareStore.getApps(); let systemApp = (await AppModel.findOne({
const systemApp = apps?.find((x) => x.name === 'system'); where: { name: 'system' },
})) as App;
if (!systemApp) { if (!systemApp) {
throw new Error('system app not found'); systemApp = await this.create({
} name: 'system',
const now = Math.round(Date.now() / 1000); scopes: ['crons', 'system'],
const currentToken = systemApp.tokens?.find((x) => x.expiration > now); } as App);
if (currentToken) {
return currentToken;
} }
const { data } = await this.authToken({ const { data } = await this.authToken({
client_id: systemApp.client_id, client_id: systemApp.client_id,
+10 -44
View File
@@ -17,7 +17,6 @@ export interface ScheduleTaskType {
command: string; command: string;
name?: string; name?: string;
schedule?: string; schedule?: string;
runOrigin: 'subscription' | 'system' | 'script';
} }
export interface TaskCallbacks { export interface TaskCallbacks {
@@ -41,13 +40,9 @@ export default class ScheduleService {
private intervalSchedule = new ToadScheduler(); private intervalSchedule = new ToadScheduler();
private taskLimitMap = { private maxBuffer = 200 * 1024 * 1024;
system: 'runWithSystemLimit' as const,
script: 'runWithScriptLimit' as const,
subscription: 'runWithSubscriptionLimit' as const,
};
constructor(@Inject('logger') private logger: winston.Logger) {} constructor(@Inject('logger') private logger: winston.Logger) { }
async runTask( async runTask(
command: string, command: string,
@@ -56,21 +51,12 @@ export default class ScheduleService {
schedule?: string; schedule?: string;
name?: string; name?: string;
command?: string; command?: string;
id: string;
runOrigin: 'subscription' | 'system' | 'script';
}, },
completionTime: 'start' | 'end' = 'end', completionTime: 'start' | 'end' = 'end',
) { ) {
const { runOrigin, ...others } = params; return taskLimit.runWithCronLimit(() => {
return taskLimit[this.taskLimitMap[runOrigin]](others, () => {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
this.logger.info( this.logger.info(`[panel][开始执行任务] 参数 ${JSON.stringify({ ...params, command })}`);
`[panel][开始执行任务] 参数: ${JSON.stringify({
...others,
command,
})}`,
);
try { try {
const startTime = dayjs(); const startTime = dayjs();
@@ -104,21 +90,13 @@ export default class ScheduleService {
}); });
cp.on('exit', async (code) => { cp.on('exit', async (code) => {
this.logger.info(
'[panel][执行任务结束] 参数: %s, 退出码: %j',
JSON.stringify({
...others,
command,
}),
code,
);
const endTime = dayjs(); const endTime = dayjs();
await callbacks.onEnd?.( await callbacks.onEnd?.(
cp, cp,
endTime, endTime,
endTime.diff(startTime, 'seconds'), endTime.diff(startTime, 'seconds'),
); );
resolve({ ...others, pid: cp.pid, code }); resolve({ ...params, pid: cp.pid, code });
}); });
} catch (error) { } catch (error) {
this.logger.error( this.logger.error(
@@ -133,7 +111,7 @@ export default class ScheduleService {
} }
async createCronTask( async createCronTask(
{ id = 0, command, name, schedule = '', runOrigin }: ScheduleTaskType, { id = 0, command, name, schedule = '' }: ScheduleTaskType,
callbacks?: TaskCallbacks, callbacks?: TaskCallbacks,
runImmediately = false, runImmediately = false,
) { ) {
@@ -153,8 +131,6 @@ export default class ScheduleService {
name, name,
schedule, schedule,
command, command,
id: _id,
runOrigin,
}); });
}), }),
); );
@@ -164,8 +140,6 @@ export default class ScheduleService {
name, name,
schedule, schedule,
command, command,
id: _id,
runOrigin,
}); });
} }
} }
@@ -180,7 +154,7 @@ export default class ScheduleService {
} }
async createIntervalTask( async createIntervalTask(
{ id = 0, command, name = '', runOrigin }: ScheduleTaskType, { id = 0, command, name = '' }: ScheduleTaskType,
schedule: SimpleIntervalSchedule, schedule: SimpleIntervalSchedule,
runImmediately = true, runImmediately = true,
callbacks?: TaskCallbacks, callbacks?: TaskCallbacks,
@@ -198,13 +172,11 @@ export default class ScheduleService {
this.runTask(command, callbacks, { this.runTask(command, callbacks, {
name, name,
command, command,
id: _id,
runOrigin,
}); });
}, },
(err) => { (err) => {
this.logger.error( this.logger.error(
'[panel][执行任务失败] 命令: %s, 错误信息: %j', '[执行任务失败] 命令: %s, 错误信息: %j',
command, command,
err, err,
); );
@@ -214,7 +186,7 @@ export default class ScheduleService {
const job = new LongIntervalJob( const job = new LongIntervalJob(
{ runImmediately: false, ...schedule }, { runImmediately: false, ...schedule },
task, task,
{ id: _id }, _id,
); );
this.intervalSchedule.addIntervalJob(job); this.intervalSchedule.addIntervalJob(job);
@@ -223,19 +195,13 @@ export default class ScheduleService {
this.runTask(command, callbacks, { this.runTask(command, callbacks, {
name, name,
command, command,
id: _id,
runOrigin,
}); });
} }
} }
async cancelIntervalTask({ id = 0, name }: ScheduleTaskType) { async cancelIntervalTask({ id = 0, name }: ScheduleTaskType) {
const _id = this.formatId(id); const _id = this.formatId(id);
this.logger.info( this.logger.info('[取消interval任务], 任务ID: %s, 任务名: %s', _id, name);
'[panel][取消interval任务], 任务ID: %s, 任务名: %s',
_id,
name,
);
this.intervalSchedule.removeById(_id); this.intervalSchedule.removeById(_id);
} }
+5 -9
View File
@@ -7,7 +7,6 @@ import ScheduleService, { TaskCallbacks } from './schedule';
import config from '../config'; import config from '../config';
import { TASK_COMMAND } from '../config/const'; import { TASK_COMMAND } from '../config/const';
import { getFileContentByName, getPid, killTask, rmPath } from '../config/util'; import { getFileContentByName, getPid, killTask, rmPath } from '../config/util';
import taskLimit from '../shared/pLimit';
@Service() @Service()
export default class ScriptService { export default class ScriptService {
@@ -44,7 +43,7 @@ export default class ScriptService {
const pid = await this.scheduleService.runTask( const pid = await this.scheduleService.runTask(
`real_time=true ${command}`, `real_time=true ${command}`,
this.taskCallbacks(filePath), this.taskCallbacks(filePath),
{ command, id: relativePath.replace(/ /g, '-'), runOrigin: 'script' }, { command },
'start', 'start',
); );
@@ -54,7 +53,6 @@ export default class ScriptService {
public async stopScript(filePath: string, pid: number) { public async stopScript(filePath: string, pid: number) {
if (!pid) { if (!pid) {
const relativePath = path.relative(config.scriptPath, filePath); const relativePath = path.relative(config.scriptPath, filePath);
taskLimit.removeQueuedCron(relativePath.replace(/ /g, '-'));
pid = (await getPid(`${TASK_COMMAND} ${relativePath} now`)) as number; pid = (await getPid(`${TASK_COMMAND} ${relativePath} now`)) as number;
} }
try { try {
@@ -65,13 +63,11 @@ export default class ScriptService {
} }
public async getFile(filePath: string, fileName: string) { public async getFile(filePath: string, fileName: string) {
const finalPath = path.resolve(config.scriptPath, filePath, fileName); let _filePath = join(config.scriptPath, filePath, fileName);
if (filePath.startsWith(config.dataPath)) {
if (!finalPath.startsWith(config.scriptPath)) { _filePath = join(filePath, fileName);
return '';
} }
const content = await getFileContentByName(_filePath);
const content = await getFileContentByName(finalPath);
return content; return content;
} }
} }
+12 -17
View File
@@ -7,7 +7,6 @@ import { Subscription } from '../data/subscription';
import { formatUrl } from '../config/subscription'; import { formatUrl } from '../config/subscription';
import config from '../config'; import config from '../config';
import { fileExist, rmPath } from '../config/util'; import { fileExist, rmPath } from '../config/util';
import { writeFileWithLock } from '../shared/utils';
@Service() @Service()
export default class SshKeyService { export default class SshKeyService {
@@ -26,12 +25,13 @@ export default class SshKeyService {
if (_exist) { if (_exist) {
config = await fs.readFile(this.sshConfigFilePath, { encoding: 'utf-8' }); config = await fs.readFile(this.sshConfigFilePath, { encoding: 'utf-8' });
} else { } else {
await writeFileWithLock(this.sshConfigFilePath, ''); await fs.writeFile(this.sshConfigFilePath, '');
} }
if (!config.includes(this.sshConfigHeader)) { if (!config.includes(this.sshConfigHeader)) {
await writeFileWithLock( await fs.writeFile(
this.sshConfigFilePath, this.sshConfigFilePath,
`${this.sshConfigHeader}\n\n${config}`, `${this.sshConfigHeader}\n\n${config}`,
{ encoding: 'utf-8' },
); );
} }
} }
@@ -41,14 +41,10 @@ export default class SshKeyService {
key: string, key: string,
): Promise<void> { ): Promise<void> {
try { try {
await writeFileWithLock( await fs.writeFile(path.join(this.sshPath, alias), `${key}${os.EOL}`, {
path.join(this.sshPath, alias), encoding: 'utf8',
`${key}${os.EOL}`, mode: '400',
{ });
encoding: 'utf8',
mode: '400',
},
);
} catch (error) { } catch (error) {
this.logger.error('生成私钥文件失败', error); this.logger.error('生成私钥文件失败', error);
} }
@@ -78,9 +74,12 @@ export default class SshKeyService {
this.sshPath, this.sshPath,
alias, alias,
)}\n StrictHostKeyChecking no\n${proxyStr}`; )}\n StrictHostKeyChecking no\n${proxyStr}`;
await writeFileWithLock( await fs.writeFile(
`${path.join(this.sshPath, `${alias}.config`)}`, `${path.join(this.sshPath, `${alias}.config`)}`,
config, config,
{
encoding: 'utf8',
},
); );
} }
@@ -103,11 +102,7 @@ export default class SshKeyService {
await this.generateSingleSshConfig(alias, host, proxy); await this.generateSingleSshConfig(alias, host, proxy);
} }
public async removeSSHKey( public async removeSSHKey(alias: string, host: string, proxy?: string): Promise<void> {
alias: string,
host: string,
proxy?: string,
): Promise<void> {
await this.removePrivateKeyFile(alias); await this.removePrivateKeyFile(alias);
await this.removeSshConfig(alias); await this.removeSshConfig(alias);
} }
+9 -19
View File
@@ -3,7 +3,6 @@ import winston from 'winston';
import config from '../config'; import config from '../config';
import { import {
Subscription, Subscription,
SubscriptionInstance,
SubscriptionModel, SubscriptionModel,
SubscriptionStatus, SubscriptionStatus,
} from '../data/subscription'; } from '../data/subscription';
@@ -30,7 +29,6 @@ import { LOG_END_SYMBOL } from '../config/const';
import { formatCommand, formatUrl } from '../config/subscription'; import { formatCommand, formatUrl } from '../config/subscription';
import { CrontabModel } from '../data/cron'; import { CrontabModel } from '../data/cron';
import CrontabService from './cron'; import CrontabService from './cron';
import taskLimit from '../shared/pLimit';
@Service() @Service()
export default class SubscriptionService { export default class SubscriptionService {
@@ -42,12 +40,8 @@ export default class SubscriptionService {
private crontabService: CrontabService, private crontabService: CrontabService,
) {} ) {}
public async list( public async list(searchText?: string): Promise<Subscription[]> {
searchText?: string,
ids?: string,
): Promise<SubscriptionInstance[]> {
let query = {}; let query = {};
const subIds = JSON.parse(ids || '[]');
if (searchText) { if (searchText) {
const reg = { const reg = {
[Op.or]: [ [Op.or]: [
@@ -68,7 +62,7 @@ export default class SubscriptionService {
} }
try { try {
const result = await SubscriptionModel.findAll({ const result = await SubscriptionModel.findAll({
where: { ...query, ...(ids ? { id: subIds } : undefined) }, where: query,
order: [ order: [
['is_disabled', 'ASC'], ['is_disabled', 'ASC'],
['createdAt', 'DESC'], ['createdAt', 'DESC'],
@@ -93,7 +87,7 @@ export default class SubscriptionService {
this.scheduleService.cancelCronTask(doc as any); this.scheduleService.cancelCronTask(doc as any);
needCreate && needCreate &&
(await this.scheduleService.createCronTask( (await this.scheduleService.createCronTask(
{ ...doc, runOrigin: 'subscription' } as any, doc as any,
this.taskCallbacks(doc), this.taskCallbacks(doc),
runImmediately, runImmediately,
)); ));
@@ -102,7 +96,7 @@ export default class SubscriptionService {
const { type, value } = doc.interval_schedule; const { type, value } = doc.interval_schedule;
needCreate && needCreate &&
(await this.scheduleService.createIntervalTask( (await this.scheduleService.createIntervalTask(
{ ...doc, runOrigin: 'subscription' } as any, doc as any,
{ [type]: value } as SimpleIntervalSchedule, { [type]: value } as SimpleIntervalSchedule,
runImmediately, runImmediately,
this.taskCallbacks(doc), this.taskCallbacks(doc),
@@ -208,12 +202,12 @@ export default class SubscriptionService {
public async create(payload: Subscription): Promise<Subscription> { public async create(payload: Subscription): Promise<Subscription> {
const tab = new Subscription(payload); const tab = new Subscription(payload);
const doc = await this.insert(tab); const doc = await this.insert(tab);
await this.handleTask(doc.get({ plain: true })); await this.handleTask(doc);
await this.setSshConfig(); await this.setSshConfig();
return doc; return doc;
} }
public async insert(payload: Subscription): Promise<SubscriptionInstance> { public async insert(payload: Subscription): Promise<Subscription> {
return await SubscriptionModel.create(payload, { returning: true }); return await SubscriptionModel.create(payload, { returning: true });
} }
@@ -265,7 +259,7 @@ export default class SubscriptionService {
public async remove(ids: number[], query: { force?: boolean }) { public async remove(ids: number[], query: { force?: boolean }) {
const docs = await SubscriptionModel.findAll({ where: { id: ids } }); const docs = await SubscriptionModel.findAll({ where: { id: ids } });
for (const doc of docs) { for (const doc of docs) {
await this.handleTask(doc.get({ plain: true }), false); await this.handleTask(doc, false);
} }
await SubscriptionModel.destroy({ where: { id: ids } }); await SubscriptionModel.destroy({ where: { id: ids } });
await this.setSshConfig(); await this.setSshConfig();
@@ -277,9 +271,7 @@ export default class SubscriptionService {
} }
for (const doc of docs) { for (const doc of docs) {
const filePath = join(config.scriptPath, doc.alias); const filePath = join(config.scriptPath, doc.alias);
const repoPath = join(config.repoPath, doc.alias);
await rmPath(filePath); await rmPath(filePath);
await rmPath(repoPath);
} }
} }
} }
@@ -334,8 +326,6 @@ export default class SubscriptionService {
name: subscription.name, name: subscription.name,
schedule: subscription.schedule, schedule: subscription.schedule,
command, command,
id: String(subscription.id),
runOrigin: 'subscription',
}); });
} }
@@ -344,7 +334,7 @@ export default class SubscriptionService {
const docs = await SubscriptionModel.findAll({ where: { id: ids } }); const docs = await SubscriptionModel.findAll({ where: { id: ids } });
await this.setSshConfig(); await this.setSshConfig();
for (const doc of docs) { for (const doc of docs) {
await this.handleTask(doc.get({ plain: true }), false); await this.handleTask(doc, false);
} }
} }
@@ -353,7 +343,7 @@ export default class SubscriptionService {
const docs = await SubscriptionModel.findAll({ where: { id: ids } }); const docs = await SubscriptionModel.findAll({ where: { id: ids } });
await this.setSshConfig(); await this.setSshConfig();
for (const doc of docs) { for (const doc of docs) {
await this.handleTask(doc.get({ plain: true })); await this.handleTask(doc);
} }
} }
+13 -76
View File
@@ -15,8 +15,6 @@ import {
parseVersion, parseVersion,
promiseExec, promiseExec,
readDirs, readDirs,
rmPath,
setSystemTimezone,
} from '../config/util'; } from '../config/util';
import { import {
DependenceModel, DependenceModel,
@@ -35,8 +33,6 @@ import taskLimit from '../shared/pLimit';
import NotificationService from './notify'; import NotificationService from './notify';
import ScheduleService, { TaskCallbacks } from './schedule'; import ScheduleService, { TaskCallbacks } from './schedule';
import SockService from './sock'; import SockService from './sock';
import os from 'os';
import dayjs from 'dayjs';
@Service() @Service()
export default class SystemService { export default class SystemService {
@@ -51,21 +47,17 @@ export default class SystemService {
public async getSystemConfig() { public async getSystemConfig() {
const doc = await this.getDb({ type: AuthDataType.systemConfig }); const doc = await this.getDb({ type: AuthDataType.systemConfig });
return { return doc;
...doc,
info: { ...doc.info, timezone: doc.info?.timezone || 'Asia/Shanghai' },
};
} }
private async updateAuthDb(payload: SystemInfo): Promise<SystemInfo> { private async updateAuthDb(payload: SystemInfo): Promise<SystemInfo> {
const { id, ...others } = payload; await SystemModel.upsert({ ...payload });
await SystemModel.update(others, { where: { id } }); const doc = await this.getDb({ type: payload.type });
const doc = await this.getDb({ id });
return doc; return doc;
} }
public async getDb(query: any): Promise<SystemInfo> { public async getDb(query: any): Promise<SystemInfo> {
const doc = await SystemModel.findOne({ where: query }); const doc = await SystemModel.findOne({ where: { ...query } });
if (!doc) { if (!doc) {
throw new Error(`System ${JSON.stringify(query)} not found`); throw new Error(`System ${JSON.stringify(query)} not found`);
} }
@@ -97,22 +89,17 @@ export default class SystemService {
info: { ...oDoc.info, ...info }, info: { ...oDoc.info, ...info },
}); });
const cron = { const cron = {
id: result.id as number, id: result.id || NaN,
name: '删除日志', name: '删除日志',
command: `ql rmlog ${info.logRemoveFrequency}`, command: `ql rmlog ${info.logRemoveFrequency}`,
runOrigin: 'system' as const,
}; };
if (oDoc.info?.logRemoveFrequency) { if (oDoc.info?.logRemoveFrequency) {
await this.scheduleService.cancelIntervalTask(cron); await this.scheduleService.cancelIntervalTask(cron);
} }
if (info.logRemoveFrequency && info.logRemoveFrequency > 0) { if (info.logRemoveFrequency && info.logRemoveFrequency > 0) {
this.scheduleService.createIntervalTask( this.scheduleService.createIntervalTask(cron, {
cron, days: info.logRemoveFrequency,
{ });
days: info.logRemoveFrequency,
},
true,
);
} }
return { code: 200, data: info }; return { code: 200, data: info };
} }
@@ -188,8 +175,6 @@ export default class SystemService {
}, },
{ {
command, command,
id: 'update-node-mirror',
runOrigin: 'system',
}, },
); );
} }
@@ -220,9 +205,6 @@ export default class SystemService {
}); });
let defaultDomain = 'https://dl-cdn.alpinelinux.org'; let defaultDomain = 'https://dl-cdn.alpinelinux.org';
let targetDomain = 'https://dl-cdn.alpinelinux.org'; let targetDomain = 'https://dl-cdn.alpinelinux.org';
if (os.platform() !== 'linux') {
return;
}
const content = await fs.promises.readFile('/etc/apk/repositories', { const content = await fs.promises.readFile('/etc/apk/repositories', {
encoding: 'utf-8', encoding: 'utf-8',
}); });
@@ -264,8 +246,6 @@ export default class SystemService {
}, },
{ {
command, command,
id: 'update-linux-mirror',
runOrigin: 'system',
}, },
); );
} }
@@ -282,7 +262,7 @@ export default class SystemService {
timeout: 30000, timeout: 30000,
}, },
); );
lastVersionContent = parseContentVersion(result.body); lastVersionContent = await parseContentVersion(result.body);
} catch (error) {} } catch (error) {}
if (!lastVersionContent) { if (!lastVersionContent) {
@@ -377,8 +357,6 @@ export default class SystemService {
} }
this.scheduleService.runTask(`real_time=true ${command}`, callback, { this.scheduleService.runTask(`real_time=true ${command}`, callback, {
command, command,
id: command.replace(/ /g, '-'),
runOrigin: 'system',
}); });
} }
@@ -407,7 +385,7 @@ export default class SystemService {
public async exportData(res: Response) { public async exportData(res: Response) {
try { try {
await promiseExec( await promiseExec(
`cd ${config.dataPath} && cd ../ && tar -zcvf ${config.dataTgzFile} data/`, `cd ${config.rootPath} && tar -zcvf ${config.dataTgzFile} data/`,
); );
res.download(config.dataTgzFile); res.download(config.dataTgzFile);
} catch (error: any) { } catch (error: any) {
@@ -419,7 +397,7 @@ export default class SystemService {
try { try {
await promiseExec(`rm -rf ${path.join(config.tmpPath, 'data')}`); await promiseExec(`rm -rf ${path.join(config.tmpPath, 'data')}`);
const res = await promiseExec( const res = await promiseExec(
`cd ${config.tmpPath} && tar -zxvf ${config.dataTgzFile}`, `cd ${config.tmpPath} && tar -zxvf data.tgz`,
); );
return { code: 200, data: res }; return { code: 200, data: res };
} catch (error: any) { } catch (error: any) {
@@ -427,25 +405,9 @@ export default class SystemService {
} }
} }
public async getSystemLog( public async getSystemLog(res: Response) {
res: Response,
query: {
startTime?: string;
endTime?: string;
},
) {
const startTime = dayjs(query.startTime || undefined)
.startOf('d')
.valueOf();
const endTime = dayjs(query.endTime || undefined)
.endOf('d')
.valueOf();
const result = await readDirs(config.systemLogPath, config.systemLogPath); const result = await readDirs(config.systemLogPath, config.systemLogPath);
const logs = result const logs = result.reverse().filter((x) => x.title.endsWith('.log'));
.reverse()
.filter((x) => x.title.endsWith('.log'))
.filter((x) => x.createTime >= startTime && x.createTime <= endTime);
res.set({ res.set({
'Content-Length': sum(logs.map((x) => x.size)), 'Content-Length': sum(logs.map((x) => x.size)),
}); });
@@ -467,29 +429,4 @@ export default class SystemService {
} }
})(res, logs); })(res, logs);
} }
public async deleteSystemLog() {
const result = await readDirs(config.systemLogPath, config.systemLogPath);
const logs = result.reverse().filter((x) => x.title.endsWith('.log'));
for (const log of logs) {
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: '设置时区失败' };
}
}
} }
+191 -152
View File
@@ -1,7 +1,15 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import winston from 'winston'; import winston from 'winston';
import { createRandomString, getNetIp } from '../config/util'; import {
createFile,
createRandomString,
fileExist,
getNetIp,
getPlatform,
safeJSONParse,
} from '../config/util';
import config from '../config'; import config from '../config';
import * as fs from 'fs/promises';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { authenticator } from '@otplib/preset-default'; import { authenticator } from '@otplib/preset-default';
import { import {
@@ -10,7 +18,6 @@ import {
SystemModel, SystemModel,
SystemModelInfo, SystemModelInfo,
LoginStatus, LoginStatus,
AuthInfo,
} from '../data/system'; } from '../data/system';
import { NotificationInfo } from '../data/notify'; import { NotificationInfo } from '../data/notify';
import NotificationService from './notify'; import NotificationService from './notify';
@@ -21,7 +28,6 @@ import dayjs from 'dayjs';
import IP2Region from 'ip2region'; import IP2Region from 'ip2region';
import requestIp from 'request-ip'; import requestIp from 'request-ip';
import uniq from 'lodash/uniq'; import uniq from 'lodash/uniq';
import { shareStore } from '../shared/store';
@Service() @Service()
export default class UserService { export default class UserService {
@@ -42,138 +48,159 @@ export default class UserService {
req: Request, req: Request,
needTwoFactor = true, needTwoFactor = true,
): Promise<any> { ): Promise<any> {
const _exist = await fileExist(config.authConfigFile);
if (!_exist) {
return this.initAuthInfo();
}
let { username, password } = payloads; let { username, password } = payloads;
const content = await this.getAuthInfo(); const content = await this.getAuthInfo();
const timestamp = Date.now(); const timestamp = Date.now();
let { if (content) {
username: cUsername, let {
password: cPassword, username: cUsername,
retries = 0, password: cPassword,
lastlogon, retries = 0,
lastip, lastlogon,
lastaddr, lastip,
twoFactorActivated, lastaddr,
tokens = {}, twoFactorActivated,
platform, twoFactorActived,
} = content; tokens = {},
const retriesTime = Math.pow(3, retries) * 1000; platform,
if (retries > 2 && timestamp - lastlogon < retriesTime) { } = content;
const waitTime = Math.ceil( // patch old field
(retriesTime - (timestamp - lastlogon)) / 1000, twoFactorActivated = twoFactorActivated || twoFactorActived;
);
return {
code: 410,
message: `失败次数过多,请${waitTime}秒后重试`,
data: waitTime,
};
}
if ( if (
username === cUsername && (cUsername === 'admin' && cPassword === 'admin') ||
password === cPassword && !cUsername ||
twoFactorActivated && !cPassword
needTwoFactor ) {
) { return this.initAuthInfo();
await this.updateAuthInfo(content, { }
isTwoFactorChecking: true,
});
return {
code: 420,
message: '',
};
}
const ip = requestIp.getClientIp(req) || ''; const retriesTime = Math.pow(3, retries) * 1000;
const query = new IP2Region(); if (retries > 2 && timestamp - lastlogon < retriesTime) {
const ipAddress = query.search(ip); const waitTime = Math.ceil(
let address = ''; (retriesTime - (timestamp - lastlogon)) / 1000,
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 { return {
code: 410, code: 410,
message: `失败次数过多,请${waitTime}秒后重试`, message: `失败次数过多,请${waitTime}秒后重试`,
data: waitTime, data: waitTime,
}; };
} else {
return { code: 400, message: config.authError };
} }
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,
},
});
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,
},
});
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 };
}
}
} else {
return this.initAuthInfo();
} }
} }
public async logout(platform: string): Promise<any> { public async logout(platform: string): Promise<any> {
const authInfo = await this.getAuthInfo(); const authInfo = await this.getAuthInfo();
await this.updateAuthInfo(authInfo, { this.updateAuthInfo(authInfo, {
token: '', token: '',
tokens: { ...authInfo.tokens, [platform]: '' }, tokens: { ...authInfo.tokens, [platform]: '' },
}); });
@@ -188,9 +215,8 @@ export default class UserService {
(a, b) => b.info!.timestamp! - a.info!.timestamp!, (a, b) => b.info!.timestamp! - a.info!.timestamp!,
); );
if (result.length > 100) { if (result.length > 100) {
const ids = result.slice(100).map((x) => x.id!);
await SystemModel.destroy({ await SystemModel.destroy({
where: { id: ids }, where: { id: result[result.length - 1].id },
}); });
} }
return result.map((x) => x.info); return result.map((x) => x.info);
@@ -203,6 +229,20 @@ export default class UserService {
return doc; return doc;
} }
private async initAuthInfo() {
await fs.writeFile(
config.authConfigFile,
JSON.stringify({
username: 'admin',
password: 'admin',
}),
);
return {
code: 100,
message: '未找到认证文件,重新初始化',
};
}
public async updateUsernameAndPassword({ public async updateUsernameAndPassword({
username, username,
password, password,
@@ -214,21 +254,35 @@ export default class UserService {
return { code: 400, message: '密码不能设置为admin' }; return { code: 400, message: '密码不能设置为admin' };
} }
const authInfo = await this.getAuthInfo(); const authInfo = await this.getAuthInfo();
await this.updateAuthInfo(authInfo, { username, password }); this.updateAuthInfo(authInfo, { username, password });
return { code: 200, message: '更新成功' }; return { code: 200, message: '更新成功' };
} }
public async updateAvatar(avatar: string) { public async updateAvatar(avatar: string) {
const authInfo = await this.getAuthInfo(); const authInfo = await this.getAuthInfo();
await this.updateAuthInfo(authInfo, { avatar }); this.updateAuthInfo(authInfo, { avatar });
return { code: 200, data: avatar, message: '更新成功' }; 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() { public async initTwoFactor() {
const secret = authenticator.generateSecret(); const secret = authenticator.generateSecret();
const authInfo = await this.getAuthInfo(); const authInfo = await this.getAuthInfo();
const otpauth = authenticator.keyuri(authInfo.username, 'qinglong', secret); const otpauth = authenticator.keyuri(authInfo.username, 'qinglong', secret);
await this.updateAuthInfo(authInfo, { twoFactorSecret: secret }); this.updateAuthInfo(authInfo, { twoFactorSecret: secret });
return { secret, url: otpauth }; return { secret, url: otpauth };
} }
@@ -239,7 +293,7 @@ export default class UserService {
secret: authInfo.twoFactorSecret, secret: authInfo.twoFactorSecret,
}); });
if (isValid) { if (isValid) {
await this.updateAuthInfo(authInfo, { twoFactorActivated: true }); this.updateAuthInfo(authInfo, { twoFactorActivated: true });
} }
return isValid; return isValid;
} }
@@ -265,7 +319,7 @@ export default class UserService {
return this.login({ username, password }, req, false); return this.login({ username, password }, req, false);
} else { } else {
const { ip, address } = await getNetIp(req); const { ip, address } = await getNetIp(req);
await this.updateAuthInfo(authInfo, { this.updateAuthInfo(authInfo, {
lastip: ip, lastip: ip,
lastaddr: address, lastaddr: address,
platform: req.platform, platform: req.platform,
@@ -276,29 +330,24 @@ export default class UserService {
public async deactiveTwoFactor() { public async deactiveTwoFactor() {
const authInfo = await this.getAuthInfo(); const authInfo = await this.getAuthInfo();
await this.updateAuthInfo(authInfo, { this.updateAuthInfo(authInfo, {
twoFactorActivated: false, twoFactorActivated: false,
twoFactorActived: false,
twoFactorSecret: '', twoFactorSecret: '',
}); });
return true; return true;
} }
public async getAuthInfo() { private async getAuthInfo() {
const authInfo = await shareStore.getAuthInfo(); const content = await fs.readFile(config.authConfigFile, 'utf8');
if (authInfo) { return safeJSONParse(content);
return authInfo;
}
const doc = await this.getDb({ type: AuthDataType.authConfig });
return (doc.info || {}) as AuthInfo;
} }
private async updateAuthInfo(authInfo: AuthInfo, info: Partial<AuthInfo>) { private async updateAuthInfo(authInfo: any, info: any) {
const result = { ...authInfo, ...info }; await fs.writeFile(
await shareStore.updateAuthInfo(result); config.authConfigFile,
await this.updateAuthDb({ JSON.stringify({ ...authInfo, ...info }),
type: AuthDataType.authConfig, );
info: result,
});
} }
public async getNotificationMode(): Promise<NotificationInfo> { public async getNotificationMode(): Promise<NotificationInfo> {
@@ -307,7 +356,7 @@ export default class UserService {
} }
private async updateAuthDb(payload: SystemInfo): Promise<any> { private async updateAuthDb(payload: SystemInfo): Promise<any> {
let doc = await SystemModel.findOne({ where: { type: payload.type } }); let doc = await SystemModel.findOne({ type: payload.type });
if (doc) { if (doc) {
const updateResult = await SystemModel.update(payload, { const updateResult = await SystemModel.update(payload, {
where: { id: doc.id }, where: { id: doc.id },
@@ -345,14 +394,4 @@ export default class UserService {
return { code: 400, message: '通知发送失败,请检查参数' }; 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,
});
}
} }
-33
View File
@@ -1,33 +0,0 @@
import { Dependence } from '../data/dependence';
import { ICron } from '../protos/cron';
export type Override<
T,
K extends Partial<{ [P in keyof T]: any }> | string,
> = K extends string
? Omit<T, K> & { [P in keyof T]: T[P] | unknown }
: Omit<T, keyof K> & K;
export type TCron = Override<Partial<ICron>, { id: string }>;
export interface IDependencyFn<T> {
(): Promise<T>;
dependency?: Dependence;
}
export interface ICronFn<T> {
(): Promise<T>;
cron?: TCron;
}
export interface ISchedule {
schedule?: string;
name?: string;
command?: string;
id: string;
}
export interface IScheduleFn<T> {
(): Promise<T>;
schedule?: ISchedule;
}
+5 -93
View File
@@ -3,23 +3,14 @@ import os from 'os';
import { AuthDataType, SystemModel } from '../data/system'; import { AuthDataType, SystemModel } from '../data/system';
import Logger from '../loaders/logger'; import Logger from '../loaders/logger';
import { Dependence } from '../data/dependence'; import { Dependence } from '../data/dependence';
import NotificationService from '../services/notify';
import {
ICronFn,
IDependencyFn,
ISchedule,
IScheduleFn,
TCron,
} from './interface';
import config from '../config';
import { credentials } from '@grpc/grpc-js';
import { ApiClient } from '../protos/api';
interface IDependencyFn<T> {
(): Promise<T>;
dependency?: Dependence;
}
class TaskLimit { class TaskLimit {
private dependenyLimit = new PQueue({ concurrency: 1 }); private dependenyLimit = new PQueue({ concurrency: 1 });
private queuedDependencyIds = new Set<number>([]); private queuedDependencyIds = new Set<number>([]);
private queuedCrons = new Map<string, ICronFn<any>[]>();
private repeatCronNotifyMap = new Map<string, number>();
private updateLogLimit = new PQueue({ concurrency: 1 }); private updateLogLimit = new PQueue({ concurrency: 1 });
private cronLimit = new PQueue({ private cronLimit = new PQueue({
concurrency: Math.max(os.cpus().length, 4), concurrency: Math.max(os.cpus().length, 4),
@@ -27,20 +18,6 @@ class TaskLimit {
private manualCronoLimit = new PQueue({ private manualCronoLimit = new PQueue({
concurrency: Math.max(os.cpus().length, 4), concurrency: Math.max(os.cpus().length, 4),
}); });
private subscriptionLimit = new PQueue({
concurrency: Math.max(os.cpus().length, 4),
});
private scriptLimit = new PQueue({
concurrency: Math.max(os.cpus().length, 4),
});
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() { get cronLimitActiveCount() {
return this.cronLimit.pending; return this.cronLimit.pending;
@@ -54,8 +31,6 @@ class TaskLimit {
return [...this.queuedDependencyIds.values()][0]; return [...this.queuedDependencyIds.values()][0];
} }
private notificationService: NotificationService = new NotificationService();
constructor() { constructor() {
this.setCustomLimit(); this.setCustomLimit();
this.handleEvents(); this.handleEvents();
@@ -96,16 +71,6 @@ class TaskLimit {
} }
} }
public removeQueuedCron(id: string) {
if (this.queuedCrons.has(id)) {
const runs = this.queuedCrons.get(id);
if (runs && runs.length > 0) {
runs.pop();
this.queuedCrons.set(id, runs);
}
}
}
public async setCustomLimit(limit?: number) { public async setCustomLimit(limit?: number) {
if (limit) { if (limit) {
this.cronLimit.concurrency = limit; this.cronLimit.concurrency = limit;
@@ -123,35 +88,9 @@ class TaskLimit {
} }
public async runWithCronLimit<T>( public async runWithCronLimit<T>(
cron: TCron, fn: () => Promise<T>,
fn: ICronFn<T>,
options?: Partial<QueueAddOptions>, options?: Partial<QueueAddOptions>,
): Promise<T | void> { ): Promise<T | void> {
fn.cron = cron;
let runs = this.queuedCrons.get(cron.id);
const result = runs?.length ? [...runs, fn] : [fn];
const repeatTimes = this.repeatCronNotifyMap.get(cron.id) || 0;
if (result?.length > 5) {
if (repeatTimes < 3) {
this.repeatCronNotifyMap.set(cron.id, repeatTimes + 1);
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)}`);
return;
}
this.queuedCrons.set(cron.id, result);
return this.cronLimit.add(fn, options); return this.cronLimit.add(fn, options);
} }
@@ -162,33 +101,6 @@ class TaskLimit {
return this.manualCronoLimit.add(fn, options); return this.manualCronoLimit.add(fn, options);
} }
public async runWithSubscriptionLimit<T>(
schedule: TCron,
fn: IScheduleFn<T>,
options?: Partial<QueueAddOptions>,
): Promise<T | void> {
fn.schedule = schedule;
return this.subscriptionLimit.add(fn, options);
}
public async runWithSystemLimit<T>(
schedule: TCron,
fn: IScheduleFn<T>,
options?: Partial<QueueAddOptions>,
): Promise<T | void> {
fn.schedule = schedule;
return this.systemLimit.add(fn, options);
}
public async runWithScriptLimit<T>(
schedule: ISchedule,
fn: IScheduleFn<T>,
options?: Partial<QueueAddOptions>,
): Promise<T | void> {
fn.schedule = schedule;
return this.scriptLimit.add(fn, options);
}
public runDependeny<T>( public runDependeny<T>(
dependency: Dependence, dependency: Dependence,
fn: IDependencyFn<T>, fn: IDependencyFn<T>,
+4 -19
View File
@@ -1,17 +1,11 @@
import { spawn } from 'cross-spawn'; import { spawn } from 'cross-spawn';
import taskLimit from './pLimit'; import taskLimit from './pLimit';
import Logger from '../loaders/logger'; import Logger from '../loaders/logger';
import { ICron } from '../protos/cron';
export function runCron(cmd: string, cron: ICron): Promise<number | void> { export function runCron(cmd: string, options?: { schedule: string; extraSchedules: Array<{ schedule: string }>; name: string }): Promise<number | void> {
return taskLimit.runWithCronLimit(cron, () => { return taskLimit.runWithCronLimit(() => {
return new Promise(async (resolve: any) => { return new Promise(async (resolve: any) => {
Logger.info( Logger.info(`[schedule][开始执行任务] 参数 ${JSON.stringify({ ...options, command: cmd })}`);
`[schedule][开始执行任务] 参数 ${JSON.stringify({
...cron,
command: cmd,
})}`,
);
const cp = spawn(cmd, { shell: '/bin/bash' }); const cp = spawn(cmd, { shell: '/bin/bash' });
cp.stderr.on('data', (data) => { cp.stderr.on('data', (data) => {
@@ -30,16 +24,7 @@ export function runCron(cmd: string, cron: ICron): Promise<number | void> {
}); });
cp.on('exit', async (code) => { cp.on('exit', async (code) => {
taskLimit.removeQueuedCron(cron.id); resolve({ ...options, command: cmd, pid: cp.pid, code });
Logger.info(
'[schedule][执行任务结束] 参数: %s, 退出码: %j',
JSON.stringify({
...cron,
command: cmd,
}),
code,
);
resolve({ ...cron, command: cmd, pid: cp.pid, code });
}); });
}); });
}); });
-34
View File
@@ -1,34 +0,0 @@
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);
},
};
-42
View File
@@ -1,42 +0,0 @@
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();
}
+10 -2
View File
@@ -6,7 +6,6 @@ import fs from 'fs';
import config from './config'; import config from './config';
import path from 'path'; import path from 'path';
import os from 'os'; import os from 'os';
import { writeFileWithLock } from './shared/utils';
const tokenFile = path.join(config.configPath, 'token.json'); const tokenFile = path.join(config.configPath, 'token.json');
@@ -26,7 +25,16 @@ async function getToken() {
} }
async function writeFile(data: any) { async function writeFile(data: any) {
await writeFileWithLock(tokenFile, `${JSON.stringify(data)}${os.EOL}`); return new Promise<void>((resolve, reject) => {
fs.writeFile(
tokenFile,
`${JSON.stringify(data)}${os.EOL}`,
{ encoding: 'utf8' },
() => {
resolve();
},
);
});
} }
getToken(); getToken();
-40
View File
@@ -1,40 +0,0 @@
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),
};
Executable
+274
View File
@@ -0,0 +1,274 @@
#!/usr/bin/env zx
import path from 'path';
const dir_root = process.env.QL_DIR;
const file_auth_token = path.join(dir_root, 'static/auth.json');
const token_file = path.join(dir_root, 'static/build/token.js');
let token;
const createToken = async () => {
let tokenCommand = `tsx ${dir_root}/back/token.ts`;
if (await fs.exists(token_file)) {
tokenCommand = `node ${token_file}`;
}
token = (await $([tokenCommand])).stdout.trim();
};
const getToken = async () => {
if (fs.existsSync(file_auth_token)) {
const authTokenData = JSON.parse(fs.readFileSync(file_auth_token, 'utf8'));
token = authTokenData.value;
const expiration = authTokenData.expiration;
const currentTimeStamp = Math.floor(Date.now() / 1000);
if (currentTimeStamp >= expiration) {
await createToken();
}
} else {
await createToken();
}
};
export const addCronApi = async (schedule, command, name, subId = null) => {
const currentTimeStamp = Math.floor(Date.now() / 1000);
const data = {
name,
command,
schedule,
sub_id: subId,
};
try {
const response = await fetch(
`http://0.0.0.0:5600/open/crons?t=${currentTimeStamp}`,
{
method: 'POST',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`,
'User-Agent': 'Mozilla/5.0',
'Content-Type': 'application/json;charset=UTF-8',
},
body: JSON.stringify(data),
},
);
const responseData = await response.json();
const { code, message } = responseData;
if (code === 200) {
console.log(`${name} -> 添加成功`);
} else {
console.log(`${name} -> 添加失败(${message})`);
}
} catch (error) {
console.error(`${name} -> 添加失败(${error.message})`);
}
};
export const updateCronApi = async (schedule, command, name, id) => {
const currentTimeStamp = Math.floor(Date.now() / 1000);
const data = {
name,
command,
schedule,
id,
};
try {
const response = await fetch(
`http://0.0.0.0:5600/open/crons?t=${currentTimeStamp}`,
{
method: 'PUT',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`,
'User-Agent': 'Mozilla/5.0',
'Content-Type': 'application/json;charset=UTF-8',
},
body: JSON.stringify(data),
},
);
const responseData = await response.json();
const { code, message } = responseData;
if (code === 200) {
console.log(`${name} -> 更新成功`);
} else {
console.log(`${name} -> 更新失败(${message})`);
}
} catch (error) {
console.error(`${name} -> 更新失败(${error.message})`);
}
};
export const updateCronCommandApi = async (command, id) => {
const currentTimeStamp = Math.floor(Date.now() / 1000);
const data = {
command,
id,
};
try {
const response = await fetch(
`http://0.0.0.0:5600/open/crons?t=${currentTimeStamp}`,
{
method: 'PUT',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`,
'User-Agent': 'Mozilla/5.0',
'Content-Type': 'application/json;charset=UTF-8',
},
body: JSON.stringify(data),
},
);
const responseData = await response.json();
const { code, message } = responseData;
if (code === 200) {
console.log(`${command} -> 更新成功`);
} else {
console.log(`${command} -> 更新失败(${message})`);
}
} catch (error) {
console.error(`${command} -> 更新失败(${error.message})`);
}
};
export const delCronApi = async (ids) => {
const currentTimeStamp = Math.floor(Date.now() / 1000);
try {
const response = await fetch(
`http://0.0.0.0:5600/open/crons?t=${currentTimeStamp}`,
{
method: 'DELETE',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`,
'User-Agent': 'Mozilla/5.0',
'Content-Type': 'application/json;charset=UTF-8',
},
body: JSON.stringify(ids),
},
);
const responseData = await response.json();
const { code, message } = responseData;
if (code === 200) {
console.log('成功');
} else {
console.log(`失败(${message})`);
}
} catch (error) {
console.error(`删除失败(${error.message})`);
}
};
export const updateCron = async (
ids,
status,
pid,
logPath,
lastExecutingTime = 0,
runningTime = 0,
) => {
const currentTimeStamp = Math.floor(Date.now() / 1000);
const data = {
ids,
status,
pid,
log_path: logPath,
last_execution_time: lastExecutingTime,
last_running_time: runningTime,
};
try {
const response = await fetch(
`http://0.0.0.0:5600/open/crons/status?t=${currentTimeStamp}`,
{
method: 'PUT',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`,
'User-Agent': 'Mozilla/5.0',
'Content-Type': 'application/json;charset=UTF-8',
},
body: JSON.stringify(data),
},
);
const responseData = await response.json();
const { code, message } = responseData;
if (code !== 200) {
console.log(`更新任务状态失败(${message})`);
}
} catch (error) {
console.error(`更新任务状态失败(${error.message})`);
}
};
export const notifyApi = async (title, content) => {
const currentTimeStamp = Math.floor(Date.now() / 1000);
const data = {
title,
content,
};
try {
const response = await fetch(
`http://0.0.0.0:5600/open/system/notify?t=${currentTimeStamp}`,
{
method: 'PUT',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`,
'User-Agent': 'Mozilla/5.0',
'Content-Type': 'application/json;charset=UTF-8',
},
body: JSON.stringify(data),
},
);
const responseData = await response.json();
const { code, message } = responseData;
if (code === 200) {
console.log('通知发送成功🎉');
} else {
console.log(`通知失败(${message})`);
}
} catch (error) {
console.error(`通知失败(${error.message})`);
}
};
export const findCronApi = async (params) => {
const currentTimeStamp = Math.floor(Date.now() / 1000);
try {
const response = await fetch(
`http://0.0.0.0:5600/open/crons/detail?${params}&t=${currentTimeStamp}`,
{
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`,
'User-Agent': 'Mozilla/5.0',
'Content-Type': 'application/json;charset=UTF-8',
},
},
);
const responseData = await response.json();
const { data } = responseData;
if (data === 'null') {
console.log('');
} else {
const { name } = data;
console.log(name);
}
} catch (error) {
console.error(`查找失败(${error.message})`);
}
};
await getToken();
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env zx
import 'zx/globals';
let initialVars = [];
export const storeEnvVars = async () => {
const stdout = (await $`env`).lines();
initialVars = stdout.map((line) => line.split('=')[0]);
};
export const restoreEnvVars = async () => {
const stdout = (await $`env`).lines();
const currentVars = stdout.map((line) => line.split('=')[0]);
for (const key of currentVars) {
if (!initialVars.includes(key)) {
await $`unset ${key}`;
}
}
};
await storeEnvVars();
Executable
+302
View File
@@ -0,0 +1,302 @@
#!/usr/bin/env zx
import {
dirScripts,
dirLog,
handleTaskStart,
runTaskBefore,
runTaskAfter,
handleTaskEnd,
globalState,
formatLogTime,
} from './share.mjs';
import { basename, dirname } from 'path';
function defineProgram(fileParam) {
if (fileParam.endsWith('.js') || fileParam.endsWith('.mjs')) {
return 'node';
} else if (fileParam.endsWith('.py') || fileParam.endsWith('.pyc')) {
return 'python3';
} else if (fileParam.endsWith('.sh')) {
return 'bash';
} else if (fileParam.endsWith('.ts')) {
return $`command -v tsx`
.then(() => 'tsx')
.catch(() => 'ts-node-transpile-only');
} else {
return '';
}
}
async function randomDelay(fileParam) {
const randomDelayMax = process.env.RandomDelay;
if (randomDelayMax && randomDelayMax > 0) {
const fileExtensions = process.env.RandomDelayFileExtensions || 'js';
const ignoredMinutes = process.env.RandomDelayIgnoredMinutes || '0 30';
const currentMin = new Date().getMinutes();
if (
fileExtensions.split(' ').some((ext) => fileParam.endsWith(`.${ext}`))
) {
if (
ignoredMinutes.split(' ').some((min) => parseInt(min) === currentMin)
) {
return;
}
const delaySecond = Math.floor(Math.random() * randomDelayMax) + 1;
console.log(
`任务随机延迟 ${delaySecond} 秒,配置文件参数 RandomDelay 置空可取消延迟`,
);
await sleep(delaySecond * 1000);
}
}
}
async function genArrayScripts() {
const arrayScripts = [];
const arrayScriptsName = [];
const files = await fs.readdir(dirScripts);
for (const file of files) {
if (file.endsWith('.js') && file !== 'sendNotify.js') {
arrayScripts.push(file);
const content = await fs.readFile(`${dirScripts}/${file}`, 'utf8');
const match = content.match(/new Env\(['"]([^'"]+)['"]\)/);
arrayScriptsName.push(match ? match[1] : '<未识别出活动名称>');
}
}
return { arrayScripts, arrayScriptsName };
}
async function usage() {
const { arrayScripts, arrayScriptsName } = await genArrayScripts();
console.log(
`task命令运行本程序自动添加进crontab的脚本,需要输入脚本的绝对路径或去掉 “${dirScripts}/” 目录后的相对路径(定时任务中请写作相对路径),用法为:`,
);
console.log(
`1.$cmdTask <fileName> # 依次执行,如果设置了随机延迟,将随机延迟一定秒数`,
);
console.log(
`2.$cmdTask <fileName> now # 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日志,同时记录在日志文件中`,
);
console.log(
`3.$cmdTask <fileName> conc <环境变量名称> <账号编号,空格分隔>(可选的) # 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日志,直接记录在日志文件中,且可指定账号执行`,
);
console.log(
`4.$cmdTask <fileName> desi <环境变量名称> <账号编号,空格分隔> # 指定账号执行,无论是否设置了随机延迟,均立即运行`,
);
if (arrayScripts.length > 0) {
console.log(`\n当前有以下脚本可以运行:`);
arrayScripts.forEach((script, i) =>
console.log(`${i + 1}. ${arrayScriptsName[i]}${script}`),
);
} else {
console.log(`\n暂无脚本可以执行`);
}
}
export function parseDuration(d) {
if (typeof d == 'number') {
if (isNaN(d) || d < 0) throw new Error(`Invalid duration: "${d}".`);
return d;
} else if (/\d+s/.test(d)) {
return +d.slice(0, -1) * 1000;
} else if (/\d+ms/.test(d)) {
return +d.slice(0, -2);
} else if (/\d+m/.test(d)) {
return +d.slice(0, -1) * 1000 * 60;
} else if (/\d+h/.test(d)) {
return +d.slice(0, -1) * 1000 * 60 * 60;
} else if (/\d+d/.test(d)) {
return +d.slice(0, -1) * 1000 * 60 * 60 * 24;
}
throw new Error(`Unknown duration: "${d}".`);
}
async function runWithTimeout(command) {
if (globalState.commandTimeoutTime) {
const timeoutNumber = parseDuration(globalState.commandTimeoutTime);
await $([command]).timeout(timeoutNumber).nothrow().pipe(process.stdout);
} else {
await $([command]).nothrow().pipe(process.stdout);
}
}
async function runNormal(fileParam, scriptParams) {
if (
!scriptParams.includes('now') &&
process.env.realTime !== 'true' &&
process.env.noDelay !== 'true'
) {
await randomDelay(fileParam);
}
cd(dirScripts);
const relativePath = dirname(fileParam);
if (!fileParam.startsWith('/') && relativePath) {
cd(relativePath);
fileParam = fileParam.replace(`${relativePath}/`, '');
}
await runWithTimeout(
`${globalState.whichProgram} ${fileParam} ${scriptParams}`,
);
}
async function runConcurrent(fileParam, envParam, numParam, scriptParams) {
if (!envParam || !numParam) {
console.log(`缺少并发运行的环境变量参数 task xxx.js conc Test 1 3`);
return;
}
const array = (process.env[envParam] || '').split('&');
const runArr = expandRange(numParam, array.length);
const arrayRun = runArr.map((i) => array[i - 1]).filter(Boolean);
const singleLogTime = formatLogTime(new Date());
cd(dirScripts);
const relativePath = dirname(fileParam);
if (relativePath && fileParam.includes('/')) {
cd(relativePath);
fileParam = fileParam.replace(`${relativePath}/`, '');
}
await Promise.all(
arrayRun.map(async (env, i) => {
const singleLogPath = `${dirLog}/${globalState.logDir}/${singleLogTime}_${
i + 1
}.log`;
await runWithTimeout(
`${envParam}="${env.replace('"', '\\"')}" ${
globalState.whichProgram
} ${fileParam} ${scriptParams} &>${singleLogPath}`,
);
}),
);
for (let i = 0; i < arrayRun.length; i++) {
const singleLogPath = `${dirLog}/${globalState.logDir}/${singleLogTime}_${
i + 1
}.log`;
const log = await fs.readFile(singleLogPath, 'utf8');
console.log(log);
await fs.unlink(singleLogPath);
}
}
async function runDesignated(fileParam, envParam, numParam, scriptParams) {
if (!envParam || !numParam) {
console.log(`缺少单独运行的参数 task xxx.js desi Test 1 3`);
return;
}
const array = (process.env[envParam] || '').split('&');
const runArr = expandRange(numParam, array.length);
const arrayRun = runArr.map((i) => array[i - 1]).filter(Boolean);
const cookieStr = arrayRun.join('&');
cd(dirScripts);
const relativePath = dirname(fileParam);
if (relativePath && fileParam.includes('/')) {
cd(relativePath);
fileParam = fileParam.replace(`${relativePath}/`, '');
}
console.log('cookieStr', cookieStr.length, arrayRun.length);
// ${envParam}="${cookieStr.replace('"', '\\"')}"
await runWithTimeout(
`${globalState.whichProgram} ${fileParam} ${scriptParams}`,
);
}
async function runElse(fileParam, scriptParams) {
cd(dirScripts);
const relativePath = dirname(fileParam);
if (relativePath && fileParam.includes('/')) {
cd(relativePath);
fileParam = fileParam.replace(`${relativePath}/`, './');
}
await runWithTimeout(
`${globalState.whichProgram} ${fileParam} ${scriptParams}`,
);
}
function expandRange(rangeStr, max) {
const tempRangeStr = rangeStr
.replace(/-max/g, `-${max}`)
.replace(/max-/g, `${max}-`);
return tempRangeStr.split(' ').flatMap((part) => {
const rangeMatch = part.match(/^(\d+)([-~_])(\d+)$/);
if (rangeMatch) {
const [, start, , end] = rangeMatch.map(Number);
return Array.from({ length: end - start + 1 }, (_, i) => start + i);
}
return Number(part);
});
}
async function main(taskShellParams, scriptParams) {
const [fileParam, action, envParam, ...others] = taskShellParams;
if (taskShellParams.length === 0) {
return await usage();
}
if (fileParam && /\.(js|py|pyc|sh|ts)$/.test(fileParam)) {
const filePath = fileParam.startsWith('/')
? fileParam
: path.join(dirScripts, fileParam);
if (!(await fs.exists(filePath))) {
console.log(`文件不存在 ${fileParam}`);
return;
}
switch (action) {
case undefined:
return await runNormal(fileParam, scriptParams);
case 'now':
return await runNormal(fileParam, scriptParams);
case 'conc':
return await runConcurrent(
fileParam,
envParam,
others.join(' '),
scriptParams,
);
case 'desi':
return await runDesignated(
fileParam,
envParam,
others.join(' '),
scriptParams,
);
}
}
await runElse(fileParam, taskShellParams.slice(1).concat(scriptParams));
}
async function run() {
const taskArgv = minimist(process.argv.slice(3), {
'--': true,
});
const { _: taskShellParams, m, '--': scriptParams, GlobalState } = taskArgv;
const cacheState = JSON.parse(GlobalState || '{}');
for (const key in cacheState) {
globalState[key] = cacheState[key];
}
if (m) {
globalState.commandTimeoutTime = m;
}
globalState.whichProgram = await defineProgram(taskShellParams[0]);
await handleTaskStart();
await runTaskBefore();
await main(taskShellParams, scriptParams);
await runTaskAfter();
await handleTaskEnd();
}
async function singleHandle() {}
const signals = ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGQUIT', 'SIGTSTP'];
signals.forEach((sig) => process.on(sig, singleHandle));
await run();
Executable
+500
View File
@@ -0,0 +1,500 @@
#!/usr/bin/env zx
import 'zx/globals';
// $.verbose = true;
import { updateCron, notifyApi } from './api.mjs';
import { restoreEnvVars } from './env.mjs';
export const dirRoot = process.env.QL_DIR;
export const dirTmp = path.join(dirRoot, '.tmp');
export const dirData = process.env.QL_DATA_DIR
? process.env.QL_DATA_DIR.endsWith('/')
? process.env.QL_DATA_DIR.slice(-1)
: process.env.QL_DATA_DIR
: path.join(dirRoot, 'data');
export const dirShell = path.join(dirRoot, 'shell');
export const dirCli = path.join(dirRoot, 'cli');
export const dirSample = path.join(dirRoot, 'sample');
export const dirStatic = path.join(dirRoot, 'static');
export const dirConfig = path.join(dirData, 'config');
export const dirScripts = path.join(dirData, 'scripts');
export const dirRepo = path.join(dirData, 'repo');
export const dirRaw = path.join(dirData, 'raw');
export const dirLog = path.join(dirData, 'log');
export const dirDb = path.join(dirData, 'db');
export const dirDep = path.join(dirData, 'deps');
export const dirListTmp = path.join(dirLog, '.tmp');
export const dirUpdateLog = path.join(dirLog, 'update');
export const qlStaticRepo = path.join(dirRepo, 'static');
export const fileConfigSample = path.join(dirSample, 'config.sample.sh');
export const fileEnv = path.join(dirConfig, 'env.sh');
export const jsFileEnv = path.join(dirConfig, 'env.js');
export const fileConfigUser = path.join(dirConfig, 'config.sh');
export const fileAuthSample = path.join(dirSample, 'auth.sample.json');
export const fileAuthUser = path.join(dirConfig, 'auth.json');
export const fileAuthToken = path.join(dirConfig, 'token.json');
export const fileExtraShell = path.join(dirConfig, 'extra.sh');
export const fileTaskBefore = path.join(dirConfig, 'task_before.sh');
export const fileTaskAfter = path.join(dirConfig, 'task_after.sh');
export const fileTaskSample = path.join(dirSample, 'task.sample.sh');
export const fileExtraSample = path.join(dirSample, 'extra.sample.sh');
export const fileNotifyJsSample = path.join(dirSample, 'notify.js');
export const fileNotifyPySample = path.join(dirSample, 'notify.py');
export const fileTestJsSample = path.join(dirSample, 'ql_sample.js');
export const fileTestPySample = path.join(dirSample, 'ql_sample.py');
export const fileNotifyPy = path.join(dirScripts, 'notify.py');
export const fileNotifyJs = path.join(dirScripts, 'sendNotify.js');
export const fileTestJs = path.join(dirScripts, 'ql_sample.js');
export const fileTestPy = path.join(dirScripts, 'ql_sample.py');
export const nginxAppConf = path.join(dirRoot, 'docker/front.conf');
export const nginxConf = path.join(dirRoot, 'docker/nginx.conf');
export const depNotifyPy = path.join(dirDep, 'notify.py');
export const depNotifyJs = path.join(dirDep, 'sendNotify.js');
export const listCrontabUser = path.join(dirConfig, 'crontab.list');
export const listCrontabSample = path.join(dirSample, 'crontab.sample.list');
export const listOwnScripts = path.join(dirListTmp, 'own_scripts.list');
export const listOwnUser = path.join(dirListTmp, 'own_user.list');
export const listOwnAdd = path.join(dirListTmp, 'own_add.list');
export const listOwnDrop = path.join(dirListTmp, 'own_drop.list');
export const globalState = {};
export const initEnv = () => {
$.prefix +=
'export NODE_PATH=/usr/local/bin:/usr/local/pnpm-global/5/node_modules:/usr/local/lib/node_modules:/root/.local/share/pnpm/global/5/node_modules;';
$.prefix += 'export PYTHONUNBUFFERED=1;';
$.prefix += 'export TERM=xterm-color;';
};
export const importConfig = async () => {
if (await fs.exists(fileConfigUser)) {
$.prefix += (await fs.readFile(fileConfigUser, 'utf8'));
}
// if (process.env.LOAD_ENV !== 'false' && (await fs.exists(fileEnv))) {
// $.prefix += (await fs.readFile(fileEnv, 'utf8'));
// }
require(jsFileEnv)
globalState.qlBaseUrl = process.env.QlBaseUrl || '/';
globalState.qlPort = process.env.QlPort || '5700';
globalState.commandTimeoutTime = process.env.CommandTimeoutTime;
globalState.fileExtensions = process.env.RepoFileExtensions || 'js py';
globalState.proxyUrl = process.env.ProxyUrl || '';
globalState.currentBranch = process.env.QL_BRANCH;
if (process.env.DefaultCronRule) {
globalState.defaultCron = process.env.DefaultCronRule;
} else {
globalState.defaultCron = `${Math.floor(Math.random() * 60)} ${Math.floor(
Math.random() * 24,
)} * * *`;
}
globalState.cpuWarn = process.env.CpuWarn;
globalState.memWarn = process.env.MemoryWarn;
globalState.diskWarn = process.env.DiskWarn;
};
export const setProxy = (proxy) => {
if (proxy) {
globalState.proxyUrl = proxy;
}
if (globalState.proxyUrl) {
$`export http_proxy=${globalState.proxyUrl}`;
$`export https_proxy=${globalState.proxyUrl}`;
}
};
export const unsetProxy = () => {
$`unset http_proxy`;
$`unset https_proxy`;
};
export const makeDir = async (dir) => {
if (!(await fs.exists(dir))) {
await fs.mkdir(dir, { recursive: true });
}
};
export const detectTermux = () => {
globalState.isTermux = process.env.PATH?.includes('com.termux') ? 1 : 0;
};
export const detectMacos = () => {
globalState.isMacos = os.type() === 'Darwin' ? 1 : 0;
};
export const genRandomNum = (number) => {
return Math.floor(Math.random() * number);
};
export const fixConfig = async () => {
await makeDir(dirTmp);
await makeDir(dirStatic);
await makeDir(dirData);
await makeDir(dirConfig);
await makeDir(dirLog);
await makeDir(dirDb);
await makeDir(dirScripts);
await makeDir(dirListTmp);
await makeDir(dirRepo);
await makeDir(dirRaw);
await makeDir(dirUpdateLog);
await makeDir(dirDep);
if (!(await fs.exists(fileConfigUser))) {
console.log(
`复制一份 ${fileConfigSample}${fileConfigUser},随后请按注释编辑你的配置文件:${fileConfigUser}`,
);
await fs.copyFile(fileConfigSample, fileConfigUser);
}
if (!(await fs.exists(fileEnv))) {
console.log(
'检测到config配置目录下不存在env.sh,创建一个空文件用于初始化...',
);
await fs.writeFile(fileEnv, '');
}
if (!(await fs.exists(fileTaskBefore))) {
console.log(`复制一份 ${fileTaskSample}${fileTaskBefore}`);
await fs.copyFile(fileTaskSample, fileTaskBefore);
}
if (!(await fs.exists(fileTaskAfter))) {
console.log(`复制一份 ${fileTaskSample}${fileTaskAfter}`);
await fs.copyFile(fileTaskSample, fileTaskAfter);
}
if (!(await fs.exists(fileExtraShell))) {
console.log(`复制一份 ${fileExtraSample}${fileExtraShell}`);
await fs.copyFile(fileExtraSample, fileExtraShell);
}
if (!(await fs.exists(fileAuthUser))) {
console.log(`复制一份 ${fileAuthSample}${fileAuthUser}`);
await fs.copyFile(fileAuthSample, fileAuthUser);
}
if (!(await fs.exists(fileNotifyPy))) {
console.log(`复制一份 ${fileNotifyPySample}${fileNotifyPy}`);
await fs.copyFile(fileNotifyPySample, fileNotifyPy);
}
if (!(await fs.exists(fileNotifyJs))) {
console.log(`复制一份 ${fileNotifyJsSample}${fileNotifyJs}`);
await fs.copyFile(fileNotifyJsSample, fileNotifyJs);
}
if (!(await fs.exists(fileTestJs))) {
await fs.copyFile(fileTestJsSample, fileTestJs);
}
if (!(await fs.exists(fileTestPy))) {
await fs.copyFile(fileTestPySample, fileTestPy);
}
if (await fs.exists('/etc/nginx/conf.d/default.conf')) {
console.log('检测到你可能未修改过默认nginx配置,将帮你删除');
await fs.unlink('/etc/nginx/conf.d/default.conf');
}
if (!(await fs.exists(depNotifyJs))) {
console.log(`复制一份 ${fileNotifyJsSample}${depNotifyJs}`);
await fs.copyFile(fileNotifyJsSample, depNotifyJs);
}
if (!(await fs.exists(depNotifyPy))) {
console.log(`复制一份 ${fileNotifyPySample}${depNotifyPy}`);
await fs.copyFile(fileNotifyPySample, depNotifyPy);
}
};
export const npmInstallSub = async () => {
if (globalState.isTermux === 1) {
await $`npm install --production --no-bin-links`;
} else if (!(await $`command -v pnpm`)) {
await $`npm install --production`;
} else {
await $`pnpm install --loglevel error --production`;
}
};
export const npmInstall = async (dirWork) => {
const dirCurrent = process.cwd();
await $`cd ${dirWork}`;
console.log(`安装 ${dirWork} 依赖包...`);
await npmInstallSub();
await $`cd ${dirCurrent}`;
};
export const diffAndCopy = async (copySource, copyTo) => {
if (
!(await fs.exists(copyTo)) ||
(await $`diff ${copySource} ${copyTo}`).exitCode !== 0
) {
await fs.copyFile(copySource, copyTo);
}
};
export const gitCloneScripts = async (url, dir, branch, proxy) => {
const partCmd = branch ? `-b ${branch}` : '';
console.log(`开始拉取仓库 ${globalState.uniqPath}${dir}`);
setProxy(proxy);
const res = await $`git clone -q --depth=1 ${partCmd} ${url} ${dir}`;
globalState.exitStatus = res.exitCode;
unsetProxy();
};
export const randomRange = (begin, end) => {
return Math.floor(Math.random() * (end - begin) + begin);
};
export const deletePm2 = async () => {
await $`cd ${dirRoot}`;
await $`pm2 delete ecosystem.config.js`;
};
export const reloadPm2 = async () => {
await $`cd ${dirRoot}`;
restoreEnvVars();
await $`pm2 flush &>/dev/null`;
await $`pm2 startOrGracefulReload ecosystem.config.js`;
};
export const reloadUpdate = async () => {
await $`cd ${dirRoot}`;
restoreEnvVars();
await $`pm2 flush &>/dev/null`;
await $`pm2 startOrGracefulReload other.config.js`;
};
export const diffTime = (beginTime, endTime) => {
let diffTime;
if (globalState.isMacos === 1) {
diffTime = (+new Date(endTime) - +new Date(beginTime)) / 1000;
} else {
diffTime =
(new Date(endTime).getTime() - new Date(beginTime).getTime()) / 1000;
}
return diffTime;
};
export const formatTime = (time) => {
// 秒
return new Date(time).toLocaleString();
};
function pad(n, min = 10) {
return n < min ? '0' + n : n;
}
export function formatDate(date) {
const year = date.getFullYear();
const month = pad(date.getMonth() + 1);
const day = pad(date.getDate());
const hour = pad(date.getHours());
const minute = pad(date.getMinutes());
const second = pad(date.getSeconds());
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
}
export const formatLogTime = (date) => {
const year = date.getFullYear();
const month = pad(date.getMonth() + 1);
const day = pad(date.getDate());
const hour = pad(date.getHours());
const minute = pad(date.getMinutes());
const second = pad(date.getSeconds());
const milliSecond = pad(date.getMilliseconds(), 100);
return `${year}-${month}-${day}-${hour}-${minute}-${second}-${milliSecond}`;
};
export const formatTimestamp = (date) => {
return Math.floor(date.getTime() / 1000);
};
export const patchVersion = async () => {
await $`git config --global pull.rebase false`;
if (await fs.exists(path.join(dirRoot, 'db/cookie.db'))) {
console.log('检测到旧的db文件,拷贝为新db...');
await $`mv ${path.join(dirRoot, 'db/cookie.db')} ${path.join(
dirRoot,
'db/env.db',
)}`;
await $`rm -rf ${path.join(dirRoot, 'db/cookie.db')}`;
}
if (await fs.exists(path.join(dirRoot, 'db'))) {
console.log('检测到旧的db目录,拷贝到data目录...');
await $`cp -rf ${path.join(dirRoot, 'config')} ${dirData}`;
}
if (await fs.exists(path.join(dirRoot, 'scripts'))) {
console.log('检测到旧的scripts目录,拷贝到data目录...');
await $`cp -rf ${path.join(dirRoot, 'scripts')} ${dirData}`;
}
if (await fs.exists(path.join(dirRoot, 'log'))) {
console.log('检测到旧的log目录,拷贝到data目录...');
await $`cp -rf ${path.join(dirRoot, 'log')} ${dirData}`;
}
if (await fs.exists(path.join(dirRoot, 'config'))) {
console.log('检测到旧的config目录,拷贝到data目录...');
await $`cp -rf ${path.join(dirRoot, 'config')} ${dirData}`;
}
};
export const initNginx = async () => {
await fs.copyFile(nginxConf, '/etc/nginx/nginx.conf');
await fs.copyFile(nginxAppConf, '/etc/nginx/conf.d/front.conf');
let locationUrl = '/';
let aliasStr = '';
let rootStr = '';
let qlBaseUrl = globalState.qlBaseUrl;
let qlPort = globalState.qlPort;
if (qlBaseUrl !== '/') {
if (!qlBaseUrl.startsWith('/')) {
qlBaseUrl = `/${qlBaseUrl}`;
}
if (!qlBaseUrl.endsWith('/')) {
qlBaseUrl = `${qlBaseUrl}/`;
}
locationUrl = `^~${qlBaseUrl.slice(0, -1)}`;
aliasStr = `alias ${path.join(dirStatic, 'dist')};`;
const file = await fs.readFile(
path.join(dirStatic, 'dist/index.html'),
'utf8',
);
if (!file.includes(`<base href="${qlBaseUrl}">`)) {
await fs.writeFile(
path.join(dirStatic, 'dist/index.html'),
`<base href="${qlBaseUrl}">\n${file}`,
);
}
} else {
rootStr = `root ${path.join(dirStatic, 'dist')};`;
}
await $`sed -i "s,QL_ALIAS_CONFIG,${aliasStr},g" /etc/nginx/conf.d/front.conf`;
await $`sed -i "s,QL_ROOT_CONFIG,${rootStr},g" /etc/nginx/conf.d/front.conf`;
await $`sed -i "s,QL_BASE_URL_LOCATION,${locationUrl},g" /etc/nginx/conf.d/front.conf`;
let ipv6Str = '';
const ipv6 = await $`ip a | grep inet6`;
if (ipv6.stdout.trim()) {
ipv6Str = 'listen [::]:${qlPort} ipv6only=on;';
}
const ipv4Str = `listen ${qlPort};`;
await $`sed -i "s,IPV6_CONFIG,${ipv6Str},g" /etc/nginx/conf.d/front.conf`;
await $`sed -i "s,IPV4_CONFIG,${ipv4Str},g" /etc/nginx/conf.d/front.conf`;
};
async function checkServer() {
const cpuWarn = parseInt(process.env.cpuWarn || '0');
const memWarn = parseInt(process.env.memWarn || '0');
const diskWarn = parseInt(process.env.diskWarn || '0');
if (cpuWarn && memWarn && diskWarn) {
const topResult = await $`top -b -n 1`;
const cpuUse = parseInt(
topResult.stdout.match(/CPU\s+(\d+)\%/)?.[1] || '0',
);
const memFree = parseInt(
(await $`free -m`).stdout.match(/Mem:\s+(\d+)/)?.[1] || '0',
);
const memTotal = parseInt(
(await $`free -m`).stdout.match(/Mem:\s+\d+\s+(\d+)/)?.[1] || '0',
);
const diskUse = parseInt(
(await $`df -P`).stdout.match(/\/dev.*\s+(\d+)\%/)?.[1] || '0',
);
if (memFree && memTotal && diskUse && cpuUse) {
const memUse = Math.floor((memFree * 100) / memTotal);
if (cpuUse > cpuWarn || memFree < memWarn || diskUse > diskWarn) {
const resource = topResult.stdout
.split('\n')
.slice(7, 17)
.map((line) => line.replace(/\s+/g, ' '))
.join('\\n');
await notifyApi(
'服务器资源异常警告',
`当前CPU占用 ${cpuUse}% 内存占用 ${memUse}% 磁盘占用 ${diskUse}% \n资源占用详情 \n\n ${resource}`,
);
}
}
}
}
export const handleTaskStart = async () => {
if (globalState.ID) {
await updateCron(
[globalState.ID],
'0',
String(process.pid),
globalState.logPath,
globalState.beginTimestamp,
);
}
console.log(`## 开始执行... ${globalState.beginTime}\n`);
};
export const runTaskBefore = async () => {
if (globalState.isMacos === 0) {
await checkServer();
}
await $`. ${fileTaskBefore} "$@"`;
if (globalState.taskBefore) {
console.log('执行前置命令');
await $`eval ${globalState.taskBefore}`;
console.log('执行前置命令结束');
}
};
export const runTaskAfter = async () => {
await $`. ${fileTaskAfter} "$@"`;
if (globalState.taskAfter) {
console.log('执行后置命令');
await $`eval "${globalState.taskAfter}"`;
console.log('执行后置命令结束');
}
};
export const handleTaskEnd = async () => {
const etime = new Date();
const endTime = formatDate(etime);
const endTimestamp = formatTimestamp(etime);
let diffTime = endTimestamp - globalState.beginTimestamp;
if (diffTime === 0) {
diffTime = 1;
}
if (globalState.ID) {
await updateCron(
[globalState.ID],
'1',
`${process.pid}`,
globalState.logPath,
globalState.beginTimestamp,
diffTime,
);
}
console.log(`\n## 执行结束... ${endTime} 耗时 ${diffTime}`);
};
initEnv();
detectTermux();
detectMacos();
await importConfig();
Executable
+94
View File
@@ -0,0 +1,94 @@
#!/usr/bin/env zx
import 'zx/globals';
import { PassThrough } from 'node:stream';
import {
listCrontabUser,
formatLogTime,
formatTimestamp,
formatDate,
dirLog,
dirCli,
globalState,
handleTaskEnd,
} from './share.mjs';
import './api.mjs';
// $.verbose = true;
async function singleHandle() {}
const signals = ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGQUIT', 'SIGTSTP'];
signals.forEach((sig) => process.on(sig, singleHandle));
export async function handleLogPath(fileParam) {
let ID = $.env.ID;
if (!ID) {
const grepResult =
await $`grep -E "task.* ${fileParam}" ${listCrontabUser}`.nothrow();
ID = grepResult.stdout.match(/ID=(\d+)/)?.[1];
}
globalState.ID = ID;
const suffix = ID && parseInt(ID, 10) > 0 ? `_${ID}` : '';
globalState.time = new Date();
const logTime = formatLogTime(globalState.time);
let logDirTmp = path.basename(fileParam);
let logDirTmpPath = '';
if (fileParam.includes('/')) {
logDirTmpPath = path.isAbsolute(fileParam)
? fileParam.substring(1)
: fileParam;
logDirTmpPath = path.basename(path.dirname(logDirTmpPath));
}
if (logDirTmpPath) {
logDirTmp = `${logDirTmpPath}_${logDirTmp}`;
}
const logDir = `${logDirTmp.replace(/\.[^/.]+$/, '')}${suffix}`;
globalState.logDir = logDir;
globalState.logPath = `${logDir}/${logTime}.log`;
if ($.env.real_log_path) {
globalState.logPath = realLogPath;
}
await $`mkdir -p ${dirLog}/${logDir}`;
}
export function initBeginTime() {
globalState.beginTime = formatDate(globalState.time);
globalState.beginTimestamp = formatTimestamp(globalState.time);
}
async function main() {
const taskArgv = minimist(process.argv.slice(3), {
'--': true,
});
const {
_: [scriptFile],
} = taskArgv;
await handleLogPath(scriptFile);
initBeginTime();
cd(`${dirCli}`);
const logStream = fs.createWriteStream(`${dirLog}/${globalState.logPath}`);
const passThrough = new PassThrough();
passThrough.pipe(logStream);
const p = $`./otask.mjs ${process.argv.slice(
3,
)} --GlobalState=${JSON.stringify(globalState)} 2>&1`.nothrow();
p.stdout.pipe(passThrough).pipe(process.stdout);
await p;
}
main()
.then(() => {
process.exit(0);
})
.catch((err) => {
console.error(err);
process.exit(1);
});
+4 -5
View File
@@ -1,9 +1,9 @@
FROM python:3.10-alpine3.18 AS builder FROM python:3.10-alpine3.18 as builder
COPY package.json .npmrc pnpm-lock.yaml /tmp/build/ COPY package.json .npmrc pnpm-lock.yaml /tmp/build/
RUN set -x \ RUN set -x \
&& apk update \ && apk update \
&& apk add nodejs npm git \ && apk add nodejs npm git \
&& npm i -g pnpm@8.3.1 pm2 ts-node \ && npm i -g pnpm@8.3.1 pm2 tsx \
&& cd /tmp/build \ && cd /tmp/build \
&& pnpm install --prod && pnpm install --prod
@@ -15,7 +15,7 @@ ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git
ARG QL_BRANCH=develop ARG QL_BRANCH=develop
ENV PNPM_HOME=/root/.local/share/pnpm \ ENV PNPM_HOME=/root/.local/share/pnpm \
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.local/share/pnpm:/root/.local/share/pnpm/global/5/node_modules \ PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.local/share/pnpm:/root/.local/share/pnpm/global/5/node_modules:$PNPM_HOME \
NODE_PATH=/usr/local/bin:/usr/local/pnpm-global/5/node_modules:/usr/local/lib/node_modules:/root/.local/share/pnpm/global/5/node_modules \ NODE_PATH=/usr/local/bin:/usr/local/pnpm-global/5/node_modules:/usr/local/lib/node_modules:/root/.local/share/pnpm/global/5/node_modules \
LANG=C.UTF-8 \ LANG=C.UTF-8 \
SHELL=/bin/bash \ SHELL=/bin/bash \
@@ -59,8 +59,7 @@ RUN set -x \
&& rm -rf /root/.pnpm-store \ && rm -rf /root/.pnpm-store \
&& rm -rf /root/.local/share/pnpm/store \ && rm -rf /root/.local/share/pnpm/store \
&& rm -rf /root/.cache \ && rm -rf /root/.cache \
&& ulimit -c 0 \ && ulimit -c 0
&& pip3 install requests
ARG SOURCE_COMMIT ARG SOURCE_COMMIT
RUN git clone --depth=1 -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \ RUN git clone --depth=1 -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
+4 -5
View File
@@ -1,9 +1,9 @@
FROM python:3.11-alpine3.18 AS builder FROM python:3.11-alpine3.18 as builder
COPY package.json .npmrc pnpm-lock.yaml /tmp/build/ COPY package.json .npmrc pnpm-lock.yaml /tmp/build/
RUN set -x \ RUN set -x \
&& apk update \ && apk update \
&& apk add nodejs npm git \ && apk add nodejs npm git \
&& npm i -g pnpm@8.3.1 pm2 ts-node \ && npm i -g pnpm@8.3.1 pm2 tsx \
&& cd /tmp/build \ && cd /tmp/build \
&& pnpm install --prod && pnpm install --prod
@@ -15,7 +15,7 @@ ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git
ARG QL_BRANCH=develop ARG QL_BRANCH=develop
ENV PNPM_HOME=/root/.local/share/pnpm \ ENV PNPM_HOME=/root/.local/share/pnpm \
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.local/share/pnpm:/root/.local/share/pnpm/global/5/node_modules \ PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.local/share/pnpm:/root/.local/share/pnpm/global/5/node_modules:$PNPM_HOME \
NODE_PATH=/usr/local/bin:/usr/local/pnpm-global/5/node_modules:/usr/local/lib/node_modules:/root/.local/share/pnpm/global/5/node_modules \ NODE_PATH=/usr/local/bin:/usr/local/pnpm-global/5/node_modules:/usr/local/lib/node_modules:/root/.local/share/pnpm/global/5/node_modules \
LANG=C.UTF-8 \ LANG=C.UTF-8 \
SHELL=/bin/bash \ SHELL=/bin/bash \
@@ -59,8 +59,7 @@ RUN set -x \
&& rm -rf /root/.pnpm-store \ && rm -rf /root/.pnpm-store \
&& rm -rf /root/.local/share/pnpm/store \ && rm -rf /root/.local/share/pnpm/store \
&& rm -rf /root/.cache \ && rm -rf /root/.cache \
&& ulimit -c 0 \ && ulimit -c 0
&& pip3 install requests
ARG SOURCE_COMMIT ARG SOURCE_COMMIT
RUN git clone --depth=1 -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \ RUN git clone --depth=1 -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
+8 -3
View File
@@ -1,10 +1,15 @@
version: '2'
services: services:
web: web:
image: whyour/qinglong:latest # 基于 Debian 的版本:whyour/qinglong:debian # alpine 基础镜像版本
image: whyour/qinglong:latest
# debian-slim 基础镜像版本
# image: whyour/qinglong:debian
volumes: volumes:
- ./data:/ql/data - ./data:/ql/data
ports: ports:
- "5700:5700" - "0.0.0.0:5700:5700"
environment: environment:
QlBaseUrl: '/' # 部署路径非必须,以斜杠开头和结尾,比如 /test/ # 部署路径非必须,以斜杠开头和结尾,比如 /test/
QlBaseUrl: '/'
restart: unless-stopped restart: unless-stopped
+1 -2
View File
@@ -1,11 +1,10 @@
#!/bin/bash #!/bin/bash
dir_shell=/ql/shell dir_shell=/ql/shell
. $dir_shell/share.sh
. $dir_shell/env.sh . $dir_shell/env.sh
. $dir_shell/share.sh
echo -e "======================1. 检测配置文件========================\n" echo -e "======================1. 检测配置文件========================\n"
import_config "$@"
make_dir /etc/nginx/conf.d make_dir /etc/nginx/conf.d
make_dir /run/nginx make_dir /run/nginx
init_nginx init_nginx
+52 -54
View File
@@ -2,24 +2,26 @@
"private": true, "private": true,
"scripts": { "scripts": {
"start": "concurrently -n w: npm:start:*", "start": "concurrently -n w: npm:start:*",
"start:front": "max dev",
"start:back": "nodemon",
"start:update": "ts-node -P tsconfig.back.json ./back/update.ts", "start:update": "ts-node -P tsconfig.back.json ./back/update.ts",
"start:public": "ts-node -P tsconfig.back.json ./back/public.ts", "start:public": "ts-node -P tsconfig.back.json ./back/public.ts",
"start:rpc": "ts-node -P tsconfig.back.json ./back/schedule/index.ts", "start:rpc": "ts-node -P tsconfig.back.json ./back/schedule/index.ts",
"start:back": "nodemon",
"start:front": "max dev",
"build:front": "max build", "build:front": "max build",
"build:back": "tsc -p tsconfig.back.json", "build:back": "tsc -p tsconfig.back.json",
"panel": "npm run build:back && node static/build/app.js", "panel": "npm run build:back && node static/build/app.js",
"schedule": "npm run build:back && node static/build/schedule/index.js", "schedule": "npm run build:back && node static/build/schedule/index.js",
"public": "npm run build:back && node static/build/public.js", "public": "npm run build:back && node static/build/public.js",
"update": "npm run build:back && node static/build/update.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,snakeToCamel=false", "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: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}'", "prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
"postinstall": "max setup 2>/dev/null || true", "postinstall": "max setup 2>/dev/null || true",
"test": "umi-test", "test": "umi-test",
"test:coverage": "umi-test --coverage" "test:coverage": "umi-test --coverage"
}, },
"bin": {
"task": "./cli/task.mjs"
},
"gitHooks": { "gitHooks": {
"pre-commit": "lint-staged" "pre-commit": "lint-staged"
}, },
@@ -55,66 +57,60 @@
"react-dom": "18", "react-dom": "18",
"dva-core": "2" "dva-core": "2"
} }
},
"overrides": {
"sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0.3"
} }
}, },
"dependencies": { "dependencies": {
"@grpc/grpc-js": "^1.12.3", "@grpc/grpc-js": "^1.8.13",
"@grpc/proto-loader": "^0.7.13",
"@otplib/preset-default": "^12.0.1", "@otplib/preset-default": "^12.0.1",
"body-parser": "^1.20.3", "@sentry/node": "^7.12.1",
"celebrate": "^15.0.3", "body-parser": "^1.19.2",
"chokidar": "^4.0.1", "celebrate": "^15.0.1",
"chokidar": "^3.5.3",
"cors": "^2.8.5", "cors": "^2.8.5",
"cron-parser": "^4.9.0", "cron-parser": "^4.2.1",
"cross-spawn": "^7.0.6", "cross-spawn": "^7.0.3",
"dayjs": "^1.11.13", "dayjs": "^1.11.2",
"dotenv": "^16.4.6", "dotenv": "^16.0.0",
"express": "^4.21.2", "express": "^4.17.3",
"express-jwt": "^8.4.1", "express-jwt": "^6.1.1",
"express-rate-limit": "^7.4.1", "express-rate-limit": "^7.0.0",
"express-urlrewrite": "^2.0.3", "express-urlrewrite": "^1.4.0",
"form-data": "^4.0.0", "form-data": "^4.0.0",
"got": "^11.8.2", "got": "^11.8.2",
"hpagent": "^1.2.0", "hpagent": "^1.2.0",
"http-proxy-middleware": "^3.0.3", "http-proxy-middleware": "^2.0.6",
"iconv-lite": "^0.6.3", "iconv-lite": "^0.6.3",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^8.5.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"multer": "1.4.5-lts.1", "multer": "1.4.5-lts.1",
"nedb": "^1.8.0",
"node-schedule": "^2.1.0", "node-schedule": "^2.1.0",
"nodemailer": "^6.9.16", "nodemailer": "^6.7.2",
"p-queue-cjs": "7.3.4", "p-queue-cjs": "7.3.4",
"@bufbuild/protobuf": "^2.2.3", "protobufjs": "^7.3.0",
"pstree.remy": "^1.1.8", "pstree.remy": "^1.1.8",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.1.13",
"sequelize": "^6.37.5", "sequelize": "^6.25.5",
"serve-handler": "^6.1.6", "serve-handler": "^6.1.3",
"sockjs": "^0.3.24", "sockjs": "^0.3.24",
"sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0.3", "sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0.3",
"toad-scheduler": "^3.0.1", "toad-scheduler": "^1.6.0",
"typedi": "^0.10.0", "typedi": "^0.10.0",
"uuid": "^11.0.3", "uuid": "^8.3.2",
"winston": "^3.17.0", "winston": "^3.6.0",
"winston-daily-rotate-file": "^5.0.0", "winston-daily-rotate-file": "^4.7.1",
"zx": "^8.1.4",
"tough-cookie": "^4.0.0",
"request-ip": "3.3.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": { "devDependencies": {
"moment": "2.30.1", "@ant-design/icons": "^4.7.0",
"@ant-design/icons": "^5.0.1",
"@ant-design/pro-layout": "6.38.22", "@ant-design/pro-layout": "6.38.22",
"@codemirror/view": "^6.34.1",
"@codemirror/state": "^6.4.1",
"@monaco-editor/react": "4.2.1", "@monaco-editor/react": "4.2.1",
"@react-hook/resize-observer": "^2.0.2", "@react-hook/resize-observer": "^1.2.6",
"react-router-dom": "6.26.1", "@sentry/react": "^7.12.1",
"@types/body-parser": "^1.19.2", "@types/body-parser": "^1.19.2",
"@types/cors": "^2.8.12", "@types/cors": "^2.8.12",
"@types/cross-spawn": "^6.0.2", "@types/cross-spawn": "^6.0.2",
@@ -125,6 +121,7 @@
"@types/jsonwebtoken": "^8.5.8", "@types/jsonwebtoken": "^8.5.8",
"@types/lodash": "^4.14.185", "@types/lodash": "^4.14.185",
"@types/multer": "^1.4.7", "@types/multer": "^1.4.7",
"@types/nedb": "^1.8.12",
"@types/node": "^17.0.21", "@types/node": "^17.0.21",
"@types/node-schedule": "^1.3.2", "@types/node-schedule": "^1.3.2",
"@types/nodemailer": "^6.4.4", "@types/nodemailer": "^6.4.4",
@@ -137,19 +134,18 @@
"@types/sockjs-client": "^1.5.1", "@types/sockjs-client": "^1.5.1",
"@types/uuid": "^8.3.4", "@types/uuid": "^8.3.4",
"@types/request-ip": "0.0.41", "@types/request-ip": "0.0.41",
"@types/proper-lockfile": "^4.1.4",
"@uiw/codemirror-extensions-langs": "^4.21.9", "@uiw/codemirror-extensions-langs": "^4.21.9",
"@uiw/react-codemirror": "^4.21.9", "@uiw/react-codemirror": "^4.21.9",
"@umijs/max": "^4.4.4", "@umijs/max": "^4.0.72",
"@umijs/ssr-darkreader": "^4.9.45", "@umijs/ssr-darkreader": "^4.9.45",
"ahooks": "^3.7.8", "ahooks": "^3.7.8",
"ansi-to-react": "^6.1.6", "ansi-to-react": "^6.1.6",
"antd": "^4.24.16", "antd": "^4.24.8",
"antd-img-crop": "^4.23.0", "antd-img-crop": "^4.2.3",
"axios": "^1.4.0", "axios": "^1.4.0",
"compression-webpack-plugin": "9.2.0", "compression-webpack-plugin": "9.2.0",
"concurrently": "^7.0.0", "concurrently": "^7.0.0",
"react-hotkeys-hook": "^4.6.1", "react-hotkeys-hook": "^4.4.1",
"file-saver": "2.0.2", "file-saver": "2.0.2",
"lint-staged": "^13.0.3", "lint-staged": "^13.0.3",
"monaco-editor": "0.33.0", "monaco-editor": "0.33.0",
@@ -160,22 +156,24 @@
"qrcode.react": "^1.0.1", "qrcode.react": "^1.0.1",
"query-string": "^7.1.1", "query-string": "^7.1.1",
"rc-tween-one": "^3.0.6", "rc-tween-one": "^3.0.6",
"rc-virtual-list": "3.15.0", "rc-virtual-list": "3.5.3",
"react": "18.3.1", "react": "18.2.0",
"react-copy-to-clipboard": "^5.1.0", "react-copy-to-clipboard": "^5.1.0",
"react-diff-viewer": "^3.1.1", "react-diff-viewer": "^3.1.1",
"react-dnd": "^16.0.1", "react-dnd": "^14.0.2",
"react-dnd-html5-backend": "^16.0.1", "react-dnd-html5-backend": "^14.0.0",
"react-dom": "18.3.1", "react-dom": "18.2.0",
"react-intl-universal": "^2.12.0", "react-intl-universal": "^2.6.21",
"react-split-pane": "^0.1.92", "react-split-pane": "^0.1.92",
"sockjs-client": "^1.6.0", "sockjs-client": "^1.6.0",
"ts-node": "^10.9.2", "ts-node": "^10.6.0",
"ts-proto": "^2.6.1", "ts-proto": "^1.146.0",
"tslib": "^2.4.0", "tslib": "^2.4.0",
"tsx": "^4.7.3",
"typescript": "5.2.2", "typescript": "5.2.2",
"vh-check": "^2.0.5", "vh-check": "^2.0.5",
"virtualizedtableforantd4": "1.3.0", "virtualizedtableforantd4": "1.3.0",
"webpack": "^5.70.0",
"yorkie": "^2.0.0" "yorkie": "^2.0.0"
} }
} }
+6101 -4828
View File
File diff suppressed because it is too large Load Diff
+3 -40
View File
@@ -8,7 +8,7 @@ AutoAddCron="true"
DefaultCronRule="" DefaultCronRule=""
## ql repo命令拉取脚本时需要拉取的文件后缀,直接写文件后缀名即可 ## ql repo命令拉取脚本时需要拉取的文件后缀,直接写文件后缀名即可
RepoFileExtensions="js mjs py pyc" RepoFileExtensions="js py"
## 代理地址,支持HTTP/SOCK5,例如 http://127.0.0.1:7890 ## 代理地址,支持HTTP/SOCK5,例如 http://127.0.0.1:7890
ProxyUrl="" ProxyUrl=""
@@ -43,7 +43,7 @@ BotRepoUrl=""
## 通知环境变量 ## 通知环境变量
## 1. Server酱 ## 1. Server酱
## https://sct.ftqq.com/r/13363 ## https://sct.ftqq.com
## 下方填写 SCHKEY 值或 SendKey 值 ## 下方填写 SCHKEY 值或 SendKey 值
export PUSH_KEY="" export PUSH_KEY=""
@@ -117,16 +117,6 @@ export PUSH_PLUS_TOKEN=""
## 下方填写您的一对多推送的 "群组编码" ,(一对多推送下面->您的群组(如无则新建)->群组编码) ## 下方填写您的一对多推送的 "群组编码" ,(一对多推送下面->您的群组(如无则新建)->群组编码)
## 1. 需订阅者扫描二维码 2、如果您是创建群组所属人,也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送 ## 1. 需订阅者扫描二维码 2、如果您是创建群组所属人,也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送
export PUSH_PLUS_USER="" 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. 微加机器人 ## 9. 微加机器人
## 官方网站:http://www.weplusbot.com ## 官方网站:http://www.weplusbot.com
@@ -185,16 +175,8 @@ export CHRONOCAT_QQ=""
export CHRONOCAT_TOKEN="" export CHRONOCAT_TOKEN=""
## 16. SMTP ## 16. SMTP
## JavaScript 参数
## 邮箱服务名称,比如126、163、Gmail、QQ等,支持列表 https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json ## 邮箱服务名称,比如126、163、Gmail、QQ等,支持列表 https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json
export SMTP_SERVICE="" export SMTP_SERVICE=""
## Python 参数
## SMTP 发送邮件服务器,形如 smtp.exmail.qq.com:465
export SMTP_SERVER=""
## SMTP 发送邮件服务器是否使用 SSL,填写 true 或 false
export SMTP_SSL=""
## smtp_email 填写 SMTP 收发件邮箱,通知将会由自己发给自己 ## smtp_email 填写 SMTP 收发件邮箱,通知将会由自己发给自己
export SMTP_EMAIL="" export SMTP_EMAIL=""
## smtp_password 填写 SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定 ## smtp_password 填写 SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
@@ -221,26 +203,7 @@ export FSKEY=""
export QMSG_KEY="" export QMSG_KEY=""
export QMSG_TYPE="" export QMSG_TYPE=""
## 20.Ntfy ## 20. 自定义通知
## 官方文档: 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 ## 自定义通知 接收回调的URL
export WEBHOOK_URL="" export WEBHOOK_URL=""
## WEBHOOK_BODY 和 WEBHOOK_HEADERS 多个参数时,直接换行或者使用 $'\n' 连接多行字符串,比如 export dd="line 1"$'\n'"line 2" ## WEBHOOK_BODY 和 WEBHOOK_HEADERS 多个参数时,直接换行或者使用 $'\n' 连接多行字符串,比如 export dd="line 1"$'\n'"line 2"
+30 -180
View File
@@ -40,14 +40,9 @@ const push_config = {
CHAT_URL: '', // synology chat url CHAT_URL: '', // synology chat url
CHAT_TOKEN: '', // synology chat token CHAT_TOKEN: '', // synology chat token
// 官方文档:https://www.pushplus.plus/ // 官方文档:http://www.pushplus.plus/
PUSH_PLUS_TOKEN: '', // pushplus 推送的用户令牌 PUSH_PLUS_TOKEN: '', // push+ 微信推送的用户令牌
PUSH_PLUS_USER: '', // pushplus 推送的群组编码 PUSH_PLUS_USER: '', // push+ 微信推送的群组编码
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/ // 微加机器人,官方网站:https://www.weplusbot.com/
WE_PLUS_BOT_TOKEN: '', // 微加机器人的用户令牌 WE_PLUS_BOT_TOKEN: '', // 微加机器人的用户令牌
@@ -100,16 +95,6 @@ const push_config = {
WEBHOOK_HEADERS: '', // 自定义通知 请求头 WEBHOOK_HEADERS: '', // 自定义通知 请求头
WEBHOOK_METHOD: '', // 自定义通知 请求方法 WEBHOOK_METHOD: '', // 自定义通知 请求方法
WEBHOOK_CONTENT_TYPE: '', // 自定义通知 content-type 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) { for (const key in push_config) {
@@ -239,16 +224,11 @@ function serverNotify(text, desp) {
if (PUSH_KEY) { if (PUSH_KEY) {
// 微信server酱推送通知一个\n不会换行,需要两个\n才能换行,故做此替换 // 微信server酱推送通知一个\n不会换行,需要两个\n才能换行,故做此替换
desp = desp.replace(/[\n\r]/g, '\n\n'); desp = desp.replace(/[\n\r]/g, '\n\n');
const matchResult = PUSH_KEY.match(/^sctp(\d+)t/i);
const options = { const options = {
url: url: PUSH_KEY.includes('SCT')
matchResult && matchResult[1] ? `https://sctapi.ftqq.com/${PUSH_KEY}.send`
? `https://${matchResult[1]}.push.ft07.com/send/${PUSH_KEY}.send` : `https://sc.ftqq.com/${PUSH_KEY}.send`,
: `https://sctapi.ftqq.com/${PUSH_KEY}.send`, body: `text=${text}&desp=${desp}`,
body: `text=${encodeURIComponent(text)}&desp=${encodeURIComponent(
desp,
)}`,
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
}, },
@@ -428,7 +408,7 @@ function tgBotNotify(text, desp) {
TG_PROXY_AUTH, TG_PROXY_AUTH,
} = push_config; } = push_config;
if (TG_BOT_TOKEN && TG_USER_ID) { if (TG_BOT_TOKEN && TG_USER_ID) {
let options = { const options = {
url: `${TG_API_HOST}/bot${TG_BOT_TOKEN}/sendMessage`, url: `${TG_API_HOST}/bot${TG_BOT_TOKEN}/sendMessage`,
json: { json: {
chat_id: `${TG_USER_ID}`, chat_id: `${TG_USER_ID}`,
@@ -442,20 +422,20 @@ function tgBotNotify(text, desp) {
}; };
if (TG_PROXY_HOST && TG_PROXY_PORT) { if (TG_PROXY_HOST && TG_PROXY_PORT) {
const { HttpProxyAgent, HttpsProxyAgent } = require('hpagent'); const { HttpProxyAgent, HttpsProxyAgent } = require('hpagent');
const _options = { const options = {
keepAlive: true, keepAlive: true,
keepAliveMsecs: 1000, keepAliveMsecs: 1000,
maxSockets: 256, maxSockets: 256,
maxFreeSockets: 256, maxFreeSockets: 256,
proxy: `http://${TG_PROXY_AUTH}${TG_PROXY_HOST}:${TG_PROXY_PORT}`, proxy: `http://${TG_PROXY_AUTH}${TG_PROXY_HOST}:${TG_PROXY_PORT}`,
}; };
const httpAgent = new HttpProxyAgent(_options); const httpAgent = new HttpProxyAgent(options);
const httpsAgent = new HttpsProxyAgent(_options); const httpsAgent = new HttpsProxyAgent(options);
const agent = { const agent = {
http: httpAgent, http: httpAgent,
https: httpsAgent, https: httpsAgent,
}; };
options.agent = agent; Object.assign(options, { agent });
} }
$.post(options, (err, resp, data) => { $.post(options, (err, resp, data) => {
try { try {
@@ -779,15 +759,7 @@ function iGotNotify(text, desp, params = {}) {
function pushPlusNotify(text, desp) { function pushPlusNotify(text, desp) {
return new Promise((resolve) => { return new Promise((resolve) => {
const { const { PUSH_PLUS_TOKEN, PUSH_PLUS_USER } = push_config;
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) { if (PUSH_PLUS_TOKEN) {
desp = desp.replace(/[\n\r]/g, '<br>'); // 默认为html, 不支持plaintext desp = desp.replace(/[\n\r]/g, '<br>'); // 默认为html, 不支持plaintext
const body = { const body = {
@@ -795,11 +767,6 @@ function pushPlusNotify(text, desp) {
title: `${text}`, title: `${text}`,
content: `${desp}`, content: `${desp}`,
topic: `${PUSH_PLUS_USER}`, 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 = { const options = {
url: `https://www.pushplus.plus/send`, url: `https://www.pushplus.plus/send`,
@@ -813,7 +780,7 @@ function pushPlusNotify(text, desp) {
try { try {
if (err) { if (err) {
console.log( console.log(
`pushplus 发送${ `Push+ 发送${
PUSH_PLUS_USER ? '一对多' : '一对一' PUSH_PLUS_USER ? '一对多' : '一对一'
}通知消息失败😞\n`, }通知消息失败😞\n`,
err, err,
@@ -821,15 +788,13 @@ function pushPlusNotify(text, desp) {
} else { } else {
if (data.code === 200) { if (data.code === 200) {
console.log( console.log(
`pushplus 发送${ `Push+ 发送${
PUSH_PLUS_USER ? '一对多' : '一对一' PUSH_PLUS_USER ? '一对多' : '一对一'
}通知请求成功🎉,可根据流水号查询推送结果:${ }通知消息完成🎉\n`,
data.data
}\n注意:请求成功并不代表推送成功,如未收到消息,请到pushplus官网使用流水号查询推送最终结果`,
); );
} else { } else {
console.log( console.log(
`pushplus 发送${ `Push+ 发送${
PUSH_PLUS_USER ? '一对多' : '一对一' PUSH_PLUS_USER ? '一对多' : '一对一'
}通知消息异常 ${data.msg}\n`, }通知消息异常 ${data.msg}\n`,
); );
@@ -852,7 +817,7 @@ function wePlusBotNotify(text, desp) {
const { WE_PLUS_BOT_TOKEN, WE_PLUS_BOT_RECEIVER, WE_PLUS_BOT_VERSION } = const { WE_PLUS_BOT_TOKEN, WE_PLUS_BOT_RECEIVER, WE_PLUS_BOT_VERSION } =
push_config; push_config;
if (WE_PLUS_BOT_TOKEN) { if (WE_PLUS_BOT_TOKEN) {
let template = 'txt'; const template = 'txt';
if (desp.length > 800) { if (desp.length > 800) {
desp = desp.replace(/[\n\r]/g, '<br>'); desp = desp.replace(/[\n\r]/g, '<br>');
template = 'html'; template = 'html';
@@ -1178,20 +1143,14 @@ function webhookNotify(text, desp) {
WEBHOOK_CONTENT_TYPE, WEBHOOK_CONTENT_TYPE,
WEBHOOK_METHOD, WEBHOOK_METHOD,
} = push_config; } = push_config;
if ( if (!WEBHOOK_URL.includes('$title') && !WEBHOOK_BODY.includes('$title')) {
!WEBHOOK_METHOD ||
!WEBHOOK_URL ||
(!WEBHOOK_URL.includes('$title') && !WEBHOOK_BODY.includes('$title'))
) {
resolve(); resolve();
return; return;
} }
const headers = parseHeaders(WEBHOOK_HEADERS); const headers = parseHeaders(WEBHOOK_HEADERS);
const body = parseBody(WEBHOOK_BODY, WEBHOOK_CONTENT_TYPE, (v) => const body = parseBody(WEBHOOK_BODY, WEBHOOK_CONTENT_TYPE, (v) =>
v v?.replaceAll('$title', text)?.replaceAll('$content', desp),
?.replaceAll('$title', text?.replaceAll('\n', '\\n'))
?.replaceAll('$content', desp?.replaceAll('\n', '\\n')),
); );
const bodyParam = formatBodyFun(WEBHOOK_CONTENT_TYPE, body); const bodyParam = formatBodyFun(WEBHOOK_CONTENT_TYPE, body);
const options = { const options = {
@@ -1203,129 +1162,22 @@ function webhookNotify(text, desp) {
retry: 1, retry: 1,
}; };
const formatUrl = WEBHOOK_URL.replaceAll( if (WEBHOOK_METHOD) {
'$title', const formatUrl = WEBHOOK_URL.replaceAll(
encodeURIComponent(text), '$title',
).replaceAll('$content', encodeURIComponent(desp)); encodeURIComponent(text),
got(formatUrl, options).then((resp) => { ).replaceAll('$content', encodeURIComponent(desp));
try { got(formatUrl, options).then((resp) => {
if (resp.statusCode !== 200) {
console.log(`自定义发送通知消息失败😞 ${resp.body}\n`);
} else {
console.log(`自定义发送通知消息成功🎉 ${resp.body}\n`);
}
} catch (e) {
$.logErr(e, resp);
} finally {
resolve(resp.body);
}
});
});
}
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 { try {
if (err) { if (resp.statusCode !== 200) {
console.log('Ntfy 通知调用API失败😞\n', err); console.log(`自定义发送通知消息失败😞 ${resp.body}\n`);
} else { } else {
if (data.id) { console.log(`自定义发送通知消息成功🎉 ${resp.body}\n`);
console.log('Ntfy 发送通知消息成功🎉\n');
} else {
console.log(`Ntfy 发送通知消息异常 ${JSON.stringify(data)}`);
}
} }
} catch (e) { } catch (e) {
$.logErr(e, resp); $.logErr(e, resp);
} finally { } finally {
resolve(data); resolve(resp.body);
}
});
} 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 { } else {
@@ -1462,8 +1314,6 @@ async function sendNotify(text, desp, params = {}) {
chronocatNotify(text, desp), // Chronocat chronocatNotify(text, desp), // Chronocat
webhookNotify(text, desp), // 自定义通知 webhookNotify(text, desp), // 自定义通知
qmsgNotify(text, desp), // 自定义通知 qmsgNotify(text, desp), // 自定义通知
ntfyNotify(text, desp), // Ntfy
wxPusherNotify(text, desp), // wxpusher
]); ]);
} }
+37 -138
View File
@@ -72,13 +72,8 @@ push_config = {
'CHAT_URL': '', # synology chat url 'CHAT_URL': '', # synology chat url
'CHAT_TOKEN': '', # synology chat token 'CHAT_TOKEN': '', # synology chat token
'PUSH_PLUS_TOKEN': '', # pushplus 推送的用户令牌 'PUSH_PLUS_TOKEN': '', # push+ 微信推送的用户令牌
'PUSH_PLUS_USER': '', # pushplus 推送的群组编码 'PUSH_PLUS_USER': '', # push+ 微信推送的群组编码
'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_TOKEN': '', # 微加机器人的用户令牌
'WE_PLUS_BOT_RECEIVER': '', # 微加机器人的消息接收者 'WE_PLUS_BOT_RECEIVER': '', # 微加机器人的消息接收者
@@ -121,15 +116,7 @@ push_config = {
'WEBHOOK_BODY': '', # 自定义通知 请求体 'WEBHOOK_BODY': '', # 自定义通知 请求体
'WEBHOOK_HEADERS': '', # 自定义通知 请求头 'WEBHOOK_HEADERS': '', # 自定义通知 请求头
'WEBHOOK_METHOD': '', # 自定义通知 请求方法 '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 # fmt: on
@@ -144,6 +131,7 @@ def bark(title: str, content: str) -> None:
使用 bark 推送消息。 使用 bark 推送消息。
""" """
if not push_config.get("BARK_PUSH"): if not push_config.get("BARK_PUSH"):
print("bark 服务的 BARK_PUSH 未设置!!\n取消推送")
return return
print("bark 服务启动") print("bark 服务启动")
@@ -195,6 +183,7 @@ def dingding_bot(title: str, content: str) -> None:
使用 钉钉机器人 推送消息。 使用 钉钉机器人 推送消息。
""" """
if not push_config.get("DD_BOT_SECRET") or not push_config.get("DD_BOT_TOKEN"): 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 return
print("钉钉机器人 服务启动") print("钉钉机器人 服务启动")
@@ -224,6 +213,7 @@ def feishu_bot(title: str, content: str) -> None:
使用 飞书机器人 推送消息。 使用 飞书机器人 推送消息。
""" """
if not push_config.get("FSKEY"): if not push_config.get("FSKEY"):
print("飞书 服务的 FSKEY 未设置!!\n取消推送")
return return
print("飞书 服务启动") print("飞书 服务启动")
@@ -242,6 +232,7 @@ def go_cqhttp(title: str, content: str) -> None:
使用 go_cqhttp 推送消息。 使用 go_cqhttp 推送消息。
""" """
if not push_config.get("GOBOT_URL") or not push_config.get("GOBOT_QQ"): if not push_config.get("GOBOT_URL") or not push_config.get("GOBOT_QQ"):
print("go-cqhttp 服务的 GOBOT_URL 或 GOBOT_QQ 未设置!!\n取消推送")
return return
print("go-cqhttp 服务启动") print("go-cqhttp 服务启动")
@@ -259,6 +250,7 @@ def gotify(title: str, content: str) -> None:
使用 gotify 推送消息。 使用 gotify 推送消息。
""" """
if not push_config.get("GOTIFY_URL") or not push_config.get("GOTIFY_TOKEN"): if not push_config.get("GOTIFY_URL") or not push_config.get("GOTIFY_TOKEN"):
print("gotify 服务的 GOTIFY_URL 或 GOTIFY_TOKEN 未设置!!\n取消推送")
return return
print("gotify 服务启动") print("gotify 服务启动")
@@ -281,6 +273,7 @@ def iGot(title: str, content: str) -> None:
使用 iGot 推送消息。 使用 iGot 推送消息。
""" """
if not push_config.get("IGOT_PUSH_KEY"): if not push_config.get("IGOT_PUSH_KEY"):
print("iGot 服务的 IGOT_PUSH_KEY 未设置!!\n取消推送")
return return
print("iGot 服务启动") print("iGot 服务启动")
@@ -300,18 +293,15 @@ def serverJ(title: str, content: str) -> None:
通过 serverJ 推送消息。 通过 serverJ 推送消息。
""" """
if not push_config.get("PUSH_KEY"): if not push_config.get("PUSH_KEY"):
print("serverJ 服务的 PUSH_KEY 未设置!!\n取消推送")
return return
print("serverJ 服务启动") print("serverJ 服务启动")
data = {"text": title, "desp": content.replace("\n", "\n\n")} data = {"text": title, "desp": content.replace("\n", "\n\n")}
if push_config.get("PUSH_KEY").find("SCT") != -1:
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://sctapi.ftqq.com/{push_config.get("PUSH_KEY")}.send' url = f'https://sctapi.ftqq.com/{push_config.get("PUSH_KEY")}.send'
else:
url = f'https://sc.ftqq.com/{push_config.get("PUSH_KEY")}.send'
response = requests.post(url, data=data).json() response = requests.post(url, data=data).json()
if response.get("errno") == 0 or response.get("code") == 0: if response.get("errno") == 0 or response.get("code") == 0:
@@ -325,6 +315,7 @@ def pushdeer(title: str, content: str) -> None:
通过PushDeer 推送消息 通过PushDeer 推送消息
""" """
if not push_config.get("DEER_KEY"): if not push_config.get("DEER_KEY"):
print("PushDeer 服务的 DEER_KEY 未设置!!\n取消推送")
return return
print("PushDeer 服务启动") print("PushDeer 服务启动")
data = { data = {
@@ -350,6 +341,7 @@ def chat(title: str, content: str) -> None:
通过Chat 推送消息 通过Chat 推送消息
""" """
if not push_config.get("CHAT_URL") or not push_config.get("CHAT_TOKEN"): if not push_config.get("CHAT_URL") or not push_config.get("CHAT_TOKEN"):
print("chat 服务的 CHAT_URL或CHAT_TOKEN 未设置!!\n取消推送")
return return
print("chat 服务启动") print("chat 服务启动")
data = "payload=" + json.dumps({"text": title + "\n" + content}) data = "payload=" + json.dumps({"text": title + "\n" + content})
@@ -364,36 +356,26 @@ def chat(title: str, content: str) -> None:
def pushplus_bot(title: str, content: str) -> None: def pushplus_bot(title: str, content: str) -> None:
""" """
通过 pushplus 推送消息。 通过 push+ 推送消息。
""" """
if not push_config.get("PUSH_PLUS_TOKEN"): if not push_config.get("PUSH_PLUS_TOKEN"):
print("PUSHPLUS 服务的 PUSH_PLUS_TOKEN 未设置!!\n取消推送")
return return
print("PUSHPLUS 服务启动") print("PUSHPLUS 服务启动")
url = "https://www.pushplus.plus/send" url = "http://www.pushplus.plus/send"
data = { data = {
"token": push_config.get("PUSH_PLUS_TOKEN"), "token": push_config.get("PUSH_PLUS_TOKEN"),
"title": title, "title": title,
"content": content, "content": content,
"topic": push_config.get("PUSH_PLUS_USER"), "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") body = json.dumps(data).encode(encoding="utf-8")
headers = {"Content-Type": "application/json"} headers = {"Content-Type": "application/json"}
response = requests.post(url=url, data=body, headers=headers).json() response = requests.post(url=url, data=body, headers=headers).json()
code = response["code"] if response["code"] == 200:
if code == 200: print("PUSHPLUS 推送成功!")
print("PUSHPLUS 推送请求成功,可根据流水号查询推送结果:" + response["data"])
print(
"注意:请求成功并不代表推送成功,如未收到消息,请到pushplus官网使用流水号查询推送最终结果"
)
elif code == 900 or code == 903 or code == 905 or code == 999:
print(response["msg"])
else: else:
url_old = "http://pushplus.hxtrip.com/send" url_old = "http://pushplus.hxtrip.com/send"
@@ -412,6 +394,7 @@ def weplus_bot(title: str, content: str) -> None:
通过 微加机器人 推送消息。 通过 微加机器人 推送消息。
""" """
if not push_config.get("WE_PLUS_BOT_TOKEN"): if not push_config.get("WE_PLUS_BOT_TOKEN"):
print("微加机器人 服务的 WE_PLUS_BOT_TOKEN 未设置!!\n取消推送")
return return
print("微加机器人 服务启动") print("微加机器人 服务启动")
@@ -443,6 +426,7 @@ def qmsg_bot(title: str, content: str) -> None:
使用 qmsg 推送消息。 使用 qmsg 推送消息。
""" """
if not push_config.get("QMSG_KEY") or not push_config.get("QMSG_TYPE"): if not push_config.get("QMSG_KEY") or not push_config.get("QMSG_TYPE"):
print("qmsg 的 QMSG_KEY 或者 QMSG_TYPE 未设置!!\n取消推送")
return return
print("qmsg 服务启动") print("qmsg 服务启动")
@@ -461,10 +445,11 @@ def wecom_app(title: str, content: str) -> None:
通过 企业微信 APP 推送消息。 通过 企业微信 APP 推送消息。
""" """
if not push_config.get("QYWX_AM"): if not push_config.get("QYWX_AM"):
print("QYWX_AM 未设置!!\n取消推送")
return return
QYWX_AM_AY = re.split(",", push_config.get("QYWX_AM")) QYWX_AM_AY = re.split(",", push_config.get("QYWX_AM"))
if 4 < len(QYWX_AM_AY) > 5: if 4 < len(QYWX_AM_AY) > 5:
print("QYWX_AM 设置错误!!") print("QYWX_AM 设置错误!!\n取消推送")
return return
print("企业微信 APP 服务启动") print("企业微信 APP 服务启动")
@@ -557,6 +542,7 @@ def wecom_bot(title: str, content: str) -> None:
通过 企业微信机器人 推送消息。 通过 企业微信机器人 推送消息。
""" """
if not push_config.get("QYWX_KEY"): if not push_config.get("QYWX_KEY"):
print("企业微信机器人 服务的 QYWX_KEY 未设置!!\n取消推送")
return return
print("企业微信机器人服务启动") print("企业微信机器人服务启动")
@@ -582,6 +568,7 @@ def telegram_bot(title: str, content: str) -> None:
使用 telegram 机器人 推送消息。 使用 telegram 机器人 推送消息。
""" """
if not push_config.get("TG_BOT_TOKEN") or not push_config.get("TG_USER_ID"): if not push_config.get("TG_BOT_TOKEN") or not push_config.get("TG_USER_ID"):
print("tg 服务的 bot_token 或者 user_id 未设置!!\n取消推送")
return return
print("tg 服务启动") print("tg 服务启动")
@@ -630,6 +617,9 @@ def aibotk(title: str, content: str) -> None:
or not push_config.get("AIBOTK_TYPE") or not push_config.get("AIBOTK_TYPE")
or not push_config.get("AIBOTK_NAME") or not push_config.get("AIBOTK_NAME")
): ):
print(
"智能微秘书 的 AIBOTK_KEY 或者 AIBOTK_TYPE 或者 AIBOTK_NAME 未设置!!\n取消推送"
)
return return
print("智能微秘书 服务启动") print("智能微秘书 服务启动")
@@ -668,6 +658,9 @@ def smtp(title: str, content: str) -> None:
or not push_config.get("SMTP_PASSWORD") or not push_config.get("SMTP_PASSWORD")
or not push_config.get("SMTP_NAME") or not push_config.get("SMTP_NAME")
): ):
print(
"SMTP 邮件 的 SMTP_SERVER 或者 SMTP_SSL 或者 SMTP_EMAIL 或者 SMTP_PASSWORD 或者 SMTP_NAME 未设置!!\n取消推送"
)
return return
print("SMTP 邮件 服务启动") print("SMTP 邮件 服务启动")
@@ -711,6 +704,7 @@ def pushme(title: str, content: str) -> None:
使用 PushMe 推送消息。 使用 PushMe 推送消息。
""" """
if not push_config.get("PUSHME_KEY"): if not push_config.get("PUSHME_KEY"):
print("PushMe 服务的 PUSHME_KEY 未设置!!\n取消推送")
return return
print("PushMe 服务启动") print("PushMe 服务启动")
@@ -743,6 +737,7 @@ def chronocat(title: str, content: str) -> None:
or not push_config.get("CHRONOCAT_QQ") or not push_config.get("CHRONOCAT_QQ")
or not push_config.get("CHRONOCAT_TOKEN") or not push_config.get("CHRONOCAT_TOKEN")
): ):
print("CHRONOCAT 服务的 CHRONOCAT_URL 或 CHRONOCAT_QQ 未设置!!\n取消推送")
return return
print("CHRONOCAT 服务启动") print("CHRONOCAT 服务启动")
@@ -782,96 +777,6 @@ def chronocat(title: str, content: str) -> None:
print(f"QQ群消息:{ids}推送失败!") 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): def parse_headers(headers):
if not headers: if not headers:
return {} return {}
@@ -928,6 +833,7 @@ def custom_notify(title: str, content: str) -> None:
通过 自定义通知 推送消息。 通过 自定义通知 推送消息。
""" """
if not push_config.get("WEBHOOK_URL") or not push_config.get("WEBHOOK_METHOD"): if not push_config.get("WEBHOOK_URL") or not push_config.get("WEBHOOK_METHOD"):
print("自定义通知的 WEBHOOK_URL 或 WEBHOOK_METHOD 未设置!!\n取消推送")
return return
print("自定义通知服务启动") print("自定义通知服务启动")
@@ -946,9 +852,7 @@ def custom_notify(title: str, content: str) -> None:
body = parse_body( body = parse_body(
WEBHOOK_BODY, WEBHOOK_BODY,
WEBHOOK_CONTENT_TYPE, WEBHOOK_CONTENT_TYPE,
lambda v: v.replace("$title", title.replace("\n", "\\n")).replace( lambda v: v.replace("$title", title).replace("$content", content),
"$content", content.replace("\n", "\\n")
),
) )
formatted_url = WEBHOOK_URL.replace( formatted_url = WEBHOOK_URL.replace(
"$title", urllib.parse.quote_plus(title) "$title", urllib.parse.quote_plus(title)
@@ -1031,12 +935,7 @@ def add_notify_function():
notify_function.append(chronocat) notify_function.append(chronocat)
if push_config.get("WEBHOOK_URL") and push_config.get("WEBHOOK_METHOD"): if push_config.get("WEBHOOK_URL") and push_config.get("WEBHOOK_METHOD"):
notify_function.append(custom_notify) 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: if not notify_function:
print(f"无推送渠道,请检查通知变量是否正确") print(f"无推送渠道,请检查通知变量是否正确")
return notify_function return notify_function
File diff suppressed because it is too large Load Diff
+4 -8
View File
@@ -4,12 +4,8 @@
* 定时规则 * 定时规则
* cron: 1 9 * * * * cron: 1 9 * * *
*/ */
const { sendNotify } = require('./sendNotify.js'); // commonjs
// import { sendNotify } from './sendNotify'; // es6
console.log('test scripts'); console.log('test scripts');
QLAPI.notify('test scripts', 'test desc'); sendNotify('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');
+2 -6
View File
@@ -4,11 +4,7 @@ name: script name
定时规则 定时规则
cron: 1 9 * * * cron: 1 9 * * *
""" """
import notify
print("test script") print("test script")
print(QLAPI.notify("test script", "test desc")) notify.send('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")
+11 -40
View File
@@ -1,17 +1,17 @@
#!/usr/bin/env bash #!/usr/bin/env bash
create_token() { create_token() {
local token_command="ts-node-transpile-only ${dir_root}/back/token.ts" local token_command="tsx ${dir_root}/back/token.ts"
local token_file="${dir_root}/static/build/token.js" local token_file="${dir_root}/static/build/token.js"
if [[ -f $token_file ]]; then if [[ -f $token_file ]]; then
token_command="node ${token_file}" token_command="node ${token_file}"
fi fi
__ql_token__=$(eval "$token_command") token=$(eval "$token_command")
} }
get_token() { get_token() {
if [[ -f $file_auth_token ]]; then if [[ -f $file_auth_token ]]; then
__ql_token__=$(cat $file_auth_token | jq -r .value) token=$(cat $file_auth_token | jq -r .value)
local expiration=$(cat $file_auth_token | jq -r .expiration) local expiration=$(cat $file_auth_token | jq -r .expiration)
local currentTimeStamp=$(date +%s) local currentTimeStamp=$(date +%s)
if [[ $currentTimeStamp -ge $expiration ]]; then if [[ $currentTimeStamp -ge $expiration ]]; then
@@ -43,7 +43,7 @@ add_cron_api() {
local api=$( local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \ curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
-H "Accept: application/json" \ -H "Accept: application/json" \
-H "Authorization: Bearer ${__ql_token__}" \ -H "Authorization: Bearer $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 "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 "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \ -H "Origin: http://0.0.0.0:5700" \
@@ -79,7 +79,7 @@ update_cron_api() {
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \ curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
-X 'PUT' \ -X 'PUT' \
-H "Accept: application/json" \ -H "Accept: application/json" \
-H "Authorization: Bearer ${__ql_token__}" \ -H "Authorization: Bearer $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 "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 "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \ -H "Origin: http://0.0.0.0:5700" \
@@ -111,7 +111,7 @@ update_cron_command_api() {
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \ curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
-X 'PUT' \ -X 'PUT' \
-H "Accept: application/json" \ -H "Accept: application/json" \
-H "Authorization: Bearer ${__ql_token__}" \ -H "Authorization: Bearer $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 "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 "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \ -H "Origin: http://0.0.0.0:5700" \
@@ -136,7 +136,7 @@ del_cron_api() {
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \ curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
-X 'DELETE' \ -X 'DELETE' \
-H "Accept: application/json" \ -H "Accept: application/json" \
-H "Authorization: Bearer ${__ql_token__}" \ -H "Authorization: Bearer $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 "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 "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \ -H "Origin: http://0.0.0.0:5700" \
@@ -166,7 +166,7 @@ update_cron() {
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons/status?t=$currentTimeStamp" \ curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons/status?t=$currentTimeStamp" \
-X 'PUT' \ -X 'PUT' \
-H "Accept: application/json" \ -H "Accept: application/json" \
-H "Authorization: Bearer ${__ql_token__}" \ -H "Authorization: Bearer $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 "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 "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \ -H "Origin: http://0.0.0.0:5700" \
@@ -178,10 +178,7 @@ update_cron() {
code=$(echo "$api" | jq -r .code) code=$(echo "$api" | jq -r .code)
message=$(echo "$api" | jq -r .message) message=$(echo "$api" | jq -r .message)
if [[ $code != 200 ]]; then if [[ $code != 200 ]]; then
if [[ ! $message ]]; then echo -e "\n## 更新任务状态失败(${message})\n"
message="$api"
fi
echo -e "${message}"
fi fi
} }
@@ -193,7 +190,7 @@ notify_api() {
curl -s --noproxy "*" "http://0.0.0.0:5600/open/system/notify?t=$currentTimeStamp" \ curl -s --noproxy "*" "http://0.0.0.0:5600/open/system/notify?t=$currentTimeStamp" \
-X 'PUT' \ -X 'PUT' \
-H "Accept: application/json" \ -H "Accept: application/json" \
-H "Authorization: Bearer ${__ql_token__}" \ -H "Authorization: Bearer $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 "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 "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \ -H "Origin: http://0.0.0.0:5700" \
@@ -217,7 +214,7 @@ find_cron_api() {
local api=$( local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons/detail?$params&t=$currentTimeStamp" \ curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons/detail?$params&t=$currentTimeStamp" \
-H "Accept: application/json" \ -H "Accept: application/json" \
-H "Authorization: Bearer ${__ql_token__}" \ -H "Authorization: Bearer $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 "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 "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \ -H "Origin: http://0.0.0.0:5700" \
@@ -234,30 +231,4 @@ find_cron_api() {
fi 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 get_token
+1 -1
View File
@@ -73,7 +73,7 @@ check_pm2() {
main() { main() {
echo -e "=====> 开始检测" echo -e "=====> 开始检测"
npm i -g pnpm@8.3.1 pm2 ts-node npm i -g pnpm@8.3.1 pm2 tsx
patch_version patch_version
reset_env reset_env
+65 -113
View File
@@ -69,18 +69,26 @@ run_nohup() {
nohup node $file_name &>$log_path & nohup node $file_name &>$log_path &
} }
env_str_to_array() { check_server() {
. $file_env if [[ $cpu_warn ]] && [[ $mem_warn ]] && [[ $disk_warn ]]; then
local IFS="&" local top_result=$(top -b -n 1)
read -ra array <<<"${!env_param}" cpu_use=$(echo "$top_result" | grep CPU | grep -v -E 'grep|PID' | awk '{print $2}' | cut -f 1 -d "%" | head -n 1)
array_length=${#array[@]}
clear_env mem_free=$(free -m | grep "Mem" | awk '{print $3}' | head -n 1)
mem_total=$(free -m | grep "Mem" | awk '{print $2}' | head -n 1)
mem_use=$(printf "%d%%" $((mem_free * 100 / mem_total)) | cut -f 1 -d "%" | head -n 1)
disk_use=$(df -P | grep /dev | grep -v -E '(tmp|boot|shm)' | awk '{print $5}' | cut -f 1 -d "%" | head -n 1)
if [[ $cpu_use -gt $cpu_warn ]] && [[ $cpu_warn ]] || [[ $mem_free -lt $mem_warn ]] || [[ $disk_use -gt $disk_warn ]]; then
local resource=$(echo "$top_result" | grep -v -E 'grep|Mem|idle|Load|tr' | awk '{$2="";$3="";$4="";$5="";$7="";print $0}' | head -n 10 | tr '\n' '|' | sed s/\|/\\\\n/g)
notify_api "服务器资源异常警告" "当前CPU占用 $cpu_use% 内存占用 $mem_use% 磁盘占用 $disk_use% \n资源占用详情 \n\n $resource"
fi
fi
} }
clear_non_sh_env() { env_str_to_array() {
if [[ $file_param != *.sh ]]; then local IFS="&"
clear_env read -ra array <<<"${!env_param}"
fi
} }
## 正常运行单个脚本,$1:传入参数 ## 正常运行单个脚本,$1:传入参数
@@ -97,36 +105,38 @@ run_normal() {
file_param=${file_param/$relative_path\//} file_param=${file_param/$relative_path\//}
fi fi
if [[ $isJsOrPythonFile == 'false' ]]; then
clear_non_sh_env
fi
$timeoutCmd $which_program $file_param "${script_params[@]}" $timeoutCmd $which_program $file_param "${script_params[@]}"
} }
handle_env_split() {
if [[ ! $num_param ]]; then
num_param="1-max"
fi
env_str_to_array
local tempArr=$(echo $num_param | sed "s/-max/-${array_length}/g" | sed "s/max-/${array_length}-/g" | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g")
local runArr=($(eval echo $tempArr))
array_run=($(awk -v RS=' ' '!a[$1]++' <<<${runArr[@]}))
}
## 并发执行时,设定的 RandomDelay 不会生效,即所有任务立即执行 ## 并发执行时,设定的 RandomDelay 不会生效,即所有任务立即执行
run_concurrent() { run_concurrent() {
local file_param="$1" local file_param="$1"
local env_param="$2" local env_param="$2"
local num_param=$(echo "$3" | perl -pe "s|.*$2(.*)|\1|" | awk '{$1=$1};1') local num_param=$(echo "$3" | perl -pe "s|.*$2(.*)|\1|")
if [[ ! $env_param ]]; then if [[ ! $env_param ]]; then
echo -e "\n 缺少并发运行的环境变量参数" echo -e "\n 缺少并发运行的环境变量参数"
exit 1 exit 1
fi fi
handle_env_split env_str_to_array
time=$(date "+$mtime_format") local tempArr=$(echo $num_param | sed "s/-max/-${#array[@]}/g" | sed "s/max-/${#array[@]}-/g" | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g")
single_log_time=$(format_log_time "$mtime_format" "$time") local runArr=($(eval echo $tempArr))
runArr=($(awk -v RS=' ' '!a[$1]++' <<<${runArr[@]}))
local n=0
for i in ${runArr[@]}; do
array_run[n]=${array[$i - 1]}
let n++
done
local cookieStr=$(
IFS="&"
echo "${array_run[*]}"
)
[[ ! -z $cookieStr ]] && export "${env_param}=${cookieStr}"
env_str_to_array
single_log_time=$(date "+%Y-%m-%d-%H-%M-%S.%3N")
cd $dir_scripts cd $dir_scripts
local relative_path="${file_param%/*}" local relative_path="${file_param%/*}"
@@ -134,24 +144,15 @@ run_concurrent() {
cd ${relative_path} cd ${relative_path}
file_param=${file_param/$relative_path\//} file_param=${file_param/$relative_path\//}
fi fi
for i in "${!array[@]}"; do
local j=0 export "${env_param}=${array[i]}"
for i in ${array_run[@]}; do single_log_path="$dir_log/$log_dir/${single_log_time}_$((i + 1)).log"
single_log_path="$dir_log/$log_dir/${single_log_time}_$((j + 1)).log" eval $timeoutCmd $which_program $file_param "${script_params[@]}" &>$single_log_path &
let j++
if [[ $isJsOrPythonFile == 'false' ]]; then
export "${env_param}=${array[$i - 1]}"
clear_non_sh_env
fi
eval envParam="${env_param}" numParam="${i}" $timeoutCmd $which_program $file_param "${script_params[@]}" &>$single_log_path &
done done
wait wait
local k=0 for i in "${!array[@]}"; do
for i in ${array_run[@]}; do single_log_path="$dir_log/$log_dir/${single_log_time}_$((i + 1)).log"
single_log_path="$dir_log/$log_dir/${single_log_time}_$((k + 1)).log"
let k++
cat $single_log_path cat $single_log_path
[[ -f $single_log_path ]] && rm -f $single_log_path [[ -f $single_log_path ]] && rm -f $single_log_path
done done
@@ -160,27 +161,28 @@ run_concurrent() {
run_designated() { run_designated() {
local file_param="$1" local file_param="$1"
local env_param="$2" local env_param="$2"
local num_param=$(echo "$3" | perl -pe "s|.*$2(.*)|\1|" | awk '{$1=$1};1') local num_param=$(echo "$3" | perl -pe "s|.*$2(.*)|\1|")
if [[ ! $env_param ]]; then if [[ ! $env_param ]] || [[ ! $num_param ]]; then
echo -e "\n 缺少单独运行的参数 task xxx.js desi Test" echo -e "\n 缺少单独运行的参数 task xxx.js desi Test 1 3"
exit 1 exit 1
fi fi
handle_env_split env_str_to_array
local tempArr=$(echo $num_param | sed "s/-max/-${#array[@]}/g" | sed "s/max-/${#array[@]}-/g" | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g")
local runArr=($(eval echo $tempArr))
runArr=($(awk -v RS=' ' '!a[$1]++' <<<${runArr[@]}))
if [[ $isJsOrPythonFile == 'false' ]]; then local n=0
local n=0 for i in ${runArr[@]}; do
for i in ${array_run[@]}; do array_run[n]=${array[$i - 1]}
array_str[n]=${array[$i - 1]} let n++
let n++ done
done
local envStr=$( local cookieStr=$(
IFS="&" IFS="&"
echo "${array_str[*]}" echo "${array_run[*]}"
) )
[[ ! -z $envStr ]] && export "${env_param}=${envStr}" [[ ! -z $cookieStr ]] && export "${env_param}=${cookieStr}"
clear_non_sh_env
fi
cd $dir_scripts cd $dir_scripts
local relative_path="${file_param%/*}" local relative_path="${file_param%/*}"
@@ -188,8 +190,7 @@ run_designated() {
cd ${relative_path} cd ${relative_path}
file_param=${file_param/$relative_path\//} file_param=${file_param/$relative_path\//}
fi fi
$timeoutCmd $which_program $file_param "${script_params[@]}"
envParam="${env_param}" numParam="${num_param}" $timeoutCmd $which_program $file_param "${script_params[@]}"
} }
## 运行其他命令 ## 运行其他命令
@@ -205,48 +206,12 @@ run_else() {
shift shift
clear_non_sh_env
$timeoutCmd $which_program $file_param "$@" $timeoutCmd $which_program $file_param "$@"
} }
check_file() { ## 命令检测
isJsOrPythonFile="false"
if [[ $1 == *.js ]] || [[ $1 == *.mjs ]] || [[ $1 == *.py ]] || [[ $1 == *.pyc ]] || [[ $1 == *.ts ]]; then
isJsOrPythonFile="true"
fi
if [[ -f $file_env ]]; then
get_env_array
if [[ $isJsOrPythonFile == 'true' ]]; then
export PREV_NODE_OPTIONS="${NODE_OPTIONS:=}"
export PREV_PYTHONPATH="${PYTHONPATH:=}"
if [[ $1 == *.js ]] || [[ $1 == *.ts ]] || [[ $1 == *.mjs ]]; then
export NODE_OPTIONS="-r ${file_preload_js} ${NODE_OPTIONS}"
else
export PYTHONPATH="${dir_preload}:${dir_config}:${PYTHONPATH}"
fi
else
. $file_env
fi
fi
}
check_nounset() {
local output=$(set -o)
while read -r line; do
if [[ "$line" =~ "nounset" ]] && [[ "$line" =~ "on" ]]; then
set_u_on="true"
set +u
break
fi
done <<<"$output"
}
main() { main() {
if [[ $1 == *.js ]] || [[ $1 == *.py ]] || [[ $1 == *.pyc ]] || [[ $1 == *.sh ]] || [[ $1 == *.ts ]]; then if [[ $1 == *.js ]] || [[ $1 == *.py ]] || [[ $1 == *.pyc ]] || [[ $1 == *.sh ]] || [[ $1 == *.ts ]]; then
if [[ $1 == *.sh ]]; then
timeoutCmd=""
fi
case $# in case $# in
1) 1)
run_normal "$1" run_normal "$1"
@@ -277,20 +242,7 @@ main() {
} }
handle_task_start "${task_shell_params[@]}" handle_task_start "${task_shell_params[@]}"
check_file "${task_shell_params[@]}" run_task_before "${task_shell_params[@]}"
if [[ $isJsOrPythonFile == 'false' ]]; then
run_task_before "${task_shell_params[@]}"
fi
set_u_on="false"
check_nounset
main "${task_shell_params[@]}" main "${task_shell_params[@]}"
if [[ "$set_u_on" == 'true' ]]; then
set -u
fi
if [[ $isJsOrPythonFile == 'true' ]]; then
export NODE_OPTIONS="${PREV_NODE_OPTIONS}"
export PYTHONPATH="${PREV_PYTHONPATH}"
fi
run_task_after "${task_shell_params[@]}" run_task_after "${task_shell_params[@]}"
clear_env
handle_task_end "${task_shell_params[@]}" handle_task_end "${task_shell_params[@]}"
-154
View File
@@ -1,154 +0,0 @@
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,
'grpc.keepalive_time_ms': 120000,
'grpc.keepalive_timeout_ms': 20000,
'grpc.max_receive_message_length': 100 * 1024 * 1024,
},
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,
);
this.#checkConnection();
} catch (error) {
console.error('Failed to initialize gRPC client:', error);
process.exit(1);
}
}
#checkConnection() {
this.#client.waitForReady(Date.now() + 5000, (error) => {
if (error) {
console.error('gRPC client connection failed:', 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();
-283
View File
@@ -1,283 +0,0 @@
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)
-108
View File
@@ -1,108 +0,0 @@
const { execSync } = require('child_process');
const client = require('./client.js');
require(`./env.js`);
function expandRange(rangeStr, max) {
const tempRangeStr = rangeStr
.trim()
.replace(/-max/g, `-${max}`)
.replace(/max-/g, `${max}-`);
return tempRangeStr.split(' ').flatMap((part) => {
const rangeMatch = part.match(/^(\d+)([-~_])(\d+)$/);
if (rangeMatch) {
const [, start, , end] = rangeMatch.map(Number);
const step = start < end ? 1 : -1;
return Array.from(
{ length: Math.abs(end - start) + 1 },
(_, i) => start + i * step,
);
}
return Number(part);
});
}
function run() {
const {
envParam,
numParam,
file_task_before,
file_task_before_js,
dir_scripts,
task_before,
PREV_NODE_OPTIONS,
} = process.env;
try {
process.env.NODE_OPTIONS = PREV_NODE_OPTIONS;
const splitStr = '__sitecustomize__';
const fileName = process.argv[1].replace(`${dir_scripts}/`, '');
let command = `bash -c "source ${file_task_before} ${fileName}`;
if (task_before) {
const escapeTaskBefore = task_before
.replace(/"/g, '\\"')
.replace(/\$/g, '\\$');
command = `${command} && eval '${escapeTaskBefore}'`;
console.log('执行前置命令\n');
}
const res = execSync(
`${command} && echo -e '${splitStr}' && node -p 'JSON.stringify(process.env)'"`,
{
encoding: 'utf-8',
},
);
const [output, envStr] = res.split(splitStr);
const newEnvObject = JSON.parse(envStr.trim());
for (const key in newEnvObject) {
process.env[key] = newEnvObject[key];
}
if (output) {
console.log(output);
}
if (task_before) {
console.log('执行前置命令结束\n');
}
} catch (error) {
if (!error.message.includes('spawnSync /bin/sh E2BIG')) {
console.log(`\ue926 run task before error: `, error);
} else {
console.log(
`\ue926 The environment variable is too large. It is recommended to use task_before.js instead of task_before.sh\n`,
);
}
if (task_before) {
console.log('执行前置命令结束\n');
}
}
require(file_task_before_js);
if (envParam && numParam) {
const array = (process.env[envParam] || '').split('&');
const runArr = expandRange(numParam, array.length);
const arrayRun = runArr.map((i) => array[i - 1]);
const envStr = arrayRun.join('&');
process.env[envParam] = envStr;
}
}
try {
if (!process.argv[1]) {
return;
}
process.on('SIGTERM', (code) => {
process.exit(15);
});
run();
const { sendNotify } = require('./__ql_notify__.js');
global.QLAPI = {
notify: sendNotify,
...client,
};
} catch (error) {
console.log(`run builtin code error: `, error, '\n');
}
-119
View File
@@ -1,119 +0,0 @@
import os
import re
import subprocess
import json
import builtins
import sys
import env
import signal
from client import Client
def try_parse_int(value):
try:
return int(value)
except ValueError:
return None
def expand_range(range_str, max_value):
temp_range_str = (
range_str.strip()
.replace("-max", f"-{max_value}")
.replace("max-", f"{max_value}-")
)
result = []
for part in temp_range_str.split(" "):
range_match = re.match(r"^(\d+)([-~_])(\d+)$", part)
if range_match:
start, _, end = map(try_parse_int, range_match.groups())
step = 1 if start < end else -1
result.extend(range(start, end + step, step))
else:
result.append(int(part))
return result
def run():
try:
prev_pythonpath = os.getenv("PREV_PYTHONPATH", "")
os.environ["PYTHONPATH"] = prev_pythonpath
split_str = "__sitecustomize__"
file_name = sys.argv[0].replace(f"{os.getenv('dir_scripts')}/", "")
command = f'bash -c "source {os.getenv("file_task_before")} {file_name}'
task_before = os.getenv("task_before")
if task_before:
escape_task_before = task_before.replace('"', '\\"').replace("$", "\\$")
command += f" && eval '{escape_task_before}'"
print("执行前置命令\n")
prev_pythonpath = os.getenv("PREV_PYTHONPATH", "")
python_command = (
"python3 -c 'import os, json; print(json.dumps(dict(os.environ)))'"
)
command += f" && echo -e '{split_str}' && {python_command}\""
res = subprocess.check_output(command, shell=True, encoding="utf-8")
output, env_str = res.split(split_str)
env_json = json.loads(env_str.strip())
for key, value in env_json.items():
os.environ[key] = value
if len(output) > 0:
print(output)
if task_before:
print("执行前置命令结束")
except subprocess.CalledProcessError as error:
print(f"run task before error: {error}")
except OSError as error:
error_message = str(error)
if "Argument list too long" not in error_message:
print(f"\ue926 run task before error: {error}")
else:
print(
"\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("执行前置命令结束")
except Exception as error:
print(f"run task before error: {error}")
import task_before
env_param = os.getenv("envParam")
num_param = os.getenv("numParam")
if env_param and num_param:
array = (os.getenv(env_param) or "").split("&")
run_arr = expand_range(num_param, len(array))
array_run = [array[i - 1] for i in run_arr if i - 1 < len(array) and i > 0]
env_str = "&".join(array_run)
os.environ[env_param] = env_str
def handle_sigterm(signum, frame):
sys.exit(15)
try:
signal.signal(signal.SIGTERM, handle_sigterm)
run()
from __ql_notify__ import send
class BaseApi(Client):
def notify(self, *args, **kwargs):
return send(*args, **kwargs)
QLAPI = BaseApi()
builtins.QLAPI = QLAPI
except Exception as error:
print(f"run builtin code error: {error}\n")
+1 -1
View File
@@ -11,7 +11,7 @@ echo -e "提交master代码"
git push git push
echo -e "更新cdn文件" echo -e "更新cdn文件"
ts-node-transpile-only sample/tool.ts tsx sample/tool.ts
string=$(cat version.yaml | grep "version" | egrep "[^ ]*" -o | egrep "\d\.*") string=$(cat version.yaml | grep "version" | egrep "[^ ]*" -o | egrep "\d\.*")
version="v$string" version="v$string"
+18 -20
View File
@@ -2,37 +2,34 @@
days=$1 days=$1
## 删除运行脚本的旧日志
remove_js_log() { remove_js_log() {
local log_full_path_list=$(find $dir_log -name "*.log") local log_full_path_list=$(find $dir_log/ -name "*.log")
local diff_time local diff_time
for log in $log_full_path_list; do for log in $log_full_path_list; do
local log_date=$(echo $log | awk -F "/" '{print $NF}' | cut -c1-10) local log_date=$(echo $log | awk -F "/" '{print $NF}' | cut -c1-10) #文件名比文件属性获得的日期要可靠
if ! [[ $log_date =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then if [[ $(date +%s -d $log_date 2>/dev/null) ]]; then
if [[ $is_macos -eq 1 ]]; then if [[ $is_macos -eq 1 ]]; then
log_date=$(stat -f %Sm -t "%Y-%m-%d" "$log") diff_time=$(($(date +%s) - $(date -j -f "%Y-%m-%d" "$log_date" +%s)))
else else
log_date=$(stat -c %y "$log" | cut -d ' ' -f 1) diff_time=$(($(date +%s) - $(date +%s -d "$log_date")))
fi fi
fi if [[ $diff_time -gt $((${days} * 86400)) ]]; then
if [[ $is_macos -eq 1 ]]; then local log_path=$(echo "$log" | sed "s,${dir_log}/,,g")
diff_time=$(($(date +%s) - $(date -j -f "%Y-%m-%d" "$log_date" +%s))) local result=$(find_cron_api "log_path=$log_path")
else echo -e "查询文件 $log_path"
diff_time=$(($(date +%s) - $(date +%s -d "$log_date"))) if [[ -z $result ]]; then
fi echo -e "删除中~"
if [[ $diff_time -gt $((${days} * 86400)) ]]; then rm -vf $log
local log_path=$(echo "$log" | sed "s,${dir_log}/,,g") else
local result=$(find_cron_api "log_path=$log_path") echo -e "正在被 $result 使用,跳过~"
echo -e "查询文件 $log_path" fi
if [[ -z $result ]]; then
echo -e "删除中~"
rm -vf $log
else
echo -e "正在被 $result 使用,跳过~"
fi fi
fi fi
done done
} }
## 删除空文件夹
remove_empty_dir() { remove_empty_dir() {
cd $dir_log cd $dir_log
for dir in $(ls); do for dir in $(ls); do
@@ -42,6 +39,7 @@ remove_empty_dir() {
done done
} }
## 运行
if [[ ${days} ]]; then if [[ ${days} ]]; then
echo -e "查找旧日志文件中...\n" echo -e "查找旧日志文件中...\n"
remove_js_log remove_js_log
+77 -107
View File
@@ -1,57 +1,53 @@
#!/usr/bin/env bash #!/usr/bin/env bash
## 目录 ## 目录
export dir_root=$QL_DIR dir_root=$QL_DIR
export dir_tmp=$dir_root/.tmp dir_tmp=$dir_root/.tmp
export dir_data=$dir_root/data dir_data=$dir_root/data
if [[ ${QL_DATA_DIR:=} ]]; then if [[ $QL_DATA_DIR ]]; then
export dir_data="${QL_DATA_DIR%/}" dir_data="${QL_DATA_DIR%/}"
fi fi
export dir_shell=$dir_root/shell dir_shell=$dir_root/shell
export dir_preload=$dir_shell/preload dir_sample=$dir_root/sample
export dir_sample=$dir_root/sample dir_static=$dir_root/static
export dir_static=$dir_root/static dir_config=$dir_data/config
export dir_config=$dir_data/config dir_scripts=$dir_data/scripts
export dir_scripts=$dir_data/scripts dir_repo=$dir_data/repo
export dir_repo=$dir_data/repo dir_raw=$dir_data/raw
export dir_raw=$dir_data/raw dir_log=$dir_data/log
export dir_log=$dir_data/log dir_db=$dir_data/db
export dir_db=$dir_data/db dir_dep=$dir_data/deps
export dir_dep=$dir_data/deps dir_list_tmp=$dir_log/.tmp
export dir_list_tmp=$dir_log/.tmp dir_update_log=$dir_log/update
export dir_update_log=$dir_log/update ql_static_repo=$dir_repo/static
export ql_static_repo=$dir_repo/static
## 文件 ## 文件
export file_config_sample=$dir_sample/config.sample.sh file_config_sample=$dir_sample/config.sample.sh
export file_env=$dir_preload/env.sh file_env=$dir_config/env.sh
export file_preload_js=$dir_preload/sitecustomize.js file_sharecode=$dir_config/sharecode.sh
export file_sharecode=$dir_config/sharecode.sh file_config_user=$dir_config/config.sh
export file_config_user=$dir_config/config.sh file_auth_sample=$dir_sample/auth.sample.json
export file_auth_sample=$dir_sample/auth.sample.json file_auth_user=$dir_config/auth.json
export file_auth_user=$dir_config/auth.json file_auth_token=$dir_config/token.json
export file_auth_token=$dir_config/token.json file_extra_shell=$dir_config/extra.sh
export file_extra_shell=$dir_config/extra.sh file_task_before=$dir_config/task_before.sh
export file_task_before=$dir_config/task_before.sh file_task_after=$dir_config/task_after.sh
export file_task_before_js=$dir_config/task_before.js file_task_sample=$dir_sample/task.sample.sh
export file_task_before_py=$dir_config/task_before.py file_extra_sample=$dir_sample/extra.sample.sh
export file_task_after=$dir_config/task_after.sh file_notify_js_sample=$dir_sample/notify.js
export file_task_sample=$dir_sample/task.sample.sh file_notify_py_sample=$dir_sample/notify.py
export file_extra_sample=$dir_sample/extra.sample.sh file_test_js_sample=$dir_sample/ql_sample.js
export file_notify_js_sample=$dir_sample/notify.js file_test_py_sample=$dir_sample/ql_sample.py
export file_notify_py_sample=$dir_sample/notify.py file_notify_py=$dir_scripts/notify.py
export file_test_js_sample=$dir_sample/ql_sample.js file_notify_js=$dir_scripts/sendNotify.js
export file_test_py_sample=$dir_sample/ql_sample.py file_test_js=$dir_scripts/ql_sample.js
export file_notify_py=$dir_scripts/notify.py file_test_py=$dir_scripts/ql_sample.py
export file_notify_js=$dir_scripts/sendNotify.js nginx_app_conf=$dir_root/docker/front.conf
export file_test_js=$dir_scripts/ql_sample.js nginx_conf=$dir_root/docker/nginx.conf
export file_test_py=$dir_scripts/ql_sample.py dep_notify_py=$dir_dep/notify.py
export nginx_app_conf=$dir_root/docker/front.conf dep_notify_js=$dir_dep/sendNotify.js
export nginx_conf=$dir_root/docker/nginx.conf
export dep_notify_py=$dir_dep/notify.py
export dep_notify_js=$dir_dep/sendNotify.js
## 清单文件 ## 清单文件
list_crontab_user=$dir_config/crontab.list list_crontab_user=$dir_config/crontab.list
@@ -63,11 +59,11 @@ list_own_drop=$dir_list_tmp/own_drop.list
## 软连接及其原始文件对应关系 ## 软连接及其原始文件对应关系
link_name=( link_name=(
task # task
ql ql
) )
original_name=( original_name=(
task.sh # task.sh
update.sh update.sh
) )
@@ -78,19 +74,26 @@ init_env() {
import_config() { import_config() {
[[ -f $file_config_user ]] && . $file_config_user [[ -f $file_config_user ]] && . $file_config_user
if [[ $LOAD_ENV != 'false' ]] && [[ -f $file_env ]]; then
. $file_env
fi
ql_base_url=${QlBaseUrl:-"/"} ql_base_url=${QlBaseUrl:-"/"}
ql_port=${QlPort:-"5700"} ql_port=${QlPort:-"5700"}
command_timeout_time=${CommandTimeoutTime:-""} command_timeout_time=${CommandTimeoutTime:-""}
file_extensions=${RepoFileExtensions:-"js py"} file_extensions=${RepoFileExtensions:-"js py"}
proxy_url=${ProxyUrl:-""} proxy_url=${ProxyUrl:-""}
current_branch=${QL_BRANCH:-""} current_branch=${QL_BRANCH}
if [[ -n "${DefaultCronRule}" ]]; then if [[ -n "${DefaultCronRule}" ]]; then
default_cron="${DefaultCronRule}" default_cron="${DefaultCronRule}"
else else
default_cron="$(random_range 0 59) $(random_range 0 23) * * *" default_cron="$(random_range 0 59) $(random_range 0 23) * * *"
fi fi
cpu_warn=${CpuWarn}
mem_warn=${MemoryWarn}
disk_warn=${DiskWarn}
} }
set_proxy() { set_proxy() {
@@ -133,28 +136,6 @@ gen_random_num() {
echo $((${RANDOM} % $divi)) echo $((${RANDOM} % $divi))
} }
define_cmd() {
local cmd_prefix cmd_suffix
if type task &>/dev/null; then
cmd_suffix=""
if [[ -f "$dir_shell/task.sh" ]]; then
cmd_prefix=""
else
cmd_prefix="bash "
fi
else
cmd_suffix=".sh"
if [[ -f "$dir_shell/task.sh" ]]; then
cmd_prefix="$dir_shell/"
else
cmd_prefix="bash $dir_shell/"
fi
fi
for ((i = 0; i < ${#link_name[*]}; i++)); do
export cmd_${link_name[i]}="${cmd_prefix}${link_name[i]}${cmd_suffix}"
done
}
fix_config() { fix_config() {
make_dir $dir_tmp make_dir $dir_tmp
make_dir $dir_static make_dir $dir_static
@@ -175,6 +156,12 @@ fix_config() {
echo echo
fi fi
if [[ ! -f $file_env ]]; then
echo -e "检测到config配置目录下不存在env.sh,创建一个空文件用于初始化...\n"
touch $file_env
echo
fi
if [[ ! -f $file_task_before ]]; then if [[ ! -f $file_task_before ]]; then
echo -e "复制一份 $file_task_sample$file_task_before\n" echo -e "复制一份 $file_task_sample$file_task_before\n"
cp -fv $file_task_sample $file_task_before cp -fv $file_task_sample $file_task_before
@@ -193,6 +180,12 @@ fix_config() {
echo echo
fi 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 if [[ ! -s $file_notify_py ]]; then
echo -e "复制一份 $file_notify_py_sample$file_notify_py\n" echo -e "复制一份 $file_notify_py_sample$file_notify_py\n"
cp -fv $file_notify_py_sample $file_notify_py cp -fv $file_notify_py_sample $file_notify_py
@@ -334,7 +327,7 @@ format_log_time() {
local time="$2" local time="$2"
if [[ $is_macos -eq 1 ]]; then if [[ $is_macos -eq 1 ]]; then
echo $(python3 -c 'from datetime import datetime; print(datetime.now().strftime("%Y-%m-%d-%H-%M-%S-%f")[:-3])') echo $(date -j -f "$format" "$time" "+%Y-%m-%d-%H-%M-%S-%3N")
else else
echo $(date -d "$time" "+%Y-%m-%d-%H-%M-%S-%3N") echo $(date -d "$time" "+%Y-%m-%d-%H-%M-%S-%3N")
fi fi
@@ -424,36 +417,19 @@ init_nginx() {
sed -i "s,IPV4_CONFIG,${ipv4Str},g" /etc/nginx/conf.d/front.conf sed -i "s,IPV4_CONFIG,${ipv4Str},g" /etc/nginx/conf.d/front.conf
} }
get_env_array() {
exported_variables=()
while IFS= read -r line; do
exported_variables+=("$line")
done < <(grep '^export ' $file_env | awk '{print $2}' | cut -d= -f1)
}
clear_env() {
for var in "${exported_variables[@]}"; do
unset "$var"
done
}
handle_task_start() { handle_task_start() {
local error_message="" [[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
if [[ $ID ]]; then echo -e "## 开始执行... $begin_time\n"
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() { run_task_before() {
[[ $is_macos -eq 0 ]] && check_server
. $file_task_before "$@" . $file_task_before "$@"
if [[ ${task_before:=} ]]; then if [[ $task_before ]]; then
echo -e "执行前置命令\n" echo -e "执行前置命令\n"
eval "${task_before%;}" eval "$task_before"
echo -e "\n执行前置命令结束\n" echo -e "\n执行前置命令结束\n"
fi fi
} }
@@ -461,9 +437,9 @@ run_task_before() {
run_task_after() { run_task_after() {
. $file_task_after "$@" . $file_task_after "$@"
if [[ ${task_after:=} ]]; then if [[ $task_after ]]; then
echo -e "\n执行后置命令\n" echo -e "\n执行后置命令\n"
eval "${task_after%;}" eval "$task_after"
echo -e "\n执行后置命令结束" echo -e "\n执行后置命令结束"
fi fi
} }
@@ -473,21 +449,15 @@ handle_task_end() {
local end_time=$(format_time "$time_format" "$etime") local end_time=$(format_time "$time_format" "$etime")
local end_timestamp=$(format_timestamp "$time_format" "$etime") local end_timestamp=$(format_timestamp "$time_format" "$etime")
local diff_time=$(($end_timestamp - $begin_timestamp)) local diff_time=$(($end_timestamp - $begin_timestamp))
local suffix=""
[[ "${MANUAL:=}" == "true" ]] && suffix="(手动停止)"
[[ "$diff_time" == 0 ]] && diff_time=1 [[ "$diff_time" == 0 ]] && diff_time=1
if [[ $ID ]]; then echo -e "\n## 执行结束... $end_time 耗时 $diff_time 秒     "
local error=$(update_cron "\"$ID\"" "1" "$$" "$log_path" "$begin_timestamp" "$diff_time") [[ $ID ]] && 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 init_env
detect_termux detect_termux
detect_macos detect_macos
define_cmd
import_config $1
+15 -10
View File
@@ -1,15 +1,17 @@
#!/usr/bin/env bash #!/usr/bin/env bash
## 导入通用变量与函数
dir_shell=$QL_DIR/shell dir_shell=$QL_DIR/shell
. $dir_shell/share.sh . $dir_shell/share.sh
. $dir_shell/api.sh . $dir_shell/api.sh
trap "single_hanle" 2 3 20 15 14 19 1 trap "single_hanle" 2 3 20 15 14 19 1
single_hanle() { single_hanle() {
eval MANUAL=true handle_task_end "$@" "$cmd" eval handle_task_end "$@" "$cmd"
exit 1 exit 1
} }
## 选择python3还是node
define_program() { define_program() {
local file_param=$1 local file_param=$1
if [[ $file_param == *.js ]] || [[ $file_param == *.mjs ]]; then if [[ $file_param == *.js ]] || [[ $file_param == *.mjs ]]; then
@@ -17,9 +19,13 @@ define_program() {
elif [[ $file_param == *.py ]] || [[ $file_param == *.pyc ]]; then elif [[ $file_param == *.py ]] || [[ $file_param == *.pyc ]]; then
which_program="python3" which_program="python3"
elif [[ $file_param == *.sh ]]; then elif [[ $file_param == *.sh ]]; then
which_program="." which_program="bash"
elif [[ $file_param == *.ts ]]; then elif [[ $file_param == *.ts ]]; then
which_program="ts-node-transpile-only" if ! type tsx &>/dev/null; then
which_program="ts-node-transpile-only"
else
which_program="tsx"
fi
else else
which_program="" which_program=""
fi fi
@@ -32,7 +38,7 @@ handle_log_path() {
file_param="task" file_param="task"
fi fi
if [[ -z ${ID:=} ]]; then if [[ -z $ID ]]; then
ID=$(cat $list_crontab_user | grep -E "$cmd_task.* $file_param" | perl -pe "s|.*ID=(.*) $cmd_task.* $file_param\.*|\1|" | head -1 | awk -F " " '{print $1}') ID=$(cat $list_crontab_user | grep -E "$cmd_task.* $file_param" | perl -pe "s|.*ID=(.*) $cmd_task.* $file_param\.*|\1|" | head -1 | awk -F " " '{print $1}')
fi fi
local suffix="" local suffix=""
@@ -60,17 +66,17 @@ handle_log_path() {
log_dir="${log_dir_tmp%.*}${suffix}" log_dir="${log_dir_tmp%.*}${suffix}"
log_path="$log_dir/$log_time.log" log_path="$log_dir/$log_time.log"
if [[ ${real_log_path:=} ]]; then if [[ $real_log_path ]]; then
log_path="$real_log_path" log_path="$real_log_path"
fi fi
cmd="2>&1 | tee -a $dir_log/$log_path" cmd="2>&1 | tee -a $dir_log/$log_path"
make_dir "$dir_log/$log_dir" make_dir "$dir_log/$log_dir"
if [[ "${no_tee:=}" == "true" ]]; then if [[ "$no_tee" == "true" ]]; then
cmd=">> $dir_log/$log_path 2>&1" cmd=">> $dir_log/$log_path 2>&1"
fi fi
if [[ "${real_time:=}" == "true" ]]; then if [[ "$real_time" == "true" ]]; then
cmd="" cmd=""
fi fi
} }
@@ -111,7 +117,6 @@ init_begin_time() {
begin_timestamp=$(format_timestamp "$time_format" "$time") begin_timestamp=$(format_timestamp "$time_format" "$time")
} }
import_config "$@"
while getopts ":lm:" opt; do while getopts ":lm:" opt; do
case $opt in case $opt in
l) l)
@@ -122,8 +127,8 @@ while getopts ":lm:" opt; do
;; ;;
esac esac
done done
[[ ${show_log:=} ]] && shift $(($OPTIND - 1)) [[ $show_log ]] && shift $(($OPTIND - 1))
if [[ ${max_time:=} ]]; then if [[ $max_time ]]; then
shift $(($OPTIND - 1)) shift $(($OPTIND - 1))
command_timeout_time="$max_time" command_timeout_time="$max_time"
fi fi
+9 -10
View File
@@ -1,9 +1,9 @@
#!/usr/bin/env bash #!/usr/bin/env bash
dir_shell=$QL_DIR/shell dir_shell=$QL_DIR/shell
. $dir_shell/env.sh
. $dir_shell/share.sh . $dir_shell/share.sh
. $dir_shell/api.sh . $dir_shell/api.sh
. $dir_shell/env.sh
send_mark=$dir_shell/send_mark send_mark=$dir_shell/send_mark
@@ -489,6 +489,7 @@ main() {
local time_format="%Y-%m-%d %H:%M:%S" local time_format="%Y-%m-%d %H:%M:%S"
local time=$(date "+$time_format") local time=$(date "+$time_format")
local begin_timestamp=$(format_timestamp "$time_format" "$time") 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") local begin_time=$(format_time "$time_format" "$time")
@@ -496,8 +497,6 @@ main() {
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
fi fi
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
case $p1 in case $p1 in
update) update)
fix_config fix_config
@@ -538,13 +537,14 @@ main() {
eval . $dir_shell/check.sh $cmd eval . $dir_shell/check.sh $cmd
;; ;;
resetlet) resetlet)
eval update_auth_config "\\\"retries\\\":0" "重置登录错误次数" $cmd auth_value=$(cat $file_auth_user | jq '.retries =0' -c)
echo "$auth_value" >$file_auth_user
eval echo -e "重置登录错误次数成功" $cmd
;; ;;
resettfa) resettfa)
eval update_auth_config "\\\"twoFactorActivated\\\":false" "禁用两步验证" $cmd auth_value=$(cat $file_auth_user | jq '.twoFactorActivated =false' | jq '.twoFactorActived =false' -c)
;; echo "$auth_value" >$file_auth_user
resetpwd) eval echo -e "禁用两步验证成功" $cmd
eval update_auth_config "\\\"password\\\":\\\"$p2\\\"" "重置密码" $cmd
;; ;;
*) *)
eval echo -e "命令输入错误...\\\n" $cmd eval echo -e "命令输入错误...\\\n" $cmd
@@ -556,14 +556,13 @@ main() {
local end_time=$(format_time "$time_format" "$etime") local end_time=$(format_time "$time_format" "$etime")
local end_timestamp=$(format_timestamp "$time_format" "$etime") local end_timestamp=$(format_timestamp "$time_format" "$etime")
local diff_time=$(($end_timestamp - $begin_timestamp)) 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 if [[ "$p1" != "repo" ]] && [[ "$p1" != "raw" ]]; then
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time 秒     " $cmd eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time 秒     " $cmd
fi fi
} }
import_config "$@"
main "$@" main "$@"
exit 0 exit 0
Binary file not shown.
Binary file not shown.
Binary file not shown.
-21
View File
@@ -8,13 +8,6 @@
url('../assets/fonts/SourceCodePro-Regular.ttf') format('truetype'); 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 { body {
// 禁止手机页面下拉刷新 // 禁止手机页面下拉刷新
overflow: hidden; overflow: hidden;
@@ -59,11 +52,6 @@ body {
width: 100%; width: 100%;
padding: 24px; padding: 24px;
overflow-y: auto; overflow-y: auto;
code,
span {
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo,
Courier, monospace, Log;
}
} }
} }
@@ -75,10 +63,6 @@ body {
} }
} }
.ͼ1 .cm-scroller {
font-family: monospace, Log;
}
.monaco-editor:not(.rename-box) { .monaco-editor:not(.rename-box) {
height: calc(100vh - 128px) !important; height: calc(100vh - 128px) !important;
height: calc(100vh - var(--vh-offset, 0px) - 128px) !important; height: calc(100vh - var(--vh-offset, 0px) - 128px) !important;
@@ -478,8 +462,3 @@ body[data-dark='true'] {
--antd-arrow-background-color: rgb(24, 26, 27); --antd-arrow-background-color: rgb(24, 26, 27);
} }
} }
.ant-tabs-content-holder {
flex: 1;
overflow-y: auto;
}
+39 -25
View File
@@ -1,21 +1,35 @@
import config from '@/utils/config'; import intl from 'react-intl-universal';
import { useCtx, useTheme } from '@/utils/hooks'; import React, { useEffect, useState } from 'react';
import { request } from '@/utils/http'; 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 { import {
LogoutOutlined, LogoutOutlined,
MenuFoldOutlined, MenuFoldOutlined,
MenuUnfoldOutlined, MenuUnfoldOutlined,
UserOutlined, UserOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import ProLayout, { PageLoading } from '@ant-design/pro-layout'; import config from '@/utils/config';
import { history, Link, Outlet, useLocation } from '@umijs/max'; import { request } from '@/utils/http';
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 defaultProps from './defaultProps';
import './index.less'; import './index.less';
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 { init } from '../utils/init'; import { init } from '../utils/init';
import WebSocketManager from '../utils/websocket'; import WebSocketManager from '../utils/websocket';
@@ -164,9 +178,9 @@ export default function () {
useEffect(() => { useEffect(() => {
if (!user || !user.username) return; if (!user || !user.username) return;
const ws = WebSocketManager.getInstance( const ws = WebSocketManager.getInstance(
`${window.location.origin}${ `${window.location.origin}${config.apiPrefix}ws?token=${localStorage.getItem(
config.apiPrefix config.authKey,
}ws?token=${localStorage.getItem(config.authKey)}`, )}`,
); );
return () => { return () => {
@@ -187,6 +201,9 @@ export default function () {
console.log( console.log(
`从开始至load总耗时: ${timing.loadEventEnd - timing.navigationStart}`, `从开始至load总耗时: ${timing.loadEventEnd - timing.navigationStart}`,
); );
Sentry.captureMessage(
`白屏时间 ${timing.responseStart - timing.navigationStart}`,
);
}; };
}, []); }, []);
@@ -237,15 +254,18 @@ export default function () {
<ProLayout <ProLayout
selectedKeys={[location.pathname]} selectedKeys={[location.pathname]}
loading={loading} loading={loading}
ErrorBoundary={Sentry.ErrorBoundary}
logo={ logo={
<> <>
<Image preview={false} src="https://qn.whyour.cn/logo.png" /> <Image preview={false} src="https://qn.whyour.cn/logo.png" />
<div className="title"> <div className="title">
<span className="title">{intl.get('青龙')}</span> <span className="title">{intl.get('青龙')}</span>
<span <a
href={systemInfo?.changeLogLink}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
window.open(systemInfo?.changeLogLink, '_blank');
}} }}
> >
<Tooltip <Tooltip
@@ -269,7 +289,7 @@ export default function () {
</span> </span>
</Badge> </Badge>
</Tooltip> </Tooltip>
</span> </a>
</div> </div>
</> </>
} }
@@ -300,9 +320,7 @@ export default function () {
shape="square" shape="square"
size="small" size="small"
icon={<UserOutlined />} icon={<UserOutlined />}
src={ src={user.avatar ? `${config.apiPrefix}static/${user.avatar}` : ''}
user.avatar ? `${config.apiPrefix}static/${user.avatar}` : ''
}
/> />
<span style={{ marginLeft: 5 }}>{user.username}</span> <span style={{ marginLeft: 5 }}>{user.username}</span>
</span> </span>
@@ -324,11 +342,7 @@ export default function () {
shape="square" shape="square"
size="small" size="small"
icon={<UserOutlined />} icon={<UserOutlined />}
src={ src={user.avatar ? `${config.apiPrefix}static/${user.avatar}` : ''}
user.avatar
? `${config.apiPrefix}static/${user.avatar}`
: ''
}
/> />
<span style={{ marginLeft: 5 }}>{user.username}</span> <span style={{ marginLeft: 5 }}>{user.username}</span>
</span> </span>
+3 -18
View File
@@ -186,7 +186,6 @@
"秒后重试": "Retry after seconds", "秒后重试": "Retry after seconds",
"在您的设备上打开两步验证应用程序以查看您的身份验证代码并验证您的身份。": "Open the two-factor authentication application on your device to view your authentication code and verify your identity.", "在您的设备上打开两步验证应用程序以查看您的身份验证代码并验证您的身份。": "Open the two-factor authentication application on your device to view your authentication code and verify your identity.",
"请选择脚本文件": "Please select a script file", "请选择脚本文件": "Please select a script file",
"当前文件不支持预览": "The current file does not support preview",
"清空日志": "Clear Logs", "清空日志": "Clear Logs",
"设置": "Settings", "设置": "Settings",
"退出": "Exit", "退出": "Exit",
@@ -345,12 +344,12 @@
"gotify的url地址,例如 https://push.example.de:8080": "gotify URL address, e.g., https://push.example.de:8080", "gotify的url地址,例如 https://push.example.de:8080": "gotify URL address, e.g., https://push.example.de:8080",
"gotify的消息应用token码": "gotify message application token code", "gotify的消息应用token码": "gotify message application token code",
"推送消息的优先级": "Priority of Push Messages", "推送消息的优先级": "Priority of Push Messages",
"synologyChat的url地址": "Synology Chat Webhook URL address", "chat的url地址": "Chat URL address",
"chat的token码": "Chat token code", "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", "推送到个人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", "访问密钥": "Access Key",
"如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群": "If GOBOT_URL is set to /send_private_msg, enter user_id=personal QQ; if set to /send_group_msg, enter group_id=QQ group", "如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群": "If GOBOT_URL is set to /send_private_msg, enter user_id=personal QQ; if set to /send_group_msg, enter group_id=QQ group",
"Server酱SENDKEY": "ServerChan SENDKEY, https://sct.ftqq.com/r/13363", "Server酱SENDKEY": "ServerChan SENDKEY",
"PushDeer的Keyhttps://github.com/easychen/pushdeer": "PushDeer Key, https://github.com/easychen/pushdeer", "PushDeer的Keyhttps://github.com/easychen/pushdeer": "PushDeer Key, https://github.com/easychen/pushdeer",
"PushDeer的自架API endpoint,默认是 https://api2.pushdeer.com/message/push": "PushDeer's self-hosted API endpoint, default is https://api2.pushdeer.com/message/push", "PushDeer的自架API endpoint,默认是 https://api2.pushdeer.com/message/push": "PushDeer's self-hosted API endpoint, default is https://api2.pushdeer.com/message/push",
"Bark的信息IP/设备码,例如:https://api.day.app/XXXXXXXX": "Bark information IP/device code, e.g., https://api.day.app/XXXXXXXX", "Bark的信息IP/设备码,例如:https://api.day.app/XXXXXXXX": "Bark information IP/device code, e.g., https://api.day.app/XXXXXXXX",
@@ -380,11 +379,6 @@
"iGot的信息推送key,例如:https://push.hellyw.com/XXXXXXXX": "iGot information push key, e.g., https://push.hellyw.com/XXXXXXXX", "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/", "微信扫码登录后一对一推送或一对多推送下面的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.", "一对多推送的“群组编码”(一对多推送下面->您的群组(如无则创建)->群组编码,如果您是创建群组人。也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送)": "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/", "用户令牌,扫描登录后 我的—>设置->令牌 中获取,参考 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", "消息接收人": "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.", "调用版本;专业版填写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.",
@@ -394,11 +388,6 @@
"SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定": "The SMTP login password may also be a special passphrase, depending on the specific email service provider's instructions", "SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定": "The SMTP login password may also be a special passphrase, depending on the specific email service provider's instructions",
"PushMe的Keyhttps://push.i-i.me/": "PushMe key, https://push.i-i.me/", "PushMe的Keyhttps://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", "自建的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", "请求方法": "Request Method",
"请求头Content-Type": "Request Header Content-Type", "请求头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.", "请求链接以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.",
@@ -497,9 +486,5 @@
"NPM 镜像源": "NPM Mirror Source", "NPM 镜像源": "NPM Mirror Source",
"PyPI 镜像源": "PyPI Mirror Source", "PyPI 镜像源": "PyPI Mirror Source",
"alpine linux 镜像源": "Alpine Linux 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"
} }
+4 -20
View File
@@ -186,7 +186,6 @@
"秒后重试": "秒后重试", "秒后重试": "秒后重试",
"在您的设备上打开两步验证应用程序以查看您的身份验证代码并验证您的身份。": "在您的设备上打开两步验证应用程序以查看您的身份验证代码并验证您的身份。", "在您的设备上打开两步验证应用程序以查看您的身份验证代码并验证您的身份。": "在您的设备上打开两步验证应用程序以查看您的身份验证代码并验证您的身份。",
"请选择脚本文件": "请选择脚本文件", "请选择脚本文件": "请选择脚本文件",
"当前文件不支持预览": "当前文件不支持预览",
"清空日志": "清空日志", "清空日志": "清空日志",
"设置": "设置", "设置": "设置",
"退出": "退出", "退出": "退出",
@@ -345,12 +344,12 @@
"gotify的url地址,例如 https://push.example.de:8080": "gotify的url地址,例如 https://push.example.de:8080", "gotify的url地址,例如 https://push.example.de:8080": "gotify的url地址,例如 https://push.example.de:8080",
"gotify的消息应用token码": "gotify的消息应用token码", "gotify的消息应用token码": "gotify的消息应用token码",
"推送消息的优先级": "推送消息的优先级", "推送消息的优先级": "推送消息的优先级",
"synologyChat的url地址": "synologyChat的webhook url地址", "chat的url地址": "chat的url地址",
"chat的token码": "chat的token码", "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", "推送到个人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",
"访问密钥": "访问密钥", "访问密钥": "访问密钥",
"如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群": "如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群", "如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群": "如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群",
"Server酱SENDKEY": "ServerSENDKEYhttps://sct.ftqq.com/r/13363", "Server酱SENDKEY": "ServerSENDKEY",
"PushDeer的Keyhttps://github.com/easychen/pushdeer": "PushDeer的Keyhttps://github.com/easychen/pushdeer", "PushDeer的Keyhttps://github.com/easychen/pushdeer": "PushDeer的Keyhttps://github.com/easychen/pushdeer",
"PushDeer的自架API endpoint,默认是 https://api2.pushdeer.com/message/push": "PushDeer的自架API endpoint,默认是 https://api2.pushdeer.com/message/push", "PushDeer的自架API endpoint,默认是 https://api2.pushdeer.com/message/push": "PushDeer的自架API endpoint,默认是 https://api2.pushdeer.com/message/push",
"Bark的信息IP/设备码,例如:https://api.day.app/XXXXXXXX": "Bark的信息IP/设备码,例如:https://api.day.app/XXXXXXXX", "Bark的信息IP/设备码,例如:https://api.day.app/XXXXXXXX": "Bark的信息IP/设备码,例如:https://api.day.app/XXXXXXXX",
@@ -378,13 +377,8 @@
"好友": "好友", "好友": "好友",
"要发送的用户昵称或群名,如果目标是群,需要填群名,如果目标是好友,需要填好友昵称": "要发送的用户昵称或群名,如果目标是群,需要填群名,如果目标是好友,需要填好友昵称", "要发送的用户昵称或群名,如果目标是群,需要填群名,如果目标是好友,需要填好友昵称": "要发送的用户昵称或群名,如果目标是群,需要填群名,如果目标是好友,需要填好友昵称",
"iGot的信息推送key,例如:https://push.hellyw.com/XXXXXXXX": "iGot的信息推送key,例如:https://push.hellyw.com/XXXXXXXX", "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/", "用户令牌,扫描登录后 我的—>设置->令牌 中获取,参考 https://www.weplusbot.com/": "用户令牌,扫描登录后 我的—>设置->令牌 中获取,参考 https://www.weplusbot.com/",
"消息接收人": "消息接收人", "消息接收人": "消息接收人",
"调用版本;专业版填写pro,个人版填写personal,为空默认使用专业版": "调用版本;专业版填写pro,个人版填写personal,为空默认使用专业版", "调用版本;专业版填写pro,个人版填写personal,为空默认使用专业版": "调用版本;专业版填写pro,个人版填写personal,为空默认使用专业版",
@@ -394,11 +388,6 @@
"SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定": "SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定", "SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定": "SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定",
"PushMe的Keyhttps://push.i-i.me/": "PushMe的Keyhttps://push.i-i.me/", "PushMe的Keyhttps://push.i-i.me/": "PushMe的Keyhttps://push.i-i.me/",
"自建的PushMeServer消息接口地址,例如:http://127.0.0.1:3010,不填则使用官方消息接口": "自建的PushMeServer消息接口地址,例如:http://127.0.0.1:3010,不填则使用官方消息接口", "自建的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", "请求头Content-Type": "请求头Content-Type",
"请求链接以http或者https开头。url或者body中必须包含$title$content可选,对应api内容的位置": "请求链接以http或者https开头。url或者body中必须包含$title$content可选,对应api内容的位置", "请求链接以http或者https开头。url或者body中必须包含$title$content可选,对应api内容的位置": "请求链接以http或者https开头。url或者body中必须包含$title$content可选,对应api内容的位置",
@@ -497,10 +486,5 @@
"NPM 镜像源": "NPM 镜像源", "NPM 镜像源": "NPM 镜像源",
"PyPI 镜像源": "PyPI 镜像源", "PyPI 镜像源": "PyPI 镜像源",
"alpine linux 镜像源": "alpine linux 镜像源", "alpine linux 镜像源": "alpine linux 镜像源",
"如果恢复失败,可进入容器执行": "如果恢复失败,可进入容器执行", "如果恢复失败,可进入容器执行": "如果恢复失败,可进入容器执行"
"常规定时": "常规定时",
"手动运行": "手动运行",
"开机运行": "开机运行",
"时区": "时区"
} }
-13
View File
@@ -1,13 +0,0 @@
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;
};
+72 -89
View File
@@ -1,68 +1,66 @@
import useTableScrollHeight from '@/hooks/useTableScrollHeight'; import intl from 'react-intl-universal';
import { SharedContext } from '@/layouts'; import React, { useState, useEffect, useRef, useMemo } from 'react';
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 { import {
Button, Button,
Dropdown,
Input,
MenuProps,
message, message,
Modal, Modal,
Space,
Table, Table,
TablePaginationConfig,
Tabs,
Tag, Tag,
Typography Space,
Tooltip,
Dropdown,
Menu,
Typography,
Input,
Popover,
Tabs,
TablePaginationConfig,
MenuProps,
} from 'antd'; } from 'antd';
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 './index.less';
import CronLogModal from './logModal';
import CronModal, { CronLabelModal } from './modal';
import { import {
CrontabStatus, ClockCircleOutlined,
ICrontab, Loading3QuartersOutlined,
OperationName, CloseCircleOutlined,
OperationPath, FileTextOutlined,
ScheduleType, EllipsisOutlined,
} from './type'; 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 CronDetailModal from './detail';
import { diffTime } from '@/utils/date';
import { history, useOutletContext } from '@umijs/max';
import './index.less';
import ViewCreateModal from './viewCreateModal'; import ViewCreateModal from './viewCreateModal';
import ViewManageModal from './viewManageModal'; 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 { Text, Paragraph, Link } = Typography;
const { Search } = Input; const { Search } = Input;
const SHOW_TAB_COUNT = 10;
const Crontab = () => { const Crontab = () => {
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>(); const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
@@ -263,15 +261,28 @@ const Crontab = () => {
}, },
}, },
render: (text, record) => { render: (text, record) => {
return record.nextRunTime return dayjs(record.nextRunTime).format('YYYY-MM-DD HH:mm:ss');
? dayjs(record.nextRunTime).format('YYYY-MM-DD HH:mm:ss')
: '-';
}, },
}, },
{ {
title: intl.get('关联订阅'), title: intl.get('关联订阅'),
width: 185, width: 185,
render: (text, record: any) => record?.subscription?.name || '-', render: (text, record: any) =>
record.sub_id ? (
<Name
service={() =>
request.get(`${config.apiPrefix}subscriptions/${record.sub_id}`, {
onError: noop,
})
}
options={{
ready: record?.sub_id,
cacheKey: record.sub_id,
}}
/>
) : (
'-'
),
}, },
{ {
title: intl.get('操作'), title: intl.get('操作'),
@@ -381,32 +392,14 @@ const Crontab = () => {
} }
request request
.get(url) .get(url)
.then(async ({ code, data: _data }) => { .then(({ code, data: _data }) => {
if (code === 200) { if (code === 200) {
const { data, total } = _data; const { data, total } = _data;
const subscriptions = await request.get(
`${config.apiPrefix}subscriptions?ids=${JSON.stringify([
...new Set(data.map((x) => x.sub_id).filter(Boolean)),
])}`,
{
onError: noop,
},
);
const subscriptionMap = Object.fromEntries(
subscriptions?.data?.map((x) => [x.id, x]),
);
setValue( setValue(
data.map((x) => { data.map((x) => {
const scheduleType = getScheduleType(x.schedule);
const nextRunTime =
scheduleType === ScheduleType.Normal
? getCrontabsNextDate(x.schedule, x.extra_schedules)
: null;
return { return {
...x, ...x,
nextRunTime, nextRunTime: getCrontabsNextDate(x.schedule, x.extra_schedules),
subscription: subscriptionMap?.[x.sub_id],
}; };
}), }),
); );
@@ -629,7 +622,6 @@ const Crontab = () => {
icon: icon:
record.isDisabled === 1 ? <CheckCircleOutlined /> : <StopOutlined />, record.isDisabled === 1 ? <CheckCircleOutlined /> : <StopOutlined />,
}, },
{ label: intl.get('复制'), key: 'copy', icon: <CopyOutlined /> },
{ label: intl.get('删除'), key: 'delete', icon: <DeleteOutlined /> }, { label: intl.get('删除'), key: 'delete', icon: <DeleteOutlined /> },
{ {
label: record.isPinned === 1 ? intl.get('取消置顶') : intl.get('置顶'), label: record.isPinned === 1 ? intl.get('取消置顶') : intl.get('置顶'),
@@ -665,9 +657,6 @@ const Crontab = () => {
case 'edit': case 'edit':
editCron(record, index); editCron(record, index);
break; break;
case 'copy':
editCron(omit(record, 'id'), index);
break;
case 'enableOrDisable': case 'enableOrDisable':
enabledOrDisabledCron(record, index); enabledOrDisabledCron(record, index);
break; break;
@@ -813,9 +802,7 @@ const Crontab = () => {
useEffect(() => { useEffect(() => {
if (viewConf && enabledCronViews && enabledCronViews.length > 0) { if (viewConf && enabledCronViews && enabledCronViews.length > 0) {
const view = enabledCronViews const view = enabledCronViews.slice(4).find((x) => x.id === viewConf.id);
.slice(SHOW_TAB_COUNT)
.find((x) => x.id === viewConf.id);
setMoreMenuActive(!!view); setMoreMenuActive(!!view);
} }
}, [viewConf, enabledCronViews]); }, [viewConf, enabledCronViews]);
@@ -845,7 +832,7 @@ const Crontab = () => {
viewAction(key); viewAction(key);
}, },
items: [ items: [
...[...enabledCronViews].slice(SHOW_TAB_COUNT).map((x) => ({ ...[...enabledCronViews].slice(4).map((x) => ({
label: ( label: (
<Space style={{ display: 'flex', justifyContent: 'space-between' }}> <Space style={{ display: 'flex', justifyContent: 'space-between' }}>
<span>{x.name}</span> <span>{x.name}</span>
@@ -871,10 +858,6 @@ const Crontab = () => {
icon: <SettingOutlined />, icon: <SettingOutlined />,
}, },
], ],
style: {
maxHeight: 350,
overflowY: 'auto',
},
}; };
const getCronViews = () => { const getCronViews = () => {
@@ -965,7 +948,7 @@ const Crontab = () => {
} }
onTabClick={tabClick} onTabClick={tabClick}
items={[ items={[
...[...enabledCronViews].slice(0, SHOW_TAB_COUNT).map((x) => ({ ...[...enabledCronViews].slice(0, 4).map((x) => ({
key: x.id, key: x.id,
label: x.name, label: x.name,
})), })),

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