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,43 @@
import { ActionName } from '@/snowcat-onebot/action/router';
import { OneBotAction } from '@/snowcat-onebot/action/OneBotAction';
import { MessageUnique } from 'snowcat-common/src/message-unique';
import { Static, Type } from '@sinclair/typebox';
const PayloadSchema = Type.Object({
message_id: Type.Union([Type.Number(), Type.String()], { description: '消息ID' }),
});
type PayloadType = Static<typeof PayloadSchema>;
const ReturnSchema = Type.Null({ description: '操作结果' });
type ReturnType = Static<typeof ReturnSchema>;
class DeleteMsg extends OneBotAction<PayloadType, ReturnType> {
override actionName = ActionName.DeleteMsg;
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
override actionSummary = '撤回消息';
override actionDescription = '撤回已发送的消息';
override actionTags = ['消息接口'];
override payloadExample = {
message_id: 12345,
};
override returnExample = null;
async _handle (payload: PayloadType) {
const msg = MessageUnique.getMsgIdAndPeerByShortId(Number(payload.message_id));
if (msg) {
this.obContext.recallEventCache.set(msg.MsgId, setTimeout(() => {
this.obContext.recallEventCache.delete(msg.MsgId);
}, 5000));
await this.core.apis.MsgApi.recallMsg(msg.Peer, msg.MsgId);
return null;
} else {
throw new Error('Recall failed');
}
}
}
export default DeleteMsg;
@@ -0,0 +1,87 @@
import { ActionName } from '@/snowcat-onebot/action/router';
import { OneBotAction } from '@/snowcat-onebot/action/OneBotAction';
import { MessageUnique } from 'snowcat-common/src/message-unique';
import { Static, Type } from '@sinclair/typebox';
import { PromiseTimer } from '@/snowcat-common/src/helper';
import { ElementType } from '@/snowcat-core';
import { MsgActionsExamples } from '@/snowcat-onebot/action/msg/examples';
const PayloadSchema = Type.Object({
message_id: Type.Union([Type.Number(), Type.String()], { description: '消息ID' }),
});
type PayloadType = Static<typeof PayloadSchema>;
const ReturnSchema = Type.Object({ text: Type.String({ description: '得到的文本' }) });
type ReturnType = Static<typeof ReturnSchema>;
export class FetchPttText extends OneBotAction<PayloadType, ReturnType> {
override actionName = ActionName.FetchPttText;
override actionSummary = '获取语音转文字结果';
override actionTags = ['消息扩展'];
override payloadExample = MsgActionsExamples.FetchPttText.payload;
override returnExample = MsgActionsExamples.FetchPttText.response;
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
async _handle (payload: PayloadType) {
if (!payload.message_id) {
throw Error('参数message_id不能为空');
}
const MsgShortId = MessageUnique.getShortIdByMsgId(payload.message_id.toString());
const msgIdWithPeer = MessageUnique.getMsgIdAndPeerByShortId(MsgShortId ?? +payload.message_id);
if (!msgIdWithPeer) {
throw new Error('消息不存在');
}
const peer = { guildId: '', peerUid: msgIdWithPeer?.Peer.peerUid, chatType: msgIdWithPeer.Peer.chatType };
const getMsgTimeout = this.obContext.configLoader.configData.timeout.baseTimeout;
const msgRes = await PromiseTimer(
this.core.apis.MsgApi.getMsgsByMsgId(peer, [msgIdWithPeer?.MsgId || payload.message_id.toString()]),
getMsgTimeout
).catch((e: unknown) => {
if (e instanceof Error && e.message.startsWith('PromiseTimer:')) {
throw new Error(`消息不存在或已被撤回: ${e.message || String(e)}`);
}
throw e;
});
const msg = msgRes.msgList[0];
if (!msg) throw Error('消息不存在或已被撤回');
const pttSegment = msg.elements.find((s) =>
s.elementType === ElementType.PTT
);
if (!pttSegment) {
throw Error('消息中不包含语音');
}
const getPttTextTimeout = this.obContext.configLoader.configData.timeout.baseTimeout;
await PromiseTimer(
this.core.context.session.getMsgService().translatePtt2Text(msgIdWithPeer.MsgId, msgIdWithPeer.Peer, pttSegment),
getPttTextTimeout
).catch((e: unknown) => {
if (e instanceof Error && e.message.startsWith('PromiseTimer:')) {
throw new Error(`获取语音转文字结果失败: ${e.message || String(e)}`);
}
throw e;
});
const msgRes2 = await PromiseTimer(
this.core.apis.MsgApi.getMsgsByMsgId(peer, [msgIdWithPeer?.MsgId || payload.message_id.toString()]),
getMsgTimeout
).catch((e: unknown) => {
if (e instanceof Error && e.message.startsWith('PromiseTimer:')) {
throw new Error(`消息不存在或已被撤回: ${e.message || String(e)}`);
}
throw e;
});
const msg2 = msgRes2.msgList[0];
if (!msg2) throw Error('消息不存在或已被撤回');
for (const s of msg2.elements) {
if (s.elementType === ElementType.PTT) {
if (s.pttElement?.text) {
return { text: s.pttElement?.text };
}
}
}
throw new Error('获取语音转文字结果失败');
}
}
@@ -0,0 +1,68 @@
import { OneBotAction } from '@/snowcat-onebot/action/OneBotAction';
import { ChatType, Peer } from 'snowcat-core/types';
import { ActionName } from '@/snowcat-onebot/action/router';
import { MessageUnique } from 'snowcat-common/src/message-unique';
import { Static, Type } from '@sinclair/typebox';
const PayloadSchema = Type.Object({
message_id: Type.Union([Type.Number(), Type.String()], { description: '消息ID' }),
group_id: Type.Optional(Type.String({ description: '目标群号' })),
user_id: Type.Optional(Type.String({ description: '目标用户QQ' })),
});
type PayloadType = Static<typeof PayloadSchema>;
const ReturnSchema = Type.Null({ description: '操作结果' });
type ReturnType = Static<typeof ReturnSchema>;
class ForwardSingleMsg extends OneBotAction<PayloadType, ReturnType> {
override actionSummary = '转发单条消息';
override actionDescription = '转发单条消息';
override actionTags = ['消息接口'];
override payloadExample = {
message_id: 12345,
group_id: '123456',
};
override returnExample = null;
protected async getTargetPeer (payload: PayloadType): Promise<Peer> {
if (payload.user_id) {
const peerUid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
if (!peerUid) {
throw new Error(`无法找到私聊对象${payload.user_id}`);
}
return { chatType: ChatType.KCHATTYPEC2C, peerUid };
}
return { chatType: ChatType.KCHATTYPEGROUP, peerUid: payload.group_id!.toString() };
}
async _handle (payload: PayloadType): Promise<null> {
const msg = MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id);
if (!msg) {
throw new Error(`无法找到消息${payload.message_id}`);
}
const peer = await this.getTargetPeer(payload);
const ret = await this.core.apis.MsgApi.forwardMsg(msg.Peer,
peer,
[msg.MsgId]
);
if (ret.result !== 0) {
throw new Error(`转发消息失败 ${ret.errMsg}`);
}
return null;
}
}
export class ForwardFriendSingleMsg extends ForwardSingleMsg {
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
override actionName = ActionName.ForwardFriendSingleMsg;
}
export class ForwardGroupSingleMsg extends ForwardSingleMsg {
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
override actionName = ActionName.ForwardGroupSingleMsg;
}
@@ -0,0 +1,87 @@
import type { MessageFormat } from '@/snowcat-onebot/config/config';
import { OneBotAction } from '@/snowcat-onebot/action/OneBotAction';
import { ActionName } from '@/snowcat-onebot/action/router';
import { MessageUnique } from 'snowcat-common/src/message-unique';
import { PromiseTimer } from 'snowcat-common/src/helper';
import { Static, Type } from '@sinclair/typebox';
import { NetworkAdapterConfig } from '@/snowcat-onebot/config/config';
import { MsgActionsExamples } from './examples';
const PayloadSchema = Type.Object({
message_id: Type.Union([Type.Number(), Type.String()], { description: '消息ID' }),
});
type PayloadType = Static<typeof PayloadSchema>;
const ReturnSchema = Type.Object({
time: Type.Number({ description: '发送时间' }),
message_type: Type.String({ description: '消息类型' }),
message_id: Type.Number({ description: '消息ID' }),
real_id: Type.Number({ description: '真实ID' }),
message_seq: Type.Number({ description: '消息序号' }),
sender: Type.Any({ description: '发送者' }),
message: Type.Any({ description: '消息内容' }),
raw_message: Type.String({ description: '原始消息内容' }),
font: Type.Number({ description: '字体' }),
group_id: Type.Optional(Type.Union([Type.Number(), Type.String()], { description: '群号' })),
user_id: Type.Union([Type.Number(), Type.String()], { description: '发送者QQ号' }),
emoji_likes_list: Type.Optional(Type.Array(Type.Any(), { description: '表情回应列表' })),
}, { description: 'OneBot 11 消息' });
type ReturnType = Static<typeof ReturnSchema>;
class GetMsg extends OneBotAction<PayloadType, ReturnType> {
override actionName = ActionName.GetMsg;
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
override actionSummary = '获取消息';
override actionDescription = '根据消息 ID 获取消息详细信息';
override actionTags = ['消息接口'];
override payloadExample = MsgActionsExamples.GetMsg.payload;
override returnExample = MsgActionsExamples.GetMsg.response;
async _handle (payload: PayloadType, _adapter: string, config: NetworkAdapterConfig) {
if (!payload.message_id) {
throw Error('参数message_id不能为空');
}
const MsgShortId = MessageUnique.getShortIdByMsgId(payload.message_id.toString());
const msgIdWithPeer = MessageUnique.getMsgIdAndPeerByShortId(MsgShortId ?? +payload.message_id);
if (!msgIdWithPeer) {
throw new Error('消息不存在');
}
const peer = { guildId: '', peerUid: msgIdWithPeer?.Peer.peerUid, chatType: msgIdWithPeer.Peer.chatType };
const getMsgTimeout = this.obContext.configLoader.configData.timeout.baseTimeout;
const msgRes = await PromiseTimer(
this.core.apis.MsgApi.getMsgsByMsgId(peer, [msgIdWithPeer?.MsgId || payload.message_id.toString()]),
getMsgTimeout
).catch((e: unknown) => {
if (e instanceof Error && e.message.startsWith('PromiseTimer:')) {
throw new Error(`消息不存在或已被撤回: ${e.message || String(e)}`);
}
throw e;
});
const msg = msgRes.msgList[0];
if (!msg) throw Error('消息不存在或已被撤回');
const retMsg = await this.obContext.apis.MsgApi.parseMessage(msg, config['messagePostFormat'] as MessageFormat | undefined ?? 'array');
if (!retMsg) throw Error('消息为空');
retMsg.emoji_likes_list = [];
msg.emojiLikesList?.forEach(emoji => {
retMsg.emoji_likes_list?.push({
emoji_id: emoji.emojiId,
emoji_type: emoji.emojiType,
likes_cnt: emoji.likesCnt,
});
});
try {
retMsg.message_id = MessageUnique.createUniqueMsgId(peer, msg.msgId)!;
retMsg.message_seq = retMsg.message_id;
retMsg.real_id = retMsg.message_id;
} catch {
// ignored
}
return retMsg;
}
}
export default GetMsg;
@@ -0,0 +1,96 @@
import { ChatType, Peer } from 'snowcat-core/types';
import { OneBotAction } from '@/snowcat-onebot/action/OneBotAction';
import { ActionName } from '@/snowcat-onebot/action/router';
import { MessageUnique } from 'snowcat-common/src/message-unique';
import { Static, Type } from '@sinclair/typebox';
const PayloadSchema = Type.Object({
user_id: Type.Optional(Type.Union([Type.String(), Type.Number()], { description: '用户QQ' })),
group_id: Type.Optional(Type.String({ description: '群号' })),
message_id: Type.Optional(Type.String({ description: '消息ID' })),
});
type PayloadType = Static<typeof PayloadSchema>;
const ReturnSchema = Type.Null({ description: '操作结果' });
type ReturnType = Static<typeof ReturnSchema>;
class MarkMsgAsRead extends OneBotAction<PayloadType, ReturnType> {
override actionSummary = '标记消息已读';
override actionDescription = '标记指定渠道的消息为已读';
override actionTags = ['消息接口'];
override payloadExample = {
message_id: 12345,
};
override returnExample = null;
async getPeer (payload: PayloadType): Promise<Peer> {
if (payload.message_id) {
const s_peer = MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id)?.Peer;
if (s_peer) {
return s_peer;
}
const l_peer = MessageUnique.getPeerByMsgId(payload.message_id.toString())?.Peer;
if (l_peer) {
return l_peer;
}
}
if (payload.user_id) {
const peerUid = await this.core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
if (!peerUid) {
throw new Error(`私聊${payload.user_id}不存在`);
}
const isBuddy = await this.core.apis.FriendApi.isBuddy(peerUid);
return { chatType: isBuddy ? ChatType.KCHATTYPEC2C : ChatType.KCHATTYPETEMPC2CFROMGROUP, peerUid };
}
if (!payload.group_id) {
throw new Error('缺少参数 group_id 或 user_id');
}
return { chatType: ChatType.KCHATTYPEGROUP, peerUid: payload.group_id.toString() };
}
async _handle (payload: PayloadType): Promise<null> {
const ret = await this.core.apis.MsgApi.setMsgRead(await this.getPeer(payload));
if (ret.result !== 0) {
throw new Error('设置已读失败,' + ret.errMsg);
}
return null;
}
}
// 以下为非标准实现
export class MarkPrivateMsgAsRead extends MarkMsgAsRead {
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
override actionName = ActionName.MarkPrivateMsgAsRead;
override actionSummary = '标记私聊已读';
}
export class MarkGroupMsgAsRead extends MarkMsgAsRead {
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
override actionName = ActionName.MarkGroupMsgAsRead;
override actionSummary = '标记群聊已读';
}
export class GoCQHTTPMarkMsgAsRead extends MarkMsgAsRead {
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
override actionName = ActionName.GoCQHTTP_MarkMsgAsRead;
override actionSummary = '标记消息已读 (Go-CQHTTP)';
}
export class MarkAllMsgAsRead extends OneBotAction<void, null> {
override actionName = ActionName._MarkAllMsgAsRead;
override actionSummary = '标记所有消息已读';
override actionTags = ['消息接口'];
override payloadExample = {};
override returnExample = null;
async _handle (): Promise<null> {
await this.core.apis.MsgApi.markAllMsgAsRead();
return null;
}
}
@@ -0,0 +1,517 @@
import {
OB11MessageData,
OB11MessageDataType,
OB11MessageMixType,
OB11MessageNode,
OB11PostContext,
} from '@/snowcat-onebot/types';
import { ActionName, BaseCheckResult } from '@/snowcat-onebot/action/router';
import { decodeCQCode } from '@/snowcat-onebot/helper/cqcode';
import { MessageUnique } from 'snowcat-common/src/message-unique';
import { ChatType, ElementType, SnowCatCore, Peer, RawMessage, SendArkElement, SendMessageElement } from 'snowcat-core';
import { OneBotAction } from '@/snowcat-onebot/action/OneBotAction';
import { snowCatStable } from '@/snowcat-onebot/stable-line';
import { ForwardMsgBuilder } from '@/snowcat-core/helper/forward-msg-builder';
import { stringifyWithBigInt } from 'snowcat-common/src/helper';
import { PacketMsg } from 'snowcat-core/packet/message/message';
import { rawMsgWithSendMsg } from 'snowcat-core/packet/message/converter';
import { Static, Type } from '@sinclair/typebox';
import { MsgActionsExamples } from '@/snowcat-onebot/action/msg/examples';
import { OB11MessageMixTypeSchema } from '@/snowcat-onebot/types/message';
import { UploadForwardMsgParams } from '@/snowcat-core/packet/transformer/message/UploadForwardMsgV2';
export const SendMsgPayloadSchema = Type.Object({
message_type: Type.Optional(Type.Union([Type.Literal('private'), Type.Literal('group')], { description: '消息类型 (private/group)' })),
user_id: Type.Optional(Type.String({ description: '用户QQ' })),
group_id: Type.Optional(Type.String({ description: '群号' })),
message: OB11MessageMixTypeSchema,
auto_escape: Type.Optional(Type.Union([Type.Boolean(), Type.String()], { description: '是否作为纯文本发送' })),
// 以下为扩展字段
source: Type.Optional(Type.String({ description: '合并转发来源' })),
news: Type.Optional(Type.Array(Type.Object({ text: Type.String() }), { description: '合并转发新闻' })),
summary: Type.Optional(Type.String({ description: '合并转发摘要' })),
prompt: Type.Optional(Type.String({ description: '合并转发提示' })),
timeout: Type.Optional(Type.Number({ description: '自定义发送超时(毫秒),覆盖自动计算值' })),
});
export type SendMsgPayload = Static<typeof SendMsgPayloadSchema>;
export const SendMsgReturnSchema = Type.Object({
message_id: Type.Number({ description: '消息ID' }),
res_id: Type.Optional(Type.String({ description: '转发消息的 res_id' })),
forward_id: Type.Optional(Type.String({ description: '转发消息的 forward_id' })),
});
export type ReturnDataType = Static<typeof SendMsgReturnSchema>;
export enum ContextMode {
Normal = 0,
Private = 1,
Group = 2,
}
// Normalizes a mixed type (CQCode/a single segment/segment array) into a segment array.
export function normalize (message: OB11MessageMixType, autoEscape = false): OB11MessageData[] {
return typeof message === 'string'
? (
autoEscape
? [{ type: OB11MessageDataType.text, data: { text: message } }]
: decodeCQCode(message)
)
: Array.isArray(message) ? message : [message];
}
export async function createContext (core: SnowCatCore, payload: OB11PostContext | undefined, contextMode: ContextMode = ContextMode.Normal): Promise<Peer> {
if (!payload) {
throw new Error('请传递请求内容');
}
if ((contextMode === ContextMode.Group || contextMode === ContextMode.Normal) && payload.group_id) {
return {
chatType: ChatType.KCHATTYPEGROUP,
peerUid: payload.group_id.toString(),
};
}
if ((contextMode === ContextMode.Private || contextMode === ContextMode.Normal) && payload.user_id) {
const Uid = await core.apis.UserApi.getUidByUinV2(payload.user_id.toString());
if (!Uid) {
if (payload.group_id) {
return {
chatType: ChatType.KCHATTYPEGROUP,
peerUid: payload.group_id.toString(),
guildId: '',
};
}
throw new Error('无法获取用户信息');
}
const isBuddy = await core.apis.FriendApi.isBuddy(Uid);
if (!isBuddy) {
if (payload.group_id) {
return {
chatType: ChatType.KCHATTYPETEMPC2CFROMGROUP,
peerUid: Uid,
guildId: payload.group_id.toString(),
};
}
const ret = await core.apis.MsgApi.getTempChatInfo(ChatType.KCHATTYPETEMPC2CFROMGROUP, Uid);
if (ret.tmpChatInfo?.groupCode) {
return {
chatType: ChatType.KCHATTYPETEMPC2CFROMGROUP,
peerUid: Uid,
guildId: '',
};
}
return {
chatType: ChatType.KCHATTYPEC2C,
peerUid: Uid,
guildId: '',
};
}
return {
chatType: ChatType.KCHATTYPEC2C,
peerUid: Uid,
guildId: '',
};
}
if (contextMode === ContextMode.Private && payload.group_id) {
throw new Error('当前私聊发送,请指定 user_id 而不是 group_id');
}
if (contextMode === ContextMode.Group && payload.user_id) {
throw new Error('当前群聊发送,请指定 group_id 而不是 user_id');
}
throw new Error('请指定正确的 group_id 或 user_id');
}
function getSpecialMsgNum (messages: OB11MessageData[], msgType: OB11MessageDataType): number {
return messages.filter(msg => msg.type === msgType).length;
}
function isNode (msg: OB11MessageData): msg is OB11MessageNode {
return msg.type === OB11MessageDataType.node;
}
export class SendMsgBase extends OneBotAction<SendMsgPayload, ReturnDataType> {
override payloadSchema = SendMsgPayloadSchema;
override returnSchema = SendMsgReturnSchema;
override actionTags = ['消息接口'];
protected override async check (payload: SendMsgPayload): Promise<BaseCheckResult> {
const messages = normalize(payload.message);
const nodeElementLength = getSpecialMsgNum(messages, OB11MessageDataType.node);
if (nodeElementLength > 0 && nodeElementLength !== messages.length) {
return {
valid: false,
message: '转发消息不能和普通消息混在一起发送,转发需要保证message只有type为node的元素',
};
}
return { valid: true };
}
async _handle (payload: SendMsgPayload): Promise<ReturnDataType> {
// SnowCat 稳定线:主动发言前过真人抖动+账号预算+熔断(被动回复不受预算惩罚)
await snowCatStable.stableDelay();
return this.base_handle(payload);
}
async base_handle (payload: SendMsgPayload, contextMode: ContextMode = ContextMode.Normal): Promise<ReturnDataType> {
if (payload.message_type === 'group') contextMode = ContextMode.Group;
if (payload.message_type === 'private') contextMode = ContextMode.Private;
const peer = await createContext(this.core, {
message_type: payload.message_type,
user_id: payload.user_id?.toString(),
group_id: payload.group_id?.toString(),
}, contextMode);
const messages = normalize(
payload.message,
typeof payload.auto_escape === 'string' ? payload.auto_escape === 'true' : !!payload.auto_escape
);
const nodeMessages = messages.filter(isNode);
if (nodeMessages.length > 0) {
const packetMode = this.core.apis.PacketApi.packetStatus;
let returnMsgAndResId: { message: RawMessage | null, res_id?: string; } | null;
try {
returnMsgAndResId = packetMode
? await this.handleForwardedNodesPacket(peer, nodeMessages, payload.source, payload.news, payload.summary, payload.prompt)
: await this.handleForwardedNodes(peer, nodeMessages);
} catch (e: unknown) {
throw Error(packetMode ? `发送伪造合并转发消息失败: ${(e as Error)?.stack}` : `发送合并转发消息失败: ${(e as Error)?.stack}`);
}
if (!returnMsgAndResId) {
throw Error('发送合并转发消息失败:returnMsgAndResId 为空!');
}
if (returnMsgAndResId.message) {
const msgShortId = MessageUnique.createUniqueMsgId({
guildId: '',
peerUid: peer.peerUid,
chatType: peer.chatType,
}, (returnMsgAndResId.message).msgId);
// 对gocq的forward_id进行兼容
const resId = returnMsgAndResId.res_id!;
return { message_id: msgShortId!, res_id: resId, forward_id: resId };
} else if (returnMsgAndResId.res_id && !returnMsgAndResId.message) {
throw Error(`发送转发消息(res_id:${returnMsgAndResId.res_id} 失败`);
}
} else {
// if (getSpecialMsgNum(payload, OB11MessageDataType.music)) {
// const music: OB11MessageCustomMusic = messages[0] as OB11MessageCustomMusic;
// if (music) {
// }
// }
}
// log("send msg:", peer, sendElements)
const { sendElements, deleteAfterSentFiles } = await this.obContext.apis.MsgApi
.createSendElements(messages, peer);
const returnMsg = await this.obContext.apis.MsgApi.sendMsgWithOb11UniqueId(peer, sendElements, deleteAfterSentFiles, payload.timeout);
return { message_id: returnMsg.id! };
}
private async uploadForwardedNodesPacket (msgPeer: Peer, messageNodes: OB11MessageNode[], source?: string, news?: {
text: string;
}[], summary?: string, prompt?: string, parentMeta?: {
user_id: string,
nickname: string,
}, dp: number = 0): Promise<{
finallySendElements: SendArkElement,
res_id?: string,
uuid?: string,
packetMsg: PacketMsg[],
deleteAfterSentFiles: string[],
innerPacketMsg?: Array<{ uuid: string, packetMsg?: PacketMsg[], actionMsgBody?: UploadForwardMsgParams['actionMsgBody']; }>;
} | null> {
const packetMsg: PacketMsg[] = [];
const delFiles: string[] = [];
const innerMsg: Array<{ uuid: string, packetMsg?: PacketMsg[], actionMsgBody?: UploadForwardMsgParams['actionMsgBody']; }> = [];
for (const node of messageNodes) {
if (dp >= 3) {
this.core.context.logger.logWarn('转发消息深度超过3层,将停止解析!');
break;
}
if (!node.data.id) {
const OB11Data = normalize(node.type === OB11MessageDataType.node ? node.data.content : node);
let sendElements: SendMessageElement[];
const subNodeMessages = OB11Data.filter(isNode);
if (subNodeMessages.length > 0) {
const uploadReturnData = await this.uploadForwardedNodesPacket(msgPeer, subNodeMessages, node.data.source, node.data.news, node.data.summary, node.data.prompt, {
user_id: (node.data.user_id ?? node.data.uin)?.toString() ?? parentMeta?.user_id ?? this.core.selfInfo.uin,
nickname: (node.data.nickname || node.data.name) ?? parentMeta?.nickname ?? 'QQ用户',
}, dp + 1);
sendElements = uploadReturnData?.finallySendElements ? [uploadReturnData.finallySendElements] : [];
delFiles.push(...(uploadReturnData?.deleteAfterSentFiles || []));
if (uploadReturnData?.uuid) {
innerMsg.push({ uuid: uploadReturnData.uuid, packetMsg: uploadReturnData.packetMsg });
uploadReturnData.innerPacketMsg?.forEach(m => {
innerMsg.push(m);
});
}
} else {
const sendElementsCreateReturn = await this.obContext.apis.MsgApi.createSendElements(
OB11Data,
msgPeer,
[],
{ usePacketVideoMetadata: true }
);
sendElements = sendElementsCreateReturn.sendElements;
delFiles.push(...sendElementsCreateReturn.deleteAfterSentFiles);
}
const packetMsgElements: rawMsgWithSendMsg = {
senderUin: Number((node.data.user_id ?? node.data.uin) ?? parentMeta?.user_id) || +this.core.selfInfo.uin,
senderName: (node.data.nickname || node.data.name) ?? parentMeta?.nickname ?? 'QQ用户',
groupId: msgPeer.chatType === ChatType.KCHATTYPEGROUP ? +msgPeer.peerUid : 0,
time: Number(node.data.time) || Date.now(),
msg: sendElements,
};
this.core.context.logger.logDebug(`handleForwardedNodesPacket[SendRaw] 开始转换 ${stringifyWithBigInt(packetMsgElements)}`);
const transformedMsg = this.core.apis.PacketApi.pkt.msgConverter.rawMsgWithSendMsgToPacketMsg(packetMsgElements);
this.core.context.logger.logDebug(`handleForwardedNodesPacket[SendRaw] 转换为 ${stringifyWithBigInt(transformedMsg)}`);
packetMsg.push(transformedMsg);
} else if (node.data.id) {
const id = node.data.id;
const nodeMsg = MessageUnique.getMsgIdAndPeerByShortId(+id) || MessageUnique.getPeerByMsgId(id);
if (!nodeMsg) {
this.core.context.logger.logError('转发消息失败,未找到消息', id);
continue;
}
const msg = (await this.core.apis.MsgApi.getMsgsByMsgId(nodeMsg.Peer, [nodeMsg.MsgId])).msgList[0];
this.core.context.logger.logDebug(`handleForwardedNodesPacket[PureRaw] 开始转换 ${stringifyWithBigInt(msg)}`);
if (msg) {
const msgCache = await this.core.apis.FileApi.downloadRawMsgMedia([msg]);
delFiles.push(...msgCache);
const transformedMsg = this.core.apis.PacketApi.pkt.msgConverter.rawMsgToPacketMsg(msg, msgPeer);
// 检测嵌套合并转发消息,拉取内层protobuf数据
for (const element of msg.elements) {
let resId: string | undefined;
let uuid: string | undefined;
if (element.multiForwardMsgElement?.resId) {
resId = element.multiForwardMsgElement.resId;
uuid = element.multiForwardMsgElement.fileName;
} else if (element.arkElement?.bytesData) {
try {
const json = JSON.parse(element.arkElement.bytesData);
if (json.app === 'com.tencent.multimsg') {
resId = json.meta?.detail?.resid;
uuid = json.meta?.detail?.uniseq || json.extra?.filename;
}
} catch { /* not a valid json */ }
}
if (resId && uuid) {
try {
const rawActions = await this.core.apis.PacketApi.pkt.operation.FetchForwardMsgRaw(resId);
for (const action of rawActions) {
if (action.actionCommand === 'MultiMsg') {
innerMsg.push({ uuid, actionMsgBody: action.actionData.msgBody });
} else {
innerMsg.push({ uuid: action.actionCommand, actionMsgBody: action.actionData.msgBody });
}
}
} catch (e) {
this.core.context.logger.logError(`获取合并转发内层消息失败: ${(e as Error)?.stack}`);
}
}
}
this.core.context.logger.logDebug(`handleForwardedNodesPacket[PureRaw] 转换为 ${stringifyWithBigInt(transformedMsg)}`);
packetMsg.push(transformedMsg);
}
} else {
this.core.context.logger.logDebug(`handleForwardedNodesPacket 跳过元素 ${stringifyWithBigInt(node)}`);
}
}
if (packetMsg.length === 0) {
this.core.context.logger.logWarn('handleForwardedNodesPacket 元素为空!');
return null;
}
const uploadMsgData: UploadForwardMsgParams[] = [{
actionCommand: 'MultiMsg',
actionMsg: packetMsg,
}];
innerMsg.forEach(({ uuid, packetMsg: msg, actionMsgBody: raw }) => {
uploadMsgData.push({
actionCommand: uuid,
actionMsg: msg,
actionMsgBody: raw,
});
});
const resid = await this.core.apis.PacketApi.pkt.operation.UploadForwardMsgV2(uploadMsgData, msgPeer.chatType === ChatType.KCHATTYPEGROUP ? +msgPeer.peerUid : 0);
const uuid = crypto.randomUUID();
const forwardJson = ForwardMsgBuilder.fromPacketMsg(resid, packetMsg, source, news, summary, prompt, uuid);
return {
deleteAfterSentFiles: delFiles,
finallySendElements: {
elementType: ElementType.ARK,
elementId: '',
arkElement: {
bytesData: JSON.stringify(forwardJson),
},
} as SendArkElement,
res_id: resid,
uuid,
packetMsg,
innerPacketMsg: innerMsg,
};
}
private async handleForwardedNodesPacket (msgPeer: Peer, messageNodes: OB11MessageNode[], source?: string, news?: {
text: string;
}[], summary?: string, prompt?: string): Promise<{
message: RawMessage | null,
res_id?: string;
}> {
const uploadReturnData = await this.uploadForwardedNodesPacket(msgPeer, messageNodes, source, news, summary, prompt);
const res_id = uploadReturnData?.res_id;
const finallySendElements = uploadReturnData?.finallySendElements;
if (!finallySendElements) throw Error('转发消息失败,生成节点为空');
const returnMsg = await this.obContext.apis.MsgApi.sendMsgWithOb11UniqueId(msgPeer, [finallySendElements], uploadReturnData.deleteAfterSentFiles || []).catch(() => undefined);
return { message: returnMsg ?? null, res_id: res_id! };
}
private async handleForwardedNodes (destPeer: Peer, messageNodes: OB11MessageNode[]): Promise<{
message: RawMessage | null,
res_id?: string;
}> {
const selfPeer = {
chatType: ChatType.KCHATTYPEC2C,
peerUid: this.core.selfInfo.uid,
};
let nodeMsgIds: string[] = [];
for (const messageNode of messageNodes) {
const nodeId = messageNode.data.id;
if (nodeId) {
// 对Msgid和OB11ID混用情况兜底
const nodeMsg = MessageUnique.getMsgIdAndPeerByShortId(parseInt(nodeId)) || MessageUnique.getPeerByMsgId(nodeId);
if (!nodeMsg) {
this.core.context.logger.logError('转发消息失败,未找到消息', nodeId);
continue;
}
nodeMsgIds.push(nodeMsg.MsgId);
} else {
// 自定义的消息
try {
const OB11Data = normalize(messageNode.data.content);
// 筛选node消息
const subNodeMessages = OB11Data.filter(isNode);
if (subNodeMessages.length !== 0) {
if (subNodeMessages.length !== OB11Data.length) {
this.core.context.logger.logError('子消息中包含非node消息 跳过不合法部分');
continue;
}
const nodeMsg = await this.handleForwardedNodes(selfPeer, subNodeMessages);
if (nodeMsg) {
nodeMsgIds.push(nodeMsg.message!.msgId);
MessageUnique.createUniqueMsgId(selfPeer, nodeMsg.message!.msgId);
}
// 完成子卡片生成跳过后续
continue;
}
const { sendElements } = await this.obContext.apis.MsgApi
.createSendElements(OB11Data, destPeer);
// 拆分消息
const MixElement = sendElements.filter(
element =>
element.elementType !== ElementType.FILE && element.elementType !== ElementType.VIDEO && element.elementType !== ElementType.ARK && element.elementType !== ElementType.PTT
);
const SingleElement = sendElements.filter(
element =>
element.elementType === ElementType.FILE || element.elementType === ElementType.VIDEO || element.elementType === ElementType.ARK || element.elementType === ElementType.PTT
).map(e => [e]);
const AllElement: SendMessageElement[][] = [MixElement, ...SingleElement].filter(e => e !== undefined && e.length !== 0);
const MsgNodeList: Promise<RawMessage | undefined>[] = [];
for (const sendElementsSplitElement of AllElement) {
MsgNodeList.push(this.obContext.apis.MsgApi.sendMsgWithOb11UniqueId(selfPeer, sendElementsSplitElement, []).catch(() => undefined));
}
(await Promise.allSettled(MsgNodeList)).map((result) => {
if (result.status === 'fulfilled' && result.value) {
nodeMsgIds.push(result.value.msgId);
MessageUnique.createUniqueMsgId(selfPeer, result.value.msgId);
}
return result;
});
} catch (e: unknown) {
this.core.context.logger.logDebug('生成转发消息节点失败', (e as Error).stack);
}
}
}
const nodeMsgArray: Array<RawMessage> = [];
let srcPeer: Peer | undefined;
let needSendSelf = false;
// 检测是否处于同一个Peer 不在同一个peer则全部消息由自身发送
for (const msgId of nodeMsgIds) {
const nodeMsgPeer = MessageUnique.getPeerByMsgId(msgId);
if (!nodeMsgPeer) {
this.core.context.logger.logError('转发消息失败,未找到消息', msgId);
continue;
}
const nodeMsg = (await this.core.apis.MsgApi.getMsgsByMsgId(nodeMsgPeer.Peer, [msgId])).msgList[0];
if (nodeMsg) {
srcPeer = srcPeer ?? { chatType: nodeMsg.chatType, peerUid: nodeMsg.peerUid };
if (srcPeer.peerUid !== nodeMsg.peerUid) {
needSendSelf = true;
}
nodeMsgArray.push(nodeMsg);
}
}
nodeMsgIds = nodeMsgArray.map(msg => msg.msgId);
let retMsgIds: string[] = [];
if (needSendSelf) {
for (const [, msg] of nodeMsgArray.entries()) {
if (msg.peerUid === this.core.selfInfo.uid) {
retMsgIds.push(msg.msgId);
continue;
}
const ClonedMsg = await this.cloneMsg(msg);
if (ClonedMsg) retMsgIds.push(ClonedMsg.msgId);
}
} else {
retMsgIds = nodeMsgIds;
}
if (retMsgIds.length === 0) throw Error('转发消息失败,生成节点为空');
try {
this.core.context.logger.logDebug('开发转发', srcPeer, destPeer, retMsgIds);
return {
message: await this.core.apis.MsgApi.multiForwardMsg(srcPeer!, destPeer, retMsgIds),
};
} catch (e: unknown) {
this.core.context.logger.logError('forward failed', (e as Error)?.stack);
return {
message: null,
};
}
}
async cloneMsg (msg: RawMessage): Promise<RawMessage | undefined> {
const selfPeer = {
chatType: ChatType.KCHATTYPEC2C,
peerUid: this.core.selfInfo.uid,
};
// msg 为待克隆消息
const sendElements: SendMessageElement[] = [];
for (const element of msg.elements) {
sendElements.push(element as SendMessageElement);
}
if (sendElements.length === 0) {
this.core.context.logger.logDebug('需要clone的消息无法解析,将会忽略掉', msg);
}
try {
return await this.core.apis.MsgApi.sendMsg(selfPeer, sendElements);
} catch (e: unknown) {
this.core.context.logger.logError((e as Error)?.stack, '克隆转发消息失败,将忽略本条消息', msg);
return undefined;
}
}
}
export default class SendMsg extends SendMsgBase {
override actionName = ActionName.SendMsg;
override actionSummary = '发送消息';
override actionDescription = '发送私聊或群聊消息';
override actionTags = ['消息接口'];
override payloadExample = MsgActionsExamples.SendMsg.payload;
override returnExample = MsgActionsExamples.SendMsg.response;
}
@@ -0,0 +1,32 @@
import { ContextMode, ReturnDataType, SendMsgBase, SendMsgPayload } from './SendMsg';
import { ActionName, BaseCheckResult } from '@/snowcat-onebot/action/router';
import { snowCatStable } from '@/snowcat-onebot/stable-line';
// 未检测参数
class SendPrivateMsg extends SendMsgBase {
override actionName = ActionName.SendPrivateMsg;
override actionSummary = '发送私聊消息';
override actionDescription = '发送私聊消息';
override actionTags = ['消息接口'];
override payloadExample = {
user_id: '123456789',
message: 'hello',
};
override returnExample = {
message_id: 123456,
};
protected override async check (payload: SendMsgPayload): Promise<BaseCheckResult> {
payload.message_type = 'private';
return super.check(payload);
}
override async _handle (payload: SendMsgPayload): Promise<ReturnDataType> {
// SnowCat 稳定线:真人抖动+账号预算+熔断
await snowCatStable.stableDelay();
return this.base_handle(payload, ContextMode.Private);
}
}
export default SendPrivateMsg;
@@ -0,0 +1,57 @@
import { ActionName } from '@/snowcat-onebot/action/router';
import { OneBotAction } from '@/snowcat-onebot/action/OneBotAction';
import { MessageUnique } from 'snowcat-common/src/message-unique';
import { Static, Type } from '@sinclair/typebox';
const PayloadSchema = Type.Object({
message_id: Type.Union([Type.Number(), Type.String()], { description: '消息ID' }),
emoji_id: Type.Union([Type.Number(), Type.String()], { description: '表情ID' }),
set: Type.Optional(Type.Union([Type.Boolean(), Type.String()], { description: '是否设置' })),
});
type PayloadType = Static<typeof PayloadSchema>;
const ReturnSchema = Type.Any({ description: '操作结果' });
type ReturnType = Static<typeof ReturnSchema>;
export class SetMsgEmojiLike extends OneBotAction<PayloadType, ReturnType> {
override actionName = ActionName.SetMsgEmojiLike;
override actionSummary = '设置消息表情点赞';
override actionTags = ['消息扩展'];
override payloadExample = {
message_id: 12345,
emoji_id: '123',
set: true,
};
override returnExample = {
result: true,
};
override payloadSchema = PayloadSchema;
override returnSchema = ReturnSchema;
async _handle (payload: PayloadType) {
const msg = MessageUnique.getMsgIdAndPeerByShortId(+payload.message_id);
if (!msg) {
throw new Error('msg not found');
}
if (!payload.emoji_id) {
throw new Error('emojiId not found');
}
payload.set = payload.set ?? true;
const msgData = (await this.core.apis.MsgApi.getMsgsByMsgId(msg.Peer, [msg.MsgId])).msgList;
if (!msgData || msgData.length === 0 || !msgData[0]?.msgSeq) {
throw new Error('find msg by msgid error');
}
return await this.core.apis.MsgApi.setEmojiLike(
msg.Peer,
msgData[0].msgSeq,
payload.emoji_id.toString(),
typeof payload.set === 'string' ? payload.set === 'true' : !!payload.set
);
}
}
@@ -0,0 +1,39 @@
export const MsgActionsExamples = {
DeleteMsg: {
payload: { message_id: 123456 },
response: {},
},
GetMsg: {
payload: { message_id: 123456 },
response: {
time: 1710000000,
message_type: 'group',
message_id: 123456,
real_id: 123456,
sender: { user_id: 123456789, nickname: '昵称' },
message: 'hello',
},
},
FetchPttText: {
payload: { message_id: 123456 },
response: {
text: 'hello',
},
},
MarkMsgAsRead: {
payload: { group_id: '123456' },
response: {},
},
SendMsg: {
payload: { message_type: 'group', group_id: '123456', message: 'hello' },
response: { message_id: 123456 },
},
SendPrivateMsg: {
payload: { user_id: '123456789', message: 'hello' },
response: { message_id: 123456 },
},
SetMsgEmojiLike: {
payload: { message_id: 123456, emoji_id: '12345' },
response: {},
},
};