From 29f28d611f13320959b8142e7fc89f62a8d2a75d Mon Sep 17 00:00:00 2001 From: DrSmoothl <1787882683@qq.com> Date: Fri, 15 May 2026 22:56:15 +0800 Subject: [PATCH] fix(send): use c2c businessType for private video / record MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Private-chat video and voice sends were getting bounced by the QQ NT server with `PbSendMsg result=79`. Highway upload logs said success, but the final SendMsg the bot constructed used the group-scene tag inside the outgoing commonElem, and the c2c routing path doesn't recognise it. The convention — verified against the receive-side decoder at msg-push/rich-body-decoder.ts:261-330 which explicitly branches on the pairs — is: image : commonElem.businessType c2c=10 group=20 video : commonElem.businessType c2c=11 group=21 record : commonElem.businessType c2c=12 group=22 makeImageElem already honoured the split (`isGroup ? 20 : 10`). makeVideoElem and makePttElem hardcoded the group value for both scenes. Fix them to follow the same pattern. Also fix a symmetric asymmetry inside the highway upload itself: uploadVideoMsgInfo hardcoded `compatQmsgSceneType: 2` while uploadImageMsgInfo and uploadPttMsgInfo both use `isGroup ? 2 : 1`. Same root cause — group value leaking into the c2c path. Regression test (tests/element-builder.test.ts) pins all six businessType pairs by mocking the highway upload functions and inspecting the proto element the builder returns; would have caught the bug had it existed at the time element-builder.ts was written. --- packages/core/src/bridge/element-builder.ts | 10 +- .../core/src/bridge/highway/video-upload.ts | 6 +- packages/core/tests/element-builder.test.ts | 94 +++++++++++++++++++ 3 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 packages/core/tests/element-builder.test.ts diff --git a/packages/core/src/bridge/element-builder.ts b/packages/core/src/bridge/element-builder.ts index 8158a319..b07f7980 100644 --- a/packages/core/src/bridge/element-builder.ts +++ b/packages/core/src/bridge/element-builder.ts @@ -208,11 +208,14 @@ async function makePttElem(ctx: SendContext, element: MessageElement): Promise
({ + uploadImageMsgInfo: vi.fn(async () => new Uint8Array([7, 8, 9])), +})); +vi.mock('../src/bridge/highway/ptt-upload', () => ({ + uploadPttMsgInfo: vi.fn(async () => new Uint8Array([4, 5, 6])), +})); +vi.mock('../src/bridge/highway/video-upload', () => ({ + uploadVideoMsgInfo: vi.fn(async () => new Uint8Array([1, 2, 3])), +})); + +import { buildSendElems } from '../src/bridge/element-builder'; + +const fakeBridge = {} as any; + +function commonElem(elem: any): { serviceType: number; businessType: number; pbElem: Uint8Array } { + return elem.commonElem; +} + +describe('element-builder / commonElem.businessType per scene', () => { + describe('image', () => { + it('c2c → businessType 10', async () => { + const [elem] = await buildSendElems( + [{ type: 'image', url: 'file:///tmp/a.png' } as any], + { bridge: fakeBridge, userUid: 'u_peer' }, + ); + expect(commonElem(elem).serviceType).toBe(48); + expect(commonElem(elem).businessType).toBe(10); + }); + + it('group → businessType 20', async () => { + const [elem] = await buildSendElems( + [{ type: 'image', url: 'file:///tmp/a.png' } as any], + { bridge: fakeBridge, groupId: 12345 }, + ); + expect(commonElem(elem).businessType).toBe(20); + }); + }); + + describe('video', () => { + it('c2c → businessType 11 (regression: was 21, server returned result=79)', async () => { + const [elem] = await buildSendElems( + [{ type: 'video', url: 'file:///tmp/clip.mp4' } as any], + { bridge: fakeBridge, userUid: 'u_peer' }, + ); + expect(commonElem(elem).serviceType).toBe(48); + expect(commonElem(elem).businessType).toBe(11); + }); + + it('group → businessType 21', async () => { + const [elem] = await buildSendElems( + [{ type: 'video', url: 'file:///tmp/clip.mp4' } as any], + { bridge: fakeBridge, groupId: 12345 }, + ); + expect(commonElem(elem).businessType).toBe(21); + }); + }); + + describe('record', () => { + it('c2c → businessType 12 (regression: was 22)', async () => { + const [elem] = await buildSendElems( + [{ type: 'record', url: 'file:///tmp/voice.amr' } as any], + { bridge: fakeBridge, userUid: 'u_peer' }, + ); + expect(commonElem(elem).serviceType).toBe(48); + expect(commonElem(elem).businessType).toBe(12); + }); + + it('group → businessType 22', async () => { + const [elem] = await buildSendElems( + [{ type: 'record', url: 'file:///tmp/voice.amr' } as any], + { bridge: fakeBridge, groupId: 12345 }, + ); + expect(commonElem(elem).businessType).toBe(22); + }); + }); +});