forked from xiangwang25/whale-town-end-v2
227 lines
7.6 KiB
TypeScript
227 lines
7.6 KiB
TypeScript
/**
|
||
* 聊天 HTTP 控制器
|
||
*
|
||
* 功能描述:
|
||
* - 处理聊天相关的 REST API 请求
|
||
* - 只做协议转换,不包含业务逻辑
|
||
* - 提供聊天历史查询和系统状态接口
|
||
*
|
||
* 架构层级:Gateway Layer(网关层)
|
||
*
|
||
* 最近修改:
|
||
* - 2026-01-14: 代码规范优化 - 处理未使用的参数 (修改者: moyin)
|
||
* - 2026-01-14: 代码规范优化 - 完善注释规范 (修改者: moyin)
|
||
*
|
||
* @author moyin
|
||
* @version 1.0.2
|
||
* @since 2026-01-14
|
||
* @lastModified 2026-01-14
|
||
*/
|
||
|
||
import {
|
||
Controller,
|
||
Post,
|
||
Get,
|
||
Body,
|
||
Query,
|
||
UseGuards,
|
||
HttpStatus,
|
||
HttpException,
|
||
Logger,
|
||
Headers,
|
||
} from '@nestjs/common';
|
||
import {
|
||
ApiTags,
|
||
ApiOperation,
|
||
ApiResponse,
|
||
ApiBearerAuth,
|
||
ApiQuery,
|
||
} from '@nestjs/swagger';
|
||
import { JwtAuthGuard } from '../auth/jwt_auth.guard';
|
||
import { ChatService } from '../../business/chat/chat.service';
|
||
import { ChatWebSocketGateway } from './chat.gateway';
|
||
import { WorldNpcService } from '../../business/world_npc/world_npc.service';
|
||
import { SendChatMessageDto, GetChatHistoryDto } from './chat.dto';
|
||
import {
|
||
ChatMessageResponseDto,
|
||
ChatHistoryResponseDto,
|
||
SystemStatusResponseDto,
|
||
} from './chat_response.dto';
|
||
|
||
@ApiTags('chat')
|
||
@Controller('chat')
|
||
/**
|
||
* 聊天 HTTP 控制器类
|
||
*
|
||
* 职责:
|
||
* - 处理聊天相关的 REST API 请求
|
||
* - 提供聊天历史查询接口
|
||
* - 提供系统状态监控接口
|
||
*
|
||
* 主要方法:
|
||
* - getChatHistory() - 获取聊天历史记录
|
||
* - getSystemStatus() - 获取系统状态
|
||
* - getWebSocketInfo() - 获取 WebSocket 连接信息
|
||
*/
|
||
export class ChatController {
|
||
private readonly logger = new Logger(ChatController.name);
|
||
|
||
constructor(
|
||
private readonly chatService: ChatService,
|
||
private readonly websocketGateway: ChatWebSocketGateway,
|
||
private readonly worldNpcService: WorldNpcService,
|
||
) {}
|
||
|
||
@Get('world-npcs/status')
|
||
@ApiOperation({ summary: '查看AI小镇NPC的当前计划、位置与动作状态' })
|
||
getWorldNpcStatus() {
|
||
return this.worldNpcService.getTownStatus();
|
||
}
|
||
|
||
@Post('world-npcs/test-time')
|
||
@ApiOperation({ summary: '开发环境设置AI小镇测试时间' })
|
||
async setWorldNpcTestTime(
|
||
@Body() body: { timestamp?: number | string | null },
|
||
@Headers('x-world-npc-test-token') token?: string,
|
||
) {
|
||
const enabled = process.env.NODE_ENV !== 'production'
|
||
&& process.env.WORLD_NPC_TEST_CONTROLS === 'enabled';
|
||
const expectedToken = String(process.env.WORLD_NPC_TEST_CONTROL_TOKEN || '').trim();
|
||
if (!enabled || !expectedToken || token !== expectedToken) {
|
||
throw new HttpException('测试时间控制未启用', HttpStatus.FORBIDDEN);
|
||
}
|
||
if (body.timestamp === null || body.timestamp === undefined || body.timestamp === '') {
|
||
return this.worldNpcService.setTownTimeForTesting(undefined);
|
||
}
|
||
const numeric = typeof body.timestamp === 'number'
|
||
? body.timestamp
|
||
: Date.parse(String(body.timestamp));
|
||
if (!Number.isFinite(numeric)) throw new HttpException('timestamp无效', HttpStatus.BAD_REQUEST);
|
||
return this.worldNpcService.setTownTimeForTesting(numeric);
|
||
}
|
||
|
||
/**
|
||
* 发送聊天消息(REST API 方式)
|
||
*
|
||
* @param dto 发送消息请求参数
|
||
* @returns 消息发送响应
|
||
* @throws HttpException 聊天消息需要通过 WebSocket 发送
|
||
*/
|
||
@Post('send')
|
||
@UseGuards(JwtAuthGuard)
|
||
@ApiBearerAuth('JWT-auth')
|
||
@ApiOperation({
|
||
summary: '发送聊天消息',
|
||
description: '通过 REST API 发送聊天消息。推荐使用 WebSocket 接口以获得更好的实时性。'
|
||
})
|
||
@ApiResponse({ status: 200, description: '消息发送成功', type: ChatMessageResponseDto })
|
||
@ApiResponse({ status: 400, description: '请求参数错误' })
|
||
@ApiResponse({ status: 401, description: '未授权访问' })
|
||
async sendMessage(@Body() _dto: SendChatMessageDto): Promise<ChatMessageResponseDto> {
|
||
this.logger.log('收到REST API聊天消息发送请求');
|
||
|
||
// REST API 没有 WebSocket 连接,提示使用 WebSocket
|
||
throw new HttpException(
|
||
'聊天消息发送需要通过 WebSocket 连接。请使用 WebSocket 接口:wss://whaletown.novamailio.com/game',
|
||
HttpStatus.BAD_REQUEST,
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 获取聊天历史记录
|
||
*
|
||
* @param query 查询参数(mapId, limit, offset)
|
||
* @returns 聊天历史响应
|
||
* @throws HttpException 获取失败时抛出异常
|
||
*/
|
||
@Get('history')
|
||
@UseGuards(JwtAuthGuard)
|
||
@ApiBearerAuth('JWT-auth')
|
||
@ApiOperation({ summary: '获取聊天历史记录' })
|
||
@ApiQuery({ name: 'mapId', required: false, description: '地图ID' })
|
||
@ApiQuery({ name: 'limit', required: false, description: '消息数量限制' })
|
||
@ApiQuery({ name: 'offset', required: false, description: '偏移量' })
|
||
@ApiResponse({ status: 200, description: '获取成功', type: ChatHistoryResponseDto })
|
||
async getChatHistory(@Query() query: GetChatHistoryDto): Promise<ChatHistoryResponseDto> {
|
||
this.logger.log('获取聊天历史记录', { mapId: query.mapId });
|
||
|
||
try {
|
||
const result = await this.chatService.getChatHistory(query);
|
||
return result;
|
||
} catch (error) {
|
||
this.logger.error('获取聊天历史失败', error);
|
||
throw new HttpException('获取聊天历史失败', HttpStatus.INTERNAL_SERVER_ERROR);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取系统状态
|
||
*
|
||
* @returns 系统状态响应(WebSocket连接数、Zulip状态、内存使用等)
|
||
* @throws HttpException 获取失败时抛出异常
|
||
*/
|
||
@Get('status')
|
||
@ApiOperation({ summary: '获取聊天系统状态' })
|
||
@ApiResponse({ status: 200, description: '获取成功', type: SystemStatusResponseDto })
|
||
async getSystemStatus(): Promise<SystemStatusResponseDto> {
|
||
try {
|
||
const totalConnections = this.websocketGateway.getConnectionCount();
|
||
const authenticatedConnections = this.websocketGateway.getAuthenticatedConnectionCount();
|
||
const mapPlayerCounts = this.websocketGateway.getMapPlayerCounts();
|
||
|
||
const memoryUsage = process.memoryUsage();
|
||
const memoryUsedMB = (memoryUsage.heapUsed / 1024 / 1024).toFixed(1);
|
||
const memoryTotalMB = (memoryUsage.heapTotal / 1024 / 1024).toFixed(1);
|
||
const memoryPercentage = (memoryUsage.heapUsed / memoryUsage.heapTotal) * 100;
|
||
|
||
return {
|
||
websocket: {
|
||
totalConnections,
|
||
authenticatedConnections,
|
||
activeSessions: authenticatedConnections,
|
||
mapPlayerCounts,
|
||
},
|
||
zulip: {
|
||
serverConnected: true,
|
||
serverVersion: '11.4',
|
||
botAccountActive: true,
|
||
availableStreams: 12,
|
||
gameStreams: ['Whale Port', 'Pumpkin Valley', 'Novice Village'],
|
||
recentMessageCount: 156,
|
||
},
|
||
uptime: Math.floor(process.uptime()),
|
||
memory: {
|
||
used: `${memoryUsedMB} MB`,
|
||
total: `${memoryTotalMB} MB`,
|
||
percentage: Math.round(memoryPercentage * 100) / 100,
|
||
},
|
||
};
|
||
} catch (error) {
|
||
this.logger.error('获取系统状态失败', error);
|
||
throw new HttpException('获取系统状态失败', HttpStatus.INTERNAL_SERVER_ERROR);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取 WebSocket 连接信息
|
||
*
|
||
* @returns WebSocket 连接配置信息
|
||
*/
|
||
@Get('websocket/info')
|
||
@ApiOperation({ summary: '获取 WebSocket 连接信息' })
|
||
async getWebSocketInfo() {
|
||
return {
|
||
websocketUrl: 'wss://whaletown.novamailio.com/game',
|
||
protocol: 'native-websocket',
|
||
path: '/game',
|
||
supportedEvents: ['login', 'chat', 'position'],
|
||
supportedResponses: [
|
||
'connected', 'login_success', 'login_error',
|
||
'chat_sent', 'chat_error', 'chat_render', 'error'
|
||
],
|
||
authRequired: true,
|
||
tokenType: 'JWT',
|
||
};
|
||
}
|
||
}
|