Compare commits

..
66 Commits
Author SHA1 Message Date
whyour 79b342d1d2 更新版本 v2.15.11 2023-03-31 22:38:32 +08:00
whyour 20b0d75b0d 修改 pnpm 版本 2023-03-30 23:20:38 +08:00
whyour c89724906e 修复定时任务表头样式 2023-03-23 22:19:48 +08:00
whyour c3a548ece3 修改定时任务列表滚动 2023-03-22 22:30:47 +08:00
whyour b431e39f8f 修复系统设置中登录日志样式 2023-03-20 23:23:03 +08:00
whyour 710746809b 修复删除视图筛选条件 2023-03-20 23:00:13 +08:00
whyour 8a225e9beb 修复视图筛选条件样式 2023-03-20 18:03:09 +08:00
whyour a5ecb204b2 修复 python Server酱推送 2023-03-19 22:54:50 +08:00
whyour f1f009da3b 禁止手机浏览器下拉刷新 2023-03-19 15:15:01 +08:00
whyour 7291098f98 修复手机页面滚动 2023-03-19 15:11:28 +08:00
whyour 666545ff03 修改初始化依赖安装逻辑 2023-03-18 22:26:29 +08:00
whyour 81e2bbb14c 更新版本 v2.15.10 2023-03-17 21:30:13 +08:00
whyour 970dff144c 修复订阅下载 http 协议文件 2023-03-15 21:44:11 +08:00
whyour 819b15b15e 容器增加 ipv6 检测,自动配置 nginx 2023-03-14 22:39:10 +08:00
whyour 3fa57c69ac 修改检查 pm2 状态 api 2023-03-13 23:57:56 +08:00
whyour 18689c8119 修复 shell 引入通用变量 2023-03-13 22:30:53 +08:00
adams549659584andGitHub 61e7049baa 修复系统 bark 推送铃声 (#1850) 2023-03-13 22:13:20 +08:00
whyour 39c40a328d 修复检查更新弹框逻辑 2023-03-12 21:48:31 +08:00
whyour 955d815d14 修改 task 获取 token 逻辑 2023-03-12 21:33:46 +08:00
adams549659584andGitHub 3a3b945de8 修复 js 下 bark 推送声音 bug (#1847) 2023-03-12 20:58:26 +08:00
whyour 73c53dbc8d 修改 typescript execute 2023-03-12 20:53:15 +08:00
whyour f76828d4ec 修改 shell 文件权限 2023-03-11 01:10:03 +08:00
whyour 7b2956eb0a 修复日志删除逻辑 2023-03-10 14:21:52 +08:00
whyour b2167ae5e4 修复自定义通知 2023-03-07 22:20:08 +08:00
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
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
58 changed files with 3071 additions and 1998 deletions
+6 -6
View File
@@ -9,9 +9,7 @@ on:
- '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
workflow_dispatch: workflow_dispatch:
jobs: jobs:
@@ -47,7 +45,7 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: pnpm/action-setup@v2 - uses: pnpm/action-setup@v2
with: with:
version: latest version: 7
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
@@ -87,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
@@ -99,7 +99,7 @@ jobs:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: pnpm/action-setup@v2 - uses: pnpm/action-setup@v2
with: with:
version: latest version: 7
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
cache: 'pnpm' cache: 'pnpm'
@@ -155,7 +155,7 @@ jobs:
QL_BRANCH=${{ github.ref_name }} QL_BRANCH=${{ github.ref_name }}
SOURCE_COMMIT=${{ github.sha }} 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: . context: .
file: ./docker/Dockerfile file: ./docker/Dockerfile
push: true push: true
+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( route.post(
'/', '/',
celebrate({ celebrate({
@@ -160,6 +175,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 +332,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(),
}), }),
}), }),
+1 -1
View File
@@ -1,4 +1,4 @@
export const LOG_END_SYMBOL = '\n          '; export const LOG_END_SYMBOL = '     ';
export const TASK_COMMAND = 'task'; export const TASK_COMMAND = 'task';
export const QL_COMMAND = 'ql'; export const QL_COMMAND = 'ql';
+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');
+7 -10
View File
@@ -4,8 +4,7 @@ import { Container } from 'typedi';
import { Crontab, CrontabModel, CrontabStatus } from '../data/cron'; import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
import CronService from '../services/cron'; import CronService from '../services/cron';
import EnvService from '../services/env'; import EnvService from '../services/env';
import groupBy from 'lodash/groupBy'; import { DependenceModel, DependenceStatus } from '../data/dependence';
import { DependenceModel } from '../data/dependence';
import { Op } from 'sequelize'; import { Op } from 'sequelize';
import config from '../config'; import config from '../config';
import { CrontabViewModel, CronViewType } from '../data/cronView'; import { CrontabViewModel, CronViewType } from '../data/cronView';
@@ -42,16 +41,14 @@ export default async () => {
order: [['type', 'DESC']], order: [['type', 'DESC']],
raw: true, raw: true,
}).then(async (docs) => { }).then(async (docs) => {
const groups = groupBy(docs, 'type'); await DependenceModel.update(
const keys = Object.keys(groups).sort((a, b) => parseInt(b) - parseInt(a)); { status: DependenceStatus.installing, log: [] },
for (const key of keys) { { where: { id: docs.map((x) => x.id!) } },
const group = groups[key]; );
const depIds = group.map((x) => x.id); dependenceService.installDependenceOneByOne(docs);
await dependenceService.reInstall(depIds as number[]);
}
}); });
// 初始化时执行一次所有的ql repo 任务 // 初始化时执行一次所有的 ql repo 任务
CrontabModel.findAll({ CrontabModel.findAll({
where: { where: {
isDisabled: { [Op.ne]: 1 }, isDisabled: { [Op.ne]: 1 },
+7 -9
View File
@@ -11,7 +11,7 @@ export default async () => {
const subscriptionService = Container.get(SubscriptionService); const subscriptionService = Container.get(SubscriptionService);
// 生成内置token // 生成内置token
let tokenCommand = `ts-node-transpile-only ${config.rootPath}/back/token.ts`; let tokenCommand = `tsx ${config.rootPath}/back/token.ts`;
const tokenFile = `${config.rootPath}static/build/token.js`; const tokenFile = `${config.rootPath}static/build/token.js`;
if (await fileExist(tokenFile)) { if (await fileExist(tokenFile)) {
tokenCommand = `node ${tokenFile}`; tokenCommand = `node ${tokenFile}`;
@@ -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,24 +29,21 @@ 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,
}); });
} }
// 运行所有订阅 // 运行所有订阅
await subscriptionService.setSshConfig();
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,
);
} }
}; };
+18 -6
View File
@@ -41,8 +41,9 @@ export default class CronService {
} }
public async update(payload: Crontab): Promise<Crontab> { public async update(payload: Crontab): Promise<Crontab> {
payload.saved = false; const tab = new Crontab(payload);
const newDoc = await this.updateDb(payload); tab.saved = false;
const newDoc = await this.updateDb(tab);
await this.set_crontab(); await this.set_crontab();
return newDoc; return newDoc;
} }
@@ -244,8 +245,11 @@ export default class CronService {
const filterKeys: any = Object.keys(filterQuery); const filterKeys: any = Object.keys(filterQuery);
for (const key of filterKeys) { for (const key of filterKeys) {
let q: any = {}; let q: any = {};
if (filterKeys[key]) { if (!filterQuery[key]) continue;
q[key] = filterKeys[key]; 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); query[Op.and].push(q);
} }
@@ -260,6 +264,15 @@ export default class CronService {
} }
} }
public async find({ log_path }: { log_path: string }): Promise<Crontab | null> {
try {
const result = await CrontabModel.findOne({ where: { log_path } });
return result;
} catch (error) {
throw error;
}
}
public async crontabs(params?: { public async crontabs(params?: {
searchValue: string; searchValue: string;
page: string; page: string;
@@ -428,7 +441,7 @@ export default class CronService {
if (logFileExist) { if (logFileExist) {
return getFileContentByName(`${absolutePath}`); return getFileContentByName(`${absolutePath}`);
} else { } else {
return '任务未运行或运行失败,请尝试手动运行'; return '任务未运行';
} }
} }
@@ -452,7 +465,6 @@ export default class CronService {
} else { } else {
return []; return [];
} }
} }
private make_command(tab: Crontab) { private make_command(tab: Crontab) {
+1 -1
View File
@@ -31,7 +31,7 @@ export default class CronViewService {
} }
public async update(payload: CrontabView): Promise<CrontabView> { public async update(payload: CrontabView): Promise<CrontabView> {
const newDoc = await this.updateDb(payload); const newDoc = await this.updateDb(new CrontabView(payload));
return newDoc; return newDoc;
} }
+1 -1
View File
@@ -99,7 +99,7 @@ export default class DependenceService {
} }
} }
private installDependenceOneByOne( public installDependenceOneByOne(
docs: Dependence[], docs: Dependence[],
isInstall: boolean = true, isInstall: boolean = true,
force: boolean = false, force: boolean = false,
+8 -4
View File
@@ -49,7 +49,7 @@ export default class EnvService {
} }
public async update(payload: Env): Promise<Env> { 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(); await this.set_envs();
return newDoc; return newDoc;
} }
@@ -92,13 +92,17 @@ 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) {
+5 -5
View File
@@ -161,7 +161,7 @@ export default class NotificationService {
this.title, this.title,
)}/${encodeURIComponent( )}/${encodeURIComponent(
this.content, this.content,
)}?icon=${barkIcon}?sound=${barkSound}&group=${barkGroup}`; )}?icon=${barkIcon}&sound=${barkSound}&group=${barkGroup}`;
const res: any = await got const res: any = await got
.get(url, { .get(url, {
...this.gotOption, ...this.gotOption,
@@ -484,11 +484,11 @@ export default class NotificationService {
return { return {
formatUrl: url formatUrl: url
.replaceAll('$title', encodeURIComponent(this.title)) ?.replaceAll('$title', encodeURIComponent(this.title))
.replaceAll('$content', encodeURIComponent(this.content)), ?.replaceAll('$content', encodeURIComponent(this.content)),
formatBody: body formatBody: body
.replaceAll('$title', this.title) ?.replaceAll('$title', this.title)
.replaceAll('$content', this.content), ?.replaceAll('$content', this.content),
}; };
} }
} }
+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);
} }
} }
+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 {
} }
} }
public 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) {
+3 -2
View File
@@ -2,7 +2,7 @@ FROM python:3.10-alpine as builder
COPY package.json .npmrc pnpm-lock.yaml /tmp/build/ COPY package.json .npmrc pnpm-lock.yaml /tmp/build/
RUN set -x \ RUN set -x \
&& apk update \ && apk update \
&& apk add nodejs npm \ && apk add nodejs npm git \
&& npm i -g pnpm \ && npm i -g pnpm \
&& cd /tmp/build \ && cd /tmp/build \
&& pnpm install --prod && pnpm install --prod
@@ -16,6 +16,7 @@ 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 \$ " \
@@ -48,7 +49,7 @@ RUN set -x \
&& git config --global user.name "qinglong" \ && git config --global user.name "qinglong" \
&& 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 tsx \
&& rm -rf /root/.pnpm-store \ && rm -rf /root/.pnpm-store \
&& rm -rf /root/.local/share/pnpm/store \ && rm -rf /root/.local/share/pnpm/store \
&& rm -rf /root/.cache \ && rm -rf /root/.cache \
+4 -5
View File
@@ -9,9 +9,8 @@ export isFirstStartServer=true
echo -e "======================1. 检测配置文件========================\n" echo -e "======================1. 检测配置文件========================\n"
make_dir /etc/nginx/conf.d make_dir /etc/nginx/conf.d
make_dir /run/nginx make_dir /run/nginx
cp -fv $nginx_conf /etc/nginx/nginx.conf init_nginx
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
sed -i "s,QL_BASE_URL,${qlBaseUrl},g" /etc/nginx/conf.d/front.conf
pm2 l &>/dev/null pm2 l &>/dev/null
patch_version &>/dev/null patch_version &>/dev/null
@@ -42,13 +41,13 @@ echo -e "定时任务启动成功...\n"
if [[ $AutoStartBot == true ]]; then if [[ $AutoStartBot == true ]]; then
echo -e "======================7. 启动bot========================\n" 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" echo -e "bot后台启动中...\n"
fi fi
if [[ $EnableExtraShell == true ]]; then if [[ $EnableExtraShell == true ]]; then
echo -e "======================8. 执行自定义脚本========================\n" 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" echo -e "自定义脚本后台执行中...\n"
fi fi
+1
View File
@@ -13,6 +13,7 @@ map $http_upgrade $connection_upgrade {
server { server {
listen 5700; listen 5700;
IPV6_CONFIG
root /ql/static/dist; root /ql/static/dist;
ssl_session_timeout 5m; ssl_session_timeout 5m;
+7 -5
View File
@@ -5,6 +5,7 @@
"start:front": "max dev", "start:front": "max dev",
"start:back": "nodemon", "start:back": "nodemon",
"start:public": "ts-node --transpile-only ./back/public.ts", "start:public": "ts-node --transpile-only ./back/public.ts",
"start:schedule": "ts-node --transpile-only ./back/schedule.ts",
"build:front": "max build", "build:front": "max build",
"build:back": "tsc -p tsconfig.back.json", "build:back": "tsc -p tsconfig.back.json",
"panel": "npm run build:back && node static/build/app.js", "panel": "npm run build:back && node static/build/app.js",
@@ -82,7 +83,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.1.2", "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",
@@ -92,7 +93,7 @@
"devDependencies": { "devDependencies": {
"@ant-design/icons": "^4.7.0", "@ant-design/icons": "^4.7.0",
"@ant-design/pro-layout": "6.38.22", "@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", "@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",
@@ -115,10 +116,10 @@
"@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.42", "@umijs/max": "^4.0.55",
"@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.24.7", "antd": "^4.24.8",
"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",
@@ -142,10 +143,11 @@
"sockjs-client": "^1.6.0", "sockjs-client": "^1.6.0",
"ts-node": "^10.6.0", "ts-node": "^10.6.0",
"tslib": "^2.4.0", "tslib": "^2.4.0",
"tsx": "^3.12.3",
"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", "virtualizedtableforantd4": "1.3.0",
"webpack": "^5.70.0", "webpack": "^5.70.0",
"yorkie": "^2.0.0" "yorkie": "^2.0.0"
} }
+1764 -1392
View File
File diff suppressed because it is too large Load Diff
+11 -1
View File
@@ -296,6 +296,16 @@ async function sendNotify(
) { ) {
//提供6种通知 //提供6种通知
desp += author; //增加作者信息,防止被贩卖等 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([ await Promise.all([
serverNotify(text, desp), //微信server酱 serverNotify(text, desp), //微信server酱
pushPlusNotify(text, desp), //pushplus(推送加) pushPlusNotify(text, desp), //pushplus(推送加)
@@ -523,7 +533,7 @@ function BarkNotify(text, desp, params = {}) {
const options = { const options = {
url: `${BARK_PUSH}/${encodeURIComponent(text)}/${encodeURIComponent( url: `${BARK_PUSH}/${encodeURIComponent(text)}/${encodeURIComponent(
desp, desp,
)}?icon=${BARK_ICON}?sound=${BARK_SOUND}&group=${BARK_GROUP}&${querystring.stringify( )}?icon=${BARK_ICON}&sound=${BARK_SOUND}&group=${BARK_GROUP}&${querystring.stringify(
params, params,
)}`, )}`,
headers: { headers: {
+35 -18
View File
@@ -164,7 +164,8 @@ def dingding_bot(title: str, content: str) -> None:
timestamp = str(round(time.time() * 1000)) timestamp = str(round(time.time() * 1000))
secret_enc = push_config.get("DD_BOT_SECRET").encode("utf-8") secret_enc = push_config.get("DD_BOT_SECRET").encode("utf-8")
string_to_sign = "{}\n{}".format(timestamp, push_config.get("DD_BOT_SECRET")) string_to_sign = "{}\n{}".format(
timestamp, push_config.get("DD_BOT_SECRET"))
string_to_sign_enc = string_to_sign.encode("utf-8") string_to_sign_enc = string_to_sign.encode("utf-8")
hmac_code = hmac.new( hmac_code = hmac.new(
secret_enc, string_to_sign_enc, digestmod=hashlib.sha256 secret_enc, string_to_sign_enc, digestmod=hashlib.sha256
@@ -220,7 +221,7 @@ def go_cqhttp(title: str, content: str) -> None:
print("go-cqhttp 推送失败!") print("go-cqhttp 推送失败!")
def gotify(title:str,content:str) -> None: def gotify(title: str, content: str) -> None:
""" """
使用 gotify 推送消息。 使用 gotify 推送消息。
""" """
@@ -230,8 +231,9 @@ def gotify(title:str,content:str) -> None:
print("gotify 服务启动") print("gotify 服务启动")
url = f'{push_config.get("GOTIFY_URL")}/message?token={push_config.get("GOTIFY_TOKEN")}' url = f'{push_config.get("GOTIFY_URL")}/message?token={push_config.get("GOTIFY_TOKEN")}'
data = {"title": title,"message": content,"priority": push_config.get("GOTIFY_PRIORITY")} data = {"title": title, "message": content,
response = requests.post(url,data=data).json() "priority": push_config.get("GOTIFY_PRIORITY")}
response = requests.post(url, data=data).json()
if response.get("id"): if response.get("id"):
print("gotify 推送成功!") print("gotify 推送成功!")
@@ -269,10 +271,10 @@ def serverJ(title: str, content: str) -> None:
print("serverJ 服务启动") print("serverJ 服务启动")
data = {"text": title, "desp": content.replace("\n", "\n\n")} data = {"text": title, "desp": content.replace("\n", "\n\n")}
if push_config.get("PUSH_KEY").index("SCT") != -1: if push_config.get("PUSH_KEY").find("SCT") != -1:
url = f'https://sctapi.ftqq.com/{push_config.get("PUSH_KEY")}.send' url = f'https://sctapi.ftqq.com/{push_config.get("PUSH_KEY")}.send'
else: else:
url = f'https://sc.ftqq.com/${push_config.get("PUSH_KEY")}.send' url = f'https://sc.ftqq.com/{push_config.get("PUSH_KEY")}.send'
response = requests.post(url, data=data).json() response = requests.post(url, data=data).json()
if response.get("errno") == 0 or response.get("code") == 0: if response.get("errno") == 0 or response.get("code") == 0:
@@ -289,7 +291,8 @@ def pushdeer(title: str, content: str) -> None:
print("PushDeer 服务的 DEER_KEY 未设置!!\n取消推送") print("PushDeer 服务的 DEER_KEY 未设置!!\n取消推送")
return return
print("PushDeer 服务启动") print("PushDeer 服务启动")
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")
@@ -320,7 +323,6 @@ def chat(title: str, content: str) -> None:
print("Chat 推送失败!错误信息:", response) print("Chat 推送失败!错误信息:", response)
def pushplus_bot(title: str, content: str) -> None: def pushplus_bot(title: str, content: str) -> None:
""" """
通过 push+ 推送消息。 通过 push+ 推送消息。
@@ -348,7 +350,8 @@ def pushplus_bot(title: str, content: str) -> None:
url_old = "http://pushplus.hxtrip.com/send" url_old = "http://pushplus.hxtrip.com/send"
headers["Accept"] = "application/json" headers["Accept"] = "application/json"
response = requests.post(url=url_old, data=body, headers=headers).json() response = requests.post(
url=url_old, data=body, headers=headers).json()
if response["code"] == 200: if response["code"] == 200:
print("PUSHPLUS(hxtrip) 推送成功!") print("PUSHPLUS(hxtrip) 推送成功!")
@@ -367,7 +370,8 @@ def qmsg_bot(title: str, content: str) -> None:
print("qmsg 服务启动") print("qmsg 服务启动")
url = f'https://qmsg.zendee.cn/{push_config.get("QMSG_TYPE")}/{push_config.get("QMSG_KEY")}' url = f'https://qmsg.zendee.cn/{push_config.get("QMSG_TYPE")}/{push_config.get("QMSG_KEY")}'
payload = {"msg": f'{title}\n\n{content.replace("----", "-")}'.encode("utf-8")} payload = {
"msg": f'{title}\n\n{content.replace("----", "-")}'.encode("utf-8")}
response = requests.post(url=url, params=payload).json() response = requests.post(url=url, params=payload).json()
if response["code"] == 0: if response["code"] == 0:
@@ -553,14 +557,14 @@ def aibotk(title: str, content: str) -> None:
data = { data = {
"apiKey": push_config.get("AIBOTK_KEY"), "apiKey": push_config.get("AIBOTK_KEY"),
"roomName": push_config.get("AIBOTK_NAME"), "roomName": push_config.get("AIBOTK_NAME"),
"message": {"type": 1, "content": f'【青龙快讯】\n\n${title}\n${content}' } "message": {"type": 1, "content": f'【青龙快讯】\n\n{title}\n{content}'}
} }
else: else:
url = "https://api-bot.aibotk.com/openapi/v1/chat/contact" url = "https://api-bot.aibotk.com/openapi/v1/chat/contact"
data = { data = {
"apiKey": push_config.get("AIBOTK_KEY"), "apiKey": push_config.get("AIBOTK_KEY"),
"name": push_config.get("AIBOTK_NAME"), "name": push_config.get("AIBOTK_NAME"),
"message": {"type": 1, "content": f'【青龙快讯】\n\n${title}\n${content}' } "message": {"type": 1, "content": f'【青龙快讯】\n\n{title}\n{content}'}
} }
body = json.dumps(data).encode(encoding="utf-8") body = json.dumps(data).encode(encoding="utf-8")
headers = {"Content-Type": "application/json"} headers = {"Content-Type": "application/json"}
@@ -582,14 +586,19 @@ def smtp(title: str, content: str) -> None:
print("SMTP 邮件 服务启动") print("SMTP 邮件 服务启动")
message = MIMEText(content, 'plain', 'utf-8') message = MIMEText(content, 'plain', 'utf-8')
message['From'] = formataddr((Header(push_config.get("SMTP_NAME"), 'utf-8').encode(), push_config.get("SMTP_EMAIL"))) message['From'] = formataddr((Header(push_config.get(
message['To'] = formataddr((Header(push_config.get("SMTP_NAME"), 'utf-8').encode(), push_config.get("SMTP_EMAIL"))) "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') message['Subject'] = Header(title, 'utf-8')
try: 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 = smtplib.SMTP_SSL(push_config.get("SMTP_SERVER")) if push_config.get(
smtp_server.login(push_config.get("SMTP_EMAIL"), push_config.get("SMTP_PASSWORD")) "SMTP_SSL") == 'true' else smtplib.SMTP(push_config.get("SMTP_SERVER"))
smtp_server.sendmail(push_config.get("SMTP_EMAIL"), push_config.get("SMTP_EMAIL"), message.as_bytes()) 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() smtp_server.close()
print("SMTP 邮件 推送成功!") print("SMTP 邮件 推送成功!")
except Exception as e: except Exception as e:
@@ -647,13 +656,21 @@ def send(title: str, content: str) -> None:
print(f"{title} 推送内容为空!") print(f"{title} 推送内容为空!")
return 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") hitokoto = push_config.get("HITOKOTO")
text = one() if hitokoto else "" text = one() if hitokoto else ""
content += "\n\n" + text content += "\n\n" + text
ts = [ ts = [
threading.Thread(target=mode, args=(title, content), name=mode.__name__) threading.Thread(target=mode, args=(
title, content), name=mode.__name__)
for mode in notify_function for mode in notify_function
] ]
[t.start() for t in ts] [t.start() for t in ts]
+57 -19
View File
@@ -1,15 +1,24 @@
#!/usr/bin/env bash #!/usr/bin/env bash
create_token() {
local token_command="tsx ${dir_root}/back/token.ts"
local token_file="${dir_root}static/build/token.js"
if [[ -f $token_file ]]; then
token_command="node ${token_file}"
fi
token=$(eval "$token_command")
}
get_token() { get_token() {
if [[ -f $file_auth_token ]]; then if [[ -f $file_auth_token ]]; then
token=$(cat $file_auth_token | jq -r .value) token=$(cat $file_auth_token | jq -r .value)
else local expiration=$(cat $file_auth_token | jq -r .expiration)
local token_command="ts-node-transpile-only ${dir_root}/back/token.ts" local currentTimeStamp=$(date +%s)
local token_file="${dir_root}static/build/token.js" if [[ $currentTimeStamp -ge $expiration ]]; then
if [[ -f $token_file ]]; then create_token
token_command="node ${token_file}"
fi fi
token=$(eval "$token_command") else
create_token
fi fi
} }
@@ -19,10 +28,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 +49,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 +88,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 +120,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 +145,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 +175,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 +199,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
@@ -193,4 +208,27 @@ notify_api() {
fi 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 get_token
Regular → Executable
-4
View File
@@ -1,9 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
## 导入通用变量与函数
dir_shell=$QL_DIR/shell
. $dir_shell/share.sh
if [[ -z ${BotRepoUrl} ]]; then if [[ -z ${BotRepoUrl} ]]; then
url="https://github.com/SuMaiKaDe/bot.git" url="https://github.com/SuMaiKaDe/bot.git"
repo_path="${dir_repo}/dockerbot" repo_path="${dir_repo}/dockerbot"
Regular → Executable
+2 -10
View File
@@ -1,9 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
dir_shell=$QL_DIR/shell
. $dir_shell/share.sh
. $dir_shell/api.sh
reset_env() { reset_env() {
echo -e "---> 1. 开始检测配置文件\n" echo -e "---> 1. 开始检测配置文件\n"
fix_config fix_config
@@ -29,9 +25,7 @@ copy_dep() {
echo -e "---> 通知文件复制完成\n" echo -e "---> 通知文件复制完成\n"
echo -e "---> 2. 复制nginx配置文件\n" echo -e "---> 2. 复制nginx配置文件\n"
cp -fv $nginx_conf /etc/nginx/nginx.conf init_nginx
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
sed -i "s,QL_BASE_URL,${qlBaseUrl},g" /etc/nginx/conf.d/front.conf
echo -e "---> 配置文件复制完成\n" echo -e "---> 配置文件复制完成\n"
} }
@@ -70,9 +64,8 @@ check_pm2() {
pm2_log pm2_log
local currentTimeStamp=$(date +%s) local currentTimeStamp=$(date +%s)
local api=$( local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/api/user?t=$currentTimeStamp" \ curl -s --noproxy "*" "http://0.0.0.0:5600/api/system?t=$currentTimeStamp" \
-H 'Accept: */*' \ -H 'Accept: */*' \
-H "Authorization: Bearer $token" \
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36' \ -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36' \
-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' \
@@ -96,7 +89,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
Regular → Executable
+2 -8
View File
@@ -1,10 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
## 导入通用变量与函数
dir_shell=$QL_DIR/shell
. $dir_shell/share.sh
. $dir_shell/api.sh
trap "single_hanle" 2 20 15 14 trap "single_hanle" 2 20 15 14
single_hanle() { single_hanle() {
handle_task_after "$@" handle_task_after "$@"
@@ -124,8 +119,7 @@ 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" echo -e "\n\n## 执行结束... $end_time 耗时 $diff_time     "
echo -e "\n     "
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time" [[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
} }
@@ -174,7 +168,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%/*}"
Regular → Executable
+1 -1
View File
@@ -11,7 +11,7 @@ echo -e "提交master代码"
git push git push
echo -e "更新cdn文件" echo -e "更新cdn文件"
ts-node sample/tool.ts tsx sample/tool.ts
string=$(cat version.yaml | grep "version" | egrep "[^ ]*" -o | egrep "\d\.*") string=$(cat version.yaml | grep "version" | egrep "[^ ]*" -o | egrep "\d\.*")
version="v$string" version="v$string"
+12 -6
View File
@@ -1,12 +1,8 @@
#!/usr/bin/env bash #!/usr/bin/env bash
## 导入通用变量与函数
dir_shell=$QL_DIR/shell
. $dir_shell/share.sh
days=$1 days=$1
## 删除运行js脚本的旧日志 ## 删除运行脚本的旧日志
remove_js_log() { remove_js_log() {
local log_full_path_list=$(find $dir_log/ -name "*.log") local log_full_path_list=$(find $dir_log/ -name "*.log")
local diff_time local diff_time
@@ -18,7 +14,17 @@ remove_js_log() {
else else
diff_time=$(($(date +%s) - $(date +%s -d "$log_date"))) diff_time=$(($(date +%s) - $(date +%s -d "$log_date")))
fi 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")
echo -e "查询文件 $log_path"
if [[ -z $result ]]; then
echo -e "删除中~"
rm -vf $log
else
echo -e "正在被 $result 使用,跳过~"
fi
fi
fi fi
done done
} }
+24 -12
View File
@@ -326,12 +326,12 @@ 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
git pull 1>/dev/null
exit_status=$? exit_status=$?
git pull &>/dev/null
unset_proxy unset_proxy
cd $dir_current cd $dir_current
@@ -357,7 +357,7 @@ reset_romote_url() {
reset_branch() { reset_branch() {
local branch="$1" local branch="$1"
local part_cmd="" local part_cmd="HEAD"
if [[ $branch ]]; then if [[ $branch ]]; then
part_cmd="origin/${branch}" part_cmd="origin/${branch}"
git checkout -B "$branch" &>/dev/null git checkout -B "$branch" &>/dev/null
@@ -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
} }
@@ -431,16 +431,17 @@ format_timestamp() {
} }
patch_version() { patch_version() {
# 兼容pnpm@7
pnpm setup &>/dev/null
source ~/.bashrc
if [[ $PipMirror ]]; then if [[ $PipMirror ]]; then
pip3 config set global.index-url $PipMirror pip3 config set global.index-url $PipMirror
fi fi
if [[ $NpmMirror ]]; then if [[ $NpmMirror ]]; then
npm config set registry $NpmMirror pnpm config set registry $NpmMirror
fi fi
# 兼容pnpm@7
pnpm setup &>/dev/null
source ~/.bashrc
pnpm install -g &>/dev/null pnpm install -g &>/dev/null
if [[ -f "$dir_root/db/cookie.db" ]]; then if [[ -f "$dir_root/db/cookie.db" ]]; then
@@ -450,9 +451,7 @@ patch_version() {
echo echo
fi fi
if ! type ts-node &>/dev/null; then pnpm add -g pm2 tsx
pnpm add -g ts-node typescript tslib
fi
git config --global pull.rebase false git config --global pull.rebase false
@@ -483,6 +482,19 @@ patch_version() {
fi fi
} }
init_nginx() {
cp -fv $nginx_conf /etc/nginx/nginx.conf
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
sed -i "s,QL_BASE_URL,${qlBaseUrl},g" /etc/nginx/conf.d/front.conf
ipv6=$(ip a | grep inet6)
ipv6Str=""
if [[ $ipv6 ]]; then
ipv6Str="listen [::]:5700 ipv6only=on;"
fi
sed -i "s,IPV6_CONFIG,${ipv6Str},g" /etc/nginx/conf.d/front.conf
}
init_env init_env
detect_termux detect_termux
detect_macos detect_macos
+19 -7
View File
@@ -15,7 +15,11 @@ define_program() {
elif [[ $file_param == *.sh ]]; then elif [[ $file_param == *.sh ]]; then
which_program="bash" which_program="bash"
elif [[ $file_param == *.ts ]]; then elif [[ $file_param == *.ts ]]; then
which_program="ts-node-transpile-only" if ! type tsx &>/dev/null; then
which_program="ts-node-transpile-only"
else
which_program="tsx"
fi
else else
which_program="" which_program=""
fi fi
@@ -32,8 +36,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 +52,25 @@ 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"
if [[ $is_macos -eq 1 ]]; then
mtime_format=$time_format
else
mtime_format="%Y-%m-%d %H:%M:%S.%3N"
fi
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 +93,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
+75 -48
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,58 @@ 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 proxyStr=""
if [[ $proxy ]]; then
if [[ $url == http:* ]]; then
proxyStr="-e \"http_proxy=${proxy}\""
elif [[ $url == https:* ]]; then
proxyStr="-e \"http_proxy=${proxy};https_proxy=${proxy}\""
fi
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 $proxyStr -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 cd $dir_raw
local filename="raw_${raw_file_name}" local filename="raw_${raw_file_name}"
local cron_id=$(cat $list_crontab_user | grep -E "$cmd_task.* $filename" | perl -pe "s|.*ID=(.*) $cmd_task.* $filename\.*|\1|" | head -1 | head -1 | awk -F " " '{print $1}') local cron_id=$(cat $list_crontab_user | grep -E "$cmd_task.* $filename" | perl -pe "s|.*ID=(.*) $cmd_task.* $filename\.*|\1|" | head -1 | awk -F " " '{print $1}')
cp -f $raw_file_name $dir_scripts/${filename} cp -f $raw_file_name $dir_scripts/${filename}
cron_line=$( if [[ -z $cron_id ]] && [[ ${autoAddCron} == true ]]; then
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"
@@ -193,13 +212,10 @@ update_raw() {
## 调用用户自定义的extra.sh ## 调用用户自定义的extra.sh
run_extra_shell() { run_extra_shell() {
if [[ ${EnableExtraShell} == true ]]; then if [[ -f $file_extra_shell ]]; then
if [[ -f $file_extra_shell ]]; then . $file_extra_shell
echo -e "--------------------------------------------------------------\n" else
. $file_extra_shell echo -e "$file_extra_shell文件不存在,跳过执行...\n"
else
echo -e "$file_extra_shell文件不存在,跳过执行...\n"
fi
fi fi
} }
@@ -219,10 +235,10 @@ usage() {
## 更新qinglong ## 更新qinglong
update_qinglong() { update_qinglong() {
local mirror="github" local mirror="gitee"
local githubStatus=$(curl -s -m 2 -IL "https://github.com" | grep 200) local githubStatus=$(curl -s -m 2 -IL "https://google.com" | grep 200)
if [ "$githubStatus" == "" ]; then if [[ ! -z $githubStatus ]]; then
mirror="gitee" mirror="github"
fi fi
echo -e "使用 ${mirror} 源更新...\n" echo -e "使用 ${mirror} 源更新...\n"
export isFirstStartServer=false export isFirstStartServer=false
@@ -288,6 +304,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"
@@ -297,13 +322,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
@@ -404,13 +429,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")
@@ -452,7 +480,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
@@ -461,7 +489,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
@@ -499,8 +527,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
+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 { MutableRefObject, useLayoutEffect, useState } from 'react';
import useResizeObserver from '@react-hook/resize-observer' import useResizeObserver from '@react-hook/resize-observer';
import { getTableScroll } from '@/utils'; import { getTableScroll } from '@/utils';
export default <T extends HTMLElement>(target: MutableRefObject<T>, extraHeight?: number) => { export default <T extends HTMLElement>(
const [height, setHeight] = useState<number>() target: MutableRefObject<T>,
extraHeight?: number,
) => {
const [height, setHeight] = useState<number>(0);
useResizeObserver(target, (entry) => { useResizeObserver(target, (entry) => {
let _targe = entry.target as any let _target = entry.target as any;
if (!_targe.classList.contains('ant-table-wrapper')) { if (!_target.classList.contains('ant-table-wrapper')) {
_targe = entry.target.querySelector('.ant-table-wrapper') _target = entry.target.querySelector('.ant-table-wrapper');
} }
setHeight(getTableScroll({ extraHeight, target: _targe as HTMLElement })) setHeight(getTableScroll({ extraHeight, target: _target as HTMLElement }));
}) });
return height return height;
} };
+48
View File
@@ -9,6 +9,22 @@
url('../assets/fonts/SourceCodePro-Regular.ttf') format('truetype'); url('../assets/fonts/SourceCodePro-Regular.ttf') format('truetype');
} }
body {
// 禁止手机页面下拉刷新
overflow: hidden;
// 禁止手机页面弹簧效果
position: fixed;
top: 0;
left: 0;
}
#root {
height: 100vh;
height: calc(100vh - var(--vh-offset, 0px));
-webkit-overflow-scrolling: touch;
}
.ant-modal-body { .ant-modal-body {
max-height: calc(80vh - 110px); max-height: calc(80vh - 110px);
max-height: calc(80vh - var(--vh-offset, 110px)); max-height: calc(80vh - var(--vh-offset, 110px));
@@ -349,3 +365,35 @@ pre {
white-space: break-spaces !important; white-space: break-spaces !important;
padding: 0 !important; padding: 0 !important;
} }
.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;
}
+20 -1
View File
@@ -155,7 +155,7 @@
.view-create-modal-sorts { .view-create-modal-sorts {
display: flex; display: flex;
.ant-space-item:nth-child(2) { .ant-space-item:nth-child(1) {
flex: 1; flex: 1;
} }
} }
@@ -177,5 +177,24 @@ tr.drop-over-upward td {
.view-filters-container.active { .view-filters-container.active {
.filter-item > div > .ant-form-item-control { .filter-item > div > .ant-form-item-control {
margin-left: 40px; margin-left: 40px;
width: calc(100% - 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;
}
}
.crontab-wrapper {
tbody .ant-table-cell {
height: 69px !important;
} }
} }
+12 -29
View File
@@ -51,9 +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 { ColumnProps } from 'antd/lib/table';
import { VList } from 'virtuallist-antd'; import { useVT } from 'virtualizedtableforantd4';
const { Text, Paragraph } = Typography; const { Text, Paragraph } = Typography;
const { Search } = Input; const { Search } = Input;
@@ -107,7 +107,6 @@ const Crontab = () => {
dataIndex: 'name', dataIndex: 'name',
key: 'name', key: 'name',
width: 150, width: 150,
align: 'center' as const,
render: (text: string, record: any) => ( render: (text: string, record: any) => (
<> <>
<a <a
@@ -163,14 +162,12 @@ const Crontab = () => {
dataIndex: 'command', dataIndex: 'command',
key: 'command', key: 'command',
width: 300, width: 300,
align: 'center' as const,
render: (text, record) => { render: (text, record) => {
return ( return (
<Paragraph <Paragraph
style={{ style={{
wordBreak: 'break-all', wordBreak: 'break-all',
marginBottom: 0, marginBottom: 0,
textAlign: 'left',
}} }}
ellipsis={{ tooltip: text, rows: 2 }} ellipsis={{ tooltip: text, rows: 2 }}
> >
@@ -193,14 +190,12 @@ const Crontab = () => {
dataIndex: 'schedule', dataIndex: 'schedule',
key: 'schedule', key: 'schedule',
width: 110, width: 110,
align: 'center' as const,
sorter: { sorter: {
compare: (a, b) => a.schedule.localeCompare(b.schedule), compare: (a, b) => a.schedule.localeCompare(b.schedule),
}, },
}, },
{ {
title: '最后运行时间', title: '最后运行时间',
align: 'center' as const,
dataIndex: 'last_execution_time', dataIndex: 'last_execution_time',
key: 'last_execution_time', key: 'last_execution_time',
width: 150, width: 150,
@@ -230,7 +225,6 @@ const Crontab = () => {
}, },
{ {
title: '最后运行时长', title: '最后运行时长',
align: 'center' as const,
width: 120, width: 120,
dataIndex: 'last_running_time', dataIndex: 'last_running_time',
key: 'last_running_time', key: 'last_running_time',
@@ -247,7 +241,6 @@ const Crontab = () => {
}, },
{ {
title: '下次运行时间', title: '下次运行时间',
align: 'center' as const,
width: 150, width: 150,
sorter: { sorter: {
compare: (a: any, b: any) => { compare: (a: any, b: any) => {
@@ -263,13 +256,11 @@ const Crontab = () => {
.replace(' 24:', ' 00:'); .replace(' 24:', ' 00:');
}, },
}, },
{ {
title: '状态', title: '状态',
key: 'status', key: 'status',
dataIndex: 'status', dataIndex: 'status',
align: 'center' as const, width: 88,
width: 85,
filters: [ filters: [
{ {
text: '运行中', text: '运行中',
@@ -330,7 +321,6 @@ const Crontab = () => {
{ {
title: '操作', title: '操作',
key: 'action', key: 'action',
align: 'center' as const,
width: 100, width: 100,
render: (text, record, index) => { render: (text, record, index) => {
const isPc = !isPhone; const isPc = !isPhone;
@@ -425,9 +415,9 @@ const Crontab = () => {
}crons?searchValue=${searchText}&page=${page}&size=${size}&filters=${JSON.stringify( }crons?searchValue=${searchText}&page=${page}&size=${size}&filters=${JSON.stringify(
filters, filters,
)}`; )}`;
if (sorter && sorter.field) { if (sorter && sorter.column && sorter.order) {
url += `&sorter=${JSON.stringify({ url += `&sorter=${JSON.stringify({
field: sorter.field, field: sorter.column.key,
type: sorter.order === 'ascend' ? 'ASC' : 'DESC', type: sorter.order === 'ascend' ? 'ASC' : 'DESC',
})}`; })}`;
} }
@@ -447,10 +437,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(),
}; };
}), }),
); );
@@ -737,10 +724,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,
@@ -928,11 +912,10 @@ const Crontab = () => {
setViewConf(view ? view : null); setViewConf(view ? view : null);
}; };
const vComponents = useMemo(() => { const [vt] = useVT(
return VList({ () => ({ scroll: { y: tableScrollHeight } }),
height: tableScrollHeight!, [tableScrollHeight],
}); );
}, [tableScrollHeight]);
return ( return (
<PageContainer <PageContainer
@@ -1068,7 +1051,7 @@ const Crontab = () => {
rowSelection={rowSelection} rowSelection={rowSelection}
rowClassName={getRowClassName} rowClassName={getRowClassName}
onChange={onPageChange} onChange={onPageChange}
components={vComponents} components={isPhone ? undefined : vt}
/> />
</div> </div>
<CronLogModal <CronLogModal
+52 -25
View File
@@ -34,6 +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 scrollInfoRef = useRef({ value: 0, down: true });
const getCronLog = (isFirst?: boolean) => { const getCronLog = (isFirst?: boolean) => {
if (isFirst) { if (isFirst) {
@@ -44,17 +45,19 @@ 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 || '暂无日志');
const hasNext = log && !logEnded(log) && !log.includes('重启面板') && !log.includes('任务未运行或运行失败,请尝试手动运行'); const hasNext = Boolean(
log &&
!logEnded(log) &&
!log.includes('重启面板') &&
!log.includes('任务未运行'),
);
setExecuting(hasNext); setExecuting(hasNext);
setTimeout(() => { autoScroll();
document
.querySelector('#log-flag')!
.scrollIntoView({ behavior: 'smooth' });
}, 1000);
if (hasNext) { if (hasNext) {
setTimeout(() => { setTimeout(() => {
getCronLog(); getCronLog();
@@ -92,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 (
<> <>
@@ -141,24 +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 id="log-flag"></div>
</div>
</Modal> </Modal>
); );
}; };
+84 -85
View File
@@ -70,6 +70,7 @@ const ViewCreateModal = ({
const [form] = Form.useForm(); const [form] = Form.useForm();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [filterRelation, setFilterRelation] = useState<'and' | 'or'>('and'); const [filterRelation, setFilterRelation] = useState<'and' | 'or'>('and');
const filtersValue = Form.useWatch('filters', form);
const handleOk = async (values: any) => { const handleOk = async (values: any) => {
setLoading(true); setLoading(true);
@@ -126,7 +127,7 @@ const ViewCreateModal = ({
}; };
const typeElement = ( const typeElement = (
<Select> <Select style={{ width: 80 }}>
{SORTTYPES.map((x) => ( {SORTTYPES.map((x) => (
<Select.Option key={x.name} value={x.value}> <Select.Option key={x.name} value={x.value}>
{x.name} {x.name}
@@ -204,7 +205,7 @@ const ViewCreateModal = ({
position: 'absolute', position: 'absolute',
top: '50%', top: '50%',
translate: '-50% -50%', translate: '-50% -50%',
padding: '0 0 0 3px', padding: '0 3px',
cursor: 'pointer', cursor: 'pointer',
}} }}
onClick={() => { onClick={() => {
@@ -221,9 +222,9 @@ const ViewCreateModal = ({
</div> </div>
)} )}
<div> <div>
{fields.map(({ key, name, ...restField }, index) => ( {fields.map(({ key, name, ...restField }) => (
<Form.Item <Form.Item
label={index === 0 ? '筛选条件' : ''} label={name === 0 ? '筛选条件' : ''}
key={key} key={key}
style={{ marginBottom: 0 }} style={{ marginBottom: 0 }}
required required
@@ -232,9 +233,6 @@ const ViewCreateModal = ({
<Space <Space
className="view-create-modal-filters" className="view-create-modal-filters"
align="baseline" align="baseline"
style={
fields.length > 1 ? { width: 'calc(100% - 40px)' } : {}
}
> >
<Form.Item <Form.Item
{...restField} {...restField}
@@ -251,55 +249,18 @@ const ViewCreateModal = ({
{operationElement} {operationElement}
</Form.Item> </Form.Item>
<Form.Item <Form.Item
noStyle {...restField}
shouldUpdate={(prevValues, nextValues) => { name={[name, 'value']}
const preOperation = rules={[{ required: true, message: '请输入内容' }]}
EOperation[
get(prevValues, ['filters', name, 'operation'])
];
const nextOperation =
EOperation[
get(nextValues, ['filters', name, 'operation'])
];
const flag = preOperation !== nextOperation;
if (flag) {
form.setFieldValue(
['filters', name, 'value'],
nextOperation === 'select' ? [] : '',
);
}
return flag;
}}
> >
{() => { {EOperation[filtersValue[name]['operation']] ===
const property = form.getFieldValue([ 'select' ? (
'filters', statusElement(filtersValue[name]['property'])
index, ) : (
'property', <Input placeholder="请输入内容" />
]) as 'status'; )}
const operate = form.getFieldValue([
'filters',
name,
'operation',
]);
return (
<Form.Item
{...restField}
name={[name, 'value']}
rules={[
{ required: true, message: '请输入内容' },
]}
>
{EOperation[operate] === 'select' ? (
statusElement(property)
) : (
<Input placeholder="请输入内容" />
)}
</Form.Item>
);
}}
</Form.Item> </Form.Item>
{index !== 0 && ( {name !== 0 && (
<MinusCircleOutlined onClick={() => remove(name)} /> <MinusCircleOutlined onClick={() => remove(name)} />
)} )}
</Space> </Space>
@@ -321,39 +282,77 @@ const ViewCreateModal = ({
</Form.List> </Form.List>
<Form.List name="sorts"> <Form.List name="sorts">
{(fields, { add, remove }) => ( {(fields, { add, remove }) => (
<> <div
{fields.map(({ key, name, ...restField }, index) => ( style={{ position: 'relative' }}
<Form.Item className={`view-filters-container ${
label={index === 0 ? '排序方式' : ''} fields.length > 1 ? 'active' : ''
key={key} }`}
style={{ marginBottom: 0 }} >
{fields.length > 1 && (
<div
style={{
position: 'absolute',
width: 50,
borderRadius: 10,
border: '1px solid rgb(190, 220, 255)',
borderRight: 'none',
height: 56 * (fields.length - 1),
top: 46,
left: 15,
}}
> >
<Space className="view-create-modal-sorts" align="baseline"> <Button
<Form.Item type="primary"
{...restField} size="small"
name={[name, 'property']} style={{
rules={[{ required: true }]} position: 'absolute',
> top: '50%',
{propertyElement(PROPERTIES, { width: 240 })} translate: '-50% -50%',
</Form.Item> padding: '0 3px',
<Form.Item cursor: 'pointer',
{...restField} }}
name={[name, 'type']} >
rules={[{ required: true }]} <>
> <span>{ViewFilterRelation[filterRelation]}</span>
{typeElement} </>
</Form.Item> </Button>
<MinusCircleOutlined onClick={() => remove(name)} /> </div>
</Space> )}
<div>
{fields.map(({ key, name, ...restField }) => (
<Form.Item
label={name === 0 ? '排序方式' : ''}
key={key}
style={{ marginBottom: 0 }}
className="filter-item"
>
<Space className="view-create-modal-sorts" align="baseline">
<Form.Item
{...restField}
name={[name, 'property']}
rules={[{ required: true }]}
>
{propertyElement(PROPERTIES)}
</Form.Item>
<Form.Item
{...restField}
name={[name, 'type']}
rules={[{ required: true }]}
>
{typeElement}
</Form.Item>
<MinusCircleOutlined onClick={() => remove(name)} />
</Space>
</Form.Item>
))}
<Form.Item>
<a onClick={() => add({ property: 'command', type: 'ASC' })}>
<PlusOutlined />
</a>
</Form.Item> </Form.Item>
))} </div>
<Form.Item> </div>
<a onClick={() => add({ property: 'command', type: 'ASC' })}>
<PlusOutlined />
</a>
</Form.Item>
</>
)} )}
</Form.List> </Form.List>
</Form> </Form>
-8
View File
@@ -80,23 +80,17 @@ const ViewManageModal = ({
title: '名称', title: '名称',
dataIndex: 'name', dataIndex: 'name',
key: 'name', key: 'name',
align: 'center' as const,
render: (text) => (
<div style={{ textAlign: 'left', paddingLeft: 30 }}>{text}</div>
),
}, },
{ {
title: '类型', title: '类型',
dataIndex: 'type', dataIndex: 'type',
key: 'type', key: 'type',
align: 'center' as const,
render: (v) => (v === 1 ? '系统' : '个人'), render: (v) => (v === 1 ? '系统' : '个人'),
}, },
{ {
title: '显示', title: '显示',
key: 'isDisabled', key: 'isDisabled',
dataIndex: 'isDisabled', dataIndex: 'isDisabled',
align: 'center' as const,
width: 100, width: 100,
render: (text: string, record: any, index: number) => { render: (text: string, record: any, index: number) => {
return ( return (
@@ -111,7 +105,6 @@ const ViewManageModal = ({
title: '操作', title: '操作',
key: 'action', key: 'action',
width: 100, width: 100,
align: 'center' as const,
render: (text: string, record: any, index: number) => { render: (text: string, record: any, index: number) => {
return record.type !== 1 ? ( return record.type !== 1 ? (
<Space size="middle"> <Space size="middle">
@@ -167,7 +160,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],
+1 -8
View File
@@ -32,7 +32,6 @@ import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts'; import { SharedContext } from '@/layouts';
import useTableScrollHeight from '@/hooks/useTableScrollHeight'; import useTableScrollHeight from '@/hooks/useTableScrollHeight';
const { Text } = Typography; const { Text } = Typography;
const { Search } = Input; const { Search } = Input;
@@ -57,7 +56,6 @@ const Dependence = () => {
const columns: any = [ const columns: any = [
{ {
title: '序号', title: '序号',
align: 'center' as const,
width: 50, width: 50,
render: (text: string, record: any, index: number) => { render: (text: string, record: any, index: number) => {
return <span style={{ cursor: 'text' }}>{index + 1} </span>; return <span style={{ cursor: 'text' }}>{index + 1} </span>;
@@ -67,13 +65,11 @@ const Dependence = () => {
title: '名称', title: '名称',
dataIndex: 'name', dataIndex: 'name',
key: 'name', key: 'name',
align: 'center' as const,
}, },
{ {
title: '状态', title: '状态',
key: 'status', key: 'status',
dataIndex: 'status', dataIndex: 'status',
align: 'center' as const,
render: (text: string, record: any, index: number) => { render: (text: string, record: any, index: number) => {
return ( return (
<Space size="middle" style={{ cursor: 'text' }}> <Space size="middle" style={{ cursor: 'text' }}>
@@ -91,13 +87,11 @@ const Dependence = () => {
title: '备注', title: '备注',
dataIndex: 'remark', dataIndex: 'remark',
key: 'remark', key: 'remark',
align: 'center' as const,
}, },
{ {
title: '创建时间', title: '创建时间',
key: 'timestamp', key: 'timestamp',
dataIndex: 'timestamp', dataIndex: 'timestamp',
align: 'center' as const,
render: (text: string, record: any) => { render: (text: string, record: any) => {
const language = navigator.language || navigator.languages[0]; const language = navigator.language || navigator.languages[0];
const time = record.createdAt || record.timestamp; const time = record.createdAt || record.timestamp;
@@ -120,7 +114,6 @@ const Dependence = () => {
{ {
title: '操作', title: '操作',
key: 'action', key: 'action',
align: 'center' as const,
render: (text: string, record: any, index: number) => { render: (text: string, record: any, index: number) => {
const isPc = !isPhone; const isPc = !isPhone;
return ( return (
@@ -169,7 +162,7 @@ const Dependence = () => {
const [isLogModalVisible, setIsLogModalVisible] = useState(false); const [isLogModalVisible, setIsLogModalVisible] = useState(false);
const [type, setType] = useState('nodejs'); const [type, setType] = useState('nodejs');
const tableRef = useRef<any>(); const tableRef = useRef<any>();
const tableScrollHeight = useTableScrollHeight(tableRef, 59) const tableScrollHeight = useTableScrollHeight(tableRef, 59);
const getDependencies = () => { const getDependencies = () => {
setLoading(true); setLoading(true);
+89 -102
View File
@@ -39,7 +39,7 @@ 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 Copy from '../../components/copy';
import { VList } from 'virtuallist-antd'; import { useVT } from 'virtualizedtableforantd4';
const { Text } = Typography; const { Text } = Typography;
const { Search } = Input; const { Search } = Input;
@@ -71,7 +71,6 @@ const Env = () => {
const columns: any = [ const columns: any = [
{ {
title: '序号', title: '序号',
align: 'center' as const,
width: 60, width: 60,
render: (text: string, record: any, index: number) => { render: (text: string, record: any, index: number) => {
return <span style={{ cursor: 'text' }}>{index + 1} </span>; return <span style={{ cursor: 'text' }}>{index + 1} </span>;
@@ -81,14 +80,12 @@ const Env = () => {
title: '名称', title: '名称',
dataIndex: 'name', dataIndex: 'name',
key: 'name', key: 'name',
align: 'center' as const,
sorter: (a: any, b: any) => a.name.localeCompare(b.name), sorter: (a: any, b: any) => a.name.localeCompare(b.name),
}, },
{ {
title: '值', title: '值',
dataIndex: 'value', dataIndex: 'value',
key: 'value', key: 'value',
align: 'center' as const,
width: '35%', width: '35%',
render: (text: string, record: any) => { render: (text: string, record: any) => {
return ( return (
@@ -105,7 +102,6 @@ const Env = () => {
title: '备注', title: '备注',
dataIndex: 'remarks', dataIndex: 'remarks',
key: 'remarks', key: 'remarks',
align: 'center' as const,
render: (text: string, record: any) => { render: (text: string, record: any) => {
return ( return (
<Tooltip title={text} placement="topLeft"> <Tooltip title={text} placement="topLeft">
@@ -118,7 +114,6 @@ const Env = () => {
title: '更新时间', title: '更新时间',
dataIndex: 'timestamp', dataIndex: 'timestamp',
key: 'timestamp', key: 'timestamp',
align: 'center' as const,
width: 165, width: 165,
ellipsis: { ellipsis: {
showTitle: false, showTitle: false,
@@ -153,7 +148,6 @@ const Env = () => {
title: '状态', title: '状态',
key: 'status', key: 'status',
dataIndex: 'status', dataIndex: 'status',
align: 'center' as const,
width: 70, width: 70,
filters: [ filters: [
{ {
@@ -180,7 +174,6 @@ const Env = () => {
title: '操作', title: '操作',
key: 'action', key: 'action',
width: 120, width: 120,
align: 'center' as const,
render: (text: string, record: any, index: number) => { render: (text: string, record: any, index: number) => {
const isPc = !isPhone; const isPc = !isPhone;
return ( return (
@@ -331,16 +324,13 @@ const Env = () => {
getEnvs(); getEnvs();
}; };
const vComponents = useMemo(() => { const [vt, setVT] = useVT(
return VList({ () => ({ scroll: { y: tableScrollHeight } }),
height: tableScrollHeight!, [tableScrollHeight],
resetTopWhenDataChange: false, );
});
}, [tableScrollHeight]);
const DragableBodyRow = (props: any) => { const DragableBodyRow = React.forwardRef((props: any, ref) => {
const { index, moveRow, className, style, ...restProps } = props; const { index, moveRow, className, style, ...restProps } = props;
const ref = useRef();
const [{ isOver, dropClassName }, drop] = useDrop({ const [{ isOver, dropClassName }, drop] = useDrop({
accept: type, accept: type,
collect: (monitor) => { collect: (monitor) => {
@@ -368,31 +358,27 @@ const Env = () => {
useEffect(() => { useEffect(() => {
drop(drag(ref)); drop(drag(ref));
}, [drag, drop]); }, [ref]);
const components = useMemo(() => vComponents.body.row, []); return (
<tr
ref={ref}
className={`${className}${isOver ? dropClassName : ''}`}
style={{ cursor: 'move', ...style }}
{...restProps}
/>
);
});
const tempProps = useMemo(() => { useEffect(
return { () =>
ref: ref, setVT({
className: `${className}${isOver ? dropClassName : ''}`, body: {
style: { cursor: 'move', ...style }, row: DragableBodyRow,
...restProps, },
}; }),
}, [className, dropClassName, restProps, style, isOver]); [],
);
return <> {components(tempProps, ref)} </>;
};
const components = useMemo(() => {
return {
...vComponents,
body: {
...vComponents.body,
row: DragableBodyRow,
},
};
}, [vComponents]);
const moveRow = useCallback( const moveRow = useCallback(
(dragIndex: number, hoverIndex: number) => { (dragIndex: number, hoverIndex: number) => {
@@ -542,69 +528,70 @@ const Env = () => {
style: headerStyle, style: headerStyle,
}} }}
> >
{selectedRowIds.length > 0 && ( <div ref={tableRef}>
<div style={{ marginBottom: 16 }}> {selectedRowIds.length > 0 && (
<Button <div style={{ marginBottom: 16 }}>
type="primary" <Button
style={{ marginBottom: 5 }} type="primary"
onClick={modifyName} style={{ marginBottom: 5 }}
> onClick={modifyName}
>
</Button>
<Button </Button>
type="primary" <Button
style={{ marginBottom: 5, marginLeft: 8 }} type="primary"
onClick={delEnvs} style={{ marginBottom: 5, marginLeft: 8 }}
> onClick={delEnvs}
>
</Button>
<Button </Button>
type="primary" <Button
onClick={() => exportEnvs()} type="primary"
style={{ marginLeft: 8, marginRight: 8 }} onClick={() => exportEnvs()}
> style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<Button </Button>
type="primary" <Button
onClick={() => operateEnvs(0)} type="primary"
style={{ marginLeft: 8, marginBottom: 5 }} onClick={() => operateEnvs(0)}
> style={{ marginLeft: 8, marginBottom: 5 }}
>
</Button>
<Button </Button>
type="primary" <Button
onClick={() => operateEnvs(1)} type="primary"
style={{ marginLeft: 8, marginRight: 8 }} onClick={() => operateEnvs(1)}
> style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<span style={{ marginLeft: 8 }}> </Button>
<span style={{ marginLeft: 8 }}>
<a>{selectedRowIds?.length}</a>
</span> <a>{selectedRowIds?.length}</a>
</div> </span>
)} </div>
<DndProvider backend={HTML5Backend}> )}
<Table <DndProvider backend={HTML5Backend}>
ref={tableRef} <Table
columns={columns} columns={columns}
rowSelection={rowSelection} rowSelection={rowSelection}
pagination={false} pagination={false}
dataSource={value} dataSource={value}
rowKey="id" rowKey="id"
size="middle" size="middle"
scroll={{ x: 1000, y: tableScrollHeight }} scroll={{ x: 1000, y: tableScrollHeight }}
components={components} components={vt}
loading={loading} loading={loading}
onRow={(record: any, index: number | undefined) => { onRow={(record: any, index: number | undefined) => {
return { return {
index, index,
moveRow, moveRow,
} as any; } as any;
}} }}
/> />
</DndProvider> </DndProvider>
</div>
<EnvModal <EnvModal
visible={isModalVisible} visible={isModalVisible}
handleCancel={handleCancel} handleCancel={handleCancel}
+9 -6
View File
@@ -36,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';
@@ -80,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));
@@ -100,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}`;
@@ -111,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);
}
} }
}; };
@@ -387,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':
-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);
+3 -3
View File
@@ -47,9 +47,8 @@ const CheckUpdate = ({ socketMessage, systemInfo }: any) => {
</div> </div>
</> </>
), ),
okText: '确认', okText: '强制更新',
cancelText: '强制更新', onOk() {
onCancel() {
showUpdatingModal(); showUpdatingModal();
request request
.put(`${config.apiPrefix}system/update`) .put(`${config.apiPrefix}system/update`)
@@ -103,6 +102,7 @@ const CheckUpdate = ({ socketMessage, systemInfo }: any) => {
width: 600, width: 600,
maskClosable: false, maskClosable: false,
closable: false, closable: false,
keyboard: false,
okButtonProps: { disabled: true }, okButtonProps: { disabled: true },
title: '更新中...', title: '更新中...',
centered: true, centered: true,
-8
View File
@@ -27,11 +27,3 @@
} }
} }
} }
.ql-setting-container {
.ant-tabs-content-holder {
max-height: calc(100vh - 114px);
max-height: calc(100vh - var(--vh-offset, 114px));
overflow-y: auto;
}
}
+3 -12
View File
@@ -49,13 +49,11 @@ const Setting = () => {
title: '名称', title: '名称',
dataIndex: 'name', dataIndex: 'name',
key: 'name', key: 'name',
align: 'center' as const,
}, },
{ {
title: 'Client ID', title: 'Client ID',
dataIndex: 'client_id', dataIndex: 'client_id',
key: 'client_id', key: 'client_id',
align: 'center' as const,
render: (text: string, record: any) => { render: (text: string, record: any) => {
return <Text copyable>{record.client_id}</Text>; return <Text copyable>{record.client_id}</Text>;
}, },
@@ -64,7 +62,6 @@ const Setting = () => {
title: 'Client Secret', title: 'Client Secret',
dataIndex: 'client_secret', dataIndex: 'client_secret',
key: 'client_secret', key: 'client_secret',
align: 'center' as const,
render: (text: string, record: any) => { render: (text: string, record: any) => {
return <Text copyable={{ text: record.client_secret }}>*******</Text>; return <Text copyable={{ text: record.client_secret }}>*******</Text>;
}, },
@@ -73,22 +70,16 @@ const Setting = () => {
title: '权限', title: '权限',
dataIndex: 'scopes', dataIndex: 'scopes',
key: 'scopes', key: 'scopes',
align: 'center' as const,
width: '40%', width: '40%',
render: (text: string, record: any) => { render: (text: string, record: any) => {
return ( return record.scopes.map((scope: any) => {
<div style={{ textAlign: 'left' }}> return <Tag key={scope}>{(config.scopesMap as any)[scope]}</Tag>;
{record.scopes.map((scope: any) => { });
return <Tag key={scope}>{(config.scopesMap as any)[scope]}</Tag>;
})}
</div>
);
}, },
}, },
{ {
title: '操作', title: '操作',
key: 'action', key: 'action',
align: 'center' as const,
render: (text: string, record: any, index: number) => { render: (text: string, record: any, index: number) => {
const isPc = !isPhone; const isPc = !isPhone;
return ( return (
+3 -6
View File
@@ -18,7 +18,6 @@ enum LoginStatusColor {
const columns = [ const columns = [
{ {
title: '序号', title: '序号',
align: 'center' as const,
width: 50, width: 50,
render: (text: string, record: any, index: number) => { render: (text: string, record: any, index: number) => {
return index + 1; return index + 1;
@@ -28,7 +27,6 @@ const columns = [
title: '登录时间', title: '登录时间',
dataIndex: 'timestamp', dataIndex: 'timestamp',
key: 'timestamp', key: 'timestamp',
align: 'center' as const,
render: (text: string, record: any) => { render: (text: string, record: any) => {
return new Date(record.timestamp).toLocaleString(); return new Date(record.timestamp).toLocaleString();
}, },
@@ -37,25 +35,23 @@ const columns = [
title: '登录地址', title: '登录地址',
dataIndex: 'address', dataIndex: 'address',
key: 'address', key: 'address',
align: 'center' as const,
}, },
{ {
title: '登录IP', title: '登录IP',
dataIndex: 'ip', dataIndex: 'ip',
key: 'ip', key: 'ip',
align: 'center' as const,
}, },
{ {
title: '登录设备', title: '登录设备',
dataIndex: 'platform', dataIndex: 'platform',
key: 'platform', key: 'platform',
align: 'center' as const, width: 80,
}, },
{ {
title: '登录状态', title: '登录状态',
dataIndex: 'status', dataIndex: 'status',
key: 'status', key: 'status',
align: 'center' as const, width: 80,
render: (text: string, record: any) => { render: (text: string, record: any) => {
return ( return (
<Tag color={LoginStatusColor[record.status]} style={{ marginRight: 0 }}> <Tag color={LoginStatusColor[record.status]} style={{ marginRight: 0 }}>
@@ -76,6 +72,7 @@ const LoginLog = ({ data }: any) => {
rowKey="id" rowKey="id"
size="middle" size="middle"
scroll={{ x: 768 }} scroll={{ x: 768 }}
sticky
/> />
</> </>
); );
+9
View File
@@ -0,0 +1,9 @@
.inline-form-item {
margin-bottom: 0;
.ant-form-item {
display: inline-block;
width: 50%;
margin-bottom: 0px;
}
}
-8
View File
@@ -68,7 +68,6 @@ const Subscription = () => {
dataIndex: 'name', dataIndex: 'name',
key: 'name', key: 'name',
width: 150, width: 150,
align: 'center' as const,
sorter: { sorter: {
compare: (a: any, b: any) => a.name.localeCompare(b.name), compare: (a: any, b: any) => a.name.localeCompare(b.name),
multiple: 2, multiple: 2,
@@ -78,7 +77,6 @@ const Subscription = () => {
title: '链接', title: '链接',
dataIndex: 'url', dataIndex: 'url',
key: 'url', key: 'url',
align: 'center' as const,
sorter: { sorter: {
compare: (a: any, b: any) => a.name.localeCompare(b.name), compare: (a: any, b: any) => a.name.localeCompare(b.name),
multiple: 2, multiple: 2,
@@ -89,7 +87,6 @@ const Subscription = () => {
style={{ style={{
wordBreak: 'break-all', wordBreak: 'break-all',
marginBottom: 0, marginBottom: 0,
textAlign: 'left',
}} }}
ellipsis={{ tooltip: text, rows: 2 }} ellipsis={{ tooltip: text, rows: 2 }}
> >
@@ -103,7 +100,6 @@ const Subscription = () => {
dataIndex: 'type', dataIndex: 'type',
key: 'type', key: 'type',
width: 130, width: 130,
align: 'center' as const,
render: (text: string, record: any) => { render: (text: string, record: any) => {
return (SubscriptionType as any)[record.type]; return (SubscriptionType as any)[record.type];
}, },
@@ -113,7 +109,6 @@ const Subscription = () => {
dataIndex: 'branch', dataIndex: 'branch',
key: 'branch', key: 'branch',
width: 130, width: 130,
align: 'center' as const,
render: (text: string, record: any) => { render: (text: string, record: any) => {
return record.branch || '-'; return record.branch || '-';
}, },
@@ -121,7 +116,6 @@ const Subscription = () => {
{ {
title: '定时规则', title: '定时规则',
width: 180, width: 180,
align: 'center' as const,
render: (text: string, record: any) => { render: (text: string, record: any) => {
if (record.schedule_type === 'interval') { if (record.schedule_type === 'interval') {
const { type, value } = record.interval_schedule; const { type, value } = record.interval_schedule;
@@ -134,7 +128,6 @@ const Subscription = () => {
title: '状态', title: '状态',
key: 'status', key: 'status',
dataIndex: 'status', dataIndex: 'status',
align: 'center' as const,
width: 110, width: 110,
filters: [ filters: [
{ {
@@ -189,7 +182,6 @@ const Subscription = () => {
{ {
title: '操作', title: '操作',
key: 'action', key: 'action',
align: 'center' as const,
width: 130, width: 130,
render: (text: string, record: any, index: number) => { render: (text: string, record: any, index: number) => {
const isPc = !isPhone; const isPc = !isPhone;
+55 -11
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);
} }
@@ -228,8 +243,17 @@ const SubscriptionModal = ({
if (text.startsWith('ql ')) { if (text.startsWith('ql ')) {
e.preventDefault(); 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(() => { useEffect(() => {
if (visible) { if (visible) {
window.addEventListener('paste', onPaste); window.addEventListener('paste', onPaste);
@@ -239,7 +263,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');
@@ -269,9 +295,9 @@ const SubscriptionModal = ({
confirmLoading={loading} confirmLoading={loading}
> >
<Form form={form} name="form_in_modal" layout="vertical"> <Form form={form} name="form_in_modal" layout="vertical">
<Form.Item name="name" label="名称"> <Form.Item name="name" label="名称" rules={[{ required: true }]}>
<Input <Input
placeholder="支持拷贝ql repo/raw命令,粘贴导入" placeholder="支持拷贝 ql repo/raw 命令,粘贴导入"
onPaste={onNamePaste} onPaste={onNamePaste}
/> />
</Form.Item> </Form.Item>
@@ -452,6 +478,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>
); );
+1 -1
View File
@@ -1 +1 @@
export const LOG_END_SYMBOL = '\n          '; export const LOG_END_SYMBOL = '     ';
+39 -4
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() {
// 权重:系统 + 系统版本 > 平台 > 内核 + 载体 + 内核版本 + 载体版本 > 外壳 + 外壳版本 // 权重:系统 + 系统版本 > 平台 > 内核 + 载体 + 内核版本 + 载体版本 > 外壳 + 外壳版本
@@ -152,9 +153,9 @@ export default function browserType() {
shell === 'none' shell === 'none'
? {} ? {}
: { : {
shell, // wechat qq uc 360 2345 sougou liebao maxthon shell, // wechat qq uc 360 2345 sougou liebao maxthon
shellVs, shellVs,
}, },
); );
console.log( console.log(
@@ -182,7 +183,7 @@ export function getTableScroll({
extraHeight, extraHeight,
target, target,
}: { extraHeight?: number; target?: HTMLElement } = {}) { }: { extraHeight?: number; target?: HTMLElement } = {}) {
if (typeof extraHeight == 'undefined') { if (typeof extraHeight === 'undefined') {
// 47 + 40 + 12 // 47 + 40 + 12
extraHeight = 99; extraHeight = 99;
} }
@@ -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));
@@ -303,3 +327,14 @@ export function getCommandScript(
} }
return [s, 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');
}
+3 -2
View File
@@ -1,7 +1,6 @@
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
@@ -27,7 +26,9 @@ export function init(version: string) {
// monaco 编辑器配置cdn和locale // monaco 编辑器配置cdn和locale
loader.config({ loader.config({
monaco, paths: {
vs: 'https://cdn.staticfile.org/monaco-editor/0.33.0/min/vs',
},
'vs/nls': { 'vs/nls': {
availableLanguages: { availableLanguages: {
'*': 'zh-cn', '*': 'zh-cn',
+8 -7
View File
@@ -1,8 +1,9 @@
version: 2.15.6 version: 2.15.11
changeLogLink: https://t.me/jiao_long/358 changeLogLink: https://t.me/jiao_long/366
changeLog: | changeLog: |
1. 修复定时任务数据量大时卡顿,丝滑滚动 1. 优化手机端操作,更丝滑
2. 修复查看运行中任务日志时自动向下滚动 2. 修复 python Server酱 推送
3. 支持SMTP邮件通知,感谢 https://github.com/WankkoReehttps://github.com/catlair 3. 修改初始化依赖安装逻辑
4. 修改资源通知逻辑 4. 修复删除视图筛选条件
5. 其他bug修复 5. 修改定时任务和环境变量列表滚动
6. 其他优化