修改任务状态更新失败提示,重复运行提示

This commit is contained in:
whyour 2025-01-12 00:19:14 +08:00
parent e5b35273f9
commit 51ef4e7476
11 changed files with 115 additions and 88 deletions

View File

@ -2,7 +2,7 @@ import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi'; import { Container } from 'typedi';
import { Logger } from 'winston'; import { Logger } from 'winston';
import config from '../config'; import config from '../config';
import { getFileContentByName, readDirs, rmPath } from '../config/util'; import { getFileContentByName, readDirs, removeAnsi, rmPath } from '../config/util';
import { join, resolve } from 'path'; import { join, resolve } from 'path';
import { celebrate, Joi } from 'celebrate'; import { celebrate, Joi } from 'celebrate';
const route = Router(); const route = Router();
@ -42,7 +42,7 @@ export default (app: Router) => {
return res.send({ code: 403, message: '暂无权限' }); return res.send({ code: 403, message: '暂无权限' });
} }
const content = await getFileContentByName(finalPath); const content = await getFileContentByName(finalPath);
res.send({ code: 200, data: content }); res.send({ code: 200, data: removeAnsi(content) });
} catch (e) { } catch (e) {
return next(e); return next(e);
} }

View File

@ -51,7 +51,6 @@ const extraFile = path.join(configPath, 'extra.sh');
const confBakDir = path.join(dataPath, 'config/bak/'); const confBakDir = path.join(dataPath, 'config/bak/');
const sampleFile = path.join(samplePath, 'config.sample.sh'); const sampleFile = path.join(samplePath, 'config.sample.sh');
const sqliteFile = path.join(samplePath, 'database.sqlite'); const sqliteFile = path.join(samplePath, 'database.sqlite');
const systemNotifyFile = path.join(preloadPath, 'system-notify.json');
const authError = '错误的用户名密码,请重试'; const authError = '错误的用户名密码,请重试';
const loginFaild = '请先登录!'; const loginFaild = '请先登录!';
@ -135,5 +134,4 @@ export default {
sqliteFile, sqliteFile,
sshdPath, sshdPath,
systemLogPath, systemLogPath,
systemNotifyFile,
}; };

View File

@ -22,6 +22,10 @@ export async function getFileContentByName(fileName: string) {
return ''; return '';
} }
export function removeAnsi(text: string) {
return text.replace(/\x1b\[\d+m/g, '');
}
export async function getLastModifyFilePath(dir: string) { export async function getLastModifyFilePath(dir: string) {
let filePath = ''; let filePath = '';
@ -153,7 +157,12 @@ export function getPlatform(userAgent: string): 'mobile' | 'desktop' {
let platform = 'desktop'; let platform = 'desktop';
if (system === 'windows' || system === 'macos' || system === 'linux') { if (system === 'windows' || system === 'macos' || system === 'linux') {
platform = 'desktop'; platform = 'desktop';
} else if (system === 'android' || system === 'ios' || system === 'openharmony' || testUa(/mobile/g)) { } else if (
system === 'android' ||
system === 'ios' ||
system === 'openharmony' ||
testUa(/mobile/g)
) {
platform = 'mobile'; platform = 'mobile';
} }
@ -233,14 +242,14 @@ interface IFile {
key: string; key: string;
type: 'directory' | 'file'; type: 'directory' | 'file';
parent: string; parent: string;
mtime: number; createTime: number;
size?: number; size?: number;
children?: IFile[]; children?: IFile[];
} }
export function dirSort(a: IFile, b: IFile): number { export function dirSort(a: IFile, b: IFile): number {
if (a.type === 'file' && b.type === 'file') { if (a.type === 'file' && b.type === 'file') {
return b.mtime - a.mtime; return b.createTime - a.createTime;
} else if (a.type === 'directory' && b.type === 'directory') { } else if (a.type === 'directory' && b.type === 'directory') {
return a.title.localeCompare(b.title); return a.title.localeCompare(b.title);
} else { } else {
@ -274,7 +283,7 @@ export async function readDirs(
key, key,
type: 'directory', type: 'directory',
parent: relativePath, parent: relativePath,
mtime: stats.mtime.getTime(), createTime: stats.birthtime.getTime(),
children: children.sort(sort), children: children.sort(sort),
}); });
} else { } else {
@ -284,7 +293,7 @@ export async function readDirs(
key, key,
parent: relativePath, parent: relativePath,
size: stats.size, size: stats.size,
mtime: stats.mtime.getTime(), createTime: stats.birthtime.getTime(),
}); });
} }
} }

