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
This commit is contained in:
天天的鸟蛋蛋 committed 2026-11-12 06:27:36 +08:00
commit b9655a5ab1
1048 files changed
+130851

No files matched your search

@@ -0,0 +1,83 @@
import { NativePacketClient } from '@/snowcat-core/packet/client/nativeClient';
import { OidbPacket } from '@/snowcat-core/packet/transformer/base';
import { PacketLogger } from '@/snowcat-core/packet/context/loggerContext';
import { NapCoreContext } from '@/snowcat-core/packet/context/napCoreContext';
import { Napi2NativeLoader } from '@/snowcat-core/packet/handler/napi2nativeLoader';
export class LogStack {
private stack: string[] = [];
private readonly logger: PacketLogger;
constructor (logger: PacketLogger) {
this.logger = logger;
}
push (msg: string) {
this.stack.push(msg);
}
pushLogInfo (msg: string) {
this.logger.info(msg);
this.stack.push(`${new Date().toISOString()} [INFO] ${msg}`);
}
pushLogWarn (msg: string) {
this.logger.warn(msg);
this.stack.push(`${new Date().toISOString()} [WARN] ${msg}`);
}
pushLogError (msg: string) {
this.logger.error(msg);
this.stack.push(`${new Date().toISOString()} [ERROR] ${msg}`);
}
clear () {
this.stack = [];
}
content () {
return this.stack.join('\n');
}
}
export class PacketClientContext {
private readonly napCore: NapCoreContext;
private readonly logger: PacketLogger;
private readonly logStack: LogStack;
private readonly napi2nativeLoader: Napi2NativeLoader;
private readonly _client: NativePacketClient;
constructor (napCore: NapCoreContext, logger: PacketLogger, napi2nativeLoader: Napi2NativeLoader) {
this.napCore = napCore;
this.logger = logger;
this.logStack = new LogStack(logger);
this.napi2nativeLoader = napi2nativeLoader;
this._client = this.newClient();
}
get available (): boolean {
return this._client.available;
}
get clientLogStack (): string {
return this._client.logStack.content();
}
async init (pid: number, recv: string, send: string): Promise<void> {
await this._client.init(pid, recv, send);
}
async sendOidbPacket<T extends boolean = false> (pkt: OidbPacket, rsp?: T, timeout?: number): Promise<T extends true ? Buffer : void> {
const raw = await this._client.sendOidbPacket(pkt, rsp, timeout);
return raw.data as T extends true ? Buffer : void;
}
private newClient (): NativePacketClient {
this.logger.info('使用 NativePacketClient 作为后端');
const client = new NativePacketClient(this.napCore, this.logger, this.logStack, this.napi2nativeLoader);
if (!client.check()) {
throw new Error('[Core] [Packet] NativePacketClient 不可用,NapCat.Packet将不会加载!');
}
return client;
}
}
@@ -0,0 +1,35 @@
import { NapCoreContext } from '@/snowcat-core/packet/context/napCoreContext';
import { LogWrapper, LogLevel } from '@/snowcat-core/helper/log';
// TODO: check bind?
export class PacketLogger {
private readonly napLogger: LogWrapper;
constructor (napcore: NapCoreContext) {
this.napLogger = napcore.logger;
}
private _log (level: LogLevel, ...msg: any[]): void {
this.napLogger._log(level, '[Core] [Packet] ' + msg);
}
debug (...msg: any[]): void {
this._log(LogLevel.DEBUG, msg);
}
info (...msg: any[]): void {
this._log(LogLevel.INFO, msg);
}
warn (...msg: any[]): void {
this._log(LogLevel.WARN, msg);
}
error (...msg: any[]): void {
this._log(LogLevel.ERROR, msg);
}
fatal (...msg: any[]): void {
this._log(LogLevel.FATAL, msg);
}
}
@@ -0,0 +1,42 @@
import { SnowCatCore } from '@/snowcat-core/index';
export interface NapCoreCompatBasicInfo {
readonly requireMinNTQQBuild: (buildVer: string) => boolean;
readonly uin: number;
readonly uid: string;
readonly uin2uid: (uin: number) => Promise<string>;
readonly uid2uin: (uid: string) => Promise<number>;
readonly sendSsoCmdReqByContend: (cmd: string, trace_id: string) => Promise<void>;
}
export class NapCoreContext {
private readonly core: SnowCatCore;
constructor (core: SnowCatCore) {
this.core = core;
}
get logger () {
return this.core.context.logger;
}
get basicInfo () {
return {
requireMinNTQQBuild: (buildVer: string) => this.core.context.basicInfoWrapper.requireMinNTQQBuild(buildVer),
uin: +this.core.selfInfo.uin,
uid: this.core.selfInfo.uid,
uin2uid: (uin: number) => this.core.apis.UserApi.getUidByUinV2(String(uin)).then(res => res ?? ''),
uid2uin: (uid: string) => this.core.apis.UserApi.getUinByUidV2(uid).then(res => +res),
} as NapCoreCompatBasicInfo;
}
get config () {
return this.core.configLoader.configData;
}
get napi2nativeLoader () {
return this.core.context.napi2nativeLoader;
}
sendSsoCmdReqByContend = (cmd: string, data: Buffer) => this.core.context.session.getMsgService().sendSsoCmdReqByContend(cmd, data);
}
@@ -0,0 +1,428 @@
import * as crypto from 'crypto';
import { PacketContext } from '@/snowcat-core/packet/context/packetContext';
import * as trans from '@/snowcat-core/packet/transformer';
import { PacketMsg } from '@/snowcat-core/packet/message/message';
import {
PacketMsgFileElement,
PacketMsgPicElement,
PacketMsgPttElement,
PacketMsgReplyElement,
PacketMsgVideoElement,
} from '@/snowcat-core/packet/message/element';
import { ChatType, MsgSourceType, NTMsgType, RawMessage } from '@/snowcat-core/index';
import { MiniAppRawData, MiniAppReqParams } from '@/snowcat-core/packet/entities/miniApp';
import { AIVoiceChatType } from '@/snowcat-core/packet/entities/aiChat';
import { NapProtoDecodeStructType, NapProtoEncodeStructType, NapProtoMsg } from 'snowcat-protobuf';
import { IndexNode, LongMsgResult, MsgInfo, PushMsgBody } from '@/snowcat-core/packet/transformer/proto';
import { OidbPacket } from '@/snowcat-core/packet/transformer/base';
import { ImageOcrResult } from '@/snowcat-core/packet/entities/ocrResult';
import { gunzipSync } from 'zlib';
import { PacketMsgConverter } from '@/snowcat-core/packet/message/converter';
import { UploadForwardMsgParams } from '@/snowcat-core/packet/transformer/message/UploadForwardMsgV2';
import { assertUploadResults } from '@/snowcat-core/packet/context/uploadResult';
export class PacketOperationContext {
private readonly context: PacketContext;
constructor (context: PacketContext) {
this.context = context;
}
async sendPacket<T extends boolean = false> (pkt: OidbPacket, rsp?: T): Promise<T extends true ? Buffer : void> {
return await this.context.client.sendOidbPacket(pkt, rsp);
}
async SendPoke (is_group: boolean, peer: number, target?: number) {
const req = trans.SendPoke.build(is_group, peer, target ?? peer);
await this.context.client.sendOidbPacket(req);
}
async SetGroupTodo (groupUin: number, msgSeq: string) {
const req = trans.SetGroupTodo.build(groupUin, msgSeq);
await this.context.client.sendOidbPacket(req, true);
}
async CompleteGroupTodo (groupUin: number, msgSeq: string) {
const req = trans.CompleteGroupTodo.build(groupUin, msgSeq);
await this.context.client.sendOidbPacket(req, true);
}
async CancelGroupTodo (groupUin: number, msgSeq: string) {
const req = trans.CancelGroupTodo.build(groupUin, msgSeq);
await this.context.client.sendOidbPacket(req, true);
}
async FetchRkey (timeout: number = 10000) {
const req = trans.FetchRkey.build();
const resp = await this.context.client.sendOidbPacket(req, true, timeout);
const res = trans.FetchRkey.parse(resp);
return res.data.rkeyList;
}
async GroupSign (groupUin: number) {
const req = trans.GroupSign.build(this.context.napcore.basicInfo.uin, groupUin);
await this.context.client.sendOidbPacket(req);
}
async GetStrangerStatus (uin: number) {
let status = 0;
try {
const req = trans.GetStrangerInfo.build(uin);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.GetStrangerInfo.parse(resp);
const extBigInt = BigInt(res.data.status.value);
if (extBigInt <= 10n) {
return { status: Number(extBigInt) * 10, ext_status: 0 };
}
status = Number((extBigInt & 0xff00n) + ((extBigInt >> 16n) & 0xffn));
return { status: 10, ext_status: status };
} catch {
return undefined;
}
}
async SetGroupSpecialTitle (groupUin: number, uid: string, title: string) {
const req = trans.SetSpecialTitle.build(groupUin, uid, title);
await this.context.client.sendOidbPacket(req);
}
async UploadResources (msg: PacketMsg[], groupUin: number = 0) {
const chatType = groupUin ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C;
const peerUid = groupUin ? String(groupUin) : this.context.napcore.basicInfo.uid;
const reqList = msg.flatMap((m) =>
m.msg
.map((e) => {
if (e instanceof PacketMsgPicElement) {
return this.context.highway.uploadImage({ chatType, peerUid }, e);
} else if (e instanceof PacketMsgVideoElement) {
return this.context.highway.uploadVideo({ chatType, peerUid }, e);
} else if (e instanceof PacketMsgPttElement) {
return this.context.highway.uploadPtt({ chatType, peerUid }, e);
} else if (e instanceof PacketMsgFileElement) {
return this.context.highway.uploadFile({ chatType, peerUid }, e);
}
return null;
})
.filter(Boolean)
);
const res = await Promise.allSettled(reqList);
assertUploadResults(res, this.context.logger);
}
async UploadImage (img: PacketMsgPicElement) {
await this.context.highway.uploadImage(
{
chatType: ChatType.KCHATTYPEC2C,
peerUid: this.context.napcore.basicInfo.uid,
},
img
);
const index = img.msgInfo?.msgInfoBody?.at(0)?.index;
if (!index) {
throw new Error('img.msgInfo?.msgInfoBody![0].index! is undefined');
}
return await this.GetImageUrl(this.context.napcore.basicInfo.uid, index);
}
async GetImageUrl (selfUid: string, node: NapProtoEncodeStructType<typeof IndexNode>) {
const req = trans.DownloadImage.build(selfUid, node);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.DownloadImage.parse(resp);
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
}
async GetPttUrl (selfUid: string, node: NapProtoEncodeStructType<typeof IndexNode>, timeout?: number) {
const req = trans.DownloadPtt.build(selfUid, node);
const resp = await this.context.client.sendOidbPacket(req, true, timeout);
const res = trans.DownloadPtt.parse(resp);
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
}
async GetVideoUrl (selfUid: string, node: NapProtoEncodeStructType<typeof IndexNode>, timeout?: number) {
const req = trans.DownloadVideo.build(selfUid, node);
const resp = await this.context.client.sendOidbPacket(req, true, timeout);
const res = trans.DownloadVideo.parse(resp);
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
}
async GetGroupImageUrl (groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>, timeout?: number) {
const req = trans.DownloadGroupImage.build(groupUin, node);
const resp = await this.context.client.sendOidbPacket(req, true, timeout);
const res = trans.DownloadImage.parse(resp);
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
}
async GetGroupPttUrl (groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>, timeout?: number) {
const req = trans.DownloadGroupPtt.build(groupUin, node);
const resp = await this.context.client.sendOidbPacket(req, true, timeout);
const res = trans.DownloadImage.parse(resp);
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
}
async GetGroupVideoUrl (groupUin: number, node: NapProtoEncodeStructType<typeof IndexNode>, timeout: number = 20000) {
const req = trans.DownloadGroupVideo.build(groupUin, node);
const resp = await this.context.client.sendOidbPacket(req, true, timeout);
const res = trans.DownloadImage.parse(resp);
return `https://${res.download.info.domain}${res.download.info.urlPath}${res.download.rKeyParam}`;
}
async ImageOCR (imgUrl: string) {
const req = trans.ImageOCR.build(imgUrl);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.ImageOCR.parse(resp);
return {
texts: res.ocrRspBody.textDetections.map((item) => {
return {
text: item.detectedText,
confidence: item.confidence,
coordinates: item.polygon.coordinates.map((c) => {
return {
x: c.x,
y: c.y,
};
}),
};
}),
language: res.ocrRspBody.language,
} as ImageOcrResult;
}
private async SendPreprocess (msg: PacketMsg[], groupUin: number = 0) {
const ps = msg.map((m) => {
return m.msg.map(async (e) => {
if (e instanceof PacketMsgReplyElement && !e.targetElems) {
this.context.logger.debug('Cannot find reply element\'s targetElems, prepare to fetch it...');
if (!e.targetPeer?.peerUid) {
this.context.logger.error('targetPeer is undefined!');
}
let targetMsg: NapProtoEncodeStructType<typeof PushMsgBody>[] | undefined;
if (e.isGroupReply) {
targetMsg = await this.FetchGroupMessage(+(e.targetPeer?.peerUid ?? 0), e.targetMessageSeq, e.targetMessageSeq);
} else {
targetMsg = await this.FetchC2CMessage(await this.context.napcore.basicInfo.uin2uid(e.targetUin), e.targetMessageSeq, e.targetMessageSeq);
}
e.targetElems = targetMsg.at(0)?.body?.richText?.elems;
e.targetSourceMsg = targetMsg.at(0);
}
});
}).flat();
await Promise.all(ps);
await this.UploadResources(msg, groupUin);
}
async FetchGroupMessage (groupUin: number, startSeq: number, endSeq: number): Promise<NapProtoDecodeStructType<typeof PushMsgBody>[]> {
const req = trans.FetchGroupMessage.build(groupUin, startSeq, endSeq);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.FetchGroupMessage.parse(resp);
return res.body.messages;
}
async FetchC2CMessage (targetUid: string, startSeq: number, endSeq: number): Promise<NapProtoDecodeStructType<typeof PushMsgBody>[]> {
const req = trans.FetchC2CMessage.build(targetUid, startSeq, endSeq);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.FetchC2CMessage.parse(resp);
return res.messages;
}
async UploadForwardMsg (msg: PacketMsg[], groupUin: number = 0) {
await this.SendPreprocess(msg, groupUin);
const req = trans.UploadForwardMsg.build(this.context.napcore.basicInfo.uid, msg, groupUin);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.UploadForwardMsg.parse(resp);
return res.result.resId;
}
async UploadForwardMsgV2 (msg: UploadForwardMsgParams[], groupUin: number = 0) {
// 遍历上传资源(跳过已有原始数据的项)
await Promise.all(msg.filter(item => item.actionMsg).map(async (item) => {
await this.SendPreprocess(item.actionMsg!, groupUin);
}));
const req = trans.UploadForwardMsgV2.build(this.context.napcore.basicInfo.uid, msg, groupUin);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.UploadForwardMsg.parse(resp);
return res.result.resId;
}
async MoveGroupFile (
groupUin: number,
fileUUID: string,
currentParentDirectory: string,
targetParentDirectory: string
) {
const req = trans.MoveGroupFile.build(groupUin, fileUUID, currentParentDirectory, targetParentDirectory);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.MoveGroupFile.parse(resp);
return res.move.retCode;
}
async RenameGroupFile (groupUin: number, fileUUID: string, currentParentDirectory: string, newName: string) {
const req = trans.RenameGroupFile.build(groupUin, fileUUID, currentParentDirectory, newName);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.RenameGroupFile.parse(resp);
return res.rename.retCode;
}
async GetGroupFileUrl (groupUin: number, fileUUID: string, timeout?: number) {
const req = trans.DownloadGroupFile.build(groupUin, fileUUID);
const resp = await this.context.client.sendOidbPacket(req, true, timeout);
const res = trans.DownloadGroupFile.parse(resp);
return `https://${res.download.downloadDns}/ftn_handler/${Buffer.from(res.download.downloadUrl).toString('hex')}/?fname=`;
}
async GetPrivateFileUrl (self_id: string, fileUUID: string, md5: string, timeout?: number) {
const req = trans.DownloadPrivateFile.build(self_id, fileUUID, md5);
const resp = await this.context.client.sendOidbPacket(req, true, timeout);
const res = trans.DownloadPrivateFile.parse(resp);
return `http://${res.body?.result?.server}:${res.body?.result?.port}${res.body?.result?.url?.slice(8)}&isthumb=0`;
}
async GetMiniAppAdaptShareInfo (param: MiniAppReqParams) {
const req = trans.GetMiniAppAdaptShareInfo.build(param);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.GetMiniAppAdaptShareInfo.parse(resp);
return JSON.parse(res.content.jsonContent) as MiniAppRawData;
}
async FetchAiVoiceList (groupUin: number, chatType: AIVoiceChatType) {
const req = trans.FetchAiVoiceList.build(groupUin, chatType);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.FetchAiVoiceList.parse(resp);
if (!res.content) return null;
return res.content.map((item) => {
return {
category: item.category,
voices: item.voices,
};
});
}
async GetAiVoice (
groupUin: number,
voiceId: string,
text: string,
chatType: AIVoiceChatType
): Promise<NapProtoDecodeStructType<typeof MsgInfo>> {
let reqTime = 0;
const reqMaxTime = 30;
const sessionId = crypto.randomBytes(4).readUInt32BE(0);
while (true) {
if (reqTime >= reqMaxTime) {
throw new Error(`sendAiVoiceChatReq failed after ${reqMaxTime} times`);
}
reqTime++;
const req = trans.GetAiVoice.build(groupUin, voiceId, text, sessionId, chatType);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.GetAiVoice.parse(resp);
if (!res.msgInfo) continue;
return res.msgInfo;
}
}
async FetchForwardMsgRaw (resId: string) {
const req = trans.DownloadForwardMsg.build(this.context.napcore.basicInfo.uid, resId);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.DownloadForwardMsg.parse(resp);
const inflate = gunzipSync(res.result.payload);
const result = new NapProtoMsg(LongMsgResult).decode(inflate);
return result.action;
}
async FetchForwardMsg (res_id: string): Promise<RawMessage[]> {
const req = trans.DownloadForwardMsg.build(this.context.napcore.basicInfo.uid, res_id);
const resp = await this.context.client.sendOidbPacket(req, true);
const res = trans.DownloadForwardMsg.parse(resp);
const inflate = gunzipSync(res.result.payload);
const result = new NapProtoMsg(LongMsgResult).decode(inflate);
const main = result.action.find((r) => r.actionCommand === 'MultiMsg');
if (!main?.actionData.msgBody) {
throw new Error('msgBody is empty');
}
this.context.logger.debug('rawChains ', inflate.toString('hex'));
const messagesPromises = main.actionData.msgBody.map(async (msg) => {
if (!msg?.body?.richText?.elems) {
throw new Error('msg.body.richText.elems is empty');
}
const rawChains = new PacketMsgConverter().packetMsgToRaw(msg?.body?.richText?.elems);
const elements = await Promise.all(
rawChains.map(async ([element, rawElem]) => {
if (element.picElement && rawElem?.commonElem?.pbElem) {
const extra = new NapProtoMsg(MsgInfo).decode(rawElem.commonElem.pbElem);
const index = extra?.msgInfoBody[0]?.index;
if (msg?.responseHead.grp !== undefined) {
const groupUin = msg?.responseHead.grp?.groupUin ?? 0;
element.picElement = {
...element.picElement,
originImageUrl: await this.GetGroupImageUrl(groupUin, index!),
};
} else {
element.picElement = {
...element.picElement,
originImageUrl: await this.GetImageUrl(this.context.napcore.basicInfo.uid, index!),
};
}
return element;
}
if (element.videoElement && rawElem?.commonElem?.pbElem) {
const extra = new NapProtoMsg(MsgInfo).decode(rawElem.commonElem.pbElem);
const index = extra?.msgInfoBody[0]?.index;
if (!index) return element;
if (msg?.responseHead.grp !== undefined) {
element.videoElement = {
...element.videoElement,
filePath: await this.GetGroupVideoUrl(msg.responseHead.grp.groupUin ?? 0, index),
};
} else {
element.videoElement = {
...element.videoElement,
filePath: await this.GetVideoUrl(this.context.napcore.basicInfo.uid, index),
};
}
return element;
}
return element;
})
);
const isGroup = msg?.responseHead.grp !== undefined;
const peerUin = isGroup
? String(msg?.responseHead.grp?.groupUin ?? 0)
: String(msg?.responseHead.fromUin ?? 0);
const peerUid = isGroup
? String(msg?.responseHead.grp?.groupUin ?? 0)
: String(msg?.responseHead.fromUid ?? '');
const sendNickName = isGroup
? (msg?.responseHead.grp?.memberName ?? '')
: (msg?.responseHead.forward?.friendName ?? '');
return {
chatType: isGroup ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C,
elements,
guildId: '',
isOnlineMsg: false,
msgId: '7467703692092974645', // TODO: no necessary
msgRandom: '0',
msgSeq: String(msg.contentHead.sequence ?? 0),
msgTime: String(msg.contentHead.timeStamp ?? 0),
msgType: NTMsgType.KMSGTYPEMIX,
parentMsgIdList: [],
parentMsgPeer: {
chatType: isGroup ? ChatType.KCHATTYPEGROUP : ChatType.KCHATTYPEC2C,
peerUid,
},
peerName: '',
peerUid,
peerUin,
recallTime: '0',
records: [],
sendNickName,
sendRemarkName: sendNickName,
senderUid: msg?.responseHead.fromUid ?? '',
senderUin: String(msg?.responseHead.fromUin ?? 0),
sourceType: MsgSourceType.K_DOWN_SOURCETYPE_UNKNOWN,
subMsgType: 1,
};
});
return await Promise.all(messagesPromises);
}
}
@@ -0,0 +1,25 @@
import { PacketHighwayContext } from '@/snowcat-core/packet/highway/highwayContext';
import { SnowCatCore } from '@/snowcat-core/index';
import { PacketLogger } from '@/snowcat-core/packet/context/loggerContext';
import { NapCoreContext } from '@/snowcat-core/packet/context/napCoreContext';
import { PacketClientContext } from '@/snowcat-core/packet/context/clientContext';
import { PacketOperationContext } from '@/snowcat-core/packet/context/operationContext';
import { PacketMsgConverter } from '@/snowcat-core/packet/message/converter';
export class PacketContext {
readonly msgConverter: PacketMsgConverter;
readonly napcore: NapCoreContext;
readonly logger: PacketLogger;
readonly client: PacketClientContext;
readonly highway: PacketHighwayContext;
readonly operation: PacketOperationContext;
constructor (core: SnowCatCore) {
this.msgConverter = new PacketMsgConverter();
this.napcore = new NapCoreContext(core);
this.logger = new PacketLogger(this.napcore);
this.client = new PacketClientContext(this.napcore, this.logger, this.napcore.napi2nativeLoader);
this.highway = new PacketHighwayContext(this.napcore, this.logger, this.client);
this.operation = new PacketOperationContext(this);
}
}
@@ -0,0 +1,26 @@
export interface UploadResultLogger {
warn (message: string): unknown;
error (message: string): unknown;
}
export function assertUploadResults (
results: PromiseSettledResult<unknown>[],
logger: UploadResultLogger
): void {
const failures = results.filter((result): result is PromiseRejectedResult => result.status === 'rejected');
if (failures.length === 0) return;
logger.warn(`上传资源${results.length}个,失败${failures.length}个`);
results.forEach((result, index) => {
if (result.status === 'rejected') {
const reason = result.reason instanceof Error
? result.reason.stack ?? result.reason.message
: String(result.reason);
logger.error(`上传第${index + 1}个资源失败:${reason}`);
}
});
throw new AggregateError(
failures.map(result => result.reason),
`上传转发消息资源失败:${failures.length}/${results.length}`
);
}