Compare commits

...

16 Commits

Author SHA1 Message Date
whyour ddee69d291 更新版本 v2.10.13 2021-12-25 21:31:52 +08:00
whyour 28960c9fab 添加sentry-cli cdn 2021-12-25 20:55:13 +08:00
whyour 449301e519 修改热更新目录 2021-12-25 20:13:49 +08:00
whyour 28dab3dc2f 修改两步验证界面逻辑 2021-12-25 20:09:02 +08:00
whyour 7995d98db5 修改批量运行任务逻辑 2021-12-24 23:31:11 +08:00
whyour 1f319f3533 调试脚本增加停止,修复调试交互 2021-12-23 23:25:39 +08:00
whyour 7d2dbaa62d 修改文本 2021-12-23 13:39:16 +08:00
whyour ed5999526b 优化初始化通知设置交互 2021-12-21 23:49:52 +08:00
whyour 2409f75491 修复登录退出接口路径 2021-12-21 23:42:05 +08:00
whyour 0c840095d1 增加系统openapi 2021-12-21 23:22:34 +08:00
whyour 3eaf0b4856 system接口增加版本号,应用权限增加依赖管理 2021-12-21 22:53:19 +08:00
琚涛涛 394735a5d9 优化定时任务名称展示效果,提升用户体验 (#1017) 2021-12-21 22:39:17 +08:00
whyour 5bfa10bfe7 修复任务跳转脚本参数,task运行脚本支持更多参数 2021-12-21 00:23:20 +08:00
whyour 3f12d9cbd5 更新版本号 v2.10.12 2021-12-09 23:24:10 +08:00
whyour 54d89754df 修复darkreader最新版bug 2021-12-09 23:22:17 +08:00
whyour 9470524615 更新readme 2021-12-06 23:42:14 +08:00
39 changed files with 680 additions and 327 deletions
+1
View File
@@ -0,0 +1 @@
sentrycli_cdnurl=https://cdn.npm.taobao.org/dist/sentry-cli
+1 -1
View File
@@ -39,7 +39,7 @@ export default defineConfig({
scripts: [
'https://gw.alipayobjects.com/os/lib/react/16.13.1/umd/react.production.min.js',
'https://gw.alipayobjects.com/os/lib/react-dom/16.13.1/umd/react-dom.production.min.js',
'https://cdn.jsdelivr.net/npm/darkreader@4/darkreader.min.js',
'https://cdn.jsdelivr.net/npm/darkreader@4.9.40/darkreader.min.js',
'https://cdn.jsdelivr.net/npm/codemirror@5/lib/codemirror.min.js',
'https://cdn.jsdelivr.net/npm/codemirror@5/mode/shell/shell.js',
'https://cdn.jsdelivr.net/npm/codemirror@5/mode/python/python.js',
+1 -1
View File
@@ -1,6 +1,6 @@
<p align="center">
<a href="https://github.com/whyour/qinglong">
<img width="150" src="https://pic.imgdb.cn/item/61acd0dd2ab3f51d912b1986.png">
<img width="150" src="https://z3.ax1x.com/2021/11/18/I7MpAe.png">
</a>
</p>
+4 -4
View File
@@ -8,10 +8,10 @@ import { celebrate, Joi } from 'celebrate';
const route = Router();
export default (app: Router) => {
app.use('/', route);
app.use('/configs', route);
route.get(
'/configs/files',
'/files',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
@@ -32,7 +32,7 @@ export default (app: Router) => {
);
route.get(
'/configs/:file',
'/:file',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
@@ -58,7 +58,7 @@ export default (app: Router) => {
);
route.post(
'/configs/save',
'/save',
celebrate({
body: Joi.object({
name: Joi.string().required(),
+26 -30
View File
@@ -7,26 +7,22 @@ import cron_parser from 'cron-parser';
const route = Router();
export default (app: Router) => {
app.use('/', route);
route.get(
'/crons',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronService = Container.get(CronService);
const data = await cronService.crontabs(
req.query.searchValue as string,
);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
app.use('/crons', route);
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronService = Container.get(CronService);
const data = await cronService.crontabs(req.query.searchValue as string);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
});
route.post(
'/crons',
'/',
celebrate({
body: Joi.object({
command: Joi.string().required(),
@@ -52,7 +48,7 @@ export default (app: Router) => {
);
route.put(
'/crons/run',
'/run',
celebrate({
body: Joi.array().items(Joi.string().required()),
}),
@@ -70,7 +66,7 @@ export default (app: Router) => {
);
route.put(
'/crons/stop',
'/stop',
celebrate({
body: Joi.array().items(Joi.string().required()),
}),
@@ -88,7 +84,7 @@ export default (app: Router) => {
);
route.put(
'/crons/disable',
'/disable',
celebrate({
body: Joi.array().items(Joi.string().required()),
}),
@@ -106,7 +102,7 @@ export default (app: Router) => {
);
route.put(
'/crons/enable',
'/enable',
celebrate({
body: Joi.array().items(Joi.string().required()),
}),
@@ -124,7 +120,7 @@ export default (app: Router) => {
);
route.get(
'/crons/:id/log',
'/:id/log',
celebrate({
params: Joi.object({
id: Joi.string().required(),
@@ -144,7 +140,7 @@ export default (app: Router) => {
);
route.put(
'/crons',
'',
celebrate({
body: Joi.object({
command: Joi.string().optional(),
@@ -174,7 +170,7 @@ export default (app: Router) => {
);
route.delete(
'/crons',
'/',
celebrate({
body: Joi.array().items(Joi.string().required()),
}),
@@ -192,7 +188,7 @@ export default (app: Router) => {
);
route.put(
'/crons/pin',
'/pin',
celebrate({
body: Joi.array().items(Joi.string().required()),
}),
@@ -210,7 +206,7 @@ export default (app: Router) => {
);
route.put(
'/crons/unpin',
'/unpin',
celebrate({
body: Joi.array().items(Joi.string().required()),
}),
@@ -228,7 +224,7 @@ export default (app: Router) => {
);
route.get(
'/crons/import',
'/import',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
@@ -243,7 +239,7 @@ export default (app: Router) => {
);
route.get(
'/crons/:id',
'/:id',
celebrate({
params: Joi.object({
id: Joi.string().required(),
@@ -263,7 +259,7 @@ export default (app: Router) => {
);
route.put(
'/crons/status',
'/status',
celebrate({
body: Joi.object({
ids: Joi.array().items(Joi.string().required()),
+19 -21
View File
@@ -6,24 +6,22 @@ import { celebrate, Joi } from 'celebrate';
const route = Router();
export default (app: Router) => {
app.use('/', route);
route.get(
'/dependencies',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const dependenceService = Container.get(DependenceService);
const data = await dependenceService.dependencies(req.query as any);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
app.use('/dependencies', route);
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const dependenceService = Container.get(DependenceService);
const data = await dependenceService.dependencies(req.query as any);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
});
route.post(
'/dependencies',
'/',
celebrate({
body: Joi.array().items(
Joi.object({
@@ -47,7 +45,7 @@ export default (app: Router) => {
);
route.put(
'/dependencies',
'/',
celebrate({
body: Joi.object({
name: Joi.string().required(),
@@ -70,7 +68,7 @@ export default (app: Router) => {
);
route.delete(
'/dependencies',
'/',
celebrate({
body: Joi.array().items(Joi.string().required()),
}),
@@ -88,7 +86,7 @@ export default (app: Router) => {
);
route.delete(
'/dependencies/force',
'/force',
celebrate({
body: Joi.array().items(Joi.string().required()),
}),
@@ -106,7 +104,7 @@ export default (app: Router) => {
);
route.get(
'/dependencies/:id',
'/:id',
celebrate({
params: Joi.object({
id: Joi.string().required(),
@@ -126,7 +124,7 @@ export default (app: Router) => {
);
route.put(
'/dependencies/reinstall',
'/reinstall',
celebrate({
body: Joi.array().items(Joi.string().required()),
}),
+21 -23
View File
@@ -6,24 +6,22 @@ import { celebrate, Joi } from 'celebrate';
const route = Router();
export default (app: Router) => {
app.use('/', route);
route.get(
'/envs',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const envService = Container.get(EnvService);
const data = await envService.envs(req.query.searchValue as string);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
app.use('/envs', route);
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const envService = Container.get(EnvService);
const data = await envService.envs(req.query.searchValue as string);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
});
route.post(
'/envs',
'/',
celebrate({
body: Joi.array().items(
Joi.object({
@@ -47,7 +45,7 @@ export default (app: Router) => {
);
route.put(
'/envs',
'/',
celebrate({
body: Joi.object({
value: Joi.string().required(),
@@ -70,7 +68,7 @@ export default (app: Router) => {
);
route.delete(
'/envs',
'/',
celebrate({
body: Joi.array().items(Joi.string().required()),
}),
@@ -88,7 +86,7 @@ export default (app: Router) => {
);
route.put(
'/envs/:id/move',
'/:id/move',
celebrate({
params: Joi.object({
id: Joi.string().required(),
@@ -112,7 +110,7 @@ export default (app: Router) => {
);
route.put(
'/envs/disable',
'/disable',
celebrate({
body: Joi.array().items(Joi.string().required()),
}),
@@ -130,7 +128,7 @@ export default (app: Router) => {
);
route.put(
'/envs/enable',
'/enable',
celebrate({
body: Joi.array().items(Joi.string().required()),
}),
@@ -148,7 +146,7 @@ export default (app: Router) => {
);
route.put(
'/envs/name',
'/name',
celebrate({
body: Joi.object({
ids: Joi.array().items(Joi.string().required()),
@@ -169,7 +167,7 @@ export default (app: Router) => {
);
route.get(
'/envs/:id',
'/:id',
celebrate({
params: Joi.object({
id: Joi.string().required(),
+2
View File
@@ -7,6 +7,7 @@ import cron from './cron';
import script from './script';
import open from './open';
import dependence from './dependence';
import system from './system';
export default () => {
const app = Router();
@@ -18,6 +19,7 @@ export default () => {
script(app);
open(app);
dependence(app);
system(app);
return app;
};
+33 -35
View File
@@ -7,44 +7,42 @@ import { getFileContentByName } from '../config/util';
const route = Router();
export default (app: Router) => {
app.use('/', route);
route.get(
'/logs',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const fileList = fs.readdirSync(config.logPath, 'utf-8');
const dirs = [];
for (let i = 0; i < fileList.length; i++) {
const stat = fs.lstatSync(config.logPath + fileList[i]);
if (stat.isDirectory()) {
const fileListTmp = fs.readdirSync(
`${config.logPath}/${fileList[i]}`,
'utf-8',
);
dirs.push({
name: fileList[i],
isDir: true,
files: fileListTmp.reverse(),
});
} else {
dirs.push({
name: fileList[i],
isDir: false,
files: [],
});
}
app.use('/logs', route);
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const fileList = fs.readdirSync(config.logPath, 'utf-8');
const dirs = [];
for (let i = 0; i < fileList.length; i++) {
const stat = fs.lstatSync(config.logPath + fileList[i]);
if (stat.isDirectory()) {
const fileListTmp = fs.readdirSync(
`${config.logPath}/${fileList[i]}`,
'utf-8',
);
dirs.push({
name: fileList[i],
isDir: true,
files: fileListTmp.reverse(),
});
} else {
dirs.push({
name: fileList[i],
isDir: false,
files: [],
});
}
res.send({ code: 200, dirs });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
res.send({ code: 200, dirs });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
});
route.get(
'/logs/:dir/:file',
'/:dir/:file',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
@@ -61,7 +59,7 @@ export default (app: Router) => {
);
route.get(
'/logs/:file',
'/:file',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
+34 -8
View File
@@ -14,10 +14,10 @@ import ScriptService from '../services/script';
const route = Router();
export default (app: Router) => {
app.use('/', route);
app.use('/scripts', route);
route.get(
'/scripts/files',
'/files',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
@@ -80,7 +80,7 @@ export default (app: Router) => {
);
route.get(
'/scripts/:file',
'/:file',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
@@ -99,7 +99,7 @@ export default (app: Router) => {
);
route.post(
'/scripts',
'/',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
@@ -159,7 +159,7 @@ export default (app: Router) => {
);
route.put(
'/scripts',
'/',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
@@ -186,7 +186,7 @@ export default (app: Router) => {
);
route.delete(
'/scripts',
'/',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
@@ -211,7 +211,7 @@ export default (app: Router) => {
);
route.post(
'/scripts/download',
'/download',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
@@ -242,7 +242,7 @@ export default (app: Router) => {
);
route.put(
'/scripts/run',
'/run',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
@@ -266,4 +266,30 @@ export default (app: Router) => {
}
},
);
route.put(
'/stop',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
path: Joi.string().optional().allow(''),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
let { filename, path } = req.body as {
filename: string;
path: string;
};
const filePath = join(path, filename);
const scriptService = Container.get(ScriptService);
const result = await scriptService.stopScript(filePath);
res.send(result);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
};
+114
View File
@@ -0,0 +1,114 @@
import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import { Logger } from 'winston';
import * as fs from 'fs';
import config from '../config';
import SystemService from '../services/system';
import { celebrate, Joi } from 'celebrate';
import { getFileContentByName } from '../config/util';
import UserService from '../services/user';
const route = Router();
export default (app: Router) => {
app.use('/system', route);
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const authInfo = await userService.getUserInfo();
const envDbContent = getFileContentByName(config.envDbFile);
const versionRegx = /.*export const version = \'(.*)\'\;/;
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
const version = currentVersionFile.match(versionRegx)![1];
let isInitialized = true;
if (
Object.keys(authInfo).length === 2 &&
authInfo.username === 'admin' &&
authInfo.password === 'admin' &&
envDbContent.length === 0
) {
isInitialized = false;
}
res.send({
code: 200,
data: {
isInitialized,
version,
},
});
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
});
route.get(
'/log/remove',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const systemService = Container.get(SystemService);
const data = await systemService.getLogRemoveFrequency();
res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/log/remove',
celebrate({
body: Joi.object({
frequency: Joi.number().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const systemService = Container.get(SystemService);
const result = await systemService.updateLogRemoveFrequency(
req.body.frequency,
);
res.send(result);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/update-check',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const systemService = Container.get(SystemService);
const result = await systemService.checkUpdate();
res.send(result);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/update',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const systemService = Container.get(SystemService);
const result = await systemService.updateSystem();
res.send(result);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
};
+29 -130
View File
@@ -9,7 +9,8 @@ import { getFileContentByName } from '../config/util';
const route = Router();
export default (app: Router) => {
app.use('/', route);
app.use('/user', route);
route.post(
'/login',
celebrate({
@@ -47,7 +48,7 @@ export default (app: Router) => {
);
route.put(
'/user',
'/',
celebrate({
body: Joi.object({
username: Joi.string().required(),
@@ -67,29 +68,26 @@ export default (app: Router) => {
},
);
route.get(
'/user',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const authInfo = await userService.getUserInfo();
res.send({
code: 200,
data: {
username: authInfo.username,
twoFactorActivated: authInfo.twoFactorActivated,
},
});
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const authInfo = await userService.getUserInfo();
res.send({
code: 200,
data: {
username: authInfo.username,
twoFactorActivated: authInfo.twoFactorActivated,
},
});
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
});
route.get(
'/user/two-factor/init',
'/two-factor/init',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
@@ -104,7 +102,7 @@ export default (app: Router) => {
);
route.put(
'/user/two-factor/active',
'/two-factor/active',
celebrate({
body: Joi.object({
code: Joi.string().required(),
@@ -124,7 +122,7 @@ export default (app: Router) => {
);
route.put(
'/user/two-factor/deactive',
'/two-factor/deactive',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
@@ -139,7 +137,7 @@ export default (app: Router) => {
);
route.put(
'/user/two-factor/login',
'/two-factor/login',
celebrate({
body: Joi.object({
code: Joi.string().required(),
@@ -161,7 +159,7 @@ export default (app: Router) => {
);
route.get(
'/user/login-log',
'/login-log',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
@@ -176,7 +174,7 @@ export default (app: Router) => {
);
route.get(
'/user/notification',
'/notification',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
@@ -191,7 +189,7 @@ export default (app: Router) => {
);
route.put(
'/user/notification',
'/notification',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
@@ -205,40 +203,8 @@ export default (app: Router) => {
},
);
route.get(
'/system',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const authInfo = await userService.getUserInfo();
const envDbContent = getFileContentByName(config.envDbFile);
let isInitialized = true;
if (
Object.keys(authInfo).length === 2 &&
authInfo.username === 'admin' &&
authInfo.password === 'admin' &&
envDbContent.length === 0
) {
isInitialized = false;
}
res.send({
code: 200,
data: {
isInitialized,
},
});
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
// 初始化api
route.put(
'/init/user',
'/init',
celebrate({
body: Joi.object({
username: Joi.string().required(),
@@ -259,7 +225,7 @@ export default (app: Router) => {
);
route.put(
'/init/notification',
'/notification/init',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
@@ -272,71 +238,4 @@ export default (app: Router) => {
}
},
);
route.get(
'/system/log/remove',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const data = await userService.getLogRemoveFrequency();
res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/system/log/remove',
celebrate({
body: Joi.object({
frequency: Joi.number().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const result = await userService.updateLogRemoveFrequency(
req.body.frequency,
);
res.send(result);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/system/update-check',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const result = await userService.checkUpdate();
res.send(result);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/system/update',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const result = await userService.updateSystem();
res.send(result);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
};
+3 -3
View File
@@ -77,12 +77,12 @@ export default {
writePathList: [configPath, scriptPath],
bakPath,
apiWhiteList: [
'/api/login',
'/api/user/login',
'/open/auth/token',
'/api/user/two-factor/login',
'/api/system',
'/api/init/user',
'/api/init/notification',
'/api/user/init',
'/api/user/notification/init',
],
versionFile,
lastVersionFile,
+37
View File
@@ -231,3 +231,40 @@ export async function fileExist(file: any) {
}
});
}
export async function concurrentRun(
fnList: Array<() => Promise<any>> = [],
max = 5,
) {
if (!fnList.length) return;
const replyList: any[] = []; // 收集任务执行结果
const startTime = new Date().getTime(); // 记录任务执行开始时间
// 任务执行程序
const schedule = async (index: number) => {
return new Promise(async (resolve) => {
const fn = fnList[index];
if (!fn) return resolve(null);
// 执行当前异步任务
const reply = await fn();
replyList[index] = reply;
// 执行完当前任务后,继续执行任务池的剩余任务
await schedule(index + max);
resolve(null);
});
};
// 任务池执行程序
const scheduleList = new Array(max)
.fill(0)
.map((_, index) => schedule(index));
// 使用 Promise.all 批量执行
const r = await Promise.all(scheduleList);
const cost = (new Date().getTime() - startTime) / 1000;
return replyList;
}
+1 -1
View File
@@ -98,7 +98,7 @@ export default ({ app }: { app: Application }) => {
});
app.use(async (req, res, next) => {
if (!['/api/init/user', '/api/init/notification'].includes(req.path)) {
if (!['/api/user/init', '/api/user/notification/init'].includes(req.path)) {
return next();
}
const userService = Container.get(UserService);
+1 -1
View File
@@ -5,7 +5,7 @@ import Logger from './logger';
export default ({ expressApp }: { expressApp: Application }) => {
Sentry.init({
dsn: 'https://e14681bce55f4849b11024a7d424b711@o1051273.ingest.sentry.io/6047906',
dsn: 'https://f4b5b55fb3c645b29a5dc2d70a1a4ef4@o1098464.ingest.sentry.io/6122819',
integrations: [
new Sentry.Integrations.Http({ tracing: true }),
new Tracing.Integrations.Express({ app: expressApp }),
+6 -6
View File
@@ -6,7 +6,7 @@ import { Crontab, CrontabStatus } from '../data/cron';
import { exec, execSync, spawn } from 'child_process';
import fs from 'fs';
import cron_parser from 'cron-parser';
import { getFileContentByName } from '../config/util';
import { getFileContentByName, concurrentRun } from '../config/util';
import PQueue from 'p-queue';
import { promises, existsSync } from 'fs';
import { promisify } from 'util';
@@ -200,10 +200,10 @@ export default class CronService {
{ $set: { status: CrontabStatus.queued } },
{ multi: true },
);
for (let i = 0; i < ids.length; i++) {
const id = ids[i];
this.queue.add(() => this.runSingle(id));
}
concurrentRun(
ids.map((id) => () => this.runSingle(id)),
10,
);
}
public async stop(ids: string[]) {
@@ -241,7 +241,7 @@ export default class CronService {
});
}
private async killTask(name: string) {
public async killTask(name: string) {
let taskCommand = `ps -ef | grep "${name}" | grep -v grep | awk '{print $1}'`;
const execAsync = promisify(exec);
try {
+22 -4
View File
@@ -2,21 +2,19 @@ import { Service, Inject } from 'typedi';
import winston from 'winston';
import { spawn } from 'child_process';
import SockService from './sock';
import CronService from './cron';
@Service()
export default class ScriptService {
constructor(
@Inject('logger') private logger: winston.Logger,
private sockService: SockService,
private cronService: CronService,
) {}
public async runScript(path: string) {
const cp = spawn(`task -l ${path} now`, { shell: '/bin/bash' });
this.sockService.sendMessage({
type: 'manuallyRunScript',
message: `开始执行脚本`,
});
cp.stdout.on('data', (data) => {
this.sockService.sendMessage({
type: 'manuallyRunScript',
@@ -38,6 +36,26 @@ export default class ScriptService {
});
});
cp.on('close', (err) => {
this.sockService.sendMessage({
type: 'manuallyRunScript',
message: `执行结束`,
});
});
return { code: 200 };
}
public async stopScript(path: string) {
const err = await this.cronService.killTask(`task -l ${path} now`);
const str = err ? `\n${err}` : '';
this.sockService.sendMessage({
type: 'manuallyRunScript',
message: `${str}\n## 执行结束... ${new Date()
.toLocaleString('zh', { hour12: false })
.replace(' 24:', ' 00:')} `,
});
return { code: 200 };
}
}
+184
View File
@@ -0,0 +1,184 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import config from '../config';
import * as fs from 'fs';
import _ from 'lodash';
import { AuthDataType, AuthInfo, LoginStatus } from '../data/auth';
import { NotificationInfo } from '../data/notify';
import NotificationService from './notify';
import ScheduleService from './schedule';
import { spawn } from 'child_process';
import SockService from './sock';
import got from 'got';
import { dbs } from '../loaders/db';
@Service()
export default class SystemService {
@Inject((type) => NotificationService)
private notificationService!: NotificationService;
private authDb = dbs.authDb;
constructor(
@Inject('logger') private logger: winston.Logger,
private scheduleService: ScheduleService,
private sockService: SockService,
) {}
public async getLogRemoveFrequency() {
return new Promise((resolve) => {
this.authDb
.find({ type: AuthDataType.removeLogFrequency })
.exec((err, docs) => {
if (err || docs.length === 0) {
resolve({});
} else {
resolve(docs[0].info);
}
});
});
}
private async updateAuthDb(payload: AuthInfo): Promise<any> {
return new Promise((resolve) => {
this.authDb.update(
{ type: payload.type },
{ ...payload },
{ upsert: true, returnUpdatedDocs: true },
(err, num, doc: any) => {
if (err) {
resolve({} as NotificationInfo);
} else {
resolve({ ...doc.info, _id: doc._id });
}
},
);
});
}
public async updateNotificationMode(notificationInfo: NotificationInfo) {
const code = Math.random().toString().slice(-6);
const isSuccess = await this.notificationService.testNotify(
notificationInfo,
'青龙',
`【蛟龙】测试通知 https://t.me/jiao_long`,
);
if (isSuccess) {
const result = await this.updateAuthDb({
type: AuthDataType.notification,
info: { ...notificationInfo },
});
return { code: 200, data: { ...result, code } };
} else {
return { code: 400, data: '通知发送失败,请检查参数' };
}
}
public async updateLogRemoveFrequency(frequency: number) {
const result = await this.updateAuthDb({
type: AuthDataType.removeLogFrequency,
info: { frequency },
});
const cron = {
_id: result._id,
name: '删除日志',
command: `ql rmlog ${frequency}`,
schedule: `5 23 */${frequency} * *`,
};
await this.scheduleService.cancelSchedule(cron);
if (frequency > 0) {
await this.scheduleService.generateSchedule(cron);
}
return { code: 200, data: { ...cron } };
}
public async checkUpdate() {
try {
const versionRegx = /.*export const version = \'(.*)\'\;/;
const logRegx = /.*export const changeLog = \`((.*\n.*)+)\`;/;
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
const currentVersion = currentVersionFile.match(versionRegx)![1];
let lastVersion = '';
let lastLog = '';
try {
const result = await Promise.race([
got.get(config.lastVersionFile, { timeout: 1000, retry: 0 }),
got.get(`https://ghproxy.com/${config.lastVersionFile}`, {
timeout: 5000,
retry: 0,
}),
]);
const lastVersionFileContent = result.body;
lastVersion = lastVersionFileContent.match(versionRegx)![1];
lastLog = lastVersionFileContent.match(logRegx)
? lastVersionFileContent.match(logRegx)![1]
: '';
} catch (error) {}
return {
code: 200,
data: {
hasNewVersion: this.checkHasNewVersion(currentVersion, lastVersion),
lastVersion,
lastLog,
},
};
} catch (error: any) {
return {
code: 400,
data: error.message,
};
}
}
private checkHasNewVersion(curVersion: string, lastVersion: string) {
const curArr = curVersion.split('.').map((x) => parseInt(x, 10));
const lastArr = lastVersion.split('.').map((x) => parseInt(x, 10));
if (curArr[0] < lastArr[0]) {
return true;
}
if (curArr[0] === lastArr[0] && curArr[1] < lastArr[1]) {
return true;
}
if (
curArr[0] === lastArr[0] &&
curArr[1] === lastArr[1] &&
curArr[2] < lastArr[2]
) {
return true;
}
return false;
}
public async updateSystem() {
const cp = spawn('ql -l update', { shell: '/bin/bash' });
this.sockService.sendMessage({
type: 'updateSystemVersion',
message: `开始更新系统`,
});
cp.stdout.on('data', (data) => {
this.sockService.sendMessage({
type: 'updateSystemVersion',
message: data.toString(),
});
});
cp.stderr.on('data', (data) => {
this.sockService.sendMessage({
type: 'updateSystemVersion',
message: data.toString(),
});
});
cp.on('error', (err) => {
this.sockService.sendMessage({
type: 'updateSystemVersion',
message: JSON.stringify(err),
});
});
return { code: 200 };
}
}
+13
View File
@@ -0,0 +1,13 @@
import { CommandModule } from 'yargs';
export const updateCommand: CommandModule = {
command: 'update',
describe: 'Update and restart qinglong',
builder: (yargs) => {
return yargs.option('repositority', {
type: 'string',
alias: 'r',
describe: `Specify the release warehouse address of the package`,
});
},
handler: async (argv) => {},
};
+11 -1
View File
@@ -1,3 +1,13 @@
import * as yargs from 'yargs';
import { green, red } from 'chalk';
import { updateCommand } from './commands/update';
yargs.help('h').alias('h', 'help').help().argv;
yargs
.usage('Usage: ql [command] <options>')
.command(updateCommand)
.fail((err) => {
console.error(`${red(err)}`);
})
.alias('h', 'help')
.showHelp()
.recommendCommands().argv;
+20 -7
View File
@@ -264,13 +264,18 @@ run_designated() {
## 运行其他命令
run_else() {
local log_time=$(date "+%Y-%m-%d-%H-%M-%S")
local log_dir_tmp="${1##*/}"
local log_dir="$dir_log/${log_dir_tmp%%.*}"
local file_param="$1"
define_program "$file_param"
log_time=$(date "+%Y-%m-%d-%H-%M-%S")
log_dir_tmp="${file_param##*/}"
log_dir_tmp_path="${file_param%%/*}"
log_dir_tmp_path="${log_dir_tmp_path##*/}"
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
log_dir="$dir_log/${log_dir_tmp%%.*}"
log_path="$log_dir/$log_time.log"
cmd="&>> $log_path"
[[ "$show_log" == "true" ]] && cmd=""
make_dir "$log_dir"
make_dir $log_dir
local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
local begin_timestamp=$(date "+%s" -d "$begin_time")
@@ -278,13 +283,21 @@ run_else() {
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 $first_param" | perl -pe "s|.*ID=(.*) $cmd_task $first_param\.*|\1|" | head -1 | awk -F " " '{print $1}')
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
eval timeout -k 10s $command_timeout_time "$@" $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 . $file_task_after "$@" $cmd
shift
eval timeout -k 10s $command_timeout_time $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" -d "$end_time")
local diff_time=$(( $end_timestamp - $begin_timestamp ))
+2 -2
View File
@@ -27,7 +27,7 @@ import * as Sentry from '@sentry/react';
import { Integrations } from '@sentry/tracing';
Sentry.init({
dsn: 'https://ea2fede373244db99c536210b910d9da@o1051273.ingest.sentry.io/6047851',
dsn: 'https://3406424fb1dc4813a62d39e844a9d0ac@o1098464.ingest.sentry.io/6122818',
integrations: [new Integrations.BrowserTracing()],
release: version,
tracesSampleRate: 1.0,
@@ -44,7 +44,7 @@ export default function (props: any) {
const [collapsed, setCollapsed] = useState(false);
const logout = () => {
request.post(`${config.apiPrefix}logout`).then(() => {
request.post(`${config.apiPrefix}user/logout`).then(() => {
localStorage.removeItem(config.authKey);
history.push('/login');
});
-8
View File
@@ -1,11 +1,3 @@
.ant-table-pagination.ant-pagination {
margin-bottom: 0 !important;
}
tr {
td:nth-child(2) {
span {
cursor: pointer;
}
}
}
+8 -4
View File
@@ -77,7 +77,7 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
width: 150,
align: 'center' as const,
render: (text: string, record: any) => (
<span
<a
onClick={() => {
goToScriptManager(record);
}}
@@ -90,7 +90,7 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
) : (
''
)}
</span>
</a>
),
sorter: {
compare: (a: any, b: any) => a.name.localeCompare(b.name),
@@ -335,8 +335,12 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
const [tableScrollHeight, setTableScrollHeight] = useState<number>();
const goToScriptManager = (record: any) => {
const cmd = record.command.split(' ');
const cmd = record.command.split(' ') as string[];
if (cmd[0] === 'task') {
if (cmd[1].startsWith('/ql/scripts')) {
cmd[1] = cmd[1].replace('/ql/scripts/', '');
}
let [p, s] = cmd[1].split('/');
if (!s) {
s = p;
@@ -799,7 +803,7 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
onSearch={onSearch}
/>,
<Button key="2" type="primary" onClick={() => addCron()}>
</Button>,
]}
header={{
+1 -1
View File
@@ -27,7 +27,7 @@ const CronModal = ({
data: payload,
});
if (code === 200) {
message.success(cron ? '更新Cron成功' : '添加Cron成功');
message.success(cron ? '更新Cron成功' : '新建Cron成功');
} else {
message.error(data);
}
+1 -1
View File
@@ -398,7 +398,7 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
onSearch={onSearch}
/>,
<Button key="2" type="primary" onClick={() => addDependence()}>
</Button>,
]}
header={{
+1 -1
View File
@@ -485,7 +485,7 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
onSearch={onSearch}
/>,
<Button key="2" type="primary" onClick={() => addEnv()}>
</Button>,
]}
header={{
+1 -1
View File
@@ -40,7 +40,7 @@ const EnvModal = ({
data: payload,
});
if (code === 200) {
message.success(env ? '更新变量成功' : '添加变量成功');
message.success(env ? '更新变量成功' : '新建变量成功');
} else {
message.error(data);
}
+7 -2
View File
@@ -35,7 +35,7 @@ const Initialization = () => {
const submitAccountSetting = (values: any) => {
setLoading(true);
request
.put(`${config.apiPrefix}init/user`, {
.put(`${config.apiPrefix}user/init`, {
data: {
username: values.username,
password: values.password,
@@ -52,8 +52,9 @@ const Initialization = () => {
};
const submitNotification = (values: any) => {
setLoading(true);
request
.put(`${config.apiPrefix}init/notification`, {
.put(`${config.apiPrefix}user/notification/init`, {
data: {
...values,
},
@@ -73,6 +74,10 @@ const Initialization = () => {
setFields(_fields || []);
};
useEffect(() => {
localStorage.removeItem(config.authKey);
}, []);
const steps = [
{
title: '欢迎使用',
+15 -4
View File
@@ -31,7 +31,7 @@ const Login = () => {
setTwoFactor(false);
setWaitTime(null);
request
.post(`${config.apiPrefix}login`, {
.post(`${config.apiPrefix}user/login`, {
data: {
username: values.username,
password: values.password,
@@ -113,6 +113,14 @@ const Login = () => {
}
};
const codeInputChange = (e: React.ChangeEvent) => {
const { value } = e.target as any;
const regx = /^[0-9]{6}$/;
if (regx.test(value)) {
completeTowFactor({ code: value });
}
};
useEffect(() => {
const isAuth = localStorage.getItem(config.authKey);
if (isAuth) {
@@ -144,12 +152,15 @@ const Login = () => {
{
pattern: /^[0-9]{6}$/,
message: '验证码为6位数字',
validateTrigger: 'onBlur',
},
]}
hasFeedback
>
<Input placeholder="6位数字" autoFocus autoComplete="off" />
<Input
placeholder="6位数字"
onChange={codeInputChange}
autoFocus
autoComplete="off"
/>
</FormItem>
<Button
type="primary"
+38 -7
View File
@@ -47,6 +47,7 @@ const EditModal = ({
const [log, setLog] = useState<string>('');
const { theme } = useTheme();
const editorRef = useRef<any>(null);
const [isRunning, setIsRunning] = useState(false);
const cancel = () => {
handleCancel();
@@ -80,7 +81,22 @@ const EditModal = ({
path: cNode.parent || '',
},
})
.then((data) => {});
.then((data) => {
setIsRunning(true);
});
};
const stop = () => {
request
.put(`${config.apiPrefix}scripts/stop`, {
data: {
filename: cNode.value,
path: cNode.parent || '',
},
})
.then((data) => {
setIsRunning(false);
});
};
useEffect(() => {
@@ -94,6 +110,13 @@ const EditModal = ({
return;
}
if (_message === '执行结束') {
_message = '';
setTimeout(() => {
setIsRunning(false);
}, 300);
}
if (log) {
_message = `\n${_message}`;
}
@@ -135,8 +158,12 @@ const EditModal = ({
<Option value="shell">shell</Option>
<Option value="python">python</Option>
</Select>
<Button type="primary" style={{ marginRight: 8 }} onClick={run}>
<Button
type="primary"
style={{ marginRight: 8 }}
onClick={isRunning ? stop : run}
>
{isRunning ? '停止' : '运行'}
</Button>
<Button
type="primary"
@@ -169,6 +196,7 @@ const EditModal = ({
type="primary"
style={{ marginRight: 8 }}
onClick={() => {
stop();
handleCancel();
}}
>
@@ -181,7 +209,12 @@ const EditModal = ({
onClose={cancel}
visible={visible}
>
<SplitPane split="vertical" minSize={200} defaultSize="50%">
<SplitPane
split="vertical"
minSize={200}
defaultSize="50%"
style={{ height: 'calc(100vh - 55px)' }}
>
<Editor
language={language}
value={value}
@@ -196,9 +229,7 @@ const EditModal = ({
editorRef.current = editor;
}}
/>
<div>
<pre>{log}</pre>
</div>
<pre style={{ height: '100%', whiteSpace: 'break-spaces' }}>{log}</pre>
</SplitPane>
<SaveModal
visible={saveModalVisible}
+1 -1
View File
@@ -377,7 +377,7 @@ const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
) : (
<Menu>
<Menu.Item key="add" icon={<PlusOutlined />} onClick={addFile}>
</Menu.Item>
<Menu.Item
key="edit"
+1 -1
View File
@@ -26,7 +26,7 @@ const AppModal = ({
data: payload,
});
if (code === 200) {
message.success(app ? '更新应用成功' : '添加应用成功');
message.success(app ? '更新应用成功' : '新建应用成功');
} else {
message.error(data);
}
+1 -1
View File
@@ -319,7 +319,7 @@ const Setting = ({
tabActiveKey === 'app'
? [
<Button key="2" type="primary" onClick={() => addApp()}>
</Button>,
]
: []
+8
View File
@@ -55,6 +55,14 @@ export default {
name: '任务日志',
value: 'logs',
},
{
name: '依赖管理',
value: 'dependencies',
},
{
name: '系统信息',
value: 'system',
},
],
scopesMap: {
crons: '定时任务',
+3 -3
View File
@@ -34,12 +34,12 @@ const errorHandler = function (error: any) {
const _request = extend({ timeout: 60000, params: { t: time }, errorHandler });
const apiWhiteList = [
'/api/login',
'/api/user/login',
'/open/auth/token',
'/api/user/two-factor/login',
'/api/system',
'/api/init/user',
'/api/init/notification',
'/api/user/init',
'/api/user/notification/init',
];
_request.interceptors.request.use((url, options) => {
+8 -7
View File
@@ -1,8 +1,9 @@
export const version = '2.10.11';
export const changeLogLink = 'https://t.me/jiao_long/230';
export const changeLog = `2.10.11 版本说明
1. 修复环境变量中包含双引号变量异常,感谢 https://github.com/mengshouer
2. 修复最后运行时长覆盖逻辑
3. 修复内容区高度和logo样式
4. 修复搜索urlencode的文本时,结果异常
export const version = '2.10.13';
export const changeLogLink = 'https://t.me/jiao_long/233';
export const changeLog = `2.10.13 版本说明
1. task 命令执行脚本支持任意参数,比如 task xxx.js a b c d
2. 应用权限增加依赖管理、系统信息
3. 调试脚本增加停止操作,优化交互
4. 定时任务任务名修改展示效果,感谢https://github.com/tinet-jutt
5. 其他bug修复
`;
+1 -7
View File
@@ -27,13 +27,7 @@
"allowJs": true,
"noEmit": false
},
"include": [
"src/**/*",
"config/**/*",
".umirc.ts",
"typings.d.ts",
"back/**/*"
],
"include": ["src/**/*", ".umirc.ts", "typings.d.ts", "back/**/*"],
"exclude": [
"node_modules",
"lib",