diff --git a/package.json b/package.json index abcaa9f..7e0973c 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "build": "tsc" }, "dependencies": { + "axios": "^1.8.4", "chalk": "4", "dotenv": "^16.0.0", "express": "^4.18.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 78e7f01..cd111d4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + axios: + specifier: ^1.8.4 + version: 1.8.4 chalk: specifier: '4' version: 4.1.2 @@ -153,6 +156,12 @@ packages: array-flatten@1.1.1: resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.8.4: + resolution: {integrity: sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -205,6 +214,10 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -243,6 +256,10 @@ packages: supports-color: optional: true + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + denque@2.1.0: resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} engines: {node: '>=0.10'} @@ -293,6 +310,10 @@ packages: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} @@ -312,6 +333,19 @@ packages: resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} engines: {node: '>= 0.8'} + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.2: + resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} + engines: {node: '>= 6'} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -359,6 +393,10 @@ packages: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -511,6 +549,9 @@ packages: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + qs@6.13.0: resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} engines: {node: '>=0.6'} @@ -805,6 +846,16 @@ snapshots: array-flatten@1.1.1: {} + asynckit@0.4.0: {} + + axios@1.8.4: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.2 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + balanced-match@1.0.2: {} binary-extensions@2.3.0: {} @@ -874,6 +925,10 @@ snapshots: color-name@1.1.4: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + concat-map@0.0.1: {} content-disposition@0.5.4: @@ -896,6 +951,8 @@ snapshots: dependencies: ms: 2.1.3 + delayed-stream@1.0.0: {} + denque@2.1.0: {} depd@2.0.0: {} @@ -930,6 +987,13 @@ snapshots: dependencies: es-errors: 1.3.0 + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + escape-html@1.0.3: {} etag@1.8.1: {} @@ -986,6 +1050,15 @@ snapshots: transitivePeerDependencies: - supports-color + follow-redirects@1.15.9: {} + + form-data@4.0.2: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + mime-types: 2.1.35 + forwarded@0.2.0: {} fresh@0.5.2: {} @@ -1034,6 +1107,10 @@ snapshots: has-symbols@1.1.0: {} + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + hasown@2.0.2: dependencies: function-bind: 1.1.2 @@ -1156,6 +1233,8 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 + proxy-from-env@1.1.0: {} + qs@6.13.0: dependencies: side-channel: 1.1.0 diff --git a/src/modules/sample/sample.controller.ts b/src/modules/sample/sample.controller.ts index 857a35c..79c8103 100644 --- a/src/modules/sample/sample.controller.ts +++ b/src/modules/sample/sample.controller.ts @@ -34,7 +34,7 @@ class SampleController { if (!name) { return response.error(res, '姓名不能为空!', 400); } - const result = sampleService.generateGreeting(name); + const result = await sampleService.generateGreeting(name); await response.success(res, result); } catch (error) { await response.error(res, '请求失败了..', 500, error); diff --git a/src/modules/trss/keep b/src/modules/trss/keep deleted file mode 100644 index 2fa992c..0000000 --- a/src/modules/trss/keep +++ /dev/null @@ -1 +0,0 @@ -keep diff --git a/src/services/ws/wsServer.ts b/src/services/ws/wsServer.ts index 2cca1d3..14c7d03 100644 --- a/src/services/ws/wsServer.ts +++ b/src/services/ws/wsServer.ts @@ -19,40 +19,54 @@ class WSServer { } private init(): void { - this.wss.on('connection', (socket: AuthenticatedSocket) => { + this.wss.on('connection', (socket: AuthenticatedSocket, req) => { + const ip = req.socket.remoteAddress || 'unknown'; + logger.info(`收到来自 ${ip} 的 WebSocket 连接请求..`); + socket.heartbeat = WsTools.setUpHeartbeat(socket); socket.on('message', async (raw) => { - const msg = WsTools.parseMessage(raw); - if (!msg) return this.handleInvalidMessage(socket); + logger.debug(`Received raw message from ${ip}: ${raw.toString()}`); - await this.routeMessage(socket, msg); + const msg = WsTools.parseMessage(raw); + if (!msg) return this.handleInvalidMessage(socket, ip); + + await this.routeMessage(socket, msg, ip); }); socket.on('close', () => { + logger.info(`ws断开连接 ${ip} (${socket.clientId || 'unauthenticated'})`); this.handleDisconnect(socket); }); + + socket.on('error', (err) => { + logger.error(`WS error from ${ip}: ${err.message}`); + }); }); } - private async handleInvalidMessage(socket: WebSocket) { + private async handleInvalidMessage(socket: WebSocket, ip: string) { + logger.warn(`Invalid message received from ${ip}`); await WsTools.send(socket, { type: 'error', message: 'Invalid message format', }); } - private async routeMessage(socket: AuthenticatedSocket, msg: WSMessage) { + private async routeMessage(socket: AuthenticatedSocket, msg: WSMessage, ip: string) { if (!socket.isAuthed) { if (this.isAuthMessage(msg)) { - await this.handleAuth(socket, msg); + logger.info(`Attempting auth from ${ip} as ${msg.clientId}`); + await this.handleAuth(socket, msg, ip); + } else { + logger.warn(`Received message before auth from ${ip}: ${JSON.stringify(msg)}`); + await this.handleInvalidMessage(socket, ip); } return; } - if (socket.clientId) { - await wsHandler.handle(socket, socket.clientId, msg); - } + logger.debug(`Routing message from ${socket.clientId}: ${JSON.stringify(msg)}`); + await wsHandler.handle(socket, socket.clientId!, msg); } private isAuthMessage(msg: WSMessage): msg is AuthMessage { @@ -63,20 +77,25 @@ class WSServer { ); } - private async handleAuth(socket: AuthenticatedSocket, msg: AuthMessage) { + private async handleAuth(socket: AuthenticatedSocket, msg: AuthMessage, ip: string) { if (msg.secret === this.secret) { socket.isAuthed = true; socket.clientId = msg.clientId; wsClientManager.add(msg.clientId, socket); + logger.info(`Auth success from ${ip}, clientId: ${msg.clientId}`); await WsTools.send(socket, { type: 'auth', success: true }); } else { + logger.warn(`Auth failed from ${ip} (invalid secret), clientId: ${msg.clientId}`); await WsTools.send(socket, { type: 'auth', success: false }); } } private handleDisconnect(socket: AuthenticatedSocket) { if (socket.heartbeat) clearInterval(socket.heartbeat); - if (socket.clientId) wsClientManager.remove(socket.clientId); + if (socket.clientId) { + wsClientManager.remove(socket.clientId); + logger.info(`Removed client ${socket.clientId} from manager`); + } } } diff --git a/src/test/wsTestClient.ts b/src/test/wsTestClient.ts new file mode 100644 index 0000000..dd05db1 --- /dev/null +++ b/src/test/wsTestClient.ts @@ -0,0 +1,69 @@ +import WebSocket from 'ws'; +import axios from 'axios'; + +const WS_URL = 'ws://127.0.0.1:3001'; +const WS_SECRET = '114514'; +const CLIENT_ID = 'test'; + +function createWebSocketClient() { + const socket = new WebSocket(WS_URL); + + socket.on('open', () => { + console.log('[WS] Connected to server'); + + const authPayload = { + type: 'auth', + secret: WS_SECRET, + clientId: CLIENT_ID, + }; + socket.send(JSON.stringify(authPayload)); + }); + + socket.on('message', (raw) => { + const msg = JSON.parse(raw.toString()); + console.log('[WS] Message from server:', msg); + + if (msg.type === 'auth' && msg.success === true) { + socket.send(JSON.stringify({ type: 'test' })); + } + }); + + socket.on('close', () => { + console.log('[WS] Connection closed'); + }); + + socket.on('error', (err) => { + console.error('[WS] Error:', err); + }); +} + +async function testGetAPI() { + try { + const response = await axios.get('http://localhost:3000/api/sample/hello'); + console.log('[HTTP][GET] Response:', response.data); + } catch (err) { + console.error('[HTTP][GET] Error:', err); + } +} + +async function testPostAPI() { + try { + const response = await axios.post('http://localhost:3000/api/sample/greet', { + name: 'Jerry', + }); + console.log('[HTTP][POST] Response:', response.data); + } catch (err) { + console.error('[HTTP][POST] Error:', err); + } +} + +async function main() { + createWebSocketClient(); + + setTimeout(() => { + testGetAPI(); + testPostAPI(); + }, 1000); +} + +main();