Compare commits

..
51 Commits
Author SHA1 Message Date
whyour 47c194c1f4 更新版本 v2.19.1 2025-05-24 15:03:08 +08:00
whyour 7d65d96ebd 修复 demo 环境提示 2025-05-24 14:56:49 +08:00
whyour 224000b63b 修复依赖是否安装检查逻辑 2025-05-23 23:45:43 +08:00
whyour 1c18668bad 修复文件下载参数 2025-05-22 00:09:19 +08:00
whyour f94582b68d 修复查询 python 依赖存在逻辑 2025-05-21 01:25:24 +08:00
whyour eb1c00984c 修复任务视图状态包含筛选 2025-05-20 23:40:18 +08:00
whyour 1a185f5682 修复创建脚本可能失败 2025-05-20 01:00:08 +08:00
whyour b6ea8565ec 更新版本 v2.19.0 2025-05-18 22:02:57 +08:00
whyour 2e94d58758 修改启动命令环境变量 2025-05-18 22:02:55 +08:00
whyour 472a3088df 修复启动逻辑 2025-05-17 17:25:50 +08:00
whyour ec3d61a713 修复 /:file 获取日志接口 2025-05-16 00:57:27 +08:00
whyour 95459c33ed 修改 import undici 2025-05-15 01:19:24 +08:00
whyour 5459b72f63 更新 pnpm-lock 2025-05-15 01:15:55 +08:00
whyour 05db2b1df8 修复重置用户名失败 2025-05-15 01:12:01 +08:00
whyour c3072e7712 got 替换为 uudici 2025-05-15 01:01:39 +08:00
whyour 3fafe4d24d 修复依赖强制删除未移除队列 2025-05-13 02:00:37 +08:00
whyour 32bccb3f3e 修复无法识别 python 依赖安装的命令 2025-05-13 00:22:15 +08:00
whyour ac04478d1d 修复重用户名 2025-05-11 18:59:15 +08:00
whyour 8a18baa921 修复登录通知失败造成服务重启 2025-05-11 14:36:16 +08:00
whyour 9a399f8de8 更新 workflow 2025-05-09 23:36:14 +08:00
whyour da639a1f8f 移除部分历史兼容逻辑 2025-05-09 15:37:05 +08:00
whyour 425e49675a 修复删除日志 2025-05-09 00:20:53 +08:00
whyour 8174762c18 demo 环境不自动运行任务 2025-05-08 01:43:14 +08:00
whyour 710a107e73 邮箱通知支持多个收件人 2025-05-08 01:27:25 +08:00
whyour d871585eee 修改服务启动逻辑 2025-05-07 09:30:00 +08:00
whyour 729b405b0f 修复 bootAfter 目录 2025-04-26 23:26:11 +08:00
whyour 71a7c1b9d3 boot 任务改为在依赖安装完成后执行 2025-04-25 23:52:33 +08:00
whyour 40a831f3a2 修复脚本管理查询逻辑 2025-04-25 01:40:37 +08:00
whyour 1befa1bb8c 缓存 node 和 python 依赖 2025-04-23 02:10:39 +08:00
whyour 8009634b44 修复脚本管理增加文件夹 2025-04-19 16:34:28 +08:00
whyour 03a23f6f31 更新 workflow 运行环境 2025-04-19 14:36:21 +08:00
whyour 124e01e93a 修复 QLAPI 修复环境变量 remarks,esm 依赖查不到 2025-04-19 01:38:43 +08:00
whyour 2370923a75 更新版本 v2.18.3 2025-03-19 23:47:53 +08:00
whyour 256e4e7a83 修复任务打开脚本链接 2025-03-19 23:47:51 +08:00
whyour c0ec063333 修复删除脚本参数 2025-03-18 23:17:03 +08:00
whyour 576408de01 脚本管理增加强制打开文件操作 2025-03-16 17:57:13 +08:00
whyour cf94ecfb11 脚本管理和日志管理支持下载 2025-03-13 00:22:24 +08:00
whyour 118c92d9e5 更新版本 v2.18.2 2025-02-28 00:56:43 +08:00
whyour 280085668e 修复未初始化时区设置 2025-02-28 00:56:40 +08:00
whyour bae4073a64 增加重置密码命令 2025-02-27 23:57:26 +08:00
whyour af3e358a6a 系统设置增加时区设置 2025-02-27 00:45:21 +08:00
whyour 64fcbff715 脚本管理增加可预览检查 2025-02-26 01:17:11 +08:00
whyour f9f78b4e05 修改系统内置通知模块名称,避免重复 2025-02-25 00:32:13 +08:00
whyour fa83761d27 修改定时规则类型 2025-02-21 01:35:08 +08:00
whyour 8173075b67 定时任务支持 @once 和 @boot 任务 2025-02-20 02:18:59 +08:00
whyour 496918131f 修复群晖通知参数,任务视图不属于筛选 2025-02-16 12:15:45 +08:00
whyour 5d64aae452 修复 mirror action 2025-02-07 01:56:23 +08:00
whyour 952ea2859e 修复最后运行时长多语言显示 2025-01-31 23:02:25 +08:00
whyour 25e8e8198d 修改 QLAPI 调用校验 2025-01-30 00:55:48 +08:00
whyour 3ab5b0d86b 修复登录失败没有提示 2025-01-29 23:50:46 +08:00
whyour 6df651aa63 更新 ts-proto 版本 2025-01-29 23:29:45 +08:00
121 changed files with 5518 additions and 2371 deletions
+7 -9
View File
@@ -1,14 +1,12 @@
UPDATE_PORT=5300 GRPC_PORT=5500
PUBLIC_PORT=5400
CRON_PORT=5500
BACK_PORT=5600 BACK_PORT=5600
PORT=5700 PORT=5700
LOG_LEVEL='debug' LOG_LEVEL='info'
SECRET='whyour' JWT_SECRET=
JWT_EXPIRES_IN=
QINIU_AK='' QINIU_AK=
QINIU_SK='' QINIU_SK=
QINIU_SCOPE='' QINIU_SCOPE=
TEMP=''
+46 -26
View File
@@ -14,27 +14,31 @@ on:
workflow_dispatch: workflow_dispatch:
jobs: jobs:
to_gitlab: code_gitlab:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: pixta-dev/repository-mirroring-action@v1 - uses: wearerequired/git-mirror-action@v1
env:
SSH_PRIVATE_KEY: ${{ secrets.GITLAB_SSH_PK }}
with: with:
target_repo_url: git@gitlab.com:whyour/qinglong.git source-repo: https://github.com/whyour/qinglong.git
ssh_private_key: ${{ secrets.GITLAB_SSH_PK }} destination-repo: git@gitlab.com:whyour/qinglong.git
to_gitee: code_gitee:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: pixta-dev/repository-mirroring-action@v1 - uses: wearerequired/git-mirror-action@v1
env:
SSH_PRIVATE_KEY: ${{ secrets.GITLAB_SSH_PK }}
with: with:
target_repo_url: git@gitee.com:whyour/qinglong.git source-repo: https://github.com/whyour/qinglong.git
ssh_private_key: ${{ secrets.GITLAB_SSH_PK }} destination-repo: git@gitee.com:whyour/qinglong.git
build-static: build-static:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -70,24 +74,39 @@ jobs:
git config --local user.email 'github-actions[bot]@users.noreply.github.com' git config --local user.email 'github-actions[bot]@users.noreply.github.com'
git commit --allow-empty -m "copy static at $(date +'%Y-%m-%d %H:%M:%S')" git commit --allow-empty -m "copy static at $(date +'%Y-%m-%d %H:%M:%S')"
git push --force --quiet "https://${{ secrets.API_TOKEN }}@${GITHUB_REPO}.git" ${GITHUB_BRANCH}:${GITHUB_BRANCH} git push --force --quiet "https://${{ secrets.API_TOKEN }}@${GITHUB_REPO}.git" ${GITHUB_BRANCH}:${GITHUB_BRANCH}
static_gitlab:
needs: build-static
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: wearerequired/git-mirror-action@v1
env:
SSH_PRIVATE_KEY: ${{ secrets.GITLAB_SSH_PK }}
with:
source-repo: https://github.com/whyour/qinglong-static.git
destination-repo: git@gitlab.com:whyour/qinglong-static.git
mkdir -p ~/.ssh static_gitee:
echo "${PRIVATE_KEY}" > ~/.ssh/id_rsa needs: build-static
chmod 600 ~/.ssh/id_rsa runs-on: ubuntu-latest
steps:
export GIT_SSH_COMMAND="ssh -v -i ~/.ssh/id_rsa -o StrictHostKeyChecking=no -l git" - uses: actions/checkout@v4
git remote add gitee "${REPO_GITEE}" with:
git remote add gitlab "${REPO_GITLAB}" fetch-depth: 0
git gc - uses: wearerequired/git-mirror-action@v1
git push --force --quiet gitee ${GITHUB_BRANCH}:${GITHUB_BRANCH} env:
git push --force --quiet gitlab ${GITHUB_BRANCH}:${GITHUB_BRANCH} SSH_PRIVATE_KEY: ${{ secrets.GITLAB_SSH_PK }}
with:
source-repo: https://github.com/whyour/qinglong-static.git
destination-repo: git@gitee.com:whyour/qinglong-static.git
build: build:
needs: build-static needs: build-static
# 由于 ubuntu-latest 中使用 linux6.x 内核 linux/s390x npm 无法使用 runs-on: ubuntu-22.04
runs-on: ubuntu-20.04
# runs-on: self-hosted
permissions: permissions:
packages: write packages: write
@@ -156,8 +175,8 @@ jobs:
QL_BRANCH=${{ github.ref_name }} QL_BRANCH=${{ github.ref_name }}
SOURCE_COMMIT=${{ github.sha }} SOURCE_COMMIT=${{ github.sha }}
network: host network: host
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x,linux/386 # linux/s390x npm 暂不可用
# platforms: linux/amd64,linux/arm64,linux/ppc64le,linux/s390x,linux/386 platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/386
context: . context: .
file: ./docker/Dockerfile file: ./docker/Dockerfile
push: true push: true
@@ -169,12 +188,12 @@ jobs:
- name: Image digest - name: Image digest
run: | run: |
echo ${{ steps.docker_build.outputs.digest }} echo ${{ steps.docker_build.outputs.digest }}
build310: build310:
if: ${{ github.ref_name == 'master' }} if: ${{ github.ref_name == 'master' }}
needs: build-static needs: build-static
runs-on: ubuntu-20.04 runs-on: ubuntu-22.04
permissions: permissions:
packages: write packages: write
@@ -222,7 +241,8 @@ jobs:
QL_BRANCH=${{ github.ref_name }} QL_BRANCH=${{ github.ref_name }}
SOURCE_COMMIT=${{ github.sha }} SOURCE_COMMIT=${{ github.sha }}
network: host network: host
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x,linux/386 # linux/s390x npm 暂不可用
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/386
context: . context: .
file: ./docker/310.Dockerfile file: ./docker/310.Dockerfile
push: true push: true
+1
View File
@@ -27,3 +27,4 @@ __pycache__
/shell/preload/env.* /shell/preload/env.*
/shell/preload/notify.* /shell/preload/notify.*
/shell/preload/*-notify.json /shell/preload/*-notify.json
/shell/preload/__ql_notify__.*
-10
View File
@@ -16,16 +16,6 @@ export default defineConfig({
favicons: [`https://qn.whyour.cn/favicon.svg`], favicons: [`https://qn.whyour.cn/favicon.svg`],
publicPath: process.env.NODE_ENV === 'production' ? './' : '/', publicPath: process.env.NODE_ENV === 'production' ? './' : '/',
proxy: { proxy: {
[`${baseUrl}api/update`]: {
target: 'http://127.0.0.1:5300/',
changeOrigin: true,
pathRewrite: { [`^${baseUrl}api/update`]: '/api' },
},
[`${baseUrl}api/public`]: {
target: 'http://127.0.0.1:5400/',
changeOrigin: true,
pathRewrite: { [`^${baseUrl}api/public`]: '/api' },
},
[`${baseUrl}api`]: { [`${baseUrl}api`]: {
target: 'http://127.0.0.1:5600/', target: 'http://127.0.0.1:5600/',
changeOrigin: true, changeOrigin: true,
Vendored
-9
View File
@@ -1,9 +0,0 @@
import 'express';
declare global {
namespace Express {
interface Request {
platform: string;
}
}
}
+11 -37
View File
@@ -4,7 +4,8 @@ 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 cron_parser from 'cron-parser'; import { commonCronSchema } from '../validation/schedule';
const route = Router(); const route = Router();
export default (app: Router) => { export default (app: Router) => {
@@ -170,27 +171,14 @@ export default (app: Router) => {
route.post( route.post(
'/', '/',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object(commonCronSchema),
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 {
if (cron_parser.parseExpression(req.body.schedule).hasNext()) { const cronService = Container.get(CronService);
const cronService = Container.get(CronService); const data = await cronService.create(req.body);
const data = await cronService.create(req.body); return res.send({ code: 200, data });
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);
} }
@@ -331,30 +319,16 @@ export default (app: Router) => {
'/', '/',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
labels: Joi.array().optional().allow(null), ...commonCronSchema,
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 {
if ( const cronService = Container.get(CronService);
!req.body.schedule || const data = await cronService.update(req.body);
cron_parser.parseExpression(req.body.schedule).hasNext() return res.send({ code: 200, data });
) {
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);
} }
@@ -418,7 +392,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.import_crontab(); const data = await cronService.importCrontab();
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
return next(e); return next(e);
-6
View File
@@ -32,7 +32,6 @@ export default (app: Router) => {
), ),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try { try {
const dependenceService = Container.get(DependenceService); const dependenceService = Container.get(DependenceService);
const data = await dependenceService.create(req.body); const data = await dependenceService.create(req.body);
@@ -54,7 +53,6 @@ export default (app: Router) => {
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try { try {
const dependenceService = Container.get(DependenceService); const dependenceService = Container.get(DependenceService);
const data = await dependenceService.update(req.body); const data = await dependenceService.update(req.body);
@@ -71,7 +69,6 @@ export default (app: Router) => {
body: Joi.array().items(Joi.number().required()), body: Joi.array().items(Joi.number().required()),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try { try {
const dependenceService = Container.get(DependenceService); const dependenceService = Container.get(DependenceService);
const data = await dependenceService.remove(req.body); const data = await dependenceService.remove(req.body);
@@ -88,7 +85,6 @@ export default (app: Router) => {
body: Joi.array().items(Joi.number().required()), body: Joi.array().items(Joi.number().required()),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try { try {
const dependenceService = Container.get(DependenceService); const dependenceService = Container.get(DependenceService);
const data = await dependenceService.remove(req.body, true); const data = await dependenceService.remove(req.body, true);
@@ -107,7 +103,6 @@ export default (app: Router) => {
}), }),
}), }),
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => { async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try { try {
const dependenceService = Container.get(DependenceService); const dependenceService = Container.get(DependenceService);
const data = await dependenceService.getDb({ id: req.params.id }); const data = await dependenceService.getDb({ id: req.params.id });
@@ -124,7 +119,6 @@ export default (app: Router) => {
body: Joi.array().items(Joi.number().required()), body: Joi.array().items(Joi.number().required()),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try { try {
const dependenceService = Container.get(DependenceService); const dependenceService = Container.get(DependenceService);
const data = await dependenceService.reInstall(req.body); const data = await dependenceService.reInstall(req.body);
+27
View File
@@ -0,0 +1,27 @@
import { Router } from 'express';
import Logger from '../loaders/logger';
import { HealthService } from '../services/health';
import Container from 'typedi';
const route = Router();
export default (app: Router) => {
app.use('/', route);
route.get('/health', async (req, res) => {
try {
const healthService = Container.get(HealthService);
const health = await healthService.check();
res.status(200).send({
code: 200,
data: health,
});
} catch (err: any) {
Logger.error('Health check failed:', err);
res.status(500).send({
code: 500,
message: 'Health check failed',
error: err.message,
});
}
});
};
+4
View File
@@ -9,6 +9,8 @@ import open from './open';
import dependence from './dependence'; import dependence from './dependence';
import system from './system'; import system from './system';
import subscription from './subscription'; import subscription from './subscription';
import update from './update';
import health from './health';
export default () => { export default () => {
const app = Router(); const app = Router();
@@ -22,6 +24,8 @@ export default () => {
dependence(app); dependence(app);
system(app); system(app);
subscription(app); subscription(app);
update(app);
health(app);
return app; return app;
}; };
+66 -23
View File
@@ -1,10 +1,15 @@
import { Router, Request, Response, NextFunction } from 'express'; import { celebrate, Joi } from 'celebrate';
import { NextFunction, Request, Response, Router } from 'express';
import { Container } from 'typedi'; import { Container } from 'typedi';
import { Logger } from 'winston'; import { Logger } from 'winston';
import config from '../config'; import config from '../config';
import { getFileContentByName, readDirs, removeAnsi, rmPath } from '../config/util'; import {
import { join, resolve } from 'path'; getFileContentByName,
import { celebrate, Joi } from 'celebrate'; readDirs,
removeAnsi,
rmPath,
} from '../config/util';
import LogService from '../services/log';
const route = Router(); const route = Router();
const blacklist = ['.tmp']; const blacklist = ['.tmp'];
@@ -29,17 +34,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( const logService = Container.get(LogService);
config.logPath, const finalPath = logService.checkFilePath(
(req.query.path as string) || '', (req.query.path as string) || '',
(req.query.file as string) || '', (req.query.file as string) || '',
); );
if (!finalPath || blacklist.includes(req.query.path as string)) {
if ( return res.send({
blacklist.includes(req.query.path as string) || code: 403,
!finalPath.startsWith(config.logPath) message: '暂无权限',
) { });
return res.send({ code: 403, message: '暂无权限' });
} }
const content = await getFileContentByName(finalPath); const content = await getFileContentByName(finalPath);
res.send({ code: 200, data: removeAnsi(content) }); res.send({ code: 200, data: removeAnsi(content) });
@@ -53,16 +57,16 @@ 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( const logService = Container.get(LogService);
config.logPath, const finalPath = logService.checkFilePath(
(req.query.path as string) || '', (req.query.path as string) || '',
(req.params.file as string) || '', (req.params.file as string) || '',
); );
if ( if (!finalPath || blacklist.includes(req.query.path as string)) {
blacklist.includes(req.path) || return res.send({
!finalPath.startsWith(config.logPath) code: 403,
) { message: '暂无权限',
return res.send({ code: 403, message: '暂无权限' }); });
} }
const content = await getFileContentByName(finalPath); const content = await getFileContentByName(finalPath);
res.send({ code: 200, data: content }); res.send({ code: 200, data: content });
@@ -83,17 +87,56 @@ export default (app: Router) => {
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
try { try {
let { filename, path, type } = req.body as { let { filename, path } = req.body as {
filename: string; filename: string;
path: string; path: string;
type: string;
}; };
const filePath = join(config.logPath, path, filename); const logService = Container.get(LogService);
await rmPath(filePath); const finalPath = logService.checkFilePath(path, filename);
if (!finalPath || blacklist.includes(path)) {
return res.send({
code: 403,
message: '暂无权限',
});
}
await rmPath(finalPath);
res.send({ code: 200 }); res.send({ code: 200 });
} catch (e) { } catch (e) {
return next(e); return next(e);
} }
}, },
); );
route.post(
'/download',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
path: Joi.string().allow(''),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
let { filename, path } = req.body as {
filename: string;
path: string;
};
const logService = Container.get(LogService);
const filePath = logService.checkFilePath(path, filename);
if (!filePath) {
return res.send({
code: 403,
message: '暂无权限',
});
}
return res.download(filePath, filename, (err) => {
if (err) {
return next(err);
}
});
} catch (e) {
return next(e);
}
},
);
}; };
+41 -25
View File
@@ -1,4 +1,4 @@
import { fileExist, readDirs, readDir, rmPath } from '../config/util'; import { fileExist, readDirs, readDir, rmPath, IFile } from '../config/util';
import { Router, Request, Response, NextFunction } from 'express'; import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi'; import { Container } from 'typedi';
import { Logger } from 'winston'; import { Logger } from 'winston';
@@ -27,7 +27,7 @@ export default (app: Router) => {
route.get('/', async (req: Request, res: Response, next: NextFunction) => { route.get('/', async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
let result = []; let result: IFile[] = [];
const blacklist = [ const blacklist = [
'node_modules', 'node_modules',
'.git', '.git',
@@ -37,11 +37,11 @@ export default (app: Router) => {
'package-lock.json', 'package-lock.json',
]; ];
if (req.query.path) { if (req.query.path) {
const targetPath = path.join( result = await readDir(
config.scriptPath,
req.query.path as string, req.query.path as string,
config.scriptPath,
blacklist,
); );
result = await readDir(targetPath, config.scriptPath, blacklist);
} else { } else {
result = await readDirs( result = await readDirs(
config.scriptPath, config.scriptPath,
@@ -102,7 +102,6 @@ export default (app: Router) => {
'/', '/',
upload.single('file'), upload.single('file'),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try { try {
let { filename, path, content, originFilename, directory } = let { filename, path, content, originFilename, directory } =
req.body as { req.body as {
@@ -124,8 +123,8 @@ export default (app: Router) => {
} }
if (config.writePathList.every((x) => !path.startsWith(x))) { if (config.writePathList.every((x) => !path.startsWith(x))) {
return res.send({ return res.send({
code: 430, code: 403,
message: '文件路径禁止访问', message: '暂无权限',
}); });
} }
@@ -175,14 +174,20 @@ export default (app: Router) => {
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try { try {
let { filename, content, path } = req.body as { let { filename, content, path } = req.body as {
filename: string; filename: string;
content: string; content: string;
path: string; path: string;
}; };
const filePath = join(config.scriptPath, path, filename); const scriptService = Container.get(ScriptService);
const filePath = scriptService.checkFilePath(path, filename);
if (!filePath) {
return res.send({
code: 403,
message: '暂无权限',
});
}
await writeFileWithLock(filePath, content); await writeFileWithLock(filePath, content);
return res.send({ code: 200 }); return res.send({ code: 200 });
} catch (e) { } catch (e) {
@@ -201,14 +206,19 @@ export default (app: Router) => {
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try { try {
let { filename, path, type } = req.body as { let { filename, path } = req.body as {
filename: string; filename: string;
path: string; path: string;
type: string;
}; };
const filePath = join(config.scriptPath, path, filename); const scriptService = Container.get(ScriptService);
const filePath = scriptService.checkFilePath(path, filename);
if (!filePath) {
return res.send({
code: 403,
message: '暂无权限',
});
}
await rmPath(filePath); await rmPath(filePath);
res.send({ code: 200 }); res.send({ code: 200 });
} catch (e) { } catch (e) {
@@ -222,24 +232,30 @@ export default (app: Router) => {
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
filename: Joi.string().required(), filename: Joi.string().required(),
path: Joi.string().optional().allow(''),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try { try {
let { filename } = req.body as { let { filename, path } = req.body as {
filename: string; filename: string;
path: string;
}; };
const filePath = join(config.scriptPath, filename); if (!path) {
// const stats = fs.statSync(filePath); path = '';
// res.set({ }
// 'Content-Type': 'application/octet-stream', //告诉浏览器这是一个二进制文件 const scriptService = Container.get(ScriptService);
// 'Content-Disposition': 'attachment; filename=' + filename, //告诉浏览器这是一个需要下载的文件 const filePath = scriptService.checkFilePath(path, filename);
// 'Content-Length': stats.size //文件大小 if (!filePath) {
// }); return res.send({
// fs.createReadStream(filePath).pipe(res); code: 403,
message: '暂无权限',
});
}
return res.download(filePath, filename, (err) => { return res.download(filePath, filename, (err) => {
return next(err); if (err) {
return next(err);
}
}); });
} catch (e) { } catch (e) {
return next(e); return next(e);
+20
View File
@@ -384,6 +384,8 @@ export default (app: Router) => {
body: Joi.object({ body: Joi.object({
retries: Joi.number().optional(), retries: Joi.number().optional(),
twoFactorActivated: Joi.boolean().optional(), twoFactorActivated: Joi.boolean().optional(),
password: Joi.string().optional(),
username: Joi.string().optional(),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
@@ -396,4 +398,22 @@ export default (app: Router) => {
} }
}, },
); );
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);
}
},
);
}; };
+51
View File
@@ -0,0 +1,51 @@
import { NextFunction, Request, Response, Router } from 'express';
import Container from 'typedi';
import Logger from '../loaders/logger';
import SystemService from '../services/system';
const route = Router();
export default (app: Router) => {
app.use('/update', route);
route.put(
'/reload',
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
const result = await systemService.reloadSystem();
res.send(result);
} catch (e) {
Logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/system',
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
const result = await systemService.reloadSystem('system');
res.send(result);
} catch (e) {
Logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/data',
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
const result = await systemService.reloadSystem('data');
res.send(result);
} catch (e) {
Logger.error('🔥 error: %o', e);
return next(e);
}
},
);
};
+2 -2
View File
@@ -8,6 +8,7 @@ import path from 'path';
import { v4 as uuidV4 } from 'uuid'; import { v4 as uuidV4 } from 'uuid';
import rateLimit from 'express-rate-limit'; import rateLimit from 'express-rate-limit';
import config from '../config'; import config from '../config';
import { isDemoEnv } from '../config/util';
const route = Router(); const route = Router();
const storage = multer.diskStorage({ const storage = multer.diskStorage({
@@ -72,9 +73,8 @@ export default (app: Router) => {
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try { try {
if (process.env.DeployEnv === 'demo') { if (isDemoEnv()) {
return res.send({ code: 450, message: '未知错误' }); return res.send({ code: 450, message: '未知错误' });
} }
const userService = Container.get(UserService); const userService = Container.get(UserService);
+85 -19
View File
@@ -1,30 +1,96 @@
import 'reflect-metadata'; // We need this in order to use @Decorators import 'reflect-metadata';
import config from './config'; import compression from 'compression';
import cors from 'cors';
import express from 'express'; import express from 'express';
import helmet from 'helmet';
import { Container } from 'typedi';
import config from './config';
import Logger from './loaders/logger'; import Logger from './loaders/logger';
import { monitoringMiddleware } from './middlewares/monitoring';
import { type GrpcServerService } from './services/grpc';
import { type HttpServerService } from './services/http';
async function startServer() { class Application {
const app = express(); private app: express.Application;
private httpServerService?: HttpServerService;
private grpcServerService?: GrpcServerService;
private isShuttingDown = false;
await require('./loaders/db').default(); constructor() {
this.app = express();
}
await require('./loaders/initFile').default(); async start() {
try {
await this.initializeDatabase();
await this.initServer();
this.setupMiddlewares();
await this.initializeServices();
this.setupGracefulShutdown();
await require('./loaders/app').default({ expressApp: app });
const server = app
.listen(config.port, '0.0.0.0', () => {
Logger.debug(`✌️ 后端服务启动成功!`);
console.debug(`✌️ 后端服务启动成功!`);
process.send?.('ready'); process.send?.('ready');
}) } catch (error) {
.on('error', (err) => { Logger.error('Failed to start application:', error);
Logger.error(err);
console.error(err);
process.exit(1); process.exit(1);
}); }
}
await require('./loaders/server').default({ server }); async initServer() {
const { HttpServerService } = await import('./services/http');
const { GrpcServerService } = await import('./services/grpc');
this.httpServerService = Container.get(HttpServerService);
this.grpcServerService = Container.get(GrpcServerService);
}
private async initializeDatabase() {
await require('./loaders/db').default();
}
private setupMiddlewares() {
this.app.use(helmet());
this.app.use(cors(config.cors));
this.app.use(compression());
this.app.use(monitoringMiddleware);
}
private async initializeServices() {
await this.grpcServerService?.initialize();
await require('./loaders/app').default({ app: this.app });
const server = await this.httpServerService?.initialize(
this.app,
config.port,
);
await require('./loaders/server').default({ server });
}
private setupGracefulShutdown() {
const shutdown = async () => {
if (this.isShuttingDown) return;
this.isShuttingDown = true;
Logger.info('Shutting down services...');
try {
await Promise.all([
this.grpcServerService?.shutdown(),
this.httpServerService?.shutdown(),
]);
process.exit(0);
} catch (error) {
Logger.error('Error during shutdown:', error);
process.exit(1);
}
};
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
}
} }
startServer(); const app = new Application();
app.start().catch((error) => {
Logger.error('Application failed to start:', error);
process.exit(1);
});
+2
View File
@@ -23,3 +23,5 @@ export const SAMPLE_FILES = [
target: 'data/scripts/notify.py', target: 'data/scripts/notify.py',
}, },
]; ];
export const PYTHON_INSTALL_DIR = process.env.PYTHON_HOME;
+68
View File
@@ -0,0 +1,68 @@
import { request as undiciRequest, Dispatcher } from 'undici';
type RequestBaseOptions = {
dispatcher?: Dispatcher;
json?: Record<string, any>;
form?: string;
headers?: Record<string, string>;
} & Omit<Dispatcher.RequestOptions<null>, 'origin' | 'path' | 'method'>;
type RequestOptionsWithOptions = RequestBaseOptions &
Partial<Pick<Dispatcher.RequestOptions, 'method'>>;
type ResponseTypeMap = {
json: Record<string, any>;
text: string;
};
type ResponseTypeKey = keyof ResponseTypeMap;
async function request(
url: string,
options?: RequestOptionsWithOptions,
): Promise<Dispatcher.ResponseData<null>> {
const { json, form, body, headers = {}, ...rest } = options || {};
const finalHeaders = { ...headers } as Record<string, string>;
let finalBody = body;
if (json) {
finalHeaders['content-type'] = 'application/json';
finalBody = JSON.stringify(json);
} else if (form) {
finalBody = form;
delete finalHeaders['content-type'];
}
const res = await undiciRequest(url, {
method: 'POST',
headers: finalHeaders,
body: finalBody,
...rest,
});
return res;
}
async function post<T extends ResponseTypeKey = 'json'>(
url: string,
options?: RequestBaseOptions & { responseType?: T },
): Promise<ResponseTypeMap[T]> {
const resp = await request(url, { ...options, method: 'POST' });
const rawText = await resp.body.text();
if (options?.responseType === 'text') {
return rawText as ResponseTypeMap[T];
}
try {
return JSON.parse(rawText) as ResponseTypeMap[T];
} catch {
return rawText as ResponseTypeMap[T];
}
}
export const httpClient = {
post,
request,
};
+55 -15
View File
@@ -2,12 +2,60 @@ import dotenv from 'dotenv';
import path from 'path'; import path from 'path';
import { createRandomString } from './share'; import { createRandomString } from './share';
dotenv.config({
path: path.join(__dirname, '../../.env'),
});
interface Config {
port: number;
grpcPort: number;
nodeEnv: string;
isDevelopment: boolean;
isProduction: boolean;
jwt: {
secret: string;
expiresIn?: string;
};
cors: {
origin: string[];
methods: string[];
};
logs: {
level: string;
};
api: {
prefix: string;
};
}
const config: Config = {
port: parseInt(process.env.BACK_PORT || '5600', 10),
grpcPort: parseInt(process.env.GRPC_PORT || '5500', 10),
nodeEnv: process.env.NODE_ENV || 'development',
isDevelopment: process.env.NODE_ENV === 'development',
isProduction: process.env.NODE_ENV === 'production',
logs: {
level: process.env.LOG_LEVEL || 'silly',
},
api: {
prefix: '/api',
},
jwt: {
secret: process.env.JWT_SECRET || createRandomString(16, 32),
expiresIn: process.env.JWT_EXPIRES_IN,
},
cors: {
origin: process.env.CORS_ORIGIN
? process.env.CORS_ORIGIN.split(',')
: ['*'],
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
},
};
process.env.NODE_ENV = process.env.NODE_ENV || 'development'; process.env.NODE_ENV = process.env.NODE_ENV || 'development';
if (!process.env.QL_DIR) { if (!process.env.QL_DIR) {
// 声明QL_DIR环境变量
let qlHomePath = path.join(__dirname, '../../'); let qlHomePath = path.join(__dirname, '../../');
// 生产环境
if (qlHomePath.endsWith('/static/')) { if (qlHomePath.endsWith('/static/')) {
qlHomePath = path.join(qlHomePath, '../'); qlHomePath = path.join(qlHomePath, '../');
} }
@@ -42,8 +90,8 @@ const systemLogPath = path.join(dataPath, 'syslog/');
const envFile = path.join(preloadPath, 'env.sh'); const envFile = path.join(preloadPath, 'env.sh');
const jsEnvFile = path.join(preloadPath, 'env.js'); const jsEnvFile = path.join(preloadPath, 'env.js');
const pyEnvFile = path.join(preloadPath, 'env.py'); const pyEnvFile = path.join(preloadPath, 'env.py');
const jsNotifyFile = path.join(preloadPath, 'notify.js'); const jsNotifyFile = path.join(preloadPath, '__ql_notify__.js');
const pyNotifyFile = path.join(preloadPath, 'notify.py'); 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');
@@ -65,17 +113,8 @@ if (envFound.error) {
} }
export default { export default {
port: parseInt(process.env.BACK_PORT as string, 10), ...config,
cronPort: parseInt(process.env.CRON_PORT as string, 10), jwt: config.jwt,
publicPort: parseInt(process.env.PUBLIC_PORT as string, 10),
updatePort: parseInt(process.env.UPDATE_PORT as string, 10),
secret: process.env.SECRET || createRandomString(16, 32),
logs: {
level: process.env.LOG_LEVEL || 'silly',
},
api: {
prefix: '/api',
},
rootPath, rootPath,
tmpPath, tmpPath,
dataPath, dataPath,
@@ -118,6 +157,7 @@ export default {
bakPath, bakPath,
apiWhiteList: [ apiWhiteList: [
'/api/user/login', '/api/user/login',
'/api/health',
'/open/auth/token', '/open/auth/token',
'/api/user/two-factor/login', '/api/user/two-factor/login',
'/api/system', '/api/system',
+126 -99
View File
@@ -1,16 +1,15 @@
import * as fs from 'fs/promises'; import * as fs from 'fs/promises';
import * as path from 'path'; import * as path from 'path';
import got from 'got';
import iconv from 'iconv-lite';
import { exec } from 'child_process'; import { exec } from 'child_process';
import FormData from 'form-data'; import psTreeFun from 'ps-tree';
import psTreeFun from 'pstree.remy';
import { promisify } from 'util'; import { promisify } from 'util';
import { load } from 'js-yaml'; import { load } from 'js-yaml';
import config from './index'; import config from './index';
import { TASK_COMMAND } from './const'; import { PYTHON_INSTALL_DIR, TASK_COMMAND } from './const';
import Logger from '../loaders/logger'; import Logger from '../loaders/logger';
import { writeFileWithLock } from '../shared/utils'; import { writeFileWithLock } from '../shared/utils';
import { DependenceTypes } from '../data/dependence';
import { FormData } from 'undici';
export * from './share'; export * from './share';
@@ -57,78 +56,6 @@ export function getToken(req: any) {
return ''; return '';
} }
export async function getNetIp(req: any) {
const ipArray = [
...new Set([
...(req.headers['x-real-ip'] || '').split(','),
...(req.headers['x-forwarded-for'] || '').split(','),
req.ip,
...req.ips,
req.socket.remoteAddress,
]),
].filter(Boolean);
let ip = ipArray[0];
if (ipArray.length > 1) {
for (let i = 0; i < ipArray.length; i++) {
const ipNumArray = ipArray[i].split('.');
const tmp = ipNumArray[0] + '.' + ipNumArray[1];
if (
tmp === '192.168' ||
(ipNumArray[0] === '172' &&
ipNumArray[1] >= 16 &&
ipNumArray[1] <= 32) ||
tmp === '10.7' ||
tmp === '127.0'
) {
continue;
}
ip = ipArray[i];
break;
}
}
ip = ip.substr(ip.lastIndexOf(':') + 1, ip.length);
if (ip.includes('127.0') || ip.includes('192.168') || ip.includes('10.7')) {
ip = '';
}
if (!ip) {
return { address: `获取失败`, ip };
}
try {
const csdnApi = got
.get(`https://searchplugin.csdn.net/api/v1/ip/get?ip=${ip}`, {
timeout: 10000,
retry: 0,
})
.text();
const pconlineApi = got
.get(`https://whois.pconline.com.cn/ipJson.jsp?ip=${ip}&json=true`, {
timeout: 10000,
retry: 0,
})
.buffer();
const [csdnBody, pconlineBody] = await await Promise.all<any>([
csdnApi,
pconlineApi,
]);
const csdnRes = JSON.parse(csdnBody);
const pconlineRes = JSON.parse(iconv.decode(pconlineBody, 'GBK'));
let address = '';
if (csdnBody && csdnRes.code == 200) {
address = csdnRes.data.address;
} else if (pconlineRes && pconlineRes.addr) {
address = pconlineRes.addr;
}
return { address, ip };
} catch (error) {
return { address: `获取失败`, ip };
}
}
export function getPlatform(userAgent: string): 'mobile' | 'desktop' { export function getPlatform(userAgent: string): 'mobile' | 'desktop' {
const ua = userAgent.toLowerCase(); const ua = userAgent.toLowerCase();
const testUa = (regexp: RegExp) => regexp.test(ua); const testUa = (regexp: RegExp) => regexp.test(ua);
@@ -237,7 +164,7 @@ enum FileType {
'file', 'file',
} }
interface IFile { export interface IFile {
title: string; title: string;
key: string; key: string;
type: 'directory' | 'file'; type: 'directory' | 'file';
@@ -305,23 +232,51 @@ export async function readDir(
dir: string, dir: string,
baseDir: string = '', baseDir: string = '',
blacklist: string[] = [], blacklist: string[] = [],
) { ): Promise<IFile[]> {
const relativePath = path.relative(baseDir, dir); const absoluteDir = path.join(baseDir, dir);
const files = await fs.readdir(dir); const relativePath = path.relative(baseDir, absoluteDir);
const result: any = files
.filter((x) => !blacklist.includes(x)) try {
.map(async (file: string) => { const files = await fs.readdir(absoluteDir);
const subPath = path.join(dir, file); const result: IFile[] = [];
for (const file of files) {
const subPath = path.join(absoluteDir, file);
const stats = await fs.lstat(subPath); const stats = await fs.lstat(subPath);
const key = path.join(relativePath, file); const key = path.join(relativePath, file);
return {
title: file, if (blacklist.includes(file) || stats.isSymbolicLink()) {
type: stats.isDirectory() ? 'directory' : 'file', continue;
key, }
parent: relativePath,
}; if (stats.isDirectory()) {
}); result.push({
return result; title: file,
type: 'directory',
key,
parent: relativePath,
createTime: stats.birthtime.getTime(),
children: [],
});
} else {
result.push({
title: file,
type: 'file',
key,
parent: relativePath,
size: stats.size,
createTime: stats.birthtime.getTime(),
});
}
}
return result;
} catch (error: any) {
if (error.code === 'ENOENT') {
return [];
}
throw error;
}
} }
export async function promiseExec(command: string): Promise<string> { export async function promiseExec(command: string): Promise<string> {
@@ -352,9 +307,9 @@ export function parseHeaders(headers: string) {
if (!headers) return {}; if (!headers) return {};
const parsed: any = {}; const parsed: any = {};
let key; let key: string;
let val; let val: string;
let i; let i: number;
headers && headers &&
headers.split('\n').forEach(function parser(line) { headers.split('\n').forEach(function parser(line) {
@@ -433,11 +388,11 @@ export function parseBody(
export function psTree(pid: number): Promise<number[]> { export function psTree(pid: number): Promise<number[]> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
psTreeFun(pid, (err: any, pids: number[]) => { psTreeFun(pid, (err: any, children) => {
if (err) { if (err) {
reject(err); reject(err);
} }
resolve(pids.filter((x) => !isNaN(x))); resolve(children.map((x) => Number(x.PID)).filter((x) => !isNaN(x)));
}); });
}); });
} }
@@ -527,7 +482,7 @@ export function safeJSONParse(value?: string) {
try { try {
return JSON.parse(value); return JSON.parse(value);
} catch (error) { } catch (error) {
Logger.error('[JSON.parse失败]', error); Logger.error('[safeJSONParse失败]', error);
return {}; return {};
} }
} }
@@ -542,3 +497,75 @@ 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;
}
}
export function getGetCommand(type: DependenceTypes, name: string): string {
const baseCommands = {
[DependenceTypes.nodejs]: `pnpm ls -g | grep "${name}" | head -1`,
[DependenceTypes.python3]: `
python3 -c "exec('''
name='${name}'
try:
from importlib.metadata import version
print(version(name))
except:
import importlib.util as u
import importlib.metadata as m
spec=u.find_spec(name)
print(name if spec else '')
''')"`,
[DependenceTypes.linux]: `apk info -es ${name}`,
};
return baseCommands[type];
}
export function getInstallCommand(type: DependenceTypes, name: string): string {
const baseCommands = {
[DependenceTypes.nodejs]: 'pnpm add -g',
[DependenceTypes.python3]:
'pip3 install --disable-pip-version-check --root-user-action=ignore',
[DependenceTypes.linux]: 'apk add --no-check-certificate',
};
let command = baseCommands[type];
if (type === DependenceTypes.python3 && PYTHON_INSTALL_DIR) {
command = `${command} --prefix=${PYTHON_INSTALL_DIR}`;
}
return `${command} ${name.trim()}`;
}
export function getUninstallCommand(
type: DependenceTypes,
name: string,
): string {
const baseCommands = {
[DependenceTypes.nodejs]: 'pnpm remove -g',
[DependenceTypes.python3]:
'pip3 uninstall --disable-pip-version-check --root-user-action=ignore -y',
[DependenceTypes.linux]: 'apk del',
};
return `${baseCommands[type]} ${name.trim()}`;
}
export function isDemoEnv() {
return process.env.DeployEnv === 'demo';
}
-18
View File
@@ -41,30 +41,12 @@ export enum DependenceTypes {
'linux', 'linux',
} }
export enum InstallDependenceCommandTypes {
'pnpm add -g',
'pip3 install --disable-pip-version-check --root-user-action=ignore',
'apk add --no-check-certificate',
}
export enum GetDependenceCommandTypes {
'pnpm ls -g ',
'pip3 show --disable-pip-version-check',
'apk info -es',
}
export enum versionDependenceCommandTypes { export enum versionDependenceCommandTypes {
'@', '@',
'==', '==',
'=', '=',
} }
export enum unInstallDependenceCommandTypes {
'pnpm remove -g',
'pip3 uninstall --disable-pip-version-check --root-user-action=ignore -y',
'apk del',
}
export interface DependenceInstance export interface DependenceInstance
extends Model<Dependence, Dependence>, extends Model<Dependence, Dependence>,
Dependence {} Dependence {}
+5 -5
View File
@@ -49,9 +49,8 @@ export class PushDeerNotification extends NotificationBaseInfo {
public pushDeerUrl = ''; public pushDeerUrl = '';
} }
export class ChatNotification extends NotificationBaseInfo { export class synologyChatNotification extends NotificationBaseInfo {
public chatUrl = ''; public synologyChatUrl = '';
public chatToken = '';
} }
export class BarkNotification extends NotificationBaseInfo { export class BarkNotification extends NotificationBaseInfo {
@@ -61,7 +60,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 {
@@ -118,6 +117,7 @@ export class EmailNotification extends NotificationBaseInfo {
public emailService: string = ''; public emailService: string = '';
public emailUser: string = ''; public emailUser: string = '';
public emailPass: string = ''; public emailPass: string = '';
public emailTo: string = '';
} }
export class PushMeNotification extends NotificationBaseInfo { export class PushMeNotification extends NotificationBaseInfo {
@@ -163,7 +163,7 @@ export interface NotificationInfo
GotifyNotification, GotifyNotification,
ServerChanNotification, ServerChanNotification,
PushDeerNotification, PushDeerNotification,
ChatNotification, synologyChatNotification,
BarkNotification, BarkNotification,
TelegramBotNotification, TelegramBotNotification,
DingtalkBotNotification, DingtalkBotNotification,
+1
View File
@@ -37,6 +37,7 @@ export interface SystemConfigInfo {
nodeMirror?: string; nodeMirror?: string;
pythonMirror?: string; pythonMirror?: string;
linuxMirror?: string; linuxMirror?: string;
timezone?: string;
} }
export interface LoginLogInfo { export interface LoginLogInfo {
-7
View File
@@ -1,7 +0,0 @@
declare namespace Express {
interface Request {
platform: 'desktop' | 'mobile';
}
}
declare module 'pstree.remy';
+13
View File
@@ -0,0 +1,13 @@
export enum ScheduleType {
BOOT = '@boot',
ONCE = '@once',
}
export type ScheduleValidator = (schedule?: string) => boolean;
export type CronSchedulerPayload = {
name: string;
id: string;
schedule: string;
command: string;
extra_schedules: Array<{ schedule: string }>;
};
+9 -10
View File
@@ -5,25 +5,24 @@ import initData from './initData';
import { Application } from 'express'; import { Application } from 'express';
import linkDeps from './deps'; import linkDeps from './deps';
import initTask from './initTask'; import initTask from './initTask';
import initFile from './initFile';
export default async ({ expressApp }: { expressApp: Application }) => { export default async ({ app }: { app: Application }) => {
depInjectorLoader(); depInjectorLoader();
Logger.info('✌️ Dependency loaded'); Logger.info('✌️ Dependency loaded');
console.log('✌️ Dependency loaded');
await initData();
Logger.info('✌️ Init data loaded');
console.log('✌️ Init data loaded');
await linkDeps(); await linkDeps();
Logger.info('✌️ Link deps loaded'); Logger.info('✌️ Link deps loaded');
console.log('✌️ Link deps loaded');
initFile();
Logger.info('✌️ Init file loaded');
await initData();
Logger.info('✌️ Init data loaded');
initTask(); initTask();
Logger.info('✌️ Init task loaded'); Logger.info('✌️ Init task loaded');
console.log('✌️ Init task loaded');
expressLoader({ app: expressApp }); expressLoader({ app });
Logger.info('✌️ Express loaded'); Logger.info('✌️ Express loaded');
console.log('✌️ Express loaded');
}; };
+8
View File
@@ -0,0 +1,8 @@
import Container from 'typedi';
import CronService from '../services/cron';
export default async () => {
const cronService = Container.get(CronService);
await cronService.bootTask();
};
+1 -3
View File
@@ -57,10 +57,8 @@ export default async () => {
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) {}
console.log('✌️ DB loaded');
Logger.info('✌️ DB loaded'); Logger.info('✌️ DB loaded');
} catch (error) { } catch (error) {
console.error('✌️ DB load failed'); Logger.error('✌️ DB load failed', error);
Logger.error(error);
} }
}; };
+2 -14
View File
@@ -7,9 +7,7 @@ import { UnauthorizedError, expressjwt } from 'express-jwt';
import { getPlatform, getToken } from '../config/util'; import { getPlatform, getToken } from '../config/util';
import rewrite from 'express-urlrewrite'; import rewrite from 'express-urlrewrite';
import { errors } from 'celebrate'; import { errors } from 'celebrate';
import { createProxyMiddleware } from 'http-proxy-middleware';
import { serveEnv } from '../config/serverEnv'; import { serveEnv } from '../config/serverEnv';
import Logger from './logger';
import { IKeyvStore, shareStore } from '../shared/store'; import { IKeyvStore, shareStore } from '../shared/store';
export default ({ app }: { app: Application }) => { export default ({ app }: { app: Application }) => {
@@ -18,22 +16,12 @@ export default ({ app }: { app: Application }) => {
app.get(`${config.api.prefix}/env.js`, serveEnv); app.get(`${config.api.prefix}/env.js`, serveEnv);
app.use(`${config.api.prefix}/static`, express.static(config.uploadPath)); app.use(`${config.api.prefix}/static`, express.static(config.uploadPath));
app.use(
'/api/public',
createProxyMiddleware({
target: `http://0.0.0.0:${config.publicPort}/api`,
changeOrigin: true,
pathRewrite: { '/api/public': '' },
logger: Logger,
}),
);
app.use(bodyParser.json({ limit: '50mb' })); app.use(bodyParser.json({ limit: '50mb' }));
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true })); app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
app.use( app.use(
expressjwt({ expressjwt({
secret: config.secret, secret: config.jwt.secret,
algorithms: ['HS384'], algorithms: ['HS384'],
}).unless({ }).unless({
path: [...config.apiWhiteList, /^\/open\//], path: [...config.apiWhiteList, /^\/open\//],
@@ -50,7 +38,7 @@ export default ({ app }: { app: Application }) => {
return next(); return next();
}); });
app.use(async (req, res, next) => { app.use(async (req: Request, 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 apps = await shareStore.getApps();
+19 -14
View File
@@ -13,7 +13,7 @@ import { AuthDataType, SystemModel } from '../data/system';
import SystemService from '../services/system'; import SystemService from '../services/system';
import UserService from '../services/user'; import UserService from '../services/user';
import { writeFile, readFile } from 'fs/promises'; import { writeFile, readFile } from 'fs/promises';
import { createRandomString, safeJSONParse } from '../config/util'; import { createRandomString, fileExist, safeJSONParse } from '../config/util';
import OpenService from '../services/open'; import OpenService from '../services/open';
import { shareStore } from '../shared/store'; import { shareStore } from '../shared/store';
import Logger from './logger'; import Logger from './logger';
@@ -56,8 +56,11 @@ export default async () => {
password: 'admin', password: 'admin',
}; };
try { try {
const content = await readFile(config.authConfigFile, 'utf8'); const authFileExist = await fileExist(config.authConfigFile);
authInfo = safeJSONParse(content); if (authFileExist) {
const content = await readFile(config.authConfigFile, 'utf8');
authInfo = safeJSONParse(content);
}
} catch (error) { } catch (error) {
Logger.warn('Failed to read auth config file, using default credentials'); Logger.warn('Failed to read auth config file, using default credentials');
} }
@@ -68,24 +71,26 @@ export default async () => {
}); });
} }
const installDependencies = () => { const installDependencies = async () => {
// 初始化时安装所有处于安装中,安装成功,安装失败的依赖 const docs = await DependenceModel.findAll({
DependenceModel.findAll({
where: {}, where: {},
order: [ order: [
['type', 'DESC'], ['type', 'DESC'],
['createdAt', 'DESC'], ['createdAt', 'DESC'],
], ],
raw: true, raw: true,
}).then(async (docs) => {
await DependenceModel.update(
{ status: DependenceStatus.queued, log: [] },
{ where: { id: docs.map((x) => x.id!) } },
);
setTimeout(() => {
dependenceService.installDependenceOneByOne(docs);
}, 5000);
}); });
await DependenceModel.update(
{ status: DependenceStatus.queued, log: [] },
{ where: { id: docs.map((x) => x.id!) } },
);
setTimeout(async () => {
await dependenceService.installDependenceOneByOne(docs);
require('./bootAfter').default();
}, 5000);
}; };
// 初始化更新 linux/python/nodejs 镜像源配置 // 初始化更新 linux/python/nodejs 镜像源配置
+5 -4
View File
@@ -27,8 +27,8 @@ 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, 'notify.js'); const jsNotifyFile = path.join(preloadPath, '__ql_notify__.js');
const pyNotifyFile = path.join(preloadPath, 'notify.py'); 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 TaskBeforeJsFile = path.join(configPath, 'task_before.js');
const TaskBeforePyFile = path.join(configPath, 'task_before.py'); const TaskBeforePyFile = path.join(configPath, 'task_before.py');
@@ -116,11 +116,12 @@ export default async () => {
`Neither content nor source specified for ${item.target}`, `Neither content nor source specified for ${item.target}`,
); );
} }
const content = item.content || (await fs.readFile(item.source!)); const content =
item.content ||
(await fs.readFile(item.source!, { encoding: 'utf-8' }));
await writeFileWithLock(item.target, content); await writeFileWithLock(item.target, content);
} }
} }
Logger.info('✌️ Init file down'); Logger.info('✌️ Init file down');
console.log('✌️ Init file down');
}; };
+19 -15
View File
@@ -38,21 +38,25 @@ export default async () => {
// 运行删除日志任务 // 运行删除日志任务
const data = await systemService.getSystemConfig(); const data = await systemService.getSystemConfig();
if (data && data.info && data.info.logRemoveFrequency) { if (data && data.info) {
const rmlogCron = { if (data.info.logRemoveFrequency) {
id: data.id as number, const rmlogCron = {
name: '删除日志', id: data.id as number,
command: `ql rmlog ${data.info.logRemoveFrequency}`, name: '删除日志',
runOrigin: 'system' as const, command: `ql rmlog ${data.info.logRemoveFrequency}`,
}; runOrigin: 'system' as const,
await scheduleService.cancelIntervalTask(rmlogCron); };
scheduleService.createIntervalTask( await scheduleService.cancelIntervalTask(rmlogCron);
rmlogCron, scheduleService.createIntervalTask(
{ rmlogCron,
days: data.info.logRemoveFrequency, {
}, days: data.info.logRemoveFrequency,
true, },
); true,
);
}
systemService.updateTimezone(data.info);
} }
await subscriptionService.setSshConfig(); await subscriptionService.setSshConfig();
+50 -25
View File
@@ -4,35 +4,60 @@ 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: '️', // info图标
warn: '\ue880', warn: '⚠️', // 警告图标
error: '\ue602', error: '❌', // 错误图标
debug: '\ue67f' debug: '🐛', // debug调试图标
}
const customFormat = winston.format.combine(
winston.format.splat(),
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
winston.format.align(),
winston.format.printf((i) => `[${levelMap[i.level]}${i.level}] [${[i.timestamp]}]: ${i.message}`),
);
const defaultOptions = {
format: customFormat,
datePattern: "YYYY-MM-DD",
maxSize: "20m",
maxFiles: "7d",
}; };
const baseFormat = [
winston.format.splat(),
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.align(),
];
const consoleFormat = winston.format.combine(
winston.format.colorize({ level: true }),
...baseFormat,
winston.format.printf((info) => {
return `[${info.level} ${info.timestamp}]:${info.message}`;
}),
);
const plainFormat = winston.format.combine(
winston.format.uncolorize(),
...baseFormat,
winston.format.printf((info) => {
return `[${levelMap[info.level] || ''}${info.level} ${info.timestamp}]:${
info.message
}`;
}),
);
const consoleTransport = new winston.transports.Console({
format: consoleFormat,
level: 'debug',
});
const fileTransport = new winston.transports.DailyRotateFile({
filename: path.join(config.systemLogPath, '%DATE%.log'),
datePattern: 'YYYY-MM-DD',
maxSize: '20m',
maxFiles: '7d',
format: plainFormat,
level: config.logs.level || 'info',
});
const LoggerInstance = winston.createLogger({ const LoggerInstance = winston.createLogger({
level: config.logs.level, level: 'debug',
levels: winston.config.npm.levels, levels: winston.config.npm.levels,
transports: [ transports: [consoleTransport, fileTransport],
new winston.transports.DailyRotateFile({ exceptionHandlers: [consoleTransport, fileTransport],
filename: path.join(config.systemLogPath, '%DATE%.log'), rejectionHandlers: [consoleTransport, fileTransport],
...defaultOptions, });
})
], LoggerInstance.on('error', (error) => {
console.error('Logger error:', error);
}); });
export default LoggerInstance; export default LoggerInstance;
-106
View File
@@ -1,106 +0,0 @@
import bodyParser from 'body-parser';
import { errors } from 'celebrate';
import cors from 'cors';
import { Application, NextFunction, Request, Response } from 'express';
import { expressjwt } from 'express-jwt';
import Container from 'typedi';
import config from '../config';
import SystemService from '../services/system';
import Logger from './logger';
export default ({ app }: { app: Application }) => {
app.set('trust proxy', 'loopback');
app.use(cors());
app.use(bodyParser.json({ limit: '50mb' }));
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
app.use(
expressjwt({
secret: config.secret,
algorithms: ['HS384'],
}),
);
app.put(
'/api/reload',
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
const result = await systemService.reloadSystem();
res.send(result);
} catch (e) {
Logger.error('🔥 error: %o', e);
return next(e);
}
},
);
app.put(
'/api/system',
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
const result = await systemService.reloadSystem('system');
res.send(result);
} catch (e) {
Logger.error('🔥 error: %o', e);
return next(e);
}
},
);
app.put(
'/api/data',
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
const result = await systemService.reloadSystem('data');
res.send(result);
} catch (e) {
Logger.error('🔥 error: %o', e);
return next(e);
}
},
);
app.use((req, res, next) => {
const err: any = new Error('Not Found');
err['status'] = 404;
next(err);
});
app.use(errors());
app.use(
(
err: Error & { status: number },
req: Request,
res: Response,
next: NextFunction,
) => {
if (err.name === 'UnauthorizedError') {
return res
.status(err.status)
.send({ code: 401, message: err.message })
.end();
}
return next(err);
},
);
app.use(
(
err: Error & { status: number },
req: Request,
res: Response,
next: NextFunction,
) => {
res.status(err.status || 500);
res.json({
code: err.status || 500,
message: err.message,
});
},
);
};
+80
View File
@@ -0,0 +1,80 @@
import { Request, Response, NextFunction } from 'express';
import Logger from '../loaders/logger';
import { performance } from 'perf_hooks';
import { metricsService } from '../services/metrics';
interface RequestMetrics {
method: string;
path: string;
duration: number;
statusCode: number;
timestamp: number;
platform?: string;
}
const requestMetrics: RequestMetrics[] = [];
export const monitoringMiddleware = (
req: Request,
res: Response,
next: NextFunction,
) => {
const start = performance.now();
const originalEnd = res.end;
res.end = function (chunk?: any, encoding?: any, cb?: any) {
const duration = performance.now() - start;
const metric: RequestMetrics = {
method: req.method,
path: req.path,
duration,
statusCode: res.statusCode,
timestamp: Date.now(),
platform: req.platform,
};
requestMetrics.push(metric);
metricsService.record('http_request', duration, {
method: req.method,
path: req.path,
statusCode: res.statusCode.toString(),
...(req.platform && { platform: req.platform }),
});
if (requestMetrics.length > 1000) {
requestMetrics.shift();
}
if (duration > 1000) {
Logger.warn(
`Slow request detected: ${req.method} ${
req.path
} took ${duration.toFixed(2)}ms`,
);
}
return originalEnd.call(this, chunk, encoding, cb);
};
next();
};
export const getMetrics = () => {
return {
totalRequests: requestMetrics.length,
averageDuration:
requestMetrics.reduce((acc, curr) => acc + curr.duration, 0) /
requestMetrics.length,
requestsByMethod: requestMetrics.reduce((acc, curr) => {
acc[curr.method] = (acc[curr.method] || 0) + 1;
return acc;
}, {} as Record<string, number>),
requestsByPlatform: requestMetrics.reduce((acc, curr) => {
if (curr.platform) {
acc[curr.platform] = (acc[curr.platform] || 0) + 1;
}
return acc;
}, {} as Record<string, number>),
recentRequests: requestMetrics.slice(-10),
};
};
+71 -1
View File
@@ -8,7 +8,7 @@ message EnvItem {
optional string value = 3; optional string value = 3;
optional string remarks = 4; optional string remarks = 4;
optional int32 status = 5; optional int32 status = 5;
optional int32 position = 6; optional int64 position = 6;
} }
message GetEnvsRequest { string searchValue = 1; } message GetEnvsRequest { string searchValue = 1; }
@@ -58,6 +58,72 @@ message SystemNotifyRequest {
string content = 2; 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 { service Api {
rpc GetEnvs(GetEnvsRequest) returns (EnvsResponse) {} rpc GetEnvs(GetEnvsRequest) returns (EnvsResponse) {}
rpc CreateEnv(CreateEnvRequest) returns (EnvsResponse) {} rpc CreateEnv(CreateEnvRequest) returns (EnvsResponse) {}
@@ -69,4 +135,8 @@ service Api {
rpc UpdateEnvNames(UpdateEnvNamesRequest) returns (Response) {} rpc UpdateEnvNames(UpdateEnvNamesRequest) returns (Response) {}
rpc GetEnvById(GetEnvByIdRequest) returns (EnvResponse) {} rpc GetEnvById(GetEnvByIdRequest) returns (EnvResponse) {}
rpc SystemNotify(SystemNotifyRequest) returns (Response) {} 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) {}
} }
+1460 -110
View File
File diff suppressed because it is too large Load Diff
+68 -51
View File
@@ -1,10 +1,11 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT. // Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions: // versions:
// protoc-gen-ts_proto v1.181.2 // protoc-gen-ts_proto v2.6.1
// protoc v3.17.3 // protoc v3.17.3
// source: back/protos/cron.proto // source: back/protos/cron.proto
/* eslint-disable */ /* eslint-disable */
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
import { import {
type CallOptions, type CallOptions,
ChannelCredentials, ChannelCredentials,
@@ -17,7 +18,6 @@ import {
type ServiceError, type ServiceError,
type UntypedServiceImplementation, type 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 +29,7 @@ export interface ICron {
id: string; id: string;
schedule: string; schedule: string;
command: string; command: string;
extraSchedules: ISchedule[]; extra_schedules: ISchedule[];
name: string; name: string;
} }
@@ -51,33 +51,34 @@ function createBaseISchedule(): ISchedule {
return { schedule: "" }; return { schedule: "" };
} }
export const ISchedule = { export const ISchedule: MessageFns<ISchedule> = {
encode(message: ISchedule, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { encode(message: ISchedule, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.schedule !== "") { if (message.schedule !== "") {
writer.uint32(10).string(message.schedule); writer.uint32(10).string(message.schedule);
} }
return writer; return writer;
}, },
decode(input: _m0.Reader | Uint8Array, length?: number): ISchedule { decode(input: BinaryReader | Uint8Array, length?: number): ISchedule {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); const reader = input instanceof BinaryReader ? input : new BinaryReader(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.skipType(tag & 7); reader.skip(tag & 7);
} }
return message; return message;
}, },
@@ -105,11 +106,11 @@ export const ISchedule = {
}; };
function createBaseICron(): ICron { function createBaseICron(): ICron {
return { id: "", schedule: "", command: "", extraSchedules: [], name: "" }; return { id: "", schedule: "", command: "", extra_schedules: [], name: "" };
} }
export const ICron = { export const ICron: MessageFns<ICron> = {
encode(message: ICron, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { encode(message: ICron, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.id !== "") { if (message.id !== "") {
writer.uint32(10).string(message.id); writer.uint32(10).string(message.id);
} }
@@ -119,8 +120,8 @@ export const ICron = {
if (message.command !== "") { if (message.command !== "") {
writer.uint32(26).string(message.command); writer.uint32(26).string(message.command);
} }
for (const v of message.extraSchedules) { for (const v of message.extra_schedules) {
ISchedule.encode(v!, writer.uint32(34).fork()).ldelim(); ISchedule.encode(v!, writer.uint32(34).fork()).join();
} }
if (message.name !== "") { if (message.name !== "") {
writer.uint32(42).string(message.name); writer.uint32(42).string(message.name);
@@ -128,53 +129,58 @@ export const ICron = {
return writer; return writer;
}, },
decode(input: _m0.Reader | Uint8Array, length?: number): ICron { decode(input: BinaryReader | Uint8Array, length?: number): ICron {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); const reader = input instanceof BinaryReader ? input : new BinaryReader(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.extraSchedules.push(ISchedule.decode(reader, reader.uint32())); message.extra_schedules.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.skipType(tag & 7); reader.skip(tag & 7);
} }
return message; return message;
}, },
@@ -184,8 +190,8 @@ export const ICron = {
id: isSet(object.id) ? globalThis.String(object.id) : "", id: isSet(object.id) ? globalThis.String(object.id) : "",
schedule: isSet(object.schedule) ? globalThis.String(object.schedule) : "", schedule: isSet(object.schedule) ? globalThis.String(object.schedule) : "",
command: isSet(object.command) ? globalThis.String(object.command) : "", command: isSet(object.command) ? globalThis.String(object.command) : "",
extraSchedules: globalThis.Array.isArray(object?.extraSchedules) extra_schedules: globalThis.Array.isArray(object?.extra_schedules)
? object.extraSchedules.map((e: any) => ISchedule.fromJSON(e)) ? object.extra_schedules.map((e: any) => ISchedule.fromJSON(e))
: [], : [],
name: isSet(object.name) ? globalThis.String(object.name) : "", name: isSet(object.name) ? globalThis.String(object.name) : "",
}; };
@@ -202,8 +208,8 @@ export const ICron = {
if (message.command !== "") { if (message.command !== "") {
obj.command = message.command; obj.command = message.command;
} }
if (message.extraSchedules?.length) { if (message.extra_schedules?.length) {
obj.extraSchedules = message.extraSchedules.map((e) => ISchedule.toJSON(e)); obj.extra_schedules = message.extra_schedules.map((e) => ISchedule.toJSON(e));
} }
if (message.name !== "") { if (message.name !== "") {
obj.name = message.name; obj.name = message.name;
@@ -219,7 +225,7 @@ export const ICron = {
message.id = object.id ?? ""; message.id = object.id ?? "";
message.schedule = object.schedule ?? ""; message.schedule = object.schedule ?? "";
message.command = object.command ?? ""; message.command = object.command ?? "";
message.extraSchedules = object.extraSchedules?.map((e) => ISchedule.fromPartial(e)) || []; message.extra_schedules = object.extra_schedules?.map((e) => ISchedule.fromPartial(e)) || [];
message.name = object.name ?? ""; message.name = object.name ?? "";
return message; return message;
}, },
@@ -229,33 +235,34 @@ function createBaseAddCronRequest(): AddCronRequest {
return { crons: [] }; return { crons: [] };
} }
export const AddCronRequest = { export const AddCronRequest: MessageFns<AddCronRequest> = {
encode(message: AddCronRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { encode(message: AddCronRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
for (const v of message.crons) { for (const v of message.crons) {
ICron.encode(v!, writer.uint32(10).fork()).ldelim(); ICron.encode(v!, writer.uint32(10).fork()).join();
} }
return writer; return writer;
}, },
decode(input: _m0.Reader | Uint8Array, length?: number): AddCronRequest { decode(input: BinaryReader | Uint8Array, length?: number): AddCronRequest {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === undefined ? reader.len : reader.pos + length; let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseAddCronRequest(); const message = createBaseAddCronRequest();
while (reader.pos < end) { while (reader.pos < end) {
const tag = reader.uint32(); const tag = reader.uint32();
switch (tag >>> 3) { switch (tag >>> 3) {
case 1: case 1: {
if (tag !== 10) { if (tag !== 10) {
break; break;
} }
message.crons.push(ICron.decode(reader, reader.uint32())); message.crons.push(ICron.decode(reader, reader.uint32()));
continue; continue;
}
} }
if ((tag & 7) === 4 || tag === 0) { if ((tag & 7) === 4 || tag === 0) {
break; break;
} }
reader.skipType(tag & 7); reader.skip(tag & 7);
} }
return message; return message;
}, },
@@ -286,13 +293,13 @@ function createBaseAddCronResponse(): AddCronResponse {
return {}; return {};
} }
export const AddCronResponse = { export const AddCronResponse: MessageFns<AddCronResponse> = {
encode(_: AddCronResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { encode(_: AddCronResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
return writer; return writer;
}, },
decode(input: _m0.Reader | Uint8Array, length?: number): AddCronResponse { decode(input: BinaryReader | Uint8Array, length?: number): AddCronResponse {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); const reader = input instanceof BinaryReader ? input : new BinaryReader(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) {
@@ -302,7 +309,7 @@ export const AddCronResponse = {
if ((tag & 7) === 4 || tag === 0) { if ((tag & 7) === 4 || tag === 0) {
break; break;
} }
reader.skipType(tag & 7); reader.skip(tag & 7);
} }
return message; return message;
}, },
@@ -329,33 +336,34 @@ function createBaseDeleteCronRequest(): DeleteCronRequest {
return { ids: [] }; return { ids: [] };
} }
export const DeleteCronRequest = { export const DeleteCronRequest: MessageFns<DeleteCronRequest> = {
encode(message: DeleteCronRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { encode(message: DeleteCronRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
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: _m0.Reader | Uint8Array, length?: number): DeleteCronRequest { decode(input: BinaryReader | Uint8Array, length?: number): DeleteCronRequest {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === undefined ? reader.len : reader.pos + length; let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseDeleteCronRequest(); const message = createBaseDeleteCronRequest();
while (reader.pos < end) { while (reader.pos < end) {
const tag = reader.uint32(); const tag = reader.uint32();
switch (tag >>> 3) { switch (tag >>> 3) {
case 1: case 1: {
if (tag !== 10) { if (tag !== 10) {
break; break;
} }
message.ids.push(reader.string()); message.ids.push(reader.string());
continue; continue;
}
} }
if ((tag & 7) === 4 || tag === 0) { if ((tag & 7) === 4 || tag === 0) {
break; break;
} }
reader.skipType(tag & 7); reader.skip(tag & 7);
} }
return message; return message;
}, },
@@ -386,13 +394,13 @@ function createBaseDeleteCronResponse(): DeleteCronResponse {
return {}; return {};
} }
export const DeleteCronResponse = { export const DeleteCronResponse: MessageFns<DeleteCronResponse> = {
encode(_: DeleteCronResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { encode(_: DeleteCronResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
return writer; return writer;
}, },
decode(input: _m0.Reader | Uint8Array, length?: number): DeleteCronResponse { decode(input: BinaryReader | Uint8Array, length?: number): DeleteCronResponse {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); const reader = input instanceof BinaryReader ? input : new BinaryReader(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) {
@@ -402,7 +410,7 @@ export const DeleteCronResponse = {
if ((tag & 7) === 4 || tag === 0) { if ((tag & 7) === 4 || tag === 0) {
break; break;
} }
reader.skipType(tag & 7); reader.skip(tag & 7);
} }
return message; return message;
}, },
@@ -506,3 +514,12 @@ 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;
}
+27 -16
View File
@@ -1,25 +1,25 @@
// Code generated by protoc-gen-ts_proto. DO NOT EDIT. // Code generated by protoc-gen-ts_proto. DO NOT EDIT.
// versions: // versions:
// protoc-gen-ts_proto v1.181.2 // protoc-gen-ts_proto v2.6.1
// protoc v3.17.3 // protoc v3.17.3
// source: back/protos/health.proto // source: back/protos/health.proto
/* eslint-disable */ /* eslint-disable */
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
import { import {
type CallOptions, type CallOptions,
ChannelCredentials, ChannelCredentials,
Client, Client,
type ClientOptions, type ClientOptions,
ClientReadableStream, type ClientReadableStream,
type ClientUnaryCall, type ClientUnaryCall,
handleServerStreamingCall, type handleServerStreamingCall,
type handleUnaryCall, type handleUnaryCall,
makeGenericClientConstructor, makeGenericClientConstructor,
Metadata, Metadata,
type ServiceError, type ServiceError,
type UntypedServiceImplementation, type 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,33 +80,34 @@ function createBaseHealthCheckRequest(): HealthCheckRequest {
return { service: "" }; return { service: "" };
} }
export const HealthCheckRequest = { export const HealthCheckRequest: MessageFns<HealthCheckRequest> = {
encode(message: HealthCheckRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { encode(message: HealthCheckRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
if (message.service !== "") { if (message.service !== "") {
writer.uint32(10).string(message.service); writer.uint32(10).string(message.service);
} }
return writer; return writer;
}, },
decode(input: _m0.Reader | Uint8Array, length?: number): HealthCheckRequest { decode(input: BinaryReader | Uint8Array, length?: number): HealthCheckRequest {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === undefined ? reader.len : reader.pos + length; let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseHealthCheckRequest(); const message = createBaseHealthCheckRequest();
while (reader.pos < end) { while (reader.pos < end) {
const tag = reader.uint32(); const tag = reader.uint32();
switch (tag >>> 3) { switch (tag >>> 3) {
case 1: case 1: {
if (tag !== 10) { if (tag !== 10) {
break; break;
} }
message.service = reader.string(); message.service = reader.string();
continue; continue;
}
} }
if ((tag & 7) === 4 || tag === 0) { if ((tag & 7) === 4 || tag === 0) {
break; break;
} }
reader.skipType(tag & 7); reader.skip(tag & 7);
} }
return message; return message;
}, },
@@ -137,33 +138,34 @@ function createBaseHealthCheckResponse(): HealthCheckResponse {
return { status: 0 }; return { status: 0 };
} }
export const HealthCheckResponse = { export const HealthCheckResponse: MessageFns<HealthCheckResponse> = {
encode(message: HealthCheckResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { encode(message: HealthCheckResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
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: _m0.Reader | Uint8Array, length?: number): HealthCheckResponse { decode(input: BinaryReader | Uint8Array, length?: number): HealthCheckResponse {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === undefined ? reader.len : reader.pos + length; let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseHealthCheckResponse(); const message = createBaseHealthCheckResponse();
while (reader.pos < end) { while (reader.pos < end) {
const tag = reader.uint32(); const tag = reader.uint32();
switch (tag >>> 3) { switch (tag >>> 3) {
case 1: case 1: {
if (tag !== 8) { if (tag !== 8) {
break; break;
} }
message.status = reader.int32() as any; message.status = reader.int32() as any;
continue; continue;
}
} }
if ((tag & 7) === 4 || tag === 0) { if ((tag & 7) === 4 || tag === 0) {
break; break;
} }
reader.skipType(tag & 7); reader.skip(tag & 7);
} }
return message; return message;
}, },
@@ -262,3 +264,12 @@ 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;
}
-35
View File
@@ -1,35 +0,0 @@
import express from 'express';
import Logger from './loaders/logger';
import config from './config';
import { HealthClient } from './protos/health';
import { credentials } from '@grpc/grpc-js';
const app = express();
const client = new HealthClient(
`0.0.0.0:${config.cronPort}`,
credentials.createInsecure(),
{ 'grpc.enable_http_proxy': 0 },
);
app.get('/api/health', (req, res) => {
client.check({ service: 'cron' }, (err, response) => {
if (err) {
return res.status(200).send({ code: 500, error: err });
}
return res.status(200).send({ code: 200, data: response });
});
});
app
.listen(config.publicPort, '0.0.0.0', async () => {
await require('./loaders/db').default();
Logger.debug(`✌️ 公共服务启动成功!`);
console.debug(`✌️ 公共服务启动成功!`);
process.send?.('ready');
})
.on('error', (err) => {
Logger.error(err);
console.error(err);
process.exit(1);
});
+5 -5
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, extraSchedules, name } = item; const { id, schedule, command, extra_schedules, 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 (extraSchedules?.length) { if (extra_schedules?.length) {
extraSchedules.forEach((x) => { extra_schedules.forEach((x) => {
Logger.info( Logger.info(
'[schedule][创建定时任务], 任务ID: %s, 名称: %s, cron: %s, 执行命令: %s', '[schedule][创建定时任务], 任务ID: %s, 名称: %s, cron: %s, 执行命令: %s',
id, id,
@@ -40,8 +40,8 @@ const addCron = (
Logger.info(`[schedule][准备运行任务] 命令: ${command}`); Logger.info(`[schedule][准备运行任务] 命令: ${command}`);
runCron(command, item); runCron(command, item);
}), }),
...(extraSchedules?.length ...(extra_schedules?.length
? extraSchedules.map((x) => ? extra_schedules.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, item);
+148 -1
View File
@@ -4,6 +4,7 @@ import EnvService from '../services/env';
import { sendUnaryData, ServerUnaryCall } from '@grpc/grpc-js'; import { sendUnaryData, ServerUnaryCall } from '@grpc/grpc-js';
import { import {
CreateEnvRequest, CreateEnvRequest,
CronItem,
DeleteEnvsRequest, DeleteEnvsRequest,
DisableEnvsRequest, DisableEnvsRequest,
EnableEnvsRequest, EnableEnvsRequest,
@@ -21,6 +22,15 @@ import {
import LoggerInstance from '../loaders/logger'; import LoggerInstance from '../loaders/logger';
import pick from 'lodash/pick'; import pick from 'lodash/pick';
import SystemService from '../services/system'; import SystemService from '../services/system';
import CronService from '../services/cron';
import {
CronDetailRequest,
CronDetailResponse,
CreateCronRequest,
UpdateCronRequest,
DeleteCronsRequest,
CronResponse,
} from '../protos/api';
Container.set('logger', LoggerInstance); Container.set('logger', LoggerInstance);
@@ -62,9 +72,17 @@ export const updateEnv = async (
callback: sendUnaryData<EnvResponse>, callback: sendUnaryData<EnvResponse>,
) => { ) => {
try { try {
if (!call.request.env?.id) {
return callback(null, {
code: 400,
data: undefined,
message: 'id parameter is required',
});
}
const envService = Container.get(EnvService); const envService = Container.get(EnvService);
const data = await envService.update( const data = await envService.update(
pick(call.request.env, ['id', 'name', 'value', 'remark']) as EnvItem, pick(call.request.env, ['id', 'name', 'value', 'remarks']) as EnvItem,
); );
callback(null, { code: 200, data }); callback(null, { code: 200, data });
} catch (e: any) { } catch (e: any) {
@@ -77,6 +95,13 @@ export const deleteEnvs = async (
callback: sendUnaryData<Response>, callback: sendUnaryData<Response>,
) => { ) => {
try { try {
if (!call.request.ids || call.request.ids.length === 0) {
return callback(null, {
code: 400,
message: 'ids parameter is required',
});
}
const envService = Container.get(EnvService); const envService = Container.get(EnvService);
await envService.remove(call.request.ids); await envService.remove(call.request.ids);
callback(null, { code: 200 }); callback(null, { code: 200 });
@@ -90,6 +115,14 @@ export const moveEnv = async (
callback: sendUnaryData<EnvResponse>, callback: sendUnaryData<EnvResponse>,
) => { ) => {
try { try {
if (!call.request.id) {
return callback(null, {
code: 400,
data: undefined,
message: 'id parameter is required',
});
}
const envService = Container.get(EnvService); const envService = Container.get(EnvService);
const data = await envService.move(call.request.id, { const data = await envService.move(call.request.id, {
fromIndex: call.request.fromIndex, fromIndex: call.request.fromIndex,
@@ -106,6 +139,13 @@ export const disableEnvs = async (
callback: sendUnaryData<Response>, callback: sendUnaryData<Response>,
) => { ) => {
try { try {
if (!call.request.ids || call.request.ids.length === 0) {
return callback(null, {
code: 400,
message: 'ids parameter is required',
});
}
const envService = Container.get(EnvService); const envService = Container.get(EnvService);
await envService.disabled(call.request.ids); await envService.disabled(call.request.ids);
callback(null, { code: 200 }); callback(null, { code: 200 });
@@ -119,6 +159,13 @@ export const enableEnvs = async (
callback: sendUnaryData<Response>, callback: sendUnaryData<Response>,
) => { ) => {
try { try {
if (!call.request.ids || call.request.ids.length === 0) {
return callback(null, {
code: 400,
message: 'ids parameter is required',
});
}
const envService = Container.get(EnvService); const envService = Container.get(EnvService);
await envService.enabled(call.request.ids); await envService.enabled(call.request.ids);
callback(null, { code: 200 }); callback(null, { code: 200 });
@@ -132,6 +179,13 @@ export const updateEnvNames = async (
callback: sendUnaryData<Response>, callback: sendUnaryData<Response>,
) => { ) => {
try { try {
if (!call.request.ids || call.request.ids.length === 0) {
return callback(null, {
code: 400,
message: 'ids parameter is required',
});
}
const envService = Container.get(EnvService); const envService = Container.get(EnvService);
await envService.updateNames({ await envService.updateNames({
ids: call.request.ids, ids: call.request.ids,
@@ -148,6 +202,14 @@ export const getEnvById = async (
callback: sendUnaryData<EnvResponse>, callback: sendUnaryData<EnvResponse>,
) => { ) => {
try { try {
if (!call.request.id) {
return callback(null, {
code: 400,
data: undefined,
message: 'id parameter is required',
});
}
const envService = Container.get(EnvService); const envService = Container.get(EnvService);
const data = await envService.getDb({ id: call.request.id }); const data = await envService.getDb({ id: call.request.id });
callback(null, { callback(null, {
@@ -171,3 +233,88 @@ export const systemNotify = async (
callback(e); 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 -1
View File
@@ -10,7 +10,7 @@ import config from '../config';
class Client { class Client {
private client = new CronClient( private client = new CronClient(
`0.0.0.0:${config.cronPort}`, `0.0.0.0:${config.grpcPort}`,
credentials.createInsecure(), credentials.createInsecure(),
{ 'grpc.enable_http_proxy': 0 }, { 'grpc.enable_http_proxy': 0 },
); );
-27
View File
@@ -1,27 +0,0 @@
import { Server, ServerCredentials } from '@grpc/grpc-js';
import { CronService } from '../protos/cron';
import { addCron } from './addCron';
import { delCron } from './delCron';
import { HealthService } from '../protos/health';
import { check } from './health';
import config from '../config';
import Logger from '../loaders/logger';
import { ApiService } from '../protos/api';
import * as Api from './api';
const server = new Server({ 'grpc.enable_http_proxy': 0 });
server.addService(HealthService, { check });
server.addService(CronService, { addCron, delCron });
server.addService(ApiService, Api);
server.bindAsync(
`0.0.0.0:${config.cronPort}`,
ServerCredentials.createInsecure(),
(err, port) => {
if (err) {
throw err;
}
Logger.debug(`✌️ 定时服务启动成功!`);
console.debug(`✌️ 定时服务启动成功!`);
process.send?.('ready');
},
);
+3 -3
View File
@@ -3,7 +3,7 @@ import path, { join } from 'path';
import config from '../config'; import config from '../config';
import { getFileContentByName } from '../config/util'; import { getFileContentByName } from '../config/util';
import { Response } from 'express'; import { Response } from 'express';
import got from 'got'; import { request } from 'undici';
@Service() @Service()
export default class ConfigService { export default class ConfigService {
@@ -27,10 +27,10 @@ export default class ConfigService {
} }
if (filePath.startsWith('sample/')) { if (filePath.startsWith('sample/')) {
const res = await got.get( const res = await request(
`https://gitlab.com/whyour/qinglong/-/raw/master/${filePath}`, `https://gitlab.com/whyour/qinglong/-/raw/master/${filePath}`,
); );
content = res.body; content = await res.body.text();
} else if (filePath.startsWith('data/scripts/')) { } else if (filePath.startsWith('data/scripts/')) {
content = await getFileContentByName(join(config.rootPath, filePath)); content = await getFileContentByName(join(config.rootPath, filePath));
} else { } else {
+114 -42
View File
@@ -11,6 +11,7 @@ import {
killTask, killTask,
getUniqPath, getUniqPath,
safeJSONParse, safeJSONParse,
isDemoEnv,
} 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, Order } from 'sequelize';
import path from 'path'; import path from 'path';
@@ -22,6 +23,7 @@ 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 { writeFileWithLock } from '../shared/utils';
import { ScheduleType } from '../interface/schedule';
@Service() @Service()
export default class CronService { export default class CronService {
@@ -35,22 +37,40 @@ 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 (isDemoEnv()) {
return 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),
extraSchedules: doc.extra_schedules || [], extra_schedules: doc.extra_schedules || [],
}, },
]); ]);
} }
await this.set_crontab();
await this.setCrontab();
return doc; return doc;
} }
@@ -58,29 +78,33 @@ export default class CronService {
return await CrontabModel.create(payload, { returning: true }); return await CrontabModel.create(payload, { returning: true });
} }
public async update(payload: Crontab): Promise<Crontab> { public async update(payload: Partial<Crontab>): Promise<Crontab> {
const doc = await this.getDb({ id: payload.id }); const 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 || isDemoEnv()) {
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),
extraSchedules: newDoc.extra_schedules || [], extra_schedules: newDoc.extra_schedules || [],
}, },
]); ]);
} }
await this.set_crontab();
await this.setCrontab();
return newDoc; return newDoc;
} }
@@ -135,7 +159,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.set_crontab(); await this.setCrontab();
} }
public async pin(ids: number[]) { public async pin(ids: number[]) {
@@ -179,8 +203,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 = null; let operate2: any = null;
let operate = null; let operate: any = null;
switch (operation) { switch (operation) {
case 'Reg': case 'Reg':
operate = Op.like; operate = Op.like;
@@ -191,21 +215,38 @@ export default class CronService {
operate2 = Op.and; operate2 = Op.and;
break; break;
case 'In': case 'In':
q[Op.or] = [ if (
{ property === 'status' &&
[property]: Array.isArray(value) ? value : [value], !value.includes(CrontabStatus.disabled)
}, ) {
property === 'status' && value.includes(2) q[Op.and] = [
? { isDisabled: 1 } { [property]: Array.isArray(value) ? value : [value] },
: {}, { isDisabled: 0 },
]; ];
} else {
q[Op.or] = [
{
[property]: Array.isArray(value) ? value : [value],
},
property === 'status' && value.includes(CrontabStatus.disabled)
? { isDisabled: 1 }
: {},
];
}
break; break;
case 'Nin': case 'Nin':
q[Op.and] = [ q[Op.and] = [
{ {
[property]: { [Op.or]: [
[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 } }
@@ -320,10 +361,10 @@ export default class CronService {
log_path, log_path,
}: { }: {
log_path: string; log_path: string;
}): Promise<Crontab | null> { }): Promise<Crontab | undefined> {
try { try {
const result = await CrontabModel.findOne({ where: { log_path } }); const result = await CrontabModel.findOne({ where: { log_path } });
return result; return result?.get({ plain: true });
} catch (error) { } catch (error) {
throw error; throw error;
} }
@@ -374,7 +415,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, total: count }; return { data: result.map((x) => x.get({ plain: true })), total: count };
} catch (error) { } catch (error) {
throw error; throw error;
} }
@@ -424,7 +465,7 @@ export default class CronService {
name: cron.name, name: cron.name,
command: cron.command, command: cron.command,
schedule: cron.schedule, schedule: cron.schedule,
extraSchedules: cron.extra_schedules, extra_schedules: cron.extra_schedules,
}; };
if (cron.status !== CrontabStatus.queued) { if (cron.status !== CrontabStatus.queued) {
resolve(params); resolve(params);
@@ -495,23 +536,27 @@ 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.set_crontab(); await this.setCrontab();
} }
public async enabled(ids: number[]) { public async enabled(ids: number[]) {
await CrontabModel.update({ isDisabled: 0 }, { where: { id: ids } }); await CrontabModel.update({ isDisabled: 0 }, { where: { id: ids } });
const docs = await CrontabModel.findAll({ where: { id: ids } }); const docs = await CrontabModel.findAll({ where: { id: ids } });
const sixCron = docs const sixCron = docs
.filter((x) => this.isNodeCron(x)) .filter((x) => this.isNodeCron(x) && !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),
extraSchedules: doc.extra_schedules || [], extra_schedules: doc.extra_schedules || [],
})); }));
if (isDemoEnv()) {
return;
}
await cronClient.addCron(sixCron); await cronClient.addCron(sixCron);
await this.set_crontab(); await this.setCrontab();
} }
public async log(id: number) { public async log(id: number) {
@@ -525,7 +570,10 @@ export default class CronService {
if (logFileExist) { if (logFileExist) {
return await getFileContentByName(`${absolutePath}`); return await getFileContentByName(`${absolutePath}`);
} else { } else {
return '任务未运行'; return typeof doc.status === 'number' &&
[CrontabStatus.queued, CrontabStatus.running].includes(doc.status)
? '运行中...'
: '任务空闲中';
} }
} }
@@ -579,15 +627,14 @@ export default class CronService {
return crontab_job_string; return crontab_job_string;
} }
private async set_crontab(data?: { data: Crontab[]; total: number }) { private async setCrontab(data?: { data: Crontab[]; total: number }) {
const tabs = data ?? (await this.crontabs()); const tabs = data ?? (await this.crontabs());
var crontab_string = ''; var crontab_string = '';
tabs.data.forEach((tab) => { tabs.data.forEach((tab) => {
const _schedule = tab.schedule && tab.schedule.split(/ +/);
if ( if (
tab.isDisabled === 1 || tab.isDisabled === 1 ||
_schedule!.length !== 5 || this.isNodeCron(tab) ||
tab.extra_schedules?.length this.isSpecialSchedule(tab.schedule)
) { ) {
crontab_string += '# '; crontab_string += '# ';
crontab_string += tab.schedule; crontab_string += tab.schedule;
@@ -608,7 +655,7 @@ export default class CronService {
await CrontabModel.update({ saved: true }, { where: {} }); await CrontabModel.update({ saved: true }, { where: {} });
} }
public import_crontab() { public importCrontab() {
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();
@@ -644,17 +691,42 @@ export default class CronService {
public async autosave_crontab() { public async autosave_crontab() {
const tabs = await this.crontabs(); const tabs = await this.crontabs();
this.set_crontab(tabs); const regularCrons = tabs.data
.filter(
const sixCron = tabs.data (x) =>
.filter((x) => this.isNodeCron(x) && x.isDisabled !== 1) x.isDisabled !== 1 &&
this.isNodeCron(x) &&
!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),
extraSchedules: doc.extra_schedules || [], extra_schedules: doc.extra_schedules || [],
})); }));
await cronClient.addCron(sixCron);
if (isDemoEnv()) {
await writeFileWithLock(config.crontabFile, '');
return;
}
await cronClient.addCron(regularCrons);
this.setCrontab(tabs);
}
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!);
}
}
} }
} }
+22 -25
View File
@@ -3,12 +3,9 @@ import winston from 'winston';
import config from '../config'; import config from '../config';
import { import {
Dependence, Dependence,
InstallDependenceCommandTypes,
DependenceStatus, DependenceStatus,
DependenceTypes, DependenceTypes,
unInstallDependenceCommandTypes,
DependenceModel, DependenceModel,
GetDependenceCommandTypes,
versionDependenceCommandTypes, versionDependenceCommandTypes,
} from '../data/dependence'; } from '../data/dependence';
import { spawn } from 'cross-spawn'; import { spawn } from 'cross-spawn';
@@ -19,6 +16,9 @@ import {
getPid, getPid,
killTask, killTask,
promiseExecSuccess, promiseExecSuccess,
getInstallCommand,
getUninstallCommand,
getGetCommand,
} from '../config/util'; } from '../config/util';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import taskLimit from '../shared/pLimit'; import taskLimit from '../shared/pLimit';
@@ -67,6 +67,9 @@ export default class DependenceService {
public async remove(ids: number[], force = false): Promise<Dependence[]> { public async remove(ids: number[], force = false): Promise<Dependence[]> {
const docs = await DependenceModel.findAll({ where: { id: ids } }); const docs = await DependenceModel.findAll({ where: { id: ids } });
for (const doc of docs) {
taskLimit.removeQueuedDependency(doc);
}
const unInstalledDeps = docs.filter( const unInstalledDeps = docs.filter(
(x) => x.status !== DependenceStatus.installed, (x) => x.status !== DependenceStatus.installed,
); );
@@ -132,10 +135,12 @@ export default class DependenceService {
docs: Dependence[], docs: Dependence[],
isInstall: boolean = true, isInstall: boolean = true,
force: boolean = false, force: boolean = false,
) { ): Promise<void> {
docs.forEach((dep) => { docs.forEach((dep) => {
this.installOrUninstallDependency(dep, isInstall, force); this.installOrUninstallDependency(dep, isInstall, force);
}); });
return taskLimit.waitDependencyQueueDone();
} }
public async reInstall(ids: number[]): Promise<Dependence[]> { public async reInstall(ids: number[]): Promise<Dependence[]> {
@@ -145,6 +150,9 @@ export default class DependenceService {
); );
const docs = await DependenceModel.findAll({ where: { id: ids } }); const docs = await DependenceModel.findAll({ where: { id: ids } });
for (const doc of docs) {
taskLimit.removeQueuedDependency(doc);
}
this.installDependenceOneByOne(docs, true, true); this.installDependenceOneByOne(docs, true, true);
return docs; return docs;
} }
@@ -153,8 +161,8 @@ export default class DependenceService {
const docs = await DependenceModel.findAll({ where: { id: ids } }); const docs = await DependenceModel.findAll({ where: { id: ids } });
for (const doc of docs) { for (const doc of docs) {
taskLimit.removeQueuedDependency(doc); taskLimit.removeQueuedDependency(doc);
const depInstallCommand = InstallDependenceCommandTypes[doc.type]; const depInstallCommand = getInstallCommand(doc.type, doc.name);
const depUnInstallCommand = unInstallDependenceCommandTypes[doc.type]; const depUnInstallCommand = getUninstallCommand(doc.type, doc.name);
const installCmd = `${depInstallCommand} ${doc.name.trim()}`; const installCmd = `${depInstallCommand} ${doc.name.trim()}`;
const unInstallCmd = `${depUnInstallCommand} ${doc.name.trim()}`; const unInstallCmd = `${depUnInstallCommand} ${doc.name.trim()}`;
const pids = await Promise.all([ const pids = await Promise.all([
@@ -226,11 +234,9 @@ export default class DependenceService {
? 'installDependence' ? 'installDependence'
: 'uninstallDependence'; : 'uninstallDependence';
let depName = dependency.name.trim(); let depName = dependency.name.trim();
const depRunCommand = ( const command = isInstall
isInstall ? getInstallCommand(dependency.type, depName)
? InstallDependenceCommandTypes : getUninstallCommand(dependency.type, depName);
: unInstallDependenceCommandTypes
)[dependency.type];
const actionText = isInstall ? '安装' : '删除'; const actionText = isInstall ? '安装' : '删除';
const startTime = dayjs(); const startTime = dayjs();
@@ -246,7 +252,7 @@ export default class DependenceService {
// 判断是否已经安装过依赖 // 判断是否已经安装过依赖
if (isInstall && !force) { if (isInstall && !force) {
const getCommandPrefix = GetDependenceCommandTypes[dependency.type]; const getCommand = getGetCommand(dependency.type, depName);
const depVersionStr = versionDependenceCommandTypes[dependency.type]; const depVersionStr = versionDependenceCommandTypes[dependency.type];
let depVersion = ''; let depVersion = '';
if (depName.includes(depVersionStr)) { if (depName.includes(depVersionStr)) {
@@ -263,13 +269,7 @@ export default class DependenceService {
const isLinuxDependence = dependency.type === DependenceTypes.linux; const isLinuxDependence = dependency.type === DependenceTypes.linux;
const isPythonDependence = const isPythonDependence =
dependency.type === DependenceTypes.python3; dependency.type === DependenceTypes.python3;
const depInfo = ( const depInfo = (await promiseExecSuccess(getCommand))
await promiseExecSuccess(
isNodeDependence
? `${getCommandPrefix} | grep "${depName}" | head -1`
: `${getCommandPrefix} ${depName}`,
)
)
.replace(/\s{2,}/, ' ') .replace(/\s{2,}/, ' ')
.replace(/\s+$/, ''); .replace(/\s+$/, '');
@@ -304,12 +304,9 @@ export default class DependenceService {
const proxyStr = dependenceProxyFileExist const proxyStr = dependenceProxyFileExist
? `source ${config.dependenceProxyFile} &&` ? `source ${config.dependenceProxyFile} &&`
: ''; : '';
const cp = spawn( const cp = spawn(`${proxyStr} ${command}`, {
`${proxyStr} ${depRunCommand} ${dependency.name.trim()}`, shell: '/bin/bash',
{ });
shell: '/bin/bash',
},
);
cp.stdout.on('data', async (data) => { cp.stdout.on('data', async (data) => {
this.sockService.sendMessage({ this.sockService.sendMessage({
+64
View File
@@ -0,0 +1,64 @@
import { Server, ServerCredentials } from '@grpc/grpc-js';
import { CronService } from '../protos/cron';
import { HealthService } from '../protos/health';
import { ApiService } from '../protos/api';
import { addCron } from '../schedule/addCron';
import { delCron } from '../schedule/delCron';
import { check } from '../schedule/health';
import * as Api from '../schedule/api';
import Logger from '../loaders/logger';
import { promisify } from 'util';
import config from '../config';
import { metricsService } from './metrics';
import { Service } from 'typedi';
@Service()
export class GrpcServerService {
private server: Server = new Server({ 'grpc.enable_http_proxy': 0 });
async initialize() {
try {
this.server.addService(HealthService, { check });
this.server.addService(CronService, { addCron, delCron });
this.server.addService(ApiService, Api);
const grpcPort = config.grpcPort;
const bindAsync = promisify(this.server.bindAsync).bind(this.server);
await bindAsync(
`0.0.0.0:${grpcPort}`,
ServerCredentials.createInsecure(),
);
Logger.debug(`✌️ gRPC service started successfully`);
metricsService.record('grpc_service_start', 1, {
port: grpcPort.toString(),
});
return grpcPort;
} catch (err) {
Logger.error('Failed to start gRPC service:', err);
throw err;
}
}
async shutdown() {
try {
if (this.server) {
await new Promise((resolve) => {
this.server.tryShutdown(() => {
Logger.debug('gRPC service stopped');
metricsService.record('grpc_service_stop', 1);
resolve(null);
});
});
}
} catch (err) {
Logger.error('Error while shutting down gRPC service:', err);
throw err;
}
}
getServer() {
return this.server;
}
}
+72
View File
@@ -0,0 +1,72 @@
import { Service } from 'typedi';
import Logger from '../loaders/logger';
import { GrpcServerService } from './grpc';
import { HttpServerService } from './http';
interface HealthStatus {
status: 'ok' | 'error';
services: {
http: boolean;
grpc: boolean;
};
metrics: {
uptime: number;
memory: {
used: number;
total: number;
};
};
}
@Service()
export class HealthService {
private startTime = Date.now();
constructor(
private grpcServerService: GrpcServerService,
private httpServerService: HttpServerService,
) {}
async check(): Promise<HealthStatus> {
const status: HealthStatus = {
status: 'ok',
services: {
http: true,
grpc: true,
},
metrics: {
uptime: Math.floor((Date.now() - this.startTime) / 1000),
memory: {
used: process.memoryUsage().heapUsed,
total: process.memoryUsage().heapTotal,
},
},
};
try {
const httpServer = this.httpServerService.getServer();
if (!httpServer) {
status.services.http = false;
status.status = 'error';
}
} catch (err) {
status.services.http = false;
status.status = 'error';
Logger.error('HTTP server check failed:', err);
}
try {
const grpcServer = this.grpcServerService.getServer();
if (!grpcServer) {
status.services.grpc = false;
status.status = 'error';
}
} catch (err) {
status.services.grpc = false;
status.status = 'error';
Logger.error('gRPC server check failed:', err);
}
return status;
}
}
+53
View File
@@ -0,0 +1,53 @@
import express from 'express';
import Logger from '../loaders/logger';
import { metricsService } from './metrics';
import { Service } from 'typedi';
import { Server } from 'http';
@Service()
export class HttpServerService {
private server?: Server = undefined;
async initialize(expressApp: express.Application, port: number) {
try {
return new Promise((resolve, reject) => {
this.server = expressApp.listen(port, '0.0.0.0', () => {
Logger.debug(`✌️ HTTP service started successfully`);
metricsService.record('http_service_start', 1, {
port: port.toString(),
});
resolve(this.server);
});
this.server?.on('error', (err: Error) => {
Logger.error('Failed to start HTTP service:', err);
reject(err);
});
});
} catch (err) {
Logger.error('Failed to start HTTP service:', err);
throw err;
}
}
async shutdown() {
try {
if (this.server) {
await new Promise((resolve) => {
this.server?.close(() => {
Logger.debug('HTTP service stopped');
metricsService.record('http_service_stop', 1);
resolve(null);
});
});
}
} catch (err) {
Logger.error('Error while shutting down HTTP service:', err);
throw err;
}
}
getServer() {
return this.server;
}
}
+14
View File
@@ -0,0 +1,14 @@
import path from 'path';
import { Inject, Service } from 'typedi';
import winston from 'winston';
import config from '../config';
@Service()
export default class LogService {
constructor(@Inject('logger') private logger: winston.Logger) {}
public checkFilePath(filePath: string, fileName: string) {
const finalPath = path.resolve(config.logPath, filePath, fileName);
return finalPath.startsWith(config.logPath) ? finalPath : '';
}
}
+92
View File
@@ -0,0 +1,92 @@
import { performance } from 'perf_hooks';
import Logger from '../loaders/logger';
interface Metric {
name: string;
value: number;
timestamp: number;
tags?: Record<string, string>;
}
class MetricsService {
private metrics: Metric[] = [];
private static instance: MetricsService;
private constructor() {
// 定期清理旧数据
setInterval(() => {
const oneHourAgo = Date.now() - 3600000;
this.metrics = this.metrics.filter(m => m.timestamp > oneHourAgo);
}, 60000);
}
static getInstance(): MetricsService {
if (!MetricsService.instance) {
MetricsService.instance = new MetricsService();
}
return MetricsService.instance;
}
record(name: string, value: number, tags?: Record<string, string>) {
this.metrics.push({
name,
value,
timestamp: Date.now(),
tags,
});
}
measure(name: string, fn: () => void, tags?: Record<string, string>) {
const start = performance.now();
try {
fn();
} finally {
const duration = performance.now() - start;
this.record(name, duration, tags);
}
}
async measureAsync(name: string, fn: () => Promise<void>, tags?: Record<string, string>) {
const start = performance.now();
try {
await fn();
} finally {
const duration = performance.now() - start;
this.record(name, duration, tags);
}
}
getMetrics(name?: string, tags?: Record<string, string>) {
let filtered = this.metrics;
if (name) {
filtered = filtered.filter(m => m.name === name);
}
if (tags) {
filtered = filtered.filter(m => {
if (!m.tags) return false;
return Object.entries(tags).every(([key, value]) => m.tags![key] === value);
});
}
return {
count: filtered.length,
average: filtered.reduce((acc, curr) => acc + curr.value, 0) / filtered.length,
min: Math.min(...filtered.map(m => m.value)),
max: Math.max(...filtered.map(m => m.value)),
metrics: filtered,
};
}
report() {
const report = {
timestamp: Date.now(),
metrics: this.getMetrics(),
};
Logger.info('性能指标报告:', report);
return report;
}
}
export const metricsService = MetricsService.getInstance();
+150 -183
View File
@@ -1,11 +1,11 @@
import crypto from 'crypto'; import crypto from 'crypto';
import got from 'got';
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
import nodemailer from 'nodemailer'; import nodemailer from 'nodemailer';
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
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';
import { httpClient } from '../config/http';
import { ProxyAgent } from 'undici';
@Service() @Service()
export default class NotificationService { export default class NotificationService {
@@ -59,7 +59,7 @@ export default class NotificationService {
try { try {
return await notificationModeAction?.call(this); return await notificationModeAction?.call(this);
} catch (error: any) { } catch (error: any) {
throw error; console.error(error);
} }
} }
return false; return false;
@@ -84,8 +84,9 @@ export default class NotificationService {
private async gotify() { private async gotify() {
const { gotifyUrl, gotifyToken, gotifyPriority = 1 } = this.params; const { gotifyUrl, gotifyToken, gotifyPriority = 1 } = this.params;
try { try {
const res: any = await got const res = await httpClient.post(
.post(`${gotifyUrl}/message?token=${gotifyToken}`, { `${gotifyUrl}/message?token=${gotifyToken}`,
{
...this.gotOption, ...this.gotOption,
body: `title=${encodeURIComponent( body: `title=${encodeURIComponent(
this.title, this.title,
@@ -95,8 +96,8 @@ export default class NotificationService {
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
}, },
}) },
.json(); );
if (typeof res.id === 'number') { if (typeof res.id === 'number') {
return true; return true;
} else { } else {
@@ -110,13 +111,11 @@ export default class NotificationService {
private async goCqHttpBot() { private async goCqHttpBot() {
const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params; const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params;
try { try {
const res: any = await got const res = await httpClient.post(`${goCqHttpBotUrl}?${goCqHttpBotQq}`, {
.post(`${goCqHttpBotUrl}?${goCqHttpBotQq}`, { ...this.gotOption,
...this.gotOption, json: { message: `${this.title}\n${this.content}` },
json: { message: `${this.title}\n${this.content}` }, headers: { Authorization: 'Bearer ' + goCqHttpBotToken },
headers: { Authorization: 'Bearer ' + goCqHttpBotToken }, });
})
.json();
if (res.retcode === 0) { if (res.retcode === 0) {
return true; return true;
} else { } else {
@@ -136,15 +135,13 @@ export default class NotificationService {
: `https://sctapi.ftqq.com/${serverChanKey}.send`; : `https://sctapi.ftqq.com/${serverChanKey}.send`;
try { try {
const res: any = await got const res = await httpClient.post(url, {
.post(url, { ...this.gotOption,
...this.gotOption, body: `title=${encodeURIComponent(
body: `title=${encodeURIComponent( this.title,
this.title, )}&desp=${encodeURIComponent(this.content)}`,
)}&desp=${encodeURIComponent(this.content)}`, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, });
})
.json();
if (res.errno === 0 || res.data.errno === 0) { if (res.errno === 0 || res.data.errno === 0) {
return true; return true;
} else { } else {
@@ -159,15 +156,13 @@ export default class NotificationService {
const { pushDeerKey, pushDeerUrl } = this.params; const { pushDeerKey, pushDeerUrl } = this.params;
const url = pushDeerUrl || `https://api2.pushdeer.com/message/push`; const url = pushDeerUrl || `https://api2.pushdeer.com/message/push`;
try { try {
const res: any = await got const res = await httpClient.post(url, {
.post(url, { ...this.gotOption,
...this.gotOption, body: `pushkey=${pushDeerKey}&text=${encodeURIComponent(
body: `pushkey=${pushDeerKey}&text=${encodeURIComponent( this.title,
this.title, )}&desp=${encodeURIComponent(this.content)}&type=markdown`,
)}&desp=${encodeURIComponent(this.content)}&type=markdown`, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, });
})
.json();
if ( if (
res.content.result.length !== undefined && res.content.result.length !== undefined &&
res.content.result.length > 0 res.content.result.length > 0
@@ -182,16 +177,13 @@ export default class NotificationService {
} }
private async chat() { private async chat() {
const { chatUrl, chatToken } = this.params; const { synologyChatUrl } = this.params;
const url = `${chatUrl}${chatToken}`;
try { try {
const res: any = await got const res = await httpClient.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' }, });
})
.json();
if (res.success) { if (res.success) {
return true; return true;
} else { } else {
@@ -227,13 +219,11 @@ export default class NotificationService {
url: barkUrl, url: barkUrl,
}; };
try { try {
const res: any = await got const res = await httpClient.post(url, {
.post(url, { ...this.gotOption,
...this.gotOption, json: body,
json: body, headers: { 'Content-Type': 'application/json' },
headers: { 'Content-Type': 'application/json' }, });
})
.json();
if (res.code === 200) { if (res.code === 200) {
return true; return true;
} else { } else {
@@ -259,29 +249,17 @@ export default class NotificationService {
}/bot${telegramBotToken}/sendMessage`; }/bot${telegramBotToken}/sendMessage`;
let agent; let agent;
if (telegramBotProxyHost && telegramBotProxyPort) { if (telegramBotProxyHost && telegramBotProxyPort) {
const options: any = { agent = new ProxyAgent({
keepAlive: true, uri: `http://${authStr}${telegramBotProxyHost}:${telegramBotProxyPort}`,
keepAliveMsecs: 1000, });
maxSockets: 256,
maxFreeSockets: 256,
proxy: `http://${authStr}${telegramBotProxyHost}:${telegramBotProxyPort}`,
};
const httpAgent = new HttpProxyAgent(options);
const httpsAgent = new HttpsProxyAgent(options);
agent = {
http: httpAgent,
https: httpsAgent,
};
} }
try { try {
const res: any = await got const res = await httpClient.post(url, {
.post(url, { ...this.gotOption,
...this.gotOption, body: `chat_id=${telegramBotUserId}&text=${this.title}\n\n${this.content}&disable_web_page_preview=true`,
body: `chat_id=${telegramBotUserId}&text=${this.title}\n\n${this.content}&disable_web_page_preview=true`, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, dispatcher: agent,
agent, });
})
.json();
if (res.ok) { if (res.ok) {
return true; return true;
} else { } else {
@@ -304,17 +282,15 @@ export default class NotificationService {
} }
const url = `https://oapi.dingtalk.com/robot/send?access_token=${dingtalkBotToken}${secretParam}`; const url = `https://oapi.dingtalk.com/robot/send?access_token=${dingtalkBotToken}${secretParam}`;
try { try {
const res: any = await got const res = await httpClient.post(url, {
.post(url, { ...this.gotOption,
...this.gotOption, json: {
json: { msgtype: 'text',
msgtype: 'text', text: {
text: { content: ` ${this.title}\n\n${this.content}`,
content: ` ${this.title}\n\n${this.content}`,
},
}, },
}) },
.json(); });
if (res.errcode === 0) { if (res.errcode === 0) {
return true; return true;
} else { } else {
@@ -330,17 +306,15 @@ export default class NotificationService {
this.params; this.params;
const url = `${weWorkOrigin}/cgi-bin/webhook/send?key=${weWorkBotKey}`; const url = `${weWorkOrigin}/cgi-bin/webhook/send?key=${weWorkBotKey}`;
try { try {
const res: any = await got const res = await httpClient.post(url, {
.post(url, { ...this.gotOption,
...this.gotOption, json: {
json: { msgtype: 'text',
msgtype: 'text', text: {
text: { content: ` ${this.title}\n\n${this.content}`,
content: ` ${this.title}\n\n${this.content}`,
},
}, },
}) },
.json(); });
if (res.errcode === 0) { if (res.errcode === 0) {
return true; return true;
} else { } else {
@@ -357,15 +331,13 @@ export default class NotificationService {
const [corpid, corpsecret, touser, agentid, thumb_media_id = '1'] = const [corpid, corpsecret, touser, agentid, thumb_media_id = '1'] =
weWorkAppKey.split(','); weWorkAppKey.split(',');
const url = `${weWorkOrigin}/cgi-bin/gettoken`; const url = `${weWorkOrigin}/cgi-bin/gettoken`;
const tokenRes: any = await got const tokenRes = await httpClient.post(url, {
.post(url, { ...this.gotOption,
...this.gotOption, json: {
json: { corpid,
corpid, corpsecret,
corpsecret, },
}, });
})
.json();
let options: any = { let options: any = {
msgtype: 'mpnews', msgtype: 'mpnews',
@@ -406,20 +378,18 @@ export default class NotificationService {
} }
try { try {
const res: any = await got const res = await httpClient.post(
.post( `${weWorkOrigin}/cgi-bin/message/send?access_token=${tokenRes.access_token}`,
`${weWorkOrigin}/cgi-bin/message/send?access_token=${tokenRes.access_token}`, {
{ ...this.gotOption,
...this.gotOption, json: {
json: { touser,
touser, agentid,
agentid, safe: '0',
safe: '0', ...options,
...options,
},
}, },
) },
.json(); );
if (res.errcode === 0) { if (res.errcode === 0) {
return true; return true;
@@ -461,14 +431,12 @@ export default class NotificationService {
} }
try { try {
const res: any = await got const res = await httpClient.post(url, {
.post(url, { ...this.gotOption,
...this.gotOption, json: {
json: { ...json,
...json, },
}, });
})
.json();
if (res.code === 0) { if (res.code === 0) {
return true; return true;
} else { } else {
@@ -483,13 +451,11 @@ export default class NotificationService {
const { iGotPushKey } = this.params; const { iGotPushKey } = this.params;
const url = `https://push.hellyw.com/${iGotPushKey.toLowerCase()}`; const url = `https://push.hellyw.com/${iGotPushKey.toLowerCase()}`;
try { try {
const res: any = await got const res = await httpClient.post(url, {
.post(url, { ...this.gotOption,
...this.gotOption, body: `title=${this.title}&content=${this.content}`,
body: `title=${this.title}&content=${this.content}`, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, });
})
.json();
if (res.ret === 0) { if (res.ret === 0) {
return true; return true;
@@ -528,7 +494,7 @@ export default class NotificationService {
}, },
}; };
const res: any = await got.post(url, body).json(); const res = await httpClient.post(url, body);
if (res.code === 200) { if (res.code === 200) {
return true; return true;
@@ -552,19 +518,17 @@ export default class NotificationService {
const url = `https://www.weplusbot.com/send`; const url = `https://www.weplusbot.com/send`;
try { try {
const res: any = await got const res = await httpClient.post(url, {
.post(url, { ...this.gotOption,
...this.gotOption, json: {
json: { token: `${wePlusBotToken}`,
token: `${wePlusBotToken}`, title: `${this.title}`,
title: `${this.title}`, template: `${template}`,
template: `${template}`, content: `${content}`,
content: `${content}`, receiver: `${wePlusBotReceiver || ''}`,
receiver: `${wePlusBotReceiver || ''}`, version: `${wePlusBotVersion || 'pro'}`,
version: `${wePlusBotVersion || 'pro'}`, },
}, });
})
.json();
if (res.code === 200) { if (res.code === 200) {
return true; return true;
@@ -584,16 +548,14 @@ export default class NotificationService {
} }
try { try {
const res: any = await got const res = await httpClient.post(larkKey, {
.post(larkKey, { ...this.gotOption,
...this.gotOption, json: {
json: { msg_type: 'text',
msg_type: 'text', content: { text: `${this.title}\n\n${this.content}` },
content: { text: `${this.title}\n\n${this.content}` }, },
}, headers: { 'Content-Type': 'application/json' },
headers: { 'Content-Type': 'application/json' }, });
})
.json();
if (res.StatusCode === 0 || res.code === 0) { if (res.StatusCode === 0 || res.code === 0) {
return true; return true;
} else { } else {
@@ -605,7 +567,7 @@ export default class NotificationService {
} }
private async email() { private async email() {
const { emailPass, emailService, emailUser } = this.params; const { emailPass, emailService, emailUser, emailTo } = this.params;
try { try {
const transporter = nodemailer.createTransport({ const transporter = nodemailer.createTransport({
@@ -618,7 +580,7 @@ export default class NotificationService {
const info = await transporter.sendMail({ const info = await transporter.sendMail({
from: `"青龙快讯" <${emailUser}>`, from: `"青龙快讯" <${emailUser}>`,
to: `${emailUser}`, to: emailTo ? emailTo.split(';') : emailUser,
subject: `${this.title}`, subject: `${this.title}`,
html: `${this.content.replace(/\n/g, '<br/>')}`, html: `${this.content.replace(/\n/g, '<br/>')}`,
}); });
@@ -638,19 +600,22 @@ export default class NotificationService {
private async pushMe() { private async pushMe() {
const { pushMeKey, pushMeUrl } = this.params; const { pushMeKey, pushMeUrl } = this.params;
try { try {
const res: any = await got.post(pushMeUrl || 'https://push.i-i.me/', { const res = await httpClient.post<'text'>(
...this.gotOption, pushMeUrl || 'https://push.i-i.me/',
json: { {
push_key: pushMeKey, ...this.gotOption,
title: this.title, json: {
content: this.content, push_key: pushMeKey,
title: this.title,
content: this.content,
},
headers: { 'Content-Type': 'application/json' },
}, },
headers: { 'Content-Type': 'application/json' }, );
}); if (res === 'success') {
if (res.body === 'success') {
return true; return true;
} else { } else {
throw new Error(res.body); throw new Error(res);
} }
} catch (error: any) { } catch (error: any) {
throw new Error(error.response ? error.response.body : error); throw new Error(error.response ? error.response.body : error);
@@ -666,18 +631,19 @@ export default class NotificationService {
}; };
try { try {
const encodedTitle = encodeRfc2047(this.title); const encodedTitle = encodeRfc2047(this.title);
const res: any = await got.post( const res = await httpClient.request(
`${ntfyUrl || 'https://ntfy.sh'}/${ntfyTopic}`, `${ntfyUrl || 'https://ntfy.sh'}/${ntfyTopic}`,
{ {
...this.gotOption, ...this.gotOption,
body: `${this.content}`, body: `${this.content}`,
headers: { Title: encodedTitle, Priority: `${ntfyPriority || '3'}` }, headers: { Title: encodedTitle, Priority: `${ntfyPriority || '3'}` },
method: 'POST',
}, },
); );
if (res.statusCode === 200) { if (res.statusCode === 200) {
return true; return true;
} else { } else {
throw new Error(JSON.stringify(res)); throw new Error(await res.body.text());
} }
} catch (error: any) { } catch (error: any) {
throw new Error(error.response ? error.response.body : error); throw new Error(error.response ? error.response.body : error);
@@ -711,20 +677,18 @@ export default class NotificationService {
const url = `https://wxpusher.zjiecode.com/api/send/message`; const url = `https://wxpusher.zjiecode.com/api/send/message`;
try { try {
const res: any = await got const res = await httpClient.post(url, {
.post(url, { ...this.gotOption,
...this.gotOption, json: {
json: { appToken: wxPusherBotAppToken,
appToken: wxPusherBotAppToken, content: `<h1>${this.title}</h1><br/><div style='white-space: pre-wrap;'>${this.content}</div>`,
content: `<h1>${this.title}</h1><br/><div style='white-space: pre-wrap;'>${this.content}</div>`, summary: this.title,
summary: this.title, contentType: 2,
contentType: 2, topicIds: topicIds,
topicIds: topicIds, uids: uids,
uids: uids, verifyPayType: 0,
verifyPayType: 0, },
}, });
})
.json();
if (res.code === 1000) { if (res.code === 1000) {
return true; return true;
@@ -775,15 +739,16 @@ export default class NotificationService {
}, },
], ],
}; };
const res: any = await got.post(url, { const res = await httpClient.request(url, {
...this.gotOption, ...this.gotOption,
json: data, json: data,
headers, headers,
method: 'POST',
}); });
if (res.statusCode === 200) { if (res.statusCode === 200) {
return true; return true;
} else { } else {
throw new Error(res.body); throw new Error(await res.body.text());
} }
} }
} }
@@ -802,7 +767,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');
} }
@@ -818,15 +783,17 @@ export default class NotificationService {
allowGetBody: true, allowGetBody: true,
...bodyParam, ...bodyParam,
}; };
try { try {
const formatUrl = webhookUrl const formatUrl = webhookUrl
?.replaceAll('$title', encodeURIComponent(this.title)) ?.replaceAll('$title', encodeURIComponent(this.title))
?.replaceAll('$content', encodeURIComponent(this.content)); ?.replaceAll('$content', encodeURIComponent(this.content));
const res = await got(formatUrl, options); const res = await httpClient.request(formatUrl, options);
const text = await res.body.text();
if (String(res.statusCode).startsWith('20')) { if (String(res.statusCode).startsWith('20')) {
return true; return true;
} else { } else {
throw new Error(JSON.stringify(res)); throw new Error(await res.body.text());
} }
} catch (error: any) { } catch (error: any) {
throw new Error(error.response ? error.response.body : error); throw new Error(error.response ? error.response.body : error);
+7 -2
View File
@@ -64,10 +64,15 @@ export default class ScriptService {
return { code: 200 }; return { code: 200 };
} }
public async getFile(filePath: string, fileName: string) { public checkFilePath(filePath: string, fileName: string) {
const finalPath = path.resolve(config.scriptPath, filePath, fileName); const finalPath = path.resolve(config.scriptPath, filePath, fileName);
return finalPath.startsWith(config.scriptPath) ? finalPath : '';
}
if (!finalPath.startsWith(config.scriptPath)) { public async getFile(filePath: string, fileName: string) {
const finalPath = this.checkFilePath(filePath, fileName);
if (!finalPath) {
return ''; return '';
} }
+38 -6
View File
@@ -1,7 +1,7 @@
import { spawn } from 'cross-spawn'; import { spawn } from 'cross-spawn';
import { Response } from 'express'; import { Response } from 'express';
import fs from 'fs'; import fs from 'fs';
import got from 'got'; import { Agent, request } from 'undici';
import sum from 'lodash/sum'; import sum from 'lodash/sum';
import path from 'path'; import path from 'path';
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
@@ -16,6 +16,7 @@ import {
promiseExec, promiseExec,
readDirs, readDirs,
rmPath, rmPath,
setSystemTimezone,
} from '../config/util'; } from '../config/util';
import { import {
DependenceModel, DependenceModel,
@@ -50,7 +51,10 @@ 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 doc; return {
...doc,
info: { ...doc.info, timezone: doc.info?.timezone || 'Asia/Shanghai' },
};
} }
private async updateAuthDb(payload: SystemInfo): Promise<SystemInfo> { private async updateAuthDb(payload: SystemInfo): Promise<SystemInfo> {
@@ -272,13 +276,17 @@ export default class SystemService {
let lastVersionContent; let lastVersionContent;
try { try {
const result = await got.get( const { body } = await request(
`${config.lastVersionFile}?t=${Date.now()}`, `${config.lastVersionFile}?t=${Date.now()}`,
{ {
timeout: 30000, dispatcher: new Agent({
keepAliveTimeout: 30000,
keepAliveMaxTimeout: 30000,
}),
}, },
); );
lastVersionContent = parseContentVersion(result.body); const text = await body.text();
lastVersionContent = parseContentVersion(text);
} catch (error) {} } catch (error) {}
if (!lastVersionContent) { if (!lastVersionContent) {
@@ -353,8 +361,15 @@ export default class SystemService {
public async reloadSystem(target?: 'system' | 'data') { public async reloadSystem(target?: 'system' | 'data') {
const cmd = `real_time=true ql reload ${target || ''}`; const cmd = `real_time=true ql reload ${target || ''}`;
const cp = spawn(cmd, { shell: '/bin/bash' }); const cp = spawn(cmd, {
shell: '/bin/bash',
detached: true,
stdio: 'ignore',
});
cp.unref(); cp.unref();
setTimeout(() => {
process.exit(0);
});
return { code: 200 }; return { code: 200 };
} }
@@ -471,4 +486,21 @@ export default class SystemService {
await rmPath(path.join(config.systemLogPath, log.title)); 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: '设置时区失败' };
}
}
} }
+36 -11
View File
@@ -1,6 +1,6 @@
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 { createRandomString } from '../config/util';
import config from '../config'; import config from '../config';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { authenticator } from '@otplib/preset-default'; import { authenticator } from '@otplib/preset-default';
@@ -21,6 +21,8 @@ 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 pickBy from 'lodash/pickBy';
import isNil from 'lodash/isNil';
import { shareStore } from '../shared/store'; import { shareStore } from '../shared/store';
@Service() @Service()
@@ -93,9 +95,9 @@ export default class UserService {
} }
if (username === cUsername && password === cPassword) { if (username === cUsername && password === cPassword) {
const data = createRandomString(50, 100); const data = createRandomString(50, 100);
const expiration = twoFactorActivated ? 60 : 20; const expiration = twoFactorActivated ? '60d' : '20d';
let token = jwt.sign({ data }, config.secret as any, { let token = jwt.sign({ data }, config.jwt.secret, {
expiresIn: 60 * 60 * 24 * expiration, expiresIn: config.jwt.expiresIn || expiration,
algorithm: 'HS384', algorithm: 'HS384',
}); });
@@ -131,7 +133,14 @@ export default class UserService {
this.getLoginLog(); this.getLoginLog();
return { return {
code: 200, code: 200,
data: { token, lastip, lastaddr, lastlogon, retries, platform }, data: {
token,
lastip,
lastaddr,
lastlogon,
retries,
platform,
},
}; };
} else { } else {
await this.updateAuthInfo(content, { await this.updateAuthInfo(content, {
@@ -264,7 +273,16 @@ export default class UserService {
if (isValid) { if (isValid) {
return this.login({ username, password }, req, false); return this.login({ username, password }, req, false);
} else { } else {
const { ip, address } = await getNetIp(req); 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(' ');
}
await this.updateAuthInfo(authInfo, { await this.updateAuthInfo(authInfo, {
lastip: ip, lastip: ip,
lastaddr: address, lastaddr: address,
@@ -347,11 +365,18 @@ export default class UserService {
} }
public async resetAuthInfo(info: Partial<AuthInfo>) { public async resetAuthInfo(info: Partial<AuthInfo>) {
const { retries, twoFactorActivated } = info; const { retries, twoFactorActivated, password, username } = info;
const authInfo = await this.getAuthInfo(); const authInfo = await this.getAuthInfo();
await this.updateAuthInfo(authInfo, { const payload = pickBy(
retries, {
twoFactorActivated, retries,
}); twoFactorActivated,
password,
username,
},
(x) => !isNil(x),
);
await this.updateAuthInfo(authInfo, payload);
} }
} }
+14 -1
View File
@@ -37,7 +37,7 @@ class TaskLimit {
concurrency: Math.max(os.cpus().length, 4), concurrency: Math.max(os.cpus().length, 4),
}); });
private client = new ApiClient( private client = new ApiClient(
`0.0.0.0:${config.cronPort}`, `0.0.0.0:${config.grpcPort}`,
credentials.createInsecure(), credentials.createInsecure(),
{ 'grpc.enable_http_proxy': 0 }, { 'grpc.enable_http_proxy': 0 },
); );
@@ -189,6 +189,19 @@ class TaskLimit {
return this.scriptLimit.add(fn, options); return this.scriptLimit.add(fn, options);
} }
public async waitDependencyQueueDone(): Promise<void> {
if (this.dependenyLimit.size === 0 && this.dependenyLimit.pending === 0) {
return;
}
return new Promise((resolve) => {
const onIdle = () => {
this.dependenyLimit.removeListener('idle', onIdle);
resolve();
};
this.dependenyLimit.on('idle', onIdle);
});
}
public runDependeny<T>( public runDependeny<T>(
dependency: Dependence, dependency: Dependence,
fn: IDependencyFn<T>, fn: IDependencyFn<T>,
+1 -1
View File
@@ -13,7 +13,7 @@ function getUniqueLockPath(filePath: string) {
export async function writeFileWithLock( export async function writeFileWithLock(
filePath: string, filePath: string,
content: string | Buffer, content: string,
options: Parameters<typeof writeFile>[2] = {}, options: Parameters<typeof writeFile>[2] = {},
) { ) {
if (typeof options === 'string') { if (typeof options === 'string') {
-1
View File
@@ -2,7 +2,6 @@ import 'reflect-metadata';
import OpenService from './services/open'; import OpenService from './services/open';
import { Container } from 'typedi'; import { Container } from 'typedi';
import LoggerInstance from './loaders/logger'; import LoggerInstance from './loaders/logger';
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';
+8 -5
View File
@@ -2,7 +2,11 @@
"compilerOptions": { "compilerOptions": {
"target": "es2017", "target": "es2017",
"lib": ["ESNext"], "lib": ["ESNext"],
"typeRoots": ["./node_modules/celebrate/lib", "./node_modules/@types"], "typeRoots": [
"./types",
"../node_modules/celebrate/lib",
"../node_modules/@types"
],
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
@@ -13,12 +17,11 @@
"module": "commonjs", "module": "commonjs",
"pretty": true, "pretty": true,
"sourceMap": true, "sourceMap": true,
"outDir": "./static/build", "outDir": "../static/build",
"allowJs": true, "allowJs": true,
"noEmit": false, "noEmit": false,
"esModuleInterop": true "esModuleInterop": true
}, },
"include": ["./back/**/*", "./back.d.ts"], "include": ["./**/*"],
"exclude": ["node_modules"], "exclude": ["node_modules"]
"files": ["./back/index.d.ts"]
} }
+11
View File
@@ -0,0 +1,11 @@
/// <reference types="express" />
export {};
declare global {
namespace Express {
interface Request {
platform: 'desktop' | 'mobile';
}
}
}
-27
View File
@@ -1,27 +0,0 @@
import 'reflect-metadata'; // We need this in order to use @Decorators
import config from './config';
import express from 'express';
import depInjectorLoader from './loaders/depInjector';
import Logger from './loaders/logger';
async function startServer() {
const app = express();
depInjectorLoader();
await require('./loaders/update').default({ app });
app
.listen(config.updatePort, '0.0.0.0', () => {
Logger.debug(`✌️ 更新服务启动成功!`);
console.debug(`✌️ 更新服务启动成功!`);
process.send?.('ready');
})
.on('error', (err) => {
Logger.error(err);
console.error(err);
process.exit(1);
});
}
startServer();
+40
View File
@@ -0,0 +1,40 @@
import { Joi } from 'celebrate';
import cron_parser from 'cron-parser';
import { ScheduleType } from '../interface/schedule';
const validateSchedule = (value: string, helpers: any) => {
if (
value.startsWith(ScheduleType.ONCE) ||
value.startsWith(ScheduleType.BOOT)
) {
return value;
}
try {
if (cron_parser.parseExpression(value).hasNext()) {
return value;
}
} catch (e) {
return helpers.error('any.invalid');
}
return helpers.error('any.invalid');
};
export const scheduleSchema = Joi.string()
.required()
.custom(validateSchedule)
.messages({
'any.invalid': '无效的定时规则',
'string.empty': '定时规则不能为空',
});
export const commonCronSchema = {
name: Joi.string().optional(),
command: Joi.string().required(),
schedule: scheduleSchema,
labels: Joi.array().optional(),
sub_id: Joi.number().optional().allow(null),
extra_schedules: Joi.array().optional().allow(null),
task_before: Joi.string().optional().allow('').allow(null),
task_after: Joi.string().optional().allow('').allow(null),
};
+18 -12
View File
@@ -13,15 +13,13 @@ ARG QL_MAINTAINER="whyour"
LABEL maintainer="${QL_MAINTAINER}" LABEL maintainer="${QL_MAINTAINER}"
ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git
ARG QL_BRANCH=develop ARG QL_BRANCH=develop
ARG PYTHON_SHORT_VERSION=3.10
ENV PNPM_HOME=/root/.local/share/pnpm \ ENV QL_DIR=/ql \
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 \ QL_BRANCH=${QL_BRANCH} \
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 \
PS1="\u@\h:\w \$ " \ PS1="\u@\h:\w \$ "
QL_DIR=/ql \
QL_BRANCH=${QL_BRANCH}
VOLUME /ql/data VOLUME /ql/data
@@ -53,14 +51,11 @@ RUN set -x \
&& apk update \ && apk update \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone \ && echo "Asia/Shanghai" > /etc/timezone \
&& git config --global user.email "qinglong@@users.noreply.github.com" \ && git config --global user.email "qinglong@users.noreply.github.com" \
&& git config --global user.name "qinglong" \ && git config --global user.name "qinglong" \
&& git config --global http.postBuffer 524288000 \ && git config --global http.postBuffer 524288000 \
&& rm -rf /root/.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} \
@@ -73,11 +68,22 @@ RUN git clone --depth=1 -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
&& cp -rf /static/* ${QL_DIR}/static \ && cp -rf /static/* ${QL_DIR}/static \
&& rm -rf /static && rm -rf /static
ENV PNPM_HOME=${QL_DIR}/data/dep_cache/node \
PYTHON_HOME=${QL_DIR}/data/dep_cache/python3 \
PYTHONUSERBASE=${QL_DIR}/data/dep_cache/python3
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PNPM_HOME}:${PYTHON_HOME}/bin \
NODE_PATH=/usr/local/bin:/usr/local/lib/node_modules:${PNPM_HOME}/global/5/node_modules \
PIP_CACHE_DIR=${PYTHON_HOME}/pip \
PYTHONPATH=${PYTHON_HOME}:${PYTHON_HOME}/lib/python${PYTHON_SHORT_VERSION}:${PYTHON_HOME}/lib/python${PYTHON_SHORT_VERSION}/site-packages
RUN pip3 install --prefix ${PYTHON_HOME} requests
COPY --from=builder /tmp/build/node_modules/. /ql/node_modules/ COPY --from=builder /tmp/build/node_modules/. /ql/node_modules/
WORKDIR ${QL_DIR} WORKDIR ${QL_DIR}
HEALTHCHECK --interval=5s --timeout=2s --retries=20 \ HEALTHCHECK --interval=5s --timeout=2s --retries=20 \
CMD curl -sf --noproxy '*' http://127.0.0.1:5400/api/health || exit 1 CMD curl -sf --noproxy '*' http://127.0.0.1:5600/api/health || exit 1
ENTRYPOINT ["./docker/docker-entrypoint.sh"] ENTRYPOINT ["./docker/docker-entrypoint.sh"]
+18 -12
View File
@@ -13,15 +13,13 @@ ARG QL_MAINTAINER="whyour"
LABEL maintainer="${QL_MAINTAINER}" LABEL maintainer="${QL_MAINTAINER}"
ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git
ARG QL_BRANCH=develop ARG QL_BRANCH=develop
ARG PYTHON_SHORT_VERSION=3.11
ENV PNPM_HOME=/root/.local/share/pnpm \ ENV QL_DIR=/ql \
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 \ QL_BRANCH=${QL_BRANCH} \
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 \
PS1="\u@\h:\w \$ " \ PS1="\u@\h:\w \$ "
QL_DIR=/ql \
QL_BRANCH=${QL_BRANCH}
VOLUME /ql/data VOLUME /ql/data
@@ -53,14 +51,11 @@ RUN set -x \
&& apk update \ && apk update \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone \ && echo "Asia/Shanghai" > /etc/timezone \
&& git config --global user.email "qinglong@@users.noreply.github.com" \ && git config --global user.email "qinglong@users.noreply.github.com" \
&& git config --global user.name "qinglong" \ && git config --global user.name "qinglong" \
&& git config --global http.postBuffer 524288000 \ && git config --global http.postBuffer 524288000 \
&& rm -rf /root/.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} \
@@ -73,11 +68,22 @@ RUN git clone --depth=1 -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
&& cp -rf /static/* ${QL_DIR}/static \ && cp -rf /static/* ${QL_DIR}/static \
&& rm -rf /static && rm -rf /static
ENV PNPM_HOME=${QL_DIR}/data/dep_cache/node \
PYTHON_HOME=${QL_DIR}/data/dep_cache/python3 \
PYTHONUSERBASE=${QL_DIR}/data/dep_cache/python3
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PNPM_HOME}:${PYTHON_HOME}/bin \
NODE_PATH=/usr/local/bin:/usr/local/lib/node_modules:${PNPM_HOME}/global/5/node_modules \
PIP_CACHE_DIR=${PYTHON_HOME}/pip \
PYTHONPATH=${PYTHON_HOME}:${PYTHON_HOME}/lib/python${PYTHON_SHORT_VERSION}:${PYTHON_HOME}/lib/python${PYTHON_SHORT_VERSION}/site-packages
RUN pip3 install --prefix ${PYTHON_HOME} requests
COPY --from=builder /tmp/build/node_modules/. /ql/node_modules/ COPY --from=builder /tmp/build/node_modules/. /ql/node_modules/
WORKDIR ${QL_DIR} WORKDIR ${QL_DIR}
HEALTHCHECK --interval=5s --timeout=2s --retries=20 \ HEALTHCHECK --interval=5s --timeout=2s --retries=20 \
CMD curl -sf --noproxy '*' http://127.0.0.1:5400/api/health || exit 1 CMD curl -sf --noproxy '*' http://127.0.0.1:5600/api/health || exit 1
ENTRYPOINT ["./docker/docker-entrypoint.sh"] ENTRYPOINT ["./docker/docker-entrypoint.sh"]
+14 -15
View File
@@ -4,7 +4,15 @@ dir_shell=/ql/shell
. $dir_shell/share.sh . $dir_shell/share.sh
. $dir_shell/env.sh . $dir_shell/env.sh
echo -e "======================1. 检测配置文件========================\n" log_with_style() {
local level="$1"
local message="$2"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
printf "\n[%s] [%7s] %s\n" "${timestamp}" "${level}" "${message}"
}
log_with_style "INFO" "🚀 1. 检测配置文件..."
import_config "$@" import_config "$@"
make_dir /etc/nginx/conf.d make_dir /etc/nginx/conf.d
make_dir /run/nginx make_dir /run/nginx
@@ -13,32 +21,23 @@ fix_config
pm2 l &>/dev/null pm2 l &>/dev/null
echo -e "======================2. 安装依赖========================\n" log_with_style "INFO" "🔄 2. 启动 nginx..."
patch_version
echo -e "======================3. 启动nginx========================\n"
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
echo -e "nginx启动成功...\n"
echo -e "======================4. 启动pm2服务========================\n" log_with_style "INFO" "⚙️ 3. 启动 pm2 服务..."
reload_update
reload_pm2 reload_pm2
if [[ $AutoStartBot == true ]]; then if [[ $AutoStartBot == true ]]; then
echo -e "======================5. 启动bot========================\n" log_with_style "INFO" "🤖 4. 启动 bot..."
nohup ql bot >$dir_log/bot.log 2>&1 & nohup ql bot >$dir_log/bot.log 2>&1 &
echo -e "bot后台启动中...\n"
fi fi
if [[ $EnableExtraShell == true ]]; then if [[ $EnableExtraShell == true ]]; then
echo -e "====================6. 执行自定义脚本========================\n" log_with_style "INFO" "🛠️ 5. 执行自定义脚本..."
nohup ql extra >$dir_log/extra.log 2>&1 & nohup ql extra >$dir_log/extra.log 2>&1 &
echo -e "自定义脚本后台执行中...\n"
fi fi
echo -e "############################################################\n" log_with_style "SUCCESS" "🎉 容器启动成功!"
echo -e "容器启动成功..."
echo -e "############################################################\n"
crond -f >/dev/null crond -f >/dev/null
-32
View File
@@ -2,14 +2,6 @@ upstream baseApi {
server 0.0.0.0:5600; server 0.0.0.0:5600;
} }
upstream publicApi {
server 0.0.0.0:5400;
}
upstream updateApi {
server 0.0.0.0:5300;
}
map $http_upgrade $connection_upgrade { map $http_upgrade $connection_upgrade {
default keep-alive; default keep-alive;
'websocket' upgrade; 'websocket' upgrade;
@@ -20,30 +12,6 @@ server {
IPV6_CONFIG IPV6_CONFIG
ssl_session_timeout 5m; ssl_session_timeout 5m;
location QL_BASE_URLapi/update/ {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://updateApi/api/;
proxy_buffering off;
proxy_redirect default;
proxy_connect_timeout 1800;
proxy_send_timeout 1800;
proxy_read_timeout 1800;
}
location QL_BASE_URLapi/public/ {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://publicApi/api/;
proxy_buffering off;
proxy_redirect default;
proxy_connect_timeout 1800;
proxy_send_timeout 1800;
proxy_read_timeout 1800;
}
location QL_BASE_URLapi/ { location QL_BASE_URLapi/ {
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
+5 -25
View File
@@ -1,14 +1,14 @@
module.exports = { module.exports = {
apps: [ apps: [
{ {
name: 'schedule', name: 'qinglong',
max_restarts: 10, max_restarts: 5,
kill_timeout: 15000, kill_timeout: 1000,
wait_ready: true, wait_ready: true,
listen_timeout: 10000, listen_timeout: 5000,
source_map_support: true, source_map_support: true,
time: true, time: true,
script: 'static/build/schedule/index.js', script: 'static/build/app.js',
env: { env: {
http_proxy: '', http_proxy: '',
https_proxy: '', https_proxy: '',
@@ -18,25 +18,5 @@ module.exports = {
ALL_PROXY: '', ALL_PROXY: '',
}, },
}, },
{
name: 'public',
max_restarts: 10,
kill_timeout: 15000,
wait_ready: true,
listen_timeout: 10000,
source_map_support: true,
time: true,
script: 'static/build/public.js',
},
{
name: 'panel',
max_restarts: 10,
kill_timeout: 15000,
wait_ready: true,
listen_timeout: 10000,
source_map_support: true,
time: true,
script: 'static/build/app.js',
},
], ],
}; };
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"watch": ["back", ".env"], "watch": ["back", ".env"],
"ext": "js,ts,json", "ext": "js,ts,json",
"exec": "ts-node -P tsconfig.back.json ./back/app.ts" "exec": "ts-node -P ./back/tsconfig.json ./back/app.ts"
} }
-13
View File
@@ -1,13 +0,0 @@
module.exports = {
apps: [
{
name: 'update',
max_restarts: 10,
kill_timeout: 15000,
wait_ready: true,
listen_timeout: 10000,
time: true,
script: 'static/build/update.js',
},
],
};
+12 -15
View File
@@ -2,18 +2,12 @@
"private": true, "private": true,
"scripts": { "scripts": {
"start": "concurrently -n w: npm:start:*", "start": "concurrently -n w: npm:start:*",
"start:update": "ts-node -P tsconfig.back.json ./back/update.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:back": "nodemon", "start:back": "nodemon",
"start:front": "max dev", "start:front": "max dev",
"build:front": "max build", "build:front": "max build",
"build:back": "tsc -p tsconfig.back.json", "build:back": "tsc -p back/tsconfig.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", "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",
"public": "npm run build:back && node static/build/public.js",
"update": "npm run build:back && node static/build/update.js",
"gen:proto": "protoc --experimental_allow_proto3_optional --plugin=./node_modules/.bin/protoc-gen-ts_proto ./back/protos/*.proto --ts_proto_out=./ --ts_proto_opt=outputServices=grpc-js,env=node,esModuleInterop=true",
"gen:api": "python3 -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. ./back/protos/api.proto", "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",
@@ -76,8 +70,7 @@
"express-jwt": "^8.4.1", "express-jwt": "^8.4.1",
"express-rate-limit": "^7.4.1", "express-rate-limit": "^7.4.1",
"express-urlrewrite": "^2.0.3", "express-urlrewrite": "^2.0.3",
"form-data": "^4.0.0", "undici": "^7.9.0",
"got": "^11.8.2",
"hpagent": "^1.2.0", "hpagent": "^1.2.0",
"http-proxy-middleware": "^3.0.3", "http-proxy-middleware": "^3.0.3",
"iconv-lite": "^0.6.3", "iconv-lite": "^0.6.3",
@@ -88,8 +81,8 @@
"node-schedule": "^2.1.0", "node-schedule": "^2.1.0",
"nodemailer": "^6.9.16", "nodemailer": "^6.9.16",
"p-queue-cjs": "7.3.4", "p-queue-cjs": "7.3.4",
"protobufjs": "^7.4.0", "@bufbuild/protobuf": "^2.2.3",
"pstree.remy": "^1.1.8", "ps-tree": "^1.2.0",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"sequelize": "^6.37.5", "sequelize": "^6.37.5",
"serve-handler": "^6.1.6", "serve-handler": "^6.1.6",
@@ -104,7 +97,9 @@
"ip2region": "2.3.0", "ip2region": "2.3.0",
"keyv": "^5.2.3", "keyv": "^5.2.3",
"@keyv/sqlite": "^4.0.1", "@keyv/sqlite": "^4.0.1",
"proper-lockfile": "^4.1.2" "proper-lockfile": "^4.1.2",
"compression": "^1.7.4",
"helmet": "^6.0.1"
}, },
"devDependencies": { "devDependencies": {
"moment": "2.30.1", "moment": "2.30.1",
@@ -138,6 +133,7 @@
"@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", "@types/proper-lockfile": "^4.1.4",
"@types/ps-tree": "^1.1.6",
"@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.4.4",
@@ -171,11 +167,12 @@
"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.9.2",
"ts-proto": "^1.146.0", "ts-proto": "^2.6.1",
"tslib": "^2.4.0", "tslib": "^2.4.0",
"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",
"yorkie": "^2.0.0" "@types/compression": "^1.7.2",
"@types/helmet": "^4.0.0"
} }
} }
+126 -285
View File
@@ -8,6 +8,9 @@ overrides:
sqlite3: git+https://github.com/whyour/node-sqlite3.git#v1.0.3 sqlite3: git+https://github.com/whyour/node-sqlite3.git#v1.0.3
dependencies: dependencies:
'@bufbuild/protobuf':
specifier: ^2.2.3
version: 2.2.3
'@grpc/grpc-js': '@grpc/grpc-js':
specifier: ^1.12.3 specifier: ^1.12.3
version: 1.12.3 version: 1.12.3
@@ -29,6 +32,9 @@ dependencies:
chokidar: chokidar:
specifier: ^4.0.1 specifier: ^4.0.1
version: 4.0.1 version: 4.0.1
compression:
specifier: ^1.7.4
version: 1.7.5
cors: cors:
specifier: ^2.8.5 specifier: ^2.8.5
version: 2.8.5 version: 2.8.5
@@ -56,12 +62,9 @@ dependencies:
express-urlrewrite: express-urlrewrite:
specifier: ^2.0.3 specifier: ^2.0.3
version: 2.0.3 version: 2.0.3
form-data: helmet:
specifier: ^4.0.0 specifier: ^6.0.1
version: 4.0.1 version: 6.2.0
got:
specifier: ^11.8.2
version: 11.8.6
hpagent: hpagent:
specifier: ^1.2.0 specifier: ^1.2.0
version: 1.2.0 version: 1.2.0
@@ -101,12 +104,9 @@ dependencies:
proper-lockfile: proper-lockfile:
specifier: ^4.1.2 specifier: ^4.1.2
version: 4.1.2 version: 4.1.2
protobufjs: ps-tree:
specifier: ^7.4.0 specifier: ^1.2.0
version: 7.4.0 version: 1.2.0
pstree.remy:
specifier: ^1.1.8
version: 1.1.8
reflect-metadata: reflect-metadata:
specifier: ^0.2.2 specifier: ^0.2.2
version: 0.2.2 version: 0.2.2
@@ -131,6 +131,9 @@ dependencies:
typedi: typedi:
specifier: ^0.10.0 specifier: ^0.10.0
version: 0.10.0 version: 0.10.0
undici:
specifier: ^7.9.0
version: 7.9.0
uuid: uuid:
specifier: ^11.0.3 specifier: ^11.0.3
version: 11.0.3 version: 11.0.3
@@ -163,6 +166,9 @@ devDependencies:
'@types/body-parser': '@types/body-parser':
specifier: ^1.19.2 specifier: ^1.19.2
version: 1.19.5 version: 1.19.5
'@types/compression':
specifier: ^1.7.2
version: 1.7.5
'@types/cors': '@types/cors':
specifier: ^2.8.12 specifier: ^2.8.12
version: 2.8.17 version: 2.8.17
@@ -178,6 +184,9 @@ devDependencies:
'@types/file-saver': '@types/file-saver':
specifier: 2.0.2 specifier: 2.0.2
version: 2.0.2 version: 2.0.2
'@types/helmet':
specifier: ^4.0.0
version: 4.0.0
'@types/js-yaml': '@types/js-yaml':
specifier: ^4.0.5 specifier: ^4.0.5
version: 4.0.9 version: 4.0.9
@@ -202,6 +211,9 @@ devDependencies:
'@types/proper-lockfile': '@types/proper-lockfile':
specifier: ^4.1.4 specifier: ^4.1.4
version: 4.1.4 version: 4.1.4
'@types/ps-tree':
specifier: ^1.1.6
version: 1.1.6
'@types/qrcode.react': '@types/qrcode.react':
specifier: ^1.0.2 specifier: ^1.0.2
version: 1.0.5 version: 1.0.5
@@ -335,8 +347,8 @@ devDependencies:
specifier: ^10.9.2 specifier: ^10.9.2
version: 10.9.2(@types/node@17.0.45)(typescript@5.2.2) version: 10.9.2(@types/node@17.0.45)(typescript@5.2.2)
ts-proto: ts-proto:
specifier: ^1.146.0 specifier: ^2.6.1
version: 1.181.2 version: 2.6.1
tslib: tslib:
specifier: ^2.4.0 specifier: ^2.4.0
version: 2.8.1 version: 2.8.1
@@ -349,9 +361,6 @@ devDependencies:
virtualizedtableforantd4: virtualizedtableforantd4:
specifier: 1.3.0 specifier: 1.3.0
version: 1.3.0(antd@4.24.16)(react-dom@18.3.1)(react@18.3.1) version: 1.3.0(antd@4.24.16)(react-dom@18.3.1)(react@18.3.1)
yorkie:
specifier: ^2.0.0
version: 2.0.0
packages: packages:
@@ -1380,6 +1389,9 @@ packages:
resolution: {integrity: sha512-h0OYmPR3A5Dfbetra/GzxBAzQk8sH7LhRkRUTdagX6nrtlUgJGYCTv4bBK33jsTQw9HDd8PE2x1Ma+iRKEDUsw==} resolution: {integrity: sha512-h0OYmPR3A5Dfbetra/GzxBAzQk8sH7LhRkRUTdagX6nrtlUgJGYCTv4bBK33jsTQw9HDd8PE2x1Ma+iRKEDUsw==}
dev: true dev: true
/@bufbuild/protobuf@2.2.3:
resolution: {integrity: sha512-tFQoXHJdkEOSwj5tRIZSPNUuXK3RaR7T1nUrPgbYX1pUbvqqaaZAsfo+NXBPsz5rZMSKVFrgK1WL8Q/MSLvprg==}
/@chenshuai2144/sketch-color@1.0.9(react@18.3.1): /@chenshuai2144/sketch-color@1.0.9(react@18.3.1):
resolution: {integrity: sha512-obzSy26cb7Pm7OprWyVpgMpIlrZpZ0B7vbrU0RMbvRg0YAI890S5Xy02Aj1Nhl4+KTbi1lVYHt6HQP8Hm9s+1w==} resolution: {integrity: sha512-obzSy26cb7Pm7OprWyVpgMpIlrZpZ0B7vbrU0RMbvRg0YAI890S5Xy02Aj1Nhl4+KTbi1lVYHt6HQP8Hm9s+1w==}
peerDependencies: peerDependencies:
@@ -3271,36 +3283,46 @@ packages:
/@protobufjs/aspromise@1.1.2: /@protobufjs/aspromise@1.1.2:
resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==}
dev: false
/@protobufjs/base64@1.1.2: /@protobufjs/base64@1.1.2:
resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==}
dev: false
/@protobufjs/codegen@2.0.4: /@protobufjs/codegen@2.0.4:
resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==}
dev: false
/@protobufjs/eventemitter@1.1.0: /@protobufjs/eventemitter@1.1.0:
resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==}
dev: false
/@protobufjs/fetch@1.1.0: /@protobufjs/fetch@1.1.0:
resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==}
dependencies: dependencies:
'@protobufjs/aspromise': 1.1.2 '@protobufjs/aspromise': 1.1.2
'@protobufjs/inquire': 1.1.0 '@protobufjs/inquire': 1.1.0
dev: false
/@protobufjs/float@1.0.2: /@protobufjs/float@1.0.2:
resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==}
dev: false
/@protobufjs/inquire@1.1.0: /@protobufjs/inquire@1.1.0:
resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==}
dev: false
/@protobufjs/path@1.1.2: /@protobufjs/path@1.1.2:
resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==}
dev: false
/@protobufjs/pool@1.1.0: /@protobufjs/pool@1.1.0:
resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==}
dev: false
/@protobufjs/utf8@1.1.0: /@protobufjs/utf8@1.1.0:
resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==}
dev: false
/@qixian.cs/path-to-regexp@6.1.0: /@qixian.cs/path-to-regexp@6.1.0:
resolution: {integrity: sha512-2jIiLiVZB1jnY7IIRQKtoV8Gnr7XIhk4mC88ONGunZE3hYt5IHUG4BE/6+JiTBjjEWQLBeWnZB8hGpppkufiVw==} resolution: {integrity: sha512-2jIiLiVZB1jnY7IIRQKtoV8Gnr7XIhk4mC88ONGunZE3hYt5IHUG4BE/6+JiTBjjEWQLBeWnZB8hGpppkufiVw==}
@@ -3472,11 +3494,6 @@ packages:
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
dev: true dev: true
/@sindresorhus/is@4.6.0:
resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==}
engines: {node: '>=10'}
dev: false
/@stylelint/postcss-css-in-js@0.38.0(postcss-syntax@0.36.2)(postcss@8.4.49): /@stylelint/postcss-css-in-js@0.38.0(postcss-syntax@0.36.2)(postcss@8.4.49):
resolution: {integrity: sha512-XOz5CAe49kS95p5yRd+DAIWDojTjfmyAQ4bbDlXMdbZTQ5t0ThjSLvWI6JI2uiS7MFurVBkZ6zUqcimzcLTBoQ==} resolution: {integrity: sha512-XOz5CAe49kS95p5yRd+DAIWDojTjfmyAQ4bbDlXMdbZTQ5t0ThjSLvWI6JI2uiS7MFurVBkZ6zUqcimzcLTBoQ==}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
@@ -3664,13 +3681,6 @@ packages:
tslib: 2.8.1 tslib: 2.8.1
dev: true dev: true
/@szmarczak/http-timer@4.0.6:
resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==}
engines: {node: '>=10'}
dependencies:
defer-to-connect: 2.0.1
dev: false
/@tanstack/match-sorter-utils@8.19.4: /@tanstack/match-sorter-utils@8.19.4:
resolution: {integrity: sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==} resolution: {integrity: sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -3786,14 +3796,11 @@ packages:
'@types/node': 17.0.45 '@types/node': 17.0.45
dev: true dev: true
/@types/cacheable-request@6.0.3: /@types/compression@1.7.5:
resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} resolution: {integrity: sha512-AAQvK5pxMpaT+nDvhHrsBhLSYG5yQdtkaJE1WYieSNY2mVFKAgmU4ks65rkZD5oqnGCFLyQpUr1CqI4DmUMyDg==}
dependencies: dependencies:
'@types/http-cache-semantics': 4.0.4 '@types/express': 4.17.21
'@types/keyv': 3.1.4 dev: true
'@types/node': 17.0.45
'@types/responselike': 1.0.3
dev: false
/@types/connect@3.4.38: /@types/connect@3.4.38:
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
@@ -3865,6 +3872,13 @@ packages:
resolution: {integrity: sha512-oOMFT8vmCTFncsF1engrs04jatz8/Anwx3De9uxnOK4chgSEgWBvFtpSoJo8u3784JNO+ql5tzRR6phHoRnscQ==} resolution: {integrity: sha512-oOMFT8vmCTFncsF1engrs04jatz8/Anwx3De9uxnOK4chgSEgWBvFtpSoJo8u3784JNO+ql5tzRR6phHoRnscQ==}
dev: true dev: true
/@types/helmet@4.0.0:
resolution: {integrity: sha512-ONIn/nSNQA57yRge3oaMQESef/6QhoeX7llWeDli0UZIfz8TQMkfNPTXA8VnnyeA1WUjG2pGqdjEIueYonMdfQ==}
deprecated: This is a stub types definition. helmet provides its own type definitions, so you do not need this installed.
dependencies:
helmet: 6.2.0
dev: true
/@types/hoist-non-react-statics@3.3.5: /@types/hoist-non-react-statics@3.3.5:
resolution: {integrity: sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==} resolution: {integrity: sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==}
dependencies: dependencies:
@@ -3876,10 +3890,6 @@ packages:
resolution: {integrity: sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==} resolution: {integrity: sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==}
dev: true dev: true
/@types/http-cache-semantics@4.0.4:
resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==}
dev: false
/@types/http-errors@2.0.4: /@types/http-errors@2.0.4:
resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==}
dev: true dev: true
@@ -3937,12 +3947,6 @@ packages:
'@types/node': 17.0.45 '@types/node': 17.0.45
dev: false dev: false
/@types/keyv@3.1.4:
resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==}
dependencies:
'@types/node': 17.0.45
dev: false
/@types/lodash@4.17.13: /@types/lodash@4.17.13:
resolution: {integrity: sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==} resolution: {integrity: sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==}
dev: true dev: true
@@ -3998,6 +4002,10 @@ packages:
'@types/retry': 0.12.5 '@types/retry': 0.12.5
dev: true dev: true
/@types/ps-tree@1.1.6:
resolution: {integrity: sha512-PtrlVaOaI44/3pl3cvnlK+GxOM3re2526TJvPvh7W+keHIXdV4TE0ylpPBAcvFQCbGitaTXwL9u+RF7qtVeazQ==}
dev: true
/@types/qrcode.react@1.0.5: /@types/qrcode.react@1.0.5:
resolution: {integrity: sha512-BghPtnlwvrvq8QkGa1H25YnN+5OIgCKFuQruncGWLGJYOzeSKiix/4+B9BtfKF2wf5ja8yfyWYA3OXju995G8w==} resolution: {integrity: sha512-BghPtnlwvrvq8QkGa1H25YnN+5OIgCKFuQruncGWLGJYOzeSKiix/4+B9BtfKF2wf5ja8yfyWYA3OXju995G8w==}
dependencies: dependencies:
@@ -4041,12 +4049,6 @@ packages:
resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==} resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==}
dev: true dev: true
/@types/responselike@1.0.3:
resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==}
dependencies:
'@types/node': 17.0.45
dev: false
/@types/retry@0.12.5: /@types/retry@0.12.5:
resolution: {integrity: sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==} resolution: {integrity: sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==}
dev: true dev: true
@@ -5531,6 +5533,7 @@ packages:
/asynckit@0.4.0: /asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
dev: true
/atomic-sleep@1.0.0: /atomic-sleep@1.0.0:
resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
@@ -5996,24 +5999,6 @@ packages:
dev: false dev: false
optional: true optional: true
/cacheable-lookup@5.0.4:
resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==}
engines: {node: '>=10.6.0'}
dev: false
/cacheable-request@7.0.4:
resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==}
engines: {node: '>=8'}
dependencies:
clone-response: 1.0.3
get-stream: 5.2.0
http-cache-semantics: 4.1.1
keyv: 4.5.4
lowercase-keys: 2.0.0
normalize-url: 6.1.0
responselike: 2.0.1
dev: false
/call-bind@1.0.7: /call-bind@1.0.7:
resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -6131,10 +6116,6 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
dev: false dev: false
/ci-info@1.6.0:
resolution: {integrity: sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==}
dev: true
/ci-info@3.9.0: /ci-info@3.9.0:
resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -6217,12 +6198,6 @@ packages:
is-regexp: 2.1.0 is-regexp: 2.1.0
dev: true dev: true
/clone-response@1.0.3:
resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==}
dependencies:
mimic-response: 1.0.1
dev: false
/codemirror-lang-mermaid@0.5.0: /codemirror-lang-mermaid@0.5.0:
resolution: {integrity: sha512-Taw/2gPCyNArQJCxIP/HSUif+3zrvD+6Ugt7KJZ2dUKou/8r3ZhcfG8krNTZfV2iu8AuGnymKuo7bLPFyqsh/A==} resolution: {integrity: sha512-Taw/2gPCyNArQJCxIP/HSUif+3zrvD+6Ugt7KJZ2dUKou/8r3ZhcfG8krNTZfV2iu8AuGnymKuo7bLPFyqsh/A==}
dependencies: dependencies:
@@ -6301,6 +6276,7 @@ packages:
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
dependencies: dependencies:
delayed-stream: 1.0.0 delayed-stream: 1.0.0
dev: true
/commander@11.0.0: /commander@11.0.0:
resolution: {integrity: sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==} resolution: {integrity: sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==}
@@ -6330,7 +6306,6 @@ packages:
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
dependencies: dependencies:
mime-db: 1.53.0 mime-db: 1.53.0
dev: true
/compression-webpack-plugin@9.2.0: /compression-webpack-plugin@9.2.0:
resolution: {integrity: sha512-R/Oi+2+UHotGfu72fJiRoVpuRifZT0tTC6UqFD/DUo+mv8dbOow9rVOuTvDv5nPPm3GZhHL/fKkwxwIHnJ8Nyw==} resolution: {integrity: sha512-R/Oi+2+UHotGfu72fJiRoVpuRifZT0tTC6UqFD/DUo+mv8dbOow9rVOuTvDv5nPPm3GZhHL/fKkwxwIHnJ8Nyw==}
@@ -6358,7 +6333,6 @@ packages:
vary: 1.1.2 vary: 1.1.2
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
dev: true
/compute-scroll-into-view@1.0.20: /compute-scroll-into-view@1.0.20:
resolution: {integrity: sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==} resolution: {integrity: sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==}
@@ -6568,14 +6542,6 @@ packages:
engines: {node: '>=6.0'} engines: {node: '>=6.0'}
dev: false dev: false
/cross-spawn@5.1.0:
resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==}
dependencies:
lru-cache: 4.1.5
shebang-command: 1.2.0
which: 1.3.1
dev: true
/cross-spawn@7.0.6: /cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@@ -6853,13 +6819,6 @@ packages:
engines: {node: '>=0.10'} engines: {node: '>=0.10'}
dev: true dev: true
/decompress-response@6.0.0:
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
engines: {node: '>=10'}
dependencies:
mimic-response: 3.1.0
dev: false
/deep-is@0.1.4: /deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
dev: true dev: true
@@ -6899,11 +6858,6 @@ packages:
os-name: 1.0.3 os-name: 1.0.3
dev: true dev: true
/defer-to-connect@2.0.1:
resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==}
engines: {node: '>=10'}
dev: false
/define-data-property@1.1.4: /define-data-property@1.1.4:
resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -6934,6 +6888,7 @@ packages:
/delayed-stream@1.0.0: /delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
dev: true
/delegates@1.0.0: /delegates@1.0.0:
resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==}
@@ -7094,6 +7049,10 @@ packages:
detect-libc: 1.0.3 detect-libc: 1.0.3
dev: true dev: true
/duplexer@0.1.2:
resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
dev: false
/duplexify@4.1.3: /duplexify@4.1.3:
resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==} resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==}
dependencies: dependencies:
@@ -7220,6 +7179,7 @@ packages:
resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
dependencies: dependencies:
once: 1.4.0 once: 1.4.0
dev: true
/enhanced-resolve@5.17.1: /enhanced-resolve@5.17.1:
resolution: {integrity: sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==} resolution: {integrity: sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==}
@@ -7742,6 +7702,18 @@ packages:
es5-ext: 0.10.64 es5-ext: 0.10.64
dev: true dev: true
/event-stream@3.3.4:
resolution: {integrity: sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==}
dependencies:
duplexer: 0.1.2
from: 0.1.7
map-stream: 0.1.0
pause-stream: 0.0.11
split: 0.3.3
stream-combiner: 0.0.4
through: 2.3.8
dev: false
/eventemitter3@4.0.7: /eventemitter3@4.0.7:
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
dev: false dev: false
@@ -7772,19 +7744,6 @@ packages:
safe-buffer: 5.2.1 safe-buffer: 5.2.1
dev: true dev: true
/execa@0.8.0:
resolution: {integrity: sha512-zDWS+Rb1E8BlqqhALSt9kUhss8Qq4nN3iof3gsOdyINksElaPyNBtKUMTR62qhvgVWR0CqCX7sdnKe4MnUbFEA==}
engines: {node: '>=4'}
dependencies:
cross-spawn: 5.1.0
get-stream: 3.0.0
is-stream: 1.1.0
npm-run-path: 2.0.2
p-finally: 1.0.0
signal-exit: 3.0.7
strip-eof: 1.0.0
dev: true
/execa@5.1.1: /execa@5.1.1:
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -8144,6 +8103,7 @@ packages:
asynckit: 0.4.0 asynckit: 0.4.0
combined-stream: 1.0.8 combined-stream: 1.0.8
mime-types: 2.1.35 mime-types: 2.1.35
dev: true
/formdata-polyfill@4.0.10: /formdata-polyfill@4.0.10:
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
@@ -8173,6 +8133,10 @@ packages:
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
/from@0.1.7:
resolution: {integrity: sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==}
dev: false
/fs-extra@10.1.0: /fs-extra@10.1.0:
resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -8284,18 +8248,6 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
dev: true dev: true
/get-stream@3.0.0:
resolution: {integrity: sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==}
engines: {node: '>=4'}
dev: true
/get-stream@5.2.0:
resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==}
engines: {node: '>=8'}
dependencies:
pump: 3.0.2
dev: false
/get-stream@6.0.1: /get-stream@6.0.1:
resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -8439,23 +8391,6 @@ packages:
dependencies: dependencies:
get-intrinsic: 1.2.4 get-intrinsic: 1.2.4
/got@11.8.6:
resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==}
engines: {node: '>=10.19.0'}
dependencies:
'@sindresorhus/is': 4.6.0
'@szmarczak/http-timer': 4.0.6
'@types/cacheable-request': 6.0.3
'@types/responselike': 1.0.3
cacheable-lookup: 5.0.4
cacheable-request: 7.0.4
decompress-response: 6.0.0
http2-wrapper: 1.0.3
lowercase-keys: 2.0.0
p-cancelable: 2.1.1
responselike: 2.0.1
dev: false
/graceful-fs@4.2.11: /graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
@@ -8545,6 +8480,10 @@ packages:
hasBin: true hasBin: true
dev: true dev: true
/helmet@6.2.0:
resolution: {integrity: sha512-DWlwuXLLqbrIOltR6tFQXShj/+7Cyp0gLi6uAb8qMdFh/YBBFbKSgQ6nbXmScYd8emMctuthmgIa7tUfo9Rtyg==}
engines: {node: '>=14.0.0'}
/history@5.3.0: /history@5.3.0:
resolution: {integrity: sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==} resolution: {integrity: sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==}
dependencies: dependencies:
@@ -8644,7 +8583,9 @@ packages:
/http-cache-semantics@4.1.1: /http-cache-semantics@4.1.1:
resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==}
requiresBuild: true
dev: false dev: false
optional: true
/http-deceiver@1.2.7: /http-deceiver@1.2.7:
resolution: {integrity: sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==} resolution: {integrity: sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==}
@@ -8701,14 +8642,6 @@ packages:
- debug - debug
dev: false dev: false
/http2-wrapper@1.0.3:
resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==}
engines: {node: '>=10.19.0'}
dependencies:
quick-lru: 5.1.1
resolve-alpn: 1.2.1
dev: false
/https-browserify@1.0.0: /https-browserify@1.0.0:
resolution: {integrity: sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==} resolution: {integrity: sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==}
dev: true dev: true
@@ -8982,13 +8915,6 @@ packages:
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
dev: true dev: true
/is-ci@1.2.1:
resolution: {integrity: sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==}
hasBin: true
dependencies:
ci-info: 1.6.0
dev: true
/is-core-module@2.15.1: /is-core-module@2.15.1:
resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -9177,11 +9103,6 @@ packages:
call-bind: 1.0.7 call-bind: 1.0.7
dev: true dev: true
/is-stream@1.1.0:
resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==}
engines: {node: '>=0.10.0'}
dev: true
/is-stream@2.0.1: /is-stream@2.0.1:
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -9430,6 +9351,7 @@ packages:
/json-buffer@3.0.1: /json-buffer@3.0.1:
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
dev: true
/json-parse-even-better-errors@2.3.1: /json-parse-even-better-errors@2.3.1:
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
@@ -9512,6 +9434,7 @@ packages:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
dependencies: dependencies:
json-buffer: 3.0.1 json-buffer: 3.0.1
dev: true
/keyv@5.2.3: /keyv@5.2.3:
resolution: {integrity: sha512-AGKecUfzrowabUv0bH1RIR5Vf7w+l4S3xtQAypKaUpTdIR1EbrAcTxHCrpo9Q+IWeUlFE2palRtgIQcgm+PQJw==} resolution: {integrity: sha512-AGKecUfzrowabUv0bH1RIR5Vf7w+l4S3xtQAypKaUpTdIR1EbrAcTxHCrpo9Q+IWeUlFE2palRtgIQcgm+PQJw==}
@@ -9846,6 +9769,7 @@ packages:
/long@5.2.3: /long@5.2.3:
resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==}
dev: false
/loose-envify@1.4.0: /loose-envify@1.4.0:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
@@ -9860,22 +9784,10 @@ packages:
tslib: 2.8.1 tslib: 2.8.1
dev: true dev: true
/lowercase-keys@2.0.0:
resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==}
engines: {node: '>=8'}
dev: false
/lru-cache@10.4.3: /lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
dev: true dev: true
/lru-cache@4.1.5:
resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==}
dependencies:
pseudomap: 1.0.2
yallist: 2.1.2
dev: true
/lru-cache@5.1.1: /lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
dependencies: dependencies:
@@ -9957,6 +9869,10 @@ packages:
engines: {node: '>=8'} engines: {node: '>=8'}
dev: true dev: true
/map-stream@0.1.0:
resolution: {integrity: sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==}
dev: false
/mathml-tag-names@2.1.3: /mathml-tag-names@2.1.3:
resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==} resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==}
dev: true dev: true
@@ -10057,7 +9973,6 @@ packages:
/mime-db@1.53.0: /mime-db@1.53.0:
resolution: {integrity: sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==} resolution: {integrity: sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
dev: true
/mime-types@2.1.18: /mime-types@2.1.18:
resolution: {integrity: sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==} resolution: {integrity: sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==}
@@ -10093,16 +10008,6 @@ packages:
engines: {node: '>=12'} engines: {node: '>=12'}
dev: true dev: true
/mimic-response@1.0.1:
resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==}
engines: {node: '>=4'}
dev: false
/mimic-response@3.1.0:
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
engines: {node: '>=10'}
dev: false
/min-document@2.19.0: /min-document@2.19.0:
resolution: {integrity: sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==} resolution: {integrity: sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==}
dependencies: dependencies:
@@ -10507,11 +10412,6 @@ packages:
validate-npm-package-license: 3.0.4 validate-npm-package-license: 3.0.4
dev: true dev: true
/normalize-path@1.0.0:
resolution: {integrity: sha512-7WyT0w8jhpDStXRq5836AMmihQwq2nrUVQrgjvUo/p/NZf9uy/MeJ246lBJVmWuYXMlJuG9BNZHF0hWjfTbQUA==}
engines: {node: '>=0.10.0'}
dev: true
/normalize-path@3.0.0: /normalize-path@3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -10526,22 +10426,10 @@ packages:
resolution: {integrity: sha512-dxvWdI8gw6eAvk9BlPffgEoGfM7AdijoCwOEJge3e3ulT2XLgmU7KvvxprOaCu05Q1uGRHmOhHe1r6emZoKyFw==} resolution: {integrity: sha512-dxvWdI8gw6eAvk9BlPffgEoGfM7AdijoCwOEJge3e3ulT2XLgmU7KvvxprOaCu05Q1uGRHmOhHe1r6emZoKyFw==}
dev: true dev: true
/normalize-url@6.1.0:
resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==}
engines: {node: '>=10'}
dev: false
/normalize-wheel@1.0.1: /normalize-wheel@1.0.1:
resolution: {integrity: sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA==} resolution: {integrity: sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA==}
dev: true dev: true
/npm-run-path@2.0.2:
resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==}
engines: {node: '>=4'}
dependencies:
path-key: 2.0.1
dev: true
/npm-run-path@4.0.1: /npm-run-path@4.0.1:
resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -10681,7 +10569,6 @@ packages:
/on-headers@1.0.2: /on-headers@1.0.2:
resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
dev: true
/once@1.4.0: /once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
@@ -10760,16 +10647,6 @@ packages:
minimist: 1.2.8 minimist: 1.2.8
dev: true dev: true
/p-cancelable@2.1.1:
resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==}
engines: {node: '>=8'}
dev: false
/p-finally@1.0.0:
resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==}
engines: {node: '>=4'}
dev: true
/p-limit@2.3.0: /p-limit@2.3.0:
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -10903,11 +10780,6 @@ packages:
resolution: {integrity: sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==} resolution: {integrity: sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==}
dev: false dev: false
/path-key@2.0.1:
resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==}
engines: {node: '>=4'}
dev: true
/path-key@3.1.1: /path-key@3.1.1:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -10964,7 +10836,6 @@ packages:
resolution: {integrity: sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==} resolution: {integrity: sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==}
dependencies: dependencies:
through: 2.3.8 through: 2.3.8
dev: true
/pbkdf2@3.1.2: /pbkdf2@3.1.2:
resolution: {integrity: sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==} resolution: {integrity: sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==}
@@ -11749,6 +11620,7 @@ packages:
'@protobufjs/utf8': 1.1.0 '@protobufjs/utf8': 1.1.0
'@types/node': 17.0.45 '@types/node': 17.0.45
long: 5.2.3 long: 5.2.3
dev: false
/proxy-addr@2.0.7: /proxy-addr@2.0.7:
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
@@ -11771,12 +11643,17 @@ packages:
dev: true dev: true
optional: true optional: true
/pseudomap@1.0.2: /ps-tree@1.2.0:
resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} resolution: {integrity: sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==}
dev: true engines: {node: '>= 0.10'}
hasBin: true
dependencies:
event-stream: 3.3.4
dev: false
/pstree.remy@1.1.8: /pstree.remy@1.1.8:
resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==}
dev: true
/public-encrypt@4.0.3: /public-encrypt@4.0.3:
resolution: {integrity: sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==} resolution: {integrity: sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==}
@@ -11794,6 +11671,7 @@ packages:
dependencies: dependencies:
end-of-stream: 1.4.4 end-of-stream: 1.4.4
once: 1.4.0 once: 1.4.0
dev: true
/punycode-okam@1.4.1: /punycode-okam@1.4.1:
resolution: {integrity: sha512-e4mSfzGfrVBJmhjp+8PHjXIz5WrvEEWB2FT+RJ6YS/ozGttTcnocuj0CtMo3dujWYe2708bTd79zeIrKBtRzCg==} resolution: {integrity: sha512-e4mSfzGfrVBJmhjp+8PHjXIz5WrvEEWB2FT+RJ6YS/ozGttTcnocuj0CtMo3dujWYe2708bTd79zeIrKBtRzCg==}
@@ -11911,11 +11789,6 @@ packages:
engines: {node: '>=8'} engines: {node: '>=8'}
dev: true dev: true
/quick-lru@5.1.1:
resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==}
engines: {node: '>=10'}
dev: false
/raf@3.4.1: /raf@3.4.1:
resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==} resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==}
dependencies: dependencies:
@@ -13238,10 +13111,6 @@ packages:
resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==}
dev: true dev: true
/resolve-alpn@1.2.1:
resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==}
dev: false
/resolve-from@4.0.0: /resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'} engines: {node: '>=4'}
@@ -13274,12 +13143,6 @@ packages:
supports-preserve-symlinks-flag: 1.0.0 supports-preserve-symlinks-flag: 1.0.0
dev: true dev: true
/responselike@2.0.1:
resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==}
dependencies:
lowercase-keys: 2.0.0
dev: false
/restore-cursor@4.0.0: /restore-cursor@4.0.0:
resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@@ -13620,24 +13483,12 @@ packages:
resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==}
dev: true dev: true
/shebang-command@1.2.0:
resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==}
engines: {node: '>=0.10.0'}
dependencies:
shebang-regex: 1.0.0
dev: true
/shebang-command@2.0.0: /shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'} engines: {node: '>=8'}
dependencies: dependencies:
shebang-regex: 3.0.0 shebang-regex: 3.0.0
/shebang-regex@1.0.0:
resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==}
engines: {node: '>=0.10.0'}
dev: true
/shebang-regex@3.0.0: /shebang-regex@3.0.0:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -13879,6 +13730,12 @@ packages:
engines: {node: '>= 10.x'} engines: {node: '>= 10.x'}
dev: true dev: true
/split@0.3.3:
resolution: {integrity: sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==}
dependencies:
through: 2.3.8
dev: false
/sprintf-js@1.0.3: /sprintf-js@1.0.3:
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
dev: true dev: true
@@ -13938,6 +13795,12 @@ packages:
readable-stream: 2.3.8 readable-stream: 2.3.8
dev: true dev: true
/stream-combiner@0.0.4:
resolution: {integrity: sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==}
dependencies:
duplexer: 0.1.2
dev: false
/stream-http@2.8.3: /stream-http@2.8.3:
resolution: {integrity: sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==} resolution: {integrity: sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==}
dependencies: dependencies:
@@ -14062,11 +13925,6 @@ packages:
ansi-regex: 6.1.0 ansi-regex: 6.1.0
dev: true dev: true
/strip-eof@1.0.0:
resolution: {integrity: sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==}
engines: {node: '>=0.10.0'}
dev: true
/strip-final-newline@2.0.0: /strip-final-newline@2.0.0:
resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -14077,11 +13935,6 @@ packages:
engines: {node: '>=12'} engines: {node: '>=12'}
dev: true dev: true
/strip-indent@2.0.0:
resolution: {integrity: sha512-RsSNPLpq6YUL7QYy44RnPVTn/lcVZtb48Uof3X5JLbF4zD/Gs7ZFDv2HWol+leoQN2mT86LAzSshGfkTlSOpsA==}
engines: {node: '>=4'}
dev: true
/strip-indent@3.0.0: /strip-indent@3.0.0:
resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -14405,7 +14258,6 @@ packages:
/through@2.3.8: /through@2.3.8:
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
dev: true
/timers-browserify@2.0.12: /timers-browserify@2.0.12:
resolution: {integrity: sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==} resolution: {integrity: sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==}
@@ -14528,21 +14380,20 @@ packages:
dprint-node: 1.0.8 dprint-node: 1.0.8
dev: true dev: true
/ts-proto-descriptors@1.16.0: /ts-proto-descriptors@2.0.0:
resolution: {integrity: sha512-3yKuzMLpltdpcyQji1PJZRfoo4OJjNieKTYkQY8pF7xGKsYz/RHe3aEe4KiRxcinoBmnEhmuI+yJTxLb922ULA==} resolution: {integrity: sha512-wHcTH3xIv11jxgkX5OyCSFfw27agpInAd6yh89hKG6zqIXnjW9SYqSER2CVQxdPj4czeOhGagNvZBEbJPy7qkw==}
dependencies: dependencies:
long: 5.2.3 '@bufbuild/protobuf': 2.2.3
protobufjs: 7.4.0
dev: true dev: true
/ts-proto@1.181.2: /ts-proto@2.6.1:
resolution: {integrity: sha512-knJ8dtjn2Pd0c5ZGZG8z9DMiD4PUY8iGI9T9tb8DvGdWRMkLpf0WcPO7G+7cmbZyxvNTAG6ci3fybEaFgMZIvg==} resolution: {integrity: sha512-4LTT99MkwkF1+fIA0b2mZu/58Qlpq3Q1g53TwEMZZgR1w/uX00PoVT4Z8aKJxMw0LeKQD4s9NrJYsF27Clckrg==}
hasBin: true hasBin: true
dependencies: dependencies:
'@bufbuild/protobuf': 2.2.3
case-anything: 2.1.13 case-anything: 2.1.13
protobufjs: 7.4.0
ts-poet: 6.9.0 ts-poet: 6.9.0
ts-proto-descriptors: 1.16.0 ts-proto-descriptors: 2.0.0
dev: true dev: true
/tslib@1.14.1: /tslib@1.14.1:
@@ -14771,6 +14622,11 @@ packages:
resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==}
dev: true dev: true
/undici@7.9.0:
resolution: {integrity: sha512-e696y354tf5cFZPXsF26Yg+5M63+5H3oE6Vtkh2oqbvsE2Oe7s2nIbcQh5lmG7Lp/eS29vJtTpw9+p6PX0qNSg==}
engines: {node: '>=20.18.1'}
dev: false
/unescape@1.0.1: /unescape@1.0.1:
resolution: {integrity: sha512-O0+af1Gs50lyH1nUu3ZyYS1cRh01Q/kUKatTOkSs7jukXE6/NebucDVxyiDsA9AQ4JC1V1jUH9EO8JX2nMDgGQ==} resolution: {integrity: sha512-O0+af1Gs50lyH1nUu3ZyYS1cRh01Q/kUKatTOkSs7jukXE6/NebucDVxyiDsA9AQ4JC1V1jUH9EO8JX2nMDgGQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -15327,10 +15183,6 @@ packages:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'} engines: {node: '>=10'}
/yallist@2.1.2:
resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==}
dev: true
/yallist@3.1.1: /yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
dev: true dev: true
@@ -15379,17 +15231,6 @@ packages:
engines: {node: '>=10'} engines: {node: '>=10'}
dev: true dev: true
/yorkie@2.0.0:
resolution: {integrity: sha512-jcKpkthap6x63MB4TxwCyuIGkV0oYP/YRyuQU5UO0Yz/E/ZAu+653/uov+phdmO54n6BcvFRyyt0RRrWdN2mpw==}
engines: {node: '>=4'}
requiresBuild: true
dependencies:
execa: 0.8.0
is-ci: 1.2.1
normalize-path: 1.0.0
strip-indent: 2.0.0
dev: true
/zod-validation-error@2.1.0(zod@3.23.8): /zod-validation-error@2.1.0(zod@3.23.8):
resolution: {integrity: sha512-VJh93e2wb4c3tWtGgTa0OF/dTt/zoPCPzXq4V11ZjxmEAFaPi/Zss1xIZdEB5RD8GD00U0/iVXgqkF77RV7pdQ==} resolution: {integrity: sha512-VJh93e2wb4c3tWtGgTa0OF/dTt/zoPCPzXq4V11ZjxmEAFaPi/Zss1xIZdEB5RD8GD00U0/iVXgqkF77RV7pdQ==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
+59 -31
View File
@@ -1,7 +1,42 @@
const querystring = require('node:querystring'); const querystring = require('node:querystring');
const got = require('got'); const { request: undiciRequest, ProxyAgent, FormData } = require('undici');
const timeout = 15000; const timeout = 15000;
async function request(url, options = {}) {
const { json, form, body, headers = {}, ...rest } = options;
const finalHeaders = { ...headers };
let finalBody = body;
if (json) {
finalHeaders['content-type'] = 'application/json';
finalBody = JSON.stringify(json);
} else if (form) {
finalBody = form;
delete finalHeaders['content-type'];
}
return undiciRequest(url, {
headers: finalHeaders,
body: finalBody,
...rest,
});
}
function post(url, options = {}) {
return request(url, { ...options, method: 'POST' });
}
function get(url, options = {}) {
return request(url, { ...options, method: 'GET' });
}
const httpClient = {
request,
post,
get,
};
const push_config = { const push_config = {
HITOKOTO: true, // 启用一言(随机句子) HITOKOTO: true, // 启用一言(随机句子)
@@ -84,7 +119,8 @@ const push_config = {
AIBOTK_NAME: '', // 智能微秘书 发送群名 或者好友昵称和type要对应好 AIBOTK_NAME: '', // 智能微秘书 发送群名 或者好友昵称和type要对应好
SMTP_SERVICE: '', // 邮箱服务名称,比如 126、163、Gmail、QQ 等,支持列表 https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json SMTP_SERVICE: '', // 邮箱服务名称,比如 126、163、Gmail、QQ 等,支持列表 https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json
SMTP_EMAIL: '', // SMTP 发件邮箱,通知将会由自己发给自己 SMTP_EMAIL: '', // SMTP 发件邮箱
SMTP_TO: '', // SMTP 收件邮箱,默认通知将会发给发件邮箱
SMTP_PASSWORD: '', // SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定 SMTP_PASSWORD: '', // SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
SMTP_NAME: '', // SMTP 收发件人姓名,可随意填写 SMTP_NAME: '', // SMTP 收发件人姓名,可随意填写
@@ -122,9 +158,9 @@ for (const key in push_config) {
const $ = { const $ = {
post: (params, callback) => { post: (params, callback) => {
const { url, ...others } = params; const { url, ...others } = params;
got.post(url, others).then( httpClient.post(url, others).then(
(res) => { async (res) => {
let body = res.body; let body = await res.body.text();
try { try {
body = JSON.parse(body); body = JSON.parse(body);
} catch (error) {} } catch (error) {}
@@ -137,9 +173,9 @@ const $ = {
}, },
get: (params, callback) => { get: (params, callback) => {
const { url, ...others } = params; const { url, ...others } = params;
got.get(url, others).then( httpClient.get(url, others).then(
(res) => { async (res) => {
let body = res.body; let body = await res.body.text();
try { try {
body = JSON.parse(body); body = JSON.parse(body);
} catch (error) {} } catch (error) {}
@@ -155,8 +191,8 @@ const $ = {
async function one() { async function one() {
const url = 'https://v1.hitokoto.cn/'; const url = 'https://v1.hitokoto.cn/';
const res = await got.get(url); const res = await httpClient.request(url);
const body = JSON.parse(res.body); const body = await res.body.json();
return `${body.hitokoto} ----${body.from}`; return `${body.hitokoto} ----${body.from}`;
} }
@@ -441,21 +477,11 @@ function tgBotNotify(text, desp) {
timeout, timeout,
}; };
if (TG_PROXY_HOST && TG_PROXY_PORT) { if (TG_PROXY_HOST && TG_PROXY_PORT) {
const { HttpProxyAgent, HttpsProxyAgent } = require('hpagent'); let agent;
const _options = { agent = new ProxyAgent({
keepAlive: true, uri: `http://${TG_PROXY_AUTH}${TG_PROXY_HOST}:${TG_PROXY_PORT}`,
keepAliveMsecs: 1000, });
maxSockets: 256, options.dispatcher = agent;
maxFreeSockets: 256,
proxy: `http://${TG_PROXY_AUTH}${TG_PROXY_HOST}:${TG_PROXY_PORT}`,
};
const httpAgent = new HttpProxyAgent(_options);
const httpsAgent = new HttpsProxyAgent(_options);
const agent = {
http: httpAgent,
https: httpsAgent,
};
options.agent = agent;
} }
$.post(options, (err, resp, data) => { $.post(options, (err, resp, data) => {
try { try {
@@ -993,7 +1019,8 @@ function fsBotNotify(text, desp) {
} }
async function smtpNotify(text, desp) { async function smtpNotify(text, desp) {
const { SMTP_EMAIL, SMTP_PASSWORD, SMTP_SERVICE, SMTP_NAME } = push_config; const { SMTP_EMAIL, SMTP_TO, SMTP_PASSWORD, SMTP_SERVICE, SMTP_NAME } =
push_config;
if (![SMTP_EMAIL, SMTP_PASSWORD].every(Boolean) || !SMTP_SERVICE) { if (![SMTP_EMAIL, SMTP_PASSWORD].every(Boolean) || !SMTP_SERVICE) {
return; return;
} }
@@ -1011,7 +1038,7 @@ async function smtpNotify(text, desp) {
const addr = SMTP_NAME ? `"${SMTP_NAME}" <${SMTP_EMAIL}>` : SMTP_EMAIL; const addr = SMTP_NAME ? `"${SMTP_NAME}" <${SMTP_EMAIL}>` : SMTP_EMAIL;
const info = await transporter.sendMail({ const info = await transporter.sendMail({
from: addr, from: addr,
to: addr, to: SMTP_TO ? SMTP_TO.split(';') : addr,
subject: text, subject: text,
html: `${desp.replace(/\n/g, '<br/>')}`, html: `${desp.replace(/\n/g, '<br/>')}`,
}); });
@@ -1207,17 +1234,18 @@ function webhookNotify(text, desp) {
'$title', '$title',
encodeURIComponent(text), encodeURIComponent(text),
).replaceAll('$content', encodeURIComponent(desp)); ).replaceAll('$content', encodeURIComponent(desp));
got(formatUrl, options).then((resp) => { httpClient.request(formatUrl, options).then(async (resp) => {
const body = await resp.body.text();
try { try {
if (resp.statusCode !== 200) { if (resp.statusCode !== 200) {
console.log(`自定义发送通知消息失败😞 ${resp.body}\n`); console.log(`自定义发送通知消息失败😞 ${body}\n`);
} else { } else {
console.log(`自定义发送通知消息成功🎉 ${resp.body}\n`); console.log(`自定义发送通知消息成功🎉 ${body}\n`);
} }
} catch (e) { } catch (e) {
$.logErr(e, resp); $.logErr(e, resp);
} finally { } finally {
resolve(resp.body); resolve(body);
} }
}); });
}); });
-2
View File
@@ -74,14 +74,12 @@ 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 ts-node
patch_version
reset_env reset_env
copy_dep copy_dep
check_ql check_ql
check_nginx check_nginx
check_pm2 check_pm2
reload_update
reload_pm2 reload_pm2
echo -e "\n=====> 检测结束\n" echo -e "\n=====> 检测结束\n"
} }
+134 -39
View File
@@ -1,45 +1,140 @@
const grpc = require('@grpc/grpc-js'); const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader'); const protoLoader = require('@grpc/proto-loader');
const { join } = require('path');
const PROTO_PATH = `${process.env.QL_DIR}/back/protos/api.proto`; class GrpcClient {
const options = { static #config = {
keepCase: true, protoPath: join(process.env.QL_DIR, 'back/protos/api.proto'),
longs: String, serverAddress: '0.0.0.0:5500',
enums: String, protoOptions: {
defaults: true, keepCase: true,
}; longs: String,
enums: String,
const packageDefinition = protoLoader.loadSync(PROTO_PATH, options); defaults: true,
const apiProto = grpc.loadPackageDefinition(packageDefinition).com.ql.api; },
grpcOptions: {
const client = new apiProto.Api( 'grpc.enable_http_proxy': 0,
`0.0.0.0:5500`, },
grpc.credentials.createInsecure(), defaultTimeout: 30000,
{ 'grpc.enable_http_proxy': 0 },
);
const promisify = (fn) => {
return (...args) => {
return new Promise((resolve, reject) => {
fn.call(client, ...args, (err, response) => {
if (err) return reject(err);
resolve(response);
});
});
}; };
};
const api = { static #methods = [
getEnvs: promisify(client.GetEnvs), 'getEnvs',
createEnv: promisify(client.CreateEnv), 'createEnv',
updateEnv: promisify(client.UpdateEnv), 'updateEnv',
deleteEnvs: promisify(client.DeleteEnvs), 'deleteEnvs',
moveEnv: promisify(client.MoveEnv), 'moveEnv',
disableEnvs: promisify(client.DisableEnvs), 'disableEnvs',
enableEnvs: promisify(client.EnableEnvs), 'enableEnvs',
updateEnvNames: promisify(client.UpdateEnvNames), 'updateEnvNames',
getEnvById: promisify(client.GetEnvById), 'getEnvById',
systemNotify: promisify(client.SystemNotify), 'systemNotify',
}; 'getCronDetail',
'createCron',
'updateCron',
'deleteCrons',
];
module.exports = api; #client;
#api = {};
constructor() {
this.#initializeClient();
this.#bindMethods();
}
#initializeClient() {
try {
const { protoPath, protoOptions, serverAddress, grpcOptions } =
GrpcClient.#config;
const packageDefinition = protoLoader.loadSync(protoPath, protoOptions);
const apiProto = grpc.loadPackageDefinition(packageDefinition).com.ql.api;
this.#client = new apiProto.Api(
serverAddress,
grpc.credentials.createInsecure(),
grpcOptions,
);
} catch (error) {
console.error('Failed to initialize gRPC client:', error);
process.exit(1);
}
}
#promisifyMethod(methodName) {
const capitalizedMethod =
methodName.charAt(0).toUpperCase() + methodName.slice(1);
const method = this.#client[capitalizedMethod].bind(this.#client);
return async (params = {}) => {
return new Promise((resolve, reject) => {
const metadata = new grpc.Metadata();
const deadline = new Date(
Date.now() + GrpcClient.#config.defaultTimeout,
);
method(params, metadata, { deadline }, (error, response) => {
if (error) {
return reject(error);
}
resolve(response);
});
});
};
}
#bindMethods() {
GrpcClient.#methods.forEach((method) => {
this.#api[method] = this.#promisifyMethod(method);
});
}
getApi() {
return {
...this.#api,
close: this.close.bind(this),
};
}
close() {
if (this.#client) {
this.#client.close();
this.#client = null;
}
}
}
const grpcClient = new GrpcClient();
process.on('SIGTERM', () => {
grpcClient.close();
process.exit(0);
});
process.on('SIGINT', () => {
grpcClient.close();
process.exit(0);
});
process.on('unhandledRejection', (reason, promise) => {
if (reason instanceof Error) {
if (reason.stack) {
const relevantStack = reason.stack
.split('\n')
.filter((line) => {
return (
!line.includes('node:internal') &&
!line.includes('node_modules/@grpc') &&
!line.includes('processTicksAndRejections')
);
})
.join('\n');
console.error(relevantStack);
}
} else {
console.error(reason);
}
});
module.exports = grpcClient.getApi();
+200 -30
View File
@@ -2,7 +2,7 @@ import subprocess
import json import json
import tempfile import tempfile
import os import os
from typing import Dict, List from typing import Dict, List, TypedDict, Optional
from functools import wraps from functools import wraps
@@ -11,16 +11,170 @@ def error_handler(func):
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
try: try:
return func(*args, **kwargs) return func(*args, **kwargs)
except json.JSONDecodeError as e: except TypeError as e:
raise Exception(f"parse json error: {str(e)}") if "missing" in str(e):
except subprocess.SubprocessError as e: func_name = func.__name__
raise Exception(f"node process error: {str(e)}") 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: except Exception as e:
raise Exception(f"unknown error: {str(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 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: class Client:
def __init__(self): def __init__(self):
self.temp_dir = tempfile.mkdtemp(prefix="node_client_") self.temp_dir = tempfile.mkdtemp(prefix="node_client_")
@@ -46,7 +200,8 @@ class Client:
}} catch (error) {{ }} catch (error) {{
console.error(JSON.stringify({{ console.error(JSON.stringify({{
error: error.message, error: error.message,
stack: error.stack stack: error.stack,
name: error.name
}})); }}));
process.exit(1); process.exit(1);
}} }}
@@ -56,58 +211,73 @@ class Client:
with open(self.temp_script, "w", encoding="utf-8") as f: with open(self.temp_script, "w", encoding="utf-8") as f:
f.write(node_code) f.write(node_code)
try: result = subprocess.run(
result = subprocess.run( ["node", self.temp_script],
["node", self.temp_script], capture_output=True,
capture_output=True, text=True,
text=True, timeout=30,
timeout=30, )
if result.returncode != 0:
error_data = json.loads(result.stderr)
raise Exception(
f"{error_data.get('name', 'Error')}: {error_data.get('stack')}"
) )
if result.returncode != 0: return json.loads(result.stdout)
error_data = json.loads(result.stderr)
raise Exception(f"{error_data.get('stack')}")
return json.loads(result.stdout)
except subprocess.TimeoutExpired:
raise Exception("node process timeout")
@error_handler @error_handler
def getEnvs(self, params: Dict = None) -> Dict: def getEnvs(self, params: GetEnvsParams = None) -> EnvsResponse:
return self._execute_node("getEnvs", params) return self._execute_node("getEnvs", params)
@error_handler @error_handler
def createEnv(self, data: Dict) -> Dict: def createEnv(self, data: CreateEnvParams) -> EnvsResponse:
return self._execute_node("createEnv", data) return self._execute_node("createEnv", data)
@error_handler @error_handler
def updateEnv(self, data: Dict) -> Dict: def updateEnv(self, data: UpdateEnvParams) -> EnvResponse:
return self._execute_node("updateEnv", data) return self._execute_node("updateEnv", data)
@error_handler @error_handler
def deleteEnvs(self, data: Dict) -> Dict: def deleteEnvs(self, data: DeleteEnvsParams) -> Response:
return self._execute_node("deleteEnvs", data) return self._execute_node("deleteEnvs", data)
@error_handler @error_handler
def moveEnv(self, data: Dict) -> Dict: def moveEnv(self, data: MoveEnvParams) -> EnvResponse:
return self._execute_node("moveEnv", data) return self._execute_node("moveEnv", data)
@error_handler @error_handler
def disableEnvs(self, data: Dict) -> Dict: def disableEnvs(self, data: DisableEnvsParams) -> Response:
return self._execute_node("disableEnvs", data) return self._execute_node("disableEnvs", data)
@error_handler @error_handler
def enableEnvs(self, data: Dict) -> Dict: def enableEnvs(self, data: EnableEnvsParams) -> Response:
return self._execute_node("enableEnvs", data) return self._execute_node("enableEnvs", data)
@error_handler @error_handler
def updateEnvNames(self, data: Dict) -> Dict: def updateEnvNames(self, data: UpdateEnvNamesParams) -> Response:
return self._execute_node("updateEnvNames", data) return self._execute_node("updateEnvNames", data)
@error_handler @error_handler
def getEnvById(self, data: Dict) -> Dict: def getEnvById(self, data: GetEnvByIdParams) -> EnvResponse:
return self._execute_node("getEnvById", data) return self._execute_node("getEnvById", data)
@error_handler @error_handler
def systemNotify(self, data: Dict) -> Dict: def systemNotify(self, data: SystemNotifyParams) -> Response:
return self._execute_node("systemNotify", data) 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)
+39 -16
View File
@@ -38,25 +38,48 @@ function run() {
const splitStr = '__sitecustomize__'; const splitStr = '__sitecustomize__';
const fileName = process.argv[1].replace(`${dir_scripts}/`, ''); const fileName = process.argv[1].replace(`${dir_scripts}/`, '');
let command = `bash -c "source ${file_task_before} ${fileName}`; const tempFile = `/tmp/env_${process.pid}.json`;
const commands = [
`source ${file_task_before} ${fileName}`,
task_before ? `eval '${task_before.replace(/'/g, "'\\''")}'` : null,
`echo -e '${splitStr}'`,
`node -e "require('fs').writeFileSync('${tempFile}', JSON.stringify(process.env))"`,
].filter(Boolean);
if (task_before) { if (task_before) {
const escapeTaskBefore = task_before
.replace(/"/g, '\\"')
.replace(/\$/g, '\\$');
command = `${command} && eval '${escapeTaskBefore}'`;
console.log('执行前置命令\n'); console.log('执行前置命令\n');
} }
const res = execSync(
`${command} && echo -e '${splitStr}' && node -p 'JSON.stringify(process.env)'"`, const res = execSync(commands.join(' && '), {
{ encoding: 'utf-8',
encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024,
}, shell: '/bin/bash',
); });
const [output, envStr] = res.split(splitStr);
const newEnvObject = JSON.parse(envStr.trim()); const [output] = res.split(splitStr);
for (const key in newEnvObject) {
process.env[key] = newEnvObject[key]; try {
const envStr = require('fs').readFileSync(tempFile, 'utf-8');
const newEnvObject = JSON.parse(envStr);
if (typeof newEnvObject === 'object' && newEnvObject !== null) {
for (const key in newEnvObject) {
if (Object.prototype.hasOwnProperty.call(newEnvObject, key)) {
process.env[key] = newEnvObject[key];
}
}
}
require('fs').unlinkSync(tempFile);
} catch (jsonError) {
console.log(
'\ue926 Failed to parse environment variables:',
jsonError.message,
);
try {
require('fs').unlinkSync(tempFile);
} catch (e) {}
} }
if (output) { if (output) {
console.log(output); console.log(output);
} }
@@ -98,7 +121,7 @@ try {
run(); run();
const { sendNotify } = require('./notify.js'); const { sendNotify } = require('./__ql_notify__.js');
global.QLAPI = { global.QLAPI = {
notify: sendNotify, notify: sendNotify,
...client, ...client,
+43 -19
View File
@@ -43,35 +43,57 @@ def run():
split_str = "__sitecustomize__" split_str = "__sitecustomize__"
file_name = sys.argv[0].replace(f"{os.getenv('dir_scripts')}/", "") file_name = sys.argv[0].replace(f"{os.getenv('dir_scripts')}/", "")
command = f'bash -c "source {os.getenv("file_task_before")} {file_name}'
# 创建临时文件路径
temp_file = f"/tmp/env_{os.getpid()}.json"
# 构建命令数组
commands = [
f'source {os.getenv("file_task_before")} {file_name}'
]
task_before = os.getenv("task_before") task_before = os.getenv("task_before")
if task_before: if task_before:
escape_task_before = task_before.replace('"', '\\"').replace("$", "\\$") escaped_task_before = task_before.replace('"', '\\"').replace("$", "\\$")
command += f" && eval '{escape_task_before}'" commands.append(f"eval '{escaped_task_before}'")
print("执行前置命令\n") print("执行前置命令\n")
prev_pythonpath = os.getenv("PREV_PYTHONPATH", "") commands.append(f"echo -e '{split_str}'")
python_command = (
"python3 -c 'import os, json; print(json.dumps(dict(os.environ)))'" # 修改 Python 命令,使用单行并正确处理引号
) python_cmd = f"python3 -c 'import os,json; f=open(\\\"{temp_file}\\\",\\\"w\\\"); json.dump(dict(os.environ),f); f.close()'"
command += f" && echo -e '{split_str}' && {python_command}\"" commands.append(python_cmd)
command = " && ".join(cmd for cmd in commands if cmd)
command = f'bash -c "{command}"'
res = subprocess.check_output(command, shell=True, encoding="utf-8") res = subprocess.check_output(command, shell=True, encoding="utf-8")
output, env_str = res.split(split_str) output = res.split(split_str)[0]
env_json = json.loads(env_str.strip()) try:
with open(temp_file, 'r') as f:
env_json = json.loads(f.read())
for key, value in env_json.items(): for key, value in env_json.items():
os.environ[key] = value os.environ[key] = value
os.unlink(temp_file)
except Exception as json_error:
print(f"\ue926 Failed to parse environment variables: {json_error}")
try:
os.unlink(temp_file)
except:
pass
if len(output) > 0: if len(output) > 0:
print(output) print(output)
if task_before: if task_before:
print("执行前置命令结束") print("执行前置命令结束\n")
except subprocess.CalledProcessError as error: except subprocess.CalledProcessError as error:
print(f"run task before error: {error}") print(f"\ue926 run task before error: {error}")
if task_before:
print("执行前置命令结束\n")
except OSError as error: except OSError as error:
error_message = str(error) error_message = str(error)
if "Argument list too long" not in error_message: if "Argument list too long" not in error_message:
@@ -81,9 +103,11 @@ def run():
"\ue926 The environment variable is too large. It is recommended to use task_before.py instead of task_before.sh\n" "\ue926 The environment variable is too large. It is recommended to use task_before.py instead of task_before.sh\n"
) )
if task_before: if task_before:
print("执行前置命令结束") print("执行前置命令结束\n")
except Exception as error: except Exception as error:
print(f"run task before error: {error}") print(f"\ue926 run task before error: {error}")
if task_before:
print("执行前置命令结束\n")
import task_before import task_before
@@ -107,7 +131,7 @@ try:
run() run()
from notify import send from __ql_notify__ import send
class BaseApi(Client): class BaseApi(Client):
def notify(self, *args, **kwargs): def notify(self, *args, **kwargs):
+25 -77
View File
@@ -72,7 +72,17 @@ original_name=(
) )
init_env() { init_env() {
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 local pnpm_global_path=$(pnpm root -g 2>/dev/null)
export NODE_PATH="/usr/local/bin:/usr/local/lib/node_modules${pnpm_global_path:+:${pnpm_global_path}}"
# 如果存在 pnpm 全局路径,创建软链接
if [[ -n "$pnpm_global_path" ]]; then
# 确保目标目录存在
mkdir -p "${dir_root}/node_modules"
# 链接全局模块到项目的 node_modules
ln -sf "${pnpm_global_path}/"* "${dir_root}/node_modules/" 2>/dev/null || true
fi
export PYTHONUNBUFFERED=1 export PYTHONUNBUFFERED=1
} }
@@ -170,67 +180,47 @@ fix_config() {
make_dir $dir_dep make_dir $dir_dep
if [[ ! -s $file_config_user ]]; then if [[ ! -s $file_config_user ]]; then
echo -e "复制一份 $file_config_sample$file_config_user,随后请按注释编辑你的配置文件:$file_config_user\n" cp -f $file_config_sample $file_config_user
cp -fv $file_config_sample $file_config_user
echo
fi fi
if [[ ! -f $file_task_before ]]; then if [[ ! -f $file_task_before ]]; then
echo -e "复制一份 $file_task_sample$file_task_before\n" cp -f $file_task_sample $file_task_before
cp -fv $file_task_sample $file_task_before
echo
fi fi
if [[ ! -f $file_task_after ]]; then if [[ ! -f $file_task_after ]]; then
echo -e "复制一份 $file_task_sample$file_task_after\n" cp -f $file_task_sample $file_task_after
cp -fv $file_task_sample $file_task_after
echo
fi fi
if [[ ! -f $file_extra_shell ]]; then if [[ ! -f $file_extra_shell ]]; then
echo -e "复制一份 $file_extra_sample$file_extra_shell\n" cp -f $file_extra_sample $file_extra_shell
cp -fv $file_extra_sample $file_extra_shell
echo
fi fi
if [[ ! -s $file_notify_py ]]; then if [[ ! -s $file_notify_py ]]; then
echo -e "复制一份 $file_notify_py_sample$file_notify_py\n" cp -f $file_notify_py_sample $file_notify_py
cp -fv $file_notify_py_sample $file_notify_py
echo
fi fi
if [[ ! -s $file_notify_js ]]; then if [[ ! -s $file_notify_js ]]; then
echo -e "复制一份 $file_notify_js_sample$file_notify_js\n" cp -f $file_notify_js_sample $file_notify_js
cp -fv $file_notify_js_sample $file_notify_js
echo
fi fi
if [[ ! -s $file_test_js ]]; then if [[ ! -s $file_test_js ]]; then
cp -fv $file_test_js_sample $file_test_js cp -f $file_test_js_sample $file_test_js
echo
fi fi
if [[ ! -s $file_test_py ]]; then if [[ ! -s $file_test_py ]]; then
cp -fv $file_test_py_sample $file_test_py cp -f $file_test_py_sample $file_test_py
echo
fi fi
if [[ -s /etc/nginx/conf.d/default.conf ]]; then if [[ -s /etc/nginx/conf.d/default.conf ]]; then
echo -e "检测到默认nginx配置文件,清空...\n"
cat /dev/null >/etc/nginx/conf.d/default.conf cat /dev/null >/etc/nginx/conf.d/default.conf
echo
fi fi
if [[ ! -s $dep_notify_js ]]; then if [[ ! -s $dep_notify_js ]]; then
echo -e "复制一份 $file_notify_js_sample$dep_notify_js\n" cp -f $file_notify_js_sample $dep_notify_js
cp -fv $file_notify_js_sample $dep_notify_js
echo
fi fi
if [[ ! -s $dep_notify_py ]]; then if [[ ! -s $dep_notify_py ]]; then
echo -e "复制一份 $file_notify_py_sample$dep_notify_py\n" cp -f $file_notify_py_sample $dep_notify_py
cp -fv $file_notify_py_sample $dep_notify_py
echo
fi fi
} }
@@ -295,14 +285,7 @@ reload_pm2() {
cd $dir_root cd $dir_root
restore_env_vars restore_env_vars
pm2 flush &>/dev/null pm2 flush &>/dev/null
pm2 startOrGracefulReload ecosystem.config.js pm2 startOrGracefulReload ecosystem.config.js --update-env
}
reload_update() {
cd $dir_root
restore_env_vars
pm2 flush &>/dev/null
pm2 startOrGracefulReload other.config.js
} }
diff_time() { diff_time() {
@@ -351,44 +334,9 @@ format_timestamp() {
fi fi
} }
patch_version() {
git config --global pull.rebase false
if [[ -f "$dir_root/db/cookie.db" ]]; then
echo -e "检测到旧的db文件,拷贝为新db...\n"
mv $dir_root/db/cookie.db $dir_root/db/env.db
rm -rf $dir_root/db/cookie.db
echo
fi
if [[ -d "$dir_root/db" ]]; then
echo -e "检测到旧的db目录,拷贝到data目录...\n"
cp -rf $dir_root/config $dir_data
echo
fi
if [[ -d "$dir_root/scripts" ]]; then
echo -e "检测到旧的scripts目录,拷贝到data目录...\n"
cp -rf $dir_root/scripts $dir_data
echo
fi
if [[ -d "$dir_root/log" ]]; then
echo -e "检测到旧的log目录,拷贝到data目录...\n"
cp -rf $dir_root/log $dir_data
echo
fi
if [[ -d "$dir_root/config" ]]; then
echo -e "检测到旧的config目录,拷贝到data目录...\n"
cp -rf $dir_root/config $dir_data
echo
fi
}
init_nginx() { init_nginx() {
cp -fv $nginx_conf /etc/nginx/nginx.conf cp -f $nginx_conf /etc/nginx/nginx.conf
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf cp -f $nginx_app_conf /etc/nginx/conf.d/front.conf
local location_url="/" local location_url="/"
local aliasStr="" local aliasStr=""
local rootStr="" local rootStr=""
@@ -479,7 +427,7 @@ handle_task_end() {
[[ "$diff_time" == 0 ]] && diff_time=1 [[ "$diff_time" == 0 ]] && diff_time=1
if [[ $ID ]]; then if [[ $ID ]]; then
local error=$(update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time") local error=$(update_cron "\"$ID\"" "1" "$$" "$log_path" "$begin_timestamp" "$diff_time")
if [[ $error ]]; then if [[ $error ]]; then
error_message=", 任务状态更新失败(${error})" error_message=", 任务状态更新失败(${error})"
fi fi
+25 -21
View File
@@ -212,20 +212,25 @@ run_extra_shell() {
## 脚本用法 ## 脚本用法
usage() { usage() {
echo -e "ql命令使用方法:" echo -e "$cmd_update 命令使用方法:"
echo -e "1. $cmd_update update # 更新并重启青龙" echo -e "1. $cmd_update update # 更新并重启青龙"
echo -e "2. $cmd_update extra # 运行自定义脚本" echo -e "2. $cmd_update extra # 运行自定义脚本"
echo -e "3. $cmd_update raw <fileurl> # 更新单个脚本文件" echo -e "3. $cmd_update raw <fileurl> # 更新单个脚本文件"
echo -e "4. $cmd_update repo <repourl> <path> <blacklist> <dependence> <branch> <extensions> # 更新单个仓库的脚本" echo -e "4. $cmd_update repo <repourl> <path> <blacklist> <dependence> <branch> <extensions> # 更新单个仓库的脚本"
echo -e "5. $cmd_update rmlog <days> # 删除旧日志" echo -e "5. $cmd_update rmlog <days> # 删除旧日志"
echo -e "6. $cmd_update bot # 启动tg-bot" echo -e "6. $cmd_update bot # 启动tg-bot"
echo -e "7. $cmd_update check # 检测青龙环境并修复" echo -e "7. $cmd_update check # 检测青龙环境并修复"
echo -e "8. $cmd_update resetlet # 重置登录错误次数" echo -e "8. $cmd_update resetlet # 重置登录错误次数"
echo -e "9. $cmd_update resettfa # 禁用两步登录" echo -e "9. $cmd_update resettfa # 禁用两步登录"
echo -e "10. $cmd_update resetpwd # 修改登录密码"
echo -e "11. $cmd_update resetname # 修改登录用户名"
} }
reload_qinglong() { reload_qinglong() {
echo -e "[reload_qinglong] deleting Triggered at $(date)" >>${dir_log}/reload.log
sleep 3
delete_pm2 delete_pm2
echo -e "[reload_qinglong] deleted Triggered at $(date)" >>${dir_log}/reload.log
local reload_target="${1}" local reload_target="${1}"
local primary_branch="master" local primary_branch="master"
@@ -245,8 +250,9 @@ reload_qinglong() {
rm -rf ${dir_data}/* rm -rf ${dir_data}/*
mv -f ${dir_tmp}/data/* ${dir_data}/ mv -f ${dir_tmp}/data/* ${dir_data}/
fi fi
echo -e "[reload_qinglong] starting Triggered at $(date)" >>${dir_log}/reload.log
reload_pm2 reload_pm2
echo -e "[reload_qinglong] started Triggered at $(date)\n" >>${dir_log}/reload.log
} }
## 更新 qinglong ## 更新 qinglong
@@ -307,15 +313,7 @@ check_update_dep() {
echo -e "更新包下载成功..." echo -e "更新包下载成功..."
if [[ "$needRestart" == 'true' ]]; then if [[ "$needRestart" == 'true' ]]; then
delete_pm2 reload_qinglong "system"
rm -rf ${dir_root}/back ${dir_root}/cli ${dir_root}/docker ${dir_root}/sample ${dir_root}/shell ${dir_root}/src
mv -f ${dir_tmp}/qinglong-${primary_branch}/* ${dir_root}/
rm -rf $dir_static/*
mv -f ${dir_tmp}/qinglong-static-${primary_branch}/* ${dir_static}/
cp -f $file_config_sample $dir_config/config.sample.sh
reload_pm2
fi fi
else else
echo -e "\n依赖检测安装失败,请检查网络...\n" echo -e "\n依赖检测安装失败,请检查网络...\n"
@@ -543,6 +541,12 @@ main() {
resettfa) resettfa)
eval update_auth_config "\\\"twoFactorActivated\\\":false" "禁用两步验证" $cmd eval update_auth_config "\\\"twoFactorActivated\\\":false" "禁用两步验证" $cmd
;; ;;
resetpwd)
eval update_auth_config "\\\"password\\\":\\\"$p2\\\"" "重置密码" $cmd
;;
resetname)
eval update_auth_config "\\\"username\\\":\\\"$p2\\\"" "重置用户名" $cmd
;;
*) *)
eval echo -e "命令输入错误...\\\n" $cmd eval echo -e "命令输入错误...\\\n" $cmd
eval usage $cmd eval usage $cmd
@@ -553,7 +557,7 @@ 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
+6 -3
View File
@@ -101,16 +101,19 @@ export default function () {
const getHealthStatus = () => { const getHealthStatus = () => {
request request
.get(`${config.apiPrefix}public/health`) .get(`${config.apiPrefix}health`)
.then((res) => { .then((res) => {
if (res?.data?.status === 1) { if (res?.data?.status === 'ok') {
getSystemInfo(); getSystemInfo();
} else { } else {
history.push('/error'); history.push('/error');
} }
}) })
.catch((error) => { .catch((error) => {
history.push('/error'); const responseStatus = error.response.status;
if (responseStatus !== 401) {
history.push('/error');
}
}) })
.finally(() => setInitLoading(false)); .finally(() => setInitLoading(false));
}; };
+12 -2
View File
@@ -186,6 +186,7 @@
"秒后重试": "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",
"当前文件不支持预览": "Current file type is not supported for preview",
"清空日志": "Clear Logs", "清空日志": "Clear Logs",
"设置": "Settings", "设置": "Settings",
"退出": "Exit", "退出": "Exit",
@@ -344,7 +345,7 @@
"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",
"chat的url地址": "Chat URL address", "synologyChat的url地址": "Synology Chat Webhook 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",
@@ -496,5 +497,14 @@
"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",
"强制打开": "Force Open",
"强制打开可能会导致编辑器显示异常": "Force opening may cause display issues in the editor",
"确认离开": "Confirm Leave",
"当前文件未保存,确认离开吗": "Current file is not saved, are you sure to leave?",
"收件邮箱地址,多个分号分隔,默认发送给发件邮箱地址": "Receiving email address, multiple semicolon separated, sent to the sending email address by default"
} }
+13 -2
View File
@@ -186,6 +186,7 @@
"秒后重试": "秒后重试", "秒后重试": "秒后重试",
"在您的设备上打开两步验证应用程序以查看您的身份验证代码并验证您的身份。": "在您的设备上打开两步验证应用程序以查看您的身份验证代码并验证您的身份。", "在您的设备上打开两步验证应用程序以查看您的身份验证代码并验证您的身份。": "在您的设备上打开两步验证应用程序以查看您的身份验证代码并验证您的身份。",
"请选择脚本文件": "请选择脚本文件", "请选择脚本文件": "请选择脚本文件",
"当前文件不支持预览": "当前文件不支持预览",
"清空日志": "清空日志", "清空日志": "清空日志",
"设置": "设置", "设置": "设置",
"退出": "退出", "退出": "退出",
@@ -344,7 +345,7 @@
"gotify的url地址,例如 https://push.example.de:8080": "gotify的url地址,例如 https://push.example.de:8080", "gotify的url地址,例如 https://push.example.de:8080": "gotify的url地址,例如 https://push.example.de:8080",
"gotify的消息应用token码": "gotify的消息应用token码", "gotify的消息应用token码": "gotify的消息应用token码",
"推送消息的优先级": "推送消息的优先级", "推送消息的优先级": "推送消息的优先级",
"chat的url地址": "chat的url地址", "synologyChat的url地址": "synologyChat的webhook 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",
"访问密钥": "访问密钥", "访问密钥": "访问密钥",
@@ -496,5 +497,15 @@
"NPM 镜像源": "NPM 镜像源", "NPM 镜像源": "NPM 镜像源",
"PyPI 镜像源": "PyPI 镜像源", "PyPI 镜像源": "PyPI 镜像源",
"alpine linux 镜像源": "alpine linux 镜像源", "alpine linux 镜像源": "alpine linux 镜像源",
"如果恢复失败,可进入容器执行": "如果恢复失败,可进入容器执行" "如果恢复失败,可进入容器执行": "如果恢复失败,可进入容器执行",
"常规定时": "常规定时",
"手动运行": "手动运行",
"开机运行": "开机运行",
"时区": "时区",
"强制打开": "强制打开",
"强制打开可能会导致编辑器显示异常": "强制打开可能会导致编辑器显示异常",
"确认离开": "确认离开",
"当前文件未保存,确认离开吗": "当前文件未保存,确认离开吗",
"收件邮箱地址,多个分号分隔,默认发送给发件邮箱地址": "收件邮箱地址,多个分号分隔,默认发送给发件邮箱地址"
} }
+13
View File
@@ -0,0 +1,13 @@
import { ScheduleType } from './type';
export const scheduleTypeMap = {
[ScheduleType.Normal]: '',
[ScheduleType.Once]: '@once',
[ScheduleType.Boot]: '@boot',
};
export const getScheduleType = (schedule?: string): ScheduleType => {
if (schedule?.startsWith('@once')) return ScheduleType.Once;
if (schedule?.startsWith('@boot')) return ScheduleType.Boot;
return ScheduleType.Normal;
};
+11 -27
View File
@@ -56,12 +56,10 @@ interface LogItem {
const CronDetailModal = ({ const CronDetailModal = ({
cron = {}, cron = {},
handleCancel, handleCancel,
visible,
theme, theme,
isPhone, isPhone,
}: { }: {
cron?: any; cron?: any;
visible: boolean;
handleCancel: (needUpdate?: boolean) => void; handleCancel: (needUpdate?: boolean) => void;
theme: string; theme: string;
isPhone: boolean; isPhone: boolean;
@@ -202,9 +200,6 @@ const CronDetailModal = ({
.catch((e) => reject(e)); .catch((e) => reject(e));
}); });
}, },
onCancel() {
console.log('Cancel');
},
}); });
}; };
@@ -232,9 +227,6 @@ const CronDetailModal = ({
} }
}); });
}, },
onCancel() {
console.log('Cancel');
},
}); });
}; };
@@ -259,9 +251,6 @@ const CronDetailModal = ({
} }
}); });
}, },
onCancel() {
console.log('Cancel');
},
}); });
}; };
@@ -298,9 +287,6 @@ const CronDetailModal = ({
} }
}); });
}, },
onCancel() {
console.log('Cancel');
},
}); });
}; };
@@ -337,9 +323,6 @@ const CronDetailModal = ({
} }
}); });
}, },
onCancel() {
console.log('Cancel');
},
}); });
}; };
@@ -455,7 +438,7 @@ const CronDetailModal = ({
</div> </div>
} }
centered centered
open={visible} open={true}
forceRender forceRender
footer={false} footer={false}
onCancel={() => handleCancel()} onCancel={() => handleCancel()}
@@ -574,15 +557,16 @@ const CronDetailModal = ({
{contentList[activeTabKey]} {contentList[activeTabKey]}
</Card> </Card>
</div> </div>
<CronLogModal {isLogModalVisible && (
visible={isLogModalVisible} <CronLogModal
handleCancel={() => { handleCancel={() => {
setIsLogModalVisible(false); setIsLogModalVisible(false);
}} }}
cron={cron} cron={cron}
data={log} data={log}
logUrl={logUrl} logUrl={logUrl}
/> />
)}
</Modal> </Modal>
); );
}; };
+115 -124
View File
@@ -1,64 +1,64 @@
import intl from 'react-intl-universal'; import useTableScrollHeight from '@/hooks/useTableScrollHeight';
import React, { useState, useEffect, useRef, useMemo } from 'react'; import { SharedContext } from '@/layouts';
import { getCommandScript, getCrontabsNextDate } from '@/utils';
import config from '@/utils/config';
import { diffTime } from '@/utils/date';
import { request } from '@/utils/http';
import {
CheckCircleOutlined,
CheckOutlined,
ClockCircleOutlined,
CloseCircleOutlined,
CopyOutlined,
DeleteOutlined,
DownOutlined,
EditOutlined,
EllipsisOutlined,
FieldTimeOutlined,
Loading3QuartersOutlined,
PlusOutlined,
PushpinOutlined,
SettingOutlined,
StopOutlined,
UnorderedListOutlined,
} from '@ant-design/icons';
import { PageContainer } from '@ant-design/pro-layout';
import { history, useOutletContext } from '@umijs/max';
import { import {
Button, Button,
Dropdown,
Input,
MenuProps,
message, message,
Modal, Modal,
Table,
Tag,
Space, Space,
Tooltip, Table,
Dropdown,
Menu,
Typography,
Input,
Popover,
Tabs,
TablePaginationConfig, TablePaginationConfig,
MenuProps, Tabs,
Tag,
Typography,
} from 'antd'; } from 'antd';
import {
ClockCircleOutlined,
Loading3QuartersOutlined,
CloseCircleOutlined,
FileTextOutlined,
EllipsisOutlined,
PlayCircleOutlined,
CheckCircleOutlined,
EditOutlined,
StopOutlined,
DeleteOutlined,
PauseCircleOutlined,
FieldTimeOutlined,
PushpinOutlined,
DownOutlined,
SettingOutlined,
PlusOutlined,
UnorderedListOutlined,
CheckOutlined,
CopyOutlined,
} 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 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 { ColumnProps } from 'antd/lib/table';
import { useVT } from 'virtualizedtableforantd4'; import { FilterValue, SorterResult } from 'antd/lib/table/interface';
import { ICrontab, OperationName, OperationPath, CrontabStatus } from './type';
import Name from '@/components/name';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { noop, omit } from 'lodash'; 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 {
CrontabStatus,
ICrontab,
OperationName,
OperationPath,
ScheduleType,
} from './type';
import ViewCreateModal from './viewCreateModal';
import ViewManageModal from './viewManageModal';
const { Text, Paragraph, Link } = Typography; const { Text, Paragraph, Link } = Typography;
const { Search } = Input; const { Search } = Input;
@@ -263,7 +263,9 @@ const Crontab = () => {
}, },
}, },
render: (text, record) => { render: (text, record) => {
return dayjs(record.nextRunTime).format('YYYY-MM-DD HH:mm:ss'); return record.nextRunTime
? dayjs(record.nextRunTime).format('YYYY-MM-DD HH:mm:ss')
: '-';
}, },
}, },
{ {
@@ -396,9 +398,14 @@ const Crontab = () => {
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: getCrontabsNextDate(x.schedule, x.extra_schedules), nextRunTime,
subscription: subscriptionMap?.[x.sub_id], subscription: subscriptionMap?.[x.sub_id],
}; };
}), }),
@@ -446,9 +453,6 @@ const Crontab = () => {
} }
}); });
}, },
onCancel() {
console.log('Cancel');
},
}); });
}; };
@@ -481,9 +485,6 @@ const Crontab = () => {
} }
}); });
}, },
onCancel() {
console.log('Cancel');
},
}); });
}; };
@@ -517,9 +518,6 @@ const Crontab = () => {
} }
}); });
}, },
onCancel() {
console.log('Cancel');
},
}); });
}; };
@@ -562,9 +560,6 @@ const Crontab = () => {
} }
}); });
}, },
onCancel() {
console.log('Cancel');
},
}); });
}; };
@@ -607,9 +602,6 @@ const Crontab = () => {
} }
}); });
}, },
onCancel() {
console.log('Cancel');
},
}); });
}; };
@@ -731,9 +723,6 @@ const Crontab = () => {
} }
}); });
}, },
onCancel() {
console.log('Cancel');
},
}); });
}; };
@@ -759,9 +748,6 @@ const Crontab = () => {
} }
}); });
}, },
onCancel() {
console.log('Cancel');
},
}); });
}; };
@@ -806,7 +792,9 @@ const Crontab = () => {
useEffect(() => { useEffect(() => {
if (viewConf && enabledCronViews && enabledCronViews.length > 0) { if (viewConf && enabledCronViews && enabledCronViews.length > 0) {
const view = enabledCronViews.slice(SHOW_TAB_COUNT).find((x) => x.id === viewConf.id); const view = enabledCronViews
.slice(SHOW_TAB_COUNT)
.find((x) => x.id === viewConf.id);
setMoreMenuActive(!!view); setMoreMenuActive(!!view);
} }
}, [viewConf, enabledCronViews]); }, [viewConf, enabledCronViews]);
@@ -1049,55 +1037,58 @@ const Crontab = () => {
components={isPhone || pageConf.size < 50 ? undefined : vt} components={isPhone || pageConf.size < 50 ? undefined : vt}
/> />
</div> </div>
<CronLogModal {isLogModalVisible && (
visible={isLogModalVisible} <CronLogModal
handleCancel={() => { handleCancel={() => {
getCronDetail(logCron); getCronDetail(logCron);
setIsLogModalVisible(false); setIsLogModalVisible(false);
}} }}
cron={logCron} cron={logCron}
/> />
<CronModal )}
visible={isModalVisible} {isModalVisible && (
handleCancel={handleCancel} <CronModal handleCancel={handleCancel} cron={editedCron} />
cron={editedCron} )}
/> {isLabelModalVisible && (
<CronLabelModal <CronLabelModal
visible={isLabelModalVisible} handleCancel={(needUpdate?: boolean) => {
handleCancel={(needUpdate?: boolean) => { setIsLabelModalVisible(false);
setIsLabelModalVisible(false); if (needUpdate) {
if (needUpdate) { getCrons();
getCrons(); }
} }}
}} ids={selectedRowIds}
ids={selectedRowIds} />
/> )}
<CronDetailModal {isDetailModalVisible && (
visible={isDetailModalVisible} <CronDetailModal
handleCancel={() => { handleCancel={() => {
setIsDetailModalVisible(false); setIsDetailModalVisible(false);
}} }}
cron={detailCron} cron={detailCron}
theme={theme} theme={theme}
isPhone={isPhone} isPhone={isPhone}
/> />
<ViewCreateModal )}
visible={isCreateViewModalVisible} {isCreateViewModalVisible && (
handleCancel={(data) => { <ViewCreateModal
setIsCreateViewModalVisible(false); handleCancel={(data) => {
getCronViews(); setIsCreateViewModalVisible(false);
}} getCronViews();
/> }}
<ViewManageModal />
cronViews={cronViews} )}
visible={isViewManageModalVisible} {isViewManageModalVisible && (
handleCancel={() => { <ViewManageModal
setIsViewManageModalVisible(false); cronViews={cronViews}
}} handleCancel={() => {
cronViewChange={(data) => { setIsViewManageModalVisible(false);
getCronViews(); }}
}} cronViewChange={(data) => {
/> getCronViews();
}}
/>
)}
</PageContainer> </PageContainer>
); );
}; };
+3 -6
View File
@@ -25,12 +25,10 @@ const { Countdown } = Statistic;
const CronLogModal = ({ const CronLogModal = ({
cron, cron,
handleCancel, handleCancel,
visible,
data, data,
logUrl, logUrl,
}: { }: {
cron?: any; cron?: any;
visible: boolean;
handleCancel: () => void; handleCancel: () => void;
data?: string; data?: string;
logUrl?: string; logUrl?: string;
@@ -120,11 +118,10 @@ const CronLogModal = ({
}; };
useEffect(() => { useEffect(() => {
if (cron && cron.id && visible) { if (cron && cron.id) {
getCronLog(true); getCronLog(true);
scrollInfoRef.current.down = true;
} }
}, [cron, visible]); }, [cron]);
useEffect(() => { useEffect(() => {
if (data) { if (data) {
@@ -139,7 +136,7 @@ const CronLogModal = ({
return ( return (
<Modal <Modal
title={titleElement()} title={titleElement()}
open={visible} open={true}
centered centered
className="log-modal" className="log-modal"
forceRender forceRender
+105 -77
View File
@@ -1,32 +1,43 @@
import intl from 'react-intl-universal';
import React, { useEffect, useState } from 'react';
import { Modal, message, Input, Form, Button, Space } from 'antd';
import { request } from '@/utils/http';
import config from '@/utils/config';
import cronParse from 'cron-parser';
import EditableTagGroup from '@/components/tag'; import EditableTagGroup from '@/components/tag';
import config from '@/utils/config';
import { request } from '@/utils/http';
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import { Button, Form, Input, Modal, Select, Space, message } from 'antd';
import cronParse from 'cron-parser';
import { useEffect, useState } from 'react';
import intl from 'react-intl-universal';
import { getScheduleType, scheduleTypeMap } from './const';
import { ScheduleType } from './type';
const CronModal = ({ const CronModal = ({
cron, cron,
handleCancel, handleCancel,
visible,
}: { }: {
cron?: any; cron?: any;
visible: boolean;
handleCancel: (needUpdate?: boolean) => void; handleCancel: (needUpdate?: boolean) => void;
}) => { }) => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [scheduleType, setScheduleType] = useState<ScheduleType>(
cron ? getScheduleType(cron.schedule) : ScheduleType.Normal,
);
const handleOk = async (values: any) => { const handleOk = async (values: any) => {
setLoading(true); setLoading(true);
const method = cron?.id ? 'put' : 'post';
const payload = { ...values };
if (cron?.id) {
payload.id = cron.id;
}
try { try {
const method = cron?.id ? 'put' : 'post';
const payload = {
...values,
schedule:
scheduleType !== ScheduleType.Normal
? scheduleTypeMap[scheduleType]
: values.schedule,
};
if (cron?.id) {
payload.id = cron.id;
}
const { code, data } = await request[method]( const { code, data } = await request[method](
`${config.apiPrefix}crons`, `${config.apiPrefix}crons`,
payload, payload,
@@ -38,74 +49,52 @@ const CronModal = ({
); );
handleCancel(data); handleCancel(data);
} }
setLoading(false);
} catch (error: any) { } catch (error: any) {
console.error(error);
} finally {
setLoading(false); setLoading(false);
} }
}; };
useEffect(() => { const handleScheduleTypeChange = (type: ScheduleType) => {
form.resetFields(); setScheduleType(type);
}, [cron, visible]); form.setFieldValue('schedule', '');
};
return ( const renderScheduleOptions = () => (
<Modal <Select
title={cron?.id ? intl.get('编辑任务') : intl.get('创建任务')} defaultValue={scheduleType}
open={visible} value={scheduleType}
forceRender onChange={handleScheduleTypeChange}
centered
maskClosable={false}
onOk={() => {
form
.validateFields()
.then((values) => {
handleOk(values);
})
.catch((info) => {
console.log('Validate Failed:', info);
});
}}
onCancel={() => handleCancel()}
confirmLoading={loading}
> >
<Form <Select.Option value={ScheduleType.Normal}>
form={form} {intl.get('常规定时')}
layout="vertical" </Select.Option>
name="form_in_modal" <Select.Option value={ScheduleType.Once}>
initialValues={cron} {intl.get('手动运行')}
> </Select.Option>
<Form.Item <Select.Option value={ScheduleType.Boot}>
name="name" {intl.get('开机运行')}
label={intl.get('名称')} </Select.Option>
rules={[{ required: true, whitespace: true }]} </Select>
> );
<Input placeholder={intl.get('请输入任务名称')} />
</Form.Item> const renderScheduleFields = () => {
<Form.Item if (scheduleType !== ScheduleType.Normal) return null;
name="command"
label={intl.get('命令/脚本')} return (
rules={[{ required: true, whitespace: true }]} <>
>
<Input.TextArea
rows={4}
autoSize={{ minRows: 1, maxRows: 5 }}
placeholder={intl.get(
'支持输入脚本路径/任意系统可执行命令/task 脚本路径',
)}
/>
</Form.Item>
<Form.Item <Form.Item
name="schedule" name="schedule"
label={intl.get('定时规则')} label={intl.get('定时规则')}
rules={[ rules={[
{ required: true }, { required: true },
{ {
validator: (rule, value) => { validator: (_, value) => {
if (!value || cronParse.parseExpression(value).hasNext()) { if (!value || cronParse.parseExpression(value).hasNext()) {
return Promise.resolve(); return Promise.resolve();
} else {
return Promise.reject(intl.get('Cron表达式格式有误'));
} }
return Promise.reject(intl.get('Cron表达式格式有误'));
}, },
}, },
]} ]}
@@ -136,14 +125,58 @@ const CronModal = ({
))} ))}
<Form.Item> <Form.Item>
<a onClick={() => add({ schedule: '' })}> <a onClick={() => add({ schedule: '' })}>
<PlusOutlined /> <PlusOutlined /> {intl.get('新增定时规则')}
{intl.get('新增定时规则')}
</a> </a>
</Form.Item> </Form.Item>
<Form.ErrorList errors={errors} /> <Form.ErrorList errors={errors} />
</> </>
)} )}
</Form.List> </Form.List>
</>
);
};
return (
<Modal
title={cron?.id ? intl.get('编辑任务') : intl.get('创建任务')}
open={true}
forceRender
centered
maskClosable={false}
onOk={() => form.validateFields().then(handleOk)}
onCancel={() => handleCancel()}
confirmLoading={loading}
>
<Form
form={form}
layout="vertical"
name="form_in_modal"
initialValues={cron}
>
<Form.Item
name="name"
label={intl.get('名称')}
rules={[{ required: true, whitespace: true }]}
>
<Input placeholder={intl.get('请输入任务名称')} />
</Form.Item>
<Form.Item
name="command"
label={intl.get('命令/脚本')}
rules={[{ required: true, whitespace: true }]}
>
<Input.TextArea
rows={4}
autoSize={{ minRows: 1, maxRows: 5 }}
placeholder={intl.get(
'支持输入脚本路径/任意系统可执行命令/task 脚本路径',
)}
/>
</Form.Item>
<Form.Item label={intl.get('定时类型')} required>
{renderScheduleOptions()}
</Form.Item>
{renderScheduleFields()}
<Form.Item name="labels" label={intl.get('标签')}> <Form.Item name="labels" label={intl.get('标签')}>
<EditableTagGroup /> <EditableTagGroup />
</Form.Item> </Form.Item>
@@ -155,7 +188,7 @@ const CronModal = ({
)} )}
rules={[ rules={[
{ {
validator(rule, value) { validator(_, value) {
if ( if (
value && value &&
(value.includes(' task ') || value.startsWith('task ')) (value.includes(' task ') || value.startsWith('task '))
@@ -183,7 +216,7 @@ const CronModal = ({
)} )}
rules={[ rules={[
{ {
validator(rule, value) { validator(_, value) {
if ( if (
value && value &&
(value.includes(' task ') || value.startsWith('task ')) (value.includes(' task ') || value.startsWith('task '))
@@ -211,10 +244,8 @@ const CronModal = ({
const CronLabelModal = ({ const CronLabelModal = ({
ids, ids,
handleCancel, handleCancel,
visible,
}: { }: {
ids: Array<string>; ids: Array<string>;
visible: boolean;
handleCancel: (needUpdate?: boolean) => void; handleCancel: (needUpdate?: boolean) => void;
}) => { }) => {
const [form] = Form.useForm(); const [form] = Form.useForm();
@@ -250,10 +281,6 @@ const CronLabelModal = ({
}); });
}; };
useEffect(() => {
form.resetFields();
}, [ids, visible]);
const buttons = [ const buttons = [
<Button onClick={() => handleCancel(false)}>{intl.get('取消')}</Button>, <Button onClick={() => handleCancel(false)}>{intl.get('取消')}</Button>,
<Button type="primary" danger onClick={() => update('delete')}> <Button type="primary" danger onClick={() => update('delete')}>
@@ -267,7 +294,7 @@ const CronLabelModal = ({
return ( return (
<Modal <Modal
title={intl.get('批量修改标签')} title={intl.get('批量修改标签')}
open={visible} open={true}
footer={buttons} footer={buttons}
centered centered
maskClosable={false} maskClosable={false}
@@ -284,4 +311,5 @@ const CronLabelModal = ({
); );
}; };
export { CronModal as default, CronLabelModal }; export { CronLabelModal, CronModal as default };
+7 -1
View File
@@ -36,5 +36,11 @@ export interface ICrontab {
last_execution_time?: number; last_execution_time?: number;
nextRunTime: Date; nextRunTime: Date;
sub_id: number; sub_id: number;
extra_schedules?: Array<{ schedule: string; }>; extra_schedules?: Array<{ schedule: string }>;
}
export enum ScheduleType {
Normal = 'normal',
Once = 'once',
Boot = 'boot',
} }
+11 -15
View File
@@ -56,10 +56,8 @@ enum ViewFilterRelation {
const ViewCreateModal = ({ const ViewCreateModal = ({
view, view,
handleCancel, handleCancel,
visible,
}: { }: {
view?: any; view?: any;
visible: boolean;
handleCancel: (param?: any) => void; handleCancel: (param?: any) => void;
}) => { }) => {
const [form] = Form.useForm(); const [form] = Form.useForm();
@@ -101,17 +99,6 @@ const ViewCreateModal = ({
} }
}; };
useEffect(() => {
if (!view) {
form.resetFields();
}
form.setFieldsValue(
view || {
filters: [{ property: 'command' }],
},
);
}, [view, visible]);
const OperationElement = ({ name, ...others }: { name: number }) => { const OperationElement = ({ name, ...others }: { name: number }) => {
const property = form.getFieldValue(['filters', name, 'property']); const property = form.getFieldValue(['filters', name, 'property']);
return ( return (
@@ -172,7 +159,7 @@ const ViewCreateModal = ({
return ( return (
<Modal <Modal
title={view ? intl.get('编辑视图') : intl.get('创建视图')} title={view ? intl.get('编辑视图') : intl.get('创建视图')}
open={visible} open={true}
forceRender forceRender
width={580} width={580}
centered centered
@@ -190,7 +177,16 @@ const ViewCreateModal = ({
onCancel={() => handleCancel()} onCancel={() => handleCancel()}
confirmLoading={loading} confirmLoading={loading}
> >
<Form form={form} layout="vertical" name="env_modal"> <Form
form={form}
layout="vertical"
initialValues={
view || {
filters: [{ property: 'command' }],
}
}
name="env_modal"
>
<Form.Item <Form.Item
name="name" name="name"
label={intl.get('视图名称')} label={intl.get('视图名称')}
+11 -15
View File
@@ -68,11 +68,9 @@ const DragableBodyRow = ({
const ViewManageModal = ({ const ViewManageModal = ({
cronViews, cronViews,
handleCancel, handleCancel,
visible,
cronViewChange, cronViewChange,
}: { }: {
cronViews: any[]; cronViews: any[];
visible: boolean;
handleCancel: () => void; handleCancel: () => void;
cronViewChange: (data?: any) => void; cronViewChange: (data?: any) => void;
}) => { }) => {
@@ -85,7 +83,7 @@ const ViewManageModal = ({
title: intl.get('名称'), title: intl.get('名称'),
dataIndex: 'name', dataIndex: 'name',
key: 'name', key: 'name',
render: (v) => (v === '全部任务' ? intl.get('全部任务') : v) render: (v) => (v === '全部任务' ? intl.get('全部任务') : v),
}, },
{ {
title: intl.get('类型'), title: intl.get('类型'),
@@ -162,9 +160,6 @@ const ViewManageModal = ({
} }
}); });
}, },
onCancel() {
console.log('Cancel');
},
}); });
}; };
@@ -221,7 +216,7 @@ const ViewManageModal = ({
return ( return (
<Modal <Modal
title={intl.get('视图管理')} title={intl.get('视图管理')}
open={visible} open={true}
centered centered
width={620} width={620}
onCancel={() => handleCancel()} onCancel={() => handleCancel()}
@@ -266,14 +261,15 @@ const ViewManageModal = ({
}} }}
/> />
</DndProvider> </DndProvider>
<ViewCreateModal {isCreateViewModalVisible && (
view={editedView} <ViewCreateModal
visible={isCreateViewModalVisible} view={editedView}
handleCancel={(data) => { handleCancel={(data) => {
setIsCreateViewModalVisible(false); setIsCreateViewModalVisible(false);
cronViewChange(data); cronViewChange(data);
}} }}
/> />
)}
</Modal> </Modal>
); );
}; };
+8 -23
View File
@@ -315,9 +315,6 @@ const Dependence = () => {
} }
}); });
}, },
onCancel() {
console.log('Cancel');
},
}); });
}; };
@@ -342,9 +339,6 @@ const Dependence = () => {
} }
}); });
}, },
onCancel() {
console.log('Cancel');
},
}); });
}; };
@@ -367,9 +361,6 @@ const Dependence = () => {
getDependencies(); getDependencies();
}); });
}, },
onCancel() {
console.log('Cancel');
},
}); });
}; };
@@ -419,9 +410,6 @@ const Dependence = () => {
} }
}); });
}, },
onCancel() {
console.log('Cancel');
},
}); });
}; };
@@ -439,9 +427,6 @@ const Dependence = () => {
} }
}); });
}, },
onCancel() {
console.log('Cancel');
},
}); });
}; };
@@ -633,15 +618,15 @@ const Dependence = () => {
]} ]}
/> />
{children} {children}
<DependenceModal {isModalVisible && (
visible={isModalVisible} <DependenceModal
handleCancel={handleCancel} handleCancel={handleCancel}
dependence={editedDependence} dependence={editedDependence}
defaultType={type} defaultType={type}
/> />
{logDependence && ( )}
{logDependence && isLogModalVisible && (
<DependenceLogModal <DependenceLogModal
visible={isLogModalVisible}
handleCancel={(needRemove?: boolean) => { handleCancel={(needRemove?: boolean) => {
setIsLogModalVisible(false); setIsLogModalVisible(false);
if (needRemove) { if (needRemove) {
+1 -3
View File
@@ -15,10 +15,8 @@ import { Status } from './type';
const DependenceLogModal = ({ const DependenceLogModal = ({
dependence, dependence,
handleCancel, handleCancel,
visible,
}: { }: {
dependence?: any; dependence?: any;
visible: boolean;
handleCancel: (needRemove?: boolean) => void; handleCancel: (needRemove?: boolean) => void;
}) => { }) => {
const [value, setValue] = useState<string>(''); const [value, setValue] = useState<string>('');
@@ -128,7 +126,7 @@ const DependenceLogModal = ({
return ( return (
<Modal <Modal
title={titleElement()} title={titleElement()}
open={visible} open={true}
centered centered
className="log-modal" className="log-modal"
forceRender forceRender
+1 -7
View File
@@ -14,11 +14,9 @@ enum DependenceTypes {
const DependenceModal = ({ const DependenceModal = ({
dependence, dependence,
handleCancel, handleCancel,
visible,
defaultType, defaultType,
}: { }: {
dependence?: any; dependence?: any;
visible: boolean;
handleCancel: (cks?: any[]) => void; handleCancel: (cks?: any[]) => void;
defaultType: string; defaultType: string;
}) => { }) => {
@@ -61,14 +59,10 @@ const DependenceModal = ({
} }
}; };
useEffect(() => {
form.resetFields();
}, [dependence, visible]);
return ( return (
<Modal <Modal
title={dependence ? intl.get('编辑依赖') : intl.get('创建依赖')} title={dependence ? intl.get('编辑依赖') : intl.get('创建依赖')}
open={visible} open={true}
forceRender forceRender
centered centered
maskClosable={false} maskClosable={false}
+1 -7
View File
@@ -7,10 +7,8 @@ import config from '@/utils/config';
const EditNameModal = ({ const EditNameModal = ({
ids, ids,
handleCancel, handleCancel,
visible,
}: { }: {
ids?: string[]; ids?: string[];
visible: boolean;
handleCancel: () => void; handleCancel: () => void;
}) => { }) => {
const [form] = Form.useForm(); const [form] = Form.useForm();
@@ -34,14 +32,10 @@ const EditNameModal = ({
} }
}; };
useEffect(() => {
form.resetFields();
}, [ids, visible]);
return ( return (
<Modal <Modal
title={intl.get('修改环境变量名称')} title={intl.get('修改环境变量名称')}
open={visible} open={true}
forceRender forceRender
centered centered
maskClosable={false} maskClosable={false}
+9 -22
View File
@@ -292,9 +292,6 @@ const Env = () => {
} }
}); });
}, },
onCancel() {
console.log('Cancel');
},
}); });
}; };
@@ -337,9 +334,6 @@ const Env = () => {
} }
}); });
}, },
onCancel() {
console.log('Cancel');
},
}); });
}; };
@@ -456,9 +450,6 @@ const Env = () => {
} }
}); });
}, },
onCancel() {
console.log('Cancel');
},
}); });
}; };
@@ -484,9 +475,6 @@ const Env = () => {
} }
}); });
}, },
onCancel() {
console.log('Cancel');
},
}); });
}; };
@@ -628,16 +616,15 @@ const Env = () => {
/> />
</DndProvider> </DndProvider>
</div> </div>
<EnvModal {isModalVisible && (
visible={isModalVisible} <EnvModal handleCancel={handleCancel} env={editedEnv} />
handleCancel={handleCancel} )}
env={editedEnv} {isEditNameModalVisible && (
/> <EditNameModal
<EditNameModal handleCancel={handleEditNameCancel}
visible={isEditNameModalVisible} ids={selectedRowIds}
handleCancel={handleEditNameCancel} />
ids={selectedRowIds} )}
/>
</PageContainer> </PageContainer>
); );
}; };
+1 -7
View File
@@ -7,10 +7,8 @@ import config from '@/utils/config';
const EnvModal = ({ const EnvModal = ({
env, env,
handleCancel, handleCancel,
visible,
}: { }: {
env?: any; env?: any;
visible: boolean;
handleCancel: (cks?: any[]) => void; handleCancel: (cks?: any[]) => void;
}) => { }) => {
const [form] = Form.useForm(); const [form] = Form.useForm();
@@ -55,14 +53,10 @@ const EnvModal = ({
} }
}; };
useEffect(() => {
form.resetFields();
}, [env, visible]);
return ( return (
<Modal <Modal
title={env ? intl.get('编辑变量') : intl.get('创建变量')} title={env ? intl.get('编辑变量') : intl.get('创建变量')}
open={visible} open={true}
forceRender forceRender
centered centered
maskClosable={false} maskClosable={false}
+23 -10
View File
@@ -14,12 +14,23 @@ const Error = () => {
const [data, setData] = useState(intl.get('暂无日志')); const [data, setData] = useState(intl.get('暂无日志'));
const retryTimes = useRef(1); const retryTimes = useRef(1);
const loopStatus = (message: string) => {
if (retryTimes.current > 3) {
setData(message);
return;
}
retryTimes.current += 1;
setTimeout(() => {
getHealthStatus(false);
}, 3000);
};
const getHealthStatus = (needLoading: boolean = true) => { const getHealthStatus = (needLoading: boolean = true) => {
needLoading && setLoading(true); needLoading && setLoading(true);
request request
.get(`${config.apiPrefix}public/health`) .get(`${config.apiPrefix}health`)
.then(({ error, data }) => { .then(({ error, data }) => {
if (data?.status === 1) { if (data?.status === 'ok') {
if (retryTimes.current > 1) { if (retryTimes.current > 1) {
setTimeout(() => { setTimeout(() => {
window.location.reload(); window.location.reload();
@@ -27,14 +38,16 @@ const Error = () => {
} }
return; return;
} }
if (retryTimes.current > 3) {
setData(error?.details); loopStatus(error?.details);
return; })
.catch((error) => {
const responseStatus = error.response.status;
if (responseStatus === 401) {
history.push('/login');
} else {
loopStatus(error.response?.message || error?.message);
} }
retryTimes.current += 1;
setTimeout(() => {
getHealthStatus(false);
}, 3000);
}) })
.finally(() => needLoading && setLoading(false)); .finally(() => needLoading && setLoading(false));
}; };
@@ -74,7 +87,7 @@ const Error = () => {
<div>{intl.get('2. 容器内执行 ql check、ql update')}</div> <div>{intl.get('2. 容器内执行 ql check、ql update')}</div>
<div> <div>
{intl.get( {intl.get(
'3. 如果无法解决,容器内执行 pm2 logs,拷贝执行结果' '3. 如果无法解决,容器内执行 pm2 logs,拷贝执行结果',
)} )}
<Typography.Link href="https://github.com/whyour/qinglong/issues/new?assignees=&labels=&template=bug_report.yml"> <Typography.Link href="https://github.com/whyour/qinglong/issues/new?assignees=&labels=&template=bug_report.yml">
{intl.get('提交 issue')} {intl.get('提交 issue')}
+43 -22
View File
@@ -1,31 +1,32 @@
import intl from 'react-intl-universal'; import useFilterTreeData from '@/hooks/useFilterTreeData';
import { useState, useEffect, useCallback, Key, useRef } from 'react'; import { SharedContext } from '@/layouts';
import { depthFirstSearch } from '@/utils';
import config from '@/utils/config';
import { request } from '@/utils/http';
import { CloudDownloadOutlined, DeleteOutlined } from '@ant-design/icons';
import { PageContainer } from '@ant-design/pro-layout';
import Editor from '@monaco-editor/react';
import CodeMirror from '@uiw/react-codemirror';
import { useOutletContext } from '@umijs/max';
import { import {
TreeSelect,
Tree,
Input,
Empty,
Button, Button,
Empty,
Input,
message, message,
Modal, Modal,
Tooltip, Tooltip,
Tree,
TreeSelect,
Typography, Typography,
} from 'antd'; } from 'antd';
import config from '@/utils/config'; import { saveAs } from 'file-saver';
import { PageContainer } from '@ant-design/pro-layout';
import Editor from '@monaco-editor/react';
import { request } from '@/utils/http';
import styles from './index.module.less';
import CodeMirror from '@uiw/react-codemirror';
import SplitPane from 'react-split-pane';
import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts';
import { DeleteOutlined } from '@ant-design/icons';
import { depthFirstSearch } from '@/utils';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import uniq from 'lodash/uniq'; import uniq from 'lodash/uniq';
import useFilterTreeData from '@/hooks/useFilterTreeData';
import prettyBytes from 'pretty-bytes'; import prettyBytes from 'pretty-bytes';
import { Key, useCallback, useEffect, useRef, useState } from 'react';
import intl from 'react-intl-universal';
import SplitPane from 'react-split-pane';
import styles from './index.module.less';
const { Text } = Typography; const { Text } = Typography;
@@ -67,6 +68,21 @@ const Log = () => {
}); });
}; };
const downloadLog = () => {
request
.post<Blob>(
`${config.apiPrefix}logs/download`,
{
filename: currentNode.title,
path: currentNode.parent || '',
},
{ responseType: 'blob' },
)
.then((res) => {
saveAs(res, currentNode.title);
});
};
const onSelect = (value: any, node: any) => { const onSelect = (value: any, node: any) => {
if (node.key === select || !value) { if (node.key === select || !value) {
return; return;
@@ -159,9 +175,6 @@ const Log = () => {
} }
}); });
}, },
onCancel() {
console.log('Cancel');
},
}); });
}; };
@@ -225,10 +238,18 @@ const Log = () => {
/>, />,
] ]
: [ : [
<Tooltip title={intl.get('下载')}>
<Button
disabled={!currentNode || currentNode.type === 'directory'}
type="primary"
onClick={downloadLog}
icon={<CloudDownloadOutlined />}
/>
</Tooltip>,
<Tooltip title={intl.get('删除')}> <Tooltip title={intl.get('删除')}>
<Button <Button
type="primary" type="primary"
disabled={!select} disabled={!currentNode}
onClick={deleteFile} onClick={deleteFile}
icon={<DeleteOutlined />} icon={<DeleteOutlined />}
/> />
@@ -0,0 +1,67 @@
.container {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: var(--background-color);
padding: 16px;
}
.content {
text-align: center;
background: var(--card-background);
padding: 24px;
border-radius: 12px;
max-width: 390px;
width: 100%;
transition: all 0.3s ease;
}
.iconWrapper {
display: inline-flex;
align-items: center;
justify-content: center;
width: 64px;
height: 64px;
border-radius: 50%;
background: var(--background-color);
margin-bottom: 16px;
}
.icon {
font-size: 32px;
color: var(--text-color-secondary);
}
.message {
font-size: 16px;
color: var(--text-color);
margin-bottom: 16px;
font-weight: 500;
line-height: 1.5;
}
.actionArea {
width: 100%;
}
.button {
min-width: 140px;
height: 36px;
font-size: 14px;
}
.warning {
font-size: 13px;
color: var(--text-color-secondary);
line-height: 1.5;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.warningIcon {
font-size: 14px;
color: #faad14;
}
@@ -0,0 +1,41 @@
import React from 'react';
import { Button, Space } from 'antd';
import { FileUnknownOutlined, WarningOutlined } from '@ant-design/icons';
import intl from 'react-intl-universal';
import styles from './index.module.less';
interface UnsupportedFilePreviewProps {
onForceOpen: () => void;
}
const UnsupportedFilePreview: React.FC<UnsupportedFilePreviewProps> = ({
onForceOpen,
}) => {
return (
<div className={styles.container}>
<div className={styles.content}>
<div className={styles.iconWrapper}>
<FileUnknownOutlined className={styles.icon} />
</div>
<div className={styles.message}>
{intl.get('当前文件不支持预览')}
</div>
<Space direction="vertical" size={8} className={styles.actionArea}>
<Button
type="primary"
onClick={onForceOpen}
className={styles.button}
>
{intl.get('强制打开')}
</Button>
<div className={styles.warning}>
<WarningOutlined className={styles.warningIcon} />
{intl.get('强制打开可能会导致编辑器显示异常')}
</div>
</Space>
</div>
</div>
);
};
export default UnsupportedFilePreview;

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