Compare commits

...
98 Commits
Author SHA1 Message Date
whyour b31b054d0c 更新版本 v2.14.11 2022-11-05 15:24:16 +08:00
whyour fb4a87f5ce 修复版本文件缓存 2022-11-05 15:23:17 +08:00
whyour 049c73880b 修改pre样式 2022-11-05 14:57:21 +08:00
whyour 7b2c54f6a6 修改版本信息获取 2022-11-05 12:14:46 +08:00
whyour 23bda39812 关于增加版本信息展示 2022-11-05 11:01:58 +08:00
whyour 90af5801ee 修复表格滚动 2022-11-04 19:14:03 +08:00
whyour 543c241bc6 更新版本 v2.14.10 2022-11-03 21:16:46 +08:00
whyour 19a58a3510 环境变量值增加复制功能 2022-11-03 17:34:36 +08:00
whyour 73c6fd18ed 修复资源预警通知 2022-11-03 13:42:14 +08:00
whyour 409297c6aa 修改调试日志样式 2022-11-02 19:54:49 +08:00
whyour 4272b9ea35 修复task命令参数 2022-11-02 19:40:16 +08:00
whyour 3c5c5b0bbc 修复&符号转义 2022-11-02 19:25:03 +08:00
whyour 802b2d722e 修复任务命令中的&和引号 2022-11-02 18:51:09 +08:00
whyour 8f90d3f8ff 修复环境变量位置计算逻辑 2022-11-02 00:02:15 +08:00
whyour f93cbbf508 修复 notify.js 飞书通知 2022-10-30 18:59:48 +08:00
whyour 2ba8756bd3 修改cdn协议 2022-10-30 12:00:12 +08:00
whyour 8013de56c9 修改sqlite配置 2022-10-30 10:54:39 +08:00
whyour 4af9b22442 更新版本 v2.14.9 2022-10-29 18:50:16 +08:00
whyour 8a392170cf webhook通知支持嵌套body 2022-10-29 18:43:16 +08:00
whyour 6385e053ae 增加飞书机器人通知 2022-10-29 16:41:37 +08:00
Leo_chenandGitHub 2789119882 添加智能微秘书通知 (#1680) 2022-10-29 15:45:16 +08:00
whyour 3e0553c3e8 修改登陆逻辑 2022-10-29 10:05:54 +08:00
whyour 5e16ace1e7 修改检查更新日志 2022-10-27 10:34:55 +08:00
whyour 0ca23cc4ca 修改环境变量默认排序 2022-10-27 00:02:50 +08:00
whyour 4949457001 修复ql命令日志打印 2022-10-26 23:50:07 +08:00
whyour 8ae13fe823 增加 -max 指定环境变量 2022-10-26 14:27:14 +08:00
whyour fdf672092a 修改获取version文件超时时间 2022-10-25 23:52:30 +08:00
whyour 48dd6ea826 修复表格滚动高度 2022-10-25 23:12:38 +08:00
whyour 68bb4f15e2 修复shell获取磁盘使用量 2022-10-24 22:45:06 +08:00
whyour 9c6d9ec22d 更新版本 v2.14.8 2022-10-22 18:36:38 +08:00
whyour ac075ace73 修复调试脚本语言判断 2022-10-22 00:31:34 +08:00
whyour eb58774ee9 优化task.sh 2022-10-22 00:25:54 +08:00
whyour 04613bf9a9 修复tsconfig 2022-10-20 15:30:22 +08:00
whyour 4194f1964d 修改日志结束符判定 2022-10-20 15:16:17 +08:00
whyour 42c64c82d7 修复 ql 命令日志目录 2022-10-16 21:59:19 +08:00
whyour d44a8966f2 修复环境变量名称验证 2022-10-16 09:22:37 +08:00
whyour ff44330aa1 修复 ql 命令日志目录 2022-10-15 18:40:15 +08:00
whyour ed259e8168 修复 task 命令执行时参数引号丢失 2022-10-15 18:29:34 +08:00
whyour 444836975d 修改 ql 命令打印日志逻辑 2022-10-14 23:02:20 +08:00
whyour 3756fa93e4 修复 ql 命令 2022-10-14 21:17:04 +08:00
whyour 064b52b306 修改 timezone action 2022-10-14 20:55:58 +08:00
whyour 648dc51474 更新ql指令 2022-10-14 15:17:37 +08:00
whyour 7c0bc32759 修改环境变量导入 2022-10-13 23:41:54 +08:00
whyour d4b6cc5e4d 修复调试脚本选择脚本 2022-10-13 23:39:28 +08:00
whyour 75fca715e3 增加环境变量导入 2022-10-11 22:16:36 +08:00
whyour 6e9a4a29ef 修复shell语法 2022-10-09 13:49:19 +08:00
whyour 78f643c50f 更新版本v2.14.7 2022-10-01 23:12:57 +08:00
whyour 2db4f1ba87 修改资源判定逻辑 2022-10-01 23:04:12 +08:00
whyour 5741dbb78c 修复获取task_before参数 2022-09-30 15:32:42 +08:00
whyour 88cf671465 更新版本v2.14.6 2022-09-29 23:06:31 +08:00
whyour 29f082dea2 增加config资源配置 2022-09-29 23:03:22 +08:00
whyour 9a55968ed6 修改资源异常推送 2022-09-29 13:29:22 +08:00
whyour 6ec6e25855 修改资源提示 2022-09-29 00:59:27 +08:00
whyour af48bc378b 修复获取cpu占用 2022-09-29 00:57:31 +08:00
whyour 3f5ae4bcb9 修复 echo 2022-09-29 00:44:07 +08:00
whyour c6624e9001 增加资源占用提示 2022-09-29 00:43:07 +08:00
whyour 5530ce76e3 添加系统资源消耗提示 2022-09-29 00:32:17 +08:00
whyour 654b51e476 修复脚本和日志搜索逻辑 2022-09-25 21:03:11 +08:00
whyour 6428ac3624 修复SplitPane类型错误 2022-09-25 14:02:27 +08:00
whyour c36450f436 修复ant-tree高度获取 2022-09-25 13:38:10 +08:00
whyour dcea231249 更新版本 v2.14.5 2022-09-24 21:00:47 +08:00
whyour f236119bd6 修改输入框自动填充样式 2022-09-24 14:40:13 +08:00
whyour cb870fad85 修改检查更新逻辑 2022-09-24 14:20:35 +08:00
whyour 44e2caedfc 修改更新日志清除评率 2022-09-24 13:59:56 +08:00
whyour c0b5192296 修改移动端脚本高亮 2022-09-24 11:27:16 +08:00
whyour ba43882c0b 修复脚本管理删除 2022-09-23 23:16:57 +08:00
whyour dfc706e16d 日志管理支持删除日志文件和目录 2022-09-23 20:09:10 +08:00
whyour 25b03d4345 脚本管理支持删除文件夹 2022-09-23 19:12:51 +08:00
whyour 4e8f36d9a4 修复登录错误提示 2022-09-23 17:44:48 +08:00
whyour 8f4d67ffa7 修改task命令ID获取 2022-09-23 14:37:56 +08:00
whyour ad47277149 修复登录验证 2022-09-23 12:15:15 +08:00
whyour 98c2a37ab6 修复初始化逻辑 2022-09-23 00:17:53 +08:00
whyour f2fea47336 重构前端错误提示 2022-09-22 23:59:23 +08:00
whyour e274d3e2f9 sentry增加版本号 2022-09-22 12:14:42 +08:00
whyour 36f4c3c02c 修改任务启动参数 2022-09-21 21:28:31 +08:00
whyour ae891f8e55 修复docker启动逻辑 2022-09-21 17:42:13 +08:00
whyour c874dc9705 更新readme 2022-09-21 16:03:53 +08:00
whyour 23bfbeb995 修改新建订阅提示 2022-09-21 14:54:45 +08:00
whyour a75df03243 更新版本 2.14.4 2022-09-20 19:59:51 +08:00
whyour e44a2b5995 修改tsconfig 2022-09-20 19:43:49 +08:00
whyour f23e7a9e49 修复自定义通知 2022-09-20 19:13:11 +08:00
whyour e08f0cd308 修复设置自定义通知 2022-09-20 18:00:36 +08:00
whyour 1ebcfd8001 修复自定义通知 2022-09-20 17:34:36 +08:00
whyour 5168b044a3 修复遍历key值 2022-09-20 01:23:33 +08:00
whyour 931638b687 增加自定义hook通知 2022-09-20 01:16:36 +08:00
whyour 4d9bbcfd3f 升级antd 2022-09-19 18:45:56 +08:00
whyour b0e3b26369 修改环境变量默认排序 2022-09-18 20:47:42 +08:00
whyour 1e55f4065d 升级umi4.0,修复新建脚本 2022-09-18 20:40:59 +08:00
whyour 518e8903a3 兼容Modal属性 2022-09-17 22:50:58 +08:00
whyour 6e56a55f6f 修改弹框表单逻辑 2022-09-16 00:28:04 +08:00
whyour 967071ad4e 脚本管理支持新增文件夹 2022-09-16 00:14:10 +08:00
whyour d23fcfaa5a 修复创建应用 2022-09-15 23:45:12 +08:00
whyour a2f2306430 增加批量重新安装依赖 2022-09-11 22:47:43 +08:00
whyour 144b543091 修改依赖管理默认排序 2022-09-11 22:26:33 +08:00
whyour e4ad8f512e 修改订阅默认排序 2022-09-11 22:10:17 +08:00
whyour 591a5006da 修复运行任务shell 2022-09-11 19:53:57 +08:00
whyour 281ed6cfba 修改任务执行日志目录规则 2022-09-11 19:43:37 +08:00
whyour 7354a61184 修改config配置说明 2022-09-10 14:23:16 +08:00
91 changed files with 3575 additions and 2634 deletions
+3 -5
View File
@@ -57,12 +57,10 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
- name: Set time zone - name: Setup timezone
uses: szenius/set-timezone@v1.0 uses: zcong1993/setup-timezone@master
with: with:
timezoneLinux: "Asia/Shanghai" timezone: Asia/Shanghai
timezoneMacos: "Asia/Shanghai"
timezoneWindows: "China Standard Time"
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v2 uses: docker/login-action@v2
+11 -16
View File
@@ -1,20 +1,15 @@
import { defineConfig } from 'umi'; import { defineConfig } from '@umijs/max';
const CompressionPlugin = require('compression-webpack-plugin'); const CompressionPlugin = require('compression-webpack-plugin');
export default defineConfig({ export default defineConfig({
hash: true, hash: true,
layout: false, antd: {},
outputPath: 'static/dist', outputPath: 'static/dist',
nodeModulesTransform: { fastRefresh: true,
type: 'none', favicons: ['/images/favicon.svg'],
mfsu: {
strategy: 'eager',
}, },
fastRefresh: {},
esbuild: {},
webpack5: {},
dynamicImport: {
loading: '@/components/pageLoading',
},
favicon: '/images/favicon.svg',
proxy: { proxy: {
'/api/public': { '/api/public': {
target: 'http://127.0.0.1:5400/', target: 'http://127.0.0.1:5400/',
@@ -26,7 +21,7 @@ export default defineConfig({
ws: true, ws: true,
}, },
}, },
chainWebpack: (config) => { chainWebpack: ((config: any) => {
config.plugin('compression-webpack-plugin').use( config.plugin('compression-webpack-plugin').use(
new CompressionPlugin({ new CompressionPlugin({
algorithm: 'gzip', algorithm: 'gzip',
@@ -35,13 +30,13 @@ export default defineConfig({
minRatio: 0.6, minRatio: 0.6,
}), }),
); );
}, }) as any,
externals: { externals: {
react: 'window.React', react: 'window.React',
'react-dom': 'window.ReactDOM', 'react-dom': 'window.ReactDOM',
}, },
scripts: [ headScripts: [
'https://gw.alipayobjects.com/os/lib/react/16.13.1/umd/react.production.min.js', 'https://gw.alipayobjects.com/os/lib/react/18.2.0/umd/react.production.min.js',
'https://gw.alipayobjects.com/os/lib/react-dom/16.13.1/umd/react-dom.production.min.js', 'https://gw.alipayobjects.com/os/lib/react-dom/18.2.0/umd/react-dom.production.min.js',
], ],
}); });
+2 -3
View File
@@ -1,6 +1,6 @@
<p align="center"> <p align="center">
<a href="https://github.com/whyour/qinglong"> <a href="https://github.com/whyour/qinglong">
<img width="150" src="https://z3.ax1x.com/2021/11/18/I7MpAe.png"> <img width="150" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png">
</a> </a>
</p> </p>
@@ -61,7 +61,6 @@ podman run -dit \
-p 5700:5700 \ -p 5700:5700 \
--name qinglong \ --name qinglong \
--hostname qinglong \ --hostname qinglong \
--restart unless-stopped \
docker.io/whyour/qinglong:latest docker.io/whyour/qinglong:latest
``` ```
@@ -183,7 +182,7 @@ task <file_path> desi <env_name> <account_number>
- [crontab-ui](https://github.com/alseambusher/crontab-ui) - [crontab-ui](https://github.com/alseambusher/crontab-ui)
- [Ant Design](https://ant.design) - [Ant Design](https://ant.design)
- [Ant Design Pro](https://pro.ant.design/) - [Ant Design Pro](https://pro.ant.design/)
- [Umijs3.0](https://umijs.org) - [Umijs](https://umijs.org)
- [darkreader](https://github.com/darkreader/darkreader) - [darkreader](https://github.com/darkreader/darkreader)
- [admin-server](https://github.com/sunpu007/admin-server) - [admin-server](https://github.com/sunpu007/admin-server)
+2 -3
View File
@@ -1,6 +1,6 @@
<p align="center"> <p align="center">
<a href="https://github.com/whyour/qinglong"> <a href="https://github.com/whyour/qinglong">
<img width="150" src="https://z3.ax1x.com/2021/11/18/I7MpAe.png"> <img width="150" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png">
</a> </a>
</p> </p>
@@ -61,7 +61,6 @@ podman run -dit \
-p 5700:5700 \ -p 5700:5700 \
--name qinglong \ --name qinglong \
--hostname qinglong \ --hostname qinglong \
--restart unless-stopped \
docker.io/whyour/qinglong:latest docker.io/whyour/qinglong:latest
``` ```
@@ -183,7 +182,7 @@ task <file_path> desi <env_name> <account_number>
- [crontab-ui](https://github.com/alseambusher/crontab-ui) - [crontab-ui](https://github.com/alseambusher/crontab-ui)
- [Ant Design](https://ant.design) - [Ant Design](https://ant.design)
- [Ant Design Pro](https://pro.ant.design/) - [Ant Design Pro](https://pro.ant.design/)
- [Umijs3.0](https://umijs.org) - [Umijs](https://umijs.org)
- [darkreader](https://github.com/darkreader/darkreader) - [darkreader](https://github.com/darkreader/darkreader)
- [admin-server](https://github.com/sunpu007/admin-server) - [admin-server](https://github.com/sunpu007/admin-server)
+49 -2
View File
@@ -3,8 +3,21 @@ import { Container } from 'typedi';
import EnvService from '../services/env'; import EnvService from '../services/env';
import { Logger } from 'winston'; import { Logger } from 'winston';
import { celebrate, Joi } from 'celebrate'; import { celebrate, Joi } from 'celebrate';
import multer from 'multer';
import config from '../config';
import fs from 'fs';
const route = Router(); const route = Router();
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, config.scriptPath);
},
filename: function (req, file, cb) {
cb(null, file.originalname);
},
});
const upload = multer({ storage: storage });
export default (app: Router) => { export default (app: Router) => {
app.use('/envs', route); app.use('/envs', route);
@@ -26,7 +39,9 @@ export default (app: Router) => {
body: Joi.array().items( body: Joi.array().items(
Joi.object({ Joi.object({
value: Joi.string().required(), value: Joi.string().required(),
name: Joi.string().required(), name: Joi.string()
.required()
.pattern(/^[a-zA-Z_][0-9a-zA-Z_]*$/),
remarks: Joi.string().optional().allow(''), remarks: Joi.string().optional().allow(''),
}), }),
), ),
@@ -94,7 +109,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 envService = Container.get(EnvService); const envService = Container.get(EnvService);
const data = await envService.move(req.params.id, req.body); const data = await envService.move(req.params.id, req.body);
@@ -177,4 +191,37 @@ export default (app: Router) => {
} }
}, },
); );
route.post(
'/upload',
upload.single('env'),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const envService = Container.get(EnvService);
const fileContent = await fs.promises.readFile(req!.file!.path, 'utf8');
const parseContent = JSON.parse(fileContent);
const data = Array.isArray(parseContent)
? parseContent
: [parseContent];
if (data.every((x) => x.name && x.value)) {
const result = await envService.create(
data.map((x) => ({
name: x.name,
value: x.value,
remarks: x.remarks,
})),
);
return res.send({ code: 200, data: result });
} else {
return res.send({
code: 400,
message: '文件缺少name或者value字段,参考导出文件格式',
});
}
} catch (e) {
return next(e);
}
},
);
}; };
+32 -1
View File
@@ -3,8 +3,9 @@ import { Container } from 'typedi';
import { Logger } from 'winston'; import { Logger } from 'winston';
import * as fs from 'fs'; import * as fs from 'fs';
import config from '../config'; import config from '../config';
import { getFileContentByName, readDirs } from '../config/util'; import { emptyDir, getFileContentByName, readDirs } from '../config/util';
import { join } from 'path'; import { join } from 'path';
import { celebrate, Joi } from 'celebrate';
const route = Router(); const route = Router();
const blacklist = ['.tmp']; const blacklist = ['.tmp'];
@@ -45,4 +46,34 @@ export default (app: Router) => {
} }
}, },
); );
route.delete(
'/',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
path: Joi.string().allow(''),
type: Joi.string().optional()
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
let { filename, path, type } = req.body as {
filename: string;
path: string;
type: string;
};
const filePath = join(config.logPath, path, filename);
if (type === 'directory') {
emptyDir(filePath);
} else {
fs.unlinkSync(filePath);
}
res.send({ code: 200 });
} catch (e) {
return next(e);
}
},
);
}; };
+27 -10
View File
@@ -4,6 +4,7 @@ import {
readDirs, readDirs,
getLastModifyFilePath, getLastModifyFilePath,
readDir, readDir,
emptyDir,
} from '../config/util'; } 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';
@@ -77,12 +78,14 @@ export default (app: Router) => {
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 {
let { filename, path, content, originFilename } = req.body as { let { filename, path, content, originFilename, directory } =
filename: string; req.body as {
path: string; filename: string;
content: string; path: string;
originFilename: string; content: string;
}; originFilename: string;
directory: string;
};
if (!path) { if (!path) {
path = config.scriptPath; path = config.scriptPath;
@@ -105,11 +108,19 @@ export default (app: Router) => {
return res.send({ code: 200 }); return res.send({ code: 200 });
} }
if (directory) {
fs.mkdirSync(join(path, directory), { recursive: true });
return res.send({ code: 200 });
}
if (!originFilename) { if (!originFilename) {
originFilename = filename; originFilename = filename;
} }
const originFilePath = `${path}${originFilename.replace(/\//g, '')}`; const originFilePath = join(
const filePath = `${path}${filename.replace(/\//g, '')}`; path,
`${originFilename.replace(/\//g, '')}`,
);
const filePath = join(path, `${filename.replace(/\//g, '')}`);
if (fs.existsSync(originFilePath)) { if (fs.existsSync(originFilePath)) {
fs.copyFileSync( fs.copyFileSync(
originFilePath, originFilePath,
@@ -159,17 +170,23 @@ export default (app: Router) => {
body: Joi.object({ body: Joi.object({
filename: Joi.string().required(), filename: Joi.string().required(),
path: Joi.string().allow(''), path: Joi.string().allow(''),
type: Joi.string().optional()
}), }),
}), }),
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 {
let { filename, path } = req.body as { let { filename, path, type } = req.body as {
filename: string; filename: string;
path: string; path: string;
type: string;
}; };
const filePath = join(config.scriptPath, path, filename); const filePath = join(config.scriptPath, path, filename);
fs.unlinkSync(filePath); if (type === 'directory') {
emptyDir(filePath);
} else {
fs.unlinkSync(filePath);
}
res.send({ code: 200 }); res.send({ code: 200 });
} catch (e) { } catch (e) {
return next(e); return next(e);
+14
View File
@@ -7,6 +7,8 @@ import SystemService from '../services/system';
import { celebrate, Joi } from 'celebrate'; import { celebrate, Joi } from 'celebrate';
import UserService from '../services/user'; import UserService from '../services/user';
import { EnvModel } from '../data/env'; import { EnvModel } from '../data/env';
import { promiseExec } from '../config/util';
const route = Router(); const route = Router();
export default (app: Router) => { export default (app: Router) => {
@@ -22,6 +24,15 @@ export default (app: Router) => {
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8'); const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
const version = currentVersionFile.match(versionRegx)![1]; const version = currentVersionFile.match(versionRegx)![1];
const lastCommitTime = (
await promiseExec('git show -s --format=%ai')
).replace('\n', '');
const lastCommitId = (
await promiseExec('git rev-parse --short HEAD')
).replace('\n', '');
const branch = (
await promiseExec('git symbolic-ref --short HEAD')
).replace('\n', '');
let isInitialized = true; let isInitialized = true;
if ( if (
@@ -37,6 +48,9 @@ export default (app: Router) => {
data: { data: {
isInitialized, isInitialized,
version, version,
lastCommitTime,
lastCommitId,
branch,
}, },
}); });
} catch (e) { } catch (e) {
+1 -1
View File
@@ -17,7 +17,7 @@ async function startServer() {
const server = app const server = app
.listen(config.port, () => { .listen(config.port, () => {
Logger.debug(`✌️ Back server launched on port ${config.port}`); Logger.debug(`✌️ 后端服务启动成功!`);
}) })
.on('error', (err) => { .on('error', (err) => {
Logger.error(err); Logger.error(err);
+1
View File
@@ -0,0 +1 @@
export const LOG_END_SYMBOL = '\n          ';
+2 -2
View File
@@ -11,10 +11,10 @@ if (!process.env.QL_DIR) {
if (qlHomePath.endsWith('/static/')) { if (qlHomePath.endsWith('/static/')) {
qlHomePath = path.join(qlHomePath, '../'); qlHomePath = path.join(qlHomePath, '../');
} }
process.env.QL_DIR = qlHomePath; process.env.QL_DIR = qlHomePath.replace(/\/$/g, '');
} }
const lastVersionFile = `http://qn.whyour.cn/version.ts?v=${Date.now()}`; const lastVersionFile = `https://qn.whyour.cn/version.ts`;
const rootPath = process.env.QL_DIR as string; const rootPath = process.env.QL_DIR as string;
const envFound = dotenv.config({ path: path.join(rootPath, '.env') }); const envFound = dotenv.config({ path: path.join(rootPath, '.env') });
+87 -1
View File
@@ -3,6 +3,7 @@ import * as path from 'path';
import got from 'got'; import got from 'got';
import iconv from 'iconv-lite'; import iconv from 'iconv-lite';
import { exec } from 'child_process'; import { exec } from 'child_process';
import FormData from 'form-data';
export function getFileContentByName(fileName: string) { export function getFileContentByName(fileName: string) {
if (fs.existsSync(fileName)) { if (fs.existsSync(fileName)) {
@@ -301,7 +302,6 @@ export function readDirs(
title: file, title: file,
key, key,
type: 'directory', type: 'directory',
disabled: true,
parent: relativePath, parent: relativePath,
children: readDirs(subPath, baseDir).sort( children: readDirs(subPath, baseDir).sort(
(a: any, b: any) => (a: any, b: any) =>
@@ -312,6 +312,7 @@ export function readDirs(
return { return {
title: file, title: file,
type: 'file', type: 'file',
isLeaf: true,
key, key,
parent: relativePath, parent: relativePath,
}; };
@@ -344,6 +345,20 @@ export function readDir(
return result; return result;
} }
export function emptyDir(path: string) {
const files = fs.readdirSync(path);
files.forEach((file) => {
const filePath = `${path}/${file}`;
const stats = fs.statSync(filePath);
if (stats.isDirectory()) {
emptyDir(filePath);
} else {
fs.unlinkSync(filePath);
}
});
fs.rmdirSync(path);
}
export function promiseExec(command: string): Promise<string> { export function promiseExec(command: string): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
exec( exec(
@@ -355,3 +370,74 @@ export function promiseExec(command: string): Promise<string> {
); );
}); });
} }
export function parseHeaders(headers: string) {
if (!headers) return {};
const parsed: any = {};
let key;
let val;
let i;
headers &&
headers.split('\n').forEach(function parser(line) {
i = line.indexOf(':');
key = line.substring(0, i).trim().toLowerCase();
val = line.substring(i + 1).trim();
if (!key) {
return;
}
parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val;
});
return parsed;
}
export function parseBody(
body: string,
contentType:
| 'application/json'
| 'multipart/form-data'
| 'application/x-www-form-urlencoded',
) {
if (!body) return '';
const parsed: any = {};
let key;
let val;
let i;
body &&
body.split('\n').forEach(function parser(line) {
i = line.indexOf(':');
key = line.substring(0, i).trim().toLowerCase();
val = line.substring(i + 1).trim();
if (!key || parsed[key]) {
return;
}
try {
const jsonValue = JSON.parse(val);
parsed[key] = jsonValue;
} catch (error) {
parsed[key] = val;
}
});
switch (contentType) {
case 'multipart/form-data':
return Object.keys(parsed).reduce((p, c) => {
p.append(c, parsed[c]);
return p;
}, new FormData());
case 'application/x-www-form-urlencoded':
return Object.keys(parsed).reduce((p, c) => {
return p ? `${p}&${c}=${parsed[c]}` : `${c}=${parsed[c]}`;
});
}
return parsed;
}
+4 -1
View File
@@ -26,7 +26,10 @@ export enum EnvStatus {
'disabled', 'disabled',
} }
export const initEnvPosition = 9999999999; export const maxPosition = 9000000000000000;
export const initPosition = 4500000000000000;
export const stepPosition = 10000000;
export const minPosition = 100;
interface EnvInstance extends Model<Env, Env>, Env {} interface EnvInstance extends Model<Env, Env>, Env {}
export const EnvModel = sequelize.define<EnvInstance>('Env', { export const EnvModel = sequelize.define<EnvInstance>('Env', {
+12 -5
View File
@@ -1,15 +1,22 @@
import { Sequelize } from 'sequelize'; import { Sequelize, Transaction } from 'sequelize';
import config from '../config/index'; import config from '../config/index';
export const sequelize = new Sequelize({ export const sequelize = new Sequelize({
dialect: 'sqlite', dialect: 'sqlite',
storage: `${config.dbPath}database.sqlite`, storage: `${config.dbPath}database.sqlite`,
logging: false, logging: false,
pool: { retry: {
max: 6, max: 10,
min: 0, match: ['SQLITE_BUSY: database is locked'],
idle: 30000,
}, },
pool: {
max: 5,
min: 2,
idle: 30000,
acquire: 30000,
evict: 10000,
},
transactionType: Transaction.TYPES.IMMEDIATE,
}); });
export type ResponseType<T> = { code: number; data?: T; message?: string }; export type ResponseType<T> = { code: number; data?: T; message?: string };
+31 -2
View File
@@ -1,3 +1,5 @@
import { IncomingHttpHeaders } from 'http';
export enum NotificationMode { export enum NotificationMode {
'gotify' = 'gotify', 'gotify' = 'gotify',
'goCqHttpBot' = 'goCqHttpBot', 'goCqHttpBot' = 'goCqHttpBot',
@@ -9,9 +11,12 @@ export enum NotificationMode {
'dingtalkBot' = 'dingtalkBot', 'dingtalkBot' = 'dingtalkBot',
'weWorkBot' = 'weWorkBot', 'weWorkBot' = 'weWorkBot',
'weWorkApp' = 'weWorkApp', 'weWorkApp' = 'weWorkApp',
'aibotk' = 'aibotk',
'iGot' = 'iGot', 'iGot' = 'iGot',
'pushPlus' = 'pushPlus', 'pushPlus' = 'pushPlus',
'email' = 'email', 'email' = 'email',
'feishu' = 'feishu',
'webhook' = 'webhook',
} }
abstract class NotificationBaseInfo { abstract class NotificationBaseInfo {
@@ -45,7 +50,7 @@ export class ChatNotification extends NotificationBaseInfo {
export class BarkNotification extends NotificationBaseInfo { export class BarkNotification extends NotificationBaseInfo {
public barkPush = ''; public barkPush = '';
public barkIcon = 'http://qn.whyour.cn/logo.png'; public barkIcon = 'https://qn.whyour.cn/logo.png';
public barkSound = ''; public barkSound = '';
public barkGroup = 'qinglong'; public barkGroup = 'qinglong';
} }
@@ -72,6 +77,12 @@ export class WeWorkAppNotification extends NotificationBaseInfo {
public weWorkAppKey = ''; public weWorkAppKey = '';
} }
export class AibotkNotification extends NotificationBaseInfo {
public aibotkKey: string = '';
public aibotkType: 'room' | 'contact' = 'room';
public aibotkName: string = '';
}
export class IGotNotification extends NotificationBaseInfo { export class IGotNotification extends NotificationBaseInfo {
public iGotPushKey = ''; public iGotPushKey = '';
} }
@@ -87,6 +98,21 @@ export class EmailNotification extends NotificationBaseInfo {
public emailPass: string = ''; public emailPass: string = '';
} }
export class WebhookNotification extends NotificationBaseInfo {
public webhookHeaders: string = '';
public webhookBody: string = '';
public webhookUrl: string = '';
public webhookMethod: 'GET' | 'POST' | 'PUT' = 'GET';
public webhookContentType:
| 'application/json'
| 'multipart/form-data'
| 'application/x-www-form-urlencoded' = 'application/json';
}
export class LarkNotification extends NotificationBaseInfo {
public larkKey = '';
}
export interface NotificationInfo export interface NotificationInfo
extends GoCqHttpBotNotification, extends GoCqHttpBotNotification,
GotifyNotification, GotifyNotification,
@@ -98,6 +124,9 @@ export interface NotificationInfo
DingtalkBotNotification, DingtalkBotNotification,
WeWorkBotNotification, WeWorkBotNotification,
WeWorkAppNotification, WeWorkAppNotification,
AibotkNotification,
IGotNotification, IGotNotification,
PushPlusNotification, PushPlusNotification,
EmailNotification {} EmailNotification,
WebhookNotification,
LarkNotification {}
+2 -2
View File
@@ -4,7 +4,7 @@ import { Container } from 'typedi';
import { Crontab, CrontabModel, CrontabStatus } from '../data/cron'; import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
import CronService from '../services/cron'; import CronService from '../services/cron';
import EnvService from '../services/env'; import EnvService from '../services/env';
import _ from 'lodash'; import groupBy from 'lodash/groupBy';
import { DependenceModel } from '../data/dependence'; import { DependenceModel } from '../data/dependence';
import { Op } from 'sequelize'; import { Op } from 'sequelize';
import config from '../config'; import config from '../config';
@@ -26,7 +26,7 @@ export default async () => {
order: [['type', 'DESC']], order: [['type', 'DESC']],
raw: true, raw: true,
}).then(async (docs) => { }).then(async (docs) => {
const groups = _.groupBy(docs, 'type'); const groups = groupBy(docs, 'type');
const keys = Object.keys(groups).sort((a, b) => parseInt(b) - parseInt(a)); const keys = Object.keys(groups).sort((a, b) => parseInt(b) - parseInt(a));
for (const key of keys) { for (const key of keys) {
const group = groups[key]; const group = groups[key];
+3 -6
View File
@@ -1,5 +1,4 @@
import { Container } from 'typedi'; import { Container } from 'typedi';
import _ from 'lodash';
import SystemService from '../services/system'; import SystemService from '../services/system';
import ScheduleService from '../services/schedule'; import ScheduleService from '../services/schedule';
import SubscriptionService from '../services/subscription'; import SubscriptionService from '../services/subscription';
@@ -13,18 +12,17 @@ export default async () => {
// 生成内置token // 生成内置token
let tokenCommand = `ts-node-transpile-only ${config.rootPath}/back/token.ts`; let tokenCommand = `ts-node-transpile-only ${config.rootPath}/back/token.ts`;
const tokenFile = `${config.rootPath}/static/build/token.js`; const tokenFile = `${config.rootPath}static/build/token.js`;
if (await fileExist(tokenFile)) { if (await fileExist(tokenFile)) {
tokenCommand = `node ${tokenFile}`; tokenCommand = `node ${tokenFile}`;
} }
const cron = { const cron = {
id: 'token', id: NaN,
name: '生成token', name: '生成token',
command: tokenCommand, command: tokenCommand,
}; };
scheduleService.createIntervalTask(cron as any, { scheduleService.createIntervalTask(cron, {
days: 28, days: 28,
runImmediately: true,
}); });
// 运行删除日志任务 // 运行删除日志任务
@@ -37,7 +35,6 @@ export default async () => {
}; };
scheduleService.createIntervalTask(cron, { scheduleService.createIntervalTask(cron, {
days: data.info.frequency, days: data.info.frequency,
runImmediately: true,
}); });
} }
+9 -2
View File
@@ -2,16 +2,23 @@ import { Application } from 'express';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import * as Tracing from '@sentry/tracing'; import * as Tracing from '@sentry/tracing';
import Logger from './logger'; import Logger from './logger';
import config from '../config';
import fs from 'fs';
export default ({ expressApp }: { expressApp: Application }) => { export default ({ expressApp }: { expressApp: Application }) => {
const versionRegx = /.*export const version = \'(.*)\'\;/;
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
const currentVersion = currentVersionFile.match(versionRegx)![1];
Sentry.init({ Sentry.init({
dsn: 'https://f4b5b55fb3c645b29a5dc2d70a1a4ef4@o1098464.ingest.sentry.io/6122819', dsn: 'https://f4b5b55fb3c645b29a5dc2d70a1a4ef4@o1098464.ingest.sentry.io/6122819',
integrations: [ integrations: [
new Sentry.Integrations.Http({ tracing: true }), new Sentry.Integrations.Http({ tracing: true }),
new Tracing.Integrations.Express({ app: expressApp }), new Tracing.Integrations.Express({ app: expressApp }),
], ],
tracesSampleRate: 0.1,
tracesSampleRate: 1.0, release: currentVersion,
}); });
expressApp.use(Sentry.Handlers.requestHandler()); expressApp.use(Sentry.Handlers.requestHandler());
+1 -1
View File
@@ -22,7 +22,7 @@ app
await require('./loaders/sentry').default({ expressApp: app }); await require('./loaders/sentry').default({ expressApp: app });
await require('./loaders/db').default(); await require('./loaders/db').default();
Logger.debug(`✌️ Back server launched on port ${config.publicPort}`); Logger.debug(`✌️ 公共服务启动成功!`);
}) })
.on('error', (err) => { .on('error', (err) => {
Logger.error(err); Logger.error(err);
+4 -7
View File
@@ -18,14 +18,15 @@ const run = async () => {
_schedule && _schedule &&
_schedule.length > 5 && _schedule.length > 5 &&
task.status !== CrontabStatus.disabled && task.status !== CrontabStatus.disabled &&
!task.isDisabled !task.isDisabled &&
task.schedule
) { ) {
schedule.scheduleJob(task.schedule, function () { schedule.scheduleJob(task.schedule, function () {
let command = task.command as string; let command = task.command as string;
if (!command.includes('task ') && !command.includes('ql ')) { if (!command.includes('task ') && !command.includes('ql ')) {
command = `task ${command}`; command = `task ${command}`;
} }
exec(command); exec(`ID=${task.id} ${command}`);
}); });
} }
} }
@@ -43,11 +44,7 @@ app
await require('./loaders/db').default(); await require('./loaders/db').default();
await run(); await run();
Logger.info(` Logger.debug('定时任务服务启动成功!');
################################################
🛡️ Schedule listening on port: ${config.cronPort} 🛡️
################################################
`);
}) })
.on('error', (err) => { .on('error', (err) => {
Logger.error(err); Logger.error(err);
+4 -3
View File
@@ -11,6 +11,7 @@ import { promisify } from 'util';
import { Op } from 'sequelize'; import { Op } from 'sequelize';
import path from 'path'; import path from 'path';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { LOG_END_SYMBOL } from '../config/const';
@Service() @Service()
export default class CronService { export default class CronService {
@@ -335,7 +336,7 @@ export default class CronService {
`${absolutePath}`, `${absolutePath}`,
`${str}\n## 执行结束... ${endTime.format( `${str}\n## 执行结束... ${endTime.format(
'YYYY-MM-DD HH:mm:ss', 'YYYY-MM-DD HH:mm:ss',
)}${diffTimeStr}`, )}${diffTimeStr}${LOG_END_SYMBOL}`,
); );
} }
} }
@@ -412,7 +413,7 @@ export default class CronService {
cmdStr = `${cmdStr} now`; cmdStr = `${cmdStr} now`;
} }
const cp = spawn(cmdStr, { shell: '/bin/bash' }); const cp = spawn(`ID=${id} ${cmdStr}`, { shell: '/bin/bash' });
await CrontabModel.update( await CrontabModel.update(
{ status: CrontabStatus.running, pid: cp.pid }, { status: CrontabStatus.running, pid: cp.pid },
@@ -420,7 +421,7 @@ export default class CronService {
); );
cp.stderr.on('data', (data) => { cp.stderr.on('data', (data) => {
if (logFileExist) { if (logFileExist) {
fs.appendFileSync(`${absolutePath}`, `${data}`); fs.appendFileSync(`${absolutePath}`, `${data.toString()}`);
} }
}); });
cp.on('error', (err) => { cp.on('error', (err) => {
+6 -6
View File
@@ -1,14 +1,14 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import winston from 'winston'; import winston from 'winston';
import { CrontabView, CrontabViewModel } from '../data/cronView'; import { CrontabView, CrontabViewModel } from '../data/cronView';
import { initEnvPosition } from '../data/env'; import { initPosition } from '../data/env';
@Service() @Service()
export default class CronViewService { export default class CronViewService {
constructor(@Inject('logger') private logger: winston.Logger) {} constructor(@Inject('logger') private logger: winston.Logger) {}
public async create(payload: CrontabView): Promise<CrontabView> { public async create(payload: CrontabView): Promise<CrontabView> {
let position = initEnvPosition; let position = initPosition;
const views = await this.list(); const views = await this.list();
if (views && views.length > 0 && views[views.length - 1].position) { if (views && views.length > 0 && views[views.length - 1].position) {
position = views[views.length - 1].position as number; position = views[views.length - 1].position as number;
@@ -76,12 +76,12 @@ export default class CronViewService {
const views = await this.list(); const views = await this.list();
if (toIndex === 0 || toIndex === views.length - 1) { if (toIndex === 0 || toIndex === views.length - 1) {
targetPosition = isUpward targetPosition = isUpward
? views[0].position * 2 ? views[0].position! * 2
: views[toIndex].position / 2; : views[toIndex].position! / 2;
} else { } else {
targetPosition = isUpward targetPosition = isUpward
? (views[toIndex].position + views[toIndex - 1].position) / 2 ? (views[toIndex].position! + views[toIndex - 1].position!) / 2
: (views[toIndex].position + views[toIndex + 1].position) / 2; : (views[toIndex].position! + views[toIndex + 1].position!) / 2;
} }
const newDoc = await this.update({ const newDoc = await this.update({
id, id,
+5 -3
View File
@@ -9,7 +9,6 @@ import {
unInstallDependenceCommandTypes, unInstallDependenceCommandTypes,
DependenceModel, DependenceModel,
} from '../data/dependence'; } from '../data/dependence';
import _ from 'lodash';
import { spawn } from 'child_process'; import { spawn } from 'child_process';
import SockService from './sock'; import SockService from './sock';
import { Op } from 'sequelize'; import { Op } from 'sequelize';
@@ -125,8 +124,11 @@ export default class DependenceService {
return docs; return docs;
} }
private async find(query: any, sort?: any): Promise<Dependence[]> { private async find(query: any, sort: any = []): Promise<Dependence[]> {
const docs = await DependenceModel.findAll({ where: { ...query } }); const docs = await DependenceModel.findAll({
where: { ...query },
order: [...sort, ['createdAt', 'DESC']],
});
return docs; return docs;
} }
+57 -28
View File
@@ -1,10 +1,17 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import winston from 'winston'; import winston from 'winston';
import { getFileContentByName } from '../config/util';
import config from '../config'; import config from '../config';
import * as fs from 'fs'; import * as fs from 'fs';
import { Env, EnvModel, EnvStatus, initEnvPosition } from '../data/env'; import {
import _ from 'lodash'; Env,
EnvModel,
EnvStatus,
initPosition,
maxPosition,
minPosition,
stepPosition,
} from '../data/env';
import groupBy from 'lodash/groupBy';
import { Op } from 'sequelize'; import { Op } from 'sequelize';
@Service() @Service()
@@ -13,17 +20,22 @@ export default class EnvService {
public async create(payloads: Env[]): Promise<Env[]> { public async create(payloads: Env[]): Promise<Env[]> {
const envs = await this.envs(); const envs = await this.envs();
let position = initEnvPosition; let position = initPosition;
if (envs && envs.length > 0 && envs[envs.length - 1].position) { if (
position = envs[envs.length - 1].position as number; envs &&
envs.length > 0 &&
typeof envs[envs.length - 1].position === 'number'
) {
position = envs[envs.length - 1].position!;
} }
const tabs = payloads.map((x) => { const tabs = payloads.map((x) => {
position = position / 2; position = position - stepPosition;
const tab = new Env({ ...x, position }); const tab = new Env({ ...x, position });
return tab; return tab;
}); });
const docs = await this.insert(tabs); const docs = await this.insert(tabs);
await this.set_envs(); await this.set_envs();
await this.checkPosition(tabs[tabs.length - 1].position!);
return docs; return docs;
} }
@@ -67,25 +79,40 @@ export default class EnvService {
const envs = await this.envs(); const envs = await this.envs();
if (toIndex === 0 || toIndex === envs.length - 1) { if (toIndex === 0 || toIndex === envs.length - 1) {
targetPosition = isUpward targetPosition = isUpward
? envs[0].position * 2 ? envs[0].position! + stepPosition
: envs[toIndex].position / 2; : envs[toIndex].position! - stepPosition;
} else { } else {
targetPosition = isUpward targetPosition = isUpward
? (envs[toIndex].position + envs[toIndex - 1].position) / 2 ? (envs[toIndex].position! + envs[toIndex - 1].position!) / 2
: (envs[toIndex].position + envs[toIndex + 1].position) / 2; : (envs[toIndex].position! + envs[toIndex + 1].position!) / 2;
} }
const newDoc = await this.update({ const newDoc = await this.update({
id, id,
position: targetPosition, position: this.getPrecisionPosition(targetPosition),
}); });
await this.checkPosition(targetPosition);
return newDoc; return newDoc;
} }
public async envs( private async checkPosition(position: number) {
searchText: string = '', const precisionPosition = parseFloat(position.toPrecision(16));
sort: any = { position: -1 }, if (precisionPosition < minPosition || precisionPosition > maxPosition) {
query: any = {}, const envs = await this.envs();
): Promise<Env[]> { let position = initPosition;
for (const env of envs) {
position = position - stepPosition;
await this.updateDb({ ...env, position });
}
}
}
private getPrecisionPosition(position: number): number {
return parseFloat(position.toPrecision(16));
}
public async envs(searchText: string = '', query: any = {}): Promise<Env[]> {
let condition = { ...query }; let condition = { ...query };
if (searchText) { if (searchText) {
const encodeText = encodeURIComponent(searchText); const encodeText = encodeURIComponent(searchText);
@@ -112,7 +139,11 @@ export default class EnvService {
}; };
} }
try { try {
const result = await this.find(condition, [['position', 'DESC']]); const result = await this.find(condition, [
['status', 'ASC'],
['position', 'DESC'],
['createdAt', 'ASC'],
]);
return result as any; return result as any;
} catch (error) { } catch (error) {
throw error; throw error;
@@ -151,22 +182,20 @@ export default class EnvService {
} }
public async set_envs() { public async set_envs() {
const envs = await this.envs( const envs = await this.envs('', {
'', name: { [Op.not]: null },
{ position: -1 }, status: EnvStatus.normal,
{ name: { [Op.not]: null } }, });
); const groups = groupBy(envs, 'name');
const groups = _.groupBy(envs, 'name');
let env_string = ''; let env_string = '';
for (const key in groups) { for (const key in groups) {
if (Object.prototype.hasOwnProperty.call(groups, key)) { if (Object.prototype.hasOwnProperty.call(groups, key)) {
const group = groups[key]; const group = groups[key];
// 忽略不符合bash要求的环境变量名称 // 忽略不符合bash要求的环境变量名称
if (/^[a-zA-Z_][0-9a-zA-Z_]+$/.test(key)) { if (/^[a-zA-Z_][0-9a-zA-Z_]*$/.test(key)) {
let value = _(group) let value = group
.filter((x) => x.status !== EnvStatus.disabled) .map((x) => x.value)
.map('value')
.join('&') .join('&')
.replace(/(\\)[^n]/g, '\\\\') .replace(/(\\)[^n]/g, '\\\\')
.replace(/(\\$)/, '\\\\') .replace(/(\\$)/, '\\\\')
+135 -27
View File
@@ -6,6 +6,7 @@ import got from 'got';
import nodemailer from 'nodemailer'; import nodemailer from 'nodemailer';
import crypto from 'crypto'; import crypto from 'crypto';
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
import { parseBody, parseHeaders } from '../config/util';
@Service() @Service()
export default class NotificationService { export default class NotificationService {
@@ -23,15 +24,21 @@ export default class NotificationService {
['dingtalkBot', this.dingtalkBot], ['dingtalkBot', this.dingtalkBot],
['weWorkBot', this.weWorkBot], ['weWorkBot', this.weWorkBot],
['weWorkApp', this.weWorkApp], ['weWorkApp', this.weWorkApp],
['aibotk', this.aibotk],
['iGot', this.iGot], ['iGot', this.iGot],
['pushPlus', this.pushPlus], ['pushPlus', this.pushPlus],
['email', this.email], ['email', this.email],
['webhook', this.webhook],
['lark', this.lark],
]); ]);
private timeout = 10000;
private title = ''; private title = '';
private content = ''; private content = '';
private params!: Omit<NotificationInfo, 'type'>; private params!: Omit<NotificationInfo, 'type'>;
private gotOption = {
timeout: 30000,
retry: 1,
};
constructor(@Inject('logger') private logger: winston.Logger) {} constructor(@Inject('logger') private logger: winston.Logger) {}
@@ -74,8 +81,7 @@ export default class NotificationService {
const { gotifyUrl, gotifyToken, gotifyPriority } = this.params; const { gotifyUrl, gotifyToken, gotifyPriority } = this.params;
const res: any = await got const res: any = await got
.post(`${gotifyUrl}/message?token=${gotifyToken}`, { .post(`${gotifyUrl}/message?token=${gotifyToken}`, {
timeout: this.timeout, ...this.gotOption,
retry: 0,
body: `title=${encodeURIComponent( body: `title=${encodeURIComponent(
this.title, this.title,
)}&message=${encodeURIComponent( )}&message=${encodeURIComponent(
@@ -93,8 +99,7 @@ export default class NotificationService {
const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params; const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params;
const res: any = await got const res: any = await got
.post(`${goCqHttpBotUrl}?${goCqHttpBotQq}`, { .post(`${goCqHttpBotUrl}?${goCqHttpBotQq}`, {
timeout: this.timeout, ...this.gotOption,
retry: 0,
json: { message: `${this.title}\n${this.content}` }, json: { message: `${this.title}\n${this.content}` },
headers: { Authorization: 'Bearer ' + goCqHttpBotToken }, headers: { Authorization: 'Bearer ' + goCqHttpBotToken },
}) })
@@ -109,8 +114,7 @@ export default class NotificationService {
: `https://sc.ftqq.com/${serverChanKey}.send`; : `https://sc.ftqq.com/${serverChanKey}.send`;
const res: any = await got const res: any = await got
.post(url, { .post(url, {
timeout: this.timeout, ...this.gotOption,
retry: 0,
body: `title=${this.title}&desp=${this.content}`, body: `title=${this.title}&desp=${this.content}`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
}) })
@@ -123,8 +127,7 @@ export default class NotificationService {
const url = `https://api2.pushdeer.com/message/push`; const url = `https://api2.pushdeer.com/message/push`;
const res: any = await got const res: any = await got
.post(url, { .post(url, {
timeout: this.timeout, ...this.gotOption,
retry: 0,
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`,
@@ -141,8 +144,7 @@ export default class NotificationService {
const url = `${chatUrl}${chatToken}`; const url = `${chatUrl}${chatToken}`;
const res: any = await got const res: any = await got
.post(url, { .post(url, {
timeout: this.timeout, ...this.gotOption,
retry: 0,
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' },
}) })
@@ -162,8 +164,7 @@ export default class NotificationService {
)}?icon=${barkIcon}?sound=${barkSound}&group=${barkGroup}`; )}?icon=${barkIcon}?sound=${barkSound}&group=${barkGroup}`;
const res: any = await got const res: any = await got
.get(url, { .get(url, {
timeout: this.timeout, ...this.gotOption,
retry: 0,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
}) })
.json(); .json();
@@ -201,8 +202,7 @@ export default class NotificationService {
} }
const res: any = await got const res: any = await got
.post(url, { .post(url, {
timeout: this.timeout, ...this.gotOption,
retry: 0,
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' },
agent, agent,
@@ -224,8 +224,7 @@ 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}`;
const res: any = await got const res: any = await got
.post(url, { .post(url, {
timeout: this.timeout, ...this.gotOption,
retry: 0,
json: { json: {
msgtype: 'text', msgtype: 'text',
text: { text: {
@@ -242,8 +241,7 @@ export default class NotificationService {
const url = `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${weWorkBotKey}`; const url = `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${weWorkBotKey}`;
const res: any = await got const res: any = await got
.post(url, { .post(url, {
timeout: this.timeout, ...this.gotOption,
retry: 0,
json: { json: {
msgtype: 'text', msgtype: 'text',
text: { text: {
@@ -262,8 +260,7 @@ export default class NotificationService {
const url = `https://qyapi.weixin.qq.com/cgi-bin/gettoken`; const url = `https://qyapi.weixin.qq.com/cgi-bin/gettoken`;
const tokenRes: any = await got const tokenRes: any = await got
.post(url, { .post(url, {
timeout: this.timeout, ...this.gotOption,
retry: 0,
json: { json: {
corpid, corpid,
corpsecret, corpsecret,
@@ -313,8 +310,7 @@ export default class NotificationService {
.post( .post(
`https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${tokenRes.access_token}`, `https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${tokenRes.access_token}`,
{ {
timeout: this.timeout, ...this.gotOption,
retry: 0,
json: { json: {
touser, touser,
agentid, agentid,
@@ -328,13 +324,53 @@ export default class NotificationService {
return res.errcode === 0; return res.errcode === 0;
} }
private async aibotk() {
const { aibotkKey, aibotkType, aibotkName } = this.params;
let url = '';
let json = {};
switch (aibotkType) {
case 'room':
url = 'https://api-bot.aibotk.com/openapi/v1/chat/room';
json = {
apiKey: `${aibotkKey}`,
roomName: `${aibotkName}`,
message: {
type: 1,
content: `【青龙快讯】\n\n${this.title}\n${this.content}`,
},
};
break;
case 'contact':
url = 'https://api-bot.aibotk.com/openapi/v1/chat/contact';
json = {
apiKey: `${aibotkKey}`,
name: `${aibotkName}`,
message: {
type: 1,
content: `【青龙快讯】\n\n${this.title}\n${this.content}`,
},
};
break;
}
const res: any = await got
.post(url, {
...this.gotOption,
json: {
...json,
},
})
.json();
return res.code === 0;
}
private async iGot() { private async iGot() {
const { iGotPushKey } = this.params; const { iGotPushKey } = this.params;
const url = `https://push.hellyw.com/${iGotPushKey.toLowerCase()}`; const url = `https://push.hellyw.com/${iGotPushKey.toLowerCase()}`;
const res: any = await got const res: any = await got
.post(url, { .post(url, {
timeout: this.timeout, ...this.gotOption,
retry: 0,
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' },
}) })
@@ -348,8 +384,7 @@ export default class NotificationService {
const url = `https://www.pushplus.plus/send`; const url = `https://www.pushplus.plus/send`;
const res: any = await got const res: any = await got
.post(url, { .post(url, {
timeout: this.timeout, ...this.gotOption,
retry: 0,
json: { json: {
token: `${pushPlusToken}`, token: `${pushPlusToken}`,
title: `${this.title}`, title: `${this.title}`,
@@ -362,6 +397,21 @@ export default class NotificationService {
return res.code === 200; return res.code === 200;
} }
private async lark() {
const { larkKey } = this.params;
const res: any = await got
.post(`https://open.feishu.cn/open-apis/bot/v2/hook/${larkKey}`, {
...this.gotOption,
json: {
msg_type: 'text',
content: { text: `${this.title}\n\n${this.content}` },
},
headers: { 'Content-Type': 'application/json' },
})
.json();
return res.StatusCode === 0;
}
private async email() { private async email() {
const { emailPass, emailService, emailUser } = this.params; const { emailPass, emailService, emailUser } = this.params;
const transporter = nodemailer.createTransport({ const transporter = nodemailer.createTransport({
@@ -383,4 +433,62 @@ export default class NotificationService {
return !!info.messageId; return !!info.messageId;
} }
private async webhook() {
const {
webhookUrl,
webhookBody,
webhookHeaders,
webhookMethod,
webhookContentType,
} = this.params;
const { formatBody, formatUrl } = this.formatNotifyContent(
webhookUrl,
webhookBody,
);
if (!formatUrl && !formatBody) {
return false;
}
const headers = parseHeaders(webhookHeaders);
const body = parseBody(formatBody, webhookContentType);
const bodyParam = this.formatBody(webhookContentType, body);
const options = {
method: webhookMethod,
headers,
...this.gotOption,
allowGetBody: true,
...bodyParam,
};
const res = await got(formatUrl, options);
return String(res.statusCode).startsWith('20');
}
private formatBody(contentType: string, body: any): object {
if (!body) return {};
switch (contentType) {
case 'application/json':
return { json: body };
case 'multipart/form-data':
return { form: body };
case 'application/x-www-form-urlencoded':
return { body };
}
return {};
}
private formatNotifyContent(url: string, body: string) {
if (!url.includes('$title') && !body.includes('$title')) {
return {};
}
return {
formatUrl: url
.replaceAll('$title', encodeURIComponent(this.title))
.replaceAll('$content', encodeURIComponent(this.content)),
formatBody: body
.replaceAll('$title', this.title)
.replaceAll('$content', this.content),
};
}
} }
+2 -1
View File
@@ -6,6 +6,7 @@ import SockService from './sock';
import CronService from './cron'; import CronService from './cron';
import ScheduleService, { TaskCallbacks } from './schedule'; import ScheduleService, { TaskCallbacks } from './schedule';
import config from '../config'; import config from '../config';
import { LOG_END_SYMBOL } from '../config/const';
@Service() @Service()
export default class ScriptService { export default class ScriptService {
@@ -55,7 +56,7 @@ export default class ScriptService {
type: 'manuallyRunScript', type: 'manuallyRunScript',
message: `${str}\n## 执行结束... ${new Date() message: `${str}\n## 执行结束... ${new Date()
.toLocaleString('zh', { hour12: false }) .toLocaleString('zh', { hour12: false })
.replace(' 24:', ' 00:')} `, .replace(' 24:', ' 00:')}${LOG_END_SYMBOL}`,
}); });
return { code: 200 }; return { code: 200 };
+7 -3
View File
@@ -29,6 +29,7 @@ import { SimpleIntervalSchedule } from 'toad-scheduler';
import SockService from './sock'; import SockService from './sock';
import SshKeyService from './sshKey'; import SshKeyService from './sshKey';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { LOG_END_SYMBOL } from '../config/const';
@Service() @Service()
export default class SubscriptionService { export default class SubscriptionService {
@@ -62,7 +63,10 @@ export default class SubscriptionService {
try { try {
const result = await SubscriptionModel.findAll({ const result = await SubscriptionModel.findAll({
where: query, where: query,
order: [['createdAt', 'DESC']], order: [
['is_disabled', 'ASC'],
['createdAt', 'DESC'],
],
}); });
return result as any; return result as any;
} catch (error) { } catch (error) {
@@ -229,7 +233,7 @@ export default class SubscriptionService {
absolutePath, absolutePath,
`\n## 执行结束... ${endTime.format( `\n## 执行结束... ${endTime.format(
'YYYY-MM-DD HH:mm:ss', 'YYYY-MM-DD HH:mm:ss',
)} 耗时 ${diff}`, )} 耗时 ${diff}${LOG_END_SYMBOL}`,
); );
await SubscriptionModel.update( await SubscriptionModel.update(
@@ -350,7 +354,7 @@ export default class SubscriptionService {
fs.appendFileSync( fs.appendFileSync(
`${absolutePath}`, `${absolutePath}`,
`${str}\n## 执行结束... ${dayjs().format('YYYY-MM-DD HH:mm:ss')} `, `${str}\n## 执行结束... ${dayjs().format('YYYY-MM-DD HH:mm:ss')}${LOG_END_SYMBOL}`,
); );
} }
+5 -12
View File
@@ -2,7 +2,6 @@ import { Service, Inject } from 'typedi';
import winston from 'winston'; import winston from 'winston';
import config from '../config'; import config from '../config';
import * as fs from 'fs'; import * as fs from 'fs';
import _ from 'lodash';
import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth'; import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth';
import { NotificationInfo } from '../data/notify'; import { NotificationInfo } from '../data/notify';
import NotificationService from './notify'; import NotificationService from './notify';
@@ -10,6 +9,7 @@ import ScheduleService from './schedule';
import { spawn } from 'child_process'; import { spawn } from 'child_process';
import SockService from './sock'; import SockService from './sock';
import got from 'got'; import got from 'got';
import { promiseExec } from '../config/util';
@Service() @Service()
export default class SystemService { export default class SystemService {
@@ -70,9 +70,8 @@ export default class SystemService {
}; };
await this.scheduleService.cancelIntervalTask(cron); await this.scheduleService.cancelIntervalTask(cron);
if (frequency > 0) { if (frequency > 0) {
await this.scheduleService.createIntervalTask(cron, { this.scheduleService.createIntervalTask(cron, {
days: frequency, days: frequency,
runImmediately: true,
}); });
} }
return { code: 200, data: { ...cron } }; return { code: 200, data: { ...cron } };
@@ -89,11 +88,9 @@ export default class SystemService {
let lastVersion = ''; let lastVersion = '';
let lastLog = ''; let lastLog = '';
try { try {
const result = await got.get(config.lastVersionFile, { const lastVersionFileContent = await promiseExec(
timeout: 6000, `curl ${config.lastVersionFile}?t=${Date.now()}`,
retry: 0, );
});
const lastVersionFileContent = result.body;
lastVersion = lastVersionFileContent.match(versionRegx)![1]; lastVersion = lastVersionFileContent.match(versionRegx)![1];
lastLog = lastVersionFileContent.match(logRegx) lastLog = lastVersionFileContent.match(logRegx)
? lastVersionFileContent.match(logRegx)![1] ? lastVersionFileContent.match(logRegx)![1]
@@ -138,10 +135,6 @@ export default class SystemService {
public async updateSystem() { public async updateSystem() {
const cp = spawn('ql -l update', { shell: '/bin/bash' }); const cp = spawn('ql -l update', { shell: '/bin/bash' });
this.sockService.sendMessage({
type: 'updateSystemVersion',
message: `开始更新系统`,
});
cp.stdout.on('data', (data) => { cp.stdout.on('data', (data) => {
this.sockService.sendMessage({ this.sockService.sendMessage({
type: 'updateSystemVersion', type: 'updateSystemVersion',
+33 -20
View File
@@ -3,7 +3,6 @@ import winston from 'winston';
import { createRandomString, getNetIp, getPlatform } from '../config/util'; import { createRandomString, getNetIp, getPlatform } from '../config/util';
import config from '../config'; import config from '../config';
import * as fs from 'fs'; import * as fs from 'fs';
import _ from 'lodash';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { authenticator } from '@otplib/preset-default'; import { authenticator } from '@otplib/preset-default';
import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth'; import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth';
@@ -65,32 +64,37 @@ export default class UserService {
return this.initAuthInfo(); return this.initAuthInfo();
} }
if (retries > 2 && Date.now() - lastlogon < Math.pow(3, retries) * 1000) { const retriesTime = Math.pow(3, retries) * 1000;
if (retries > 2 && timestamp - lastlogon < retriesTime) {
const waitTime = Math.ceil(
(retriesTime - (timestamp - lastlogon)) / 1000,
);
return { return {
code: 410, code: 410,
message: `失败次数过多,请${Math.round( message: `失败次数过多,请${waitTime}秒后重试`,
(Math.pow(3, retries) * 1000 - Date.now() + lastlogon) / 1000, data: waitTime,
)}秒后重试`, };
data: Math.round( }
(Math.pow(3, retries) * 1000 - Date.now() + lastlogon) / 1000,
), if (
username === cUsername &&
password === cPassword &&
twoFactorActivated &&
needTwoFactor
) {
this.updateAuthInfo(content, {
isTwoFactorChecking: true,
});
return {
code: 420,
message: '',
}; };
} }
const { ip, address } = await getNetIp(req); const { ip, address } = await getNetIp(req);
if (username === cUsername && password === cPassword) { if (username === cUsername && password === cPassword) {
if (twoFactorActivated && needTwoFactor) {
this.updateAuthInfo(content, {
isTwoFactorChecking: true,
});
return {
code: 420,
message: '请输入两步验证token',
};
}
const data = createRandomString(50, 100); const data = createRandomString(50, 100);
const expiration = twoFactorActivated ? 30 : 3; const expiration = twoFactorActivated ? 60 : 20;
let token = jwt.sign({ data }, config.secret as any, { let token = jwt.sign({ data }, config.secret as any, {
expiresIn: 60 * 60 * 24 * expiration, expiresIn: 60 * 60 * 24 * expiration,
algorithm: 'HS384', algorithm: 'HS384',
@@ -155,7 +159,16 @@ export default class UserService {
status: LoginStatus.fail, status: LoginStatus.fail,
}, },
}); });
return { code: 400, message: config.authError }; if (retries > 2) {
const waitTime = Math.round(Math.pow(3, retries + 1));
return {
code: 410,
message: `失败次数过多,请${waitTime}秒后重试`,
data: waitTime,
};
} else {
return { code: 400, message: config.authError };
}
} }
} else { } else {
return this.initAuthInfo(); return this.initAuthInfo();
+5 -8
View File
@@ -4,6 +4,8 @@ dir_shell=/ql/shell
. $dir_shell/share.sh . $dir_shell/share.sh
link_shell link_shell
export isFirstStartServer=true
echo -e "======================1. 检测配置文件========================\n" echo -e "======================1. 检测配置文件========================\n"
make_dir /etc/nginx/conf.d make_dir /etc/nginx/conf.d
make_dir /run/nginx make_dir /run/nginx
@@ -12,12 +14,7 @@ cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
sed -i "s,QL_BASE_URL,${qlBaseUrl},g" /etc/nginx/conf.d/front.conf sed -i "s,QL_BASE_URL,${qlBaseUrl},g" /etc/nginx/conf.d/front.conf
pm2 l &>/dev/null pm2 l &>/dev/null
if [[ $PipMirror ]]; then patch_version &>/dev/null
pip3 config set global.index-url $PipMirror
fi
if [[ $NpmMirror ]]; then
npm config set registry $NpmMirror
fi
echo echo
echo -e "======================2. 安装依赖========================\n" echo -e "======================2. 安装依赖========================\n"
@@ -45,13 +42,13 @@ echo -e "定时任务启动成功...\n"
if [[ $AutoStartBot == true ]]; then if [[ $AutoStartBot == true ]]; then
echo -e "======================7. 启动bot========================\n" echo -e "======================7. 启动bot========================\n"
nohup ql bot >>$dir_log/start.log 2>&1 & nohup ql bot >$dir_log/bot.log 2>&1 &
echo -e "bot后台启动中...\n" echo -e "bot后台启动中...\n"
fi fi
if [[ $EnableExtraShell == true ]]; then if [[ $EnableExtraShell == true ]]; then
echo -e "======================8. 执行自定义脚本========================\n" echo -e "======================8. 执行自定义脚本========================\n"
nohup ql extra >>$dir_log/start.log 2>&1 & nohup ql extra >$dir_log/extra.log 2>&1 &
echo -e "自定义脚本后台执行中...\n" echo -e "自定义脚本后台执行中...\n"
fi fi
+34 -17
View File
@@ -2,16 +2,16 @@
"private": true, "private": true,
"scripts": { "scripts": {
"start": "concurrently -n w: npm:start:*", "start": "concurrently -n w: npm:start:*",
"start:front": "umi dev", "start:front": "max dev",
"start:back": "nodemon", "start:back": "nodemon",
"start:public": "ts-node back/public.ts", "start:public": "ts-node back/public.ts",
"build:front": "umi build", "build:front": "max build",
"build:back": "tsc -p tsconfig.back.json", "build:back": "tsc -p tsconfig.back.json",
"panel": "npm run build:back && node static/build/app.js", "panel": "npm run build:back && node static/build/app.js",
"schedule": "npm run build:back && node static/build/schedule.js", "schedule": "npm run build:back && node static/build/schedule.js",
"public": "npm run build:back && node static/build/public.js", "public": "npm run build:back && node static/build/public.js",
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'", "prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
"postinstall": "umi generate tmp 2>/dev/null || true", "postinstall": "max setup 2>/dev/null || true",
"test": "umi-test", "test": "umi-test",
"test:coverage": "umi-test --coverage" "test:coverage": "umi-test --coverage"
}, },
@@ -28,9 +28,26 @@
}, },
"pnpm": { "pnpm": {
"peerDependencyRules": { "peerDependencyRules": {
"ignoreMissing": [
"react",
"react-dom",
"antd",
"dva",
"postcss",
"webpack",
"eslint",
"stylelint",
"redux",
"@babel/core",
"monaco-editor",
"rc-field-form",
"@types/lodash.merge",
"rollup"
],
"allowedVersions": { "allowedVersions": {
"react": "17", "react": "18",
"react-dom": "17" "react-dom": "18",
"dva-core": "2"
} }
} }
}, },
@@ -48,6 +65,7 @@
"express": "^4.17.3", "express": "^4.17.3",
"express-jwt": "^6.1.1", "express-jwt": "^6.1.1",
"express-urlrewrite": "^1.4.0", "express-urlrewrite": "^1.4.0",
"form-data": "^4.0.0",
"got": "^11.8.2", "got": "^11.8.2",
"hpagent": "^0.1.2", "hpagent": "^0.1.2",
"iconv-lite": "^0.6.3", "iconv-lite": "^0.6.3",
@@ -59,7 +77,7 @@
"nodemailer": "^6.7.2", "nodemailer": "^6.7.2",
"p-queue": "7.2.0", "p-queue": "7.2.0",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"sequelize": "^6.20.1", "sequelize": "^6.25.3",
"serve-handler": "^6.1.3", "serve-handler": "^6.1.3",
"sockjs": "^0.3.24", "sockjs": "^0.3.24",
"sqlite3": "npm:@louislam/sqlite3@^15.0.6", "sqlite3": "npm:@louislam/sqlite3@^15.0.6",
@@ -72,32 +90,31 @@
"devDependencies": { "devDependencies": {
"@ant-design/icons": "^4.7.0", "@ant-design/icons": "^4.7.0",
"@ant-design/pro-layout": "^6.33.1", "@ant-design/pro-layout": "^6.33.1",
"@monaco-editor/react": "^4.3.1", "@monaco-editor/react": "4.2.1",
"@react-hook/resize-observer": "^1.2.6",
"@sentry/react": "^7.12.1", "@sentry/react": "^7.12.1",
"@types/body-parser": "^1.19.2", "@types/body-parser": "^1.19.2",
"@types/cors": "^2.8.12", "@types/cors": "^2.8.12",
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"@types/express-jwt": "^6.0.4", "@types/express-jwt": "^6.0.4",
"@types/jsonwebtoken": "^8.5.8", "@types/jsonwebtoken": "^8.5.8",
"@types/lodash": "^4.14.179", "@types/lodash": "^4.14.185",
"@types/multer": "^1.4.7", "@types/multer": "^1.4.7",
"@types/nedb": "^1.8.12", "@types/nedb": "^1.8.12",
"@types/node": "^17.0.21", "@types/node": "^17.0.21",
"@types/node-schedule": "^1.3.2", "@types/node-schedule": "^1.3.2",
"@types/nodemailer": "^6.4.4", "@types/nodemailer": "^6.4.4",
"@types/qrcode.react": "^1.0.2", "@types/qrcode.react": "^1.0.2",
"@types/react": "^17.0.39", "@types/react": "^18.0.20",
"@types/react-dom": "^17.0.13", "@types/react-dom": "^18.0.6",
"@types/serve-handler": "^6.1.1", "@types/serve-handler": "^6.1.1",
"@types/sockjs": "^0.3.33", "@types/sockjs": "^0.3.33",
"@types/sockjs-client": "^1.5.1", "@types/sockjs-client": "^1.5.1",
"@types/uuid": "^8.3.4", "@types/uuid": "^8.3.4",
"@umijs/plugin-antd": "^0.15.0", "@umijs/max": "^4.0.21",
"@umijs/plugin-esbuild": "^1.4.1",
"@umijs/ssr-darkreader": "^4.9.45", "@umijs/ssr-darkreader": "^4.9.45",
"@umijs/test": "^3.5.21",
"ansi-to-react": "^6.1.6", "ansi-to-react": "^6.1.6",
"antd": "^4.20.5", "antd": "^4.23.0",
"antd-img-crop": "^4.2.3", "antd-img-crop": "^4.2.3",
"codemirror": "^5.65.2", "codemirror": "^5.65.2",
"compression-webpack-plugin": "9.2.0", "compression-webpack-plugin": "9.2.0",
@@ -107,19 +124,19 @@
"prettier": "^2.5.1", "prettier": "^2.5.1",
"qiniu": "^7.4.0", "qiniu": "^7.4.0",
"qrcode.react": "^1.0.1", "qrcode.react": "^1.0.1",
"query-string": "^7.1.1",
"rc-tween-one": "^3.0.6", "rc-tween-one": "^3.0.6",
"react": "17.x", "react": "18.2.0",
"react-codemirror2": "^7.2.1", "react-codemirror2": "^7.2.1",
"react-diff-viewer": "^3.1.1", "react-diff-viewer": "^3.1.1",
"react-dnd": "^14.0.2", "react-dnd": "^14.0.2",
"react-dnd-html5-backend": "^14.0.0", "react-dnd-html5-backend": "^14.0.0",
"react-dom": "17.x", "react-dom": "18.2.0",
"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.6.0", "ts-node": "^10.6.0",
"tslib": "^2.4.0", "tslib": "^2.4.0",
"typescript": "^4.6.2", "typescript": "^4.6.2",
"umi": "^3.5.21",
"umi-request": "^1.4.0", "umi-request": "^1.4.0",
"vh-check": "^2.0.5", "vh-check": "^2.0.5",
"webpack": "^5.70.0", "webpack": "^5.70.0",
+17 -3
View File
@@ -19,6 +19,11 @@ RepoFileExtensions="js py"
## 代理地址,支持http/https/socks,例如 http://127.0.0.1:7890 ## 代理地址,支持http/https/socks,例如 http://127.0.0.1:7890
ProxyUrl="" ProxyUrl=""
## 资源告警阙值,默认CPU 80%、内存80%、磁盘90%
CpuWarn=80
MemoryWarn=80
DiskWarn=90
## 设置定时任务执行的超时时间,默认1h,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天 ## 设置定时任务执行的超时时间,默认1h,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
CommandTimeoutTime="1h" CommandTimeoutTime="1h"
@@ -46,10 +51,10 @@ AutoStartBot=""
## 是否使用第三方bot,默认不使用,使用时填入仓库地址,存到ql/repo目录下,文件夹命名为diybot ## 是否使用第三方bot,默认不使用,使用时填入仓库地址,存到ql/repo目录下,文件夹命名为diybot
BotRepoUrl="" BotRepoUrl=""
## 安装python依赖时指定pip源,默认使用清华源 ## 安装python依赖时指定pip源
PipMirror="https://pypi.doubanio.com/simple/" PipMirror="https://pypi.doubanio.com/simple/"
## 安装node依赖时指定pip源,默认使用清华 ## 安装node依赖时指定npm
NpmMirror="https://registry.npmmirror.com" NpmMirror="https://registry.npmmirror.com"
## 通知环境变量 ## 通知环境变量
@@ -62,7 +67,7 @@ export PUSH_KEY=""
## 下方填写app提供的设备码,例如:https://api.day.app/123 那么此处的设备码就是123 ## 下方填写app提供的设备码,例如:https://api.day.app/123 那么此处的设备码就是123
export BARK_PUSH="" export BARK_PUSH=""
## 下方填写推送图标设置,自定义推送图标(需iOS15或以上) ## 下方填写推送图标设置,自定义推送图标(需iOS15或以上)
export BARK_ICON="http://qn.whyour.cn/logo.png" export BARK_ICON="https://qn.whyour.cn/logo.png"
## 下方填写推送声音设置,例如choo,具体值请在bark-推送铃声-查看所有铃声 ## 下方填写推送声音设置,例如choo,具体值请在bark-推送铃声-查看所有铃声
export BARK_SOUND="" export BARK_SOUND=""
## 下方填写推送消息分组,默认为"QingLong" ## 下方填写推送消息分组,默认为"QingLong"
@@ -146,4 +151,13 @@ export DEER_KEY=""
export CHAT_URL="" export CHAT_URL=""
export CHAT_TOKEN="" export CHAT_TOKEN=""
## 13. aibotk
## 官方说明文档:http://wechat.aibotk.com/oapi/oapi?from=ql
## aibotk_key (必填)填写智能微秘书个人中心的apikey
export AIBOTK_KEY=""
## aibotk_type (必填)填写发送的目标 room 或 contact, 填其他的不生效
export AIBOTK_TYPE=""
## aibotk_name (必填)填写群名或用户昵称,和上面的type类型要对应
export AIBOTK_NAME=""
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可 ## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可
+189 -154
View File
@@ -49,7 +49,7 @@ let CHAT_TOKEN = '';
//此处填你BarkAPP的信息(IP/设备码,例如:https://api.day.app/XXXXXXXX) //此处填你BarkAPP的信息(IP/设备码,例如:https://api.day.app/XXXXXXXX)
let BARK_PUSH = ''; let BARK_PUSH = '';
//BARK app推送图标,自定义推送图标(需iOS15或以上) //BARK app推送图标,自定义推送图标(需iOS15或以上)
let BARK_ICON = 'http://qn.whyour.cn/logo.png'; let BARK_ICON = 'https://qn.whyour.cn/logo.png';
//BARK app推送铃声,铃声列表去APP查看复制填写 //BARK app推送铃声,铃声列表去APP查看复制填写
let BARK_SOUND = ''; let BARK_SOUND = '';
//BARK app推送消息的分组, 默认为"QingLong" //BARK app推送消息的分组, 默认为"QingLong"
@@ -110,6 +110,20 @@ let PUSH_PLUS_USER = '';
let QQ_SKEY = ''; let QQ_SKEY = '';
let QQ_MODE = ''; let QQ_MODE = '';
// =======================================智能微秘书设置区域=======================================
//官方文档:http://wechat.aibotk.com/docs/about
//AIBOTK_KEY 填写智能微秘书个人中心的apikey
//AIBOTK_TYPE:填写发送的目标 room 或 contact, 填其他的不生效
//AIBOTK_NAME: 填写群名或用户昵称,和上面的type类型要对应
let AIBOTK_KEY = '';
let AIBOTK_TYPE = '';
let AIBOTK_NAME = '';
// =======================================飞书机器人设置区域=======================================
//官方文档:https://www.feishu.cn/hc/zh-CN/articles/360024984973
//FSKEY 飞书机器人的 FSKEY
let FSKEY = '';
//==========================云端环境变量的判断与接收========================= //==========================云端环境变量的判断与接收=========================
if (process.env.GOTIFY_URL) { if (process.env.GOTIFY_URL) {
GOTIFY_URL = process.env.GOTIFY_URL; GOTIFY_URL = process.env.GOTIFY_URL;
@@ -220,6 +234,20 @@ if (process.env.PUSH_PLUS_TOKEN) {
if (process.env.PUSH_PLUS_USER) { if (process.env.PUSH_PLUS_USER) {
PUSH_PLUS_USER = process.env.PUSH_PLUS_USER; PUSH_PLUS_USER = process.env.PUSH_PLUS_USER;
} }
if (process.env.AIBOTK_KEY) {
AIBOTK_KEY = process.env.AIBOTK_KEY;
}
if (process.env.AIBOTK_TYPE) {
AIBOTK_TYPE = process.env.AIBOTK_TYPE;
}
if (process.env.AIBOTK_NAME) {
AIBOTK_NAME = process.env.AIBOTK_NAME;
}
if (process.env.FSKEY) {
FSKEY = process.env.FSKEY;
}
//==========================云端环境变量的判断与接收========================= //==========================云端环境变量的判断与接收=========================
/** /**
@@ -255,6 +283,8 @@ async function sendNotify(
gotifyNotify(text, desp), //gotify gotifyNotify(text, desp), //gotify
ChatNotify(text, desp), //synolog chat ChatNotify(text, desp), //synolog chat
PushDeerNotify(text, desp), //PushDeer PushDeerNotify(text, desp), //PushDeer
aibotkNotify(text, desp), //智能微秘书
fsBotNotify(text, desp), //飞书机器人
]); ]);
} }
@@ -295,7 +325,7 @@ function gotifyNotify(text, desp) {
}); });
} }
function gobotNotify(text, desp, time = 2100) { function gobotNotify(text, desp) {
return new Promise((resolve) => { return new Promise((resolve) => {
if (GOBOT_URL) { if (GOBOT_URL) {
const options = { const options = {
@@ -306,38 +336,34 @@ function gobotNotify(text, desp, time = 2100) {
}, },
timeout, timeout,
}; };
setTimeout(() => { $.post(options, (err, resp, data) => {
$.post(options, (err, resp, data) => { try {
try { if (err) {
if (err) { console.log('发送go-cqhttp通知调用API失败!!\n');
console.log('发送go-cqhttp通知调用API失败!!\n'); console.log(err);
console.log(err); } else {
data = JSON.parse(data);
if (data.retcode === 0) {
console.log('go-cqhttp发送通知消息成功🎉\n');
} else if (data.retcode === 100) {
console.log(`go-cqhttp发送通知消息异常: ${data.errmsg}\n`);
} else { } else {
data = JSON.parse(data); console.log(`go-cqhttp发送通知消息异常\n${JSON.stringify(data)}`);
if (data.retcode === 0) {
console.log('go-cqhttp发送通知消息成功🎉\n');
} else if (data.retcode === 100) {
console.log(`go-cqhttp发送通知消息异常: ${data.errmsg}\n`);
} else {
console.log(
`go-cqhttp发送通知消息异常\n${JSON.stringify(data)}`,
);
}
} }
} catch (e) {
$.logErr(e, resp);
} finally {
resolve(data);
} }
}); } catch (e) {
}, time); $.logErr(e, resp);
} finally {
resolve(data);
}
});
} else { } else {
resolve(); resolve();
} }
}); });
} }
function serverNotify(text, desp, time = 2100) { function serverNotify(text, desp) {
return new Promise((resolve) => { return new Promise((resolve) => {
if (SCKEY) { if (SCKEY) {
//微信server酱推送通知一个\n不会换行,需要两个\n才能换行,故做此替换 //微信server酱推送通知一个\n不会换行,需要两个\n才能换行,故做此替换
@@ -352,33 +378,29 @@ function serverNotify(text, desp, time = 2100) {
}, },
timeout, timeout,
}; };
setTimeout(() => { $.post(options, (err, resp, data) => {
$.post(options, (err, resp, data) => { try {
try { if (err) {
if (err) { console.log('发送通知调用API失败!!\n');
console.log('发送通知调用API失败!!\n'); console.log(err);
console.log(err); } else {
data = JSON.parse(data);
//server酱和Server酱·Turbo版的返回json格式不太一样
if (data.errno === 0 || data.data.errno === 0) {
console.log('server酱发送通知消息成功🎉\n');
} else if (data.errno === 1024) {
// 一分钟内发送相同的内容会触发
console.log(`server酱发送通知消息异常: ${data.errmsg}\n`);
} else { } else {
data = JSON.parse(data); console.log(`server酱发送通知消息异常\n${JSON.stringify(data)}`);
//server酱和Server酱·Turbo版的返回json格式不太一样
if (data.errno === 0 || data.data.errno === 0) {
console.log('server酱发送通知消息成功🎉\n');
} else if (data.errno === 1024) {
// 一分钟内发送相同的内容会触发
console.log(`server酱发送通知消息异常: ${data.errmsg}\n`);
} else {
console.log(
`server酱发送通知消息异常\n${JSON.stringify(data)}`,
);
}
} }
} catch (e) {
$.logErr(e, resp);
} finally {
resolve(data);
} }
}); } catch (e) {
}, time); $.logErr(e, resp);
} finally {
resolve(data);
}
});
} else { } else {
resolve(); resolve();
} }
@@ -398,35 +420,29 @@ function PushDeerNotify(text, desp) {
}, },
timeout, timeout,
}; };
$.post( $.post(options, (err, resp, data) => {
options, try {
(err, resp, data) => { if (err) {
try { console.log('发送通知调用API失败!!\n');
if (err) { console.log(err);
console.log('发送通知调用API失败!!\n'); } else {
console.log(err); data = JSON.parse(data);
// 通过返回的result的长度来判断是否成功
if (
data.content.result.length !== undefined &&
data.content.result.length > 0
) {
console.log('PushDeer发送通知消息成功🎉\n');
} else { } else {
data = JSON.parse(data); console.log(`PushDeer发送通知消息异常\n${JSON.stringify(data)}`);
// 通过返回的result的长度来判断是否成功
if (
data.content.result.length !== undefined &&
data.content.result.length > 0
) {
console.log('PushDeer发送通知消息成功🎉\n');
} else {
console.log(
`PushDeer发送通知消息异常\n${JSON.stringify(data)}`,
);
}
} }
} catch (e) {
$.logErr(e, resp);
} finally {
resolve(data);
} }
}, } catch (e) {
time, $.logErr(e, resp);
); } finally {
resolve(data);
}
});
} else { } else {
resolve(); resolve();
} }
@@ -470,83 +486,6 @@ function ChatNotify(text, desp) {
}); });
} }
function CoolPush(text, desp) {
return new Promise((resolve) => {
if (QQ_SKEY) {
let options = {
url: `https://push.xuthus.cc/${QQ_MODE}/${QQ_SKEY}`,
headers: {
'Content-Type': 'application/json',
},
};
// 已知敏感词
text = text.replace(/京豆/g, '豆豆');
desp = desp.replace(/京豆/g, '');
desp = desp.replace(/🐶/g, '');
desp = desp.replace(/红包/g, 'H包');
switch (QQ_MODE) {
case 'email':
options.json = {
t: text,
c: desp,
};
break;
default:
options.body = `${text}\n\n${desp}`;
}
let pushMode = function (t) {
switch (t) {
case 'send':
return '个人';
case 'group':
return 'QQ群';
case 'wx':
return '微信';
case 'ww':
return '企业微信';
case 'email':
return '邮件';
default:
return '未知方式';
}
};
$.post(options, (err, resp, data) => {
try {
if (err) {
console.log(`发送${pushMode(QQ_MODE)}通知调用API失败!!\n`);
console.log(err);
} else {
data = JSON.parse(data);
if (data.code === 200) {
console.log(`酷推发送${pushMode(QQ_MODE)}通知消息成功🎉\n`);
} else if (data.code === 400) {
console.log(
`QQ酷推(Cool Push)发送${pushMode(QQ_MODE)}推送失败:${
data.msg
}\n`,
);
} else if (data.code === 503) {
console.log(`QQ酷推出错,${data.message}${data.data}\n`);
} else {
console.log(`酷推推送异常: ${JSON.stringify(data)}`);
}
}
} catch (e) {
$.logErr(e, resp);
} finally {
resolve(data);
}
});
} else {
resolve();
}
});
}
function BarkNotify(text, desp, params = {}) { function BarkNotify(text, desp, params = {}) {
return new Promise((resolve) => { return new Promise((resolve) => {
if (BARK_PUSH) { if (BARK_PUSH) {
@@ -649,7 +588,7 @@ function ddBotNotify(text, desp) {
json: { json: {
msgtype: 'text', msgtype: 'text',
text: { text: {
content: ` ${text}\n\n${desp}`, content: `${text}\n\n${desp}`,
}, },
}, },
headers: { headers: {
@@ -716,7 +655,7 @@ function qywxBotNotify(text, desp) {
json: { json: {
msgtype: 'text', msgtype: 'text',
text: { text: {
content: ` ${text}\n\n${desp}`, content: `${text}\n\n${desp}`,
}, },
}, },
headers: { headers: {
@@ -983,6 +922,102 @@ function pushPlusNotify(text, desp) {
}); });
} }
function aibotkNotify(text, desp) {
return new Promise((resolve) => {
if (AIBOTK_KEY && AIBOTK_TYPE && AIBOTK_NAME) {
let json = {};
let url = '';
switch (AIBOTK_TYPE) {
case 'room':
url = 'https://api-bot.aibotk.com/openapi/v1/chat/room';
json = {
apiKey: `${AIBOTK_KEY}`,
roomName: `${AIBOTK_NAME}`,
message: {
type: 1,
content: `【青龙快讯】\n\n${text}\n${desp}`,
},
};
break;
case 'contact':
url = 'https://api-bot.aibotk.com/openapi/v1/chat/contact';
json = {
apiKey: `${AIBOTK_KEY}`,
name: `${AIBOTK_NAME}`,
message: {
type: 1,
content: `【青龙快讯】\n\n${text}\n${desp}`,
},
};
break;
}
const options = {
url: url,
json,
headers: {
'Content-Type': 'application/json',
},
timeout,
};
$.post(options, (err, resp, data) => {
try {
if (err) {
console.log('智能微秘书发送通知消息失败!!\n');
console.log(err);
} else {
data = JSON.parse(data);
if (data.code === 0) {
console.log('智能微秘书发送通知消息成功🎉。\n');
} else {
console.log(`${data.error}\n`);
}
}
} catch (e) {
$.logErr(e, resp);
} finally {
resolve(data);
}
});
}
});
}
function fsBotNotify(text, desp) {
return new Promise((resolve) => {
if (FSKEY) {
const options = {
url: `https://open.feishu.cn/open-apis/bot/v2/hook/${FSKEY}`,
json: { msg_type: 'text', content: { text: `${text}\n\n${desp}` } },
headers: {
'Content-Type': 'application/json',
},
timeout,
};
$.post(options, (err, resp, data) => {
try {
if (err) {
console.log('发送通知调用API失败!!\n');
console.log(err);
} else {
data = JSON.parse(data);
if (data.StatusCode === 0) {
console.log('飞书发送通知消息成功🎉\n');
} else {
console.log(`${data.msg}\n`);
}
}
} catch (e) {
$.logErr(e, resp);
} finally {
resolve(data);
}
});
} else {
resolve();
}
});
}
module.exports = { module.exports = {
sendNotify, sendNotify,
BARK_PUSH, BARK_PUSH,
+39
View File
@@ -81,6 +81,10 @@ push_config = {
'TG_PROXY_AUTH': '', # tg 代理认证参数 'TG_PROXY_AUTH': '', # tg 代理认证参数
'TG_PROXY_HOST': '', # tg 机器人的 TG_PROXY_HOST 'TG_PROXY_HOST': '', # tg 机器人的 TG_PROXY_HOST
'TG_PROXY_PORT': '', # tg 机器人的 TG_PROXY_PORT 'TG_PROXY_PORT': '', # tg 机器人的 TG_PROXY_PORT
'AIBOTK_KEY': '', # 智能微秘书 个人中心的apikey 文档地址:http://wechat.aibotk.com/docs/about
'AIBOTK_TYPE': '', # 智能微秘书 发送目标 room 或 contact
'AIBOTK_NAME': '', # 智能微秘书 发送群名 或者好友昵称和type要对应好
} }
notify_function = [] notify_function = []
# fmt: on # fmt: on
@@ -521,6 +525,39 @@ def telegram_bot(title: str, content: str) -> None:
print("tg 推送失败!") print("tg 推送失败!")
def aibotk(title: str, content: str) -> None:
"""
使用 智能微秘书 推送消息。
"""
if not push_config.get("AIBOTK_KEY") or not push_config.get("AIBOTK_TYPE") or not push_config.get("AIBOTK_NAME"):
print("智能微秘书 的 AIBOTK_KEY 或者 AIBOTK_TYPE 或者 AIBOTK_NAME 未设置!!\n取消推送")
return
print("智能微秘书 服务启动")
if push_config.get("AIBOTK_TYPE") == 'room':
url = "https://api-bot.aibotk.com/openapi/v1/chat/room"
data = {
"apiKey": push_config.get("AIBOTK_KEY"),
"roomName": push_config.get("AIBOTK_NAME"),
"message": {"type": 1, "content": f'【青龙快讯】\n\n${title}\n${content}' }
}
else:
url = "https://api-bot.aibotk.com/openapi/v1/chat/contact"
data = {
"apiKey": push_config.get("AIBOTK_KEY"),
"name": push_config.get("AIBOTK_NAME"),
"message": {"type": 1, "content": f'【青龙快讯】\n\n${title}\n${content}' }
}
body = json.dumps(data).encode(encoding="utf-8")
headers = {"Content-Type": "application/json"}
response = requests.post(url=url, data=body, headers=headers).json()
print(response)
if response["code"] == 0:
print("智能微秘书 推送成功!")
else:
print(f'智能微秘书 推送失败!{response["error"]}')
def one() -> str: def one() -> str:
""" """
获取一条一言。 获取一条一言。
@@ -561,6 +598,8 @@ if push_config.get("QYWX_KEY"):
notify_function.append(wecom_bot) notify_function.append(wecom_bot)
if push_config.get("TG_BOT_TOKEN") and push_config.get("TG_USER_ID"): if push_config.get("TG_BOT_TOKEN") and push_config.get("TG_USER_ID"):
notify_function.append(telegram_bot) notify_function.append(telegram_bot)
if push_config.get("AIBOTK_KEY") and push_config.get("AIBOTK_TYPE") and push_config.get("AIBOTK_NAME"):
notify_function.append(aibotk)
def send(title: str, content: str) -> None: def send(title: str, content: str) -> None:
+160 -160
View File
@@ -1,187 +1,187 @@
#!/usr/bin/env bash #!/usr/bin/env bash
get_token() { get_token() {
token=$(cat $file_auth_token | jq -r .value) token=$(cat $file_auth_token | jq -r .value)
} }
add_cron_api() { add_cron_api() {
local currentTimeStamp=$(date +%s) local currentTimeStamp=$(date +%s)
if [[ $# -eq 1 ]]; then if [[ $# -eq 1 ]]; then
local schedule=$(echo "$1" | awk -F ":" '{print $1}') local schedule=$(echo "$1" | awk -F ":" '{print $1}')
local command=$(echo "$1" | awk -F ":" '{print $2}') local command=$(echo "$1" | awk -F ":" '{print $2}')
local name=$(echo "$1" | awk -F ":" '{print $3}') local name=$(echo "$1" | awk -F ":" '{print $3}')
else else
local schedule=$1 local schedule=$1
local command=$2 local command=$2
local name=$3 local name=$3
fi fi
local api=$( local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \ curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
-H "Accept: application/json" \ -H "Accept: application/json" \
-H "Authorization: Bearer $token" \ -H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \ -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \ -H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \ -H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \ -H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \ -H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\"}" \ --data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\"}" \
--compressed --compressed
) )
code=$(echo $api | jq -r .code) code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message) message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then if [[ $code == 200 ]]; then
echo -e "$name -> 添加成功" echo -e "$name -> 添加成功"
else else
echo -e "$name -> 添加失败(${message})" echo -e "$name -> 添加失败(${message})"
fi fi
} }
update_cron_api() { update_cron_api() {
local currentTimeStamp=$(date +%s) local currentTimeStamp=$(date +%s)
if [[ $# -eq 1 ]]; then if [[ $# -eq 1 ]]; then
local schedule=$(echo "$1" | awk -F ":" '{print $1}') local schedule=$(echo "$1" | awk -F ":" '{print $1}')
local command=$(echo "$1" | awk -F ":" '{print $2}') local command=$(echo "$1" | awk -F ":" '{print $2}')
local name=$(echo "$1" | awk -F ":" '{print $3}') local name=$(echo "$1" | awk -F ":" '{print $3}')
local id=$(echo "$1" | awk -F ":" '{print $4}') local id=$(echo "$1" | awk -F ":" '{print $4}')
else else
local schedule=$1 local schedule=$1
local command=$2 local command=$2
local name=$3 local name=$3
local id=$4 local id=$4
fi fi
local api=$( local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \ curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
-X 'PUT' \ -X 'PUT' \
-H "Accept: application/json" \ -H "Accept: application/json" \
-H "Authorization: Bearer $token" \ -H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \ -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \ -H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \ -H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \ -H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \ -H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"id\":\"$id\"}" \ --data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"id\":\"$id\"}" \
--compressed --compressed
) )
code=$(echo $api | jq -r .code) code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message) message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then if [[ $code == 200 ]]; then
echo -e "$name -> 更新成功" echo -e "$name -> 更新成功"
else else
echo -e "$name -> 更新失败(${message})" echo -e "$name -> 更新失败(${message})"
fi fi
} }
update_cron_command_api() { update_cron_command_api() {
local currentTimeStamp=$(date +%s) local currentTimeStamp=$(date +%s)
if [[ $# -eq 1 ]]; then if [[ $# -eq 1 ]]; then
local command=$(echo "$1" | awk -F ":" '{print $1}') local command=$(echo "$1" | awk -F ":" '{print $1}')
local id=$(echo "$1" | awk -F ":" '{print $2}') local id=$(echo "$1" | awk -F ":" '{print $2}')
else else
local command=$1 local command=$1
local id=$2 local id=$2
fi fi
local api=$( local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \ curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
-X 'PUT' \ -X 'PUT' \
-H "Accept: application/json" \ -H "Accept: application/json" \
-H "Authorization: Bearer $token" \ -H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \ -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \ -H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \ -H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \ -H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \ -H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"command\":\"$command\",\"id\":\"$id\"}" \ --data-raw "{\"command\":\"$command\",\"id\":\"$id\"}" \
--compressed --compressed
) )
code=$(echo $api | jq -r .code) code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message) message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then if [[ $code == 200 ]]; then
echo -e "$command -> 更新成功" echo -e "$command -> 更新成功"
else else
echo -e "$command -> 更新失败(${message})" echo -e "$command -> 更新失败(${message})"
fi fi
} }
del_cron_api() { del_cron_api() {
local ids=$1 local ids=$1
local currentTimeStamp=$(date +%s) local currentTimeStamp=$(date +%s)
local api=$( local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \ curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
-X 'DELETE' \ -X 'DELETE' \
-H "Accept: application/json" \ -H "Accept: application/json" \
-H "Authorization: Bearer $token" \ -H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \ -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \ -H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \ -H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \ -H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \ -H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "[$ids]" \ --data-raw "[$ids]" \
--compressed --compressed
) )
code=$(echo $api | jq -r .code) code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message) message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then if [[ $code == 200 ]]; then
echo -e "成功" echo -e "成功"
else else
echo -e "失败(${message})" echo -e "失败(${message})"
fi fi
} }
update_cron() { update_cron() {
local ids="$1" local ids="$1"
local status="$2" local status="$2"
local pid="${3:-''}" local pid="${3:-''}"
local logPath="$4" local logPath="$4"
local lastExecutingTime="${5:-0}" local lastExecutingTime="${5:-0}"
local runningTime="${6:-0}" local runningTime="${6:-0}"
local currentTimeStamp=$(date +%s) local currentTimeStamp=$(date +%s)
local api=$( local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons/status?t=$currentTimeStamp" \ curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons/status?t=$currentTimeStamp" \
-X 'PUT' \ -X 'PUT' \
-H "Accept: application/json" \ -H "Accept: application/json" \
-H "Authorization: Bearer $token" \ -H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \ -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \ -H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \ -H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \ -H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \ -H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"ids\":[$ids],\"status\":\"$status\",\"pid\":\"$pid\",\"log_path\":\"$logPath\",\"last_execution_time\":$lastExecutingTime,\"last_running_time\":$runningTime}" \ --data-raw "{\"ids\":[$ids],\"status\":\"$status\",\"pid\":\"$pid\",\"log_path\":\"$logPath\",\"last_execution_time\":$lastExecutingTime,\"last_running_time\":$runningTime}" \
--compressed --compressed
) )
code=$(echo $api | jq -r .code) code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message) message=$(echo $api | jq -r .message)
if [[ $code != 200 ]]; then if [[ $code != 200 ]]; then
echo -e "\n## 更新任务状态失败(${message})\n" >> $dir_log/$log_path echo -e "\n## 更新任务状态失败(${message})\n" >>$dir_log/$log_path
fi fi
} }
notify_api() { notify_api() {
local title=$1 local title=$1
local content=$2 local content=$2
local currentTimeStamp=$(date +%s) local currentTimeStamp=$(date +%s)
local api=$( local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/system/notify?t=$currentTimeStamp" \ curl -s --noproxy "*" "http://0.0.0.0:5600/open/system/notify?t=$currentTimeStamp" \
-X 'PUT' \ -X 'PUT' \
-H "Accept: application/json" \ -H "Accept: application/json" \
-H "Authorization: Bearer $token" \ -H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \ -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \ -H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \ -H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \ -H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \ -H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"title\":\"$title\",\"content\":\"$content\"}" \ --data-raw "{\"title\":\"$title\",\"content\":\"$content\"}" \
--compressed --compressed
) )
code=$(echo $api | jq -r .code) code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message) message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then if [[ $code == 200 ]]; then
echo -e "通知发送成功" echo -e "通知发送成功"
else else
echo -e "通知失败(${message})" echo -e "通知失败(${message})"
fi fi
} }
get_token get_token
+4 -5
View File
@@ -35,11 +35,10 @@ fi
cp -f "$repo_path/jbot/requirements.txt" "$dir_data" cp -f "$repo_path/jbot/requirements.txt" "$dir_data"
cd $dir_data cd $dir_data
cat requirements.txt | while read LREAD cat requirements.txt | while read LREAD; do
do if [[ ! $(pip3 show "${LREAD%%=*}" 2>/dev/null) ]]; then
if [[ ! $(pip3 show "${LREAD%%=*}" 2>/dev/null) ]]; then pip3 --default-timeout=100 install ${LREAD}
pip3 --default-timeout=100 install ${LREAD} fi
fi
done done
echo -e "\npython3依赖安装成功...\n" echo -e "\npython3依赖安装成功...\n"
+272
View File
@@ -0,0 +1,272 @@
#!/usr/bin/env bash
## 导入通用变量与函数
dir_shell=$QL_DIR/shell
. $dir_shell/share.sh
. $dir_shell/api.sh
random_delay() {
local random_delay_max=$RandomDelay
if [[ $random_delay_max ]] && [[ $random_delay_max -gt 0 ]]; then
local file_param=$1
local file_extensions=${RandomDelayFileExtensions-"js"}
local ignored_minutes=${RandomDelayIgnoredMinutes-"0 30"}
if [[ -n $file_extensions ]]; then
if ! echo "$file_param" | grep -qE "\.${file_extensions// /$|\\.}$"; then
# echo -e "\n当前文件需要准点运行, 放弃随机延迟\n"
return
fi
fi
local current_min
current_min=$(date "+%-M")
for minute in $ignored_minutes; do
if [[ $current_min -eq $minute ]]; then
# echo -e "\n当前时间需要准点运行, 放弃随机延迟\n"
return
fi
done
local delay_second=$(($(gen_random_num "$random_delay_max") + 1))
echo -e "\n命令未添加 \"now\",随机延迟 $delay_second 秒后再执行任务,如需立即终止,请按 CTRL+C...\n"
sleep $delay_second
fi
}
## scripts目录下所有可运行脚本数组
gen_array_scripts() {
local dir_current=$(pwd)
local i="-1"
cd $dir_scripts
for file in $(ls); do
if [[ -f $file ]] && [[ $file == *.js && $file != sendNotify.js ]]; then
let i++
array_scripts[i]=$(echo "$file" | perl -pe "s|$dir_scripts/||g")
array_scripts_name[i]=$(grep "new Env" $file | awk -F "'|\"" '{print $2}' | head -1)
[[ -z ${array_scripts_name[i]} ]] && array_scripts_name[i]="<未识别出活动名称>"
fi
done
cd $dir_current
}
## 使用说明
usage() {
gen_array_scripts
echo -e "task命令运行本程序自动添加进crontab的脚本,需要输入脚本的绝对路径或去掉 “$dir_scripts/” 目录后的相对路径(定时任务中请写作相对路径),用法为:"
echo -e "1.$cmd_task <file_name> # 依次执行,如果设置了随机延迟,将随机延迟一定秒数"
echo -e "2.$cmd_task <file_name> now # 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日志,同时记录在日志文件中"
echo -e "3.$cmd_task <file_name> conc <环境变量名称> <账号编号,空格分隔>(可选的) # 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日志,直接记录在日志文件中,且可指定账号执行"
echo -e "4.$cmd_task <file_name> desi <环境变量名称> <账号编号,空格分隔> # 指定账号执行,无论是否设置了随机延迟,均立即运行"
if [[ ${#array_scripts[*]} -gt 0 ]]; then
echo -e "\n当前有以下脚本可以运行:"
for ((i = 0; i < ${#array_scripts[*]}; i++)); do
echo -e "$(($i + 1)). ${array_scripts_name[i]}${array_scripts[i]}"
done
else
echo -e "\n暂无脚本可以执行"
fi
}
## run nohup,$1:文件名,不含路径,带后缀
run_nohup() {
local file_name=$1
nohup node $file_name &>$log_path &
}
check_server() {
cpu_use=$(top -b -n 1 | grep CPU | grep -v -E 'grep|PID' | awk '{print $2}' | cut -f 1 -d "%" | head -n 1)
mem_free=$(free -m | grep "Mem" | awk '{print $3}' | head -n 1)
mem_total=$(free -m | grep "Mem" | awk '{print $2}' | head -n 1)
mem_use=$(printf "%d%%" $((mem_free * 100 / mem_total)) | cut -f 1 -d "%" | head -n 1)
disk_use=$(df -P | grep /dev | grep -v -E '(tmp|boot|shm)' | awk '{print $5}' | cut -f 1 -d "%" | head -n 1)
if [[ $cpu_use -gt $cpu_warn ]] || [[ $mem_free -lt $mem_warn ]] || [[ $disk_use -gt $disk_warn ]]; then
local resource=$(top -b -n 1 | grep -v -E 'grep|Mem|idle|Load|tr' | awk '{$2="";$3="";$4="";$5="";$7="";print $0}' | head -n 10 | tr '\n' '|' | sed s/\|/\\\\n/g)
notify_api "服务器资源异常警告" "当前CPU占用 $cpu_use% 内存占用 $mem_use% 磁盘占用 $disk_use% \n资源占用详情 \n\n $resource"
fi
}
handle_task_before() {
begin_time=$(format_time "$time_format" "$time")
begin_timestamp=$(format_timestamp "$time_format" "$time")
echo -e "## 开始执行... $begin_time\n"
[[ $is_macos -eq 0 ]] && check_server
if [[ -s $task_error_log_path ]]; then
eval cat $task_error_log_path $cmd
eval echo -e "加载 config.sh 出错,请手动检查" $cmd
eval echo $cmd
fi
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
. $file_task_before "$@"
}
handle_task_after() {
. $file_task_after "$@"
local etime=$(date "+$time_format")
local end_time=$(format_time "$time_format" "$etime")
local end_timestamp=$(format_timestamp "$time_format" "$etime")
local diff_time=$(($end_timestamp - $begin_timestamp))
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
echo -e "\n\n## 执行结束... $end_time 耗时 $diff_time"
echo -e "\n     "
}
## 正常运行单个脚本,$1:传入参数
run_normal() {
local file_param=$1
if [[ $# -eq 1 ]]; then
random_delay "$file_param"
fi
cd $dir_scripts
local relative_path="${file_param%/*}"
if [[ ${file_param} != /* ]] && [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path}
file_param=${file_param/$relative_path\//}
fi
$timeoutCmd $which_program $file_param
}
## 并发执行时,设定的 RandomDelay 不会生效,即所有任务立即执行
run_concurrent() {
local file_param="$1"
local env_param="$2"
local num_param=$(echo "$3" | perl -pe "s|.*$2(.*)|\1|")
if [[ ! $env_param ]]; then
echo -e "\n 缺少并发运行的环境变量参数"
exit 1
fi
local envs=$(eval echo "\$${env_param}")
local array=($(echo $envs | sed 's/&/ /g'))
local tempArr=$(echo $num_param | sed "s/-max/-${#array[@]}/g" | sed "s/max-/${#array[@]}-/g" | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g")
local runArr=($(eval echo $tempArr))
runArr=($(awk -v RS=' ' '!a[$1]++' <<<${runArr[@]}))
local n=0
for i in ${runArr[@]}; do
array_run[n]=${array[$i - 1]}
let n++
done
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
local envs=$(eval echo "\$${env_param}")
local array=($(echo $envs | sed 's/&/ /g'))
single_log_time=$(date "+%Y-%m-%d-%H-%M-%S.%N")
cd $dir_scripts
local relative_path="${file_param%/*}"
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path}
file_param=${file_param/$relative_path\//}
fi
for i in "${!array[@]}"; do
export ${env_param}=${array[i]}
single_log_path="$dir_log/$log_dir/${single_log_time}_$((i + 1)).log"
eval $timeoutCmd $which_program $file_param &>$single_log_path &
done
wait
for i in "${!array[@]}"; do
single_log_path="$dir_log/$log_dir/${single_log_time}_$((i + 1)).log"
cat $single_log_path
[[ -f $single_log_path ]] && rm -f $single_log_path
done
}
run_designated() {
local file_param="$1"
local env_param="$2"
local num_param=$(echo "$3" | perl -pe "s|.*$2(.*)|\1|")
if [[ ! $env_param ]] || [[ ! $num_param ]]; then
echo -e "\n 缺少单独运行的参数 task xxx.js desi Test 1 3"
exit 1
fi
local envs=$(eval echo "\$${env_param}")
local array=($(echo $envs | sed 's/&/ /g'))
local tempArr=$(echo $num_param | sed "s/-max/-${#array[@]}/g" | sed "s/max-/${#array[@]}-/g" | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g")
local runArr=($(eval echo $tempArr))
runArr=($(awk -v RS=' ' '!a[$1]++' <<<${runArr[@]}))
local n=0
for i in ${runArr[@]}; do
array_run[n]=${array[$i - 1]}
let n++
done
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
cd $dir_scripts
local relative_path="${file_param%/*}"
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path}
file_param=${file_param/$relative_path\//}
fi
$timeoutCmd $which_program $file_param
}
## 运行其他命令
run_else() {
local file_param="$1"
cd $dir_scripts
local relative_path="${file_param%/*}"
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path}
file_param=${file_param/$relative_path\//}
fi
shift
$timeoutCmd $which_program $file_param "$@"
}
## 命令检测
main() {
if [[ $1 == *.js ]] || [[ $1 == *.py ]] || [[ $1 == *.pyc ]] || [[ $1 == *.sh ]] || [[ $1 == *.ts ]]; then
case $# in
1)
run_normal "$1"
;;
*)
case $2 in
now)
run_normal "$1" "$2"
;;
conc)
run_concurrent "$1" "$3" "$*"
;;
desi)
run_designated "$1" "$3" "$*"
;;
*)
run_else "$@"
;;
esac
;;
esac
elif [[ $# -eq 0 ]]; then
echo
usage
else
run_else "$@"
fi
}
handle_task_before "$@"
main "$@"
handle_task_after "$@"
+23 -23
View File
@@ -8,35 +8,35 @@ days=$1
## 删除运行js脚本的旧日志 ## 删除运行js脚本的旧日志
remove_js_log() { remove_js_log() {
local log_full_path_list=$(find $dir_log/ -name "*.log") local log_full_path_list=$(find $dir_log/ -name "*.log")
local diff_time local diff_time
for log in $log_full_path_list; do for log in $log_full_path_list; do
local log_date=$(echo $log | awk -F "/" '{print $NF}' | cut -c1-10) #文件名比文件属性获得的日期要可靠 local log_date=$(echo $log | awk -F "/" '{print $NF}' | cut -c1-10) #文件名比文件属性获得的日期要可靠
if [[ $(date +%s -d $log_date 2>/dev/null) ]]; then if [[ $(date +%s -d $log_date 2>/dev/null) ]]; then
if [[ $is_macos -eq 1 ]]; then if [[ $is_macos -eq 1 ]]; then
diff_time=$(($(date +%s) - $(date -j -f "%Y-%m-%d" "$log_date" +%s))) diff_time=$(($(date +%s) - $(date -j -f "%Y-%m-%d" "$log_date" +%s)))
else else
diff_time=$(($(date +%s) - $(date +%s -d "$log_date"))) diff_time=$(($(date +%s) - $(date +%s -d "$log_date")))
fi fi
[[ $diff_time -gt $((${days} * 86400)) ]] && rm -vf $log [[ $diff_time -gt $((${days} * 86400)) ]] && rm -vf $log
fi fi
done done
} }
## 删除空文件夹 ## 删除空文件夹
remove_empty_dir() { remove_empty_dir() {
cd $dir_log cd $dir_log
for dir in $(ls); do for dir in $(ls); do
if [[ -d $dir ]] && [[ -z $(ls $dir) ]]; then if [[ -d $dir ]] && [[ -z $(ls $dir) ]]; then
rm -rf $dir rm -rf $dir
fi fi
done done
} }
## 运行 ## 运行
if [[ ${days} ]]; then if [[ ${days} ]]; then
echo -e "查找旧日志文件中...\n" echo -e "查找旧日志文件中...\n"
remove_js_log remove_js_log
remove_empty_dir remove_empty_dir
echo -e "删除旧日志执行完毕\n" echo -e "删除旧日志执行完毕\n"
fi fi
+314 -257
View File
@@ -50,377 +50,434 @@ list_own_drop=$dir_list_tmp/own_drop.list
## 软连接及其原始文件对应关系 ## 软连接及其原始文件对应关系
link_name=( link_name=(
task task
ql ql
) )
original_name=( original_name=(
task.sh task.sh
update.sh update.sh
) )
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 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
export PYTHONUNBUFFERED=1 export PYTHONUNBUFFERED=1
} }
import_config() { import_config() {
[[ -f $file_config_user ]] && . $file_config_user [[ -f $file_config_user ]] && . $file_config_user
[[ -f $file_env ]] && . $file_env [[ -f $file_env ]] && . $file_env
ql_base_url=${QlBaseUrl:-""} ql_base_url=${QlBaseUrl:-""}
command_timeout_time=${CommandTimeoutTime:-"1h"} command_timeout_time=${CommandTimeoutTime:-"1h"}
proxy_url=${ProxyUrl:-""} proxy_url=${ProxyUrl:-""}
file_extensions=${RepoFileExtensions:-"js py"} file_extensions=${RepoFileExtensions:-"js py"}
current_branch=${QL_BRANCH} current_branch=${QL_BRANCH}
if [[ -n "${DefaultCronRule}" ]]; then if [[ -n "${DefaultCronRule}" ]]; then
default_cron="${DefaultCronRule}" default_cron="${DefaultCronRule}"
else else
default_cron="$(random_range 0 59) $(random_range 0 23) * * *" default_cron="$(random_range 0 59) $(random_range 0 23) * * *"
fi fi
cpu_warn=${CpuWarn:-80}
mem_warn=${MemoryWarn:-80}
disk_warn=${DiskWarn:-90}
} }
set_proxy() { set_proxy() {
if [[ $proxy_url ]]; then if [[ $proxy_url ]]; then
export http_proxy="${proxy_url}" export http_proxy="${proxy_url}"
export https_proxy="${proxy_url}" export https_proxy="${proxy_url}"
fi fi
} }
unset_proxy() { unset_proxy() {
unset http_proxy unset http_proxy
unset https_proxy unset https_proxy
} }
make_dir() { make_dir() {
local dir=$1 local dir=$1
if [[ ! -d $dir ]]; then if [[ ! -d $dir ]]; then
mkdir -p $dir mkdir -p $dir
fi fi
} }
detect_termux() { detect_termux() {
if [[ $PATH == *com.termux* ]]; then if [[ $PATH == *com.termux* ]]; then
is_termux=1 is_termux=1
else else
is_termux=0 is_termux=0
fi fi
} }
detect_macos() { detect_macos() {
[[ $(uname -s) == Darwin ]] && is_macos=1 || is_macos=0 [[ $(uname -s) == Darwin ]] && is_macos=1 || is_macos=0
} }
gen_random_num() { gen_random_num() {
local divi=$1 local divi=$1
echo $((${RANDOM} % $divi)) echo $((${RANDOM} % $divi))
} }
link_shell_sub() { link_shell_sub() {
local link_path="$1" local link_path="$1"
local original_path="$2" local original_path="$2"
if [[ ! -L $link_path ]] || [[ $(readlink -f $link_path) != $original_path ]]; then if [[ ! -L $link_path ]] || [[ $(readlink -f $link_path) != $original_path ]]; then
rm -f $link_path 2>/dev/null rm -f $link_path 2>/dev/null
ln -sf $original_path $link_path ln -sf $original_path $link_path
fi fi
} }
link_shell() { link_shell() {
if [[ $is_termux -eq 1 ]]; then if [[ $is_termux -eq 1 ]]; then
local path="/data/data/com.termux/files/usr/bin/" local path="/data/data/com.termux/files/usr/bin/"
elif [[ $PATH == */usr/local/bin* ]] && [[ -d /usr/local/bin ]]; then elif [[ $PATH == */usr/local/bin* ]] && [[ -d /usr/local/bin ]]; then
local path="/usr/local/bin/" local path="/usr/local/bin/"
else else
local path="" local path=""
echo -e "脚本功能受限,请自行添加命令的软连接...\n" echo -e "脚本功能受限,请自行添加命令的软连接...\n"
fi fi
if [[ $path ]]; then if [[ $path ]]; then
for ((i = 0; i < ${#link_name[*]}; i++)); do for ((i = 0; i < ${#link_name[*]}; i++)); do
link_shell_sub "$path${link_name[i]}" "$dir_shell/${original_name[i]}" link_shell_sub "$path${link_name[i]}" "$dir_shell/${original_name[i]}"
done done
fi fi
} }
define_cmd() { define_cmd() {
local cmd_prefix cmd_suffix local cmd_prefix cmd_suffix
if type task &>/dev/null; then if type task &>/dev/null; then
cmd_suffix="" cmd_suffix=""
if [[ -f "$dir_shell/task.sh" ]]; then if [[ -f "$dir_shell/task.sh" ]]; then
cmd_prefix="" cmd_prefix=""
else
cmd_prefix="bash "
fi
else else
cmd_suffix=".sh" cmd_prefix="bash "
if [[ -f "$dir_shell/task.sh" ]]; then
cmd_prefix="$dir_shell/"
else
cmd_prefix="bash $dir_shell/"
fi
fi fi
for ((i = 0; i < ${#link_name[*]}; i++)); do else
export cmd_${link_name[i]}="${cmd_prefix}${link_name[i]}${cmd_suffix}" cmd_suffix=".sh"
done if [[ -f "$dir_shell/task.sh" ]]; then
cmd_prefix="$dir_shell/"
else
cmd_prefix="bash $dir_shell/"
fi
fi
for ((i = 0; i < ${#link_name[*]}; i++)); do
export cmd_${link_name[i]}="${cmd_prefix}${link_name[i]}${cmd_suffix}"
done
} }
fix_config() { fix_config() {
make_dir $dir_static make_dir $dir_static
make_dir $dir_data make_dir $dir_data
make_dir $dir_config make_dir $dir_config
make_dir $dir_log make_dir $dir_log
make_dir $dir_db make_dir $dir_db
make_dir $dir_scripts make_dir $dir_scripts
make_dir $dir_list_tmp make_dir $dir_list_tmp
make_dir $dir_repo make_dir $dir_repo
make_dir $dir_raw make_dir $dir_raw
make_dir $dir_update_log make_dir $dir_update_log
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" echo -e "复制一份 $file_config_sample$file_config_user,随后请按注释编辑你的配置文件:$file_config_user\n"
cp -fv $file_config_sample $file_config_user cp -fv $file_config_sample $file_config_user
echo echo
fi fi
if [[ ! -f $file_env ]]; then if [[ ! -f $file_env ]]; then
echo -e "检测到config配置目录下不存在env.sh,创建一个空文件用于初始化...\n" echo -e "检测到config配置目录下不存在env.sh,创建一个空文件用于初始化...\n"
touch $file_env touch $file_env
echo echo
fi fi
if [[ ! -f $file_task_before ]]; then if [[ ! -f $file_task_before ]]; then
echo -e "复制一份 $file_task_sample$file_task_before\n" echo -e "复制一份 $file_task_sample$file_task_before\n"
cp -fv $file_task_sample $file_task_before cp -fv $file_task_sample $file_task_before
echo echo
fi fi
if [[ ! -f $file_task_after ]]; then if [[ ! -f $file_task_after ]]; then
echo -e "复制一份 $file_task_sample$file_task_after\n" echo -e "复制一份 $file_task_sample$file_task_after\n"
cp -fv $file_task_sample $file_task_after cp -fv $file_task_sample $file_task_after
echo echo
fi fi
if [[ ! -f $file_extra_shell ]]; then if [[ ! -f $file_extra_shell ]]; then
echo -e "复制一份 $file_extra_sample$file_extra_shell\n" echo -e "复制一份 $file_extra_sample$file_extra_shell\n"
cp -fv $file_extra_sample $file_extra_shell cp -fv $file_extra_sample $file_extra_shell
echo echo
fi fi
if [[ ! -s $file_auth_user ]]; then if [[ ! -s $file_auth_user ]]; then
echo -e "复制一份 $file_auth_sample$file_auth_user\n" echo -e "复制一份 $file_auth_sample$file_auth_user\n"
cp -fv $file_auth_sample $file_auth_user cp -fv $file_auth_sample $file_auth_user
echo 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" echo -e "复制一份 $file_notify_py_sample$file_notify_py\n"
cp -fv $file_notify_py_sample $file_notify_py cp -fv $file_notify_py_sample $file_notify_py
echo 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" echo -e "复制一份 $file_notify_js_sample$file_notify_js\n"
cp -fv $file_notify_js_sample $file_notify_js cp -fv $file_notify_js_sample $file_notify_js
echo 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" echo -e "检测到默认nginx配置文件,清空...\n"
cat /dev/null >/etc/nginx/conf.d/default.conf cat /dev/null >/etc/nginx/conf.d/default.conf
echo 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" echo -e "复制一份 $file_notify_js_sample$dep_notify_js\n"
cp -fv $file_notify_js_sample $dep_notify_js cp -fv $file_notify_js_sample $dep_notify_js
echo 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" echo -e "复制一份 $file_notify_py_sample$dep_notify_py\n"
cp -fv $file_notify_py_sample $dep_notify_py cp -fv $file_notify_py_sample $dep_notify_py
echo echo
fi fi
} }
npm_install_sub() { npm_install_sub() {
if [ $is_termux -eq 1 ]; then if [ $is_termux -eq 1 ]; then
npm install --production --no-bin-links npm install --production --no-bin-links
elif ! type pnpm &>/dev/null; then elif ! type pnpm &>/dev/null; then
npm install --production npm install --production
else else
pnpm install --loglevel error --production pnpm install --loglevel error --production
fi fi
} }
npm_install_1() { npm_install_1() {
local dir_current=$(pwd) local dir_current=$(pwd)
local dir_work=$1 local dir_work=$1
cd $dir_work cd $dir_work
echo -e "运行 npm install...\n" echo -e "运行 npm install...\n"
npm_install_sub npm_install_sub
[[ $? -ne 0 ]] && echo -e "\nnpm install 运行不成功,请进入 $dir_work 目录后手动运行 npm install...\n" [[ $? -ne 0 ]] && echo -e "\nnpm install 运行不成功,请进入 $dir_work 目录后手动运行 npm install...\n"
cd $dir_current cd $dir_current
} }
npm_install_2() { npm_install_2() {
local dir_current=$(pwd) local dir_current=$(pwd)
local dir_work=$1 local dir_work=$1
cd $dir_work cd $dir_work
echo -e "检测到 $dir_work 的依赖包有变化,运行 npm install...\n" echo -e "检测到 $dir_work 的依赖包有变化,运行 npm install...\n"
npm_install_sub npm_install_sub
if [[ $? -ne 0 ]]; then if [[ $? -ne 0 ]]; then
echo -e "\n安装 $dir_work 的依赖包运行不成功,再次尝试一遍...\n" echo -e "\n安装 $dir_work 的依赖包运行不成功,再次尝试一遍...\n"
npm_install_1 $dir_work npm_install_1 $dir_work
fi fi
cd $dir_current cd $dir_current
} }
diff_and_copy() { diff_and_copy() {
local copy_source=$1 local copy_source=$1
local copy_to=$2 local copy_to=$2
if [[ ! -s $copy_to ]] || [[ $(diff $copy_source $copy_to) ]]; then if [[ ! -s $copy_to ]] || [[ $(diff $copy_source $copy_to) ]]; then
cp -f $copy_source $copy_to cp -f $copy_source $copy_to
fi fi
} }
update_depend() { update_depend() {
local dir_current=$(pwd) local dir_current=$(pwd)
if [[ ! -s $dir_scripts/package.json ]] || [[ $(diff $dir_sample/package.json $dir_scripts/package.json) ]]; then if [[ ! -s $dir_scripts/package.json ]] || [[ $(diff $dir_sample/package.json $dir_scripts/package.json) ]]; then
cp -f $dir_sample/package.json $dir_scripts/package.json cp -f $dir_sample/package.json $dir_scripts/package.json
npm_install_2 $dir_scripts npm_install_2 $dir_scripts
fi fi
cd $dir_current cd $dir_current
} }
git_clone_scripts() { git_clone_scripts() {
local url=$1 local url=$1
local dir=$2 local dir=$2
local branch=$3 local branch=$3
[[ $branch ]] && local part_cmd="-b $branch " [[ $branch ]] && local part_cmd="-b $branch "
echo -e "开始克隆仓库 $url$dir\n" echo -e "开始克隆仓库 $url$dir\n"
set_proxy set_proxy
git clone $part_cmd $url $dir git clone $part_cmd $url $dir
exit_status=$? exit_status=$?
unset_proxy unset_proxy
} }
git_pull_scripts() { git_pull_scripts() {
local dir_current=$(pwd) local dir_current=$(pwd)
local dir_work="$1" local dir_work="$1"
local branch="$2" local branch="$2"
cd $dir_work cd $dir_work
echo -e "开始更新仓库:$dir_work\n" echo -e "开始更新仓库:$dir_work\n"
set_proxy set_proxy
git fetch --all git fetch --all
exit_status=$? exit_status=$?
git pull &>/dev/null git pull &>/dev/null
unset_proxy unset_proxy
reset_branch "$branch" reset_branch "$branch"
cd $dir_current cd $dir_current
} }
reset_romote_url() { reset_romote_url() {
local dir_current=$(pwd) local dir_current=$(pwd)
local dir_work=$1 local dir_work=$1
local url=$2 local url=$2
local branch="$3" local branch="$3"
if [[ -d "$dir_work/.git" ]]; then if [[ -d "$dir_work/.git" ]]; then
cd $dir_work cd $dir_work
[[ -f ".git/index.lock" ]] && rm -f .git/index.lock >/dev/null [[ -f ".git/index.lock" ]] && rm -f .git/index.lock >/dev/null
git remote set-url origin $url &>/dev/null git remote set-url origin $url &>/dev/null
local part_cmd="" local part_cmd=""
reset_branch "$branch" reset_branch "$branch"
cd $dir_current cd $dir_current
fi fi
} }
reset_branch() { reset_branch() {
local branch="$1" local branch="$1"
if [[ $branch ]]; then if [[ $branch ]]; then
part_cmd="origin/${branch}" part_cmd="origin/${branch}"
git checkout -B "$branch" git checkout -B "$branch" &>/dev/null
git branch --set-upstream-to=$part_cmd $branch git branch --set-upstream-to=$part_cmd $branch &>/dev/null
fi fi
git reset --hard $part_cmd &>/dev/null git reset --hard $part_cmd &>/dev/null
} }
random_range() { random_range() {
local beg=$1 local beg=$1
local end=$2 local end=$2
echo $((RANDOM % ($end - $beg) + $beg)) echo $((RANDOM % ($end - $beg) + $beg))
} }
reload_pm2() { reload_pm2() {
pm2 l &>/dev/null pm2 l &>/dev/null
echo -e "启动面板服务\n" echo -e "启动面板服务\n"
pm2 delete panel --source-map-support --time &>/dev/null pm2 delete panel --source-map-support --time &>/dev/null
pm2 start $dir_static/build/app.js -n panel --source-map-support --time &>/dev/null pm2 start $dir_static/build/app.js -n panel --source-map-support --time &>/dev/null
echo -e "启动定时任务服务\n" echo -e "启动定时任务服务\n"
pm2 delete schedule --source-map-support --time &>/dev/null pm2 delete schedule --source-map-support --time &>/dev/null
pm2 start $dir_static/build/schedule.js -n schedule --source-map-support --time &>/dev/null pm2 start $dir_static/build/schedule.js -n schedule --source-map-support --time &>/dev/null
echo -e "启动公开服务\n" echo -e "启动公开服务\n"
pm2 delete public --source-map-support --time &>/dev/null pm2 delete public --source-map-support --time &>/dev/null
pm2 start $dir_static/build/public.js -n public --source-map-support --time &>/dev/null pm2 start $dir_static/build/public.js -n public --source-map-support --time &>/dev/null
} }
diff_time() { diff_time() {
local format="$1" local format="$1"
local begin_time="$2" local begin_time="$2"
local end_time="$3" local end_time="$3"
if [[ $is_macos -eq 1 ]]; then if [[ $is_macos -eq 1 ]]; then
diff_time=$(($(date -j -f "$format" "$end_time" +%s) - $(date -j -f "$format" "$begin_time" +%s))) diff_time=$(($(date -j -f "$format" "$end_time" +%s) - $(date -j -f "$format" "$begin_time" +%s)))
else else
diff_time=$(($(date +%s -d "$end_time") - $(date +%s -d "$begin_time"))) diff_time=$(($(date +%s -d "$end_time") - $(date +%s -d "$begin_time")))
fi fi
echo "$diff_time" echo "$diff_time"
} }
format_time() { format_time() {
local format="$1" local format="$1"
local time="$2" local time="$2"
if [[ $is_macos -eq 1 ]]; then if [[ $is_macos -eq 1 ]]; then
echo $(date -j -f "$format" "$time" "+%Y-%m-%d %H:%M:%S") echo $(date -j -f "$format" "$time" "+%Y-%m-%d %H:%M:%S")
else else
echo $(date -d "$time" "+%Y-%m-%d %H:%M:%S") echo $(date -d "$time" "+%Y-%m-%d %H:%M:%S")
fi fi
} }
format_log_time() { format_log_time() {
local format="$1" local format="$1"
local time="$2" local time="$2"
if [[ $is_macos -eq 1 ]]; then if [[ $is_macos -eq 1 ]]; then
echo $(date -j -f "$format" "$time" "+%Y-%m-%d-%H-%M-%S") echo $(date -j -f "$format" "$time" "+%Y-%m-%d-%H-%M-%S")
else else
echo $(date -d "$time" "+%Y-%m-%d-%H-%M-%S") echo $(date -d "$time" "+%Y-%m-%d-%H-%M-%S")
fi fi
} }
format_timestamp() { format_timestamp() {
local format="$1" local format="$1"
local time="$2" local time="$2"
if [[ $is_macos -eq 1 ]]; then if [[ $is_macos -eq 1 ]]; then
echo $(date -j -f "$format" "$time" "+%s") echo $(date -j -f "$format" "$time" "+%s")
else else
echo $(date -d "$time" "+%s") echo $(date -d "$time" "+%s")
fi fi
}
patch_version() {
if [[ $PipMirror ]]; then
pip3 config set global.index-url $PipMirror
fi
if [[ $NpmMirror ]]; then
npm config set registry $NpmMirror
fi
# 兼容pnpm@7
pnpm setup &>/dev/null
source ~/.bashrc
pnpm install -g &>/dev/null
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 ! type ts-node &>/dev/null; then
pnpm add -g ts-node typescript tslib
fi
git config --global pull.rebase false
cp -f $dir_root/.env.example $dir_root/.env
if [[ -d "$dir_root/db" ]]; then
echo -e "检测到旧的db目录,拷贝到data目录...\n"
cp -rf $dir_root/config $dir_root/data
echo
fi
if [[ -d "$dir_root/scripts" ]]; then
echo -e "检测到旧的scripts目录,拷贝到data目录...\n"
cp -rf $dir_root/scripts $dir_root/data
echo
fi
if [[ -d "$dir_root/log" ]]; then
echo -e "检测到旧的log目录,拷贝到data目录...\n"
cp -rf $dir_root/log $dir_root/data
echo
fi
if [[ -d "$dir_root/config" ]]; then
echo -e "检测到旧的config目录,拷贝到data目录...\n"
cp -rf $dir_root/config $dir_root/data
echo
fi
} }
init_env init_env
@@ -429,4 +486,4 @@ detect_macos
define_cmd define_cmd
fix_config fix_config
import_config $1 >$task_error_log_path 2>&1 import_config $1 2>$task_error_log_path
+62 -388
View File
@@ -7,401 +7,75 @@ dir_shell=$QL_DIR/shell
## 选择python3还是node ## 选择python3还是node
define_program() { define_program() {
local file_param=$1 local file_param=$1
if [[ $file_param == *.js ]]; then if [[ $file_param == *.js ]]; then
which_program="node" which_program="node"
elif [[ $file_param == *.py ]] || [[ $file_param == *.pyc ]]; then elif [[ $file_param == *.py ]] || [[ $file_param == *.pyc ]]; then
which_program="python3" which_program="python3"
elif [[ $file_param == *.sh ]]; then elif [[ $file_param == *.sh ]]; then
which_program="bash" which_program="bash"
elif [[ $file_param == *.ts ]]; then elif [[ $file_param == *.ts ]]; then
which_program="ts-node-transpile-only" which_program="ts-node-transpile-only"
else
which_program=""
fi
}
handle_log_path() {
local file_param=$1
if [[ -z $file_param ]];then
file_param="task"
fi
local suffix=""
if [[ ! -z $ID ]]; then
suffix="_${ID}"
fi
time=$(date "+$time_format")
log_time=$(format_log_time "$time_format" "$time")
log_dir_tmp="${file_param##*/}"
if [[ $file_param =~ "/" ]]; then
if [[ $file_param == /* ]]; then
log_dir_tmp_path="${file_param:1}"
else else
which_program="" log_dir_tmp_path="${file_param}"
fi fi
fi
log_dir_tmp_path="${log_dir_tmp_path%/*}"
log_dir_tmp_path="${log_dir_tmp_path##*/}"
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
log_dir="${log_dir_tmp%.*}${suffix}"
log_path="$log_dir/$log_time.log"
cmd=">> $dir_log/$log_path 2>&1"
[[ "$show_log" == "true" ]] && cmd=""
make_dir "$dir_log/$log_dir"
} }
random_delay() { format_params() {
local random_delay_max=$RandomDelay time_format="%Y-%m-%d %H:%M:%S"
if [[ $random_delay_max ]] && [[ $random_delay_max -gt 0 ]]; then timeoutCmd=""
local file_param=$1 if type timeout &>/dev/null; then
local file_extensions=${RandomDelayFileExtensions-"js"} timeoutCmd="timeout -k 10s $command_timeout_time "
local ignored_minutes=${RandomDelayIgnoredMinutes-"0 30"} fi
params=$(echo "$@" | sed -E 's/([^ ])&([^ ])/\1\\\&\2/g')
if [[ -n $file_extensions ]]; then
if ! echo "$file_param" | grep -qE "\.${file_extensions// /$|\\.}$"; then
# echo -e "\n当前文件需要准点运行, 放弃随机延迟\n"
return
fi
fi
local current_min
current_min=$(date "+%-M")
for minute in $ignored_minutes; do
if [[ $current_min -eq $minute ]]; then
# echo -e "\n当前时间需要准点运行, 放弃随机延迟\n"
return
fi
done
local delay_second=$(($(gen_random_num "$random_delay_max") + 1))
echo -e "\n命令未添加 \"now\",随机延迟 $delay_second 秒后再执行任务,如需立即终止,请按 CTRL+C...\n"
sleep $delay_second
fi
} }
## scripts目录下所有可运行脚本数组 show_log="false"
gen_array_scripts() { while getopts ":l" opt; do
local dir_current=$(pwd) case $opt in
local i="-1" l)
cd $dir_scripts show_log="true"
for file in $(ls); do ;;
if [[ -f $file ]] && [[ $file == *.js && $file != sendNotify.js ]]; then esac
let i++ done
array_scripts[i]=$(echo "$file" | perl -pe "s|$dir_scripts/||g") [[ "$show_log" == "true" ]] && shift $(($OPTIND - 1))
array_scripts_name[i]=$(grep "new Env" $file | awk -F "'|\"" '{print $2}' | head -1)
[[ -z ${array_scripts_name[i]} ]] && array_scripts_name[i]="<未识别出活动名称>"
fi
done
cd $dir_current
}
## 使用说明 format_params "$@"
usage() { define_program "$@"
define_cmd handle_log_path "$@"
gen_array_scripts
echo -e "task命令运行本程序自动添加进crontab的脚本,需要输入脚本的绝对路径或去掉 “$dir_scripts/” 目录后的相对路径(定时任务中请写作相对路径),用法为:"
echo -e "1.$cmd_task <file_name> # 依次执行,如果设置了随机延迟,将随机延迟一定秒数"
echo -e "2.$cmd_task <file_name> now # 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日志,同时记录在日志文件中"
echo -e "3.$cmd_task <file_name> conc <环境变量名称> <账号编号,空格分隔>(可选的) # 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日志,直接记录在日志文件中,且可指定账号执行"
echo -e "4.$cmd_task <file_name> desi <环境变量名称> <账号编号,空格分隔> # 指定账号执行,无论是否设置了随机延迟,均立即运行"
if [[ ${#array_scripts[*]} -gt 0 ]]; then
echo -e "\n当前有以下脚本可以运行:"
for ((i = 0; i < ${#array_scripts[*]}; i++)); do
echo -e "$(($i + 1)). ${array_scripts_name[i]}${array_scripts[i]}"
done
else
echo -e "\n暂无脚本可以执行"
fi
}
## run nohup,$1:文件名,不含路径,带后缀 eval . $dir_shell/otask.sh "$params" "$cmd"
run_nohup() { [[ -f "$dir_log/$log_path" ]] && cat "$dir_log/$log_path"
local file_name=$1
nohup node $file_name &>$log_path &
}
## 正常运行单个脚本,$1:传入参数
run_normal() {
local file_param=$1
define_program "$file_param"
if [[ $# -eq 1 ]]; then
random_delay "$file_param"
fi
local time=$(date "+$time_format")
log_time=$(format_log_time "$time_format" "$time")
log_dir_tmp="${file_param##*/}"
if [[ $file_param =~ "/" ]]; then
if [[ $file_param == /* ]]; then
log_dir_tmp_path="${file_param:1}"
else
log_dir_tmp_path="${file_param}"
fi
fi
log_dir_tmp_path="${log_dir_tmp_path%/*}"
log_dir_tmp_path="${log_dir_tmp_path##*/}"
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
log_dir="${log_dir_tmp%.*}"
log_path="$log_dir/$log_time.log"
cmd=">> $dir_log/$log_path 2>&1"
[[ "$show_log" == "true" ]] && cmd=""
make_dir "$dir_log/$log_dir"
local begin_time=$(format_time "$time_format" "$time")
local begin_timestamp=$(format_timestamp "$time_format" "$time")
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
local id=$(cat $list_crontab_user | grep -E "$cmd_task $file_param" | perl -pe "s|.*ID=(.*) $cmd_task $file_param\.*|\1|" | head -1 | awk -F " " '{print $1}')
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
eval . $file_task_before "$@" $cmd
cd $dir_scripts
local relative_path="${file_param%/*}"
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path}
file_param=${file_param/$relative_path\//}
fi
eval $timeoutCmd $which_program $file_param $cmd
eval . $file_task_after "$@" $cmd
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
local end_timestamp=$(date "+%s")
local diff_time=$(expr $end_timestamp - $begin_timestamp)
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd
}
## 并发执行时,设定的 RandomDelay 不会生效,即所有任务立即执行
run_concurrent() {
local file_param="$1"
local env_param="$2"
local num_param=$(echo "$3" | perl -pe "s|.*$2(.*)|\1|")
if [[ ! $env_param ]]; then
echo -e "\n 缺少并发运行的环境变量参数"
exit 1
fi
local envs=$(eval echo "\$${env_param}")
local array=($(echo $envs | sed 's/&/ /g'))
local tempArr=$(echo $num_param | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g")
local runArr=($(eval echo $tempArr))
runArr=($(awk -v RS=' ' '!a[$1]++' <<< ${runArr[@]}))
local n=0
for i in ${runArr[@]}; do
array_run[n]=${array[$i - 1]}
let n++
done
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
define_program "$file_param"
local time=$(date "+$time_format")
log_time=$(format_log_time "$time_format" "$time")
log_dir_tmp="${file_param##*/}"
if [[ $file_param =~ "/" ]]; then
if [[ $file_param == /* ]]; then
log_dir_tmp_path="${file_param:1}"
else
log_dir_tmp_path="${file_param}"
fi
fi
log_dir_tmp_path="${log_dir_tmp_path%/*}"
log_dir_tmp_path="${log_dir_tmp_path##*/}"
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
log_dir="${log_dir_tmp%.*}"
log_path="$log_dir/$log_time.log"
cmd=">> $dir_log/$log_path 2>&1"
[[ "$show_log" == "true" ]] && cmd=""
make_dir "$dir_log/$log_dir"
local begin_time=$(format_time "$time_format" "$time")
local begin_timestamp=$(format_timestamp "$time_format" "$time")
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
local id=$(cat $list_crontab_user | grep -E "$cmd_task $file_param" | perl -pe "s|.*ID=(.*) $cmd_task $file_param\.*|\1|" | head -1 | awk -F " " '{print $1}')
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
eval . $file_task_before "$@" $cmd
local envs=$(eval echo "\$${env_param}")
local array=($(echo $envs | sed 's/&/ /g'))
single_log_time=$(date "+%Y-%m-%d-%H-%M-%S.%N")
cd $dir_scripts
local relative_path="${file_param%/*}"
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path}
file_param=${file_param/$relative_path\//}
fi
for i in "${!array[@]}"; do
export ${env_param}=${array[i]}
single_log_path="$dir_log/$log_dir/${single_log_time}_$((i + 1)).log"
eval $timeoutCmd $which_program $file_param &>$single_log_path &
done
wait
for i in "${!array[@]}"; do
single_log_path="$dir_log/$log_dir/${single_log_time}_$((i + 1)).log"
eval cat $single_log_path $cmd
[[ -f $single_log_path ]] && rm -f $single_log_path
done
eval . $file_task_after "$@" $cmd
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
local end_timestamp=$(date "+%s")
local diff_time=$(( $end_timestamp - $begin_timestamp ))
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd
}
run_designated() {
local file_param="$1"
local env_param="$2"
local num_param=$(echo "$3" | perl -pe "s|.*$2(.*)|\1|")
if [[ ! $env_param ]] || [[ ! $num_param ]]; then
echo -e "\n 缺少单独运行的参数 task xxx.js desi Test 1 3"
exit 1
fi
define_program "$file_param"
local time=$(date "+$time_format")
log_time=$(format_log_time "$time_format" "$time")
log_dir_tmp="${file_param##*/}"
if [[ $file_param =~ "/" ]]; then
if [[ $file_param == /* ]]; then
log_dir_tmp_path="${file_param:1}"
else
log_dir_tmp_path="${file_param}"
fi
fi
log_dir_tmp_path="${log_dir_tmp_path%/*}"
log_dir_tmp_path="${log_dir_tmp_path##*/}"
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
log_dir="${log_dir_tmp%.*}"
log_path="$log_dir/$log_time.log"
cmd=">> $dir_log/$log_path 2>&1"
[[ "$show_log" == "true" ]] && cmd=""
make_dir "$dir_log/$log_dir"
local begin_time=$(format_time "$time_format" "$time")
local begin_timestamp=$(format_timestamp "$time_format" "$time")
local envs=$(eval echo "\$${env_param}")
local array=($(echo $envs | sed 's/&/ /g'))
local tempArr=$(echo $num_param | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g")
local runArr=($(eval echo $tempArr))
runArr=($(awk -v RS=' ' '!a[$1]++' <<< ${runArr[@]}))
local n=0
for i in ${runArr[@]}; do
array_run[n]=${array[$i - 1]}
let n++
done
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
local id=$(cat $list_crontab_user | grep -E "$cmd_task $file_param" | perl -pe "s|.*ID=(.*) $cmd_task $file_param\.*|\1|" | head -1 | awk -F " " '{print $1}')
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
eval . $file_task_before "$@" $cmd
cd $dir_scripts
local relative_path="${file_param%/*}"
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path}
file_param=${file_param/$relative_path\//}
fi
eval $timeoutCmd $which_program $file_param $cmd
eval . $file_task_after "$@" $cmd
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
local end_timestamp=$(date "+%s")
local diff_time=$(( $end_timestamp - $begin_timestamp ))
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd
}
## 运行其他命令
run_else() {
local file_param="$1"
define_program "$file_param"
local time=$(date "+$time_format")
log_time=$(format_log_time "$time_format" "$time")
log_dir_tmp="${file_param##*/}"
if [[ $file_param =~ "/" ]]; then
if [[ $file_param == /* ]]; then
log_dir_tmp_path="${file_param:1}"
else
log_dir_tmp_path="${file_param}"
fi
fi
log_dir_tmp_path="${log_dir_tmp_path%/*}"
log_dir_tmp_path="${log_dir_tmp_path##*/}"
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
log_dir="${log_dir_tmp%.*}"
log_path="$log_dir/$log_time.log"
cmd=">> $dir_log/$log_path 2>&1"
[[ "$show_log" == "true" ]] && cmd=""
make_dir "$dir_log/$log_dir"
local begin_time=$(format_time "$time_format" "$time")
local begin_timestamp=$(format_timestamp "$time_format" "$time")
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
local id=$(cat $list_crontab_user | grep -E "$cmd_task $@" | perl -pe "s|.*ID=(.*) $cmd_task $@\.*|\1|" | head -1 | awk -F " " '{print $1}')
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
eval . $file_task_before "$@" $cmd
cd $dir_scripts
local relative_path="${file_param%/*}"
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path}
file_param=${file_param/$relative_path\//}
fi
shift
eval $timeoutCmd $which_program "$file_param" "$@" $cmd
eval . $file_task_after "$file_param" "$@" $cmd
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
local end_timestamp=$(date "+%s")
local diff_time=$(( $end_timestamp - $begin_timestamp ))
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd
}
## 命令检测
main() {
show_log="false"
while getopts ":l" opt
do
case $opt in
l)
show_log="true"
;;
esac
done
[[ "$show_log" == "true" ]] && shift $(($OPTIND - 1))
timeoutCmd=""
if type timeout &>/dev/null; then
timeoutCmd="timeout -k 10s $command_timeout_time "
fi
time_format="%Y-%m-%d %H:%M:%S"
if [[ $1 == *.js ]] || [[ $1 == *.py ]] || [[ $1 == *.pyc ]] || [[ $1 == *.sh ]] || [[ $1 == *.ts ]]; then
case $# in
1)
run_normal "$1"
;;
*)
case $2 in
now)
run_normal "$1" "$2"
;;
conc)
run_concurrent "$1" "$3" "$*"
;;
desi)
run_designated "$1" "$3" "$*"
;;
*)
run_else "$@"
;;
esac
;;
esac
[[ -f "$dir_log/$log_path" ]] && cat "$dir_log/$log_path"
elif [[ $# -eq 0 ]]; then
echo
usage
else
run_else "$@"
fi
}
main "$@"
exit 0 exit 0
+410 -442
View File
@@ -8,552 +8,520 @@ send_mark=$dir_shell/send_mark
## 检测cron的差异,$1:脚本清单文件路径,$2:cron任务清单文件路径,$3:增加任务清单文件路径,$4:删除任务清单文件路径 ## 检测cron的差异,$1:脚本清单文件路径,$2:cron任务清单文件路径,$3:增加任务清单文件路径,$4:删除任务清单文件路径
diff_cron() { diff_cron() {
local list_scripts="$1" local list_scripts="$1"
local list_task="$2" local list_task="$2"
local list_add="$3" local list_add="$3"
local list_drop="$4" local list_drop="$4"
if [[ -s $list_task ]] && [[ -s $list_scripts ]]; then if [[ -s $list_task ]] && [[ -s $list_scripts ]]; then
grep -vwf $list_task $list_scripts >$list_add grep -vwf $list_task $list_scripts >$list_add
grep -vwf $list_scripts $list_task >$list_drop grep -vwf $list_scripts $list_task >$list_drop
fi fi
if [[ ! -s $list_task ]] && [[ -s $list_scripts ]]; then if [[ ! -s $list_task ]] && [[ -s $list_scripts ]]; then
cp -f $list_scripts $list_add cp -f $list_scripts $list_add
fi fi
if [[ ! -s $list_scripts ]] && [[ -s $list_task ]]; then if [[ ! -s $list_scripts ]] && [[ -s $list_task ]]; then
cp -f $list_task $list_drop cp -f $list_task $list_drop
fi fi
} }
## 检测配置文件版本 ## 检测配置文件版本
detect_config_version() { detect_config_version() {
## 识别出两个文件的版本号 ## 识别出两个文件的版本号
ver_config_sample=$(grep " Version: " $file_config_sample | perl -pe "s|.+v((\d+\.?){3})|\1|") ver_config_sample=$(grep " Version: " $file_config_sample | perl -pe "s|.+v((\d+\.?){3})|\1|")
[[ -f $file_config_user ]] && ver_config_user=$(grep " Version: " $file_config_user | perl -pe "s|.+v((\d+\.?){3})|\1|") [[ -f $file_config_user ]] && ver_config_user=$(grep " Version: " $file_config_user | perl -pe "s|.+v((\d+\.?){3})|\1|")
## 删除旧的发送记录文件 ## 删除旧的发送记录文件
[[ -f $send_mark ]] && [[ $(cat $send_mark) != $ver_config_sample ]] && rm -f $send_mark [[ -f $send_mark ]] && [[ $(cat $send_mark) != $ver_config_sample ]] && rm -f $send_mark
## 识别出更新日期和更新内容 ## 识别出更新日期和更新内容
update_date=$(grep " Date: " $file_config_sample | awk -F ": " '{print $2}') update_date=$(grep " Date: " $file_config_sample | awk -F ": " '{print $2}')
update_content=$(grep " Update Content: " $file_config_sample | awk -F ": " '{print $2}') update_content=$(grep " Update Content: " $file_config_sample | awk -F ": " '{print $2}')
## 如果是今天,并且版本号不一致,则发送通知 ## 如果是今天,并且版本号不一致,则发送通知
if [[ -f $file_config_user ]] && [[ $ver_config_user != $ver_config_sample ]] && [[ $update_date == $(date "+%Y-%m-%d") ]]; then if [[ -f $file_config_user ]] && [[ $ver_config_user != $ver_config_sample ]] && [[ $update_date == $(date "+%Y-%m-%d") ]]; then
if [[ ! -f $send_mark ]]; then if [[ ! -f $send_mark ]]; then
local notify_title="配置文件更新通知" local notify_title="配置文件更新通知"
local notify_content="更新日期: $update_date\n用户版本: $ver_config_user\n新的版本: $ver_config_sample\n更新内容: $update_content\n更新说明: 如需使用新功能请对照config.sample.sh,将相关新参数手动增加到你自己的config.sh中,否则请无视本消息。本消息只在该新版本配置文件更新当天发送一次。\n" local notify_content="更新日期: $update_date\n用户版本: $ver_config_user\n新的版本: $ver_config_sample\n更新内容: $update_content\n更新说明: 如需使用新功能请对照config.sample.sh,将相关新参数手动增加到你自己的config.sh中,否则请无视本消息。本消息只在该新版本配置文件更新当天发送一次。\n"
echo -e $notify_content echo -e $notify_content
notify_api "$notify_title" "$notify_content" notify_api "$notify_title" "$notify_content"
[[ $? -eq 0 ]] && echo $ver_config_sample >$send_mark [[ $? -eq 0 ]] && echo $ver_config_sample >$send_mark
fi
else
[[ -f $send_mark ]] && rm -f $send_mark
fi fi
else
[[ -f $send_mark ]] && rm -f $send_mark
fi
} }
## 输出是否有新的或失效的定时任务,$1:新的或失效的任务清单文件路径,$2:新/失效 ## 输出是否有新的或失效的定时任务,$1:新的或失效的任务清单文件路径,$2:新/失效
output_list_add_drop() { output_list_add_drop() {
local list=$1 local list=$1
local type=$2 local type=$2
if [[ -s $list ]]; then if [[ -s $list ]]; then
echo -e "检测到有$type的定时任务\n" echo -e "检测到有$type的定时任务\n"
cat $list cat $list
echo echo
fi fi
} }
## 自动删除失效的脚本与定时任务,需要:1.AutoDelCron 设置为 true2.正常更新js脚本,没有报错;3.存在失效任务 ## 自动删除失效的脚本与定时任务,需要:1.AutoDelCron 设置为 true2.正常更新js脚本,没有报错;3.存在失效任务
## $1:失效任务清单文件路径 ## $1:失效任务清单文件路径
del_cron() { del_cron() {
local list_drop=$1 local list_drop=$1
local path=$2 local path=$2
local detail="" local detail=""
local ids="" local ids=""
echo -e "开始尝试自动删除失效的定时任务...\n" echo -e "开始尝试自动删除失效的定时任务...\n"
for cron in $(cat $list_drop); do for cron in $(cat $list_drop); do
local id=$(cat $list_crontab_user | grep -E "$cmd_task $cron" | perl -pe "s|.*ID=(.*) $cmd_task $cron\.*|\1|" | head -1 | head -1 | awk -F " " '{print $1}') local id=$(cat $list_crontab_user | grep -E "$cmd_task $cron" | perl -pe "s|.*ID=(.*) $cmd_task $cron\.*|\1|" | head -1 | head -1 | awk -F " " '{print $1}')
if [[ $ids ]]; then
ids="$ids,\"$id\""
else
ids="\"$id\""
fi
cron_file="$dir_scripts/${cron}"
if [[ -f $cron_file ]]; then
cron_name=$(grep "new Env" $cron_file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1)
rm -f $cron_file
fi
[[ -z $cron_name ]] && cron_name="$cron"
if [[ $detail ]]; then
detail="${detail}\n${cron_name}"
else
detail="${cron_name}"
fi
done
if [[ $ids ]]; then if [[ $ids ]]; then
result=$(del_cron_api "$ids") ids="$ids,\"$id\""
notify_api "$path 删除任务${result}" "$detail" else
ids="\"$id\""
fi fi
cron_file="$dir_scripts/${cron}"
if [[ -f $cron_file ]]; then
cron_name=$(grep "new Env" $cron_file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1)
rm -f $cron_file
fi
[[ -z $cron_name ]] && cron_name="$cron"
if [[ $detail ]]; then
detail="${detail}\n${cron_name}"
else
detail="${cron_name}"
fi
done
if [[ $ids ]]; then
result=$(del_cron_api "$ids")
notify_api "$path 删除任务${result}" "$detail"
fi
} }
## 自动增加定时任务,需要:1.AutoAddCron 设置为 true2.正常更新js脚本,没有报错;3.存在新任务;4.crontab.list存在并且不为空 ## 自动增加定时任务,需要:1.AutoAddCron 设置为 true2.正常更新js脚本,没有报错;3.存在新任务;4.crontab.list存在并且不为空
## $1:新任务清单文件路径 ## $1:新任务清单文件路径
add_cron() { add_cron() {
local list_add=$1 local list_add=$1
local path=$2 local path=$2
echo -e "开始尝试自动添加定时任务...\n" echo -e "开始尝试自动添加定时任务...\n"
local detail="" local detail=""
cd $dir_scripts cd $dir_scripts
for file in $(cat $list_add); do for file in $(cat $list_add); do
local file_name=${file/${path}\//} local file_name=${file/${path}\//}
file_name=${file_name/${path}\_/} file_name=${file_name/${path}\_/}
if [[ -f $file ]]; then if [[ -f $file ]]; then
cron_line=$( cron_line=$(
perl -ne "{ perl -ne "{
print if /.*([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*]( |,|\").*$file_name/ print if /.*([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*]( |,|\").*$file_name/
}" $file | }" $file |
perl -pe "{ perl -pe "{
s|[^\d\*]*(([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*])( \|,\|\").*/?$file_name.*|\1|g; s|[^\d\*]*(([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*])( \|,\|\").*/?$file_name.*|\1|g;
s|\*([\d\*])(.*)|\1\2|g; s|\*([\d\*])(.*)|\1\2|g;
s| | |g; s| | |g;
}" | sort -u | head -1 }" | sort -u | head -1
) )
cron_name=$(grep "new Env" $file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1) cron_name=$(grep "new Env" $file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1)
[[ -z $cron_name ]] && cron_name="$file_name" [[ -z $cron_name ]] && cron_name="$file_name"
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $file | awk -F ":" '{print $2}' | head -1 | xargs) [[ -z $cron_line ]] && cron_line=$(grep "cron:" $file | awk -F ":" '{print $2}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line=$(grep "cron " $file | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs) [[ -z $cron_line ]] && cron_line=$(grep "cron " $file | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line="$default_cron" [[ -z $cron_line ]] && cron_line="$default_cron"
result=$(add_cron_api "$cron_line:$cmd_task $file:$cron_name") result=$(add_cron_api "$cron_line:$cmd_task $file:$cron_name")
echo -e "$result" echo -e "$result"
if [[ $detail ]]; then if [[ $detail ]]; then
detail="${detail}${result}\n" detail="${detail}${result}\n"
else else
detail="${result}\n" detail="${result}\n"
fi fi
fi fi
done done
notify_api "$path 新增任务" "$detail" notify_api "$path 新增任务" "$detail"
} }
## 更新仓库 ## 更新仓库
update_repo() { update_repo() {
local url="$1" local url="$1"
local path="$2" local path="$2"
local blackword="$3" local blackword="$3"
local dependence="$4" local dependence="$4"
local branch="$5" local branch="$5"
local extensions="$6" local extensions="$6"
local tmp="${url%/*}" local tmp="${url%/*}"
local authorTmp1="${tmp##*/}" local authorTmp1="${tmp##*/}"
local authorTmp2="${authorTmp1##*:}" local authorTmp2="${authorTmp1##*:}"
local author="${authorTmp2##*.}" local author="${authorTmp2##*.}"
local repo_path="${dir_repo}/${uniq_path}" local repo_path="${dir_repo}/${uniq_path}"
make_dir "${dir_scripts}/${uniq_path}" make_dir "${dir_scripts}/${uniq_path}"
local formatUrl="$url" local formatUrl="$url"
if [[ -d ${repo_path}/.git ]]; then if [[ -d ${repo_path}/.git ]]; then
reset_romote_url ${repo_path} "${formatUrl}" "${branch}" reset_romote_url ${repo_path} "${formatUrl}" "${branch}"
git_pull_scripts ${repo_path} "${branch}" git_pull_scripts ${repo_path} "${branch}"
else else
git_clone_scripts "${formatUrl}" ${repo_path} "${branch}" git_clone_scripts "${formatUrl}" ${repo_path} "${branch}"
fi fi
if [[ $exit_status -eq 0 ]]; then if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新${repo_path}成功...\n" echo -e "\n更新${repo_path}成功...\n"
diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions" diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions"
else else
echo -e "\n更新${repo_path}失败,请检查网络...\n" echo -e "\n更新${repo_path}失败,请检查网络...\n"
fi fi
} }
## 更新所有 raw 文件 ## 更新所有 raw 文件
update_raw() { update_raw() {
echo -e "--------------------------------------------------------------\n" echo -e "--------------------------------------------------------------\n"
local url="$1" local url="$1"
local raw_url="$url" local raw_url="$url"
local suffix="${raw_url##*.}" local suffix="${raw_url##*.}"
local raw_file_name="${uniq_path}.${suffix}" local raw_file_name="${uniq_path}.${suffix}"
echo -e "开始下载:${raw_url} \n\n保存路径:$dir_raw/${raw_file_name}\n" echo -e "开始下载:${raw_url} \n\n保存路径:$dir_raw/${raw_file_name}\n"
set_proxy set_proxy
wget -q --no-check-certificate -O "$dir_raw/${raw_file_name}.new" ${raw_url} wget -q --no-check-certificate -O "$dir_raw/${raw_file_name}.new" ${raw_url}
unset_proxy unset_proxy
if [[ $? -eq 0 ]]; then if [[ $? -eq 0 ]]; then
mv "$dir_raw/${raw_file_name}.new" "$dir_raw/${raw_file_name}" mv "$dir_raw/${raw_file_name}.new" "$dir_raw/${raw_file_name}"
echo -e "下载 ${raw_file_name} 成功...\n" echo -e "下载 ${raw_file_name} 成功...\n"
cd $dir_raw cd $dir_raw
local filename="raw_${raw_file_name}" local filename="raw_${raw_file_name}"
local cron_id=$(cat $list_crontab_user | grep -E "$cmd_task $filename" | perl -pe "s|.*ID=(.*) $cmd_task $filename\.*|\1|" | head -1 | head -1 | awk -F " " '{print $1}') local cron_id=$(cat $list_crontab_user | grep -E "$cmd_task $filename" | perl -pe "s|.*ID=(.*) $cmd_task $filename\.*|\1|" | head -1 | head -1 | awk -F " " '{print $1}')
cp -f $raw_file_name $dir_scripts/${filename} cp -f $raw_file_name $dir_scripts/${filename}
cron_line=$( cron_line=$(
perl -ne "{ perl -ne "{
print if /.*([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*]( |,|\").*$raw_file_name/ print if /.*([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*]( |,|\").*$raw_file_name/
}" $raw_file_name | }" $raw_file_name |
perl -pe "{ perl -pe "{
s|[^\d\*]*(([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*])( \|,\|\").*/?$raw_file_name.*|\1|g; s|[^\d\*]*(([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*])( \|,\|\").*/?$raw_file_name.*|\1|g;
s|\*([\d\*])(.*)|\1\2|g; s|\*([\d\*])(.*)|\1\2|g;
s| | |g; s| | |g;
}" | sort -u | head -1 }" | sort -u | head -1
) )
cron_name=$(grep "new Env" $raw_file_name | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1) cron_name=$(grep "new Env" $raw_file_name | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1)
[[ -z $cron_name ]] && cron_name="$raw_file_name" [[ -z $cron_name ]] && cron_name="$raw_file_name"
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $raw_file_name | awk -F ":" '{print $2}' | head -1 | xargs) [[ -z $cron_line ]] && cron_line=$(grep "cron:" $raw_file_name | awk -F ":" '{print $2}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line=$(grep "cron " $raw_file_name | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs) [[ -z $cron_line ]] && cron_line=$(grep "cron " $raw_file_name | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line="$default_cron" [[ -z $cron_line ]] && cron_line="$default_cron"
if [[ -z $cron_id ]]; then if [[ -z $cron_id ]]; then
result=$(add_cron_api "$cron_line:$cmd_task $filename:$cron_name") result=$(add_cron_api "$cron_line:$cmd_task $filename:$cron_name")
echo -e "$result\n" echo -e "$result\n"
notify_api "新增任务通知" "\n$result" notify_api "新增任务通知" "\n$result"
# update_cron_api "$cron_line:$cmd_task $filename:$cron_name:$cron_id" # update_cron_api "$cron_line:$cmd_task $filename:$cron_name:$cron_id"
fi
else
echo -e "下载 ${raw_file_name} 失败,保留之前正常下载的版本...\n"
[[ -f "$dir_raw/${raw_file_name}.new" ]] && rm -f "$dir_raw/${raw_file_name}.new"
fi fi
else
echo -e "下载 ${raw_file_name} 失败,保留之前正常下载的版本...\n"
[[ -f "$dir_raw/${raw_file_name}.new" ]] && rm -f "$dir_raw/${raw_file_name}.new"
fi
} }
## 调用用户自定义的extra.sh ## 调用用户自定义的extra.sh
run_extra_shell() { run_extra_shell() {
if [[ ${EnableExtraShell} == true ]]; then if [[ ${EnableExtraShell} == true ]]; then
if [[ -f $file_extra_shell ]]; then if [[ -f $file_extra_shell ]]; then
echo -e "--------------------------------------------------------------\n" echo -e "--------------------------------------------------------------\n"
. $file_extra_shell . $file_extra_shell
else else
echo -e "$file_extra_shell文件不存在,跳过执行...\n" echo -e "$file_extra_shell文件不存在,跳过执行...\n"
fi
fi fi
fi
} }
## 脚本用法 ## 脚本用法
usage() { usage() {
echo -e "本脚本用法:" echo -e "ql命令使用方法:"
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 # 禁用两步登录"
} }
## 更新qinglong ## 更新qinglong
update_qinglong() { update_qinglong() {
patch_version patch_version &>/dev/null
local no_restart="$1" export isFirstStartServer=false
local all_branch=$(git branch -a)
local primary_branch="master"
if [[ "${all_branch}" =~ "${current_branch}" ]]; then
primary_branch="${current_branch}"
fi
[[ -f $dir_root/package.json ]] && ql_depend_old=$(cat $dir_root/package.json)
reset_romote_url ${dir_root} "https://github.com/whyour/qinglong.git" ${primary_branch}
git_pull_scripts $dir_root ${primary_branch}
if [[ $exit_status -eq 0 ]]; then local all_branch=$(git branch -a)
echo -e "\n更新青龙源文件成功...\n" local primary_branch="master"
cp -f $file_config_sample $dir_config/config.sample.sh if [[ "${all_branch}" =~ "${current_branch}" ]]; then
detect_config_version primary_branch="${current_branch}"
update_depend fi
[[ -f $dir_root/package.json ]] && ql_depend_old=$(cat $dir_root/package.json)
reset_romote_url ${dir_root} "https://github.com/whyour/qinglong.git" ${primary_branch}
git_pull_scripts $dir_root ${primary_branch}
[[ -f $dir_root/package.json ]] && ql_depend_new=$(cat $dir_root/package.json) if [[ $exit_status -eq 0 ]]; then
[[ "$ql_depend_old" != "$ql_depend_new" ]] && npm_install_2 $dir_root echo -e "\n更新青龙源文件成功...\n"
else cp -f $file_config_sample $dir_config/config.sample.sh
echo -e "\n更新青龙源文件失败,请检查原因...\n" detect_config_version
fi update_depend
local url="https://github.com/whyour/qinglong-static.git" [[ -f $dir_root/package.json ]] && ql_depend_new=$(cat $dir_root/package.json)
if [[ -d ${ql_static_repo}/.git ]]; then [[ "$ql_depend_old" != "$ql_depend_new" ]] && npm_install_2 $dir_root
reset_romote_url ${ql_static_repo} ${url} ${primary_branch}
git_pull_scripts ${ql_static_repo} ${primary_branch}
else
git_clone_scripts ${url} ${ql_static_repo} ${primary_branch}
fi
if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新青龙静态资源成功...\n"
local static_version=$(cat $dir_root/src/version.ts | perl -pe "s|.*\'(.*)\';\.*|\1|" | head -1)
echo -e "\n当前版本 $static_version...\n"
rm -rf $dir_static/*
cp -rf $ql_static_repo/* $dir_static
if [[ $no_restart != "no-restart" ]]; then
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
echo -e "重启面板中..."
sleep 3
reload_pm2
fi
else
echo -e "\n更新青龙静态资源失败,请检查原因...\n"
fi
update_qinglong_static "$1" "$primary_branch"
else
echo -e "\n更新青龙源文件失败,请检查网络...\n"
fi
} }
patch_version() { update_qinglong_static() {
# 兼容pnpm@7 local no_restart="$1"
pnpm setup &>/dev/null local primary_branch="$2"
source ~/.bashrc local url="https://github.com/whyour/qinglong-static.git"
pnpm install -g &>/dev/null if [[ -d ${ql_static_repo}/.git ]]; then
reset_romote_url ${ql_static_repo} ${url} ${primary_branch}
git_pull_scripts ${ql_static_repo} ${primary_branch}
else
git_clone_scripts ${url} ${ql_static_repo} ${primary_branch}
fi
if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新青龙静态资源成功...\n"
local static_version=$(cat $dir_root/src/version.ts | perl -pe "s|.*\'(.*)\';\.*|\1|" | head -1)
echo -e "\n当前版本 $static_version...\n"
if [[ -f "$dir_root/db/cookie.db" ]]; then rm -rf $dir_static/*
echo -e "检测到旧的db文件,拷贝为新db...\n" cp -rf $ql_static_repo/* $dir_static
mv $dir_root/db/cookie.db $dir_root/db/env.db if [[ $no_restart != "no-restart" ]]; then
rm -rf $dir_root/db/cookie.db nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
echo echo -e "重启面板中..."
sleep 3
reload_pm2
fi fi
else
if ! type ts-node &>/dev/null; then echo -e "\n更新青龙静态资源失败,请检查网络...\n"
pnpm add -g ts-node typescript tslib fi
fi
git config --global pull.rebase false
cp -f $dir_root/.env.example $dir_root/.env
if [[ -d "$dir_root/db" ]]; then
echo -e "检测到旧的db目录,拷贝到data目录...\n"
cp -rf $dir_root/config $dir_root/data
echo
fi
if [[ -d "$dir_root/scripts" ]]; then
echo -e "检测到旧的scripts目录,拷贝到data目录...\n"
cp -rf $dir_root/scripts $dir_root/data
echo
fi
if [[ -d "$dir_root/log" ]]; then
echo -e "检测到旧的log目录,拷贝到data目录...\n"
cp -rf $dir_root/log $dir_root/data
echo
fi
if [[ -d "$dir_root/config" ]]; then
echo -e "检测到旧的config目录,拷贝到data目录...\n"
cp -rf $dir_root/config $dir_root/data
echo
fi
if [[ $PipMirror ]]; then
pip3 config set global.index-url $PipMirror
fi
if [[ $NpmMirror ]]; then
npm config set registry $NpmMirror
fi
} }
## 对比脚本 ## 对比脚本
diff_scripts() { diff_scripts() {
local dir_current=$(pwd) local dir_current=$(pwd)
local repo_path="$1" local repo_path="$1"
local author="$2" local author="$2"
local path="$3" local path="$3"
local blackword="$4" local blackword="$4"
local dependence="$5" local dependence="$5"
local extensions="$6" local extensions="$6"
gen_list_repo "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions" gen_list_repo "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions"
local list_add="$dir_list_tmp/${uniq_path}_add.list" local list_add="$dir_list_tmp/${uniq_path}_add.list"
local list_drop="$dir_list_tmp/${uniq_path}_drop.list" local list_drop="$dir_list_tmp/${uniq_path}_drop.list"
diff_cron "$dir_list_tmp/${uniq_path}_scripts.list" "$dir_list_tmp/${uniq_path}_user.list" $list_add $list_drop diff_cron "$dir_list_tmp/${uniq_path}_scripts.list" "$dir_list_tmp/${uniq_path}_user.list" $list_add $list_drop
if [[ -s $list_drop ]]; then if [[ -s $list_drop ]]; then
output_list_add_drop $list_drop "失效" output_list_add_drop $list_drop "失效"
if [[ ${AutoDelCron} == true ]]; then if [[ ${AutoDelCron} == true ]]; then
del_cron $list_drop $uniq_path del_cron $list_drop $uniq_path
fi
fi fi
if [[ -s $list_add ]]; then fi
output_list_add_drop $list_add "新" if [[ -s $list_add ]]; then
if [[ ${AutoAddCron} == true ]]; then output_list_add_drop $list_add "新"
add_cron $list_add $uniq_path if [[ ${AutoAddCron} == true ]]; then
fi add_cron $list_add $uniq_path
fi fi
cd $dir_current fi
cd $dir_current
} }
## 生成脚本的路径清单文件 ## 生成脚本的路径清单文件
gen_list_repo() { gen_list_repo() {
local dir_current=$(pwd) local dir_current=$(pwd)
local repo_path="$1" local repo_path="$1"
local author="$2" local author="$2"
local path="$3" local path="$3"
local blackword="$4" local blackword="$4"
local dependence="$5" local dependence="$5"
rm -f $dir_list_tmp/${uniq_path}*.list &>/dev/null rm -f $dir_list_tmp/${uniq_path}*.list &>/dev/null
cd ${repo_path}
local cmd="find ."
local index=0
if [[ $6 ]]; then
file_extensions="$6"
if [[ $file_extensions =~ "|" ]]; then
file_extensions=$(echo $file_extensions | sed 's/|/ /g')
fi
fi
for extension in $file_extensions; do
if [[ $index -eq 0 ]]; then
cmd="${cmd} -name \"*.${extension}\""
else
cmd="${cmd} -o -name \"*.${extension}\""
fi
let index+=1
done
files=$(eval $cmd | sed 's/^..//')
if [[ $path ]]; then
files=$(echo "$files" | egrep $path)
fi
if [[ $blackword ]]; then
files=$(echo "$files" | egrep -v $blackword)
fi
cp -f $file_notify_js "${dir_scripts}/${uniq_path}"
cp -f $file_notify_py "${dir_scripts}/${uniq_path}"
if [[ $dependence ]]; then
cd ${repo_path} cd ${repo_path}
results=$(eval $cmd | sed 's/^..//' | egrep $dependence)
local cmd="find ." for _file in ${results}; do
local index=0 file_path=$(dirname $_file)
if [[ $6 ]]; then make_dir "${dir_scripts}/${uniq_path}/${file_path}"
file_extensions="$6" cp -f $_file "${dir_scripts}/${uniq_path}/${file_path}"
if [[ $file_extensions =~ "|" ]]; then
file_extensions=$(echo $file_extensions | sed 's/|/ /g')
fi
fi
for extension in $file_extensions; do
if [[ $index -eq 0 ]]; then
cmd="${cmd} -name \"*.${extension}\""
else
cmd="${cmd} -o -name \"*.${extension}\""
fi
let index+=1
done done
files=$(eval $cmd | sed 's/^..//') fi
if [[ $path ]]; then
files=$(echo "$files" | egrep $path)
fi
if [[ $blackword ]]; then
files=$(echo "$files" | egrep -v $blackword)
fi
cp -f $file_notify_js "${dir_scripts}/${uniq_path}" if [[ -d $dir_dep ]]; then
cp -f $file_notify_py "${dir_scripts}/${uniq_path}" cp -rf $dir_dep/* "${dir_scripts}/${uniq_path}" &>/dev/null
fi
if [[ $dependence ]]; then for file in ${files}; do
cd ${repo_path} filename=$(basename $file)
results=$(eval $cmd | sed 's/^..//' | egrep $dependence) cp -f $file "$dir_scripts/${uniq_path}/${filename}"
for _file in ${results}; do echo "${uniq_path}/${filename}" >>"$dir_list_tmp/${uniq_path}_scripts.list"
file_path=$(dirname $_file) cron_id=$(cat $list_crontab_user | grep -E "$cmd_task ${uniq_path}_${filename}" | perl -pe "s|.*ID=(.*) $cmd_task ${uniq_path}_${filename}\.*|\1|" | head -1 | awk -F " " '{print $1}')
make_dir "${dir_scripts}/${uniq_path}/${file_path}" if [[ $cron_id ]]; then
cp -f $_file "${dir_scripts}/${uniq_path}/${file_path}" result=$(update_cron_command_api "$cmd_task ${uniq_path}/${filename}:$cron_id")
done
fi fi
done
if [[ -d $dir_dep ]]; then grep -E "${cmd_task} ${uniq_path}" ${list_crontab_user} | perl -pe "s|.*ID=(.*) ${cmd_task} (${uniq_path}.*)\.*|\2|" | awk -F " " '{print $1}' | sort -u >"$dir_list_tmp/${uniq_path}_user.list"
cp -rf $dir_dep/* "${dir_scripts}/${uniq_path}" &>/dev/null cd $dir_current
fi
for file in ${files}; do
filename=$(basename $file)
cp -f $file "$dir_scripts/${uniq_path}/${filename}"
echo "${uniq_path}/${filename}" >>"$dir_list_tmp/${uniq_path}_scripts.list"
cron_id=$(cat $list_crontab_user | grep -E "$cmd_task ${uniq_path}_${filename}" | perl -pe "s|.*ID=(.*) $cmd_task ${uniq_path}_${filename}\.*|\1|" | head -1 | awk -F " " '{print $1}')
if [[ $cron_id ]]; then
result=$(update_cron_command_api "$cmd_task ${uniq_path}/${filename}:$cron_id")
fi
done
grep -E "${cmd_task} ${uniq_path}" ${list_crontab_user} | perl -pe "s|.*ID=(.*) ${cmd_task} (${uniq_path}.*)\.*|\2|" | awk -F " " '{print $1}' | sort -u >"$dir_list_tmp/${uniq_path}_user.list"
cd $dir_current
} }
get_uniq_path() { get_uniq_path() {
local url="$1" local url="$1"
local branch="$2" local branch="$2"
local urlTmp="${url%*/}" local urlTmp="${url%*/}"
local repoTmp="${urlTmp##*/}" local repoTmp="${urlTmp##*/}"
local repo="${repoTmp%.*}" local repo="${repoTmp%.*}"
local tmp="${url%/*}" local tmp="${url%/*}"
local authorTmp1="${tmp##*/}" local authorTmp1="${tmp##*/}"
local authorTmp2="${authorTmp1##*:}" local authorTmp2="${authorTmp1##*:}"
local author="${authorTmp2##*.}" local author="${authorTmp2##*.}"
uniq_path="${author}_${repo}" uniq_path="${author}_${repo}"
[[ $branch ]] && uniq_path="${uniq_path}_${branch}" [[ $branch ]] && uniq_path="${uniq_path}_${branch}"
} }
main() { main() {
## for ql update ## for ql update
show_log="false" show_log="false"
while getopts ":l" opt while getopts ":l" opt; do
do case $opt in
case $opt in l)
l) show_log="true"
show_log="true" ;;
;;
esac
done
[[ "$show_log" == "true" ]] && shift $(($OPTIND - 1))
local p1=$1
local p2=$2
local p3=$3
local p4=$4
local p5=$5
local p6=$6
local p7=$7
local log_time=$(date "+%Y-%m-%d-%H-%M-%S")
local log_path="$dir_log/update/${log_time}_$p1.log"
local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
case $p1 in
update)
cmd=">> $log_path 2>&1"
[[ "$show_log" == "true" ]] && cmd=""
eval echo -e "## 开始执行... $begin_time\n" $cmd
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
eval update_qinglong "$2" $cmd
;;
extra)
echo -e "## 开始执行... $begin_time\n" >>$log_path
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
run_extra_shell >>$log_path
;;
repo)
get_uniq_path "$p2" "$p6"
if [[ -n $p2 ]]; then
update_repo "$p2" "$p3" "$p4" "$p5" "$p6" "$p7"
else
echo -e "命令输入错误...\n"
usage
fi
;;
raw)
get_uniq_path "$p2"
if [[ -n $p2 ]]; then
update_raw "$p2"
else
echo -e "命令输入错误...\n"
usage
fi
;;
rmlog)
echo -e "## 开始执行... $begin_time\n" >>$log_path
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
. $dir_shell/rmlog.sh "$p2" >>$log_path
;;
bot)
echo -e "## 开始执行... $begin_time\n" >>$log_path
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
. $dir_shell/bot.sh >>$log_path
;;
check)
echo -e "## 开始执行... $begin_time\n" >>$log_path
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
. $dir_shell/check.sh >>$log_path
;;
resetlet)
echo -e "## 开始执行... $begin_time\n" >>$log_path
auth_value=$(cat $file_auth_user | jq '.retries =0' -c)
echo -e "重置登录错误次数成功 \n $auth_value" >>$log_path
echo "$auth_value" >$file_auth_user
;;
resettfa)
echo -e "## 开始执行... $begin_time\n" >>$log_path
auth_value=$(cat $file_auth_user | jq '.twoFactorActivated =false' | jq '.twoFactorActived =false' -c)
echo -e "禁用两步验证成功 \n $auth_value" >>$log_path
echo "$auth_value" >$file_auth_user
;;
*)
echo -e "命令输入错误...\n"
usage
;;
esac esac
local end_time=$(date '+%Y-%m-%d %H:%M:%S') done
local diff_time=$(diff_time "%Y-%m-%d %H:%M:%S" "$begin_time" "$end_time") [[ "$show_log" == "true" ]] && shift $(($OPTIND - 1))
if [[ $p1 != "repo" ]] && [[ $p1 != "raw" ]]; then
echo -e "\n## 执行结束... $end_time 耗时 $diff_time" >>$log_path local p1=$1
cat $log_path local p2=$2
local p3=$3
local p4=$4
local p5=$5
local p6=$6
local p7=$7
local log_dir="${p1}"
make_dir "$dir_log/$log_dir"
local log_time=$(date "+%Y-%m-%d-%H-%M-%S")
local log_path="${log_dir}/${log_time}.log"
local file_path="$dir_log/$log_path"
cmd=">> $file_path 2>&1"
[[ "$show_log" == "true" ]] && cmd=""
local time_format="%Y-%m-%d %H:%M:%S"
local time=$(date "+$time_format")
local begin_timestamp=$(format_timestamp "$time_format" "$time")
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
local begin_time=$(format_time "$time_format" "$time")
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
if [[ -s $task_error_log_path ]]; then
eval cat $task_error_log_path $cmd
eval echo -e "加载 config.sh 出错,请手动检查" $cmd
eval echo $cmd
fi
if [[ "$show_log" == "true" ]] && [[ $ID ]]; then
eval echo -e "请移除 -l 参数" $cmd
exit 1
fi
case $p1 in
update)
eval update_qinglong "$2" $cmd
;;
extra)
eval run_extra_shell $cmd
;;
repo)
get_uniq_path "$p2" "$p6"
if [[ -n $p2 ]]; then
update_repo "$p2" "$p3" "$p4" "$p5" "$p6" "$p7"
else
eval echo -e "命令输入错误...\\\n" $cmd
eval usage $cmd
fi fi
;;
raw)
get_uniq_path "$p2"
if [[ -n $p2 ]]; then
update_raw "$p2"
else
eval echo -e "命令输入错误...\\\n" $cmd
eval usage $cmd
fi
;;
rmlog)
eval . $dir_shell/rmlog.sh "$p2" $cmd
;;
bot)
eval . $dir_shell/bot.sh $cmd
;;
check)
eval . $dir_shell/check.sh $cmd
;;
resetlet)
auth_value=$(cat $file_auth_user | jq '.retries =0' -c)
echo "$auth_value" >$file_auth_user
echo -e "重置登录错误次数成功" $cmd
;;
resettfa)
auth_value=$(cat $file_auth_user | jq '.twoFactorActivated =false' | jq '.twoFactorActived =false' -c)
echo "$auth_value" >$file_auth_user
eval echo -e "禁用两步验证成功" $cmd
;;
*)
eval echo -e "命令输入错误...\\\n" $cmd
eval usage $cmd
;;
esac
local etime=$(date "+$time_format")
local end_time=$(format_time "$time_format" "$etime")
local end_timestamp=$(format_timestamp "$time_format" "$etime")
local diff_time=$(($end_timestamp - $begin_timestamp))
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
eval echo -e "\\\n          " $cmd
if [[ -f $file_path ]]; then
cat $file_path
fi
} }
main "$@" main "$@"
+1
View File
@@ -50,6 +50,7 @@ const EditableTagGroup = ({
{tag} {tag}
</Tag> </Tag>
); );
return ( return (
<span key={tag} style={{ display: 'inline-block', marginBottom: 8 }}> <span key={tag} style={{ display: 'inline-block', marginBottom: 8 }}>
{tagElem} {tagElem}
+48
View File
@@ -0,0 +1,48 @@
import { useMemo, useState } from 'react';
export default (
treeData: any[],
searchValue: string,
{
treeNodeFilterProp,
}: {
treeNodeFilterProp: string;
},
) => {
return useMemo(() => {
const keys: string[] = [];
if (!searchValue) {
return { treeData, keys };
}
const upperStr = searchValue.toUpperCase();
function filterOptionFunc(_: string, dataNode: any[]) {
const value = dataNode[treeNodeFilterProp as any];
return String(value).toUpperCase().includes(upperStr);
}
function dig(list: any[], keepAll: boolean = false): any[] {
return list
.map((dataNode) => {
const children = dataNode.children;
const match = keepAll || filterOptionFunc!(searchValue, dataNode);
const childList = dig(children || [], match);
if (match || childList.length) {
childList.length && keys.push(dataNode.key);
return {
...dataNode,
children: childList,
};
}
return null;
})
.filter((node) => node);
}
return { treeData: dig(treeData), keys };
}, [treeData, searchValue, treeNodeFilterProp]);
};
+16
View File
@@ -0,0 +1,16 @@
import { MutableRefObject, useLayoutEffect, useState } from 'react';
import useResizeObserver from '@react-hook/resize-observer'
import { getTableScroll } from '@/utils';
export default <T extends HTMLElement>(target: MutableRefObject<T>, extraHeight?: number) => {
const [height, setHeight] = useState<number>()
useResizeObserver(target, (entry) => {
let _targe = entry.target as any
if (!_targe.classList.contains('ant-table-wrapper')) {
_targe = entry.target.querySelector('.ant-table-wrapper')
}
setHeight(getTableScroll({ extraHeight, target: _targe as HTMLElement }))
})
return height
}
+6 -6
View File
@@ -68,18 +68,18 @@ export default {
icon: <IconFont type="ql-icon-dependence" />, icon: <IconFont type="ql-icon-dependence" />,
component: '@/pages/dependence/index', component: '@/pages/dependence/index',
}, },
{
path: '/log',
name: '日志管理',
icon: <IconFont type="ql-icon-log" />,
component: '@/pages/log/index',
},
{ {
path: '/diff', path: '/diff',
name: '对比工具', name: '对比工具',
icon: <IconFont type="ql-icon-diff" />, icon: <IconFont type="ql-icon-diff" />,
component: '@/pages/diff/index', component: '@/pages/diff/index',
}, },
{
path: '/log',
name: '任务日志',
icon: <IconFont type="ql-icon-log" />,
component: '@/pages/log/index',
},
{ {
path: '/setting', path: '/setting',
name: '系统设置', name: '系统设置',
+28 -30
View File
@@ -161,7 +161,7 @@
&.env-wrapper, &.env-wrapper,
&.config-wrapper { &.config-wrapper {
.CodeMirror { .CodeMirror {
width: calc(100vw - 80px); width: calc(100vw - 24px);
} }
} }
} }
@@ -179,8 +179,8 @@
.Resizer { .Resizer {
background: @component-background; background: @component-background;
opacity: 0.2; opacity: 0.8;
z-index: 1; z-index: 100;
-moz-box-sizing: border-box; -moz-box-sizing: border-box;
-webkit-box-sizing: border-box; -webkit-box-sizing: border-box;
box-sizing: border-box; box-sizing: border-box;
@@ -243,31 +243,28 @@
font-size: 13px; font-size: 13px;
} }
[data-dark='true'] { /* Change autocomplete styles in WebKit */
/* Change autocomplete styles in WebKit */ input:-webkit-autofill,
input:-webkit-autofill, input:-webkit-autofill:hover,
input:-webkit-autofill:hover, input:-webkit-autofill:focus,
input:-webkit-autofill:focus, textarea:-webkit-autofill,
textarea:-webkit-autofill, textarea:-webkit-autofill:hover,
textarea:-webkit-autofill:hover, textarea:-webkit-autofill:focus,
textarea:-webkit-autofill:focus, select:-webkit-autofill,
select:-webkit-autofill, select:-webkit-autofill:hover,
select:-webkit-autofill:hover, select:-webkit-autofill:focus {
select:-webkit-autofill:focus { border: none;
border: 1px solid @border-color-base; box-shadow: none;
box-shadow: none; transition: background-color 5000s ease-in-out 0s;
transition: background-color 5000s ease-in-out 0s; -webkit-text-fill-color: @text-color;
-webkit-text-fill-color: @text-color; }
caret-color: #e8e6f3 !important;
}
::placeholder { ::placeholder {
opacity: 0.5 !important; opacity: 0.5 !important;
} }
.ant-select-selection-placeholder { .ant-select-selection-placeholder {
opacity: 0.5 !important; opacity: 0.5 !important;
}
} }
.ant-pro-basicLayout-content { .ant-pro-basicLayout-content {
@@ -326,10 +323,6 @@
background-color: #373739; background-color: #373739;
} }
.ant-layout-sider {
border-right: 1px solid rgba(0, 0, 0, 0.06) !important;
}
.ant-pro-sider-logo { .ant-pro-sider-logo {
padding: 16px 8px !important; padding: 16px 8px !important;
@@ -349,3 +342,8 @@
width: auto !important; width: auto !important;
} }
} }
pre {
word-break: break-all !important;
white-space: break-spaces !important;
}
+94 -52
View File
@@ -2,7 +2,7 @@ import React, { useEffect, useState, useRef } from 'react';
import ProLayout, { PageLoading } from '@ant-design/pro-layout'; import ProLayout, { PageLoading } from '@ant-design/pro-layout';
import * as DarkReader from '@umijs/ssr-darkreader'; import * as DarkReader from '@umijs/ssr-darkreader';
import defaultProps from './defaultProps'; import defaultProps from './defaultProps';
import { Link, history } from 'umi'; import { Link, history, Outlet, useLocation } from '@umijs/max';
import { import {
LogoutOutlined, LogoutOutlined,
MenuFoldOutlined, MenuFoldOutlined,
@@ -15,21 +15,56 @@ import './index.less';
import vhCheck from 'vh-check'; import vhCheck from 'vh-check';
import { version, changeLogLink, changeLog } from '../version'; import { version, changeLogLink, changeLog } from '../version';
import { useCtx, useTheme } from '@/utils/hooks'; import { useCtx, useTheme } from '@/utils/hooks';
import { message, Badge, Modal, Avatar, Dropdown, Menu, Image } from 'antd'; import {
message,
Badge,
Modal,
Avatar,
Dropdown,
Menu,
Image,
Popover,
Descriptions,
Tooltip,
} from 'antd';
// @ts-ignore // @ts-ignore
import SockJS from 'sockjs-client'; import SockJS from 'sockjs-client';
import * as Sentry from '@sentry/react'; import * as Sentry from '@sentry/react';
import { init } from '../utils/init'; import { init } from '../utils/init';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/mode/python/python';
import 'codemirror/mode/shell/shell';
export default function (props: any) { export interface SharedContext {
headerStyle: React.CSSProperties;
isPhone: boolean;
theme: 'vs' | 'vs-dark';
user: any;
reloadUser: (needLoading?: boolean) => void;
reloadTheme: () => void;
socketMessage: any;
systemInfo: TSystemInfo;
}
interface TSystemInfo {
branch: 'develop' | 'master';
isInitialized: boolean;
lastCommitId: string;
lastCommitTime: string;
version: string;
}
export default function () {
const location = useLocation();
const ctx = useCtx(); const ctx = useCtx();
const { theme, reloadTheme } = useTheme(); const { theme, reloadTheme } = useTheme();
const [user, setUser] = useState<any>({}); const [user, setUser] = useState<any>({});
const [loading, setLoading] = useState<boolean>(true); const [loading, setLoading] = useState<boolean>(true);
const [systemInfo, setSystemInfo] = useState<{ isInitialized: boolean }>(); const [systemInfo, setSystemInfo] = useState<TSystemInfo>();
const ws = useRef<any>(null); const ws = useRef<any>(null);
const [socketMessage, setSocketMessage] = useState<any>(); const [socketMessage, setSocketMessage] = useState<any>();
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
const [initLoading, setInitLoading] = useState<boolean>(true);
const { const {
enable: enableDarkMode, enable: enableDarkMode,
disable: disableDarkMode, disable: disableDarkMode,
@@ -53,17 +88,15 @@ export default function (props: any) {
setSystemInfo(data); setSystemInfo(data);
if (!data.isInitialized) { if (!data.isInitialized) {
history.push('/initialization'); history.push('/initialization');
setLoading(false);
} else { } else {
getUser(); getUser();
} }
} else {
message.error(data);
} }
}) })
.catch((error) => { .catch((error) => {
console.log(error); console.log(error);
}); })
.finally(() => setInitLoading(false));
}; };
const getUser = (needLoading = true) => { const getUser = (needLoading = true) => {
@@ -73,11 +106,9 @@ export default function (props: any) {
.then(({ code, data }) => { .then(({ code, data }) => {
if (code === 200 && data.username) { if (code === 200 && data.username) {
setUser(data); setUser(data);
if (props.location.pathname === '/') { if (location.pathname === '/') {
history.push('/crontab'); history.push('/crontab');
} }
} else {
message.error(data);
} }
needLoading && setLoading(false); needLoading && setLoading(false);
}) })
@@ -94,7 +125,7 @@ export default function (props: any) {
if (systemInfo && systemInfo.isInitialized && !user) { if (systemInfo && systemInfo.isInitialized && !user) {
getUser(); getUser();
} }
}, [props.location.pathname]); }, [location.pathname]);
useEffect(() => { useEffect(() => {
if (!systemInfo) { if (!systemInfo) {
@@ -136,9 +167,11 @@ export default function (props: any) {
}, []); }, []);
useEffect(() => { useEffect(() => {
if (!user) return; if (!user || !user.username) return;
ws.current = new SockJS( ws.current = new SockJS(
`${location.origin}/api/ws?token=${localStorage.getItem(config.authKey)}`, `${window.location.origin}/api/ws?token=${localStorage.getItem(
config.authKey,
)}`,
); );
ws.current.onmessage = (e: any) => { ws.current.onmessage = (e: any) => {
@@ -146,9 +179,9 @@ export default function (props: any) {
const data = JSON.parse(e.data); const data = JSON.parse(e.data);
if (data.type === 'ping') { if (data.type === 'ping') {
if (data && data.message === 'hanhh') { if (data && data.message === 'hanhh') {
console.log('websocket连接成功', e); console.log('WS connection succeeded !!!');
} else { } else {
console.log('websocket连接失败', e); console.log('WS connection Failed !!!', e);
} }
} }
setSocketMessage(data); setSocketMessage(data);
@@ -183,29 +216,31 @@ export default function (props: any) {
}; };
}, []); }, []);
if ( if (initLoading) {
['/login', '/initialization', '/error'].includes(props.location.pathname) return <PageLoading />;
) { }
document.title = `${(config.documentTitleMap as any)[props.location.pathname]
} - `; if (['/login', '/initialization', '/error'].includes(location.pathname)) {
if ( document.title = `${
systemInfo?.isInitialized && (config.documentTitleMap as any)[location.pathname]
props.location.pathname === '/initialization' } - `;
) { if (systemInfo?.isInitialized && location.pathname === '/initialization') {
history.push('/crontab'); history.push('/crontab');
} }
if (systemInfo || props.location.pathname === '/error') { if (systemInfo || location.pathname === '/error') {
return React.Children.map(props.children, (child) => { return (
return React.cloneElement(child, { <Outlet
...ctx, context={{
theme, ...ctx,
user, theme,
reloadUser, user,
reloadTheme, reloadUser,
ws: ws.current, reloadTheme,
}); ws: ws.current,
}); }}
/>
);
} }
} }
@@ -226,10 +261,10 @@ export default function (props: any) {
<PageLoading /> <PageLoading />
) : ( ) : (
<ProLayout <ProLayout
selectedKeys={[props.location.pathname]} selectedKeys={[location.pathname]}
loading={loading} loading={loading}
ErrorBoundary={Sentry.ErrorBoundary} ErrorBoundary={Sentry.ErrorBoundary}
logo={<Image preview={false} src="http://qn.whyour.cn/logo.png" />} logo={<Image preview={false} src="https://qn.whyour.cn/logo.png" />}
title={ title={
<> <>
<span style={{ fontSize: 16 }}></span> <span style={{ fontSize: 16 }}></span>
@@ -241,17 +276,23 @@ export default function (props: any) {
e.stopPropagation(); e.stopPropagation();
}} }}
> >
<span <Tooltip
style={{ title={systemInfo?.branch === 'develop' ? '开发版' : '正式版'}
fontSize: isFirefox ? 9 : 12,
color: '#666',
marginLeft: 2,
zoom: isSafari ? 0.66 : 0.8,
letterSpacing: isQQBrowser ? -2 : 0,
}}
> >
v{version} <Badge size="small" dot={systemInfo?.branch === 'develop'}>
</span> <span
style={{
fontSize: isFirefox ? 9 : 12,
color: '#666',
marginLeft: 2,
zoom: isSafari ? 0.66 : 0.8,
letterSpacing: isQQBrowser ? -2 : 0,
}}
>
v{version}
</span>
</Badge>
</Tooltip>
</a> </a>
</> </>
} }
@@ -319,16 +360,17 @@ export default function (props: any) {
)} )}
{...defaultProps} {...defaultProps}
> >
{React.Children.map(props.children, (child) => { <Outlet
return React.cloneElement(child, { context={{
...ctx, ...ctx,
theme, theme,
user, user,
reloadUser, reloadUser,
reloadTheme, reloadTheme,
socketMessage, socketMessage,
}); systemInfo,
})} }}
/>
</ProLayout> </ProLayout>
); );
} }
+17 -7
View File
@@ -11,8 +11,11 @@ import { PageContainer } from '@ant-design/pro-layout';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import Editor from '@monaco-editor/react'; import Editor from '@monaco-editor/react';
import { Controlled as CodeMirror } from 'react-codemirror2'; import { Controlled as CodeMirror } from 'react-codemirror2';
import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts';
const Config = ({ headerStyle, isPhone, theme }: any) => { const Config = () => {
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
const [value, setValue] = useState(''); const [value, setValue] = useState('');
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [title, setTitle] = useState('config.sh'); const [title, setTitle] = useState('config.sh');
@@ -22,8 +25,10 @@ const Config = ({ headerStyle, isPhone, theme }: any) => {
const [confirmLoading, setConfirmLoading] = useState(false); const [confirmLoading, setConfirmLoading] = useState(false);
const getConfig = (name: string) => { const getConfig = (name: string) => {
request.get(`${config.apiPrefix}configs/${name}`).then((data: any) => { request.get(`${config.apiPrefix}configs/${name}`).then(({ code, data }) => {
setValue(data.data); if (code === 200) {
setValue(data);
}
}); });
}; };
@@ -31,8 +36,10 @@ const Config = ({ headerStyle, isPhone, theme }: any) => {
setLoading(true); setLoading(true);
request request
.get(`${config.apiPrefix}configs/files`) .get(`${config.apiPrefix}configs/files`)
.then((data: any) => { .then(({ code, data }) => {
setData(data.data); if (code === 200) {
setData(data);
}
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
@@ -47,8 +54,10 @@ const Config = ({ headerStyle, isPhone, theme }: any) => {
.post(`${config.apiPrefix}configs/save`, { .post(`${config.apiPrefix}configs/save`, {
data: { content, name: select }, data: { content, name: select },
}) })
.then((data: any) => { .then(({ code, data }) => {
message.success(data.message); if (code === 200) {
message.success('保存成功');
}
setConfirmLoading(false); setConfirmLoading(false);
}); });
}; };
@@ -71,6 +80,7 @@ const Config = ({ headerStyle, isPhone, theme }: any) => {
loading={loading} loading={loading}
extra={[ extra={[
<TreeSelect <TreeSelect
treeExpandAction="click"
className="config-select" className="config-select"
value={select} value={select}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }} dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
+24 -31
View File
@@ -103,7 +103,6 @@ const CronDetailModal = ({
fontSize: 12, fontSize: 12,
lineNumbersMinChars: 3, lineNumbersMinChars: 3,
fontFamily: 'Source Code Pro', fontFamily: 'Source Code Pro',
folding: false,
glyphMargin: false, glyphMargin: false,
wordWrap: 'on', wordWrap: 'on',
}} }}
@@ -123,9 +122,11 @@ const CronDetailModal = ({
.get( .get(
`${config.apiPrefix}logs/${item.filename}?path=${item.directory || ''}`, `${config.apiPrefix}logs/${item.filename}?path=${item.directory || ''}`,
) )
.then((data) => { .then(({ code, data }) => {
setLog(data.data); if (code === 200) {
setIsLogModalVisible(true); setLog(data);
setIsLogModalVisible(true);
}
}); });
}; };
@@ -137,9 +138,9 @@ const CronDetailModal = ({
setLoading(true); setLoading(true);
request request
.get(`${config.apiPrefix}crons/${cron.id}/logs`) .get(`${config.apiPrefix}crons/${cron.id}/logs`)
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
setLogs(data.data); setLogs(data);
} }
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
@@ -165,8 +166,10 @@ const CronDetailModal = ({
setScriptInfo({ parent: p, filename: s }); setScriptInfo({ parent: p, filename: s });
request request
.get(`${config.apiPrefix}scripts/${s}?path=${p || ''}`) .get(`${config.apiPrefix}scripts/${s}?path=${p || ''}`)
.then((data) => { .then(({ code, data }) => {
setValue(data.data); if (code === 200) {
setValue(data);
}
}); });
} else { } else {
setValidTabs([validTabs[0]]); setValidTabs([validTabs[0]]);
@@ -198,12 +201,10 @@ const CronDetailModal = ({
content, content,
}, },
}) })
.then((_data: any) => { .then(({ code, data }) => {
if (_data.code === 200) { if (code === 200) {
setValue(content); setValue(content);
message.success(`保存成功`); message.success(`保存成功`);
} else {
message.error(_data);
} }
resolve(null); resolve(null);
}) })
@@ -231,14 +232,12 @@ const CronDetailModal = ({
onOk() { onOk() {
request request
.put(`${config.apiPrefix}crons/run`, { data: [currentCron.id] }) .put(`${config.apiPrefix}crons/run`, { data: [currentCron.id] })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
setCurrentCron({ ...currentCron, status: CrontabStatus.running }); setCurrentCron({ ...currentCron, status: CrontabStatus.running });
setTimeout(() => { setTimeout(() => {
getLogs(); getLogs();
}, 1000); }, 1000);
} else {
message.error(data);
} }
}); });
}, },
@@ -263,11 +262,9 @@ const CronDetailModal = ({
onOk() { onOk() {
request request
.put(`${config.apiPrefix}crons/stop`, { data: [currentCron.id] }) .put(`${config.apiPrefix}crons/stop`, { data: [currentCron.id] })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
setCurrentCron({ ...currentCron, status: CrontabStatus.idle }); setCurrentCron({ ...currentCron, status: CrontabStatus.idle });
} else {
message.error(data);
} }
}); });
}, },
@@ -300,14 +297,12 @@ const CronDetailModal = ({
data: [currentCron.id], data: [currentCron.id],
}, },
) )
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
setCurrentCron({ setCurrentCron({
...currentCron, ...currentCron,
isDisabled: currentCron.isDisabled === 1 ? 0 : 1, isDisabled: currentCron.isDisabled === 1 ? 0 : 1,
}); });
} else {
message.error(data);
} }
}); });
}, },
@@ -340,14 +335,12 @@ const CronDetailModal = ({
data: [currentCron.id], data: [currentCron.id],
}, },
) )
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
setCurrentCron({ setCurrentCron({
...currentCron, ...currentCron,
isPinned: currentCron.isPinned === 1 ? 0 : 1, isPinned: currentCron.isPinned === 1 ? 0 : 1,
}); });
} else {
message.error(data);
} }
}); });
}, },
@@ -382,7 +375,7 @@ const CronDetailModal = ({
{currentCron.labels?.length > 0 && {currentCron.labels?.length > 0 &&
currentCron.labels[0] !== '' && currentCron.labels[0] !== '' &&
currentCron.labels?.map((label: string, i: number) => ( currentCron.labels?.map((label: string, i: number) => (
<Tag color="blue" style={{ marginRight: 5 }}> <Tag key={label} color="blue" style={{ marginRight: 5 }}>
{label} {label}
</Tag> </Tag>
))} ))}
@@ -445,7 +438,7 @@ const CronDetailModal = ({
</div> </div>
} }
centered centered
visible={visible} open={visible}
forceRender forceRender
footer={false} footer={false}
onCancel={() => handleCancel()} onCancel={() => handleCancel()}
+168 -172
View File
@@ -1,4 +1,4 @@
import React, { PureComponent, Fragment, useState, useEffect } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { import {
Button, Button,
message, message,
@@ -43,13 +43,13 @@ import CronLogModal from './logModal';
import CronDetailModal from './detail'; import CronDetailModal from './detail';
import cron_parser from 'cron-parser'; import cron_parser from 'cron-parser';
import { diffTime } from '@/utils/date'; import { diffTime } from '@/utils/date';
import { getTableScroll } from '@/utils/index'; import { history, useOutletContext } from '@umijs/max';
import { history } from 'umi';
import './index.less'; import './index.less';
import ViewCreateModal from './viewCreateModal'; import ViewCreateModal from './viewCreateModal';
import ViewManageModal from './viewManageModal'; import ViewManageModal from './viewManageModal';
import pagination from 'antd/lib/pagination';
import { FilterValue, SorterResult } from 'antd/lib/table/interface'; import { FilterValue, SorterResult } from 'antd/lib/table/interface';
import { SharedContext } from '@/layouts';
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
const { Text, Paragraph } = Typography; const { Text, Paragraph } = Typography;
const { Search } = Input; const { Search } = Input;
@@ -81,7 +81,8 @@ enum OperationPath {
'unpin', 'unpin',
} }
const Crontab = ({ headerStyle, isPhone, theme }: any) => { const Crontab = () => {
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
const columns: any = [ const columns: any = [
{ {
title: '名称', title: '名称',
@@ -106,6 +107,7 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
{record.labels?.map((label: string) => ( {record.labels?.map((label: string) => (
<Tag <Tag
color="blue" color="blue"
key={label}
style={{ cursor: 'point' }} style={{ cursor: 'point' }}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
@@ -373,7 +375,6 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
filters: any; filters: any;
}>({} as any); }>({} as any);
const [viewConf, setViewConf] = useState<any>(); const [viewConf, setViewConf] = useState<any>();
const [tableScrollHeight, setTableScrollHeight] = useState<number>();
const [isDetailModalVisible, setIsDetailModalVisible] = useState(false); const [isDetailModalVisible, setIsDetailModalVisible] = useState(false);
const [detailCron, setDetailCron] = useState<any>(); const [detailCron, setDetailCron] = useState<any>();
const [searchValue, setSearchValue] = useState(''); const [searchValue, setSearchValue] = useState('');
@@ -385,6 +386,8 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
const [cronViews, setCronViews] = useState<any[]>([]); const [cronViews, setCronViews] = useState<any[]>([]);
const [enabledCronViews, setEnabledCronViews] = useState<any[]>([]); const [enabledCronViews, setEnabledCronViews] = useState<any[]>([]);
const [moreMenuActive, setMoreMenuActive] = useState(false); const [moreMenuActive, setMoreMenuActive] = useState(false);
const tableRef = useRef<any>();
const tableScrollHeight = useTableScrollHeight(tableRef)
const goToScriptManager = (record: any) => { const goToScriptManager = (record: any) => {
const cmd = record.command.split(' ') as string[]; const cmd = record.command.split(' ') as string[];
@@ -411,9 +414,15 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
const getCrons = () => { const getCrons = () => {
setLoading(true); setLoading(true);
const { page, size, sorter, filters } = pageConf; const { page, size, sorter, filters } = pageConf;
let url = `${config.apiPrefix}crons?searchValue=${searchText}&page=${page}&size=${size}&filters=${JSON.stringify(filters)}`; let url = `${config.apiPrefix
}crons?searchValue=${searchText}&page=${page}&size=${size}&filters=${JSON.stringify(
filters,
)}`;
if (sorter && sorter.field) { if (sorter && sorter.field) {
url += `&sorter=${JSON.stringify({ field: sorter.field, type: sorter.order === 'ascend' ? 'ASC' : 'DESC' })}`; url += `&sorter=${JSON.stringify({
field: sorter.field,
type: sorter.order === 'ascend' ? 'ASC' : 'DESC',
})}`;
} }
if (viewConf) { if (viewConf) {
url += `&queryString=${JSON.stringify({ url += `&queryString=${JSON.stringify({
@@ -423,20 +432,22 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
} }
request request
.get(url) .get(url)
.then((_data: any) => { .then(({ code, data: _data }) => {
const { data, total } = _data.data; if (code === 200) {
setValue( const { data, total } = _data;
data.map((x) => { setValue(
return { data.map((x) => {
...x, return {
nextRunTime: cron_parser ...x,
.parseExpression(x.schedule) nextRunTime: cron_parser
.next() .parseExpression(x.schedule)
.toDate(), .next()
}; .toDate(),
}), };
); }),
setTotal(total); );
setTotal(total);
}
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
@@ -466,8 +477,8 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
onOk() { onOk() {
request request
.delete(`${config.apiPrefix}crons`, { data: [record.id] }) .delete(`${config.apiPrefix}crons`, { data: [record.id] })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
message.success('删除成功'); message.success('删除成功');
const result = [...value]; const result = [...value];
const i = result.findIndex((x) => x.id === record.id); const i = result.findIndex((x) => x.id === record.id);
@@ -475,8 +486,6 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
result.splice(i, 1); result.splice(i, 1);
setValue(result); setValue(result);
} }
} else {
message.error(data);
} }
}); });
}, },
@@ -501,8 +510,8 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
onOk() { onOk() {
request request
.put(`${config.apiPrefix}crons/run`, { data: [record.id] }) .put(`${config.apiPrefix}crons/run`, { data: [record.id] })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
const result = [...value]; const result = [...value];
const i = result.findIndex((x) => x.id === record.id); const i = result.findIndex((x) => x.id === record.id);
if (i !== -1) { if (i !== -1) {
@@ -512,8 +521,6 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
}); });
setValue(result); setValue(result);
} }
} else {
message.error(data);
} }
}); });
}, },
@@ -538,8 +545,8 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
onOk() { onOk() {
request request
.put(`${config.apiPrefix}crons/stop`, { data: [record.id] }) .put(`${config.apiPrefix}crons/stop`, { data: [record.id] })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
const result = [...value]; const result = [...value];
const i = result.findIndex((x) => x.id === record.id); const i = result.findIndex((x) => x.id === record.id);
if (i !== -1) { if (i !== -1) {
@@ -550,8 +557,6 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
}); });
setValue(result); setValue(result);
} }
} else {
message.error(data);
} }
}); });
}, },
@@ -583,8 +588,8 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
data: [record.id], data: [record.id],
}, },
) )
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
const newStatus = record.isDisabled === 1 ? 0 : 1; const newStatus = record.isDisabled === 1 ? 0 : 1;
const result = [...value]; const result = [...value];
const i = result.findIndex((x) => x.id === record.id); const i = result.findIndex((x) => x.id === record.id);
@@ -595,8 +600,6 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
}); });
setValue(result); setValue(result);
} }
} else {
message.error(data);
} }
}); });
}, },
@@ -628,8 +631,8 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
data: [record.id], data: [record.id],
}, },
) )
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
const newStatus = record.isPinned === 1 ? 0 : 1; const newStatus = record.isPinned === 1 ? 0 : 1;
const result = [...value]; const result = [...value];
const i = result.findIndex((x) => x.id === record.id); const i = result.findIndex((x) => x.id === record.id);
@@ -640,8 +643,6 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
}); });
setValue(result); setValue(result);
} }
} else {
message.error(data);
} }
}); });
}, },
@@ -743,19 +744,21 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
const getCronDetail = (cron: any) => { const getCronDetail = (cron: any) => {
request request
.get(`${config.apiPrefix}crons/${cron.id}`) .get(`${config.apiPrefix}crons/${cron.id}`)
.then((data: any) => { .then(({ code, data }) => {
const index = value.findIndex((x) => x.id === cron.id); if (code === 200) {
const result = [...value]; const index = value.findIndex((x) => x.id === cron.id);
data.data.nextRunTime = cron_parser const result = [...value];
.parseExpression(data.data.schedule) data.nextRunTime = cron_parser
.next() .parseExpression(data.schedule)
.toDate(); .next()
if (index !== -1) { .toDate();
result.splice(index, 1, { if (index !== -1) {
...cron, result.splice(index, 1, {
...data.data, ...cron,
}); ...data,
setValue(result); });
setValue(result);
}
} }
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
@@ -763,16 +766,10 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
const onSelectChange = (selectedIds: any[]) => { const onSelectChange = (selectedIds: any[]) => {
setSelectedRowIds(selectedIds); setSelectedRowIds(selectedIds);
setTimeout(() => {
if (selectedRowIds.length === 0 || selectedIds.length === 0) {
setTableScrollHeight(getTableScroll());
}
});
}; };
const rowSelection = { const rowSelection = {
selectedRowIds, selectedRowKeys: selectedRowIds,
onChange: onSelectChange, onChange: onSelectChange,
}; };
@@ -783,13 +780,11 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
onOk() { onOk() {
request request
.delete(`${config.apiPrefix}crons`, { data: selectedRowIds }) .delete(`${config.apiPrefix}crons`, { data: selectedRowIds })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
message.success('批量删除成功'); message.success('批量删除成功');
setSelectedRowIds([]); setSelectedRowIds([]);
getCrons(); getCrons();
} else {
message.error(data);
} }
}); });
}, },
@@ -808,11 +803,9 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
.put(`${config.apiPrefix}crons/${OperationPath[operationStatus]}`, { .put(`${config.apiPrefix}crons/${OperationPath[operationStatus]}`, {
data: selectedRowIds, data: selectedRowIds,
}) })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
getCrons(); getCrons();
} else {
message.error(data);
} }
}); });
}, },
@@ -828,7 +821,12 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
sorter: SorterResult<any> | SorterResult<any>[], sorter: SorterResult<any> | SorterResult<any>[],
) => { ) => {
const { current, pageSize } = pagination; const { current, pageSize } = pagination;
setPageConf({ page: current as number, size: pageSize as number, sorter, filters }); setPageConf({
page: current as number,
size: pageSize as number,
sorter,
filters,
});
localStorage.setItem('pageSize', String(pageSize)); localStorage.setItem('pageSize', String(pageSize));
}; };
@@ -865,98 +863,11 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
page: 1, page: 1,
size: parseInt(localStorage.getItem('pageSize') || '20'), size: parseInt(localStorage.getItem('pageSize') || '20'),
sorter: {}, sorter: {},
filters: {} filters: {},
});
setTimeout(() => {
setTableScrollHeight(getTableScroll());
}); });
getCronViews(); getCronViews();
}, []); }, []);
const panelContent = (
<>
{selectedRowIds.length > 0 && (
<div style={{ marginBottom: 16 }}>
<Button type="primary" style={{ marginBottom: 5 }} onClick={delCrons}>
</Button>
<Button
type="primary"
onClick={() => operateCrons(0)}
style={{ marginLeft: 8, marginBottom: 5 }}
>
</Button>
<Button
type="primary"
onClick={() => operateCrons(1)}
style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<Button
type="primary"
style={{ marginRight: 8 }}
onClick={() => operateCrons(2)}
>
</Button>
<Button type="primary" onClick={() => operateCrons(3)}>
</Button>
<Button
type="primary"
onClick={() => operateCrons(4)}
style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<Button
type="primary"
onClick={() => operateCrons(5)}
style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<Button
type="primary"
onClick={() => setIsLabelModalVisible(true)}
style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<span style={{ marginLeft: 8 }}>
<a>{selectedRowIds?.length}</a>
</span>
</div>
)}
<Table
columns={columns}
pagination={{
current: pageConf.page,
pageSize: pageConf.size,
showSizeChanger: true,
simple: isPhone,
total,
showTotal: (total: number, range: number[]) =>
`${range[0]}-${range[1]} 条/总共 ${total}`,
pageSizeOptions: [10, 20, 50, 100, 200, 500, total || 10000].sort(
(a, b) => a - b,
),
}}
dataSource={value}
rowKey="id"
size="middle"
scroll={{ x: 1000, y: tableScrollHeight }}
loading={loading}
rowSelection={rowSelection}
rowClassName={getRowClassName}
onChange={onPageChange}
/>
</>
);
const viewAction = (key: string) => { const viewAction = (key: string) => {
switch (key) { switch (key) {
case 'new': case 'new':
@@ -1012,9 +923,11 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
setLoading(true); setLoading(true);
request request
.get(`${config.apiPrefix}crons/views`) .get(`${config.apiPrefix}crons/views`)
.then((data: any) => { .then(({ code, data }) => {
setCronViews(data.data); if (code === 200) {
setEnabledCronViews(data.data.filter((x) => !x.isDisabled)); setCronViews(data);
setEnabledCronViews(data.filter((x) => !x.isDisabled));
}
}) })
.finally(() => { .finally(() => {
setLoading(false); setLoading(false);
@@ -1023,6 +936,7 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
const tabClick = (key: string) => { const tabClick = (key: string) => {
const view = enabledCronViews.find((x) => x.id == key); const view = enabledCronViews.find((x) => x.id == key);
setSelectedRowIds([]);
setPageConf({ ...pageConf, page: 1 }); setPageConf({ ...pageConf, page: 1 });
setViewConf(view ? view : null); setViewConf(view ? view : null);
}; };
@@ -1071,16 +985,98 @@ const Crontab = ({ headerStyle, isPhone, theme }: any) => {
</Dropdown> </Dropdown>
} }
onTabClick={tabClick} onTabClick={tabClick}
> items={[
<Tabs.TabPane tab="全部任务" key="all"> {
{panelContent} key: 'all',
</Tabs.TabPane> label: '全部任务',
{[...enabledCronViews].slice(0, 2).map((x) => ( },
<Tabs.TabPane tab={x.name} key={x.id}> ...[...enabledCronViews].slice(0, 2).map((x) => ({
{panelContent} key: x.id,
</Tabs.TabPane> label: x.name,
))} })),
</Tabs> ]}
/>
<div ref={tableRef}>
{selectedRowIds.length > 0 && (
<div style={{ marginBottom: 16 }}>
<Button type="primary" style={{ marginBottom: 5 }} onClick={delCrons}>
</Button>
<Button
type="primary"
onClick={() => operateCrons(0)}
style={{ marginLeft: 8, marginBottom: 5 }}
>
</Button>
<Button
type="primary"
onClick={() => operateCrons(1)}
style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<Button
type="primary"
style={{ marginRight: 8 }}
onClick={() => operateCrons(2)}
>
</Button>
<Button type="primary" onClick={() => operateCrons(3)}>
</Button>
<Button
type="primary"
onClick={() => operateCrons(4)}
style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<Button
type="primary"
onClick={() => operateCrons(5)}
style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<Button
type="primary"
onClick={() => setIsLabelModalVisible(true)}
style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<span style={{ marginLeft: 8 }}>
<a>{selectedRowIds?.length}</a>
</span>
</div>
)}
<Table
columns={columns}
pagination={{
current: pageConf.page,
pageSize: pageConf.size,
showSizeChanger: true,
simple: isPhone,
total,
showTotal: (total: number, range: number[]) =>
`${range[0]}-${range[1]} 条/总共 ${total}`,
pageSizeOptions: [10, 20, 50, 100, 200, 500, total || 10000].sort(
(a, b) => a - b,
),
}}
dataSource={value}
rowKey="id"
size="middle"
scroll={{ x: 1000, y: tableScrollHeight }}
loading={loading}
rowSelection={rowSelection}
rowClassName={getRowClassName}
onChange={onPageChange}
/>
</div>
<CronLogModal <CronLogModal
visible={isLogModalVisible} visible={isLogModalVisible}
handleCancel={() => { handleCancel={() => {
+10 -6
View File
@@ -7,6 +7,7 @@ import {
CheckCircleOutlined, CheckCircleOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { PageLoading } from '@ant-design/pro-layout'; import { PageLoading } from '@ant-design/pro-layout';
import { logEnded } from '@/utils';
enum CrontabStatus { enum CrontabStatus {
'running', 'running',
@@ -41,14 +42,17 @@ const CronLogModal = ({
} }
request request
.get(logUrl ? logUrl : `${config.apiPrefix}crons/${cron.id}/log`) .get(logUrl ? logUrl : `${config.apiPrefix}crons/${cron.id}/log`)
.then((data: any) => { .then(({ code, data }) => {
if (localStorage.getItem('logCron') === String(cron.id)) { if (
const log = data.data as string; code === 200 &&
localStorage.getItem('logCron') === String(cron.id)
) {
const log = data as string;
setValue(log || '暂无日志'); setValue(log || '暂无日志');
setExecuting( setExecuting(
log && !log.includes('执行结束') && !log.includes('重启面板'), log && !logEnded(log) && !log.includes('重启面板'),
); );
if (log && !log.includes('执行结束') && !log.includes('重启面板')) { if (log && !logEnded(log) && !log.includes('重启面板')) {
setTimeout(() => { setTimeout(() => {
getCronLog(); getCronLog();
}, 2000); }, 2000);
@@ -119,7 +123,7 @@ const CronLogModal = ({
return ( return (
<Modal <Modal
title={titleElement()} title={titleElement()}
visible={visible} open={visible}
centered centered
className="log-modal" className="log-modal"
bodyStyle={{ bodyStyle={{
+20 -18
View File
@@ -28,13 +28,12 @@ const CronModal = ({
const { code, data } = await request[method](`${config.apiPrefix}crons`, { const { code, data } = await request[method](`${config.apiPrefix}crons`, {
data: payload, data: payload,
}); });
if (code === 200) { if (code === 200) {
message.success(cron ? '更新Cron成功' : '新建Cron成功'); message.success(cron ? '更新Cron成功' : '新建Cron成功');
} else { handleCancel(data);
message.error(data);
} }
setLoading(false); setLoading(false);
handleCancel(data);
} catch (error: any) { } catch (error: any) {
setLoading(false); setLoading(false);
} }
@@ -47,7 +46,7 @@ const CronModal = ({
return ( return (
<Modal <Modal
title={cron ? '编辑任务' : '新建任务'} title={cron ? '编辑任务' : '新建任务'}
visible={visible} open={visible}
forceRender forceRender
centered centered
maskClosable={false} maskClosable={false}
@@ -128,21 +127,24 @@ const CronLabelModal = ({
.then(async (values) => { .then(async (values) => {
setLoading(true); setLoading(true);
const payload = { ids, labels: values.labels }; const payload = { ids, labels: values.labels };
const { code, data } = await request[action]( try {
`${config.apiPrefix}crons/labels`, const { code, data } = await request[action](
{ `${config.apiPrefix}crons/labels`,
data: payload, {
}, data: payload,
); },
if (code === 200) {
message.success(
action === 'post' ? '添加Labels成功' : '删除Labels成功',
); );
} else {
message.error(data); if (code === 200) {
message.success(
action === 'post' ? '添加Labels成功' : '删除Labels成功',
);
handleCancel(true);
}
setLoading(false);
} catch (error) {
setLoading(false);
} }
setLoading(false);
handleCancel(true);
}) })
.catch((info) => { .catch((info) => {
console.log('Validate Failed:', info); console.log('Validate Failed:', info);
@@ -166,7 +168,7 @@ const CronLabelModal = ({
return ( return (
<Modal <Modal
title="批量修改标签" title="批量修改标签"
visible={visible} open={visible}
footer={buttons} footer={buttons}
centered centered
maskClosable={false} maskClosable={false}
+4 -4
View File
@@ -65,11 +65,11 @@ const ViewCreateModal = ({
data: view ? { ...values, id: view.id } : values, data: view ? { ...values, id: view.id } : values,
}, },
); );
if (code !== 200) {
message.error(data); if (code === 200) {
handleCancel(data);
} }
setLoading(false); setLoading(false);
handleCancel(data);
} catch (error: any) { } catch (error: any) {
setLoading(false); setLoading(false);
} }
@@ -136,7 +136,7 @@ const ViewCreateModal = ({
return ( return (
<Modal <Modal
title={view ? '编辑视图' : '新建视图'} title={view ? '编辑视图' : '新建视图'}
visible={visible} open={visible}
forceRender forceRender
width={580} width={580}
centered centered
+8 -14
View File
@@ -141,12 +141,10 @@ const ViewManageModal = ({
onOk() { onOk() {
request request
.delete(`${config.apiPrefix}crons/views`, { data: [record.id] }) .delete(`${config.apiPrefix}crons/views`, { data: [record.id] })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
message.success('删除成功'); message.success('删除成功');
cronViewChange(); cronViewChange();
} else {
message.error(data);
} }
}); });
}, },
@@ -162,14 +160,12 @@ const ViewManageModal = ({
.put(`${config.apiPrefix}crons/views/${checked ? 'enable' : 'disable'}`, { .put(`${config.apiPrefix}crons/views/${checked ? 'enable' : 'disable'}`, {
data: [record.id], data: [record.id],
}) })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
const _list = [...list]; const _list = [...list];
_list.splice(index, 1, { ...list[index], isDisabled: !checked }); _list.splice(index, 1, { ...list[index], isDisabled: !checked });
setList(_list); setList(_list);
cronViewChange(); cronViewChange();
} else {
message.error(data);
} }
}); });
}; };
@@ -190,15 +186,13 @@ const ViewManageModal = ({
.put(`${config.apiPrefix}crons/views/move`, { .put(`${config.apiPrefix}crons/views/move`, {
data: { fromIndex: dragIndex, toIndex: hoverIndex, id: dragRow.id }, data: { fromIndex: dragIndex, toIndex: hoverIndex, id: dragRow.id },
}) })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
const newData = [...list]; const newData = [...list];
newData.splice(dragIndex, 1); newData.splice(dragIndex, 1);
newData.splice(hoverIndex, 0, { ...dragRow, ...data.data }); newData.splice(hoverIndex, 0, { ...dragRow, ...data });
setList(newData); setList(newData);
cronViewChange(); cronViewChange();
} else {
message.error(data);
} }
}); });
}, },
@@ -212,7 +206,7 @@ const ViewManageModal = ({
return ( return (
<Modal <Modal
title="视图管理" title="视图管理"
visible={visible} open={visible}
centered centered
width={620} width={620}
onCancel={() => handleCancel()} onCancel={() => handleCancel()}
+118 -97
View File
@@ -27,8 +27,11 @@ import DependenceModal from './modal';
import { DndProvider, useDrag, useDrop } from 'react-dnd'; import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend'; import { HTML5Backend } from 'react-dnd-html5-backend';
import './index.less'; import './index.less';
import { getTableScroll } from '@/utils/index';
import DependenceLogModal from './logModal'; import DependenceLogModal from './logModal';
import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts';
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
const { Text } = Typography; const { Text } = Typography;
const { Search } = Input; const { Search } = Input;
@@ -48,7 +51,9 @@ enum StatusColor {
'error', 'error',
} }
const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => { const Dependence = () => {
const { headerStyle, isPhone, socketMessage } =
useOutletContext<SharedContext>();
const columns: any = [ const columns: any = [
{ {
title: '序号', title: '序号',
@@ -160,10 +165,11 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
const [editedDependence, setEditedDependence] = useState(); const [editedDependence, setEditedDependence] = useState();
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]); const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
const [searchText, setSearchText] = useState(''); const [searchText, setSearchText] = useState('');
const [tableScrollHeight, setTableScrollHeight] = useState<number>();
const [logDependence, setLogDependence] = useState<any>(); const [logDependence, setLogDependence] = useState<any>();
const [isLogModalVisible, setIsLogModalVisible] = useState(false); const [isLogModalVisible, setIsLogModalVisible] = useState(false);
const [type, setType] = useState('nodejs'); const [type, setType] = useState('nodejs');
const tableRef = useRef<any>();
const tableScrollHeight = useTableScrollHeight(tableRef, 59)
const getDependencies = () => { const getDependencies = () => {
setLoading(true); setLoading(true);
@@ -171,8 +177,10 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
.get( .get(
`${config.apiPrefix}dependencies?searchValue=${searchText}&type=${type}`, `${config.apiPrefix}dependencies?searchValue=${searchText}&type=${type}`,
) )
.then((data: any) => { .then(({ code, data }) => {
setValue(data.data); if (code === 200) {
setValue(data);
}
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
@@ -208,18 +216,14 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
.delete(`${config.apiPrefix}dependencies${force ? '/force' : ''}`, { .delete(`${config.apiPrefix}dependencies${force ? '/force' : ''}`, {
data: [record.id], data: [record.id],
}) })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200 && force) {
if (force) { const i = value.findIndex((x) => x.id === data.data[0].id);
const i = value.findIndex((x) => x.id === data.data[0].id); if (i !== -1) {
if (i !== -1) { const result = [...value];
const result = [...value]; result.splice(i, 1);
result.splice(i, 1); setValue(result);
setValue(result);
}
} }
} else {
message.error(data);
} }
}); });
}, },
@@ -246,11 +250,9 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
.put(`${config.apiPrefix}dependencies/reinstall`, { .put(`${config.apiPrefix}dependencies/reinstall`, {
data: [record.id], data: [record.id],
}) })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
handleDependence(data.data[0]); handleDependence(data[0]);
} else {
message.error(data);
} }
}); });
}, },
@@ -282,16 +284,10 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
const onSelectChange = (selectedIds: any[]) => { const onSelectChange = (selectedIds: any[]) => {
setSelectedRowIds(selectedIds); setSelectedRowIds(selectedIds);
setTimeout(() => {
if (selectedRowIds.length === 0 || selectedIds.length === 0) {
setTableScrollHeight(getTableScroll({ extraHeight: 87 }));
}
});
}; };
const rowSelection = { const rowSelection = {
selectedRowIds, selectedRowKeys: selectedRowIds,
onChange: onSelectChange, onChange: onSelectChange,
}; };
@@ -305,12 +301,32 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
.delete(`${config.apiPrefix}dependencies${forceUrl}`, { .delete(`${config.apiPrefix}dependencies${forceUrl}`, {
data: selectedRowIds, data: selectedRowIds,
}) })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
setSelectedRowIds([]);
getDependencies();
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const handlereInstallDependencies = () => {
Modal.confirm({
title: '确认重新安装',
content: <></>,
onOk() {
request
.put(`${config.apiPrefix}dependencies/reinstall`, {
data: selectedRowIds,
})
.then(({ code, data }) => {
if (code === 200) {
setSelectedRowIds([]); setSelectedRowIds([]);
getDependencies(); getDependencies();
} else {
message.error(data);
} }
}); });
}, },
@@ -323,15 +339,17 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
const getDependenceDetail = (dependence: any) => { const getDependenceDetail = (dependence: any) => {
request request
.get(`${config.apiPrefix}dependencies/${dependence.id}`) .get(`${config.apiPrefix}dependencies/${dependence.id}`)
.then((data: any) => { .then(({ code, data }) => {
const index = value.findIndex((x) => x.id === dependence.id); if (code === 200) {
const result = [...value]; const index = value.findIndex((x) => x.id === dependence.id);
if (index !== -1) { const result = [...value];
result.splice(index, 1, { if (index !== -1) {
...dependence, result.splice(index, 1, {
...data.data, ...dependence,
}); ...data,
setValue(result); });
setValue(result);
}
} }
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
@@ -345,12 +363,6 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
getDependencies(); getDependencies();
}, [searchText, type]); }, [searchText, type]);
useEffect(() => {
setTimeout(() => {
setTableScrollHeight(getTableScroll({ extraHeight: 87 }));
});
}, []);
useEffect(() => { useEffect(() => {
if (logDependence) { if (logDependence) {
localStorage.setItem('logDependence', logDependence.id); localStorage.setItem('logDependence', logDependence.id);
@@ -399,46 +411,8 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
} }
}, [socketMessage]); }, [socketMessage]);
const panelContent = () => (
<>
{selectedRowIds.length > 0 && (
<div style={{ marginBottom: 16 }}>
<Button
type="primary"
style={{ marginBottom: 5, marginLeft: 8 }}
onClick={() => delDependencies(false)}
>
</Button>
<Button
type="primary"
style={{ marginBottom: 5, marginLeft: 8 }}
onClick={() => delDependencies(true)}
>
</Button>
<span style={{ marginLeft: 8 }}>
<a>{selectedRowIds?.length}</a>
</span>
</div>
)}
<DndProvider backend={HTML5Backend}>
<Table
columns={columns}
rowSelection={rowSelection}
pagination={false}
dataSource={value}
rowKey="id"
size="middle"
scroll={{ x: 768, y: tableScrollHeight }}
loading={loading}
/>
</DndProvider>
</>
);
const onTabChange = (activeKey: string) => { const onTabChange = (activeKey: string) => {
setSelectedRowIds([]);
setType(activeKey); setType(activeKey);
}; };
@@ -467,17 +441,64 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
size="small" size="small"
tabPosition="top" tabPosition="top"
onChange={onTabChange} onChange={onTabChange}
> items={[
<Tabs.TabPane tab="NodeJs" key="nodejs"> {
{panelContent()} key: 'nodejs',
</Tabs.TabPane> label: 'NodeJs',
<Tabs.TabPane tab="Python3" key="python3"> },
{panelContent()} {
</Tabs.TabPane> key: 'python3',
<Tabs.TabPane tab="Linux" key="linux"> label: 'Python3',
{panelContent()} },
</Tabs.TabPane> {
</Tabs> key: 'linux',
label: 'Linux',
},
]}
/>
<div ref={tableRef}>
{selectedRowIds.length > 0 && (
<div style={{ marginBottom: 16 }}>
<Button
type="primary"
style={{ marginBottom: 5, marginLeft: 8 }}
onClick={() => handlereInstallDependencies()}
>
</Button>
<Button
type="primary"
style={{ marginBottom: 5, marginLeft: 8 }}
onClick={() => delDependencies(false)}
>
</Button>
<Button
type="primary"
style={{ marginBottom: 5, marginLeft: 8 }}
onClick={() => delDependencies(true)}
>
</Button>
<span style={{ marginLeft: 8 }}>
<a>{selectedRowIds?.length}</a>
</span>
</div>
)}
<DndProvider backend={HTML5Backend}>
<Table
columns={columns}
rowSelection={rowSelection}
pagination={false}
dataSource={value}
rowKey="id"
size="middle"
scroll={{ x: 768, y: tableScrollHeight }}
loading={loading}
/>
</DndProvider>
</div>
<DependenceModal <DependenceModal
visible={isModalVisible} visible={isModalVisible}
handleCancel={handleCancel} handleCancel={handleCancel}
+11 -6
View File
@@ -48,9 +48,12 @@ const DependenceLogModal = ({
setLoading(true); setLoading(true);
request request
.get(`${config.apiPrefix}dependencies/${dependence.id}`) .get(`${config.apiPrefix}dependencies/${dependence.id}`)
.then((data: any) => { .then(({ code, data }) => {
if (localStorage.getItem('logDependence') === String(dependence.id)) { if (
const log = (data.data.log || []).join('') as string; code === 200 &&
localStorage.getItem('logDependence') === String(dependence.id)
) {
const log = (data.log || []).join('') as string;
setValue(log); setValue(log);
setExecuting(!log.includes('结束时间')); setExecuting(!log.includes('结束时间'));
setIsRemoveFailed(log.includes('删除失败')); setIsRemoveFailed(log.includes('删除失败'));
@@ -67,8 +70,10 @@ const DependenceLogModal = ({
.delete(`${config.apiPrefix}dependencies/force`, { .delete(`${config.apiPrefix}dependencies/force`, {
data: [dependence.id], data: [dependence.id],
}) })
.then((data: any) => { .then(({ code, data }) => {
cancel(true); if (code === 200) {
cancel(true);
}
}) })
.finally(() => { .finally(() => {
setRemoveLoading(false); setRemoveLoading(false);
@@ -112,7 +117,7 @@ const DependenceLogModal = ({
return ( return (
<Modal <Modal
title={titleElement()} title={titleElement()}
visible={visible} open={visible}
centered centered
className="log-modal" className="log-modal"
bodyStyle={{ bodyStyle={{
+7 -5
View File
@@ -52,11 +52,11 @@ const DependenceModal = ({
data: payload, data: payload,
}, },
); );
if (code !== 200) {
message.error(data); if (code === 200) {
handleCancel(data);
} }
setLoading(false); setLoading(false);
handleCancel(data);
} catch (error) { } catch (error) {
setLoading(false); setLoading(false);
} }
@@ -69,7 +69,7 @@ const DependenceModal = ({
return ( return (
<Modal <Modal
title={dependence ? '编辑依赖' : '新建依赖'} title={dependence ? '编辑依赖' : '新建依赖'}
visible={visible} open={visible}
forceRender forceRender
centered centered
maskClosable={false} maskClosable={false}
@@ -99,7 +99,9 @@ const DependenceModal = ({
> >
<Select> <Select>
{config.dependenceTypes.map((x, i) => ( {config.dependenceTypes.map((x, i) => (
<Option value={i}>{x}</Option> <Option key={i} value={i}>
{x}
</Option>
))} ))}
</Select> </Select>
</Form.Item> </Form.Item>
+4
View File
@@ -17,4 +17,8 @@
.ant-form-item { .ant-form-item {
margin-bottom: 8px; margin-bottom: 8px;
} }
+ section {
height: calc(100% - 40px) !important;
}
} }
+32 -13
View File
@@ -6,10 +6,13 @@ import { request } from '@/utils/http';
import './index.less'; import './index.less';
import { DiffEditor } from '@monaco-editor/react'; import { DiffEditor } from '@monaco-editor/react';
import ReactDiffViewer from 'react-diff-viewer'; import ReactDiffViewer from 'react-diff-viewer';
import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts';
const { Option } = Select; const { Option } = Select;
const Diff = ({ headerStyle, isPhone, theme }: any) => { const Diff = () => {
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
const [origin, setOrigin] = useState('config.sample.sh'); const [origin, setOrigin] = useState('config.sample.sh');
const [current, setCurrent] = useState('config.sh'); const [current, setCurrent] = useState('config.sh');
const [originValue, setOriginValue] = useState(''); const [originValue, setOriginValue] = useState('');
@@ -19,15 +22,23 @@ const Diff = ({ headerStyle, isPhone, theme }: any) => {
const editorRef = useRef<any>(null); const editorRef = useRef<any>(null);
const getConfig = () => { const getConfig = () => {
request.get(`${config.apiPrefix}configs/${current}`).then((data) => { request
setCurrentValue(data.data); .get(`${config.apiPrefix}configs/${current}`)
}); .then(({ code, data }) => {
if (code === 200) {
setCurrentValue(data);
}
});
}; };
const getSample = () => { const getSample = () => {
request.get(`${config.apiPrefix}configs/${origin}`).then((data) => { request
setOriginValue(data.data); .get(`${config.apiPrefix}configs/${origin}`)
}); .then(({ code, data }) => {
if (code === 200) {
setOriginValue(data);
}
});
}; };
const updateConfig = () => { const updateConfig = () => {
@@ -39,8 +50,10 @@ const Diff = ({ headerStyle, isPhone, theme }: any) => {
.post(`${config.apiPrefix}configs/save`, { .post(`${config.apiPrefix}configs/save`, {
data: { content, name: current }, data: { content, name: current },
}) })
.then((data: any) => { .then(({ code, data }) => {
message.success(data.message); if (code === 200) {
message.success('保存成功');
}
}); });
}; };
@@ -48,8 +61,10 @@ const Diff = ({ headerStyle, isPhone, theme }: any) => {
setLoading(true); setLoading(true);
request request
.get(`${config.apiPrefix}configs/files`) .get(`${config.apiPrefix}configs/files`)
.then((data: any) => { .then(({ code, data }) => {
setFiles(data.data); if (code === 200) {
setFiles(data);
}
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
@@ -95,7 +110,9 @@ const Diff = ({ headerStyle, isPhone, theme }: any) => {
<Form.Item label="源文件"> <Form.Item label="源文件">
<Select value={origin} onChange={originFileChange}> <Select value={origin} onChange={originFileChange}>
{files.map((x) => ( {files.map((x) => (
<Option value={x.value}>{x.title}</Option> <Option key={x.value} value={x.value}>
{x.title}
</Option>
))} ))}
</Select> </Select>
</Form.Item> </Form.Item>
@@ -104,7 +121,9 @@ const Diff = ({ headerStyle, isPhone, theme }: any) => {
<Form.Item label="当前文件"> <Form.Item label="当前文件">
<Select value={current} onChange={currentFileChange}> <Select value={current} onChange={currentFileChange}>
{files.map((x) => ( {files.map((x) => (
<Option value={x.value}>{x.title}</Option> <Option key={x.value} value={x.value}>
{x.title}
</Option>
))} ))}
</Select> </Select>
</Form.Item> </Form.Item>
+16 -13
View File
@@ -17,19 +17,22 @@ const EditNameModal = ({
const handleOk = async (values: any) => { const handleOk = async (values: any) => {
setLoading(true); setLoading(true);
const { code, data } = await request.put(`${config.apiPrefix}envs/name`, { try {
data: { const { code, data } = await request.put(`${config.apiPrefix}envs/name`, {
ids, data: {
name: values.name, ids,
}, name: values.name,
}); },
if (code === 200) { });
message.success('更新环境变量名称成功');
} else { if (code === 200) {
message.error(data); message.success('更新环境变量名称成功');
handleCancel();
}
setLoading(false);
} catch (error) {
setLoading(false);
} }
setLoading(false);
handleCancel();
}; };
useEffect(() => { useEffect(() => {
@@ -39,7 +42,7 @@ const EditNameModal = ({
return ( return (
<Modal <Modal
title="修改环境变量名称" title="修改环境变量名称"
visible={visible} open={visible}
forceRender forceRender
centered centered
maskClosable={false} maskClosable={false}
+67 -41
View File
@@ -9,6 +9,8 @@ import {
Typography, Typography,
Tooltip, Tooltip,
Input, Input,
UploadProps,
Upload,
} from 'antd'; } from 'antd';
import { import {
EditOutlined, EditOutlined,
@@ -16,6 +18,7 @@ import {
SyncOutlined, SyncOutlined,
CheckCircleOutlined, CheckCircleOutlined,
StopOutlined, StopOutlined,
UploadOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import config from '@/utils/config'; import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout'; import { PageContainer } from '@ant-design/pro-layout';
@@ -25,7 +28,10 @@ import EditNameModal from './editNameModal';
import { DndProvider, useDrag, useDrop } from 'react-dnd'; import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend'; import { HTML5Backend } from 'react-dnd-html5-backend';
import './index.less'; import './index.less';
import { exportJson, getTableScroll } from '@/utils/index'; import { exportJson } from '@/utils/index';
import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts';
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
const { Text, Paragraph } = Typography; const { Text, Paragraph } = Typography;
const { Search } = Input; const { Search } = Input;
@@ -96,7 +102,8 @@ const DragableBodyRow = ({
); );
}; };
const Env = ({ headerStyle, isPhone, theme }: any) => { const Env = () => {
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
const columns: any = [ const columns: any = [
{ {
title: '序号', title: '序号',
@@ -128,6 +135,7 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
textAlign: 'left', textAlign: 'left',
}} }}
ellipsis={{ tooltip: text, rows: 2 }} ellipsis={{ tooltip: text, rows: 2 }}
copyable
> >
{text} {text}
</Paragraph> </Paragraph>
@@ -246,14 +254,18 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
const [editedEnv, setEditedEnv] = useState(); const [editedEnv, setEditedEnv] = useState();
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]); const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
const [searchText, setSearchText] = useState(''); const [searchText, setSearchText] = useState('');
const [tableScrollHeight, setTableScrollHeight] = useState<number>(); const [importLoading, setImportLoading] = useState(false);
const tableRef = useRef<any>();
const tableScrollHeight = useTableScrollHeight(tableRef, 59)
const getEnvs = () => { const getEnvs = () => {
setLoading(true); setLoading(true);
request request
.get(`${config.apiPrefix}envs?searchValue=${searchText}`) .get(`${config.apiPrefix}envs?searchValue=${searchText}`)
.then((data: any) => { .then(({ code, data }) => {
setValue(data.data); if (code === 200) {
setValue(data);
}
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
@@ -274,15 +286,14 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
onOk() { onOk() {
request request
.put( .put(
`${config.apiPrefix}envs/${ `${config.apiPrefix}envs/${record.status === Status. ? 'enable' : 'disable'
record.status === Status. ? 'enable' : 'disable'
}`, }`,
{ {
data: [record.id], data: [record.id],
}, },
) )
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
message.success( message.success(
`${record.status === Status. ? '启用' : '禁用'}成功`, `${record.status === Status. ? '启用' : '禁用'}成功`,
); );
@@ -294,8 +305,6 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
status: newStatus, status: newStatus,
}); });
setValue(result); setValue(result);
} else {
message.error(data);
} }
}); });
}, },
@@ -330,14 +339,12 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
onOk() { onOk() {
request request
.delete(`${config.apiPrefix}envs`, { data: [record.id] }) .delete(`${config.apiPrefix}envs`, { data: [record.id] })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
message.success('删除成功'); message.success('删除成功');
const result = [...value]; const result = [...value];
result.splice(index, 1); result.splice(index, 1);
setValue(result); setValue(result);
} else {
message.error(data);
} }
}); });
}, },
@@ -387,14 +394,12 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
.put(`${config.apiPrefix}envs/${dragRow.id}/move`, { .put(`${config.apiPrefix}envs/${dragRow.id}/move`, {
data: { fromIndex: dragIndex, toIndex: hoverIndex }, data: { fromIndex: dragIndex, toIndex: hoverIndex },
}) })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
const newData = [...value]; const newData = [...value];
newData.splice(dragIndex, 1); newData.splice(dragIndex, 1);
newData.splice(hoverIndex, 0, { ...dragRow, ...data.data }); newData.splice(hoverIndex, 0, { ...dragRow, ...data.data });
setValue([...newData]); setValue([...newData]);
} else {
message.error(data);
} }
}); });
}, },
@@ -403,16 +408,10 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
const onSelectChange = (selectedIds: any[]) => { const onSelectChange = (selectedIds: any[]) => {
setSelectedRowIds(selectedIds); setSelectedRowIds(selectedIds);
setTimeout(() => {
if (selectedRowIds.length === 0 || selectedIds.length === 0) {
setTableScrollHeight(getTableScroll({ extraHeight: 87 }));
}
});
}; };
const rowSelection = { const rowSelection = {
selectedRowIds, selectedRowKeys: selectedRowIds,
onChange: onSelectChange, onChange: onSelectChange,
}; };
@@ -423,13 +422,11 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
onOk() { onOk() {
request request
.delete(`${config.apiPrefix}envs`, { data: selectedRowIds }) .delete(`${config.apiPrefix}envs`, { data: selectedRowIds })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
message.success('批量删除成功'); message.success('批量删除成功');
setSelectedRowIds([]); setSelectedRowIds([]);
getEnvs(); getEnvs();
} else {
message.error(data);
} }
}); });
}, },
@@ -448,11 +445,9 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
.put(`${config.apiPrefix}envs/${OperationPath[operationStatus]}`, { .put(`${config.apiPrefix}envs/${OperationPath[operationStatus]}`, {
data: selectedRowIds, data: selectedRowIds,
}) })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
getEnvs(); getEnvs();
} else {
message.error(data);
} }
}); });
}, },
@@ -477,16 +472,37 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
setSearchText(value.trim()); setSearchText(value.trim());
}; };
const uploadProps: UploadProps = {
accept: 'application/json',
beforeUpload: async (file) => {
const formData = new FormData();
formData.append('env', file);
setImportLoading(true);
try {
const { code, data } = await request.post(
`${config.apiPrefix}envs/upload`,
{
data: formData,
},
);
if (code === 200) {
message.success(`成功上传${data.length}个环境变量`);
getEnvs();
}
setImportLoading(false);
} catch (error: any) {
setImportLoading(false);
}
return false;
},
fileList: [],
};
useEffect(() => { useEffect(() => {
getEnvs(); getEnvs();
}, [searchText]); }, [searchText]);
useEffect(() => {
setTimeout(() => {
setTableScrollHeight(getTableScroll({ extraHeight: 87 }));
});
}, []);
return ( return (
<PageContainer <PageContainer
className="ql-container-wrapper env-wrapper" className="ql-container-wrapper env-wrapper"
@@ -499,6 +515,15 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
loading={loading} loading={loading}
onSearch={onSearch} onSearch={onSearch}
/>, />,
<Upload {...uploadProps}>
<Button
type="primary"
icon={<UploadOutlined />}
loading={importLoading}
>
</Button>
</Upload>,
<Button key="2" type="primary" onClick={() => addEnv()}> <Button key="2" type="primary" onClick={() => addEnv()}>
</Button>, </Button>,
@@ -552,6 +577,7 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
)} )}
<DndProvider backend={HTML5Backend}> <DndProvider backend={HTML5Backend}>
<Table <Table
ref={tableRef}
columns={columns} columns={columns}
rowSelection={rowSelection} rowSelection={rowSelection}
pagination={false} pagination={false}
@@ -561,7 +587,7 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
scroll={{ x: 1000, y: tableScrollHeight }} scroll={{ x: 1000, y: tableScrollHeight }}
components={components} components={components}
loading={loading} loading={loading}
onRow={(record: any, index: number) => { onRow={(record: any, index: number | undefined) => {
return { return {
index, index,
moveRow, moveRow,
+3 -4
View File
@@ -40,13 +40,12 @@ const EnvModal = ({
const { code, data } = await request[method](`${config.apiPrefix}envs`, { const { code, data } = await request[method](`${config.apiPrefix}envs`, {
data: payload, data: payload,
}); });
if (code === 200) { if (code === 200) {
message.success(env ? '更新变量成功' : '新建变量成功'); message.success(env ? '更新变量成功' : '新建变量成功');
} else { handleCancel(data);
message.error(data);
} }
setLoading(false); setLoading(false);
handleCancel(data);
} catch (error: any) { } catch (error: any) {
setLoading(false); setLoading(false);
} }
@@ -59,7 +58,7 @@ const EnvModal = ({
return ( return (
<Modal <Modal
title={env ? '编辑变量' : '新建变量'} title={env ? '编辑变量' : '新建变量'}
visible={visible} open={visible}
forceRender forceRender
centered centered
maskClosable={false} maskClosable={false}
+8 -4
View File
@@ -3,11 +3,13 @@ import config from '@/utils/config';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import Terminal, { ColorMode, LineType } from '../../components/terminal'; import Terminal, { ColorMode, LineType } from '../../components/terminal';
import { PageLoading } from '@ant-design/pro-layout'; import { PageLoading } from '@ant-design/pro-layout';
import { history } from 'umi'; import { history, useOutletContext } from '@umijs/max';
import Ansi from 'ansi-to-react'; import Ansi from 'ansi-to-react';
import './index.less'; import './index.less';
import { SharedContext } from '@/layouts';
const Error = ({ user, theme }: any) => { const Error = () => {
const { user, theme } = useOutletContext<SharedContext>();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [data, setData] = useState('暂无日志'); const [data, setData] = useState('暂无日志');
@@ -15,8 +17,10 @@ const Error = ({ user, theme }: any) => {
setLoading(true); setLoading(true);
request request
.get(`${config.apiPrefix}public/panel/log`) .get(`${config.apiPrefix}public/panel/log`)
.then((data: any) => { .then(({ code, data }) => {
setData(data.data); if (code === 200) {
setData(data);
}
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
+10 -11
View File
@@ -10,7 +10,7 @@ import {
Select, Select,
} from 'antd'; } from 'antd';
import config from '@/utils/config'; import config from '@/utils/config';
import { history } from 'umi'; import { history } from '@umijs/max';
import styles from './index.less'; import styles from './index.less';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
@@ -41,11 +41,9 @@ const Initialization = () => {
password: values.password, password: values.password,
}, },
}) })
.then((data) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
next(); next();
} else {
message.error(data.message);
} }
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
@@ -59,11 +57,9 @@ const Initialization = () => {
...values, ...values,
}, },
}) })
.then((_data: any) => { .then(({ code, data }) => {
if (_data && _data.code === 200) { if (code === 200) {
next(); next();
} else {
message.error(_data.message);
} }
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
@@ -116,12 +112,15 @@ const Initialization = () => {
{config.notificationModes {config.notificationModes
.filter((x) => x.value !== 'closed') .filter((x) => x.value !== 'closed')
.map((x) => ( .map((x) => (
<Option value={x.value}>{x.label}</Option> <Option key={x.value} value={x.value}>
{x.label}
</Option>
))} ))}
</Select> </Select>
</Form.Item> </Form.Item>
{fields.map((x) => ( {fields.map((x) => (
<Form.Item <Form.Item
key={x.label}
label={x.label} label={x.label}
name={x.label} name={x.label}
extra={x.tip} extra={x.tip}
@@ -233,7 +232,7 @@ const Initialization = () => {
<img <img
alt="logo" alt="logo"
className={styles.logo} className={styles.logo}
src="http://qn.whyour.cn/logo.png" src="https://qn.whyour.cn/logo.png"
/> />
<span className={styles.title}></span> <span className={styles.title}></span>
</div> </div>
+1 -2
View File
@@ -6,8 +6,7 @@
overflow: hidden; overflow: hidden;
position: relative; position: relative;
background-color: @component-background; background-color: @component-background;
height: calc(100vh - 128px); height: 100%;
height: calc(100vh - var(--vh-offset, 0px) - 128px);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
+156 -61
View File
@@ -1,5 +1,15 @@
import { useState, useEffect, useCallback, Key, useRef } from 'react'; import { useState, useEffect, useCallback, Key, useRef } from 'react';
import { TreeSelect, Tree, Input, Empty } from 'antd'; import {
TreeSelect,
Tree,
Input,
Empty,
Button,
message,
Modal,
Tooltip,
Typography,
} from 'antd';
import config from '@/utils/config'; import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout'; import { PageContainer } from '@ant-design/pro-layout';
import Editor from '@monaco-editor/react'; import Editor from '@monaco-editor/react';
@@ -7,53 +17,36 @@ import { request } from '@/utils/http';
import styles from './index.module.less'; import styles from './index.module.less';
import { Controlled as CodeMirror } from 'react-codemirror2'; import { Controlled as CodeMirror } from 'react-codemirror2';
import SplitPane from 'react-split-pane'; 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/groupBy';
import uniq from 'lodash/uniq';
import useFilterTreeData from '@/hooks/useFilterTreeData';
function getFilterData(keyword: string, data: any) { const { Text } = Typography;
const expandedKeys: string[] = [];
if (keyword) {
const tree: any = [];
data.forEach((item: any) => {
if (item.title.toLocaleLowerCase().includes(keyword)) {
tree.push(item);
} else {
const children: any[] = [];
(item.children || []).forEach((subItem: any) => {
if (subItem.title.toLocaleLowerCase().includes(keyword)) {
children.push(subItem);
}
});
if (children.length > 0) {
tree.push({
...item,
children,
});
expandedKeys.push(item.key);
}
}
});
return { tree, expandedKeys };
}
return { tree: data, expandedKeys };
}
const Log = ({ headerStyle, isPhone, theme }: any) => { const Log = () => {
const [title, setTitle] = useState('请选择日志文件'); const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
const [value, setValue] = useState('请选择日志文件'); const [value, setValue] = useState('请选择日志文件');
const [select, setSelect] = useState<any>(); const [select, setSelect] = useState<string>('');
const [data, setData] = useState<any[]>([]); const [data, setData] = useState<any[]>([]);
const [filterData, setFilterData] = useState<any[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [height, setHeight] = useState<number>(); const [height, setHeight] = useState<number>();
const treeDom = useRef<any>(); const treeDom = useRef<any>();
const [expandedKeys, setExpandedKeys] = useState<string[]>([]); const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
const [currentNode, setCurrentNode] = useState<any>();
const [searchValue, setSearchValue] = useState('');
const getLogs = () => { const getLogs = () => {
setLoading(true); setLoading(true);
request request
.get(`${config.apiPrefix}logs`) .get(`${config.apiPrefix}logs`)
.then((data) => { .then(({ code, data }) => {
setData(data.data); if (code === 200) {
setFilterData(data.data); setData(data);
}
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
@@ -61,18 +54,27 @@ const Log = ({ headerStyle, isPhone, theme }: any) => {
const getLog = (node: any) => { const getLog = (node: any) => {
request request
.get(`${config.apiPrefix}logs/${node.title}?path=${node.parent || ''}`) .get(`${config.apiPrefix}logs/${node.title}?path=${node.parent || ''}`)
.then((data) => { .then(({ code, data }) => {
setValue(data.data); if (code === 200) {
setValue(data);
}
}); });
}; };
const onSelect = (value: any, node: any) => { const onSelect = (value: any, node: any) => {
setCurrentNode(node);
setSelect(value);
if (node.key === select || !value) { if (node.key === select || !value) {
return; return;
} }
if (node.type === 'directory') {
setValue('请选择日志文件');
return;
}
setValue('加载中...'); setValue('加载中...');
setSelect(value);
setTitle(node.key);
getLog(node); getLog(node);
}; };
@@ -83,41 +85,130 @@ const Log = ({ headerStyle, isPhone, theme }: any) => {
const onSearch = useCallback( const onSearch = useCallback(
(e) => { (e) => {
const keyword = e.target.value; const keyword = e.target.value;
const { tree, expandedKeys } = getFilterData( debounceSearch(keyword);
keyword.toLocaleLowerCase(),
data,
);
setFilterData(tree);
setExpandedKeys(expandedKeys);
}, },
[data, setFilterData], [data],
);
const debounceSearch = useCallback(
debounce((keyword) => {
setSearchValue(keyword);
}, 300),
[data],
);
const { treeData: filterData, keys: searchExpandedKeys } = useFilterTreeData(
data,
searchValue,
{ treeNodeFilterProp: 'title' },
); );
useEffect(() => {
setExpandedKeys(uniq([...expandedKeys, ...searchExpandedKeys]));
}, [searchExpandedKeys]);
const deleteFile = () => {
Modal.confirm({
title: `确认删除`,
content: (
<>
<Text style={{ wordBreak: 'break-all' }} type="warning">
{select}
</Text>
{currentNode.type === 'directory' ? '夹下所以日志' : ''}
</>
),
onOk() {
request
.delete(`${config.apiPrefix}logs`, {
data: {
filename: currentNode.title,
path: currentNode.parent || '',
type: currentNode.type,
},
})
.then(({ code }) => {
if (code === 200) {
message.success(`删除成功`);
let newData = [...data];
if (currentNode.parent) {
newData = depthFirstSearch(
newData,
(c) => c.key === currentNode.key,
);
} else {
const index = newData.findIndex(
(x) => x.key === currentNode.key,
);
if (index !== -1) {
newData.splice(index, 1);
}
}
setData(newData);
initState();
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const initState = () => {
setSelect('');
setCurrentNode(null);
setValue('请选择脚本文件');
};
const onExpand = (expKeys: any) => {
setExpandedKeys(expKeys);
};
useEffect(() => { useEffect(() => {
getLogs(); getLogs();
if (treeDom && treeDom.current) { }, []);
useEffect(() => {
if (treeDom.current) {
setHeight(treeDom.current.clientHeight); setHeight(treeDom.current.clientHeight);
} }
}, []); }, [treeDom.current, data]);
return ( return (
<PageContainer <PageContainer
className="ql-container-wrapper log-wrapper" className="ql-container-wrapper log-wrapper"
title={title} title={select}
loading={loading} loading={loading}
extra={ extra={
isPhone && [ isPhone
<TreeSelect ? [
className="log-select" <TreeSelect
value={select} treeExpandAction="click"
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }} className="log-select"
treeData={data} value={select}
placeholder="请选择日志" dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
fieldNames={{ value: 'key', label: 'title' }} treeData={data}
showSearch placeholder="请选择日志"
onSelect={onSelect} fieldNames={{ value: 'key' }}
/>, treeNodeFilterProp="title"
] showSearch
allowClear
onSelect={onSelect}
/>,
]
: [
<Tooltip title="删除">
<Button
type="primary"
disabled={!select}
onClick={deleteFile}
icon={<DeleteOutlined />}
/>
</Tooltip>,
]
} }
header={{ header={{
style: headerStyle, style: headerStyle,
@@ -125,6 +216,7 @@ const Log = ({ headerStyle, isPhone, theme }: any) => {
> >
<div className={`${styles['log-container']} log-container`}> <div className={`${styles['log-container']} log-container`}>
{!isPhone && ( {!isPhone && (
/*// @ts-ignore*/
<SplitPane split="vertical" size={200} maxSize={-100}> <SplitPane split="vertical" size={200} maxSize={-100}>
<div className={styles['left-tree-container']}> <div className={styles['left-tree-container']}>
{data.length > 0 ? ( {data.length > 0 ? (
@@ -137,6 +229,7 @@ const Log = ({ headerStyle, isPhone, theme }: any) => {
></Input.Search> ></Input.Search>
<div className={styles['left-tree-scroller']} ref={treeDom}> <div className={styles['left-tree-scroller']} ref={treeDom}>
<Tree <Tree
expandAction="click"
className={styles['left-tree']} className={styles['left-tree']}
treeData={filterData} treeData={filterData}
showIcon={true} showIcon={true}
@@ -144,6 +237,8 @@ const Log = ({ headerStyle, isPhone, theme }: any) => {
selectedKeys={[select]} selectedKeys={[select]}
showLine={{ showLeafIcon: true }} showLine={{ showLeafIcon: true }}
onSelect={onTreeSelect} onSelect={onTreeSelect}
expandedKeys={expandedKeys}
onExpand={onExpand}
></Tree> ></Tree>
</div> </div>
</> </>
+21 -27
View File
@@ -9,16 +9,18 @@ import {
Statistic, Statistic,
} from 'antd'; } from 'antd';
import config from '@/utils/config'; import config from '@/utils/config';
import { history, Link } from 'umi'; import { history, useOutletContext } from '@umijs/max';
import styles from './index.less'; import styles from './index.less';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import { useTheme } from '@/utils/hooks'; import { useTheme } from '@/utils/hooks';
import { MobileOutlined } from '@ant-design/icons'; import { MobileOutlined } from '@ant-design/icons';
import { SharedContext } from '@/layouts';
const FormItem = Form.Item; const FormItem = Form.Item;
const { Countdown } = Statistic; const { Countdown } = Statistic;
const Login = ({ reloadUser }: any) => { const Login = () => {
const { reloadUser } = useOutletContext<SharedContext>();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [waitTime, setWaitTime] = useState<any>(); const [waitTime, setWaitTime] = useState<any>();
const { theme } = useTheme(); const { theme } = useTheme();
@@ -38,15 +40,7 @@ const Login = ({ reloadUser }: any) => {
}, },
}) })
.then((data) => { .then((data) => {
if (data.code === 420) { checkResponse(data, values);
setLoginInfo({
username: values.username,
password: values.password,
});
setTwoFactor(true);
} else {
checkResponse(data);
}
setLoading(false); setLoading(false);
}) })
.catch(function (error) { .catch(function (error) {
@@ -62,11 +56,7 @@ const Login = ({ reloadUser }: any) => {
data: { ...loginInfo, code: values.code }, data: { ...loginInfo, code: values.code },
}) })
.then((data: any) => { .then((data: any) => {
if (data.code === 430) { checkResponse(data);
message.error(data.message);
} else {
checkResponse(data);
}
setVerifying(false); setVerifying(false);
}) })
.catch((error: any) => { .catch((error: any) => {
@@ -75,8 +65,11 @@ const Login = ({ reloadUser }: any) => {
}); });
}; };
const checkResponse = (data: any) => { const checkResponse = (
if (data.code === 200) { { code, data, message: _message }: any,
values?: any,
) => {
if (code === 200) {
const { const {
token, token,
lastip, lastip,
@@ -84,7 +77,7 @@ const Login = ({ reloadUser }: any) => {
lastlogon, lastlogon,
retries = 0, retries = 0,
platform, platform,
} = data.data; } = data;
localStorage.setItem(config.authKey, token); localStorage.setItem(config.authKey, token);
notification.success({ notification.success({
message: '登录成功!', message: '登录成功!',
@@ -103,13 +96,14 @@ const Login = ({ reloadUser }: any) => {
}); });
reloadUser(true); reloadUser(true);
history.push('/crontab'); history.push('/crontab');
} else if (data.code === 100) { } else if (code === 410) {
message.warn(data.message); setWaitTime(data);
} else if (data.code === 410) { } else if (code === 420) {
message.error(data.message); setLoginInfo({
setWaitTime(data.data); username: values.username,
} else { password: values.password,
message.error(data.message); });
setTwoFactor(true);
} }
}; };
@@ -135,7 +129,7 @@ const Login = ({ reloadUser }: any) => {
<img <img
alt="logo" alt="logo"
className={styles.logo} className={styles.logo}
src="http://qn.whyour.cn/logo.png" src="https://qn.whyour.cn/logo.png"
/> />
<span className={styles.title}> <span className={styles.title}>
{twoFactor ? '两步验证' : config.siteName} {twoFactor ? '两步验证' : config.siteName}
+33 -9
View File
@@ -7,6 +7,7 @@ import Editor from '@monaco-editor/react';
import SaveModal from './saveModal'; import SaveModal from './saveModal';
import SettingModal from './setting'; import SettingModal from './setting';
import { useTheme } from '@/utils/hooks'; import { useTheme } from '@/utils/hooks';
import { logEnded } from '@/utils';
const { Option } = Select; const { Option } = Select;
const LangMap: any = { const LangMap: any = {
@@ -57,6 +58,11 @@ const EditModal = ({
if (node.key === selectedKey || !value) { if (node.key === selectedKey || !value) {
return; return;
} }
if (node.type === 'directory') {
return;
}
const newMode = LangMap[value.slice(-3)] || ''; const newMode = LangMap[value.slice(-3)] || '';
setCNode(node); setCNode(node);
setLanguage(newMode); setLanguage(newMode);
@@ -67,8 +73,10 @@ const EditModal = ({
const getDetail = (node: any) => { const getDetail = (node: any) => {
request request
.get(`${config.apiPrefix}scripts/${node.title}?path=${node.parent || ''}`) .get(`${config.apiPrefix}scripts/${node.title}?path=${node.parent || ''}`)
.then((data) => { .then(({ code, data }) => {
setValue(data.data); if (code === 200) {
setValue(data);
}
}); });
}; };
@@ -83,8 +91,10 @@ const EditModal = ({
content, content,
}, },
}) })
.then((data) => { .then(({ code, data }) => {
setIsRunning(true); if (code === 200) {
setIsRunning(true);
}
}); });
}; };
@@ -101,8 +111,10 @@ const EditModal = ({
content, content,
}, },
}) })
.then((data) => { .then(({ code, data }) => {
setIsRunning(false); if (code === 200) {
setIsRunning(false);
}
}); });
}; };
@@ -117,7 +129,7 @@ const EditModal = ({
return; return;
} }
if (_message.includes('执行结束')) { if (logEnded(_message)) {
setTimeout(() => { setTimeout(() => {
setIsRunning(false); setIsRunning(false);
}, 300); }, 300);
@@ -130,10 +142,13 @@ const EditModal = ({
}, [socketMessage]); }, [socketMessage]);
useEffect(() => { useEffect(() => {
setLog('');
if (currentNode) { if (currentNode) {
setCNode(currentNode); setCNode(currentNode);
setValue(content as string); setValue(content as string);
setSelectedKey(currentNode.key); setSelectedKey(currentNode.key);
const newMode = LangMap[currentNode.title.slice(-3)] || '';
setLanguage(newMode);
} }
}, [content, currentNode]); }, [content, currentNode]);
@@ -144,6 +159,7 @@ const EditModal = ({
title={ title={
<> <>
<TreeSelect <TreeSelect
treeExpandAction="click"
style={{ marginRight: 8, width: 150 }} style={{ marginRight: 8, width: 150 }}
value={selectedKey} value={selectedKey}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }} dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
@@ -214,8 +230,9 @@ const EditModal = ({
width={'100%'} width={'100%'}
headerStyle={{ padding: '11px 24px' }} headerStyle={{ padding: '11px 24px' }}
onClose={cancel} onClose={cancel}
visible={visible} open={visible}
> >
{/* @ts-ignore */}
<SplitPane <SplitPane
split="vertical" split="vertical"
minSize={200} minSize={200}
@@ -236,7 +253,14 @@ const EditModal = ({
editorRef.current = editor; editorRef.current = editor;
}} }}
/> />
<pre style={{ height: '100%', whiteSpace: 'break-spaces' }}>{log}</pre> <pre
style={{
height: '100%',
padding: '0 15px',
}}
>
{log}
</pre>
</SplitPane> </SplitPane>
<SaveModal <SaveModal
visible={saveModalVisible} visible={saveModalVisible}
+31 -14
View File
@@ -32,32 +32,31 @@ const EditScriptNameModal = ({
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [dirs, setDirs] = useState<any[]>([]); const [dirs, setDirs] = useState<any[]>([]);
const [file, setFile] = useState<File>(); const [file, setFile] = useState<File>();
const [type, setType] = useState<'blank' | 'upload'>('blank'); const [type, setType] = useState<'blank' | 'upload' | 'directory'>('blank');
const handleOk = async (values: any) => { const handleOk = async (values: any) => {
setLoading(true); setLoading(true);
values.path = values.path || ''; const { path = '', filename: inputFilename, directory = '' } = values;
const formData = new FormData(); const formData = new FormData();
formData.append('file', file as any); formData.append('file', file as any);
formData.append('filename', values.filename); formData.append('filename', inputFilename);
formData.append('path', values.path); formData.append('path', path);
formData.append('content', ''); formData.append('content', '');
formData.append('directory', directory);
request request
.post(`${config.apiPrefix}scripts`, { .post(`${config.apiPrefix}scripts`, {
data: formData, data: formData,
}) })
.then(({ code, data }) => { .then(({ code, data }) => {
if (code === 200) { if (code === 200) {
message.success('新建文件成功'); message.success(directory ? '新建文件夹成功' : '新建文件成功');
const key = values.path ? `${values.path}/` : ''; const key = path ? `${path}/` : '';
const filename = file ? file.name : values.filename; const filename = file ? file.name : inputFilename;
handleCancel({ handleCancel({
filename, filename,
path: values.path, path,
key: `${key}${filename}`, key: `${key}${filename}`,
}); });
} else {
message.error(data);
} }
setLoading(false); setLoading(false);
}) })
@@ -99,8 +98,8 @@ const EditScriptNameModal = ({
return ( return (
<Modal <Modal
title="新建脚本" title="新建"
visible={visible} open={visible}
forceRender forceRender
centered centered
maskClosable={false} maskClosable={false}
@@ -126,18 +125,36 @@ const EditScriptNameModal = ({
> >
<Radio.Group onChange={typeChange}> <Radio.Group onChange={typeChange}>
<Radio value="blank"></Radio> <Radio value="blank"></Radio>
<Radio value="upload"></Radio> <Radio value="upload"></Radio>
<Radio value="directory"></Radio>
</Radio.Group> </Radio.Group>
</Form.Item> </Form.Item>
{type === 'blank' && ( {type === 'blank' && (
<Form.Item <Form.Item
name="filename" name="filename"
label="文件名" label="文件名"
rules={[{ required: true, message: '请输入文件名' }]} rules={[
{ required: true, message: '请输入文件名' },
{
validator: (_, value) =>
value.includes('/')
? Promise.reject(new Error('文件名不能包含斜杠'))
: Promise.resolve(),
},
]}
> >
<Input placeholder="请输入文件名" /> <Input placeholder="请输入文件名" />
</Form.Item> </Form.Item>
)} )}
{type === 'directory' && (
<Form.Item
name="directory"
label="文件夹名"
rules={[{ required: true, message: '请输入文件夹名' }]}
>
<Input placeholder="请输入文件夹名" />
</Form.Item>
)}
<Form.Item label="父目录" name="path"> <Form.Item label="父目录" name="path">
<TreeSelect <TreeSelect
allowClear allowClear
+1 -2
View File
@@ -6,8 +6,7 @@
overflow: hidden; overflow: hidden;
position: relative; position: relative;
background-color: @component-background; background-color: @component-background;
height: calc(100vh - 128px); height: 100%;
height: calc(100vh - var(--vh-offset, 0px) - 128px);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
+97 -109
View File
@@ -33,38 +33,15 @@ import {
} from '@ant-design/icons'; } from '@ant-design/icons';
import EditScriptNameModal from './editNameModal'; import EditScriptNameModal from './editNameModal';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { history } from 'umi'; import { history, useOutletContext, useLocation } from '@umijs/max';
import { parse } from 'query-string';
import { depthFirstSearch } from '@/utils';
import { SharedContext } from '@/layouts';
import useFilterTreeData from '@/hooks/useFilterTreeData';
import uniq from 'lodash/uniq';
const { Text } = Typography; const { Text } = Typography;
function getFilterData(keyword: string, data: any) {
const expandedKeys: string[] = [];
if (keyword) {
const tree: any = [];
data.forEach((item: any) => {
if (item.title.toLocaleLowerCase().includes(keyword)) {
tree.push(item);
} else {
const children: any[] = [];
(item.children || []).forEach((subItem: any) => {
if (subItem.title.toLocaleLowerCase().includes(keyword)) {
children.push(subItem);
}
});
if (children.length > 0) {
tree.push({
...item,
children,
});
expandedKeys.push(item.key);
}
}
});
return { tree, expandedKeys };
}
return { tree: data, expandedKeys };
}
const LangMap: any = { const LangMap: any = {
'.py': 'python', '.py': 'python',
'.js': 'javascript', '.js': 'javascript',
@@ -72,12 +49,12 @@ const LangMap: any = {
'.ts': 'typescript', '.ts': 'typescript',
}; };
const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => { const Script = () => {
const [title, setTitle] = useState('请选择脚本文件'); const { headerStyle, isPhone, theme, socketMessage } =
useOutletContext<SharedContext>();
const [value, setValue] = useState('请选择脚本文件'); const [value, setValue] = useState('请选择脚本文件');
const [select, setSelect] = useState<any>(); const [select, setSelect] = useState<string>('');
const [data, setData] = useState<any[]>([]); const [data, setData] = useState<any[]>([]);
const [filterData, setFilterData] = useState<any[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [mode, setMode] = useState(''); const [mode, setMode] = useState('');
const [height, setHeight] = useState<number>(); const [height, setHeight] = useState<number>();
@@ -94,24 +71,31 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
setLoading(true); setLoading(true);
request request
.get(`${config.apiPrefix}scripts`) .get(`${config.apiPrefix}scripts`)
.then((data) => { .then(({ code, data }) => {
setData(data.data); if (code === 200) {
setFilterData(data.data); setData(data);
initGetScript(); initGetScript();
}
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
const getDetail = (node: any) => { const getDetail = (node: any) => {
request request
.get(`${config.apiPrefix}scripts/${node.title}?path=${node.parent || ''}`) .get(
.then((data) => { `${config.apiPrefix}scripts/${encodeURIComponent(node.title)}?path=${
setValue(data.data); node.parent || ''
}`,
)
.then(({ code, data }) => {
if (code === 200) {
setValue(data);
}
}); });
}; };
const initGetScript = () => { const initGetScript = () => {
const { p, s } = history.location.query as any; const { p, s } = parse(history.location.search);
if (s) { if (s) {
const vkey = `${p}/${s}`; const vkey = `${p}/${s}`;
const obj = { const obj = {
@@ -127,22 +111,24 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
}; };
const onSelect = (value: any, node: any) => { const onSelect = (value: any, node: any) => {
setSelect(node.key);
setCurrentNode(node);
if (node.key === select || !value) { if (node.key === select || !value) {
return; return;
} }
setValue('加载中...');
if (node.type === 'directory') {
setValue('请选择脚本文件');
return;
}
const newMode = value ? LangMap[value.slice(-3)] : ''; const newMode = value ? LangMap[value.slice(-3)] : '';
setMode(isPhone && newMode === 'typescript' ? 'javascript' : newMode); setMode(isPhone && newMode === 'typescript' ? 'javascript' : newMode);
setSelect(node.key); setValue('加载中...');
setTitle(node.key);
setCurrentNode(node);
getDetail(node); getDetail(node);
}; };
const onExpand = (expKeys: any) => {
setExpandedKeys(expKeys);
};
const onTreeSelect = useCallback( const onTreeSelect = useCallback(
(keys: Key[], e: any) => { (keys: Key[], e: any) => {
const content = editorRef.current const content = editorRef.current
@@ -173,22 +159,30 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
const keyword = e.target.value; const keyword = e.target.value;
debounceSearch(keyword); debounceSearch(keyword);
}, },
[data, setFilterData], [data],
); );
const debounceSearch = useCallback( const debounceSearch = useCallback(
debounce((keyword) => { debounce((keyword) => {
setSearchValue(keyword); setSearchValue(keyword);
const { tree, expandedKeys } = getFilterData(
keyword.toLocaleLowerCase(),
data,
);
setExpandedKeys(expandedKeys);
setFilterData(tree);
}, 300), }, 300),
[data, setFilterData], [data],
); );
const { treeData: filterData, keys: searchExpandedKeys } = useFilterTreeData(
data,
searchValue,
{ treeNodeFilterProp: 'title' },
);
useEffect(() => {
setExpandedKeys(uniq([...expandedKeys, ...searchExpandedKeys]));
}, [searchExpandedKeys]);
const onExpand = (expKeys: any) => {
setExpandedKeys(expKeys);
};
const editFile = () => { const editFile = () => {
setTimeout(() => { setTimeout(() => {
setIsEditing(true); setIsEditing(true);
@@ -226,13 +220,11 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
content, content,
}, },
}) })
.then((_data: any) => { .then(({ code, data }) => {
if (_data.code === 200) { if (code === 200) {
message.success(`保存成功`); message.success(`保存成功`);
setValue(content); setValue(content);
setIsEditing(false); setIsEditing(false);
} else {
message.error(_data);
} }
resolve(null); resolve(null);
}) })
@@ -250,10 +242,11 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
title: `确认删除`, title: `确认删除`,
content: ( content: (
<> <>
<Text style={{ wordBreak: 'break-all' }} type="warning"> <Text style={{ wordBreak: 'break-all' }} type="warning">
{select} {select}
</Text>{' '} </Text>
{currentNode.type === 'directory' ? '夹及其子文件' : ''}
</> </>
), ),
@@ -263,24 +256,18 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
data: { data: {
filename: currentNode.title, filename: currentNode.title,
path: currentNode.parent || '', path: currentNode.parent || '',
type: currentNode.type,
}, },
}) })
.then((_data: any) => { .then(({ code }) => {
if (_data.code === 200) { if (code === 200) {
message.success(`删除成功`); message.success(`删除成功`);
let newData = [...data]; let newData = [...data];
if (currentNode.parent) { if (currentNode.parent) {
const parentNodeIndex = newData.findIndex( newData = depthFirstSearch(
(x) => x.key === currentNode.parent, newData,
(c) => c.key === currentNode.key,
); );
const parentNode = newData[parentNodeIndex];
const index = parentNode.children.findIndex(
(y) => y.key === currentNode.key,
);
if (index !== -1 && parentNodeIndex !== -1) {
parentNode.children.splice(index, 1);
newData.splice(parentNodeIndex, 1, { ...parentNode });
}
} else { } else {
const index = newData.findIndex( const index = newData.findIndex(
(x) => x.key === currentNode.key, (x) => x.key === currentNode.key,
@@ -290,8 +277,7 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
} }
} }
setData(newData); setData(newData);
} else { initState();
message.error(_data);
} }
}); });
}, },
@@ -313,20 +299,17 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
}, },
) => { ) => {
if (filename) { if (filename) {
const newData = [...data]; let newData = [...data];
const _file = { title: filename, key, parent: path }; const _file = { title: filename, key, parent: path };
if (path) { if (path) {
// TODO: 更新左侧树数据 newData = depthFirstSearch(newData, (c) => c.key === path, _file);
const parentNodeIndex = newData.findIndex((x) => x.key === path); const keys = path.split('/');
if (parentNodeIndex !== -1) { const sKeys: string[] = [];
const parentNode = newData[parentNodeIndex]; keys.reduce((p, c) => {
if (parentNode.children && parentNode.children.length > 0) { sKeys.push(p);
parentNode.children.unshift(_file); return `${p}/${c}`;
} else { });
parentNode.children = [_file]; setExpandedKeys([...expandedKeys, ...sKeys, path]);
}
newData.splice(parentNodeIndex, 1, { ...parentNode });
}
} else { } else {
newData.unshift(_file); newData.unshift(_file);
} }
@@ -344,34 +327,35 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
filename: currentNode.title, filename: currentNode.title,
}, },
}) })
.then((_data: any) => { .then(({ code, data }) => {
const blob = new Blob([_data], { type: 'application/json' }); if (code === 200) {
const url = URL.createObjectURL(blob); const blob = new Blob([data], { type: 'application/json' });
const a = document.createElement('a'); const url = URL.createObjectURL(blob);
a.href = url; const a = document.createElement('a');
a.download = currentNode.title; a.href = url;
document.documentElement.appendChild(a); a.download = currentNode.title;
a.click(); document.documentElement.appendChild(a);
document.documentElement.removeChild(a); a.click();
document.documentElement.removeChild(a);
}
}); });
}; };
useEffect(() => { const initState = () => {
const word = searchValue || '';
const { tree } = getFilterData(word.toLocaleLowerCase(), data);
setFilterData(tree);
setSelect(''); setSelect('');
setCurrentNode(null); setCurrentNode(null);
setTitle('请选择脚本文件');
setValue('请选择脚本文件'); setValue('请选择脚本文件');
}, [data]); };
useEffect(() => { useEffect(() => {
getScripts(); getScripts();
if (treeDom && treeDom.current) { }, []);
useEffect(() => {
if (treeDom.current) {
setHeight(treeDom.current.clientHeight); setHeight(treeDom.current.clientHeight);
} }
}, []); }, [treeDom.current, data]);
const action = (key: string | number) => { const action = (key: string | number) => {
switch (key) { switch (key) {
@@ -440,19 +424,22 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
return ( return (
<PageContainer <PageContainer
className="ql-container-wrapper log-wrapper" className="ql-container-wrapper log-wrapper"
title={title} title={select}
loading={loading} loading={loading}
extra={ extra={
isPhone isPhone
? [ ? [
<TreeSelect <TreeSelect
treeExpandAction="click"
className="log-select" className="log-select"
value={select} value={select}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }} dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
treeData={data} treeData={data}
placeholder="请选择脚本" placeholder="请选择脚本"
fieldNames={{ value: 'key', label: 'title' }} fieldNames={{ value: 'key' }}
treeNodeFilterProp="title"
showSearch showSearch
allowClear
onSelect={onSelect} onSelect={onSelect}
/>, />,
<Dropdown overlay={menu} trigger={['click']}> <Dropdown overlay={menu} trigger={['click']}>
@@ -508,6 +495,7 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
> >
<div className={`${styles['log-container']} log-container`}> <div className={`${styles['log-container']} log-container`}>
{!isPhone && ( {!isPhone && (
/*// @ts-ignore*/
<SplitPane split="vertical" size={200} maxSize={-100}> <SplitPane split="vertical" size={200} maxSize={-100}>
<div className={styles['left-tree-container']}> <div className={styles['left-tree-container']}>
{data.length > 0 ? ( {data.length > 0 ? (
@@ -520,6 +508,7 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
></Input.Search> ></Input.Search>
<div className={styles['left-tree-scroller']} ref={treeDom}> <div className={styles['left-tree-scroller']} ref={treeDom}>
<Tree <Tree
expandAction="click"
className={styles['left-tree']} className={styles['left-tree']}
treeData={filterData} treeData={filterData}
showIcon={true} showIcon={true}
@@ -556,7 +545,6 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
readOnly: !isEditing, readOnly: !isEditing,
fontSize: 12, fontSize: 12,
lineNumbersMinChars: 3, lineNumbersMinChars: 3,
folding: false,
glyphMargin: false, glyphMargin: false,
}} }}
onMount={(editor) => { onMount={(editor) => {
+1 -3
View File
@@ -26,8 +26,6 @@ const SaveModal = ({
if (code === 200) { if (code === 200) {
message.success('保存文件成功'); message.success('保存文件成功');
handleCancel(data); handleCancel(data);
} else {
message.error(data);
} }
setLoading(false); setLoading(false);
}); });
@@ -41,7 +39,7 @@ const SaveModal = ({
return ( return (
<Modal <Modal
title="保存文件" title="保存文件"
visible={visible} open={visible}
forceRender forceRender
centered centered
maskClosable={false} maskClosable={false}
+1 -3
View File
@@ -26,8 +26,6 @@ const SettingModal = ({
if (code === 200) { if (code === 200) {
message.success('保存文件成功'); message.success('保存文件成功');
handleCancel(data); handleCancel(data);
} else {
message.error(data);
} }
setLoading(false); setLoading(false);
}); });
@@ -41,7 +39,7 @@ const SettingModal = ({
return ( return (
<Modal <Modal
title="运行设置" title="运行设置"
visible={visible} open={visible}
forceRender forceRender
centered centered
onCancel={() => handleCancel()} onCancel={() => handleCancel()}
+21 -3
View File
@@ -1,16 +1,23 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Typography, Input, Form, Button, message } from 'antd'; import { Typography, Input, Form, Button, message, Descriptions } from 'antd';
import styles from './index.less'; import styles from './index.less';
import { SharedContext } from '@/layouts';
import dayjs from 'dayjs';
const { Link } = Typography; const { Link } = Typography;
const About = () => { enum TVersion {
'develop' = '开发版',
'master' = '正式版',
}
const About = ({ systemInfo }: { systemInfo: SharedContext['systemInfo'] }) => {
return ( return (
<div className={styles.container}> <div className={styles.container}>
<img <img
alt="logo" alt="logo"
style={{ width: 140, marginRight: 20 }} style={{ width: 140, marginRight: 20 }}
src="http://qn.whyour.cn/logo.png" src="https://qn.whyour.cn/logo.png"
/> />
<div className={styles.right}> <div className={styles.right}>
<span className={styles.title}></span> <span className={styles.title}></span>
@@ -19,6 +26,17 @@ const About = () => {
task management panel that supports typescript, javaScript, python3, task management panel that supports typescript, javaScript, python3,
and shell. and shell.
</span> </span>
<Descriptions>
<Descriptions.Item label="版本" span={3}>
{TVersion[systemInfo.branch]} v{systemInfo.version}
</Descriptions.Item>
<Descriptions.Item label="更新时间" span={3}>
{dayjs(systemInfo.lastCommitTime).format('YYYY-MM-DD HH:mm:ss')}
</Descriptions.Item>
<Descriptions.Item label="更新ID" span={3}>
{systemInfo.lastCommitId}
</Descriptions.Item>
</Descriptions>
<div> <div>
<Link <Link
href="https://github.com/whyour/qinglong" href="https://github.com/whyour/qinglong"
+30 -12
View File
@@ -22,16 +22,19 @@ const AppModal = ({
if (app) { if (app) {
payload.id = app.id; payload.id = app.id;
} }
const { code, data } = await request[method](`${config.apiPrefix}apps`, { try {
data: payload, const { code, data } = await request[method](`${config.apiPrefix}apps`, {
}); data: payload,
if (code === 200) { });
message.success(app ? '更新应用成功' : '新建应用成功');
} else { if (code === 200) {
message.error(data); message.success(app ? '更新应用成功' : '新建应用成功');
handleCancel(data);
}
setLoading(false);
} catch (error) {
setLoading(false);
} }
setLoading(false);
handleCancel(data);
}; };
useEffect(() => { useEffect(() => {
@@ -41,7 +44,7 @@ const AppModal = ({
return ( return (
<Modal <Modal
title={app ? '编辑应用' : '新建应用'} title={app ? '编辑应用' : '新建应用'}
visible={visible} open={visible}
forceRender forceRender
centered centered
maskClosable={false} maskClosable={false}
@@ -64,7 +67,18 @@ const AppModal = ({
name="form_app_modal" name="form_app_modal"
initialValues={app} initialValues={app}
> >
<Form.Item name="name" label="名称"> <Form.Item
name="name"
label="名称"
rules={[
{
validator: (_, value) =>
['system'].includes(value)
? Promise.reject(new Error('名称不能为保留关键字'))
: Promise.resolve(),
},
]}
>
<Input placeholder="请输入应用名称" /> <Input placeholder="请输入应用名称" />
</Form.Item> </Form.Item>
<Form.Item name="scopes" label="权限" rules={[{ required: true }]}> <Form.Item name="scopes" label="权限" rules={[{ required: true }]}>
@@ -75,7 +89,11 @@ const AppModal = ({
style={{ width: '100%' }} style={{ width: '100%' }}
> >
{config.scopes.map((x) => { {config.scopes.map((x) => {
return <Select.Option value={x.value}>{x.name}</Select.Option>; return (
<Select.Option key={x.value} value={x.value}>
{x.name}
</Select.Option>
);
})} })}
</Select> </Select>
</Form.Item> </Form.Item>
+23 -23
View File
@@ -17,17 +17,14 @@ const CheckUpdate = ({ socketMessage }: any) => {
message.loading('检查更新中...', 0); message.loading('检查更新中...', 0);
request request
.put(`${config.apiPrefix}system/update-check`) .put(`${config.apiPrefix}system/update-check`)
.then((_data: any) => { .then(({ code, data }) => {
message.destroy(); message.destroy();
const { code, data } = _data;
if (code === 200) { if (code === 200) {
if (data.hasNewVersion) { if (data.hasNewVersion) {
showConfirmUpdateModal(data); showConfirmUpdateModal(data);
} else { } else {
showForceUpdateModal(); showForceUpdateModal();
} }
} else {
message.error(data);
} }
}) })
.catch((error: any) => { .catch((error: any) => {
@@ -80,8 +77,6 @@ const CheckUpdate = ({ socketMessage }: any) => {
content: ( content: (
<pre <pre
style={{ style={{
wordBreak: 'break-all',
whiteSpace: 'pre-wrap',
paddingTop: 15, paddingTop: 15,
fontSize: 12, fontSize: 12,
fontWeight: 400, fontWeight: 400,
@@ -105,26 +100,23 @@ const CheckUpdate = ({ socketMessage }: any) => {
}; };
const showUpdatingModal = () => { const showUpdatingModal = () => {
setValue('');
modalRef.current = Modal.info({ modalRef.current = Modal.info({
width: 600, width: 600,
maskClosable: false, maskClosable: false,
closable: false, closable: false,
okButtonProps: { disabled: true }, okButtonProps: { disabled: true },
title: '更新日志', title: '更新中...',
centered: true, centered: true,
content: ( content: (
<div style={{ height: '60vh', overflowY: 'auto' }}> <pre
<pre style={{
style={{ fontSize: 12,
wordBreak: 'break-all', fontWeight: 400,
whiteSpace: 'pre-wrap', }}
fontSize: 12, >
fontWeight: 400, {value}
}} </pre>
>
{value}
</pre>
</div>
), ),
}); });
}; };
@@ -140,13 +132,16 @@ const CheckUpdate = ({ socketMessage }: any) => {
} }
const newMessage = `${value}${_message}`; const newMessage = `${value}${_message}`;
const updateFailed = newMessage.includes('失败,请检查');
modalRef.current.update({ modalRef.current.update({
maskClosable: updateFailed,
closable: updateFailed,
okButtonProps: { disabled: !updateFailed },
content: ( content: (
<div style={{ height: '60vh', overflowY: 'auto' }}> <>
<pre <pre
style={{ style={{
wordBreak: 'break-all',
whiteSpace: 'pre-wrap',
fontSize: 12, fontSize: 12,
fontWeight: 400, fontWeight: 400,
}} }}
@@ -154,9 +149,14 @@ const CheckUpdate = ({ socketMessage }: any) => {
{newMessage} {newMessage}
</pre> </pre>
<div id="log-identifier" style={{ paddingBottom: 5 }}></div> <div id="log-identifier" style={{ paddingBottom: 5 }}></div>
</div> </>
), ),
}); });
if (updateFailed && !value.includes('失败,请检查')) {
message.error('更新失败,请检查网络及日志或稍后再试');
}
setValue(newMessage); setValue(newMessage);
document.getElementById('log-identifier') && document.getElementById('log-identifier') &&
+7
View File
@@ -18,5 +18,12 @@
.desc { .desc {
margin-bottom: 10px; margin-bottom: 10px;
} }
:global {
.ant-descriptions-row > th,
.ant-descriptions-row > td {
padding-bottom: 10px;
}
}
} }
} }
+116 -91
View File
@@ -29,6 +29,8 @@ import LoginLog from './loginLog';
import NotificationSetting from './notification'; import NotificationSetting from './notification';
import CheckUpdate from './checkUpdate'; import CheckUpdate from './checkUpdate';
import About from './about'; import About from './about';
import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts';
const { Text } = Typography; const { Text } = Typography;
const optionsWithDisabled = [ const optionsWithDisabled = [
@@ -37,14 +39,16 @@ const optionsWithDisabled = [
{ label: '跟随系统', value: 'auto' }, { label: '跟随系统', value: 'auto' },
]; ];
const Setting = ({ const Setting = () => {
headerStyle, const {
isPhone, headerStyle,
user, isPhone,
reloadUser, user,
reloadTheme, reloadUser,
socketMessage, reloadTheme,
}: any) => { socketMessage,
systemInfo,
} = useOutletContext<SharedContext>();
const columns = [ const columns = [
{ {
title: '名称', title: '名称',
@@ -147,8 +151,10 @@ const Setting = ({
setLoading(true); setLoading(true);
request request
.get(`${config.apiPrefix}apps`) .get(`${config.apiPrefix}apps`)
.then((data: any) => { .then(({ code, data }) => {
setDataSource(data.data); if (code === 200) {
setDataSource(data);
}
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
@@ -178,14 +184,12 @@ const Setting = ({
onOk() { onOk() {
request request
.delete(`${config.apiPrefix}apps`, { data: [record.id] }) .delete(`${config.apiPrefix}apps`, { data: [record.id] })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
message.success('删除成功'); message.success('删除成功');
const result = [...dataSource]; const result = [...dataSource];
result.splice(index, 1); result.splice(index, 1);
setDataSource(result); setDataSource(result);
} else {
message.error(data);
} }
}); });
}, },
@@ -212,12 +216,10 @@ const Setting = ({
onOk() { onOk() {
request request
.put(`${config.apiPrefix}apps/${record.id}/reset-secret`) .put(`${config.apiPrefix}apps/${record.id}/reset-secret`)
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
message.success('重置成功'); message.success('重置成功');
handleApp(data.data); handleApp(data);
} else {
message.error(data);
} }
}); });
}, },
@@ -250,8 +252,10 @@ const Setting = ({
const getLoginLog = () => { const getLoginLog = () => {
request request
.get(`${config.apiPrefix}user/login-log`) .get(`${config.apiPrefix}user/login-log`)
.then((data: any) => { .then(({ code, data }) => {
setLoginLogData(data.data); if (code === 200) {
setLoginLogData(data);
}
}) })
.catch((error: any) => { .catch((error: any) => {
console.log(error); console.log(error);
@@ -274,8 +278,10 @@ const Setting = ({
const getNotification = () => { const getNotification = () => {
request request
.get(`${config.apiPrefix}user/notification`) .get(`${config.apiPrefix}user/notification`)
.then((data: any) => { .then(({ code, data }) => {
setNotificationInfo(data.data); if (code === 200) {
setNotificationInfo(data);
}
}) })
.catch((error: any) => { .catch((error: any) => {
console.log(error); console.log(error);
@@ -285,9 +291,9 @@ const Setting = ({
const getLogRemoveFrequency = () => { const getLogRemoveFrequency = () => {
request request
.get(`${config.apiPrefix}system/log/remove`) .get(`${config.apiPrefix}system/log/remove`)
.then((data: any) => { .then(({ code, data }) => {
if (data.data.info) { if (code === 200 && data.info) {
const { frequency } = data.data.info; const { frequency } = data.info;
setLogRemoveFrequency(frequency); setLogRemoveFrequency(frequency);
} }
}) })
@@ -302,8 +308,10 @@ const Setting = ({
.put(`${config.apiPrefix}system/log/remove`, { .put(`${config.apiPrefix}system/log/remove`, {
data: { frequency: logRemoveFrequency }, data: { frequency: logRemoveFrequency },
}) })
.then((data: any) => { .then(({ code, data }) => {
message.success('更新成功'); if (code === 200) {
message.success('更新成功');
}
}) })
.catch((error: any) => { .catch((error: any) => {
console.log(error); console.log(error);
@@ -333,70 +341,87 @@ const Setting = ({
size="small" size="small"
tabPosition="top" tabPosition="top"
onChange={tabChange} onChange={tabChange}
> items={[
<Tabs.TabPane tab="安全设置" key="security"> {
<SecuritySettings user={user} userChange={reloadUser} /> key: 'security',
</Tabs.TabPane> label: '安全设置',
<Tabs.TabPane tab="应用设置" key="app"> children: <SecuritySettings user={user} userChange={reloadUser} />,
<Table },
columns={columns} {
pagination={false} key: 'app',
dataSource={dataSource} label: '应用设置',
rowKey="id" children: (
size="middle" <Table
scroll={{ x: 768 }} columns={columns}
loading={loading} pagination={false}
/> dataSource={dataSource}
</Tabs.TabPane> rowKey="id"
<Tabs.TabPane tab="通知设置" key="notification"> size="middle"
<NotificationSetting data={notificationInfo} /> scroll={{ x: 768 }}
</Tabs.TabPane> loading={loading}
<Tabs.TabPane tab="登录日志" key="login">
<LoginLog data={loginLogData} />
</Tabs.TabPane>
<Tabs.TabPane tab="其他设置" key="other">
<Form layout="vertical" form={form}>
<Form.Item
label="主题设置"
name="theme"
initialValue={defaultTheme}
>
<Radio.Group
options={optionsWithDisabled}
onChange={themeChange}
value={defaultTheme}
optionType="button"
buttonStyle="solid"
/> />
</Form.Item> ),
<Form.Item },
label="日志删除频率" {
name="frequency" key: 'notification',
tooltip="每x天自动删除x天以前的日志" label: '通知设置',
> children: <NotificationSetting data={notificationInfo} />,
<Input.Group compact> },
<InputNumber {
addonBefore="每" key: 'login',
addonAfter="天" label: '登录日志',
style={{ width: 150 }} children: <LoginLog data={loginLogData} />,
min={0} },
value={logRemoveFrequency} {
onChange={(value) => setLogRemoveFrequency(value)} key: 'other',
/> label: '其他设置',
<Button type="primary" onClick={updateRemoveLogFrequency}> children: (
<Form layout="vertical" form={form}>
</Button> <Form.Item
</Input.Group> label="主题设置"
</Form.Item> name="theme"
<Form.Item label="检查更新" name="update"> initialValue={defaultTheme}
<CheckUpdate socketMessage={socketMessage} /> >
</Form.Item> <Radio.Group
</Form> options={optionsWithDisabled}
</Tabs.TabPane> onChange={themeChange}
<Tabs.TabPane tab="关于" key="about"> value={defaultTheme}
<About /> optionType="button"
</Tabs.TabPane> buttonStyle="solid"
</Tabs> />
</Form.Item>
<Form.Item
label="日志删除频率"
name="frequency"
tooltip="每x天自动删除x天以前的日志"
>
<Input.Group compact>
<InputNumber
addonBefore="每"
addonAfter="天"
style={{ width: 150 }}
min={0}
value={logRemoveFrequency}
onChange={(value) => setLogRemoveFrequency(value)}
/>
<Button type="primary" onClick={updateRemoveLogFrequency}>
</Button>
</Input.Group>
</Form.Item>
<Form.Item label="检查更新" name="update">
<CheckUpdate socketMessage={socketMessage} />
</Form.Item>
</Form>
),
},
{
key: 'about',
label: '关于',
children: <About systemInfo={systemInfo} />,
},
]}
></Tabs>
<AppModal <AppModal
visible={isModalVisible} visible={isModalVisible}
handleCancel={handleCancel} handleCancel={handleCancel}
+23 -7
View File
@@ -12,20 +12,20 @@ const NotificationSetting = ({ data }: any) => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const handleOk = (values: any) => { const handleOk = (values: any) => {
if (values.type == 'closed') { const { type } = values;
if (type == 'closed') {
values.type = ''; values.type = '';
} }
request request
.put(`${config.apiPrefix}user/notification`, { .put(`${config.apiPrefix}user/notification`, {
data: { data: {
...values, ...values,
}, },
}) })
.then((_data: any) => { .then(({ code, data }) => {
if (_data && _data.code === 200) { if (code === 200) {
message.success(values.type ? '通知发送成功' : '通知关闭成功'); message.success(values.type ? '通知发送成功' : '通知关闭成功');
} else {
message.error(_data.data);
} }
}) })
.catch((error: any) => { .catch((error: any) => {
@@ -58,19 +58,35 @@ const NotificationSetting = ({ data }: any) => {
> >
<Select onChange={notificationModeChange}> <Select onChange={notificationModeChange}>
{config.notificationModes.map((x) => ( {config.notificationModes.map((x) => (
<Option value={x.value}>{x.label}</Option> <Option key={x.value} value={x.value}>
{x.label}
</Option>
))} ))}
</Select> </Select>
</Form.Item> </Form.Item>
{fields.map((x) => ( {fields.map((x) => (
<Form.Item <Form.Item
key={x.label}
label={x.label} label={x.label}
name={x.label} name={x.label}
extra={x.tip} extra={x.tip}
rules={[{ required: x.required }]} rules={[{ required: x.required }]}
style={{ maxWidth: 400 }} style={{ maxWidth: 400 }}
> >
<Input.TextArea autoSize={true} placeholder={`请输入${x.label}`} /> {x.items ? (
<Select placeholder={x.placeholder || `请选择${x.label}`}>
{x.items.map((y) => (
<Option key={y.value} value={y.value}>
{y.label || y.value}
</Option>
))}
</Select>
) : (
<Input.TextArea
autoSize={true}
placeholder={x.placeholder || `请输入${x.label}`}
/>
)}
</Form.Item> </Form.Item>
))} ))}
<Button type="primary" htmlType="submit"> <Button type="primary" htmlType="submit">
+22 -16
View File
@@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
import { Typography, Input, Form, Button, message, Avatar, Upload } from 'antd'; import { Typography, Input, Form, Button, message, Avatar, Upload } from 'antd';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import config from '@/utils/config'; import config from '@/utils/config';
import { history } from 'umi'; import { history } from '@umijs/max';
import QRCode from 'qrcode.react'; import QRCode from 'qrcode.react';
import { PageLoading } from '@ant-design/pro-layout'; import { PageLoading } from '@ant-design/pro-layout';
import { UploadOutlined, UserOutlined } from '@ant-design/icons'; import { UploadOutlined, UserOutlined } from '@ant-design/icons';
@@ -27,9 +27,11 @@ const SecuritySettings = ({ user, userChange }: any) => {
password: values.password, password: values.password,
}, },
}) })
.then((data: any) => { .then(({ code, data }) => {
localStorage.removeItem(config.authKey); if (code === 200) {
history.push('/login'); localStorage.removeItem(config.authKey);
history.push('/login');
}
}) })
.catch((error: any) => { .catch((error: any) => {
console.log(error); console.log(error);
@@ -48,8 +50,8 @@ const SecuritySettings = ({ user, userChange }: any) => {
const deactiveTowFactor = () => { const deactiveTowFactor = () => {
request request
.put(`${config.apiPrefix}user/two-factor/deactive`) .put(`${config.apiPrefix}user/two-factor/deactive`)
.then((data: any) => { .then(({ code, data }) => {
if (data.data) { if (code === 200 && data) {
setTwoFactorActivated(false); setTwoFactorActivated(false);
userChange(); userChange();
} }
@@ -63,14 +65,16 @@ const SecuritySettings = ({ user, userChange }: any) => {
setLoading(true); setLoading(true);
request request
.put(`${config.apiPrefix}user/two-factor/active`, { data: { code } }) .put(`${config.apiPrefix}user/two-factor/active`, { data: { code } })
.then((data: any) => { .then(({ code, data }) => {
if (data.data) { if (code === 200) {
message.success('激活成功'); if (data) {
setTwoFactoring(false); message.success('激活成功');
setTwoFactorActivated(true); setTwoFactoring(false);
userChange(); setTwoFactorActivated(true);
} else { userChange();
message.success('验证失败'); } else {
message.success('验证失败');
}
} }
}) })
.catch((error: any) => { .catch((error: any) => {
@@ -82,8 +86,10 @@ const SecuritySettings = ({ user, userChange }: any) => {
const getTwoFactorInfo = () => { const getTwoFactorInfo = () => {
request request
.get(`${config.apiPrefix}user/two-factor/init`) .get(`${config.apiPrefix}user/two-factor/init`)
.then((data: any) => { .then(({ code, data }) => {
setTwoFactorInfo(data.data); if (code === 200) {
setTwoFactorInfo(data);
}
}) })
.catch((error: any) => { .catch((error: any) => {
console.log(error); console.log(error);
+24 -27
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { import {
Button, Button,
message, message,
@@ -29,10 +29,11 @@ import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout'; import { PageContainer } from '@ant-design/pro-layout';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import SubscriptionModal from './modal'; import SubscriptionModal from './modal';
import { getTableScroll } from '@/utils/index'; import { history, useOutletContext } from '@umijs/max';
import { history } from 'umi';
import './index.less'; import './index.less';
import SubscriptionLogModal from './logModal'; import SubscriptionLogModal from './logModal';
import { SharedContext } from '@/layouts';
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
const { Text, Paragraph } = Typography; const { Text, Paragraph } = Typography;
const { Search } = Input; const { Search } = Input;
@@ -57,7 +58,10 @@ export enum SubscriptionType {
'file' = '单文件', 'file' = '单文件',
} }
const Subscription = ({ headerStyle, isPhone, socketMessage }: any) => { const Subscription = () => {
const { headerStyle, isPhone, socketMessage } =
useOutletContext<SharedContext>();
const columns: any = [ const columns: any = [
{ {
title: '名称', title: '名称',
@@ -239,10 +243,11 @@ const Subscription = ({ headerStyle, isPhone, socketMessage }: any) => {
const [searchText, setSearchText] = useState(''); const [searchText, setSearchText] = useState('');
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(20); const [pageSize, setPageSize] = useState(20);
const [tableScrollHeight, setTableScrollHeight] = useState<number>();
const [searchValue, setSearchValue] = useState(''); const [searchValue, setSearchValue] = useState('');
const [isLogModalVisible, setIsLogModalVisible] = useState(false); const [isLogModalVisible, setIsLogModalVisible] = useState(false);
const [logSubscription, setLogSubscription] = useState<any>(); const [logSubscription, setLogSubscription] = useState<any>();
const tableRef = useRef<any>();
const tableScrollHeight = useTableScrollHeight(tableRef)
const runSubscription = (record: any, index: number) => { const runSubscription = (record: any, index: number) => {
Modal.confirm({ Modal.confirm({
@@ -259,8 +264,8 @@ const Subscription = ({ headerStyle, isPhone, socketMessage }: any) => {
onOk() { onOk() {
request request
.put(`${config.apiPrefix}subscriptions/run`, { data: [record.id] }) .put(`${config.apiPrefix}subscriptions/run`, { data: [record.id] })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
const result = [...value]; const result = [...value];
const i = result.findIndex((x) => x.id === record.id); const i = result.findIndex((x) => x.id === record.id);
if (i !== -1) { if (i !== -1) {
@@ -270,8 +275,6 @@ const Subscription = ({ headerStyle, isPhone, socketMessage }: any) => {
}); });
setValue(result); setValue(result);
} }
} else {
message.error(data);
} }
}); });
}, },
@@ -296,8 +299,8 @@ const Subscription = ({ headerStyle, isPhone, socketMessage }: any) => {
onOk() { onOk() {
request request
.put(`${config.apiPrefix}subscriptions/stop`, { data: [record.id] }) .put(`${config.apiPrefix}subscriptions/stop`, { data: [record.id] })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
const result = [...value]; const result = [...value];
const i = result.findIndex((x) => x.id === record.id); const i = result.findIndex((x) => x.id === record.id);
if (i !== -1) { if (i !== -1) {
@@ -308,8 +311,6 @@ const Subscription = ({ headerStyle, isPhone, socketMessage }: any) => {
}); });
setValue(result); setValue(result);
} }
} else {
message.error(data);
} }
}); });
}, },
@@ -323,9 +324,11 @@ const Subscription = ({ headerStyle, isPhone, socketMessage }: any) => {
setLoading(true); setLoading(true);
request request
.get(`${config.apiPrefix}subscriptions?searchValue=${searchText}`) .get(`${config.apiPrefix}subscriptions?searchValue=${searchText}`)
.then((data: any) => { .then(({ code, data }) => {
setValue(data.data); if (code === 200) {
setCurrentPage(1); setValue(data);
setCurrentPage(1);
}
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
@@ -355,8 +358,8 @@ const Subscription = ({ headerStyle, isPhone, socketMessage }: any) => {
onOk() { onOk() {
request request
.delete(`${config.apiPrefix}subscriptions`, { data: [record.id] }) .delete(`${config.apiPrefix}subscriptions`, { data: [record.id] })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
message.success('删除成功'); message.success('删除成功');
const result = [...value]; const result = [...value];
const i = result.findIndex((x) => x.id === record.id); const i = result.findIndex((x) => x.id === record.id);
@@ -364,8 +367,6 @@ const Subscription = ({ headerStyle, isPhone, socketMessage }: any) => {
result.splice(i, 1); result.splice(i, 1);
setValue(result); setValue(result);
} }
} else {
message.error(data);
} }
}); });
}, },
@@ -398,8 +399,8 @@ const Subscription = ({ headerStyle, isPhone, socketMessage }: any) => {
data: [record.id], data: [record.id],
}, },
) )
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
const newStatus = record.is_disabled === 1 ? 0 : 1; const newStatus = record.is_disabled === 1 ? 0 : 1;
const result = [...value]; const result = [...value];
const i = result.findIndex((x) => x.id === record.id); const i = result.findIndex((x) => x.id === record.id);
@@ -410,8 +411,6 @@ const Subscription = ({ headerStyle, isPhone, socketMessage }: any) => {
}); });
setValue(result); setValue(result);
} }
} else {
message.error(data);
} }
}); });
}, },
@@ -541,9 +540,6 @@ const Subscription = ({ headerStyle, isPhone, socketMessage }: any) => {
useEffect(() => { useEffect(() => {
setPageSize(parseInt(localStorage.getItem('pageSize') || '20')); setPageSize(parseInt(localStorage.getItem('pageSize') || '20'));
setTimeout(() => {
setTableScrollHeight(getTableScroll());
});
}, []); }, []);
return ( return (
@@ -570,6 +566,7 @@ const Subscription = ({ headerStyle, isPhone, socketMessage }: any) => {
}} }}
> >
<Table <Table
ref={tableRef}
columns={columns} columns={columns}
pagination={{ pagination={{
current: currentPage, current: currentPage,
+7 -5
View File
@@ -7,6 +7,7 @@ import {
CheckCircleOutlined, CheckCircleOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { PageLoading } from '@ant-design/pro-layout'; import { PageLoading } from '@ant-design/pro-layout';
import { logEnded } from '@/utils';
const SubscriptionLogModal = ({ const SubscriptionLogModal = ({
subscription, subscription,
@@ -36,14 +37,15 @@ const SubscriptionLogModal = ({
? logUrl ? logUrl
: `${config.apiPrefix}subscriptions/${subscription.id}/log`, : `${config.apiPrefix}subscriptions/${subscription.id}/log`,
) )
.then((data: any) => { .then(({ code, data }) => {
if ( if (
code === 200 &&
localStorage.getItem('logSubscription') === String(subscription.id) localStorage.getItem('logSubscription') === String(subscription.id)
) { ) {
const log = data.data as string; const log = data as string;
setValue(log || '暂无日志'); setValue(log || '暂无日志');
setExecuting(log && !log.includes('执行结束')); setExecuting(log && !logEnded(log));
if (log && !log.includes('执行结束')) { if (log && !logEnded(log)) {
setTimeout(() => { setTimeout(() => {
getCronLog(); getCronLog();
}, 2000); }, 2000);
@@ -93,7 +95,7 @@ const SubscriptionLogModal = ({
return ( return (
<Modal <Modal
title={titleElement()} title={titleElement()}
visible={visible} open={visible}
centered centered
className="log-modal" className="log-modal"
bodyStyle={{ bodyStyle={{
+16 -34
View File
@@ -39,11 +39,9 @@ const SubscriptionModal = ({
); );
if (code === 200) { if (code === 200) {
message.success(subscription ? '更新订阅成功' : '新建订阅成功'); message.success(subscription ? '更新订阅成功' : '新建订阅成功');
} else { handleCancel(data);
message.error(data);
} }
setLoading(false); setLoading(false);
handleCancel(data);
} catch (error: any) { } catch (error: any) {
setLoading(false); setLoading(false);
} }
@@ -190,8 +188,8 @@ const SubscriptionModal = ({
}; };
const onPaste = useCallback((e: any) => { const onPaste = useCallback((e: any) => {
const text = e.clipboardData.getData('text'); const text = e.clipboardData.getData('text') as string;
if (!subscription && text.includes('ql ')) { if (text.startsWith('ql ')) {
const [ const [
, ,
type, type,
@@ -225,6 +223,13 @@ const SubscriptionModal = ({
} }
}, []); }, []);
const onNamePaste = useCallback((e) => {
const text = e.clipboardData.getData('text') as string;
if (text.startsWith('ql ')) {
e.preventDefault();
}
}, []);
useEffect(() => { useEffect(() => {
if (visible) { if (visible) {
window.addEventListener('paste', onPaste); window.addEventListener('paste', onPaste);
@@ -243,36 +248,10 @@ const SubscriptionModal = ({
} }
}, [subscription, visible]); }, [subscription, visible]);
const isFirefox = navigator.userAgent.includes('Firefox');
const isSafari =
navigator.userAgent.includes('Safari') &&
!navigator.userAgent.includes('Chrome');
const isQQBrowser = navigator.userAgent.includes('QQBrowser');
return ( return (
<Modal <Modal
title={ title={subscription ? '编辑订阅' : '新建订阅'}
subscription ? ( open={visible}
'编辑订阅'
) : (
<span>
<small
style={{
color: '#999',
fontWeight: 400,
fontSize: isFirefox ? 9 : 12,
marginLeft: 2,
zoom: isSafari ? 0.66 : 0.8,
letterSpacing: isQQBrowser ? -2 : 0,
}}
>
repo/raw命令
</small>
</span>
)
}
visible={visible}
forceRender forceRender
centered centered
maskClosable={false} maskClosable={false}
@@ -291,7 +270,10 @@ const SubscriptionModal = ({
> >
<Form form={form} name="form_in_modal" layout="vertical"> <Form form={form} name="form_in_modal" layout="vertical">
<Form.Item name="name" label="名称"> <Form.Item name="name" label="名称">
<Input placeholder="请输入订阅名" /> <Input
placeholder="支持拷贝ql repo/raw命令,粘贴导入"
onPaste={onNamePaste}
/>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="type" name="type"
+70 -4
View File
@@ -56,7 +56,7 @@ export default {
value: 'scripts', value: 'scripts',
}, },
{ {
name: '任务日志', name: '日志管理',
value: 'logs', value: 'logs',
}, },
{ {
@@ -74,7 +74,7 @@ export default {
subscriptions: '订阅管理', subscriptions: '订阅管理',
configs: '配置文件', configs: '配置文件',
scripts: '脚本管理', scripts: '脚本管理',
logs: '任务日志', logs: '日志管理',
dependencies: '依赖管理', dependencies: '依赖管理',
system: '系统信息', system: '系统信息',
}, },
@@ -88,10 +88,13 @@ export default {
{ value: 'dingtalkBot', label: '钉钉机器人' }, { value: 'dingtalkBot', label: '钉钉机器人' },
{ value: 'weWorkBot', label: '企业微信机器人' }, { value: 'weWorkBot', label: '企业微信机器人' },
{ value: 'weWorkApp', label: '企业微信应用' }, { value: 'weWorkApp', label: '企业微信应用' },
{ value: 'aibotk', label: '智能微秘书' },
{ value: 'iGot', label: 'IGot' }, { value: 'iGot', label: 'IGot' },
{ value: 'pushPlus', label: 'PushPlus' }, { value: 'pushPlus', label: 'PushPlus' },
{ value: 'chat', label: '群辉chat' }, { value: 'chat', label: '群辉chat' },
{ value: 'email', label: '邮箱' }, { value: 'email', label: '邮箱' },
{ value: 'lark', label: '飞书机器人' },
{ value: 'webhook', label: '自定义通知' },
{ value: 'closed', label: '已关闭' }, { value: 'closed', label: '已关闭' },
], ],
notificationModeMap: { notificationModeMap: {
@@ -195,6 +198,28 @@ export default {
required: true, required: true,
}, },
], ],
aibotk: [
{
label: 'aibotkKey',
tip: '密钥key,智能微秘书个人中心获取apikey,申请地址:https://wechat.aibotk.com/signup?from=ql',
required: true,
},
{
label: 'aibotkType',
tip: '发送的目标,群组或者好友',
required: true,
placeholder: '请输入要发送的目标',
items: [
{ value: 'room', label: '群聊' },
{ value: 'contact', label: '好友' },
],
},
{
label: 'aibotkName',
tip: '要发送的用户昵称或群名,如果目标是群,需要填群名,如果目标是好友,需要填好友昵称',
required: true,
},
],
iGot: [ iGot: [
{ {
label: 'iGotPushKey', label: 'iGotPushKey',
@@ -205,7 +230,7 @@ export default {
pushPlus: [ pushPlus: [
{ {
label: 'pushPlusToken', label: 'pushPlusToken',
tip: '微信扫码登录后一对一推送或一对多推送下面的token(您的Token),不提供PUSH_PLUS_USER则默认为一对一推送', tip: '微信扫码登录后一对一推送或一对多推送下面的token(您的Token),不提供PUSH_PLUS_USER则默认为一对一推送,参考 https://www.pushplus.plus/',
required: true, required: true,
}, },
{ {
@@ -213,6 +238,13 @@ export default {
tip: '一对多推送的“群组编码”(一对多推送下面->您的群组(如无则新建)->群组编码,如果您是创建群组人。也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送)', tip: '一对多推送的“群组编码”(一对多推送下面->您的群组(如无则新建)->群组编码,如果您是创建群组人。也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送)',
}, },
], ],
lark: [
{
label: 'larkKey',
tip: '飞书群组机器人:https://www.feishu.cn/hc/zh-CN/articles/360024984973',
required: true,
},
],
email: [ email: [
{ {
label: 'emailService', label: 'emailService',
@@ -222,6 +254,40 @@ export default {
{ label: 'emailUser', tip: '邮箱地址', required: true }, { label: 'emailUser', tip: '邮箱地址', required: true },
{ label: 'emailPass', tip: '邮箱SMTP授权码', required: true }, { label: 'emailPass', tip: '邮箱SMTP授权码', required: true },
], ],
webhook: [
{
label: 'webhookMethod',
tip: '请求方法',
required: true,
items: [{ value: 'GET' }, { value: 'POST' }, { value: 'PUT' }],
},
{
label: 'webhookContentType',
tip: '请求头Content-Type',
required: true,
items: [
{ value: 'application/json' },
{ value: 'multipart/form-data' },
{ value: 'application/x-www-form-urlencoded' },
],
},
{
label: 'webhookUrl',
tip: '请求链接以http或者https开头。url或者body中必须包含$title$content可选,对应api内容的位置',
required: true,
placeholder: 'https://xxx.cn/api?content=$title\n',
},
{
label: 'webhookHeaders',
tip: '请求头格式Custom-Header1: Header1,多个换行分割',
placeholder: 'Custom-Header1: Header1\nCustom-Header2: Header2',
},
{
label: 'webhookBody',
tip: '请求体格式key1: value1,多个换行分割。url或者body中必须包含$title$content可选,对应api内容的位置',
placeholder: 'key1: $title\nkey2: $content',
},
],
}, },
documentTitleMap: { documentTitleMap: {
'/login': '登录', '/login': '登录',
@@ -232,7 +298,7 @@ export default {
'/config': '配置文件', '/config': '配置文件',
'/script': '脚本管理', '/script': '脚本管理',
'/diff': '对比工具', '/diff': '对比工具',
'/log': '任务日志', '/log': '日志管理',
'/setting': '系统设置', '/setting': '系统设置',
'/error': '错误日志', '/error': '错误日志',
}, },
+1
View File
@@ -0,0 +1 @@
export const LOG_END_SYMBOL = '\n          ';
+18 -2
View File
@@ -1,7 +1,7 @@
import { extend } from 'umi-request'; import { extend } from 'umi-request';
import { message } from 'antd'; import { message } from 'antd';
import config from './config'; import config from './config';
import { history } from 'umi'; import { history } from '@umijs/max';
message.config({ message.config({
duration: 1.5, duration: 1.5,
@@ -54,7 +54,23 @@ _request.interceptors.request.use((url, options) => {
}); });
_request.interceptors.response.use(async (response) => { _request.interceptors.response.use(async (response) => {
const res = await response.clone(); const responseStatus = response.status;
if ([502, 504].includes(responseStatus)) {
message.error('服务异常,请稍后刷新!');
history.push('/error');
} else if (responseStatus === 401) {
if (history.location.pathname !== '/login') {
localStorage.removeItem(config.authKey);
history.push('/login');
}
} else {
const res = await response.clone().json();
if (res.code !== 200) {
const msg = res.message || res.data;
msg && message.error(msg);
}
return res;
}
return response; return response;
}); });
+43 -8
View File
@@ -1,3 +1,5 @@
import { LOG_END_SYMBOL } from './const';
export default function browserType() { export default function browserType() {
// 权重:系统 + 系统版本 > 平台 > 内核 + 载体 + 内核版本 + 载体版本 > 外壳 + 外壳版本 // 权重:系统 + 系统版本 > 平台 > 内核 + 载体 + 内核版本 + 载体版本 > 外壳 + 外壳版本
const ua = navigator.userAgent.toLowerCase(); const ua = navigator.userAgent.toLowerCase();
@@ -178,19 +180,15 @@ export default function browserType() {
*/ */
export function getTableScroll({ export function getTableScroll({
extraHeight, extraHeight,
id, target,
}: { extraHeight?: number; id?: string } = {}) { }: { extraHeight?: number; target?: HTMLElement } = {}) {
if (typeof extraHeight == 'undefined') { if (typeof extraHeight == 'undefined') {
// 47 + 40 + 12 // 47 + 40 + 12
extraHeight = 99; extraHeight = 99;
} }
let tHeader = null; let tHeader = null;
if (id) { if (target) {
tHeader = document.getElementById(id) tHeader = target;
? document
.getElementById(id)!
.getElementsByClassName('ant-table-thead')[0]
: null;
} else { } else {
tHeader = document.querySelector('.ant-table-wrapper'); tHeader = document.querySelector('.ant-table-wrapper');
} }
@@ -241,3 +239,40 @@ export function exportJson(name: string, data: string) {
createA.download = name; createA.download = name;
automaticClick(createA); automaticClick(createA);
} }
export function depthFirstSearch<
T extends Record<string, any> & { children?: T[] },
>(children: T[], condition: (column: T) => boolean, item?: T) {
const c = [...children];
const keys = [];
(function find(cls: T[] | undefined) {
if (!cls) return;
for (let i = 0; i < cls?.length; i++) {
if (condition(cls[i])) {
if (!item) {
cls.splice(i, 1);
return;
}
if (cls[i].children) {
cls[i].children!.unshift(item);
} else {
cls[i].children = [item];
}
return;
}
if (cls[i].children) {
keys.push(cls[i].key);
find(cls[i].children);
}
}
})(c);
return c;
}
export function logEnded(log: string): boolean {
const endTips = [LOG_END_SYMBOL, '执行结束'];
return endTips.some((x) => log.includes(x));
}
+6 -5
View File
@@ -1,6 +1,7 @@
export const version = '2.14.3'; export const version = '2.14.11';
export const changeLogLink = 'https://t.me/jiao_long/330'; export const changeLogLink = 'https://t.me/jiao_long/338';
export const changeLog = `2.14.8 版本说明 export const changeLog = `2.14.11 版本说明
1. ERR_PNPM_REGISTRIES_MISMATCH 1.
2. -> 2.
3. pre样式
`; `;
+4 -1
View File
@@ -1,12 +1,15 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es2017", "target": "es2017",
"lib": ["es2017", "esnext.asynciterable", "DOM"], "lib": ["es2021", "esnext.asynciterable", "DOM"],
"typeRoots": [ "typeRoots": [
"./node_modules/@types", "./node_modules/@types",
"./src/types", "./src/types",
"./node_modules/celebrate/lib/index.d.ts" "./node_modules/celebrate/lib/index.d.ts"
], ],
"paths": {
"@/*": ["./back/*"],
},
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,