Compare commits

...
26 Commits
Author SHA1 Message Date
whyour 98dfa4c425 更新版本 v2.15.9 2023-03-04 15:48:29 +08:00
whyour 5bf08b76de 修复系统更新源判断,回退 @monaco-editor/react 2023-03-03 23:45:38 +08:00
whyour 5c5b4f804e 更新 antd 2023-03-03 22:52:44 +08:00
whyour 6455245d11 更新 @umijs/max 2023-03-03 22:14:03 +08:00
pharaoh2012andGitHub 2ff3d9c601 根据标题跳过消息推送,环境变量:SKIP_PUSH_TITLE 用回车分隔 (#1833) 2023-03-03 21:59:44 +08:00
whyour d342fbf29f 修改登录日志表格样式 2023-03-03 11:58:06 +08:00
whyour f865018cd6 修改表格样式 2023-03-02 23:31:46 +08:00
whyour 3b9a4f0834 修复定时任务状态筛选 2023-03-02 23:16:18 +08:00
whyour 67bc305950 修复拉取仓库重置操作 2023-03-01 22:12:49 +08:00
whyour 97f128d59f 修改 ql extra 执行逻辑 2023-02-28 22:56:06 +08:00
whyour e25885c4d9 修改 nginx ipv6 配置 2023-02-27 23:30:00 +08:00
whyour c285026339 修改删除日志逻辑 2023-02-27 23:25:08 +08:00
whyour 6c34045a48 修改订阅名称必填 2023-02-26 23:36:32 +08:00
whyour e1e6261b4f 修复定时任务/视图/环境变量更新逻辑 2023-02-24 23:21:08 +08:00
whyour 697bcb5922 暂时去掉 vlist 2023-02-24 23:04:18 +08:00
whyour e05b0a2491 修复文件类型订阅重复添加任务 2023-02-18 14:38:13 +08:00
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
41 changed files with 2611 additions and 1710 deletions
+17
View File
@@ -152,6 +152,21 @@ export default (app: Router) => {
}
});
route.get(
'/detail',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronService = Container.get(CronService);
const data = await cronService.find(req.query as any);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.post(
'/',
celebrate({
@@ -160,6 +175,7 @@ export default (app: Router) => {
schedule: Joi.string().required(),
name: Joi.string().optional(),
labels: Joi.array().optional(),
sub_id: Joi.number().optional().allow(null),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
@@ -316,6 +332,7 @@ export default (app: Router) => {
command: Joi.string().required(),
schedule: Joi.string().required(),
name: Joi.string().optional().allow(null),
sub_id: Joi.number().optional().allow(null),
id: Joi.number().required(),
}),
}),
+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[];
last_running_time?: number;
last_execution_time?: number;
sub_id?: number;
constructor(options: Crontab) {
this.name = options.name;
@@ -37,6 +38,7 @@ export class Crontab {
this.labels = options.labels || [];
this.last_running_time = options.last_running_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,
last_running_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 initPosition = 4500000000000000;
export const stepPosition = 10000000;
export const stepPosition = 10000000000;
export const minPosition = 100;
interface EnvInstance extends Model<Env, Env>, Env {}
+3
View File
@@ -46,6 +46,9 @@ export default async () => {
'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 升级
const cronDbFile = path.join(config.rootPath, 'db/crontab.db');
+5 -8
View File
@@ -21,6 +21,7 @@ export default async () => {
name: '生成token',
command: tokenCommand,
};
await scheduleService.cancelIntervalTask(cron);
scheduleService.createIntervalTask(cron, {
days: 28,
});
@@ -28,12 +29,13 @@ export default async () => {
// 运行删除日志任务
const data = await systemService.getLogRemoveFrequency();
if (data && data.info && data.info.frequency) {
const cron = {
const rmlogCron = {
id: data.id,
name: '删除日志',
command: `ql rmlog ${data.info.frequency}`,
};
scheduleService.createIntervalTask(cron, {
await scheduleService.cancelIntervalTask(rmlogCron);
scheduleService.createIntervalTask(rmlogCron, {
days: data.info.frequency,
});
}
@@ -41,11 +43,6 @@ export default async () => {
// 运行所有订阅
const subs = await subscriptionService.list();
for (const sub of subs) {
await subscriptionService.handleTask(
sub,
!sub.is_disabled,
true,
!sub.is_disabled,
);
subscriptionService.handleTask(sub, !sub.is_disabled, !sub.is_disabled);
}
};
+19 -7
View File
@@ -18,7 +18,7 @@ import { TASK_PREFIX, QL_PREFIX } from '../config/const';
@Service()
export default class CronService {
constructor(@Inject('logger') private logger: winston.Logger) { }
constructor(@Inject('logger') private logger: winston.Logger) {}
private isSixCron(cron: Crontab) {
const { schedule } = cron;
@@ -41,8 +41,9 @@ export default class CronService {
}
public async update(payload: Crontab): Promise<Crontab> {
payload.saved = false;
const newDoc = await this.updateDb(payload);
const tab = new Crontab(payload);
tab.saved = false;
const newDoc = await this.updateDb(tab);
await this.set_crontab();
return newDoc;
}
@@ -244,8 +245,11 @@ export default class CronService {
const filterKeys: any = Object.keys(filterQuery);
for (const key of filterKeys) {
let q: any = {};
if (filterKeys[key]) {
q[key] = filterKeys[key];
if (!filterQuery[key]) continue;
if (key === 'status' && filterQuery[key].includes(2)) {
q = { [Op.or]: [{ [key]: filterQuery[key] }, { isDisabled: 1 }] };
} else {
q[key] = filterQuery[key];
}
query[Op.and].push(q);
}
@@ -260,6 +264,15 @@ export default class CronService {
}
}
public async find(params: { log_path: string }): Promise<Crontab | null> {
try {
const result = await CrontabModel.findOne({ where: { ...params } });
return result;
} catch (error) {
throw error;
}
}
public async crontabs(params?: {
searchValue: string;
page: string;
@@ -428,7 +441,7 @@ export default class CronService {
if (logFileExist) {
return getFileContentByName(`${absolutePath}`);
} else {
return '任务未运行或运行失败,请尝试手动运行';
return '任务未运行';
}
}
@@ -452,7 +465,6 @@ export default class CronService {
} else {
return [];
}
}
private make_command(tab: Crontab) {
+1 -1
View File
@@ -31,7 +31,7 @@ export default class CronViewService {
}
public async update(payload: CrontabView): Promise<CrontabView> {
const newDoc = await this.updateDb(payload);
const newDoc = await this.updateDb(new CrontabView(payload));
return newDoc;
}
+8 -4
View File
@@ -49,7 +49,7 @@ export default class EnvService {
}
public async update(payload: Env): Promise<Env> {
const newDoc = await this.updateDb(payload);
const newDoc = await this.updateDb(new Env(payload));
await this.set_envs();
return newDoc;
}
@@ -92,13 +92,17 @@ export default class EnvService {
position: this.getPrecisionPosition(targetPosition),
});
await this.checkPosition(targetPosition);
await this.checkPosition(targetPosition, envs[toIndex].position!);
return newDoc;
}
private async checkPosition(position: number) {
private async checkPosition(position: number, edge: number = 0) {
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();
let position = initPosition;
for (const env of envs) {
+9 -11
View File
@@ -5,8 +5,8 @@ import { ChildProcessWithoutNullStreams, exec, spawn } from 'child_process';
import {
ToadScheduler,
LongIntervalJob,
AsyncTask,
SimpleIntervalSchedule,
Task,
} from 'toad-scheduler';
import dayjs from 'dayjs';
@@ -63,8 +63,8 @@ export default class ScheduleService {
});
cp.stderr.on('data', async (data) => {
this.logger.error(
'执行任务 %s 失败,时间:%s, 错误信息:%j',
this.logger.info(
'[执行任务失败] %s,时间:%s, 错误信息:%j',
command,
new Date().toLocaleString(),
data.toString(),
@@ -74,7 +74,7 @@ export default class ScheduleService {
cp.on('error', async (err) => {
this.logger.error(
'创建任务 %s 失败,时间:%s, 错误信息:%j',
'[创建任务失败] %s,时间:%s, 错误信息:%j',
command,
new Date().toLocaleString(),
err,
@@ -84,7 +84,7 @@ export default class ScheduleService {
cp.on('exit', async (code, signal) => {
this.logger.info(
`任务 ${command} 进程id: ${cp.pid} 退出,退出码 ${code}`,
`[任务退出] ${command} 进程id: ${cp.pid},退出码 ${code}`,
);
});
@@ -154,12 +154,10 @@ export default class ScheduleService {
name,
command,
);
const task = new AsyncTask(
const task = new Task(
name,
async () => {
return new Promise(async (resolve, reject) => {
await this.runTask(command, callbacks);
});
() => {
this.runTask(command, callbacks);
},
(err) => {
this.logger.error(
@@ -180,7 +178,7 @@ export default class ScheduleService {
this.intervalSchedule.addIntervalJob(job);
if (runImmediately) {
await this.runTask(command, callbacks);
this.runTask(command, callbacks);
}
}
+26 -5
View File
@@ -1,8 +1,10 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import fs from 'fs';
import fs, { existsSync } from 'fs';
import os from 'os';
import path from 'path';
import { Subscription } from '../data/subscription';
import { formatUrl } from '../config/subscription';
@Service()
export default class SshKeyService {
@@ -32,7 +34,10 @@ export default class SshKeyService {
private removePrivateKeyFile(alias: string): void {
try {
const filePath = path.join(this.sshPath, alias);
if (existsSync(filePath)) {
fs.unlinkSync(`${this.sshPath}/${alias}`);
}
} catch (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`;
}
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[]) {
try {
for (const config of configs) {
fs.appendFileSync(this.sshConfigFilePath, config, {
fs.writeFileSync(this.sshConfigFilePath, configs.join('\n'), {
encoding: 'utf8',
});
}
} catch (error) {
this.logger.error('写入ssh配置文件失败', error);
}
@@ -94,4 +97,22 @@ export default class SshKeyService {
const config = this.generateSingleSshConfig(alias, host, proxy);
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);
}
}
+18 -62
View File
@@ -29,6 +29,7 @@ import SockService from './sock';
import SshKeyService from './sshKey';
import dayjs from 'dayjs';
import { LOG_END_SYMBOL } from '../config/const';
import { formatCommand, formatUrl } from '../config/subscription';
@Service()
export default class SubscriptionService {
@@ -67,75 +68,20 @@ export default class SubscriptionService {
['createdAt', 'DESC'],
],
});
return result as any;
return result;
} catch (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,
autoAddCron,
autoDelCron,
} = doc;
if (type === 'file') {
command += `raw "${_url}"`;
} else {
command += `repo "${_url}" "${whitelist || ''}" "${blacklist || ''}" "${
dependences || ''
}" "${branch || ''}" "${extensions || ''}" "${proxy || ''}" "${
Boolean(autoAddCron) || ''
}" "${Boolean(autoDelCron) || ''}"`;
}
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(
doc: Subscription,
needCreate = true,
needAddKey = true,
runImmediately = false,
) {
const { url, host } = this.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);
}
}
const { url } = formatUrl(doc);
doc.command = this.formatCommand(doc, url as string);
doc.command = formatCommand(doc, url as string);
if (doc.schedule_type === 'crontab') {
this.scheduleService.cancelCronTask(doc as any);
@@ -158,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> {
return new Promise((resolve, reject) => {
exec(
@@ -276,6 +227,7 @@ export default class SubscriptionService {
const tab = new Subscription(payload);
const doc = await this.insert(tab);
await this.handleTask(doc);
await this.setSshConfig();
return doc;
}
@@ -287,6 +239,7 @@ export default class SubscriptionService {
const tab = new Subscription(payload);
const newDoc = await this.updateDb(tab);
await this.handleTask(newDoc, !newDoc.is_disabled);
await this.setSshConfig();
return newDoc;
}
@@ -329,9 +282,10 @@ export default class SubscriptionService {
public async remove(ids: number[]) {
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
for (const doc of docs) {
await this.handleTask(doc, false, false);
await this.handleTask(doc, false);
}
await SubscriptionModel.destroy({ where: { id: ids } });
await this.setSshConfig();
}
public async getDb(query: any): Promise<Subscription> {
@@ -382,7 +336,7 @@ export default class SubscriptionService {
return;
}
const command = this.formatCommand(subscription);
const command = formatCommand(subscription);
await this.scheduleService.runTask(
command,
@@ -391,19 +345,21 @@ export default class SubscriptionService {
}
public async disabled(ids: number[]) {
await SubscriptionModel.update({ is_disabled: 1 }, { where: { id: ids } });
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
await this.setSshConfig();
for (const doc of docs) {
await this.handleTask(doc, false);
}
await SubscriptionModel.update({ is_disabled: 1 }, { where: { id: ids } });
}
public async enabled(ids: number[]) {
await SubscriptionModel.update({ is_disabled: 0 }, { where: { id: ids } });
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
await this.setSshConfig();
for (const doc of docs) {
await this.handleTask(doc);
}
await SubscriptionModel.update({ is_disabled: 0 }, { where: { id: ids } });
}
public async log(id: number) {
+1
View File
@@ -16,6 +16,7 @@ ARG QL_BRANCH=develop
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 \
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 \
SHELL=/bin/bash \
PS1="\u@\h:\w \$ " \
+2 -2
View File
@@ -42,13 +42,13 @@ echo -e "定时任务启动成功...\n"
if [[ $AutoStartBot == true ]]; then
echo -e "======================7. 启动bot========================\n"
nohup ql bot >$dir_log/bot.log 2>&1 &
nohup ql -l bot >$dir_log/bot.log 2>&1 &
echo -e "bot后台启动中...\n"
fi
if [[ $EnableExtraShell == true ]]; then
echo -e "======================8. 执行自定义脚本========================\n"
nohup ql extra >$dir_log/extra.log 2>&1 &
nohup ql -l extra >$dir_log/extra.log 2>&1 &
echo -e "自定义脚本后台执行中...\n"
fi
+1
View File
@@ -13,6 +13,7 @@ map $http_upgrade $connection_upgrade {
server {
listen 5700;
listen [::]:5700 ipv6only=on;
root /ql/static/dist;
ssl_session_timeout 5m;
+3 -4
View File
@@ -92,7 +92,7 @@
"devDependencies": {
"@ant-design/icons": "^4.7.0",
"@ant-design/pro-layout": "6.38.22",
"@monaco-editor/react": "4.4.6",
"@monaco-editor/react": "4.2.1",
"@react-hook/resize-observer": "^1.2.6",
"@sentry/react": "^7.12.1",
"@types/body-parser": "^1.19.2",
@@ -115,10 +115,10 @@
"@types/sockjs": "^0.3.33",
"@types/sockjs-client": "^1.5.1",
"@types/uuid": "^8.3.4",
"@umijs/max": "^4.0.42",
"@umijs/max": "^4.0.55",
"@umijs/ssr-darkreader": "^4.9.45",
"ansi-to-react": "^6.1.6",
"antd": "^4.24.7",
"antd": "^4.24.8",
"antd-img-crop": "^4.2.3",
"codemirror": "^5.65.2",
"compression-webpack-plugin": "9.2.0",
@@ -145,7 +145,6 @@
"typescript": "4.8.4",
"umi-request": "^1.4.0",
"vh-check": "^2.0.5",
"virtuallist-antd": "^0.7.6",
"webpack": "^5.70.0",
"yorkie": "^2.0.0"
}
+1727 -1380
View File
File diff suppressed because it is too large Load Diff
+10
View File
@@ -296,6 +296,16 @@ async function sendNotify(
) {
//提供6种通知
desp += author; //增加作者信息,防止被贩卖等
// 根据标题跳过一些消息推送,环境变量:SKIP_PUSH_TITLE 用回车分隔
let skipTitle = process.env.SKIP_PUSH_TITLE
if(skipTitle) {
if(skipTitle.split('\n').includes(text)) {
console.info(text + "在SKIP_PUSH_TITLE环境变量内,跳过推送!");
return
}
}
await Promise.all([
serverNotify(text, desp), //微信server酱
pushPlusNotify(text, desp), //pushplus(推送加)
+7
View File
@@ -647,6 +647,13 @@ def send(title: str, content: str) -> None:
print(f"{title} 推送内容为空!")
return
# 根据标题跳过一些消息推送,环境变量:SKIP_PUSH_TITLE 用回车分隔
skipTitle = os.getenv("SKIP_PUSH_TITLE")
if skipTitle:
if (title in re.split("\n", skipTitle)):
print(f"{title} 在SKIP_PUSH_TITLE环境变量内,跳过推送!")
return
hitokoto = push_config.get("HITOKOTO")
text = one() if hitokoto else ""
+42 -13
View File
@@ -19,10 +19,16 @@ add_cron_api() {
local schedule=$(echo "$1" | awk -F ":" '{print $1}')
local command=$(echo "$1" | awk -F ":" '{print $2}')
local name=$(echo "$1" | awk -F ":" '{print $3}')
local sub_id=$(echo "$1" | awk -F ":" '{print $4}')
else
local schedule=$1
local command=$2
local name=$3
local sub_id=$4
fi
if [[ ! $sub_id ]];then
sub_id="null"
fi
local api=$(
@@ -34,11 +40,11 @@ add_cron_api() {
-H "Origin: http://0.0.0.0:5700" \
-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" \
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\"}" \
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"sub_id\":$sub_id}" \
--compressed
)
code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message)
code=$(echo "$api" | jq -r .code)
message=$(echo "$api" | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "$name -> 添加成功"
else
@@ -73,8 +79,8 @@ update_cron_api() {
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"id\":\"$id\"}" \
--compressed
)
code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message)
code=$(echo "$api" | jq -r .code)
message=$(echo "$api" | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "$name -> 更新成功"
else
@@ -105,8 +111,8 @@ update_cron_command_api() {
--data-raw "{\"command\":\"$command\",\"id\":\"$id\"}" \
--compressed
)
code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message)
code=$(echo "$api" | jq -r .code)
message=$(echo "$api" | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "$command -> 更新成功"
else
@@ -130,8 +136,8 @@ del_cron_api() {
--data-raw "[$ids]" \
--compressed
)
code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message)
code=$(echo "$api" | jq -r .code)
message=$(echo "$api" | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "成功"
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}" \
--compressed
)
code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message)
code=$(echo "$api" | jq -r .code)
message=$(echo "$api" | jq -r .message)
if [[ $code != 200 ]]; then
echo -e "\n## 更新任务状态失败(${message})\n" >>$dir_log/$log_path
fi
@@ -184,8 +190,8 @@ notify_api() {
--data-raw "{\"title\":\"$title\",\"content\":\"$content\"}" \
--compressed
)
code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message)
code=$(echo "$api" | jq -r .code)
message=$(echo "$api" | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "通知发送成功"
else
@@ -193,4 +199,27 @@ notify_api() {
fi
}
find_cron_api() {
local params=$1
local currentTimeStamp=$(date +%s)
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons/detail?$params&t=$currentTimeStamp" \
-H "Accept: application/json" \
-H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \
-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" \
--compressed
)
data=$(echo "$api" | jq -r .data)
if [[ $data == 'null' ]]; then
echo -e ""
else
name=$(echo "$api" | jq -r .data.name)
echo -e "$name"
fi
}
get_token
-1
View File
@@ -96,7 +96,6 @@ start_public() {
main() {
echo -e "=====> 开始检测"
npm i -g pnpm
pnpm add -g pm2
patch_version
start_public
copy_dep
+1 -1
View File
@@ -173,7 +173,7 @@ run_concurrent() {
local envs=$(eval echo "\$${env_param}")
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
local relative_path="${file_param%/*}"
+8 -2
View File
@@ -6,7 +6,7 @@ dir_shell=$QL_DIR/shell
days=$1
## 删除运行js脚本的旧日志
## 删除运行脚本的旧日志
remove_js_log() {
local log_full_path_list=$(find $dir_log/ -name "*.log")
local diff_time
@@ -18,7 +18,13 @@ remove_js_log() {
else
diff_time=$(($(date +%s) - $(date +%s -d "$log_date")))
fi
[[ $diff_time -gt $((${days} * 86400)) ]] && rm -vf $log
if [[ $diff_time -gt $((${days} * 86400)) ]]; then
local log_path=$(echo "$log" | sed "s,${dir_log},,g")
local result=$(find_cron_api "log_path=$log_path")
if [[ $result ]]; then
rm -vf $log
fi
fi
fi
done
}
+6 -8
View File
@@ -326,12 +326,12 @@ git_pull_scripts() {
local branch="$2"
local proxy="$3"
cd $dir_work
echo -e "开始更新仓库:$dir_work\n"
echo -e "开始更新仓库:$dir_work"
set_proxy "$proxy"
git fetch --all
git pull 1>/dev/null
exit_status=$?
git pull &>/dev/null
unset_proxy
cd $dir_current
@@ -357,7 +357,7 @@ reset_romote_url() {
reset_branch() {
local branch="$1"
local part_cmd=""
local part_cmd="HEAD"
if [[ $branch ]]; then
part_cmd="origin/${branch}"
git checkout -B "$branch" &>/dev/null
@@ -413,9 +413,9 @@ format_log_time() {
local time="$2"
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
echo $(date -d "$time" "+%Y-%m-%d-%H-%M-%S")
echo $(date -d "$time" "+%Y-%m-%d-%H-%M-%S-%3N")
fi
}
@@ -450,9 +450,7 @@ patch_version() {
echo
fi
if ! type ts-node &>/dev/null; then
pnpm add -g ts-node typescript tslib
fi
pnpm add -g pm2 ts-node typescript tslib
git config --global pull.rebase false
+3 -2
View File
@@ -32,8 +32,8 @@ handle_log_path() {
if [[ ! -z $ID ]]; then
suffix="_${ID}"
fi
time=$(date "+$time_format")
log_time=$(format_log_time "$time_format" "$time")
time=$(date "+$mtime_format")
log_time=$(format_log_time "$mtime_format" "$time")
log_dir_tmp="${file_param##*/}"
if [[ $file_param =~ "/" ]]; then
if [[ $file_param == /* ]]; then
@@ -57,6 +57,7 @@ handle_log_path() {
format_params() {
time_format="%Y-%m-%d %H:%M:%S"
mtime_format="%Y-%m-%d %H:%M:%S.%3N"
timeoutCmd=""
if type timeout &>/dev/null; then
timeoutCmd="timeout --foreground -s 14 -k 10s $command_timeout_time "
+21 -16
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 "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
[[ -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"
if [[ $detail ]]; then
detail="${detail}${result}\n"
@@ -147,24 +147,33 @@ update_repo() {
## 更新所有 raw 文件
update_raw() {
echo -e "--------------------------------------------------------------\n"
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 suffix="${raw_url##*.}"
local raw_file_name="${uniq_path}.${suffix}"
echo -e "开始下载:${raw_url} \n\n保存路径:$dir_raw/${raw_file_name}\n"
set_proxy
wget -q --no-check-certificate -O "$dir_raw/${raw_file_name}.new" ${raw_url}
unset_proxy
wget -q --no-check-certificate -e "http_proxy=${proxy};https_proxy=${proxy}" -O "$dir_raw/${raw_file_name}.new" ${raw_url}
if [[ $? -eq 0 ]]; then
mv "$dir_raw/${raw_file_name}.new" "$dir_raw/${raw_file_name}"
echo -e "下载 ${raw_file_name} 成功...\n"
cd $dir_raw
local filename="raw_${raw_file_name}"
local cron_id=$(cat $list_crontab_user | grep -E "$cmd_task.* $filename" | perl -pe "s|.*ID=(.*) $cmd_task.* $filename\.*|\1|" | head -1 | head -1 | awk -F " " '{print $1}')
local cron_id=$(cat $list_crontab_user | grep -E "$cmd_task.* $filename" | perl -pe "s|.*ID=(.*) $cmd_task.* $filename\.*|\1|" | head -1 | awk -F " " '{print $1}')
cp -f $raw_file_name $dir_scripts/${filename}
if [[ -z $cron_id ]] && [[ ${autoAddCron} == true ]]; then
cron_line=$(
perl -ne "{
print if /.*([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*]( |,|\").*$raw_file_name/
@@ -180,8 +189,7 @@ update_raw() {
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $raw_file_name | awk -F ":" '{print $2}' | head -1 | xargs)
[[ -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="$default_cron"
if [[ -z $cron_id ]]; then
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"
notify_api "新增任务通知" "\n$result"
# update_cron_api "$cron_line:$cmd_task $filename:$cron_name:$cron_id"
@@ -195,14 +203,11 @@ update_raw() {
## 调用用户自定义的extra.sh
run_extra_shell() {
if [[ ${EnableExtraShell} == true ]]; then
if [[ -f $file_extra_shell ]]; then
echo -e "--------------------------------------------------------------\n"
. $file_extra_shell
else
echo -e "$file_extra_shell文件不存在,跳过执行...\n"
fi
fi
}
## 脚本用法
@@ -221,10 +226,10 @@ usage() {
## 更新qinglong
update_qinglong() {
local mirror="github"
local githubStatus=$(curl -s -m 2 -IL "https://github.com" | grep 200)
if [ "$githubStatus" == "" ]; then
mirror="gitee"
local mirror="gitee"
local githubStatus=$(curl -s -m 2 -IL "https://google.com" | grep 200)
if [[ ! -z $githubStatus ]]; then
mirror="github"
fi
echo -e "使用 ${mirror} 源更新...\n"
export isFirstStartServer=false
@@ -475,7 +480,7 @@ main() {
raw)
get_uniq_path "$p2"
if [[ -n $p2 ]]; then
update_raw "$p2"
update_raw "$p2" "$p3" "$p4"
else
eval echo -e "命令输入错误...\\\n" $cmd
eval usage $cmd
+404
View File
@@ -0,0 +1,404 @@
import React, {
useRef,
useEffect,
useContext,
createContext,
useReducer,
useState,
useMemo,
} from 'react';
import { throttle, isNumber, debounce } from 'lodash';
const initialState = {
// 行高度
rowHeight: 0,
// 当前的scrollTop
curScrollTop: 0,
// 总行数
totalLen: 0,
};
function reducer(state, action) {
const { curScrollTop, totalLen, ifScrollTopClear, scrollTop } = action;
let stateScrollTop = state.curScrollTop;
switch (action.type) {
// 改变trs 即 改变渲染的列表trs
case 'changeTrs':
return {
...state,
curScrollTop,
};
// 更改totalLen
case 'changeTotalLen':
if (totalLen === 0) {
stateScrollTop = 0;
}
return {
...state,
totalLen,
curScrollTop: stateScrollTop,
};
case 'reset':
return {
...state,
curScrollTop: ifScrollTopClear ? 0 : scrollTop ?? state.curScrollTop,
};
default:
throw new Error();
}
}
// ==============全局常量 ================== //
const DEFAULT_VID = 'vtable';
const vidMap = new Map();
let preData = 0;
// ===============context ============== //
const ScrollContext = createContext({
dispatch: undefined,
renderLen: 1,
start: 0,
offsetStart: 0,
// =============
rowHeight: initialState.rowHeight,
totalLen: 0,
vid: DEFAULT_VID,
});
// =============组件 =================== //
function VCell(props: any): JSX.Element {
const { children, ...restProps } = props;
return (
<td {...restProps}>
<div>{children[1]}</div>
</td>
);
}
function VRow(props: any, ref: any): JSX.Element {
const { rowHeight } = useContext(ScrollContext);
const { children, style, ...restProps } = props;
const trRef = useRef<HTMLTableRowElement>(null);
return (
<tr
{...restProps}
ref={Object.prototype.hasOwnProperty.call(ref, 'current') ? ref : trRef}
style={{
...style,
height: rowHeight || 'auto',
boxSizing: 'border-box',
}}
>
{children}
</tr>
);
}
function VWrapper(props: any): JSX.Element {
const { children, ...restProps } = props;
const { renderLen, start, dispatch, vid, totalLen } =
useContext(ScrollContext);
const contents = useMemo(() => {
return children[1];
}, [children]);
const contentsLen = useMemo(() => {
return contents?.length ?? 0;
}, [contents]);
useEffect(() => {
if (totalLen !== contentsLen) {
dispatch({
type: 'changeTotalLen',
totalLen: contentsLen ?? 0,
});
}
}, [contentsLen, dispatch, vid, totalLen]);
let tempNode = null;
if (Array.isArray(contents) && contents.length) {
tempNode = [
children[0],
contents.slice(start, start + (renderLen ?? 1)).map((item) => {
if (Array.isArray(item)) {
// 兼容antd v4.3.5 --- rc-table 7.8.1及以下
return item[0];
}
// 处理antd ^v4.4.0 --- rc-table ^7.8.2
return item;
}),
];
} else {
tempNode = children;
}
return <tbody {...restProps}>{tempNode}</tbody>;
}
function VTable(props: any, otherParams): JSX.Element {
const { style, children, ...rest } = props;
const { width, ...rest_style } = style;
const { vid, scrollY, resetScrollTopWhenDataChange, rowHeight, scrollTop } =
otherParams ?? {};
const [state, dispatch] = useReducer(reducer, {
...initialState,
curScrollTop: scrollTop,
rowHeight,
});
const wrap_tableRef = useRef<HTMLDivElement>(null);
const tableRef = useRef<HTMLTableElement>(null);
const ifChangeRef = useRef(false);
// 数据的总条数
const [totalLen, setTotalLen] = useState<number>(
children[1]?.props?.data?.length ?? 0,
);
useEffect(() => {
setTotalLen(state.totalLen);
}, [state.totalLen]);
// 组件卸载的清除操作
useEffect(() => {
return () => {
vidMap.delete(vid);
};
}, [vid]);
// 数据变更
useEffect(() => {
ifChangeRef.current = true;
// console.log('数据变更')
if (isNumber(children[1]?.props?.data?.length)) {
dispatch({
type: 'changeTotalLen',
totalLen: children[1]?.props?.data?.length ?? 0,
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [children[1].props.data]);
// table总高度
const tableHeight = useMemo<string | number>(() => {
let temp: string | number = 'auto';
if (rowHeight && totalLen) {
temp = rowHeight * totalLen;
}
return temp;
}, [totalLen]);
// table的scrollY值
const [tableScrollY, setTableScrollY] = useState(0);
// tableScrollY 随scrollY / tableHeight 进行变更
useEffect(() => {
let temp = 0;
if (typeof scrollY === 'string') {
temp =
(wrap_tableRef.current?.parentNode as HTMLElement)?.offsetHeight ?? 0;
} else {
temp = scrollY;
}
// if (isNumber(tableHeight) && tableHeight < temp) {
// temp = tableHeight;
// }
// 处理tableScrollY <= 0的情况
if (temp <= 0) {
temp = 0;
}
setTableScrollY(temp);
}, [scrollY, tableHeight]);
// 渲染的条数
const renderLen = useMemo<number>(() => {
let temp = 1;
if (rowHeight && totalLen && tableScrollY) {
if (tableScrollY <= 0) {
temp = 0;
} else {
const tempRenderLen = ((tableScrollY / rowHeight) | 0) + 10;
// console.log('tempRenderLen', tempRenderLen)
// temp = tempRenderLen > totalLen ? totalLen : tempRenderLen;
temp = tempRenderLen;
}
}
return temp;
}, [totalLen, tableScrollY]);
// 渲染中的第一条
let start = rowHeight ? (state.curScrollTop / rowHeight) | 0 : 0;
start = start < 5 ? 0 : start - 5 + 1;
// 偏移量
let offsetStart = state.curScrollTop % (rowHeight * 5);
if (start > 0) {
offsetStart = offsetStart % rowHeight;
}
// console.log(offsetStart)
// offsetStart= offsetStart%rowHeight
// 用来优化向上滚动出现的空白
if (state.curScrollTop && state.curScrollTop >= rowHeight * 5) {
// start -= 1
// if (offsetStart >= rowHeight) {
// offsetStart +=
// } else {
offsetStart += rowHeight * 4;
// }
} else {
start = 0;
}
// console.log(state.curScrollTop, start, offsetStart)
// 数据变更 操作scrollTop
useEffect(() => {
const scrollNode = wrap_tableRef.current?.parentNode as HTMLElement;
if (ifChangeRef?.current) {
// console.log(scrollNode)
ifChangeRef.current = false;
if (resetScrollTopWhenDataChange) {
// 重置scrollTop
if (scrollNode) {
scrollNode.scrollTop = 0;
}
dispatch({ type: 'reset', ifScrollTopClear: true });
} else {
// console.log(preData)
// scrollNode.scrollTop = preData+53
// 不重置scrollTop 不清空curScrollTop
dispatch({ type: 'reset', ifScrollTopClear: false });
}
}
if (vidMap.has(vid)) {
vidMap.set(vid, {
...vidMap.get(vid),
scrollNode,
});
}
}, [totalLen, resetScrollTopWhenDataChange, vid, children]);
useEffect(() => {
const throttleScroll = throttle((e) => {
const scrollTop: number = e?.target?.scrollTop ?? 0;
// const scrollHeight: number = e?.target?.scrollHeight ?? 0
// const clientHeight: number = e?.target?.clientHeight ?? 0
if (scrollTop) {
preData = scrollTop;
}
// 到底了 没有滚动条就不会触发reachEnd. 建议设置scrolly高度少点或者数据量多点.
// 若renderLen大于totalLen, 置空curScrollTop. => table paddingTop会置空.
dispatch({
type: 'changeTrs',
curScrollTop: renderLen <= totalLen ? scrollTop : 0,
});
}, 60);
const ref = wrap_tableRef?.current?.parentNode as HTMLElement;
if (ref) {
ref.addEventListener('scroll', throttleScroll, { passive: true });
}
return () => {
ref.removeEventListener('scroll', throttleScroll);
};
}, [renderLen, totalLen]);
return (
<div
className="virtuallist"
ref={wrap_tableRef}
style={{
width: '100%',
position: 'relative',
height: tableHeight,
boxSizing: 'border-box',
paddingTop: state.curScrollTop,
}}
>
<ScrollContext.Provider
value={{
dispatch,
start,
offsetStart,
renderLen,
totalLen,
vid,
rowHeight,
}}
>
<table
{...rest}
ref={tableRef}
style={{
...rest_style,
width,
position: 'relative',
transform: `translateY(-${offsetStart}px)`,
}}
>
{children}
</table>
</ScrollContext.Provider>
</div>
);
}
// ================导出===================
export function VList(props: {
height: number;
// 唯一标识
vid?: string;
rowHeight: number | string;
reset?: boolean;
scrollTop?: number | string;
}): any {
const { vid = DEFAULT_VID, rowHeight, height, reset, scrollTop } = props;
const resetScrollTopWhenDataChange = reset ?? true;
if (!vidMap.has(vid)) {
vidMap.set(vid, { _id: vid });
}
return {
table: (p) =>
VTable(p, {
vid,
scrollY: height,
rowHeight,
resetScrollTopWhenDataChange,
scrollTop,
}),
body: {
wrapper: VWrapper,
row: VRow,
cell: VCell,
},
};
}
+13 -10
View File
@@ -1,16 +1,19 @@
import { MutableRefObject, useLayoutEffect, useState } from 'react';
import useResizeObserver from '@react-hook/resize-observer'
import useResizeObserver from '@react-hook/resize-observer';
import { getTableScroll } from '@/utils';
export default <T extends HTMLElement>(target: MutableRefObject<T>, extraHeight?: number) => {
const [height, setHeight] = useState<number>()
export default <T extends HTMLElement>(
target: MutableRefObject<T>,
extraHeight?: number,
) => {
const [height, setHeight] = useState<number>(0);
useResizeObserver(target, (entry) => {
let _targe = entry.target as any
if (!_targe.classList.contains('ant-table-wrapper')) {
_targe = entry.target.querySelector('.ant-table-wrapper')
let _target = entry.target as any;
if (!_target.classList.contains('ant-table-wrapper')) {
_target = entry.target.querySelector('.ant-table-wrapper');
}
setHeight(getTableScroll({ extraHeight, target: _targe as HTMLElement }))
})
return height
}
setHeight(getTableScroll({ extraHeight, target: _target as HTMLElement }));
});
return height;
};
+34 -1
View File
@@ -9,6 +9,11 @@
url('../assets/fonts/SourceCodePro-Regular.ttf') format('truetype');
}
#root {
height: 100vh;
height: calc(100vh - var(--vh-offset, 0px));
}
.ant-modal-body {
max-height: calc(80vh - 110px);
max-height: calc(80vh - var(--vh-offset, 110px));
@@ -350,6 +355,34 @@ pre {
padding: 0 !important;
}
.virtuallist .ant-table-tbody > tr > td > div {
.virtuallist {
.ant-table-tbody > tr > td > div {
white-space: unset !important;
}
}
.virtuallist .ant-table-tbody > tr > td > div {
box-sizing: border-box;
white-space: nowrap;
vertical-align: middle;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
.virtuallist .ant-table-tbody > tr > td.ant-table-row-expand-icon-cell > div {
overflow: inherit;
}
.ant-table-bordered .virtuallist > table > .ant-table-tbody > tr > td {
border-right: 1px solid #f0f0f0;
}
.ant-table-column-title {
flex: unset;
}
.ant-table-column-sorters,
.ant-table-filter-column {
justify-content: unset;
}
+26 -15
View File
@@ -53,7 +53,7 @@ import { SharedContext } from '@/layouts';
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
import { getCommandScript, parseCrontab } from '@/utils';
import { ColumnProps } from 'antd/lib/table';
import { VList } from 'virtuallist-antd';
import { VList } from '../../components/vlist';
const { Text, Paragraph } = Typography;
const { Search } = Input;
@@ -107,7 +107,6 @@ const Crontab = () => {
dataIndex: 'name',
key: 'name',
width: 150,
align: 'center' as const,
render: (text: string, record: any) => (
<>
<a
@@ -163,14 +162,12 @@ const Crontab = () => {
dataIndex: 'command',
key: 'command',
width: 300,
align: 'center' as const,
render: (text, record) => {
return (
<Paragraph
style={{
wordBreak: 'break-all',
marginBottom: 0,
textAlign: 'left',
}}
ellipsis={{ tooltip: text, rows: 2 }}
>
@@ -193,14 +190,12 @@ const Crontab = () => {
dataIndex: 'schedule',
key: 'schedule',
width: 110,
align: 'center' as const,
sorter: {
compare: (a, b) => a.schedule.localeCompare(b.schedule),
},
},
{
title: '最后运行时间',
align: 'center' as const,
dataIndex: 'last_execution_time',
key: 'last_execution_time',
width: 150,
@@ -230,7 +225,6 @@ const Crontab = () => {
},
{
title: '最后运行时长',
align: 'center' as const,
width: 120,
dataIndex: 'last_running_time',
key: 'last_running_time',
@@ -247,7 +241,6 @@ const Crontab = () => {
},
{
title: '下次运行时间',
align: 'center' as const,
width: 150,
sorter: {
compare: (a: any, b: any) => {
@@ -267,7 +260,6 @@ const Crontab = () => {
title: '状态',
key: 'status',
dataIndex: 'status',
align: 'center' as const,
width: 88,
filters: [
{
@@ -329,7 +321,6 @@ const Crontab = () => {
{
title: '操作',
key: 'action',
align: 'center' as const,
width: 100,
render: (text, record, index) => {
const isPc = !isPhone;
@@ -339,6 +330,7 @@ const Crontab = () => {
<Tooltip title={isPc ? '运行' : ''}>
<a
onClick={(e) => {
setReset(false);
e.stopPropagation();
runCron(record, index);
}}
@@ -351,6 +343,7 @@ const Crontab = () => {
<Tooltip title={isPc ? '停止' : ''}>
<a
onClick={(e) => {
setReset(false);
e.stopPropagation();
stopCron(record, index);
}}
@@ -362,6 +355,7 @@ const Crontab = () => {
<Tooltip title={isPc ? '日志' : ''}>
<a
onClick={(e) => {
setReset(false);
e.stopPropagation();
setLogCron({ ...record, timestamp: Date.now() });
}}
@@ -405,6 +399,10 @@ const Crontab = () => {
const [moreMenuActive, setMoreMenuActive] = useState(false);
const tableRef = useRef<any>();
const tableScrollHeight = useTableScrollHeight(tableRef);
const resetRef = useRef<boolean>(true);
const setReset = (v) => {
resetRef.current = v;
};
const goToScriptManager = (record: any) => {
const result = getCommandScript(record.command);
@@ -424,9 +422,9 @@ const Crontab = () => {
}crons?searchValue=${searchText}&page=${page}&size=${size}&filters=${JSON.stringify(
filters,
)}`;
if (sorter && sorter.field) {
if (sorter && sorter.column && sorter.order) {
url += `&sorter=${JSON.stringify({
field: sorter.field,
field: sorter.column.key,
type: sorter.order === 'ascend' ? 'ASC' : 'DESC',
})}`;
}
@@ -688,6 +686,7 @@ const Crontab = () => {
items: getMenuItems(record),
onClick: ({ key, domEvent }) => {
domEvent.stopPropagation();
setReset(false);
action(key, record, index);
},
}}
@@ -723,6 +722,7 @@ const Crontab = () => {
};
const onSearch = (value: string) => {
setReset(true);
setSearchText(value.trim());
};
@@ -750,6 +750,10 @@ const Crontab = () => {
setSelectedRowIds(selectedIds);
};
useEffect(() => {
setReset(false);
}, [selectedRowIds]);
const rowSelection = {
selectedRowKeys: selectedRowIds,
onChange: onSelectChange,
@@ -777,6 +781,7 @@ const Crontab = () => {
};
const operateCrons = (operationStatus: number) => {
setReset(false);
Modal.confirm({
title: `确认${OperationName[operationStatus]}`,
content: <>{OperationName[operationStatus]}</>,
@@ -803,6 +808,7 @@ const Crontab = () => {
sorter: SorterResult<any> | SorterResult<any>[],
) => {
const { current, pageSize } = pagination;
setReset(true);
setPageConf({
page: current as number,
size: pageSize as number,
@@ -917,16 +923,21 @@ const Crontab = () => {
const tabClick = (key: string) => {
const view = enabledCronViews.find((x) => x.id == key);
setSelectedRowIds([]);
setReset(true);
setPageConf({ ...pageConf, page: 1 });
setViewConf(view ? view : null);
};
const vComponents = useMemo(() => {
return VList({
height: tableScrollHeight!,
resetTopWhenDataChange: false,
height: tableScrollHeight,
reset: resetRef.current,
rowHeight: 69,
scrollTop: resetRef.current
? 0
: tableRef.current?.querySelector('.ant-table-body')?.scrollTop,
});
}, [tableScrollHeight]);
}, [tableScrollHeight, resetRef.current]);
return (
<PageContainer
+1 -1
View File
@@ -54,7 +54,7 @@ const CronLogModal = ({
log &&
!logEnded(log) &&
!log.includes('重启面板') &&
!log.includes('任务未运行或运行失败,请尝试手动运行'),
!log.includes('任务未运行'),
);
setExecuting(hasNext);
autoScroll();
-7
View File
@@ -80,23 +80,17 @@ const ViewManageModal = ({
title: '名称',
dataIndex: 'name',
key: 'name',
align: 'center' as const,
render: (text) => (
<div style={{ textAlign: 'left', paddingLeft: 30 }}>{text}</div>
),
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
align: 'center' as const,
render: (v) => (v === 1 ? '系统' : '个人'),
},
{
title: '显示',
key: 'isDisabled',
dataIndex: 'isDisabled',
align: 'center' as const,
width: 100,
render: (text: string, record: any, index: number) => {
return (
@@ -111,7 +105,6 @@ const ViewManageModal = ({
title: '操作',
key: 'action',
width: 100,
align: 'center' as const,
render: (text: string, record: any, index: number) => {
return record.type !== 1 ? (
<Space size="middle">
+1 -8
View File
@@ -32,7 +32,6 @@ import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts';
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
const { Text } = Typography;
const { Search } = Input;
@@ -57,7 +56,6 @@ const Dependence = () => {
const columns: any = [
{
title: '序号',
align: 'center' as const,
width: 50,
render: (text: string, record: any, index: number) => {
return <span style={{ cursor: 'text' }}>{index + 1} </span>;
@@ -67,13 +65,11 @@ const Dependence = () => {
title: '名称',
dataIndex: 'name',
key: 'name',
align: 'center' as const,
},
{
title: '状态',
key: 'status',
dataIndex: 'status',
align: 'center' as const,
render: (text: string, record: any, index: number) => {
return (
<Space size="middle" style={{ cursor: 'text' }}>
@@ -91,13 +87,11 @@ const Dependence = () => {
title: '备注',
dataIndex: 'remark',
key: 'remark',
align: 'center' as const,
},
{
title: '创建时间',
key: 'timestamp',
dataIndex: 'timestamp',
align: 'center' as const,
render: (text: string, record: any) => {
const language = navigator.language || navigator.languages[0];
const time = record.createdAt || record.timestamp;
@@ -120,7 +114,6 @@ const Dependence = () => {
{
title: '操作',
key: 'action',
align: 'center' as const,
render: (text: string, record: any, index: number) => {
const isPc = !isPhone;
return (
@@ -169,7 +162,7 @@ const Dependence = () => {
const [isLogModalVisible, setIsLogModalVisible] = useState(false);
const [type, setType] = useState('nodejs');
const tableRef = useRef<any>();
const tableScrollHeight = useTableScrollHeight(tableRef, 59)
const tableScrollHeight = useTableScrollHeight(tableRef, 59);
const getDependencies = () => {
setLoading(true);
+24 -11
View File
@@ -39,7 +39,7 @@ import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts';
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
import Copy from '../../components/copy';
import { VList } from 'virtuallist-antd';
import { VList } from '../../components/vlist';
const { Text } = Typography;
const { Search } = Input;
@@ -71,7 +71,6 @@ const Env = () => {
const columns: any = [
{
title: '序号',
align: 'center' as const,
width: 60,
render: (text: string, record: any, index: number) => {
return <span style={{ cursor: 'text' }}>{index + 1} </span>;
@@ -81,14 +80,12 @@ const Env = () => {
title: '名称',
dataIndex: 'name',
key: 'name',
align: 'center' as const,
sorter: (a: any, b: any) => a.name.localeCompare(b.name),
},
{
title: '值',
dataIndex: 'value',
key: 'value',
align: 'center' as const,
width: '35%',
render: (text: string, record: any) => {
return (
@@ -105,7 +102,6 @@ const Env = () => {
title: '备注',
dataIndex: 'remarks',
key: 'remarks',
align: 'center' as const,
render: (text: string, record: any) => {
return (
<Tooltip title={text} placement="topLeft">
@@ -118,7 +114,6 @@ const Env = () => {
title: '更新时间',
dataIndex: 'timestamp',
key: 'timestamp',
align: 'center' as const,
width: 165,
ellipsis: {
showTitle: false,
@@ -153,7 +148,6 @@ const Env = () => {
title: '状态',
key: 'status',
dataIndex: 'status',
align: 'center' as const,
width: 70,
filters: [
{
@@ -180,7 +174,6 @@ const Env = () => {
title: '操作',
key: 'action',
width: 120,
align: 'center' as const,
render: (text: string, record: any, index: number) => {
const isPc = !isPhone;
return (
@@ -223,6 +216,10 @@ const Env = () => {
const [importLoading, setImportLoading] = useState(false);
const tableRef = useRef<any>();
const tableScrollHeight = useTableScrollHeight(tableRef, 59);
const resetRef = useRef<boolean>(true);
const setReset = (v) => {
resetRef.current = v;
};
const getEnvs = () => {
setLoading(true);
@@ -237,6 +234,7 @@ const Env = () => {
};
const enabledOrDisabledEnv = (record: any, index: number) => {
setReset(false);
Modal.confirm({
title: `确认${record.status === Status. ? '启用' : '禁用'}`,
content: (
@@ -282,16 +280,19 @@ const Env = () => {
};
const addEnv = () => {
setReset(false);
setEditedEnv(null as any);
setIsModalVisible(true);
};
const editEnv = (record: any, index: number) => {
setReset(false);
setEditedEnv(record);
setIsModalVisible(true);
};
const deleteEnv = (record: any, index: number) => {
setReset(false);
Modal.confirm({
title: '确认删除',
content: (
@@ -334,9 +335,13 @@ const Env = () => {
const vComponents = useMemo(() => {
return VList({
height: tableScrollHeight!,
resetTopWhenDataChange: false,
reset: resetRef.current,
rowHeight: 48,
scrollTop: resetRef.current
? 0
: tableRef.current?.querySelector('.ant-table-body')?.scrollTop,
});
}, [tableScrollHeight]);
}, [tableScrollHeight, resetRef.current]);
const DragableBodyRow = (props: any) => {
const { index, moveRow, className, style, ...restProps } = props;
@@ -420,12 +425,17 @@ const Env = () => {
setSelectedRowIds(selectedIds);
};
useEffect(() => {
setReset(false);
}, [selectedRowIds]);
const rowSelection = {
selectedRowKeys: selectedRowIds,
onChange: onSelectChange,
};
const delEnvs = () => {
setReset(false);
Modal.confirm({
title: '确认删除',
content: <></>,
@@ -447,6 +457,7 @@ const Env = () => {
};
const operateEnvs = (operationStatus: number) => {
setReset(false);
Modal.confirm({
title: `确认${OperationName[operationStatus]}`,
content: <>{OperationName[operationStatus]}</>,
@@ -479,6 +490,7 @@ const Env = () => {
};
const onSearch = (value: string) => {
setReset(true);
setSearchText(value.trim());
};
@@ -542,6 +554,7 @@ const Env = () => {
style: headerStyle,
}}
>
<div ref={tableRef}>
{selectedRowIds.length > 0 && (
<div style={{ marginBottom: 16 }}>
<Button
@@ -587,7 +600,6 @@ const Env = () => {
)}
<DndProvider backend={HTML5Backend}>
<Table
ref={tableRef}
columns={columns}
rowSelection={rowSelection}
pagination={false}
@@ -605,6 +617,7 @@ const Env = () => {
}}
/>
</DndProvider>
</div>
<EnvModal
visible={isModalVisible}
handleCancel={handleCancel}
+2 -11
View File
@@ -49,13 +49,11 @@ const Setting = () => {
title: '名称',
dataIndex: 'name',
key: 'name',
align: 'center' as const,
},
{
title: 'Client ID',
dataIndex: 'client_id',
key: 'client_id',
align: 'center' as const,
render: (text: string, record: any) => {
return <Text copyable>{record.client_id}</Text>;
},
@@ -64,7 +62,6 @@ const Setting = () => {
title: 'Client Secret',
dataIndex: 'client_secret',
key: 'client_secret',
align: 'center' as const,
render: (text: string, record: any) => {
return <Text copyable={{ text: record.client_secret }}>*******</Text>;
},
@@ -73,22 +70,16 @@ const Setting = () => {
title: '权限',
dataIndex: 'scopes',
key: 'scopes',
align: 'center' as const,
width: '40%',
render: (text: string, record: any) => {
return (
<div style={{ textAlign: 'left' }}>
{record.scopes.map((scope: any) => {
return record.scopes.map((scope: any) => {
return <Tag key={scope}>{(config.scopesMap as any)[scope]}</Tag>;
})}
</div>
);
});
},
},
{
title: '操作',
key: 'action',
align: 'center' as const,
render: (text: string, record: any, index: number) => {
const isPc = !isPhone;
return (
+2 -6
View File
@@ -18,7 +18,6 @@ enum LoginStatusColor {
const columns = [
{
title: '序号',
align: 'center' as const,
width: 50,
render: (text: string, record: any, index: number) => {
return index + 1;
@@ -28,7 +27,6 @@ const columns = [
title: '登录时间',
dataIndex: 'timestamp',
key: 'timestamp',
align: 'center' as const,
render: (text: string, record: any) => {
return new Date(record.timestamp).toLocaleString();
},
@@ -37,25 +35,23 @@ const columns = [
title: '登录地址',
dataIndex: 'address',
key: 'address',
align: 'center' as const,
},
{
title: '登录IP',
dataIndex: 'ip',
key: 'ip',
align: 'center' as const,
},
{
title: '登录设备',
dataIndex: 'platform',
key: 'platform',
align: 'center' as const,
width: 80,
},
{
title: '登录状态',
dataIndex: 'status',
key: 'status',
align: 'center' as const,
width: 80,
render: (text: string, record: any) => {
return (
<Tag color={LoginStatusColor[record.status]} style={{ marginRight: 0 }}>
-8
View File
@@ -68,7 +68,6 @@ const Subscription = () => {
dataIndex: 'name',
key: 'name',
width: 150,
align: 'center' as const,
sorter: {
compare: (a: any, b: any) => a.name.localeCompare(b.name),
multiple: 2,
@@ -78,7 +77,6 @@ const Subscription = () => {
title: '链接',
dataIndex: 'url',
key: 'url',
align: 'center' as const,
sorter: {
compare: (a: any, b: any) => a.name.localeCompare(b.name),
multiple: 2,
@@ -89,7 +87,6 @@ const Subscription = () => {
style={{
wordBreak: 'break-all',
marginBottom: 0,
textAlign: 'left',
}}
ellipsis={{ tooltip: text, rows: 2 }}
>
@@ -103,7 +100,6 @@ const Subscription = () => {
dataIndex: 'type',
key: 'type',
width: 130,
align: 'center' as const,
render: (text: string, record: any) => {
return (SubscriptionType as any)[record.type];
},
@@ -113,7 +109,6 @@ const Subscription = () => {
dataIndex: 'branch',
key: 'branch',
width: 130,
align: 'center' as const,
render: (text: string, record: any) => {
return record.branch || '-';
},
@@ -121,7 +116,6 @@ const Subscription = () => {
{
title: '定时规则',
width: 180,
align: 'center' as const,
render: (text: string, record: any) => {
if (record.schedule_type === 'interval') {
const { type, value } = record.interval_schedule;
@@ -134,7 +128,6 @@ const Subscription = () => {
title: '状态',
key: 'status',
dataIndex: 'status',
align: 'center' as const,
width: 110,
filters: [
{
@@ -189,7 +182,6 @@ const Subscription = () => {
{
title: '操作',
key: 'action',
align: 'center' as const,
width: 130,
render: (text: string, record: any, index: number) => {
const isPc = !isPhone;
+20 -7
View File
@@ -12,10 +12,11 @@ import {
import { request } from '@/utils/http';
import config from '@/utils/config';
import cron_parser from 'cron-parser';
import isNil from 'lodash/isNil';
const { Option } = Select;
const repoUrlRegx = /[^\/\:]+\/[^\/]+(?=\.git)/;
const fileUrlRegx = /[^\/\:]+\/[^\/]+$/;
const repoUrlRegx = /([^\/\:]+\/[^\/]+)(?=\.git)/;
const fileUrlRegx = /([^\/\:]+\/[^\/\.]+)\.[a-z]+$/;
const SubscriptionModal = ({
subscription,
@@ -99,7 +100,7 @@ const SubscriptionModal = ({
let _alias = '';
const _regx = _type === 'file' ? fileUrlRegx : repoUrlRegx;
if (_regx.test(_url)) {
_alias = _url.match(_regx)![0].replaceAll('/', '_').replaceAll('.', '_');
_alias = _url.match(_regx)![1].replaceAll('/', '_').replaceAll('.', '_');
}
if (_branch) {
_alias = _alias + '_' + _branch;
@@ -138,6 +139,7 @@ const SubscriptionModal = ({
setIntervalNumber(value.value);
}
}, [value]);
return (
<Input.Group compact>
<InputNumber
@@ -230,7 +232,7 @@ const SubscriptionModal = ({
dependences,
branch,
extensions,
alias: formatAlias(url, branch),
alias: formatAlias(url, branch, _type),
});
setType(_type);
}
@@ -241,8 +243,17 @@ const SubscriptionModal = ({
if (text.startsWith('ql ')) {
e.preventDefault();
}
onPaste(e);
}, []);
const formatParams = (sub) => {
return {
...sub,
autoAddCron: isNil(sub?.autoAddCron) ? true : Boolean(sub?.autoAddCron),
autoDelCron: isNil(sub?.autoDelCron) ? true : Boolean(sub?.autoDelCron),
};
};
useEffect(() => {
if (visible) {
window.addEventListener('paste', onPaste);
@@ -252,7 +263,9 @@ const SubscriptionModal = ({
}, [visible]);
useEffect(() => {
form.setFieldsValue(subscription || {});
form.setFieldsValue(
{ ...subscription, ...formatParams(subscription) } || {},
);
setType((subscription && subscription.type) || 'public-repo');
setScheduleType((subscription && subscription.schedule_type) || 'crontab');
setPullType((subscription && subscription.pull_type) || 'ssh-key');
@@ -282,9 +295,9 @@ const SubscriptionModal = ({
confirmLoading={loading}
>
<Form form={form} name="form_in_modal" layout="vertical">
<Form.Item name="name" label="名称">
<Form.Item name="name" label="名称" rules={[{ required: true }]}>
<Input
placeholder="支持拷贝ql repo/raw命令,粘贴导入"
placeholder="支持拷贝 ql repo/raw 命令,粘贴导入"
onPaste={onNamePaste}
/>
</Form.Item>
+1 -1
View File
@@ -183,7 +183,7 @@ export function getTableScroll({
extraHeight,
target,
}: { extraHeight?: number; target?: HTMLElement } = {}) {
if (typeof extraHeight == 'undefined') {
if (typeof extraHeight === 'undefined') {
// 47 + 40 + 12
extraHeight = 99;
}
+3 -2
View File
@@ -1,7 +1,6 @@
import * as Sentry from '@sentry/react';
import { Integrations } from '@sentry/tracing';
import { loader } from '@monaco-editor/react';
import * as monaco from 'monaco-editor';
export function init(version: string) {
// sentry监控 init
@@ -27,7 +26,9 @@ export function init(version: string) {
// monaco 编辑器配置cdn和locale
loader.config({
monaco,
paths: {
vs: 'https://cdn.staticfile.org/monaco-editor/0.33.0/min/vs',
},
'vs/nls': {
availableLanguages: {
'*': 'zh-cn',
+11 -9
View File
@@ -1,10 +1,12 @@
version: 2.15.7
changeLogLink: https://t.me/jiao_long/363
version: 2.15.9
changeLogLink: https://t.me/jiao_long/364
changeLog: |
1. 订阅支持自动添加和删除任务设置
2. 增加 ppc64le/s390x/386 镜像
3. 修改 curl 等命令参数处理逻辑
4. 修复移动端新建脚本
5. 修改任务日志自动滚动逻辑
6. 修复查看任务日志,列表位置重置
7. 其他优化
1. 通知脚本增加环境变量 SKIP_PUSH_TITLE,设置需要跳过推送的标题,多个换行符分割分隔,感谢 https://github.com/pharaoh2012
2. nginx 增加 ipv6 配置
3. 对比工具增加通知文件对比
4. 修复文件类型订阅重复添加任务
5. 修改自动删除日志逻辑
6. 修复定时任务状态筛选,排序
7. 修改表格样式
8. 修复切换导航,编辑器页面可能崩溃
9. 其他 bug 修复