Compare commits

...
26 Commits
Author SHA1 Message Date
whyour bac306f9b3 更新版本 v2.15.5 2023-01-04 21:09:52 +08:00
whyour a6fd96e97b 修复定时规则修改 2023-01-04 21:09:27 +08:00
whyour 7edd91f923 修改服务异常判断逻辑 2023-01-04 19:27:15 +08:00
whyour 66df445bd5 修改添加任务命令提示 2023-01-04 17:28:26 +08:00
whyour 5c03034bb4 修复定时任务不以task开头时,任务无效 2023-01-04 17:25:30 +08:00
whyour 0eab181d46 修改update逻辑 2022-12-29 12:15:25 +08:00
whyour f6f95308c7 monaco editor改为本地加载 2022-12-28 22:12:37 +08:00
whyour e93cad91b4 更新版本 v2.15.4 2022-12-28 11:15:20 +08:00
whyour 08ee61b179 修改 pub 脚本 2022-12-28 11:14:57 +08:00
whyour 0ab756665e 修改版本文件 2022-12-28 11:06:47 +08:00
whyour 3570cddce0 关于增加更新日志查看 2022-12-27 17:10:21 +08:00
whyour a2d9f1a5db 脚本管理支持重命名文件/文件夹 2022-12-27 16:02:15 +08:00
whyour 0c6a214e55 修复启动public服务命令 2022-12-27 12:24:24 +08:00
whyour 095fcdbe69 修复任务脚本快捷跳转 2022-12-17 12:46:26 +08:00
whyour 3afb1d18f6 更新版本 v2.15.3 2022-12-10 19:06:39 +08:00
whyour 4526c84330 任务视图支持标签筛选 2022-12-10 19:00:03 +08:00
whyour a9cc1cb4b9 修改token获取逻辑 2022-12-10 17:12:45 +08:00
whyour 7acf1eace3 修改添加任务视图筛选条件交互 2022-12-09 23:34:23 +08:00
whyour bb2e8cb287 修复调试保存文件默认目录和任务视图创建 2022-12-09 22:59:38 +08:00
whyour 3b389259c1 修复调试脚本日志 2022-12-06 00:22:05 +08:00
whyour 310ce55b09 修改默认基础镜像 2022-12-05 23:49:33 +08:00
CoolBoyandwhyour 4662e3fec2 修复notify.js中智能微秘书 (#1736) 2022-12-05 23:44:34 +08:00
whyour 10fab9d415 修改进程默认退出信号 2022-12-05 21:00:14 +08:00
whyour acecf01cbe 修复 killTask 返回值 2022-12-05 15:38:21 +08:00
whyour b95fb9cda4 修改退出进程逻辑 2022-12-05 15:26:22 +08:00
whyour 23fd595582 修改 timeout 命令默认信号 2022-12-05 13:18:10 +08:00
46 changed files with 3892 additions and 2274 deletions
+31 -4
View File
@@ -170,7 +170,7 @@ export default (app: Router) => {
body: Joi.object({ body: Joi.object({
filename: Joi.string().required(), filename: Joi.string().required(),
path: Joi.string().allow(''), path: Joi.string().allow(''),
type: Joi.string().optional() type: Joi.string().optional(),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
@@ -255,23 +255,50 @@ export default (app: Router) => {
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
filename: Joi.string().required(), filename: Joi.string().required(),
content: Joi.string().optional().allow(''),
path: Joi.string().optional().allow(''), path: Joi.string().optional().allow(''),
pid: Joi.number().optional().allow(''),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
let { filename, content, path } = req.body; let { filename, path, pid } = req.body;
const { name, ext } = parse(filename); const { name, ext } = parse(filename);
const filePath = join(config.scriptPath, path, `${name}.swap${ext}`); const filePath = join(config.scriptPath, path, `${name}.swap${ext}`);
const scriptService = Container.get(ScriptService); const scriptService = Container.get(ScriptService);
const result = await scriptService.stopScript(filePath); const result = await scriptService.stopScript(filePath, pid);
res.send(result); res.send(result);
} catch (e) { } catch (e) {
return next(e); return next(e);
} }
}, },
); );
route.put(
'/rename',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
path: Joi.string().allow(''),
newFilename: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
let { filename, path, type, newFilename } = req.body as {
filename: string;
path: string;
type: string;
newFilename: string;
};
const filePath = join(config.scriptPath, path, filename);
const newPath = join(config.scriptPath, path, newFilename);
fs.renameSync(filePath, newPath);
res.send({ code: 200 });
} catch (e) {
return next(e);
}
},
);
}; };
+6 -5
View File
@@ -7,7 +7,7 @@ import SystemService from '../services/system';
import { celebrate, Joi } from 'celebrate'; import { celebrate, Joi } from 'celebrate';
import UserService from '../services/user'; import UserService from '../services/user';
import { EnvModel } from '../data/env'; import { EnvModel } from '../data/env';
import { promiseExec } from '../config/util'; import { parseVersion, promiseExec } from '../config/util';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
const route = Router(); const route = Router();
@@ -21,10 +21,9 @@ export default (app: Router) => {
const userService = Container.get(UserService); const userService = Container.get(UserService);
const authInfo = await userService.getUserInfo(); const authInfo = await userService.getUserInfo();
const envCount = await EnvModel.count(); const envCount = await EnvModel.count();
const versionRegx = /.*export const version = \'(.*)\'\;/; const { version, changeLog, changeLogLink } = await parseVersion(
config.versionFile,
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8'); );
const version = currentVersionFile.match(versionRegx)![1];
const lastCommitTime = ( const lastCommitTime = (
await promiseExec( await promiseExec(
`cd ${config.rootPath} && git show -s --format=%ai | head -1`, `cd ${config.rootPath} && git show -s --format=%ai | head -1`,
@@ -56,6 +55,8 @@ export default (app: Router) => {
lastCommitTime: dayjs(lastCommitTime).unix(), lastCommitTime: dayjs(lastCommitTime).unix(),
lastCommitId, lastCommitId,
branch, branch,
changeLog,
changeLogLink,
}, },
}); });
} catch (e) { } catch (e) {
+6
View File
@@ -1 +1,7 @@
export const LOG_END_SYMBOL = '\n          '; export const LOG_END_SYMBOL = '\n          ';
export const TASK_COMMAND = 'task';
export const QL_COMMAND = 'ql';
export const TASK_PREFIX = `${TASK_COMMAND} `;
export const QL_PREFIX = `${QL_COMMAND} `;
+2 -2
View File
@@ -14,7 +14,7 @@ if (!process.env.QL_DIR) {
process.env.QL_DIR = qlHomePath.replace(/\/$/g, ''); process.env.QL_DIR = qlHomePath.replace(/\/$/g, '');
} }
const lastVersionFile = `https://qn.whyour.cn/version.ts`; const lastVersionFile = `https://qn.whyour.cn/version.yaml`;
const rootPath = process.env.QL_DIR as string; const rootPath = process.env.QL_DIR as string;
const envFound = dotenv.config({ path: path.join(rootPath, '.env') }); const envFound = dotenv.config({ path: path.join(rootPath, '.env') });
@@ -40,7 +40,7 @@ const sqliteFile = path.join(samplePath, 'database.sqlite');
const authError = '错误的用户名密码,请重试'; const authError = '错误的用户名密码,请重试';
const loginFaild = '请先登录!'; const loginFaild = '请先登录!';
const configString = 'config sample crontab shareCode diy'; const configString = 'config sample crontab shareCode diy';
const versionFile = path.join(rootPath, 'src/version.ts'); const versionFile = path.join(rootPath, 'version.yaml');
if (envFound.error) { if (envFound.error) {
throw new Error("⚠️ Couldn't find .env file ⚠️"); throw new Error("⚠️ Couldn't find .env file ⚠️");
+49 -4
View File
@@ -4,6 +4,9 @@ import got from 'got';
import iconv from 'iconv-lite'; import iconv from 'iconv-lite';
import { exec } from 'child_process'; import { exec } from 'child_process';
import FormData from 'form-data'; import FormData from 'form-data';
import psTreeFun from 'pstree.remy';
import { promisify } from 'util';
import { load } from 'js-yaml';
export function getFileContentByName(fileName: string) { export function getFileContentByName(fileName: string) {
if (fs.existsSync(fileName)) { if (fs.existsSync(fileName)) {
@@ -287,15 +290,15 @@ enum FileType {
interface IFile { interface IFile {
title: string; title: string;
key: string; key: string;
type: 'directory' | 'file', type: 'directory' | 'file';
parent: string; parent: string;
mtime: number; mtime: number;
children?: IFile[], children?: IFile[];
} }
export function dirSort(a: IFile, b: IFile) { export function dirSort(a: IFile, b: IFile) {
if (a.type !== b.type) return FileType[a.type] < FileType[b.type] ? -1 : 1 if (a.type !== b.type) return FileType[a.type] < FileType[b.type] ? -1 : 1;
else if (a.mtime !== b.mtime) return a.mtime > b.mtime ? -1 : 1 else if (a.mtime !== b.mtime) return a.mtime > b.mtime ? -1 : 1;
} }
export function readDirs( export function readDirs(
@@ -452,3 +455,45 @@ export function parseBody(
return parsed; return parsed;
} }
export function psTree(pid: number): Promise<number[]> {
return new Promise((resolve, reject) => {
psTreeFun(pid, (err: any, pids: number[]) => {
if (err) {
reject(err);
}
resolve(pids.filter((x) => !isNaN(x)));
});
});
}
export async function killTask(pid: number) {
const pids = await psTree(pid);
// SIGALRM 14 时钟信号
if (pids.length) {
process.kill(pids[0], 14);
} else {
process.kill(pid, 14);
}
}
export async function getPid(name: string) {
let taskCommand = `ps -ef | grep "${name}" | grep -v grep | awk '{print $1}'`;
const execAsync = promisify(exec);
let pid = (await execAsync(taskCommand)).stdout;
return Number(pid);
}
interface IVersion {
version: string;
changeLogLink: string;
changeLog: string;
}
export async function parseVersion(path: string): Promise<IVersion> {
return load(await promisify(fs.readFile)(path, 'utf8')) as IVersion;
}
export async function parseContentVersion(content: string): Promise<IVersion> {
return load(content) as IVersion;
}
+4 -6
View File
@@ -4,12 +4,10 @@ import * as Tracing from '@sentry/tracing';
import Logger from './logger'; import Logger from './logger';
import config from '../config'; import config from '../config';
import fs from 'fs'; import fs from 'fs';
import { parseVersion } from '../config/util';
export default ({ expressApp }: { expressApp: Application }) => { export default async ({ expressApp }: { expressApp: Application }) => {
const versionRegx = /.*export const version = \'(.*)\'\;/; const { version } = await parseVersion(config.versionFile);
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
const currentVersion = currentVersionFile.match(versionRegx)![1];
Sentry.init({ Sentry.init({
dsn: 'https://f4b5b55fb3c645b29a5dc2d70a1a4ef4@o1098464.ingest.sentry.io/6122819', dsn: 'https://f4b5b55fb3c645b29a5dc2d70a1a4ef4@o1098464.ingest.sentry.io/6122819',
@@ -18,7 +16,7 @@ export default ({ expressApp }: { expressApp: Application }) => {
new Tracing.Integrations.Express({ app: expressApp }), new Tracing.Integrations.Express({ app: expressApp }),
], ],
tracesSampleRate: 0.1, tracesSampleRate: 0.1,
release: currentVersion, release: version,
}); });
expressApp.use(Sentry.Handlers.requestHandler()); expressApp.use(Sentry.Handlers.requestHandler());
+2 -5
View File
@@ -6,15 +6,12 @@ import config from './config';
const app = express(); const app = express();
app.get('/api/public/panel/log', (req, res) => { app.get('/api/public/panel/log', (req, res) => {
exec( exec('tail -n 300 ~/.pm2/logs/panel-error.log', (err, stdout, stderr) => {
'pm2 logs panel --lines 500 --nostream --timestamp',
(err, stdout, stderr) => {
if (err || stderr) { if (err || stderr) {
return res.send({ code: 400, message: (err && err.message) || stderr }); return res.send({ code: 400, message: (err && err.message) || stderr });
} }
return res.send({ code: 200, data: stdout }); return res.send({ code: 200, data: stdout });
}, });
);
}); });
app app
+6 -2
View File
@@ -4,6 +4,7 @@ import { exec } from 'child_process';
import Logger from './loaders/logger'; import Logger from './loaders/logger';
import { CrontabModel, CrontabStatus } from './data/cron'; import { CrontabModel, CrontabStatus } from './data/cron';
import config from './config'; import config from './config';
import { QL_PREFIX, TASK_PREFIX } from './config/const';
const app = express(); const app = express();
@@ -23,8 +24,11 @@ const run = async () => {
) { ) {
schedule.scheduleJob(task.schedule, function () { schedule.scheduleJob(task.schedule, function () {
let command = task.command as string; let command = task.command as string;
if (!command.includes('task ') && !command.includes('ql ')) { if (
command = `task ${command}`; !command.startsWith(TASK_PREFIX) &&
!command.startsWith(QL_PREFIX)
) {
command = `${TASK_PREFIX}${command}`;
} }
exec(`ID=${task.id} ${command}`); exec(`ID=${task.id} ${command}`);
}); });
+37 -72
View File
@@ -5,13 +5,16 @@ import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
import { exec, execSync, spawn } from 'child_process'; import { exec, execSync, spawn } from 'child_process';
import fs from 'fs'; import fs from 'fs';
import cron_parser from 'cron-parser'; import cron_parser from 'cron-parser';
import { getFileContentByName, concurrentRun, fileExist } from '../config/util'; import {
getFileContentByName,
concurrentRun,
fileExist,
killTask,
} from '../config/util';
import { promises, existsSync } from 'fs'; import { promises, existsSync } from 'fs';
import { promisify } from 'util'; import { Op, where, col as colFn } from 'sequelize';
import { Op } from 'sequelize';
import path from 'path'; import path from 'path';
import dayjs from 'dayjs'; import { TASK_PREFIX, QL_PREFIX } from '../config/const';
import { LOG_END_SYMBOL } from '../config/const';
@Service() @Service()
export default class CronService { export default class CronService {
@@ -29,7 +32,7 @@ export default class CronService {
const tab = new Crontab(payload); const tab = new Crontab(payload);
tab.saved = false; tab.saved = false;
const doc = await this.insert(tab); const doc = await this.insert(tab);
await this.set_crontab(this.isSixCron(doc)); await this.set_crontab();
return doc; return doc;
} }
@@ -40,7 +43,7 @@ export default class CronService {
public async update(payload: Crontab): Promise<Crontab> { public async update(payload: Crontab): Promise<Crontab> {
payload.saved = false; payload.saved = false;
const newDoc = await this.updateDb(payload); const newDoc = await this.updateDb(payload);
await this.set_crontab(this.isSixCron(newDoc)); await this.set_crontab();
return newDoc; return newDoc;
} }
@@ -79,7 +82,7 @@ export default class CronService {
public async remove(ids: number[]) { public async remove(ids: number[]) {
await CrontabModel.destroy({ where: { id: ids } }); await CrontabModel.destroy({ where: { id: ids } });
await this.set_crontab(true); await this.set_crontab();
} }
public async pin(ids: number[]) { public async pin(ids: number[]) {
@@ -161,10 +164,24 @@ export default class CronService {
} }
if (operate && operate2) { if (operate && operate2) {
q[property] = { q[property] = {
[Op.or]: [
{
[operate2]: [ [operate2]: [
{ [operate]: `%${value}%` }, { [operate]: `%${value}%` },
{ [operate]: `%${encodeURIComponent(value)}%` }, { [operate]: `%${encodeURIComponent(value)}%` },
], ],
},
{
[operate2]: [
where(colFn(property), operate, `%${value}%`),
where(
colFn(property),
operate,
`%${encodeURIComponent(value)}%`,
),
],
},
],
}; };
} }
query[primaryOperate].push(q); query[primaryOperate].push(q);
@@ -315,31 +332,11 @@ export default class CronService {
for (const doc of docs) { for (const doc of docs) {
if (doc.pid) { if (doc.pid) {
try { try {
process.kill(-doc.pid); await killTask(doc.pid);
} catch (error) { } catch (error) {
this.logger.silly(error); this.logger.silly(error);
} }
} }
const err = await this.killTask(doc.command);
const absolutePath = path.resolve(config.logPath, `${doc.log_path}`);
const logFileExist = doc.log_path && (await fileExist(absolutePath));
const endTime = dayjs();
const diffTimeStr = doc.last_execution_time
? ` 耗时 ${endTime.diff(
dayjs(doc.last_execution_time * 1000),
'second',
)}`
: '';
if (logFileExist) {
const str = err ? `\n${err}` : '';
fs.appendFileSync(
`${absolutePath}`,
`${str}\n## 执行结束... ${endTime.format(
'YYYY-MM-DD HH:mm:ss',
)}${diffTimeStr}${LOG_END_SYMBOL}`,
);
}
} }
await CrontabModel.update( await CrontabModel.update(
@@ -348,42 +345,6 @@ export default class CronService {
); );
} }
public async killTask(name: string) {
let taskCommand = `ps -ef | grep "${name}" | grep -v grep | awk '{print $1}'`;
const execAsync = promisify(exec);
try {
let pid = (await execAsync(taskCommand)).stdout;
if (pid) {
pid = (await execAsync(`pstree -p ${pid}`)).stdout;
} else {
return;
}
let pids = pid.match(/\(\d+/g);
const killLogs = [];
if (pids && pids.length > 0) {
// node 执行脚本时还会有10个子进程,但是ps -ef中不存在,所以截取前三个
pids = pids.slice(0, 3);
for (const id of pids) {
const c = `kill -9 ${id.slice(1)}`;
try {
const { stdout, stderr } = await execAsync(c);
if (stderr) {
killLogs.push(stderr);
}
if (stdout) {
killLogs.push(stdout);
}
} catch (error: any) {
killLogs.push(error.message);
}
}
}
return killLogs.length > 0 ? JSON.stringify(killLogs) : '';
} catch (e) {
return JSON.stringify(e);
}
}
private async runSingle(cronId: number): Promise<number> { private async runSingle(cronId: number): Promise<number> {
return new Promise(async (resolve: any) => { return new Promise(async (resolve: any) => {
const cron = await this.getDb({ id: cronId }); const cron = await this.getDb({ id: cronId });
@@ -401,8 +362,8 @@ export default class CronService {
this.logger.silly('Original command: ' + command); this.logger.silly('Original command: ' + command);
let cmdStr = command; let cmdStr = command;
if (!cmdStr.includes('task ') && !cmdStr.includes('ql ')) { if (!cmdStr.startsWith(TASK_PREFIX) && !cmdStr.startsWith(QL_PREFIX)) {
cmdStr = `task ${cmdStr}`; cmdStr = `${TASK_PREFIX}${cmdStr}`;
} }
if ( if (
cmdStr.endsWith('.js') || cmdStr.endsWith('.js') ||
@@ -448,12 +409,12 @@ export default class CronService {
public async disabled(ids: number[]) { public async disabled(ids: number[]) {
await CrontabModel.update({ isDisabled: 1 }, { where: { id: ids } }); await CrontabModel.update({ isDisabled: 1 }, { where: { id: ids } });
await this.set_crontab(true); await this.set_crontab();
} }
public async enabled(ids: number[]) { public async enabled(ids: number[]) {
await CrontabModel.update({ isDisabled: 0 }, { where: { id: ids } }); await CrontabModel.update({ isDisabled: 0 }, { where: { id: ids } });
await this.set_crontab(true); await this.set_crontab();
} }
public async log(id: number) { public async log(id: number) {
@@ -559,11 +520,17 @@ export default class CronService {
} }
private make_command(tab: Crontab) { private make_command(tab: Crontab) {
if (
!tab.command.startsWith(TASK_PREFIX) &&
!tab.command.startsWith(QL_PREFIX)
) {
tab.command = `${TASK_PREFIX}${tab.command}`;
}
const crontab_job_string = `ID=${tab.id} ${tab.command}`; const crontab_job_string = `ID=${tab.id} ${tab.command}`;
return crontab_job_string; return crontab_job_string;
} }
private async set_crontab(needReloadSchedule: boolean = false) { private async set_crontab() {
const tabs = await this.crontabs(); const tabs = await this.crontabs();
var crontab_string = ''; var crontab_string = '';
tabs.data.forEach((tab) => { tabs.data.forEach((tab) => {
@@ -586,9 +553,7 @@ export default class CronService {
fs.writeFileSync(config.crontabFile, crontab_string); fs.writeFileSync(config.crontabFile, crontab_string);
execSync(`crontab ${config.crontabFile}`); execSync(`crontab ${config.crontabFile}`);
if (needReloadSchedule) {
exec(`pm2 reload schedule`); exec(`pm2 reload schedule`);
}
await CrontabModel.update({ saved: true }, { where: {} }); await CrontabModel.update({ saved: true }, { where: {} });
} }
+6 -2
View File
@@ -42,7 +42,11 @@ export default class ScheduleService {
constructor(@Inject('logger') private logger: winston.Logger) {} constructor(@Inject('logger') private logger: winston.Logger) {}
async runTask(command: string, callbacks: TaskCallbacks = {}) { async runTask(
command: string,
callbacks: TaskCallbacks = {},
completionTime: 'start' | 'end' = 'end',
) {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
try { try {
const startTime = dayjs(); const startTime = dayjs();
@@ -52,6 +56,7 @@ export default class ScheduleService {
// TODO: // TODO:
callbacks.onStart?.(cp, startTime); callbacks.onStart?.(cp, startTime);
completionTime === 'start' && resolve(cp.pid);
cp.stdout.on('data', async (data) => { cp.stdout.on('data', async (data) => {
await callbacks.onLog?.(data.toString()); await callbacks.onLog?.(data.toString());
@@ -100,7 +105,6 @@ export default class ScheduleService {
error, error,
); );
await callbacks.onError?.(JSON.stringify(error)); await callbacks.onError?.(JSON.stringify(error));
resolve(null);
} }
}); });
} }
+17 -14
View File
@@ -6,7 +6,8 @@ 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'; import { TASK_COMMAND } from '../config/const';
import { getPid, killTask } from '../config/util';
@Service() @Service()
export default class ScriptService { export default class ScriptService {
@@ -41,23 +42,25 @@ export default class ScriptService {
public async runScript(filePath: string) { public async runScript(filePath: string) {
const relativePath = path.relative(config.scriptPath, filePath); const relativePath = path.relative(config.scriptPath, filePath);
const command = `task -l ${relativePath} now`; const command = `${TASK_COMMAND} -l ${relativePath} now`;
this.scheduleService.runTask(command, this.taskCallbacks(filePath)); const pid = await this.scheduleService.runTask(
command,
this.taskCallbacks(filePath),
'start',
);
return { code: 200 }; return { code: 200, data: pid };
} }
public async stopScript(filePath: string) { public async stopScript(filePath: string, pid: number) {
let str = '';
if (!pid) {
const relativePath = path.relative(config.scriptPath, filePath); const relativePath = path.relative(config.scriptPath, filePath);
const err = await this.cronService.killTask(`task -l ${relativePath} now`); pid = await getPid(`${TASK_COMMAND} -l ${relativePath} now`);
}
const str = err ? `\n${err}` : ''; try {
this.sockService.sendMessage({ await killTask(pid);
type: 'manuallyRunScript', } catch (error) {}
message: `${str}\n## 执行结束... ${new Date()
.toLocaleString('zh', { hour12: false })
.replace(' 24:', ' 00:')}${LOG_END_SYMBOL}`,
});
return { code: 200 }; return { code: 200 };
} }
+1 -1
View File
@@ -14,7 +14,7 @@ export default class SshKeyService {
private generatePrivateKeyFile(alias: string, key: string): void { private generatePrivateKeyFile(alias: string, key: string): void {
try { try {
fs.writeFileSync(`${this.sshPath}/${alias}`, key, { fs.writeFileSync(`${this.sshPath}/${alias}`, `${key}${os.EOL}`, {
encoding: 'utf8', encoding: 'utf8',
mode: '400', mode: '400',
}); });
+3 -41
View File
@@ -19,9 +19,9 @@ import {
concurrentRun, concurrentRun,
fileExist, fileExist,
createFile, createFile,
killTask,
} from '../config/util'; } from '../config/util';
import { promises, existsSync } from 'fs'; import { promises, existsSync } from 'fs';
import { promisify } from 'util';
import { Op } from 'sequelize'; import { Op } from 'sequelize';
import path from 'path'; import path from 'path';
import ScheduleService, { TaskCallbacks } from './schedule'; import ScheduleService, { TaskCallbacks } from './schedule';
@@ -351,19 +351,16 @@ export default class SubscriptionService {
for (const doc of docs) { for (const doc of docs) {
if (doc.pid) { if (doc.pid) {
try { try {
process.kill(-doc.pid); await killTask(doc.pid);
} catch (error) { } catch (error) {
this.logger.silly(error); this.logger.silly(error);
} }
} }
const command = this.formatCommand(doc);
const err = await this.killTask(command);
const absolutePath = await this.handleLogPath(doc.log_path as string); const absolutePath = await this.handleLogPath(doc.log_path as string);
const str = err ? `\n${err}` : '';
fs.appendFileSync( fs.appendFileSync(
`${absolutePath}`, `${absolutePath}`,
`${str}\n## 执行结束... ${dayjs().format( `\n## 执行结束... ${dayjs().format(
'YYYY-MM-DD HH:mm:ss', 'YYYY-MM-DD HH:mm:ss',
)}${LOG_END_SYMBOL}`, )}${LOG_END_SYMBOL}`,
); );
@@ -375,41 +372,6 @@ export default class SubscriptionService {
); );
} }
public async killTask(name: string) {
let taskCommand = `ps -ef | grep "${name}" | grep -v grep | awk '{print $1}'`;
const execAsync = promisify(exec);
try {
let pid = (await execAsync(taskCommand)).stdout;
if (pid) {
pid = (await execAsync(`pstree -p ${pid}`)).stdout;
} else {
return;
}
let pids = pid.match(/\(\d+/g);
const killLogs = [];
if (pids && pids.length > 0) {
// node 执行脚本时还会有10个子进程,但是ps -ef中不存在,所以截取前三个
for (const id of pids) {
const c = `kill -9 ${id.slice(1)}`;
try {
const { stdout, stderr } = await execAsync(c);
if (stderr) {
killLogs.push(stderr);
}
if (stdout) {
killLogs.push(stdout);
}
} catch (error: any) {
killLogs.push(error.message);
}
}
}
return killLogs.length > 0 ? JSON.stringify(killLogs) : '';
} catch (e) {
return JSON.stringify(e);
}
}
private async runSingle(subscriptionId: number) { private async runSingle(subscriptionId: number) {
const subscription = await this.getDb({ id: subscriptionId }); const subscription = await this.getDb({ id: subscriptionId });
if (subscription.status !== SubscriptionStatus.queued) { if (subscription.status !== SubscriptionStatus.queued) {
+15 -15
View File
@@ -9,6 +9,7 @@ import ScheduleService from './schedule';
import { spawn } from 'child_process'; import { spawn } from 'child_process';
import SockService from './sock'; import SockService from './sock';
import got from 'got'; import got from 'got';
import { parseContentVersion, parseVersion } from '../config/util';
@Service() @Service()
export default class SystemService { export default class SystemService {
@@ -78,14 +79,9 @@ export default class SystemService {
public async checkUpdate() { public async checkUpdate() {
try { try {
const versionRegx = /.*export const version = \'(.*)\'\;/; const currentVersionContent = await parseVersion(config.versionFile);
const logRegx = /.*export const changeLog = \`((.*\n.*)+)\`;/;
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8'); let lastVersionContent;
const currentVersion = currentVersionFile.match(versionRegx)![1];
let lastVersion = '';
let lastLog = '';
try { try {
const result = await got.get( const result = await got.get(
`${config.lastVersionFile}?t=${Date.now()}`, `${config.lastVersionFile}?t=${Date.now()}`,
@@ -93,19 +89,23 @@ export default class SystemService {
timeout: 30000, timeout: 30000,
}, },
); );
const lastVersionFileContent = result.body; lastVersionContent = await parseContentVersion(result.body);
lastVersion = lastVersionFileContent.match(versionRegx)![1];
lastLog = lastVersionFileContent.match(logRegx)
? lastVersionFileContent.match(logRegx)![1]
: '';
} catch (error) {} } catch (error) {}
if (!lastVersionContent) {
lastVersionContent = currentVersionContent;
}
return { return {
code: 200, code: 200,
data: { data: {
hasNewVersion: this.checkHasNewVersion(currentVersion, lastVersion), hasNewVersion: this.checkHasNewVersion(
lastVersion, currentVersionContent.version,
lastLog, lastVersionContent.version,
),
lastVersion: lastVersionContent.version,
lastLog: lastVersionContent.changeLog,
lastLogLink: lastVersionContent.changeLogLink,
}, },
}; };
} catch (error: any) { } catch (error: any) {
+8 -2
View File
@@ -5,6 +5,7 @@ import LoggerInstance from './loaders/logger';
import fs from 'fs'; import fs from 'fs';
import config from './config'; import config from './config';
import path from 'path'; import path from 'path';
import os from 'os';
const tokenFile = path.join(config.configPath, 'token.json'); const tokenFile = path.join(config.configPath, 'token.json');
@@ -25,9 +26,14 @@ async function getToken() {
async function writeFile(data: any) { async function writeFile(data: any) {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
fs.writeFile(tokenFile, JSON.stringify(data), { encoding: 'utf8' }, () => { fs.writeFile(
tokenFile,
`${JSON.stringify(data)}${os.EOL}`,
{ encoding: 'utf8' },
() => {
resolve(); resolve();
}); },
);
}); });
} }
+1 -1
View File
@@ -1,4 +1,4 @@
FROM python:alpine FROM python:3.10-alpine
ARG QL_MAINTAINER="whyour" ARG QL_MAINTAINER="whyour"
LABEL maintainer="${QL_MAINTAINER}" LABEL maintainer="${QL_MAINTAINER}"
+6 -2
View File
@@ -4,7 +4,7 @@
"start": "concurrently -n w: npm:start:*", "start": "concurrently -n w: npm:start:*",
"start:front": "max dev", "start:front": "max dev",
"start:back": "nodemon", "start:back": "nodemon",
"start:public": "ts-node back/public.ts", "start:public": "ts-node --transpile-only ./back/public.ts",
"build:front": "max build", "build:front": "max build",
"build:back": "tsc -p tsconfig.back.json", "build:back": "tsc -p tsconfig.back.json",
"panel": "npm run build:back && node static/build/app.js", "panel": "npm run build:back && node static/build/app.js",
@@ -69,12 +69,14 @@
"got": "^11.8.2", "got": "^11.8.2",
"hpagent": "^0.1.2", "hpagent": "^0.1.2",
"iconv-lite": "^0.6.3", "iconv-lite": "^0.6.3",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"multer": "^1.4.4", "multer": "^1.4.4",
"nedb": "^1.8.0", "nedb": "^1.8.0",
"node-schedule": "^2.1.0", "node-schedule": "^2.1.0",
"nodemailer": "^6.7.2", "nodemailer": "^6.7.2",
"pstree.remy": "^1.1.8",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"sequelize": "^6.25.5", "sequelize": "^6.25.5",
"serve-handler": "^6.1.3", "serve-handler": "^6.1.3",
@@ -89,13 +91,14 @@
"devDependencies": { "devDependencies": {
"@ant-design/icons": "^4.7.0", "@ant-design/icons": "^4.7.0",
"@ant-design/pro-layout": "^6.33.1", "@ant-design/pro-layout": "^6.33.1",
"@monaco-editor/react": "4.2.1", "@monaco-editor/react": "4.4.6",
"@react-hook/resize-observer": "^1.2.6", "@react-hook/resize-observer": "^1.2.6",
"@sentry/react": "^7.12.1", "@sentry/react": "^7.12.1",
"@types/body-parser": "^1.19.2", "@types/body-parser": "^1.19.2",
"@types/cors": "^2.8.12", "@types/cors": "^2.8.12",
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"@types/express-jwt": "^6.0.4", "@types/express-jwt": "^6.0.4",
"@types/js-yaml": "^4.0.5",
"@types/jsonwebtoken": "^8.5.8", "@types/jsonwebtoken": "^8.5.8",
"@types/lodash": "^4.14.185", "@types/lodash": "^4.14.185",
"@types/multer": "^1.4.7", "@types/multer": "^1.4.7",
@@ -119,6 +122,7 @@
"compression-webpack-plugin": "9.2.0", "compression-webpack-plugin": "9.2.0",
"concurrently": "^7.0.0", "concurrently": "^7.0.0",
"lint-staged": "^13.0.3", "lint-staged": "^13.0.3",
"monaco-editor": "^0.34.1",
"nodemon": "^2.0.15", "nodemon": "^2.0.15",
"prettier": "^2.5.1", "prettier": "^2.5.1",
"qiniu": "^7.4.0", "qiniu": "^7.4.0",
+3346 -1938
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -980,6 +980,8 @@ function aibotkNotify(text, desp) {
resolve(data); resolve(data);
} }
}); });
} else {
resolve();
} }
}); });
} }
+2 -2
View File
@@ -5,14 +5,14 @@ const envFound = dotenv.config();
const accessKey = process.env.QINIU_AK; const accessKey = process.env.QINIU_AK;
const secretKey = process.env.QINIU_SK; const secretKey = process.env.QINIU_SK;
const mac = new qiniu.auth.digest.Mac(accessKey, secretKey); const mac = new qiniu.auth.digest.Mac(accessKey, secretKey);
const key = 'version.ts'; const key = 'version.yaml';
const options = { const options = {
scope: `${process.env.QINIU_SCOPE}:${key}`, scope: `${process.env.QINIU_SCOPE}:${key}`,
}; };
const putPolicy = new qiniu.rs.PutPolicy(options); const putPolicy = new qiniu.rs.PutPolicy(options);
const uploadToken = putPolicy.uploadToken(mac); const uploadToken = putPolicy.uploadToken(mac);
const localFile = 'src/version.ts'; const localFile = 'version.yaml';
const config = new qiniu.conf.Config({ zone: qiniu.zone.Zone_z1 }); const config = new qiniu.conf.Config({ zone: qiniu.zone.Zone_z1 });
const formUploader = new qiniu.form_up.FormUploader(config); const formUploader = new qiniu.form_up.FormUploader(config);
const putExtra = new qiniu.form_up.PutExtra( const putExtra = new qiniu.form_up.PutExtra(
+9
View File
@@ -1,7 +1,16 @@
#!/usr/bin/env bash #!/usr/bin/env bash
get_token() { get_token() {
if [[ -f $file_auth_token ]]; then
token=$(cat $file_auth_token | jq -r .value) token=$(cat $file_auth_token | jq -r .value)
else
local token_command="ts-node-transpile-only ${dir_root}/back/token.ts"
local token_file="${dir_root}static/build/token.js"
if [[ -f $token_file ]]; then
token_command="node ${token_file}"
fi
token=$(eval "$token_command")
fi
} }
add_cron_api() { add_cron_api() {
+7 -1
View File
@@ -5,6 +5,12 @@ dir_shell=$QL_DIR/shell
. $dir_shell/share.sh . $dir_shell/share.sh
. $dir_shell/api.sh . $dir_shell/api.sh
trap "single_hanle" 2 20 15 14
single_hanle() {
handle_task_after "$@"
exit 1
}
random_delay() { random_delay() {
local random_delay_max=$RandomDelay local random_delay_max=$RandomDelay
if [[ $random_delay_max ]] && [[ $random_delay_max -gt 0 ]]; then if [[ $random_delay_max ]] && [[ $random_delay_max -gt 0 ]]; then
@@ -29,7 +35,7 @@ random_delay() {
done done
local delay_second=$(($(gen_random_num "$random_delay_max") + 1)) local delay_second=$(($(gen_random_num "$random_delay_max") + 1))
echo -e "\n命令未添加 \"now\",随机延迟 $delay_second 秒后执行任务,如需立即终止,请按 CTRL+C...\n" echo -e "\n命令未添加 \"now\",随机延迟 $delay_second 秒后执行\n"
sleep $delay_second sleep $delay_second
fi fi
} }
+1 -1
View File
@@ -13,7 +13,7 @@ git push
echo -e "更新cdn文件" echo -e "更新cdn文件"
ts-node sample/tool.ts ts-node sample/tool.ts
string=$(cat src/version.ts | grep "version" | egrep "[^\']*" -o | egrep "\d\.*") string=$(cat version.yaml | grep "version" | egrep "[^ ]*" -o | egrep "\d\.*")
version="v$string" version="v$string"
echo -e "当前版本$version" echo -e "当前版本$version"
+1 -1
View File
@@ -56,7 +56,7 @@ format_params() {
time_format="%Y-%m-%d %H:%M:%S" time_format="%Y-%m-%d %H:%M:%S"
timeoutCmd="" timeoutCmd=""
if type timeout &>/dev/null; then if type timeout &>/dev/null; then
timeoutCmd="timeout -k 10s $command_timeout_time " timeoutCmd="timeout --foreground -s 14 -k 10s $command_timeout_time "
fi fi
params=$(echo "$@" | sed -E 's/([^ ])&([^ ])/\1\\\&\2/g') params=$(echo "$@" | sed -E 's/([^ ])&([^ ])/\1\\\&\2/g')
} }
+8 -36
View File
@@ -26,33 +26,6 @@ diff_cron() {
fi fi
} }
## 检测配置文件版本
detect_config_version() {
## 识别出两个文件的版本号
ver_config_sample=$(grep " Version: " $file_config_sample | perl -pe "s|.+v((\d+\.?){3})|\1|")
[[ -f $file_config_user ]] && ver_config_user=$(grep " Version: " $file_config_user | perl -pe "s|.+v((\d+\.?){3})|\1|")
## 删除旧的发送记录文件
[[ -f $send_mark ]] && [[ $(cat $send_mark) != $ver_config_sample ]] && rm -f $send_mark
## 识别出更新日期和更新内容
update_date=$(grep " Date: " $file_config_sample | awk -F ": " '{print $2}')
update_content=$(grep " Update Content: " $file_config_sample | awk -F ": " '{print $2}')
## 如果是今天,并且版本号不一致,则发送通知
if [[ -f $file_config_user ]] && [[ $ver_config_user != $ver_config_sample ]] && [[ $update_date == $(date "+%Y-%m-%d") ]]; then
if [[ ! -f $send_mark ]]; then
local notify_title="配置文件更新通知"
local notify_content="更新日期: $update_date\n用户版本: $ver_config_user\n新的版本: $ver_config_sample\n更新内容: $update_content\n更新说明: 如需使用新功能请对照config.sample.sh,将相关新参数手动增加到你自己的config.sh中,否则请无视本消息。本消息只在该新版本配置文件更新当天发送一次。\n"
echo -e $notify_content
notify_api "$notify_title" "$notify_content"
[[ $? -eq 0 ]] && echo $ver_config_sample >$send_mark
fi
else
[[ -f $send_mark ]] && rm -f $send_mark
fi
}
## 输出是否有新的或失效的定时任务,$1:新的或失效的任务清单文件路径,$2:新/失效 ## 输出是否有新的或失效的定时任务,$1:新的或失效的任务清单文件路径,$2:新/失效
output_list_add_drop() { output_list_add_drop() {
local list=$1 local list=$1
@@ -188,7 +161,7 @@ update_raw() {
echo -e "下载 ${raw_file_name} 成功...\n" echo -e "下载 ${raw_file_name} 成功...\n"
cd $dir_raw cd $dir_raw
local filename="raw_${raw_file_name}" local filename="raw_${raw_file_name}"
local cron_id=$(cat $list_crontab_user | grep -E "$cmd_task $filename" | perl -pe "s|.*ID=(.*) $cmd_task $filename\.*|\1|" | head -1 | head -1 | awk -F " " '{print $1}') local cron_id=$(cat $list_crontab_user | grep -E "$cmd_task.* $filename" | perl -pe "s|.*ID=(.*) $cmd_task.* $filename\.*|\1|" | head -1 | head -1 | awk -F " " '{print $1}')
cp -f $raw_file_name $dir_scripts/${filename} cp -f $raw_file_name $dir_scripts/${filename}
cron_line=$( cron_line=$(
perl -ne "{ perl -ne "{
@@ -265,8 +238,8 @@ update_qinglong() {
if [[ $exit_status -eq 0 ]]; then if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新青龙源文件成功...\n" echo -e "\n更新青龙源文件成功...\n"
reset_romote_url ${dir_root} "https://${mirror}.com/whyour/qinglong.git" ${primary_branch}
cp -f $file_config_sample $dir_config/config.sample.sh cp -f $file_config_sample $dir_config/config.sample.sh
detect_config_version
update_depend update_depend
[[ -f $dir_root/package.json ]] && ql_depend_new=$(cat $dir_root/package.json) [[ -f $dir_root/package.json ]] && ql_depend_new=$(cat $dir_root/package.json)
@@ -291,8 +264,7 @@ update_qinglong_static() {
fi fi
if [[ $exit_status -eq 0 ]]; then if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新青龙静态资源成功...\n" echo -e "\n更新青龙静态资源成功...\n"
local static_version=$(cat $dir_root/src/version.ts | perl -pe "s|.*\'(.*)\';\.*|\1|" | head -1) reset_romote_url ${ql_static_repo} ${url} ${primary_branch}
echo -e "\n当前版本 $static_version...\n"
rm -rf $dir_static/* rm -rf $dir_static/*
cp -rf $ql_static_repo/* $dir_static cp -rf $ql_static_repo/* $dir_static
@@ -396,12 +368,12 @@ gen_list_repo() {
filename=$(basename $file) filename=$(basename $file)
cp -f $file "$dir_scripts/${uniq_path}/${filename}" cp -f $file "$dir_scripts/${uniq_path}/${filename}"
echo "${uniq_path}/${filename}" >>"$dir_list_tmp/${uniq_path}_scripts.list" echo "${uniq_path}/${filename}" >>"$dir_list_tmp/${uniq_path}_scripts.list"
cron_id=$(cat $list_crontab_user | grep -E "$cmd_task ${uniq_path}_${filename}" | perl -pe "s|.*ID=(.*) $cmd_task ${uniq_path}_${filename}\.*|\1|" | head -1 | awk -F " " '{print $1}') # cron_id=$(cat $list_crontab_user | grep -E "$cmd_task.* ${uniq_path}_${filename}" | perl -pe "s|.*ID=(.*) $cmd_task.* ${uniq_path}_${filename}\.*|\1|" | head -1 | awk -F " " '{print $1}')
if [[ $cron_id ]]; then # if [[ $cron_id ]]; then
result=$(update_cron_command_api "$cmd_task ${uniq_path}/${filename}:$cron_id") # result=$(update_cron_command_api "$cmd_task ${uniq_path}/${filename}:$cron_id")
fi # fi
done done
grep -E "${cmd_task} ${uniq_path}" ${list_crontab_user} | perl -pe "s|.*ID=(.*) ${cmd_task} (${uniq_path}.*)\.*|\2|" | awk -F " " '{print $1}' | sort -u >"$dir_list_tmp/${uniq_path}_user.list" grep -E "${cmd_task}.* ${uniq_path}" ${list_crontab_user} | perl -pe "s|.*ID=(.*) ${cmd_task}.* (${uniq_path}.*)\.*|\2|" | awk -F " " '{print $1}' | sort -u >"$dir_list_tmp/${uniq_path}_user.list"
cd $dir_current cd $dir_current
} }
+1 -1
View File
@@ -1,7 +1,7 @@
import { createFromIconfontCN } from '@ant-design/icons'; import { createFromIconfontCN } from '@ant-design/icons';
const IconFont = createFromIconfontCN({ const IconFont = createFromIconfontCN({
scriptUrl: ['//at.alicdn.com/t/c/font_3354854_z0d9rbri1ci.js'], scriptUrl: ['//at.alicdn.com/t/c/font_3354854_ob5y15ewlyq.js'],
}); });
export default IconFont; export default IconFont;
+1
View File
@@ -347,4 +347,5 @@ select:-webkit-autofill:focus {
pre { pre {
word-break: break-all !important; word-break: break-all !important;
white-space: break-spaces !important; white-space: break-spaces !important;
padding: 0 !important;
} }
+5 -4
View File
@@ -13,7 +13,6 @@ import config from '@/utils/config';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import './index.less'; import './index.less';
import vhCheck from 'vh-check'; import vhCheck from 'vh-check';
import { version, changeLogLink, changeLog } from '../version';
import { useCtx, useTheme } from '@/utils/hooks'; import { useCtx, useTheme } from '@/utils/hooks';
import { import {
message, message,
@@ -52,6 +51,8 @@ interface TSystemInfo {
lastCommitId: string; lastCommitId: string;
lastCommitTime: number; lastCommitTime: number;
version: string; version: string;
changeLog: string;
changeLogLink: string;
} }
export default function () { export default function () {
@@ -89,6 +90,7 @@ export default function () {
if (!data.isInitialized) { if (!data.isInitialized) {
history.push('/initialization'); history.push('/initialization');
} else { } else {
init(data.version);
getUser(); getUser();
} }
} }
@@ -143,7 +145,6 @@ export default function () {
useEffect(() => { useEffect(() => {
vhCheck(); vhCheck();
init();
const _theme = localStorage.getItem('qinglong_dark_theme') || 'auto'; const _theme = localStorage.getItem('qinglong_dark_theme') || 'auto';
if (typeof window === 'undefined') return; if (typeof window === 'undefined') return;
@@ -269,7 +270,7 @@ export default function () {
<> <>
<span style={{ fontSize: 16 }}></span> <span style={{ fontSize: 16 }}></span>
<a <a
href={changeLogLink} href={systemInfo?.changeLogLink}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
onClick={(e) => { onClick={(e) => {
@@ -289,7 +290,7 @@ export default function () {
letterSpacing: isQQBrowser ? -2 : 0, letterSpacing: isQQBrowser ? -2 : 0,
}} }}
> >
v{version} v{systemInfo?.version}
</span> </span>
</Badge> </Badge>
</Tooltip> </Tooltip>
+5 -16
View File
@@ -29,6 +29,7 @@ import config from '@/utils/config';
import CronLogModal from './logModal'; import CronLogModal from './logModal';
import Editor from '@monaco-editor/react'; import Editor from '@monaco-editor/react';
import IconFont from '@/components/iconfont'; import IconFont from '@/components/iconfont';
import { getCommandScript } from '@/utils';
const { Text } = Typography; const { Text } = Typography;
@@ -147,22 +148,10 @@ const CronDetailModal = ({
}; };
const getScript = () => { const getScript = () => {
const cmd = cron.command.split(' ') as string[]; const result = getCommandScript(cron.command);
if (cmd[0] === 'task') { if (Array.isArray(result)) {
setValidTabs(validTabs); setValidTabs(validTabs);
if (cmd[1].startsWith('/ql/data/scripts')) { const [s, p] = result;
cmd[1] = cmd[1].replace('/ql/data/scripts/', '');
}
let p: string, s: string;
let index = cmd[1].lastIndexOf('/');
if (index >= 0) {
s = cmd[1].slice(index + 1);
p = cmd[1].slice(0, index);
} else {
s = cmd[1];
p = '';
}
setScriptInfo({ parent: p, filename: s }); setScriptInfo({ parent: p, filename: s });
request request
.get(`${config.apiPrefix}scripts/${s}?path=${p || ''}`) .get(`${config.apiPrefix}scripts/${s}?path=${p || ''}`)
@@ -171,7 +160,7 @@ const CronDetailModal = ({
setValue(data); setValue(data);
} }
}); });
} else { } else if (result) {
setValidTabs([validTabs[0]]); setValidTabs([validTabs[0]]);
} }
}; };
+6 -17
View File
@@ -50,6 +50,7 @@ import ViewManageModal from './viewManageModal';
import { FilterValue, SorterResult } from 'antd/lib/table/interface'; import { FilterValue, SorterResult } from 'antd/lib/table/interface';
import { SharedContext } from '@/layouts'; import { SharedContext } from '@/layouts';
import useTableScrollHeight from '@/hooks/useTableScrollHeight'; import useTableScrollHeight from '@/hooks/useTableScrollHeight';
import { getCommandScript } from '@/utils';
const { Text, Paragraph } = Typography; const { Text, Paragraph } = Typography;
const { Search } = Input; const { Search } = Input;
@@ -390,24 +391,12 @@ const Crontab = () => {
const tableScrollHeight = useTableScrollHeight(tableRef); const tableScrollHeight = useTableScrollHeight(tableRef);
const goToScriptManager = (record: any) => { const goToScriptManager = (record: any) => {
const cmd = record.command.split(' ') as string[]; const result = getCommandScript(record.command);
if (cmd[0] === 'task') { if (Array.isArray(result)) {
if (cmd[1].startsWith('/ql/data/scripts')) { const [s, p] = result;
cmd[1] = cmd[1].replace('/ql/data/scripts/', '');
}
let p: string, s: string;
let index = cmd[1].lastIndexOf('/');
if (index >= 0) {
s = cmd[1].slice(index + 1);
p = cmd[1].slice(0, index);
} else {
s = cmd[1];
p = '';
}
history.push(`/script?p=${p}&s=${s}`); history.push(`/script?p=${p}&s=${s}`);
} else if (cmd[1] === 'repo') { } else if (result) {
location.href = cmd[2]; location.href = result;
} }
}; };
+1 -1
View File
@@ -80,7 +80,7 @@ const CronModal = ({
<Input.TextArea <Input.TextArea
rows={4} rows={4}
autoSize={true} autoSize={true}
placeholder="请输入要执行命令" placeholder="使用task命令运行脚本或其他任意LINUX可执行命令"
/> />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
+60 -12
View File
@@ -13,19 +13,27 @@ import { request } from '@/utils/http';
import config from '@/utils/config'; import config from '@/utils/config';
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import IconFont from '@/components/iconfont'; import IconFont from '@/components/iconfont';
import get from 'lodash/get';
const PROPERTIES = [ const PROPERTIES = [
{ name: '命令', value: 'command' }, { name: '命令', value: 'command' },
{ name: '名称', value: 'name' }, { name: '名称', value: 'name' },
{ name: '定时规则', value: 'schedule' }, { name: '定时规则', value: 'schedule' },
{ name: '状态', value: 'status' }, { name: '状态', value: 'status' },
{ name: '标签', value: 'labels' },
]; ];
const EOperation: any = {
Reg: '',
NotReg: '',
In: 'select',
Nin: 'select',
};
const OPERATIONS = [ const OPERATIONS = [
{ name: '包含', value: 'Reg' }, { name: '包含', value: 'Reg' },
{ name: '不包含', value: 'NotReg' }, { name: '不包含', value: 'NotReg' },
{ name: '属于', value: 'In' }, { name: '属于', value: 'In', type: 'select' },
{ name: '不属于', value: 'Nin' }, { name: '不属于', value: 'Nin', type: 'select' },
// { name: '等于', value: 'Eq' }, // { name: '等于', value: 'Eq' },
// { name: '不等于', value: 'Ne' }, // { name: '不等于', value: 'Ne' },
// { name: '为空', value: 'IsNull' }, // { name: '为空', value: 'IsNull' },
@@ -37,11 +45,13 @@ const SORTTYPES = [
{ name: '倒序', value: 'DESC' }, { name: '倒序', value: 'DESC' },
]; ];
const STATUS = [ const STATUS_MAP = {
status: [
{ name: '运行中', value: 0 }, { name: '运行中', value: 0 },
{ name: '空闲中', value: 1 }, { name: '空闲中', value: 1 },
{ name: '已禁用', value: 2 }, { name: '已禁用', value: 2 },
]; ],
};
enum ViewFilterRelation { enum ViewFilterRelation {
'and' = '且', 'and' = '且',
@@ -125,15 +135,17 @@ const ViewCreateModal = ({
</Select> </Select>
); );
const statusElement = ( const statusElement = (property: keyof typeof STATUS_MAP) => {
<Select mode="multiple" allowClear placeholder="请选择状态"> return (
{STATUS.map((x) => ( <Select mode="tags" allowClear placeholder="输入后回车增加自定义选项">
{STATUS_MAP[property]?.map((x) => (
<Select.Option key={x.name} value={x.value}> <Select.Option key={x.name} value={x.value}>
{x.name} {x.name}
</Select.Option> </Select.Option>
))} ))}
</Select> </Select>
); );
};
return ( return (
<Modal <Modal
@@ -238,19 +250,55 @@ const ViewCreateModal = ({
> >
{operationElement} {operationElement}
</Form.Item> </Form.Item>
<Form.Item
noStyle
shouldUpdate={(prevValues, nextValues) => {
const preOperation =
EOperation[
get(prevValues, ['filters', name, 'operation'])
];
const nextOperation =
EOperation[
get(nextValues, ['filters', name, 'operation'])
];
const flag = preOperation !== nextOperation;
if (flag) {
form.setFieldValue(
['filters', name, 'value'],
nextOperation === 'select' ? [] : '',
);
}
return flag;
}}
>
{() => {
const property = form.getFieldValue([
'filters',
index,
'property',
]) as 'status';
const operate = form.getFieldValue([
'filters',
name,
'operation',
]);
return (
<Form.Item <Form.Item
{...restField} {...restField}
name={[name, 'value']} name={[name, 'value']}
rules={[{ required: true, message: '请输入内容' }]} rules={[
{ required: true, message: '请输入内容' },
]}
> >
{['In', 'Nin'].includes( {EOperation[operate] === 'select' ? (
form.getFieldValue(['filters', index, 'operation']), statusElement(property)
) ? (
statusElement
) : ( ) : (
<Input placeholder="请输入内容" /> <Input placeholder="请输入内容" />
)} )}
</Form.Item> </Form.Item>
);
}}
</Form.Item>
{index !== 0 && ( {index !== 0 && (
<MinusCircleOutlined onClick={() => remove(name)} /> <MinusCircleOutlined onClick={() => remove(name)} />
)} )}
+26 -5
View File
@@ -9,20 +9,37 @@ import './index.less';
import { SharedContext } from '@/layouts'; import { SharedContext } from '@/layouts';
const Error = () => { const Error = () => {
const { user, theme } = useOutletContext<SharedContext>(); const { user, theme, reloadUser } = useOutletContext<SharedContext>();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [data, setData] = useState('暂无日志'); const [data, setData] = useState('暂无日志');
const getLog = () => { const getTimes = () => {
setLoading(true); return parseInt(localStorage.getItem('error_retry_times') || '0', 10);
};
let times = getTimes();
const getLog = (needLoading: boolean = true) => {
needLoading && setLoading(true);
request request
.get(`${config.apiPrefix}public/panel/log`) .get(`${config.apiPrefix}public/panel/log`)
.then(({ code, data }) => { .then(({ code, data }) => {
if (code === 200) { if (code === 200) {
setData(data); setData(data);
if (!data) {
times = getTimes();
if (times > 5) {
return;
}
localStorage.setItem('error_retry_times', `${times + 1}`);
setTimeout(() => {
reloadUser();
getLog(false);
}, 3000);
}
} }
}) })
.finally(() => setLoading(false)); .finally(() => needLoading && setLoading(false));
}; };
useEffect(() => { useEffect(() => {
@@ -39,7 +56,7 @@ const Error = () => {
<div className="error-wrapper"> <div className="error-wrapper">
{loading ? ( {loading ? (
<PageLoading /> <PageLoading />
) : ( ) : data ? (
<Terminal <Terminal
name="服务错误" name="服务错误"
colorMode={theme === 'vs-dark' ? ColorMode.Dark : ColorMode.Light} colorMode={theme === 'vs-dark' ? ColorMode.Dark : ColorMode.Light}
@@ -55,6 +72,10 @@ const Error = () => {
}, },
]} ]}
/> />
) : times > 5 ? (
<> ql -l check </>
) : (
<PageLoading tip="启动中,请稍后..." />
)} )}
</div> </div>
); );
+5 -4
View File
@@ -49,6 +49,7 @@ const EditModal = ({
const { theme } = useTheme(); const { theme } = useTheme();
const editorRef = useRef<any>(null); const editorRef = useRef<any>(null);
const [isRunning, setIsRunning] = useState(false); const [isRunning, setIsRunning] = useState(false);
const [currentPid, setCurrentPid] = useState(null);
const cancel = () => { const cancel = () => {
handleCancel(); handleCancel();
@@ -94,21 +95,21 @@ const EditModal = ({
.then(({ code, data }) => { .then(({ code, data }) => {
if (code === 200) { if (code === 200) {
setIsRunning(true); setIsRunning(true);
setCurrentPid(data);
} }
}); });
}; };
const stop = () => { const stop = () => {
if (!cNode || !cNode.title) { if (!cNode || !cNode.title || !currentPid) {
return; return;
} }
const content = editorRef.current.getValue().replace(/\r\n/g, '\n');
request request
.put(`${config.apiPrefix}scripts/stop`, { .put(`${config.apiPrefix}scripts/stop`, {
data: { data: {
filename: cNode.title, filename: cNode.title,
path: cNode.parent || '', path: cNode.parent || '',
content, pid: currentPid,
}, },
}) })
.then(({ code, data }) => { .then(({ code, data }) => {
@@ -271,7 +272,7 @@ const EditModal = ({
content: content:
editorRef.current && editorRef.current &&
editorRef.current.getValue().replace(/\r\n/g, '\n'), editorRef.current.getValue().replace(/\r\n/g, '\n'),
filename: cNode?.title, ...cNode,
}} }}
/> />
<SettingModal <SettingModal
+38 -3
View File
@@ -39,6 +39,8 @@ import { depthFirstSearch } from '@/utils';
import { SharedContext } from '@/layouts'; import { SharedContext } from '@/layouts';
import useFilterTreeData from '@/hooks/useFilterTreeData'; import useFilterTreeData from '@/hooks/useFilterTreeData';
import uniq from 'lodash/uniq'; import uniq from 'lodash/uniq';
import IconFont from '@/components/iconfont';
import RenameModal from './renameModal';
const { Text } = Typography; const { Text } = Typography;
@@ -64,20 +66,22 @@ const Script = () => {
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const editorRef = useRef<any>(null); const editorRef = useRef<any>(null);
const [isAddFileModalVisible, setIsAddFileModalVisible] = useState(false); const [isAddFileModalVisible, setIsAddFileModalVisible] = useState(false);
const [isRenameFileModalVisible, setIsRenameFileModalVisible] = useState(false);
const [currentNode, setCurrentNode] = useState<any>(); const [currentNode, setCurrentNode] = useState<any>();
const [expandedKeys, setExpandedKeys] = useState<string[]>([]); const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
const getScripts = () => { const getScripts = (needLoading: boolean = true) => {
setLoading(true); needLoading && setLoading(true);
request request
.get(`${config.apiPrefix}scripts`) .get(`${config.apiPrefix}scripts`)
.then(({ code, data }) => { .then(({ code, data }) => {
if (code === 200) { if (code === 200) {
setData(data); setData(data);
initState();
initGetScript(); initGetScript();
} }
}) })
.finally(() => setLoading(false)); .finally(() => needLoading && setLoading(false));
}; };
const getDetail = (node: any) => { const getDetail = (node: any) => {
@@ -287,6 +291,15 @@ const Script = () => {
}); });
}; };
const renameFile = () => {
setIsRenameFileModalVisible(true);
}
const handleRenameFileCancel = () => {
setIsRenameFileModalVisible(false);
getScripts(false);
}
const addFile = () => { const addFile = () => {
setIsAddFileModalVisible(true); setIsAddFileModalVisible(true);
}; };
@@ -381,6 +394,9 @@ const Script = () => {
case 'delete': case 'delete':
deleteFile(); deleteFile();
break; break;
case 'rename':
renameFile();
break;
default: default:
break; break;
} }
@@ -407,6 +423,12 @@ const Script = () => {
icon: <EditOutlined />, icon: <EditOutlined />,
disabled: !select, disabled: !select,
}, },
{
label: '重命名',
key: 'rename',
icon: <IconFont type="ql-icon-rename" />,
disabled: !select,
},
{ {
label: '删除', label: '删除',
key: 'delete', key: 'delete',
@@ -471,6 +493,14 @@ const Script = () => {
icon={<EditOutlined />} icon={<EditOutlined />}
/> />
</Tooltip>, </Tooltip>,
<Tooltip title="重命名">
<Button
disabled={!select}
type="primary"
onClick={renameFile}
icon={<IconFont type="ql-icon-rename" />}
/>
</Tooltip>,
<Tooltip title="删除"> <Tooltip title="删除">
<Button <Button
type="primary" type="primary"
@@ -585,6 +615,11 @@ const Script = () => {
treeData={data} treeData={data}
handleCancel={addFileModalClose} handleCancel={addFileModalClose}
/> />
<RenameModal
visible={isRenameFileModalVisible}
handleCancel={handleRenameFileCancel}
currentNode={currentNode}
/>
</div> </div>
</PageContainer> </PageContainer>
); );
+79
View File
@@ -0,0 +1,79 @@
import React, { useEffect, useState } from 'react';
import { Modal, message, Input, Form } from 'antd';
import { request } from '@/utils/http';
import config from '@/utils/config';
const RenameModal = ({
currentNode,
handleCancel,
visible,
}: {
currentNode?: any;
visible: boolean;
handleCancel: () => void;
}) => {
console.log(currentNode);
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const handleOk = async (values: any) => {
setLoading(true);
try {
const { code, data } = await request.put(
`${config.apiPrefix}scripts/rename`,
{
data: {
filename: currentNode.title,
path: currentNode.parent || '',
newFilename: values.name,
},
},
);
if (code === 200) {
message.success('更新名称成功');
handleCancel();
}
setLoading(false);
} catch (error) {
setLoading(false);
}
};
useEffect(() => {
form.resetFields();
}, [currentNode, visible]);
return (
<Modal
title="重命名"
open={visible}
forceRender
centered
maskClosable={false}
onOk={() => {
form
.validateFields()
.then((values) => {
handleOk(values);
})
.catch((info) => {
console.log('Validate Failed:', info);
});
}}
onCancel={() => handleCancel()}
confirmLoading={loading}
>
<Form form={form} layout="vertical" name="edit_name_modal">
<Form.Item
name="name"
rules={[{ required: true, message: '请输入新名称' }]}
>
<Input placeholder="请输入新名称" />
</Form.Item>
</Form>
</Modal>
);
};
export default RenameModal;
+2 -2
View File
@@ -17,7 +17,7 @@ const SaveModal = ({
const handleOk = async (values: any) => { const handleOk = async (values: any) => {
setLoading(true); setLoading(true);
const payload = { ...file, ...values, originFilename: file.filename }; const payload = { ...file, ...values, originFilename: file.title };
request request
.post(`${config.apiPrefix}scripts`, { .post(`${config.apiPrefix}scripts`, {
data: payload, data: payload,
@@ -60,7 +60,7 @@ const SaveModal = ({
form={form} form={form}
layout="vertical" layout="vertical"
name="script_modal" name="script_modal"
initialValues={file} initialValues={{ filename: file?.title, path: file?.parent || '' }}
> >
<Form.Item <Form.Item
name="filename" name="filename"
+8
View File
@@ -38,6 +38,14 @@ const About = ({ systemInfo }: { systemInfo: SharedContext['systemInfo'] }) => {
<Descriptions.Item label="更新ID" span={3}> <Descriptions.Item label="更新ID" span={3}>
{systemInfo.lastCommitId} {systemInfo.lastCommitId}
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label="更新日志" span={3}>
<Link
href={`https://qn.whyour.cn/version.yaml?t=${Date.now()}`}
target="_blank"
>
</Link>
</Descriptions.Item>
</Descriptions> </Descriptions>
<div> <div>
<Link <Link
+5 -7
View File
@@ -2,11 +2,10 @@ import React, { useEffect, useState, useRef } from 'react';
import { Statistic, Modal, Tag, Button, Spin, message } from 'antd'; import { Statistic, Modal, Tag, Button, Spin, message } from 'antd';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import config from '@/utils/config'; import config from '@/utils/config';
import { version } from '../../version';
const { Countdown } = Statistic; const { Countdown } = Statistic;
const CheckUpdate = ({ socketMessage }: any) => { const CheckUpdate = ({ socketMessage, systemInfo }: any) => {
const [updateLoading, setUpdateLoading] = useState(false); const [updateLoading, setUpdateLoading] = useState(false);
const [value, setValue] = useState(''); const [value, setValue] = useState('');
const modalRef = useRef<any>(); const modalRef = useRef<any>();
@@ -23,7 +22,7 @@ const CheckUpdate = ({ socketMessage }: any) => {
if (data.hasNewVersion) { if (data.hasNewVersion) {
showConfirmUpdateModal(data); showConfirmUpdateModal(data);
} else { } else {
showForceUpdateModal(); showForceUpdateModal(data);
} }
} }
}) })
@@ -36,7 +35,7 @@ const CheckUpdate = ({ socketMessage }: any) => {
}); });
}; };
const showForceUpdateModal = () => { const showForceUpdateModal = (data: any) => {
Modal.confirm({ Modal.confirm({
width: 500, width: 500,
title: '更新', title: '更新',
@@ -44,7 +43,7 @@ const CheckUpdate = ({ socketMessage }: any) => {
<> <>
<div></div> <div></div>
<div style={{ fontSize: 12, fontWeight: 400, marginTop: 5 }}> <div style={{ fontSize: 12, fontWeight: 400, marginTop: 5 }}>
{version} {data.lastVersion}
</div> </div>
</> </>
), ),
@@ -70,14 +69,13 @@ const CheckUpdate = ({ socketMessage }: any) => {
<> <>
<div></div> <div></div>
<div style={{ fontSize: 12, fontWeight: 400, marginTop: 5 }}> <div style={{ fontSize: 12, fontWeight: 400, marginTop: 5 }}>
{lastVersion}使{version} {lastVersion} 使 {systemInfo.version}
</div> </div>
</> </>
), ),
content: ( content: (
<pre <pre
style={{ style={{
paddingTop: 15,
fontSize: 12, fontSize: 12,
fontWeight: 400, fontWeight: 400,
}} }}
+4 -1
View File
@@ -416,7 +416,10 @@ const Setting = () => {
</Input.Group> </Input.Group>
</Form.Item> </Form.Item>
<Form.Item label="检查更新" name="update"> <Form.Item label="检查更新" name="update">
<CheckUpdate socketMessage={socketMessage} /> <CheckUpdate
systemInfo={systemInfo}
socketMessage={socketMessage}
/>
</Form.Item> </Form.Item>
</Form> </Form>
), ),
-1
View File
@@ -56,7 +56,6 @@ _request.interceptors.request.use((url, options) => {
_request.interceptors.response.use(async (response) => { _request.interceptors.response.use(async (response) => {
const responseStatus = response.status; const responseStatus = response.status;
if ([502, 504].includes(responseStatus)) { if ([502, 504].includes(responseStatus)) {
message.error('服务异常,请稍后刷新!');
history.push('/error'); history.push('/error');
} else if (responseStatus === 401) { } else if (responseStatus === 401) {
if (history.location.pathname !== '/login') { if (history.location.pathname !== '/login') {
+28
View File
@@ -276,3 +276,31 @@ export function logEnded(log: string): boolean {
const endTips = [LOG_END_SYMBOL, '执行结束']; const endTips = [LOG_END_SYMBOL, '执行结束'];
return endTips.some((x) => log.includes(x)); return endTips.some((x) => log.includes(x));
} }
export function getCommandScript(
command: string,
): [string, string] | string | undefined {
const cmd = command.split(' ') as string[];
if (cmd[0] === 'task') {
let scriptsPart = cmd.find((x) =>
['.js', '.ts', '.sh', '.py'].some((y) => x.endsWith(y)),
);
if (!scriptsPart) return;
if (scriptsPart.startsWith('/ql/data/scripts')) {
scriptsPart = scriptsPart.replace('/ql/data/scripts/', '');
}
let p: string, s: string;
let index = scriptsPart.lastIndexOf('/');
if (index >= 0) {
s = scriptsPart.slice(index + 1);
p = scriptsPart.slice(0, index);
} else {
s = scriptsPart;
p = '';
}
return [s, p];
} else if (cmd[1] === 'repo') {
return cmd[2];
}
}
+3 -5
View File
@@ -1,9 +1,9 @@
import * as Sentry from '@sentry/react'; import * as Sentry from '@sentry/react';
import { Integrations } from '@sentry/tracing'; import { Integrations } from '@sentry/tracing';
import { loader } from '@monaco-editor/react'; import { loader } from '@monaco-editor/react';
import { version } from '../version'; import * as monaco from 'monaco-editor';
export function init() { export function init(version: string) {
// sentry监控 init // sentry监控 init
Sentry.init({ Sentry.init({
dsn: 'https://3406424fb1dc4813a62d39e844a9d0ac@o1098464.ingest.sentry.io/6122818', dsn: 'https://3406424fb1dc4813a62d39e844a9d0ac@o1098464.ingest.sentry.io/6122818',
@@ -27,9 +27,7 @@ export function init() {
// monaco 编辑器配置cdn和locale // monaco 编辑器配置cdn和locale
loader.config({ loader.config({
paths: { monaco,
vs: 'https://cdn.staticfile.org/monaco-editor/0.33.0/min/vs',
},
'vs/nls': { 'vs/nls': {
availableLanguages: { availableLanguages: {
'*': 'zh-cn', '*': 'zh-cn',
-8
View File
@@ -1,8 +0,0 @@
export const version = '2.15.2';
export const changeLogLink = 'https://t.me/jiao_long/353';
export const changeLog = `2.15.2 版本说明
1. 任务视图增加系统视图排序
2. task 命令增加 -m 参数,支持设置任务超时时间,格式参考 CommandTimeoutTime。例如 task -m 1h xxx.js
3. 修复资源占用检查逻辑
4. 其他优化
`;
+2
View File
@@ -8,3 +8,5 @@ declare module '*.svg' {
const url: string; const url: string;
export default url; export default url;
} }
declare module 'pstree.remy';
+7
View File
@@ -0,0 +1,7 @@
version: 2.15.5
changeLogLink: https://t.me/jiao_long/356
changeLog: |
1. 修复定时任务命令不以task开头时,不能自动执行
2. 修改服务异常逻辑
3. 修复退出调试报错
4. 修复脚本编辑器加载慢