mirror of
https://github.com/crystelf/crystelf-core.git
synced 2025-10-14 21:39:19 +00:00
Compare commits
No commits in common. "ccda3ec27195ffc38003b7c903ee38d09b557ced" and "08f74445da17994596b32ff415f0d9eba9d82849" have entirely different histories.
ccda3ec271
...
08f74445da
@ -20,17 +20,13 @@
|
|||||||
"@nestjs/config": "^4.0.2",
|
"@nestjs/config": "^4.0.2",
|
||||||
"@nestjs/core": "^11.0.1",
|
"@nestjs/core": "^11.0.1",
|
||||||
"@nestjs/platform-express": "^11.0.1",
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
"@nestjs/platform-socket.io": "^11.1.6",
|
|
||||||
"@nestjs/platform-ws": "^11.1.6",
|
|
||||||
"@nestjs/swagger": "^11.2.0",
|
"@nestjs/swagger": "^11.2.0",
|
||||||
"@nestjs/websockets": "^11.1.6",
|
"axios": "^1.10.0",
|
||||||
"axios": "1.11.0",
|
|
||||||
"ioredis": "^5.6.1",
|
"ioredis": "^5.6.1",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"simple-git": "^3.28.0",
|
"simple-git": "^3.28.0",
|
||||||
"ssh2": "^1.16.0",
|
"ssh2": "^1.16.0",
|
||||||
"uuid": "^11.1.0",
|
|
||||||
"ws": "^8.18.3"
|
"ws": "^8.18.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
8305
pnpm-lock.yaml
generated
8305
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -7,7 +7,6 @@ import { ToolsModule } from './core/tools/tools.module';
|
|||||||
import { AutoUpdateModule } from './core/auto-update/auto-update.module';
|
import { AutoUpdateModule } from './core/auto-update/auto-update.module';
|
||||||
import { PersistenceModule } from './core/persistence/persistence.module';
|
import { PersistenceModule } from './core/persistence/persistence.module';
|
||||||
import { RedisModule } from './core/redis/redis.module';
|
import { RedisModule } from './core/redis/redis.module';
|
||||||
import { WsModule } from './core/ws/ws.module';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -19,7 +18,6 @@ import { WsModule } from './core/ws/ws.module';
|
|||||||
PersistenceModule,
|
PersistenceModule,
|
||||||
AutoUpdateModule,
|
AutoUpdateModule,
|
||||||
RedisModule,
|
RedisModule,
|
||||||
WsModule,
|
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
@ -22,7 +22,7 @@ export class ResponseInterceptor<T>
|
|||||||
map((data) => ({
|
map((data) => ({
|
||||||
success: true,
|
success: true,
|
||||||
data,
|
data,
|
||||||
message: '欢迎使用晶灵核心 | Welcome to use crystelf-core',
|
message: '操作成功',
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ export class AppConfigService implements OnModuleInit {
|
|||||||
* @param key 键值
|
* @param key 键值
|
||||||
* @param defaultValue 默认
|
* @param defaultValue 默认
|
||||||
*/
|
*/
|
||||||
public get<T = string>(key: string, defaultValue?: T): T | undefined {
|
get<T = string>(key: string, defaultValue?: T): T | undefined {
|
||||||
const value = this.nestConfigService.get<T>(key);
|
const value = this.nestConfigService.get<T>(key);
|
||||||
if (value === undefined || value === null) {
|
if (value === undefined || value === null) {
|
||||||
if (defaultValue !== undefined) {
|
if (defaultValue !== undefined) {
|
||||||
|
@ -19,11 +19,6 @@ export class PersistenceService {
|
|||||||
private readonly fileService: FilesService,
|
private readonly fileService: FilesService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
|
||||||
* 确保有某个目录
|
|
||||||
* @param dataName 目录名/资源名
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private async ensureDataPath(dataName: string): Promise<void> {
|
private async ensureDataPath(dataName: string): Promise<void> {
|
||||||
const dataPath = path.join(this.paths.get('userData'), dataName);
|
const dataPath = path.join(this.paths.get('userData'), dataName);
|
||||||
try {
|
try {
|
||||||
|
@ -2,6 +2,7 @@ import { Inject, Injectable, Logger } from '@nestjs/common';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { PathService } from '../path/path.service';
|
import { PathService } from '../path/path.service';
|
||||||
|
import { AutoUpdateModule } from '../auto-update/auto-update.module';
|
||||||
import { AutoUpdateService } from '../auto-update/auto-update.service';
|
import { AutoUpdateService } from '../auto-update/auto-update.service';
|
||||||
import * as process from 'node:process';
|
import * as process from 'node:process';
|
||||||
|
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ToolsService } from './tools.service';
|
import { ToolsService } from './tools.service';
|
||||||
import { AppConfigModule } from '../../config/config.module';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [AppConfigModule],
|
|
||||||
providers: [ToolsService],
|
providers: [ToolsService],
|
||||||
exports: [ToolsService],
|
exports: [ToolsService],
|
||||||
})
|
})
|
||||||
|
@ -1,19 +1,10 @@
|
|||||||
import {
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
Inject,
|
|
||||||
Injectable,
|
|
||||||
Logger,
|
|
||||||
UnauthorizedException,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { RetryOptions } from './retry-options.interface';
|
import { RetryOptions } from './retry-options.interface';
|
||||||
import { AppConfigService } from '../../config/config.service';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ToolsService {
|
export class ToolsService {
|
||||||
private readonly logger = new Logger(ToolsService.name);
|
private readonly logger = new Logger(ToolsService.name);
|
||||||
constructor(
|
|
||||||
@Inject(AppConfigService)
|
|
||||||
private readonly config: AppConfigService,
|
|
||||||
) {}
|
|
||||||
/**
|
/**
|
||||||
* 异步重试
|
* 异步重试
|
||||||
* @param operation
|
* @param operation
|
||||||
@ -57,26 +48,4 @@ export class ToolsService {
|
|||||||
getRandomDelay(min: number, max: number): number {
|
getRandomDelay(min: number, max: number): number {
|
||||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查 token 是否有效
|
|
||||||
* @param token 待验证的 token
|
|
||||||
*/
|
|
||||||
checkToken(token: string): boolean {
|
|
||||||
const expected = this.config.get<string>('TOKEN');
|
|
||||||
if (!expected) {
|
|
||||||
this.logger.error('环境变量 TOKEN 未配置,无法进行验证!');
|
|
||||||
throw new UnauthorizedException('系统配置错误,缺少 TOKEN');
|
|
||||||
}
|
|
||||||
return token === expected;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* token 验证失败时的逻辑
|
|
||||||
* @param token 无效的 token
|
|
||||||
*/
|
|
||||||
tokenCheckFailed(token: string): never {
|
|
||||||
this.logger.warn(`有个小可爱使用了错误的 token: ${JSON.stringify(token)}`);
|
|
||||||
throw new UnauthorizedException('token 验证失败..');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { WsTools } from '../ws.tools';
|
|
||||||
import { IMessageHandler } from 'src/types/ws/ws.handlers.interface';
|
|
||||||
import { AuthenticatedSocket } from '../../../types/ws/ws.interface';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class PingHandler implements IMessageHandler {
|
|
||||||
type = 'ping';
|
|
||||||
async handle(socket: AuthenticatedSocket, msg: any) {
|
|
||||||
await WsTools.send(socket, { type: 'pong' });
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { AuthenticatedSocket } from '../../../types/ws/ws.interface';
|
|
||||||
import { IMessageHandler } from '../../../types/ws/ws.handlers.interface';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class PongHandler implements IMessageHandler {
|
|
||||||
type = 'pong';
|
|
||||||
|
|
||||||
async handle(socket: AuthenticatedSocket, msg: any) {
|
|
||||||
//this.logger.debug(`收到 pong 消息: ${JSON.stringify(msg)}`);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
|
||||||
import { Logger } from '@nestjs/common';
|
|
||||||
import { IMessageHandler } from '../../../types/ws/ws.handlers.interface';
|
|
||||||
import { RedisService } from '../../redis/redis.service';
|
|
||||||
import { AuthenticatedSocket } from '../../../types/ws/ws.interface';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ReportBotsHandler implements IMessageHandler {
|
|
||||||
type = 'reportBots';
|
|
||||||
private readonly logger = new Logger(ReportBotsHandler.name);
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
@Inject(RedisService)
|
|
||||||
private readonly redisService: RedisService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async handle(socket: AuthenticatedSocket, msg: any) {
|
|
||||||
this.logger.debug(`received reportBots: ${msg.data}`);
|
|
||||||
const clientId = msg.data[0].client;
|
|
||||||
const botsData = msg.data.slice(1);
|
|
||||||
await this.redisService.persistData('crystelfBots', botsData, clientId);
|
|
||||||
this.logger.debug(
|
|
||||||
`保存了 ${botsData.length} 个 bot(client: ${clientId})`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { WsTools } from '../ws.tools';
|
|
||||||
import { IMessageHandler } from '../../../types/ws/ws.handlers.interface';
|
|
||||||
import { AuthenticatedSocket } from '../../../types/ws/ws.interface';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class TestHandler implements IMessageHandler {
|
|
||||||
type = 'test';
|
|
||||||
|
|
||||||
async handle(socket: AuthenticatedSocket, msg: any) {
|
|
||||||
await WsTools.send(socket, {
|
|
||||||
type: 'test',
|
|
||||||
data: { status: 'ok' },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
|
||||||
import { WsTools } from '../ws.tools';
|
|
||||||
import { IMessageHandler } from '../../../types/ws/ws.handlers.interface';
|
|
||||||
import { AuthenticatedSocket } from '../../../types/ws/ws.interface';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class UnknownHandler implements IMessageHandler {
|
|
||||||
type = 'unknown';
|
|
||||||
private readonly logger = new Logger(UnknownHandler.name);
|
|
||||||
|
|
||||||
async handle(socket: AuthenticatedSocket, msg: any) {
|
|
||||||
this.logger.warn(`收到未知消息类型: ${msg.type}`);
|
|
||||||
await WsTools.send(socket, {
|
|
||||||
type: 'error',
|
|
||||||
message: `未知消息类型: ${msg.type}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,128 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import WebSocket from 'ws';
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
|
|
||||||
type ClientID = string;
|
|
||||||
const pendingRequests = new Map<string, (data: any) => void>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 客户端管理
|
|
||||||
*/
|
|
||||||
@Injectable()
|
|
||||||
export class WsClientManager {
|
|
||||||
private clients = new Map<ClientID, WebSocket>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 增添新的客户端
|
|
||||||
* @param id 编号
|
|
||||||
* @param socket 客户端
|
|
||||||
*/
|
|
||||||
add(id: ClientID, socket: WebSocket) {
|
|
||||||
this.clients.set(id, socket);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 移除客户端
|
|
||||||
* @param id 编号
|
|
||||||
*/
|
|
||||||
remove(id: ClientID) {
|
|
||||||
this.clients.delete(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取客户端单例
|
|
||||||
* @param id 编号
|
|
||||||
*/
|
|
||||||
get(id: ClientID): WebSocket | undefined {
|
|
||||||
return this.clients.get(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送消息到客户端
|
|
||||||
* @param id 编号
|
|
||||||
* @param data 要发送的信息
|
|
||||||
*/
|
|
||||||
async send(id: ClientID, data: any): Promise<boolean> {
|
|
||||||
const socket = this.clients.get(id);
|
|
||||||
if (!socket || socket.readyState !== WebSocket.OPEN) return false;
|
|
||||||
return this.safeSend(socket, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送消息并等待返回
|
|
||||||
* @param id 编号
|
|
||||||
* @param data 消息
|
|
||||||
* @param timeout
|
|
||||||
*/
|
|
||||||
async sendAndWait(id: ClientID, data: any, timeout = 5000): Promise<any> {
|
|
||||||
const socket = this.clients.get(id);
|
|
||||||
if (!socket) return;
|
|
||||||
|
|
||||||
data.requestId = uuidv4();
|
|
||||||
const requestId = data.requestId;
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
pendingRequests.delete(requestId);
|
|
||||||
reject(new Error(`${requestId}: 请求超时`));
|
|
||||||
}, timeout);
|
|
||||||
|
|
||||||
pendingRequests.set(requestId, (response) => {
|
|
||||||
clearTimeout(timer);
|
|
||||||
pendingRequests.delete(requestId);
|
|
||||||
resolve(response);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.safeSend(socket, data).catch((err) => {
|
|
||||||
clearTimeout(timer);
|
|
||||||
pendingRequests.delete(requestId);
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理回调
|
|
||||||
* @param requestId 请求id
|
|
||||||
* @param data 内容
|
|
||||||
*/
|
|
||||||
resolvePendingRequest(requestId: string, data: any): boolean {
|
|
||||||
const callback = pendingRequests.get(requestId);
|
|
||||||
if (callback) {
|
|
||||||
pendingRequests.delete(requestId);
|
|
||||||
callback(data);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 广播消息
|
|
||||||
* @param data 内容
|
|
||||||
*/
|
|
||||||
async broadcast(data: any): Promise<void> {
|
|
||||||
const tasks = Array.from(this.clients.values()).map((socket) => {
|
|
||||||
if (socket.readyState === WebSocket.OPEN) {
|
|
||||||
return this.safeSend(socket, data);
|
|
||||||
} else {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
await Promise.all(tasks);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 安全发送
|
|
||||||
* @param socket
|
|
||||||
* @param data
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private async safeSend(socket: WebSocket, data: any): Promise<boolean> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
socket.send(JSON.stringify(data), (err) => {
|
|
||||||
if (err) reject(new Error('发送失败'));
|
|
||||||
else resolve(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,56 +0,0 @@
|
|||||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
|
||||||
import { WsTools } from './ws.tools';
|
|
||||||
import { WsClientManager } from './ws-client.manager';
|
|
||||||
import { IMessageHandler } from '../../types/ws/ws.handlers.interface';
|
|
||||||
import { AuthenticatedSocket } from '../../types/ws/ws.interface';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class WsMessageHandler {
|
|
||||||
private readonly logger = new Logger(WsMessageHandler.name);
|
|
||||||
private handlers = new Map<string, IMessageHandler>();
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly wsClientManager: WsClientManager,
|
|
||||||
@Inject('WS_HANDLERS') handlers: IMessageHandler[],
|
|
||||||
) {
|
|
||||||
handlers.forEach((h) => this.handlers.set(h.type, h));
|
|
||||||
this.logger.log(`已注册 ${handlers.length} 个 WS handler`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async handle(socket: AuthenticatedSocket, clientId: string, msg: any) {
|
|
||||||
try {
|
|
||||||
// 如果是 pendingRequests 的回包
|
|
||||||
if (
|
|
||||||
msg.requestId &&
|
|
||||||
this.wsClientManager.resolvePendingRequest(msg.requestId, msg)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const handler =
|
|
||||||
this.handlers.get(msg.type) || this.handlers.get('unknown');
|
|
||||||
if (handler) {
|
|
||||||
await handler.handle(socket, msg);
|
|
||||||
} else {
|
|
||||||
await this.handleUnknown(socket, msg);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
this.logger.error(`ws消息处理时出错: ${err}`);
|
|
||||||
await WsTools.send(socket, {
|
|
||||||
type: 'error',
|
|
||||||
message: 'error message',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async handleUnknown(socket: AuthenticatedSocket, msg: any) {
|
|
||||||
this.logger.warn(`收到未知消息类型: ${msg.type}`);
|
|
||||||
await WsTools.send(socket, {
|
|
||||||
type: 'error',
|
|
||||||
message: `未知消息类型: ${msg.type}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public registerHandler(handler: IMessageHandler): void {
|
|
||||||
this.handlers.set(handler.type, handler);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,153 +0,0 @@
|
|||||||
import {
|
|
||||||
OnGatewayConnection,
|
|
||||||
OnGatewayDisconnect,
|
|
||||||
WebSocketGateway,
|
|
||||||
WebSocketServer,
|
|
||||||
} from '@nestjs/websockets';
|
|
||||||
import { Inject, Logger } from '@nestjs/common';
|
|
||||||
import { Server, WebSocket } from 'ws';
|
|
||||||
import { WsTools } from './ws.tools';
|
|
||||||
import { WsClientManager } from './ws-client.manager';
|
|
||||||
import {
|
|
||||||
AuthenticatedSocket,
|
|
||||||
AuthMessage,
|
|
||||||
WSMessage,
|
|
||||||
} from '../../types/ws/ws.interface';
|
|
||||||
import { AppConfigService } from '../../config/config.service';
|
|
||||||
import { WsMessageHandler } from './ws-message.handler';
|
|
||||||
|
|
||||||
@WebSocketGateway(7001, {
|
|
||||||
cors: { origin: '*' },
|
|
||||||
driver: 'ws',
|
|
||||||
})
|
|
||||||
export class WsGateway implements OnGatewayConnection, OnGatewayDisconnect {
|
|
||||||
private readonly logger = new Logger(WsGateway.name);
|
|
||||||
private readonly secret: string | undefined;
|
|
||||||
|
|
||||||
@WebSocketServer()
|
|
||||||
server: Server;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
@Inject(AppConfigService)
|
|
||||||
private readonly configService: AppConfigService,
|
|
||||||
@Inject(WsClientManager)
|
|
||||||
private readonly wsClientManager: WsClientManager,
|
|
||||||
@Inject(WsMessageHandler)
|
|
||||||
private readonly wsMessageHandler: WsMessageHandler,
|
|
||||||
) {
|
|
||||||
this.secret = this.configService.get<string>('WS_SECRET');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 新的连接请求
|
|
||||||
* @param client 客户端
|
|
||||||
* @param req
|
|
||||||
*/
|
|
||||||
async handleConnection(client: AuthenticatedSocket, req: any) {
|
|
||||||
const ip = req.socket.remoteAddress || 'unknown';
|
|
||||||
this.logger.log(`收到来自 ${ip} 的 WebSocket 连接请求..`);
|
|
||||||
|
|
||||||
client.heartbeat = WsTools.setUpHeartbeat(client);
|
|
||||||
|
|
||||||
client.on('message', async (raw) => {
|
|
||||||
this.logger.debug(`Received raw message from ${ip}: ${raw.toString()}`);
|
|
||||||
|
|
||||||
const msg = WsTools.parseMessage<WSMessage>(raw);
|
|
||||||
if (!msg) return this.handleInvalidMessage(client, ip);
|
|
||||||
|
|
||||||
await this.routeMessage(client, msg, ip);
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on('error', (err) => {
|
|
||||||
this.logger.error(`WS error from ${ip}: ${err.message}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 断开某个连接
|
|
||||||
* @param client 客户端
|
|
||||||
*/
|
|
||||||
async handleDisconnect(client: AuthenticatedSocket) {
|
|
||||||
if (client.heartbeat) clearInterval(client.heartbeat);
|
|
||||||
if (client.clientId) {
|
|
||||||
this.wsClientManager.remove(client.clientId);
|
|
||||||
this.logger.log(`Removed client ${client.clientId} from manager`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 不合法消息
|
|
||||||
* @param client 客户端
|
|
||||||
* @param ip
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private async handleInvalidMessage(client: WebSocket, ip: string) {
|
|
||||||
this.logger.warn(`Invalid message received from ${ip}`);
|
|
||||||
await WsTools.send(client, {
|
|
||||||
type: 'error',
|
|
||||||
message: 'Invalid message format',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 消息路由
|
|
||||||
* @param client 客户端
|
|
||||||
* @param msg 消息
|
|
||||||
* @param ip
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private async routeMessage(
|
|
||||||
client: AuthenticatedSocket,
|
|
||||||
msg: WSMessage,
|
|
||||||
ip: string,
|
|
||||||
) {
|
|
||||||
if (!client.isAuthed) {
|
|
||||||
if (this.isAuthMessage(msg)) {
|
|
||||||
this.logger.log(`Attempting auth from ${ip} as ${msg.clientId}`);
|
|
||||||
await this.handleAuth(client, msg, ip);
|
|
||||||
} else {
|
|
||||||
this.logger.warn(
|
|
||||||
`Received message before auth from ${ip}: ${JSON.stringify(msg)}`,
|
|
||||||
);
|
|
||||||
await this.handleInvalidMessage(client, ip);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.debug(
|
|
||||||
`Routing message from ${client.clientId}: ${JSON.stringify(msg)}`,
|
|
||||||
);
|
|
||||||
await this.wsMessageHandler.handle(client, client.clientId!, msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
private isAuthMessage(msg: WSMessage): msg is AuthMessage {
|
|
||||||
return msg.type === 'auth';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 连接验证
|
|
||||||
* @param client 客户端
|
|
||||||
* @param msg 消息
|
|
||||||
* @param ip
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
private async handleAuth(
|
|
||||||
client: AuthenticatedSocket,
|
|
||||||
msg: AuthMessage,
|
|
||||||
ip: string,
|
|
||||||
) {
|
|
||||||
if (msg.secret === this.secret) {
|
|
||||||
client.isAuthed = true;
|
|
||||||
client.clientId = msg.clientId;
|
|
||||||
this.wsClientManager.add(msg.clientId, client);
|
|
||||||
this.logger.log(`Auth success from ${ip}, clientId: ${msg.clientId}`);
|
|
||||||
await WsTools.send(client, { type: 'auth', success: true });
|
|
||||||
} else {
|
|
||||||
this.logger.warn(
|
|
||||||
`Auth failed from ${ip} (invalid secret), clientId: ${msg.clientId}`,
|
|
||||||
);
|
|
||||||
await WsTools.send(client, { type: 'auth', success: false });
|
|
||||||
client.close(4001, 'Authentication failed');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { WsGateway } from './ws.gateway';
|
|
||||||
import { WsClientManager } from './ws-client.manager';
|
|
||||||
import { AppConfigModule } from '../../config/config.module';
|
|
||||||
import { WsMessageHandler } from './ws-message.handler';
|
|
||||||
import { TestHandler } from './handlers/test.handler';
|
|
||||||
import { PingHandler } from './handlers/ping.handler';
|
|
||||||
import { PongHandler } from './handlers/pong.handler';
|
|
||||||
import { ReportBotsHandler } from './handlers/report-bots.handler';
|
|
||||||
import { UnknownHandler } from './handlers/unknown.handler';
|
|
||||||
import { RedisModule } from '../redis/redis.module';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [AppConfigModule, RedisModule],
|
|
||||||
providers: [
|
|
||||||
WsGateway,
|
|
||||||
WsClientManager,
|
|
||||||
WsMessageHandler,
|
|
||||||
TestHandler,
|
|
||||||
PingHandler,
|
|
||||||
PongHandler,
|
|
||||||
ReportBotsHandler,
|
|
||||||
UnknownHandler,
|
|
||||||
{
|
|
||||||
provide: 'WS_HANDLERS',
|
|
||||||
useFactory: (
|
|
||||||
test: TestHandler,
|
|
||||||
ping: PingHandler,
|
|
||||||
pong: PongHandler,
|
|
||||||
reportBots: ReportBotsHandler,
|
|
||||||
unknown: UnknownHandler,
|
|
||||||
) => [test, ping, pong, reportBots, unknown],
|
|
||||||
inject: [
|
|
||||||
TestHandler,
|
|
||||||
PingHandler,
|
|
||||||
PongHandler,
|
|
||||||
ReportBotsHandler,
|
|
||||||
UnknownHandler,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
exports: [WsClientManager, WsMessageHandler, WsGateway],
|
|
||||||
})
|
|
||||||
export class WsModule {}
|
|
@ -1,47 +0,0 @@
|
|||||||
import type { WebSocket, RawData } from 'ws';
|
|
||||||
import { Logger } from '@nestjs/common';
|
|
||||||
|
|
||||||
export class WsTools {
|
|
||||||
private static readonly logger = new Logger(WsTools.name);
|
|
||||||
|
|
||||||
static async send(socket: WebSocket, data: unknown): Promise<boolean> {
|
|
||||||
if (socket.readyState !== 1) {
|
|
||||||
this.logger.warn('尝试向非 OPEN 状态的 socket 发送消息,已丢弃');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
try {
|
|
||||||
socket.send(JSON.stringify(data), (err) => {
|
|
||||||
if (err) {
|
|
||||||
this.logger.error(`WS send error: ${err.message}`);
|
|
||||||
resolve(false);
|
|
||||||
} else {
|
|
||||||
resolve(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (err: any) {
|
|
||||||
this.logger.error(`WS send exception: ${err.message}`);
|
|
||||||
resolve(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static parseMessage<T>(data: RawData): T | null {
|
|
||||||
try {
|
|
||||||
return JSON.parse(data.toString()) as T;
|
|
||||||
} catch (err) {
|
|
||||||
this.logger.error(`WS parse error: ${err}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static setUpHeartbeat(socket: WebSocket, interval = 30000): NodeJS.Timeout {
|
|
||||||
const heartbeat = async () => {
|
|
||||||
if (socket.readyState === 1) {
|
|
||||||
await WsTools.send(socket, { type: 'ping' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return setInterval(heartbeat, interval);
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,7 +5,6 @@ import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
|||||||
import { ResponseInterceptor } from './common/interceptors/response.interceptor';
|
import { ResponseInterceptor } from './common/interceptors/response.interceptor';
|
||||||
import { AllExceptionsFilter } from './common/filters/all-exception.filter';
|
import { AllExceptionsFilter } from './common/filters/all-exception.filter';
|
||||||
import { SystemService } from './core/system/system.service';
|
import { SystemService } from './core/system/system.service';
|
||||||
import { WsAdapter } from '@nestjs/platform-ws';
|
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
Logger.log('晶灵核心初始化..');
|
Logger.log('晶灵核心初始化..');
|
||||||
@ -25,7 +24,6 @@ async function bootstrap() {
|
|||||||
.build();
|
.build();
|
||||||
const document = () => SwaggerModule.createDocument(app, config);
|
const document = () => SwaggerModule.createDocument(app, config);
|
||||||
SwaggerModule.setup('', app, document);
|
SwaggerModule.setup('', app, document);
|
||||||
app.useWebSocketAdapter(new WsAdapter(app));
|
|
||||||
await app.listen(7000);
|
await app.listen(7000);
|
||||||
await systemService.checkUpdate().catch((err) => {
|
await systemService.checkUpdate().catch((err) => {
|
||||||
Logger.error(`自动更新失败: ${err?.message}`, '', 'System');
|
Logger.error(`自动更新失败: ${err?.message}`, '', 'System');
|
||||||
|
@ -1,58 +0,0 @@
|
|||||||
import {
|
|
||||||
Controller,
|
|
||||||
Post,
|
|
||||||
Body,
|
|
||||||
UnauthorizedException,
|
|
||||||
Inject,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { ApiTags, ApiOperation, ApiBody } from '@nestjs/swagger';
|
|
||||||
import { SystemWebService } from './system.service';
|
|
||||||
import { ToolsService } from '../../core/tools/tools.service';
|
|
||||||
|
|
||||||
class TokenDto {
|
|
||||||
token: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@ApiTags('System')
|
|
||||||
@Controller('system')
|
|
||||||
export class SystemController {
|
|
||||||
constructor(
|
|
||||||
@Inject(SystemWebService)
|
|
||||||
private readonly systemService: SystemWebService,
|
|
||||||
@Inject(ToolsService)
|
|
||||||
private readonly toolService: ToolsService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重启系统
|
|
||||||
*/
|
|
||||||
@Post('restart')
|
|
||||||
@ApiOperation({
|
|
||||||
summary: '系统重启',
|
|
||||||
description: '传入正确的 token 后,核心将执行重启。',
|
|
||||||
})
|
|
||||||
@ApiBody({ type: TokenDto })
|
|
||||||
async systemRestart(@Body() body: TokenDto): Promise<string> {
|
|
||||||
if (!this.toolService.checkToken(body.token)) {
|
|
||||||
throw new UnauthorizedException('Token 无效');
|
|
||||||
}
|
|
||||||
await this.systemService.systemRestart();
|
|
||||||
return '核心正在重启..';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取系统重启耗时
|
|
||||||
*/
|
|
||||||
@Post('getRestartTime')
|
|
||||||
@ApiOperation({
|
|
||||||
summary: '获取重启所需时间',
|
|
||||||
description: '传入正确的 token,返回上次核心重启的耗时',
|
|
||||||
})
|
|
||||||
@ApiBody({ type: TokenDto })
|
|
||||||
async getRestartTime(@Body() body: TokenDto): Promise<string> {
|
|
||||||
if (!this.toolService.checkToken(body.token)) {
|
|
||||||
throw new UnauthorizedException('Token 无效');
|
|
||||||
}
|
|
||||||
return await this.systemService.getRestartTime();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { SystemController } from './system.controller';
|
|
||||||
import { SystemWebService } from './system.service';
|
|
||||||
import { ToolsModule } from '../../core/tools/tools.module';
|
|
||||||
import { PathModule } from '../../core/path/path.module';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [ToolsModule, SystemModule, PathModule],
|
|
||||||
controllers: [SystemController],
|
|
||||||
providers: [SystemWebService],
|
|
||||||
})
|
|
||||||
export class SystemModule {}
|
|
@ -1,34 +0,0 @@
|
|||||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
|
||||||
import fs from 'fs/promises';
|
|
||||||
import path from 'path';
|
|
||||||
import { PathService } from '../../core/path/path.service';
|
|
||||||
import { SystemService } from 'src/core/system/system.service';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class SystemWebService {
|
|
||||||
private readonly logger = new Logger(SystemWebService.name);
|
|
||||||
@Inject(SystemService)
|
|
||||||
private readonly system: SystemService;
|
|
||||||
@Inject(PathService)
|
|
||||||
private readonly pathService: PathService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重启系统
|
|
||||||
*/
|
|
||||||
async systemRestart(): Promise<void> {
|
|
||||||
this.logger.debug(`有个小可爱正在请求重启核心..`);
|
|
||||||
await this.system.restart();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取上次重启所耗时间
|
|
||||||
*/
|
|
||||||
async getRestartTime(): Promise<string> {
|
|
||||||
this.logger.debug(`有个小可爱想知道核心重启花了多久..`);
|
|
||||||
const restartTimePath = path.join(
|
|
||||||
this.pathService.get('temp'),
|
|
||||||
'restart_time',
|
|
||||||
);
|
|
||||||
return await fs.readFile(restartTimePath, 'utf8');
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
import { AuthenticatedSocket } from './ws.interface';
|
|
||||||
|
|
||||||
export interface IMessageHandler {
|
|
||||||
type: string; //消息类型
|
|
||||||
handle(socket: AuthenticatedSocket, msg: any): Promise<void>;
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user