Address code review feedback - extract constants and utility functions

Co-authored-by: whyour <22700758+whyour@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2025-11-07 16:25:04 +00:00
parent 168bdb4178
commit 013f44b2bd
40 changed files with 1537 additions and 778 deletions

View File

@ -11,12 +11,11 @@ export default (app: Router) => {
route.get(
'/',
celebrate({
query:
Joi.object({
searchValue: Joi.string().optional().allow(''),
type: Joi.string().optional().allow(''),
status: Joi.string().optional().allow(''),
}),
query: Joi.object({
searchValue: Joi.string().optional().allow(''),
type: Joi.string().optional().allow(''),
status: Joi.string().optional().allow(''),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');

View File

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

View File

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

View File

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

View File

@ -81,4 +81,4 @@ export function createRandomString(min: number, max: number): string {
}
return newArr.join('');
}
}

View File

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

View File

@ -24,7 +24,13 @@ export interface AppToken {
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 const AppModel = sequelize.define<AppInstance>('App', {

View File

@ -54,6 +54,10 @@ export interface TokenInfo {
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;
}

View File

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

View File

@ -9,6 +9,7 @@ import rewrite from 'express-urlrewrite';
import { errors } from 'celebrate';
import { serveEnv } from '../config/serverEnv';
import { IKeyvStore, shareStore } from '../shared/store';
import { isValidToken } from '../shared/auth';
import path from 'path';
export default ({ app }: { app: Application }) => {
@ -77,30 +78,8 @@ export default ({ app }: { app: Application }) => {
}
const authInfo = await shareStore.getAuthInfo();
if (authInfo && headerToken) {
const { token = '', tokens = {} } = authInfo;
// Check legacy token field
if (headerToken === token) {
return next();
}
// Check platform-specific tokens (support both legacy string and new TokenInfo[] format)
const platformTokens = tokens[req.platform];
if (platformTokens) {
if (typeof platformTokens === 'string') {
// Legacy format: single string token
if (headerToken === platformTokens) {
return next();
}
} else if (Array.isArray(platformTokens)) {
// New format: array of TokenInfo objects
const tokenExists = platformTokens.some((t) => t.value === headerToken);
if (tokenExists) {
return next();
}
}
}
if (isValidToken(authInfo, headerToken, req.platform)) {
return next();
}
const errorCode = headerToken ? 'invalid_token' : 'credentials_required';

View File

@ -4,6 +4,7 @@ import { Container } from 'typedi';
import SockService from '../services/sock';
import { getPlatform } from '../config/util';
import { shareStore } from '../shared/store';
import { isValidToken } from '../shared/auth';
export default async ({ server }: { server: Server }) => {
const echo = sockJs.createServer({ prefix: '/api/ws', log: () => {} });
@ -17,51 +18,19 @@ export default async ({ server }: { server: Server }) => {
const authInfo = await shareStore.getAuthInfo();
const platform = getPlatform(conn.headers['user-agent'] || '') || 'desktop';
const headerToken = conn.url.replace(`${conn.pathname}?token=`, '');
if (authInfo) {
const { token = '', tokens = {} } = authInfo;
// Check legacy token field
if (headerToken === token) {
sockService.addClient(conn);
conn.on('data', (message) => {
conn.write(message);
});
if (isValidToken(authInfo, headerToken, platform)) {
sockService.addClient(conn);
conn.on('close', function () {
sockService.removeClient(conn);
});
conn.on('data', (message) => {
conn.write(message);
});
return;
}
// Check platform-specific tokens (support both legacy string and new TokenInfo[] format)
const platformTokens = tokens[platform];
if (platformTokens) {
let isValidToken = false;
if (typeof platformTokens === 'string') {
// Legacy format: single string token
isValidToken = headerToken === platformTokens;
} else if (Array.isArray(platformTokens)) {
// New format: array of TokenInfo objects
isValidToken = platformTokens.some((t) => t.value === headerToken);
}
if (isValidToken) {
sockService.addClient(conn);
conn.on('close', function () {
sockService.removeClient(conn);
});
conn.on('data', (message) => {
conn.write(message);
});
conn.on('close', function () {
sockService.removeClient(conn);
});
return;
}
}
return;
}
conn.close('404');

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,7 +16,7 @@ class MetricsService {
// 定期清理旧数据
setInterval(() => {
const oneHourAgo = Date.now() - 3600000;
this.metrics = this.metrics.filter(m => m.timestamp > oneHourAgo);
this.metrics = this.metrics.filter((m) => m.timestamp > oneHourAgo);
}, 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();
try {
await fn();
@ -58,23 +62,26 @@ class MetricsService {
getMetrics(name?: string, tags?: Record<string, string>) {
let filtered = this.metrics;
if (name) {
filtered = filtered.filter(m => m.name === name);
filtered = filtered.filter((m) => m.name === name);
}
if (tags) {
filtered = filtered.filter(m => {
filtered = filtered.filter((m) => {
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 {
count: filtered.length,
average: filtered.reduce((acc, curr) => acc + curr.value, 0) / filtered.length,
min: Math.min(...filtered.map(m => m.value)),
max: Math.max(...filtered.map(m => m.value)),
average:
filtered.reduce((acc, curr) => acc + curr.value, 0) / filtered.length,
min: Math.min(...filtered.map((m) => m.value)),
max: Math.max(...filtered.map((m) => m.value)),
metrics: filtered,
};
}
@ -89,4 +96,4 @@ class MetricsService {
}
}
export const metricsService = MetricsService.getInstance();
export const metricsService = MetricsService.getInstance();

View File

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

View File

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

View File

@ -110,7 +110,11 @@ export default class UserService {
platform: req.platform,
};
const updatedTokens = this.addTokenToList(tokens, req.platform, tokenInfo);
const updatedTokens = this.addTokenToList(
tokens,
req.platform,
tokenInfo,
);
await this.updateAuthInfo(content, {
token,
@ -190,8 +194,24 @@ export default class UserService {
public async logout(platform: string, tokenValue: string): Promise<any> {
const authInfo = await this.getAuthInfo();
const updatedTokens = this.removeTokenFromList(authInfo.tokens, platform, tokenValue);
// 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
return;
}
const updatedTokens = this.removeTokenFromList(
authInfo.tokens,
platform,
tokenValue,
);
await this.updateAuthInfo(authInfo, {
token: authInfo.token === tokenValue ? '' : authInfo.token,
tokens: updatedTokens,
@ -374,20 +394,24 @@ export default class UserService {
}
}
private normalizeTokens(tokens: Record<string, string | TokenInfo[]>): Record<string, TokenInfo[]> {
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,
}];
normalized[platform] = [
{
value,
timestamp: Date.now(),
ip: '',
address: '',
platform,
},
];
} else {
normalized[platform] = [];
}
@ -396,7 +420,7 @@ export default class UserService {
normalized[platform] = value || [];
}
}
return normalized;
}
@ -404,52 +428,55 @@ export default class UserService {
tokens: Record<string, string | TokenInfo[]>,
platform: string,
tokenInfo: TokenInfo,
maxTokensPerPlatform: number = 10
maxTokensPerPlatform: number = config.maxTokensPerPlatform,
): Record<string, TokenInfo[]> {
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);
normalized[platform] = normalized[platform].slice(
0,
maxTokensPerPlatform,
);
}
return normalized;
}
private removeTokenFromList(
tokens: Record<string, string | TokenInfo[]>,
platform: string,
tokenValue: string
tokenValue: string,
): Record<string, TokenInfo[]> {
const normalized = this.normalizeTokens(tokens);
if (normalized[platform]) {
normalized[platform] = normalized[platform].filter(
(t) => t.value !== tokenValue
(t) => t.value !== tokenValue,
);
}
return normalized;
}
private findTokenInList(
tokens: Record<string, string | TokenInfo[]>,
platform: string,
tokenValue: string
tokenValue: string,
): TokenInfo | undefined {
const normalized = this.normalizeTokens(tokens);
if (normalized[platform]) {
return normalized[platform].find((t) => t.value === tokenValue);
}
return undefined;
}

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

@ -0,0 +1,41 @@
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];
if (platformTokens) {
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.value === headerToken);
}
}
return false;
}

View File

@ -8,4 +8,4 @@ declare global {
platform: 'desktop' | 'mobile';
}
}
}
}

View File

@ -1,8 +1,5 @@
{
"watch": [
"back",
".env"
],
"watch": ["back", ".env"],
"ext": "js,ts,json",
"env": {
"NODE_ENV": "development",
@ -12,4 +9,4 @@
"execMap": {
"ts": "node --require ts-node/register"
}
}
}

View File

@ -1262,7 +1262,15 @@ function ntfyNotify(text, desp) {
}
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) {
const options = {
url: `${NTFY_URL || 'https://ntfy.sh'}/${NTFY_TOPIC}`,
@ -1277,7 +1285,9 @@ function ntfyNotify(text, desp) {
if (NTFY_TOKEN) {
options.headers['Authorization'] = `Bearer ${NTFY_TOKEN}`;
} 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) {
options.headers['Actions'] = encodeRFC2047(NTFY_ACTIONS);

View File

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

View File

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

View File

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

View File

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

View File

@ -18,4 +18,4 @@ export enum Status {
'删除失败',
'队列中',
'已取消',
}
}

View File

@ -26,7 +26,9 @@ const Diff = () => {
const getConfig = () => {
request
.get(`${config.apiPrefix}configs/detail?path=${encodeURIComponent(current)}`)
.get(
`${config.apiPrefix}configs/detail?path=${encodeURIComponent(current)}`,
)
.then(({ code, data }) => {
if (code === 200) {
setCurrentValue(data);
@ -36,7 +38,9 @@ const Diff = () => {
const getSample = () => {
request
.get(`${config.apiPrefix}configs/detail?path=${encodeURIComponent(origin)}`)
.get(
`${config.apiPrefix}configs/detail?path=${encodeURIComponent(origin)}`,
)
.then(({ code, data }) => {
if (code === 200) {
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 { Modal, message, Input, Form } from 'antd';
import { request } from '@/utils/http';
@ -55,7 +55,9 @@ const EditNameModal = ({
<Form form={form} layout="vertical" name="edit_name_modal">
<Form.Item
name="name"
rules={[{ required: true, message: intl.get('请输入新的环境变量名称') }]}
rules={[
{ required: true, message: intl.get('请输入新的环境变量名称') },
]}
>
<Input placeholder={intl.get('请输入新的环境变量名称')} />
</Form.Item>

View File

@ -64,4 +64,4 @@
.warningIcon {
font-size: 14px;
color: #faad14;
}
}

View File

@ -17,12 +17,10 @@ const UnsupportedFilePreview: React.FC<UnsupportedFilePreviewProps> = ({
<div className={styles.iconWrapper}>
<FileUnknownOutlined className={styles.icon} />
</div>
<div className={styles.message}>
{intl.get('当前文件不支持预览')}
</div>
<div className={styles.message}>{intl.get('当前文件不支持预览')}</div>
<Space direction="vertical" size={8} className={styles.actionArea}>
<Button
type="primary"
<Button
type="primary"
onClick={onForceOpen}
className={styles.button}
>
@ -38,4 +36,4 @@ const UnsupportedFilePreview: React.FC<UnsupportedFilePreviewProps> = ({
);
};
export default UnsupportedFilePreview;
export default UnsupportedFilePreview;

View File

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

View File

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

View File

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

View File

@ -1,11 +1,11 @@
import * as monaco from 'monaco-editor';
interface FileTypeConfig {
extensions?: string[]; // 文件扩展名
filenames?: string[]; // 完整文件名
patterns?: RegExp[]; // 文件名正则匹配
startsWith?: string[]; // 文件名前缀匹配
endsWith?: string[]; // 文件名后缀匹配
extensions?: string[]; // 文件扩展名
filenames?: string[]; // 完整文件名
patterns?: RegExp[]; // 文件名正则匹配
startsWith?: string[]; // 文件名前缀匹配
endsWith?: string[]; // 文件名后缀匹配
}
// 文件类型分类配置(只包含特殊文件类型)
@ -13,13 +13,13 @@ const fileTypeConfigs: Record<string, FileTypeConfig> = {
// 前端特殊文件
frontend: {
extensions: [
'.json5', // JSON5
'.vue', // Vue
'.svelte', // Svelte
'.astro', // Astro
'.wxss', // 微信小程序样式
'.pcss', // PostCSS
'.acss', // 支付宝小程序样式
'.json5', // JSON5
'.vue', // Vue
'.svelte', // Svelte
'.astro', // Astro
'.wxss', // 微信小程序样式
'.pcss', // PostCSS
'.acss', // 支付宝小程序样式
],
patterns: [
/\.env\.(local|development|production|test)$/,
@ -32,28 +32,28 @@ const fileTypeConfigs: Record<string, FileTypeConfig> = {
// 小程序相关
miniprogram: {
extensions: [
'.wxml', // 微信小程序
'.wxs', // 微信小程序
'.axml', // 支付宝小程序
'.sjs', // 支付宝小程序
'.swan', // 百度小程序
'.ttml', // 字节跳动小程序
'.ttss', // 字节跳动小程序
'.wxl', // 微信小程序语言包
'.qml', // QQ小程序
'.qss', // QQ小程序
'.ksml', // 快手小程序
'.kss', // 快手小程序
'.wxml', // 微信小程序
'.wxs', // 微信小程序
'.axml', // 支付宝小程序
'.sjs', // 支付宝小程序
'.swan', // 百度小程序
'.ttml', // 字节跳动小程序
'.ttss', // 字节跳动小程序
'.wxl', // 微信小程序语言包
'.qml', // QQ小程序
'.qss', // QQ小程序
'.ksml', // 快手小程序
'.kss', // 快手小程序
],
},
// 开发工具相关
devtools: {
extensions: [
'.prisma', // Prisma
'.mdx', // MDX
'.swagger', // Swagger
'.openapi', // OpenAPI
'.prisma', // Prisma
'.mdx', // MDX
'.swagger', // Swagger
'.openapi', // OpenAPI
],
},
@ -84,9 +84,7 @@ const fileTypeConfigs: Record<string, FileTypeConfig> = {
'.gcloudignore',
'.htaccess',
],
patterns: [
/^\.env\./,
],
patterns: [/^\.env\./],
},
// CI/CD 配置
@ -106,28 +104,33 @@ const fileTypeConfigs: Record<string, FileTypeConfig> = {
*/
export function canPreviewInMonaco(fileName: string): boolean {
if (!fileName) return false;
// 获取 Monaco 支持的语言
const supportedLanguages = monaco.languages.getLanguages();
const ext = fileName.slice(fileName.lastIndexOf('.')).toLowerCase();
const lowercaseFileName = fileName.toLowerCase();
// 检查 Monaco 原生支持
if (supportedLanguages.some((lang) =>
lang.extensions?.includes(ext) ||
(lang.filenames?.includes(lowercaseFileName))
)) {
if (
supportedLanguages.some(
(lang) =>
lang.extensions?.includes(ext) ||
lang.filenames?.includes(lowercaseFileName),
)
) {
return true;
}
// 检查额外支持的文件类型
return Object.values(fileTypeConfigs).some(config => {
return Object.values(fileTypeConfigs).some((config) => {
return (
(config.extensions?.includes(ext)) ||
(config.filenames?.includes(lowercaseFileName)) ||
(config.patterns?.some(pattern => pattern.test(lowercaseFileName))) ||
(config.startsWith?.some(prefix => lowercaseFileName.startsWith(prefix))) ||
(config.endsWith?.some(suffix => lowercaseFileName.endsWith(suffix)))
config.extensions?.includes(ext) ||
config.filenames?.includes(lowercaseFileName) ||
config.patterns?.some((pattern) => pattern.test(lowercaseFileName)) ||
config.startsWith?.some((prefix) =>
lowercaseFileName.startsWith(prefix),
) ||
config.endsWith?.some((suffix) => lowercaseFileName.endsWith(suffix))
);
});
}
@ -139,17 +142,19 @@ export function canPreviewInMonaco(fileName: string): boolean {
*/
export function getFileCategory(fileName: string): string {
if (!fileName) return 'unknown';
const lowercaseFileName = fileName.toLowerCase();
const ext = fileName.slice(fileName.lastIndexOf('.')).toLowerCase();
for (const [category, config] of Object.entries(fileTypeConfigs)) {
if (
(config.extensions?.includes(ext)) ||
(config.filenames?.includes(lowercaseFileName)) ||
(config.patterns?.some(pattern => pattern.test(lowercaseFileName))) ||
(config.startsWith?.some(prefix => lowercaseFileName.startsWith(prefix))) ||
(config.endsWith?.some(suffix => lowercaseFileName.endsWith(suffix)))
config.extensions?.includes(ext) ||
config.filenames?.includes(lowercaseFileName) ||
config.patterns?.some((pattern) => pattern.test(lowercaseFileName)) ||
config.startsWith?.some((prefix) =>
lowercaseFileName.startsWith(prefix),
) ||
config.endsWith?.some((suffix) => lowercaseFileName.endsWith(suffix))
) {
return category;
}
@ -157,10 +162,13 @@ export function getFileCategory(fileName: string): string {
// 检查 Monaco 原生支持
const supportedLanguages = monaco.languages.getLanguages();
if (supportedLanguages.some((lang) =>
lang.extensions?.includes(ext) ||
(lang.filenames?.includes(lowercaseFileName))
)) {
if (
supportedLanguages.some(
(lang) =>
lang.extensions?.includes(ext) ||
lang.filenames?.includes(lowercaseFileName),
)
) {
return 'monaco-native';
}

View File

@ -5,7 +5,8 @@ class WebSocketManager {
private static instance: WebSocketManager | null = null;
private url: string;
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: {
maxReconnectAttempts: number;
reconnectInterval: number;
@ -15,7 +16,10 @@ class WebSocketManager {
private heartbeatTimeout: NodeJS.Timeout | null = null;
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.options = {
maxReconnectAttempts: options.maxReconnectAttempts || 5,
@ -26,7 +30,10 @@ class WebSocketManager {
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) {
WebSocketManager.instance = new WebSocketManager(url, options);
}
@ -47,7 +54,9 @@ class WebSocketManager {
this.socket = null;
this.reconnectAttempts++;
await new Promise((resolve) => setTimeout(resolve, this.options.reconnectInterval));
await new Promise((resolve) =>
setTimeout(resolve, this.options.reconnectInterval),
);
}
} catch (error) {
this.handleError(error);