mirror of
https://github.com/whyour/qinglong.git
synced 2026-08-07 17:24:31 +08:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e93cad91b4 | |||
| 08ee61b179 | |||
| 0ab756665e | |||
| 3570cddce0 | |||
| a2d9f1a5db | |||
| 0c6a214e55 | |||
| 095fcdbe69 | |||
| 3afb1d18f6 | |||
| 4526c84330 | |||
| a9cc1cb4b9 | |||
| 7acf1eace3 | |||
| bb2e8cb287 | |||
| 3b389259c1 | |||
| 310ce55b09 | |||
| 4662e3fec2 | |||
| 10fab9d415 | |||
| acecf01cbe | |||
| b95fb9cda4 | |||
| 23fd595582 |
+32
-5
@@ -170,7 +170,7 @@ export default (app: Router) => {
|
||||
body: Joi.object({
|
||||
filename: Joi.string().required(),
|
||||
path: Joi.string().allow(''),
|
||||
type: Joi.string().optional()
|
||||
type: Joi.string().optional(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
@@ -183,7 +183,7 @@ export default (app: Router) => {
|
||||
};
|
||||
const filePath = join(config.scriptPath, path, filename);
|
||||
if (type === 'directory') {
|
||||
emptyDir(filePath);
|
||||
emptyDir(filePath);
|
||||
} else {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
@@ -255,23 +255,50 @@ export default (app: Router) => {
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
filename: Joi.string().required(),
|
||||
content: Joi.string().optional().allow(''),
|
||||
path: Joi.string().optional().allow(''),
|
||||
pid: Joi.number().optional().allow(''),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
let { filename, content, path } = req.body;
|
||||
let { filename, path, pid } = req.body;
|
||||
const { name, ext } = parse(filename);
|
||||
const filePath = join(config.scriptPath, path, `${name}.swap${ext}`);
|
||||
|
||||
const scriptService = Container.get(ScriptService);
|
||||
const result = await scriptService.stopScript(filePath);
|
||||
const result = await scriptService.stopScript(filePath, pid);
|
||||
res.send(result);
|
||||
} catch (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
@@ -7,7 +7,7 @@ import SystemService from '../services/system';
|
||||
import { celebrate, Joi } from 'celebrate';
|
||||
import UserService from '../services/user';
|
||||
import { EnvModel } from '../data/env';
|
||||
import { promiseExec } from '../config/util';
|
||||
import { parseVersion, promiseExec } from '../config/util';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const route = Router();
|
||||
@@ -21,10 +21,9 @@ export default (app: Router) => {
|
||||
const userService = Container.get(UserService);
|
||||
const authInfo = await userService.getUserInfo();
|
||||
const envCount = await EnvModel.count();
|
||||
const versionRegx = /.*export const version = \'(.*)\'\;/;
|
||||
|
||||
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
|
||||
const version = currentVersionFile.match(versionRegx)![1];
|
||||
const { version, changeLog, changeLogLink } = await parseVersion(
|
||||
config.versionFile,
|
||||
);
|
||||
const lastCommitTime = (
|
||||
await promiseExec(
|
||||
`cd ${config.rootPath} && git show -s --format=%ai | head -1`,
|
||||
@@ -56,6 +55,8 @@ export default (app: Router) => {
|
||||
lastCommitTime: dayjs(lastCommitTime).unix(),
|
||||
lastCommitId,
|
||||
branch,
|
||||
changeLog,
|
||||
changeLogLink,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
|
||||
@@ -14,7 +14,7 @@ if (!process.env.QL_DIR) {
|
||||
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 envFound = dotenv.config({ path: path.join(rootPath, '.env') });
|
||||
@@ -40,7 +40,7 @@ const sqliteFile = path.join(samplePath, 'database.sqlite');
|
||||
const authError = '错误的用户名密码,请重试';
|
||||
const loginFaild = '请先登录!';
|
||||
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) {
|
||||
throw new Error("⚠️ Couldn't find .env file ⚠️");
|
||||
|
||||
+49
-4
@@ -4,6 +4,9 @@ import got from 'got';
|
||||
import iconv from 'iconv-lite';
|
||||
import { exec } from 'child_process';
|
||||
import FormData from 'form-data';
|
||||
import psTreeFun from 'pstree.remy';
|
||||
import { promisify } from 'util';
|
||||
import { load } from 'js-yaml';
|
||||
|
||||
export function getFileContentByName(fileName: string) {
|
||||
if (fs.existsSync(fileName)) {
|
||||
@@ -287,15 +290,15 @@ enum FileType {
|
||||
interface IFile {
|
||||
title: string;
|
||||
key: string;
|
||||
type: 'directory' | 'file',
|
||||
type: 'directory' | 'file';
|
||||
parent: string;
|
||||
mtime: number;
|
||||
children?: IFile[],
|
||||
children?: IFile[];
|
||||
}
|
||||
|
||||
export function dirSort(a: IFile, b: IFile) {
|
||||
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
|
||||
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;
|
||||
}
|
||||
|
||||
export function readDirs(
|
||||
@@ -452,3 +455,45 @@ export function parseBody(
|
||||
|
||||
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,13 +4,11 @@ import * as Tracing from '@sentry/tracing';
|
||||
import Logger from './logger';
|
||||
import config from '../config';
|
||||
import fs from 'fs';
|
||||
import { parseVersion } from '../config/util';
|
||||
|
||||
export default ({ expressApp }: { expressApp: Application }) => {
|
||||
const versionRegx = /.*export const version = \'(.*)\'\;/;
|
||||
export default async ({ expressApp }: { expressApp: Application }) => {
|
||||
const { version } = await parseVersion(config.versionFile);
|
||||
|
||||
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
|
||||
const currentVersion = currentVersionFile.match(versionRegx)![1];
|
||||
|
||||
Sentry.init({
|
||||
dsn: 'https://f4b5b55fb3c645b29a5dc2d70a1a4ef4@o1098464.ingest.sentry.io/6122819',
|
||||
integrations: [
|
||||
@@ -18,7 +16,7 @@ export default ({ expressApp }: { expressApp: Application }) => {
|
||||
new Tracing.Integrations.Express({ app: expressApp }),
|
||||
],
|
||||
tracesSampleRate: 0.1,
|
||||
release: currentVersion,
|
||||
release: version,
|
||||
});
|
||||
|
||||
expressApp.use(Sentry.Handlers.requestHandler());
|
||||
|
||||
+25
-65
@@ -5,13 +5,15 @@ import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
|
||||
import { exec, execSync, spawn } from 'child_process';
|
||||
import fs from 'fs';
|
||||
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 { promisify } from 'util';
|
||||
import { Op } from 'sequelize';
|
||||
import { Op, where, col as colFn } from 'sequelize';
|
||||
import path from 'path';
|
||||
import dayjs from 'dayjs';
|
||||
import { LOG_END_SYMBOL } from '../config/const';
|
||||
|
||||
@Service()
|
||||
export default class CronService {
|
||||
@@ -161,9 +163,23 @@ export default class CronService {
|
||||
}
|
||||
if (operate && operate2) {
|
||||
q[property] = {
|
||||
[operate2]: [
|
||||
{ [operate]: `%${value}%` },
|
||||
{ [operate]: `%${encodeURIComponent(value)}%` },
|
||||
[Op.or]: [
|
||||
{
|
||||
[operate2]: [
|
||||
{ [operate]: `%${value}%` },
|
||||
{ [operate]: `%${encodeURIComponent(value)}%` },
|
||||
],
|
||||
},
|
||||
{
|
||||
[operate2]: [
|
||||
where(colFn(property), operate, `%${value}%`),
|
||||
where(
|
||||
colFn(property),
|
||||
operate,
|
||||
`%${encodeURIComponent(value)}%`,
|
||||
),
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -315,31 +331,11 @@ export default class CronService {
|
||||
for (const doc of docs) {
|
||||
if (doc.pid) {
|
||||
try {
|
||||
process.kill(-doc.pid);
|
||||
await killTask(doc.pid);
|
||||
} catch (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(
|
||||
@@ -348,42 +344,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> {
|
||||
return new Promise(async (resolve: any) => {
|
||||
const cron = await this.getDb({ id: cronId });
|
||||
|
||||
@@ -42,7 +42,11 @@ export default class ScheduleService {
|
||||
|
||||
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) => {
|
||||
try {
|
||||
const startTime = dayjs();
|
||||
@@ -52,6 +56,7 @@ export default class ScheduleService {
|
||||
|
||||
// TODO:
|
||||
callbacks.onStart?.(cp, startTime);
|
||||
completionTime === 'start' && resolve(cp.pid);
|
||||
|
||||
cp.stdout.on('data', async (data) => {
|
||||
await callbacks.onLog?.(data.toString());
|
||||
@@ -100,7 +105,6 @@ export default class ScheduleService {
|
||||
error,
|
||||
);
|
||||
await callbacks.onError?.(JSON.stringify(error));
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
+16
-13
@@ -7,6 +7,7 @@ import CronService from './cron';
|
||||
import ScheduleService, { TaskCallbacks } from './schedule';
|
||||
import config from '../config';
|
||||
import { LOG_END_SYMBOL } from '../config/const';
|
||||
import { getPid, killTask } from '../config/util';
|
||||
|
||||
@Service()
|
||||
export default class ScriptService {
|
||||
@@ -42,22 +43,24 @@ export default class ScriptService {
|
||||
public async runScript(filePath: string) {
|
||||
const relativePath = path.relative(config.scriptPath, filePath);
|
||||
const command = `task -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) {
|
||||
const relativePath = path.relative(config.scriptPath, filePath);
|
||||
const err = await this.cronService.killTask(`task -l ${relativePath} now`);
|
||||
|
||||
const str = err ? `\n${err}` : '';
|
||||
this.sockService.sendMessage({
|
||||
type: 'manuallyRunScript',
|
||||
message: `${str}\n## 执行结束... ${new Date()
|
||||
.toLocaleString('zh', { hour12: false })
|
||||
.replace(' 24:', ' 00:')}${LOG_END_SYMBOL}`,
|
||||
});
|
||||
public async stopScript(filePath: string, pid: number) {
|
||||
let str = '';
|
||||
if (!pid) {
|
||||
const relativePath = path.relative(config.scriptPath, filePath);
|
||||
pid = await getPid(`task -l ${relativePath} now`);
|
||||
}
|
||||
try {
|
||||
await killTask(pid);
|
||||
} catch (error) {}
|
||||
|
||||
return { code: 200 };
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export default class SshKeyService {
|
||||
|
||||
private generatePrivateKeyFile(alias: string, key: string): void {
|
||||
try {
|
||||
fs.writeFileSync(`${this.sshPath}/${alias}`, key, {
|
||||
fs.writeFileSync(`${this.sshPath}/${alias}`, `${key}${os.EOL}`, {
|
||||
encoding: 'utf8',
|
||||
mode: '400',
|
||||
});
|
||||
|
||||
@@ -19,9 +19,9 @@ import {
|
||||
concurrentRun,
|
||||
fileExist,
|
||||
createFile,
|
||||
killTask,
|
||||
} from '../config/util';
|
||||
import { promises, existsSync } from 'fs';
|
||||
import { promisify } from 'util';
|
||||
import { Op } from 'sequelize';
|
||||
import path from 'path';
|
||||
import ScheduleService, { TaskCallbacks } from './schedule';
|
||||
@@ -351,19 +351,16 @@ export default class SubscriptionService {
|
||||
for (const doc of docs) {
|
||||
if (doc.pid) {
|
||||
try {
|
||||
process.kill(-doc.pid);
|
||||
await killTask(doc.pid);
|
||||
} catch (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 str = err ? `\n${err}` : '';
|
||||
|
||||
fs.appendFileSync(
|
||||
`${absolutePath}`,
|
||||
`${str}\n## 执行结束... ${dayjs().format(
|
||||
`\n## 执行结束... ${dayjs().format(
|
||||
'YYYY-MM-DD HH:mm:ss',
|
||||
)}${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) {
|
||||
const subscription = await this.getDb({ id: subscriptionId });
|
||||
if (subscription.status !== SubscriptionStatus.queued) {
|
||||
|
||||
+15
-15
@@ -9,6 +9,7 @@ import ScheduleService from './schedule';
|
||||
import { spawn } from 'child_process';
|
||||
import SockService from './sock';
|
||||
import got from 'got';
|
||||
import { parseContentVersion, parseVersion } from '../config/util';
|
||||
|
||||
@Service()
|
||||
export default class SystemService {
|
||||
@@ -78,14 +79,9 @@ export default class SystemService {
|
||||
|
||||
public async checkUpdate() {
|
||||
try {
|
||||
const versionRegx = /.*export const version = \'(.*)\'\;/;
|
||||
const logRegx = /.*export const changeLog = \`((.*\n.*)+)\`;/;
|
||||
const currentVersionContent = await parseVersion(config.versionFile);
|
||||
|
||||
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
|
||||
const currentVersion = currentVersionFile.match(versionRegx)![1];
|
||||
|
||||
let lastVersion = '';
|
||||
let lastLog = '';
|
||||
let lastVersionContent;
|
||||
try {
|
||||
const result = await got.get(
|
||||
`${config.lastVersionFile}?t=${Date.now()}`,
|
||||
@@ -93,19 +89,23 @@ export default class SystemService {
|
||||
timeout: 30000,
|
||||
},
|
||||
);
|
||||
const lastVersionFileContent = result.body;
|
||||
lastVersion = lastVersionFileContent.match(versionRegx)![1];
|
||||
lastLog = lastVersionFileContent.match(logRegx)
|
||||
? lastVersionFileContent.match(logRegx)![1]
|
||||
: '';
|
||||
lastVersionContent = await parseContentVersion(result.body);
|
||||
} catch (error) {}
|
||||
|
||||
if (!lastVersionContent) {
|
||||
lastVersionContent = currentVersionContent;
|
||||
}
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
hasNewVersion: this.checkHasNewVersion(currentVersion, lastVersion),
|
||||
lastVersion,
|
||||
lastLog,
|
||||
hasNewVersion: this.checkHasNewVersion(
|
||||
currentVersionContent.version,
|
||||
lastVersionContent.version,
|
||||
),
|
||||
lastVersion: lastVersionContent.version,
|
||||
lastLog: lastVersionContent.changeLog,
|
||||
lastLogLink: lastVersionContent.changeLogLink,
|
||||
},
|
||||
};
|
||||
} catch (error: any) {
|
||||
|
||||
+9
-3
@@ -5,6 +5,7 @@ import LoggerInstance from './loaders/logger';
|
||||
import fs from 'fs';
|
||||
import config from './config';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
|
||||
const tokenFile = path.join(config.configPath, 'token.json');
|
||||
|
||||
@@ -25,9 +26,14 @@ async function getToken() {
|
||||
|
||||
async function writeFile(data: any) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
fs.writeFile(tokenFile, JSON.stringify(data), { encoding: 'utf8' }, () => {
|
||||
resolve();
|
||||
});
|
||||
fs.writeFile(
|
||||
tokenFile,
|
||||
`${JSON.stringify(data)}${os.EOL}`,
|
||||
{ encoding: 'utf8' },
|
||||
() => {
|
||||
resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM python:alpine
|
||||
FROM python:3.10-alpine
|
||||
|
||||
ARG QL_MAINTAINER="whyour"
|
||||
LABEL maintainer="${QL_MAINTAINER}"
|
||||
|
||||
+4
-1
@@ -4,7 +4,7 @@
|
||||
"start": "concurrently -n w: npm:start:*",
|
||||
"start:front": "max dev",
|
||||
"start:back": "nodemon",
|
||||
"start:public": "ts-node back/public.ts",
|
||||
"start:public": "ts-node --transpile-only ./back/public.ts",
|
||||
"build:front": "max build",
|
||||
"build:back": "tsc -p tsconfig.back.json",
|
||||
"panel": "npm run build:back && node static/build/app.js",
|
||||
@@ -69,12 +69,14 @@
|
||||
"got": "^11.8.2",
|
||||
"hpagent": "^0.1.2",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"lodash": "^4.17.21",
|
||||
"multer": "^1.4.4",
|
||||
"nedb": "^1.8.0",
|
||||
"node-schedule": "^2.1.0",
|
||||
"nodemailer": "^6.7.2",
|
||||
"pstree.remy": "^1.1.8",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"sequelize": "^6.25.5",
|
||||
"serve-handler": "^6.1.3",
|
||||
@@ -96,6 +98,7 @@
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/express-jwt": "^6.0.4",
|
||||
"@types/js-yaml": "^4.0.5",
|
||||
"@types/jsonwebtoken": "^8.5.8",
|
||||
"@types/lodash": "^4.14.185",
|
||||
"@types/multer": "^1.4.7",
|
||||
|
||||
Generated
+10
-3
@@ -13,6 +13,7 @@ specifiers:
|
||||
'@types/cors': ^2.8.12
|
||||
'@types/express': ^4.17.13
|
||||
'@types/express-jwt': ^6.0.4
|
||||
'@types/js-yaml': ^4.0.5
|
||||
'@types/jsonwebtoken': ^8.5.8
|
||||
'@types/lodash': ^4.14.185
|
||||
'@types/multer': ^1.4.7
|
||||
@@ -49,6 +50,7 @@ specifiers:
|
||||
got: ^11.8.2
|
||||
hpagent: ^0.1.2
|
||||
iconv-lite: ^0.6.3
|
||||
js-yaml: ^4.1.0
|
||||
jsonwebtoken: ^8.5.1
|
||||
lint-staged: ^13.0.3
|
||||
lodash: ^4.17.21
|
||||
@@ -58,6 +60,7 @@ specifiers:
|
||||
nodemailer: ^6.7.2
|
||||
nodemon: ^2.0.15
|
||||
prettier: ^2.5.1
|
||||
pstree.remy: ^1.1.8
|
||||
qiniu: ^7.4.0
|
||||
qrcode.react: ^1.0.1
|
||||
query-string: ^7.1.1
|
||||
@@ -106,12 +109,14 @@ dependencies:
|
||||
got: 11.8.5
|
||||
hpagent: 0.1.2
|
||||
iconv-lite: 0.6.3
|
||||
js-yaml: 4.1.0
|
||||
jsonwebtoken: 8.5.1
|
||||
lodash: 4.17.21
|
||||
multer: 1.4.4
|
||||
nedb: 1.8.0
|
||||
node-schedule: 2.1.0
|
||||
nodemailer: 6.7.8
|
||||
pstree.remy: 1.1.8
|
||||
reflect-metadata: 0.1.13
|
||||
sequelize: 6.25.6_@louislam+sqlite3@15.0.6
|
||||
serve-handler: 6.1.3
|
||||
@@ -133,6 +138,7 @@ devDependencies:
|
||||
'@types/cors': 2.8.12
|
||||
'@types/express': 4.17.14
|
||||
'@types/express-jwt': 6.0.4
|
||||
'@types/js-yaml': 4.0.5
|
||||
'@types/jsonwebtoken': 8.5.9
|
||||
'@types/lodash': 4.14.185
|
||||
'@types/multer': 1.4.7
|
||||
@@ -2628,6 +2634,10 @@ packages:
|
||||
'@types/istanbul-lib-report': 3.0.0
|
||||
dev: true
|
||||
|
||||
/@types/js-yaml/4.0.5:
|
||||
resolution: {integrity: sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==}
|
||||
dev: true
|
||||
|
||||
/@types/json-schema/7.0.11:
|
||||
resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==}
|
||||
dev: true
|
||||
@@ -3839,7 +3849,6 @@ packages:
|
||||
|
||||
/argparse/2.0.1:
|
||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||
dev: true
|
||||
|
||||
/aria-hidden/1.2.1_jobltc72ducwijlk5r742icaw4:
|
||||
resolution: {integrity: sha512-PN344VAf9j1EAi+jyVHOJ8XidQdPVssGco39eNcsGdM4wcsILtxrKLkbuiMfLWYROK1FjRQasMWCBttrhjnr6A==}
|
||||
@@ -7992,7 +8001,6 @@ packages:
|
||||
hasBin: true
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
dev: true
|
||||
|
||||
/jsbn/0.1.1:
|
||||
resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==}
|
||||
@@ -10352,7 +10360,6 @@ packages:
|
||||
|
||||
/pstree.remy/1.1.8:
|
||||
resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==}
|
||||
dev: true
|
||||
|
||||
/public-encrypt/4.0.3:
|
||||
resolution: {integrity: sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==}
|
||||
|
||||
@@ -980,6 +980,8 @@ function aibotkNotify(text, desp) {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
+2
-2
@@ -5,14 +5,14 @@ const envFound = dotenv.config();
|
||||
const accessKey = process.env.QINIU_AK;
|
||||
const secretKey = process.env.QINIU_SK;
|
||||
const mac = new qiniu.auth.digest.Mac(accessKey, secretKey);
|
||||
const key = 'version.ts';
|
||||
const key = 'version.yaml';
|
||||
const options = {
|
||||
scope: `${process.env.QINIU_SCOPE}:${key}`,
|
||||
};
|
||||
const putPolicy = new qiniu.rs.PutPolicy(options);
|
||||
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 formUploader = new qiniu.form_up.FormUploader(config);
|
||||
const putExtra = new qiniu.form_up.PutExtra(
|
||||
|
||||
+10
-1
@@ -1,7 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
get_token() {
|
||||
token=$(cat $file_auth_token | jq -r .value)
|
||||
if [[ -f $file_auth_token ]]; then
|
||||
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() {
|
||||
|
||||
+9
-3
@@ -5,6 +5,12 @@ dir_shell=$QL_DIR/shell
|
||||
. $dir_shell/share.sh
|
||||
. $dir_shell/api.sh
|
||||
|
||||
trap "single_hanle" 2 20 15 14
|
||||
single_hanle() {
|
||||
handle_task_after "$@"
|
||||
exit 1
|
||||
}
|
||||
|
||||
random_delay() {
|
||||
local random_delay_max=$RandomDelay
|
||||
if [[ $random_delay_max ]] && [[ $random_delay_max -gt 0 ]]; then
|
||||
@@ -115,7 +121,7 @@ handle_task_after() {
|
||||
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 "
|
||||
@@ -150,7 +156,7 @@ run_concurrent() {
|
||||
|
||||
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 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[@]}))
|
||||
|
||||
@@ -198,7 +204,7 @@ run_designated() {
|
||||
|
||||
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 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[@]}))
|
||||
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ git push
|
||||
echo -e "更新cdn文件"
|
||||
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"
|
||||
echo -e "当前版本$version"
|
||||
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ format_params() {
|
||||
time_format="%Y-%m-%d %H:%M:%S"
|
||||
timeoutCmd=""
|
||||
if type timeout &>/dev/null; then
|
||||
timeoutCmd="timeout -k 10s $command_timeout_time "
|
||||
timeoutCmd="timeout --foreground -s 14 -k 10s $command_timeout_time "
|
||||
fi
|
||||
params=$(echo "$@" | sed -E 's/([^ ])&([^ ])/\1\\\&\2/g')
|
||||
}
|
||||
|
||||
+6
-36
@@ -26,33 +26,6 @@ diff_cron() {
|
||||
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:新/失效
|
||||
output_list_add_drop() {
|
||||
local list=$1
|
||||
@@ -188,7 +161,7 @@ update_raw() {
|
||||
echo -e "下载 ${raw_file_name} 成功...\n"
|
||||
cd $dir_raw
|
||||
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}
|
||||
cron_line=$(
|
||||
perl -ne "{
|
||||
@@ -266,7 +239,6 @@ update_qinglong() {
|
||||
if [[ $exit_status -eq 0 ]]; then
|
||||
echo -e "\n更新青龙源文件成功...\n"
|
||||
cp -f $file_config_sample $dir_config/config.sample.sh
|
||||
detect_config_version
|
||||
update_depend
|
||||
|
||||
[[ -f $dir_root/package.json ]] && ql_depend_new=$(cat $dir_root/package.json)
|
||||
@@ -291,8 +263,6 @@ update_qinglong_static() {
|
||||
fi
|
||||
if [[ $exit_status -eq 0 ]]; then
|
||||
echo -e "\n更新青龙静态资源成功...\n"
|
||||
local static_version=$(cat $dir_root/src/version.ts | perl -pe "s|.*\'(.*)\';\.*|\1|" | head -1)
|
||||
echo -e "\n当前版本 $static_version...\n"
|
||||
|
||||
rm -rf $dir_static/*
|
||||
cp -rf $ql_static_repo/* $dir_static
|
||||
@@ -396,12 +366,12 @@ gen_list_repo() {
|
||||
filename=$(basename $file)
|
||||
cp -f $file "$dir_scripts/${uniq_path}/${filename}"
|
||||
echo "${uniq_path}/${filename}" >>"$dir_list_tmp/${uniq_path}_scripts.list"
|
||||
cron_id=$(cat $list_crontab_user | grep -E "$cmd_task ${uniq_path}_${filename}" | perl -pe "s|.*ID=(.*) $cmd_task ${uniq_path}_${filename}\.*|\1|" | head -1 | awk -F " " '{print $1}')
|
||||
if [[ $cron_id ]]; then
|
||||
result=$(update_cron_command_api "$cmd_task ${uniq_path}/${filename}:$cron_id")
|
||||
fi
|
||||
# cron_id=$(cat $list_crontab_user | grep -E "$cmd_task.* ${uniq_path}_${filename}" | perl -pe "s|.*ID=(.*) $cmd_task.* ${uniq_path}_${filename}\.*|\1|" | head -1 | awk -F " " '{print $1}')
|
||||
# if [[ $cron_id ]]; then
|
||||
# result=$(update_cron_command_api "$cmd_task ${uniq_path}/${filename}:$cron_id")
|
||||
# fi
|
||||
done
|
||||
grep -E "${cmd_task} ${uniq_path}" ${list_crontab_user} | perl -pe "s|.*ID=(.*) ${cmd_task} (${uniq_path}.*)\.*|\2|" | awk -F " " '{print $1}' | sort -u >"$dir_list_tmp/${uniq_path}_user.list"
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createFromIconfontCN } from '@ant-design/icons';
|
||||
|
||||
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;
|
||||
|
||||
@@ -13,7 +13,6 @@ import config from '@/utils/config';
|
||||
import { request } from '@/utils/http';
|
||||
import './index.less';
|
||||
import vhCheck from 'vh-check';
|
||||
import { version, changeLogLink, changeLog } from '../version';
|
||||
import { useCtx, useTheme } from '@/utils/hooks';
|
||||
import {
|
||||
message,
|
||||
@@ -52,6 +51,8 @@ interface TSystemInfo {
|
||||
lastCommitId: string;
|
||||
lastCommitTime: number;
|
||||
version: string;
|
||||
changeLog: string;
|
||||
changeLogLink: string;
|
||||
}
|
||||
|
||||
export default function () {
|
||||
@@ -89,6 +90,7 @@ export default function () {
|
||||
if (!data.isInitialized) {
|
||||
history.push('/initialization');
|
||||
} else {
|
||||
init(data.version);
|
||||
getUser();
|
||||
}
|
||||
}
|
||||
@@ -143,7 +145,6 @@ export default function () {
|
||||
|
||||
useEffect(() => {
|
||||
vhCheck();
|
||||
init();
|
||||
|
||||
const _theme = localStorage.getItem('qinglong_dark_theme') || 'auto';
|
||||
if (typeof window === 'undefined') return;
|
||||
@@ -269,7 +270,7 @@ export default function () {
|
||||
<>
|
||||
<span style={{ fontSize: 16 }}>控制面板</span>
|
||||
<a
|
||||
href={changeLogLink}
|
||||
href={systemInfo?.changeLogLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => {
|
||||
@@ -289,7 +290,7 @@ export default function () {
|
||||
letterSpacing: isQQBrowser ? -2 : 0,
|
||||
}}
|
||||
>
|
||||
v{version}
|
||||
v{systemInfo?.version}
|
||||
</span>
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
|
||||
@@ -29,6 +29,7 @@ import config from '@/utils/config';
|
||||
import CronLogModal from './logModal';
|
||||
import Editor from '@monaco-editor/react';
|
||||
import IconFont from '@/components/iconfont';
|
||||
import { getCommandScript } from '@/utils';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
@@ -147,22 +148,10 @@ const CronDetailModal = ({
|
||||
};
|
||||
|
||||
const getScript = () => {
|
||||
const cmd = cron.command.split(' ') as string[];
|
||||
if (cmd[0] === 'task') {
|
||||
const result = getCommandScript(cron.command);
|
||||
if (Array.isArray(result)) {
|
||||
setValidTabs(validTabs);
|
||||
if (cmd[1].startsWith('/ql/data/scripts')) {
|
||||
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 = '';
|
||||
}
|
||||
const [s, p] = result;
|
||||
setScriptInfo({ parent: p, filename: s });
|
||||
request
|
||||
.get(`${config.apiPrefix}scripts/${s}?path=${p || ''}`)
|
||||
@@ -171,7 +160,7 @@ const CronDetailModal = ({
|
||||
setValue(data);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
} else if (result) {
|
||||
setValidTabs([validTabs[0]]);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -50,6 +50,7 @@ import ViewManageModal from './viewManageModal';
|
||||
import { FilterValue, SorterResult } from 'antd/lib/table/interface';
|
||||
import { SharedContext } from '@/layouts';
|
||||
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
|
||||
import { getCommandScript } from '@/utils';
|
||||
|
||||
const { Text, Paragraph } = Typography;
|
||||
const { Search } = Input;
|
||||
@@ -390,24 +391,12 @@ const Crontab = () => {
|
||||
const tableScrollHeight = useTableScrollHeight(tableRef);
|
||||
|
||||
const goToScriptManager = (record: any) => {
|
||||
const cmd = record.command.split(' ') as string[];
|
||||
if (cmd[0] === 'task') {
|
||||
if (cmd[1].startsWith('/ql/data/scripts')) {
|
||||
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 = '';
|
||||
}
|
||||
const result = getCommandScript(record.command);
|
||||
if (Array.isArray(result)) {
|
||||
const [s, p] = result;
|
||||
history.push(`/script?p=${p}&s=${s}`);
|
||||
} else if (cmd[1] === 'repo') {
|
||||
location.href = cmd[2];
|
||||
} else if (result) {
|
||||
location.href = result;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -13,19 +13,27 @@ import { request } from '@/utils/http';
|
||||
import config from '@/utils/config';
|
||||
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import IconFont from '@/components/iconfont';
|
||||
import get from 'lodash/get';
|
||||
|
||||
const PROPERTIES = [
|
||||
{ name: '命令', value: 'command' },
|
||||
{ name: '名称', value: 'name' },
|
||||
{ name: '定时规则', value: 'schedule' },
|
||||
{ name: '状态', value: 'status' },
|
||||
{ name: '标签', value: 'labels' },
|
||||
];
|
||||
|
||||
const EOperation: any = {
|
||||
Reg: '',
|
||||
NotReg: '',
|
||||
In: 'select',
|
||||
Nin: 'select',
|
||||
};
|
||||
const OPERATIONS = [
|
||||
{ name: '包含', value: 'Reg' },
|
||||
{ name: '不包含', value: 'NotReg' },
|
||||
{ name: '属于', value: 'In' },
|
||||
{ name: '不属于', value: 'Nin' },
|
||||
{ name: '属于', value: 'In', type: 'select' },
|
||||
{ name: '不属于', value: 'Nin', type: 'select' },
|
||||
// { name: '等于', value: 'Eq' },
|
||||
// { name: '不等于', value: 'Ne' },
|
||||
// { name: '为空', value: 'IsNull' },
|
||||
@@ -37,11 +45,13 @@ const SORTTYPES = [
|
||||
{ name: '倒序', value: 'DESC' },
|
||||
];
|
||||
|
||||
const STATUS = [
|
||||
{ name: '运行中', value: 0 },
|
||||
{ name: '空闲中', value: 1 },
|
||||
{ name: '已禁用', value: 2 },
|
||||
];
|
||||
const STATUS_MAP = {
|
||||
status: [
|
||||
{ name: '运行中', value: 0 },
|
||||
{ name: '空闲中', value: 1 },
|
||||
{ name: '已禁用', value: 2 },
|
||||
],
|
||||
};
|
||||
|
||||
enum ViewFilterRelation {
|
||||
'and' = '且',
|
||||
@@ -125,15 +135,17 @@ const ViewCreateModal = ({
|
||||
</Select>
|
||||
);
|
||||
|
||||
const statusElement = (
|
||||
<Select mode="multiple" allowClear placeholder="请选择状态">
|
||||
{STATUS.map((x) => (
|
||||
<Select.Option key={x.name} value={x.value}>
|
||||
{x.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
const statusElement = (property: keyof typeof STATUS_MAP) => {
|
||||
return (
|
||||
<Select mode="tags" allowClear placeholder="输入后回车增加自定义选项">
|
||||
{STATUS_MAP[property]?.map((x) => (
|
||||
<Select.Option key={x.name} value={x.value}>
|
||||
{x.name}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -239,17 +251,53 @@ const ViewCreateModal = ({
|
||||
{operationElement}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'value']}
|
||||
rules={[{ required: true, message: '请输入内容' }]}
|
||||
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;
|
||||
}}
|
||||
>
|
||||
{['In', 'Nin'].includes(
|
||||
form.getFieldValue(['filters', index, 'operation']),
|
||||
) ? (
|
||||
statusElement
|
||||
) : (
|
||||
<Input placeholder="请输入内容" />
|
||||
)}
|
||||
{() => {
|
||||
const property = form.getFieldValue([
|
||||
'filters',
|
||||
index,
|
||||
'property',
|
||||
]) as 'status';
|
||||
const operate = form.getFieldValue([
|
||||
'filters',
|
||||
name,
|
||||
'operation',
|
||||
]);
|
||||
return (
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'value']}
|
||||
rules={[
|
||||
{ required: true, message: '请输入内容' },
|
||||
]}
|
||||
>
|
||||
{EOperation[operate] === 'select' ? (
|
||||
statusElement(property)
|
||||
) : (
|
||||
<Input placeholder="请输入内容" />
|
||||
)}
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
{index !== 0 && (
|
||||
<MinusCircleOutlined onClick={() => remove(name)} />
|
||||
|
||||
@@ -49,6 +49,7 @@ const EditModal = ({
|
||||
const { theme } = useTheme();
|
||||
const editorRef = useRef<any>(null);
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
const [currentPid, setCurrentPid] = useState(null);
|
||||
|
||||
const cancel = () => {
|
||||
handleCancel();
|
||||
@@ -94,6 +95,7 @@ const EditModal = ({
|
||||
.then(({ code, data }) => {
|
||||
if (code === 200) {
|
||||
setIsRunning(true);
|
||||
setCurrentPid(data);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -102,13 +104,12 @@ const EditModal = ({
|
||||
if (!cNode || !cNode.title) {
|
||||
return;
|
||||
}
|
||||
const content = editorRef.current.getValue().replace(/\r\n/g, '\n');
|
||||
request
|
||||
.put(`${config.apiPrefix}scripts/stop`, {
|
||||
data: {
|
||||
filename: cNode.title,
|
||||
path: cNode.parent || '',
|
||||
content,
|
||||
pid: currentPid,
|
||||
},
|
||||
})
|
||||
.then(({ code, data }) => {
|
||||
@@ -271,7 +272,7 @@ const EditModal = ({
|
||||
content:
|
||||
editorRef.current &&
|
||||
editorRef.current.getValue().replace(/\r\n/g, '\n'),
|
||||
filename: cNode?.title,
|
||||
...cNode,
|
||||
}}
|
||||
/>
|
||||
<SettingModal
|
||||
|
||||
@@ -39,6 +39,8 @@ import { depthFirstSearch } from '@/utils';
|
||||
import { SharedContext } from '@/layouts';
|
||||
import useFilterTreeData from '@/hooks/useFilterTreeData';
|
||||
import uniq from 'lodash/uniq';
|
||||
import IconFont from '@/components/iconfont';
|
||||
import RenameModal from './renameModal';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
@@ -64,20 +66,22 @@ const Script = () => {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const editorRef = useRef<any>(null);
|
||||
const [isAddFileModalVisible, setIsAddFileModalVisible] = useState(false);
|
||||
const [isRenameFileModalVisible, setIsRenameFileModalVisible] = useState(false);
|
||||
const [currentNode, setCurrentNode] = useState<any>();
|
||||
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
|
||||
|
||||
const getScripts = () => {
|
||||
setLoading(true);
|
||||
const getScripts = (needLoading: boolean = true) => {
|
||||
needLoading && setLoading(true);
|
||||
request
|
||||
.get(`${config.apiPrefix}scripts`)
|
||||
.then(({ code, data }) => {
|
||||
if (code === 200) {
|
||||
setData(data);
|
||||
initState();
|
||||
initGetScript();
|
||||
}
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
.finally(() => needLoading && setLoading(false));
|
||||
};
|
||||
|
||||
const getDetail = (node: any) => {
|
||||
@@ -287,6 +291,15 @@ const Script = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const renameFile = () => {
|
||||
setIsRenameFileModalVisible(true);
|
||||
}
|
||||
|
||||
const handleRenameFileCancel = () => {
|
||||
setIsRenameFileModalVisible(false);
|
||||
getScripts(false);
|
||||
}
|
||||
|
||||
const addFile = () => {
|
||||
setIsAddFileModalVisible(true);
|
||||
};
|
||||
@@ -381,6 +394,9 @@ const Script = () => {
|
||||
case 'delete':
|
||||
deleteFile();
|
||||
break;
|
||||
case 'rename':
|
||||
renameFile();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -407,6 +423,12 @@ const Script = () => {
|
||||
icon: <EditOutlined />,
|
||||
disabled: !select,
|
||||
},
|
||||
{
|
||||
label: '重命名',
|
||||
key: 'rename',
|
||||
icon: <IconFont type="ql-icon-rename" />,
|
||||
disabled: !select,
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
key: 'delete',
|
||||
@@ -471,6 +493,14 @@ const Script = () => {
|
||||
icon={<EditOutlined />}
|
||||
/>
|
||||
</Tooltip>,
|
||||
<Tooltip title="重命名">
|
||||
<Button
|
||||
disabled={!select}
|
||||
type="primary"
|
||||
onClick={renameFile}
|
||||
icon={<IconFont type="ql-icon-rename" />}
|
||||
/>
|
||||
</Tooltip>,
|
||||
<Tooltip title="删除">
|
||||
<Button
|
||||
type="primary"
|
||||
@@ -585,6 +615,11 @@ const Script = () => {
|
||||
treeData={data}
|
||||
handleCancel={addFileModalClose}
|
||||
/>
|
||||
<RenameModal
|
||||
visible={isRenameFileModalVisible}
|
||||
handleCancel={handleRenameFileCancel}
|
||||
currentNode={currentNode}
|
||||
/>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
@@ -17,7 +17,7 @@ const SaveModal = ({
|
||||
|
||||
const handleOk = async (values: any) => {
|
||||
setLoading(true);
|
||||
const payload = { ...file, ...values, originFilename: file.filename };
|
||||
const payload = { ...file, ...values, originFilename: file.title };
|
||||
request
|
||||
.post(`${config.apiPrefix}scripts`, {
|
||||
data: payload,
|
||||
@@ -60,7 +60,7 @@ const SaveModal = ({
|
||||
form={form}
|
||||
layout="vertical"
|
||||
name="script_modal"
|
||||
initialValues={file}
|
||||
initialValues={{ filename: file?.title, path: file?.parent || '' }}
|
||||
>
|
||||
<Form.Item
|
||||
name="filename"
|
||||
|
||||
@@ -38,6 +38,14 @@ const About = ({ systemInfo }: { systemInfo: SharedContext['systemInfo'] }) => {
|
||||
<Descriptions.Item label="更新ID" span={3}>
|
||||
{systemInfo.lastCommitId}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="更新日志" span={3}>
|
||||
<Link
|
||||
href={`https://qn.whyour.cn/version.yaml?t=${Date.now()}`}
|
||||
target="_blank"
|
||||
>
|
||||
查看
|
||||
</Link>
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<div>
|
||||
<Link
|
||||
|
||||
@@ -2,11 +2,10 @@ import React, { useEffect, useState, useRef } from 'react';
|
||||
import { Statistic, Modal, Tag, Button, Spin, message } from 'antd';
|
||||
import { request } from '@/utils/http';
|
||||
import config from '@/utils/config';
|
||||
import { version } from '../../version';
|
||||
|
||||
const { Countdown } = Statistic;
|
||||
|
||||
const CheckUpdate = ({ socketMessage }: any) => {
|
||||
const CheckUpdate = ({ socketMessage, systemInfo }: any) => {
|
||||
const [updateLoading, setUpdateLoading] = useState(false);
|
||||
const [value, setValue] = useState('');
|
||||
const modalRef = useRef<any>();
|
||||
@@ -23,7 +22,7 @@ const CheckUpdate = ({ socketMessage }: any) => {
|
||||
if (data.hasNewVersion) {
|
||||
showConfirmUpdateModal(data);
|
||||
} else {
|
||||
showForceUpdateModal();
|
||||
showForceUpdateModal(data);
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -36,7 +35,7 @@ const CheckUpdate = ({ socketMessage }: any) => {
|
||||
});
|
||||
};
|
||||
|
||||
const showForceUpdateModal = () => {
|
||||
const showForceUpdateModal = (data: any) => {
|
||||
Modal.confirm({
|
||||
width: 500,
|
||||
title: '更新',
|
||||
@@ -44,7 +43,7 @@ const CheckUpdate = ({ socketMessage }: any) => {
|
||||
<>
|
||||
<div>已经是最新版了!</div>
|
||||
<div style={{ fontSize: 12, fontWeight: 400, marginTop: 5 }}>
|
||||
青龙 {version} 是目前检测到的最新可用版本了。
|
||||
青龙 {data.lastVersion} 是目前检测到的最新可用版本了。
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
@@ -70,14 +69,13 @@ const CheckUpdate = ({ socketMessage }: any) => {
|
||||
<>
|
||||
<div>更新可用</div>
|
||||
<div style={{ fontSize: 12, fontWeight: 400, marginTop: 5 }}>
|
||||
新版本{lastVersion}可用。你使用的版本为{version}。
|
||||
新版本 {lastVersion} 可用,你使用的版本为 {systemInfo.version}。
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
content: (
|
||||
<pre
|
||||
style={{
|
||||
paddingTop: 15,
|
||||
fontSize: 12,
|
||||
fontWeight: 400,
|
||||
}}
|
||||
|
||||
@@ -416,7 +416,10 @@ const Setting = () => {
|
||||
</Input.Group>
|
||||
</Form.Item>
|
||||
<Form.Item label="检查更新" name="update">
|
||||
<CheckUpdate socketMessage={socketMessage} />
|
||||
<CheckUpdate
|
||||
systemInfo={systemInfo}
|
||||
socketMessage={socketMessage}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
),
|
||||
|
||||
+29
-1
@@ -198,7 +198,7 @@ export function getTableScroll({
|
||||
if (tHeader) {
|
||||
mainTop = tHeader.getBoundingClientRect().top;
|
||||
}
|
||||
|
||||
|
||||
//窗体高度-表格内容顶部的高度-表格内容底部的高度
|
||||
let height = document.body.clientHeight - mainTop - extraHeight;
|
||||
return height;
|
||||
@@ -276,3 +276,31 @@ export function logEnded(log: string): boolean {
|
||||
const endTips = [LOG_END_SYMBOL, '执行结束'];
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -1,9 +1,8 @@
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Integrations } from '@sentry/tracing';
|
||||
import { loader } from '@monaco-editor/react';
|
||||
import { version } from '../version';
|
||||
|
||||
export function init() {
|
||||
export function init(version: string) {
|
||||
// sentry监控 init
|
||||
Sentry.init({
|
||||
dsn: 'https://3406424fb1dc4813a62d39e844a9d0ac@o1098464.ingest.sentry.io/6122818',
|
||||
|
||||
@@ -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. 其他优化
|
||||
`;
|
||||
Vendored
+2
@@ -8,3 +8,5 @@ declare module '*.svg' {
|
||||
const url: string;
|
||||
export default url;
|
||||
}
|
||||
|
||||
declare module 'pstree.remy';
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
version: 2.15.4
|
||||
changeLogLink: https://t.me/jiao_long/355
|
||||
changeLog: |
|
||||
1. 脚本管理支持重命名文件/文件夹
|
||||
2. 关于增加更新日志查看
|
||||
3. 修复定时任务脚本识别和跳转
|
||||
Reference in New Issue
Block a user