const HEADER_SIZE_4 = 0x1; const VERSION_1 = 0x10; const SERIALIZATION_JSON = 0x1 << 4; const SERIALIZATION_RAW = 0; const COMPRESSION_NONE = 0; const MSG_TYPE_FLAG_WITH_EVENT = 0b100; const MsgType = { INVALID: 0, FULL_CLIENT: 1, AUDIO_ONLY_CLIENT: 2, FULL_SERVER: 9, AUDIO_ONLY_SERVER: 11, FRONT_END_RESULT_SERVER: 12, ERROR: 15, }; function getMessageTypeName(value) { return Object.keys(MsgType).find((key) => MsgType[key] === value) || 'INVALID'; } function containsEvent(typeFlag) { return (typeFlag & MSG_TYPE_FLAG_WITH_EVENT) === MSG_TYPE_FLAG_WITH_EVENT; } function shouldHandleSessionId(event) { return event !== 1 && event !== 2 && event !== 50 && event !== 51 && event !== 52; } function writeInt(buffer, value, offset) { buffer.writeInt32BE(value, offset); return offset + 4; } function writeStringWithLength(buffer, value, offset) { const strBuffer = Buffer.from(value || '', 'utf8'); offset = writeInt(buffer, strBuffer.length, offset); strBuffer.copy(buffer, offset); return offset + strBuffer.length; } function writePayload(buffer, payload, offset) { const payloadBuffer = Buffer.isBuffer(payload) ? payload : Buffer.from(payload || ''); offset = writeInt(buffer, payloadBuffer.length, offset); payloadBuffer.copy(buffer, offset); return offset + payloadBuffer.length; } function buildHeader(type, typeFlag, serialization) { return Buffer.from([ VERSION_1 | HEADER_SIZE_4, ((type & 0x0f) << 4) | (typeFlag & 0x0f), serialization | COMPRESSION_NONE, 0, ]); } function marshal(message, { rawPayload = false } = {}) { const type = message.type; const typeFlag = message.typeFlag || MSG_TYPE_FLAG_WITH_EVENT; const payload = Buffer.isBuffer(message.payload) ? message.payload : Buffer.from(message.payload || ''); const serialization = rawPayload ? SERIALIZATION_RAW : SERIALIZATION_JSON; let size = 4; if (containsEvent(typeFlag)) { size += 4; } if (containsEvent(typeFlag) && shouldHandleSessionId(message.event)) { size += 4 + Buffer.byteLength(message.sessionId || '', 'utf8'); } size += 4 + payload.length; const buffer = Buffer.allocUnsafe(size); buildHeader(type, typeFlag, serialization).copy(buffer, 0); let offset = 4; if (containsEvent(typeFlag)) { offset = writeInt(buffer, message.event || 0, offset); } if (containsEvent(typeFlag) && shouldHandleSessionId(message.event)) { offset = writeStringWithLength(buffer, message.sessionId || '', offset); } writePayload(buffer, payload, offset); return buffer; } function readStringWithLength(buffer, offsetObj) { const size = buffer.readInt32BE(offsetObj.offset); offsetObj.offset += 4; if (size <= 0) { return ''; } const value = buffer.subarray(offsetObj.offset, offsetObj.offset + size).toString('utf8'); offsetObj.offset += size; return value; } function readPayload(buffer, offsetObj) { const size = buffer.readInt32BE(offsetObj.offset); offsetObj.offset += 4; if (size <= 0) { return Buffer.alloc(0); } const payload = buffer.subarray(offsetObj.offset, offsetObj.offset + size); offsetObj.offset += size; return payload; } function unmarshal(data) { const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data); if (buffer.length < 4) { throw new Error('protocol message too short'); } const typeAndFlag = buffer[1]; const type = (typeAndFlag >> 4) & 0x0f; const typeFlag = typeAndFlag & 0x0f; const offsetObj = { offset: 4 }; const message = { type, typeName: getMessageTypeName(type), typeFlag, event: 0, sessionId: '', payload: Buffer.alloc(0), }; if (containsEvent(typeFlag)) { message.event = buffer.readInt32BE(offsetObj.offset); offsetObj.offset += 4; } if (containsEvent(typeFlag) && shouldHandleSessionId(message.event)) { message.sessionId = readStringWithLength(buffer, offsetObj); } message.payload = readPayload(buffer, offsetObj); return message; } function createStartConnectionMessage() { return marshal({ type: MsgType.FULL_CLIENT, typeFlag: MSG_TYPE_FLAG_WITH_EVENT, event: 1, payload: Buffer.from('{}', 'utf8'), }); } function createStartSessionMessage(sessionId, payload) { return marshal({ type: MsgType.FULL_CLIENT, typeFlag: MSG_TYPE_FLAG_WITH_EVENT, event: 100, sessionId, payload: Buffer.from(JSON.stringify(payload), 'utf8'), }); } function createAudioMessage(sessionId, audioBuffer) { return marshal({ type: MsgType.AUDIO_ONLY_CLIENT, typeFlag: MSG_TYPE_FLAG_WITH_EVENT, event: 200, sessionId, payload: Buffer.isBuffer(audioBuffer) ? audioBuffer : Buffer.from(audioBuffer), }, { rawPayload: true }); } function createChatTTSTextMessage(sessionId, payload) { return marshal({ type: MsgType.FULL_CLIENT, typeFlag: MSG_TYPE_FLAG_WITH_EVENT, event: 500, sessionId, payload: Buffer.from(JSON.stringify({ session_id: sessionId, start: !!payload.start, end: !!payload.end, content: payload.content || '', }), 'utf8'), }); } function createChatRAGTextMessage(sessionId, externalRag) { return marshal({ type: MsgType.FULL_CLIENT, typeFlag: MSG_TYPE_FLAG_WITH_EVENT, event: 502, sessionId, payload: Buffer.from(JSON.stringify({ session_id: sessionId, external_rag: externalRag || '[]', }), 'utf8'), }); } module.exports = { MsgType, MSG_TYPE_FLAG_WITH_EVENT, unmarshal, createStartConnectionMessage, createStartSessionMessage, createAudioMessage, createChatTTSTextMessage, createChatRAGTextMessage, };