diff --git a/src/business/zulip/controllers/websocket-docs.controller.ts b/src/business/zulip/controllers/websocket-docs.controller.ts new file mode 100644 index 0000000..9694ef7 --- /dev/null +++ b/src/business/zulip/controllers/websocket-docs.controller.ts @@ -0,0 +1,421 @@ +/** + * WebSocket API 文档控制器 + * + * 功能描述: + * - 提供 WebSocket API 的详细文档 + * - 展示消息格式和事件类型 + * - 提供连接示例和测试工具 + * + * @author angjustinl + * @version 1.0.0 + * @since 2025-01-07 + */ + +import { Controller, Get } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; + +@ApiTags('chat') +@Controller('websocket') +export class WebSocketDocsController { + + /** + * 获取 WebSocket API 文档 + */ + @Get('docs') + @ApiOperation({ + summary: 'WebSocket API 文档', + description: '获取 WebSocket 连接和消息格式的详细文档' + }) + @ApiResponse({ + status: 200, + description: 'WebSocket API 文档', + schema: { + type: 'object', + properties: { + connection: { + type: 'object', + properties: { + url: { + type: 'string', + example: 'ws://localhost:3000/game', + description: 'WebSocket 连接地址' + }, + namespace: { + type: 'string', + example: '/game', + description: 'Socket.IO 命名空间' + }, + transports: { + type: 'array', + items: { type: 'string' }, + example: ['websocket', 'polling'], + description: '支持的传输协议' + } + } + }, + authentication: { + type: 'object', + properties: { + required: { + type: 'boolean', + example: true, + description: '是否需要认证' + }, + method: { + type: 'string', + example: 'JWT Token', + description: '认证方式' + }, + tokenFormat: { + type: 'object', + description: 'JWT Token 格式要求' + } + } + }, + events: { + type: 'object', + description: '支持的事件和消息格式' + } + } + } + }) + getWebSocketDocs() { + return { + connection: { + url: 'ws://localhost:3000/game', + namespace: '/game', + transports: ['websocket', 'polling'], + options: { + timeout: 20000, + forceNew: true, + reconnection: true, + reconnectionAttempts: 3, + reconnectionDelay: 1000 + } + }, + authentication: { + required: true, + method: 'JWT Token', + tokenFormat: { + issuer: 'whale-town', + audience: 'whale-town-users', + type: 'access', + requiredFields: ['sub', 'username', 'email', 'role'], + example: { + sub: 'user_123', + username: 'player_name', + email: 'user@example.com', + role: 'user', + type: 'access', + aud: 'whale-town-users', + iss: 'whale-town', + iat: 1767768599, + exp: 1768373399 + } + } + }, + events: { + clientToServer: { + login: { + description: '用户登录', + format: { + type: 'login', + token: 'JWT_TOKEN_HERE' + }, + example: { + type: 'login', + token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...' + }, + responses: ['login_success', 'login_error'] + }, + chat: { + description: '发送聊天消息', + format: { + t: 'chat', + content: 'string', + scope: 'local | global' + }, + example: { + t: 'chat', + content: '大家好!我刚进入游戏', + scope: 'local' + }, + responses: ['chat_sent', 'chat_error'] + }, + position_update: { + description: '更新玩家位置', + format: { + t: 'position', + x: 'number', + y: 'number', + mapId: 'string' + }, + example: { + t: 'position', + x: 150, + y: 400, + mapId: 'whale_port' + }, + responses: [] + } + }, + serverToClient: { + login_success: { + description: '登录成功响应', + format: { + t: 'login_success', + sessionId: 'string', + userId: 'string', + username: 'string', + currentMap: 'string' + }, + example: { + t: 'login_success', + sessionId: '89aff162-52d9-484e-9a35-036ba63a2280', + userId: 'user_123', + username: 'Player_123', + currentMap: 'whale_port' + } + }, + login_error: { + description: '登录失败响应', + format: { + t: 'login_error', + message: 'string' + }, + example: { + t: 'login_error', + message: 'Token验证失败' + } + }, + chat_sent: { + description: '消息发送成功确认', + format: { + t: 'chat_sent', + messageId: 'number', + message: 'string' + }, + example: { + t: 'chat_sent', + messageId: 137, + message: '消息发送成功' + } + }, + chat_error: { + description: '消息发送失败', + format: { + t: 'chat_error', + message: 'string' + }, + example: { + t: 'chat_error', + message: '消息内容不能为空' + } + }, + chat_render: { + description: '接收到聊天消息', + format: { + t: 'chat_render', + from: 'string', + txt: 'string', + bubble: 'boolean' + }, + example: { + t: 'chat_render', + from: 'Player_456', + txt: '欢迎新玩家!', + bubble: true + } + } + } + }, + maps: { + whale_port: { + name: 'Whale Port', + displayName: '鲸鱼港', + zulipStream: 'Whale Port', + description: '游戏的主要港口区域' + }, + pumpkin_valley: { + name: 'Pumpkin Valley', + displayName: '南瓜谷', + zulipStream: 'Pumpkin Valley', + description: '充满南瓜的神秘山谷' + }, + novice_village: { + name: 'Novice Village', + displayName: '新手村', + zulipStream: 'Novice Village', + description: '新玩家的起始区域' + } + }, + examples: { + javascript: { + connection: ` +// 使用 Socket.IO 客户端连接 +const io = require('socket.io-client'); + +const socket = io('ws://localhost:3000/game', { + transports: ['websocket', 'polling'], + timeout: 20000, + forceNew: true, + reconnection: true, + reconnectionAttempts: 3, + reconnectionDelay: 1000 +}); + +// 连接成功 +socket.on('connect', () => { + console.log('连接成功:', socket.id); + + // 发送登录消息 + socket.emit('login', { + type: 'login', + token: 'YOUR_JWT_TOKEN_HERE' + }); +}); + +// 登录成功 +socket.on('login_success', (data) => { + console.log('登录成功:', data); + + // 发送聊天消息 + socket.emit('chat', { + t: 'chat', + content: '大家好!', + scope: 'local' + }); +}); + +// 接收聊天消息 +socket.on('chat_render', (data) => { + console.log('收到消息:', data.from, '说:', data.txt); +}); + `, + godot: ` +# Godot WebSocket 客户端示例 +extends Node + +var socket = WebSocketClient.new() +var url = "ws://localhost:3000/game" + +func _ready(): + socket.connect("connection_closed", self, "_closed") + socket.connect("connection_error", self, "_error") + socket.connect("connection_established", self, "_connected") + socket.connect("data_received", self, "_on_data") + + var err = socket.connect_to_url(url) + if err != OK: + print("连接失败") + +func _connected(protocol): + print("WebSocket 连接成功") + # 发送登录消息 + var login_msg = { + "type": "login", + "token": "YOUR_JWT_TOKEN_HERE" + } + socket.get_peer(1).put_packet(JSON.print(login_msg).to_utf8()) + +func _on_data(): + var packet = socket.get_peer(1).get_packet() + var message = JSON.parse(packet.get_string_from_utf8()) + print("收到消息: ", message.result) + ` + } + }, + troubleshooting: { + commonIssues: [ + { + issue: 'Token验证失败', + solution: '确保JWT Token包含正确的issuer、audience和type字段' + }, + { + issue: '连接超时', + solution: '检查服务器是否运行,防火墙设置是否正确' + }, + { + issue: '消息发送失败', + solution: '确保已经成功登录,消息内容不为空' + } + ], + testTools: [ + { + name: 'WebSocket King', + url: 'https://websocketking.com/', + description: '在线WebSocket测试工具' + }, + { + name: 'Postman', + description: 'Postman也支持WebSocket连接测试' + } + ] + } + }; + } + + /** + * 获取消息格式示例 + */ + @Get('message-examples') + @ApiOperation({ + summary: '消息格式示例', + description: '获取各种 WebSocket 消息的格式示例' + }) + @ApiResponse({ + status: 200, + description: '消息格式示例', + }) + getMessageExamples() { + return { + login: { + request: { + type: 'login', + token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0X3VzZXJfMTIzIiwidXNlcm5hbWUiOiJ0ZXN0X3VzZXIiLCJlbWFpbCI6InRlc3RfdXNlckBleGFtcGxlLmNvbSIsInJvbGUiOiJ1c2VyIiwidHlwZSI6ImFjY2VzcyIsImF1ZCI6IndoYWxlLXRvd24tdXNlcnMiLCJpc3MiOiJ3aGFsZS10b3duIiwiaWF0IjoxNzY3NzY4NTk5LCJleHAiOjE3NjgzNzMzOTl9.Mq3YccSV_pMKxIAbeNRAUws1j7doqFqvlSv4Z9DhGjI' + }, + successResponse: { + t: 'login_success', + sessionId: '89aff162-52d9-484e-9a35-036ba63a2280', + userId: 'test_user_123', + username: 'test_user', + currentMap: 'whale_port' + }, + errorResponse: { + t: 'login_error', + message: 'Token验证失败' + } + }, + chat: { + request: { + t: 'chat', + content: '大家好!我刚进入游戏', + scope: 'local' + }, + successResponse: { + t: 'chat_sent', + messageId: 137, + message: '消息发送成功' + }, + errorResponse: { + t: 'chat_error', + message: '消息内容不能为空' + }, + incomingMessage: { + t: 'chat_render', + from: 'Player_456', + txt: '欢迎新玩家!', + bubble: true + } + }, + position: { + request: { + t: 'position', + x: 150, + y: 400, + mapId: 'pumpkin_valley' + } + } + }; + } +} \ No newline at end of file