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

+416
View File
@@ -0,0 +1,416 @@
import {
ChatType,
FileElement,
GrayTipElement,
InstanceContext,
JsonGrayBusiId,
MessageElement,
SnowCatCore,
NTGrayTipElementSubTypeV2,
RawMessage,
TipGroupElement,
TipGroupElementType,
} from 'snowcat-core';
import { OB11GroupBanEvent } from '@/snowcat-onebot/event/notice/OB11GroupBanEvent';
import fastXmlParser from 'fast-xml-parser';
import { OB11GroupMsgEmojiLikeEvent } from '@/snowcat-onebot/event/notice/OB11MsgEmojiLikeEvent';
import { MessageUnique } from 'snowcat-common/src/message-unique';
import { OB11GroupCardEvent } from '@/snowcat-onebot/event/notice/OB11GroupCardEvent';
import { OB11GroupPokeEvent } from '@/snowcat-onebot/event/notice/OB11PokeEvent';
import { OB11GroupEssenceEvent } from '@/snowcat-onebot/event/notice/OB11GroupEssenceEvent';
import { OB11GroupTitleEvent } from '@/snowcat-onebot/event/notice/OB11GroupTitleEvent';
import { OB11GroupGrayTipEvent } from '@/snowcat-onebot/event/notice/OB11GroupGrayTipEvent';
import { OB11GroupUploadNoticeEvent } from '../event/notice/OB11GroupUploadNoticeEvent';
import { OB11GroupNameEvent } from '../event/notice/OB11GroupNameEvent';
import { FileNapCatOneBotUUID } from 'snowcat-common/src/file-uuid';
import { OB11GroupIncreaseEvent } from '../event/notice/OB11GroupIncreaseEvent';
import { NapProtoMsg } from 'snowcat-protobuf';
import { GroupReactNotify, PushMsg } from 'snowcat-core/packet/transformer/proto';
import { NapCatOneBot11Adapter } from '..';
export class OneBotGroupApi {
obContext: NapCatOneBot11Adapter;
core: SnowCatCore;
constructor (obContext: NapCatOneBot11Adapter, core: SnowCatCore) {
this.obContext = obContext;
this.core = core;
}
async parseGroupBanEvent (GroupCode: string, grayTipElement: GrayTipElement) {
const groupElement = grayTipElement?.groupElement;
if (!groupElement?.shutUp) return undefined;
const memberUid = groupElement.shutUp.member.uid;
const adminUid = groupElement.shutUp.admin.uid;
let memberUin: string;
let duration = parseInt(groupElement.shutUp.duration);
const subType: 'ban' | 'lift_ban' = duration > 0 ? 'ban' : 'lift_ban';
if (memberUid) {
memberUin = (await this.core.apis.GroupApi.getGroupMember(GroupCode, memberUid))?.uin ?? '';
} else {
memberUin = '0'; // 0表示全员禁言
if (duration > 0) {
duration = -1;
}
}
await this.core.apis.GroupApi.refreshGroupMemberCachePartial(GroupCode, memberUid);
const adminUin = (await this.core.apis.GroupApi.getGroupMember(GroupCode, adminUid))?.uin;
if (memberUin && adminUin) {
return new OB11GroupBanEvent(
this.core,
parseInt(GroupCode),
parseInt(memberUin),
parseInt(adminUin),
duration,
subType
);
}
return undefined;
}
async parseGroupEmojiLikeEventByGrayTip (
groupCode: string,
grayTipElement: GrayTipElement
) {
if (this.core.apis.PacketApi.packetStatus === true) {
return;
// Raw包解析支持时禁用NT解析
}
const emojiLikeData = new fastXmlParser.XMLParser({
ignoreAttributes: false,
attributeNamePrefix: '',
}).parse(grayTipElement.xmlElement.content);
this.core.context.logger.logDebug('收到表情回应我的消息', emojiLikeData);
const senderUin = emojiLikeData.gtip.qq.jp;
const msgSeq = emojiLikeData.gtip.url.msgseq;
const emojiId = emojiLikeData.gtip.face.id;
return await this.createGroupEmojiLikeEvent(groupCode, senderUin, msgSeq, emojiId, true, 1);
}
async createGroupEmojiLikeEvent (
groupCode: string,
senderUin: string,
msgSeq: string,
emojiId: string,
isAdd: boolean = true,
count: number = 1
) {
const peer = {
chatType: ChatType.KCHATTYPEGROUP,
guildId: '',
peerUid: groupCode,
};
const replyMsgList = (await this.core.apis.MsgApi.queryFirstMsgBySeq(peer, msgSeq)).msgList;
if (replyMsgList.length < 1) {
return;
}
const replyMsg = replyMsgList[0];
if (!replyMsg) {
this.core.context.logger.logError('解析表情回应消息失败: 未找到回应消息');
return undefined;
}
return new OB11GroupMsgEmojiLikeEvent(
this.core,
parseInt(groupCode),
parseInt(senderUin),
MessageUnique.createUniqueMsgId({ chatType: ChatType.KCHATTYPEGROUP, guildId: '', peerUid: groupCode }, replyMsg.msgId),
[{
emoji_id: emojiId,
count,
}],
isAdd,
msgSeq
);
}
async parseCardChangedEvent (msg: RawMessage) {
if (msg.senderUin && msg.senderUin !== '0') {
const member = await this.core.apis.GroupApi.getGroupMember(msg.peerUid, msg.senderUin);
if (member && member.cardName !== msg.sendMemberName) {
const newCardName = msg.sendMemberName ?? '';
const event = new OB11GroupCardEvent(this.core, parseInt(msg.peerUid), parseInt(msg.senderUin), newCardName, member.cardName);
member.cardName = newCardName;
return event;
}
if (member && member.nick !== msg.sendNickName) {
await this.core.apis.GroupApi.refreshGroupMemberCachePartial(msg.peerUid, msg.senderUid);
}
}
return undefined;
}
async registerParseGroupReactEvent () {
this.obContext.core.context.packetHandler.onCmd('trpc.msg.olpush.OlPushService.MsgPush', async (packet) => {
const data = new NapProtoMsg(PushMsg).decode(Buffer.from(packet.hex_data, 'hex'));
if (data.message.contentHead.type === 732 && data.message.contentHead.subType === 16) {
const pbNotify = data.message.body?.msgContent?.slice(7);
if (!pbNotify) {
return;
}
// 开始解析Notify
const notify = new NapProtoMsg(GroupReactNotify).decode(pbNotify);
if ((notify.field13 ?? 0) === 35) {
// Group React Notify
const groupCode = notify.groupUin?.toString() ?? '';
const operatorUid = notify.groupReactionData?.data?.data?.groupReactionDataContent?.operatorUid ?? '';
const type = notify.groupReactionData?.data?.data?.groupReactionDataContent?.type ?? 0;
const seq = notify.groupReactionData?.data?.data?.groupReactionTarget?.seq?.toString() ?? '';
const code = notify.groupReactionData?.data?.data?.groupReactionDataContent?.code ?? '';
// const count = notify.groupReactionData?.data?.data?.groupReactionDataContent?.count ?? 0;
const senderUin = await this.core.apis.UserApi.getUinByUidV2(operatorUid);
const event = await this.createGroupEmojiLikeEvent(
groupCode,
senderUin,
seq,
code,
type === 1,
1
);
if (event) {
this.obContext.networkManager.emitEvent(event);
}
}
}
});
}
async registerParseGroupReactEventByCore () {
this.core.event.on('event:emoji_like', async (data) => {
const event = await this.createGroupEmojiLikeEvent(
data.groupId,
data.senderUin,
data.msgSeq,
data.emojiId,
data.isAdd,
data.count
);
if (event) {
this.obContext.networkManager.emitEvent(event);
}
});
}
async parsePaiYiPai (msg: RawMessage, jsonStr: string) {
const json = JSON.parse(jsonStr);
// 判断业务类型
// Poke事件
const pokedetail: Array<{ uid: string; }> = json.items;
// 筛选item带有uid的元素
const poke_uid = pokedetail.filter(item => item.uid);
if (poke_uid.length === 2 && poke_uid[0]?.uid && poke_uid[1]?.uid) {
return new OB11GroupPokeEvent(
this.core,
parseInt(msg.peerUid),
+await this.core.apis.UserApi.getUinByUidV2(poke_uid[0].uid),
+await this.core.apis.UserApi.getUinByUidV2(poke_uid[1].uid),
pokedetail
);
}
return undefined;
}
async parseOtherJsonEvent (msg: RawMessage, jsonGrayTipElement: GrayTipElement['jsonGrayTipElement'], context: InstanceContext) {
let json: { items?: { txt?: string; param?: string[] }[] };
try {
json = JSON.parse(jsonGrayTipElement.jsonStr);
} catch (e) {
context.logger.logWarn('灰条消息JSON解析失败', jsonGrayTipElement.jsonStr, e);
return undefined;
}
const type = json.items?.[json.items.length - 1]?.txt;
await this.core.apis.GroupApi.refreshGroupMemberCachePartial(msg.peerUid, msg.senderUid);
if (type === '头衔') {
const memberUin = json.items?.[1]?.param?.[0];
const title = json.items?.[3]?.txt;
context.logger.logDebug('收到群成员新头衔消息', json);
if (memberUin == null || title == null) {
context.logger.logWarn('收到格式异常的群成员新头衔灰条消息', json);
return undefined;
}
return new OB11GroupTitleEvent(
this.core,
+msg.peerUid,
+memberUin,
title
);
} else if (type === '移出') {
context.logger.logDebug('收到机器人被踢消息', json);
} else {
context.logger.logWarn('收到未知的灰条消息', json);
// 如果有真实发送者(非0),生成事件上报,可用于检测和撤回伪造灰条
const senderUin = Number(msg.senderUin) || 0;
if (senderUin !== 0) {
const peer = { chatType: ChatType.KCHATTYPEGROUP, guildId: '', peerUid: msg.peerUid };
const messageId = MessageUnique.createUniqueMsgId(peer, msg.msgId);
return new OB11GroupGrayTipEvent(
this.core,
+msg.peerUin,
senderUin,
messageId,
jsonGrayTipElement.busiId,
jsonGrayTipElement.jsonStr,
{
msgSeq: msg.msgSeq,
msgTime: msg.msgTime,
msgId: msg.msgId,
json,
}
);
}
}
return undefined;
}
async parseEssenceMsg (msg: RawMessage, jsonStr: string) {
const json = JSON.parse(jsonStr);
const searchParams = new URL(json.items[0].jp).searchParams;
const msgSeq = searchParams.get('msgSeq')!;
const Group = searchParams.get('groupCode');
if (!Group) return undefined;
// const businessId = searchParams.get('businessid');
const Peer = {
guildId: '',
chatType: ChatType.KCHATTYPEGROUP,
peerUid: Group,
};
const msgData = await this.core.apis.MsgApi.getMsgsBySeqAndCount(Peer, msgSeq.toString(), 1, true, true);
const msgList = (await this.core.apis.WebApi.getGroupEssenceMsgAll(Group)).flatMap((e) => e.data.msg_list);
const realMsg = msgList.find((e) => e.msg_seq.toString() === msgSeq);
if (msgData.msgList[0]) {
return new OB11GroupEssenceEvent(
this.core,
parseInt(msg.peerUid),
MessageUnique.getShortIdByMsgId(msgData.msgList[0].msgId)!,
parseInt(msgData.msgList[0].senderUin ?? await this.core.apis.UserApi.getUinByUidV2(msgData.msgList[0].senderUid)),
parseInt(realMsg?.add_digest_uin ?? '0')
);
}
return undefined;
// 获取MsgSeq+Peer可获取具体消息
}
async parseGroupUploadFileEvene (msg: RawMessage, element: FileElement, elementWrapper: MessageElement) {
return new OB11GroupUploadNoticeEvent(
this.core,
parseInt(msg.peerUid), parseInt(msg.senderUin || ''),
{
id: FileNapCatOneBotUUID.encode({
chatType: ChatType.KCHATTYPEGROUP,
peerUid: msg.peerUid,
}, msg.msgId, elementWrapper.elementId, elementWrapper?.fileElement?.fileUuid, element.fileMd5 ?? element.fileUuid),
name: element.fileName,
size: parseInt(element.fileSize),
busid: element.fileBizId ?? 0,
}
);
}
async parseGroupElement (msg: RawMessage, element: TipGroupElement, elementWrapper: GrayTipElement) {
if (element.type === TipGroupElementType.KGROUPNAMEMODIFIED) {
this.core.context.logger.logDebug('收到群名称变更事件', element);
return new OB11GroupNameEvent(
this.core,
parseInt(msg.peerUid),
parseInt(await this.core.apis.UserApi.getUinByUidV2(element.memberUid)),
element.groupName
);
} else if (element.type === TipGroupElementType.KSHUTUP) {
const event = await this.parseGroupBanEvent(msg.peerUid, elementWrapper);
return event;
} else if (element.type === TipGroupElementType.KMEMBERADD) {
// 自己的通知 协议推送为type->85 在这里实现为了避免邀请出现问题
if (element.memberUid === this.core.selfInfo.uid) {
await this.core.apis.GroupApi.refreshGroupMemberCache(msg.peerUid, true);
return new OB11GroupIncreaseEvent(
this.core,
parseInt(msg.peerUid),
+this.core.selfInfo.uin,
element.adminUid ? +await this.core.apis.UserApi.getUinByUidV2(element.adminUid) : 0,
'approve'
);
}
}
return undefined;
}
async parseSelfInviteEvent (msg: RawMessage, inviterUin: string, inviteeUin: string) {
return new OB11GroupIncreaseEvent(
this.core,
parseInt(msg.peerUid),
+inviteeUin,
+inviterUin,
'invite'
);
}
async parse51TypeEvent (msg: RawMessage, grayTipElement: GrayTipElement) {
// 神经腾讯 没了妈妈想出来的
// Warn 下面存在高并发危险
if (grayTipElement.jsonGrayTipElement.jsonStr) {
const json: {
align: string,
items: Array<{ txt: string, type: string; }>;
} = JSON.parse(grayTipElement.jsonGrayTipElement.jsonStr);
if (json.items.length === 1 && json.items[0]?.txt.endsWith('加入群')) {
const old_members = structuredClone(this.core.apis.GroupApi.groupMemberCache.get(msg.peerUid));
if (!old_members) return;
const new_members_map = await this.core.apis.GroupApi.refreshGroupMemberCache(msg.peerUid, true);
if (!new_members_map) return;
const new_members = Array.from(new_members_map.values());
// 对比members查找新成员
const new_member = new_members.find((member) => old_members.get(member.uid) === undefined);
if (!new_member) return;
return new OB11GroupIncreaseEvent(
this.core,
+msg.peerUid,
+new_member.uin,
0,
'invite'
);
}
}
return undefined;
}
async parseGrayTipElement (msg: RawMessage, grayTipElement: GrayTipElement) {
if (grayTipElement.subElementType === NTGrayTipElementSubTypeV2.GRAYTIP_ELEMENT_SUBTYPE_GROUP) {
// 解析群组事件 由sysmsg解析
return await this.parseGroupElement(msg, grayTipElement.groupElement, grayTipElement);
} else if (grayTipElement.subElementType === NTGrayTipElementSubTypeV2.GRAYTIP_ELEMENT_SUBTYPE_XMLMSG) {
// 筛选自身入群情况
// if (grayTipElement.xmlElement.busiId === '10145') {
// const inviteData = new fastXmlParser.XMLParser({
// ignoreAttributes: false,
// attributeNamePrefix: '',
// }).parse(grayTipElement.xmlElement.content);
// const inviterUin: string = inviteData.gtip.qq[0].jp;
// const inviteeUin: string = inviteData.gtip.qq[1].jp;
// //刷新群缓存
// if (inviteeUin === this.core.selfInfo.uin) {
// this.core.apis.GroupApi.refreshGroupMemberCache(msg.peerUid).then().catch();
// return this.parseSelfInviteEvent(msg, inviterUin, inviteeUin);
// }
// } else
if (grayTipElement.xmlElement?.templId === '10382') {
return await this.obContext.apis.GroupApi.parseGroupEmojiLikeEventByGrayTip(msg.peerUid, grayTipElement);
} else {
// return await this.obContext.apis.GroupApi.parseGroupIncreaseEvent(msg.peerUid, grayTipElement);
}
} else if (grayTipElement.subElementType === NTGrayTipElementSubTypeV2.GRAYTIP_ELEMENT_SUBTYPE_JSON) {
// 解析json事件 busiId好像是string类型的?
if (grayTipElement.jsonGrayTipElement.busiId === '1061') {
return await this.parsePaiYiPai(msg, grayTipElement.jsonGrayTipElement.jsonStr);
} else if (grayTipElement.jsonGrayTipElement.busiId === JsonGrayBusiId.AIO_GROUP_ESSENCE_MSG_TIP.toString()) {
return await this.parseEssenceMsg(msg, grayTipElement.jsonGrayTipElement.jsonStr);
} else if (grayTipElement.jsonGrayTipElement.busiId === '51') {
// 51是什么?{"align":"center","items":[{"txt":"下一秒起床通过王者荣耀加入群","type":"nor"}]
return await this.parse51TypeEvent(msg, grayTipElement);
} else {
console.log('Unknown JSON event:', grayTipElement.jsonGrayTipElement, JSON.stringify(grayTipElement));
return await this.parseOtherJsonEvent(msg, grayTipElement.jsonGrayTipElement, this.core.context);
}
}
return undefined;
}
}