From 26b95927def8bd13fe391f389a59b3ecb2618ac9 Mon Sep 17 00:00:00 2001 From: Shijia Huang Date: Wed, 25 Mar 2026 18:34:19 +1100 Subject: [PATCH 1/3] fix the error handling for join room --- .../packageA/pages/waiting-room/index.ts | 45 ++++++++++++------- .../services/room-websocket-service.ts | 24 +++++++++- 2 files changed, 52 insertions(+), 17 deletions(-) diff --git a/miniprogram/packageA/pages/waiting-room/index.ts b/miniprogram/packageA/pages/waiting-room/index.ts index 91f93cc..627fc1a 100644 --- a/miniprogram/packageA/pages/waiting-room/index.ts +++ b/miniprogram/packageA/pages/waiting-room/index.ts @@ -11,6 +11,8 @@ import { roomService } from '../../../services/room-service'; import { roomWebSocketService } from '../../../services/room-websocket-service'; import { wsManager } from '../../../services/websocket-manager'; import type { IJoinAckData } from '../../../types/room-websocket'; +import type { IErrorData } from '../../../types/websocket-common'; +import { EWSErrorCode } from '../../../types/websocket-common'; import { logger } from '../../../utils/logger'; /** @@ -196,9 +198,14 @@ Page({ }, }); - roomWebSocketService.initialize((data: IJoinAckData) => { - this.handleRoomJoined(data); - }); + roomWebSocketService.initialize( + (data: IJoinAckData) => { + this.handleRoomJoined(data); + }, + (error: IErrorData) => { + this.handleJoinError(error); + } + ); }, /** @@ -555,18 +562,6 @@ Page({ roomCodeInput, nicknameService.getNickName() ); - - // 设置超时处理 - setTimeout(() => { - if (!this.data.currentRoom) { - void wx.hideLoading(); - this.isJoiningRoom = false; - this.setData({ - errorType: 'not_found', - errorMessage: '加入超时,请重试', - }); - } - }, 5000); }, /** @@ -652,6 +647,26 @@ Page({ } }, + /** + * 处理房间加入失败 + */ + handleJoinError(error: IErrorData): void { + void wx.hideLoading(); + this.isJoiningRoom = false; + + const errorMap: Partial> = { + [EWSErrorCode.RoomNotFound]: 'not_found', + [EWSErrorCode.RoomFull]: 'full', + [EWSErrorCode.RoomClosed]: 'started', + }; + + const errorType: ErrorType = errorMap[error.code] ?? 'not_found'; + this.setData({ + errorType, + errorMessage: ERROR_MESSAGES[errorType], + }); + }, + /** * 启动等待文案轮播 */ diff --git a/miniprogram/services/room-websocket-service.ts b/miniprogram/services/room-websocket-service.ts index 6254dc9..7a06a29 100644 --- a/miniprogram/services/room-websocket-service.ts +++ b/miniprogram/services/room-websocket-service.ts @@ -13,23 +13,30 @@ import type { IJoinAckMessage, IJoinAckData, } from '../types/room-websocket'; +import type { IErrorMessage, IErrorData } from '../types/websocket-common'; import { EWSMessageType } from '../types/websocket-common'; import { logger } from '../utils/logger'; import { wsManager } from './websocket-manager'; type JoinAckHandler = (data: IJoinAckData) => void; +type JoinErrorHandler = (error: IErrorData) => void; class RoomWebSocketService { private currentRoomCode: string | null = null; private currentNickname: string | null = null; private joinAckHandler: JoinAckHandler | null = null; + private joinErrorHandler: JoinErrorHandler | null = null; /** * Initialize WebSocket connection for room operations */ - initialize(onJoinAck: JoinAckHandler): void { + initialize( + onJoinAck: JoinAckHandler, + onJoinError?: JoinErrorHandler + ): void { this.joinAckHandler = onJoinAck; + this.joinErrorHandler = onJoinError ?? null; wsManager.updateCallbacks({ onMessage: (data: string) => { @@ -75,10 +82,12 @@ class RoomWebSocketService { */ private handleMessage(data: string): void { try { - const message = JSON.parse(data) as IJoinAckMessage; + const message = JSON.parse(data) as IJoinAckMessage | IErrorMessage; if (message.type === EWSMessageType.JoinAck) { this.handleJoinAck(message); + } else if (message.type === EWSMessageType.Error) { + this.handleError(message); } // Other message types will be handled by other services } catch (error) { @@ -97,6 +106,17 @@ class RoomWebSocketService { } } + /** + * Handle ERROR message + */ + private handleError(message: IErrorMessage): void { + logger.error('RoomWS', 'ERROR received:', message.data); + + if (this.joinErrorHandler) { + this.joinErrorHandler(message.data); + } + } + /** * Clear room state */ From e0951f1dc109b8bc5b538df0e8155688699a8c42 Mon Sep 17 00:00:00 2001 From: Shijia Huang Date: Wed, 25 Mar 2026 19:30:43 +1100 Subject: [PATCH 2/3] refactor logger --- .claude/settings.local.json | 3 +- .github/workflows/deploy.yml | 2 + backend/package-lock.json | 266 ++++++++++++++++++ backend/package.json | 3 +- backend/src/app.ts | 4 + .../controllers/llm-judgement.controller.ts | 14 + backend/src/controllers/room-controller.ts | 6 + backend/src/controllers/tencent-controller.ts | 4 + backend/src/controllers/ws-controller.ts | 37 ++- backend/src/middleware/requestLogger.ts | 28 ++ .../core/verdict-orchestrator.service.ts | 9 + backend/src/utils/logger.ts | 188 ++++++++++++- backend/src/ws.ts | 1 + 13 files changed, 547 insertions(+), 18 deletions(-) create mode 100644 backend/src/middleware/requestLogger.ts diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 6ab58fd..60a2e4a 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -13,7 +13,8 @@ "Bash(npx tsc:*)", "Bash(findstr:*)", "Bash(npx prettier:*)", - "Bash(cd:*)" + "Bash(cd:*)", + "Bash(npm install:*)" ] } } diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 7b88939..5395978 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -34,6 +34,8 @@ jobs: - name: Write .env file run: | cat > /tmp/.env << EOF + NODE_ENV=${{ github.event.inputs.environment == 'release' && 'production' || 'development' }} + LOG_LEVEL=${{ github.event.inputs.environment == 'release' && 'info' || 'debug' }} AI_API_KEY=${{ secrets.AI_API_KEY }} AI_MODEL=${{ vars.AI_MODEL }} AI_BASE_URL=${{ vars.AI_BASE_URL }} diff --git a/backend/package-lock.json b/backend/package-lock.json index 01df057..3639d92 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -13,6 +13,7 @@ "express": "^5.2.1", "openai": "^4.77.0", "tencentcloud-sdk-nodejs-sts": "^4.1.100", + "winston": "^3.19.0", "ws": "^8.19.0", "zod": "^3.24.0" }, @@ -31,6 +32,15 @@ "typescript": "^5.9.3" } }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -43,6 +53,17 @@ "node": ">=12" } }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", + "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", + "license": "MIT", + "dependencies": { + "@so-ric/colorspace": "^1.1.6", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", @@ -695,6 +716,16 @@ "url": "https://opencollective.com/pkgr" } }, + "node_modules/@so-ric/colorspace": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", + "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", + "license": "MIT", + "dependencies": { + "color": "^5.0.2", + "text-hex": "1.0.x" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", @@ -866,6 +897,12 @@ "@types/node": "*" } }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -1215,6 +1252,12 @@ "node": ">=8" } }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1340,6 +1383,19 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/color": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz", + "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", + "license": "MIT", + "dependencies": { + "color-convert": "^3.1.3", + "color-string": "^2.1.3" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1358,6 +1414,48 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/color-string": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", + "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-string/node_modules/color-name": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", + "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz", + "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=14.6" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", + "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -1533,6 +1631,12 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -1971,6 +2075,12 @@ "reusify": "^1.0.4" } }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -2051,6 +2161,12 @@ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, "node_modules/form-data": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.4.tgz", @@ -2598,6 +2714,12 @@ "json-buffer": "3.0.1" } }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -2632,6 +2754,23 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -2814,6 +2953,15 @@ "wrappy": "1" } }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, "node_modules/openai": { "version": "4.104.0", "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz", @@ -3097,6 +3245,20 @@ "node": ">= 0.10" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3180,6 +3342,35 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -3343,6 +3534,15 @@ "node": ">=8" } }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -3351,6 +3551,15 @@ "node": ">= 0.8" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -3435,6 +3644,12 @@ "node": ">=10" } }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -3467,6 +3682,15 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "license": "MIT" }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/ts-api-utils": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", @@ -3621,6 +3845,12 @@ "punycode": "^2.1.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/uuid": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", @@ -3688,6 +3918,42 @@ "node": ">= 8" } }, + "node_modules/winston": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", + "integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.8", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/backend/package.json b/backend/package.json index 4056e29..a476b52 100644 --- a/backend/package.json +++ b/backend/package.json @@ -5,7 +5,7 @@ "scripts": { "dev": "tsx watch src/index.ts", "build": "tsc", - "start": "node dist/index.js", + "start": "mkdir -p logs && node dist/index.js", "ws:test": "ts-node scripts/ws-test.ts", "test:llm": "ts-node scripts/test-llm-e2e.ts", "lint": "eslint . --ext .ts", @@ -22,6 +22,7 @@ "express": "^5.2.1", "openai": "^4.77.0", "tencentcloud-sdk-nodejs-sts": "^4.1.100", + "winston": "^3.19.0", "ws": "^8.19.0", "zod": "^3.24.0" }, diff --git a/backend/src/app.ts b/backend/src/app.ts index a066e9b..be31731 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -13,12 +13,16 @@ import roomRoutes from './routes/room-routes'; import llmJudgementRoutes from './routes/llm-judgement.routes'; import tencentRoutes from './routes/tencent-routes'; import verdictRoutes from './routes/verdict-routes'; +import { requestLogger } from './middleware/requestLogger'; const app = express(); // Parse JSON request bodies app.use(express.json()); +// Log all HTTP requests +app.use(requestLogger); + /** * Health check endpoint */ diff --git a/backend/src/controllers/llm-judgement.controller.ts b/backend/src/controllers/llm-judgement.controller.ts index dec6a92..d17ba1d 100644 --- a/backend/src/controllers/llm-judgement.controller.ts +++ b/backend/src/controllers/llm-judgement.controller.ts @@ -33,6 +33,8 @@ export class LlmJudgementController { * - 502: LLM call failed */ static async createJudgment(req: Request, res: Response): Promise { + let llmRoomId: string | undefined; + let llmStartMs = 0; try { // Validate path params const paramResult = RoomIdParamSchema.safeParse(req.params); @@ -68,6 +70,7 @@ export class LlmJudgementController { const { roomId } = paramResult.data; const { idempotencyKey } = bodyResult.data; + llmRoomId = roomId; // Look up room to get participant identity and accumulated speech const room = roomManager.getRoomById(roomId); @@ -87,6 +90,8 @@ export class LlmJudgementController { const texts = room.speechState?.texts ?? {}; // Call service (synchronous LLM call) + llmStartMs = Date.now(); + logger.info('llm.judgment.start', { roomId }); const result: IJudgmentResponse = await llmJudgementService.createJudgment(roomId, { player1: { @@ -110,6 +115,10 @@ export class LlmJudgementController { success: true, data: result, }; + logger.info('llm.judgment.ok', { + roomId: llmRoomId, + durationMs: Date.now() - llmStartMs, + }); res.status(200).json(response); } catch (error: unknown) { logger.error( @@ -117,6 +126,11 @@ export class LlmJudgementController { 'createJudgment failed:', error instanceof Error ? error.message : String(error) ); + logger.error('llm.judgment.failed', { + roomId: llmRoomId, + durationMs: Date.now() - llmStartMs, + error: error instanceof Error ? error.message : String(error), + }); const response: IBaseResponse = { success: false, diff --git a/backend/src/controllers/room-controller.ts b/backend/src/controllers/room-controller.ts index 3cbd5eb..b6602de 100644 --- a/backend/src/controllers/room-controller.ts +++ b/backend/src/controllers/room-controller.ts @@ -47,6 +47,12 @@ export class RoomController { }; res.status(201).json(response); + logger.info('room.created', { + roomId: room.roomId, + roomCode: room.roomCode, + hostUserId: room.hostUserId, + createdAt: room.createdAt, + }); } catch (error: unknown) { logger.error('RoomController', 'Room creation failed:', error); diff --git a/backend/src/controllers/tencent-controller.ts b/backend/src/controllers/tencent-controller.ts index c84392f..5363052 100644 --- a/backend/src/controllers/tencent-controller.ts +++ b/backend/src/controllers/tencent-controller.ts @@ -37,9 +37,13 @@ export class TencentController { try { const response = await this._getSTSTokenWithCache(); res.status(200).json(response); + logger.info('tencent.credentials.ok', {}); return; } catch (error: unknown) { logger.error('TencentController', 'Get STS token failed:', error); + logger.error('tencent.credentials.failed', { + error: error instanceof Error ? error.message : String(error), + }); const response: IBaseResponse = { success: false, error: { diff --git a/backend/src/controllers/ws-controller.ts b/backend/src/controllers/ws-controller.ts index 036509a..0110ba9 100644 --- a/backend/src/controllers/ws-controller.ts +++ b/backend/src/controllers/ws-controller.ts @@ -67,9 +67,11 @@ export class WebSocketController { * Routes message to appropriate handler based on type */ static handleMessage(connectionId: string, data: RawData): void { + let messageType = 'unknown'; try { const messageText = WebSocketController.rawDataToText(data); const message = JSON.parse(messageText) as IWSMessage; + messageType = String(message.type); // Route message to appropriate handler switch (message.type) { @@ -144,6 +146,11 @@ export class WebSocketController { break; default: + logger.warn('ws.validation_failed', { + connectionId, + type: messageType, + error: `Unknown message type: ${message.type}`, + }); WebSocketController.sendError( connectionId, EWSErrorCode.InvalidPayload, @@ -151,7 +158,12 @@ export class WebSocketController { ); } } catch (error) { - logger.error('WSController', 'Message handling error:', error); + logger.error('ws.internal_error', { + connectionId, + type: messageType, + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + }); WebSocketController.sendError( connectionId, EWSErrorCode.InternalError, @@ -197,6 +209,13 @@ export class WebSocketController { }); } + logger.info('ws.join_room', { + roomId: result.room.roomId, + userId: result.userId, + nickname: message.data.nickname, + roomStatus: result.room.status, + }); + // If room is ready (2 players), initialize drum game and wait for // frontend to send DRUM_START_REQUEST before launching if (result.room.status === ERoomStatus.Ready) { @@ -589,6 +608,10 @@ export class WebSocketController { 'WSController', `Speech turn end for user ${result.userId} in room ${result.roomId}` ); + logger.info('ws.speech_turn_end', { + roomId: result.roomId, + userId: result.userId, + }); if (result.bothFinished) { // Both players finished, broadcast CHAT_COMPLETE and trigger verdict @@ -596,6 +619,7 @@ export class WebSocketController { 'WSController', `Chat complete, triggering verdict generation for room ${result.roomId}` ); + logger.info('ws.chat_complete', { roomId: result.roomId }); const chatCompleteData: IChatCompleteData = { roomId: result.roomId, @@ -775,6 +799,11 @@ export class WebSocketController { logger.log('WSController', `Client disconnected: ${connectionId}`); const connection = connectionManager.getConnection(connectionId); + logger.info('ws.disconnected', { + connectionId, + roomId: connection?.roomId, + userId: connection?.userId, + }); if (connection?.userId && connection?.roomId) { const room = roomManager.getRoomById(connection.roomId); @@ -812,6 +841,12 @@ export class WebSocketController { code: EWSErrorCode, message: string ): void { + if (code === EWSErrorCode.InvalidPayload) { + logger.warn('ws.validation_failed', { + connectionId, + error: message, + }); + } connectionManager.sendToConnection(connectionId, { type: EWSMessageType.Error, data: { diff --git a/backend/src/middleware/requestLogger.ts b/backend/src/middleware/requestLogger.ts new file mode 100644 index 0000000..22961cd --- /dev/null +++ b/backend/src/middleware/requestLogger.ts @@ -0,0 +1,28 @@ +/** + * HTTP Request Logger Middleware + * Logs each HTTP request with method, path, status, and duration + * + * ARCHITECTURE: Middleware layer + * - Listens for the response 'finish' event to capture status + duration + * - Does NOT contain business logic + */ + +import type { Request, Response, NextFunction } from 'express'; +import { logger } from '../utils/logger'; + +export function requestLogger( + req: Request, + res: Response, + next: NextFunction +): void { + const startMs = Date.now(); + res.on('finish', () => { + logger.info('http.request', { + method: req.method, + path: req.path, + status: res.statusCode, + durationMs: Date.now() - startMs, + }); + }); + next(); +} diff --git a/backend/src/services/core/verdict-orchestrator.service.ts b/backend/src/services/core/verdict-orchestrator.service.ts index ed01fa0..0e1cb17 100644 --- a/backend/src/services/core/verdict-orchestrator.service.ts +++ b/backend/src/services/core/verdict-orchestrator.service.ts @@ -124,6 +124,10 @@ export class VerdictOrchestratorService { data: resultData, timestamp: Date.now(), }); + logger.info('ws.verdict_result', { + roomId, + caseNumber: verdict.caseNumber, + }); } catch (error) { // Handle error this.handleVerdictError(roomId, error, connectionManager); @@ -176,6 +180,11 @@ export class VerdictOrchestratorService { 'VerdictOrchestrator', `Verdict generation failed for room ${roomId}: ${errorMessage}` ); + logger.error('ws.verdict_failed', { + roomId, + retryCount: room.verdictRetryCount, + error: errorMessage, + }); // Check if can retry const canRetry = room.verdictRetryCount < VERDICT_CONFIG.MAX_RETRIES; diff --git a/backend/src/utils/logger.ts b/backend/src/utils/logger.ts index 6011eed..4c99711 100644 --- a/backend/src/utils/logger.ts +++ b/backend/src/utils/logger.ts @@ -1,26 +1,184 @@ /** * Logger Utility - * Environment-aware logging for backend services + * Winston-based structured logger for backend services * - * - log/warn: only output in non-production environments - * - error: always output regardless of environment + * Calling conventions: + * Legacy: logger.log('Tag', 'message text', optionalError) + * Structured: logger.info('event.name', { key: value, ... }) + * + * Structured mode activates when there is exactly one argument and it is a + * plain (non-Error) object. This produces top-level JSON fields for + * grep-ability (e.g. grep roomId in logs/combined.log). + * + * Transports: + * - Console: colorized readable format (dev) or JSON (prod) + * - logs/error.log: error level only, always JSON + * - logs/combined.log: all levels, always JSON + * + * Log level is controlled by LOG_LEVEL env var (default: info). */ -type LogLevel = 'log' | 'warn' | 'error'; +import winston from 'winston'; + +const { combine, timestamp, errors, json, printf } = winston.format; + +const isDev: boolean = process.env.NODE_ENV !== 'production'; +const logLevel: string = process.env.LOG_LEVEL ?? 'info'; + +// ANSI color codes applied inline to avoid colorize() mutating the shared +// info object and corrupting JSON written to file transports. +const LEVEL_COLORS: Record = { + error: '\x1b[31m', + warn: '\x1b[33m', + info: '\x1b[32m', + debug: '\x1b[36m', + verbose: '\x1b[37m', +}; +const RESET = '\x1b[0m'; + +/** + * Human-readable format for development console. + * Structured metadata is appended as inline JSON for easy scanning. + */ +const devConsoleFormat = combine( + timestamp({ format: 'HH:mm:ss.SSS' }), + errors({ stack: true }), + printf((rawInfo: winston.Logform.TransformableInfo): string => { + const info = rawInfo as Record; + const ts = String(info['timestamp'] ?? ''); + const levelStr = String(info['level'] ?? 'info'); + const color = LEVEL_COLORS[levelStr] ?? ''; + const coloredLevel = `${color}${levelStr}${RESET}`; + const message = String(info['message'] ?? ''); + const tag = info['tag'] ? `[${String(info['tag'])}] ` : ''; + const stack = + typeof info['stack'] === 'string' ? `\n${info['stack']}` : ''; + + const known = new Set([ + 'timestamp', + 'level', + 'message', + 'tag', + 'stack', + ]); + const rest: Record = {}; + for (const [k, v] of Object.entries(info)) { + if (!known.has(k)) rest[k] = v; + } + const restPart = + Object.keys(rest).length > 0 ? ` ${JSON.stringify(rest)}` : ''; + + return `${ts} ${coloredLevel} ${tag}${message}${restPart}${stack}`; + }) +); + +const jsonFormat = combine(timestamp(), errors({ stack: true }), json()); -const DEBUG: boolean = process.env.NODE_ENV !== 'production'; -const ENV: string = process.env.NODE_ENV ?? 'development'; +const winstonLogger = winston.createLogger({ + level: logLevel, + format: winston.format.json(), + transports: [ + new winston.transports.Console({ + format: isDev ? devConsoleFormat : jsonFormat, + }), + new winston.transports.File({ + filename: 'logs/error.log', + level: 'error', + format: jsonFormat, + }), + new winston.transports.File({ + filename: 'logs/combined.log', + format: jsonFormat, + }), + ], +}); -function print(level: LogLevel, tag: string, ...args: unknown[]): void { - if (level !== 'error' && !DEBUG) return; - const prefix = `[${ENV}][${tag}]`; - console[level](prefix, ...args); +type LogMeta = Record; + +/** + * Core log function used by all public methods. + * + * Two calling conventions: + * 1. Structured: coreLog('info', 'event.name', [{ key: value }]) + * → { level, message: 'event.name', key: value, ... } + * 2. Legacy: coreLog('info', 'Tag', ['message text', optionalError]) + * → { level, message: 'message text', tag: 'Tag' } + * + * Structured mode activates when args contains exactly one plain object. + */ +function coreLog( + level: 'info' | 'warn' | 'error', + tagOrMessage: string, + args: unknown[] +): void { + // Structured call: logger.info('event', { key: value }) + if ( + args.length === 1 && + args[0] !== null && + typeof args[0] === 'object' && + !(args[0] instanceof Error) + ) { + const meta = args[0] as LogMeta; + switch (level) { + case 'info': + winstonLogger.info(tagOrMessage, meta); + break; + case 'warn': + winstonLogger.warn(tagOrMessage, meta); + break; + case 'error': + winstonLogger.error(tagOrMessage, meta); + break; + } + return; + } + + // Legacy call: logger.log('Tag', 'message', ...args) + const parts: string[] = []; + let stack: string | undefined; + + for (const arg of args) { + if (arg instanceof Error) { + parts.push(arg.message); + if (arg.stack) stack = arg.stack; + } else if (typeof arg === 'string') { + parts.push(arg); + } else if (arg !== null && arg !== undefined) { + parts.push(JSON.stringify(arg)); + } + } + + const message = parts.join(' '); + const meta: LogMeta = { tag: tagOrMessage }; + if (stack !== undefined) meta.stack = stack; + + switch (level) { + case 'info': + winstonLogger.info(message, meta); + break; + case 'warn': + winstonLogger.warn(message, meta); + break; + case 'error': + winstonLogger.error(message, meta); + break; + } } export const logger = { - log: (tag: string, ...args: unknown[]): void => print('log', tag, ...args), - warn: (tag: string, ...args: unknown[]): void => - print('warn', tag, ...args), - error: (tag: string, ...args: unknown[]): void => - print('error', tag, ...args), + /** Legacy API: logger.log('Tag', 'message', ...args) */ + log: (tagOrMessage: string, ...args: unknown[]): void => + coreLog('info', tagOrMessage, args), + + /** Structured API: logger.info('event.name', { key: value }) */ + info: (tagOrMessage: string, ...args: unknown[]): void => + coreLog('info', tagOrMessage, args), + + /** Legacy + structured warn: logger.warn('Tag'|'event', ...) */ + warn: (tagOrMessage: string, ...args: unknown[]): void => + coreLog('warn', tagOrMessage, args), + + /** Legacy + structured error: logger.error('Tag'|'event', ...) */ + error: (tagOrMessage: string, ...args: unknown[]): void => + coreLog('error', tagOrMessage, args), }; diff --git a/backend/src/ws.ts b/backend/src/ws.ts index 313c59a..d51aef1 100644 --- a/backend/src/ws.ts +++ b/backend/src/ws.ts @@ -31,6 +31,7 @@ export function initWebSocket(server: HttpServer): void { // Register connection connectionManager.registerConnection(connectionId, ws); + logger.info('ws.connected', { connectionId }); logger.log( 'WS', From 5c090107e635a148a74e4b8adfa94483f80388e7 Mon Sep 17 00:00:00 2001 From: Shijia Huang Date: Wed, 25 Mar 2026 19:38:20 +1100 Subject: [PATCH 3/3] update docs --- CLAUDE.md | 1 + README.md | 18 ++++++- backend/CLAUDE.md | 17 +++++-- docs/README.md | 5 +- docs/backend/architecture-visual.md | 9 ++-- docs/backend/features/05-error-handling.md | 24 ++++++--- docs/backend/features/08-tencent-sts-token.md | 50 +++++++++---------- docs/backend/features/09-llm-judgment.md | 25 ++++++++++ 8 files changed, 103 insertions(+), 46 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 31f4f72..0fb7b16 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -314,6 +314,7 @@ Required environment variables (see `backend/.env.example`): PORT=8080 NODE_ENV=development WS_PATH=/ws +LOG_LEVEL=debug # Winston log level: debug|info|warn|error (default: info) TENCENT_SECRET_ID=your_secret_id # Required for ASR service TENCENT_SECRET_KEY=your_secret_key # Required for ASR service TENCENT_REGION=ap-guangzhou # Required for ASR service diff --git a/README.md b/README.md index 137202f..4c3f885 100644 --- a/README.md +++ b/README.md @@ -33,12 +33,15 @@ │ └── utils/ # 工具函数 │ ├── backend/ # Node.js 后端 +│ ├── logs/ # 运行日志 (error.log, combined.log) │ └── src/ │ ├── routes/ # HTTP 路由 │ ├── controllers/ # 控制器 +│ ├── middleware/ # Express 中间件 (requestLogger) │ ├── services/ # 业务逻辑 (core, websocket, handlers) │ ├── models/ # 数据模型 -│ └── types/ # 类型定义 +│ ├── types/ # 类型定义 +│ └── utils/ # 工具函数 (Winston logger) │ └── docs/ # 项目文档 ├── miniprogram/ # 前端文档 (页面、组件、服务) @@ -92,10 +95,21 @@ npm run prepare # 初始化 Husky ```bash npm run dev # 开发模式 (tsx watch, 热重载) npm run build # 编译 TypeScript -npm start # 生产模式 +npm start # 生产模式 (自动创建 logs/ 目录) npm run lint # ESLint 检查 ``` +### 日志 + +后端使用 Winston 结构化日志,日志文件写入 `backend/logs/`: + +| 文件 | 内容 | +| ------------------- | ------------------------ | +| `logs/combined.log` | 所有级别,JSON 格式 | +| `logs/error.log` | 仅 error 级别,JSON 格式 | + +开发环境 Console 输出彩色可读格式,生产环境输出 JSON。通过 `LOG_LEVEL` 环境变量控制级别(默认 `info`)。 + ## 核心功能 ### 房间系统 diff --git a/backend/CLAUDE.md b/backend/CLAUDE.md index 6014b36..00ec4dc 100644 --- a/backend/CLAUDE.md +++ b/backend/CLAUDE.md @@ -50,8 +50,10 @@ Three-layer architecture: Routes → Controllers → Services | File | Purpose | |------|---------| | `src/index.ts` | Entry point, creates HTTP server, calls `initWebSocket()` | -| `src/app.ts` | Express app — only `express.json()` middleware + routes | +| `src/app.ts` | Express app — `express.json()` + `requestLogger` middleware + routes | | `src/ws.ts` | WebSocket init, assigns `conn_*` IDs, delegates to controller | +| `src/utils/logger.ts` | Winston logger — dual API: legacy `log(tag, msg)` + structured `info(event, { meta })` | +| `src/middleware/requestLogger.ts` | Express middleware — logs every HTTP request with method/path/status/durationMs | | `src/controllers/ws-controller.ts` | Central message router — routes messages AND orchestrates drum game timing + verdict generation | | `src/controllers/verdict-http.controller.ts` | GET /v1/rooms/:roomId/verdict — fallback to fetch cached verdict | | `src/services/core/verdict-orchestrator.service.ts` | Async verdict generation (LLM call + mapping + WS push) | @@ -105,7 +107,6 @@ Schemas are in `src/models/schemas/`: `http-request.schema.ts`, `ws-message.sche - **Database**: MongoDB config in `src/database/` — stubbed, not connected. All storage is in-memory via `room-manager.ts` - **Repository layer**: Interfaces in `src/repositories/` — defined but no implementations - **room-crud.service.ts**: All methods throw "Not implemented" -- **Middleware**: Error handler, request logger, and validation middleware are defined in `src/middlewares/` but **not used** by `app.ts` ## Adding New WebSocket Message Types @@ -163,13 +164,21 @@ Key constants in `src/constants/config.ts`: Required (see `.env.example`): ```bash PORT=8080 -NODE_ENV=development +NODE_ENV=development # 'production' enables JSON console output WS_PATH=/ws -TENCENT_SECRET_ID=... # Required for ASR STS tokens +LOG_LEVEL=debug # Winston log level: debug | info | warn | error (default: info) +TENCENT_SECRET_ID=... # Required for ASR STS tokens TENCENT_SECRET_KEY=... TENCENT_REGION=ap-guangzhou ``` +In CI/CD (`deploy.yml`), `NODE_ENV` and `LOG_LEVEL` are derived from the deployment environment: + +| Environment | `NODE_ENV` | `LOG_LEVEL` | +|-------------|------------|-------------| +| `trial` | `development` | `debug` (all levels) | +| `release` | `production` | `info` (info/warn/error) | + Note: `.env.example` contains additional unused variables (OpenAI, LLM Worker) — only the above are read by the code. ## HTTP Endpoints diff --git a/docs/README.md b/docs/README.md index 319e67d..6de7c3c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -281,7 +281,7 @@ docs/ - 错误代码体系 - 错误响应格式 - 异常场景处理 - - 日志记录 + - 结构化日志(Winston):`ws.internal_error`、`ws.validation_failed` #### 06. 震天鼓游戏 @@ -310,8 +310,9 @@ docs/ - **功能**: ASR 临时安全凭证分发 - **核心内容**: - GET /v1/tencent/credentials 接口 - - STS 凭证缓存机制 + - STS 凭证缓存机制(24h 有效期,1min 刷新阈值) - 权限限制和安全说明 + - 结构化日志:`tencent.credentials.ok`、`tencent.credentials.failed` #### 09. LLM 判决书生成 diff --git a/docs/backend/architecture-visual.md b/docs/backend/architecture-visual.md index 347857c..e5597f9 100644 --- a/docs/backend/architecture-visual.md +++ b/docs/backend/architecture-visual.md @@ -80,16 +80,17 @@ backend/src/ │ └── common.ts # ✅ 通用类型 │ ├── 📁 constants/ # 🔢 常量 (Constants) -│ ├── config.ts # ✅ 配置常量(APP/WS/ROOM/DRUM/OPENAI/TENCENT) │ ├── config.ts # ✅ 配置常量(APP/WS/ROOM/DRUM/OPENAI/TENCENT/VERDICT) │ └── prompts.ts # ✅ LLM Prompt 模板 │ +├── 📁 middleware/ # 🔧 中间件层 (Middleware) +│ └── requestLogger.ts # ✅ HTTP 请求日志中间件(method/path/status/durationMs) +│ ├── 📁 utils/ # 🛠️ 工具函数 (Utilities) +│ ├── logger.ts # ✅ Winston 结构化日志(双 API:legacy tag + structured event) │ └── env-loader.ts # ✅ 环境变量加载 │ -├── 📁 middlewares/ # 🔧 中间件层 (预留) -│ -├── app.ts # ✅ Express 应用(路由注册) +├── app.ts # ✅ Express 应用(路由注册 + requestLogger 中间件) ├── ws.ts # ✅ WebSocket 服务器初始化 └── index.ts # ✅ 入口文件(HTTP + WS 启动) ``` diff --git a/docs/backend/features/05-error-handling.md b/docs/backend/features/05-error-handling.md index 3f6fafe..8cb87bd 100644 --- a/docs/backend/features/05-error-handling.md +++ b/docs/backend/features/05-error-handling.md @@ -668,17 +668,27 @@ Page({ ### 服务器日志 +后端使用 Winston 结构化日志,所有错误通过 `logger` 记录为 JSON 字段,便于按 `roomId`、`connectionId` 等过滤: + ```typescript -// 记录所有错误 -console.error(`[Error] ${code}: ${message}`, { - connectionId, - userId, - roomId, - context, - timestamp: new Date().toISOString() +// WebSocket 内部异常(ws-controller.ts) +logger.error('ws.internal_error', { + connectionId, + type: messageType, // 触发异常的消息类型 + error: errorMessage, + stack: error instanceof Error ? error.stack : undefined, +}); + +// 消息格式验证失败(ws-controller.ts) +logger.warn('ws.validation_failed', { + connectionId, + type: messageType, + error: errorMessage, }); ``` +日志输出到 `logs/error.log`(仅 error 级别)和 `logs/combined.log`(所有级别),开发环境同时输出彩色可读格式到 console。 + ### 客户端日志 ```typescript diff --git a/docs/backend/features/08-tencent-sts-token.md b/docs/backend/features/08-tencent-sts-token.md index 93ff673..6f06ac1 100644 --- a/docs/backend/features/08-tencent-sts-token.md +++ b/docs/backend/features/08-tencent-sts-token.md @@ -328,14 +328,20 @@ curl -X GET http://localhost:8080/v1/tencent/credentials ### 日志记录 +后端使用 Winston 结构化日志: + ```typescript -// 成功获取 Token -console.log('[TencentController] STS token retrieved successfully'); +// 成功获取 Token(使用缓存或新获取) +logger.info('tencent.credentials.ok', {}); -// 失败情况 -console.error('[TencentController] Get STS token failed:', error); +// 失败情况(包含错误信息,不含凭证值) +logger.error('tencent.credentials.failed', { + error: error instanceof Error ? error.message : String(error), +}); ``` +日志事件可在 `logs/combined.log` 中按 `"event":"tencent.credentials.ok"` 或 `"event":"tencent.credentials.failed"` 过滤。 + ### 建议的监控指标 1. **请求成功率**: 监控 `/v1/tencent/credentials` 的 200 vs 500 响应比例 @@ -347,35 +353,25 @@ console.error('[TencentController] Get STS token failed:', error); ## 性能考虑 -### Token 缓存策略(可选优化) +### Token 缓存策略 -当前实现每次请求都调用腾讯云 STS API。如果请求频繁,可以考虑在后端缓存 Token: +当前实现已在 `TencentController` 中内置 Token 缓存,避免每次请求都调用腾讯云 STS API: -```typescript -// 伪代码示例(未实现) -class TokenCache { - private cachedToken: GetFederationTokenResponse | null = null; - private expireTime: number = 0; - - async getToken(): Promise { - const now = Date.now() / 1000; - - // 如果 Token 还有 5 分钟以上有效期,直接返回缓存 - if (this.cachedToken && this.expireTime - now > 300) { - return this.cachedToken; - } +- Token 有效期设为 **24 小时**(`TOKEN_DURATION_SECONDS = 24 * 60 * 60`) +- 刷新阈值为 **1 分钟**(`REFRESH_THRESHOLD_SECONDS = 60`):剩余有效期不足 1 分钟时才重新获取 +- 缓存存储在模块级 `cachedToken` 变量中(进程内单例) - // 否则重新获取 - this.cachedToken = await stsClient.GetFederationToken(...); - this.expireTime = this.cachedToken.ExpiredTime; - - return this.cachedToken; - } +```typescript +// 缓存检查逻辑(tencent-controller.ts) +private static _isTokenValid(token: GetFederationTokenResponse): boolean { + const currentTimeSeconds = Math.floor(Date.now() / 1000); + const remainingSeconds = token.ExpiredTime - currentTimeSeconds; + return remainingSeconds > REFRESH_THRESHOLD_SECONDS; } ``` -**优点**: 减少对腾讯云 STS API 的调用次数 -**缺点**: 增加实现复杂度,需要处理多实例缓存一致性 +**优点**: 显著减少对腾讯云 STS API 的调用次数,降低延迟和费用 +**注意**: 缓存仅在单进程内有效;多实例部署时每个实例独立缓存 --- diff --git a/docs/backend/features/09-llm-judgment.md b/docs/backend/features/09-llm-judgment.md index c1f84ed..81674f8 100644 --- a/docs/backend/features/09-llm-judgment.md +++ b/docs/backend/features/09-llm-judgment.md @@ -301,6 +301,31 @@ controllers/ws-controller.ts → 广播 VERDICT_RESULT | 超时 | `30000ms` | `VERDICT_CONFIG.LLM_TIMEOUT_MS` | | 响应格式 | JSON | 强制 JSON 输出 | +### 日志 + +HTTP 接口在 LLM 调用前后记录结构化日志,包含 `durationMs`: + +```typescript +// LLM 调用开始 +logger.info('llm.judgment.start', { roomId }); + +// LLM 调用成功(含耗时) +logger.info('llm.judgment.ok', { roomId, durationMs }); + +// LLM 调用失败(含耗时和错误信息) +logger.error('llm.judgment.failed', { roomId, durationMs, error }); +``` + +WebSocket 流程由 `verdict-orchestrator.service.ts` 记录: + +```typescript +// 判决广播成功 +logger.info('ws.verdict_result', { roomId, caseNumber }); + +// 判决生成失败 +logger.error('ws.verdict_failed', { roomId, retryCount, error }); +``` + ### 环境变量 ```bash