diff --git a/cocoscreator_projects/YouleNexus/assets/framework/net/envelope-codec.ts b/cocoscreator_projects/YouleNexus/assets/framework/net/envelope-codec.ts new file mode 100644 index 0000000..3b05d35 --- /dev/null +++ b/cocoscreator_projects/YouleNexus/assets/framework/net/envelope-codec.ts @@ -0,0 +1,43 @@ +import { APP } from '../core/constants.ts'; +import type { InboundMessage } from '../core/types/envelope.ts'; + +/** 组装单层出站信封字符串。docs/protocol/01 §3.1。 */ +export function encodeOutbound(route: string, rpc: string, data: unknown): string { + return JSON.stringify({ app: APP, route, rpc, data }); +} + +export type DecodeResult = + | { kind: 'handshake' } // @toconcon,忽略 + | { kind: 'heartbeat' } // @serverheartbeat,忽略不回包 + | { kind: 'serverDown' } // webserve-服务器未工作 + | { kind: 'message'; message: InboundMessage } // 正常业务内层 + | { kind: 'ignore' }; // 无法解析,安全忽略 + +function parseMaybe(v: unknown): unknown { + return typeof v === 'string' ? JSON.parse(v) : v; +} + +/** + * 解一帧原始字符串为分类结果。docs/protocol/01 §3.2 步骤 3–8。 + * 不做 TcpID/isSendLoginState/submit_error 过滤(那是连接级,见 net-client)。 + */ +export function decodeFrame(frame: string): DecodeResult { + let outer: any; + try { outer = JSON.parse(frame); } catch { return { kind: 'ignore' }; } + let data = outer?.data; + + if (data === 'webserve-服务器未工作') return { kind: 'serverDown' }; + + if (typeof data === 'string') { + if (data.substr(0, 9) === '@toconcon') return { kind: 'handshake' }; + try { data = JSON.parse(data); } catch { return { kind: 'ignore' }; } + } + if (data && typeof data === 'object' && (data as any).com === '@serverheartbeat') { + return { kind: 'heartbeat' }; + } + const inner = parseMaybe(data) as InboundMessage | undefined; + if (inner && typeof inner === 'object' && 'route' in inner && 'rpc' in inner) { + return { kind: 'message', message: inner }; + } + return { kind: 'ignore' }; +} diff --git a/cocoscreator_projects/framework-tests/net/envelope-codec.test.ts b/cocoscreator_projects/framework-tests/net/envelope-codec.test.ts new file mode 100644 index 0000000..b3c8737 --- /dev/null +++ b/cocoscreator_projects/framework-tests/net/envelope-codec.test.ts @@ -0,0 +1,35 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { encodeOutbound, decodeFrame } from '../../YouleNexus/assets/framework/net/envelope-codec.ts'; + +test('encodeOutbound 产出单层信封字符串', () => { + const s = encodeOutbound('agent', 'player_login', { openid: 'o' }); + assert.deepEqual(JSON.parse(s), { app: 'youle', route: 'agent', rpc: 'player_login', data: { openid: 'o' } }); +}); + +test('decodeFrame:握手包 @toconcon 识别为 handshake', () => { + const frame = JSON.stringify({ data: '@toconconXYZ...' }); + assert.deepEqual(decodeFrame(frame), { kind: 'handshake' }); +}); + +test('decodeFrame:心跳包 @serverheartbeat 识别为 heartbeat', () => { + const frame = JSON.stringify({ data: JSON.stringify({ com: '@serverheartbeat' }) }); + assert.deepEqual(decodeFrame(frame), { kind: 'heartbeat' }); +}); + +test('decodeFrame:特殊错误包 webserve-服务器未工作', () => { + const frame = JSON.stringify({ data: 'webserve-服务器未工作' }); + assert.deepEqual(decodeFrame(frame), { kind: 'serverDown' }); +}); + +test('decodeFrame:正常业务包解出内层 {route,rpc,data}', () => { + const inner = { route: 'agent', rpc: 'player_login', data: { state: 0, playerid: 9 } }; + const frame = JSON.stringify({ data: JSON.stringify(inner) }); + assert.deepEqual(decodeFrame(frame), { kind: 'message', message: inner }); +}); + +test('decodeFrame:内层为对象(非字符串)也能解', () => { + const inner = { route: 'room', rpc: 'other_join_room', data: { seat: 1 } }; + const frame = JSON.stringify({ data: inner }); + assert.deepEqual(decodeFrame(frame), { kind: 'message', message: inner }); +});