Compare commits

...
41 Commits
Author SHA1 Message Date
whyour db564d1add 更新版本 v2.15.12 2023-04-18 15:38:06 +08:00
whyour 4f422c0658 修改 dockerfile LANG 环境变量 2023-04-18 15:37:43 +08:00
whyour f267720af4 修复初始化定时服务运行非禁用任务 2023-04-17 13:36:37 +08:00
whyour 4a0c66bcc0 修复初始化 ssh 配置文件不存在 2023-04-15 12:14:56 +08:00
whyour ef7283a9fd 修改依赖安装初始状态 2023-04-12 00:45:06 +08:00
whyour 4498449eea 修复容器启动逻辑 2023-04-11 14:15:34 +08:00
whyour ebb9676a51 修复定时服务启动路径 2023-04-08 21:58:34 +08:00
whyour 6fb39ce835 重构六位定时任务服务 2023-04-08 17:07:06 +08:00
whyour 59c26d90d3 修改手机端网页样式 2023-04-06 21:49:42 +08:00
whyour 1d2df860e8 修复 ssh 配置文件目录 2023-04-06 14:23:02 +08:00
whyour ce22cad5b2 修改列表页更多菜单弹窗 2023-04-06 14:08:08 +08:00
whyour dc44ce6b1a 修复 workflow pnpm 版本 2023-04-06 13:41:18 +08:00
whyour b27ee23cc3 重构私有仓库ssh配置逻辑 2023-04-06 13:38:55 +08:00
whyour 083c8869aa 修复更新环境变量、定时任务、订阅等操作 2023-04-06 13:00:28 +08:00
whyour 6a971a0d6e 更新 readme 2023-04-02 14:49:30 +08:00
whyour a25bfb6912 修复定时任务状态筛选 2023-04-02 14:42:18 +08:00
whyour f19dd21155 更新 readme 2023-04-01 21:05:21 +08:00
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
55 changed files with 4005 additions and 3719 deletions
+2 -2
View File
@@ -8,7 +8,7 @@
<div align="center">
Timed task management panel with python3, javaScript, shell, typescript support
Timed task management platform supporting Python3, JavaScript, Shell, Typescript
[![docker version][docker-version-image]][docker-version-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker stars][docker-stars-image]][docker-stars-url] [![docker image size][docker-image-size-image]][docker-image-size-url]
@@ -22,7 +22,7 @@ Timed task management panel with python3, javaScript, shell, typescript support
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
</div>
[![](https://user-images.githubusercontent.com/22700758/203243067-1a8a570d-b1b4-4837-9f12-d78d83e31f35.jpg)](https://whyour.cn)
[![](https://user-images.githubusercontent.com/22700758/229290661-03aabe84-8780-4ef0-8e75-2146f4636130.jpeg)](https://whyour.cn)
[简体中文](./README.md) | English
+2 -3
View File
@@ -8,7 +8,7 @@
<div align="center">
支持python3、javaScript、shell、typescript 的定时任务管理面板
支持 Python3、JavaScript、Shell、Typescript 的定时任务管理平台
[![docker version][docker-version-image]][docker-version-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker stars][docker-stars-image]][docker-stars-url] [![docker image size][docker-image-size-image]][docker-image-size-url]
@@ -22,7 +22,7 @@
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
</div>
[![](https://user-images.githubusercontent.com/22700758/203243067-1a8a570d-b1b4-4837-9f12-d78d83e31f35.jpg)](https://whyour.cn)
[![](https://user-images.githubusercontent.com/22700758/229290661-03aabe84-8780-4ef0-8e75-2146f4636130.jpeg)](https://whyour.cn)
简体中文 | [English](./README-en.md)
@@ -73,7 +73,6 @@ sudo curl -sSL get.docker.com | sh
```
2. 配置国内镜像源
Configure domestic mirror sources
```bash
mkdir -p /etc/docker
+2
View File
@@ -27,6 +27,7 @@ const bakPath = path.join(dataPath, 'bak/');
const logPath = path.join(dataPath, 'log/');
const dbPath = path.join(dataPath, 'db/');
const uploadPath = path.join(dataPath, 'upload/');
const sshdPath = path.join(dataPath, 'ssh.d/');
const envFile = path.join(configPath, 'env.sh');
const confFile = path.join(configPath, 'config.sh');
@@ -95,4 +96,5 @@ export default {
versionFile,
lastVersionFile,
sqliteFile,
sshdPath,
};
+1
View File
@@ -28,6 +28,7 @@ export enum DependenceStatus {
'removing',
'removed',
'removeFailed',
'queued',
}
export enum DependenceTypes {
+7 -10
View File
@@ -4,8 +4,7 @@ import { Container } from 'typedi';
import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
import CronService from '../services/cron';
import EnvService from '../services/env';
import groupBy from 'lodash/groupBy';
import { DependenceModel } from '../data/dependence';
import { DependenceModel, DependenceStatus } from '../data/dependence';
import { Op } from 'sequelize';
import config from '../config';
import { CrontabViewModel, CronViewType } from '../data/cronView';
@@ -42,16 +41,14 @@ export default async () => {
order: [['type', 'DESC']],
raw: true,
}).then(async (docs) => {
const groups = groupBy(docs, 'type');
const keys = Object.keys(groups).sort((a, b) => parseInt(b) - parseInt(a));
for (const key of keys) {
const group = groups[key];
const depIds = group.map((x) => x.id);
await dependenceService.reInstall(depIds as number[]);
}
await DependenceModel.update(
{ status: DependenceStatus.queued, log: [] },
{ where: { id: docs.map((x) => x.id!) } },
);
dependenceService.installDependenceOneByOne(docs);
});
// 初始化时执行一次所有的ql repo 任务
// 初始化时执行一次所有的 ql repo 任务
CrontabModel.findAll({
where: {
isDisabled: { [Op.ne]: 1 },
+6
View File
@@ -19,6 +19,7 @@ const sampleConfigFile = path.join(samplePath, 'config.sample.sh');
const sampleAuthFile = path.join(samplePath, 'auth.sample.json');
const homedir = os.homedir();
const sshPath = path.resolve(homedir, '.ssh');
const sshdPath = path.join(dataPath, 'ssh.d');
export default async () => {
const authFileExist = await fileExist(authConfigFile);
@@ -29,6 +30,7 @@ export default async () => {
const uploadDirExist = await fileExist(uploadPath);
const sshDirExist = await fileExist(sshPath);
const bakDirExist = await fileExist(bakPath);
const sshdDirExist = await fileExist(sshdPath);
if (!configDirExist) {
fs.mkdirSync(configPath);
@@ -62,6 +64,10 @@ export default async () => {
fs.mkdirSync(bakPath);
}
if (!sshdDirExist) {
fs.mkdirSync(sshdPath);
}
dotenv.config({ path: confFile });
Logger.info('✌️ Init file down');
+2 -1
View File
@@ -11,7 +11,7 @@ export default async () => {
const subscriptionService = Container.get(SubscriptionService);
// 生成内置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`;
if (await fileExist(tokenFile)) {
tokenCommand = `node ${tokenFile}`;
@@ -41,6 +41,7 @@ export default async () => {
}
// 运行所有订阅
await subscriptionService.setSshConfig();
const subs = await subscriptionService.list();
for (const sub of subs) {
subscriptionService.handleTask(sub, !sub.is_disabled, !sub.is_disabled);
+22
View File
@@ -0,0 +1,22 @@
syntax = "proto3";
package com.ql.cron;
service CronService {
rpc addCron(AddCronRequest) returns (AddCronResponse);
rpc delCron(DeleteCronRequest) returns (DeleteCronResponse);
}
message Cron {
string id = 1;
string schedule = 2;
string command = 3;
}
message AddCronRequest { repeated Cron crons = 1; }
message AddCronResponse {}
message DeleteCronRequest { repeated string ids = 1; }
message DeleteCronResponse {}
+482
View File
@@ -0,0 +1,482 @@
/* eslint-disable */
import {
CallOptions,
ChannelCredentials,
Client,
ClientOptions,
ClientUnaryCall,
handleUnaryCall,
makeGenericClientConstructor,
Metadata,
ServiceError,
UntypedServiceImplementation,
} from '@grpc/grpc-js';
import _m0 from 'protobufjs/minimal';
export const protobufPackage = 'com.ql.cron';
export interface Cron {
id: string;
schedule: string;
command: string;
}
export interface AddCronRequest {
crons: Cron[];
}
export interface AddCronResponse {}
export interface DeleteCronRequest {
ids: string[];
}
export interface DeleteCronResponse {}
function createBaseCron(): Cron {
return { id: '', schedule: '', command: '' };
}
export const Cron = {
encode(message: Cron, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.id !== '') {
writer.uint32(10).string(message.id);
}
if (message.schedule !== '') {
writer.uint32(18).string(message.schedule);
}
if (message.command !== '') {
writer.uint32(26).string(message.command);
}
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): Cron {
const reader =
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseCron();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag != 10) {
break;
}
message.id = reader.string();
continue;
case 2:
if (tag != 18) {
break;
}
message.schedule = reader.string();
continue;
case 3:
if (tag != 26) {
break;
}
message.command = reader.string();
continue;
}
if ((tag & 7) == 4 || tag == 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
fromJSON(object: any): Cron {
return {
id: isSet(object.id) ? String(object.id) : '',
schedule: isSet(object.schedule) ? String(object.schedule) : '',
command: isSet(object.command) ? String(object.command) : '',
};
},
toJSON(message: Cron): unknown {
const obj: any = {};
message.id !== undefined && (obj.id = message.id);
message.schedule !== undefined && (obj.schedule = message.schedule);
message.command !== undefined && (obj.command = message.command);
return obj;
},
create<I extends Exact<DeepPartial<Cron>, I>>(base?: I): Cron {
return Cron.fromPartial(base ?? {});
},
fromPartial<I extends Exact<DeepPartial<Cron>, I>>(object: I): Cron {
const message = createBaseCron();
message.id = object.id ?? '';
message.schedule = object.schedule ?? '';
message.command = object.command ?? '';
return message;
},
};
function createBaseAddCronRequest(): AddCronRequest {
return { crons: [] };
}
export const AddCronRequest = {
encode(
message: AddCronRequest,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
for (const v of message.crons) {
Cron.encode(v!, writer.uint32(10).fork()).ldelim();
}
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): AddCronRequest {
const reader =
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseAddCronRequest();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag != 10) {
break;
}
message.crons.push(Cron.decode(reader, reader.uint32()));
continue;
}
if ((tag & 7) == 4 || tag == 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
fromJSON(object: any): AddCronRequest {
return {
crons: Array.isArray(object?.crons)
? object.crons.map((e: any) => Cron.fromJSON(e))
: [],
};
},
toJSON(message: AddCronRequest): unknown {
const obj: any = {};
if (message.crons) {
obj.crons = message.crons.map((e) => (e ? Cron.toJSON(e) : undefined));
} else {
obj.crons = [];
}
return obj;
},
create<I extends Exact<DeepPartial<AddCronRequest>, I>>(
base?: I,
): AddCronRequest {
return AddCronRequest.fromPartial(base ?? {});
},
fromPartial<I extends Exact<DeepPartial<AddCronRequest>, I>>(
object: I,
): AddCronRequest {
const message = createBaseAddCronRequest();
message.crons = object.crons?.map((e) => Cron.fromPartial(e)) || [];
return message;
},
};
function createBaseAddCronResponse(): AddCronResponse {
return {};
}
export const AddCronResponse = {
encode(
_: AddCronResponse,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): AddCronResponse {
const reader =
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseAddCronResponse();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
}
if ((tag & 7) == 4 || tag == 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
fromJSON(_: any): AddCronResponse {
return {};
},
toJSON(_: AddCronResponse): unknown {
const obj: any = {};
return obj;
},
create<I extends Exact<DeepPartial<AddCronResponse>, I>>(
base?: I,
): AddCronResponse {
return AddCronResponse.fromPartial(base ?? {});
},
fromPartial<I extends Exact<DeepPartial<AddCronResponse>, I>>(
_: I,
): AddCronResponse {
const message = createBaseAddCronResponse();
return message;
},
};
function createBaseDeleteCronRequest(): DeleteCronRequest {
return { ids: [] };
}
export const DeleteCronRequest = {
encode(
message: DeleteCronRequest,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
for (const v of message.ids) {
writer.uint32(10).string(v!);
}
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): DeleteCronRequest {
const reader =
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseDeleteCronRequest();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag != 10) {
break;
}
message.ids.push(reader.string());
continue;
}
if ((tag & 7) == 4 || tag == 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
fromJSON(object: any): DeleteCronRequest {
return {
ids: Array.isArray(object?.ids)
? object.ids.map((e: any) => String(e))
: [],
};
},
toJSON(message: DeleteCronRequest): unknown {
const obj: any = {};
if (message.ids) {
obj.ids = message.ids.map((e) => e);
} else {
obj.ids = [];
}
return obj;
},
create<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(
base?: I,
): DeleteCronRequest {
return DeleteCronRequest.fromPartial(base ?? {});
},
fromPartial<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(
object: I,
): DeleteCronRequest {
const message = createBaseDeleteCronRequest();
message.ids = object.ids?.map((e) => e) || [];
return message;
},
};
function createBaseDeleteCronResponse(): DeleteCronResponse {
return {};
}
export const DeleteCronResponse = {
encode(
_: DeleteCronResponse,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): DeleteCronResponse {
const reader =
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseDeleteCronResponse();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
}
if ((tag & 7) == 4 || tag == 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
fromJSON(_: any): DeleteCronResponse {
return {};
},
toJSON(_: DeleteCronResponse): unknown {
const obj: any = {};
return obj;
},
create<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(
base?: I,
): DeleteCronResponse {
return DeleteCronResponse.fromPartial(base ?? {});
},
fromPartial<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(
_: I,
): DeleteCronResponse {
const message = createBaseDeleteCronResponse();
return message;
},
};
export type CronServiceService = typeof CronServiceService;
export const CronServiceService = {
addCron: {
path: '/com.ql.cron.CronService/addCron',
requestStream: false,
responseStream: false,
requestSerialize: (value: AddCronRequest) =>
Buffer.from(AddCronRequest.encode(value).finish()),
requestDeserialize: (value: Buffer) => AddCronRequest.decode(value),
responseSerialize: (value: AddCronResponse) =>
Buffer.from(AddCronResponse.encode(value).finish()),
responseDeserialize: (value: Buffer) => AddCronResponse.decode(value),
},
delCron: {
path: '/com.ql.cron.CronService/delCron',
requestStream: false,
responseStream: false,
requestSerialize: (value: DeleteCronRequest) =>
Buffer.from(DeleteCronRequest.encode(value).finish()),
requestDeserialize: (value: Buffer) => DeleteCronRequest.decode(value),
responseSerialize: (value: DeleteCronResponse) =>
Buffer.from(DeleteCronResponse.encode(value).finish()),
responseDeserialize: (value: Buffer) => DeleteCronResponse.decode(value),
},
} as const;
export interface CronServiceServer extends UntypedServiceImplementation {
addCron: handleUnaryCall<AddCronRequest, AddCronResponse>;
delCron: handleUnaryCall<DeleteCronRequest, DeleteCronResponse>;
}
export interface CronServiceClient extends Client {
addCron(
request: AddCronRequest,
callback: (error: ServiceError | null, response: AddCronResponse) => void,
): ClientUnaryCall;
addCron(
request: AddCronRequest,
metadata: Metadata,
callback: (error: ServiceError | null, response: AddCronResponse) => void,
): ClientUnaryCall;
addCron(
request: AddCronRequest,
metadata: Metadata,
options: Partial<CallOptions>,
callback: (error: ServiceError | null, response: AddCronResponse) => void,
): ClientUnaryCall;
delCron(
request: DeleteCronRequest,
callback: (
error: ServiceError | null,
response: DeleteCronResponse,
) => void,
): ClientUnaryCall;
delCron(
request: DeleteCronRequest,
metadata: Metadata,
callback: (
error: ServiceError | null,
response: DeleteCronResponse,
) => void,
): ClientUnaryCall;
delCron(
request: DeleteCronRequest,
metadata: Metadata,
options: Partial<CallOptions>,
callback: (
error: ServiceError | null,
response: DeleteCronResponse,
) => void,
): ClientUnaryCall;
}
export const CronServiceClient = makeGenericClientConstructor(
CronServiceService,
'com.ql.cron.CronService',
) as unknown as {
new (
address: string,
credentials: ChannelCredentials,
options?: Partial<ClientOptions>,
): CronServiceClient;
service: typeof CronServiceService;
};
type Builtin =
| Date
| Function
| Uint8Array
| string
| number
| boolean
| undefined;
export type DeepPartial<T> = T extends Builtin
? T
: T extends Array<infer U>
? Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U>
? ReadonlyArray<DeepPartial<U>>
: T extends {}
? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>;
type KeysOfUnion<T> = T extends T ? keyof T : never;
export type Exact<P, I extends P> = P extends Builtin
? P
: P & { [K in keyof P]: Exact<P[K], I[K]> } & {
[K in Exclude<keyof I, KeysOfUnion<P>>]: never;
};
function isSet(value: any): boolean {
return value !== null && value !== undefined;
}
-56
View File
@@ -1,56 +0,0 @@
import schedule from 'node-schedule';
import express from 'express';
import { exec } from 'child_process';
import Logger from './loaders/logger';
import { CrontabModel, CrontabStatus } from './data/cron';
import config from './config';
import { QL_PREFIX, TASK_PREFIX } from './config/const';
const app = express();
const run = async () => {
CrontabModel.findAll({ where: {} })
.then((docs) => {
if (docs && docs.length > 0) {
for (let i = 0; i < docs.length; i++) {
const task = docs[i];
const _schedule = task.schedule && task.schedule.split(/ +/);
if (
_schedule &&
_schedule.length > 5 &&
task.status !== CrontabStatus.disabled &&
!task.isDisabled &&
task.schedule
) {
schedule.scheduleJob(task.schedule, function () {
let command = task.command as string;
if (
!command.startsWith(TASK_PREFIX) &&
!command.startsWith(QL_PREFIX)
) {
command = `${TASK_PREFIX}${command}`;
}
exec(`ID=${task.id} ${command}`);
});
}
}
}
})
.catch((err) => {
Logger.error(err);
process.exit(1);
});
};
app
.listen(config.cronPort, async () => {
await require('./loaders/sentry').default({ expressApp: app });
await require('./loaders/db').default();
await run();
Logger.debug('定时任务服务启动成功!');
})
.on('error', (err) => {
Logger.error(err);
process.exit(1);
});
+27
View File
@@ -0,0 +1,27 @@
import { ServerUnaryCall, sendUnaryData } from '@grpc/grpc-js';
import { AddCronRequest, AddCronResponse } from '../protos/cron';
import nodeSchedule from 'node-schedule';
import { scheduleStacks } from './data';
import { exec } from 'child_process';
const addCron = (
call: ServerUnaryCall<AddCronRequest, AddCronResponse>,
callback: sendUnaryData<AddCronResponse>,
) => {
for (const item of call.request.crons) {
const { id, schedule, command } = item;
if (scheduleStacks.has(id)) {
scheduleStacks.get(id)?.cancel();
}
scheduleStacks.set(
id,
nodeSchedule.scheduleJob(id, schedule, async () => {
exec(`ID=${id} ${command}`);
}),
);
}
callback(null, null);
};
export { addCron };
+40
View File
@@ -0,0 +1,40 @@
import { credentials } from '@grpc/grpc-js';
import {
AddCronRequest,
AddCronResponse,
CronServiceClient,
DeleteCronRequest,
DeleteCronResponse,
} from '../protos/cron';
import config from '../config';
class Client {
private client = new CronServiceClient(
`localhost:${config.cronPort}`,
credentials.createInsecure(),
);
addCron(request: AddCronRequest['crons']): Promise<AddCronResponse> {
return new Promise((resolve, reject) => {
this.client.addCron({ crons: request }, (err, res) => {
if (err) {
reject(err);
}
resolve(res);
});
});
}
delCron(request: DeleteCronRequest['ids']): Promise<DeleteCronResponse> {
return new Promise((resolve, reject) => {
this.client.delCron({ ids: request }, (err, res) => {
if (err) {
reject(err);
}
resolve(res);
});
});
}
}
export default new Client();
+6
View File
@@ -0,0 +1,6 @@
import nodeSchedule from 'node-schedule';
import { ToadScheduler } from 'toad-scheduler';
export const scheduleStacks = new Map<string, nodeSchedule.Job>();
export const intervalSchedule = new ToadScheduler();
+19
View File
@@ -0,0 +1,19 @@
import { ServerUnaryCall, sendUnaryData } from '@grpc/grpc-js';
import { DeleteCronRequest, DeleteCronResponse } from '../protos/cron';
import { scheduleStacks } from './data';
const delCron = (
call: ServerUnaryCall<DeleteCronRequest, DeleteCronResponse>,
callback: sendUnaryData<DeleteCronResponse>,
) => {
for (const id of call.request.ids) {
if (scheduleStacks.has(id)) {
scheduleStacks.get(id)?.cancel();
scheduleStacks.delete(id);
}
}
callback(null, null);
};
export { delCron };
+17
View File
@@ -0,0 +1,17 @@
import { Server, ServerCredentials } from '@grpc/grpc-js';
import { CronServiceService } from '../protos/cron';
import { addCron } from './addCron';
import { delCron } from './delCron';
import config from '../config';
import Logger from '../loaders/logger';
const server = new Server();
server.addService(CronServiceService, { addCron, delCron });
server.bindAsync(
`localhost:${config.cronPort}`,
ServerCredentials.createInsecure(),
() => {
server.start();
Logger.debug(`✌️ 定时服务启动成功!`);
},
);
+63 -13
View File
@@ -12,13 +12,14 @@ import {
killTask,
} from '../config/util';
import { promises, existsSync } from 'fs';
import { Op, where, col as colFn } from 'sequelize';
import { Op, where, col as colFn, FindOptions } from 'sequelize';
import path from 'path';
import { TASK_PREFIX, QL_PREFIX } from '../config/const';
import cronClient from '../schedule/client';
@Service()
export default class CronService {
constructor(@Inject('logger') private logger: winston.Logger) {}
constructor(@Inject('logger') private logger: winston.Logger) { }
private isSixCron(cron: Crontab) {
const { schedule } = cron;
@@ -32,6 +33,11 @@ export default class CronService {
const tab = new Crontab(payload);
tab.saved = false;
const doc = await this.insert(tab);
if (this.isSixCron(doc)) {
await cronClient.addCron([
{ id: String(doc.id), schedule: doc.schedule!, command: doc.command },
]);
}
await this.set_crontab();
return doc;
}
@@ -41,9 +47,25 @@ export default class CronService {
}
public async update(payload: Crontab): Promise<Crontab> {
const tab = new Crontab(payload);
const doc = await this.getDb({ id: payload.id });
const tab = new Crontab({ ...doc, ...payload });
tab.saved = false;
const newDoc = await this.updateDb(tab);
if (doc.isDisabled === 1) {
return newDoc;
}
if (this.isSixCron(doc)) {
await cronClient.delCron([String(newDoc.id)]);
}
if (this.isSixCron(newDoc)) {
await cronClient.addCron([
{
id: String(newDoc.id),
schedule: newDoc.schedule!,
command: newDoc.command,
},
]);
}
await this.set_crontab();
return newDoc;
}
@@ -83,6 +105,7 @@ export default class CronService {
public async remove(ids: number[]) {
await CrontabModel.destroy({ where: { id: ids } });
await cronClient.delCron(ids.map(String));
await this.set_crontab();
}
@@ -246,8 +269,12 @@ export default class CronService {
for (const key of filterKeys) {
let q: any = {};
if (!filterQuery[key]) continue;
if (key === 'status' && filterQuery[key].includes(2)) {
q = { [Op.or]: [{ [key]: filterQuery[key] }, { isDisabled: 1 }] };
if (key === 'status') {
if (filterQuery[key].includes(2)) {
q = { [Op.or]: [{ [key]: filterQuery[key] }, { isDisabled: 1 }] };
} else {
q = { [Op.and]: [{ [key]: filterQuery[key] }, { isDisabled: 0 }] };
}
} else {
q[key] = filterQuery[key];
}
@@ -264,9 +291,13 @@ export default class CronService {
}
}
public async find(params: { log_path: string }): Promise<Crontab | null> {
public async find({
log_path,
}: {
log_path: string;
}): Promise<Crontab | null> {
try {
const result = await CrontabModel.findOne({ where: { ...params } });
const result = await CrontabModel.findOne({ where: { log_path } });
return result;
} catch (error) {
throw error;
@@ -324,7 +355,7 @@ export default class CronService {
}
}
public async getDb(query: any): Promise<Crontab> {
public async getDb(query: FindOptions<Crontab>['where']): Promise<Crontab> {
const doc: any = await CrontabModel.findOne({ where: { ...query } });
return doc && (doc.get({ plain: true }) as Crontab);
}
@@ -422,11 +453,21 @@ export default class CronService {
public async disabled(ids: number[]) {
await CrontabModel.update({ isDisabled: 1 }, { where: { id: ids } });
await cronClient.delCron(ids.map(String));
await this.set_crontab();
}
public async enabled(ids: number[]) {
await CrontabModel.update({ isDisabled: 0 }, { where: { id: ids } });
const docs = await CrontabModel.findAll({ where: { id: ids } });
const sixCron = docs
.filter((x) => this.isSixCron(x))
.map((doc) => ({
id: String(doc.id),
schedule: doc.schedule!,
command: doc.command,
}));
await cronClient.addCron(sixCron);
await this.set_crontab();
}
@@ -478,8 +519,8 @@ export default class CronService {
return crontab_job_string;
}
private async set_crontab() {
const tabs = await this.crontabs();
private async set_crontab(data?: { data: Crontab[]; total: number }) {
const tabs = data ?? (await this.crontabs());
var crontab_string = '';
tabs.data.forEach((tab) => {
const _schedule = tab.schedule && tab.schedule.split(/ +/);
@@ -501,7 +542,6 @@ export default class CronService {
fs.writeFileSync(config.crontabFile, crontab_string);
execSync(`crontab ${config.crontabFile}`);
exec(`pm2 reload schedule`);
await CrontabModel.update({ saved: true }, { where: {} });
}
@@ -539,7 +579,17 @@ export default class CronService {
});
}
public autosave_crontab() {
return this.set_crontab();
public async autosave_crontab() {
const tabs = await this.crontabs();
this.set_crontab(tabs);
const sixCron = tabs.data
.filter((x) => this.isSixCron(x) && x.isDisabled !== 1)
.map((doc) => ({
id: String(doc.id),
schedule: doc.schedule!,
command: doc.command,
}));
await cronClient.addCron(sixCron);
}
}
+5 -2
View File
@@ -7,6 +7,7 @@ import {
minPosition,
stepPosition,
} from '../data/env';
import { FindOptions } from 'sequelize';
@Service()
export default class CronViewService {
@@ -31,7 +32,9 @@ export default class CronViewService {
}
public async update(payload: CrontabView): Promise<CrontabView> {
const newDoc = await this.updateDb(new CrontabView(payload));
const doc = await this.getDb({ id: payload.id })
const tab = new CrontabView({ ...doc, ...payload });
const newDoc = await this.updateDb(tab);
return newDoc;
}
@@ -56,7 +59,7 @@ export default class CronViewService {
}
}
public async getDb(query: any): Promise<CrontabView> {
public async getDb(query: FindOptions<CrontabView>['where']): Promise<CrontabView> {
const doc: any = await CrontabViewModel.findOne({ where: { ...query } });
return doc && (doc.get({ plain: true }) as CrontabView);
}
+20 -11
View File
@@ -11,7 +11,7 @@ import {
} from '../data/dependence';
import { spawn } from 'child_process';
import SockService from './sock';
import { Op } from 'sequelize';
import { FindOptions, Op } from 'sequelize';
import { concurrentRun } from '../config/util';
import dayjs from 'dayjs';
@@ -24,7 +24,7 @@ export default class DependenceService {
public async create(payloads: Dependence[]): Promise<Dependence[]> {
const tabs = payloads.map((x) => {
const tab = new Dependence({ ...x, status: DependenceStatus.installing });
const tab = new Dependence({ ...x, status: DependenceStatus.queued });
return tab;
});
const docs = await this.insert(tabs);
@@ -45,7 +45,7 @@ export default class DependenceService {
const tab = new Dependence({
...doc,
...other,
status: DependenceStatus.installing,
status: DependenceStatus.queued,
});
const newDoc = await this.updateDb(tab);
this.installDependenceOneByOne([newDoc]);
@@ -59,7 +59,7 @@ export default class DependenceService {
public async remove(ids: number[], force = false): Promise<Dependence[]> {
await DependenceModel.update(
{ status: DependenceStatus.removing, log: [] },
{ status: DependenceStatus.queued, log: [] },
{ where: { id: ids } },
);
const docs = await DependenceModel.findAll({ where: { id: ids } });
@@ -99,23 +99,30 @@ export default class DependenceService {
}
}
private installDependenceOneByOne(
public installDependenceOneByOne(
docs: Dependence[],
isInstall: boolean = true,
force: boolean = false,
) {
concurrentRun(
docs.map(
(dep) => async () =>
await this.installOrUninstallDependencies([dep], isInstall, force),
),
docs.map((dep) => async () => {
const status = isInstall
? DependenceStatus.installing
: DependenceStatus.removing;
await DependenceModel.update({ status }, { where: { id: dep.id } });
return await this.installOrUninstallDependencies(
[dep],
isInstall,
force,
);
}),
1,
);
}
public async reInstall(ids: number[]): Promise<Dependence[]> {
await DependenceModel.update(
{ status: DependenceStatus.installing, log: [] },
{ status: DependenceStatus.queued, log: [] },
{ where: { id: ids } },
);
@@ -132,7 +139,9 @@ export default class DependenceService {
return docs;
}
public async getDb(query: any): Promise<Dependence> {
public async getDb(
query: FindOptions<Dependence>['where'],
): Promise<Dependence> {
const doc: any = await DependenceModel.findOne({ where: { ...query } });
return doc && (doc.get({ plain: true }) as Dependence);
}
+5 -3
View File
@@ -12,7 +12,7 @@ import {
stepPosition,
} from '../data/env';
import groupBy from 'lodash/groupBy';
import { Op } from 'sequelize';
import { FindOptions, Op } from 'sequelize';
@Service()
export default class EnvService {
@@ -49,7 +49,9 @@ export default class EnvService {
}
public async update(payload: Env): Promise<Env> {
const newDoc = await this.updateDb(new Env(payload));
const doc = await this.getDb({ id: payload.id })
const tab = new Env({ ...doc, ...payload });
const newDoc = await this.updateDb(tab);
await this.set_envs();
return newDoc;
}
@@ -162,7 +164,7 @@ export default class EnvService {
return docs;
}
public async getDb(query: any): Promise<Env> {
public async getDb(query: FindOptions<Env>['where']): Promise<Env> {
const doc: any = await EnvModel.findOne({ where: { ...query } });
return doc && (doc.get({ plain: true }) as Env);
}
+5 -5
View File
@@ -161,7 +161,7 @@ export default class NotificationService {
this.title,
)}/${encodeURIComponent(
this.content,
)}?icon=${barkIcon}?sound=${barkSound}&group=${barkGroup}`;
)}?icon=${barkIcon}&sound=${barkSound}&group=${barkGroup}`;
const res: any = await got
.get(url, {
...this.gotOption,
@@ -484,11 +484,11 @@ export default class NotificationService {
return {
formatUrl: url
.replaceAll('$title', encodeURIComponent(this.title))
.replaceAll('$content', encodeURIComponent(this.content)),
?.replaceAll('$title', encodeURIComponent(this.title))
?.replaceAll('$content', encodeURIComponent(this.content)),
formatBody: body
.replaceAll('$title', this.title)
.replaceAll('$content', this.content),
?.replaceAll('$title', this.title)
?.replaceAll('$content', this.content),
};
}
}
+4 -1
View File
@@ -138,7 +138,10 @@ export default class ScheduleService {
async cancelCronTask({ id = 0, name }: ScheduleTaskType) {
const _id = this.formatId(id);
this.logger.info('[取消定时任务],任务名:%s', name);
this.scheduleStacks.has(_id) && this.scheduleStacks.get(_id)?.cancel();
if (this.scheduleStacks.has(_id)) {
this.scheduleStacks.get(_id)?.cancel();
this.scheduleStacks.delete(_id);
}
}
async createIntervalTask(
+41 -45
View File
@@ -5,18 +5,38 @@ import os from 'os';
import path from 'path';
import { Subscription } from '../data/subscription';
import { formatUrl } from '../config/subscription';
import config from '../config';
@Service()
export default class SshKeyService {
private homedir = os.homedir();
private sshPath = path.resolve(this.homedir, '.ssh');
private sshConfigFilePath = path.resolve(this.sshPath, 'config');
private sshPath = config.sshdPath;
private sshConfigFilePath = path.resolve(this.homedir, '.ssh', 'config');
private sshConfigHeader = `Include ${path.join(this.sshPath, '*.config')}`;
constructor(@Inject('logger') private logger: winston.Logger) {}
constructor(@Inject('logger') private logger: winston.Logger) {
this.initSshConfigFile();
}
private initSshConfigFile() {
let config = '';
if (existsSync(this.sshConfigFilePath)) {
config = fs.readFileSync(this.sshConfigFilePath, { encoding: 'utf-8' });
} else {
fs.writeFileSync(this.sshConfigFilePath, '');
}
if (!config.includes(this.sshConfigHeader)) {
fs.writeFileSync(
this.sshConfigFilePath,
`${this.sshConfigHeader}\n\n${config}`,
{ encoding: 'utf-8' },
);
}
}
private generatePrivateKeyFile(alias: string, key: string): void {
try {
fs.writeFileSync(`${this.sshPath}/${alias}`, `${key}${os.EOL}`, {
fs.writeFileSync(path.join(this.sshPath, alias), `${key}${os.EOL}`, {
encoding: 'utf8',
mode: '400',
});
@@ -25,56 +45,37 @@ export default class SshKeyService {
}
}
private getConfigRegx(alias: string) {
return new RegExp(
`Host ${alias}\n.*[^StrictHostKeyChecking]*.*[\n]*.*StrictHostKeyChecking no`,
'g',
);
}
private removePrivateKeyFile(alias: string): void {
try {
const filePath = path.join(this.sshPath, alias);
if (existsSync(filePath)) {
fs.unlinkSync(`${this.sshPath}/${alias}`);
fs.unlinkSync(filePath);
}
} catch (error) {
this.logger.error('删除私钥文件失败', error);
}
}
private generateSingleSshConfig(
alias: string,
host: string,
proxy?: string,
): string {
private generateSingleSshConfig(alias: string, host: string, proxy?: string) {
if (host === 'github.com') {
host = `ssh.github.com\n Port 443\n HostkeyAlgorithms +ssh-rsa\n PubkeyAcceptedAlgorithms +ssh-rsa`;
}
const proxyStr = proxy ? ` ProxyCommand nc -v -x ${proxy} %h %p\n` : '';
return `Host ${alias}\n Hostname ${host}\n IdentityFile ${this.sshPath}/${alias}\n StrictHostKeyChecking no\n${proxyStr}`;
}
private generateSshConfig(configs: string[]) {
try {
fs.writeFileSync(this.sshConfigFilePath, configs.join('\n'), {
encoding: 'utf8',
});
} catch (error) {
this.logger.error('写入ssh配置文件失败', error);
}
const config = `Host ${alias}\n Hostname ${host}\n IdentityFile ${path.join(
this.sshPath,
alias,
)}\n StrictHostKeyChecking no\n${proxyStr}`;
fs.writeFileSync(`${path.join(this.sshPath, `${alias}.config`)}`, config, {
encoding: 'utf8',
});
}
private removeSshConfig(alias: string) {
try {
const configRegx = this.getConfigRegx(alias);
const data = fs
.readFileSync(this.sshConfigFilePath, { encoding: 'utf8' })
.replace(configRegx, '')
.replace(/\n[\n]+/g, '\n');
fs.writeFileSync(this.sshConfigFilePath, data, {
encoding: 'utf8',
});
const filePath = path.join(this.sshPath, `${alias}.config`);
if (existsSync(filePath)) {
fs.unlinkSync(filePath);
}
} catch (error) {
this.logger.error(`删除ssh配置文件${alias}失败`, error);
}
@@ -87,32 +88,27 @@ export default class SshKeyService {
proxy?: string,
): void {
this.generatePrivateKeyFile(alias, key);
const config = this.generateSingleSshConfig(alias, host, proxy);
this.removeSshConfig(alias);
this.generateSshConfig([config]);
this.generateSingleSshConfig(alias, host, proxy);
}
public removeSSHKey(alias: string, host: string, proxy?: string): void {
this.removePrivateKeyFile(alias);
const config = this.generateSingleSshConfig(alias, host, proxy);
this.removeSshConfig(config);
this.removeSshConfig(alias);
}
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.removeSshConfig(alias);
this.generatePrivateKeyFile(
alias,
(doc.pull_option as any).private_key,
);
const config = this.generateSingleSshConfig(alias, host, proxy);
result.push(config);
this.generateSingleSshConfig(alias, host, proxy);
}
}
this.generateSshConfig(result);
}
}
+5 -4
View File
@@ -21,7 +21,7 @@ import {
killTask,
} from '../config/util';
import { promises, existsSync } from 'fs';
import { Op } from 'sequelize';
import { FindOptions, Op } from 'sequelize';
import path from 'path';
import ScheduleService, { TaskCallbacks } from './schedule';
import { SimpleIntervalSchedule } from 'toad-scheduler';
@@ -104,7 +104,7 @@ export default class SubscriptionService {
}
}
private async setSshConfig() {
public async setSshConfig() {
const docs = await SubscriptionModel.findAll();
this.sshKeyService.setSshConfig(docs);
}
@@ -236,7 +236,8 @@ export default class SubscriptionService {
}
public async update(payload: Subscription): Promise<Subscription> {
const tab = new Subscription(payload);
const doc = await this.getDb({ id: payload.id })
const tab = new Subscription({ ...doc, ...payload });
const newDoc = await this.updateDb(tab);
await this.handleTask(newDoc, !newDoc.is_disabled);
await this.setSshConfig();
@@ -288,7 +289,7 @@ export default class SubscriptionService {
await this.setSshConfig();
}
public async getDb(query: any): Promise<Subscription> {
public async getDb(query: FindOptions<Subscription>['where']): Promise<Subscription> {
const doc: any = await SubscriptionModel.findOne({ where: { ...query } });
return doc && (doc.get({ plain: true }) as Subscription);
}
+2 -2
View File
@@ -17,7 +17,7 @@ ARG QL_BRANCH=develop
ENV PNPM_HOME=/root/.local/share/pnpm \
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.local/share/pnpm:/root/.local/share/pnpm/global/5/node_modules:$PNPM_HOME \
NODE_PATH=/usr/local/bin:/usr/local/pnpm-global/5/node_modules:/usr/local/lib/node_modules:/root/.local/share/pnpm/global/5/node_modules \
LANG=zh_CN.UTF-8 \
LANG=C.UTF-8 \
SHELL=/bin/bash \
PS1="\u@\h:\w \$ " \
QL_DIR=/ql \
@@ -49,7 +49,7 @@ RUN set -x \
&& git config --global user.name "qinglong" \
&& git config --global http.postBuffer 524288000 \
&& 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/.local/share/pnpm/store \
&& rm -rf /root/.cache \
+7 -10
View File
@@ -9,15 +9,12 @@ export isFirstStartServer=true
echo -e "======================1. 检测配置文件========================\n"
make_dir /etc/nginx/conf.d
make_dir /run/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
init_nginx
pm2 l &>/dev/null
patch_version &>/dev/null
echo
echo -e "======================2. 安装依赖========================\n"
patch_version
update_depend
echo
@@ -30,14 +27,14 @@ pm2 delete public &>/dev/null
pm2 start $dir_static/build/public.js -n public --source-map-support --time
echo -e "监控服务启动成功...\n"
echo -e "======================5. 启动控制面板========================\n"
echo -e "======================5. 启动主服务========================\n"
pm2 delete panel &>/dev/null
pm2 start $dir_static/build/app.js -n panel --source-map-support --time
echo -e "控制面板启动成功...\n"
echo -e "主服务启动成功...\n"
echo -e "======================6. 启动定时务========================\n"
echo -e "======================6. 启动定时务========================\n"
pm2 delete schedule &>/dev/null
pm2 start $dir_static/build/schedule.js -n schedule --source-map-support --time
pm2 start $dir_static/build/schedule/index.js -n schedule --source-map-support --time
echo -e "定时任务启动成功...\n"
if [[ $AutoStartBot == true ]]; then
+1 -1
View File
@@ -13,7 +13,7 @@ map $http_upgrade $connection_upgrade {
server {
listen 5700;
listen [::]:5700 ipv6only=on;
IPV6_CONFIG
root /ql/static/dist;
ssl_session_timeout 5m;
+8 -1
View File
@@ -5,11 +5,13 @@
"start:front": "max dev",
"start:back": "nodemon",
"start:public": "ts-node --transpile-only ./back/public.ts",
"start:rpc": "ts-node --transpile-only ./back/schedule/index.ts",
"build:front": "max build",
"build:back": "tsc -p tsconfig.back.json",
"panel": "npm run build:back && node static/build/app.js",
"schedule": "npm run build:back && node static/build/schedule.js",
"schedule": "npm run build:back && node static/build/schedule/index.js",
"public": "npm run build:back && node static/build/public.js",
"gen:proto": "protoc --experimental_allow_proto3_optional --plugin=./node_modules/.bin/protoc-gen-ts_proto ./back/protos/cron.proto --ts_proto_out=./ --ts_proto_opt=outputServices=grpc-js,env=node,esModuleInterop=true",
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
"postinstall": "max setup 2>/dev/null || true",
"test": "umi-test",
@@ -53,6 +55,7 @@
}
},
"dependencies": {
"@grpc/grpc-js": "^1.8.13",
"@otplib/preset-default": "^12.0.1",
"@sentry/node": "^7.12.1",
"@sentry/tracing": "^7.12.1",
@@ -77,6 +80,7 @@
"nedb": "^1.8.0",
"node-schedule": "^2.1.0",
"nodemailer": "^6.7.2",
"protobufjs": "^7.2.3",
"pstree.remy": "^1.1.8",
"reflect-metadata": "^0.1.13",
"sequelize": "^6.25.5",
@@ -141,10 +145,13 @@
"react-split-pane": "^0.1.92",
"sockjs-client": "^1.6.0",
"ts-node": "^10.6.0",
"ts-proto": "^1.146.0",
"tslib": "^2.4.0",
"tsx": "^3.12.3",
"typescript": "4.8.4",
"umi-request": "^1.4.0",
"vh-check": "^2.0.5",
"virtualizedtableforantd4": "1.3.0",
"webpack": "^5.70.0",
"yorkie": "^2.0.0"
}
+2875 -3251
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -296,7 +296,7 @@ async function sendNotify(
) {
//提供6种通知
desp += author; //增加作者信息,防止被贩卖等
// 根据标题跳过一些消息推送,环境变量:SKIP_PUSH_TITLE 用回车分隔
let skipTitle = process.env.SKIP_PUSH_TITLE
if(skipTitle) {
@@ -533,7 +533,7 @@ function BarkNotify(text, desp, params = {}) {
const options = {
url: `${BARK_PUSH}/${encodeURIComponent(text)}/${encodeURIComponent(
desp,
)}?icon=${BARK_ICON}?sound=${BARK_SOUND}&group=${BARK_GROUP}&${querystring.stringify(
)}?icon=${BARK_ICON}&sound=${BARK_SOUND}&group=${BARK_GROUP}&${querystring.stringify(
params,
)}`,
headers: {
+28 -18
View File
@@ -164,7 +164,8 @@ def dingding_bot(title: str, content: str) -> None:
timestamp = str(round(time.time() * 1000))
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")
hmac_code = hmac.new(
secret_enc, string_to_sign_enc, digestmod=hashlib.sha256
@@ -220,7 +221,7 @@ def go_cqhttp(title: str, content: str) -> None:
print("go-cqhttp 推送失败!")
def gotify(title:str,content:str) -> None:
def gotify(title: str, content: str) -> None:
"""
使用 gotify 推送消息。
"""
@@ -230,8 +231,9 @@ def gotify(title:str,content:str) -> None:
print("gotify 服务启动")
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")}
response = requests.post(url,data=data).json()
data = {"title": title, "message": content,
"priority": push_config.get("GOTIFY_PRIORITY")}
response = requests.post(url, data=data).json()
if response.get("id"):
print("gotify 推送成功!")
@@ -269,10 +271,10 @@ def serverJ(title: str, content: str) -> None:
print("serverJ 服务启动")
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'
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()
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取消推送")
return
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'
if 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)
def pushplus_bot(title: str, content: str) -> None:
"""
通过 push+ 推送消息。
@@ -348,7 +350,8 @@ def pushplus_bot(title: str, content: str) -> None:
url_old = "http://pushplus.hxtrip.com/send"
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:
print("PUSHPLUS(hxtrip) 推送成功!")
@@ -367,7 +370,8 @@ def qmsg_bot(title: str, content: str) -> None:
print("qmsg 服务启动")
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()
if response["code"] == 0:
@@ -553,14 +557,14 @@ def aibotk(title: str, content: str) -> None:
data = {
"apiKey": push_config.get("AIBOTK_KEY"),
"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:
url = "https://api-bot.aibotk.com/openapi/v1/chat/contact"
data = {
"apiKey": push_config.get("AIBOTK_KEY"),
"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")
headers = {"Content-Type": "application/json"}
@@ -582,14 +586,19 @@ def smtp(title: str, content: str) -> None:
print("SMTP 邮件 服务启动")
message = MIMEText(content, 'plain', 'utf-8')
message['From'] = formataddr((Header(push_config.get("SMTP_NAME"), 'utf-8').encode(), push_config.get("SMTP_EMAIL")))
message['To'] = formataddr((Header(push_config.get("SMTP_NAME"), 'utf-8').encode(), push_config.get("SMTP_EMAIL")))
message['From'] = formataddr((Header(push_config.get(
"SMTP_NAME"), 'utf-8').encode(), push_config.get("SMTP_EMAIL")))
message['To'] = formataddr((Header(push_config.get(
"SMTP_NAME"), 'utf-8').encode(), push_config.get("SMTP_EMAIL")))
message['Subject'] = Header(title, 'utf-8')
try:
smtp_server = smtplib.SMTP_SSL(push_config.get("SMTP_SERVER")) if push_config.get("SMTP_SSL") == 'true' else smtplib.SMTP(push_config.get("SMTP_SERVER"))
smtp_server.login(push_config.get("SMTP_EMAIL"), push_config.get("SMTP_PASSWORD"))
smtp_server.sendmail(push_config.get("SMTP_EMAIL"), push_config.get("SMTP_EMAIL"), message.as_bytes())
smtp_server = smtplib.SMTP_SSL(push_config.get("SMTP_SERVER")) if push_config.get(
"SMTP_SSL") == 'true' else smtplib.SMTP(push_config.get("SMTP_SERVER"))
smtp_server.login(push_config.get("SMTP_EMAIL"),
push_config.get("SMTP_PASSWORD"))
smtp_server.sendmail(push_config.get("SMTP_EMAIL"),
push_config.get("SMTP_EMAIL"), message.as_bytes())
smtp_server.close()
print("SMTP 邮件 推送成功!")
except Exception as e:
@@ -660,7 +669,8 @@ def send(title: str, content: str) -> None:
content += "\n\n" + text
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
]
[t.start() for t in ts]
-5
View File
@@ -3,13 +3,8 @@
"author": "",
"license": "ISC",
"dependencies": {
"crypto-js": "^4.0.0",
"download": "^8.0.0",
"got": "^11.5.1",
"http-server": "^0.12.3",
"nodemailer": "^6.8.0",
"qrcode-terminal": "^0.12.0",
"request": "^2.88.2",
"tough-cookie": "^4.0.0",
"tunnel": "0.0.6",
"ws": "^7.4.3"
+15 -6
View File
@@ -1,15 +1,24 @@
#!/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() {
if [[ -f $file_auth_token ]]; then
token=$(cat $file_auth_token | jq -r .value)
else
local token_command="ts-node-transpile-only ${dir_root}/back/token.ts"
local token_file="${dir_root}static/build/token.js"
if [[ -f $token_file ]]; then
token_command="node ${token_file}"
local expiration=$(cat $file_auth_token | jq -r .expiration)
local currentTimeStamp=$(date +%s)
if [[ $currentTimeStamp -ge $expiration ]]; then
create_token
fi
token=$(eval "$token_command")
else
create_token
fi
}
Regular → Executable
-4
View File
@@ -1,9 +1,5 @@
#!/usr/bin/env bash
## 导入通用变量与函数
dir_shell=$QL_DIR/shell
. $dir_shell/share.sh
if [[ -z ${BotRepoUrl} ]]; then
url="https://github.com/SuMaiKaDe/bot.git"
repo_path="${dir_repo}/dockerbot"
Regular → Executable
+4 -9
View File
@@ -1,9 +1,5 @@
#!/usr/bin/env bash
dir_shell=$QL_DIR/shell
. $dir_shell/share.sh
. $dir_shell/api.sh
reset_env() {
echo -e "---> 1. 开始检测配置文件\n"
fix_config
@@ -29,9 +25,7 @@ copy_dep() {
echo -e "---> 通知文件复制完成\n"
echo -e "---> 2. 复制nginx配置文件\n"
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
init_nginx
echo -e "---> 配置文件复制完成\n"
}
@@ -70,9 +64,8 @@ check_pm2() {
pm2_log
local currentTimeStamp=$(date +%s)
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 "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 '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' \
@@ -97,6 +90,8 @@ main() {
echo -e "=====> 开始检测"
npm i -g pnpm
patch_version
pnpm add -g pm2 tsx
update_depend
start_public
copy_dep
check_ql
Regular → Executable
+3 -6
View File
@@ -1,10 +1,5 @@
#!/usr/bin/env bash
## 导入通用变量与函数
dir_shell=$QL_DIR/shell
. $dir_shell/share.sh
. $dir_shell/api.sh
trap "single_hanle" 2 20 15 14
single_hanle() {
handle_task_after "$@"
@@ -35,7 +30,7 @@ random_delay() {
done
local delay_second=$(($(gen_random_num "$random_delay_max") + 1))
echo -e "\n命令未添加 \"now\"随机延迟 $delay_second后执行\n"
echo -e "任务随机延迟 $delay_second,配置文件参数 RandomDelay 置空可取消延迟 \n"
sleep $delay_second
fi
}
@@ -124,6 +119,8 @@ handle_task_after() {
local end_timestamp=$(format_timestamp "$time_format" "$etime")
local diff_time=$(($end_timestamp - $begin_timestamp))
[[ "$diff_time" == 0 ]] && diff_time=1
echo -e "\n\n## 执行结束... $end_time 耗时 $diff_time 秒     "
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
Regular → Executable
+1 -1
View File
@@ -11,7 +11,7 @@ echo -e "提交master代码"
git push
echo -e "更新cdn文件"
ts-node sample/tool.ts
tsx sample/tool.ts
string=$(cat version.yaml | grep "version" | egrep "[^ ]*" -o | egrep "\d\.*")
version="v$string"
+6 -6
View File
@@ -1,9 +1,5 @@
#!/usr/bin/env bash
## 导入通用变量与函数
dir_shell=$QL_DIR/shell
. $dir_shell/share.sh
days=$1
## 删除运行脚本的旧日志
@@ -19,10 +15,14 @@ remove_js_log() {
diff_time=$(($(date +%s) - $(date +%s -d "$log_date")))
fi
if [[ $diff_time -gt $((${days} * 86400)) ]]; then
local log_path=$(echo "$log" | sed "s,${dir_log},,g")
local log_path=$(echo "$log" | sed "s,${dir_log}/,,g")
local result=$(find_cron_api "log_path=$log_path")
if [[ $result ]]; then
echo -e "查询文件 $log_path"
if [[ -z $result ]]; then
echo -e "删除中~"
rm -vf $log
else
echo -e "正在被 $result 使用,跳过~"
fi
fi
fi
+24 -13
View File
@@ -379,9 +379,9 @@ reload_pm2() {
pm2 delete panel --source-map-support --time &>/dev/null
pm2 start $dir_static/build/app.js -n panel --source-map-support --time &>/dev/null
echo -e "启动定时任务服务\n"
echo -e "启动定时服务\n"
pm2 delete schedule --source-map-support --time &>/dev/null
pm2 start $dir_static/build/schedule.js -n schedule --source-map-support --time &>/dev/null
pm2 start $dir_static/build/schedule/index.js -n schedule --source-map-support --time &>/dev/null
}
diff_time() {
@@ -431,17 +431,21 @@ format_timestamp() {
}
patch_version() {
# 兼容pnpm@7
pnpm setup &>/dev/null
source ~/.bashrc
if [[ $PipMirror ]]; then
pip3 config set global.index-url $PipMirror
fi
if [[ $NpmMirror ]]; then
npm config set registry $NpmMirror
cd && pnpm config set registry $NpmMirror
pnpm install -g
fi
# 兼容pnpm@7
pnpm setup &>/dev/null
source ~/.bashrc
pnpm install -g &>/dev/null
git config --global pull.rebase false
cp -f $dir_root/.env.example $dir_root/.env
if [[ -f "$dir_root/db/cookie.db" ]]; then
echo -e "检测到旧的db文件,拷贝为新db...\n"
@@ -450,12 +454,6 @@ patch_version() {
echo
fi
pnpm add -g pm2 ts-node typescript tslib
git config --global pull.rebase false
cp -f $dir_root/.env.example $dir_root/.env
if [[ -d "$dir_root/db" ]]; then
echo -e "检测到旧的db目录,拷贝到data目录...\n"
cp -rf $dir_root/config $dir_root/data
@@ -481,6 +479,19 @@ patch_version() {
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
detect_termux
detect_macos
+10 -2
View File
@@ -15,7 +15,11 @@ define_program() {
elif [[ $file_param == *.sh ]]; then
which_program="bash"
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
which_program=""
fi
@@ -57,7 +61,11 @@ handle_log_path() {
format_params() {
time_format="%Y-%m-%d %H:%M:%S"
mtime_format="%Y-%m-%d %H:%M:%S.%3N"
if [[ $is_macos -eq 1 ]]; then
mtime_format=$time_format
else
mtime_format="%Y-%m-%d %H:%M:%S.%3N"
fi
timeoutCmd=""
if type timeout &>/dev/null; then
timeoutCmd="timeout --foreground -s 14 -k 10s $command_timeout_time "
+15 -6
View File
@@ -152,19 +152,28 @@ update_raw() {
local autoAddCron="$3"
local autoDelCron="$4"
if [[ ! $autoAddCron ]];then
if [[ ! $autoAddCron ]]; then
autoAddCron=${AutoAddCron}
fi
if [[ ! $autoDelCron ]];then
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 suffix="${raw_url##*.}"
local raw_file_name="${uniq_path}.${suffix}"
echo -e "开始下载:${raw_url} \n\n保存路径:$dir_raw/${raw_file_name}\n"
wget -q --no-check-certificate -e "http_proxy=${proxy};https_proxy=${proxy}" -O "$dir_raw/${raw_file_name}.new" ${raw_url}
wget -q --no-check-certificate $proxyStr -O "$dir_raw/${raw_file_name}.new" ${raw_url}
if [[ $? -eq 0 ]]; then
mv "$dir_raw/${raw_file_name}.new" "$dir_raw/${raw_file_name}"
@@ -297,11 +306,11 @@ diff_scripts() {
local extensions="$6"
local autoAddCron="$7"
local autoDelCron="$8"
if [[ ! $autoAddCron ]];then
if [[ ! $autoAddCron ]]; then
autoAddCron=${AutoAddCron}
fi
if [[ ! $autoDelCron ]];then
if [[ ! $autoDelCron ]]; then
autoDelCron=${AutoDelCron}
fi
+14 -14
View File
@@ -9,14 +9,25 @@
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 {
max-height: calc(80vh - 110px);
max-height: calc(80vh - var(--vh-offset, 110px));
max-height: calc(90vh - 110px);
max-height: calc(90vh - var(--vh-offset, 110px));
overflow-y: auto;
}
@@ -316,22 +327,11 @@ select:-webkit-autofill:focus {
.side-menu-user-drop-menu {
position: relative;
text-align: left;
outline: none;
padding: 4px 0;
border: 1px solid fade(@component-background, 0.12);
border-radius: 4px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
padding: 2px 10px;
overflow: auto;
background-color: @component-background;
}
[data-dark='true'] .side-menu-user-drop-menu {
background-color: #373739;
}
.ant-pro-sider-logo {
padding: 16px 8px !important;
h1 {
margin-left: 5px !important;
}
+2 -2
View File
@@ -271,7 +271,7 @@ export default function () {
// @ts-ignore
title={
<>
<span style={{ fontSize: 16 }}></span>
<span style={{ fontSize: 16, marginRight: 5 }}></span>
<a
href={systemInfo?.changeLogLink}
target="_blank"
@@ -313,7 +313,7 @@ export default function () {
pageTitleRender={(props, pageName, info) => {
const title =
(config.documentTitleMap as any)[location.pathname] || '未找到';
return `${title} - 控制面板`;
return `${title} - 青龙`;
}}
onCollapse={setCollapsed}
collapsed={collapsed}
+13 -3
View File
@@ -6,8 +6,9 @@
.card-wrapper {
.ant-card:last-child {
.ant-card-body {
height: calc(80vh - 367px);
height: calc(80vh - var(--vh-offset, 0px) - 367px);
min-height: 300px;
height: calc(90vh - 367px);
height: calc(90vh - var(--vh-offset, 0px) - 367px);
overflow-y: auto;
}
}
@@ -155,7 +156,7 @@
.view-create-modal-sorts {
display: flex;
.ant-space-item:nth-child(2) {
.ant-space-item:nth-child(1) {
flex: 1;
}
}
@@ -177,6 +178,7 @@ tr.drop-over-upward td {
.view-filters-container.active {
.filter-item > div > .ant-form-item-control {
margin-left: 40px;
width: calc(100% - 40px);
}
}
@@ -191,3 +193,11 @@ tr.drop-over-upward td {
margin: -24px;
}
}
body[data-mode='desktop'] {
.crontab-wrapper {
tbody .ant-table-cell {
height: 69px !important;
}
}
}
+7 -37
View File
@@ -53,7 +53,7 @@ import { SharedContext } from '@/layouts';
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
import { getCommandScript, parseCrontab } from '@/utils';
import { ColumnProps } from 'antd/lib/table';
import { VList } from '../../components/vlist';
import { useVT } from 'virtualizedtableforantd4';
const { Text, Paragraph } = Typography;
const { Search } = Input;
@@ -279,13 +279,6 @@ const Crontab = () => {
value: 3,
},
],
onFilter: (value, record) => {
if (record.isDisabled && record.status !== 0) {
return value === 2;
} else {
return record.status === value;
}
},
render: (text, record) => (
<>
{(!record.isDisabled || record.status !== CrontabStatus.idle) && (
@@ -321,7 +314,7 @@ const Crontab = () => {
{
title: '操作',
key: 'action',
width: 100,
width: 130,
render: (text, record, index) => {
const isPc = !isPhone;
return (
@@ -330,7 +323,6 @@ const Crontab = () => {
<Tooltip title={isPc ? '运行' : ''}>
<a
onClick={(e) => {
setReset(false);
e.stopPropagation();
runCron(record, index);
}}
@@ -343,7 +335,6 @@ const Crontab = () => {
<Tooltip title={isPc ? '停止' : ''}>
<a
onClick={(e) => {
setReset(false);
e.stopPropagation();
stopCron(record, index);
}}
@@ -355,7 +346,6 @@ const Crontab = () => {
<Tooltip title={isPc ? '日志' : ''}>
<a
onClick={(e) => {
setReset(false);
e.stopPropagation();
setLogCron({ ...record, timestamp: Date.now() });
}}
@@ -399,10 +389,6 @@ const Crontab = () => {
const [moreMenuActive, setMoreMenuActive] = useState(false);
const tableRef = useRef<any>();
const tableScrollHeight = useTableScrollHeight(tableRef);
const resetRef = useRef<boolean>(true);
const setReset = (v) => {
resetRef.current = v;
};
const goToScriptManager = (record: any) => {
const result = getCommandScript(record.command);
@@ -679,14 +665,12 @@ const Crontab = () => {
index: number;
}> = ({ record, index }) => (
<Dropdown
arrow={{ pointAtCenter: true }}
placement="bottomRight"
trigger={['click']}
menu={{
items: getMenuItems(record),
onClick: ({ key, domEvent }) => {
domEvent.stopPropagation();
setReset(false);
action(key, record, index);
},
}}
@@ -722,7 +706,6 @@ const Crontab = () => {
};
const onSearch = (value: string) => {
setReset(true);
setSearchText(value.trim());
};
@@ -750,10 +733,6 @@ const Crontab = () => {
setSelectedRowIds(selectedIds);
};
useEffect(() => {
setReset(false);
}, [selectedRowIds]);
const rowSelection = {
selectedRowKeys: selectedRowIds,
onChange: onSelectChange,
@@ -781,7 +760,6 @@ const Crontab = () => {
};
const operateCrons = (operationStatus: number) => {
setReset(false);
Modal.confirm({
title: `确认${OperationName[operationStatus]}`,
content: <>{OperationName[operationStatus]}</>,
@@ -808,7 +786,6 @@ const Crontab = () => {
sorter: SorterResult<any> | SorterResult<any>[],
) => {
const { current, pageSize } = pagination;
setReset(true);
setPageConf({
page: current as number,
size: pageSize as number,
@@ -923,21 +900,14 @@ const Crontab = () => {
const tabClick = (key: string) => {
const view = enabledCronViews.find((x) => x.id == key);
setSelectedRowIds([]);
setReset(true);
setPageConf({ ...pageConf, page: 1 });
setViewConf(view ? view : null);
};
const vComponents = useMemo(() => {
return VList({
height: tableScrollHeight,
reset: resetRef.current,
rowHeight: 69,
scrollTop: resetRef.current
? 0
: tableRef.current?.querySelector('.ant-table-body')?.scrollTop,
});
}, [tableScrollHeight, resetRef.current]);
const [vt] = useVT(
() => ({ scroll: { y: tableScrollHeight } }),
[tableScrollHeight],
);
return (
<PageContainer
@@ -1073,7 +1043,7 @@ const Crontab = () => {
rowSelection={rowSelection}
rowClassName={getRowClassName}
onChange={onPageChange}
components={vComponents}
// components={isPhone ? undefined : vt}
/>
</div>
<CronLogModal
+84 -85
View File
@@ -70,6 +70,7 @@ const ViewCreateModal = ({
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [filterRelation, setFilterRelation] = useState<'and' | 'or'>('and');
const filtersValue = Form.useWatch('filters', form);
const handleOk = async (values: any) => {
setLoading(true);
@@ -126,7 +127,7 @@ const ViewCreateModal = ({
};
const typeElement = (
<Select>
<Select style={{ width: 80 }}>
{SORTTYPES.map((x) => (
<Select.Option key={x.name} value={x.value}>
{x.name}
@@ -204,7 +205,7 @@ const ViewCreateModal = ({
position: 'absolute',
top: '50%',
translate: '-50% -50%',
padding: '0 0 0 3px',
padding: '0 3px',
cursor: 'pointer',
}}
onClick={() => {
@@ -221,9 +222,9 @@ const ViewCreateModal = ({
</div>
)}
<div>
{fields.map(({ key, name, ...restField }, index) => (
{fields.map(({ key, name, ...restField }) => (
<Form.Item
label={index === 0 ? '筛选条件' : ''}
label={name === 0 ? '筛选条件' : ''}
key={key}
style={{ marginBottom: 0 }}
required
@@ -232,9 +233,6 @@ const ViewCreateModal = ({
<Space
className="view-create-modal-filters"
align="baseline"
style={
fields.length > 1 ? { width: 'calc(100% - 40px)' } : {}
}
>
<Form.Item
{...restField}
@@ -251,55 +249,18 @@ const ViewCreateModal = ({
{operationElement}
</Form.Item>
<Form.Item
noStyle
shouldUpdate={(prevValues, nextValues) => {
const preOperation =
EOperation[
get(prevValues, ['filters', name, 'operation'])
];
const nextOperation =
EOperation[
get(nextValues, ['filters', name, 'operation'])
];
const flag = preOperation !== nextOperation;
if (flag) {
form.setFieldValue(
['filters', name, 'value'],
nextOperation === 'select' ? [] : '',
);
}
return flag;
}}
{...restField}
name={[name, 'value']}
rules={[{ required: true, message: '请输入内容' }]}
>
{() => {
const property = form.getFieldValue([
'filters',
index,
'property',
]) as 'status';
const operate = form.getFieldValue([
'filters',
name,
'operation',
]);
return (
<Form.Item
{...restField}
name={[name, 'value']}
rules={[
{ required: true, message: '请输入内容' },
]}
>
{EOperation[operate] === 'select' ? (
statusElement(property)
) : (
<Input placeholder="请输入内容" />
)}
</Form.Item>
);
}}
{EOperation[filtersValue[name]['operation']] ===
'select' ? (
statusElement(filtersValue[name]['property'])
) : (
<Input placeholder="请输入内容" />
)}
</Form.Item>
{index !== 0 && (
{name !== 0 && (
<MinusCircleOutlined onClick={() => remove(name)} />
)}
</Space>
@@ -321,39 +282,77 @@ const ViewCreateModal = ({
</Form.List>
<Form.List name="sorts">
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...restField }, index) => (
<Form.Item
label={index === 0 ? '排序方式' : ''}
key={key}
style={{ marginBottom: 0 }}
<div
style={{ position: 'relative' }}
className={`view-filters-container ${
fields.length > 1 ? 'active' : ''
}`}
>
{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">
<Form.Item
{...restField}
name={[name, 'property']}
rules={[{ required: true }]}
>
{propertyElement(PROPERTIES, { width: 240 })}
</Form.Item>
<Form.Item
{...restField}
name={[name, 'type']}
rules={[{ required: true }]}
>
{typeElement}
</Form.Item>
<MinusCircleOutlined onClick={() => remove(name)} />
</Space>
<Button
type="primary"
size="small"
style={{
position: 'absolute',
top: '50%',
translate: '-50% -50%',
padding: '0 3px',
cursor: 'pointer',
}}
>
<>
<span>{ViewFilterRelation[filterRelation]}</span>
</>
</Button>
</div>
)}
<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>
<a onClick={() => add({ property: 'command', type: 'ASC' })}>
<PlusOutlined />
</a>
</Form.Item>
</>
</div>
</div>
)}
</Form.List>
</Form>
+53 -1
View File
@@ -19,6 +19,8 @@ import {
DeleteFilled,
BugOutlined,
FileTextOutlined,
CloseCircleOutlined,
ClockCircleOutlined,
} from '@ant-design/icons';
import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout';
@@ -42,6 +44,7 @@ enum Status {
'删除中',
'已删除',
'删除失败',
'队列中',
}
enum StatusColor {
@@ -50,6 +53,37 @@ enum StatusColor {
'error',
}
const StatusMap: Record<number, { icon: React.ReactNode; color: string }> = {
0: {
icon: <SyncOutlined spin />,
color: 'processing',
},
1: {
icon: <CheckCircleOutlined />,
color: 'success',
},
2: {
icon: <CloseCircleOutlined />,
color: 'error',
},
3: {
icon: <SyncOutlined spin />,
color: 'processing',
},
4: {
icon: <CheckCircleOutlined />,
color: 'success',
},
5: {
icon: <CloseCircleOutlined />,
color: 'error',
},
6: {
icon: <ClockCircleOutlined />,
color: 'default',
},
};
const Dependence = () => {
const { headerStyle, isPhone, socketMessage } =
useOutletContext<SharedContext>();
@@ -74,7 +108,8 @@ const Dependence = () => {
return (
<Space size="middle" style={{ cursor: 'text' }}>
<Tag
color={StatusColor[record.status % 3]}
color={StatusMap[record.status].color}
icon={StatusMap[record.status].icon}
style={{ marginRight: 0 }}
>
{Status[record.status]}
@@ -366,6 +401,23 @@ const Dependence = () => {
useEffect(() => {
if (!socketMessage) return;
const { type, message, references } = socketMessage;
if (
type === 'installDependence' &&
message.includes('开始时间') &&
references.length > 0
) {
const result = [...value];
for (let i = 0; i < references.length; i++) {
const index = value.findIndex((x) => x.id === references[i]);
if (index !== -1) {
result.splice(index, 1, {
...value[index],
status: message.includes('安装') ? Status.安装中 : Status.删除中,
});
}
}
setValue(result);
}
if (
type === 'installDependence' &&
message.includes('结束时间') &&
+26 -52
View File
@@ -39,7 +39,7 @@ import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts';
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
import Copy from '../../components/copy';
import { VList } from '../../components/vlist';
import { useVT } from 'virtualizedtableforantd4';
const { Text } = Typography;
const { Search } = Input;
@@ -216,10 +216,6 @@ const Env = () => {
const [importLoading, setImportLoading] = useState(false);
const tableRef = useRef<any>();
const tableScrollHeight = useTableScrollHeight(tableRef, 59);
const resetRef = useRef<boolean>(true);
const setReset = (v) => {
resetRef.current = v;
};
const getEnvs = () => {
setLoading(true);
@@ -234,7 +230,6 @@ const Env = () => {
};
const enabledOrDisabledEnv = (record: any, index: number) => {
setReset(false);
Modal.confirm({
title: `确认${record.status === Status. ? '启用' : '禁用'}`,
content: (
@@ -280,19 +275,16 @@ const Env = () => {
};
const addEnv = () => {
setReset(false);
setEditedEnv(null as any);
setIsModalVisible(true);
};
const editEnv = (record: any, index: number) => {
setReset(false);
setEditedEnv(record);
setIsModalVisible(true);
};
const deleteEnv = (record: any, index: number) => {
setReset(false);
Modal.confirm({
title: '确认删除',
content: (
@@ -332,20 +324,13 @@ const Env = () => {
getEnvs();
};
const vComponents = useMemo(() => {
return VList({
height: tableScrollHeight!,
reset: resetRef.current,
rowHeight: 48,
scrollTop: resetRef.current
? 0
: tableRef.current?.querySelector('.ant-table-body')?.scrollTop,
});
}, [tableScrollHeight, resetRef.current]);
const [vt, setVT] = useVT(
() => ({ scroll: { y: tableScrollHeight } }),
[tableScrollHeight],
);
const DragableBodyRow = (props: any) => {
const DragableBodyRow = React.forwardRef((props: any, ref) => {
const { index, moveRow, className, style, ...restProps } = props;
const ref = useRef();
const [{ isOver, dropClassName }, drop] = useDrop({
accept: type,
collect: (monitor) => {
@@ -373,31 +358,27 @@ const Env = () => {
useEffect(() => {
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(() => {
return {
ref: ref,
className: `${className}${isOver ? dropClassName : ''}`,
style: { cursor: 'move', ...style },
...restProps,
};
}, [className, dropClassName, restProps, style, isOver]);
return <> {components(tempProps, ref)} </>;
};
const components = useMemo(() => {
return {
...vComponents,
body: {
...vComponents.body,
row: DragableBodyRow,
},
};
}, [vComponents]);
useEffect(
() =>
setVT({
body: {
row: DragableBodyRow,
},
}),
[],
);
const moveRow = useCallback(
(dragIndex: number, hoverIndex: number) => {
@@ -425,17 +406,12 @@ const Env = () => {
setSelectedRowIds(selectedIds);
};
useEffect(() => {
setReset(false);
}, [selectedRowIds]);
const rowSelection = {
selectedRowKeys: selectedRowIds,
onChange: onSelectChange,
};
const delEnvs = () => {
setReset(false);
Modal.confirm({
title: '确认删除',
content: <></>,
@@ -457,7 +433,6 @@ const Env = () => {
};
const operateEnvs = (operationStatus: number) => {
setReset(false);
Modal.confirm({
title: `确认${OperationName[operationStatus]}`,
content: <>{OperationName[operationStatus]}</>,
@@ -490,7 +465,6 @@ const Env = () => {
};
const onSearch = (value: string) => {
setReset(true);
setSearchText(value.trim());
};
@@ -607,7 +581,7 @@ const Env = () => {
rowKey="id"
size="middle"
scroll={{ x: 1000, y: tableScrollHeight }}
components={components}
components={vt}
loading={loading}
onRow={(record: any, index: number | undefined) => {
return {
+7 -2
View File
@@ -78,9 +78,14 @@ const Initialization = () => {
{
title: '欢迎使用',
content: (
<div className={styles.top} style={{ marginTop: 100 }}>
<div className={styles.top} style={{ marginTop: 30 }}>
<div className={styles.header}>
<span className={styles.title}>使</span>
<span className={styles.title}>使</span>
<span className={styles.desc}>
python3javaScriptshelltypescript A
timed task management panel that supports typescript, javaScript,
python3, and shell.
</span>
</div>
<div className={styles.action}>
<Button
+3 -3
View File
@@ -47,9 +47,8 @@ const CheckUpdate = ({ socketMessage, systemInfo }: any) => {
</div>
</>
),
okText: '确认',
cancelText: '强制更新',
onCancel() {
okText: '强制更新',
onOk() {
showUpdatingModal();
request
.put(`${config.apiPrefix}system/update`)
@@ -103,6 +102,7 @@ const CheckUpdate = ({ socketMessage, systemInfo }: any) => {
width: 600,
maskClosable: false,
closable: false,
keyboard: false,
okButtonProps: { disabled: true },
title: '更新中...',
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;
}
}
+1
View File
@@ -72,6 +72,7 @@ const LoginLog = ({ data }: any) => {
rowKey="id"
size="middle"
scroll={{ x: 768 }}
sticky
/>
</>
);
-1
View File
@@ -416,7 +416,6 @@ const Subscription = () => {
index: number;
}> = ({ record, index }) => (
<Dropdown
arrow={{ pointAtCenter: true }}
placement="bottomRight"
trigger={['click']}
menu={{
+1 -1
View File
@@ -1,5 +1,5 @@
export default {
siteName: '青龙控制面板',
siteName: '青龙',
apiPrefix: '/api/',
authKey: 'token',
+2
View File
@@ -14,11 +14,13 @@ export const useCtx = () => {
setMarginLeft(0);
setMarginTop(0);
setIsPhone(true);
document.body.setAttribute('data-mode', 'phone');
} else {
setWidth('100%');
setMarginLeft(0);
setMarginTop(-72);
setIsPhone(false);
document.body.setAttribute('data-mode', 'desktop');
}
}, []);
+8 -11
View File
@@ -1,12 +1,9 @@
version: 2.15.9
changeLogLink: https://t.me/jiao_long/364
version: 2.15.12
changeLogLink: https://t.me/jiao_long/368
changeLog: |
1. 通知脚本增加环境变量 SKIP_PUSH_TITLE,设置需要跳过推送的标题,多个换行符分割分隔,感谢 https://github.com/pharaoh2012
2. nginx 增加 ipv6 配
3. 对比工具增加通知文件对比
4. 修复文件类型订阅重复添加任务
5. 修改自动删除日志逻辑
6. 修复定时任务状态筛选,排序
7. 修改表格样式
8. 修复切换导航,编辑器页面可能崩溃
9. 其他 bug 修复
1. 修复定时任务筛选
2. 修复更新环境变量、定时任务、订阅,状态被重
3. 重构六位定时服务
4. 修改手机端页面样式
5. 修改依赖安装流程
6. 其他bug修复