mirror of
https://github.com/crystelf/crystelf-core.git
synced 2025-12-05 18:41:56 +00:00
Compare commits
No commits in common. "fc2ffeb14573434c2d4a005899545d99d632a1e9" and "2d3d11be889d524941bf6ad9e2278559550b2ad8" have entirely different histories.
fc2ffeb145
...
2d3d11be88
@ -4,62 +4,28 @@ import {
|
|||||||
ExceptionFilter,
|
ExceptionFilter,
|
||||||
HttpException,
|
HttpException,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
Logger,
|
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 全局异常过滤器
|
* 异常类
|
||||||
*/
|
*/
|
||||||
@Catch()
|
@Catch()
|
||||||
export class AllExceptionsFilter implements ExceptionFilter {
|
export class AllExceptionsFilter implements ExceptionFilter {
|
||||||
private readonly logger = new Logger(AllExceptionsFilter.name);
|
|
||||||
|
|
||||||
catch(exception: unknown, host: ArgumentsHost) {
|
catch(exception: unknown, host: ArgumentsHost) {
|
||||||
const ctx = host.switchToHttp();
|
const ctx = host.switchToHttp();
|
||||||
const response = ctx.getResponse();
|
const response = ctx.getResponse();
|
||||||
const request = ctx.getRequest();
|
const status =
|
||||||
|
exception instanceof HttpException
|
||||||
|
? exception.getStatus()
|
||||||
|
: HttpStatus.INTERNAL_SERVER_ERROR;
|
||||||
|
|
||||||
let status: number;
|
const message =
|
||||||
let message: string;
|
exception instanceof HttpException ? exception.message : '服务器内部错误';
|
||||||
let errorDetails: any = null;
|
|
||||||
|
|
||||||
if (exception instanceof HttpException) {
|
response.status(status).json({
|
||||||
status = exception.getStatus();
|
|
||||||
message = exception.message;
|
|
||||||
|
|
||||||
const exceptionResponse = exception.getResponse();
|
|
||||||
if (typeof exceptionResponse === 'object' && exceptionResponse !== null) {
|
|
||||||
errorDetails = exceptionResponse;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
status = HttpStatus.INTERNAL_SERVER_ERROR;
|
|
||||||
if (exception instanceof Error) {
|
|
||||||
message = exception.message || '服务器内部错误';
|
|
||||||
errorDetails = {
|
|
||||||
name: exception.name,
|
|
||||||
stack:
|
|
||||||
process.env.NODE_ENV === 'development'
|
|
||||||
? exception.stack
|
|
||||||
: undefined,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
message = '服务器内部错误';
|
|
||||||
errorDetails = String(exception);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.error(
|
|
||||||
`异常捕获 - ${request.method} ${request.url} - ${status} - ${message}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const errorResponse = {
|
|
||||||
success: false,
|
success: false,
|
||||||
data: null,
|
data: null,
|
||||||
message,
|
message,
|
||||||
...(process.env.NODE_ENV === 'development' &&
|
});
|
||||||
errorDetails && { errorDetails }),
|
|
||||||
};
|
|
||||||
|
|
||||||
response.status(status).json(errorResponse);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,108 +0,0 @@
|
|||||||
import {
|
|
||||||
CallHandler,
|
|
||||||
ExecutionContext,
|
|
||||||
Injectable,
|
|
||||||
Logger,
|
|
||||||
NestInterceptor,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
import { tap } from 'rxjs/operators';
|
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as path from 'path';
|
|
||||||
|
|
||||||
interface RequestLogEntry {
|
|
||||||
timestamp: string;
|
|
||||||
ip: string | string[] | undefined;
|
|
||||||
method: string;
|
|
||||||
url: string;
|
|
||||||
controller: string;
|
|
||||||
handler: string;
|
|
||||||
userAgent?: string;
|
|
||||||
params?: any;
|
|
||||||
query?: any;
|
|
||||||
body?: any;
|
|
||||||
statusCode?: number;
|
|
||||||
durationMs?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class RequestLogInterceptor implements NestInterceptor {
|
|
||||||
private readonly logger = new Logger(RequestLogInterceptor.name);
|
|
||||||
|
|
||||||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
|
||||||
const http = context.switchToHttp();
|
|
||||||
const req = http.getRequest();
|
|
||||||
const res = http.getResponse();
|
|
||||||
|
|
||||||
const now = Date.now();
|
|
||||||
const controller = context.getClass().name;
|
|
||||||
const handler = context.getHandler().name;
|
|
||||||
const ip = req.headers['x-forwarded-for'] || req.ip;
|
|
||||||
const method = req.method;
|
|
||||||
const url = req.originalUrl || req.url;
|
|
||||||
const userAgent = req.headers['user-agent'];
|
|
||||||
this.logger.log(
|
|
||||||
`${method} ${url} - ${controller}.${handler} - ip=${Array.isArray(ip) ? ip[0] : ip}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return next.handle().pipe(
|
|
||||||
tap({
|
|
||||||
next: () => {
|
|
||||||
this.writeLog({
|
|
||||||
timestamp: new Date(now).toISOString(),
|
|
||||||
ip,
|
|
||||||
method,
|
|
||||||
url,
|
|
||||||
controller,
|
|
||||||
handler,
|
|
||||||
userAgent,
|
|
||||||
params: req.params,
|
|
||||||
query: req.query,
|
|
||||||
body: req.body,
|
|
||||||
statusCode: res.statusCode,
|
|
||||||
durationMs: Date.now() - now,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
error: () => {
|
|
||||||
this.writeLog({
|
|
||||||
timestamp: new Date(now).toISOString(),
|
|
||||||
ip,
|
|
||||||
method,
|
|
||||||
url,
|
|
||||||
controller,
|
|
||||||
handler,
|
|
||||||
userAgent,
|
|
||||||
params: req.params,
|
|
||||||
query: req.query,
|
|
||||||
body: req.body,
|
|
||||||
statusCode: res.statusCode,
|
|
||||||
durationMs: Date.now() - now,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private writeLog(entry: RequestLogEntry) {
|
|
||||||
try {
|
|
||||||
const baseDir = path.resolve(process.cwd(), 'logs');
|
|
||||||
if (!fs.existsSync(baseDir)) {
|
|
||||||
fs.mkdirSync(baseDir, { recursive: true });
|
|
||||||
}
|
|
||||||
const fileName = `access-${this.getDateStr()}.jsonl`;
|
|
||||||
const filePath = path.join(baseDir, fileName);
|
|
||||||
const serialized = JSON.stringify(entry) + '\n';
|
|
||||||
fs.appendFile(filePath, serialized, { encoding: 'utf8' }, () => {});
|
|
||||||
} catch (err) {
|
|
||||||
this.logger.warn(`写入访问日志失败: ${err?.message || err}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private getDateStr(): string {
|
|
||||||
const d = new Date();
|
|
||||||
const yyyy = d.getFullYear();
|
|
||||||
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
||||||
const dd = String(d.getDate()).padStart(2, '0');
|
|
||||||
return `${yyyy}-${mm}-${dd}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,108 +0,0 @@
|
|||||||
import { HttpException, HttpStatus, Logger } from '@nestjs/common';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 错误处理工具类
|
|
||||||
*/
|
|
||||||
export class ErrorUtil {
|
|
||||||
private static readonly logger = new Logger(ErrorUtil.name);
|
|
||||||
/**
|
|
||||||
* 创建业务异常
|
|
||||||
* @param message 错误消息
|
|
||||||
* @param status HTTP状态码
|
|
||||||
* @param details 详细信息
|
|
||||||
*/
|
|
||||||
static createBusinessError(
|
|
||||||
message: string,
|
|
||||||
status: HttpStatus = HttpStatus.BAD_REQUEST,
|
|
||||||
details?: any,
|
|
||||||
): HttpException {
|
|
||||||
return new HttpException(
|
|
||||||
{
|
|
||||||
message,
|
|
||||||
details,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
status,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建服务器内部错误
|
|
||||||
* @param message 错误消息
|
|
||||||
* @param originalError 原始错误
|
|
||||||
*/
|
|
||||||
static createInternalError(
|
|
||||||
message: string,
|
|
||||||
originalError?: any,
|
|
||||||
): HttpException {
|
|
||||||
return new HttpException(
|
|
||||||
{
|
|
||||||
message,
|
|
||||||
originalError: originalError?.message || originalError,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建资源未找到错误
|
|
||||||
* @param resource 资源名称
|
|
||||||
* @param identifier 资源标识符
|
|
||||||
*/
|
|
||||||
static createNotFoundError(
|
|
||||||
resource: string,
|
|
||||||
identifier?: string,
|
|
||||||
): HttpException {
|
|
||||||
const message = identifier
|
|
||||||
? `${resource} '${identifier}' 不存在`
|
|
||||||
: `${resource} 不存在`;
|
|
||||||
|
|
||||||
return this.createBusinessError(message, HttpStatus.NOT_FOUND);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建验证错误
|
|
||||||
* @param message 错误消息
|
|
||||||
* @param field 字段名
|
|
||||||
*/
|
|
||||||
static createValidationError(message: string, field?: string): HttpException {
|
|
||||||
return new HttpException(
|
|
||||||
{
|
|
||||||
message,
|
|
||||||
field,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
HttpStatus.BAD_REQUEST,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理未知错误并转换为HttpException
|
|
||||||
* @param error 未知错误
|
|
||||||
* @param defaultMessage 默认错误消息
|
|
||||||
* @param context 错误上下文(可选)
|
|
||||||
*/
|
|
||||||
static handleUnknownError(
|
|
||||||
error: any,
|
|
||||||
defaultMessage: string = '服务器内部错误',
|
|
||||||
context?: string,
|
|
||||||
): HttpException {
|
|
||||||
const errorMsg = error?.message || String(error);
|
|
||||||
const logMessage = context ? `${context}: ${errorMsg}` : errorMsg;
|
|
||||||
this.logger.error(logMessage);
|
|
||||||
|
|
||||||
if (error instanceof HttpException) {
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error instanceof Error) {
|
|
||||||
return this.createInternalError(
|
|
||||||
`${defaultMessage}: ${error.message}`,
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.createInternalError(`${defaultMessage}: ${String(error)}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -3,7 +3,6 @@ import { AppModule } from './app.module';
|
|||||||
import { Logger, RequestMethod } from '@nestjs/common';
|
import { Logger, RequestMethod } from '@nestjs/common';
|
||||||
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||||
import { ResponseInterceptor } from './common/interceptors/response.interceptor';
|
import { ResponseInterceptor } from './common/interceptors/response.interceptor';
|
||||||
import { RequestLogInterceptor } from './common/interceptors/request-log.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';
|
import { WsAdapter } from '@nestjs/platform-ws';
|
||||||
@ -36,7 +35,7 @@ async function bootstrap() {
|
|||||||
{ path: 'public/(.*)', method: RequestMethod.ALL },
|
{ path: 'public/(.*)', method: RequestMethod.ALL },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
app.useGlobalInterceptors(new RequestLogInterceptor(), new ResponseInterceptor());
|
app.useGlobalInterceptors(new ResponseInterceptor());
|
||||||
app.useGlobalFilters(new AllExceptionsFilter());
|
app.useGlobalFilters(new AllExceptionsFilter());
|
||||||
const systemService = app.get(SystemService);
|
const systemService = app.get(SystemService);
|
||||||
const restartDuration = systemService.checkRestartTime();
|
const restartDuration = systemService.checkRestartTime();
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Body, Controller, Inject, Post, UseGuards } from '@nestjs/common';
|
import { Body, Controller, Inject, Post, UseGuards } from '@nestjs/common';
|
||||||
import { ApiOperation, ApiTags, ApiBody, ApiHeader } from '@nestjs/swagger';
|
import { ApiOperation, ApiTags, ApiBody } from '@nestjs/swagger';
|
||||||
import { BotService } from './bot.service';
|
import { BotService } from './bot.service';
|
||||||
import { WsClientManager } from 'src/core/ws/ws-client.manager';
|
import { WsClientManager } from 'src/core/ws/ws-client.manager';
|
||||||
import { TokenAuthGuard } from 'src/core/tools/token-auth.guard';
|
import { TokenAuthGuard } from 'src/core/tools/token-auth.guard';
|
||||||
@ -23,14 +23,6 @@ export class BotController {
|
|||||||
@Post('getBotId')
|
@Post('getBotId')
|
||||||
@UseGuards(TokenAuthGuard)
|
@UseGuards(TokenAuthGuard)
|
||||||
@ApiOperation({ summary: '获取当前连接到核心的全部 botId 数组' })
|
@ApiOperation({ summary: '获取当前连接到核心的全部 botId 数组' })
|
||||||
@ApiHeader({ name: 'x-token', description: '身份验证token', required: true })
|
|
||||||
@ApiBody({
|
|
||||||
description: '获取botId请求',
|
|
||||||
schema: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
public async postBotsId(@Body() dto: TokenDto) {
|
public async postBotsId(@Body() dto: TokenDto) {
|
||||||
return this.botService.getBotId();
|
return this.botService.getBotId();
|
||||||
}
|
}
|
||||||
@ -38,17 +30,7 @@ export class BotController {
|
|||||||
@Post('getGroupInfo')
|
@Post('getGroupInfo')
|
||||||
@UseGuards(TokenAuthGuard)
|
@UseGuards(TokenAuthGuard)
|
||||||
@ApiOperation({ summary: '获取群聊信息' })
|
@ApiOperation({ summary: '获取群聊信息' })
|
||||||
@ApiHeader({ name: 'x-token', description: '身份验证token', required: true })
|
@ApiBody({ type: GroupInfoDto })
|
||||||
@ApiBody({
|
|
||||||
description: '获取群聊信息参数',
|
|
||||||
schema: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
groupId: { type: 'number', description: '群号', example: 114514 },
|
|
||||||
},
|
|
||||||
required: ['groupId'],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
public async postGroupInfo(@Body() dto: GroupInfoDto) {
|
public async postGroupInfo(@Body() dto: GroupInfoDto) {
|
||||||
return this.botService.getGroupInfo({ groupId: dto.groupId });
|
return this.botService.getGroupInfo({ groupId: dto.groupId });
|
||||||
}
|
}
|
||||||
@ -56,14 +38,6 @@ export class BotController {
|
|||||||
@Post('reportBots')
|
@Post('reportBots')
|
||||||
@UseGuards(TokenAuthGuard)
|
@UseGuards(TokenAuthGuard)
|
||||||
@ApiOperation({ summary: '广播:要求同步群聊信息和 bot 连接情况' })
|
@ApiOperation({ summary: '广播:要求同步群聊信息和 bot 连接情况' })
|
||||||
@ApiHeader({ name: 'x-token', description: '身份验证token', required: true })
|
|
||||||
@ApiBody({
|
|
||||||
description: '广播同步请求',
|
|
||||||
schema: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
public async reportBots(@Body() dto: TokenDto) {
|
public async reportBots(@Body() dto: TokenDto) {
|
||||||
const sendMessage = {
|
const sendMessage = {
|
||||||
type: 'reportBots',
|
type: 'reportBots',
|
||||||
@ -76,18 +50,7 @@ export class BotController {
|
|||||||
@Post('sendMessage')
|
@Post('sendMessage')
|
||||||
@UseGuards(TokenAuthGuard)
|
@UseGuards(TokenAuthGuard)
|
||||||
@ApiOperation({ summary: '发送消息到群聊', description: '自动选择bot发送' })
|
@ApiOperation({ summary: '发送消息到群聊', description: '自动选择bot发送' })
|
||||||
@ApiHeader({ name: 'x-token', description: '身份验证token', required: true })
|
@ApiBody({ type: SendMessageDto })
|
||||||
@ApiBody({
|
|
||||||
description: '发送消息参数',
|
|
||||||
schema: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
groupId: { type: 'number', description: '群号', example: 114514 },
|
|
||||||
message: { type: 'string', description: '要发送的消息', example: 'Ciallo~(∠・ω< )⌒★' },
|
|
||||||
},
|
|
||||||
required: ['groupId', 'message'],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
public async sendMessage(@Body() dto: SendMessageDto) {
|
public async sendMessage(@Body() dto: SendMessageDto) {
|
||||||
const flag = await this.botService.sendMessage(dto.groupId, dto.message);
|
const flag = await this.botService.sendMessage(dto.groupId, dto.message);
|
||||||
if (!flag) {
|
if (!flag) {
|
||||||
@ -99,17 +62,7 @@ export class BotController {
|
|||||||
@Post('broadcast')
|
@Post('broadcast')
|
||||||
@UseGuards(TokenAuthGuard)
|
@UseGuards(TokenAuthGuard)
|
||||||
@ApiOperation({ summary: '广播消息到全部群聊', description: '随机延迟' })
|
@ApiOperation({ summary: '广播消息到全部群聊', description: '随机延迟' })
|
||||||
@ApiHeader({ name: 'x-token', description: '身份验证token', required: true })
|
@ApiBody({ type: BroadcastDto })
|
||||||
@ApiBody({
|
|
||||||
description: '广播消息参数',
|
|
||||||
schema: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
message: { type: 'string', description: '要广播的消息', example: '全体目光向我看齐!我宣布个事儿..' },
|
|
||||||
},
|
|
||||||
required: ['message'],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
public async smartBroadcast(@Body() dto: BroadcastDto) {
|
public async smartBroadcast(@Body() dto: BroadcastDto) {
|
||||||
await this.botService.broadcastToAllGroups(dto.message);
|
await this.botService.broadcastToAllGroups(dto.message);
|
||||||
return { message: '广播任务已开始,正在后台执行..' };
|
return { message: '广播任务已开始,正在后台执行..' };
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
export class TokenDto {
|
export class TokenDto {
|
||||||
|
@ApiProperty({ description: '访问核心的鉴权 token' })
|
||||||
|
token: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GroupInfoDto {
|
export class GroupInfoDto extends TokenDto {
|
||||||
@ApiProperty({ description: '群号', example: 114514 })
|
@ApiProperty({ description: '群号', example: 114514 })
|
||||||
groupId: number;
|
groupId: number;
|
||||||
}
|
}
|
||||||
@ -13,7 +15,7 @@ export class SendMessageDto extends GroupInfoDto {
|
|||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BroadcastDto {
|
export class BroadcastDto extends TokenDto {
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: '要广播的消息',
|
description: '要广播的消息',
|
||||||
example: '全体目光向我看齐!我宣布个事儿..',
|
example: '全体目光向我看齐!我宣布个事儿..',
|
||||||
|
|||||||
@ -1,8 +1,17 @@
|
|||||||
import { Controller, Get, Res, Logger, Inject, Req } from '@nestjs/common';
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
Res,
|
||||||
|
Logger,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
Inject,
|
||||||
|
Req,
|
||||||
|
} from '@nestjs/common';
|
||||||
import { CdnService } from './cdn.service';
|
import { CdnService } from './cdn.service';
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import { ApiOperation } from '@nestjs/swagger';
|
import { ApiOperation } from '@nestjs/swagger';
|
||||||
import { ErrorUtil } from '../../common/utils/error.util';
|
|
||||||
|
|
||||||
@Controller()
|
@Controller()
|
||||||
export class CdnController {
|
export class CdnController {
|
||||||
@ -17,19 +26,26 @@ export class CdnController {
|
|||||||
const filePath = await this.fileService.getFile(relativePath);
|
const filePath = await this.fileService.getFile(relativePath);
|
||||||
if (!filePath) {
|
if (!filePath) {
|
||||||
this.logger.warn(`${relativePath}:文件不存在..`);
|
this.logger.warn(`${relativePath}:文件不存在..`);
|
||||||
throw ErrorUtil.createNotFoundError('文件', relativePath);
|
throw new HttpException('文件不存在啦!', HttpStatus.NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
res.sendFile(filePath, (err) => {
|
res.sendFile(filePath, (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
this.logger.error(`文件投递失败: ${err.message}`);
|
this.logger.error(`文件投递失败: ${err.message}`);
|
||||||
throw ErrorUtil.createInternalError('文件投递失败', err);
|
throw new HttpException(
|
||||||
|
'Crystelf-CDN处理文件请求时出错..',
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.logger.log(`成功投递文件: ${filePath}`);
|
this.logger.log(`成功投递文件: ${filePath}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw ErrorUtil.handleUnknownError(error, 'CDN处理文件请求失败', 'deliverFile');
|
this.logger.error('晶灵数据请求处理失败:', error);
|
||||||
|
throw new HttpException(
|
||||||
|
'Crystelf-CDN处理文件请求时出错..',
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import {
|
|||||||
Body,
|
Body,
|
||||||
Query,
|
Query,
|
||||||
Res,
|
Res,
|
||||||
|
HttpException,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
Logger,
|
Logger,
|
||||||
Inject,
|
Inject,
|
||||||
@ -34,7 +35,6 @@ import { OpenListService } from '../../core/openlist/openlist.service';
|
|||||||
import { PathService } from '../../core/path/path.service';
|
import { PathService } from '../../core/path/path.service';
|
||||||
import { TokenAuthGuard } from '../../core/tools/token-auth.guard';
|
import { TokenAuthGuard } from '../../core/tools/token-auth.guard';
|
||||||
import { AppConfigService } from '../../config/config.service';
|
import { AppConfigService } from '../../config/config.service';
|
||||||
import { ErrorUtil } from '../../common/utils/error.util';
|
|
||||||
|
|
||||||
class MemeRequestDto {
|
class MemeRequestDto {
|
||||||
character?: string;
|
character?: string;
|
||||||
@ -64,17 +64,10 @@ export class MemeController {
|
|||||||
|
|
||||||
@Post('get')
|
@Post('get')
|
||||||
@ApiOperation({ summary: '获取随机表情包' })
|
@ApiOperation({ summary: '获取随机表情包' })
|
||||||
@ApiHeader({ name: 'x-token', description: '身份验证token', required: false })
|
@ApiQuery({ name: 'character', required: false, description: '角色名称' })
|
||||||
@ApiBody({
|
@ApiQuery({ name: 'status', required: false, description: '状态' })
|
||||||
description: '获取表情包参数',
|
@ApiQuery({ name: 'token', required: false, description: '可选访问令牌' })
|
||||||
schema: {
|
@ApiBody({ type: MemeRequestDto })
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
character: { type: 'string', description: '角色名称' },
|
|
||||||
status: { type: 'string', description: '状态' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
public async getRandomMemePost(
|
public async getRandomMemePost(
|
||||||
@Body() dto: MemeRequestDto,
|
@Body() dto: MemeRequestDto,
|
||||||
@Res() res: Response,
|
@Res() res: Response,
|
||||||
@ -121,7 +114,10 @@ export class MemeController {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!memePath) {
|
if (!memePath) {
|
||||||
throw ErrorUtil.createNotFoundError('表情包');
|
throw new HttpException(
|
||||||
|
'没有找到符合条件的表情包',
|
||||||
|
HttpStatus.NOT_FOUND,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ext = memePath.split('.').pop()?.toLowerCase();
|
const ext = memePath.split('.').pop()?.toLowerCase();
|
||||||
@ -133,8 +129,11 @@ export class MemeController {
|
|||||||
res.setHeader('Content-Type', contentType);
|
res.setHeader('Content-Type', contentType);
|
||||||
const stream = fs.createReadStream(memePath);
|
const stream = fs.createReadStream(memePath);
|
||||||
|
|
||||||
stream.on('error', (error) => {
|
stream.on('error', () => {
|
||||||
throw ErrorUtil.createInternalError('读取表情包失败', error);
|
throw new HttpException(
|
||||||
|
'读取表情包失败',
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const fd = await fs.promises.open(memePath, 'r');
|
const fd = await fs.promises.open(memePath, 'r');
|
||||||
@ -177,7 +176,8 @@ export class MemeController {
|
|||||||
stream.pipe(throttle).pipe(res);
|
stream.pipe(throttle).pipe(res);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw ErrorUtil.handleUnknownError(e, '获取表情包失败', 'handleMemeRequest');
|
this.logger.error(`获取表情包失败:${e.message}`);
|
||||||
|
throw new HttpException('服务器错误', HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,6 +186,7 @@ export class MemeController {
|
|||||||
* @param file
|
* @param file
|
||||||
* @param character
|
* @param character
|
||||||
* @param status
|
* @param status
|
||||||
|
* @param res
|
||||||
*/
|
*/
|
||||||
@Post('upload')
|
@Post('upload')
|
||||||
@ApiOperation({ summary: '上传表情包并同步' })
|
@ApiOperation({ summary: '上传表情包并同步' })
|
||||||
@ -210,7 +211,7 @@ export class MemeController {
|
|||||||
@Body('status') status: string,
|
@Body('status') status: string,
|
||||||
) {
|
) {
|
||||||
if (!file) {
|
if (!file) {
|
||||||
throw ErrorUtil.createValidationError('未检测到上传文件');
|
throw new HttpException('未检测到上传文件', HttpStatus.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -225,7 +226,7 @@ export class MemeController {
|
|||||||
const buffer = file.buffer || (await fsp.readFile(file.path));
|
const buffer = file.buffer || (await fsp.readFile(file.path));
|
||||||
const imgType = await imageType(buffer);
|
const imgType = await imageType(buffer);
|
||||||
if (!imgType || !['jpg', 'png', 'gif', 'webp'].includes(imgType.ext)) {
|
if (!imgType || !['jpg', 'png', 'gif', 'webp'].includes(imgType.ext)) {
|
||||||
throw ErrorUtil.createBusinessError(
|
throw new HttpException(
|
||||||
'不支持的图片格式',
|
'不支持的图片格式',
|
||||||
HttpStatus.UNSUPPORTED_MEDIA_TYPE,
|
HttpStatus.UNSUPPORTED_MEDIA_TYPE,
|
||||||
);
|
);
|
||||||
@ -242,10 +243,10 @@ export class MemeController {
|
|||||||
) {
|
) {
|
||||||
fileList = listResult.data.content.map((f) => f.name);
|
fileList = listResult.data.content.map((f) => f.name);
|
||||||
} else {
|
} else {
|
||||||
this.logger.warn(`目录为空或返回结构异常:${remoteDir}`);
|
this.logger.warn(`目录为空或返回结构异常:${remoteDir}`);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logger.warn(`获取远程目录失败(${remoteDir}),将自动创建`);
|
this.logger.warn(`获取远程目录失败(${remoteDir}),将自动创建`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const usedNumbers = fileList
|
const usedNumbers = fileList
|
||||||
@ -270,7 +271,11 @@ export class MemeController {
|
|||||||
this.logger.log(`表情包上传成功: ${remoteFilePath}`);
|
this.logger.log(`表情包上传成功: ${remoteFilePath}`);
|
||||||
return '表情上传成功..';
|
return '表情上传成功..';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw ErrorUtil.handleUnknownError(error, '上传失败', 'uploadMeme');
|
this.logger.error('表情包上传失败:', error);
|
||||||
|
throw new HttpException(
|
||||||
|
`上传失败: ${error.message || error}`,
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,16 @@
|
|||||||
import { Controller, Post, Inject, UseGuards } from '@nestjs/common';
|
import { Controller, Post, Inject, UseGuards, Param } from '@nestjs/common';
|
||||||
import { ApiTags, ApiOperation, ApiBody, ApiHeader } from '@nestjs/swagger';
|
import { ApiTags, ApiOperation, ApiBody, ApiProperty } from '@nestjs/swagger';
|
||||||
import { SystemWebService } from './systemWeb.service';
|
import { SystemWebService } from './systemWeb.service';
|
||||||
import { TokenAuthGuard } from '../../core/tools/token-auth.guard';
|
import { TokenAuthGuard } from '../../core/tools/token-auth.guard';
|
||||||
|
|
||||||
|
class WebServerDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: '密钥',
|
||||||
|
example: '1111',
|
||||||
|
})
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
@ApiTags('System')
|
@ApiTags('System')
|
||||||
@Controller('system')
|
@Controller('system')
|
||||||
export class SystemWebController {
|
export class SystemWebController {
|
||||||
@ -20,15 +28,8 @@ export class SystemWebController {
|
|||||||
description: '核心执行重启',
|
description: '核心执行重启',
|
||||||
})
|
})
|
||||||
@UseGuards(TokenAuthGuard)
|
@UseGuards(TokenAuthGuard)
|
||||||
@ApiHeader({ name: 'x-token', description: '身份验证token', required: true })
|
@ApiBody({ type: WebServerDto })
|
||||||
@ApiBody({
|
public async systemRestart(@Param('token') token: string): Promise<string> {
|
||||||
description: '系统重启请求',
|
|
||||||
schema: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
public async systemRestart(): Promise<string> {
|
|
||||||
this.systemService.systemRestart();
|
this.systemService.systemRestart();
|
||||||
return '核心正在重启..';
|
return '核心正在重启..';
|
||||||
}
|
}
|
||||||
@ -42,15 +43,8 @@ export class SystemWebController {
|
|||||||
description: '返回上次核心重启的耗时',
|
description: '返回上次核心重启的耗时',
|
||||||
})
|
})
|
||||||
@UseGuards(TokenAuthGuard)
|
@UseGuards(TokenAuthGuard)
|
||||||
@ApiHeader({ name: 'x-token', description: '身份验证token', required: true })
|
@ApiBody({ type: WebServerDto })
|
||||||
@ApiBody({
|
public async getRestartTime(@Param('token') token: string): Promise<string> {
|
||||||
description: '获取重启时间请求',
|
|
||||||
schema: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
public async getRestartTime(): Promise<string> {
|
|
||||||
return await this.systemService.getRestartTime();
|
return await this.systemService.getRestartTime();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
Post,
|
Post,
|
||||||
|
HttpException,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
Logger,
|
Logger,
|
||||||
Inject,
|
Inject,
|
||||||
@ -9,8 +10,7 @@ import {
|
|||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { WordsService } from './words.service';
|
import { WordsService } from './words.service';
|
||||||
import { TokenAuthGuard } from '../../core/tools/token-auth.guard';
|
import { TokenAuthGuard } from '../../core/tools/token-auth.guard';
|
||||||
import { ApiBody, ApiOperation, ApiProperty, ApiHeader } from '@nestjs/swagger';
|
import { ApiBody, ApiOperation, ApiProperty } from '@nestjs/swagger';
|
||||||
import { ErrorUtil } from '../../common/utils/error.util';
|
|
||||||
|
|
||||||
class WordsDto {
|
class WordsDto {
|
||||||
@ApiProperty({ description: '文案类型', example: 'poke' })
|
@ApiProperty({ description: '文案类型', example: 'poke' })
|
||||||
@ -27,7 +27,10 @@ class WordsDto {
|
|||||||
name?: string;
|
name?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class WordsReloadDto extends WordsDto {}
|
class WordsReloadDto extends WordsDto {
|
||||||
|
@ApiProperty({ description: '密钥', example: '1111' })
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
@Controller('words')
|
@Controller('words')
|
||||||
export class WordsController {
|
export class WordsController {
|
||||||
@ -42,27 +45,15 @@ export class WordsController {
|
|||||||
*/
|
*/
|
||||||
@Post('getText')
|
@Post('getText')
|
||||||
@ApiOperation({ summary: '获取随机文案' })
|
@ApiOperation({ summary: '获取随机文案' })
|
||||||
@ApiBody({
|
@ApiBody({ type: WordsDto })
|
||||||
description: '获取文案参数',
|
|
||||||
schema: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
type: { type: 'string', description: '文案类型', example: 'poke' },
|
|
||||||
id: { type: 'string', description: '文案名称', example: 'poke' },
|
|
||||||
name: {
|
|
||||||
type: 'string',
|
|
||||||
description: '可选参数:替换文案中的人名',
|
|
||||||
example: '坤坤',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ['type', 'id'],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
public async getText(@Body() dto: WordsDto) {
|
public async getText(@Body() dto: WordsDto) {
|
||||||
try {
|
try {
|
||||||
const texts = await this.wordsService.loadWord(dto.type, dto.id);
|
const texts = await this.wordsService.loadWord(dto.type, dto.id);
|
||||||
if (!texts || texts.length === 0) {
|
if (!texts || texts.length === 0) {
|
||||||
throw ErrorUtil.createNotFoundError('文案', `${dto.type}/${dto.id}`);
|
throw new HttpException(
|
||||||
|
`文案 ${dto.type}/${dto.id} 不存在或为空..`,
|
||||||
|
HttpStatus.NOT_FOUND,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const randomIndex = Math.floor(Math.random() * texts.length);
|
const randomIndex = Math.floor(Math.random() * texts.length);
|
||||||
@ -73,7 +64,8 @@ export class WordsController {
|
|||||||
|
|
||||||
return text;
|
return text;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw ErrorUtil.handleUnknownError(e, '获取文案失败', 'getText');
|
this.logger.error(`getText 失败: ${e}`);
|
||||||
|
throw new HttpException('服务器错误', HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,33 +75,18 @@ export class WordsController {
|
|||||||
@Post('reloadText')
|
@Post('reloadText')
|
||||||
@ApiOperation({ summary: '重载某条文案' })
|
@ApiOperation({ summary: '重载某条文案' })
|
||||||
@UseGuards(TokenAuthGuard)
|
@UseGuards(TokenAuthGuard)
|
||||||
@ApiHeader({ name: 'x-token', description: '身份验证token', required: true })
|
@ApiBody({ type: WordsReloadDto })
|
||||||
@ApiBody({
|
|
||||||
description: '重载文案参数',
|
|
||||||
schema: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
type: { type: 'string', description: '文案类型', example: 'poke' },
|
|
||||||
id: { type: 'string', description: '文案名称', example: 'poke' },
|
|
||||||
name: {
|
|
||||||
type: 'string',
|
|
||||||
description: '可选参数:替换文案中的人名',
|
|
||||||
example: '坤坤',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ['type', 'id'],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
public async reloadWord(@Body() dto: WordsReloadDto) {
|
public async reloadWord(@Body() dto: WordsReloadDto) {
|
||||||
try {
|
try {
|
||||||
const success = await this.wordsService.reloadWord(dto.type, dto.id);
|
const success = await this.wordsService.reloadWord(dto.type, dto.id);
|
||||||
if (success) {
|
if (success) {
|
||||||
return '成功重载..';
|
return '成功重载..';
|
||||||
} else {
|
} else {
|
||||||
throw ErrorUtil.createBusinessError('重载失败', HttpStatus.BAD_REQUEST);
|
throw new HttpException('重载失败..', HttpStatus.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw ErrorUtil.handleUnknownError(e, '重载文案失败', 'reloadWord');
|
this.logger.error(`reloadWord 失败: ${e}`);
|
||||||
|
throw new HttpException('服务器错误', HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,11 +105,15 @@ export class WordsController {
|
|||||||
try {
|
try {
|
||||||
const names = await this.wordsService.listWordNames(type);
|
const names = await this.wordsService.listWordNames(type);
|
||||||
if (names.length === 0) {
|
if (names.length === 0) {
|
||||||
throw ErrorUtil.createNotFoundError('文案类型', type);
|
throw new HttpException(
|
||||||
|
`类型 ${type} 下没有可用文案或目录不存在..`,
|
||||||
|
HttpStatus.NOT_FOUND,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return names;
|
return names;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw ErrorUtil.handleUnknownError(e, '获取文案列表失败', 'listWords');
|
this.logger.error(`listWords 失败: ${e}`);
|
||||||
|
throw new HttpException('服务器错误', HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -101,7 +101,7 @@ export class WordsService {
|
|||||||
const safeType = this.safePathSegment(type);
|
const safeType = this.safePathSegment(type);
|
||||||
const safeName = this.safePathSegment(name);
|
const safeName = this.safePathSegment(name);
|
||||||
const cacheKey = `${safeType}/${safeName}`;
|
const cacheKey = `${safeType}/${safeName}`;
|
||||||
//this.logger.log(`重载文案: ${cacheKey}..`);
|
this.logger.log(`重载文案: ${cacheKey}..`);
|
||||||
const filePath = path.join(
|
const filePath = path.join(
|
||||||
this.paths.get('words'),
|
this.paths.get('words'),
|
||||||
safeType,
|
safeType,
|
||||||
@ -136,7 +136,7 @@ export class WordsService {
|
|||||||
const names = files
|
const names = files
|
||||||
.filter((f) => f.isFile() && f.name.endsWith('.json'))
|
.filter((f) => f.isFile() && f.name.endsWith('.json'))
|
||||||
.map((f) => f.name.replace(/\.json$/, ''));
|
.map((f) => f.name.replace(/\.json$/, ''));
|
||||||
//this.logger.log(`扫描文案类型 ${safeType} 下的文件: ${names.join(', ')}`);
|
this.logger.log(`扫描文案类型 ${safeType} 下的文件: ${names.join(', ')}`);
|
||||||
return names;
|
return names;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logger.error(`读取文案目录失败: ${safeType}`, e);
|
this.logger.error(`读取文案目录失败: ${safeType}`, e);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user