mirror of
https://github.com/whyour/qinglong.git
synced 2026-08-12 03:10:48 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
118c92d9e5 | ||
|
|
280085668e | ||
|
|
bae4073a64 | ||
|
|
af3e358a6a | ||
|
|
64fcbff715 | ||
|
|
f9f78b4e05 | ||
|
|
fa83761d27 | ||
|
|
8173075b67 | ||
|
|
496918131f | ||
|
|
5d64aae452 | ||
|
|
952ea2859e | ||
|
|
25e8e8198d | ||
|
|
3ab5b0d86b | ||
|
|
6df651aa63 |
@@ -20,10 +20,12 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: pixta-dev/repository-mirroring-action@v1
|
||||
- uses: wearerequired/git-mirror-action@v1
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.GITLAB_SSH_PK }}
|
||||
with:
|
||||
target_repo_url: git@gitlab.com:whyour/qinglong.git
|
||||
ssh_private_key: ${{ secrets.GITLAB_SSH_PK }}
|
||||
source-repo: https://github.com/whyour/qinglong.git
|
||||
destination-repo: git@gitlab.com:whyour/qinglong.git
|
||||
|
||||
to_gitee:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -31,10 +33,12 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: pixta-dev/repository-mirroring-action@v1
|
||||
- uses: wearerequired/git-mirror-action@v1
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.GITLAB_SSH_PK }}
|
||||
with:
|
||||
target_repo_url: git@gitee.com:whyour/qinglong.git
|
||||
ssh_private_key: ${{ secrets.GITLAB_SSH_PK }}
|
||||
source-repo: https://github.com/whyour/qinglong.git
|
||||
destination-repo: git@gitee.com:whyour/qinglong.git
|
||||
|
||||
build-static:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -27,3 +27,4 @@ __pycache__
|
||||
/shell/preload/env.*
|
||||
/shell/preload/notify.*
|
||||
/shell/preload/*-notify.json
|
||||
/shell/preload/__ql_notify__.*
|
||||
|
||||
+11
-37
@@ -4,7 +4,8 @@ import { Logger } from 'winston';
|
||||
import CronService from '../services/cron';
|
||||
import CronViewService from '../services/cronView';
|
||||
import { celebrate, Joi } from 'celebrate';
|
||||
import cron_parser from 'cron-parser';
|
||||
import { commonCronSchema } from '../validation/schedule';
|
||||
|
||||
const route = Router();
|
||||
|
||||
export default (app: Router) => {
|
||||
@@ -170,27 +171,14 @@ export default (app: Router) => {
|
||||
route.post(
|
||||
'/',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
command: Joi.string().required(),
|
||||
schedule: Joi.string().required(),
|
||||
name: Joi.string().optional(),
|
||||
labels: Joi.array().optional(),
|
||||
sub_id: Joi.number().optional().allow(null),
|
||||
extra_schedules: Joi.array().optional().allow(null),
|
||||
task_before: Joi.string().optional().allow('').allow(null),
|
||||
task_after: Joi.string().optional().allow('').allow(null),
|
||||
}),
|
||||
body: Joi.object(commonCronSchema),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
if (cron_parser.parseExpression(req.body.schedule).hasNext()) {
|
||||
const cronService = Container.get(CronService);
|
||||
const data = await cronService.create(req.body);
|
||||
return res.send({ code: 200, data });
|
||||
} else {
|
||||
return res.send({ code: 400, message: 'param schedule error' });
|
||||
}
|
||||
const cronService = Container.get(CronService);
|
||||
const data = await cronService.create(req.body);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
@@ -331,30 +319,16 @@ export default (app: Router) => {
|
||||
'/',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
labels: Joi.array().optional().allow(null),
|
||||
command: Joi.string().required(),
|
||||
schedule: Joi.string().required(),
|
||||
name: Joi.string().optional().allow(null),
|
||||
sub_id: Joi.number().optional().allow(null),
|
||||
extra_schedules: Joi.array().optional().allow(null),
|
||||
task_before: Joi.string().optional().allow('').allow(null),
|
||||
task_after: Joi.string().optional().allow('').allow(null),
|
||||
...commonCronSchema,
|
||||
id: Joi.number().required(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
if (
|
||||
!req.body.schedule ||
|
||||
cron_parser.parseExpression(req.body.schedule).hasNext()
|
||||
) {
|
||||
const cronService = Container.get(CronService);
|
||||
const data = await cronService.update(req.body);
|
||||
return res.send({ code: 200, data });
|
||||
} else {
|
||||
return res.send({ code: 400, message: 'param schedule error' });
|
||||
}
|
||||
const cronService = Container.get(CronService);
|
||||
const data = await cronService.update(req.body);
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
@@ -418,7 +392,7 @@ export default (app: Router) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cronService = Container.get(CronService);
|
||||
const data = await cronService.import_crontab();
|
||||
const data = await cronService.importCrontab();
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
|
||||
@@ -384,6 +384,7 @@ export default (app: Router) => {
|
||||
body: Joi.object({
|
||||
retries: Joi.number().optional(),
|
||||
twoFactorActivated: Joi.boolean().optional(),
|
||||
password: Joi.string().optional(),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
@@ -396,4 +397,22 @@ export default (app: Router) => {
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
route.put(
|
||||
'/config/timezone',
|
||||
celebrate({
|
||||
body: Joi.object({
|
||||
timezone: Joi.string().allow('').allow(null),
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const systemService = Container.get(SystemService);
|
||||
const result = await systemService.updateTimezone(req.body);
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ async function startServer() {
|
||||
Logger.debug(`✌️ 后端服务启动成功!`);
|
||||
console.debug(`✌️ 后端服务启动成功!`);
|
||||
process.send?.('ready');
|
||||
require('./loaders/bootAfter').default();
|
||||
})
|
||||
.on('error', (err) => {
|
||||
Logger.error(err);
|
||||
|
||||
@@ -42,8 +42,8 @@ const systemLogPath = path.join(dataPath, 'syslog/');
|
||||
const envFile = path.join(preloadPath, 'env.sh');
|
||||
const jsEnvFile = path.join(preloadPath, 'env.js');
|
||||
const pyEnvFile = path.join(preloadPath, 'env.py');
|
||||
const jsNotifyFile = path.join(preloadPath, 'notify.js');
|
||||
const pyNotifyFile = path.join(preloadPath, 'notify.py');
|
||||
const jsNotifyFile = path.join(preloadPath, '__ql_notify__.js');
|
||||
const pyNotifyFile = path.join(preloadPath, '__ql_notify__.py');
|
||||
const confFile = path.join(configPath, 'config.sh');
|
||||
const crontabFile = path.join(configPath, 'crontab.list');
|
||||
const authConfigFile = path.join(configPath, 'auth.json');
|
||||
|
||||
+17
-1
@@ -527,7 +527,7 @@ export function safeJSONParse(value?: string) {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch (error) {
|
||||
Logger.error('[JSON.parse失败]', error);
|
||||
Logger.error('[safeJSONParse失败]', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
@@ -542,3 +542,19 @@ export async function rmPath(path: string) {
|
||||
Logger.error('[rmPath失败]', error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function setSystemTimezone(timezone: string): Promise<boolean> {
|
||||
try {
|
||||
if (!(await fileExist(`/usr/share/zoneinfo/${timezone}`))) {
|
||||
throw new Error('Invalid timezone');
|
||||
}
|
||||
|
||||
await promiseExec(`ln -sf /usr/share/zoneinfo/${timezone} /etc/localtime`);
|
||||
await promiseExec(`echo "${timezone}" > /etc/timezone`);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
Logger.error('[setSystemTimezone失败]', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
+4
-5
@@ -49,9 +49,8 @@ export class PushDeerNotification extends NotificationBaseInfo {
|
||||
public pushDeerUrl = '';
|
||||
}
|
||||
|
||||
export class ChatNotification extends NotificationBaseInfo {
|
||||
public chatUrl = '';
|
||||
public chatToken = '';
|
||||
export class synologyChatNotification extends NotificationBaseInfo {
|
||||
public synologyChatUrl = '';
|
||||
}
|
||||
|
||||
export class BarkNotification extends NotificationBaseInfo {
|
||||
@@ -61,7 +60,7 @@ export class BarkNotification extends NotificationBaseInfo {
|
||||
public barkGroup = 'qinglong';
|
||||
public barkLevel = 'active';
|
||||
public barkUrl = '';
|
||||
public barkArchive=""
|
||||
public barkArchive = '';
|
||||
}
|
||||
|
||||
export class TelegramBotNotification extends NotificationBaseInfo {
|
||||
@@ -163,7 +162,7 @@ export interface NotificationInfo
|
||||
GotifyNotification,
|
||||
ServerChanNotification,
|
||||
PushDeerNotification,
|
||||
ChatNotification,
|
||||
synologyChatNotification,
|
||||
BarkNotification,
|
||||
TelegramBotNotification,
|
||||
DingtalkBotNotification,
|
||||
|
||||
@@ -37,6 +37,7 @@ export interface SystemConfigInfo {
|
||||
nodeMirror?: string;
|
||||
pythonMirror?: string;
|
||||
linuxMirror?: string;
|
||||
timezone?: string;
|
||||
}
|
||||
|
||||
export interface LoginLogInfo {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
export enum ScheduleType {
|
||||
BOOT = '@boot',
|
||||
ONCE = '@once',
|
||||
}
|
||||
|
||||
export type ScheduleValidator = (schedule?: string) => boolean;
|
||||
export type CronSchedulerPayload = {
|
||||
name: string;
|
||||
id: string;
|
||||
schedule: string;
|
||||
command: string;
|
||||
extra_schedules: Array<{ schedule: string }>;
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import Container from 'typedi';
|
||||
import CronService from '../services/cron';
|
||||
|
||||
export default async () => {
|
||||
const cronService = Container.get(CronService);
|
||||
|
||||
await cronService.bootTask();
|
||||
};
|
||||
@@ -27,8 +27,8 @@ const sampleNotifyJsFile = path.join(samplePath, 'notify.js');
|
||||
const sampleNotifyPyFile = path.join(samplePath, 'notify.py');
|
||||
const scriptNotifyJsFile = path.join(scriptPath, 'sendNotify.js');
|
||||
const scriptNotifyPyFile = path.join(scriptPath, 'notify.py');
|
||||
const jsNotifyFile = path.join(preloadPath, 'notify.js');
|
||||
const pyNotifyFile = path.join(preloadPath, 'notify.py');
|
||||
const jsNotifyFile = path.join(preloadPath, '__ql_notify__.js');
|
||||
const pyNotifyFile = path.join(preloadPath, '__ql_notify__.py');
|
||||
const TaskBeforeFile = path.join(configPath, 'task_before.sh');
|
||||
const TaskBeforeJsFile = path.join(configPath, 'task_before.js');
|
||||
const TaskBeforePyFile = path.join(configPath, 'task_before.py');
|
||||
|
||||
+19
-15
@@ -38,21 +38,25 @@ export default async () => {
|
||||
|
||||
// 运行删除日志任务
|
||||
const data = await systemService.getSystemConfig();
|
||||
if (data && data.info && data.info.logRemoveFrequency) {
|
||||
const rmlogCron = {
|
||||
id: data.id as number,
|
||||
name: '删除日志',
|
||||
command: `ql rmlog ${data.info.logRemoveFrequency}`,
|
||||
runOrigin: 'system' as const,
|
||||
};
|
||||
await scheduleService.cancelIntervalTask(rmlogCron);
|
||||
scheduleService.createIntervalTask(
|
||||
rmlogCron,
|
||||
{
|
||||
days: data.info.logRemoveFrequency,
|
||||
},
|
||||
true,
|
||||
);
|
||||
if (data && data.info) {
|
||||
if (data.info.logRemoveFrequency) {
|
||||
const rmlogCron = {
|
||||
id: data.id as number,
|
||||
name: '删除日志',
|
||||
command: `ql rmlog ${data.info.logRemoveFrequency}`,
|
||||
runOrigin: 'system' as const,
|
||||
};
|
||||
await scheduleService.cancelIntervalTask(rmlogCron);
|
||||
scheduleService.createIntervalTask(
|
||||
rmlogCron,
|
||||
{
|
||||
days: data.info.logRemoveFrequency,
|
||||
},
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
systemService.updateTimezone(data.info);
|
||||
}
|
||||
|
||||
await subscriptionService.setSshConfig();
|
||||
|
||||
+71
-1
@@ -8,7 +8,7 @@ message EnvItem {
|
||||
optional string value = 3;
|
||||
optional string remarks = 4;
|
||||
optional int32 status = 5;
|
||||
optional int32 position = 6;
|
||||
optional int64 position = 6;
|
||||
}
|
||||
|
||||
message GetEnvsRequest { string searchValue = 1; }
|
||||
@@ -58,6 +58,72 @@ message SystemNotifyRequest {
|
||||
string content = 2;
|
||||
}
|
||||
|
||||
message ExtraScheduleItem {
|
||||
string schedule = 1;
|
||||
}
|
||||
|
||||
message CronItem {
|
||||
optional int32 id = 1;
|
||||
optional string command = 2;
|
||||
optional string schedule = 3;
|
||||
optional string name = 4;
|
||||
repeated string labels = 5;
|
||||
optional int32 sub_id = 6;
|
||||
repeated ExtraScheduleItem extra_schedules = 7;
|
||||
optional string task_before = 8;
|
||||
optional string task_after = 9;
|
||||
optional int32 status = 10;
|
||||
optional string log_path = 11;
|
||||
optional int32 pid = 12;
|
||||
optional int64 last_running_time = 13;
|
||||
optional int64 last_execution_time = 14;
|
||||
}
|
||||
|
||||
message CreateCronRequest {
|
||||
string command = 1;
|
||||
string schedule = 2;
|
||||
optional string name = 3;
|
||||
repeated string labels = 4;
|
||||
optional int32 sub_id = 5;
|
||||
repeated ExtraScheduleItem extra_schedules = 6;
|
||||
optional string task_before = 7;
|
||||
optional string task_after = 8;
|
||||
}
|
||||
|
||||
message UpdateCronRequest {
|
||||
int32 id = 1;
|
||||
optional string command = 2;
|
||||
optional string schedule = 3;
|
||||
optional string name = 4;
|
||||
repeated string labels = 5;
|
||||
optional int32 sub_id = 6;
|
||||
repeated ExtraScheduleItem extra_schedules = 7;
|
||||
optional string task_before = 8;
|
||||
optional string task_after = 9;
|
||||
}
|
||||
|
||||
message DeleteCronsRequest { repeated int32 ids = 1; }
|
||||
|
||||
message CronsResponse {
|
||||
int32 code = 1;
|
||||
repeated CronItem data = 2;
|
||||
optional string message = 3;
|
||||
}
|
||||
|
||||
message CronResponse {
|
||||
int32 code = 1;
|
||||
CronItem data = 2;
|
||||
optional string message = 3;
|
||||
}
|
||||
|
||||
message CronDetailRequest { string log_path = 1; }
|
||||
|
||||
message CronDetailResponse {
|
||||
int32 code = 1;
|
||||
CronItem data = 2;
|
||||
optional string message = 3;
|
||||
}
|
||||
|
||||
service Api {
|
||||
rpc GetEnvs(GetEnvsRequest) returns (EnvsResponse) {}
|
||||
rpc CreateEnv(CreateEnvRequest) returns (EnvsResponse) {}
|
||||
@@ -69,4 +135,8 @@ service Api {
|
||||
rpc UpdateEnvNames(UpdateEnvNamesRequest) returns (Response) {}
|
||||
rpc GetEnvById(GetEnvByIdRequest) returns (EnvResponse) {}
|
||||
rpc SystemNotify(SystemNotifyRequest) returns (Response) {}
|
||||
rpc GetCronDetail(CronDetailRequest) returns (CronDetailResponse) {}
|
||||
rpc CreateCron(CreateCronRequest) returns (CronResponse) {}
|
||||
rpc UpdateCron(UpdateCronRequest) returns (CronResponse) {}
|
||||
rpc DeleteCrons(DeleteCronsRequest) returns (Response) {}
|
||||
}
|
||||
+1460
-110
File diff suppressed because it is too large
Load Diff
+68
-51
@@ -1,10 +1,11 @@
|
||||
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-ts_proto v1.181.2
|
||||
// protoc-gen-ts_proto v2.6.1
|
||||
// protoc v3.17.3
|
||||
// source: back/protos/cron.proto
|
||||
|
||||
/* eslint-disable */
|
||||
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
|
||||
import {
|
||||
type CallOptions,
|
||||
ChannelCredentials,
|
||||
@@ -17,7 +18,6 @@ import {
|
||||
type ServiceError,
|
||||
type UntypedServiceImplementation,
|
||||
} from "@grpc/grpc-js";
|
||||
import _m0 from "protobufjs/minimal";
|
||||
|
||||
export const protobufPackage = "com.ql.cron";
|
||||
|
||||
@@ -29,7 +29,7 @@ export interface ICron {
|
||||
id: string;
|
||||
schedule: string;
|
||||
command: string;
|
||||
extraSchedules: ISchedule[];
|
||||
extra_schedules: ISchedule[];
|
||||
name: string;
|
||||
}
|
||||
|
||||
@@ -51,33 +51,34 @@ function createBaseISchedule(): ISchedule {
|
||||
return { schedule: "" };
|
||||
}
|
||||
|
||||
export const ISchedule = {
|
||||
encode(message: ISchedule, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
||||
export const ISchedule: MessageFns<ISchedule> = {
|
||||
encode(message: ISchedule, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.schedule !== "") {
|
||||
writer.uint32(10).string(message.schedule);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: _m0.Reader | Uint8Array, length?: number): ISchedule {
|
||||
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): ISchedule {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
let end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseISchedule();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1:
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.schedule = reader.string();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skipType(tag & 7);
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
@@ -105,11 +106,11 @@ export const ISchedule = {
|
||||
};
|
||||
|
||||
function createBaseICron(): ICron {
|
||||
return { id: "", schedule: "", command: "", extraSchedules: [], name: "" };
|
||||
return { id: "", schedule: "", command: "", extra_schedules: [], name: "" };
|
||||
}
|
||||
|
||||
export const ICron = {
|
||||
encode(message: ICron, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
||||
export const ICron: MessageFns<ICron> = {
|
||||
encode(message: ICron, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.id !== "") {
|
||||
writer.uint32(10).string(message.id);
|
||||
}
|
||||
@@ -119,8 +120,8 @@ export const ICron = {
|
||||
if (message.command !== "") {
|
||||
writer.uint32(26).string(message.command);
|
||||
}
|
||||
for (const v of message.extraSchedules) {
|
||||
ISchedule.encode(v!, writer.uint32(34).fork()).ldelim();
|
||||
for (const v of message.extra_schedules) {
|
||||
ISchedule.encode(v!, writer.uint32(34).fork()).join();
|
||||
}
|
||||
if (message.name !== "") {
|
||||
writer.uint32(42).string(message.name);
|
||||
@@ -128,53 +129,58 @@ export const ICron = {
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: _m0.Reader | Uint8Array, length?: number): ICron {
|
||||
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): ICron {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
let end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseICron();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1:
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.id = reader.string();
|
||||
continue;
|
||||
case 2:
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 18) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.schedule = reader.string();
|
||||
continue;
|
||||
case 3:
|
||||
}
|
||||
case 3: {
|
||||
if (tag !== 26) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.command = reader.string();
|
||||
continue;
|
||||
case 4:
|
||||
}
|
||||
case 4: {
|
||||
if (tag !== 34) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.extraSchedules.push(ISchedule.decode(reader, reader.uint32()));
|
||||
message.extra_schedules.push(ISchedule.decode(reader, reader.uint32()));
|
||||
continue;
|
||||
case 5:
|
||||
}
|
||||
case 5: {
|
||||
if (tag !== 42) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.name = reader.string();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skipType(tag & 7);
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
@@ -184,8 +190,8 @@ export const ICron = {
|
||||
id: isSet(object.id) ? globalThis.String(object.id) : "",
|
||||
schedule: isSet(object.schedule) ? globalThis.String(object.schedule) : "",
|
||||
command: isSet(object.command) ? globalThis.String(object.command) : "",
|
||||
extraSchedules: globalThis.Array.isArray(object?.extraSchedules)
|
||||
? object.extraSchedules.map((e: any) => ISchedule.fromJSON(e))
|
||||
extra_schedules: globalThis.Array.isArray(object?.extra_schedules)
|
||||
? object.extra_schedules.map((e: any) => ISchedule.fromJSON(e))
|
||||
: [],
|
||||
name: isSet(object.name) ? globalThis.String(object.name) : "",
|
||||
};
|
||||
@@ -202,8 +208,8 @@ export const ICron = {
|
||||
if (message.command !== "") {
|
||||
obj.command = message.command;
|
||||
}
|
||||
if (message.extraSchedules?.length) {
|
||||
obj.extraSchedules = message.extraSchedules.map((e) => ISchedule.toJSON(e));
|
||||
if (message.extra_schedules?.length) {
|
||||
obj.extra_schedules = message.extra_schedules.map((e) => ISchedule.toJSON(e));
|
||||
}
|
||||
if (message.name !== "") {
|
||||
obj.name = message.name;
|
||||
@@ -219,7 +225,7 @@ export const ICron = {
|
||||
message.id = object.id ?? "";
|
||||
message.schedule = object.schedule ?? "";
|
||||
message.command = object.command ?? "";
|
||||
message.extraSchedules = object.extraSchedules?.map((e) => ISchedule.fromPartial(e)) || [];
|
||||
message.extra_schedules = object.extra_schedules?.map((e) => ISchedule.fromPartial(e)) || [];
|
||||
message.name = object.name ?? "";
|
||||
return message;
|
||||
},
|
||||
@@ -229,33 +235,34 @@ function createBaseAddCronRequest(): AddCronRequest {
|
||||
return { crons: [] };
|
||||
}
|
||||
|
||||
export const AddCronRequest = {
|
||||
encode(message: AddCronRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
||||
export const AddCronRequest: MessageFns<AddCronRequest> = {
|
||||
encode(message: AddCronRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
for (const v of message.crons) {
|
||||
ICron.encode(v!, writer.uint32(10).fork()).ldelim();
|
||||
ICron.encode(v!, writer.uint32(10).fork()).join();
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: _m0.Reader | Uint8Array, length?: number): AddCronRequest {
|
||||
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): AddCronRequest {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(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:
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.crons.push(ICron.decode(reader, reader.uint32()));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skipType(tag & 7);
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
@@ -286,13 +293,13 @@ function createBaseAddCronResponse(): AddCronResponse {
|
||||
return {};
|
||||
}
|
||||
|
||||
export const AddCronResponse = {
|
||||
encode(_: AddCronResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
||||
export const AddCronResponse: MessageFns<AddCronResponse> = {
|
||||
encode(_: AddCronResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: _m0.Reader | Uint8Array, length?: number): AddCronResponse {
|
||||
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): AddCronResponse {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
let end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseAddCronResponse();
|
||||
while (reader.pos < end) {
|
||||
@@ -302,7 +309,7 @@ export const AddCronResponse = {
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skipType(tag & 7);
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
@@ -329,33 +336,34 @@ function createBaseDeleteCronRequest(): DeleteCronRequest {
|
||||
return { ids: [] };
|
||||
}
|
||||
|
||||
export const DeleteCronRequest = {
|
||||
encode(message: DeleteCronRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
||||
export const DeleteCronRequest: MessageFns<DeleteCronRequest> = {
|
||||
encode(message: DeleteCronRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
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);
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): DeleteCronRequest {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(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:
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.ids.push(reader.string());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skipType(tag & 7);
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
@@ -386,13 +394,13 @@ function createBaseDeleteCronResponse(): DeleteCronResponse {
|
||||
return {};
|
||||
}
|
||||
|
||||
export const DeleteCronResponse = {
|
||||
encode(_: DeleteCronResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
||||
export const DeleteCronResponse: MessageFns<DeleteCronResponse> = {
|
||||
encode(_: DeleteCronResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: _m0.Reader | Uint8Array, length?: number): DeleteCronResponse {
|
||||
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): DeleteCronResponse {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
let end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseDeleteCronResponse();
|
||||
while (reader.pos < end) {
|
||||
@@ -402,7 +410,7 @@ export const DeleteCronResponse = {
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skipType(tag & 7);
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
@@ -506,3 +514,12 @@ export type Exact<P, I extends P> = P extends Builtin ? P
|
||||
function isSet(value: any): boolean {
|
||||
return value !== null && value !== undefined;
|
||||
}
|
||||
|
||||
export interface MessageFns<T> {
|
||||
encode(message: T, writer?: BinaryWriter): BinaryWriter;
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): T;
|
||||
fromJSON(object: any): T;
|
||||
toJSON(message: T): unknown;
|
||||
create<I extends Exact<DeepPartial<T>, I>>(base?: I): T;
|
||||
fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T;
|
||||
}
|
||||
|
||||
+27
-16
@@ -1,25 +1,25 @@
|
||||
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-ts_proto v1.181.2
|
||||
// protoc-gen-ts_proto v2.6.1
|
||||
// protoc v3.17.3
|
||||
// source: back/protos/health.proto
|
||||
|
||||
/* eslint-disable */
|
||||
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
|
||||
import {
|
||||
type CallOptions,
|
||||
ChannelCredentials,
|
||||
Client,
|
||||
type ClientOptions,
|
||||
ClientReadableStream,
|
||||
type ClientReadableStream,
|
||||
type ClientUnaryCall,
|
||||
handleServerStreamingCall,
|
||||
type handleServerStreamingCall,
|
||||
type handleUnaryCall,
|
||||
makeGenericClientConstructor,
|
||||
Metadata,
|
||||
type ServiceError,
|
||||
type UntypedServiceImplementation,
|
||||
} from "@grpc/grpc-js";
|
||||
import _m0 from "protobufjs/minimal";
|
||||
|
||||
export const protobufPackage = "com.ql.health";
|
||||
|
||||
@@ -80,33 +80,34 @@ function createBaseHealthCheckRequest(): HealthCheckRequest {
|
||||
return { service: "" };
|
||||
}
|
||||
|
||||
export const HealthCheckRequest = {
|
||||
encode(message: HealthCheckRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
||||
export const HealthCheckRequest: MessageFns<HealthCheckRequest> = {
|
||||
encode(message: HealthCheckRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.service !== "") {
|
||||
writer.uint32(10).string(message.service);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: _m0.Reader | Uint8Array, length?: number): HealthCheckRequest {
|
||||
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): HealthCheckRequest {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
let end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseHealthCheckRequest();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1:
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.service = reader.string();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skipType(tag & 7);
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
@@ -137,33 +138,34 @@ function createBaseHealthCheckResponse(): HealthCheckResponse {
|
||||
return { status: 0 };
|
||||
}
|
||||
|
||||
export const HealthCheckResponse = {
|
||||
encode(message: HealthCheckResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
|
||||
export const HealthCheckResponse: MessageFns<HealthCheckResponse> = {
|
||||
encode(message: HealthCheckResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.status !== 0) {
|
||||
writer.uint32(8).int32(message.status);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
decode(input: _m0.Reader | Uint8Array, length?: number): HealthCheckResponse {
|
||||
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): HealthCheckResponse {
|
||||
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||
let end = length === undefined ? reader.len : reader.pos + length;
|
||||
const message = createBaseHealthCheckResponse();
|
||||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1:
|
||||
case 1: {
|
||||
if (tag !== 8) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.status = reader.int32() as any;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
}
|
||||
reader.skipType(tag & 7);
|
||||
reader.skip(tag & 7);
|
||||
}
|
||||
return message;
|
||||
},
|
||||
@@ -262,3 +264,12 @@ export type Exact<P, I extends P> = P extends Builtin ? P
|
||||
function isSet(value: any): boolean {
|
||||
return value !== null && value !== undefined;
|
||||
}
|
||||
|
||||
export interface MessageFns<T> {
|
||||
encode(message: T, writer?: BinaryWriter): BinaryWriter;
|
||||
decode(input: BinaryReader | Uint8Array, length?: number): T;
|
||||
fromJSON(object: any): T;
|
||||
toJSON(message: T): unknown;
|
||||
create<I extends Exact<DeepPartial<T>, I>>(base?: I): T;
|
||||
fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ const addCron = (
|
||||
callback: sendUnaryData<AddCronResponse>,
|
||||
) => {
|
||||
for (const item of call.request.crons) {
|
||||
const { id, schedule, command, extraSchedules, name } = item;
|
||||
const { id, schedule, command, extra_schedules, name } = item;
|
||||
if (scheduleStacks.has(id)) {
|
||||
scheduleStacks.get(id)?.forEach((x) => x.cancel());
|
||||
}
|
||||
@@ -23,8 +23,8 @@ const addCron = (
|
||||
command,
|
||||
);
|
||||
|
||||
if (extraSchedules?.length) {
|
||||
extraSchedules.forEach((x) => {
|
||||
if (extra_schedules?.length) {
|
||||
extra_schedules.forEach((x) => {
|
||||
Logger.info(
|
||||
'[schedule][创建定时任务], 任务ID: %s, 名称: %s, cron: %s, 执行命令: %s',
|
||||
id,
|
||||
@@ -40,8 +40,8 @@ const addCron = (
|
||||
Logger.info(`[schedule][准备运行任务] 命令: ${command}`);
|
||||
runCron(command, item);
|
||||
}),
|
||||
...(extraSchedules?.length
|
||||
? extraSchedules.map((x) =>
|
||||
...(extra_schedules?.length
|
||||
? extra_schedules.map((x) =>
|
||||
nodeSchedule.scheduleJob(id, x.schedule, async () => {
|
||||
Logger.info(`[schedule][准备运行任务] 命令: ${command}`);
|
||||
runCron(command, item);
|
||||
|
||||
@@ -4,6 +4,7 @@ import EnvService from '../services/env';
|
||||
import { sendUnaryData, ServerUnaryCall } from '@grpc/grpc-js';
|
||||
import {
|
||||
CreateEnvRequest,
|
||||
CronItem,
|
||||
DeleteEnvsRequest,
|
||||
DisableEnvsRequest,
|
||||
EnableEnvsRequest,
|
||||
@@ -21,6 +22,15 @@ import {
|
||||
import LoggerInstance from '../loaders/logger';
|
||||
import pick from 'lodash/pick';
|
||||
import SystemService from '../services/system';
|
||||
import CronService from '../services/cron';
|
||||
import {
|
||||
CronDetailRequest,
|
||||
CronDetailResponse,
|
||||
CreateCronRequest,
|
||||
UpdateCronRequest,
|
||||
DeleteCronsRequest,
|
||||
CronResponse,
|
||||
} from '../protos/api';
|
||||
|
||||
Container.set('logger', LoggerInstance);
|
||||
|
||||
@@ -29,6 +39,13 @@ export const getEnvs = async (
|
||||
callback: sendUnaryData<EnvsResponse>,
|
||||
) => {
|
||||
try {
|
||||
if (!call.request.searchValue) {
|
||||
return callback(null, {
|
||||
code: 400,
|
||||
data: [],
|
||||
message: 'searchValue is required',
|
||||
});
|
||||
}
|
||||
const envService = Container.get(EnvService);
|
||||
const data = await envService.envs(call.request.searchValue);
|
||||
callback(null, {
|
||||
@@ -171,3 +188,88 @@ export const systemNotify = async (
|
||||
callback(e);
|
||||
}
|
||||
};
|
||||
|
||||
const normalizeCronData = (data: CronItem | null): CronItem | undefined => {
|
||||
if (!data) return undefined;
|
||||
return {
|
||||
...data,
|
||||
sub_id: data.sub_id ?? undefined,
|
||||
extra_schedules: data.extra_schedules ?? undefined,
|
||||
pid: data.pid ?? undefined,
|
||||
task_before: data.task_before ?? undefined,
|
||||
task_after: data.task_after ?? undefined,
|
||||
};
|
||||
};
|
||||
|
||||
export const getCronDetail = async (
|
||||
call: ServerUnaryCall<CronDetailRequest, CronDetailResponse>,
|
||||
callback: sendUnaryData<CronDetailResponse>,
|
||||
) => {
|
||||
try {
|
||||
if (!call.request.log_path) {
|
||||
return callback(null, {
|
||||
code: 400,
|
||||
data: undefined,
|
||||
message: 'log_path is required',
|
||||
});
|
||||
}
|
||||
const cronService = Container.get(CronService);
|
||||
const data = (await cronService.find({
|
||||
log_path: call.request.log_path,
|
||||
})) as CronItem;
|
||||
callback(null, { code: 200, data: normalizeCronData(data) });
|
||||
} catch (e: any) {
|
||||
callback(e);
|
||||
}
|
||||
};
|
||||
|
||||
export const createCron = async (
|
||||
call: ServerUnaryCall<CreateCronRequest, CronResponse>,
|
||||
callback: sendUnaryData<CronResponse>,
|
||||
) => {
|
||||
try {
|
||||
const cronService = Container.get(CronService);
|
||||
const data = (await cronService.create(call.request)) as CronItem;
|
||||
callback(null, { code: 200, data: normalizeCronData(data) });
|
||||
} catch (e: any) {
|
||||
callback(e);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateCron = async (
|
||||
call: ServerUnaryCall<UpdateCronRequest, CronResponse>,
|
||||
callback: sendUnaryData<CronResponse>,
|
||||
) => {
|
||||
try {
|
||||
const cronService = Container.get(CronService);
|
||||
const { id, ...fields } = call.request;
|
||||
|
||||
const updateRequest = {
|
||||
id,
|
||||
...Object.entries(fields).reduce((acc: any, [key, value]) => {
|
||||
if (value !== undefined) {
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {}),
|
||||
} as UpdateCronRequest;
|
||||
|
||||
const data = (await cronService.update(updateRequest)) as CronItem;
|
||||
callback(null, { code: 200, data: normalizeCronData(data) });
|
||||
} catch (e: any) {
|
||||
callback(e);
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteCrons = async (
|
||||
call: ServerUnaryCall<DeleteCronsRequest, Response>,
|
||||
callback: sendUnaryData<Response>,
|
||||
) => {
|
||||
try {
|
||||
const cronService = Container.get(CronService);
|
||||
await cronService.remove(call.request.ids);
|
||||
callback(null, { code: 200 });
|
||||
} catch (e: any) {
|
||||
callback(e);
|
||||
}
|
||||
};
|
||||
|
||||
+76
-28
@@ -22,6 +22,7 @@ import dayjs from 'dayjs';
|
||||
import pickBy from 'lodash/pickBy';
|
||||
import omit from 'lodash/omit';
|
||||
import { writeFileWithLock } from '../shared/utils';
|
||||
import { ScheduleType } from '../interface/schedule';
|
||||
|
||||
@Service()
|
||||
export default class CronService {
|
||||
@@ -35,22 +36,36 @@ export default class CronService {
|
||||
return false;
|
||||
}
|
||||
|
||||
private isOnceSchedule(schedule?: string) {
|
||||
return schedule?.startsWith(ScheduleType.ONCE);
|
||||
}
|
||||
|
||||
private isBootSchedule(schedule?: string) {
|
||||
return schedule?.startsWith(ScheduleType.BOOT);
|
||||
}
|
||||
|
||||
private isSpecialSchedule(schedule?: string) {
|
||||
return this.isOnceSchedule(schedule) || this.isBootSchedule(schedule);
|
||||
}
|
||||
|
||||
public async create(payload: Crontab): Promise<Crontab> {
|
||||
const tab = new Crontab(payload);
|
||||
tab.saved = false;
|
||||
const doc = await this.insert(tab);
|
||||
if (this.isNodeCron(doc)) {
|
||||
|
||||
if (this.isNodeCron(doc) && !this.isSpecialSchedule(doc.schedule)) {
|
||||
await cronClient.addCron([
|
||||
{
|
||||
name: doc.name || '',
|
||||
id: String(doc.id),
|
||||
schedule: doc.schedule!,
|
||||
command: this.makeCommand(doc),
|
||||
extraSchedules: doc.extra_schedules || [],
|
||||
extra_schedules: doc.extra_schedules || [],
|
||||
},
|
||||
]);
|
||||
}
|
||||
await this.set_crontab();
|
||||
|
||||
await this.setCrontab();
|
||||
return doc;
|
||||
}
|
||||
|
||||
@@ -58,29 +73,33 @@ export default class CronService {
|
||||
return await CrontabModel.create(payload, { returning: true });
|
||||
}
|
||||
|
||||
public async update(payload: Crontab): Promise<Crontab> {
|
||||
public async update(payload: Partial<Crontab>): Promise<Crontab> {
|
||||
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.isNodeCron(doc)) {
|
||||
await cronClient.delCron([String(doc.id)]);
|
||||
}
|
||||
if (this.isNodeCron(newDoc)) {
|
||||
|
||||
if (this.isNodeCron(newDoc) && !this.isSpecialSchedule(newDoc.schedule)) {
|
||||
await cronClient.addCron([
|
||||
{
|
||||
name: doc.name || '',
|
||||
id: String(newDoc.id),
|
||||
schedule: newDoc.schedule!,
|
||||
command: this.makeCommand(newDoc),
|
||||
extraSchedules: newDoc.extra_schedules || [],
|
||||
extra_schedules: newDoc.extra_schedules || [],
|
||||
},
|
||||
]);
|
||||
}
|
||||
await this.set_crontab();
|
||||
|
||||
await this.setCrontab();
|
||||
return newDoc;
|
||||
}
|
||||
|
||||
@@ -135,7 +154,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();
|
||||
await this.setCrontab();
|
||||
}
|
||||
|
||||
public async pin(ids: number[]) {
|
||||
@@ -179,8 +198,8 @@ export default class CronService {
|
||||
for (const col of viewQuery.filters) {
|
||||
const { property, value, operation } = col;
|
||||
let q: any = {};
|
||||
let operate2 = null;
|
||||
let operate = null;
|
||||
let operate2: any = null;
|
||||
let operate: any = null;
|
||||
switch (operation) {
|
||||
case 'Reg':
|
||||
operate = Op.like;
|
||||
@@ -203,9 +222,16 @@ export default class CronService {
|
||||
case 'Nin':
|
||||
q[Op.and] = [
|
||||
{
|
||||
[property]: {
|
||||
[Op.notIn]: Array.isArray(value) ? value : [value],
|
||||
},
|
||||
[Op.or]: [
|
||||
{
|
||||
[property]: {
|
||||
[Op.notIn]: Array.isArray(value) ? value : [value],
|
||||
},
|
||||
},
|
||||
{
|
||||
[property]: { [Op.is]: null },
|
||||
},
|
||||
],
|
||||
},
|
||||
property === 'status' && value.includes(2)
|
||||
? { isDisabled: { [Op.ne]: 1 } }
|
||||
@@ -320,10 +346,10 @@ export default class CronService {
|
||||
log_path,
|
||||
}: {
|
||||
log_path: string;
|
||||
}): Promise<Crontab | null> {
|
||||
}): Promise<Crontab | undefined> {
|
||||
try {
|
||||
const result = await CrontabModel.findOne({ where: { log_path } });
|
||||
return result;
|
||||
return result?.get({ plain: true });
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
@@ -374,7 +400,7 @@ export default class CronService {
|
||||
try {
|
||||
const result = await CrontabModel.findAll(condition);
|
||||
const count = await CrontabModel.count({ where: query });
|
||||
return { data: result, total: count };
|
||||
return { data: result.map((x) => x.get({ plain: true })), total: count };
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
@@ -424,7 +450,7 @@ export default class CronService {
|
||||
name: cron.name,
|
||||
command: cron.command,
|
||||
schedule: cron.schedule,
|
||||
extraSchedules: cron.extra_schedules,
|
||||
extra_schedules: cron.extra_schedules,
|
||||
};
|
||||
if (cron.status !== CrontabStatus.queued) {
|
||||
resolve(params);
|
||||
@@ -495,7 +521,7 @@ 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();
|
||||
await this.setCrontab();
|
||||
}
|
||||
|
||||
public async enabled(ids: number[]) {
|
||||
@@ -508,10 +534,10 @@ export default class CronService {
|
||||
id: String(doc.id),
|
||||
schedule: doc.schedule!,
|
||||
command: this.makeCommand(doc),
|
||||
extraSchedules: doc.extra_schedules || [],
|
||||
extra_schedules: doc.extra_schedules || [],
|
||||
}));
|
||||
await cronClient.addCron(sixCron);
|
||||
await this.set_crontab();
|
||||
await this.setCrontab();
|
||||
}
|
||||
|
||||
public async log(id: number) {
|
||||
@@ -579,7 +605,7 @@ export default class CronService {
|
||||
return crontab_job_string;
|
||||
}
|
||||
|
||||
private async set_crontab(data?: { data: Crontab[]; total: number }) {
|
||||
private async setCrontab(data?: { data: Crontab[]; total: number }) {
|
||||
const tabs = data ?? (await this.crontabs());
|
||||
var crontab_string = '';
|
||||
tabs.data.forEach((tab) => {
|
||||
@@ -587,7 +613,8 @@ export default class CronService {
|
||||
if (
|
||||
tab.isDisabled === 1 ||
|
||||
_schedule!.length !== 5 ||
|
||||
tab.extra_schedules?.length
|
||||
tab.extra_schedules?.length ||
|
||||
this.isSpecialSchedule(tab.schedule)
|
||||
) {
|
||||
crontab_string += '# ';
|
||||
crontab_string += tab.schedule;
|
||||
@@ -608,7 +635,7 @@ export default class CronService {
|
||||
await CrontabModel.update({ saved: true }, { where: {} });
|
||||
}
|
||||
|
||||
public import_crontab() {
|
||||
public importCrontab() {
|
||||
exec('crontab -l', (error, stdout, stderr) => {
|
||||
const lines = stdout.split('\n');
|
||||
const namePrefix = new Date().getTime();
|
||||
@@ -644,17 +671,38 @@ export default class CronService {
|
||||
|
||||
public async autosave_crontab() {
|
||||
const tabs = await this.crontabs();
|
||||
this.set_crontab(tabs);
|
||||
this.setCrontab(tabs);
|
||||
|
||||
const sixCron = tabs.data
|
||||
.filter((x) => this.isNodeCron(x) && x.isDisabled !== 1)
|
||||
const regularCrons = tabs.data
|
||||
.filter(
|
||||
(x) =>
|
||||
this.isNodeCron(x) &&
|
||||
x.isDisabled !== 1 &&
|
||||
!this.isSpecialSchedule(x.schedule),
|
||||
)
|
||||
.map((doc) => ({
|
||||
name: doc.name || '',
|
||||
id: String(doc.id),
|
||||
schedule: doc.schedule!,
|
||||
command: this.makeCommand(doc),
|
||||
extraSchedules: doc.extra_schedules || [],
|
||||
extra_schedules: doc.extra_schedules || [],
|
||||
}));
|
||||
await cronClient.addCron(sixCron);
|
||||
await cronClient.addCron(regularCrons);
|
||||
}
|
||||
|
||||
public async bootTask() {
|
||||
const tabs = await this.crontabs();
|
||||
const bootTasks = tabs.data.filter(
|
||||
(x) => !x.isDisabled && this.isBootSchedule(x.schedule),
|
||||
);
|
||||
if (bootTasks.length > 0) {
|
||||
await CrontabModel.update(
|
||||
{ status: CrontabStatus.queued },
|
||||
{ where: { id: bootTasks.map((t) => t.id!) } },
|
||||
);
|
||||
for (const task of bootTasks) {
|
||||
await this.runSingle(task.id!);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,11 +182,10 @@ export default class NotificationService {
|
||||
}
|
||||
|
||||
private async chat() {
|
||||
const { chatUrl, chatToken } = this.params;
|
||||
const url = `${chatUrl}${chatToken}`;
|
||||
const { synologyChatUrl } = this.params;
|
||||
try {
|
||||
const res: any = await got
|
||||
.post(url, {
|
||||
.post(synologyChatUrl, {
|
||||
...this.gotOption,
|
||||
body: `payload={"text":"${this.title}\n${this.content}"}`,
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
@@ -802,7 +801,7 @@ export default class NotificationService {
|
||||
webhookContentType,
|
||||
} = this.params;
|
||||
|
||||
if (!webhookUrl.includes('$title') && !webhookBody.includes('$title')) {
|
||||
if (!webhookUrl?.includes('$title') && !webhookBody?.includes('$title')) {
|
||||
throw new Error('Url 或者 Body 中必须包含 $title');
|
||||
}
|
||||
|
||||
|
||||
+22
-1
@@ -16,6 +16,7 @@ import {
|
||||
promiseExec,
|
||||
readDirs,
|
||||
rmPath,
|
||||
setSystemTimezone,
|
||||
} from '../config/util';
|
||||
import {
|
||||
DependenceModel,
|
||||
@@ -50,7 +51,10 @@ export default class SystemService {
|
||||
|
||||
public async getSystemConfig() {
|
||||
const doc = await this.getDb({ type: AuthDataType.systemConfig });
|
||||
return doc;
|
||||
return {
|
||||
...doc,
|
||||
info: { ...doc.info, timezone: doc.info?.timezone || 'Asia/Shanghai' },
|
||||
};
|
||||
}
|
||||
|
||||
private async updateAuthDb(payload: SystemInfo): Promise<SystemInfo> {
|
||||
@@ -471,4 +475,21 @@ export default class SystemService {
|
||||
await rmPath(path.join(config.systemLogPath, log.title));
|
||||
}
|
||||
}
|
||||
|
||||
public async updateTimezone(info: SystemModelInfo) {
|
||||
if (!info.timezone) {
|
||||
info.timezone = 'Asia/Shanghai';
|
||||
}
|
||||
const oDoc = await this.getSystemConfig();
|
||||
await this.updateAuthDb({
|
||||
...oDoc,
|
||||
info: { ...oDoc.info, ...info },
|
||||
});
|
||||
const success = await setSystemTimezone(info.timezone);
|
||||
if (success) {
|
||||
return { code: 200, data: info };
|
||||
} else {
|
||||
return { code: 400, message: '设置时区失败' };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,11 +347,12 @@ export default class UserService {
|
||||
}
|
||||
|
||||
public async resetAuthInfo(info: Partial<AuthInfo>) {
|
||||
const { retries, twoFactorActivated } = info;
|
||||
const { retries, twoFactorActivated, password } = info;
|
||||
const authInfo = await this.getAuthInfo();
|
||||
await this.updateAuthInfo(authInfo, {
|
||||
retries,
|
||||
twoFactorActivated,
|
||||
password,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Joi } from 'celebrate';
|
||||
import cron_parser from 'cron-parser';
|
||||
import { ScheduleType } from '../interface/schedule';
|
||||
|
||||
const validateSchedule = (value: string, helpers: any) => {
|
||||
if (
|
||||
value.startsWith(ScheduleType.ONCE) ||
|
||||
value.startsWith(ScheduleType.BOOT)
|
||||
) {
|
||||
return value;
|
||||
}
|
||||
|
||||
try {
|
||||
if (cron_parser.parseExpression(value).hasNext()) {
|
||||
return value;
|
||||
}
|
||||
} catch (e) {
|
||||
return helpers.error('any.invalid');
|
||||
}
|
||||
return helpers.error('any.invalid');
|
||||
};
|
||||
|
||||
export const scheduleSchema = Joi.string()
|
||||
.required()
|
||||
.custom(validateSchedule)
|
||||
.messages({
|
||||
'any.invalid': '无效的定时规则',
|
||||
'string.empty': '定时规则不能为空',
|
||||
});
|
||||
|
||||
export const commonCronSchema = {
|
||||
name: Joi.string().optional(),
|
||||
command: Joi.string().required(),
|
||||
schedule: scheduleSchema,
|
||||
labels: Joi.array().optional(),
|
||||
sub_id: Joi.number().optional().allow(null),
|
||||
extra_schedules: Joi.array().optional().allow(null),
|
||||
task_before: Joi.string().optional().allow('').allow(null),
|
||||
task_after: Joi.string().optional().allow('').allow(null),
|
||||
};
|
||||
+3
-3
@@ -13,7 +13,7 @@
|
||||
"schedule": "npm run build:back && node static/build/schedule/index.js",
|
||||
"public": "npm run build:back && node static/build/public.js",
|
||||
"update": "npm run build:back && node static/build/update.js",
|
||||
"gen:proto": "protoc --experimental_allow_proto3_optional --plugin=./node_modules/.bin/protoc-gen-ts_proto ./back/protos/*.proto --ts_proto_out=./ --ts_proto_opt=outputServices=grpc-js,env=node,esModuleInterop=true",
|
||||
"gen:proto": "protoc --experimental_allow_proto3_optional --plugin=./node_modules/.bin/protoc-gen-ts_proto ./back/protos/*.proto --ts_proto_out=./ --ts_proto_opt=outputServices=grpc-js,env=node,esModuleInterop=true,snakeToCamel=false",
|
||||
"gen:api": "python3 -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. ./back/protos/api.proto",
|
||||
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
|
||||
"postinstall": "max setup 2>/dev/null || true",
|
||||
@@ -88,7 +88,7 @@
|
||||
"node-schedule": "^2.1.0",
|
||||
"nodemailer": "^6.9.16",
|
||||
"p-queue-cjs": "7.3.4",
|
||||
"protobufjs": "^7.4.0",
|
||||
"@bufbuild/protobuf": "^2.2.3",
|
||||
"pstree.remy": "^1.1.8",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"sequelize": "^6.37.5",
|
||||
@@ -171,7 +171,7 @@
|
||||
"react-split-pane": "^0.1.92",
|
||||
"sockjs-client": "^1.6.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"ts-proto": "^1.146.0",
|
||||
"ts-proto": "^2.6.1",
|
||||
"tslib": "^2.4.0",
|
||||
"typescript": "5.2.2",
|
||||
"vh-check": "^2.0.5",
|
||||
|
||||
Generated
+27
-13
@@ -8,6 +8,9 @@ overrides:
|
||||
sqlite3: git+https://github.com/whyour/node-sqlite3.git#v1.0.3
|
||||
|
||||
dependencies:
|
||||
'@bufbuild/protobuf':
|
||||
specifier: ^2.2.3
|
||||
version: 2.2.3
|
||||
'@grpc/grpc-js':
|
||||
specifier: ^1.12.3
|
||||
version: 1.12.3
|
||||
@@ -101,9 +104,6 @@ dependencies:
|
||||
proper-lockfile:
|
||||
specifier: ^4.1.2
|
||||
version: 4.1.2
|
||||
protobufjs:
|
||||
specifier: ^7.4.0
|
||||
version: 7.4.0
|
||||
pstree.remy:
|
||||
specifier: ^1.1.8
|
||||
version: 1.1.8
|
||||
@@ -335,8 +335,8 @@ devDependencies:
|
||||
specifier: ^10.9.2
|
||||
version: 10.9.2(@types/node@17.0.45)(typescript@5.2.2)
|
||||
ts-proto:
|
||||
specifier: ^1.146.0
|
||||
version: 1.181.2
|
||||
specifier: ^2.6.1
|
||||
version: 2.6.1
|
||||
tslib:
|
||||
specifier: ^2.4.0
|
||||
version: 2.8.1
|
||||
@@ -1380,6 +1380,9 @@ packages:
|
||||
resolution: {integrity: sha512-h0OYmPR3A5Dfbetra/GzxBAzQk8sH7LhRkRUTdagX6nrtlUgJGYCTv4bBK33jsTQw9HDd8PE2x1Ma+iRKEDUsw==}
|
||||
dev: true
|
||||
|
||||
/@bufbuild/protobuf@2.2.3:
|
||||
resolution: {integrity: sha512-tFQoXHJdkEOSwj5tRIZSPNUuXK3RaR7T1nUrPgbYX1pUbvqqaaZAsfo+NXBPsz5rZMSKVFrgK1WL8Q/MSLvprg==}
|
||||
|
||||
/@chenshuai2144/sketch-color@1.0.9(react@18.3.1):
|
||||
resolution: {integrity: sha512-obzSy26cb7Pm7OprWyVpgMpIlrZpZ0B7vbrU0RMbvRg0YAI890S5Xy02Aj1Nhl4+KTbi1lVYHt6HQP8Hm9s+1w==}
|
||||
peerDependencies:
|
||||
@@ -3271,36 +3274,46 @@ packages:
|
||||
|
||||
/@protobufjs/aspromise@1.1.2:
|
||||
resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==}
|
||||
dev: false
|
||||
|
||||
/@protobufjs/base64@1.1.2:
|
||||
resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==}
|
||||
dev: false
|
||||
|
||||
/@protobufjs/codegen@2.0.4:
|
||||
resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==}
|
||||
dev: false
|
||||
|
||||
/@protobufjs/eventemitter@1.1.0:
|
||||
resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==}
|
||||
dev: false
|
||||
|
||||
/@protobufjs/fetch@1.1.0:
|
||||
resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==}
|
||||
dependencies:
|
||||
'@protobufjs/aspromise': 1.1.2
|
||||
'@protobufjs/inquire': 1.1.0
|
||||
dev: false
|
||||
|
||||
/@protobufjs/float@1.0.2:
|
||||
resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==}
|
||||
dev: false
|
||||
|
||||
/@protobufjs/inquire@1.1.0:
|
||||
resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==}
|
||||
dev: false
|
||||
|
||||
/@protobufjs/path@1.1.2:
|
||||
resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==}
|
||||
dev: false
|
||||
|
||||
/@protobufjs/pool@1.1.0:
|
||||
resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==}
|
||||
dev: false
|
||||
|
||||
/@protobufjs/utf8@1.1.0:
|
||||
resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==}
|
||||
dev: false
|
||||
|
||||
/@qixian.cs/path-to-regexp@6.1.0:
|
||||
resolution: {integrity: sha512-2jIiLiVZB1jnY7IIRQKtoV8Gnr7XIhk4mC88ONGunZE3hYt5IHUG4BE/6+JiTBjjEWQLBeWnZB8hGpppkufiVw==}
|
||||
@@ -9846,6 +9859,7 @@ packages:
|
||||
|
||||
/long@5.2.3:
|
||||
resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==}
|
||||
dev: false
|
||||
|
||||
/loose-envify@1.4.0:
|
||||
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
||||
@@ -11749,6 +11763,7 @@ packages:
|
||||
'@protobufjs/utf8': 1.1.0
|
||||
'@types/node': 17.0.45
|
||||
long: 5.2.3
|
||||
dev: false
|
||||
|
||||
/proxy-addr@2.0.7:
|
||||
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
||||
@@ -14528,21 +14543,20 @@ packages:
|
||||
dprint-node: 1.0.8
|
||||
dev: true
|
||||
|
||||
/ts-proto-descriptors@1.16.0:
|
||||
resolution: {integrity: sha512-3yKuzMLpltdpcyQji1PJZRfoo4OJjNieKTYkQY8pF7xGKsYz/RHe3aEe4KiRxcinoBmnEhmuI+yJTxLb922ULA==}
|
||||
/ts-proto-descriptors@2.0.0:
|
||||
resolution: {integrity: sha512-wHcTH3xIv11jxgkX5OyCSFfw27agpInAd6yh89hKG6zqIXnjW9SYqSER2CVQxdPj4czeOhGagNvZBEbJPy7qkw==}
|
||||
dependencies:
|
||||
long: 5.2.3
|
||||
protobufjs: 7.4.0
|
||||
'@bufbuild/protobuf': 2.2.3
|
||||
dev: true
|
||||
|
||||
/ts-proto@1.181.2:
|
||||
resolution: {integrity: sha512-knJ8dtjn2Pd0c5ZGZG8z9DMiD4PUY8iGI9T9tb8DvGdWRMkLpf0WcPO7G+7cmbZyxvNTAG6ci3fybEaFgMZIvg==}
|
||||
/ts-proto@2.6.1:
|
||||
resolution: {integrity: sha512-4LTT99MkwkF1+fIA0b2mZu/58Qlpq3Q1g53TwEMZZgR1w/uX00PoVT4Z8aKJxMw0LeKQD4s9NrJYsF27Clckrg==}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
'@bufbuild/protobuf': 2.2.3
|
||||
case-anything: 2.1.13
|
||||
protobufjs: 7.4.0
|
||||
ts-poet: 6.9.0
|
||||
ts-proto-descriptors: 1.16.0
|
||||
ts-proto-descriptors: 2.0.0
|
||||
dev: true
|
||||
|
||||
/tslib@1.14.1:
|
||||
|
||||
+148
-39
@@ -1,45 +1,154 @@
|
||||
const grpc = require('@grpc/grpc-js');
|
||||
const protoLoader = require('@grpc/proto-loader');
|
||||
const { join } = require('path');
|
||||
|
||||
const PROTO_PATH = `${process.env.QL_DIR}/back/protos/api.proto`;
|
||||
const options = {
|
||||
keepCase: true,
|
||||
longs: String,
|
||||
enums: String,
|
||||
defaults: true,
|
||||
};
|
||||
|
||||
const packageDefinition = protoLoader.loadSync(PROTO_PATH, options);
|
||||
const apiProto = grpc.loadPackageDefinition(packageDefinition).com.ql.api;
|
||||
|
||||
const client = new apiProto.Api(
|
||||
`0.0.0.0:5500`,
|
||||
grpc.credentials.createInsecure(),
|
||||
{ 'grpc.enable_http_proxy': 0 },
|
||||
);
|
||||
|
||||
const promisify = (fn) => {
|
||||
return (...args) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fn.call(client, ...args, (err, response) => {
|
||||
if (err) return reject(err);
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
class GrpcClient {
|
||||
static #config = {
|
||||
protoPath: join(process.env.QL_DIR, 'back/protos/api.proto'),
|
||||
serverAddress: '0.0.0.0:5500',
|
||||
protoOptions: {
|
||||
keepCase: true,
|
||||
longs: String,
|
||||
enums: String,
|
||||
defaults: true,
|
||||
},
|
||||
grpcOptions: {
|
||||
'grpc.enable_http_proxy': 0,
|
||||
'grpc.keepalive_time_ms': 120000,
|
||||
'grpc.keepalive_timeout_ms': 20000,
|
||||
'grpc.max_receive_message_length': 100 * 1024 * 1024,
|
||||
},
|
||||
defaultTimeout: 30000,
|
||||
};
|
||||
};
|
||||
|
||||
const api = {
|
||||
getEnvs: promisify(client.GetEnvs),
|
||||
createEnv: promisify(client.CreateEnv),
|
||||
updateEnv: promisify(client.UpdateEnv),
|
||||
deleteEnvs: promisify(client.DeleteEnvs),
|
||||
moveEnv: promisify(client.MoveEnv),
|
||||
disableEnvs: promisify(client.DisableEnvs),
|
||||
enableEnvs: promisify(client.EnableEnvs),
|
||||
updateEnvNames: promisify(client.UpdateEnvNames),
|
||||
getEnvById: promisify(client.GetEnvById),
|
||||
systemNotify: promisify(client.SystemNotify),
|
||||
};
|
||||
static #methods = [
|
||||
'getEnvs',
|
||||
'createEnv',
|
||||
'updateEnv',
|
||||
'deleteEnvs',
|
||||
'moveEnv',
|
||||
'disableEnvs',
|
||||
'enableEnvs',
|
||||
'updateEnvNames',
|
||||
'getEnvById',
|
||||
'systemNotify',
|
||||
'getCronDetail',
|
||||
'createCron',
|
||||
'updateCron',
|
||||
'deleteCrons',
|
||||
];
|
||||
|
||||
module.exports = api;
|
||||
#client;
|
||||
#api = {};
|
||||
|
||||
constructor() {
|
||||
this.#initializeClient();
|
||||
this.#bindMethods();
|
||||
}
|
||||
|
||||
#initializeClient() {
|
||||
try {
|
||||
const { protoPath, protoOptions, serverAddress, grpcOptions } =
|
||||
GrpcClient.#config;
|
||||
|
||||
const packageDefinition = protoLoader.loadSync(protoPath, protoOptions);
|
||||
const apiProto = grpc.loadPackageDefinition(packageDefinition).com.ql.api;
|
||||
|
||||
this.#client = new apiProto.Api(
|
||||
serverAddress,
|
||||
grpc.credentials.createInsecure(),
|
||||
grpcOptions,
|
||||
);
|
||||
|
||||
this.#checkConnection();
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize gRPC client:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
#checkConnection() {
|
||||
this.#client.waitForReady(Date.now() + 5000, (error) => {
|
||||
if (error) {
|
||||
console.error('gRPC client connection failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#promisifyMethod(methodName) {
|
||||
const capitalizedMethod =
|
||||
methodName.charAt(0).toUpperCase() + methodName.slice(1);
|
||||
const method = this.#client[capitalizedMethod].bind(this.#client);
|
||||
|
||||
return async (params = {}) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const metadata = new grpc.Metadata();
|
||||
const deadline = new Date(
|
||||
Date.now() + GrpcClient.#config.defaultTimeout,
|
||||
);
|
||||
|
||||
method(params, metadata, { deadline }, (error, response) => {
|
||||
if (error) {
|
||||
return reject(error);
|
||||
}
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
#bindMethods() {
|
||||
GrpcClient.#methods.forEach((method) => {
|
||||
this.#api[method] = this.#promisifyMethod(method);
|
||||
});
|
||||
}
|
||||
|
||||
getApi() {
|
||||
return {
|
||||
...this.#api,
|
||||
close: this.close.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.#client) {
|
||||
this.#client.close();
|
||||
this.#client = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const grpcClient = new GrpcClient();
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
grpcClient.close();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
grpcClient.close();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
if (reason instanceof Error) {
|
||||
if (reason.stack) {
|
||||
const relevantStack = reason.stack
|
||||
.split('\n')
|
||||
.filter((line) => {
|
||||
return (
|
||||
!line.includes('node:internal') &&
|
||||
!line.includes('node_modules/@grpc') &&
|
||||
!line.includes('processTicksAndRejections')
|
||||
);
|
||||
})
|
||||
.join('\n');
|
||||
console.error(relevantStack);
|
||||
}
|
||||
} else {
|
||||
console.error(reason);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = grpcClient.getApi();
|
||||
|
||||
+200
-30
@@ -2,7 +2,7 @@ import subprocess
|
||||
import json
|
||||
import tempfile
|
||||
import os
|
||||
from typing import Dict, List
|
||||
from typing import Dict, List, TypedDict, Optional
|
||||
from functools import wraps
|
||||
|
||||
|
||||
@@ -11,16 +11,170 @@ def error_handler(func):
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except json.JSONDecodeError as e:
|
||||
raise Exception(f"parse json error: {str(e)}")
|
||||
except subprocess.SubprocessError as e:
|
||||
raise Exception(f"node process error: {str(e)}")
|
||||
except TypeError as e:
|
||||
if "missing" in str(e):
|
||||
func_name = func.__name__
|
||||
annotations = func.__annotations__
|
||||
param_type = next(
|
||||
(t for name, t in annotations.items() if name != "return"), None
|
||||
)
|
||||
if param_type and hasattr(param_type, "__annotations__"):
|
||||
required_fields = {
|
||||
k: v
|
||||
for k, v in param_type.__annotations__.items()
|
||||
if not getattr(param_type, "__total__", True)
|
||||
or k in getattr(param_type, "__required_keys__", set())
|
||||
}
|
||||
fields_str = ", ".join(
|
||||
f'"{k}": {v.__name__}' for k, v in required_fields.items()
|
||||
)
|
||||
raise Exception(
|
||||
f"{func_name}() requires a dictionary with parameters: {{{fields_str}}}"
|
||||
) from None
|
||||
raise Exception(f"{str(e)}") from None
|
||||
except Exception as e:
|
||||
raise Exception(f"unknown error: {str(e)}")
|
||||
error_msg = str(e)
|
||||
if "Error:" in error_msg:
|
||||
error_msg = error_msg.split("Error:")[-1].split("\n")[0].strip()
|
||||
raise Exception(f"{error_msg}") from None
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class EnvItem(TypedDict, total=False):
|
||||
id: Optional[int]
|
||||
name: Optional[str]
|
||||
value: Optional[str]
|
||||
remarks: Optional[str]
|
||||
status: Optional[int]
|
||||
position: Optional[int]
|
||||
|
||||
|
||||
class GetEnvsParams(TypedDict, total=False):
|
||||
searchValue: str
|
||||
|
||||
|
||||
class CreateEnvParams(TypedDict):
|
||||
envs: List[EnvItem]
|
||||
|
||||
|
||||
class UpdateEnvParams(TypedDict):
|
||||
env: EnvItem
|
||||
|
||||
|
||||
class DeleteEnvsParams(TypedDict):
|
||||
ids: List[int]
|
||||
|
||||
|
||||
class MoveEnvParams(TypedDict):
|
||||
id: int
|
||||
fromIndex: int
|
||||
toIndex: int
|
||||
|
||||
|
||||
class DisableEnvsParams(TypedDict):
|
||||
ids: List[int]
|
||||
|
||||
|
||||
class EnableEnvsParams(TypedDict):
|
||||
ids: List[int]
|
||||
|
||||
|
||||
class UpdateEnvNamesParams(TypedDict):
|
||||
ids: List[int]
|
||||
name: str
|
||||
|
||||
|
||||
class GetEnvByIdParams(TypedDict):
|
||||
id: int
|
||||
|
||||
|
||||
class SystemNotifyParams(TypedDict):
|
||||
title: str
|
||||
content: str
|
||||
|
||||
|
||||
class EnvsResponse(TypedDict):
|
||||
code: int
|
||||
data: List[EnvItem]
|
||||
message: Optional[str]
|
||||
|
||||
|
||||
class EnvResponse(TypedDict):
|
||||
code: int
|
||||
data: EnvItem
|
||||
message: Optional[str]
|
||||
|
||||
|
||||
class Response(TypedDict):
|
||||
code: int
|
||||
message: Optional[str]
|
||||
|
||||
|
||||
class ExtraScheduleItem(TypedDict, total=False):
|
||||
schedule: Optional[str]
|
||||
|
||||
|
||||
class CronItem(TypedDict, total=False):
|
||||
id: Optional[int]
|
||||
command: Optional[str]
|
||||
schedule: Optional[str]
|
||||
name: Optional[str]
|
||||
labels: List[str]
|
||||
sub_id: Optional[int]
|
||||
extra_schedules: List[ExtraScheduleItem]
|
||||
task_before: Optional[str]
|
||||
task_after: Optional[str]
|
||||
status: Optional[int]
|
||||
log_path: Optional[str]
|
||||
pid: Optional[int]
|
||||
last_running_time: Optional[int]
|
||||
last_execution_time: Optional[int]
|
||||
|
||||
|
||||
class CreateCronParams(TypedDict):
|
||||
command: str
|
||||
schedule: str
|
||||
name: Optional[str]
|
||||
labels: List[str]
|
||||
sub_id: Optional[int]
|
||||
extra_schedules: List[ExtraScheduleItem]
|
||||
task_before: Optional[str]
|
||||
task_after: Optional[str]
|
||||
|
||||
|
||||
class UpdateCronParams(TypedDict):
|
||||
id: int
|
||||
command: str
|
||||
schedule: str
|
||||
name: Optional[str]
|
||||
labels: List[str]
|
||||
sub_id: Optional[int]
|
||||
extra_schedules: List[ExtraScheduleItem]
|
||||
task_before: Optional[str]
|
||||
task_after: Optional[str]
|
||||
|
||||
|
||||
class DeleteCronsParams(TypedDict):
|
||||
ids: List[int]
|
||||
|
||||
|
||||
class CronDetailParams(TypedDict):
|
||||
log_path: str
|
||||
|
||||
|
||||
class CronsResponse(TypedDict):
|
||||
code: int
|
||||
data: List[CronItem]
|
||||
message: Optional[str]
|
||||
|
||||
|
||||
class CronResponse(TypedDict):
|
||||
code: int
|
||||
data: CronItem
|
||||
message: Optional[str]
|
||||
|
||||
|
||||
class Client:
|
||||
def __init__(self):
|
||||
self.temp_dir = tempfile.mkdtemp(prefix="node_client_")
|
||||
@@ -46,7 +200,8 @@ class Client:
|
||||
}} catch (error) {{
|
||||
console.error(JSON.stringify({{
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
stack: error.stack,
|
||||
name: error.name
|
||||
}}));
|
||||
process.exit(1);
|
||||
}}
|
||||
@@ -56,58 +211,73 @@ class Client:
|
||||
with open(self.temp_script, "w", encoding="utf-8") as f:
|
||||
f.write(node_code)
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["node", self.temp_script],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
result = subprocess.run(
|
||||
["node", self.temp_script],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
error_data = json.loads(result.stderr)
|
||||
raise Exception(
|
||||
f"{error_data.get('name', 'Error')}: {error_data.get('stack')}"
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
error_data = json.loads(result.stderr)
|
||||
raise Exception(f"{error_data.get('stack')}")
|
||||
|
||||
return json.loads(result.stdout)
|
||||
except subprocess.TimeoutExpired:
|
||||
raise Exception("node process timeout")
|
||||
return json.loads(result.stdout)
|
||||
|
||||
@error_handler
|
||||
def getEnvs(self, params: Dict = None) -> Dict:
|
||||
def getEnvs(self, params: GetEnvsParams = None) -> EnvsResponse:
|
||||
return self._execute_node("getEnvs", params)
|
||||
|
||||
@error_handler
|
||||
def createEnv(self, data: Dict) -> Dict:
|
||||
def createEnv(self, data: CreateEnvParams) -> EnvsResponse:
|
||||
return self._execute_node("createEnv", data)
|
||||
|
||||
@error_handler
|
||||
def updateEnv(self, data: Dict) -> Dict:
|
||||
def updateEnv(self, data: UpdateEnvParams) -> EnvResponse:
|
||||
return self._execute_node("updateEnv", data)
|
||||
|
||||
@error_handler
|
||||
def deleteEnvs(self, data: Dict) -> Dict:
|
||||
def deleteEnvs(self, data: DeleteEnvsParams) -> Response:
|
||||
return self._execute_node("deleteEnvs", data)
|
||||
|
||||
@error_handler
|
||||
def moveEnv(self, data: Dict) -> Dict:
|
||||
def moveEnv(self, data: MoveEnvParams) -> EnvResponse:
|
||||
return self._execute_node("moveEnv", data)
|
||||
|
||||
@error_handler
|
||||
def disableEnvs(self, data: Dict) -> Dict:
|
||||
def disableEnvs(self, data: DisableEnvsParams) -> Response:
|
||||
return self._execute_node("disableEnvs", data)
|
||||
|
||||
@error_handler
|
||||
def enableEnvs(self, data: Dict) -> Dict:
|
||||
def enableEnvs(self, data: EnableEnvsParams) -> Response:
|
||||
return self._execute_node("enableEnvs", data)
|
||||
|
||||
@error_handler
|
||||
def updateEnvNames(self, data: Dict) -> Dict:
|
||||
def updateEnvNames(self, data: UpdateEnvNamesParams) -> Response:
|
||||
return self._execute_node("updateEnvNames", data)
|
||||
|
||||
@error_handler
|
||||
def getEnvById(self, data: Dict) -> Dict:
|
||||
def getEnvById(self, data: GetEnvByIdParams) -> EnvResponse:
|
||||
return self._execute_node("getEnvById", data)
|
||||
|
||||
@error_handler
|
||||
def systemNotify(self, data: Dict) -> Dict:
|
||||
def systemNotify(self, data: SystemNotifyParams) -> Response:
|
||||
return self._execute_node("systemNotify", data)
|
||||
|
||||
@error_handler
|
||||
def getCronDetail(self, data: CronDetailParams) -> CronResponse:
|
||||
return self._execute_node("getCronDetail", data)
|
||||
|
||||
@error_handler
|
||||
def createCron(self, data: CreateCronParams) -> CronResponse:
|
||||
return self._execute_node("createCron", data)
|
||||
|
||||
@error_handler
|
||||
def updateCron(self, data: UpdateCronParams) -> CronResponse:
|
||||
return self._execute_node("updateCron", data)
|
||||
|
||||
@error_handler
|
||||
def deleteCrons(self, data: DeleteCronsParams) -> Response:
|
||||
return self._execute_node("deleteCrons", data)
|
||||
|
||||
@@ -98,7 +98,7 @@ try {
|
||||
|
||||
run();
|
||||
|
||||
const { sendNotify } = require('./notify.js');
|
||||
const { sendNotify } = require('./__ql_notify__.js');
|
||||
global.QLAPI = {
|
||||
notify: sendNotify,
|
||||
...client,
|
||||
|
||||
@@ -107,7 +107,7 @@ try:
|
||||
|
||||
run()
|
||||
|
||||
from notify import send
|
||||
from __ql_notify__ import send
|
||||
|
||||
class BaseApi(Client):
|
||||
def notify(self, *args, **kwargs):
|
||||
|
||||
+1
-1
@@ -479,7 +479,7 @@ handle_task_end() {
|
||||
[[ "$diff_time" == 0 ]] && diff_time=1
|
||||
|
||||
if [[ $ID ]]; then
|
||||
local error=$(update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time")
|
||||
local error=$(update_cron "\"$ID\"" "1" "$$" "$log_path" "$begin_timestamp" "$diff_time")
|
||||
if [[ $error ]]; then
|
||||
error_message=", 任务状态更新失败(${error})"
|
||||
fi
|
||||
|
||||
+4
-1
@@ -543,6 +543,9 @@ main() {
|
||||
resettfa)
|
||||
eval update_auth_config "\\\"twoFactorActivated\\\":false" "禁用两步验证" $cmd
|
||||
;;
|
||||
resetpwd)
|
||||
eval update_auth_config "\\\"password\\\":\\\"$p2\\\"" "重置密码" $cmd
|
||||
;;
|
||||
*)
|
||||
eval echo -e "命令输入错误...\\\n" $cmd
|
||||
eval usage $cmd
|
||||
@@ -553,7 +556,7 @@ main() {
|
||||
local end_time=$(format_time "$time_format" "$etime")
|
||||
local end_timestamp=$(format_timestamp "$time_format" "$etime")
|
||||
local diff_time=$(($end_timestamp - $begin_timestamp))
|
||||
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
|
||||
[[ $ID ]] && update_cron "\"$ID\"" "1" "$$" "$log_path" "$begin_timestamp" "$diff_time"
|
||||
|
||||
if [[ "$p1" != "repo" ]] && [[ "$p1" != "raw" ]]; then
|
||||
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time 秒 " $cmd
|
||||
|
||||
@@ -186,6 +186,7 @@
|
||||
"秒后重试": "Retry after seconds",
|
||||
"在您的设备上打开两步验证应用程序以查看您的身份验证代码并验证您的身份。": "Open the two-factor authentication application on your device to view your authentication code and verify your identity.",
|
||||
"请选择脚本文件": "Please select a script file",
|
||||
"当前文件不支持预览": "The current file does not support preview",
|
||||
"清空日志": "Clear Logs",
|
||||
"设置": "Settings",
|
||||
"退出": "Exit",
|
||||
@@ -344,7 +345,7 @@
|
||||
"gotify的url地址,例如 https://push.example.de:8080": "gotify URL address, e.g., https://push.example.de:8080",
|
||||
"gotify的消息应用token码": "gotify message application token code",
|
||||
"推送消息的优先级": "Priority of Push Messages",
|
||||
"chat的url地址": "Chat URL address",
|
||||
"synologyChat的url地址": "Synology Chat Webhook URL address",
|
||||
"chat的token码": "Chat token code",
|
||||
"推送到个人QQ: http://127.0.0.1/send_private_msg,群:http://127.0.0.1/send_group_msg": "Push to personal QQ: http://127.0.0.1/send_private_msg, group: http://127.0.0.1/send_group_msg",
|
||||
"访问密钥": "Access Key",
|
||||
@@ -496,5 +497,9 @@
|
||||
"NPM 镜像源": "NPM Mirror Source",
|
||||
"PyPI 镜像源": "PyPI Mirror Source",
|
||||
"alpine linux 镜像源": "Alpine Linux Mirror Source",
|
||||
"如果恢复失败,可进入容器执行": "If recovery fails, you can enter the container and execute"
|
||||
"如果恢复失败,可进入容器执行": "If recovery fails, you can enter the container and execute",
|
||||
"常规定时": "Normal Timing",
|
||||
"手动运行": "Manual Run",
|
||||
"开机运行": "Boot Run",
|
||||
"时区": "Timezone"
|
||||
}
|
||||
|
||||
@@ -186,6 +186,7 @@
|
||||
"秒后重试": "秒后重试",
|
||||
"在您的设备上打开两步验证应用程序以查看您的身份验证代码并验证您的身份。": "在您的设备上打开两步验证应用程序以查看您的身份验证代码并验证您的身份。",
|
||||
"请选择脚本文件": "请选择脚本文件",
|
||||
"当前文件不支持预览": "当前文件不支持预览",
|
||||
"清空日志": "清空日志",
|
||||
"设置": "设置",
|
||||
"退出": "退出",
|
||||
@@ -344,7 +345,7 @@
|
||||
"gotify的url地址,例如 https://push.example.de:8080": "gotify的url地址,例如 https://push.example.de:8080",
|
||||
"gotify的消息应用token码": "gotify的消息应用token码",
|
||||
"推送消息的优先级": "推送消息的优先级",
|
||||
"chat的url地址": "chat的url地址",
|
||||
"synologyChat的url地址": "synologyChat的webhook url地址",
|
||||
"chat的token码": "chat的token码",
|
||||
"推送到个人QQ: http://127.0.0.1/send_private_msg,群:http://127.0.0.1/send_group_msg": "推送到个人QQ: http://127.0.0.1/send_private_msg,群:http://127.0.0.1/send_group_msg",
|
||||
"访问密钥": "访问密钥",
|
||||
@@ -496,5 +497,10 @@
|
||||
"NPM 镜像源": "NPM 镜像源",
|
||||
"PyPI 镜像源": "PyPI 镜像源",
|
||||
"alpine linux 镜像源": "alpine linux 镜像源",
|
||||
"如果恢复失败,可进入容器执行": "如果恢复失败,可进入容器执行"
|
||||
"如果恢复失败,可进入容器执行": "如果恢复失败,可进入容器执行",
|
||||
"常规定时": "常规定时",
|
||||
"手动运行": "手动运行",
|
||||
"开机运行": "开机运行",
|
||||
"时区": "时区"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ScheduleType } from './type';
|
||||
|
||||
export const scheduleTypeMap = {
|
||||
[ScheduleType.Normal]: '',
|
||||
[ScheduleType.Once]: '@once',
|
||||
[ScheduleType.Boot]: '@boot',
|
||||
};
|
||||
|
||||
export const getScheduleType = (schedule?: string): ScheduleType => {
|
||||
if (schedule?.startsWith('@once')) return ScheduleType.Once;
|
||||
if (schedule?.startsWith('@boot')) return ScheduleType.Boot;
|
||||
return ScheduleType.Normal;
|
||||
};
|
||||
+63
-54
@@ -1,64 +1,64 @@
|
||||
import intl from 'react-intl-universal';
|
||||
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
|
||||
import { SharedContext } from '@/layouts';
|
||||
import { getCommandScript, getCrontabsNextDate } from '@/utils';
|
||||
import config from '@/utils/config';
|
||||
import { diffTime } from '@/utils/date';
|
||||
import { request } from '@/utils/http';
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
CheckOutlined,
|
||||
ClockCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
CopyOutlined,
|
||||
DeleteOutlined,
|
||||
DownOutlined,
|
||||
EditOutlined,
|
||||
EllipsisOutlined,
|
||||
FieldTimeOutlined,
|
||||
Loading3QuartersOutlined,
|
||||
PlusOutlined,
|
||||
PushpinOutlined,
|
||||
SettingOutlined,
|
||||
StopOutlined,
|
||||
UnorderedListOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { PageContainer } from '@ant-design/pro-layout';
|
||||
import { history, useOutletContext } from '@umijs/max';
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
Input,
|
||||
MenuProps,
|
||||
message,
|
||||
Modal,
|
||||
Table,
|
||||
Tag,
|
||||
Space,
|
||||
Tooltip,
|
||||
Dropdown,
|
||||
Menu,
|
||||
Typography,
|
||||
Input,
|
||||
Popover,
|
||||
Tabs,
|
||||
Table,
|
||||
TablePaginationConfig,
|
||||
MenuProps,
|
||||
Tabs,
|
||||
Tag,
|
||||
Typography
|
||||
} from 'antd';
|
||||
import {
|
||||
ClockCircleOutlined,
|
||||
Loading3QuartersOutlined,
|
||||
CloseCircleOutlined,
|
||||
FileTextOutlined,
|
||||
EllipsisOutlined,
|
||||
PlayCircleOutlined,
|
||||
CheckCircleOutlined,
|
||||
EditOutlined,
|
||||
StopOutlined,
|
||||
DeleteOutlined,
|
||||
PauseCircleOutlined,
|
||||
FieldTimeOutlined,
|
||||
PushpinOutlined,
|
||||
DownOutlined,
|
||||
SettingOutlined,
|
||||
PlusOutlined,
|
||||
UnorderedListOutlined,
|
||||
CheckOutlined,
|
||||
CopyOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import config from '@/utils/config';
|
||||
import { PageContainer } from '@ant-design/pro-layout';
|
||||
import { request } from '@/utils/http';
|
||||
import CronModal, { CronLabelModal } from './modal';
|
||||
import CronLogModal from './logModal';
|
||||
import CronDetailModal from './detail';
|
||||
import { diffTime } from '@/utils/date';
|
||||
import { history, useOutletContext } from '@umijs/max';
|
||||
import './index.less';
|
||||
import ViewCreateModal from './viewCreateModal';
|
||||
import ViewManageModal from './viewManageModal';
|
||||
import { FilterValue, SorterResult } from 'antd/lib/table/interface';
|
||||
import { SharedContext } from '@/layouts';
|
||||
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
|
||||
import { getCommandScript, getCrontabsNextDate, parseCrontab } from '@/utils';
|
||||
import { ColumnProps } from 'antd/lib/table';
|
||||
import { useVT } from 'virtualizedtableforantd4';
|
||||
import { ICrontab, OperationName, OperationPath, CrontabStatus } from './type';
|
||||
import Name from '@/components/name';
|
||||
import { FilterValue, SorterResult } from 'antd/lib/table/interface';
|
||||
import dayjs from 'dayjs';
|
||||
import { noop, omit } from 'lodash';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import { useVT } from 'virtualizedtableforantd4';
|
||||
import { getScheduleType } from './const';
|
||||
import CronDetailModal from './detail';
|
||||
import './index.less';
|
||||
import CronLogModal from './logModal';
|
||||
import CronModal, { CronLabelModal } from './modal';
|
||||
import {
|
||||
CrontabStatus,
|
||||
ICrontab,
|
||||
OperationName,
|
||||
OperationPath,
|
||||
ScheduleType,
|
||||
} from './type';
|
||||
import ViewCreateModal from './viewCreateModal';
|
||||
import ViewManageModal from './viewManageModal';
|
||||
|
||||
const { Text, Paragraph, Link } = Typography;
|
||||
const { Search } = Input;
|
||||
@@ -263,7 +263,9 @@ const Crontab = () => {
|
||||
},
|
||||
},
|
||||
render: (text, record) => {
|
||||
return dayjs(record.nextRunTime).format('YYYY-MM-DD HH:mm:ss');
|
||||
return record.nextRunTime
|
||||
? dayjs(record.nextRunTime).format('YYYY-MM-DD HH:mm:ss')
|
||||
: '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -396,9 +398,14 @@ const Crontab = () => {
|
||||
|
||||
setValue(
|
||||
data.map((x) => {
|
||||
const scheduleType = getScheduleType(x.schedule);
|
||||
const nextRunTime =
|
||||
scheduleType === ScheduleType.Normal
|
||||
? getCrontabsNextDate(x.schedule, x.extra_schedules)
|
||||
: null;
|
||||
return {
|
||||
...x,
|
||||
nextRunTime: getCrontabsNextDate(x.schedule, x.extra_schedules),
|
||||
nextRunTime,
|
||||
subscription: subscriptionMap?.[x.sub_id],
|
||||
};
|
||||
}),
|
||||
@@ -806,7 +813,9 @@ const Crontab = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (viewConf && enabledCronViews && enabledCronViews.length > 0) {
|
||||
const view = enabledCronViews.slice(SHOW_TAB_COUNT).find((x) => x.id === viewConf.id);
|
||||
const view = enabledCronViews
|
||||
.slice(SHOW_TAB_COUNT)
|
||||
.find((x) => x.id === viewConf.id);
|
||||
setMoreMenuActive(!!view);
|
||||
}
|
||||
}, [viewConf, enabledCronViews]);
|
||||
|
||||
+106
-65
@@ -1,11 +1,13 @@
|
||||
import intl from 'react-intl-universal';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Modal, message, Input, Form, Button, Space } from 'antd';
|
||||
import { request } from '@/utils/http';
|
||||
import config from '@/utils/config';
|
||||
import cronParse from 'cron-parser';
|
||||
import EditableTagGroup from '@/components/tag';
|
||||
import config from '@/utils/config';
|
||||
import { request } from '@/utils/http';
|
||||
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { Button, Form, Input, Modal, Select, Space, message } from 'antd';
|
||||
import cronParse from 'cron-parser';
|
||||
import { useEffect, useState } from 'react';
|
||||
import intl from 'react-intl-universal';
|
||||
import { getScheduleType, scheduleTypeMap } from './const';
|
||||
import { ScheduleType } from './type';
|
||||
|
||||
const CronModal = ({
|
||||
cron,
|
||||
@@ -18,15 +20,26 @@ const CronModal = ({
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [scheduleType, setScheduleType] = useState<ScheduleType>(
|
||||
cron ? getScheduleType(cron.schedule) : ScheduleType.Normal,
|
||||
);
|
||||
|
||||
const handleOk = async (values: any) => {
|
||||
setLoading(true);
|
||||
const method = cron?.id ? 'put' : 'post';
|
||||
const payload = { ...values };
|
||||
if (cron?.id) {
|
||||
payload.id = cron.id;
|
||||
}
|
||||
try {
|
||||
const method = cron?.id ? 'put' : 'post';
|
||||
const payload = {
|
||||
...values,
|
||||
schedule:
|
||||
scheduleType !== ScheduleType.Normal
|
||||
? scheduleTypeMap[scheduleType]
|
||||
: values.schedule,
|
||||
};
|
||||
|
||||
if (cron?.id) {
|
||||
payload.id = cron.id;
|
||||
}
|
||||
|
||||
const { code, data } = await request[method](
|
||||
`${config.apiPrefix}crons`,
|
||||
payload,
|
||||
@@ -38,74 +51,57 @@ const CronModal = ({
|
||||
);
|
||||
handleCancel(data);
|
||||
}
|
||||
setLoading(false);
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
form.resetFields();
|
||||
setScheduleType(getScheduleType(cron?.schedule));
|
||||
}, [cron, visible]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={cron?.id ? intl.get('编辑任务') : intl.get('创建任务')}
|
||||
open={visible}
|
||||
forceRender
|
||||
centered
|
||||
maskClosable={false}
|
||||
onOk={() => {
|
||||
form
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
handleOk(values);
|
||||
})
|
||||
.catch((info) => {
|
||||
console.log('Validate Failed:', info);
|
||||
});
|
||||
}}
|
||||
onCancel={() => handleCancel()}
|
||||
confirmLoading={loading}
|
||||
const handleScheduleTypeChange = (type: ScheduleType) => {
|
||||
setScheduleType(type);
|
||||
form.setFieldValue('schedule', '');
|
||||
};
|
||||
|
||||
const renderScheduleOptions = () => (
|
||||
<Select
|
||||
defaultValue={scheduleType}
|
||||
value={scheduleType}
|
||||
onChange={handleScheduleTypeChange}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
name="form_in_modal"
|
||||
initialValues={cron}
|
||||
>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={intl.get('名称')}
|
||||
rules={[{ required: true, whitespace: true }]}
|
||||
>
|
||||
<Input placeholder={intl.get('请输入任务名称')} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="command"
|
||||
label={intl.get('命令/脚本')}
|
||||
rules={[{ required: true, whitespace: true }]}
|
||||
>
|
||||
<Input.TextArea
|
||||
rows={4}
|
||||
autoSize={{ minRows: 1, maxRows: 5 }}
|
||||
placeholder={intl.get(
|
||||
'支持输入脚本路径/任意系统可执行命令/task 脚本路径',
|
||||
)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Select.Option value={ScheduleType.Normal}>
|
||||
{intl.get('常规定时')}
|
||||
</Select.Option>
|
||||
<Select.Option value={ScheduleType.Once}>
|
||||
{intl.get('手动运行')}
|
||||
</Select.Option>
|
||||
<Select.Option value={ScheduleType.Boot}>
|
||||
{intl.get('开机运行')}
|
||||
</Select.Option>
|
||||
</Select>
|
||||
);
|
||||
|
||||
const renderScheduleFields = () => {
|
||||
if (scheduleType !== ScheduleType.Normal) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
name="schedule"
|
||||
label={intl.get('定时规则')}
|
||||
rules={[
|
||||
{ required: true },
|
||||
{
|
||||
validator: (rule, value) => {
|
||||
validator: (_, value) => {
|
||||
if (!value || cronParse.parseExpression(value).hasNext()) {
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
return Promise.reject(intl.get('Cron表达式格式有误'));
|
||||
}
|
||||
return Promise.reject(intl.get('Cron表达式格式有误'));
|
||||
},
|
||||
},
|
||||
]}
|
||||
@@ -136,14 +132,58 @@ const CronModal = ({
|
||||
))}
|
||||
<Form.Item>
|
||||
<a onClick={() => add({ schedule: '' })}>
|
||||
<PlusOutlined />
|
||||
{intl.get('新增定时规则')}
|
||||
<PlusOutlined /> {intl.get('新增定时规则')}
|
||||
</a>
|
||||
</Form.Item>
|
||||
<Form.ErrorList errors={errors} />
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={cron?.id ? intl.get('编辑任务') : intl.get('创建任务')}
|
||||
open={visible}
|
||||
forceRender
|
||||
centered
|
||||
maskClosable={false}
|
||||
onOk={() => form.validateFields().then(handleOk)}
|
||||
onCancel={() => handleCancel()}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
name="form_in_modal"
|
||||
initialValues={cron}
|
||||
>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={intl.get('名称')}
|
||||
rules={[{ required: true, whitespace: true }]}
|
||||
>
|
||||
<Input placeholder={intl.get('请输入任务名称')} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="command"
|
||||
label={intl.get('命令/脚本')}
|
||||
rules={[{ required: true, whitespace: true }]}
|
||||
>
|
||||
<Input.TextArea
|
||||
rows={4}
|
||||
autoSize={{ minRows: 1, maxRows: 5 }}
|
||||
placeholder={intl.get(
|
||||
'支持输入脚本路径/任意系统可执行命令/task 脚本路径',
|
||||
)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label={intl.get('定时类型')} required>
|
||||
{renderScheduleOptions()}
|
||||
</Form.Item>
|
||||
{renderScheduleFields()}
|
||||
<Form.Item name="labels" label={intl.get('标签')}>
|
||||
<EditableTagGroup />
|
||||
</Form.Item>
|
||||
@@ -155,7 +195,7 @@ const CronModal = ({
|
||||
)}
|
||||
rules={[
|
||||
{
|
||||
validator(rule, value) {
|
||||
validator(_, value) {
|
||||
if (
|
||||
value &&
|
||||
(value.includes(' task ') || value.startsWith('task '))
|
||||
@@ -183,7 +223,7 @@ const CronModal = ({
|
||||
)}
|
||||
rules={[
|
||||
{
|
||||
validator(rule, value) {
|
||||
validator(_, value) {
|
||||
if (
|
||||
value &&
|
||||
(value.includes(' task ') || value.startsWith('task '))
|
||||
@@ -284,4 +324,5 @@ const CronLabelModal = ({
|
||||
);
|
||||
};
|
||||
|
||||
export { CronModal as default, CronLabelModal };
|
||||
export { CronLabelModal, CronModal as default };
|
||||
|
||||
|
||||
@@ -36,5 +36,11 @@ export interface ICrontab {
|
||||
last_execution_time?: number;
|
||||
nextRunTime: Date;
|
||||
sub_id: number;
|
||||
extra_schedules?: Array<{ schedule: string; }>;
|
||||
extra_schedules?: Array<{ schedule: string }>;
|
||||
}
|
||||
|
||||
export enum ScheduleType {
|
||||
Normal = 'normal',
|
||||
Once = 'once',
|
||||
Boot = 'boot',
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ import RenameModal from './renameModal';
|
||||
import { langs } from '@uiw/codemirror-extensions-langs';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import { canPreviewInMonaco } from '@/utils/monaco';
|
||||
const { Text } = Typography;
|
||||
|
||||
const Script = () => {
|
||||
@@ -67,6 +68,10 @@ const Script = () => {
|
||||
const [currentNode, setCurrentNode] = useState<any>();
|
||||
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
|
||||
|
||||
const handleIsEditing = (filename: string, value: boolean) => {
|
||||
setIsEditing(value && canPreviewInMonaco(filename));
|
||||
};
|
||||
|
||||
const getScripts = (needLoading: boolean = true) => {
|
||||
needLoading && setLoading(true);
|
||||
request
|
||||
@@ -128,6 +133,11 @@ const Script = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!canPreviewInMonaco(node.title)) {
|
||||
setValue(intl.get('当前文件不支持预览'));
|
||||
return;
|
||||
}
|
||||
|
||||
const newMode = getEditorMode(value);
|
||||
setMode(isPhone && newMode === 'typescript' ? 'javascript' : newMode);
|
||||
setValue(intl.get('加载中...'));
|
||||
@@ -149,14 +159,14 @@ const Script = () => {
|
||||
content: <>{intl.get('当前修改未保存,确定离开吗')}</>,
|
||||
onOk() {
|
||||
onSelect(keys[0], e.node);
|
||||
setIsEditing(false);
|
||||
handleIsEditing(e.node.title, false);
|
||||
},
|
||||
onCancel() {
|
||||
console.log('Cancel');
|
||||
},
|
||||
});
|
||||
} else {
|
||||
setIsEditing(false);
|
||||
handleIsEditing(e.node.title, false);
|
||||
onSelect(keys[0], e.node);
|
||||
}
|
||||
},
|
||||
@@ -196,18 +206,18 @@ const Script = () => {
|
||||
if (node.type === 'file') {
|
||||
setSelect(node.key);
|
||||
setCurrentNode(node);
|
||||
setIsEditing(true);
|
||||
handleIsEditing(node.title, true);
|
||||
}
|
||||
};
|
||||
|
||||
const editFile = () => {
|
||||
setTimeout(() => {
|
||||
setIsEditing(true);
|
||||
handleIsEditing(currentNode.title, true);
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const cancelEdit = () => {
|
||||
setIsEditing(false);
|
||||
handleIsEditing(currentNode.title, false);
|
||||
setValue(intl.get('加载中...'));
|
||||
getDetail(currentNode);
|
||||
};
|
||||
@@ -240,7 +250,7 @@ const Script = () => {
|
||||
if (code === 200) {
|
||||
message.success(`保存成功`);
|
||||
setValue(content);
|
||||
setIsEditing(false);
|
||||
handleIsEditing(currentNode.title, false);
|
||||
}
|
||||
resolve(null);
|
||||
})
|
||||
@@ -342,7 +352,7 @@ const Script = () => {
|
||||
}
|
||||
setData(newData);
|
||||
onSelect(_file.title, _file);
|
||||
setIsEditing(true);
|
||||
handleIsEditing(_file.title, true);
|
||||
}
|
||||
setIsAddFileModalVisible(false);
|
||||
};
|
||||
@@ -472,7 +482,9 @@ const Script = () => {
|
||||
label: intl.get('编辑'),
|
||||
key: 'edit',
|
||||
icon: <EditOutlined />,
|
||||
disabled: !select,
|
||||
disabled:
|
||||
!select ||
|
||||
(currentNode && !canPreviewInMonaco(currentNode?.title)),
|
||||
},
|
||||
{
|
||||
label: intl.get('重命名'),
|
||||
@@ -554,7 +566,10 @@ const Script = () => {
|
||||
</Tooltip>,
|
||||
<Tooltip title={intl.get('编辑')}>
|
||||
<Button
|
||||
disabled={!select}
|
||||
disabled={
|
||||
!select ||
|
||||
(currentNode && !canPreviewInMonaco(currentNode?.title))
|
||||
}
|
||||
type="primary"
|
||||
onClick={editFile}
|
||||
icon={<EditOutlined />}
|
||||
|
||||
@@ -32,3 +32,13 @@
|
||||
display: flex;
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
.ql-container-wrapper.ql-setting-container {
|
||||
.ant-tabs-tabpane > div {
|
||||
padding-left: 2px;
|
||||
}
|
||||
|
||||
.ant-tabs-tabpane > .ant-form {
|
||||
padding-left: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,10 +23,12 @@ import Countdown from 'antd/lib/statistic/Countdown';
|
||||
import useProgress from './progress';
|
||||
import pick from 'lodash/pick';
|
||||
import { disableBody } from '@/utils';
|
||||
import { TIMEZONES } from '@/utils/const';
|
||||
|
||||
const dataMap = {
|
||||
'log-remove-frequency': 'logRemoveFrequency',
|
||||
'cron-concurrency': 'cronConcurrency',
|
||||
timezone: 'timezone',
|
||||
};
|
||||
|
||||
const Other = ({
|
||||
@@ -37,6 +39,7 @@ const Other = ({
|
||||
const [systemConfig, setSystemConfig] = useState<{
|
||||
logRemoveFrequency?: number | null;
|
||||
cronConcurrency?: number | null;
|
||||
timezone?: string | null;
|
||||
}>();
|
||||
const [form] = Form.useForm();
|
||||
const [exportLoading, setExportLoading] = useState(false);
|
||||
@@ -252,6 +255,34 @@ const Other = ({
|
||||
</Button>
|
||||
</Input.Group>
|
||||
</Form.Item>
|
||||
<Form.Item label={intl.get('时区')} name="timezone">
|
||||
<Input.Group compact>
|
||||
<Select
|
||||
value={systemConfig?.timezone}
|
||||
style={{ width: 180 }}
|
||||
onChange={(value) => {
|
||||
setSystemConfig({ ...systemConfig, timezone: value });
|
||||
}}
|
||||
options={TIMEZONES.map((timezone) => ({
|
||||
value: timezone,
|
||||
label: timezone,
|
||||
}))}
|
||||
showSearch
|
||||
filterOption={(input, option) =>
|
||||
option?.value?.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
updateSystemConfig('timezone');
|
||||
}}
|
||||
style={{ width: 84 }}
|
||||
>
|
||||
{intl.get('确认')}
|
||||
</Button>
|
||||
</Input.Group>
|
||||
</Form.Item>
|
||||
<Form.Item label={intl.get('语言')} name="lang">
|
||||
<Select
|
||||
defaultValue={localStorage.getItem('lang') || ''}
|
||||
|
||||
+18
-23
@@ -135,11 +135,10 @@ export default {
|
||||
],
|
||||
chat: [
|
||||
{
|
||||
label: 'chatUrl',
|
||||
tip: intl.get('chat的url地址'),
|
||||
label: 'synologyChatUrl',
|
||||
tip: intl.get('synologyChat的url地址'),
|
||||
required: true,
|
||||
},
|
||||
{ label: 'chatToken', tip: intl.get('chat的token码'), required: true },
|
||||
],
|
||||
goCqHttpBot: [
|
||||
{
|
||||
@@ -329,33 +328,23 @@ export default {
|
||||
},
|
||||
{
|
||||
label: 'pushplusTemplate',
|
||||
tip: intl.get(
|
||||
'发送模板',
|
||||
),
|
||||
tip: intl.get('发送模板'),
|
||||
},
|
||||
{
|
||||
label: 'pushplusChannel',
|
||||
tip: intl.get(
|
||||
'发送渠道',
|
||||
),
|
||||
tip: intl.get('发送渠道'),
|
||||
},
|
||||
{
|
||||
label: 'pushplusWebhook',
|
||||
tip: intl.get(
|
||||
'webhook编码',
|
||||
),
|
||||
tip: intl.get('webhook编码'),
|
||||
},
|
||||
{
|
||||
label: 'pushplusCallbackUrl',
|
||||
tip: intl.get(
|
||||
'发送结果回调地址',
|
||||
),
|
||||
tip: intl.get('发送结果回调地址'),
|
||||
},
|
||||
{
|
||||
label: 'pushplusTo',
|
||||
tip: intl.get(
|
||||
'好友令牌',
|
||||
),
|
||||
tip: intl.get('好友令牌'),
|
||||
},
|
||||
],
|
||||
wePlusBot: [
|
||||
@@ -368,9 +357,7 @@ export default {
|
||||
},
|
||||
{
|
||||
label: 'wePlusBotReceiver',
|
||||
tip: intl.get(
|
||||
'消息接收人',
|
||||
),
|
||||
tip: intl.get('消息接收人'),
|
||||
},
|
||||
{
|
||||
label: 'wePlusBotVersion',
|
||||
@@ -414,7 +401,13 @@ export default {
|
||||
required: true,
|
||||
},
|
||||
{ label: 'emailUser', tip: intl.get('邮箱地址'), required: true },
|
||||
{ label: 'emailPass', tip: intl.get('SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定'), required: true },
|
||||
{
|
||||
label: 'emailPass',
|
||||
tip: intl.get(
|
||||
'SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定',
|
||||
),
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
pushMe: [
|
||||
{
|
||||
@@ -424,7 +417,9 @@ export default {
|
||||
},
|
||||
{
|
||||
label: 'pushMeUrl',
|
||||
tip: intl.get('自建的PushMeServer消息接口地址,例如:http://127.0.0.1:3010,不填则使用官方消息接口'),
|
||||
tip: intl.get(
|
||||
'自建的PushMeServer消息接口地址,例如:http://127.0.0.1:3010,不填则使用官方消息接口',
|
||||
),
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
|
||||
+450
-1
@@ -7,5 +7,454 @@ export const LANG_MAP = {
|
||||
'.sh': 'shell',
|
||||
'.ts': 'typescript',
|
||||
'.ini': 'ini',
|
||||
'.json': 'json'
|
||||
'.json': 'json',
|
||||
};
|
||||
|
||||
export const TIMEZONES = [
|
||||
'UTC',
|
||||
'Africa/Abidjan',
|
||||
'Africa/Accra',
|
||||
'Africa/Addis Ababa',
|
||||
'Africa/Algiers',
|
||||
'Africa/Asmara',
|
||||
'Africa/Bamako',
|
||||
'Africa/Bangui',
|
||||
'Africa/Banjul',
|
||||
'Africa/Bissau',
|
||||
'Africa/Blantyre',
|
||||
'Africa/Brazzaville',
|
||||
'Africa/Bujumbura',
|
||||
'Africa/Cairo',
|
||||
'Africa/Casablanca',
|
||||
'Africa/Ceuta',
|
||||
'Africa/Conakry',
|
||||
'Africa/Dakar',
|
||||
'Africa/Dar es Salaam',
|
||||
'Africa/Djibouti',
|
||||
'Africa/Douala',
|
||||
'Africa/El Aaiun',
|
||||
'Africa/Freetown',
|
||||
'Africa/Gaborone',
|
||||
'Africa/Harare',
|
||||
'Africa/Johannesburg',
|
||||
'Africa/Juba',
|
||||
'Africa/Kampala',
|
||||
'Africa/Khartoum',
|
||||
'Africa/Kigali',
|
||||
'Africa/Kinshasa',
|
||||
'Africa/Lagos',
|
||||
'Africa/Libreville',
|
||||
'Africa/Lome',
|
||||
'Africa/Luanda',
|
||||
'Africa/Lubumbashi',
|
||||
'Africa/Lusaka',
|
||||
'Africa/Malabo',
|
||||
'Africa/Maputo',
|
||||
'Africa/Maseru',
|
||||
'Africa/Mbabane',
|
||||
'Africa/Mogadishu',
|
||||
'Africa/Monrovia',
|
||||
'Africa/Nairobi',
|
||||
'Africa/Ndjamena',
|
||||
'Africa/Niamey',
|
||||
'Africa/Nouakchott',
|
||||
'Africa/Ouagadougou',
|
||||
'Africa/Porto-Novo',
|
||||
'Africa/Sao Tome',
|
||||
'Africa/Tripoli',
|
||||
'Africa/Tunis',
|
||||
'Africa/Windhoek',
|
||||
'America/Adak',
|
||||
'America/Anchorage',
|
||||
'America/Anguilla',
|
||||
'America/Antigua',
|
||||
'America/Araguaina',
|
||||
'America/Argentina/Buenos Aires',
|
||||
'America/Argentina/Catamarca',
|
||||
'America/Argentina/Cordoba',
|
||||
'America/Argentina/Jujuy',
|
||||
'America/Argentina/La Rioja',
|
||||
'America/Argentina/Mendoza',
|
||||
'America/Argentina/Rio Gallegos',
|
||||
'America/Argentina/Salta',
|
||||
'America/Argentina/San Juan',
|
||||
'America/Argentina/San Luis',
|
||||
'America/Argentina/Tucuman',
|
||||
'America/Argentina/Ushuaia',
|
||||
'America/Aruba',
|
||||
'America/Asuncion',
|
||||
'America/Atikokan',
|
||||
'America/Bahia',
|
||||
'America/Bahia Banderas',
|
||||
'America/Barbados',
|
||||
'America/Belem',
|
||||
'America/Belize',
|
||||
'America/Blanc-Sablon',
|
||||
'America/Boa Vista',
|
||||
'America/Bogota',
|
||||
'America/Boise',
|
||||
'America/Cambridge Bay',
|
||||
'America/Campo Grande',
|
||||
'America/Cancun',
|
||||
'America/Caracas',
|
||||
'America/Cayenne',
|
||||
'America/Cayman',
|
||||
'America/Chicago',
|
||||
'America/Chihuahua',
|
||||
'America/Ciudad Juarez',
|
||||
'America/Costa Rica',
|
||||
'America/Creston',
|
||||
'America/Cuiaba',
|
||||
'America/Curacao',
|
||||
'America/Danmarkshavn',
|
||||
'America/Dawson',
|
||||
'America/Dawson Creek',
|
||||
'America/Denver',
|
||||
'America/Detroit',
|
||||
'America/Dominica',
|
||||
'America/Edmonton',
|
||||
'America/Eirunepe',
|
||||
'America/El Salvador',
|
||||
'America/Fort Nelson',
|
||||
'America/Fortaleza',
|
||||
'America/Glace Bay',
|
||||
'America/Goose Bay',
|
||||
'America/Grand Turk',
|
||||
'America/Grenada',
|
||||
'America/Guadeloupe',
|
||||
'America/Guatemala',
|
||||
'America/Guayaquil',
|
||||
'America/Guyana',
|
||||
'America/Halifax',
|
||||
'America/Havana',
|
||||
'America/Hermosillo',
|
||||
'America/Indiana/Indianapolis',
|
||||
'America/Indiana/Knox',
|
||||
'America/Indiana/Marengo',
|
||||
'America/Indiana/Petersburg',
|
||||
'America/Indiana/Tell City',
|
||||
'America/Indiana/Vevay',
|
||||
'America/Indiana/Vincennes',
|
||||
'America/Indiana/Winamac',
|
||||
'America/Inuvik',
|
||||
'America/Iqaluit',
|
||||
'America/Jamaica',
|
||||
'America/Juneau',
|
||||
'America/Kentucky/Louisville',
|
||||
'America/Kentucky/Monticello',
|
||||
'America/Kralendijk',
|
||||
'America/La Paz',
|
||||
'America/Lima',
|
||||
'America/Los Angeles',
|
||||
'America/Lower Princes',
|
||||
'America/Maceio',
|
||||
'America/Managua',
|
||||
'America/Manaus',
|
||||
'America/Marigot',
|
||||
'America/Martinique',
|
||||
'America/Matamoros',
|
||||
'America/Mazatlan',
|
||||
'America/Menominee',
|
||||
'America/Merida',
|
||||
'America/Metlakatla',
|
||||
'America/Mexico City',
|
||||
'America/Miquelon',
|
||||
'America/Moncton',
|
||||
'America/Monterrey',
|
||||
'America/Montevideo',
|
||||
'America/Montserrat',
|
||||
'America/Nassau',
|
||||
'America/New York',
|
||||
'America/Nome',
|
||||
'America/Noronha',
|
||||
'America/North Dakota/Beulah',
|
||||
'America/North Dakota/Center',
|
||||
'America/North Dakota/New Salem',
|
||||
'America/Nuuk',
|
||||
'America/Ojinaga',
|
||||
'America/Panama',
|
||||
'America/Paramaribo',
|
||||
'America/Phoenix',
|
||||
'America/Port of Spain',
|
||||
'America/Port-au-Prince',
|
||||
'America/Porto Velho',
|
||||
'America/Puerto Rico',
|
||||
'America/Punta Arenas',
|
||||
'America/Rankin Inlet',
|
||||
'America/Recife',
|
||||
'America/Regina',
|
||||
'America/Resolute',
|
||||
'America/Rio Branco',
|
||||
'America/Santarem',
|
||||
'America/Santiago',
|
||||
'America/Santo Domingo',
|
||||
'America/Sao Paulo',
|
||||
'America/Scoresbysund',
|
||||
'America/Sitka',
|
||||
'America/St Barthelemy',
|
||||
'America/St Johns',
|
||||
'America/St Kitts',
|
||||
'America/St Lucia',
|
||||
'America/St Thomas',
|
||||
'America/St Vincent',
|
||||
'America/Swift Current',
|
||||
'America/Tegucigalpa',
|
||||
'America/Thule',
|
||||
'America/Tijuana',
|
||||
'America/Toronto',
|
||||
'America/Tortola',
|
||||
'America/Vancouver',
|
||||
'America/Whitehorse',
|
||||
'America/Winnipeg',
|
||||
'America/Yakutat',
|
||||
'Antarctica/Casey',
|
||||
'Antarctica/Davis',
|
||||
'Antarctica/DumontDUrville',
|
||||
'Antarctica/Macquarie',
|
||||
'Antarctica/Mawson',
|
||||
'Antarctica/McMurdo',
|
||||
'Antarctica/Palmer',
|
||||
'Antarctica/Rothera',
|
||||
'Antarctica/Syowa',
|
||||
'Antarctica/Troll',
|
||||
'Antarctica/Vostok',
|
||||
'Arctic/Longyearbyen',
|
||||
'Asia/Aden',
|
||||
'Asia/Almaty',
|
||||
'Asia/Amman',
|
||||
'Asia/Anadyr',
|
||||
'Asia/Aqtau',
|
||||
'Asia/Aqtobe',
|
||||
'Asia/Ashgabat',
|
||||
'Asia/Atyrau',
|
||||
'Asia/Baghdad',
|
||||
'Asia/Bahrain',
|
||||
'Asia/Baku',
|
||||
'Asia/Bangkok',
|
||||
'Asia/Barnaul',
|
||||
'Asia/Beirut',
|
||||
'Asia/Bishkek',
|
||||
'Asia/Brunei',
|
||||
'Asia/Chita',
|
||||
'Asia/Choibalsan',
|
||||
'Asia/Colombo',
|
||||
'Asia/Damascus',
|
||||
'Asia/Dhaka',
|
||||
'Asia/Dili',
|
||||
'Asia/Dubai',
|
||||
'Asia/Dushanbe',
|
||||
'Asia/Famagusta',
|
||||
'Asia/Gaza',
|
||||
'Asia/Hebron',
|
||||
'Asia/Ho Chi Minh',
|
||||
'Asia/Hong Kong',
|
||||
'Asia/Hovd',
|
||||
'Asia/Irkutsk',
|
||||
'Asia/Jakarta',
|
||||
'Asia/Jayapura',
|
||||
'Asia/Jerusalem',
|
||||
'Asia/Kabul',
|
||||
'Asia/Kamchatka',
|
||||
'Asia/Karachi',
|
||||
'Asia/Kathmandu',
|
||||
'Asia/Khandyga',
|
||||
'Asia/Kolkata',
|
||||
'Asia/Krasnoyarsk',
|
||||
'Asia/Kuala Lumpur',
|
||||
'Asia/Kuching',
|
||||
'Asia/Kuwait',
|
||||
'Asia/Macau',
|
||||
'Asia/Magadan',
|
||||
'Asia/Makassar',
|
||||
'Asia/Manila',
|
||||
'Asia/Muscat',
|
||||
'Asia/Nicosia',
|
||||
'Asia/Novokuznetsk',
|
||||
'Asia/Novosibirsk',
|
||||
'Asia/Omsk',
|
||||
'Asia/Oral',
|
||||
'Asia/Phnom Penh',
|
||||
'Asia/Pontianak',
|
||||
'Asia/Pyongyang',
|
||||
'Asia/Qatar',
|
||||
'Asia/Qostanay',
|
||||
'Asia/Qyzylorda',
|
||||
'Asia/Riyadh',
|
||||
'Asia/Sakhalin',
|
||||
'Asia/Samarkand',
|
||||
'Asia/Seoul',
|
||||
'Asia/Shanghai',
|
||||
'Asia/Singapore',
|
||||
'Asia/Srednekolymsk',
|
||||
'Asia/Taipei',
|
||||
'Asia/Tashkent',
|
||||
'Asia/Tbilisi',
|
||||
'Asia/Tehran',
|
||||
'Asia/Thimphu',
|
||||
'Asia/Tokyo',
|
||||
'Asia/Tomsk',
|
||||
'Asia/Ulaanbaatar',
|
||||
'Asia/Urumqi',
|
||||
'Asia/Ust-Nera',
|
||||
'Asia/Vientiane',
|
||||
'Asia/Vladivostok',
|
||||
'Asia/Yakutsk',
|
||||
'Asia/Yangon',
|
||||
'Asia/Yekaterinburg',
|
||||
'Asia/Yerevan',
|
||||
'Atlantic/Azores',
|
||||
'Atlantic/Bermuda',
|
||||
'Atlantic/Canary',
|
||||
'Atlantic/Cape Verde',
|
||||
'Atlantic/Faroe',
|
||||
'Atlantic/Madeira',
|
||||
'Atlantic/Reykjavik',
|
||||
'Atlantic/South Georgia',
|
||||
'Atlantic/St Helena',
|
||||
'Atlantic/Stanley',
|
||||
'Australia/Adelaide',
|
||||
'Australia/Brisbane',
|
||||
'Australia/Broken Hill',
|
||||
'Australia/Darwin',
|
||||
'Australia/Eucla',
|
||||
'Australia/Hobart',
|
||||
'Australia/Lindeman',
|
||||
'Australia/Lord Howe',
|
||||
'Australia/Melbourne',
|
||||
'Australia/Perth',
|
||||
'Australia/Sydney',
|
||||
'Etc/GMT',
|
||||
'Etc/GMT+1',
|
||||
'Etc/GMT+10',
|
||||
'Etc/GMT+11',
|
||||
'Etc/GMT+12',
|
||||
'Etc/GMT+2',
|
||||
'Etc/GMT+3',
|
||||
'Etc/GMT+4',
|
||||
'Etc/GMT+5',
|
||||
'Etc/GMT+6',
|
||||
'Etc/GMT+7',
|
||||
'Etc/GMT+8',
|
||||
'Etc/GMT+9',
|
||||
'Etc/GMT-1',
|
||||
'Etc/GMT-10',
|
||||
'Etc/GMT-11',
|
||||
'Etc/GMT-12',
|
||||
'Etc/GMT-13',
|
||||
'Etc/GMT-14',
|
||||
'Etc/GMT-2',
|
||||
'Etc/GMT-3',
|
||||
'Etc/GMT-4',
|
||||
'Etc/GMT-5',
|
||||
'Etc/GMT-6',
|
||||
'Etc/GMT-7',
|
||||
'Etc/GMT-8',
|
||||
'Etc/GMT-9',
|
||||
'Europe/Amsterdam',
|
||||
'Europe/Andorra',
|
||||
'Europe/Astrakhan',
|
||||
'Europe/Athens',
|
||||
'Europe/Belgrade',
|
||||
'Europe/Berlin',
|
||||
'Europe/Bratislava',
|
||||
'Europe/Brussels',
|
||||
'Europe/Bucharest',
|
||||
'Europe/Budapest',
|
||||
'Europe/Busingen',
|
||||
'Europe/Chisinau',
|
||||
'Europe/Copenhagen',
|
||||
'Europe/Dublin',
|
||||
'Europe/Gibraltar',
|
||||
'Europe/Guernsey',
|
||||
'Europe/Helsinki',
|
||||
'Europe/Isle of Man',
|
||||
'Europe/Istanbul',
|
||||
'Europe/Jersey',
|
||||
'Europe/Kaliningrad',
|
||||
'Europe/Kirov',
|
||||
'Europe/Kyiv',
|
||||
'Europe/Lisbon',
|
||||
'Europe/Ljubljana',
|
||||
'Europe/London',
|
||||
'Europe/Luxembourg',
|
||||
'Europe/Madrid',
|
||||
'Europe/Malta',
|
||||
'Europe/Mariehamn',
|
||||
'Europe/Minsk',
|
||||
'Europe/Monaco',
|
||||
'Europe/Moscow',
|
||||
'Europe/Oslo',
|
||||
'Europe/Paris',
|
||||
'Europe/Podgorica',
|
||||
'Europe/Prague',
|
||||
'Europe/Riga',
|
||||
'Europe/Rome',
|
||||
'Europe/Samara',
|
||||
'Europe/San Marino',
|
||||
'Europe/Sarajevo',
|
||||
'Europe/Saratov',
|
||||
'Europe/Simferopol',
|
||||
'Europe/Skopje',
|
||||
'Europe/Sofia',
|
||||
'Europe/Stockholm',
|
||||
'Europe/Tallinn',
|
||||
'Europe/Tirane',
|
||||
'Europe/Ulyanovsk',
|
||||
'Europe/Vaduz',
|
||||
'Europe/Vatican',
|
||||
'Europe/Vienna',
|
||||
'Europe/Vilnius',
|
||||
'Europe/Volgograd',
|
||||
'Europe/Warsaw',
|
||||
'Europe/Zagreb',
|
||||
'Europe/Zurich',
|
||||
'Indian/Antananarivo',
|
||||
'Indian/Chagos',
|
||||
'Indian/Christmas',
|
||||
'Indian/Cocos',
|
||||
'Indian/Comoro',
|
||||
'Indian/Kerguelen',
|
||||
'Indian/Mahe',
|
||||
'Indian/Maldives',
|
||||
'Indian/Mauritius',
|
||||
'Indian/Mayotte',
|
||||
'Indian/Reunion',
|
||||
'Pacific/Apia',
|
||||
'Pacific/Auckland',
|
||||
'Pacific/Bougainville',
|
||||
'Pacific/Chatham',
|
||||
'Pacific/Chuuk',
|
||||
'Pacific/Easter',
|
||||
'Pacific/Efate',
|
||||
'Pacific/Fakaofo',
|
||||
'Pacific/Fiji',
|
||||
'Pacific/Funafuti',
|
||||
'Pacific/Galapagos',
|
||||
'Pacific/Gambier',
|
||||
'Pacific/Guadalcanal',
|
||||
'Pacific/Guam',
|
||||
'Pacific/Honolulu',
|
||||
'Pacific/Kanton',
|
||||
'Pacific/Kiritimati',
|
||||
'Pacific/Kosrae',
|
||||
'Pacific/Kwajalein',
|
||||
'Pacific/Majuro',
|
||||
'Pacific/Marquesas',
|
||||
'Pacific/Midway',
|
||||
'Pacific/Nauru',
|
||||
'Pacific/Niue',
|
||||
'Pacific/Norfolk',
|
||||
'Pacific/Noumea',
|
||||
'Pacific/Pago Pago',
|
||||
'Pacific/Palau',
|
||||
'Pacific/Pitcairn',
|
||||
'Pacific/Pohnpei',
|
||||
'Pacific/Port Moresby',
|
||||
'Pacific/Rarotonga',
|
||||
'Pacific/Saipan',
|
||||
'Pacific/Tahiti',
|
||||
'Pacific/Tarawa',
|
||||
'Pacific/Tongatapu',
|
||||
'Pacific/Wake',
|
||||
'Pacific/Wallis',
|
||||
];
|
||||
|
||||
+6
-4
@@ -1,3 +1,5 @@
|
||||
import Intl from 'react-intl-universal';
|
||||
|
||||
export function diffTime(num: number) {
|
||||
const diff = num * 1000;
|
||||
|
||||
@@ -12,15 +14,15 @@ export function diffTime(num: number) {
|
||||
const leave3 = leave2 % (60 * 1000);
|
||||
const seconds = Math.round(leave3 / 1000);
|
||||
|
||||
let returnStr = seconds + '秒';
|
||||
let returnStr = `${seconds} ${Intl.get('秒')}`;
|
||||
if (minutes > 0) {
|
||||
returnStr = minutes + '分' + returnStr;
|
||||
returnStr = `${minutes} ${Intl.get('分')} ` + returnStr;
|
||||
}
|
||||
if (hours > 0) {
|
||||
returnStr = hours + '小时' + returnStr;
|
||||
returnStr = `${hours} ${Intl.get('时')} ` + returnStr;
|
||||
}
|
||||
if (days > 0) {
|
||||
returnStr = days + '天' + returnStr;
|
||||
returnStr = `${days} ${Intl.get('天')} ` + returnStr;
|
||||
}
|
||||
return returnStr;
|
||||
}
|
||||
|
||||
+15
-14
@@ -57,18 +57,19 @@ const errorHandler = function (
|
||||
return error.config?.onError(error.response);
|
||||
}
|
||||
|
||||
notification.error({
|
||||
message: msg,
|
||||
description: (
|
||||
<>
|
||||
{error.response?.data?.errors?.map((item: any) => (
|
||||
<div>
|
||||
{item.message} ({item.value})
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
),
|
||||
});
|
||||
msg &&
|
||||
notification.error({
|
||||
message: msg,
|
||||
description: error.response?.data?.errors ? (
|
||||
<>
|
||||
{error.response?.data?.errors?.map((item: any) => (
|
||||
<div>
|
||||
{item.message} ({item.value})
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
) : undefined,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.log(error.message);
|
||||
@@ -117,13 +118,13 @@ _request.interceptors.response.use(async (response) => {
|
||||
msg &&
|
||||
notification.error({
|
||||
message: msg,
|
||||
description: (
|
||||
description: res?.errors ? (
|
||||
<>
|
||||
{res?.errors.map((item: any) => (
|
||||
<div>{item.message}</div>
|
||||
))}
|
||||
</>
|
||||
),
|
||||
) : undefined,
|
||||
});
|
||||
}
|
||||
return res;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import * as monaco from 'monaco-editor';
|
||||
|
||||
export function canPreviewInMonaco(fileName: string): boolean {
|
||||
const supportedLanguages = monaco.languages.getLanguages();
|
||||
const ext = fileName.slice(fileName.lastIndexOf('.')).toLowerCase();
|
||||
return supportedLanguages.some((lang) => lang.extensions?.includes(ext));
|
||||
}
|
||||
+9
-6
@@ -1,7 +1,10 @@
|
||||
version: 2.18.1
|
||||
changeLogLink: https://t.me/jiao_long/426
|
||||
publishTime: 2025-01-15 08:00
|
||||
version: 2.18.2
|
||||
changeLogLink: https://t.me/jiao_long/427
|
||||
publishTime: 2025-02-28 00:00
|
||||
changeLog: |
|
||||
1. 内置 QLAPI 增加环境变量和系统通知 api
|
||||
2. 移除 nedb 和 sentry,不再支持 2.10.x 版本自动迁移
|
||||
3. 修复多语言翻译
|
||||
1. 定时任务支持 开机运行@boot 和 手动运行@once 任务
|
||||
2. 脚本管理增加可预览检查,避免无法预览文件被重复保存
|
||||
3. 系统设置增加时区设置
|
||||
4. 修复登录失败没有提示
|
||||
5. 增加重置密码命令 ql resetpwd
|
||||
6. 修复群晖通知参数,任务视图不属于筛选
|
||||
|
||||
Reference in New Issue
Block a user