Compare commits

...
43 Commits
Author SHA1 Message Date
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
44 changed files with 1990 additions and 1624 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
-1
View File
@@ -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
``` ```
-1
View File
@@ -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
``` ```
+49 -1
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(''),
}), }),
), ),
@@ -177,4 +192,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);
}
},
);
}; };
+1
View File
@@ -0,0 +1 @@
export const LOG_END_SYMBOL = '\n          ';
+21 -8
View File
@@ -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,
}; };
@@ -346,7 +347,7 @@ export function readDir(
export function emptyDir(path: string) { export function emptyDir(path: string) {
const files = fs.readdirSync(path); const files = fs.readdirSync(path);
files.forEach(file => { files.forEach((file) => {
const filePath = `${path}/${file}`; const filePath = `${path}/${file}`;
const stats = fs.statSync(filePath); const stats = fs.statSync(filePath);
if (stats.isDirectory()) { if (stats.isDirectory()) {
@@ -358,7 +359,6 @@ export function emptyDir(path: string) {
fs.rmdirSync(path); 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(
@@ -379,7 +379,8 @@ export function parseHeaders(headers: string) {
let val; let val;
let i; let i;
headers && headers.split('\n').forEach(function parser(line) { headers &&
headers.split('\n').forEach(function parser(line) {
i = line.indexOf(':'); i = line.indexOf(':');
key = line.substring(0, i).trim().toLowerCase(); key = line.substring(0, i).trim().toLowerCase();
val = line.substring(i + 1).trim(); val = line.substring(i + 1).trim();
@@ -392,9 +393,15 @@ export function parseHeaders(headers: string) {
}); });
return parsed; return parsed;
}; }
export function parseBody(body: string, contentType: 'application/json' | 'multipart/form-data' | 'application/x-www-form-urlencoded') { export function parseBody(
body: string,
contentType:
| 'application/json'
| 'multipart/form-data'
| 'application/x-www-form-urlencoded',
) {
if (!body) return ''; if (!body) return '';
const parsed: any = {}; const parsed: any = {};
@@ -402,7 +409,8 @@ export function parseBody(body: string, contentType: 'application/json' | 'multi
let val; let val;
let i; let i;
body && body.split('\n').forEach(function parser(line) { body &&
body.split('\n').forEach(function parser(line) {
i = line.indexOf(':'); i = line.indexOf(':');
key = line.substring(0, i).trim().toLowerCase(); key = line.substring(0, i).trim().toLowerCase();
val = line.substring(i + 1).trim(); val = line.substring(i + 1).trim();
@@ -411,13 +419,18 @@ export function parseBody(body: string, contentType: 'application/json' | 'multi
return; return;
} }
try {
const jsonValue = JSON.parse(val);
parsed[key] = jsonValue;
} catch (error) {
parsed[key] = val; parsed[key] = val;
}
}); });
switch (contentType) { switch (contentType) {
case 'multipart/form-data': case 'multipart/form-data':
return Object.keys(parsed).reduce((p, c) => { return Object.keys(parsed).reduce((p, c) => {
p.append(c, parsed[c]) p.append(c, parsed[c]);
return p; return p;
}, new FormData()); }, new FormData());
case 'application/x-www-form-urlencoded': case 'application/x-www-form-urlencoded':
@@ -427,4 +440,4 @@ export function parseBody(body: string, contentType: 'application/json' | 'multi
} }
return parsed; return parsed;
}; }
+19 -2
View File
@@ -11,9 +11,11 @@ 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', 'webhook' = 'webhook',
} }
@@ -75,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 = '';
} }
@@ -95,7 +103,14 @@ export class WebhookNotification extends NotificationBaseInfo {
public webhookBody: string = ''; public webhookBody: string = '';
public webhookUrl: string = ''; public webhookUrl: string = '';
public webhookMethod: 'GET' | 'POST' | 'PUT' = 'GET'; public webhookMethod: 'GET' | 'POST' | 'PUT' = 'GET';
public webhookContentType: 'application/json' | 'multipart/form-data' | 'application/x-www-form-urlencoded' = 'application/json'; 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
@@ -109,7 +124,9 @@ export interface NotificationInfo
DingtalkBotNotification, DingtalkBotNotification,
WeWorkBotNotification, WeWorkBotNotification,
WeWorkAppNotification, WeWorkAppNotification,
AibotkNotification,
IGotNotification, IGotNotification,
PushPlusNotification, PushPlusNotification,
EmailNotification, EmailNotification,
WebhookNotification {} WebhookNotification,
LarkNotification {}
+3 -2
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}`,
); );
} }
} }
@@ -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) => {
+4 -4
View File
@@ -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,
+7 -8
View File
@@ -67,12 +67,12 @@ 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! * 2
: envs[toIndex].position / 2; : envs[toIndex].position! / 2;
} 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,
@@ -115,7 +115,7 @@ export default class EnvService {
const result = await this.find(condition, [ const result = await this.find(condition, [
['status', 'ASC'], ['status', 'ASC'],
['position', 'DESC'], ['position', 'DESC'],
['createdAt', 'DESC'], ['createdAt', 'ASC'],
]); ]);
return result as any; return result as any;
} catch (error) { } catch (error) {
@@ -158,7 +158,7 @@ export default class EnvService {
const envs = await this.envs( const envs = await this.envs(
'', '',
{ position: -1 }, { position: -1 },
{ name: { [Op.not]: null } }, { name: { [Op.not]: null }, status: EnvStatus.normal },
); );
const groups = _.groupBy(envs, 'name'); const groups = _.groupBy(envs, 'name');
let env_string = ''; let env_string = '';
@@ -167,9 +167,8 @@ export default class EnvService {
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('value') .map('value')
.join('&') .join('&')
.replace(/(\\)[^n]/g, '\\\\') .replace(/(\\)[^n]/g, '\\\\')
+99 -39
View File
@@ -24,18 +24,23 @@ 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], ['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) {}
public async notify( public async notify(
title: string, title: string,
@@ -76,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(
@@ -95,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 },
}) })
@@ -111,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' },
}) })
@@ -125,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`,
@@ -143,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' },
}) })
@@ -164,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();
@@ -182,7 +181,8 @@ export default class NotificationService {
telegramBotUserId, telegramBotUserId,
} = this.params; } = this.params;
const authStr = telegramBotProxyAuth ? `${telegramBotProxyAuth}@` : ''; const authStr = telegramBotProxyAuth ? `${telegramBotProxyAuth}@` : '';
const url = `https://${telegramBotApiHost ? telegramBotApiHost : 'api.telegram.org' const url = `https://${
telegramBotApiHost ? telegramBotApiHost : 'api.telegram.org'
}/bot${telegramBotToken}/sendMessage`; }/bot${telegramBotToken}/sendMessage`;
let agent; let agent;
if (telegramBotProxyHost && telegramBotProxyPort) { if (telegramBotProxyHost && telegramBotProxyPort) {
@@ -202,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,
@@ -225,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: {
@@ -243,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: {
@@ -263,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,
@@ -314,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,
@@ -329,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' },
}) })
@@ -349,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}`,
@@ -363,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({
@@ -386,10 +435,18 @@ export default class NotificationService {
} }
private async webhook() { private async webhook() {
const { webhookUrl, webhookBody, webhookHeaders, webhookMethod, webhookContentType } = const {
this.params; webhookUrl,
webhookBody,
webhookHeaders,
webhookMethod,
webhookContentType,
} = this.params;
const { formatBody, formatUrl } = this.formatNotifyContent(webhookUrl, webhookBody); const { formatBody, formatUrl } = this.formatNotifyContent(
webhookUrl,
webhookBody,
);
if (!formatUrl && !formatBody) { if (!formatUrl && !formatBody) {
return false; return false;
} }
@@ -399,11 +456,10 @@ export default class NotificationService {
const options = { const options = {
method: webhookMethod, method: webhookMethod,
headers, headers,
timeout: this.timeout, ...this.gotOption,
retry: 0,
allowGetBody: true, allowGetBody: true,
...bodyParam ...bodyParam,
} };
const res = await got(formatUrl, options); const res = await got(formatUrl, options);
return String(res.statusCode).startsWith('20'); return String(res.statusCode).startsWith('20');
} }
@@ -427,8 +483,12 @@ export default class NotificationService {
} }
return { return {
formatUrl: url.replaceAll('$title', encodeURIComponent(this.title)).replaceAll('$content', encodeURIComponent(this.content)), formatUrl: url
formatBody: body.replaceAll('$title', this.title).replaceAll('$content', this.content), .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 };
+3 -2
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 {
@@ -232,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(
@@ -353,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}`,
); );
} }
+1 -6
View File
@@ -89,8 +89,7 @@ export default class SystemService {
let lastLog = ''; let lastLog = '';
try { try {
const result = await got.get(config.lastVersionFile, { const result = await got.get(config.lastVersionFile, {
timeout: 6000, timeout: 30000,
retry: 0,
}); });
const lastVersionFileContent = result.body; const lastVersionFileContent = result.body;
lastVersion = lastVersionFileContent.match(versionRegx)![1]; lastVersion = lastVersionFileContent.match(versionRegx)![1];
@@ -137,10 +136,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',
+16 -7
View File
@@ -24,7 +24,7 @@ export default class UserService {
@Inject('logger') private logger: winston.Logger, @Inject('logger') private logger: winston.Logger,
private scheduleService: ScheduleService, private scheduleService: ScheduleService,
private sockService: SockService, private sockService: SockService,
) { } ) {}
public async login( public async login(
payloads: { payloads: {
@@ -67,7 +67,9 @@ export default class UserService {
const retriesTime = Math.pow(3, retries) * 1000; const retriesTime = Math.pow(3, retries) * 1000;
if (retries > 2 && timestamp - lastlogon < retriesTime) { if (retries > 2 && timestamp - lastlogon < retriesTime) {
const waitTime = Math.ceil((retriesTime - (timestamp - lastlogon)) / 1000); const waitTime = Math.ceil(
(retriesTime - (timestamp - lastlogon)) / 1000,
);
return { return {
code: 410, code: 410,
message: `失败次数过多,请${waitTime}秒后重试`, message: `失败次数过多,请${waitTime}秒后重试`,
@@ -75,9 +77,12 @@ export default class UserService {
}; };
} }
const { ip, address } = await getNetIp(req); if (
if (username === cUsername && password === cPassword) { username === cUsername &&
if (twoFactorActivated && needTwoFactor) { password === cPassword &&
twoFactorActivated &&
needTwoFactor
) {
this.updateAuthInfo(content, { this.updateAuthInfo(content, {
isTwoFactorChecking: true, isTwoFactorChecking: true,
}); });
@@ -87,6 +92,8 @@ export default class UserService {
}; };
} }
const { ip, address } = await getNetIp(req);
if (username === cUsername && password === cPassword) {
const data = createRandomString(50, 100); const data = createRandomString(50, 100);
const expiration = twoFactorActivated ? 60 : 20; const expiration = twoFactorActivated ? 60 : 20;
let token = jwt.sign({ data }, config.secret as any, { let token = jwt.sign({ data }, config.secret as any, {
@@ -109,7 +116,8 @@ export default class UserService {
}); });
await this.notificationService.notify( await this.notificationService.notify(
'登录通知', '登录通知',
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${req.platform `你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${
req.platform
}端 登录成功,ip地址 ${ip}`, }端 登录成功,ip地址 ${ip}`,
); );
await this.getLoginLog(); await this.getLoginLog();
@@ -137,7 +145,8 @@ export default class UserService {
}); });
await this.notificationService.notify( await this.notificationService.notify(
'登录通知', '登录通知',
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${req.platform `你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${
req.platform
}端 登录失败,ip地址 ${ip}`, }端 登录失败,ip地址 ${ip}`,
); );
await this.getLoginLog(); await this.getLoginLog();
+2 -2
View File
@@ -125,12 +125,12 @@
"qrcode.react": "^1.0.1", "qrcode.react": "^1.0.1",
"query-string": "^7.1.1", "query-string": "^7.1.1",
"rc-tween-one": "^3.0.6", "rc-tween-one": "^3.0.6",
"react": "18.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": "18.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",
+14
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"
@@ -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 变量名= 声明即可
+135 -100
View File
@@ -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,7 +336,6 @@ 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) {
@@ -319,9 +348,7 @@ function gobotNotify(text, desp, time = 2100) {
} else if (data.retcode === 100) { } else if (data.retcode === 100) {
console.log(`go-cqhttp发送通知消息异常: ${data.errmsg}\n`); console.log(`go-cqhttp发送通知消息异常: ${data.errmsg}\n`);
} else { } else {
console.log( console.log(`go-cqhttp发送通知消息异常\n${JSON.stringify(data)}`);
`go-cqhttp发送通知消息异常\n${JSON.stringify(data)}`,
);
} }
} }
} catch (e) { } catch (e) {
@@ -330,14 +357,13 @@ function gobotNotify(text, desp, time = 2100) {
resolve(data); resolve(data);
} }
}); });
}, time);
} 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,7 +378,6 @@ 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) {
@@ -367,9 +392,7 @@ function serverNotify(text, desp, time = 2100) {
// 一分钟内发送相同的内容会触发 // 一分钟内发送相同的内容会触发
console.log(`server酱发送通知消息异常: ${data.errmsg}\n`); console.log(`server酱发送通知消息异常: ${data.errmsg}\n`);
} else { } else {
console.log( console.log(`server酱发送通知消息异常\n${JSON.stringify(data)}`);
`server酱发送通知消息异常\n${JSON.stringify(data)}`,
);
} }
} }
} catch (e) { } catch (e) {
@@ -378,7 +401,6 @@ function serverNotify(text, desp, time = 2100) {
resolve(data); resolve(data);
} }
}); });
}, time);
} else { } else {
resolve(); resolve();
} }
@@ -398,9 +420,7 @@ function PushDeerNotify(text, desp) {
}, },
timeout, timeout,
}; };
$.post( $.post(options, (err, resp, data) => {
options,
(err, resp, data) => {
try { try {
if (err) { if (err) {
console.log('发送通知调用API失败!!\n'); console.log('发送通知调用API失败!!\n');
@@ -414,9 +434,7 @@ function PushDeerNotify(text, desp) {
) { ) {
console.log('PushDeer发送通知消息成功🎉\n'); console.log('PushDeer发送通知消息成功🎉\n');
} else { } else {
console.log( console.log(`PushDeer发送通知消息异常\n${JSON.stringify(data)}`);
`PushDeer发送通知消息异常\n${JSON.stringify(data)}`,
);
} }
} }
} catch (e) { } catch (e) {
@@ -424,9 +442,7 @@ function PushDeerNotify(text, desp) {
} finally { } finally {
resolve(data); resolve(data);
} }
}, });
time,
);
} 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: `${title}\n\n${content}` } },
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:
+1 -1
View File
@@ -154,7 +154,7 @@ update_cron() {
code=$(echo $api | jq -r .code) code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message) message=$(echo $api | jq -r .message)
if [[ $code != 200 ]]; then if [[ $code != 200 ]]; then
echo -e "\n## 更新任务状态失败(${message})\n" >> $dir_log/$log_path echo -e "\n## 更新任务状态失败(${message})\n" >>$dir_log/$log_path
fi fi
} }
+3 -4
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"
+273
View File
@@ -0,0 +1,273 @@
#!/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' | awk '{$2="";$3="";$4="";$5="";$7="";print $0}' | head -n 10)
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
local params=$(echo "$@" | sed 's/ /\" \"/g')
$timeoutCmd $which_program $file_param \"$params\"
}
## 命令检测
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 "$@"
+5 -1
View File
@@ -78,6 +78,10 @@ import_config() {
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() {
@@ -482,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
+19 -298
View File
@@ -21,78 +21,12 @@ define_program() {
fi fi
} }
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() {
define_cmd
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 &
}
handle_log_path() { handle_log_path() {
define_program "$file_param" local file_param=$1
if [[ -z $file_param ]];then
file_param="task"
fi
local suffix="" local suffix=""
if [[ ! -z $ID ]]; then if [[ ! -z $ID ]]; then
@@ -118,241 +52,28 @@ handle_log_path() {
make_dir "$dir_log/$log_dir" make_dir "$dir_log/$log_dir"
} }
## 正常运行单个脚本,$1:传入参数 format_params() {
run_normal() { time_format="%Y-%m-%d %H:%M:%S"
local file_param=$1 timeoutCmd=""
if [[ $# -eq 1 ]]; then if type timeout &>/dev/null; then
random_delay "$file_param" timeoutCmd="timeout -k 10s $command_timeout_time "
fi fi
handle_log_path
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
[[ $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 不会生效,即所有任务立即执行 show_log="false"
run_concurrent() { while getopts ":l" opt; do
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}
handle_log_path
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
[[ $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
handle_log_path
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
[[ $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"
handle_log_path
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
[[ $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 case $opt in
l) l)
show_log="true" show_log="true"
;; ;;
esac esac
done done
[[ "$show_log" == "true" ]] && shift $(($OPTIND - 1)) [[ "$show_log" == "true" ]] && shift $(($OPTIND - 1))
timeoutCmd="" format_params
if type timeout &>/dev/null; then define_program "$@"
timeoutCmd="timeout -k 10s $command_timeout_time " handle_log_path "$@"
fi eval . $dir_shell/otask.sh "$@" "$cmd"
[[ -f "$dir_log/$log_path" ]] && cat "$dir_log/$log_path"
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
+52 -36
View File
@@ -230,7 +230,7 @@ run_extra_shell() {
## 脚本用法 ## 脚本用法
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> # 更新单个脚本文件"
@@ -416,8 +416,7 @@ get_uniq_path() {
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"
@@ -433,30 +432,48 @@ main() {
local p5=$5 local p5=$5
local p6=$6 local p6=$6
local p7=$7 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_time=$(date "+%Y-%m-%d-%H-%M-%S")
local log_path="$dir_log/update/${log_time}_$p1.log" local log_path="${log_dir}/${log_time}.log"
local begin_time=$(date '+%Y-%m-%d %H:%M:%S') 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 case $p1 in
update) 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 eval update_qinglong "$2" $cmd
;; ;;
extra) extra)
echo -e "## 开始执行... $begin_time\n" >>$log_path eval run_extra_shell $cmd
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
run_extra_shell >>$log_path
;; ;;
repo) repo)
get_uniq_path "$p2" "$p6" get_uniq_path "$p2" "$p6"
if [[ -n $p2 ]]; then if [[ -n $p2 ]]; then
update_repo "$p2" "$p3" "$p4" "$p5" "$p6" "$p7" update_repo "$p2" "$p3" "$p4" "$p5" "$p6" "$p7"
else else
echo -e "命令输入错误...\n" eval echo -e "命令输入错误...\\\n" $cmd
usage eval usage $cmd
fi fi
;; ;;
raw) raw)
@@ -464,47 +481,46 @@ main() {
if [[ -n $p2 ]]; then if [[ -n $p2 ]]; then
update_raw "$p2" update_raw "$p2"
else else
echo -e "命令输入错误...\n" eval echo -e "命令输入错误...\\\n" $cmd
usage eval usage $cmd
fi fi
;; ;;
rmlog) rmlog)
echo -e "## 开始执行... $begin_time\n" >>$log_path eval . $dir_shell/rmlog.sh "$p2" $cmd
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
. $dir_shell/rmlog.sh "$p2" >>$log_path
;; ;;
bot) bot)
echo -e "## 开始执行... $begin_time\n" >>$log_path eval . $dir_shell/bot.sh $cmd
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
. $dir_shell/bot.sh >>$log_path
;; ;;
check) check)
echo -e "## 开始执行... $begin_time\n" >>$log_path eval . $dir_shell/check.sh $cmd
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
. $dir_shell/check.sh >>$log_path
;; ;;
resetlet) resetlet)
echo -e "## 开始执行... $begin_time\n" >>$log_path
auth_value=$(cat $file_auth_user | jq '.retries =0' -c) auth_value=$(cat $file_auth_user | jq '.retries =0' -c)
echo -e "重置登录错误次数成功 \n $auth_value" >>$log_path
echo "$auth_value" >$file_auth_user echo "$auth_value" >$file_auth_user
echo -e "重置登录错误次数成功" $cmd
;; ;;
resettfa) resettfa)
echo -e "## 开始执行... $begin_time\n" >>$log_path
auth_value=$(cat $file_auth_user | jq '.twoFactorActivated =false' | jq '.twoFactorActived =false' -c) 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 "$auth_value" >$file_auth_user
eval echo -e "禁用两步验证成功" $cmd
;; ;;
*) *)
echo -e "命令输入错误...\n" eval echo -e "命令输入错误...\\\n" $cmd
usage eval usage $cmd
;; ;;
esac esac
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
local diff_time=$(diff_time "%Y-%m-%d %H:%M:%S" "$begin_time" "$end_time") local etime=$(date "+$time_format")
if [[ $p1 != "repo" ]] && [[ $p1 != "raw" ]]; then local end_time=$(format_time "$time_format" "$etime")
echo -e "\n## 执行结束... $end_time 耗时 $diff_time" >>$log_path local end_timestamp=$(format_timestamp "$time_format" "$etime")
cat $log_path 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 fi
} }
+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]);
};
+1
View File
@@ -80,6 +80,7 @@ const Config = () => {
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' }}
+9 -4
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,
@@ -388,6 +388,7 @@ const Crontab = () => {
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 goToScriptManager = (record: any) => { const goToScriptManager = (record: any) => {
const cmd = record.command.split(' ') as string[]; const cmd = record.command.split(' ') as string[];
@@ -874,12 +875,15 @@ const Crontab = () => {
sorter: {}, sorter: {},
filters: {}, filters: {},
}); });
setTimeout(() => {
setTableScrollHeight(getTableScroll());
});
getCronViews(); getCronViews();
}, []); }, []);
useEffect(() => {
if (tableRef.current) {
setTableScrollHeight(getTableScroll());
}
}, [tableRef.current]);
const panelContent = ( const panelContent = (
<> <>
{selectedRowIds.length > 0 && ( {selectedRowIds.length > 0 && (
@@ -939,6 +943,7 @@ const Crontab = () => {
</div> </div>
)} )}
<Table <Table
ref={tableRef}
columns={columns} columns={columns}
pagination={{ pagination={{
current: pageConf.page, current: pageConf.page,
+3 -2
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',
@@ -49,9 +50,9 @@ const CronLogModal = ({
const log = data as string; 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);
+5 -3
View File
@@ -168,6 +168,7 @@ const Dependence = () => {
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 getDependencies = () => { const getDependencies = () => {
setLoading(true); setLoading(true);
@@ -368,10 +369,10 @@ const Dependence = () => {
}, [searchText, type]); }, [searchText, type]);
useEffect(() => { useEffect(() => {
setTimeout(() => { if (tableRef.current) {
setTableScrollHeight(getTableScroll({ extraHeight: 87 })); setTableScrollHeight(getTableScroll({ extraHeight: 87 }));
}); }
}, []); }, [tableRef.current]);
useEffect(() => { useEffect(() => {
if (logDependence) { if (logDependence) {
@@ -454,6 +455,7 @@ const Dependence = () => {
)} )}
<DndProvider backend={HTML5Backend}> <DndProvider backend={HTML5Backend}>
<Table <Table
ref={tableRef}
columns={columns} columns={columns}
rowSelection={rowSelection} rowSelection={rowSelection}
pagination={false} pagination={false}
+45 -3
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';
@@ -250,6 +253,8 @@ const Env = () => {
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]); const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
const [searchText, setSearchText] = useState(''); const [searchText, setSearchText] = useState('');
const [tableScrollHeight, setTableScrollHeight] = useState<number>(); const [tableScrollHeight, setTableScrollHeight] = useState<number>();
const [importLoading, setImportLoading] = useState(false);
const tableRef = useRef<any>();
const getEnvs = () => { const getEnvs = () => {
setLoading(true); setLoading(true);
@@ -472,15 +477,42 @@ const Env = () => {
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(() => { useEffect(() => {
setTimeout(() => { if (tableRef.current) {
setTableScrollHeight(getTableScroll({ extraHeight: 87 })); setTableScrollHeight(getTableScroll({ extraHeight: 87 }));
}); }
}, []); }, [tableRef.current]);
return ( return (
<PageContainer <PageContainer
@@ -494,6 +526,15 @@ const Env = () => {
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>,
@@ -547,6 +588,7 @@ const Env = () => {
)} )}
<DndProvider backend={HTML5Backend}> <DndProvider backend={HTML5Backend}>
<Table <Table
ref={tableRef}
columns={columns} columns={columns}
rowSelection={rowSelection} rowSelection={rowSelection}
pagination={false} pagination={false}
+29 -47
View File
@@ -21,44 +21,16 @@ import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts'; import { SharedContext } from '@/layouts';
import { DeleteOutlined } from '@ant-design/icons'; import { DeleteOutlined } from '@ant-design/icons';
import { depthFirstSearch } from '@/utils'; import { depthFirstSearch } from '@/utils';
import { debounce } from 'lodash'; import { debounce, uniq } from 'lodash';
import useFilterTreeData from '@/hooks/useFilterTreeData';
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 Log = () => { const Log = () => {
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>(); const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
const [value, setValue] = useState('请选择日志文件'); const [value, setValue] = useState('请选择日志文件');
const [select, setSelect] = useState<string>(''); 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>();
@@ -73,7 +45,6 @@ const Log = () => {
.then(({ code, data }) => { .then(({ code, data }) => {
if (code === 200) { if (code === 200) {
setData(data); setData(data);
setFilterData(data);
} }
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
@@ -115,22 +86,26 @@ const Log = () => {
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,
);
setFilterData(tree);
setExpandedKeys(expandedKeys);
}, 300), }, 300),
[data, setFilterData], [data],
); );
const { treeData: filterData, keys: searchExpandedKeys } = useFilterTreeData(
data,
searchValue,
{ treeNodeFilterProp: 'title' },
);
useEffect(() => {
setExpandedKeys(uniq([...expandedKeys, ...searchExpandedKeys]));
}, [searchExpandedKeys]);
const deleteFile = () => { const deleteFile = () => {
Modal.confirm({ Modal.confirm({
title: `确认删除`, title: `确认删除`,
@@ -187,18 +162,19 @@ const Log = () => {
setValue('请选择脚本文件'); setValue('请选择脚本文件');
}; };
useEffect(() => { const onExpand = (expKeys: any) => {
const word = searchValue || ''; setExpandedKeys(expKeys);
const { tree } = getFilterData(word.toLocaleLowerCase(), data); };
setFilterData(tree);
}, [data]);
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
@@ -209,13 +185,16 @@ const Log = () => {
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}
/>, />,
] ]
@@ -236,6 +215,7 @@ const Log = () => {
> >
<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 ? (
@@ -256,6 +236,8 @@ const Log = () => {
selectedKeys={[select]} selectedKeys={[select]}
showLine={{ showLeafIcon: true }} showLine={{ showLeafIcon: true }}
onSelect={onTreeSelect} onSelect={onTreeSelect}
expandedKeys={expandedKeys}
onExpand={onExpand}
></Tree> ></Tree>
</div> </div>
</> </>
+12 -1
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);
@@ -123,7 +129,7 @@ const EditModal = ({
return; return;
} }
if (_message.includes('执行结束')) { if (logEnded(_message)) {
setTimeout(() => { setTimeout(() => {
setIsRunning(false); setIsRunning(false);
}, 300); }, 300);
@@ -136,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]);
@@ -150,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' }}
@@ -222,6 +232,7 @@ const EditModal = ({
onClose={cancel} onClose={cancel}
open={visible} open={visible}
> >
{/* @ts-ignore */}
<SplitPane <SplitPane
split="vertical" split="vertical"
minSize={200} minSize={200}
+31 -51
View File
@@ -37,37 +37,11 @@ import { history, useOutletContext, useLocation } from '@umijs/max';
import { parse } from 'query-string'; import { parse } from 'query-string';
import { depthFirstSearch } from '@/utils'; import { depthFirstSearch } from '@/utils';
import { SharedContext } from '@/layouts'; import { SharedContext } from '@/layouts';
import useFilterTreeData from '@/hooks/useFilterTreeData';
import { uniq } from 'lodash';
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',
@@ -81,7 +55,6 @@ const Script = () => {
const [value, setValue] = useState('请选择脚本文件'); const [value, setValue] = useState('请选择脚本文件');
const [select, setSelect] = useState<string>(''); 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>();
@@ -101,7 +74,6 @@ const Script = () => {
.then(({ code, data }) => { .then(({ code, data }) => {
if (code === 200) { if (code === 200) {
setData(data); setData(data);
setFilterData(data);
initGetScript(); initGetScript();
} }
}) })
@@ -110,7 +82,11 @@ const Script = () => {
const getDetail = (node: any) => { const getDetail = (node: any) => {
request request
.get(`${config.apiPrefix}scripts/${node.title}?path=${node.parent || ''}`) .get(
`${config.apiPrefix}scripts/${encodeURIComponent(node.title)}?path=${
node.parent || ''
}`,
)
.then(({ code, data }) => { .then(({ code, data }) => {
if (code === 200) { if (code === 200) {
setValue(data); setValue(data);
@@ -153,10 +129,6 @@ const Script = () => {
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
@@ -187,22 +159,30 @@ const Script = () => {
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);
@@ -368,17 +348,14 @@ const Script = () => {
}; };
useEffect(() => { useEffect(() => {
const word = searchValue || ''; getScripts();
const { tree } = getFilterData(word.toLocaleLowerCase(), data); }, []);
setFilterData(tree);
}, [data]);
useEffect(() => { useEffect(() => {
getScripts(); if (treeDom.current) {
if (treeDom && 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) {
@@ -459,8 +436,10 @@ const Script = () => {
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']}>
@@ -516,6 +495,7 @@ const Script = () => {
> >
<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 ? (
+7 -7
View File
@@ -102,15 +102,15 @@ 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={{
wordBreak: 'break-all', wordBreak: 'break-all',
@@ -121,7 +121,6 @@ const CheckUpdate = ({ socketMessage }: any) => {
> >
{value} {value}
</pre> </pre>
</div>
), ),
}); });
}; };
@@ -144,7 +143,7 @@ const CheckUpdate = ({ socketMessage }: any) => {
closable: updateFailed, closable: updateFailed,
okButtonProps: { disabled: !updateFailed }, okButtonProps: { disabled: !updateFailed },
content: ( content: (
<div style={{ height: '60vh', overflowY: 'auto' }}> <>
<pre <pre
style={{ style={{
wordBreak: 'break-all', wordBreak: 'break-all',
@@ -156,15 +155,16 @@ 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> </>
), ),
}); });
setValue(newMessage);
if (updateFailed) { if (updateFailed && !value.includes('失败,请检查')) {
message.error('更新失败,请检查网络及日志或稍后再试'); message.error('更新失败,请检查网络及日志或稍后再试');
} }
setValue(newMessage);
document.getElementById('log-identifier') && document.getElementById('log-identifier') &&
document document
.getElementById('log-identifier')! .getElementById('log-identifier')!
+9 -4
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,
@@ -247,6 +247,7 @@ const Subscription = () => {
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 runSubscription = (record: any, index: number) => { const runSubscription = (record: any, index: number) => {
Modal.confirm({ Modal.confirm({
@@ -539,11 +540,14 @@ const Subscription = () => {
useEffect(() => { useEffect(() => {
setPageSize(parseInt(localStorage.getItem('pageSize') || '20')); setPageSize(parseInt(localStorage.getItem('pageSize') || '20'));
setTimeout(() => {
setTableScrollHeight(getTableScroll());
});
}, []); }, []);
useEffect(() => {
if (tableRef.current) {
setTableScrollHeight(getTableScroll());
}
}, [tableRef.current]);
return ( return (
<PageContainer <PageContainer
className="ql-container-wrapper subscriptiontab-wrapper" className="ql-container-wrapper subscriptiontab-wrapper"
@@ -568,6 +572,7 @@ const Subscription = () => {
}} }}
> >
<Table <Table
ref={tableRef}
columns={columns} columns={columns}
pagination={{ pagination={{
current: currentPage, current: currentPage,
+3 -2
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,
@@ -43,8 +44,8 @@ const SubscriptionLogModal = ({
) { ) {
const log = 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);
+36 -1
View File
@@ -88,10 +88,12 @@ 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: 'webhook', label: '自定义通知' },
{ value: 'closed', label: '已关闭' }, { value: 'closed', label: '已关闭' },
], ],
@@ -196,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',
@@ -214,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',
@@ -234,7 +265,11 @@ export default {
label: 'webhookContentType', label: 'webhookContentType',
tip: '请求头Content-Type', tip: '请求头Content-Type',
required: true, required: true,
items: [{ value: 'application/json' }, { value: 'multipart/form-data' }, { value: 'application/x-www-form-urlencoded' }], items: [
{ value: 'application/json' },
{ value: 'multipart/form-data' },
{ value: 'application/x-www-form-urlencoded' },
],
}, },
{ {
label: 'webhookUrl', label: 'webhookUrl',
+1
View File
@@ -0,0 +1 @@
export const LOG_END_SYMBOL = '\n          ';
+7
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();
@@ -273,3 +275,8 @@ export function depthFirstSearch<
return c; return c;
} }
export function logEnded(log: string): boolean {
const endTips = [LOG_END_SYMBOL, '执行结束'];
return endTips.some((x) => log.includes(x));
}
+8 -8
View File
@@ -1,9 +1,9 @@
export const version = '2.14.5'; export const version = '2.14.9';
export const changeLogLink = 'https://t.me/jiao_long/333'; export const changeLogLink = 'https://t.me/jiao_long/336';
export const changeLog = `2.14.5 版本说明 export const changeLog = `2.14.9 版本说明
1. 1. https://github.com/leochen-g
2. 2. webhook通知body支持嵌套json
3. 3. shell获取磁盘占用
4. 4.
5. Node依赖提示ERR_PNPM_REGISTRIES_MISMATCH 5. bug修复
`; `;
+3
View File
@@ -7,6 +7,9 @@
"./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,