View File

@ -44,7 +44,7 @@ export default async () => {
const [systemConfig] = await SystemModel.findOrCreate({ const [systemConfig] = await SystemModel.findOrCreate({
where: { type: AuthDataType.systemConfig }, where: { type: AuthDataType.systemConfig },
}); });
const [notifyConfig] = await SystemModel.findOrCreate({ await SystemModel.findOrCreate({
where: { type: AuthDataType.notification }, where: { type: AuthDataType.notification },
}); });
const [authConfig] = await SystemModel.findOrCreate({ const [authConfig] = await SystemModel.findOrCreate({
@ -68,11 +68,6 @@ export default async () => {
}); });
} }
// 初始化通知配置
if (notifyConfig.info) {
await writeFile(config.systemNotifyFile, JSON.stringify(notifyConfig.info));
}
const installDependencies = () => { const installDependencies = () => {
// 初始化时安装所有处于安装中,安装成功,安装失败的依赖 // 初始化时安装所有处于安装中,安装成功,安装失败的依赖
DependenceModel.findAll({ DependenceModel.findAll({

View File

@ -3,12 +3,9 @@ import got from 'got';
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
import nodemailer from 'nodemailer'; import nodemailer from 'nodemailer';
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import winston from 'winston'; import { parseBody, parseHeaders } from '../config/util';
import { parseBody, parseHeaders, safeJSONParse } from '../config/util';
import { NotificationInfo } from '../data/notify'; import { NotificationInfo } from '../data/notify';
import UserService from './user'; import UserService from './user';
import { readFile } from 'fs/promises';
import config from '../config';
@Service() @Service()
export default class NotificationService { export default class NotificationService {
@ -49,27 +46,6 @@ export default class NotificationService {
constructor() {} constructor() {}
public async externalNotify(
title: string,
content: string,
): Promise<boolean | undefined> {
const { type, ...rest } = safeJSONParse(
await readFile(config.systemNotifyFile, 'utf-8'),
);
if (type) {
this.title = title;
this.content = content;
this.params = rest;
const notificationModeAction = this.modeMap.get(type);
try {
return await notificationModeAction?.call(this);
} catch (error: any) {
throw error;
}
}
return false;
}
public async notify( public async notify(
title: string, title: string,
content: string, content: string,
@ -154,15 +130,18 @@ export default class NotificationService {
private async serverChan() { private async serverChan() {
const { serverChanKey } = this.params; const { serverChanKey } = this.params;
const matchResult = serverChanKey.match(/^sctp(\d+)t/i); const matchResult = serverChanKey.match(/^sctp(\d+)t/i);
const url = matchResult && matchResult[1] const url =
? `https://${matchResult[1]}.push.ft07.com/send/${serverChanKey}.send` matchResult && matchResult[1]
: `https://sctapi.ftqq.com/${serverChanKey}.send`; ? `https://${matchResult[1]}.push.ft07.com/send/${serverChanKey}.send`
: `https://sctapi.ftqq.com/${serverChanKey}.send`;
try { try {
const res: any = await got const res: any = await got
.post(url, { .post(url, {
...this.gotOption, ...this.gotOption,
body: `title=${encodeURIComponent(this.title)}&desp=${encodeURIComponent(this.content)}`, body: `title=${encodeURIComponent(
this.title,
)}&desp=${encodeURIComponent(this.content)}`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
}) })
.json(); .json();
@ -520,10 +499,18 @@ export default class NotificationService {
} catch (error: any) { } catch (error: any) {
throw new Error(error.response ? error.response.body : error); throw new Error(error.response ? error.response.body : error);
} }
} }
private async pushPlus() { private async pushPlus() {
const { pushPlusToken, pushPlusUser, pushplusWebhook, pushPlusTemplate, pushplusChannel, pushplusCallbackUrl, pushplusTo} = this.params; const {
pushPlusToken,
pushPlusUser,
pushplusWebhook,
pushPlusTemplate,
pushplusChannel,
pushplusCallbackUrl,
pushplusTo,
} = this.params;
const url = `https://www.pushplus.plus/send`; const url = `https://www.pushplus.plus/send`;
try { try {
let body = { let body = {
@ -537,13 +524,11 @@ export default class NotificationService {
channel: `${pushplusChannel || 'wechat'}`, channel: `${pushplusChannel || 'wechat'}`,
webhook: `${pushplusWebhook || ''}`, webhook: `${pushplusWebhook || ''}`,
callbackUrl: `${pushplusCallbackUrl || ''}`, callbackUrl: `${pushplusCallbackUrl || ''}`,
to: `${pushplusTo || ''}` to: `${pushplusTo || ''}`,
}, },
} };
const res: any = await got const res: any = await got.post(url, body).json();
.post(url, body)
.json();
if (res.code === 200) { if (res.code === 200) {
return true; return true;
@ -678,37 +663,46 @@ export default class NotificationService {
const encodeRfc2047 = (text: string, charset: string = 'UTF-8'): string => { const encodeRfc2047 = (text: string, charset: string = 'UTF-8'): string => {
const encodedText = Buffer.from(text).toString('base64'); const encodedText = Buffer.from(text).toString('base64');
return `=?${charset}?B?${encodedText}?=`; return `=?${charset}?B?${encodedText}?=`;
}; };
try { try {
const encodedTitle = encodeRfc2047(this.title); const encodedTitle = encodeRfc2047(this.title);
const res: any = await got const res: any = await got.post(
.post(`${ntfyUrl || 'https://ntfy.sh'}/${ntfyTopic}`, { `${ntfyUrl || 'https://ntfy.sh'}/${ntfyTopic}`,
...this.gotOption, {
body: `${this.content}`, ...this.gotOption,
headers: { 'Title': encodedTitle, 'Priority': `${ntfyPriority || '3'}` }, body: `${this.content}`,
}); headers: { Title: encodedTitle, Priority: `${ntfyPriority || '3'}` },
if (res.statusCode === 200) { },
return true; );
} else { if (res.statusCode === 200) {
throw new Error(JSON.stringify(res)); return true;
} } else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) { } catch (error: any) {
throw new Error(error.response ? error.response.body : error); throw new Error(error.response ? error.response.body : error);
} }
} }
private async wxPusherBot() { private async wxPusherBot() {
const { wxPusherBotAppToken, wxPusherBotTopicIds, wxPusherBotUids } = this.params; const { wxPusherBotAppToken, wxPusherBotTopicIds, wxPusherBotUids } =
this.params;
// 处理 topicIds将分号分隔的字符串转为数组 // 处理 topicIds将分号分隔的字符串转为数组
const topicIds = wxPusherBotTopicIds ? wxPusherBotTopicIds.split(';') const topicIds = wxPusherBotTopicIds
.map(id => id.trim()) ? wxPusherBotTopicIds
.filter(id => id) .split(';')
.map(id => parseInt(id)) : []; .map((id) => id.trim())
.filter((id) => id)
.map((id) => parseInt(id))
: [];
// 处理 uids将分号分隔的字符串转为数组 // 处理 uids将分号分隔的字符串转为数组
const uids = wxPusherBotUids ? wxPusherBotUids.split(';') const uids = wxPusherBotUids
.map(uid => uid.trim()) ? wxPusherBotUids
.filter(uid => uid) : []; .split(';')
.map((uid) => uid.trim())
.filter((uid) => uid)
: [];
// topic_ids 和 uids 至少要有一个 // topic_ids 和 uids 至少要有一个
if (!topicIds.length && !uids.length) { if (!topicIds.length && !uids.length) {
@ -727,7 +721,7 @@ export default class NotificationService {
contentType: 2, contentType: 2,
topicIds: topicIds, topicIds: topicIds,
uids: uids, uids: uids,
verifyPayType: 0 verifyPayType: 0,
}, },
}) })
.json(); .json();
@ -742,7 +736,6 @@ export default class NotificationService {
} }
} }
private async chronocat() { private async chronocat() {
const { chronocatURL, chronocatQQ, chronocatToken } = this.params; const { chronocatURL, chronocatQQ, chronocatToken } = this.params;
try { try {

View File

@ -440,7 +440,7 @@ export default class SystemService {
const logs = result const logs = result
.reverse() .reverse()
.filter((x) => x.title.endsWith('.log')) .filter((x) => x.title.endsWith('.log'))
.filter((x) => x.mtime >= startTime && x.mtime <= endTime); .filter((x) => x.createTime >= startTime && x.createTime <= endTime);
res.set({ res.set({
'Content-Length': sum(logs.map((x) => x.size)), 'Content-Length': sum(logs.map((x) => x.size)),

View File

@ -11,6 +11,9 @@ import {
IScheduleFn, IScheduleFn,
TCron, TCron,
} from './interface'; } from './interface';
import config from '../config';
import { credentials } from '@grpc/grpc-js';
import { ApiClient } from '../protos/api';
class TaskLimit { class TaskLimit {
private dependenyLimit = new PQueue({ concurrency: 1 }); private dependenyLimit = new PQueue({ concurrency: 1 });
@ -33,6 +36,11 @@ class TaskLimit {
private systemLimit = new PQueue({ private systemLimit = new PQueue({
concurrency: Math.max(os.cpus().length, 4), concurrency: Math.max(os.cpus().length, 4),
}); });
private client = new ApiClient(
`0.0.0.0:${config.cronPort}`,
credentials.createInsecure(),
{ 'grpc.enable_http_proxy': 0 },
);
get cronLimitActiveCount() { get cronLimitActiveCount() {
return this.cronLimit.pending; return this.cronLimit.pending;
@ -126,9 +134,18 @@ class TaskLimit {
if (result?.length > 5) { if (result?.length > 5) {
if (repeatTimes < 3) { if (repeatTimes < 3) {
this.repeatCronNotifyMap.set(cron.id, repeatTimes + 1); this.repeatCronNotifyMap.set(cron.id, repeatTimes + 1);
this.notificationService.externalNotify( this.client.systemNotify(
'任务重复运行', {
`任务:${cron.name},命令:${cron.command},定时:${cron.schedule},处于运行中的超过 5 个,请检查定时设置`, title: '任务重复运行',
content: `任务:${cron.name},命令:${cron.command},定时:${cron.schedule},处于运行中的超过 5 个,请检查定时设置`,
},
(err, res) => {
if (err) {
Logger.error(
`[schedule][任务重复运行] 通知失败 ${JSON.stringify(err)}`,
);
}
},
); );
} }
Logger.warn(`[schedule][任务重复运行] 参数 ${JSON.stringify(cron)}`); Logger.warn(`[schedule][任务重复运行] 参数 ${JSON.stringify(cron)}`);

View File

@ -178,7 +178,10 @@ 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" if [[ ! $message ]]; then
message="$api"
fi
echo -e "${message}"
fi fi
} }

View File

@ -1,8 +1,8 @@
const grpc = require('@grpc/grpc-js'); const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader'); const protoLoader = require('@grpc/proto-loader');
const path = require('path');
const PROTO_PATH = path.resolve(__dirname, '../../back/protos/api.proto'); console.log(process.env.QL_DIR);
const PROTO_PATH = `${process.env.QL_DIR}/back/protos/api.proto`;
const options = { const options = {
keepCase: true, keepCase: true,
longs: String, longs: String,

View File

@ -438,8 +438,14 @@ clear_env() {
} }
handle_task_start() { handle_task_start() {
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp" local error_message=""
echo -e "## 开始执行... $begin_time\n" if [[ $ID ]]; then
local error=$(update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp")
if [[ $error ]]; then
error_message=", 任务状态更新失败(${error})"
fi
fi
echo -e "## 开始执行... ${begin_time}${error_message}\n"
} }
run_task_before() { run_task_before() {
@ -472,8 +478,13 @@ handle_task_end() {
[[ "$diff_time" == 0 ]] && diff_time=1 [[ "$diff_time" == 0 ]] && diff_time=1
echo -e "\n## 执行结束$suffix... $end_time 耗时 $diff_time 秒     " if [[ $ID ]]; then
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time" local error=$(update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time")
if [[ $error ]]; then
error_message=", 任务状态更新失败(${error})"
fi
fi
echo -e "\n## 执行结束$suffix... $end_time 耗时 $diff_time${error_message}     "
} }
init_env init_env

View File

@ -489,7 +489,6 @@ main() {
local time_format="%Y-%m-%d %H:%M:%S" local time_format="%Y-%m-%d %H:%M:%S"
local time=$(date "+$time_format") local time=$(date "+$time_format")
local begin_timestamp=$(format_timestamp "$time_format" "$time") local begin_timestamp=$(format_timestamp "$time_format" "$time")
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
local begin_time=$(format_time "$time_format" "$time") local begin_time=$(format_time "$time_format" "$time")
@ -497,6 +496,8 @@ main() {
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
fi fi
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
case $p1 in case $p1 in
update) update)
fix_config fix_config