From 6809d07bcfc2f68178ba3d20d88504b310e0291a Mon Sep 17 00:00:00 2001 From: Jerryplusy Date: Mon, 25 Aug 2025 21:45:03 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B2=E7=BA=BF=E6=95=91=E5=9B=BD=EF=BC=88?= =?UTF-8?q?=E4=B8=8D=E6=98=AF=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.module.ts | 4 + src/core/path/path.service.ts | 13 +- src/core/tools/token-auth.guard.ts | 37 +++ src/main.ts | 8 +- src/modules/bot/bot.controller.ts | 70 ++++++ src/modules/bot/bot.dto.ts | 24 ++ src/modules/bot/bot.module.ts | 14 ++ src/modules/bot/bot.service.ts | 255 +++++++++++++++++++++ src/modules/cdn/cdn.controller.ts | 79 +++++++ src/modules/cdn/cdn.module.ts | 11 + src/modules/cdn/cdn.service.ts | 54 +++++ src/modules/system/systemWeb.controller.ts | 23 +- src/modules/system/systemWeb.service.ts | 2 +- 13 files changed, 566 insertions(+), 28 deletions(-) create mode 100644 src/core/tools/token-auth.guard.ts create mode 100644 src/modules/bot/bot.controller.ts create mode 100644 src/modules/bot/bot.dto.ts create mode 100644 src/modules/bot/bot.module.ts create mode 100644 src/modules/bot/bot.service.ts create mode 100644 src/modules/cdn/cdn.controller.ts create mode 100644 src/modules/cdn/cdn.module.ts create mode 100644 src/modules/cdn/cdn.service.ts diff --git a/src/app.module.ts b/src/app.module.ts index fc8b54f..68822e0 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -9,6 +9,8 @@ import { PersistenceModule } from './core/persistence/persistence.module'; import { RedisModule } from './core/redis/redis.module'; import { WsModule } from './core/ws/ws.module'; import { SystemWebModule } from './modules/system/systemWeb.module'; +import { BotModule } from './modules/bot/bot.module'; +import { CdnModule } from './modules/cdn/cdn.module'; @Module({ imports: [ @@ -22,6 +24,8 @@ import { SystemWebModule } from './modules/system/systemWeb.module'; RedisModule, WsModule, SystemWebModule, + BotModule, + CdnModule, ], }) export class AppModule {} diff --git a/src/core/path/path.service.ts b/src/core/path/path.service.ts index c78b2b4..d478692 100644 --- a/src/core/path/path.service.ts +++ b/src/core/path/path.service.ts @@ -21,16 +21,12 @@ export class PathService { const mappings: Record = { root: this.baseDir, public: path.join(this.baseDir, 'public'), - images: path.join(this.baseDir, 'public/files/image'), log: path.join(this.baseDir, 'logs'), config: path.join(this.baseDir, 'config'), temp: path.join(this.baseDir, 'temp'), userData: path.join(this.baseDir, 'private/data'), - files: path.join(this.baseDir, 'public/files'), - media: path.join(this.baseDir, 'public/files/media'), package: path.join(this.baseDir, 'package.json'), modules: path.join(this.baseDir, 'src/modules'), - uploads: path.join(this.baseDir, 'public/files/uploads'), words: path.join(this.baseDir, 'private/data/word'), }; @@ -46,9 +42,8 @@ export class PathService { this.get('log'), this.get('config'), this.get('userData'), - this.get('media'), this.get('temp'), - this.get('uploads'), + this.get('public'), this.get('words'), ]; @@ -105,14 +100,10 @@ export class PathService { export type PathType = | 'root' | 'public' - | 'images' | 'log' | 'config' | 'temp' | 'userData' - | 'files' | 'package' - | 'media' | 'modules' - | 'words' - | 'uploads'; + | 'words'; diff --git a/src/core/tools/token-auth.guard.ts b/src/core/tools/token-auth.guard.ts new file mode 100644 index 0000000..ad18c4d --- /dev/null +++ b/src/core/tools/token-auth.guard.ts @@ -0,0 +1,37 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + UnauthorizedException, + Logger, + Inject, +} from '@nestjs/common'; +import { ToolsService } from './tools.service'; + +/** + * token验证守卫 + */ +@Injectable() +export class TokenAuthGuard implements CanActivate { + private readonly logger = new Logger(TokenAuthGuard.name); + + constructor( + @Inject(ToolsService) private readonly toolsService: ToolsService, + ) {} + + canActivate(context: ExecutionContext): boolean { + const request = context.switchToHttp().getRequest(); + const token = request.body?.token || request.headers['x-token']; //两种传入方式 + + if (!token) { + this.logger.warn('请求缺少 token'); + throw new UnauthorizedException('缺少 token'); + } + + if (!this.toolsService.checkToken(token)) { + this.toolsService.tokenCheckFailed(token); + } + + return true; + } +} diff --git a/src/main.ts b/src/main.ts index 6699525..9a1d2a1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,6 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; -import { Logger } from '@nestjs/common'; +import { Logger, RequestMethod } from '@nestjs/common'; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import { ResponseInterceptor } from './common/interceptors/response.interceptor'; import { AllExceptionsFilter } from './common/filters/all-exception.filter'; @@ -24,7 +24,7 @@ async function bootstrap() { .setVersion('1.0') .build(); const document = () => SwaggerModule.createDocument(app, config); - SwaggerModule.setup('', app, document); + SwaggerModule.setup('docs', app, document); app.useWebSocketAdapter(new WsAdapter(app)); await app.listen(7000); await systemService.checkUpdate().catch((err) => { @@ -32,6 +32,6 @@ async function bootstrap() { }); } bootstrap().then(() => { - Logger.log(`API服务已启动:http://localhost:7000`); - Logger.log(`API文档: http://localhost:7000/api`); + Logger.log(`API服务已启动:http://localhost:7000/api`); + Logger.log(`API文档: http://localhost:7000/docs`); }); diff --git a/src/modules/bot/bot.controller.ts b/src/modules/bot/bot.controller.ts new file mode 100644 index 0000000..2b67f4e --- /dev/null +++ b/src/modules/bot/bot.controller.ts @@ -0,0 +1,70 @@ +import { Body, Controller, Inject, Post, UseGuards } from '@nestjs/common'; +import { ApiOperation, ApiTags, ApiBody } from '@nestjs/swagger'; +import { BotService } from './bot.service'; +import { WsClientManager } from 'src/core/ws/ws-client.manager'; +import { TokenAuthGuard } from 'src/core/tools/token-auth.guard'; +import { + BroadcastDto, + GroupInfoDto, + SendMessageDto, + TokenDto, +} from './bot.dto'; + +@ApiTags('Bot相关操作') +@Controller('bot') +export class BotController { + constructor( + @Inject(BotService) + private readonly botService: BotService, + @Inject(WsClientManager) + private readonly wsClientManager: WsClientManager, + ) {} + + @Post('getBotId') + @UseGuards(TokenAuthGuard) + @ApiOperation({ summary: '获取当前连接到核心的全部 botId 数组' }) + async postBotsId(@Body() dto: TokenDto) { + return this.botService.getBotId(); + } + + @Post('getGroupInfo') + @UseGuards(TokenAuthGuard) + @ApiOperation({ summary: '获取群聊信息' }) + @ApiBody({ type: GroupInfoDto }) + async postGroupInfo(@Body() dto: GroupInfoDto) { + return this.botService.getGroupInfo({ groupId: dto.groupId }); + } + + @Post('reportBots') + @UseGuards(TokenAuthGuard) + @ApiOperation({ summary: '广播:要求同步群聊信息和 bot 连接情况' }) + async reportBots(@Body() dto: TokenDto) { + const sendMessage = { + type: 'reportBots', + data: {}, + }; + await this.wsClientManager.broadcast(sendMessage); + return { message: '正在请求同步 bot 数据..' }; + } + + @Post('sendMessage') + @UseGuards(TokenAuthGuard) + @ApiOperation({ summary: '发送消息到群聊', description: '自动选择bot发送' }) + @ApiBody({ type: SendMessageDto }) + async sendMessage(@Body() dto: SendMessageDto) { + const flag = await this.botService.sendMessage(dto.groupId, dto.message); + if (!flag) { + return { message: '消息发送失败' }; + } + return { message: '消息发送成功' }; + } + + @Post('broadcast') + @UseGuards(TokenAuthGuard) + @ApiOperation({ summary: '广播消息到全部群聊', description: '随机延迟' }) + @ApiBody({ type: BroadcastDto }) + async smartBroadcast(@Body() dto: BroadcastDto) { + await this.botService.broadcastToAllGroups(dto.message); + return { message: '广播任务已开始,正在后台执行..' }; + } +} diff --git a/src/modules/bot/bot.dto.ts b/src/modules/bot/bot.dto.ts new file mode 100644 index 0000000..4d45034 --- /dev/null +++ b/src/modules/bot/bot.dto.ts @@ -0,0 +1,24 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class TokenDto { + @ApiProperty({ description: '访问核心的鉴权 token' }) + token: string; +} + +export class GroupInfoDto extends TokenDto { + @ApiProperty({ description: '群号', example: 114514 }) + groupId: number; +} + +export class SendMessageDto extends GroupInfoDto { + @ApiProperty({ description: '要发送的消息', example: 'Ciallo~(∠・ω< )⌒★' }) + message: string; +} + +export class BroadcastDto extends TokenDto { + @ApiProperty({ + description: '要广播的消息', + example: '全体目光向我看齐!我宣布个事儿..', + }) + message: string; +} diff --git a/src/modules/bot/bot.module.ts b/src/modules/bot/bot.module.ts new file mode 100644 index 0000000..aef49d8 --- /dev/null +++ b/src/modules/bot/bot.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { RedisModule } from '../../core/redis/redis.module'; +import { WsModule } from '../../core/ws/ws.module'; +import { ToolsModule } from '../../core/tools/tools.module'; +import { PathModule } from '../../core/path/path.module'; +import { BotController } from './bot.controller'; +import { BotService } from './bot.service'; + +@Module({ + imports: [RedisModule, WsModule, ToolsModule, PathModule], + controllers: [BotController], + providers: [BotService], +}) +export class BotModule {} diff --git a/src/modules/bot/bot.service.ts b/src/modules/bot/bot.service.ts new file mode 100644 index 0000000..10468c7 --- /dev/null +++ b/src/modules/bot/bot.service.ts @@ -0,0 +1,255 @@ +import { Inject, Injectable, Logger } from '@nestjs/common'; +import * as fs from 'fs/promises'; +import * as path from 'path'; +import { RedisService } from 'src/core/redis/redis.service'; +import { WsClientManager } from 'src/core/ws/ws-client.manager'; +import { ToolsService } from '../../core/tools/tools.service'; +import { PathService } from '../../core/path/path.service'; + +@Injectable() +export class BotService { + private readonly logger = new Logger(BotService.name); + + constructor( + @Inject(RedisService) + private readonly redisService: RedisService, + @Inject(WsClientManager) + private readonly wsClientManager: WsClientManager, + @Inject(ToolsService) + private readonly tools: ToolsService, + @Inject(PathService) + private readonly paths: PathService, + ) {} + + /** + * 获取botId数组 + */ + async getBotId(): Promise<{ uin: number; nickName: string }[]> { + this.logger.debug('正在请求获取在线的bot..'); + const userPath = this.paths.get('userData'); + const botsPath = path.join(userPath, '/crystelfBots'); + const dirData = await fs.readdir(botsPath); + const uins: { uin: number; nickName: string }[] = []; + + for (const fileName of dirData) { + if (!fileName.endsWith('.json')) continue; + + try { + const raw = await this.redisService.fetch('crystelfBots', fileName); + if (!raw || !Array.isArray(raw)) continue; + + for (const bot of raw) { + const uin = Number(bot.uin); + const nickName = bot.nickName || ''; + if (!isNaN(uin)) { + uins.push({ uin, nickName }); + } + } + } catch (err) { + this.logger.error(`读取或解析 ${fileName} 出错: ${err}`); + } + } + return uins; + } + + /** + * 获取群聊信息 + * @param data + */ + async getGroupInfo(data: { + botId?: number; + groupId: number; + clientId?: string; + }): Promise { + this.logger.debug(`正在尝试获取${data.groupId}的信息..)`); + const sendBot: number | undefined = + data.botId ?? (await this.getGroupBot(data.groupId)); + if (!sendBot) { + this.logger.warn(`不存在能向群聊${data.groupId}发送消息的Bot!`); + return undefined; + } + + const sendData = { + type: 'getGroupInfo', + data: { + botId: sendBot, + groupId: data.groupId, + clientID: data.clientId ?? (await this.getBotClient(sendBot)), + }, + }; + + if (sendData.data.clientID) { + const returnData = await this.wsClientManager.sendAndWait( + sendData.data.clientID, + sendData, + ); + return returnData ?? undefined; + } + return undefined; + } + + /** + * 发送消息到群聊 + * @param groupId 群号 + * @param message 消息 + */ + async sendMessage(groupId: number, message: string): Promise { + this.logger.log(`发送${message}到${groupId}..`); + const sendBot = await this.getGroupBot(groupId); + if (!sendBot) { + this.logger.warn(`不存在能向群聊${groupId}发送消息的Bot!`); + return false; + } + const client = await this.getBotClient(sendBot); + if (!client) { + this.logger.warn(`不存在${sendBot}对应的client!`); + return false; + } + const sendData = { + type: 'sendMessage', + data: { botId: sendBot, groupId, clientId: client, message }, + }; + await this.wsClientManager.send(client, sendData); + return true; + } + + /** + * 广播消息 + * @param message 要广播的消息 + */ + async broadcastToAllGroups(message: string): Promise { + const userPath = this.paths.get('userData'); + const botsPath = path.join(userPath, '/crystelfBots'); + const dirData = await fs.readdir(botsPath); + const groupMap: Map = + new Map(); + this.logger.log(`广播消息:${message}`); + for (const fileName of dirData) { + if (!fileName.endsWith('.json')) continue; + + const clientId = path.basename(fileName, '.json'); + const botList = await this.redisService.fetch('crystelfBots', fileName); + if (!Array.isArray(botList)) continue; + + for (const bot of botList) { + const botId = Number(bot.uin); + const groups = bot.groups; + + if (!botId || !Array.isArray(groups)) continue; + + for (const group of groups) { + if (group.group_id === '未知') continue; + const groupId = Number(group.group_id); + if (isNaN(groupId)) continue; + + if (!groupMap.has(groupId)) { + groupMap.set(groupId, []); + } + groupMap.get(groupId)!.push({ botId, clientId }); + } + } + } + + for (const [groupId, botEntries] of groupMap.entries()) { + this.logger.debug( + `[群 ${groupId}] 候选Bot列表: ${JSON.stringify(botEntries)}`, + ); + + const clientGroups = new Map(); + botEntries.forEach(({ botId, clientId }) => { + if (!clientGroups.has(clientId)) clientGroups.set(clientId, []); + clientGroups.get(clientId)!.push(botId); + }); + + const selectedClientId = this.tools.getRandomItem([ + ...clientGroups.keys(), + ]); + const botCandidates = clientGroups.get(selectedClientId)!; + const selectedBotId = this.tools.getRandomItem(botCandidates); + const delay = this.tools.getRandomDelay(10_000, 150_000); + + setTimeout(() => { + const sendData = { + type: 'sendMessage', + data: { + botId: selectedBotId, + groupId, + clientId: selectedClientId, + message, + }, + }; + this.logger.log( + `[广播] 向群 ${groupId} 使用Bot ${selectedBotId}(客户端 ${selectedClientId})发送消息${message},延迟 ${ + delay / 1000 + } 秒`, + ); + this.wsClientManager.send(selectedClientId, sendData).catch((e) => { + this.logger.error(`发送到群${groupId}失败:`, e); + }); + }, delay); + } + } + + /** + * 获取botId对应的client + * @param botId + * @private + */ + private async getBotClient(botId: number): Promise { + const userPath = this.paths.get('userData'); + const botsPath = path.join(userPath, '/crystelfBots'); + const dirData = await fs.readdir(botsPath); + + for (const clientId of dirData) { + if (!clientId.endsWith('.json')) continue; + + try { + const raw = await this.redisService.fetch('crystelfBots', clientId); + if (!Array.isArray(raw)) continue; + + for (const bot of raw) { + const uin = Number(bot.uin); + if (!isNaN(uin) && uin === botId) { + return path.basename(clientId, '.json'); + } + } + } catch (err) { + this.logger.error(`读取${clientId}出错..`); + } + } + return undefined; + } + + /** + * 获取groupId对应的botId + * @param groupId + * @private + */ + private async getGroupBot(groupId: number): Promise { + const userPath = this.paths.get('userData'); + const botsPath = path.join(userPath, '/crystelfBots'); + const dirData = await fs.readdir(botsPath); + + for (const clientId of dirData) { + if (!clientId.endsWith('.json')) continue; + + try { + const raw = await this.redisService.fetch('crystelfBots', clientId); + if (!Array.isArray(raw)) continue; + + for (const bot of raw) { + const uin = Number(bot.uin); + const groups = bot.groups; + if (!uin || !Array.isArray(groups)) continue; + + if (groups.find((g) => Number(g.group_id) === groupId)) { + return uin; + } + } + } catch (err) { + this.logger.error(`读取${clientId}出错..`); + } + } + return undefined; + } +} diff --git a/src/modules/cdn/cdn.controller.ts b/src/modules/cdn/cdn.controller.ts new file mode 100644 index 0000000..bb5adb9 --- /dev/null +++ b/src/modules/cdn/cdn.controller.ts @@ -0,0 +1,79 @@ +import { + Controller, + Get, + Param, + Res, + Logger, + HttpException, + HttpStatus, + Inject, + Req, +} from '@nestjs/common'; +import { CdnService } from './cdn.service'; +import { Response } from 'express'; +import { ApiOperation } from '@nestjs/swagger'; + +@Controller() +export class CdnController { + private readonly logger = new Logger(CdnController.name); + + constructor(@Inject(CdnService) private readonly fileService: CdnService) {} + + private async deliverFile(relativePath: string, res: Response) { + try { + this.logger.log(`有个小可爱正在请求 /cdn/${relativePath} ..`); + + const filePath = await this.fileService.getFile(relativePath); + if (!filePath) { + this.logger.warn(`${relativePath}:文件不存在..`); + throw new HttpException('文件不存在啦!', HttpStatus.NOT_FOUND); + } + + res.sendFile(filePath, (err) => { + if (err) { + this.logger.error(`文件投递失败: ${err.message}`); + throw new HttpException( + 'Crystelf-CDN处理文件请求时出错..', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + }); + + this.logger.log(`成功投递文件: ${filePath}`); + } catch (error) { + this.logger.error('晶灵数据请求处理失败:', error); + throw new HttpException( + 'Crystelf-CDN处理文件请求时出错..', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + + @Get('cdn/*') + @ApiOperation({ + summary: '获取资源', + description: '由晶灵资源分发服务器(CDN)提供支持', + }) + async getFile(@Res() res: Response, @Req() req: Request) { + const relativePath = req.url.replace('/api/cdn/', ''); //params.path; + return this.deliverFile(relativePath, res); + } + + @Get('public/files/*') + async fromPublicFiles(@Res() res: Response, @Req() req: Request) { + const relativePath = req.url.replace('/api/public/files/', ''); + this.logger.debug( + `请求 /public/files/${relativePath} → 代理到 /cdn/${relativePath}`, + ); + return this.deliverFile(relativePath, res); + } + + @Get('public/cdn/*') + async fromPublicCdn(@Req() req: Request, @Res() res: Response) { + const relativePath = req.url.replace('/api/public/cdn/', ''); + this.logger.debug( + `请求 /public/cdn/${relativePath} → 代理到 /cdn/${relativePath}`, + ); + return this.deliverFile(relativePath, res); + } +} diff --git a/src/modules/cdn/cdn.module.ts b/src/modules/cdn/cdn.module.ts new file mode 100644 index 0000000..b6add60 --- /dev/null +++ b/src/modules/cdn/cdn.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { CdnController } from './cdn.controller'; +import { CdnService } from './cdn.service'; +import { PathModule } from '../../core/path/path.module'; + +@Module({ + imports: [PathModule], + controllers: [CdnController], + providers: [CdnService], +}) +export class CdnModule {} diff --git a/src/modules/cdn/cdn.service.ts b/src/modules/cdn/cdn.service.ts new file mode 100644 index 0000000..e09948c --- /dev/null +++ b/src/modules/cdn/cdn.service.ts @@ -0,0 +1,54 @@ +import { Inject, Injectable, Logger } from '@nestjs/common'; +import * as path from 'path'; +import { existsSync } from 'fs'; +import { PathService } from '../../core/path/path.service'; + +@Injectable() +export class CdnService { + private readonly logger = new Logger(CdnService.name); + private filePath: string; + @Inject(PathService) + private readonly paths: PathService; + constructor() { + this.logger.log(`晶灵云图数据中心初始化.. 数据存储在: ${this.filePath}`); + } + + /** + * 获取文件 + * @param relativePath 文件相对路径 + */ + async getFile(relativePath: string): Promise { + if (!this.filePath) this.filePath = this.paths.get('public'); + if ( + !this.isValidPath(relativePath) && + !this.isValidFilename(path.basename(relativePath)) + ) { + throw new Error('非法路径请求'); + } + + const filePath = path.join(this.filePath, relativePath); + this.logger.debug(`尝试访问文件路径: ${filePath}`); + + return existsSync(filePath) ? filePath : null; + } + + /** + * 检查路径合法性 + */ + private isValidPath(relativePath: string): boolean { + try { + const normalized = path.normalize(relativePath); + let flag = true; + if (normalized.startsWith('../') && path.isAbsolute(normalized)) + flag = false; + return flag; + } catch (err) { + this.logger.error(err); + return false; + } + } + + private isValidFilename(filename: string): boolean { + return /^[a-zA-Z0-9_\-.]+$/.test(filename); + } +} diff --git a/src/modules/system/systemWeb.controller.ts b/src/modules/system/systemWeb.controller.ts index d22ecb5..da81718 100644 --- a/src/modules/system/systemWeb.controller.ts +++ b/src/modules/system/systemWeb.controller.ts @@ -4,12 +4,15 @@ import { Body, UnauthorizedException, Inject, + UseGuards, + Param, } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiBody, ApiProperty } from '@nestjs/swagger'; import { SystemWebService } from './systemWeb.service'; import { ToolsService } from '../../core/tools/tools.service'; +import { TokenAuthGuard } from '../../core/tools/token-auth.guard'; -class TokenDto { +class WebServerDto { @ApiProperty({ description: '密钥', example: '1111', @@ -35,12 +38,10 @@ export class SystemWebController { summary: '系统重启', description: '核心执行重启', }) - @ApiBody({ type: TokenDto }) - async systemRestart(@Body() body: TokenDto): Promise { - if (!this.toolService.checkToken(body.token)) { - throw new UnauthorizedException('Token 无效'); - } - await this.systemService.systemRestart(); + @UseGuards(TokenAuthGuard) + @ApiBody({ type: WebServerDto }) + async systemRestart(@Param('token') token: string): Promise { + this.systemService.systemRestart(); return '核心正在重启..'; } @@ -52,11 +53,9 @@ export class SystemWebController { summary: '获取重启所需时间', description: '返回上次核心重启的耗时', }) - @ApiBody({ type: TokenDto }) - async getRestartTime(@Body() body: TokenDto): Promise { - if (!this.toolService.checkToken(body.token)) { - throw new UnauthorizedException('Token 无效'); - } + @UseGuards(TokenAuthGuard) + @ApiBody({ type: WebServerDto }) + async getRestartTime(@Param('token') token: string): Promise { return await this.systemService.getRestartTime(); } } diff --git a/src/modules/system/systemWeb.service.ts b/src/modules/system/systemWeb.service.ts index 99cd0fb..2def66f 100644 --- a/src/modules/system/systemWeb.service.ts +++ b/src/modules/system/systemWeb.service.ts @@ -1,6 +1,6 @@ import { Inject, Injectable, Logger } from '@nestjs/common'; import fs from 'fs/promises'; -import path from 'path'; +import * as path from 'path'; import { PathService } from '../../core/path/path.service'; import { SystemService } from 'src/core/system/system.service';