Files
天天的鸟蛋蛋 b9655a5ab1 SnowCat 雪猫底层全源码:NapCat 4.18.28 底座 + SnowLuma 能力移植 + V8 通道适配 9.9.37
26 包 monorepo(TS 全源码):
- snowcat-core: NT 内核 API 封装 + listener(含 9.9.37 六回调适配)+ Packet 发包
- snowcat-onebot: OneBot v11 全动作 + MCP Streamable HTTP 适配器 + 稳定线
- snowcat-webui-frontend/backend: 雪猫控制台(React+HeroUI)
- snowcat-shell: 单文件 bundle 出口(vite inlineDynamicImports)
- snowcat-native: napi2native / MoeHoo / ffmpeg / pty 运行时
- 56 项 SnowLuma 独门动作移植(qzone 全家桶/群相册/装扮/闪照/收藏)
- 工具件: 解包器/启动器/便携制作器/doctor

GitHub 关联已全量指向 DandanLLab/SnowCatQQ
2026-11-12 06:27:36 +08:00

171 lines
5.6 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import { NapProtoDecodeStructType, NapProtoEncodeStructType, NapProtoMsg } from 'snowcat-protobuf';
import { ElementType, NTVideoType } from '@/snowcat-core/types/msg';
import { SendVideoElement } from '@/snowcat-core/types/element';
import {
PacketMsgPicElement,
PacketMsgVideoElement,
} from '@/snowcat-core/packet/message/element';
import { PacketMsgConverter } from '@/snowcat-core/packet/message/converter';
import { assertUploadResults } from '@/snowcat-core/packet/context/uploadResult';
import { Elem, MsgInfo } from '@/snowcat-core/packet/transformer/proto';
import { NTV2RichMediaReq } from '@/snowcat-core/packet/transformer/proto/oidb/common/Ntv2.RichMediaReq';
import UploadGroupVideo from '@/snowcat-core/packet/transformer/highway/UploadGroupVideo';
import OidbBase from '@/snowcat-core/packet/transformer/oidb/oidbBase';
function createVideoMsgInfo (): NapProtoEncodeStructType<typeof MsgInfo> {
return {
msgInfoBody: [
{
index: {
info: {
fileSize: 1024,
fileHash: '00112233445566778899aabbccddeeff',
fileSha1: '00112233445566778899aabbccddeeff00112233',
fileName: 'clip.mp4',
type: {
type: 2,
videoFormat: NTVideoType.VIDEO_FORMAT_MP4,
},
width: 1920,
height: 1080,
time: 12,
},
fileUuid: 'video-uuid',
},
},
{
index: {
info: {
fileSize: 2048,
fileHash: 'ffeeddccbbaa99887766554433221100',
fileSha1: 'ffeeddccbbaa99887766554433221100ffeeddcc',
fileName: 'thumb.jpg',
type: {
type: 1,
picFormat: 0,
},
width: 640,
height: 360,
},
fileUuid: 'thumb-uuid',
},
},
],
};
}
function createSendVideoElement (): SendVideoElement {
return {
elementType: ElementType.VIDEO,
elementId: '',
videoElement: {
filePath: 'clip.mp4',
fileName: 'clip.mp4',
videoMd5: '00112233445566778899aabbccddeeff',
thumbMd5: 'ffeeddccbbaa99887766554433221100',
fileTime: 12,
fileFormat: NTVideoType.VIDEO_FORMAT_MP4,
fileSize: '1024',
fileWidth: 1920,
fileHeight: 1080,
thumbWidth: 640,
thumbHeight: 360,
thumbSize: 2048,
thumbPath: new Map([[0, 'thumb.jpg']]),
},
};
}
function decodeElem (
elem: NapProtoEncodeStructType<typeof Elem>
): NapProtoDecodeStructType<typeof Elem> {
const codec = new NapProtoMsg(Elem);
return codec.decode(codec.encode(elem));
}
function createCommonVideoElem (businessType: 11 | 21): NapProtoDecodeStructType<typeof Elem> {
return decodeElem({
commonElem: {
serviceType: 48,
businessType,
pbElem: new NapProtoMsg(MsgInfo).encode(createVideoMsgInfo()),
},
});
}
describe('packet video forwarding', () => {
it('emits only the common rich-media element so mobile rendering is not blocked', () => {
const video = new PacketMsgVideoElement(createSendVideoElement());
video.msgInfo = createVideoMsgInfo();
video.businessType = 11;
const elems = video.buildElement();
expect(elems).toHaveLength(1);
expect(elems[0]?.videoFile).toBeUndefined();
expect(elems[0]?.commonElem?.serviceType).toBe(48);
expect(elems[0]?.commonElem?.businessType).toBe(11);
expect(video.valid).toBe(true);
});
it('parses common business type 11/21 as video instead of image', () => {
for (const businessType of [11, 21] as const) {
const commonElem = createCommonVideoElem(businessType);
expect(PacketMsgPicElement.parseElement(commonElem)).toBeUndefined();
const parsed = new PacketMsgConverter().packetMsgToRaw([commonElem]);
expect(parsed).toHaveLength(1);
expect(parsed[0]?.[0].elementType).toBe(ElementType.VIDEO);
expect(parsed[0]?.[0].picElement).toBeUndefined();
expect(parsed[0]?.[0].videoElement).toMatchObject({
fileName: 'clip.mp4',
fileTime: 12,
fileWidth: 1920,
fileHeight: 1080,
thumbWidth: 640,
thumbHeight: 360,
fileUuid: 'video-uuid',
});
}
});
it('preserves video metadata in the rich-media upload request', () => {
const video = new PacketMsgVideoElement(createSendVideoElement());
video.fileSha1 = '00112233445566778899aabbccddeeff00112233';
video.thumbSha1 = 'ffeeddccbbaa99887766554433221100ffeeddcc';
const packet = UploadGroupVideo.build(123456, video);
const envelope = OidbBase.parse(packet.data);
const request = new NapProtoMsg(NTV2RichMediaReq).decode(envelope.body);
const videoInfo = request.upload?.uploadInfo[0]?.fileInfo;
const thumbInfo = request.upload?.uploadInfo[1]?.fileInfo;
expect(videoInfo).toMatchObject({
fileName: 'clip.mp4',
fileSize: 1024,
width: 1920,
height: 1080,
time: 12,
});
expect(videoInfo?.type.videoFormat).toBe(NTVideoType.VIDEO_FORMAT_MP4);
expect(thumbInfo?.fileName).toBe('ffeeddccbbaa99887766554433221100.jpg');
});
it('propagates resource upload failures instead of silently continuing', () => {
const uploadError = new Error('thumb upload failed');
const logger = {
warn: vi.fn(),
error: vi.fn(),
};
expect(() => assertUploadResults([
{ status: 'fulfilled', value: undefined },
{ status: 'rejected', reason: uploadError },
], logger)).toThrow('上传转发消息资源失败');
expect(logger.warn).toHaveBeenCalledWith('上传资源2个,失败1个');
expect(logger.error).toHaveBeenCalledOnce();
});
});