Compare commits

..
60 Commits
Author SHA1 Message Date
whyour e1655455d8 更新版本 v2.15.8 2023-02-17 23:11:41 +08:00
whyour 9921dab064 修复日志文件名格式化 2023-02-17 23:03:06 +08:00
whyour 403ab7f0f0 修改日志名称格式 2023-02-17 22:49:06 +08:00
whyour 9bc7ca4094 增加 NODE_PATH 环境变量 2023-02-15 22:27:40 +08:00
whyour e924aa76b0 修复环境变量位置重置逻辑 2023-02-15 11:54:51 +08:00
whyour 0c46606e14 修复添加文件类型订阅 2023-02-14 21:15:59 +08:00
whyour dbfa4049f5 修复订阅格式化参数 2023-02-14 11:10:25 +08:00
whyour 622fe2a8f8 修复订阅自动增加/删除任务默认值 2023-02-14 11:07:16 +08:00
whyour 7bce5c4f6a 任务增加关联订阅 2023-02-13 23:50:01 +08:00
whyour 1f7f2c8971 修复订阅生成 ssh 配置逻辑,自动添加/删除任务 2023-02-13 23:12:55 +08:00
whyour 437a849c2a 更新版本 v2.15.7 2023-02-12 17:09:07 +08:00
whyour 8f7261dd50 修复 workflow 2023-02-12 17:08:47 +08:00
whyour 76fa82c3a7 订阅支持自动添加和删除任务设置 2023-02-09 00:20:28 +08:00
whyour 46c9774efa 更新 workflow 2023-02-07 21:52:24 +08:00
whyour 1bc64a23e3 修复查看任务日志,列表位置重置 2023-02-07 21:29:56 +08:00
whyour cc9a17afa3 修复定时任务列表格式化 crontab 2023-02-06 23:26:59 +08:00
whyour fcbcfc3a03 更新 sqlite3 2023-02-06 16:24:31 +08:00
whyour 0144f3b131 修改任务日志自动滚动逻辑 2023-02-05 20:57:37 +08:00
whyour 518436d5ca 修复脚本管理跳转逻辑 2023-02-04 00:20:44 +08:00
whyour 1961a6b913 修复定时任务表格自动换行 2023-02-03 23:45:55 +08:00
whyour 5edebdd5f3 修复创建日志目录逻辑 2023-02-03 23:30:02 +08:00
whyour 48edf2c212 增加 ppc64le/s390x/386 镜像 2023-02-03 22:38:00 +08:00
whyour 9b16dca9da 修改定时任务状态宽度 2023-02-02 23:39:18 +08:00
whyour 9479c2498c 移除 console 2023-02-02 13:20:47 +08:00
whyour 4a18bcabe2 修复移动端新建脚本 2023-02-02 13:17:00 +08:00
whyour 34f690c53d 修改 shell 参数处理逻辑 2023-02-02 12:14:35 +08:00
whyour e70cd6579f 更新版本 v2.15.6 2023-02-01 22:46:23 +08:00
whyour 99c047e9c7 修复定时任务表格滚动 2023-02-01 22:45:47 +08:00
whyour 978ae326f8 更新 sqlite3 依赖 2023-02-01 15:30:53 +08:00
whyour b01f27f2d3 修改更新列表后刷新逻辑 2023-01-31 23:52:02 +08:00
whyour 4d34e0dd96 修复环境变量滚动卡顿 2023-01-31 23:46:42 +08:00
whyour 9b0065e533 修复定时任务过多搜索卡顿 2023-01-31 23:30:47 +08:00
whyour 34a51feee9 修改定时任务列表title 2023-01-31 17:42:30 +08:00
whyour 679091de59 修改定时任务输入提示 2023-01-31 17:41:35 +08:00
whyour c6220cb920 修改定时任务查询日志逻辑 2023-01-29 18:07:57 +08:00
whyour 633b30269c 修复定时任务脚本跳转 2023-01-29 10:57:08 +08:00
whyour 8ad26267d8 修改 task before/after 逻辑 2023-01-20 23:38:32 +08:00
OceanJiangandGitHub 7cfb125682 实现 Node.js 部分 SMTP 推送 (#1776) 2023-01-14 19:53:36 +08:00
Wankko ReeandGitHub a2dc5de5ce 添加基于 SMTP 邮件 的消息通知服务 (#1763) 2023-01-14 19:48:41 +08:00
whyour 4f41ff29ec 修改资源通知逻辑 2023-01-14 19:46:33 +08:00
whyour 13d95b8c19 修复日志滚动逻辑 2023-01-11 23:46:47 +08:00
whyour 335c13ab44 修改日志滚动逻辑 2023-01-11 23:44:04 +08:00
whyour 015cdc10a7 publicPath使用相对目录 2023-01-11 22:52:51 +08:00
whyour a5075ab28f 修改新建任务命令提示 2023-01-11 22:14:32 +08:00
whyour 2e3c3274c7 修复antd兼容性,日志详情自动滚动 2023-01-11 22:03:44 +08:00
whyour 443c581955 修改定时任务 shouldCellUpdate 2023-01-08 22:30:29 +08:00
whyour 5e41faf590 修改 monaco-editor 版本 2023-01-08 22:16:22 +08:00
whyour 0579fb83d3 修改环境变量值省略展示 2023-01-08 21:56:29 +08:00
whyour 2864845d5b 优化定时任务表格渲染 2023-01-08 18:15:18 +08:00
whyour 659dddace7 梅子留酸软齿牙,芭蕉分绿与窗纱。 2023-01-08 16:49:21 +08:00
whyour b096f30558 更新 dockerfile 2023-01-08 16:39:01 +08:00
whyour dcfb72143a 修复定时任务每页数据量大时搜索卡顿 2023-01-07 18:19:03 +08:00
whyour c63bcd7706 修改docker编译 2023-01-07 18:16:39 +08:00
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
60 changed files with 4656 additions and 2707 deletions
+2 -1
View File
@@ -9,4 +9,5 @@ SECRET='whyour'
QINIU_AK='' QINIU_AK=''
QINIU_SK='' QINIU_SK=''
QINIU_SCOPE='' QINIU_SCOPE=''
TEMP=''
+9 -7
View File
@@ -5,15 +5,11 @@ on:
branches: branches:
- 'master' - 'master'
- 'develop' - 'develop'
# Sequence of patterns matched against refs/tags
tags: tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
schedule: schedule:
# 参考 https://jasonet.co/posts/scheduled-actions/ # 参考 https://jasonet.co/posts/scheduled-actions/
# note: 这里是GMT时间,北京时间减去八小时即可。如北京时间 22:30 => GMT 14:30 - cron: '00 14 * * *' # GMT 15:00 => 北京时间 23:00
# minute hour day month dayOfWeek
- cron: '00 14 * * *' # GMT 14:00 => 北京时间 22:00
#- cron: '30 16 * * *' # GMT 16:30(前一天) => 北京时间 00:30
workflow_dispatch: workflow_dispatch:
jobs: jobs:
@@ -89,6 +85,8 @@ jobs:
git push --force --quiet gitlab ${GITHUB_BRANCH}:${GITHUB_BRANCH} git push --force --quiet gitlab ${GITHUB_BRANCH}:${GITHUB_BRANCH}
build: build:
if: github.ref != 'refs/heads/master'
needs: build-static needs: build-static
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -155,12 +153,16 @@ jobs:
build-args: | build-args: |
MAINTAINER=${{ github.repository_owner }} MAINTAINER=${{ github.repository_owner }}
QL_BRANCH=${{ github.ref_name }} QL_BRANCH=${{ github.ref_name }}
SOURCE_COMMIT=${{ github.sha }}
network: host network: host
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64 platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x,linux/386
context: docker/ context: .
file: ./docker/Dockerfile
push: true push: true
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=whyour/qinglong:cache
cache-to: type=registry,ref=whyour/qinglong:cache,mode=max
- name: Image digest - name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }} run: echo ${{ steps.docker_build.outputs.digest }}
+2 -1
View File
@@ -6,10 +6,11 @@ export default defineConfig({
antd: {}, antd: {},
outputPath: 'static/dist', outputPath: 'static/dist',
fastRefresh: true, fastRefresh: true,
favicons: ['/images/favicon.svg'], favicons: ['./images/favicon.svg'],
mfsu: { mfsu: {
strategy: 'eager', strategy: 'eager',
}, },
publicPath: process.env.NODE_ENV === 'production' ? './' : '/',
proxy: { proxy: {
'/api/public': { '/api/public': {
target: 'http://127.0.0.1:5400/', target: 'http://127.0.0.1:5400/',
+2
View File
@@ -160,6 +160,7 @@ export default (app: Router) => {
schedule: Joi.string().required(), schedule: Joi.string().required(),
name: Joi.string().optional(), name: Joi.string().optional(),
labels: Joi.array().optional(), labels: Joi.array().optional(),
sub_id: Joi.number().optional().allow(null),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
@@ -316,6 +317,7 @@ export default (app: Router) => {
command: Joi.string().required(), command: Joi.string().required(),
schedule: Joi.string().required(), schedule: Joi.string().required(),
name: Joi.string().optional().allow(null), name: Joi.string().optional().allow(null),
sub_id: Joi.number().optional().allow(null),
id: Joi.number().required(), id: Joi.number().required(),
}), }),
}), }),
+4
View File
@@ -50,6 +50,8 @@ export default (app: Router) => {
schedule_type: Joi.string().required(), schedule_type: Joi.string().required(),
alias: Joi.string().required(), alias: Joi.string().required(),
proxy: Joi.string().optional().allow('').allow(null), proxy: Joi.string().optional().allow('').allow(null),
autoAddCron: Joi.boolean().optional().allow('').allow(null),
autoDelCron: Joi.boolean().optional().allow('').allow(null),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
@@ -179,6 +181,8 @@ export default (app: Router) => {
sub_after: Joi.string().optional().allow('').allow(null), sub_after: Joi.string().optional().allow('').allow(null),
alias: Joi.string().required(), alias: Joi.string().required(),
proxy: Joi.string().optional().allow('').allow(null), proxy: Joi.string().optional().allow('').allow(null),
autoAddCron: Joi.boolean().optional().allow('').allow(null),
autoDelCron: Joi.boolean().optional().allow('').allow(null),
id: Joi.number().required(), id: Joi.number().required(),
}), }),
}), }),
+7 -1
View File
@@ -1 +1,7 @@
export const LOG_END_SYMBOL = '\n          '; export const LOG_END_SYMBOL = '     ';
export const TASK_COMMAND = 'task';
export const QL_COMMAND = 'ql';
export const TASK_PREFIX = `${TASK_COMMAND} `;
export const QL_PREFIX = `${QL_COMMAND} `;
+42
View File
@@ -0,0 +1,42 @@
import { Subscription } from '../data/subscription';
import isNil from 'lodash/isNil';
export function formatUrl(doc: Subscription) {
let url = doc.url;
let host = '';
if (doc.type === 'private-repo') {
if (doc.pull_type === 'ssh-key') {
host = doc.url!.replace(/.*\@([^\:]+)\:.*/, '$1');
url = doc.url!.replace(host, doc.alias);
} else {
host = doc.url!.replace(/.*\:\/\/([^\/]+)\/.*/, '$1');
const { username, password } = doc.pull_option as any;
url = doc.url!.replace(host, `${username}:${password}@${host}`);
}
}
return { url, host };
}
export function formatCommand(doc: Subscription, url?: string) {
let command = `SUB_ID=${doc.id} ql `;
let _url = url || formatUrl(doc).url;
const {
type,
whitelist,
blacklist,
dependences,
branch,
extensions,
proxy,
autoAddCron,
autoDelCron,
} = doc;
if (type === 'file') {
command += `raw "${_url}"`;
} else {
command += `repo "${_url}" "${whitelist || ''}" "${blacklist || ''}" "${dependences || ''
}" "${branch || ''}" "${extensions || ''}" "${proxy || ''}" "${isNil(autoAddCron) ? true : Boolean(autoAddCron)
}" "${isNil(autoDelCron) ? true : Boolean(autoDelCron)}"`;
}
return command;
}
+3
View File
@@ -17,6 +17,7 @@ export class Crontab {
labels?: string[]; labels?: string[];
last_running_time?: number; last_running_time?: number;
last_execution_time?: number; last_execution_time?: number;
sub_id?: number;
constructor(options: Crontab) { constructor(options: Crontab) {
this.name = options.name; this.name = options.name;
@@ -37,6 +38,7 @@ export class Crontab {
this.labels = options.labels || []; this.labels = options.labels || [];
this.last_running_time = options.last_running_time || 0; this.last_running_time = options.last_running_time || 0;
this.last_execution_time = options.last_execution_time || 0; this.last_execution_time = options.last_execution_time || 0;
this.sub_id = options.sub_id;
} }
} }
@@ -72,4 +74,5 @@ export const CrontabModel = sequelize.define<CronInstance>('Crontab', {
labels: DataTypes.JSON, labels: DataTypes.JSON,
last_running_time: DataTypes.NUMBER, last_running_time: DataTypes.NUMBER,
last_execution_time: DataTypes.NUMBER, last_execution_time: DataTypes.NUMBER,
sub_id: { type: DataTypes.NUMBER, allowNull: true },
}); });
+1 -1
View File
@@ -28,7 +28,7 @@ export enum EnvStatus {
export const maxPosition = 9000000000000000; export const maxPosition = 9000000000000000;
export const initPosition = 4500000000000000; export const initPosition = 4500000000000000;
export const stepPosition = 10000000; export const stepPosition = 10000000000;
export const minPosition = 100; export const minPosition = 100;
interface EnvInstance extends Model<Env, Env>, Env {} interface EnvInstance extends Model<Env, Env>, Env {}
+6
View File
@@ -29,6 +29,8 @@ export class Subscription {
sub_before?: string; sub_before?: string;
sub_after?: string; sub_after?: string;
proxy?: string; proxy?: string;
autoAddCron?: 1 | 0;
autoDelCron?: 1 | 0;
constructor(options: Subscription) { constructor(options: Subscription) {
this.id = options.id; this.id = options.id;
@@ -56,6 +58,8 @@ export class Subscription {
this.sub_before = options.sub_before; this.sub_before = options.sub_before;
this.sub_after = options.sub_after; this.sub_after = options.sub_after;
this.proxy = options.proxy; this.proxy = options.proxy;
this.autoAddCron = options.autoAddCron ? 1 : 0;
this.autoDelCron = options.autoDelCron ? 1 : 0;
} }
} }
@@ -105,5 +109,7 @@ export const SubscriptionModel = sequelize.define<SubscriptionInstance>(
schedule_type: DataTypes.STRING, schedule_type: DataTypes.STRING,
alias: { type: DataTypes.STRING, unique: 'alias' }, alias: { type: DataTypes.STRING, unique: 'alias' },
proxy: { type: DataTypes.STRING, allowNull: true }, proxy: { type: DataTypes.STRING, allowNull: true },
autoAddCron: { type: DataTypes.NUMBER, allowNull: true },
autoDelCron: { type: DataTypes.NUMBER, allowNull: true },
}, },
); );
+13
View File
@@ -36,6 +36,19 @@ export default async () => {
try { try {
await sequelize.query('alter table CrontabViews add column type NUMBER'); await sequelize.query('alter table CrontabViews add column type NUMBER');
} catch (error) {} } catch (error) {}
try {
await sequelize.query(
'alter table Subscriptions add column autoAddCron NUMBER',
);
} catch (error) {}
try {
await sequelize.query(
'alter table Subscriptions add column autoDelCron NUMBER',
);
} catch (error) {}
try {
await sequelize.query('alter table Crontabs add column sub_id NUMBER');
} catch (error) {}
// 2.10-2.11 升级 // 2.10-2.11 升级
const cronDbFile = path.join(config.rootPath, 'db/crontab.db'); const cronDbFile = path.join(config.rootPath, 'db/crontab.db');
+5 -8
View File
@@ -21,6 +21,7 @@ export default async () => {
name: '生成token', name: '生成token',
command: tokenCommand, command: tokenCommand,
}; };
await scheduleService.cancelIntervalTask(cron);
scheduleService.createIntervalTask(cron, { scheduleService.createIntervalTask(cron, {
days: 28, days: 28,
}); });
@@ -28,12 +29,13 @@ export default async () => {
// 运行删除日志任务 // 运行删除日志任务
const data = await systemService.getLogRemoveFrequency(); const data = await systemService.getLogRemoveFrequency();
if (data && data.info && data.info.frequency) { if (data && data.info && data.info.frequency) {
const cron = { const rmlogCron = {
id: data.id, id: data.id,
name: '删除日志', name: '删除日志',
command: `ql rmlog ${data.info.frequency}`, command: `ql rmlog ${data.info.frequency}`,
}; };
scheduleService.createIntervalTask(cron, { await scheduleService.cancelIntervalTask(rmlogCron);
scheduleService.createIntervalTask(rmlogCron, {
days: data.info.frequency, days: data.info.frequency,
}); });
} }
@@ -41,11 +43,6 @@ export default async () => {
// 运行所有订阅 // 运行所有订阅
const subs = await subscriptionService.list(); const subs = await subscriptionService.list();
for (const sub of subs) { for (const sub of subs) {
await subscriptionService.handleTask( subscriptionService.handleTask(sub, !sub.is_disabled, !sub.is_disabled);
sub,
!sub.is_disabled,
true,
!sub.is_disabled,
);
} }
}; };
+6 -9
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', if (err || stderr) {
(err, stdout, stderr) => { return res.send({ code: 400, message: (err && err.message) || stderr });
if (err || 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}`);
}); });
+25 -84
View File
@@ -14,10 +14,11 @@ import {
import { promises, existsSync } from 'fs'; import { promises, existsSync } from 'fs';
import { Op, where, col as colFn } from 'sequelize'; import { Op, where, col as colFn } from 'sequelize';
import path from 'path'; import path from 'path';
import { TASK_PREFIX, QL_PREFIX } from '../config/const';
@Service() @Service()
export default class CronService { export default class CronService {
constructor(@Inject('logger') private logger: winston.Logger) {} constructor(@Inject('logger') private logger: winston.Logger) { }
private isSixCron(cron: Crontab) { private isSixCron(cron: Crontab) {
const { schedule } = cron; const { schedule } = cron;
@@ -31,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;
} }
@@ -42,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;
} }
@@ -81,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[]) {
@@ -361,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') ||
@@ -408,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) {
@@ -426,104 +427,46 @@ export default class CronService {
const logFileExist = doc.log_path && (await fileExist(absolutePath)); const logFileExist = doc.log_path && (await fileExist(absolutePath));
if (logFileExist) { if (logFileExist) {
return getFileContentByName(`${absolutePath}`); return getFileContentByName(`${absolutePath}`);
}
const [, commandStr, url] = doc.command.split(/ +/);
let logPath = this.getKey(commandStr);
const isQlCommand = doc.command.startsWith('ql ');
const key =
(url && ['repo', 'raw'].includes(commandStr) && this.getKey(url)) ||
logPath;
if (isQlCommand) {
logPath = 'update';
}
let logDir = `${config.logPath}${logPath}`;
if (existsSync(logDir)) {
let files = await promises.readdir(logDir);
if (isQlCommand) {
files = files.filter((x) => x.includes(key));
}
return getFileContentByName(`${logDir}/${files[files.length - 1]}`);
} else { } else {
return ''; return '任务未运行或运行失败,请尝试手动运行';
} }
} }
public async logs(id: number) { public async logs(id: number) {
const doc = await this.getDb({ id }); const doc = await this.getDb({ id });
if (!doc) { if (!doc || !doc.log_path) {
return []; return [];
} }
if (doc.log_path) { const relativeDir = path.dirname(`${doc.log_path}`);
const relativeDir = path.dirname(`${doc.log_path}`); const dir = path.resolve(config.logPath, relativeDir);
const dir = path.resolve(config.logPath, relativeDir); if (existsSync(dir)) {
if (existsSync(dir)) { let files = await promises.readdir(dir);
let files = await promises.readdir(dir);
return files
.map((x) => ({
filename: x,
directory: relativeDir.replace(config.logPath, ''),
time: fs.statSync(`${dir}/${x}`).mtime.getTime(),
}))
.sort((a, b) => b.time - a.time);
}
}
const [, commandStr, url] = doc.command.split(/ +/);
let logPath = this.getKey(commandStr);
const isQlCommand = doc.command.startsWith('ql ');
const key =
(url && ['repo', 'raw'].includes(commandStr) && this.getKey(url)) ||
logPath;
if (isQlCommand) {
logPath = 'update';
}
let logDir = `${config.logPath}${logPath}`;
if (existsSync(logDir)) {
let files = await promises.readdir(logDir);
if (isQlCommand) {
files = files.filter((x) => x.includes(key));
}
return files return files
.map((x) => ({ .map((x) => ({
filename: x, filename: x,
directory: logPath, directory: relativeDir.replace(config.logPath, ''),
time: fs.statSync(`${logDir}/${x}`).mtime.getTime(), time: fs.statSync(`${dir}/${x}`).mtime.getTime(),
})) }))
.sort((a, b) => b.time - a.time); .sort((a, b) => b.time - a.time);
} else { } else {
return []; return [];
} }
}
private getKey(command: string): string {
const start =
command.lastIndexOf('/') !== -1 ? command.lastIndexOf('/') + 1 : 0;
const end =
command.lastIndexOf('.') !== -1
? command.lastIndexOf('.')
: command.length;
const tmpStr = command.substring(0, start - 1);
let index = 0;
if (tmpStr.lastIndexOf('/') !== -1 && tmpStr.startsWith('http')) {
index = tmpStr.lastIndexOf('/');
} else if (tmpStr.lastIndexOf(':') !== -1 && tmpStr.startsWith('git@')) {
index = tmpStr.lastIndexOf(':');
}
if (index) {
return `${tmpStr.substring(index + 1)}_${command.substring(start, end)}`;
} else {
return command.substring(start, end);
}
} }
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) => {
@@ -546,9 +489,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: {} });
} }
+4 -4
View File
@@ -16,7 +16,7 @@ import { Op } from 'sequelize';
@Service() @Service()
export default class EnvService { export default class EnvService {
constructor(@Inject('logger') private logger: winston.Logger) {} constructor(@Inject('logger') private logger: winston.Logger) { }
public async create(payloads: Env[]): Promise<Env[]> { public async create(payloads: Env[]): Promise<Env[]> {
const envs = await this.envs(); const envs = await this.envs();
@@ -92,13 +92,13 @@ export default class EnvService {
position: this.getPrecisionPosition(targetPosition), position: this.getPrecisionPosition(targetPosition),
}); });
await this.checkPosition(targetPosition); await this.checkPosition(targetPosition, envs[toIndex].position!);
return newDoc; return newDoc;
} }
private async checkPosition(position: number) { private async checkPosition(position: number, edge: number = 0) {
const precisionPosition = parseFloat(position.toPrecision(16)); const precisionPosition = parseFloat(position.toPrecision(16));
if (precisionPosition < minPosition || precisionPosition > maxPosition) { if (precisionPosition < minPosition || precisionPosition > maxPosition || Math.abs(precisionPosition - edge) < minPosition) {
const envs = await this.envs(); const envs = await this.envs();
let position = initPosition; let position = initPosition;
for (const env of envs) { for (const env of envs) {
+9 -11
View File
@@ -5,8 +5,8 @@ import { ChildProcessWithoutNullStreams, exec, spawn } from 'child_process';
import { import {
ToadScheduler, ToadScheduler,
LongIntervalJob, LongIntervalJob,
AsyncTask,
SimpleIntervalSchedule, SimpleIntervalSchedule,
Task,
} from 'toad-scheduler'; } from 'toad-scheduler';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
@@ -63,8 +63,8 @@ export default class ScheduleService {
}); });
cp.stderr.on('data', async (data) => { cp.stderr.on('data', async (data) => {
this.logger.error( this.logger.info(
'执行任务 %s 失败,时间:%s, 错误信息:%j', '[执行任务失败] %s,时间:%s, 错误信息:%j',
command, command,
new Date().toLocaleString(), new Date().toLocaleString(),
data.toString(), data.toString(),
@@ -74,7 +74,7 @@ export default class ScheduleService {
cp.on('error', async (err) => { cp.on('error', async (err) => {
this.logger.error( this.logger.error(
'创建任务 %s 失败,时间:%s, 错误信息:%j', '[创建任务失败] %s,时间:%s, 错误信息:%j',
command, command,
new Date().toLocaleString(), new Date().toLocaleString(),
err, err,
@@ -84,7 +84,7 @@ export default class ScheduleService {
cp.on('exit', async (code, signal) => { cp.on('exit', async (code, signal) => {
this.logger.info( this.logger.info(
`任务 ${command} 进程id: ${cp.pid} 退出,退出码 ${code}`, `[任务退出] ${command} 进程id: ${cp.pid},退出码 ${code}`,
); );
}); });
@@ -154,12 +154,10 @@ export default class ScheduleService {
name, name,
command, command,
); );
const task = new AsyncTask( const task = new Task(
name, name,
async () => { () => {
return new Promise(async (resolve, reject) => { this.runTask(command, callbacks);
await this.runTask(command, callbacks);
});
}, },
(err) => { (err) => {
this.logger.error( this.logger.error(
@@ -180,7 +178,7 @@ export default class ScheduleService {
this.intervalSchedule.addIntervalJob(job); this.intervalSchedule.addIntervalJob(job);
if (runImmediately) { if (runImmediately) {
await this.runTask(command, callbacks); this.runTask(command, callbacks);
} }
} }
+3 -3
View File
@@ -6,7 +6,7 @@ import SockService from './sock';
import CronService from './cron'; import CronService from './cron';
import ScheduleService, { TaskCallbacks } from './schedule'; import ScheduleService, { TaskCallbacks } from './schedule';
import config from '../config'; import config from '../config';
import { LOG_END_SYMBOL } from '../config/const'; import { TASK_COMMAND } from '../config/const';
import { getPid, killTask } from '../config/util'; import { getPid, killTask } from '../config/util';
@Service() @Service()
@@ -42,7 +42,7 @@ 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`;
const pid = await this.scheduleService.runTask( const pid = await this.scheduleService.runTask(
command, command,
this.taskCallbacks(filePath), this.taskCallbacks(filePath),
@@ -56,7 +56,7 @@ export default class ScriptService {
let str = ''; let str = '';
if (!pid) { if (!pid) {
const relativePath = path.relative(config.scriptPath, filePath); const relativePath = path.relative(config.scriptPath, filePath);
pid = await getPid(`task -l ${relativePath} now`); pid = await getPid(`${TASK_COMMAND} -l ${relativePath} now`);
} }
try { try {
await killTask(pid); await killTask(pid);
+29 -8
View File
@@ -1,8 +1,10 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import winston from 'winston'; import winston from 'winston';
import fs from 'fs'; import fs, { existsSync } from 'fs';
import os from 'os'; import os from 'os';
import path from 'path'; import path from 'path';
import { Subscription } from '../data/subscription';
import { formatUrl } from '../config/subscription';
@Service() @Service()
export default class SshKeyService { export default class SshKeyService {
@@ -32,7 +34,10 @@ export default class SshKeyService {
private removePrivateKeyFile(alias: string): void { private removePrivateKeyFile(alias: string): void {
try { try {
fs.unlinkSync(`${this.sshPath}/${alias}`); const filePath = path.join(this.sshPath, alias);
if (existsSync(filePath)) {
fs.unlinkSync(`${this.sshPath}/${alias}`);
}
} catch (error) { } catch (error) {
this.logger.error('删除私钥文件失败', error); this.logger.error('删除私钥文件失败', error);
} }
@@ -47,16 +52,14 @@ export default class SshKeyService {
host = `ssh.github.com\n Port 443\n HostkeyAlgorithms +ssh-rsa\n PubkeyAcceptedAlgorithms +ssh-rsa`; host = `ssh.github.com\n Port 443\n HostkeyAlgorithms +ssh-rsa\n PubkeyAcceptedAlgorithms +ssh-rsa`;
} }
const proxyStr = proxy ? ` ProxyCommand nc -v -x ${proxy} %h %p\n` : ''; const proxyStr = proxy ? ` ProxyCommand nc -v -x ${proxy} %h %p\n` : '';
return `\nHost ${alias}\n Hostname ${host}\n IdentityFile ${this.sshPath}/${alias}\n StrictHostKeyChecking no\n${proxyStr}`; return `Host ${alias}\n Hostname ${host}\n IdentityFile ${this.sshPath}/${alias}\n StrictHostKeyChecking no\n${proxyStr}`;
} }
private generateSshConfig(configs: string[]) { private generateSshConfig(configs: string[]) {
try { try {
for (const config of configs) { fs.writeFileSync(this.sshConfigFilePath, configs.join('\n'), {
fs.appendFileSync(this.sshConfigFilePath, config, { encoding: 'utf8',
encoding: 'utf8', });
});
}
} catch (error) { } catch (error) {
this.logger.error('写入ssh配置文件失败', error); this.logger.error('写入ssh配置文件失败', error);
} }
@@ -94,4 +97,22 @@ export default class SshKeyService {
const config = this.generateSingleSshConfig(alias, host, proxy); const config = this.generateSingleSshConfig(alias, host, proxy);
this.removeSshConfig(config); this.removeSshConfig(config);
} }
public setSshConfig(docs: Subscription[]) {
let result = [];
for (const doc of docs) {
if (doc.type === 'private-repo' && doc.pull_type === 'ssh-key') {
const { alias, proxy } = doc;
const { host } = formatUrl(doc);
this.removePrivateKeyFile(alias);
this.generatePrivateKeyFile(
alias,
(doc.pull_option as any).private_key,
);
const config = this.generateSingleSshConfig(alias, host, proxy);
result.push(config);
}
}
this.generateSshConfig(result);
}
} }
+20 -60
View File
@@ -13,7 +13,6 @@ import {
spawn, spawn,
} from 'child_process'; } from 'child_process';
import fs from 'fs'; import fs from 'fs';
import cron_parser from 'cron-parser';
import { import {
getFileContentByName, getFileContentByName,
concurrentRun, concurrentRun,
@@ -30,6 +29,7 @@ import SockService from './sock';
import SshKeyService from './sshKey'; import SshKeyService from './sshKey';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { LOG_END_SYMBOL } from '../config/const'; import { LOG_END_SYMBOL } from '../config/const';
import { formatCommand, formatUrl } from '../config/subscription';
@Service() @Service()
export default class SubscriptionService { export default class SubscriptionService {
@@ -68,71 +68,20 @@ export default class SubscriptionService {
['createdAt', 'DESC'], ['createdAt', 'DESC'],
], ],
}); });
return result as any; return result;
} catch (error) { } catch (error) {
throw error; throw error;
} }
} }
private formatCommand(doc: Subscription, url?: string) {
let command = 'ql ';
let _url = url || this.formatUrl(doc).url;
const {
type,
whitelist,
blacklist,
dependences,
branch,
extensions,
proxy,
} = doc;
if (type === 'file') {
command += `raw "${_url}"`;
} else {
command += `repo "${_url}" "${whitelist || ''}" "${blacklist || ''}" "${
dependences || ''
}" "${branch || ''}" "${extensions || ''}" "${proxy || ''}"`;
}
return command;
}
private formatUrl(doc: Subscription) {
let url = doc.url;
let host = '';
if (doc.type === 'private-repo') {
if (doc.pull_type === 'ssh-key') {
host = doc.url!.replace(/.*\@([^\:]+)\:.*/, '$1');
url = doc.url!.replace(host, doc.alias);
} else {
host = doc.url!.replace(/.*\:\/\/([^\/]+)\/.*/, '$1');
const { username, password } = doc.pull_option as any;
url = doc.url!.replace(host, `${username}:${password}@${host}`);
}
}
return { url, host };
}
public async handleTask( public async handleTask(
doc: Subscription, doc: Subscription,
needCreate = true, needCreate = true,
needAddKey = true,
runImmediately = false, runImmediately = false,
) { ) {
const { url, host } = this.formatUrl(doc); const { url } = formatUrl(doc);
if (doc.type === 'private-repo' && doc.pull_type === 'ssh-key') {
if (needAddKey) {
this.sshKeyService.addSSHKey(
(doc.pull_option as any).private_key,
doc.alias,
host,
doc.proxy,
);
} else {
this.sshKeyService.removeSSHKey(doc.alias, host, doc.proxy);
}
}
doc.command = this.formatCommand(doc, url as string); doc.command = formatCommand(doc, url as string);
if (doc.schedule_type === 'crontab') { if (doc.schedule_type === 'crontab') {
this.scheduleService.cancelCronTask(doc as any); this.scheduleService.cancelCronTask(doc as any);
@@ -155,6 +104,11 @@ export default class SubscriptionService {
} }
} }
private async setSshConfig() {
const docs = await SubscriptionModel.findAll();
this.sshKeyService.setSshConfig(docs);
}
private async promiseExec(command: string): Promise<string> { private async promiseExec(command: string): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
exec( exec(
@@ -273,6 +227,7 @@ export default class SubscriptionService {
const tab = new Subscription(payload); const tab = new Subscription(payload);
const doc = await this.insert(tab); const doc = await this.insert(tab);
await this.handleTask(doc); await this.handleTask(doc);
await this.setSshConfig();
return doc; return doc;
} }
@@ -281,8 +236,10 @@ export default class SubscriptionService {
} }
public async update(payload: Subscription): Promise<Subscription> { public async update(payload: Subscription): Promise<Subscription> {
const newDoc = await this.updateDb(payload); const tab = new Subscription(payload);
const newDoc = await this.updateDb(tab);
await this.handleTask(newDoc, !newDoc.is_disabled); await this.handleTask(newDoc, !newDoc.is_disabled);
await this.setSshConfig();
return newDoc; return newDoc;
} }
@@ -325,9 +282,10 @@ export default class SubscriptionService {
public async remove(ids: number[]) { public async remove(ids: number[]) {
const docs = await SubscriptionModel.findAll({ where: { id: ids } }); const docs = await SubscriptionModel.findAll({ where: { id: ids } });
for (const doc of docs) { for (const doc of docs) {
await this.handleTask(doc, false, false); await this.handleTask(doc, false);
} }
await SubscriptionModel.destroy({ where: { id: ids } }); await SubscriptionModel.destroy({ where: { id: ids } });
await this.setSshConfig();
} }
public async getDb(query: any): Promise<Subscription> { public async getDb(query: any): Promise<Subscription> {
@@ -378,7 +336,7 @@ export default class SubscriptionService {
return; return;
} }
const command = this.formatCommand(subscription); const command = formatCommand(subscription);
await this.scheduleService.runTask( await this.scheduleService.runTask(
command, command,
@@ -387,19 +345,21 @@ export default class SubscriptionService {
} }
public async disabled(ids: number[]) { public async disabled(ids: number[]) {
await SubscriptionModel.update({ is_disabled: 1 }, { where: { id: ids } });
const docs = await SubscriptionModel.findAll({ where: { id: ids } }); const docs = await SubscriptionModel.findAll({ where: { id: ids } });
await this.setSshConfig();
for (const doc of docs) { for (const doc of docs) {
await this.handleTask(doc, false); await this.handleTask(doc, false);
} }
await SubscriptionModel.update({ is_disabled: 1 }, { where: { id: ids } });
} }
public async enabled(ids: number[]) { public async enabled(ids: number[]) {
await SubscriptionModel.update({ is_disabled: 0 }, { where: { id: ids } });
const docs = await SubscriptionModel.findAll({ where: { id: ids } }); const docs = await SubscriptionModel.findAll({ where: { id: ids } });
await this.setSshConfig();
for (const doc of docs) { for (const doc of docs) {
await this.handleTask(doc); await this.handleTask(doc);
} }
await SubscriptionModel.update({ is_disabled: 0 }, { where: { id: ids } });
} }
public async log(id: number) { public async log(id: number) {
+21 -8
View File
@@ -1,3 +1,12 @@
FROM python:3.10-alpine as builder
COPY package.json .npmrc pnpm-lock.yaml /tmp/build/
RUN set -x \
&& apk update \
&& apk add nodejs npm git \
&& npm i -g pnpm \
&& cd /tmp/build \
&& pnpm install --prod
FROM python:3.10-alpine FROM python:3.10-alpine
ARG QL_MAINTAINER="whyour" ARG QL_MAINTAINER="whyour"
@@ -7,14 +16,13 @@ ARG QL_BRANCH=develop
ENV PNPM_HOME=/root/.local/share/pnpm \ ENV PNPM_HOME=/root/.local/share/pnpm \
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.local/share/pnpm:/root/.local/share/pnpm/global/5/node_modules:$PNPM_HOME \ PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.local/share/pnpm:/root/.local/share/pnpm/global/5/node_modules:$PNPM_HOME \
NODE_PATH=/usr/local/bin:/usr/local/pnpm-global/5/node_modules:/usr/local/lib/node_modules:/root/.local/share/pnpm/global/5/node_modules \
LANG=zh_CN.UTF-8 \ LANG=zh_CN.UTF-8 \
SHELL=/bin/bash \ SHELL=/bin/bash \
PS1="\u@\h:\w \$ " \ PS1="\u@\h:\w \$ " \
QL_DIR=/ql \ QL_DIR=/ql \
QL_BRANCH=${QL_BRANCH} QL_BRANCH=${QL_BRANCH}
WORKDIR ${QL_DIR}
RUN set -x \ RUN set -x \
&& sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \ && sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
&& apk update -f \ && apk update -f \
@@ -42,19 +50,24 @@ RUN set -x \
&& git config --global http.postBuffer 524288000 \ && git config --global http.postBuffer 524288000 \
&& npm install -g pnpm \ && npm install -g pnpm \
&& pnpm add -g pm2 ts-node typescript tslib \ && pnpm add -g pm2 ts-node typescript tslib \
&& git clone -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \ && rm -rf /root/.pnpm-store \
&& rm -rf /root/.local/share/pnpm/store \
&& rm -rf /root/.cache \
&& rm -rf /root/.npm
ARG SOURCE_COMMIT
RUN git clone -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
&& cd ${QL_DIR} \ && cd ${QL_DIR} \
&& cp -f .env.example .env \ && cp -f .env.example .env \
&& chmod 777 ${QL_DIR}/shell/*.sh \ && chmod 777 ${QL_DIR}/shell/*.sh \
&& chmod 777 ${QL_DIR}/docker/*.sh \ && chmod 777 ${QL_DIR}/docker/*.sh \
&& pnpm install --prod \
&& rm -rf /root/.pnpm-store \
&& rm -rf /root/.local/share/pnpm/store \
&& rm -rf /root/.cache \
&& rm -rf /root/.npm \
&& git clone -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \ && git clone -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \
&& mkdir -p ${QL_DIR}/static \ && mkdir -p ${QL_DIR}/static \
&& cp -rf /static/* ${QL_DIR}/static \ && cp -rf /static/* ${QL_DIR}/static \
&& rm -rf /static && rm -rf /static
COPY --from=builder /tmp/build/node_modules/. /ql/node_modules/
WORKDIR ${QL_DIR}
ENTRYPOINT ["./docker/docker-entrypoint.sh"] ENTRYPOINT ["./docker/docker-entrypoint.sh"]
+11 -6
View File
@@ -42,7 +42,8 @@
"monaco-editor", "monaco-editor",
"rc-field-form", "rc-field-form",
"@types/lodash.merge", "@types/lodash.merge",
"rollup" "rollup",
"styled-components"
], ],
"allowedVersions": { "allowedVersions": {
"react": "18", "react": "18",
@@ -81,7 +82,7 @@
"sequelize": "^6.25.5", "sequelize": "^6.25.5",
"serve-handler": "^6.1.3", "serve-handler": "^6.1.3",
"sockjs": "^0.3.24", "sockjs": "^0.3.24",
"sqlite3": "npm:@louislam/sqlite3@^15.0.6", "sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0.2",
"toad-scheduler": "^1.6.0", "toad-scheduler": "^1.6.0",
"typedi": "^0.10.0", "typedi": "^0.10.0",
"uuid": "^8.3.2", "uuid": "^8.3.2",
@@ -90,8 +91,8 @@
}, },
"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.38.22",
"@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",
@@ -108,20 +109,22 @@
"@types/nodemailer": "^6.4.4", "@types/nodemailer": "^6.4.4",
"@types/qrcode.react": "^1.0.2", "@types/qrcode.react": "^1.0.2",
"@types/react": "^18.0.20", "@types/react": "^18.0.20",
"@types/react-copy-to-clipboard": "^5.0.4",
"@types/react-dom": "^18.0.6", "@types/react-dom": "^18.0.6",
"@types/serve-handler": "^6.1.1", "@types/serve-handler": "^6.1.1",
"@types/sockjs": "^0.3.33", "@types/sockjs": "^0.3.33",
"@types/sockjs-client": "^1.5.1", "@types/sockjs-client": "^1.5.1",
"@types/uuid": "^8.3.4", "@types/uuid": "^8.3.4",
"@umijs/max": "^4.0.21", "@umijs/max": "^4.0.42",
"@umijs/ssr-darkreader": "^4.9.45", "@umijs/ssr-darkreader": "^4.9.45",
"ansi-to-react": "^6.1.6", "ansi-to-react": "^6.1.6",
"antd": "^4.23.0", "antd": "^4.24.7",
"antd-img-crop": "^4.2.3", "antd-img-crop": "^4.2.3",
"codemirror": "^5.65.2", "codemirror": "^5.65.2",
"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.33.0",
"nodemon": "^2.0.15", "nodemon": "^2.0.15",
"prettier": "^2.5.1", "prettier": "^2.5.1",
"qiniu": "^7.4.0", "qiniu": "^7.4.0",
@@ -130,6 +133,7 @@
"rc-tween-one": "^3.0.6", "rc-tween-one": "^3.0.6",
"react": "18.2.0", "react": "18.2.0",
"react-codemirror2": "^7.2.1", "react-codemirror2": "^7.2.1",
"react-copy-to-clipboard": "^5.1.0",
"react-diff-viewer": "^3.1.1", "react-diff-viewer": "^3.1.1",
"react-dnd": "^14.0.2", "react-dnd": "^14.0.2",
"react-dnd-html5-backend": "^14.0.0", "react-dnd-html5-backend": "^14.0.0",
@@ -141,6 +145,7 @@
"typescript": "4.8.4", "typescript": "4.8.4",
"umi-request": "^1.4.0", "umi-request": "^1.4.0",
"vh-check": "^2.0.5", "vh-check": "^2.0.5",
"virtuallist-antd": "^0.7.6",
"webpack": "^5.70.0", "webpack": "^5.70.0",
"yorkie": "^2.0.0" "yorkie": "^2.0.0"
} }
+3486 -1956
View File
File diff suppressed because it is too large Load Diff
+13
View File
@@ -160,4 +160,17 @@ export AIBOTK_TYPE=""
## aibotk_name (必填)填写群名或用户昵称,和上面的type类型要对应 ## aibotk_name (必填)填写群名或用户昵称,和上面的type类型要对应
export AIBOTK_NAME="" export AIBOTK_NAME=""
## 14. SMTP
## 暂时只支持在 Python 中调用 notify.py 以发送 SMTP 邮件通知
## smtp_server 填写 SMTP 发送邮件服务器,形如 smtp.exmail.qq.com:465
export SMTP_SERVER=""
## smtp_ssl 填写 SMTP 发送邮件服务器是否使用 SSL,内容应为 true 或 false
export SMTP_SSL="false"
## smtp_email 填写 SMTP 收发件邮箱,通知将会由自己发给自己
export SMTP_EMAIL=""
## smtp_password 填写 SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
export SMTP_PASSWORD=""
## smtp_name 填写 SMTP 收发件人姓名,可随意填写
export SMTP_NAME=""
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可 ## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可
+75
View File
@@ -125,6 +125,18 @@ let AIBOTK_NAME = '';
//FSKEY 飞书机器人的 FSKEY //FSKEY 飞书机器人的 FSKEY
let FSKEY = ''; let FSKEY = '';
// =======================================SMTP 邮件设置区域=======================================
// SMTP_SERVER: 填写 SMTP 发送邮件服务器,形如 smtp.exmail.qq.com:465
// SMTP_SSL: 填写 SMTP 发送邮件服务器是否使用 SSL,内容应为 true 或 false
// SMTP_EMAIL: 填写 SMTP 收发件邮箱,通知将会由自己发给自己
// SMTP_PASSWORD: 填写 SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
// SMTP_NAME: 填写 SMTP 收发件人姓名,可随意填写
let SMTP_SERVER = '';
let SMTP_SSL = 'false';
let SMTP_EMAIL = '';
let SMTP_PASSWORD = '';
let SMTP_NAME = '';
//==========================云端环境变量的判断与接收========================= //==========================云端环境变量的判断与接收=========================
if (process.env.GOTIFY_URL) { if (process.env.GOTIFY_URL) {
GOTIFY_URL = process.env.GOTIFY_URL; GOTIFY_URL = process.env.GOTIFY_URL;
@@ -250,6 +262,22 @@ if (process.env.AIBOTK_NAME) {
if (process.env.FSKEY) { if (process.env.FSKEY) {
FSKEY = process.env.FSKEY; FSKEY = process.env.FSKEY;
} }
if (process.env.SMTP_SERVER) {
SMTP_SERVER = process.env.SMTP_SERVER;
}
if (process.env.SMTP_SSL) {
SMTP_SSL = process.env.SMTP_SSL;
}
if (process.env.SMTP_EMAIL) {
SMTP_EMAIL = process.env.SMTP_EMAIL;
}
if (process.env.SMTP_PASSWORD) {
SMTP_PASSWORD = process.env.SMTP_PASSWORD;
}
if (process.env.SMTP_NAME) {
SMTP_NAME = process.env.SMTP_NAME;
}
//==========================云端环境变量的判断与接收========================= //==========================云端环境变量的判断与接收=========================
/** /**
@@ -287,6 +315,7 @@ async function sendNotify(
PushDeerNotify(text, desp), //PushDeer PushDeerNotify(text, desp), //PushDeer
aibotkNotify(text, desp), //智能微秘书 aibotkNotify(text, desp), //智能微秘书
fsBotNotify(text, desp), //飞书机器人 fsBotNotify(text, desp), //飞书机器人
smtpNotify(text, desp), //SMTP 邮件
]); ]);
} }
@@ -1022,6 +1051,52 @@ function fsBotNotify(text, desp) {
}); });
} }
async function smtpNotify(text, desp) {
if (![SMTP_SERVER, SMTP_EMAIL, SMTP_PASSWORD].every(Boolean)) {
return;
}
try {
const nodemailer = require('nodemailer');
const transporter = nodemailer.createTransport(
`${SMTP_SSL === 'true' ? 'smtps:' : 'smtp:'}//${SMTP_SERVER}`,
{
auth: {
user: SMTP_EMAIL,
pass: SMTP_PASSWORD,
},
},
);
const addr = SMTP_NAME ? `"${SMTP_NAME}" <${SMTP_EMAIL}>` : SMTP_EMAIL;
const info = await transporter.sendMail({
from: addr,
to: addr,
subject: text,
text: desp,
});
if (!!info.messageId) {
console.log('SMTP发送通知消息成功🎉\n');
return true;
}
console.log('SMTP发送通知消息失败!!\n');
} catch (e) {
console.log('SMTP发送通知消息出现错误!!\n');
console.log(e);
}
}
function smtpNotify(text, desp) {
return new Promise((resolve) => {
if (SMTP_SERVER && SMTP_SSL && SMTP_EMAIL && SMTP_PASSWORD && SMTP_NAME) {
// todo: Node.js并没有内置的 smtp 实现,需要调用外部库,因为不清楚这个文件的模块依赖情况,所以留给有缘人实现
} else {
resolve();
}
});
}
module.exports = { module.exports = {
sendNotify, sendNotify,
BARK_PUSH, BARK_PUSH,
+47 -11
View File
@@ -9,6 +9,10 @@ import re
import threading import threading
import time import time
import urllib.parse import urllib.parse
import smtplib
from email.mime.text import MIMEText
from email.header import Header
from email.utils import formataddr
import requests import requests
@@ -62,10 +66,10 @@ push_config = {
'DEER_KEY': '', # PushDeer 的 PUSHDEER_KEY 'DEER_KEY': '', # PushDeer 的 PUSHDEER_KEY
'DEER_URL': '', # PushDeer 的 PUSHDEER_URL 'DEER_URL': '', # PushDeer 的 PUSHDEER_URL
'CHAT_URL': '', # synology chat url 'CHAT_URL': '', # synology chat url
'CHAT_TOKEN': '', # synology chat token 'CHAT_TOKEN': '', # synology chat token
'PUSH_PLUS_TOKEN': '', # push+ 微信推送的用户令牌 'PUSH_PLUS_TOKEN': '', # push+ 微信推送的用户令牌
'PUSH_PLUS_USER': '', # push+ 微信推送的群组编码 'PUSH_PLUS_USER': '', # push+ 微信推送的群组编码
@@ -86,6 +90,12 @@ push_config = {
'AIBOTK_KEY': '', # 智能微秘书 个人中心的apikey 文档地址:http://wechat.aibotk.com/docs/about 'AIBOTK_KEY': '', # 智能微秘书 个人中心的apikey 文档地址:http://wechat.aibotk.com/docs/about
'AIBOTK_TYPE': '', # 智能微秘书 发送目标 room 或 contact 'AIBOTK_TYPE': '', # 智能微秘书 发送目标 room 或 contact
'AIBOTK_NAME': '', # 智能微秘书 发送群名 或者好友昵称和type要对应好 'AIBOTK_NAME': '', # 智能微秘书 发送群名 或者好友昵称和type要对应好
'SMTP_SERVER': '', # SMTP 发送邮件服务器,形如 smtp.exmail.qq.com:465
'SMTP_SSL': 'false', # SMTP 发送邮件服务器是否使用 SSL,填写 true 或 false
'SMTP_EMAIL': '', # SMTP 收发件邮箱,通知将会由自己发给自己
'SMTP_PASSWORD': '', # SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
'SMTP_NAME': '', # SMTP 收发件人姓名,可随意填写
} }
notify_function = [] notify_function = []
# fmt: on # fmt: on
@@ -282,16 +292,16 @@ def pushdeer(title: str, content: str) -> None:
data = {"text": title, "desp": content, "type": "markdown", "pushkey": push_config.get("DEER_KEY")} data = {"text": title, "desp": content, "type": "markdown", "pushkey": push_config.get("DEER_KEY")}
url = 'https://api2.pushdeer.com/message/push' url = 'https://api2.pushdeer.com/message/push'
if push_config.get("DEER_URL"): if push_config.get("DEER_URL"):
url = push_config.get("DEER_URL") url = push_config.get("DEER_URL")
response = requests.post(url, data=data).json() response = requests.post(url, data=data).json()
if len(response.get("content").get("result")) > 0: if len(response.get("content").get("result")) > 0:
print("PushDeer 推送成功!") print("PushDeer 推送成功!")
else: else:
print("PushDeer 推送失败!错误信息:", response) print("PushDeer 推送失败!错误信息:", response)
def chat(title: str, content: str) -> None: def chat(title: str, content: str) -> None:
""" """
通过Chat 推送消息 通过Chat 推送消息
@@ -303,14 +313,14 @@ def chat(title: str, content: str) -> None:
data = 'payload=' + json.dumps({'text': title + '\n' + content}) data = 'payload=' + json.dumps({'text': title + '\n' + content})
url = push_config.get("CHAT_URL") + push_config.get("CHAT_TOKEN") url = push_config.get("CHAT_URL") + push_config.get("CHAT_TOKEN")
response = requests.post(url, data=data) response = requests.post(url, data=data)
if response.status_code == 200: if response.status_code == 200:
print("Chat 推送成功!") print("Chat 推送成功!")
else: else:
print("Chat 推送失败!错误信息:", response) print("Chat 推送失败!错误信息:", response)
def pushplus_bot(title: str, content: str) -> None: def pushplus_bot(title: str, content: str) -> None:
""" """
通过 push+ 推送消息。 通过 push+ 推送消息。
@@ -562,6 +572,30 @@ def aibotk(title: str, content: str) -> None:
print(f'智能微秘书 推送失败!{response["error"]}') print(f'智能微秘书 推送失败!{response["error"]}')
def smtp(title: str, content: str) -> None:
"""
使用 SMTP 邮件 推送消息。
"""
if not push_config.get("SMTP_SERVER") or not push_config.get("SMTP_SSL") or not push_config.get("SMTP_EMAIL") or not push_config.get("SMTP_PASSWORD") or not push_config.get("SMTP_NAME"):
print("SMTP 邮件 的 SMTP_SERVER 或者 SMTP_SSL 或者 SMTP_EMAIL 或者 SMTP_PASSWORD 或者 SMTP_NAME 未设置!!\n取消推送")
return
print("SMTP 邮件 服务启动")
message = MIMEText(content, 'plain', 'utf-8')
message['From'] = formataddr((Header(push_config.get("SMTP_NAME"), 'utf-8').encode(), push_config.get("SMTP_EMAIL")))
message['To'] = formataddr((Header(push_config.get("SMTP_NAME"), 'utf-8').encode(), push_config.get("SMTP_EMAIL")))
message['Subject'] = Header(title, 'utf-8')
try:
smtp_server = smtplib.SMTP_SSL(push_config.get("SMTP_SERVER")) if push_config.get("SMTP_SSL") == 'true' else smtplib.SMTP(push_config.get("SMTP_SERVER"))
smtp_server.login(push_config.get("SMTP_EMAIL"), push_config.get("SMTP_PASSWORD"))
smtp_server.sendmail(push_config.get("SMTP_EMAIL"), push_config.get("SMTP_EMAIL"), message.as_bytes())
smtp_server.close()
print("SMTP 邮件 推送成功!")
except Exception as e:
print(f'SMTP 邮件 推送失败!{e}')
def one() -> str: def one() -> str:
""" """
获取一条一言。 获取一条一言。
@@ -603,7 +637,9 @@ if push_config.get("QYWX_KEY"):
if push_config.get("TG_BOT_TOKEN") and push_config.get("TG_USER_ID"): if push_config.get("TG_BOT_TOKEN") and push_config.get("TG_USER_ID"):
notify_function.append(telegram_bot) notify_function.append(telegram_bot)
if push_config.get("AIBOTK_KEY") and push_config.get("AIBOTK_TYPE") and push_config.get("AIBOTK_NAME"): if push_config.get("AIBOTK_KEY") and push_config.get("AIBOTK_TYPE") and push_config.get("AIBOTK_NAME"):
notify_function.append(aibotk) notify_function.append(aibotk)
if push_config.get("SMTP_SERVER") and push_config.get("SMTP_SSL") and push_config.get("SMTP_EMAIL") and push_config.get("SMTP_PASSWORD") and push_config.get("SMTP_NAME"):
notify_function.append(smtp)
def send(title: str, content: str) -> None: def send(title: str, content: str) -> None:
+3 -2
View File
@@ -5,12 +5,13 @@
"dependencies": { "dependencies": {
"crypto-js": "^4.0.0", "crypto-js": "^4.0.0",
"download": "^8.0.0", "download": "^8.0.0",
"http-server": "^0.12.3",
"got": "^11.5.1", "got": "^11.5.1",
"http-server": "^0.12.3",
"nodemailer": "^6.8.0",
"qrcode-terminal": "^0.12.0", "qrcode-terminal": "^0.12.0",
"request": "^2.88.2", "request": "^2.88.2",
"tough-cookie": "^4.0.0", "tough-cookie": "^4.0.0",
"tunnel": "0.0.6", "tunnel": "0.0.6",
"ws": "^7.4.3" "ws": "^7.4.3"
} }
} }
+19 -13
View File
@@ -19,10 +19,16 @@ add_cron_api() {
local schedule=$(echo "$1" | awk -F ":" '{print $1}') local schedule=$(echo "$1" | awk -F ":" '{print $1}')
local command=$(echo "$1" | awk -F ":" '{print $2}') local command=$(echo "$1" | awk -F ":" '{print $2}')
local name=$(echo "$1" | awk -F ":" '{print $3}') local name=$(echo "$1" | awk -F ":" '{print $3}')
local sub_id=$(echo "$1" | awk -F ":" '{print $4}')
else else
local schedule=$1 local schedule=$1
local command=$2 local command=$2
local name=$3 local name=$3
local sub_id=$4
fi
if [[ ! $sub_id ]];then
sub_id="null"
fi fi
local api=$( local api=$(
@@ -34,11 +40,11 @@ add_cron_api() {
-H "Origin: http://0.0.0.0:5700" \ -H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \ -H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \ -H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\"}" \ --data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"sub_id\":$sub_id}" \
--compressed --compressed
) )
code=$(echo $api | jq -r .code) code=$(echo "$api" | jq -r .code)
message=$(echo $api | jq -r .message) message=$(echo "$api" | jq -r .message)
if [[ $code == 200 ]]; then if [[ $code == 200 ]]; then
echo -e "$name -> 添加成功" echo -e "$name -> 添加成功"
else else
@@ -73,8 +79,8 @@ update_cron_api() {
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"id\":\"$id\"}" \ --data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"id\":\"$id\"}" \
--compressed --compressed
) )
code=$(echo $api | jq -r .code) code=$(echo "$api" | jq -r .code)
message=$(echo $api | jq -r .message) message=$(echo "$api" | jq -r .message)
if [[ $code == 200 ]]; then if [[ $code == 200 ]]; then
echo -e "$name -> 更新成功" echo -e "$name -> 更新成功"
else else
@@ -105,8 +111,8 @@ update_cron_command_api() {
--data-raw "{\"command\":\"$command\",\"id\":\"$id\"}" \ --data-raw "{\"command\":\"$command\",\"id\":\"$id\"}" \
--compressed --compressed
) )
code=$(echo $api | jq -r .code) code=$(echo "$api" | jq -r .code)
message=$(echo $api | jq -r .message) message=$(echo "$api" | jq -r .message)
if [[ $code == 200 ]]; then if [[ $code == 200 ]]; then
echo -e "$command -> 更新成功" echo -e "$command -> 更新成功"
else else
@@ -130,8 +136,8 @@ del_cron_api() {
--data-raw "[$ids]" \ --data-raw "[$ids]" \
--compressed --compressed
) )
code=$(echo $api | jq -r .code) code=$(echo "$api" | jq -r .code)
message=$(echo $api | jq -r .message) message=$(echo "$api" | jq -r .message)
if [[ $code == 200 ]]; then if [[ $code == 200 ]]; then
echo -e "成功" echo -e "成功"
else else
@@ -160,8 +166,8 @@ update_cron() {
--data-raw "{\"ids\":[$ids],\"status\":\"$status\",\"pid\":\"$pid\",\"log_path\":\"$logPath\",\"last_execution_time\":$lastExecutingTime,\"last_running_time\":$runningTime}" \ --data-raw "{\"ids\":[$ids],\"status\":\"$status\",\"pid\":\"$pid\",\"log_path\":\"$logPath\",\"last_execution_time\":$lastExecutingTime,\"last_running_time\":$runningTime}" \
--compressed --compressed
) )
code=$(echo $api | jq -r .code) code=$(echo "$api" | jq -r .code)
message=$(echo $api | jq -r .message) message=$(echo "$api" | jq -r .message)
if [[ $code != 200 ]]; then if [[ $code != 200 ]]; then
echo -e "\n## 更新任务状态失败(${message})\n" >>$dir_log/$log_path echo -e "\n## 更新任务状态失败(${message})\n" >>$dir_log/$log_path
fi fi
@@ -184,8 +190,8 @@ notify_api() {
--data-raw "{\"title\":\"$title\",\"content\":\"$content\"}" \ --data-raw "{\"title\":\"$title\",\"content\":\"$content\"}" \
--compressed --compressed
) )
code=$(echo $api | jq -r .code) code=$(echo "$api" | jq -r .code)
message=$(echo $api | jq -r .message) message=$(echo "$api" | jq -r .message)
if [[ $code == 200 ]]; then if [[ $code == 200 ]]; then
echo -e "通知发送成功" echo -e "通知发送成功"
else else
-1
View File
@@ -96,7 +96,6 @@ start_public() {
main() { main() {
echo -e "=====> 开始检测" echo -e "=====> 开始检测"
npm i -g pnpm npm i -g pnpm
pnpm add -g pm2
patch_version patch_version
start_public start_public
copy_dep copy_dep
+17 -15
View File
@@ -35,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
} }
@@ -81,18 +81,19 @@ run_nohup() {
} }
check_server() { check_server() {
local top_result=$(top -b -n 1) if [[ $cpu_warn ]] && [[ $mem_warn ]] && [[ $disk_warn ]]; then
cpu_use=$(echo "$top_result" | grep CPU | grep -v -E 'grep|PID' | awk '{print $2}' | cut -f 1 -d "%" | head -n 1) local top_result=$(top -b -n 1)
cpu_use=$(echo "$top_result" | grep CPU | grep -v -E 'grep|PID' | awk '{print $2}' | cut -f 1 -d "%" | head -n 1)
mem_free=$(free -m | grep "Mem" | awk '{print $3}' | head -n 1) mem_free=$(free -m | grep "Mem" | awk '{print $3}' | head -n 1)
mem_total=$(free -m | grep "Mem" | awk '{print $2}' | head -n 1) mem_total=$(free -m | grep "Mem" | awk '{print $2}' | head -n 1)
mem_use=$(printf "%d%%" $((mem_free * 100 / mem_total)) | cut -f 1 -d "%" | head -n 1) mem_use=$(printf "%d%%" $((mem_free * 100 / mem_total)) | cut -f 1 -d "%" | head -n 1)
disk_use=$(df -P | grep /dev | grep -v -E '(tmp|boot|shm)' | awk '{print $5}' | cut -f 1 -d "%" | head -n 1) disk_use=$(df -P | grep /dev | grep -v -E '(tmp|boot|shm)' | awk '{print $5}' | cut -f 1 -d "%" | head -n 1)
if [[ $cpu_use -gt $cpu_warn ]] && [[ $cpu_warn ]] || [[ $mem_free -lt $mem_warn ]] || [[ $disk_use -gt $disk_warn ]]; then
if [[ $cpu_use -gt $cpu_warn ]] || [[ $mem_free -lt $mem_warn ]] || [[ $disk_use -gt $disk_warn ]]; then local resource=$(echo "$top_result" | grep -v -E 'grep|Mem|idle|Load|tr' | awk '{$2="";$3="";$4="";$5="";$7="";print $0}' | head -n 10 | tr '\n' '|' | sed s/\|/\\\\n/g)
local resource=$(echo "$top_result" | grep -v -E 'grep|Mem|idle|Load|tr' | awk '{$2="";$3="";$4="";$5="";$7="";print $0}' | head -n 10 | tr '\n' '|' | sed s/\|/\\\\n/g) notify_api "服务器资源异常警告" "当前CPU占用 $cpu_use% 内存占用 $mem_use% 磁盘占用 $disk_use% \n资源占用详情 \n\n $resource"
notify_api "服务器资源异常警告" "当前CPU占用 $cpu_use% 内存占用 $mem_use% 磁盘占用 $disk_use% \n资源占用详情 \n\n $resource" fi
fi fi
} }
@@ -100,6 +101,8 @@ handle_task_before() {
begin_time=$(format_time "$time_format" "$time") begin_time=$(format_time "$time_format" "$time")
begin_timestamp=$(format_timestamp "$time_format" "$time") begin_timestamp=$(format_timestamp "$time_format" "$time")
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
echo -e "## 开始执行... $begin_time\n" echo -e "## 开始执行... $begin_time\n"
[[ $is_macos -eq 0 ]] && check_server [[ $is_macos -eq 0 ]] && check_server
@@ -110,7 +113,6 @@ handle_task_before() {
eval echo $cmd eval echo $cmd
fi fi
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
. $file_task_before "$@" . $file_task_before "$@"
} }
@@ -122,9 +124,9 @@ handle_task_after() {
local end_timestamp=$(format_timestamp "$time_format" "$etime") local end_timestamp=$(format_timestamp "$time_format" "$etime")
local diff_time=$(($end_timestamp - $begin_timestamp)) local diff_time=$(($end_timestamp - $begin_timestamp))
echo -e "\n\n## 执行结束... $end_time 耗时 $diff_time 秒     "
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time" [[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
echo -e "\n\n## 执行结束... $end_time 耗时 $diff_time"
echo -e "\n     "
} }
## 正常运行单个脚本,$1:传入参数 ## 正常运行单个脚本,$1:传入参数
@@ -171,7 +173,7 @@ run_concurrent() {
local envs=$(eval echo "\$${env_param}") local envs=$(eval echo "\$${env_param}")
local array=($(echo $envs | sed 's/&/ /g')) local array=($(echo $envs | sed 's/&/ /g'))
single_log_time=$(date "+%Y-%m-%d-%H-%M-%S.%N") single_log_time=$(date "+%Y-%m-%d-%H-%M-%S.%3N")
cd $dir_scripts cd $dir_scripts
local relative_path="${file_param%/*}" local relative_path="${file_param%/*}"
+7 -9
View File
@@ -79,9 +79,9 @@ import_config() {
default_cron="$(random_range 0 59) $(random_range 0 23) * * *" default_cron="$(random_range 0 59) $(random_range 0 23) * * *"
fi fi
cpu_warn=${CpuWarn:-80} cpu_warn=${CpuWarn}
mem_warn=${MemoryWarn:-80} mem_warn=${MemoryWarn}
disk_warn=${DiskWarn:-90} disk_warn=${DiskWarn}
} }
set_proxy() { set_proxy() {
@@ -326,7 +326,7 @@ git_pull_scripts() {
local branch="$2" local branch="$2"
local proxy="$3" local proxy="$3"
cd $dir_work cd $dir_work
echo -e "开始更新仓库:$dir_work\n" echo -e "开始更新仓库:$dir_work"
set_proxy "$proxy" set_proxy "$proxy"
git fetch --all git fetch --all
@@ -413,9 +413,9 @@ format_log_time() {
local time="$2" local time="$2"
if [[ $is_macos -eq 1 ]]; then if [[ $is_macos -eq 1 ]]; then
echo $(date -j -f "$format" "$time" "+%Y-%m-%d-%H-%M-%S") echo $(date -j -f "$format" "$time" "+%Y-%m-%d-%H-%M-%S-%3N")
else else
echo $(date -d "$time" "+%Y-%m-%d-%H-%M-%S") echo $(date -d "$time" "+%Y-%m-%d-%H-%M-%S-%3N")
fi fi
} }
@@ -450,9 +450,7 @@ patch_version() {
echo echo
fi fi
if ! type ts-node &>/dev/null; then pnpm add -g pm2 ts-node typescript tslib
pnpm add -g ts-node typescript tslib
fi
git config --global pull.rebase false git config --global pull.rebase false
+10 -6
View File
@@ -32,8 +32,8 @@ handle_log_path() {
if [[ ! -z $ID ]]; then if [[ ! -z $ID ]]; then
suffix="_${ID}" suffix="_${ID}"
fi fi
time=$(date "+$time_format") time=$(date "+$mtime_format")
log_time=$(format_log_time "$time_format" "$time") log_time=$(format_log_time "$mtime_format" "$time")
log_dir_tmp="${file_param##*/}" log_dir_tmp="${file_param##*/}"
if [[ $file_param =~ "/" ]]; then if [[ $file_param =~ "/" ]]; then
if [[ $file_param == /* ]]; then if [[ $file_param == /* ]]; then
@@ -48,17 +48,21 @@ handle_log_path() {
log_dir="${log_dir_tmp%.*}${suffix}" log_dir="${log_dir_tmp%.*}${suffix}"
log_path="$log_dir/$log_time.log" log_path="$log_dir/$log_time.log"
cmd=">> $dir_log/$log_path 2>&1" cmd=">> $dir_log/$log_path 2>&1"
[[ "$show_log" == "true" ]] && cmd="" if [[ "$show_log" == "true" ]]; then
make_dir "$dir_log/$log_dir" cmd=""
else
make_dir "$dir_log/$log_dir"
fi
} }
format_params() { format_params() {
time_format="%Y-%m-%d %H:%M:%S" time_format="%Y-%m-%d %H:%M:%S"
mtime_format="%Y-%m-%d %H:%M:%S.%3N"
timeoutCmd="" timeoutCmd=""
if type timeout &>/dev/null; then if type timeout &>/dev/null; then
timeoutCmd="timeout --foreground -s 14 -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')
} }
while getopts ":lm:" opt; do while getopts ":lm:" opt; do
@@ -81,7 +85,7 @@ format_params "$@"
define_program "$@" define_program "$@"
handle_log_path "$@" handle_log_path "$@"
eval . $dir_shell/otask.sh "$params" "$cmd" eval . $dir_shell/otask.sh "$cmd"
[[ -f "$dir_log/$log_path" ]] && cat "$dir_log/$log_path" [[ -f "$dir_log/$log_path" ]] && cat "$dir_log/$log_path"
exit 0 exit 0
+65 -42
View File
@@ -97,7 +97,7 @@ add_cron() {
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $file | awk -F ":" '{print $2}' | head -1 | xargs) [[ -z $cron_line ]] && cron_line=$(grep "cron:" $file | awk -F ":" '{print $2}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line=$(grep "cron " $file | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs) [[ -z $cron_line ]] && cron_line=$(grep "cron " $file | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line="$default_cron" [[ -z $cron_line ]] && cron_line="$default_cron"
result=$(add_cron_api "$cron_line:$cmd_task $file:$cron_name") result=$(add_cron_api "$cron_line:$cmd_task $file:$cron_name:$SUB_ID")
echo -e "$result" echo -e "$result"
if [[ $detail ]]; then if [[ $detail ]]; then
detail="${detail}${result}\n" detail="${detail}${result}\n"
@@ -118,6 +118,8 @@ update_repo() {
local branch="$5" local branch="$5"
local extensions="$6" local extensions="$6"
local proxy="$7" local proxy="$7"
local autoAddCron="$8"
local autoDelCron="$9"
local tmp="${url%/*}" local tmp="${url%/*}"
local authorTmp1="${tmp##*/}" local authorTmp1="${tmp##*/}"
local authorTmp2="${authorTmp1##*:}" local authorTmp2="${authorTmp1##*:}"
@@ -137,7 +139,7 @@ update_repo() {
fi fi
if [[ $exit_status -eq 0 ]]; then if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新${repo_path}成功...\n" echo -e "\n更新${repo_path}成功...\n"
diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions" diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions" "$autoAddCron" "$autoDelCron"
else else
echo -e "\n更新${repo_path}失败,请检查网络...\n" echo -e "\n更新${repo_path}失败,请检查网络...\n"
fi fi
@@ -145,41 +147,49 @@ update_repo() {
## 更新所有 raw 文件 ## 更新所有 raw 文件
update_raw() { update_raw() {
echo -e "--------------------------------------------------------------\n"
local url="$1" local url="$1"
local proxy="$2"
local autoAddCron="$3"
local autoDelCron="$4"
if [[ ! $autoAddCron ]];then
autoAddCron=${AutoAddCron}
fi
if [[ ! $autoDelCron ]];then
autoDelCron=${AutoDelCron}
fi
local raw_url="$url" local raw_url="$url"
local suffix="${raw_url##*.}" local suffix="${raw_url##*.}"
local raw_file_name="${uniq_path}.${suffix}" local raw_file_name="${uniq_path}.${suffix}"
echo -e "开始下载:${raw_url} \n\n保存路径:$dir_raw/${raw_file_name}\n" echo -e "开始下载:${raw_url} \n\n保存路径:$dir_raw/${raw_file_name}\n"
set_proxy wget -q --no-check-certificate -e "http_proxy=${proxy};https_proxy=${proxy}" -O "$dir_raw/${raw_file_name}.new" ${raw_url}
wget -q --no-check-certificate -O "$dir_raw/${raw_file_name}.new" ${raw_url}
unset_proxy
if [[ $? -eq 0 ]]; then if [[ $? -eq 0 ]]; then
mv "$dir_raw/${raw_file_name}.new" "$dir_raw/${raw_file_name}" mv "$dir_raw/${raw_file_name}.new" "$dir_raw/${raw_file_name}"
echo -e "下载 ${raw_file_name} 成功...\n" echo -e "下载 ${raw_file_name} 成功...\n"
cd $dir_raw if [[ -z $cron_id ]] && [[ ${autoAddCron} == true ]]; then
local filename="raw_${raw_file_name}" cd $dir_raw
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 filename="raw_${raw_file_name}"
cp -f $raw_file_name $dir_scripts/${filename} local cron_id=$(cat $list_crontab_user | grep -E "$cmd_task.* $filename" | perl -pe "s|.*ID=(.*) $cmd_task.* $filename\.*|\1|" | head -1 | awk -F " " '{print $1}')
cron_line=$( cp -f $raw_file_name $dir_scripts/${filename}
perl -ne "{ cron_line=$(
print if /.*([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*]( |,|\").*$raw_file_name/ perl -ne "{
}" $raw_file_name | print if /.*([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*]( |,|\").*$raw_file_name/
perl -pe "{ }" $raw_file_name |
s|[^\d\*]*(([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*])( \|,\|\").*/?$raw_file_name.*|\1|g; perl -pe "{
s|\*([\d\*])(.*)|\1\2|g; s|[^\d\*]*(([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*])( \|,\|\").*/?$raw_file_name.*|\1|g;
s| | |g; s|\*([\d\*])(.*)|\1\2|g;
}" | sort -u | head -1 s| | |g;
) }" | sort -u | head -1
cron_name=$(grep "new Env" $raw_file_name | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1) )
[[ -z $cron_name ]] && cron_name="$raw_file_name" cron_name=$(grep "new Env" $raw_file_name | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1)
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $raw_file_name | awk -F ":" '{print $2}' | head -1 | xargs) [[ -z $cron_name ]] && cron_name="$raw_file_name"
[[ -z $cron_line ]] && cron_line=$(grep "cron " $raw_file_name | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs) [[ -z $cron_line ]] && cron_line=$(grep "cron:" $raw_file_name | awk -F ":" '{print $2}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line="$default_cron" [[ -z $cron_line ]] && cron_line=$(grep "cron " $raw_file_name | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
if [[ -z $cron_id ]]; then [[ -z $cron_line ]] && cron_line="$default_cron"
result=$(add_cron_api "$cron_line:$cmd_task $filename:$cron_name") result=$(add_cron_api "$cron_line:$cmd_task $filename:$cron_name:$SUB_ID")
echo -e "$result\n" echo -e "$result\n"
notify_api "新增任务通知" "\n$result" notify_api "新增任务通知" "\n$result"
# update_cron_api "$cron_line:$cmd_task $filename:$cron_name:$cron_id" # update_cron_api "$cron_line:$cmd_task $filename:$cron_name:$cron_id"
@@ -221,10 +231,10 @@ usage() {
update_qinglong() { update_qinglong() {
local mirror="github" local mirror="github"
local githubStatus=$(curl -s -m 2 -IL "https://github.com" | grep 200) local githubStatus=$(curl -s -m 2 -IL "https://github.com" | grep 200)
if [ "$githubStatus" == "" ]; then if [[ ! -z $githubStatus ]]; then
mirror="gitee" mirror="gitee"
fi fi
echo -e "\n使用 ${mirror} 源更新...\n" echo -e "使用 ${mirror} 源更新...\n"
export isFirstStartServer=false export isFirstStartServer=false
local all_branch=$(cd ${dir_root} && git branch -a) local all_branch=$(cd ${dir_root} && git branch -a)
@@ -238,6 +248,7 @@ 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
update_depend update_depend
@@ -263,6 +274,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"
reset_romote_url ${ql_static_repo} ${url} ${primary_branch}
rm -rf $dir_static/* rm -rf $dir_static/*
cp -rf $ql_static_repo/* $dir_static cp -rf $ql_static_repo/* $dir_static
@@ -286,6 +298,15 @@ diff_scripts() {
local blackword="$4" local blackword="$4"
local dependence="$5" local dependence="$5"
local extensions="$6" local extensions="$6"
local autoAddCron="$7"
local autoDelCron="$8"
if [[ ! $autoAddCron ]];then
autoAddCron=${AutoAddCron}
fi
if [[ ! $autoDelCron ]];then
autoDelCron=${AutoDelCron}
fi
gen_list_repo "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions" gen_list_repo "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions"
@@ -295,13 +316,13 @@ diff_scripts() {
if [[ -s $list_drop ]]; then if [[ -s $list_drop ]]; then
output_list_add_drop $list_drop "失效" output_list_add_drop $list_drop "失效"
if [[ ${AutoDelCron} == true ]]; then if [[ ${autoDelCron} == true ]]; then
del_cron $list_drop $uniq_path del_cron $list_drop $uniq_path
fi fi
fi fi
if [[ -s $list_add ]]; then if [[ -s $list_add ]]; then
output_list_add_drop $list_add "新" output_list_add_drop $list_add "新"
if [[ ${AutoAddCron} == true ]]; then if [[ ${autoAddCron} == true ]]; then
add_cron $list_add $uniq_path add_cron $list_add $uniq_path
fi fi
fi fi
@@ -402,13 +423,16 @@ main() {
done done
[[ "$show_log" == "true" ]] && shift $(($OPTIND - 1)) [[ "$show_log" == "true" ]] && shift $(($OPTIND - 1))
local p1=$1 local p1="${1}"
local p2=$2 local p2="${2}"
local p3=$3 local p3="${3}"
local p4=$4 local p4="${4}"
local p5=$5 local p5="${5}"
local p6=$6 local p6="${6}"
local p7=$7 local p7="${7}"
local p8="${8}"
local p9="${9}"
local p10="${10}"
local log_dir="${p1}" local log_dir="${p1}"
make_dir "$dir_log/$log_dir" make_dir "$dir_log/$log_dir"
local log_time=$(date "+%Y-%m-%d-%H-%M-%S") local log_time=$(date "+%Y-%m-%d-%H-%M-%S")
@@ -450,7 +474,7 @@ main() {
repo) repo)
get_uniq_path "$p2" "$p6" get_uniq_path "$p2" "$p6"
if [[ -n $p2 ]]; then if [[ -n $p2 ]]; then
update_repo "$p2" "$p3" "$p4" "$p5" "$p6" "$p7" "$p8" update_repo "$p2" "$p3" "$p4" "$p5" "$p6" "$p7" "$p8" "$p9" "$p10"
else else
eval echo -e "命令输入错误...\\\n" $cmd eval echo -e "命令输入错误...\\\n" $cmd
eval usage $cmd eval usage $cmd
@@ -459,7 +483,7 @@ main() {
raw) raw)
get_uniq_path "$p2" get_uniq_path "$p2"
if [[ -n $p2 ]]; then if [[ -n $p2 ]]; then
update_raw "$p2" update_raw "$p2" "$p3" "$p4"
else else
eval echo -e "命令输入错误...\\\n" $cmd eval echo -e "命令输入错误...\\\n" $cmd
eval usage $cmd eval usage $cmd
@@ -497,8 +521,7 @@ main() {
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time" [[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
if [[ "$p1" != "repo" ]] && [[ "$p1" != "raw" ]]; then if [[ "$p1" != "repo" ]] && [[ "$p1" != "raw" ]]; then
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time     " $cmd
eval echo -e "\\\n          " $cmd
fi fi
if [[ -f $file_path ]]; then if [[ -f $file_path ]]; then
+39
View File
@@ -0,0 +1,39 @@
import React, { useRef, useState, useEffect } from 'react';
import { Tooltip, Typography } from 'antd';
import { CopyOutlined, CheckOutlined } from '@ant-design/icons';
import { CopyToClipboard } from 'react-copy-to-clipboard';
const { Link } = Typography;
const Copy = ({ text }: { text: string }) => {
const [copied, setCopied] = useState(false);
const copyIdRef = useRef<number>();
const copyText = (e?: React.MouseEvent) => {
e?.preventDefault();
e?.stopPropagation();
setCopied(true);
cleanCopyId();
copyIdRef.current = window.setTimeout(() => {
setCopied(false);
}, 3000);
};
const cleanCopyId = () => {
window.clearTimeout(copyIdRef.current!);
};
return (
<Link onClick={copyText} style={{ marginLeft: 1 }}>
<CopyToClipboard text={text}>
<Tooltip key="copy" title={copied ? '复制成功' : '复制'}>
{copied ? <CheckOutlined /> : <CopyOutlined />}
</Tooltip>
</CopyToClipboard>
</Link>
);
};
export default Copy;
+2 -1
View File
@@ -10,6 +10,7 @@ import {
ContainerOutlined, ContainerOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import IconFont from '@/components/iconfont'; import IconFont from '@/components/iconfont';
import { BasicLayoutProps } from '@ant-design/pro-layout';
export default { export default {
route: { route: {
@@ -93,4 +94,4 @@ export default {
contentWidth: 'Fixed', contentWidth: 'Fixed',
splitMenus: false, splitMenus: false,
siderWidth: 180, siderWidth: 180,
} as any; } as BasicLayoutProps;
+5
View File
@@ -347,4 +347,9 @@ 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;
}
.virtuallist .ant-table-tbody > tr > td > div {
white-space: unset !important;
} }
+18 -16
View File
@@ -25,6 +25,7 @@ import {
Popover, Popover,
Descriptions, Descriptions,
Tooltip, Tooltip,
MenuProps,
} from 'antd'; } from 'antd';
// @ts-ignore // @ts-ignore
import SockJS from 'sockjs-client'; import SockJS from 'sockjs-client';
@@ -222,9 +223,6 @@ export default function () {
} }
if (['/login', '/initialization', '/error'].includes(location.pathname)) { if (['/login', '/initialization', '/error'].includes(location.pathname)) {
document.title = `${
(config.documentTitleMap as any)[location.pathname]
} - 控制面板`;
if (systemInfo?.isInitialized && location.pathname === '/initialization') { if (systemInfo?.isInitialized && location.pathname === '/initialization') {
history.push('/crontab'); history.push('/crontab');
} }
@@ -251,13 +249,17 @@ export default function () {
!navigator.userAgent.includes('Chrome'); !navigator.userAgent.includes('Chrome');
const isQQBrowser = navigator.userAgent.includes('QQBrowser'); const isQQBrowser = navigator.userAgent.includes('QQBrowser');
const menu = ( const menu: MenuProps = {
<Menu items: [
className="side-menu-user-drop-menu" {
items={[{ label: '退出登录', key: 'logout', icon: <LogoutOutlined /> }]} label: '退出登录',
onClick={logout} className: 'side-menu-user-drop-menu',
/> onClick: logout,
); key: 'logout',
icon: <LogoutOutlined />,
},
],
};
return loading ? ( return loading ? (
<PageLoading /> <PageLoading />
) : ( ) : (
@@ -266,6 +268,7 @@ export default function () {
loading={loading} loading={loading}
ErrorBoundary={Sentry.ErrorBoundary} ErrorBoundary={Sentry.ErrorBoundary}
logo={<Image preview={false} src="https://qn.whyour.cn/logo.png" />} logo={<Image preview={false} src="https://qn.whyour.cn/logo.png" />}
// @ts-ignore
title={ title={
<> <>
<span style={{ fontSize: 16 }}></span> <span style={{ fontSize: 16 }}></span>
@@ -308,16 +311,15 @@ export default function () {
return <Link to={menuItemProps.path}>{defaultDom}</Link>; return <Link to={menuItemProps.path}>{defaultDom}</Link>;
}} }}
pageTitleRender={(props, pageName, info) => { pageTitleRender={(props, pageName, info) => {
if (info && typeof info.pageName === 'string') { const title =
return `${info.pageName} - 控制面板`; (config.documentTitleMap as any)[location.pathname] || '未找到';
} return `${title} - 控制面板`;
return '控制面板';
}} }}
onCollapse={setCollapsed} onCollapse={setCollapsed}
collapsed={collapsed} collapsed={collapsed}
rightContentRender={() => rightContentRender={() =>
ctx.isPhone && ( ctx.isPhone && (
<Dropdown overlay={menu} placement="bottomRight" trigger={['click']}> <Dropdown menu={menu} placement="bottomRight" trigger={['click']}>
<span className="side-menu-user-wrapper"> <span className="side-menu-user-wrapper">
<Avatar <Avatar
shape="square" shape="square"
@@ -339,7 +341,7 @@ export default function () {
}} }}
> >
{!collapsed && !ctx.isPhone && ( {!collapsed && !ctx.isPhone && (
<Dropdown overlay={menu} placement="topLeft" trigger={['hover']}> <Dropdown menu={menu} placement="topLeft" trigger={['hover']}>
<span className="side-menu-user-wrapper"> <span className="side-menu-user-wrapper">
<Avatar <Avatar
shape="square" shape="square"
+18
View File
@@ -0,0 +1,18 @@
import React from 'react';
import { Button, Result, Typography } from 'antd';
const { Link } = Typography;
const NotFound: React.FC = () => (
<Result
status="404"
title="404"
extra={
<Button type="primary">
<Link href="/"></Link>
</Button>
}
/>
);
export default NotFound;
+12
View File
@@ -179,3 +179,15 @@ tr.drop-over-upward td {
margin-left: 40px; margin-left: 40px;
} }
} }
.log-modal {
.log-container {
overflow-y: auto;
min-height: 300px;
max-height: calc(80vh - 110px);
max-height: calc(80vh - var(--vh-offset, 110px));
padding: 24px;
margin: -24px;
}
}
+86 -91
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef, useMemo } from 'react';
import { import {
Button, Button,
message, message,
@@ -14,6 +14,7 @@ import {
Popover, Popover,
Tabs, Tabs,
TablePaginationConfig, TablePaginationConfig,
MenuProps,
} from 'antd'; } from 'antd';
import { import {
ClockCircleOutlined, ClockCircleOutlined,
@@ -50,7 +51,9 @@ 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'; import { getCommandScript, parseCrontab } from '@/utils';
import { ColumnProps } from 'antd/lib/table';
import { VList } from 'virtuallist-antd';
const { Text, Paragraph } = Typography; const { Text, Paragraph } = Typography;
const { Search } = Input; const { Search } = Input;
@@ -82,9 +85,23 @@ enum OperationPath {
'unpin', 'unpin',
} }
export interface ICrontab {
name: string;
command: string;
schedule: string;
id: number;
status: number;
isDisabled?: 1 | 0;
isPinned?: 1 | 0;
labels?: string[];
last_running_time?: number;
last_execution_time?: number;
nextRunTime: Date;
}
const Crontab = () => { const Crontab = () => {
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>(); const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
const columns: any = [ const columns: ColumnProps<ICrontab>[] = [
{ {
title: '名称', title: '名称',
dataIndex: 'name', dataIndex: 'name',
@@ -138,16 +155,16 @@ const Crontab = () => {
</> </>
), ),
sorter: { sorter: {
compare: (a: any, b: any) => a?.name?.localeCompare(b?.name), compare: (a, b) => a?.name?.localeCompare(b?.name),
}, },
}, },
{ {
title: '命令', title: '命令/脚本',
dataIndex: 'command', dataIndex: 'command',
key: 'command', key: 'command',
width: 300, width: 300,
align: 'center' as const, align: 'center' as const,
render: (text: string, record: any) => { render: (text, record) => {
return ( return (
<Paragraph <Paragraph
style={{ style={{
@@ -178,7 +195,7 @@ const Crontab = () => {
width: 110, width: 110,
align: 'center' as const, align: 'center' as const,
sorter: { sorter: {
compare: (a: any, b: any) => a.schedule.localeCompare(b.schedule), compare: (a, b) => a.schedule.localeCompare(b.schedule),
}, },
}, },
{ {
@@ -188,11 +205,11 @@ const Crontab = () => {
key: 'last_execution_time', key: 'last_execution_time',
width: 150, width: 150,
sorter: { sorter: {
compare: (a: any, b: any) => { compare: (a, b) => {
return a.last_execution_time - b.last_execution_time; return (a.last_execution_time || 0) - (b.last_execution_time || 0);
}, },
}, },
render: (text: string, record: any) => { render: (text, record) => {
const language = navigator.language || navigator.languages[0]; const language = navigator.language || navigator.languages[0];
return ( return (
<span <span
@@ -222,7 +239,7 @@ const Crontab = () => {
return a.last_running_time - b.last_running_time; return a.last_running_time - b.last_running_time;
}, },
}, },
render: (text: string, record: any) => { render: (text, record) => {
return record.last_running_time return record.last_running_time
? diffTime(record.last_running_time) ? diffTime(record.last_running_time)
: '-'; : '-';
@@ -237,7 +254,7 @@ const Crontab = () => {
return a.nextRunTime - b.nextRunTime; return a.nextRunTime - b.nextRunTime;
}, },
}, },
render: (text: string, record: any) => { render: (text, record) => {
const language = navigator.language || navigator.languages[0]; const language = navigator.language || navigator.languages[0];
return record.nextRunTime return record.nextRunTime
.toLocaleString(language, { .toLocaleString(language, {
@@ -246,13 +263,12 @@ const Crontab = () => {
.replace(' 24:', ' 00:'); .replace(' 24:', ' 00:');
}, },
}, },
{ {
title: '状态', title: '状态',
key: 'status', key: 'status',
dataIndex: 'status', dataIndex: 'status',
align: 'center' as const, align: 'center' as const,
width: 85, width: 88,
filters: [ filters: [
{ {
text: '运行中', text: '运行中',
@@ -271,14 +287,14 @@ const Crontab = () => {
value: 3, value: 3,
}, },
], ],
onFilter: (value: number, record: any) => { onFilter: (value, record) => {
if (record.isDisabled && record.status !== 0) { if (record.isDisabled && record.status !== 0) {
return value === 2; return value === 2;
} else { } else {
return record.status === value; return record.status === value;
} }
}, },
render: (text: string, record: any) => ( render: (text, record) => (
<> <>
{(!record.isDisabled || record.status !== CrontabStatus.idle) && ( {(!record.isDisabled || record.status !== CrontabStatus.idle) && (
<> <>
@@ -315,7 +331,7 @@ const Crontab = () => {
key: 'action', key: 'action',
align: 'center' as const, align: 'center' as const,
width: 100, width: 100,
render: (text: string, record: any, index: number) => { render: (text, record, index) => {
const isPc = !isPhone; const isPc = !isPhone;
return ( return (
<Space size="middle"> <Space size="middle">
@@ -430,10 +446,7 @@ const Crontab = () => {
data.map((x) => { data.map((x) => {
return { return {
...x, ...x,
nextRunTime: cron_parser nextRunTime: parseCrontab(x.schedule),
.parseExpression(x.schedule)
.next()
.toDate(),
}; };
}), }),
); );
@@ -671,15 +684,13 @@ const Crontab = () => {
arrow={{ pointAtCenter: true }} arrow={{ pointAtCenter: true }}
placement="bottomRight" placement="bottomRight"
trigger={['click']} trigger={['click']}
overlay={ menu={{
<Menu items: getMenuItems(record),
items={getMenuItems(record)} onClick: ({ key, domEvent }) => {
onClick={({ key, domEvent }) => { domEvent.stopPropagation();
domEvent.stopPropagation(); action(key, record, index);
action(key, record, index); },
}} }}
/>
}
> >
<a onClick={(e) => e.stopPropagation()}> <a onClick={(e) => e.stopPropagation()}>
<EllipsisOutlined /> <EllipsisOutlined />
@@ -706,34 +717,15 @@ const Crontab = () => {
} }
}; };
const handleCancel = (cron?: any) => { const handleCancel = () => {
setIsModalVisible(false); setIsModalVisible(false);
if (cron) { getCrons();
handleCrons(cron);
}
}; };
const onSearch = (value: string) => { const onSearch = (value: string) => {
setSearchText(value.trim()); setSearchText(value.trim());
}; };
const handleCrons = (cron: any) => {
const index = value.findIndex((x) => x.id === cron.id);
const result = [...value];
cron.nextRunTime = cron_parser
.parseExpression(cron.schedule)
.next()
.toDate();
if (index === -1) {
result.unshift(cron);
} else {
result.splice(index, 1, {
...cron,
});
}
setValue(result);
};
const getCronDetail = (cron: any) => { const getCronDetail = (cron: any) => {
request request
.get(`${config.apiPrefix}crons/${cron.id}`) .get(`${config.apiPrefix}crons/${cron.id}`)
@@ -741,10 +733,7 @@ const Crontab = () => {
if (code === 200) { if (code === 200) {
const index = value.findIndex((x) => x.id === cron.id); const index = value.findIndex((x) => x.id === cron.id);
const result = [...value]; const result = [...value];
data.nextRunTime = cron_parser data.nextRunTime = parseCrontab(data.schedule);
.parseExpression(data.schedule)
.next()
.toDate();
if (index !== -1) { if (index !== -1) {
result.splice(index, 1, { result.splice(index, 1, {
...cron, ...cron,
@@ -876,41 +865,39 @@ const Crontab = () => {
} }
}; };
const menu = ( const menu: MenuProps = {
<Menu onClick: ({ key, domEvent }) => {
onClick={({ key, domEvent }) => { domEvent.stopPropagation();
domEvent.stopPropagation(); viewAction(key);
viewAction(key); },
}} items: [
items={[ ...[...enabledCronViews].slice(4).map((x) => ({
...[...enabledCronViews].slice(4).map((x) => ({ label: (
label: ( <Space style={{ display: 'flex', justifyContent: 'space-between' }}>
<Space style={{ display: 'flex', justifyContent: 'space-between' }}> <span>{x.name}</span>
<span>{x.name}</span> {viewConf?.id === x.id && (
{viewConf?.id === x.id && ( <CheckOutlined style={{ color: '#1890ff' }} />
<CheckOutlined style={{ color: '#1890ff' }} /> )}
)} </Space>
</Space> ),
), key: x.id,
key: x.id, icon: <UnorderedListOutlined />,
icon: <UnorderedListOutlined />, })),
})), {
{ type: 'divider' as 'group',
type: 'divider', },
}, {
{ label: '新建视图',
label: '新建视图', key: 'new',
key: 'new', icon: <PlusOutlined />,
icon: <PlusOutlined />, },
}, {
{ label: '视图管理',
label: '视图管理', key: 'manage',
key: 'manage', icon: <SettingOutlined />,
icon: <SettingOutlined />, },
}, ],
]} };
/>
);
const getCronViews = () => { const getCronViews = () => {
setLoading(true); setLoading(true);
@@ -934,6 +921,13 @@ const Crontab = () => {
setViewConf(view ? view : null); setViewConf(view ? view : null);
}; };
const vComponents = useMemo(() => {
return VList({
height: tableScrollHeight!,
resetTopWhenDataChange: false,
});
}, [tableScrollHeight]);
return ( return (
<PageContainer <PageContainer
className="ql-container-wrapper crontab-wrapper ql-container-wrapper-has-tab" className="ql-container-wrapper crontab-wrapper ql-container-wrapper-has-tab"
@@ -964,7 +958,7 @@ const Crontab = () => {
className={`crontab-view ${moreMenuActive ? 'more-active' : ''}`} className={`crontab-view ${moreMenuActive ? 'more-active' : ''}`}
tabBarExtraContent={ tabBarExtraContent={
<Dropdown <Dropdown
overlay={menu} menu={menu}
trigger={['click']} trigger={['click']}
overlayStyle={{ minWidth: 200 }} overlayStyle={{ minWidth: 200 }}
> >
@@ -1068,6 +1062,7 @@ const Crontab = () => {
rowSelection={rowSelection} rowSelection={rowSelection}
rowClassName={getRowClassName} rowClassName={getRowClassName}
onChange={onPageChange} onChange={onPageChange}
components={vComponents}
/> />
</div> </div>
<CronLogModal <CronLogModal
+54 -23
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { Modal, message, Input, Form, Statistic, Button } from 'antd'; import { Modal, message, Input, Form, Statistic, Button } from 'antd';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import config from '@/utils/config'; import config from '@/utils/config';
@@ -34,7 +34,7 @@ const CronLogModal = ({
const [loading, setLoading] = useState<any>(true); const [loading, setLoading] = useState<any>(true);
const [executing, setExecuting] = useState<any>(true); const [executing, setExecuting] = useState<any>(true);
const [isPhone, setIsPhone] = useState(false); const [isPhone, setIsPhone] = useState(false);
const [theme, setTheme] = useState<string>(''); const scrollInfoRef = useRef({ value: 0, down: true });
const getCronLog = (isFirst?: boolean) => { const getCronLog = (isFirst?: boolean) => {
if (isFirst) { if (isFirst) {
@@ -45,14 +45,20 @@ const CronLogModal = ({
.then(({ code, data }) => { .then(({ code, data }) => {
if ( if (
code === 200 && code === 200 &&
localStorage.getItem('logCron') === String(cron.id) localStorage.getItem('logCron') === String(cron.id) &&
data !== value
) { ) {
const log = data as string; const log = data as string;
setValue(log || '暂无日志'); setValue(log || '暂无日志');
setExecuting( const hasNext = Boolean(
log && !logEnded(log) && !log.includes('重启面板'), log &&
!logEnded(log) &&
!log.includes('重启面板') &&
!log.includes('任务未运行或运行失败,请尝试手动运行'),
); );
if (log && !logEnded(log) && !log.includes('重启面板')) { setExecuting(hasNext);
autoScroll();
if (hasNext) {
setTimeout(() => { setTimeout(() => {
getCronLog(); getCronLog();
}, 2000); }, 2000);
@@ -89,11 +95,33 @@ const CronLogModal = ({
}); });
}; };
const autoScroll = () => {
if (!scrollInfoRef.current.down) {
return;
}
setTimeout(() => {
document
.querySelector('#log-flag')!
.scrollIntoView({ behavior: 'smooth' });
}, 1000);
};
const cancel = () => { const cancel = () => {
localStorage.removeItem('logCron'); localStorage.removeItem('logCron');
handleCancel(); handleCancel();
}; };
const handleScroll = (e) => {
const sTop = e.target.scrollTop;
if (scrollInfoRef.current.down) {
scrollInfoRef.current = {
value: sTop,
down: sTop > scrollInfoRef.current.value,
};
}
};
const titleElement = () => { const titleElement = () => {
return ( return (
<> <>
@@ -138,23 +166,26 @@ const CronLogModal = ({
</Button>, </Button>,
]} ]}
> >
{loading ? ( <div onScroll={handleScroll} className="log-container">
<PageLoading /> {loading ? (
) : ( <PageLoading />
<pre ) : (
style={ <pre
isPhone style={
? { isPhone
fontFamily: 'Source Code Pro', ? {
width: 375, fontFamily: 'Source Code Pro',
zoom: 0.83, width: 375,
} zoom: 0.83,
: {} }
} : {}
> }
{value} >
</pre> {value}
)} </pre>
)}
<div id="log-flag"></div>
</div>
</Modal> </Modal>
); );
}; };
+2 -2
View File
@@ -74,13 +74,13 @@ const CronModal = ({
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="command" name="command"
label="命令" label="命令/脚本"
rules={[{ required: true, whitespace: true }]} rules={[{ required: true, whitespace: true }]}
> >
<Input.TextArea <Input.TextArea
rows={4} rows={4}
autoSize={true} autoSize={true}
placeholder="请输入要执行命令" placeholder="支持输入脚本路径/任意系统可执行命令/task 脚本路径"
/> />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
-1
View File
@@ -167,7 +167,6 @@ const ViewManageModal = ({
}; };
const onShowChange = (checked: boolean, record: any, index: number) => { const onShowChange = (checked: boolean, record: any, index: number) => {
console.log(checked);
request request
.put(`${config.apiPrefix}crons/views/${checked ? 'enable' : 'disable'}`, { .put(`${config.apiPrefix}crons/views/${checked ? 'enable' : 'disable'}`, {
data: [record.id], data: [record.id],
+6
View File
@@ -5,3 +5,9 @@ tr.drop-over-downward td {
tr.drop-over-upward td { tr.drop-over-upward td {
border-top: 2px dashed #1890ff; border-top: 2px dashed #1890ff;
} }
.text-ellipsis {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
+88 -78
View File
@@ -1,4 +1,10 @@
import React, { useCallback, useRef, useState, useEffect } from 'react'; import React, {
useCallback,
useRef,
useState,
useEffect,
useMemo,
} from 'react';
import { import {
Button, Button,
message, message,
@@ -32,8 +38,10 @@ import { exportJson } from '@/utils/index';
import { useOutletContext } from '@umijs/max'; import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts'; import { SharedContext } from '@/layouts';
import useTableScrollHeight from '@/hooks/useTableScrollHeight'; import useTableScrollHeight from '@/hooks/useTableScrollHeight';
import Copy from '../../components/copy';
import { VList } from 'virtuallist-antd';
const { Text, Paragraph } = Typography; const { Text } = Typography;
const { Search } = Input; const { Search } = Input;
enum Status { enum Status {
@@ -58,50 +66,6 @@ enum OperationPath {
const type = 'DragableBodyRow'; const type = 'DragableBodyRow';
const DragableBodyRow = ({
index,
moveRow,
className,
style,
...restProps
}: any) => {
const ref = useRef();
const [{ isOver, dropClassName }, drop] = useDrop({
accept: type,
collect: (monitor) => {
const { index: dragIndex } = (monitor.getItem() as any) || {};
if (dragIndex === index) {
return {};
}
return {
isOver: monitor.isOver(),
dropClassName:
dragIndex < index ? ' drop-over-downward' : ' drop-over-upward',
};
},
drop: (item: any) => {
moveRow(item.index, index);
},
});
const [, drag] = useDrag({
type,
item: { index },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
drop(drag(ref));
return (
<tr
ref={ref}
className={`${className}${isOver ? dropClassName : ''}`}
style={{ cursor: 'move', ...style }}
{...restProps}
/>
);
};
const Env = () => { const Env = () => {
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>(); const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
const columns: any = [ const columns: any = [
@@ -128,17 +92,12 @@ const Env = () => {
width: '35%', width: '35%',
render: (text: string, record: any) => { render: (text: string, record: any) => {
return ( return (
<Paragraph <div style={{ display: 'flex', alignItems: 'center' }}>
style={{ <Tooltip title={text} placement="topLeft">
wordBreak: 'break-all', <div className="text-ellipsis">{text}</div>
marginBottom: 0, </Tooltip>
textAlign: 'left', <Copy text={text} />
}} </div>
ellipsis={{ tooltip: text, rows: 2 }}
copyable
>
{text}
</Paragraph>
); );
}, },
}, },
@@ -147,6 +106,13 @@ const Env = () => {
dataIndex: 'remarks', dataIndex: 'remarks',
key: 'remarks', key: 'remarks',
align: 'center' as const, align: 'center' as const,
render: (text: string, record: any) => {
return (
<Tooltip title={text} placement="topLeft">
<div className="text-ellipsis">{text}</div>
</Tooltip>
);
},
}, },
{ {
title: '更新时间', title: '更新时间',
@@ -256,7 +222,7 @@ const Env = () => {
const [searchText, setSearchText] = useState(''); const [searchText, setSearchText] = useState('');
const [importLoading, setImportLoading] = useState(false); const [importLoading, setImportLoading] = useState(false);
const tableRef = useRef<any>(); const tableRef = useRef<any>();
const tableScrollHeight = useTableScrollHeight(tableRef, 59) const tableScrollHeight = useTableScrollHeight(tableRef, 59);
const getEnvs = () => { const getEnvs = () => {
setLoading(true); setLoading(true);
@@ -286,7 +252,8 @@ const Env = () => {
onOk() { onOk() {
request request
.put( .put(
`${config.apiPrefix}envs/${record.status === Status. ? 'enable' : 'disable' `${config.apiPrefix}envs/${
record.status === Status. ? 'enable' : 'disable'
}`, }`,
{ {
data: [record.id], data: [record.id],
@@ -356,7 +323,7 @@ const Env = () => {
const handleCancel = (env?: any[]) => { const handleCancel = (env?: any[]) => {
setIsModalVisible(false); setIsModalVisible(false);
env && handleEnv(env); getEnvs();
}; };
const handleEditNameCancel = (env?: any[]) => { const handleEditNameCancel = (env?: any[]) => {
@@ -364,28 +331,71 @@ const Env = () => {
getEnvs(); getEnvs();
}; };
const handleEnv = (env: any) => { const vComponents = useMemo(() => {
const result = [...value]; return VList({
const index = value.findIndex((x) => x.id === env.id); height: tableScrollHeight!,
if (index === -1) { resetTopWhenDataChange: false,
env = Array.isArray(env) ? env : [env]; });
result.push(...env); }, [tableScrollHeight]);
} else {
result.splice(index, 1, { const DragableBodyRow = (props: any) => {
...env, const { index, moveRow, className, style, ...restProps } = props;
}); const ref = useRef();
} const [{ isOver, dropClassName }, drop] = useDrop({
setValue(result); accept: type,
collect: (monitor) => {
const { index: dragIndex } = (monitor.getItem() as any) || {};
if (dragIndex === index) {
return {};
}
return {
isOver: monitor.isOver(),
dropClassName:
dragIndex < index ? ' drop-over-downward' : ' drop-over-upward',
};
},
drop: (item: any) => {
moveRow(item.index, index);
},
});
const [, drag] = useDrag({
type,
item: { index },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
useEffect(() => {
drop(drag(ref));
}, [drag, drop]);
const components = useMemo(() => vComponents.body.row, []);
const tempProps = useMemo(() => {
return {
ref: ref,
className: `${className}${isOver ? dropClassName : ''}`,
style: { cursor: 'move', ...style },
...restProps,
};
}, [className, dropClassName, restProps, style, isOver]);
return <> {components(tempProps, ref)} </>;
}; };
const components = { const components = useMemo(() => {
body: { return {
row: DragableBodyRow, ...vComponents,
}, body: {
}; ...vComponents.body,
row: DragableBodyRow,
},
};
}, [vComponents]);
const moveRow = useCallback( const moveRow = useCallback(
(dragIndex, hoverIndex) => { (dragIndex: number, hoverIndex: number) => {
if (dragIndex === hoverIndex) { if (dragIndex === hoverIndex) {
return; return;
} }
+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>
); );
+1 -1
View File
@@ -101,7 +101,7 @@ const EditModal = ({
}; };
const stop = () => { const stop = () => {
if (!cNode || !cNode.title) { if (!cNode || !cNode.title || !currentPid) {
return; return;
} }
request request
+51 -48
View File
@@ -11,6 +11,7 @@ import {
Dropdown, Dropdown,
Menu, Menu,
Empty, Empty,
MenuProps,
} from 'antd'; } from 'antd';
import config from '@/utils/config'; import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout'; import { PageContainer } from '@ant-design/pro-layout';
@@ -35,7 +36,7 @@ import EditScriptNameModal from './editNameModal';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { history, useOutletContext, useLocation } from '@umijs/max'; import { history, useOutletContext, useLocation } from '@umijs/max';
import { parse } from 'query-string'; import { parse } from 'query-string';
import { depthFirstSearch } from '@/utils'; import { depthFirstSearch, findNode } 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';
@@ -66,7 +67,8 @@ 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 [isRenameFileModalVisible, setIsRenameFileModalVisible] =
useState(false);
const [currentNode, setCurrentNode] = useState<any>(); const [currentNode, setCurrentNode] = useState<any>();
const [expandedKeys, setExpandedKeys] = useState<string[]>([]); const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
@@ -78,7 +80,7 @@ const Script = () => {
if (code === 200) { if (code === 200) {
setData(data); setData(data);
initState(); initState();
initGetScript(); initGetScript(data);
} }
}) })
.finally(() => needLoading && setLoading(false)); .finally(() => needLoading && setLoading(false));
@@ -98,7 +100,7 @@ const Script = () => {
}); });
}; };
const initGetScript = () => { const initGetScript = (_data: any) => {
const { p, s } = parse(history.location.search); const { p, s } = parse(history.location.search);
if (s) { if (s) {
const vkey = `${p}/${s}`; const vkey = `${p}/${s}`;
@@ -109,8 +111,11 @@ const Script = () => {
parent: p, parent: p,
}, },
}; };
setExpandedKeys([p]); const item = findNode(_data, (c) => c.key === obj.node.key);
onTreeSelect([vkey], obj); if (item) {
setExpandedKeys([p as string]);
onTreeSelect([vkey], obj);
}
} }
}; };
@@ -293,12 +298,12 @@ const Script = () => {
const renameFile = () => { const renameFile = () => {
setIsRenameFileModalVisible(true); setIsRenameFileModalVisible(true);
} };
const handleRenameFileCancel = () => { const handleRenameFileCancel = () => {
setIsRenameFileModalVisible(false); setIsRenameFileModalVisible(false);
getScripts(false); getScripts(false);
} };
const addFile = () => { const addFile = () => {
setIsAddFileModalVisible(true); setIsAddFileModalVisible(true);
@@ -385,7 +390,7 @@ const Script = () => {
const menuAction = (key: string | number) => { const menuAction = (key: string | number) => {
switch (key) { switch (key) {
case 'save': case 'add':
addFile(); addFile();
break; break;
case 'edit': case 'edit':
@@ -402,46 +407,44 @@ const Script = () => {
} }
}; };
const menu = isEditing ? ( const menu: MenuProps = isEditing
<Menu ? {
items={[ items: [
{ label: '保存', key: 'save', icon: <PlusOutlined /> }, { label: '保存', key: 'save', icon: <PlusOutlined /> },
{ label: '退出编辑', key: 'exit', icon: <EditOutlined /> }, { label: '退出编辑', key: 'exit', icon: <EditOutlined /> },
]} ],
onClick={({ key, domEvent }) => { onClick: ({ key, domEvent }) => {
domEvent.stopPropagation(); domEvent.stopPropagation();
action(key); action(key);
}}
/>
) : (
<Menu
items={[
{ label: '新建', key: 'add', icon: <PlusOutlined /> },
{
label: '编辑',
key: 'edit',
icon: <EditOutlined />,
disabled: !select,
}, },
{ }
label: '重命名', : {
key: 'rename', items: [
icon: <IconFont type="ql-icon-rename" />, { label: '新建', key: 'add', icon: <PlusOutlined /> },
disabled: !select, {
label: '编辑',
key: 'edit',
icon: <EditOutlined />,
disabled: !select,
},
{
label: '重命名',
key: 'rename',
icon: <IconFont type="ql-icon-rename" />,
disabled: !select,
},
{
label: '删除',
key: 'delete',
icon: <DeleteOutlined />,
disabled: !select,
},
],
onClick: ({ key, domEvent }) => {
domEvent.stopPropagation();
menuAction(key);
}, },
{ };
label: '删除',
key: 'delete',
icon: <DeleteOutlined />,
disabled: !select,
},
]}
onClick={({ key, domEvent }) => {
domEvent.stopPropagation();
menuAction(key);
}}
/>
);
return ( return (
<PageContainer <PageContainer
@@ -464,7 +467,7 @@ const Script = () => {
allowClear allowClear
onSelect={onSelect} onSelect={onSelect}
/>, />,
<Dropdown overlay={menu} trigger={['click']}> <Dropdown menu={menu} trigger={['click']}>
<Button type="primary" icon={<EllipsisOutlined />} /> <Button type="primary" icon={<EllipsisOutlined />} />
</Dropdown>, </Dropdown>,
] ]
-1
View File
@@ -12,7 +12,6 @@ const RenameModal = ({
visible: boolean; visible: boolean;
handleCancel: () => void; handleCancel: () => void;
}) => { }) => {
console.log(currentNode);
const [form] = Form.useForm(); const [form] = Form.useForm();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
+6 -105
View File
@@ -17,7 +17,6 @@ import {
import config from '@/utils/config'; import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout'; import { PageContainer } from '@ant-design/pro-layout';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import * as DarkReader from '@umijs/ssr-darkreader';
import AppModal from './appModal'; import AppModal from './appModal';
import { import {
EditOutlined, EditOutlined,
@@ -27,18 +26,13 @@ import {
import SecuritySettings from './security'; import SecuritySettings from './security';
import LoginLog from './loginLog'; import LoginLog from './loginLog';
import NotificationSetting from './notification'; import NotificationSetting from './notification';
import CheckUpdate from './checkUpdate'; import Other from './other';
import About from './about'; import About from './about';
import { useOutletContext } from '@umijs/max'; import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts'; import { SharedContext } from '@/layouts';
import './index.less'; import './index.less';
const { Text } = Typography; const { Text } = Typography;
const optionsWithDisabled = [
{ label: '亮色', value: 'light' },
{ label: '暗色', value: 'dark' },
{ label: '跟随系统', value: 'auto' },
];
const Setting = () => { const Setting = () => {
const { const {
@@ -121,37 +115,12 @@ const Setting = () => {
]; ];
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const defaultTheme = localStorage.getItem('qinglong_dark_theme') || 'auto';
const [dataSource, setDataSource] = useState<any[]>([]); const [dataSource, setDataSource] = useState<any[]>([]);
const [isModalVisible, setIsModalVisible] = useState(false); const [isModalVisible, setIsModalVisible] = useState(false);
const [editedApp, setEditedApp] = useState<any>(); const [editedApp, setEditedApp] = useState<any>();
const [tabActiveKey, setTabActiveKey] = useState('security'); const [tabActiveKey, setTabActiveKey] = useState('security');
const [loginLogData, setLoginLogData] = useState<any[]>([]); const [loginLogData, setLoginLogData] = useState<any[]>([]);
const [notificationInfo, setNotificationInfo] = useState<any>(); const [notificationInfo, setNotificationInfo] = useState<any>();
const [logRemoveFrequency, setLogRemoveFrequency] = useState<number>();
const [form] = Form.useForm();
const {
enable: enableDarkMode,
disable: disableDarkMode,
exportGeneratedCSS: collectCSS,
setFetchMethod,
auto: followSystemColorScheme,
} = DarkReader || {};
const themeChange = (e: any) => {
const _theme = e.target.value;
localStorage.setItem('qinglong_dark_theme', e.target.value);
setFetchMethod(fetch);
if (_theme === 'dark') {
enableDarkMode({});
} else if (_theme === 'light') {
disableDarkMode();
} else {
followSystemColorScheme({});
}
reloadTheme();
};
const getApps = () => { const getApps = () => {
setLoading(true); setLoading(true);
@@ -276,8 +245,6 @@ const Setting = () => {
getLoginLog(); getLoginLog();
} else if (activeKey === 'notification') { } else if (activeKey === 'notification') {
getNotification(); getNotification();
} else if (activeKey === 'other') {
getLogRemoveFrequency();
} }
}; };
@@ -294,37 +261,6 @@ const Setting = () => {
}); });
}; };
const getLogRemoveFrequency = () => {
request
.get(`${config.apiPrefix}system/log/remove`)
.then(({ code, data }) => {
if (code === 200 && data.info) {
const { frequency } = data.info;
setLogRemoveFrequency(frequency);
}
})
.catch((error: any) => {
console.log(error);
});
};
const updateRemoveLogFrequency = () => {
setTimeout(() => {
request
.put(`${config.apiPrefix}system/log/remove`, {
data: { frequency: logRemoveFrequency },
})
.then(({ code, data }) => {
if (code === 200) {
message.success('更新成功');
}
})
.catch((error: any) => {
console.log(error);
});
});
};
return ( return (
<PageContainer <PageContainer
className="ql-container-wrapper ql-container-wrapper-has-tab ql-setting-container" className="ql-container-wrapper ql-container-wrapper-has-tab ql-setting-container"
@@ -382,46 +318,11 @@ const Setting = () => {
key: 'other', key: 'other',
label: '其他设置', label: '其他设置',
children: ( children: (
<Form layout="vertical" form={form}> <Other
<Form.Item reloadTheme={reloadTheme}
label="主题设置" socketMessage={socketMessage}
name="theme" systemInfo={systemInfo}
initialValue={defaultTheme} />
>
<Radio.Group
options={optionsWithDisabled}
onChange={themeChange}
value={defaultTheme}
optionType="button"
buttonStyle="solid"
/>
</Form.Item>
<Form.Item
label="日志删除频率"
name="frequency"
tooltip="每x天自动删除x天以前的日志"
>
<Input.Group compact>
<InputNumber
addonBefore="每"
addonAfter="天"
style={{ width: 150 }}
min={0}
value={logRemoveFrequency}
onChange={(value) => setLogRemoveFrequency(value)}
/>
<Button type="primary" onClick={updateRemoveLogFrequency}>
</Button>
</Input.Group>
</Form.Item>
<Form.Item label="检查更新" name="update">
<CheckUpdate
systemInfo={systemInfo}
socketMessage={socketMessage}
/>
</Form.Item>
</Form>
), ),
}, },
{ {
+120
View File
@@ -0,0 +1,120 @@
import React, { useState, useEffect } from 'react';
import { Button, InputNumber, Form, Radio, message, Input } from 'antd';
import * as DarkReader from '@umijs/ssr-darkreader';
import config from '@/utils/config';
import { request } from '@/utils/http';
import CheckUpdate from './checkUpdate';
import { SharedContext } from '@/layouts';
import './index.less';
const optionsWithDisabled = [
{ label: '亮色', value: 'light' },
{ label: '暗色', value: 'dark' },
{ label: '跟随系统', value: 'auto' },
];
const Other = ({
systemInfo,
socketMessage,
reloadTheme,
}: Pick<SharedContext, 'socketMessage' | 'reloadTheme' | 'systemInfo'>) => {
const defaultTheme = localStorage.getItem('qinglong_dark_theme') || 'auto';
const [logRemoveFrequency, setLogRemoveFrequency] = useState<number | null>();
const [form] = Form.useForm();
const {
enable: enableDarkMode,
disable: disableDarkMode,
exportGeneratedCSS: collectCSS,
setFetchMethod,
auto: followSystemColorScheme,
} = DarkReader || {};
const themeChange = (e: any) => {
const _theme = e.target.value;
localStorage.setItem('qinglong_dark_theme', e.target.value);
setFetchMethod(fetch);
if (_theme === 'dark') {
enableDarkMode({});
} else if (_theme === 'light') {
disableDarkMode();
} else {
followSystemColorScheme({});
}
reloadTheme();
};
const getLogRemoveFrequency = () => {
request
.get(`${config.apiPrefix}system/log/remove`)
.then(({ code, data }) => {
if (code === 200 && data.info) {
const { frequency } = data.info;
setLogRemoveFrequency(frequency);
}
})
.catch((error: any) => {
console.log(error);
});
};
const updateRemoveLogFrequency = () => {
setTimeout(() => {
request
.put(`${config.apiPrefix}system/log/remove`, {
data: { frequency: logRemoveFrequency },
})
.then(({ code, data }) => {
if (code === 200) {
message.success('更新成功');
}
})
.catch((error: any) => {
console.log(error);
});
});
};
useEffect(() => {
getLogRemoveFrequency();
}, []);
return (
<Form layout="vertical" form={form}>
<Form.Item label="主题设置" name="theme" initialValue={defaultTheme}>
<Radio.Group
options={optionsWithDisabled}
onChange={themeChange}
value={defaultTheme}
optionType="button"
buttonStyle="solid"
/>
</Form.Item>
<Form.Item
label="日志删除频率"
name="frequency"
tooltip="每x天自动删除x天以前的日志"
>
<Input.Group compact>
<InputNumber
addonBefore="每"
addonAfter="天"
style={{ width: 150 }}
min={0}
value={logRemoveFrequency}
onChange={(value) => setLogRemoveFrequency(value)}
/>
<Button type="primary" onClick={updateRemoveLogFrequency}>
</Button>
</Input.Group>
</Form.Item>
<Form.Item label="检查更新" name="update">
<CheckUpdate systemInfo={systemInfo} socketMessage={socketMessage} />
</Form.Item>
</Form>
);
};
export default Other;
+9
View File
@@ -0,0 +1,9 @@
.inline-form-item {
margin-bottom: 0;
.ant-form-item {
display: inline-block;
width: 50%;
margin-bottom: 0px;
}
}
+21 -26
View File
@@ -243,11 +243,10 @@ const Subscription = () => {
const [searchText, setSearchText] = useState(''); const [searchText, setSearchText] = useState('');
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(20); const [pageSize, setPageSize] = useState(20);
const [searchValue, setSearchValue] = useState('');
const [isLogModalVisible, setIsLogModalVisible] = useState(false); const [isLogModalVisible, setIsLogModalVisible] = useState(false);
const [logSubscription, setLogSubscription] = useState<any>(); const [logSubscription, setLogSubscription] = useState<any>();
const tableRef = useRef<any>(); const tableRef = useRef<any>();
const tableScrollHeight = useTableScrollHeight(tableRef) const tableScrollHeight = useTableScrollHeight(tableRef);
const runSubscription = (record: any, index: number) => { const runSubscription = (record: any, index: number) => {
Modal.confirm({ Modal.confirm({
@@ -428,28 +427,26 @@ const Subscription = () => {
arrow={{ pointAtCenter: true }} arrow={{ pointAtCenter: true }}
placement="bottomRight" placement="bottomRight"
trigger={['click']} trigger={['click']}
overlay={ menu={{
<Menu items: [
items={[ { label: '编辑', key: 'edit', icon: <EditOutlined /> },
{ label: '编辑', key: 'edit', icon: <EditOutlined /> }, {
{ label: record.is_disabled === 1 ? '启用' : '禁用',
label: record.is_disabled === 1 ? '启用' : '禁用', key: 'enableOrDisable',
key: 'enableOrDisable', icon:
icon: record.is_disabled === 1 ? (
record.is_disabled === 1 ? ( <CheckCircleOutlined />
<CheckCircleOutlined /> ) : (
) : ( <StopOutlined />
<StopOutlined /> ),
), },
}, { label: '删除', key: 'delete', icon: <DeleteOutlined /> },
{ label: '删除', key: 'delete', icon: <DeleteOutlined /> }, ],
]} onClick: ({ key, domEvent }) => {
onClick={({ key, domEvent }) => { domEvent.stopPropagation();
domEvent.stopPropagation(); action(key, record, index);
action(key, record, index); },
}} }}
/>
}
> >
<a onClick={(e) => e.stopPropagation()}> <a onClick={(e) => e.stopPropagation()}>
<EllipsisOutlined /> <EllipsisOutlined />
@@ -553,8 +550,6 @@ const Subscription = () => {
enterButton enterButton
allowClear allowClear
loading={loading} loading={loading}
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
onSearch={onSearch} onSearch={onSearch}
/>, />,
<Button key="2" type="primary" onClick={() => addSubscription()}> <Button key="2" type="primary" onClick={() => addSubscription()}>
+52 -9
View File
@@ -1,12 +1,22 @@
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { Modal, message, InputNumber, Form, Radio, Select, Input } from 'antd'; import {
Modal,
message,
InputNumber,
Form,
Radio,
Select,
Input,
Switch,
} from 'antd';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import config from '@/utils/config'; import config from '@/utils/config';
import cron_parser from 'cron-parser'; import cron_parser from 'cron-parser';
import isNil from 'lodash/isNil';
const { Option } = Select; const { Option } = Select;
const repoUrlRegx = /[^\/\:]+\/[^\/]+(?=\.git)/; const repoUrlRegx = /([^\/\:]+\/[^\/]+)(?=\.git)/;
const fileUrlRegx = /[^\/\:]+\/[^\/]+$/; const fileUrlRegx = /([^\/\:]+\/[^\/\.]+)\.[a-z]+$/;
const SubscriptionModal = ({ const SubscriptionModal = ({
subscription, subscription,
@@ -26,7 +36,11 @@ const SubscriptionModal = ({
const handleOk = async (values: any) => { const handleOk = async (values: any) => {
setLoading(true); setLoading(true);
const method = subscription ? 'put' : 'post'; const method = subscription ? 'put' : 'post';
const payload = { ...values }; const payload = {
...values,
autoAddCron: Boolean(values.autoAddCron),
autoDelCron: Boolean(values.autoDelCron),
};
if (subscription) { if (subscription) {
payload.id = subscription.id; payload.id = subscription.id;
} }
@@ -86,7 +100,7 @@ const SubscriptionModal = ({
let _alias = ''; let _alias = '';
const _regx = _type === 'file' ? fileUrlRegx : repoUrlRegx; const _regx = _type === 'file' ? fileUrlRegx : repoUrlRegx;
if (_regx.test(_url)) { if (_regx.test(_url)) {
_alias = _url.match(_regx)![0].replaceAll('/', '_').replaceAll('.', '_'); _alias = _url.match(_regx)![1].replaceAll('/', '_').replaceAll('.', '_');
} }
if (_branch) { if (_branch) {
_alias = _alias + '_' + _branch; _alias = _alias + '_' + _branch;
@@ -110,8 +124,8 @@ const SubscriptionModal = ({
} }
}; };
const numberChange = (value: number) => { const numberChange = (value: number | null) => {
setIntervalNumber(value); setIntervalNumber(value || 1);
if (!value) { if (!value) {
onChange?.(null); onChange?.(null);
} else { } else {
@@ -125,6 +139,7 @@ const SubscriptionModal = ({
setIntervalNumber(value.value); setIntervalNumber(value.value);
} }
}, [value]); }, [value]);
return ( return (
<Input.Group compact> <Input.Group compact>
<InputNumber <InputNumber
@@ -217,7 +232,7 @@ const SubscriptionModal = ({
dependences, dependences,
branch, branch,
extensions, extensions,
alias: formatAlias(url, branch), alias: formatAlias(url, branch, _type),
}); });
setType(_type); setType(_type);
} }
@@ -230,6 +245,14 @@ const SubscriptionModal = ({
} }
}, []); }, []);
const formatParams = (sub) => {
return {
...sub,
autoAddCron: isNil(sub?.autoAddCron) ? true : Boolean(sub?.autoAddCron),
autoDelCron: isNil(sub?.autoDelCron) ? true : Boolean(sub?.autoDelCron),
};
};
useEffect(() => { useEffect(() => {
if (visible) { if (visible) {
window.addEventListener('paste', onPaste); window.addEventListener('paste', onPaste);
@@ -239,7 +262,9 @@ const SubscriptionModal = ({
}, [visible]); }, [visible]);
useEffect(() => { useEffect(() => {
form.setFieldsValue(subscription || {}); form.setFieldsValue(
{ ...subscription, ...formatParams(subscription) } || {},
);
setType((subscription && subscription.type) || 'public-repo'); setType((subscription && subscription.type) || 'public-repo');
setScheduleType((subscription && subscription.schedule_type) || 'crontab'); setScheduleType((subscription && subscription.schedule_type) || 'crontab');
setPullType((subscription && subscription.pull_type) || 'ssh-key'); setPullType((subscription && subscription.pull_type) || 'ssh-key');
@@ -452,6 +477,24 @@ const SubscriptionModal = ({
} }
/> />
</Form.Item> </Form.Item>
<Form.Item style={{ marginBottom: 0 }} className="inline-form-item">
<Form.Item
name="autoAddCron"
label="自动添加任务"
valuePropName="checked"
initialValue={true}
>
<Switch />
</Form.Item>
<Form.Item
name="autoDelCron"
label="自动删除任务"
valuePropName="checked"
initialValue={true}
>
<Switch />
</Form.Item>
</Form.Item>
</Form> </Form>
</Modal> </Modal>
); );
+2 -1
View File
@@ -296,7 +296,7 @@ export default {
documentTitleMap: { documentTitleMap: {
'/login': '登录', '/login': '登录',
'/initialization': '初始化', '/initialization': '初始化',
'/cron': '定时任务', '/crontab': '定时任务',
'/env': '环境变量', '/env': '环境变量',
'/subscription': '订阅管理', '/subscription': '订阅管理',
'/config': '配置文件', '/config': '配置文件',
@@ -305,6 +305,7 @@ export default {
'/log': '日志管理', '/log': '日志管理',
'/setting': '系统设置', '/setting': '系统设置',
'/error': '错误日志', '/error': '错误日志',
'/dependence': '依赖管理',
}, },
dependenceTypes: ['nodejs', 'python3', 'linux'], dependenceTypes: ['nodejs', 'python3', 'linux'],
}; };
+1 -1
View File
@@ -1 +1 @@
export const LOG_END_SYMBOL = '\n          '; export const LOG_END_SYMBOL = '     ';
-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') {
+54 -20
View File
@@ -1,4 +1,5 @@
import { LOG_END_SYMBOL } from './const'; import { LOG_END_SYMBOL } from './const';
import cron_parser from 'cron-parser';
export default function browserType() { export default function browserType() {
// 权重:系统 + 系统版本 > 平台 > 内核 + 载体 + 内核版本 + 载体版本 > 外壳 + 外壳版本 // 权重:系统 + 系统版本 > 平台 > 内核 + 载体 + 内核版本 + 载体版本 > 外壳 + 外壳版本
@@ -272,6 +273,29 @@ export function depthFirstSearch<
return c; return c;
} }
export function findNode<T extends Record<string, any> & { children?: T[] }>(
children: T[],
condition: (column: T) => boolean,
) {
const c = [...children];
let item;
function find(cls: T[] | undefined): T | undefined {
if (!cls) return;
for (let i = 0; i < cls?.length; i++) {
if (condition(cls[i])) {
item = cls[i];
} else if (cls[i].children) {
find(cls[i].children);
}
}
}
find(c);
return item;
}
export function logEnded(log: string): boolean { 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));
@@ -281,26 +305,36 @@ export function getCommandScript(
command: string, command: string,
): [string, string] | string | undefined { ): [string, string] | string | undefined {
const cmd = command.split(' ') as string[]; const cmd = command.split(' ') as string[];
if (cmd[0] === 'task') { if (cmd[1] === 'repo' || cmd[1] === 'raw') {
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]; return cmd[2];
} }
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];
}
export function parseCrontab(schedule: string): Date {
try {
const time = cron_parser.parseExpression(schedule);
if (time) {
return time.next().toDate();
}
} catch (error) {}
return new Date('1970');
} }
+2 -3
View File
@@ -1,6 +1,7 @@
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 * as monaco from 'monaco-editor';
export function init(version: string) { export function init(version: string) {
// sentry监控 init // sentry监控 init
@@ -26,9 +27,7 @@ export function init(version: string) {
// 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',
+9 -5
View File
@@ -1,6 +1,10 @@
version: 2.15.4 version: 2.15.8
changeLogLink: https://t.me/jiao_long/355 changeLogLink: https://t.me/jiao_long/364
changeLog: | changeLog: |
1. 脚本管理支持重命名文件/文件夹 1. 修复环境变量位置重置逻辑
2. 关于增加更新日志查看 2. 日志名称增加毫秒
3. 修复定时任务脚本识别和跳转 3. 修复添加文件类型订阅
4. 修复订阅生成 ssh 配置逻辑
5. 任务增加关联订阅
6. 修复订阅自动增加/删除任务参数默认值
7. 其他优化