This commit is contained in:
Copilot 2025-11-07 16:32:07 +00:00 committed by GitHub
commit fefad28fd8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 1642 additions and 714 deletions

View File

@ -11,8 +11,7 @@ export default (app: Router) => {
route.get( route.get(
'/', '/',
celebrate({ celebrate({
query: query: Joi.object({
Joi.object({
searchValue: Joi.string().optional().allow(''), searchValue: Joi.string().optional().allow(''),
type: Joi.string().optional().allow(''), type: Joi.string().optional().allow(''),
status: Joi.string().optional().allow(''), status: Joi.string().optional().allow(''),

View File

@ -71,7 +71,8 @@ export default (app: Router) => {
logger.error('🔥 error: %o', e); logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}); },
);
route.get( route.get(
'/detail', '/detail',
@ -85,7 +86,7 @@ export default (app: Router) => {
try { try {
const scriptService = Container.get(ScriptService); const scriptService = Container.get(ScriptService);
const content = await scriptService.getFile( const content = await scriptService.getFile(
req.query?.path as string || '', (req.query?.path as string) || '',
req.query.file as string, req.query.file as string,
); );
res.send({ code: 200, data: content }); res.send({ code: 200, data: content });
@ -109,7 +110,7 @@ export default (app: Router) => {
try { try {
const scriptService = Container.get(ScriptService); const scriptService = Container.get(ScriptService);
const content = await scriptService.getFile( const content = await scriptService.getFile(
req.query?.path as string || '', (req.query?.path as string) || '',
req.params.file, req.params.file,
); );
res.send({ code: 200, data: content }); res.send({ code: 200, data: content });

View File

@ -8,7 +8,7 @@ import path from 'path';
import { v4 as uuidV4 } from 'uuid'; import { v4 as uuidV4 } from 'uuid';
import rateLimit from 'express-rate-limit'; import rateLimit from 'express-rate-limit';
import config from '../config'; import config from '../config';
import { isDemoEnv } from '../config/util'; import { isDemoEnv, getToken } from '../config/util';
const route = Router(); const route = Router();
const storage = multer.diskStorage({ const storage = multer.diskStorage({
@ -56,7 +56,8 @@ export default (app: Router) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const userService = Container.get(UserService); const userService = Container.get(UserService);
await userService.logout(req.platform); const token = getToken(req);
await userService.logout(req.platform, token);
res.send({ code: 200 }); res.send({ code: 200 });
} catch (e) { } catch (e) {
return next(e); return next(e);

View File

@ -28,12 +28,18 @@ class Application {
constructor() { constructor() {
this.app = express(); this.app = express();
// 创建一个全局中间件删除查询参数中的t // 创建一个全局中间件删除查询参数中的t
this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => { this.app.use(
(
req: express.Request,
res: express.Response,
next: express.NextFunction,
) => {
if (req.query.t) { if (req.query.t) {
delete req.query.t; delete req.query.t;
} }
next(); next();
}); },
);
} }
async start() { async start() {
@ -61,7 +67,8 @@ class Application {
if (metadata) { if (metadata) {
if (!this.isShuttingDown) { if (!this.isShuttingDown) {
Logger.error( Logger.error(
`${metadata.serviceType} worker ${worker.process.pid} died (${signal || code `${metadata.serviceType} worker ${worker.process.pid} died (${
signal || code
}). Restarting...`, }). Restarting...`,
); );
const newWorker = this.forkWorker(metadata.serviceType); const newWorker = this.forkWorker(metadata.serviceType);
@ -96,9 +103,11 @@ class Application {
} }
private setupMiddlewares() { private setupMiddlewares() {
this.app.use(helmet({ this.app.use(
helmet({
contentSecurityPolicy: false, contentSecurityPolicy: false,
})); }),
);
this.app.use(cors(config.cors)); this.app.use(cors(config.cors));
this.app.use(compression()); this.app.use(compression());
this.app.use(monitoringMiddleware); this.app.use(monitoringMiddleware);

View File

@ -176,4 +176,5 @@ export default {
sshdPath, sshdPath,
systemLogPath, systemLogPath,
dependenceCachePath, dependenceCachePath,
maxTokensPerPlatform: 10, // Maximum number of concurrent sessions per platform
}; };

View File

@ -55,7 +55,7 @@ export enum CrontabStatus {
'disabled', 'disabled',
} }
export interface CronInstance extends Model<Crontab, Crontab>, Crontab { } export interface CronInstance extends Model<Crontab, Crontab>, Crontab {}
export const CrontabModel = sequelize.define<CronInstance>('Crontab', { export const CrontabModel = sequelize.define<CronInstance>('Crontab', {
name: { name: {
unique: 'compositeIndex', unique: 'compositeIndex',

View File

@ -24,7 +24,13 @@ export interface AppToken {
expiration: number; expiration: number;
} }
export type AppScope = 'envs' | 'crons' | 'configs' | 'scripts' | 'logs' | 'system'; export type AppScope =
| 'envs'
| 'crons'
| 'configs'
| 'scripts'
| 'logs'
| 'system';
export interface AppInstance extends Model<App, App>, App {} export interface AppInstance extends Model<App, App>, App {}
export const AppModel = sequelize.define<AppInstance>('App', { export const AppModel = sequelize.define<AppInstance>('App', {

View File

@ -48,6 +48,19 @@ export interface LoginLogInfo {
status?: LoginStatus; status?: LoginStatus;
} }
export interface TokenInfo {
value: string;
timestamp: number;
ip: string;
address: string;
platform: string;
/**
* Token expiration time in seconds since Unix epoch.
* If undefined, the token uses JWT's built-in expiration.
*/
expiration?: number;
}
export interface AuthInfo { export interface AuthInfo {
username: string; username: string;
password: string; password: string;
@ -58,7 +71,7 @@ export interface AuthInfo {
platform: string; platform: string;
isTwoFactorChecking: boolean; isTwoFactorChecking: boolean;
token: string; token: string;
tokens: Record<string, string>; tokens: Record<string, string | TokenInfo[]>;
twoFactorActivated: boolean; twoFactorActivated: boolean;
twoFactorSecret: string; twoFactorSecret: string;
avatar: string; avatar: string;

View File

@ -13,7 +13,7 @@ async function linkToNodeModule(src: string, dst?: string) {
if (!stats) { if (!stats) {
await fs.symlink(source, target, 'dir'); await fs.symlink(source, target, 'dir');
} }
} catch (error) { } } catch (error) {}
} }
async function linkCommand() { async function linkCommand() {
@ -41,7 +41,7 @@ async function linkCommand() {
if (stats) { if (stats) {
await fs.unlink(tmpTarget); await fs.unlink(tmpTarget);
} }
} catch (error) { } } catch (error) {}
await fs.symlink(source, tmpTarget); await fs.symlink(source, tmpTarget);
await fs.rename(tmpTarget, target); await fs.rename(tmpTarget, target);
} }

View File

@ -9,6 +9,7 @@ import rewrite from 'express-urlrewrite';
import { errors } from 'celebrate'; import { errors } from 'celebrate';
import { serveEnv } from '../config/serverEnv'; import { serveEnv } from '../config/serverEnv';
import { IKeyvStore, shareStore } from '../shared/store'; import { IKeyvStore, shareStore } from '../shared/store';
import { isValidToken } from '../shared/auth';
import path from 'path'; import path from 'path';
export default ({ app }: { app: Application }) => { export default ({ app }: { app: Application }) => {
@ -77,12 +78,9 @@ export default ({ app }: { app: Application }) => {
} }
const authInfo = await shareStore.getAuthInfo(); const authInfo = await shareStore.getAuthInfo();
if (authInfo && headerToken) { if (isValidToken(authInfo, headerToken, req.platform)) {
const { token = '', tokens = {} } = authInfo;
if (headerToken === token || tokens[req.platform] === headerToken) {
return next(); return next();
} }
}
const errorCode = headerToken ? 'invalid_token' : 'credentials_required'; const errorCode = headerToken ? 'invalid_token' : 'credentials_required';
const errorMessage = headerToken const errorMessage = headerToken

View File

@ -4,6 +4,7 @@ import { Container } from 'typedi';
import SockService from '../services/sock'; import SockService from '../services/sock';
import { getPlatform } from '../config/util'; import { getPlatform } from '../config/util';
import { shareStore } from '../shared/store'; import { shareStore } from '../shared/store';
import { isValidToken } from '../shared/auth';
export default async ({ server }: { server: Server }) => { export default async ({ server }: { server: Server }) => {
const echo = sockJs.createServer({ prefix: '/api/ws', log: () => {} }); const echo = sockJs.createServer({ prefix: '/api/ws', log: () => {} });
@ -17,9 +18,8 @@ export default async ({ server }: { server: Server }) => {
const authInfo = await shareStore.getAuthInfo(); const authInfo = await shareStore.getAuthInfo();
const platform = getPlatform(conn.headers['user-agent'] || '') || 'desktop'; const platform = getPlatform(conn.headers['user-agent'] || '') || 'desktop';
const headerToken = conn.url.replace(`${conn.pathname}?token=`, ''); const headerToken = conn.url.replace(`${conn.pathname}?token=`, '');
if (authInfo) {
const { token = '', tokens = {} } = authInfo; if (isValidToken(authInfo, headerToken, platform)) {
if (headerToken === token || tokens[platform] === headerToken) {
sockService.addClient(conn); sockService.addClient(conn);
conn.on('data', (message) => { conn.on('data', (message) => {
@ -32,7 +32,6 @@ export default async ({ server }: { server: Server }) => {
return; return;
} }
}
conn.close('404'); conn.close('404');
}); });

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,7 @@
// source: back/protos/cron.proto // source: back/protos/cron.proto
/* eslint-disable */ /* eslint-disable */
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"; import { BinaryReader, BinaryWriter } from '@bufbuild/protobuf/wire';
import { import {
type CallOptions, type CallOptions,
ChannelCredentials, ChannelCredentials,
@ -17,9 +17,9 @@ import {
Metadata, Metadata,
type ServiceError, type ServiceError,
type UntypedServiceImplementation, type UntypedServiceImplementation,
} from "@grpc/grpc-js"; } from '@grpc/grpc-js';
export const protobufPackage = "com.ql.cron"; export const protobufPackage = 'com.ql.cron';
export interface ISchedule { export interface ISchedule {
schedule: string; schedule: string;
@ -37,30 +37,32 @@ export interface AddCronRequest {
crons: ICron[]; crons: ICron[];
} }
export interface AddCronResponse { export interface AddCronResponse {}
}
export interface DeleteCronRequest { export interface DeleteCronRequest {
ids: string[]; ids: string[];
} }
export interface DeleteCronResponse { export interface DeleteCronResponse {}
}
function createBaseISchedule(): ISchedule { function createBaseISchedule(): ISchedule {
return { schedule: "" }; return { schedule: '' };
} }
export const ISchedule: MessageFns<ISchedule> = { export const ISchedule: MessageFns<ISchedule> = {
encode(message: ISchedule, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { encode(
if (message.schedule !== "") { message: ISchedule,
writer: BinaryWriter = new BinaryWriter(),
): BinaryWriter {
if (message.schedule !== '') {
writer.uint32(10).string(message.schedule); writer.uint32(10).string(message.schedule);
} }
return writer; return writer;
}, },
decode(input: BinaryReader | Uint8Array, length?: number): ISchedule { decode(input: BinaryReader | Uint8Array, length?: number): ISchedule {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const reader =
input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === undefined ? reader.len : reader.pos + length; let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseISchedule(); const message = createBaseISchedule();
while (reader.pos < end) { while (reader.pos < end) {
@ -84,12 +86,16 @@ export const ISchedule: MessageFns<ISchedule> = {
}, },
fromJSON(object: any): ISchedule { fromJSON(object: any): ISchedule {
return { schedule: isSet(object.schedule) ? globalThis.String(object.schedule) : "" }; return {
schedule: isSet(object.schedule)
? globalThis.String(object.schedule)
: '',
};
}, },
toJSON(message: ISchedule): unknown { toJSON(message: ISchedule): unknown {
const obj: any = {}; const obj: any = {};
if (message.schedule !== "") { if (message.schedule !== '') {
obj.schedule = message.schedule; obj.schedule = message.schedule;
} }
return obj; return obj;
@ -98,39 +104,45 @@ export const ISchedule: MessageFns<ISchedule> = {
create<I extends Exact<DeepPartial<ISchedule>, I>>(base?: I): ISchedule { create<I extends Exact<DeepPartial<ISchedule>, I>>(base?: I): ISchedule {
return ISchedule.fromPartial(base ?? ({} as any)); return ISchedule.fromPartial(base ?? ({} as any));
}, },
fromPartial<I extends Exact<DeepPartial<ISchedule>, I>>(object: I): ISchedule { fromPartial<I extends Exact<DeepPartial<ISchedule>, I>>(
object: I,
): ISchedule {
const message = createBaseISchedule(); const message = createBaseISchedule();
message.schedule = object.schedule ?? ""; message.schedule = object.schedule ?? '';
return message; return message;
}, },
}; };
function createBaseICron(): ICron { function createBaseICron(): ICron {
return { id: "", schedule: "", command: "", extra_schedules: [], name: "" }; return { id: '', schedule: '', command: '', extra_schedules: [], name: '' };
} }
export const ICron: MessageFns<ICron> = { export const ICron: MessageFns<ICron> = {
encode(message: ICron, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { encode(
if (message.id !== "") { message: ICron,
writer: BinaryWriter = new BinaryWriter(),
): BinaryWriter {
if (message.id !== '') {
writer.uint32(10).string(message.id); writer.uint32(10).string(message.id);
} }
if (message.schedule !== "") { if (message.schedule !== '') {
writer.uint32(18).string(message.schedule); writer.uint32(18).string(message.schedule);
} }
if (message.command !== "") { if (message.command !== '') {
writer.uint32(26).string(message.command); writer.uint32(26).string(message.command);
} }
for (const v of message.extra_schedules) { for (const v of message.extra_schedules) {
ISchedule.encode(v!, writer.uint32(34).fork()).join(); ISchedule.encode(v!, writer.uint32(34).fork()).join();
} }
if (message.name !== "") { if (message.name !== '') {
writer.uint32(42).string(message.name); writer.uint32(42).string(message.name);
} }
return writer; return writer;
}, },
decode(input: BinaryReader | Uint8Array, length?: number): ICron { decode(input: BinaryReader | Uint8Array, length?: number): ICron {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const reader =
input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === undefined ? reader.len : reader.pos + length; let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseICron(); const message = createBaseICron();
while (reader.pos < end) { while (reader.pos < end) {
@ -165,7 +177,9 @@ export const ICron: MessageFns<ICron> = {
break; break;
} }
message.extra_schedules.push(ISchedule.decode(reader, reader.uint32())); message.extra_schedules.push(
ISchedule.decode(reader, reader.uint32()),
);
continue; continue;
} }
case 5: { case 5: {
@ -187,31 +201,35 @@ export const ICron: MessageFns<ICron> = {
fromJSON(object: any): ICron { fromJSON(object: any): ICron {
return { return {
id: isSet(object.id) ? globalThis.String(object.id) : "", id: isSet(object.id) ? globalThis.String(object.id) : '',
schedule: isSet(object.schedule) ? globalThis.String(object.schedule) : "", schedule: isSet(object.schedule)
command: isSet(object.command) ? globalThis.String(object.command) : "", ? globalThis.String(object.schedule)
: '',
command: isSet(object.command) ? globalThis.String(object.command) : '',
extra_schedules: globalThis.Array.isArray(object?.extra_schedules) extra_schedules: globalThis.Array.isArray(object?.extra_schedules)
? object.extra_schedules.map((e: any) => ISchedule.fromJSON(e)) ? object.extra_schedules.map((e: any) => ISchedule.fromJSON(e))
: [], : [],
name: isSet(object.name) ? globalThis.String(object.name) : "", name: isSet(object.name) ? globalThis.String(object.name) : '',
}; };
}, },
toJSON(message: ICron): unknown { toJSON(message: ICron): unknown {
const obj: any = {}; const obj: any = {};
if (message.id !== "") { if (message.id !== '') {
obj.id = message.id; obj.id = message.id;
} }
if (message.schedule !== "") { if (message.schedule !== '') {
obj.schedule = message.schedule; obj.schedule = message.schedule;
} }
if (message.command !== "") { if (message.command !== '') {
obj.command = message.command; obj.command = message.command;
} }
if (message.extra_schedules?.length) { if (message.extra_schedules?.length) {
obj.extra_schedules = message.extra_schedules.map((e) => ISchedule.toJSON(e)); obj.extra_schedules = message.extra_schedules.map((e) =>
ISchedule.toJSON(e),
);
} }
if (message.name !== "") { if (message.name !== '') {
obj.name = message.name; obj.name = message.name;
} }
return obj; return obj;
@ -222,11 +240,12 @@ export const ICron: MessageFns<ICron> = {
}, },
fromPartial<I extends Exact<DeepPartial<ICron>, I>>(object: I): ICron { fromPartial<I extends Exact<DeepPartial<ICron>, I>>(object: I): ICron {
const message = createBaseICron(); const message = createBaseICron();
message.id = object.id ?? ""; message.id = object.id ?? '';
message.schedule = object.schedule ?? ""; message.schedule = object.schedule ?? '';
message.command = object.command ?? ""; message.command = object.command ?? '';
message.extra_schedules = object.extra_schedules?.map((e) => ISchedule.fromPartial(e)) || []; message.extra_schedules =
message.name = object.name ?? ""; object.extra_schedules?.map((e) => ISchedule.fromPartial(e)) || [];
message.name = object.name ?? '';
return message; return message;
}, },
}; };
@ -236,7 +255,10 @@ function createBaseAddCronRequest(): AddCronRequest {
} }
export const AddCronRequest: MessageFns<AddCronRequest> = { export const AddCronRequest: MessageFns<AddCronRequest> = {
encode(message: AddCronRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { encode(
message: AddCronRequest,
writer: BinaryWriter = new BinaryWriter(),
): BinaryWriter {
for (const v of message.crons) { for (const v of message.crons) {
ICron.encode(v!, writer.uint32(10).fork()).join(); ICron.encode(v!, writer.uint32(10).fork()).join();
} }
@ -244,7 +266,8 @@ export const AddCronRequest: MessageFns<AddCronRequest> = {
}, },
decode(input: BinaryReader | Uint8Array, length?: number): AddCronRequest { decode(input: BinaryReader | Uint8Array, length?: number): AddCronRequest {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const reader =
input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === undefined ? reader.len : reader.pos + length; let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseAddCronRequest(); const message = createBaseAddCronRequest();
while (reader.pos < end) { while (reader.pos < end) {
@ -268,7 +291,11 @@ export const AddCronRequest: MessageFns<AddCronRequest> = {
}, },
fromJSON(object: any): AddCronRequest { fromJSON(object: any): AddCronRequest {
return { crons: globalThis.Array.isArray(object?.crons) ? object.crons.map((e: any) => ICron.fromJSON(e)) : [] }; return {
crons: globalThis.Array.isArray(object?.crons)
? object.crons.map((e: any) => ICron.fromJSON(e))
: [],
};
}, },
toJSON(message: AddCronRequest): unknown { toJSON(message: AddCronRequest): unknown {
@ -279,10 +306,14 @@ export const AddCronRequest: MessageFns<AddCronRequest> = {
return obj; return obj;
}, },
create<I extends Exact<DeepPartial<AddCronRequest>, I>>(base?: I): AddCronRequest { create<I extends Exact<DeepPartial<AddCronRequest>, I>>(
base?: I,
): AddCronRequest {
return AddCronRequest.fromPartial(base ?? ({} as any)); return AddCronRequest.fromPartial(base ?? ({} as any));
}, },
fromPartial<I extends Exact<DeepPartial<AddCronRequest>, I>>(object: I): AddCronRequest { fromPartial<I extends Exact<DeepPartial<AddCronRequest>, I>>(
object: I,
): AddCronRequest {
const message = createBaseAddCronRequest(); const message = createBaseAddCronRequest();
message.crons = object.crons?.map((e) => ICron.fromPartial(e)) || []; message.crons = object.crons?.map((e) => ICron.fromPartial(e)) || [];
return message; return message;
@ -294,12 +325,16 @@ function createBaseAddCronResponse(): AddCronResponse {
} }
export const AddCronResponse: MessageFns<AddCronResponse> = { export const AddCronResponse: MessageFns<AddCronResponse> = {
encode(_: AddCronResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { encode(
_: AddCronResponse,
writer: BinaryWriter = new BinaryWriter(),
): BinaryWriter {
return writer; return writer;
}, },
decode(input: BinaryReader | Uint8Array, length?: number): AddCronResponse { decode(input: BinaryReader | Uint8Array, length?: number): AddCronResponse {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const reader =
input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === undefined ? reader.len : reader.pos + length; let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseAddCronResponse(); const message = createBaseAddCronResponse();
while (reader.pos < end) { while (reader.pos < end) {
@ -323,10 +358,14 @@ export const AddCronResponse: MessageFns<AddCronResponse> = {
return obj; return obj;
}, },
create<I extends Exact<DeepPartial<AddCronResponse>, I>>(base?: I): AddCronResponse { create<I extends Exact<DeepPartial<AddCronResponse>, I>>(
base?: I,
): AddCronResponse {
return AddCronResponse.fromPartial(base ?? ({} as any)); return AddCronResponse.fromPartial(base ?? ({} as any));
}, },
fromPartial<I extends Exact<DeepPartial<AddCronResponse>, I>>(_: I): AddCronResponse { fromPartial<I extends Exact<DeepPartial<AddCronResponse>, I>>(
_: I,
): AddCronResponse {
const message = createBaseAddCronResponse(); const message = createBaseAddCronResponse();
return message; return message;
}, },
@ -337,7 +376,10 @@ function createBaseDeleteCronRequest(): DeleteCronRequest {
} }
export const DeleteCronRequest: MessageFns<DeleteCronRequest> = { export const DeleteCronRequest: MessageFns<DeleteCronRequest> = {
encode(message: DeleteCronRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { encode(
message: DeleteCronRequest,
writer: BinaryWriter = new BinaryWriter(),
): BinaryWriter {
for (const v of message.ids) { for (const v of message.ids) {
writer.uint32(10).string(v!); writer.uint32(10).string(v!);
} }
@ -345,7 +387,8 @@ export const DeleteCronRequest: MessageFns<DeleteCronRequest> = {
}, },
decode(input: BinaryReader | Uint8Array, length?: number): DeleteCronRequest { decode(input: BinaryReader | Uint8Array, length?: number): DeleteCronRequest {
const reader = input instanceof BinaryReader ? input : new BinaryReader(input); const reader =
input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === undefined ? reader.len : reader.pos + length; let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseDeleteCronRequest(); const message = createBaseDeleteCronRequest();
while (reader.pos < end) { while (reader.pos < end) {
@ -369,7 +412,11 @@ export const DeleteCronRequest: MessageFns<DeleteCronRequest> = {
}, },
fromJSON(object: any): DeleteCronRequest { fromJSON(object: any): DeleteCronRequest {
return { ids: globalThis.Array.isArray(object?.ids) ? object.ids.map((e: any) => globalThis.String(e)) : [] }; return {
ids: globalThis.Array.isArray(object?.ids)
? object.ids.map((e: any) => globalThis.String(e))
: [],
};
}, },
toJSON(message: DeleteCronRequest): unknown { toJSON(message: DeleteCronRequest): unknown {
@ -380,10 +427,14 @@ export const DeleteCronRequest: MessageFns<DeleteCronRequest> = {
return obj; return obj;
}, },
create<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(base?: I): DeleteCronRequest { create<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(
base?: I,
): DeleteCronRequest {
return DeleteCronRequest.fromPartial(base ?? ({} as any)); return DeleteCronRequest.fromPartial(base ?? ({} as any));
}, },
fromPartial<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(object: I): DeleteCronRequest { fromPartial<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(
object: I,
): DeleteCronRequest {
const message = createBaseDeleteCronRequest(); const message = createBaseDeleteCronRequest();
message.ids = object.ids?.map((e) => e) || []; message.ids = object.ids?.map((e) => e) || [];
return message; return message;
@ -395,12 +446,19 @@ function createBaseDeleteCronResponse(): DeleteCronResponse {
} }
export const DeleteCronResponse: MessageFns<DeleteCronResponse> = { export const DeleteCronResponse: MessageFns<DeleteCronResponse> = {
encode(_: DeleteCronResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { encode(
_: DeleteCronResponse,
writer: BinaryWriter = new BinaryWriter(),
): BinaryWriter {
return writer; return writer;
}, },
decode(input: BinaryReader | Uint8Array, length?: number): DeleteCronResponse { decode(
const reader = input instanceof BinaryReader ? input : new BinaryReader(input); input: BinaryReader | Uint8Array,
length?: number,
): DeleteCronResponse {
const reader =
input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === undefined ? reader.len : reader.pos + length; let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseDeleteCronResponse(); const message = createBaseDeleteCronResponse();
while (reader.pos < end) { while (reader.pos < end) {
@ -424,10 +482,14 @@ export const DeleteCronResponse: MessageFns<DeleteCronResponse> = {
return obj; return obj;
}, },
create<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(base?: I): DeleteCronResponse { create<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(
base?: I,
): DeleteCronResponse {
return DeleteCronResponse.fromPartial(base ?? ({} as any)); return DeleteCronResponse.fromPartial(base ?? ({} as any));
}, },
fromPartial<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(_: I): DeleteCronResponse { fromPartial<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(
_: I,
): DeleteCronResponse {
const message = createBaseDeleteCronResponse(); const message = createBaseDeleteCronResponse();
return message; return message;
}, },
@ -436,21 +498,25 @@ export const DeleteCronResponse: MessageFns<DeleteCronResponse> = {
export type CronService = typeof CronService; export type CronService = typeof CronService;
export const CronService = { export const CronService = {
addCron: { addCron: {
path: "/com.ql.cron.Cron/addCron", path: '/com.ql.cron.Cron/addCron',
requestStream: false, requestStream: false,
responseStream: false, responseStream: false,
requestSerialize: (value: AddCronRequest) => Buffer.from(AddCronRequest.encode(value).finish()), requestSerialize: (value: AddCronRequest) =>
Buffer.from(AddCronRequest.encode(value).finish()),
requestDeserialize: (value: Buffer) => AddCronRequest.decode(value), requestDeserialize: (value: Buffer) => AddCronRequest.decode(value),
responseSerialize: (value: AddCronResponse) => Buffer.from(AddCronResponse.encode(value).finish()), responseSerialize: (value: AddCronResponse) =>
Buffer.from(AddCronResponse.encode(value).finish()),
responseDeserialize: (value: Buffer) => AddCronResponse.decode(value), responseDeserialize: (value: Buffer) => AddCronResponse.decode(value),
}, },
delCron: { delCron: {
path: "/com.ql.cron.Cron/delCron", path: '/com.ql.cron.Cron/delCron',
requestStream: false, requestStream: false,
responseStream: false, responseStream: false,
requestSerialize: (value: DeleteCronRequest) => Buffer.from(DeleteCronRequest.encode(value).finish()), requestSerialize: (value: DeleteCronRequest) =>
Buffer.from(DeleteCronRequest.encode(value).finish()),
requestDeserialize: (value: Buffer) => DeleteCronRequest.decode(value), requestDeserialize: (value: Buffer) => DeleteCronRequest.decode(value),
responseSerialize: (value: DeleteCronResponse) => Buffer.from(DeleteCronResponse.encode(value).finish()), responseSerialize: (value: DeleteCronResponse) =>
Buffer.from(DeleteCronResponse.encode(value).finish()),
responseDeserialize: (value: Buffer) => DeleteCronResponse.decode(value), responseDeserialize: (value: Buffer) => DeleteCronResponse.decode(value),
}, },
} as const; } as const;
@ -478,38 +544,68 @@ export interface CronClient extends Client {
): ClientUnaryCall; ): ClientUnaryCall;
delCron( delCron(
request: DeleteCronRequest, request: DeleteCronRequest,
callback: (error: ServiceError | null, response: DeleteCronResponse) => void, callback: (
error: ServiceError | null,
response: DeleteCronResponse,
) => void,
): ClientUnaryCall; ): ClientUnaryCall;
delCron( delCron(
request: DeleteCronRequest, request: DeleteCronRequest,
metadata: Metadata, metadata: Metadata,
callback: (error: ServiceError | null, response: DeleteCronResponse) => void, callback: (
error: ServiceError | null,
response: DeleteCronResponse,
) => void,
): ClientUnaryCall; ): ClientUnaryCall;
delCron( delCron(
request: DeleteCronRequest, request: DeleteCronRequest,
metadata: Metadata, metadata: Metadata,
options: Partial<CallOptions>, options: Partial<CallOptions>,
callback: (error: ServiceError | null, response: DeleteCronResponse) => void, callback: (
error: ServiceError | null,
response: DeleteCronResponse,
) => void,
): ClientUnaryCall; ): ClientUnaryCall;
} }
export const CronClient = makeGenericClientConstructor(CronService, "com.ql.cron.Cron") as unknown as { export const CronClient = makeGenericClientConstructor(
new (address: string, credentials: ChannelCredentials, options?: Partial<ClientOptions>): CronClient; CronService,
'com.ql.cron.Cron',
) as unknown as {
new (
address: string,
credentials: ChannelCredentials,
options?: Partial<ClientOptions>,
): CronClient;
service: typeof CronService; service: typeof CronService;
serviceName: string; serviceName: string;
}; };
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; type Builtin =
| Date
| Function
| Uint8Array
| string
| number
| boolean
| undefined;
export type DeepPartial<T> = T extends Builtin ? T export type DeepPartial<T> = T extends Builtin
: T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>> ? T
: T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>> : T extends globalThis.Array<infer U>
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> } ? globalThis.Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U>
? ReadonlyArray<DeepPartial<U>>
: T extends {}
? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>; : Partial<T>;
type KeysOfUnion<T> = T extends T ? keyof T : never; type KeysOfUnion<T> = T extends T ? keyof T : never;
export type Exact<P, I extends P> = P extends Builtin ? P export type Exact<P, I extends P> = P extends Builtin
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never }; ? P
: P & { [K in keyof P]: Exact<P[K], I[K]> } & {
[K in Exclude<keyof I, KeysOfUnion<P>>]: never;
};
function isSet(value: any): boolean { function isSet(value: any): boolean {
return value !== null && value !== undefined; return value !== null && value !== undefined;

View File

@ -5,7 +5,7 @@
// source: back/protos/health.proto // source: back/protos/health.proto
/* eslint-disable */ /* eslint-disable */
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"; import { BinaryReader, BinaryWriter } from '@bufbuild/protobuf/wire';
import { import {
type CallOptions, type CallOptions,
ChannelCredentials, ChannelCredentials,
@ -19,9 +19,9 @@ import {
Metadata, Metadata,
type ServiceError, type ServiceError,
type UntypedServiceImplementation, type UntypedServiceImplementation,
} from "@grpc/grpc-js"; } from '@grpc/grpc-js';
export const protobufPackage = "com.ql.health"; export const protobufPackage = 'com.ql.health';
export interface HealthCheckRequest { export interface HealthCheckRequest {
service: string; service: string;
@ -39,57 +39,68 @@ export enum HealthCheckResponse_ServingStatus {
UNRECOGNIZED = -1, UNRECOGNIZED = -1,
} }
export function healthCheckResponse_ServingStatusFromJSON(object: any): HealthCheckResponse_ServingStatus { export function healthCheckResponse_ServingStatusFromJSON(
object: any,
): HealthCheckResponse_ServingStatus {
switch (object) { switch (object) {
case 0: case 0:
case "UNKNOWN": case 'UNKNOWN':
return HealthCheckResponse_ServingStatus.UNKNOWN; return HealthCheckResponse_ServingStatus.UNKNOWN;
case 1: case 1:
case "SERVING": case 'SERVING':
return HealthCheckResponse_ServingStatus.SERVING; return HealthCheckResponse_ServingStatus.SERVING;
case 2: case 2:
case "NOT_SERVING": case 'NOT_SERVING':
return HealthCheckResponse_ServingStatus.NOT_SERVING; return HealthCheckResponse_ServingStatus.NOT_SERVING;
case 3: case 3:
case "SERVICE_UNKNOWN": case 'SERVICE_UNKNOWN':
return HealthCheckResponse_ServingStatus.SERVICE_UNKNOWN; return HealthCheckResponse_ServingStatus.SERVICE_UNKNOWN;
case -1: case -1:
case "UNRECOGNIZED": case 'UNRECOGNIZED':
default: default:
return HealthCheckResponse_ServingStatus.UNRECOGNIZED; return HealthCheckResponse_ServingStatus.UNRECOGNIZED;
} }
} }
export function healthCheckResponse_ServingStatusToJSON(object: HealthCheckResponse_ServingStatus): string { export function healthCheckResponse_ServingStatusToJSON(
object: HealthCheckResponse_ServingStatus,
): string {
switch (object) { switch (object) {
case HealthCheckResponse_ServingStatus.UNKNOWN: case HealthCheckResponse_ServingStatus.UNKNOWN:
return "UNKNOWN"; return 'UNKNOWN';
case HealthCheckResponse_ServingStatus.SERVING: case HealthCheckResponse_ServingStatus.SERVING:
return "SERVING"; return 'SERVING';
case HealthCheckResponse_ServingStatus.NOT_SERVING: case HealthCheckResponse_ServingStatus.NOT_SERVING:
return "NOT_SERVING"; return 'NOT_SERVING';
case HealthCheckResponse_ServingStatus.SERVICE_UNKNOWN: case HealthCheckResponse_ServingStatus.SERVICE_UNKNOWN:
return "SERVICE_UNKNOWN"; return 'SERVICE_UNKNOWN';
case HealthCheckResponse_ServingStatus.UNRECOGNIZED: case HealthCheckResponse_ServingStatus.UNRECOGNIZED:
default: default:
return "UNRECOGNIZED"; return 'UNRECOGNIZED';
} }
} }
function createBaseHealthCheckRequest(): HealthCheckRequest { function createBaseHealthCheckRequest(): HealthCheckRequest {
return { service: "" }; return { service: '' };
} }
export const HealthCheckRequest: MessageFns<HealthCheckRequest> = { export const HealthCheckRequest: MessageFns<HealthCheckRequest> = {
encode(message: HealthCheckRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { encode(
if (message.service !== "") { message: HealthCheckRequest,
writer: BinaryWriter = new BinaryWriter(),
): BinaryWriter {
if (message.service !== '') {
writer.uint32(10).string(message.service); writer.uint32(10).string(message.service);
} }
return writer; return writer;
}, },
decode(input: BinaryReader | Uint8Array, length?: number): HealthCheckRequest { decode(
const reader = input instanceof BinaryReader ? input : new BinaryReader(input); input: BinaryReader | Uint8Array,
length?: number,
): HealthCheckRequest {
const reader =
input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === undefined ? reader.len : reader.pos + length; let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseHealthCheckRequest(); const message = createBaseHealthCheckRequest();
while (reader.pos < end) { while (reader.pos < end) {
@ -113,23 +124,29 @@ export const HealthCheckRequest: MessageFns<HealthCheckRequest> = {
}, },
fromJSON(object: any): HealthCheckRequest { fromJSON(object: any): HealthCheckRequest {
return { service: isSet(object.service) ? globalThis.String(object.service) : "" }; return {
service: isSet(object.service) ? globalThis.String(object.service) : '',
};
}, },
toJSON(message: HealthCheckRequest): unknown { toJSON(message: HealthCheckRequest): unknown {
const obj: any = {}; const obj: any = {};
if (message.service !== "") { if (message.service !== '') {
obj.service = message.service; obj.service = message.service;
} }
return obj; return obj;
}, },
create<I extends Exact<DeepPartial<HealthCheckRequest>, I>>(base?: I): HealthCheckRequest { create<I extends Exact<DeepPartial<HealthCheckRequest>, I>>(
base?: I,
): HealthCheckRequest {
return HealthCheckRequest.fromPartial(base ?? ({} as any)); return HealthCheckRequest.fromPartial(base ?? ({} as any));
}, },
fromPartial<I extends Exact<DeepPartial<HealthCheckRequest>, I>>(object: I): HealthCheckRequest { fromPartial<I extends Exact<DeepPartial<HealthCheckRequest>, I>>(
object: I,
): HealthCheckRequest {
const message = createBaseHealthCheckRequest(); const message = createBaseHealthCheckRequest();
message.service = object.service ?? ""; message.service = object.service ?? '';
return message; return message;
}, },
}; };
@ -139,15 +156,22 @@ function createBaseHealthCheckResponse(): HealthCheckResponse {
} }
export const HealthCheckResponse: MessageFns<HealthCheckResponse> = { export const HealthCheckResponse: MessageFns<HealthCheckResponse> = {
encode(message: HealthCheckResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { encode(
message: HealthCheckResponse,
writer: BinaryWriter = new BinaryWriter(),
): BinaryWriter {
if (message.status !== 0) { if (message.status !== 0) {
writer.uint32(8).int32(message.status); writer.uint32(8).int32(message.status);
} }
return writer; return writer;
}, },
decode(input: BinaryReader | Uint8Array, length?: number): HealthCheckResponse { decode(
const reader = input instanceof BinaryReader ? input : new BinaryReader(input); input: BinaryReader | Uint8Array,
length?: number,
): HealthCheckResponse {
const reader =
input instanceof BinaryReader ? input : new BinaryReader(input);
let end = length === undefined ? reader.len : reader.pos + length; let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseHealthCheckResponse(); const message = createBaseHealthCheckResponse();
while (reader.pos < end) { while (reader.pos < end) {
@ -171,7 +195,11 @@ export const HealthCheckResponse: MessageFns<HealthCheckResponse> = {
}, },
fromJSON(object: any): HealthCheckResponse { fromJSON(object: any): HealthCheckResponse {
return { status: isSet(object.status) ? healthCheckResponse_ServingStatusFromJSON(object.status) : 0 }; return {
status: isSet(object.status)
? healthCheckResponse_ServingStatusFromJSON(object.status)
: 0,
};
}, },
toJSON(message: HealthCheckResponse): unknown { toJSON(message: HealthCheckResponse): unknown {
@ -182,10 +210,14 @@ export const HealthCheckResponse: MessageFns<HealthCheckResponse> = {
return obj; return obj;
}, },
create<I extends Exact<DeepPartial<HealthCheckResponse>, I>>(base?: I): HealthCheckResponse { create<I extends Exact<DeepPartial<HealthCheckResponse>, I>>(
base?: I,
): HealthCheckResponse {
return HealthCheckResponse.fromPartial(base ?? ({} as any)); return HealthCheckResponse.fromPartial(base ?? ({} as any));
}, },
fromPartial<I extends Exact<DeepPartial<HealthCheckResponse>, I>>(object: I): HealthCheckResponse { fromPartial<I extends Exact<DeepPartial<HealthCheckResponse>, I>>(
object: I,
): HealthCheckResponse {
const message = createBaseHealthCheckResponse(); const message = createBaseHealthCheckResponse();
message.status = object.status ?? 0; message.status = object.status ?? 0;
return message; return message;
@ -195,21 +227,25 @@ export const HealthCheckResponse: MessageFns<HealthCheckResponse> = {
export type HealthService = typeof HealthService; export type HealthService = typeof HealthService;
export const HealthService = { export const HealthService = {
check: { check: {
path: "/com.ql.health.Health/Check", path: '/com.ql.health.Health/Check',
requestStream: false, requestStream: false,
responseStream: false, responseStream: false,
requestSerialize: (value: HealthCheckRequest) => Buffer.from(HealthCheckRequest.encode(value).finish()), requestSerialize: (value: HealthCheckRequest) =>
Buffer.from(HealthCheckRequest.encode(value).finish()),
requestDeserialize: (value: Buffer) => HealthCheckRequest.decode(value), requestDeserialize: (value: Buffer) => HealthCheckRequest.decode(value),
responseSerialize: (value: HealthCheckResponse) => Buffer.from(HealthCheckResponse.encode(value).finish()), responseSerialize: (value: HealthCheckResponse) =>
Buffer.from(HealthCheckResponse.encode(value).finish()),
responseDeserialize: (value: Buffer) => HealthCheckResponse.decode(value), responseDeserialize: (value: Buffer) => HealthCheckResponse.decode(value),
}, },
watch: { watch: {
path: "/com.ql.health.Health/Watch", path: '/com.ql.health.Health/Watch',
requestStream: false, requestStream: false,
responseStream: true, responseStream: true,
requestSerialize: (value: HealthCheckRequest) => Buffer.from(HealthCheckRequest.encode(value).finish()), requestSerialize: (value: HealthCheckRequest) =>
Buffer.from(HealthCheckRequest.encode(value).finish()),
requestDeserialize: (value: Buffer) => HealthCheckRequest.decode(value), requestDeserialize: (value: Buffer) => HealthCheckRequest.decode(value),
responseSerialize: (value: HealthCheckResponse) => Buffer.from(HealthCheckResponse.encode(value).finish()), responseSerialize: (value: HealthCheckResponse) =>
Buffer.from(HealthCheckResponse.encode(value).finish()),
responseDeserialize: (value: Buffer) => HealthCheckResponse.decode(value), responseDeserialize: (value: Buffer) => HealthCheckResponse.decode(value),
}, },
} as const; } as const;
@ -222,20 +258,32 @@ export interface HealthServer extends UntypedServiceImplementation {
export interface HealthClient extends Client { export interface HealthClient extends Client {
check( check(
request: HealthCheckRequest, request: HealthCheckRequest,
callback: (error: ServiceError | null, response: HealthCheckResponse) => void, callback: (
error: ServiceError | null,
response: HealthCheckResponse,
) => void,
): ClientUnaryCall; ): ClientUnaryCall;
check( check(
request: HealthCheckRequest, request: HealthCheckRequest,
metadata: Metadata, metadata: Metadata,
callback: (error: ServiceError | null, response: HealthCheckResponse) => void, callback: (
error: ServiceError | null,
response: HealthCheckResponse,
) => void,
): ClientUnaryCall; ): ClientUnaryCall;
check( check(
request: HealthCheckRequest, request: HealthCheckRequest,
metadata: Metadata, metadata: Metadata,
options: Partial<CallOptions>, options: Partial<CallOptions>,
callback: (error: ServiceError | null, response: HealthCheckResponse) => void, callback: (
error: ServiceError | null,
response: HealthCheckResponse,
) => void,
): ClientUnaryCall; ): ClientUnaryCall;
watch(request: HealthCheckRequest, options?: Partial<CallOptions>): ClientReadableStream<HealthCheckResponse>; watch(
request: HealthCheckRequest,
options?: Partial<CallOptions>,
): ClientReadableStream<HealthCheckResponse>;
watch( watch(
request: HealthCheckRequest, request: HealthCheckRequest,
metadata?: Metadata, metadata?: Metadata,
@ -243,23 +291,44 @@ export interface HealthClient extends Client {
): ClientReadableStream<HealthCheckResponse>; ): ClientReadableStream<HealthCheckResponse>;
} }
export const HealthClient = makeGenericClientConstructor(HealthService, "com.ql.health.Health") as unknown as { export const HealthClient = makeGenericClientConstructor(
new (address: string, credentials: ChannelCredentials, options?: Partial<ClientOptions>): HealthClient; HealthService,
'com.ql.health.Health',
) as unknown as {
new (
address: string,
credentials: ChannelCredentials,
options?: Partial<ClientOptions>,
): HealthClient;
service: typeof HealthService; service: typeof HealthService;
serviceName: string; serviceName: string;
}; };
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; type Builtin =
| Date
| Function
| Uint8Array
| string
| number
| boolean
| undefined;
export type DeepPartial<T> = T extends Builtin ? T export type DeepPartial<T> = T extends Builtin
: T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>> ? T
: T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>> : T extends globalThis.Array<infer U>
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> } ? globalThis.Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U>
? ReadonlyArray<DeepPartial<U>>
: T extends {}
? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>; : Partial<T>;
type KeysOfUnion<T> = T extends T ? keyof T : never; type KeysOfUnion<T> = T extends T ? keyof T : never;
export type Exact<P, I extends P> = P extends Builtin ? P export type Exact<P, I extends P> = P extends Builtin
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never }; ? P
: P & { [K in keyof P]: Exact<P[K], I[K]> } & {
[K in Exclude<keyof I, KeysOfUnion<P>>]: never;
};
function isSet(value: any): boolean { function isSet(value: any): boolean {
return value !== null && value !== undefined; return value !== null && value !== undefined;

View File

@ -231,7 +231,8 @@ export const systemNotify = async (
const data = await systemService.notify({ const data = await systemService.notify({
title: call.request.title, title: call.request.title,
content: call.request.content, content: call.request.content,
notificationInfo: call.request.notificationInfo as unknown as NotificationInfo, notificationInfo: call.request
.notificationInfo as unknown as NotificationInfo,
}); });
callback(null, data); callback(null, data);
} catch (e: any) { } catch (e: any) {

View File

@ -9,11 +9,8 @@ const delCron = (
) => { ) => {
for (const id of call.request.ids) { for (const id of call.request.ids) {
if (scheduleStacks.has(id)) { if (scheduleStacks.has(id)) {
Logger.info( Logger.info('[schedule][取消定时任务] 任务ID: %s', id);
'[schedule][取消定时任务] 任务ID: %s', scheduleStacks.get(id)?.forEach((x) => x.cancel());
id,
);
scheduleStacks.get(id)?.forEach(x => x.cancel());
scheduleStacks.delete(id); scheduleStacks.delete(id);
} }
} }

View File

@ -24,7 +24,9 @@ const check = async (
`tail -n 300 ~/.pm2/logs/schedule-error.log`, `tail -n 300 ~/.pm2/logs/schedule-error.log`,
); );
return callback( return callback(
new Error(`${scheduleErrLog || ''}\n${panelErrLog || ''}\n${res}`.trim()), new Error(
`${scheduleErrLog || ''}\n${panelErrLog || ''}\n${res}`.trim(),
),
); );
default: default:

View File

@ -28,7 +28,7 @@ export default class DependenceService {
constructor( constructor(
@Inject('logger') private logger: winston.Logger, @Inject('logger') private logger: winston.Logger,
private sockService: SockService, private sockService: SockService,
) { } ) {}
public async create(payloads: Dependence[]): Promise<Dependence[]> { public async create(payloads: Dependence[]): Promise<Dependence[]> {
const tabs = payloads.map((x) => { const tabs = payloads.map((x) => {

View File

@ -16,7 +16,7 @@ class MetricsService {
// 定期清理旧数据 // 定期清理旧数据
setInterval(() => { setInterval(() => {
const oneHourAgo = Date.now() - 3600000; const oneHourAgo = Date.now() - 3600000;
this.metrics = this.metrics.filter(m => m.timestamp > oneHourAgo); this.metrics = this.metrics.filter((m) => m.timestamp > oneHourAgo);
}, 60000); }, 60000);
} }
@ -46,7 +46,11 @@ class MetricsService {
} }
} }
async measureAsync(name: string, fn: () => Promise<void>, tags?: Record<string, string>) { async measureAsync(
name: string,
fn: () => Promise<void>,
tags?: Record<string, string>,
) {
const start = performance.now(); const start = performance.now();
try { try {
await fn(); await fn();
@ -60,21 +64,24 @@ class MetricsService {
let filtered = this.metrics; let filtered = this.metrics;
if (name) { if (name) {
filtered = filtered.filter(m => m.name === name); filtered = filtered.filter((m) => m.name === name);
} }
if (tags) { if (tags) {
filtered = filtered.filter(m => { filtered = filtered.filter((m) => {
if (!m.tags) return false; if (!m.tags) return false;
return Object.entries(tags).every(([key, value]) => m.tags![key] === value); return Object.entries(tags).every(
([key, value]) => m.tags![key] === value,
);
}); });
} }
return { return {
count: filtered.length, count: filtered.length,
average: filtered.reduce((acc, curr) => acc + curr.value, 0) / filtered.length, average:
min: Math.min(...filtered.map(m => m.value)), filtered.reduce((acc, curr) => acc + curr.value, 0) / filtered.length,
max: Math.max(...filtered.map(m => m.value)), min: Math.min(...filtered.map((m) => m.value)),
max: Math.max(...filtered.map((m) => m.value)),
metrics: filtered, metrics: filtered,
}; };
} }

View File

@ -7,7 +7,7 @@ import { SockMessage } from '../data/sock';
export default class SockService { export default class SockService {
private clients: Connection[] = []; private clients: Connection[] = [];
constructor(@Inject('logger') private logger: winston.Logger) { } constructor(@Inject('logger') private logger: winston.Logger) {}
public getClients() { public getClients() {
return this.clients; return this.clients;

View File

@ -47,7 +47,7 @@ export default class SystemService {
@Inject('logger') private logger: winston.Logger, @Inject('logger') private logger: winston.Logger,
private scheduleService: ScheduleService, private scheduleService: ScheduleService,
private sockService: SockService, private sockService: SockService,
) { } ) {}
public async getSystemConfig() { public async getSystemConfig() {
const doc = await this.getDb({ type: AuthDataType.systemConfig }); const doc = await this.getDb({ type: AuthDataType.systemConfig });
@ -287,7 +287,7 @@ export default class SystemService {
); );
const text = await body.text(); const text = await body.text();
lastVersionContent = parseContentVersion(text); lastVersionContent = parseContentVersion(text);
} catch (error) { } } catch (error) {}
if (!lastVersionContent) { if (!lastVersionContent) {
lastVersionContent = currentVersionContent; lastVersionContent = currentVersionContent;
@ -401,16 +401,23 @@ export default class SystemService {
} }
} }
public async run({ command, logPath }: { command: string; logPath?: string }, callback: TaskCallbacks) { public async run(
{ command, logPath }: { command: string; logPath?: string },
callback: TaskCallbacks,
) {
if (!command.startsWith(TASK_COMMAND)) { if (!command.startsWith(TASK_COMMAND)) {
command = `${TASK_COMMAND} ${command}`; command = `${TASK_COMMAND} ${command}`;
} }
const logPathPrefix = logPath ? `real_log_path=${logPath}` : '' const logPathPrefix = logPath ? `real_log_path=${logPath}` : '';
this.scheduleService.runTask(`${logPathPrefix} real_time=true ${command}`, callback, { this.scheduleService.runTask(
`${logPathPrefix} real_time=true ${command}`,
callback,
{
command, command,
id: command.replace(/ /g, '-'), id: command.replace(/ /g, '-'),
runOrigin: 'system', runOrigin: 'system',
}); },
);
} }
public async stop({ command, pid }: { command: string; pid: number }) { public async stop({ command, pid }: { command: string; pid: number }) {
@ -443,7 +450,8 @@ export default class SystemService {
} }
const dataPaths = dataDirs.map((dir) => `data/${dir}`); const dataPaths = dataDirs.map((dir) => `data/${dir}`);
await promiseExec( await promiseExec(
`cd ${config.dataPath} && cd ../ && tar -zcvf ${config.dataTgzFile `cd ${config.dataPath} && cd ../ && tar -zcvf ${
config.dataTgzFile
} ${dataPaths.join(' ')}`, } ${dataPaths.join(' ')}`,
); );
res.download(config.dataTgzFile); res.download(config.dataTgzFile);
@ -537,7 +545,7 @@ export default class SystemService {
try { try {
const finalPath = path.join(config.dependenceCachePath, type); const finalPath = path.join(config.dependenceCachePath, type);
await fs.promises.rm(finalPath, { recursive: true }); await fs.promises.rm(finalPath, { recursive: true });
} catch (error) { } } catch (error) {}
return { code: 200 }; return { code: 200 };
} }
} }

View File

@ -11,6 +11,7 @@ import {
SystemModelInfo, SystemModelInfo,
LoginStatus, LoginStatus,
AuthInfo, AuthInfo,
TokenInfo,
} from '../data/system'; } from '../data/system';
import { NotificationInfo } from '../data/notify'; import { NotificationInfo } from '../data/notify';
import NotificationService from './notify'; import NotificationService from './notify';
@ -101,12 +102,23 @@ export default class UserService {
algorithm: 'HS384', algorithm: 'HS384',
}); });
const tokenInfo: TokenInfo = {
value: token,
timestamp,
ip,
address,
platform: req.platform,
};
const updatedTokens = this.addTokenToList(
tokens,
req.platform,
tokenInfo,
);
await this.updateAuthInfo(content, { await this.updateAuthInfo(content, {
token, token,
tokens: { tokens: updatedTokens,
...tokens,
[req.platform]: token,
},
lastlogon: timestamp, lastlogon: timestamp,
retries: 0, retries: 0,
lastip: ip, lastip: ip,
@ -180,11 +192,37 @@ export default class UserService {
} }
} }
public async logout(platform: string): Promise<any> { public async logout(platform: string, tokenValue: string): Promise<any> {
if (!platform || !tokenValue) {
this.logger.warn('Invalid logout parameters - empty platform or token');
return;
}
const authInfo = await this.getAuthInfo(); const authInfo = await this.getAuthInfo();
// Verify the token exists before attempting to remove it
const tokenExists = this.findTokenInList(
authInfo.tokens,
platform,
tokenValue,
);
if (!tokenExists && authInfo.token !== tokenValue) {
// Token not found, but don't throw error - user may have already logged out
this.logger.info(
`Logout attempted for non-existent token on platform: ${platform}`,
);
return;
}
const updatedTokens = this.removeTokenFromList(
authInfo.tokens,
platform,
tokenValue,
);
await this.updateAuthInfo(authInfo, { await this.updateAuthInfo(authInfo, {
token: '', token: authInfo.token === tokenValue ? '' : authInfo.token,
tokens: { ...authInfo.tokens, [platform]: '' }, tokens: updatedTokens,
}); });
} }
@ -364,6 +402,100 @@ export default class UserService {
} }
} }
private normalizeTokens(
tokens: Record<string, string | TokenInfo[]>,
): Record<string, TokenInfo[]> {
const normalized: Record<string, TokenInfo[]> = {};
for (const [platform, value] of Object.entries(tokens)) {
if (typeof value === 'string') {
// Legacy format: convert string token to TokenInfo array
if (value) {
normalized[platform] = [
{
value,
timestamp: Date.now(),
ip: '',
address: '',
platform,
},
];
} else {
normalized[platform] = [];
}
} else {
// Already in new format
normalized[platform] = value || [];
}
}
return normalized;
}
private addTokenToList(
tokens: Record<string, string | TokenInfo[]>,
platform: string,
tokenInfo: TokenInfo,
maxTokensPerPlatform: number = config.maxTokensPerPlatform,
): Record<string, TokenInfo[]> {
// Validate maxTokensPerPlatform parameter
if (!Number.isInteger(maxTokensPerPlatform) || maxTokensPerPlatform < 1) {
this.logger.warn(
`Invalid maxTokensPerPlatform value: ${maxTokensPerPlatform}, using default`,
);
maxTokensPerPlatform = config.maxTokensPerPlatform;
}
const normalized = this.normalizeTokens(tokens);
if (!normalized[platform]) {
normalized[platform] = [];
}
// Add new token
normalized[platform].unshift(tokenInfo);
// Limit the number of active tokens per platform
if (normalized[platform].length > maxTokensPerPlatform) {
normalized[platform] = normalized[platform].slice(
0,
maxTokensPerPlatform,
);
}
return normalized;
}
private removeTokenFromList(
tokens: Record<string, string | TokenInfo[]>,
platform: string,
tokenValue: string,
): Record<string, TokenInfo[]> {
const normalized = this.normalizeTokens(tokens);
if (normalized[platform]) {
normalized[platform] = normalized[platform].filter(
(t) => t.value !== tokenValue,
);
}
return normalized;
}
private findTokenInList(
tokens: Record<string, string | TokenInfo[]>,
platform: string,
tokenValue: string,
): TokenInfo | undefined {
const normalized = this.normalizeTokens(tokens);
if (normalized[platform]) {
return normalized[platform].find((t) => t.value === tokenValue);
}
return undefined;
}
public async resetAuthInfo(info: Partial<AuthInfo>) { public async resetAuthInfo(info: Partial<AuthInfo>) {
const { retries, twoFactorActivated, password, username } = info; const { retries, twoFactorActivated, password, username } = info;
const authInfo = await this.getAuthInfo(); const authInfo = await this.getAuthInfo();

46
back/shared/auth.ts Normal file
View File

@ -0,0 +1,46 @@
import { AuthInfo, TokenInfo } from '../data/system';
/**
* Validates if a token exists in the authentication info.
* Supports both legacy string tokens and new TokenInfo array format.
*
* @param authInfo - The authentication information
* @param headerToken - The token to validate
* @param platform - The platform (desktop, mobile)
* @returns true if the token is valid, false otherwise
*/
export function isValidToken(
authInfo: AuthInfo | null | undefined,
headerToken: string,
platform: string,
): boolean {
if (!authInfo || !headerToken) {
return false;
}
const { token = '', tokens = {} } = authInfo;
// Check legacy token field
if (headerToken === token) {
return true;
}
// Check platform-specific tokens (support both legacy string and new TokenInfo[] format)
const platformTokens = tokens[platform];
// Handle null/undefined platformTokens
if (platformTokens === null || platformTokens === undefined) {
return false;
}
if (typeof platformTokens === 'string') {
// Legacy format: single string token
return headerToken === platformTokens;
} else if (Array.isArray(platformTokens)) {
// New format: array of TokenInfo objects
return platformTokens.some((t: TokenInfo) => t && t.value === headerToken);
}
// Unexpected type - log warning and reject
return false;
}

View File

@ -1,8 +1,5 @@
{ {
"watch": [ "watch": ["back", ".env"],
"back",
".env"
],
"ext": "js,ts,json", "ext": "js,ts,json",
"env": { "env": {
"NODE_ENV": "development", "NODE_ENV": "development",

View File

@ -1262,7 +1262,15 @@ function ntfyNotify(text, desp) {
} }
return new Promise((resolve) => { return new Promise((resolve) => {
const { NTFY_URL, NTFY_TOPIC, NTFY_PRIORITY, NTFY_TOKEN, NTFY_USERNAME, NTFY_PASSWORD, NTFY_ACTIONS } = push_config; const {
NTFY_URL,
NTFY_TOPIC,
NTFY_PRIORITY,
NTFY_TOKEN,
NTFY_USERNAME,
NTFY_PASSWORD,
NTFY_ACTIONS,
} = push_config;
if (NTFY_TOPIC) { if (NTFY_TOPIC) {
const options = { const options = {
url: `${NTFY_URL || 'https://ntfy.sh'}/${NTFY_TOPIC}`, url: `${NTFY_URL || 'https://ntfy.sh'}/${NTFY_TOPIC}`,
@ -1277,7 +1285,9 @@ function ntfyNotify(text, desp) {
if (NTFY_TOKEN) { if (NTFY_TOKEN) {
options.headers['Authorization'] = `Bearer ${NTFY_TOKEN}`; options.headers['Authorization'] = `Bearer ${NTFY_TOKEN}`;
} else if (NTFY_USERNAME && NTFY_PASSWORD) { } else if (NTFY_USERNAME && NTFY_PASSWORD) {
options.headers['Authorization'] = `Basic ${Buffer.from(`${NTFY_USERNAME}:${NTFY_PASSWORD}`).toString('base64')}`; options.headers['Authorization'] = `Basic ${Buffer.from(
`${NTFY_USERNAME}:${NTFY_PASSWORD}`,
).toString('base64')}`;
} }
if (NTFY_ACTIONS) { if (NTFY_ACTIONS) {
options.headers['Actions'] = encodeRFC2047(NTFY_ACTIONS); options.headers['Actions'] = encodeRFC2047(NTFY_ACTIONS);

View File

@ -4,14 +4,16 @@ import intl from 'react-intl-universal';
export function rootContainer(container: any) { export function rootContainer(container: any) {
const locales = { const locales = {
'en': require('./locales/en-US.json'), en: require('./locales/en-US.json'),
'zh': require('./locales/zh-CN.json'), zh: require('./locales/zh-CN.json'),
}; };
let currentLocale = intl.determineLocale({ let currentLocale = intl
.determineLocale({
urlLocaleKey: 'lang', urlLocaleKey: 'lang',
cookieLocaleKey: 'lang', cookieLocaleKey: 'lang',
localStorageLocaleKey: 'lang', localStorageLocaleKey: 'lang',
}).slice(0, 2); })
.slice(0, 2);
if (!currentLocale || !Object.keys(locales).includes(currentLocale)) { if (!currentLocale || !Object.keys(locales).includes(currentLocale)) {
currentLocale = 'zh'; currentLocale = 'zh';

View File

@ -136,7 +136,7 @@ const Config = () => {
lineNumbersMinChars: 3, lineNumbersMinChars: 3,
folding: false, folding: false,
glyphMargin: false, glyphMargin: false,
accessibilitySupport: 'off' accessibilitySupport: 'off',
}} }}
onMount={(editor) => { onMount={(editor) => {
editorRef.current = editor; editorRef.current = editor;

View File

@ -1,5 +1,5 @@
import intl from "react-intl-universal"; import intl from 'react-intl-universal';
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from 'react';
import { import {
Modal, Modal,
message, message,
@ -8,17 +8,17 @@ import {
Statistic, Statistic,
Button, Button,
Typography, Typography,
} from "antd"; } from 'antd';
import { request } from "@/utils/http"; import { request } from '@/utils/http';
import config from "@/utils/config"; import config from '@/utils/config';
import { import {
Loading3QuartersOutlined, Loading3QuartersOutlined,
CheckCircleOutlined, CheckCircleOutlined,
} from "@ant-design/icons"; } from '@ant-design/icons';
import { PageLoading } from "@ant-design/pro-layout"; import { PageLoading } from '@ant-design/pro-layout';
import { logEnded } from "@/utils"; import { logEnded } from '@/utils';
import { CrontabStatus } from "./type"; import { CrontabStatus } from './type';
import Ansi from "ansi-to-react"; import Ansi from 'ansi-to-react';
const { Countdown } = Statistic; const { Countdown } = Statistic;
@ -33,7 +33,7 @@ const CronLogModal = ({
data?: string; data?: string;
logUrl?: string; logUrl?: string;
}) => { }) => {
const [value, setValue] = useState<string>(intl.get("启动中...")); const [value, setValue] = useState<string>(intl.get('启动中...'));
const [loading, setLoading] = useState<any>(true); const [loading, setLoading] = useState<any>(true);
const [executing, setExecuting] = useState<any>(true); const [executing, setExecuting] = useState<any>(true);
const [isPhone, setIsPhone] = useState(false); const [isPhone, setIsPhone] = useState(false);
@ -49,15 +49,15 @@ const CronLogModal = ({
.then(({ code, data }) => { .then(({ code, data }) => {
if ( if (
code === 200 && code === 200 &&
localStorage.getItem("logCron") === uniqPath && localStorage.getItem('logCron') === uniqPath &&
data !== value data !== value
) { ) {
const log = data as string; const log = data as string;
setValue(log || intl.get("暂无日志")); setValue(log || intl.get('暂无日志'));
const hasNext = Boolean( const hasNext = Boolean(
log && !logEnded(log) && !log.includes("日志不存在"), log && !logEnded(log) && !log.includes('日志不存在'),
); );
if (!hasNext && !logEnded(value) && value !== intl.get("启动中...")) { if (!hasNext && !logEnded(value) && value !== intl.get('启动中...')) {
setTimeout(() => { setTimeout(() => {
autoScroll(); autoScroll();
}); });
@ -85,13 +85,13 @@ const CronLogModal = ({
setTimeout(() => { setTimeout(() => {
document document
.querySelector("#log-flag") .querySelector('#log-flag')
?.scrollIntoView({ behavior: "smooth" }); ?.scrollIntoView({ behavior: 'smooth' });
}, 600); }, 600);
}; };
const cancel = () => { const cancel = () => {
localStorage.removeItem("logCron"); localStorage.removeItem('logCron');
handleCancel(); handleCancel();
}; };
@ -107,7 +107,7 @@ const CronLogModal = ({
const titleElement = () => { const titleElement = () => {
return ( return (
<div style={{ display: "flex", alignItems: "center" }}> <div style={{ display: 'flex', alignItems: 'center' }}>
{(executing || loading) && <Loading3QuartersOutlined spin />} {(executing || loading) && <Loading3QuartersOutlined spin />}
{!executing && !loading && <CheckCircleOutlined />} {!executing && !loading && <CheckCircleOutlined />}
<Typography.Text ellipsis={true} style={{ marginLeft: 5 }}> <Typography.Text ellipsis={true} style={{ marginLeft: 5 }}>
@ -144,7 +144,7 @@ const CronLogModal = ({
onCancel={() => cancel()} onCancel={() => cancel()}
footer={[ footer={[
<Button type="primary" onClick={() => cancel()}> <Button type="primary" onClick={() => cancel()}>
{intl.get("知道了")} {intl.get('知道了')}
</Button>, </Button>,
]} ]}
> >
@ -156,7 +156,7 @@ const CronLogModal = ({
style={ style={
isPhone isPhone
? { ? {
fontFamily: "Source Code Pro", fontFamily: 'Source Code Pro',
zoom: 0.83, zoom: 0.83,
} }
: {} : {}

View File

@ -312,4 +312,3 @@ const CronLabelModal = ({
}; };
export { CronLabelModal, CronModal as default }; export { CronLabelModal, CronModal as default };

View File

@ -26,7 +26,9 @@ const Diff = () => {
const getConfig = () => { const getConfig = () => {
request request
.get(`${config.apiPrefix}configs/detail?path=${encodeURIComponent(current)}`) .get(
`${config.apiPrefix}configs/detail?path=${encodeURIComponent(current)}`,
)
.then(({ code, data }) => { .then(({ code, data }) => {
if (code === 200) { if (code === 200) {
setCurrentValue(data); setCurrentValue(data);
@ -36,7 +38,9 @@ const Diff = () => {
const getSample = () => { const getSample = () => {
request request
.get(`${config.apiPrefix}configs/detail?path=${encodeURIComponent(origin)}`) .get(
`${config.apiPrefix}configs/detail?path=${encodeURIComponent(origin)}`,
)
.then(({ code, data }) => { .then(({ code, data }) => {
if (code === 200) { if (code === 200) {
setOriginValue(data); setOriginValue(data);

View File

@ -1,4 +1,4 @@
import intl from 'react-intl-universal' import intl from 'react-intl-universal';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Modal, message, Input, Form } from 'antd'; import { Modal, message, Input, Form } from 'antd';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
@ -55,7 +55,9 @@ const EditNameModal = ({
<Form form={form} layout="vertical" name="edit_name_modal"> <Form form={form} layout="vertical" name="edit_name_modal">
<Form.Item <Form.Item
name="name" name="name"
rules={[{ required: true, message: intl.get('请输入新的环境变量名称') }]} rules={[
{ required: true, message: intl.get('请输入新的环境变量名称') },
]}
> >
<Input placeholder={intl.get('请输入新的环境变量名称')} /> <Input placeholder={intl.get('请输入新的环境变量名称')} />
</Form.Item> </Form.Item>

View File

@ -17,9 +17,7 @@ const UnsupportedFilePreview: React.FC<UnsupportedFilePreviewProps> = ({
<div className={styles.iconWrapper}> <div className={styles.iconWrapper}>
<FileUnknownOutlined className={styles.icon} /> <FileUnknownOutlined className={styles.icon} />
</div> </div>
<div className={styles.message}> <div className={styles.message}>{intl.get('当前文件不支持预览')}</div>
{intl.get('当前文件不支持预览')}
</div>
<Space direction="vertical" size={8} className={styles.actionArea}> <Space direction="vertical" size={8} className={styles.actionArea}>
<Button <Button
type="primary" type="primary"

View File

@ -1,23 +1,23 @@
import { disableBody } from "@/utils"; import { disableBody } from '@/utils';
import config from "@/utils/config"; import config from '@/utils/config';
import { request } from "@/utils/http"; import { request } from '@/utils/http';
import WebSocketManager from "@/utils/websocket"; import WebSocketManager from '@/utils/websocket';
import Ansi from "ansi-to-react"; import Ansi from 'ansi-to-react';
import { Button, Modal, Statistic, message } from "antd"; import { Button, Modal, Statistic, message } from 'antd';
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from 'react';
import intl from "react-intl-universal"; import intl from 'react-intl-universal';
const { Countdown } = Statistic; const { Countdown } = Statistic;
const CheckUpdate = ({ systemInfo }: any) => { const CheckUpdate = ({ systemInfo }: any) => {
const [updateLoading, setUpdateLoading] = useState(false); const [updateLoading, setUpdateLoading] = useState(false);
const [value, setValue] = useState(""); const [value, setValue] = useState('');
const modalRef = useRef<any>(); const modalRef = useRef<any>();
const checkUpgrade = () => { const checkUpgrade = () => {
if (updateLoading) return; if (updateLoading) return;
setUpdateLoading(true); setUpdateLoading(true);
message.loading(intl.get("检查更新中..."), 0); message.loading(intl.get('检查更新中...'), 0);
request request
.put(`${config.apiPrefix}system/update-check`) .put(`${config.apiPrefix}system/update-check`)
.then(({ code, data }) => { .then(({ code, data }) => {
@ -42,22 +42,22 @@ const CheckUpdate = ({ systemInfo }: any) => {
const showForceUpdateModal = (data: any) => { const showForceUpdateModal = (data: any) => {
Modal.confirm({ Modal.confirm({
width: 500, width: 500,
title: intl.get("更新"), title: intl.get('更新'),
content: ( content: (
<> <>
<div>{intl.get("已经是最新版了!")}</div> <div>{intl.get('已经是最新版了!')}</div>
<div style={{ fontSize: 12, fontWeight: 400, marginTop: 5 }}> <div style={{ fontSize: 12, fontWeight: 400, marginTop: 5 }}>
{intl.get("青龙")} {data.lastVersion}{" "} {intl.get('青龙')} {data.lastVersion}{' '}
{intl.get("是目前检测到的最新可用版本了。")} {intl.get('是目前检测到的最新可用版本了。')}
</div> </div>
</> </>
), ),
okText: intl.get("重新下载"), okText: intl.get('重新下载'),
onOk() { onOk() {
showUpdatingModal(); showUpdatingModal();
request request
.put(`${config.apiPrefix}system/update`) .put(`${config.apiPrefix}system/update`)
.then((_data: any) => { }) .then((_data: any) => {})
.catch((error: any) => { .catch((error: any) => {
console.log(error); console.log(error);
}); });
@ -71,10 +71,10 @@ const CheckUpdate = ({ systemInfo }: any) => {
width: 500, width: 500,
title: ( title: (
<> <>
<div>{intl.get("更新可用")}</div> <div>{intl.get('更新可用')}</div>
<div style={{ fontSize: 12, fontWeight: 400, marginTop: 5 }}> <div style={{ fontSize: 12, fontWeight: 400, marginTop: 5 }}>
{intl.get("新版本")} {lastVersion}{" "} {intl.get('新版本')} {lastVersion}{' '}
{intl.get("可用,你使用的版本为")} {systemInfo.version} {intl.get('可用,你使用的版本为')} {systemInfo.version}
</div> </div>
</> </>
), ),
@ -83,13 +83,13 @@ const CheckUpdate = ({ systemInfo }: any) => {
<Ansi>{lastLog}</Ansi> <Ansi>{lastLog}</Ansi>
</pre> </pre>
), ),
okText: intl.get("下载更新"), okText: intl.get('下载更新'),
cancelText: intl.get("以后再说"), cancelText: intl.get('以后再说'),
onOk() { onOk() {
showUpdatingModal(); showUpdatingModal();
request request
.put(`${config.apiPrefix}system/update`) .put(`${config.apiPrefix}system/update`)
.then((_data: any) => { }) .then((_data: any) => {})
.catch((error: any) => { .catch((error: any) => {
console.log(error); console.log(error);
}); });
@ -98,14 +98,14 @@ const CheckUpdate = ({ systemInfo }: any) => {
}; };
const showUpdatingModal = () => { const showUpdatingModal = () => {
setValue(""); setValue('');
modalRef.current = Modal.info({ modalRef.current = Modal.info({
width: 600, width: 600,
maskClosable: false, maskClosable: false,
closable: false, closable: false,
keyboard: false, keyboard: false,
okButtonProps: { disabled: true }, okButtonProps: { disabled: true },
title: intl.get("下载更新中..."), title: intl.get('下载更新中...'),
centered: true, centered: true,
content: ( content: (
<pre> <pre>
@ -122,13 +122,13 @@ const CheckUpdate = ({ systemInfo }: any) => {
message.success({ message.success({
content: ( content: (
<span> <span>
{intl.get("系统将在")} {intl.get('系统将在')}
<Countdown <Countdown
className="inline-countdown" className="inline-countdown"
format="ss" format="ss"
value={Date.now() + 1000 * 30} value={Date.now() + 1000 * 30}
/> />
{intl.get("秒后自动刷新")} {intl.get('秒后自动刷新')}
</span> </span>
), ),
duration: 30, duration: 30,
@ -147,12 +147,12 @@ const CheckUpdate = ({ systemInfo }: any) => {
Modal.confirm({ Modal.confirm({
width: 600, width: 600,
maskClosable: false, maskClosable: false,
title: intl.get("确认重启"), title: intl.get('确认重启'),
centered: true, centered: true,
content: intl.get("系统安装包下载成功,确认重启"), content: intl.get('系统安装包下载成功,确认重启'),
okText: intl.get("重启"), okText: intl.get('重启'),
onOk() { onOk() {
reloadSystem("system"); reloadSystem('system');
}, },
onCancel() { onCancel() {
modalRef.current.update({ modalRef.current.update({
@ -166,7 +166,7 @@ const CheckUpdate = ({ systemInfo }: any) => {
useEffect(() => { useEffect(() => {
if (!value) return; if (!value) return;
const updateFailed = value.includes("失败,请检查"); const updateFailed = value.includes('失败,请检查');
modalRef.current.update({ modalRef.current.update({
maskClosable: updateFailed, maskClosable: updateFailed,
@ -185,19 +185,19 @@ const CheckUpdate = ({ systemInfo }: any) => {
const handleMessage = useCallback((payload: any) => { const handleMessage = useCallback((payload: any) => {
let { message: _message } = payload; let { message: _message } = payload;
const updateFailed = _message.includes("失败,请检查"); const updateFailed = _message.includes('失败,请检查');
if (updateFailed) { if (updateFailed) {
message.error(intl.get("更新失败,请检查网络及日志或稍后再试")); message.error(intl.get('更新失败,请检查网络及日志或稍后再试'));
} }
setTimeout(() => { setTimeout(() => {
document document
.querySelector("#log-identifier") .querySelector('#log-identifier')
?.scrollIntoView({ behavior: "smooth" }); ?.scrollIntoView({ behavior: 'smooth' });
}, 600); }, 600);
if (_message.includes("更新包下载成功")) { if (_message.includes('更新包下载成功')) {
setTimeout(() => { setTimeout(() => {
showReloadModal(); showReloadModal();
}, 1000); }, 1000);
@ -208,24 +208,24 @@ const CheckUpdate = ({ systemInfo }: any) => {
useEffect(() => { useEffect(() => {
const ws = WebSocketManager.getInstance(); const ws = WebSocketManager.getInstance();
ws.subscribe("updateSystemVersion", handleMessage); ws.subscribe('updateSystemVersion', handleMessage);
return () => { return () => {
ws.unsubscribe("updateSystemVersion", handleMessage); ws.unsubscribe('updateSystemVersion', handleMessage);
}; };
}, []); }, []);
return ( return (
<> <>
<Button type="primary" onClick={checkUpgrade}> <Button type="primary" onClick={checkUpgrade}>
{intl.get("检查更新")} {intl.get('检查更新')}
</Button> </Button>
<Button <Button
type="primary" type="primary"
onClick={() => reloadSystem("reload")} onClick={() => reloadSystem('reload')}
style={{ marginLeft: 8 }} style={{ marginLeft: 8 }}
> >
{intl.get("重新启动")} {intl.get('重新启动')}
</Button> </Button>
</> </>
); );

View File

@ -76,7 +76,9 @@ const NotificationSetting = ({ data }: any) => {
> >
{x.items ? ( {x.items ? (
<Select <Select
placeholder={x.placeholder || `${intl.get('请选择')} ${x.label}`} placeholder={
x.placeholder || `${intl.get('请选择')} ${x.label}`
}
disabled={loading} disabled={loading}
> >
{x.items.map((y) => ( {x.items.map((y) => (
@ -89,7 +91,9 @@ const NotificationSetting = ({ data }: any) => {
<Input.TextArea <Input.TextArea
disabled={loading} disabled={loading}
autoSize={{ minRows: 1, maxRows: 5 }} autoSize={{ minRows: 1, maxRows: 5 }}
placeholder={x.placeholder || `${intl.get('请输入')} ${x.label}`} placeholder={
x.placeholder || `${intl.get('请输入')} ${x.label}`
}
/> />
)} )}
</Form.Item> </Form.Item>

View File

@ -97,9 +97,7 @@ const SecuritySettings = ({ user, userChange }: any) => {
const onChange = (e) => { const onChange = (e) => {
if (e.file && e.file.response) { if (e.file && e.file.response) {
setAvatar( setAvatar(`${config.apiPrefix}static/${e.file.response.data}`);
`${config.apiPrefix}static/${e.file.response.data}`,
);
userChange(); userChange();
} }
}; };

View File

@ -84,9 +84,7 @@ const fileTypeConfigs: Record<string, FileTypeConfig> = {
'.gcloudignore', '.gcloudignore',
'.htaccess', '.htaccess',
], ],
patterns: [ patterns: [/^\.env\./],
/^\.env\./,
],
}, },
// CI/CD 配置 // CI/CD 配置
@ -113,21 +111,26 @@ export function canPreviewInMonaco(fileName: string): boolean {
const lowercaseFileName = fileName.toLowerCase(); const lowercaseFileName = fileName.toLowerCase();
// 检查 Monaco 原生支持 // 检查 Monaco 原生支持
if (supportedLanguages.some((lang) => if (
supportedLanguages.some(
(lang) =>
lang.extensions?.includes(ext) || lang.extensions?.includes(ext) ||
(lang.filenames?.includes(lowercaseFileName)) lang.filenames?.includes(lowercaseFileName),
)) { )
) {
return true; return true;
} }
// 检查额外支持的文件类型 // 检查额外支持的文件类型
return Object.values(fileTypeConfigs).some(config => { return Object.values(fileTypeConfigs).some((config) => {
return ( return (
(config.extensions?.includes(ext)) || config.extensions?.includes(ext) ||
(config.filenames?.includes(lowercaseFileName)) || config.filenames?.includes(lowercaseFileName) ||
(config.patterns?.some(pattern => pattern.test(lowercaseFileName))) || config.patterns?.some((pattern) => pattern.test(lowercaseFileName)) ||
(config.startsWith?.some(prefix => lowercaseFileName.startsWith(prefix))) || config.startsWith?.some((prefix) =>
(config.endsWith?.some(suffix => lowercaseFileName.endsWith(suffix))) lowercaseFileName.startsWith(prefix),
) ||
config.endsWith?.some((suffix) => lowercaseFileName.endsWith(suffix))
); );
}); });
} }
@ -145,11 +148,13 @@ export function getFileCategory(fileName: string): string {
for (const [category, config] of Object.entries(fileTypeConfigs)) { for (const [category, config] of Object.entries(fileTypeConfigs)) {
if ( if (
(config.extensions?.includes(ext)) || config.extensions?.includes(ext) ||
(config.filenames?.includes(lowercaseFileName)) || config.filenames?.includes(lowercaseFileName) ||
(config.patterns?.some(pattern => pattern.test(lowercaseFileName))) || config.patterns?.some((pattern) => pattern.test(lowercaseFileName)) ||
(config.startsWith?.some(prefix => lowercaseFileName.startsWith(prefix))) || config.startsWith?.some((prefix) =>
(config.endsWith?.some(suffix => lowercaseFileName.endsWith(suffix))) lowercaseFileName.startsWith(prefix),
) ||
config.endsWith?.some((suffix) => lowercaseFileName.endsWith(suffix))
) { ) {
return category; return category;
} }
@ -157,10 +162,13 @@ export function getFileCategory(fileName: string): string {
// 检查 Monaco 原生支持 // 检查 Monaco 原生支持
const supportedLanguages = monaco.languages.getLanguages(); const supportedLanguages = monaco.languages.getLanguages();
if (supportedLanguages.some((lang) => if (
supportedLanguages.some(
(lang) =>
lang.extensions?.includes(ext) || lang.extensions?.includes(ext) ||
(lang.filenames?.includes(lowercaseFileName)) lang.filenames?.includes(lowercaseFileName),
)) { )
) {
return 'monaco-native'; return 'monaco-native';
} }

View File

@ -5,7 +5,8 @@ class WebSocketManager {
private static instance: WebSocketManager | null = null; private static instance: WebSocketManager | null = null;
private url: string; private url: string;
private socket: WebSocket | null = null; private socket: WebSocket | null = null;
private subscriptions: Map<SockMessageType, Set<(p: any) => void>> = new Map(); private subscriptions: Map<SockMessageType, Set<(p: any) => void>> =
new Map();
private options: { private options: {
maxReconnectAttempts: number; maxReconnectAttempts: number;
reconnectInterval: number; reconnectInterval: number;
@ -15,7 +16,10 @@ class WebSocketManager {
private heartbeatTimeout: NodeJS.Timeout | null = null; private heartbeatTimeout: NodeJS.Timeout | null = null;
private state: 'closed' | 'connecting' | 'open' = 'closed'; private state: 'closed' | 'connecting' | 'open' = 'closed';
constructor(url: string, options: Partial<typeof WebSocketManager.prototype.options> = {}) { constructor(
url: string,
options: Partial<typeof WebSocketManager.prototype.options> = {},
) {
this.url = url; this.url = url;
this.options = { this.options = {
maxReconnectAttempts: options.maxReconnectAttempts || 5, maxReconnectAttempts: options.maxReconnectAttempts || 5,
@ -26,7 +30,10 @@ class WebSocketManager {
this.init(); this.init();
} }
public static getInstance(url: string = '', options?: Partial<typeof WebSocketManager.prototype.options>): WebSocketManager { public static getInstance(
url: string = '',
options?: Partial<typeof WebSocketManager.prototype.options>,
): WebSocketManager {
if (!WebSocketManager.instance) { if (!WebSocketManager.instance) {
WebSocketManager.instance = new WebSocketManager(url, options); WebSocketManager.instance = new WebSocketManager(url, options);
} }
@ -47,7 +54,9 @@ class WebSocketManager {
this.socket = null; this.socket = null;
this.reconnectAttempts++; this.reconnectAttempts++;
await new Promise((resolve) => setTimeout(resolve, this.options.reconnectInterval)); await new Promise((resolve) =>
setTimeout(resolve, this.options.reconnectInterval),
);
} }
} catch (error) { } catch (error) {
this.handleError(error); this.handleError(error);