diff --git a/src/core/index.ts b/src/core/index.ts index dff28680..fbe84535 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -30,6 +30,7 @@ import os from 'node:os'; import { NodeIKernelMsgListener, NodeIKernelProfileListener } from '@/core/listeners'; import { proxiedListenerOf } from '@/common/proxy-handler'; import { NTQQPacketApi } from './apis/packet'; +import { NativePacketHandler } from './packet/handler/client'; export * from './wrapper'; export * from './types'; export * from './services'; @@ -258,6 +259,7 @@ export interface InstanceContext { readonly loginService: NodeIKernelLoginService; readonly basicInfoWrapper: QQBasicInfoWrapper; readonly pathWrapper: NapCatPathWrapper; + readonly packetHandler: NativePacketHandler; } export interface StableNTApiWrapper { diff --git a/src/core/packet/handler/client.ts b/src/core/packet/handler/client.ts new file mode 100644 index 00000000..0e29e448 --- /dev/null +++ b/src/core/packet/handler/client.ts @@ -0,0 +1,214 @@ +import path, { dirname } from 'path'; +import { fileURLToPath } from 'url'; +import fs from 'fs'; +import { constants } from 'node:os'; +import { LogWrapper } from '@/common/log'; +import offset from '@/core/external/offset.json'; +interface OffsetType { + [key: string]: { + recv: string; + send: string; + }; +} + +const typedOffset: OffsetType = offset; +// 0 send 1 recv +export interface NativePacketExportType { + initHook?: (send: string, recv: string, callback: (type: PacketType, uin: string, cmd: string, seq: number, hex_data: string) => void, o3_hook: boolean) => boolean; +} + +export type PacketType = 0 | 1; // 0: send, 1: recv +export type PacketCallback = (data: { type: PacketType, uin: string, cmd: string, seq: number, hex_data: string }) => void; + +interface ListenerEntry { + callback: PacketCallback; + once: boolean; +} + +export class NativePacketHandler { + private readonly supportedPlatforms = ['win32.x64', 'linux.x64', 'linux.arm64', 'darwin.x64', 'darwin.arm64']; + private readonly MoeHooExport: { exports: NativePacketExportType } = { exports: {} }; + protected readonly logger: LogWrapper; + + // 统一的监听器存储 - key: 'all' | 'type:0' | 'type:1' | 'cmd:xxx' | 'exact:type:cmd' + private readonly listeners: Map> = new Map(); + + + constructor({ logger }: { logger: LogWrapper }) { + this.logger = logger; + } + + /** + * 添加监听器的通用方法 + */ + private addListener(key: string, callback: PacketCallback, once: boolean = false): () => void { + if (!this.listeners.has(key)) { + this.listeners.set(key, new Set()); + } + const entry: ListenerEntry = { callback, once }; + this.listeners.get(key)!.add(entry); + return () => this.removeListener(key, callback); + } + + /** + * 移除监听器的通用方法 + */ + private removeListener(key: string, callback: PacketCallback): boolean { + const entries = this.listeners.get(key); + if (!entries) return false; + + for (const entry of entries) { + if (entry.callback === callback) { + return entries.delete(entry); + } + } + return false; + } + + // ===== 永久监听器 ===== + + /** 监听所有数据包 */ + onAll(callback: PacketCallback): () => void { + return this.addListener('all', callback); + } + + /** 监听特定类型的数据包 (0: send, 1: recv) */ + onType(type: PacketType, callback: PacketCallback): () => void { + return this.addListener(`type:${type}`, callback); + } + + /** 监听所有发送的数据包 */ + onSend(callback: PacketCallback): () => void { + return this.onType(0, callback); + } + + /** 监听所有接收的数据包 */ + onRecv(callback: PacketCallback): () => void { + return this.onType(1, callback); + } + + /** 监听特定cmd的数据包(不限type) */ + onCmd(cmd: string, callback: PacketCallback): () => void { + return this.addListener(`cmd:${cmd}`, callback); + } + + /** 监听特定type和cmd的数据包(精确匹配) */ + onExact(type: PacketType, cmd: string, callback: PacketCallback): () => void { + return this.addListener(`exact:${type}:${cmd}`, callback); + } + + // ===== 一次性监听器 ===== + + /** 一次性监听所有数据包 */ + onceAll(callback: PacketCallback): () => void { + return this.addListener('all', callback, true); + } + + /** 一次性监听特定类型的数据包 */ + onceType(type: PacketType, callback: PacketCallback): () => void { + return this.addListener(`type:${type}`, callback, true); + } + + /** 一次性监听所有发送的数据包 */ + onceSend(callback: PacketCallback): () => void { + return this.onceType(0, callback); + } + + /** 一次性监听所有接收的数据包 */ + onceRecv(callback: PacketCallback): () => void { + return this.onceType(1, callback); + } + + /** 一次性监听特定cmd的数据包 */ + onceCmd(cmd: string, callback: PacketCallback): () => void { + return this.addListener(`cmd:${cmd}`, callback, true); + } + + /** 一次性监听特定type和cmd的数据包 */ + onceExact(type: PacketType, cmd: string, callback: PacketCallback): () => void { + return this.addListener(`exact:${type}:${cmd}`, callback, true); + } + + // ===== 移除监听器 ===== + + /** 移除特定的全局监听器 */ + off(key: string, callback: PacketCallback): boolean { + return this.removeListener(key, callback); + } + + /** 移除特定key下的所有监听器 */ + offAll(key: string): void { + this.listeners.delete(key); + } + + /** 移除所有监听器 */ + removeAllListeners(): void { + this.listeners.clear(); + } + + /** + * 触发监听器 - 按优先级触发: 精确匹配 > cmd匹配 > type匹配 > 全局 + */ + private emitPacket(type: PacketType, uin: string, cmd: string, seq: number, hex_data: string): void { + const keys = [ + `exact:${type}:${cmd}`, // 精确匹配 + `cmd:${cmd}`, // cmd匹配 + `type:${type}`, // type匹配 + 'all' // 全局 + ]; + + for (const key of keys) { + const entries = this.listeners.get(key); + if (!entries) continue; + + const toRemove: ListenerEntry[] = []; + for (const entry of entries) { + try { + entry.callback({ type, uin, cmd, seq, hex_data }); + if (entry.once) { + toRemove.push(entry); + } + } catch (error) { + this.logger.logError('监听器回调执行出错:', error); + } + } + + // 移除一次性监听器 + for (const entry of toRemove) { + entries.delete(entry); + } + } + } + + async init(version: string): Promise { + const version_arch = version + '-' + process.arch; + try { + const send = typedOffset[version_arch]?.send; + const recv = typedOffset[version_arch]?.recv; + if (!send || !recv) { + this.logger.logWarn(`NativePacketClient: 未找到对应版本的偏移数据: ${version_arch}`); + return false; + } + const platform = process.platform + '.' + process.arch; + if (!this.supportedPlatforms.includes(platform)) { + this.logger.logWarn(`NativePacketClient: 不支持的平台: ${platform}`); + return false; + } + const moehoo_path = path.join(dirname(fileURLToPath(import.meta.url)), './native/packet/MoeHoo.' + platform + '.node'); + + process.dlopen(this.MoeHooExport, moehoo_path, constants.dlopen.RTLD_LAZY); + if (!fs.existsSync(moehoo_path)) { + this.logger.logWarn(`NativePacketClient: 缺失运行时文件: ${moehoo_path}`); + return false; + } + this.MoeHooExport.exports.initHook?.(send, recv, (type: PacketType, uin: string, cmd: string, seq: number, hex_data: string) => { + this.emitPacket(type, uin, cmd, seq, hex_data); + }, true); + return true; + } + catch (error) { + this.logger.logError('NativePacketClient 初始化出错:', error); + return false; + } + } +} diff --git a/src/framework/napcat.ts b/src/framework/napcat.ts index 65e4123e..1f85a65d 100644 --- a/src/framework/napcat.ts +++ b/src/framework/napcat.ts @@ -10,6 +10,7 @@ import { NodeIQQNTWrapperSession, WrapperNodeApi } from '@/core/wrapper'; import { InitWebUi, WebUiConfig, webUiRuntimePort } from '@/webui'; import { NapCatOneBot11Adapter } from '@/onebot'; import { FFmpegService } from '@/common/ffmpeg'; +import { NativePacketHandler } from '@/core/packet/handler/client'; //Framework ES入口文件 export async function getWebUiUrl() { @@ -37,7 +38,13 @@ export async function NCoreInitFramework( const logger = new LogWrapper(pathWrapper.logsPath); const basicInfoWrapper = new QQBasicInfoWrapper({ logger }); const wrapper = loadQQWrapper(basicInfoWrapper.getFullQQVersion()); - + const nativePacketHandler = new NativePacketHandler({ logger }); // 初始化 NativePacketHandler 用于后续使用 + nativePacketHandler.onAll((packet) => { + console.log('[Packet]', packet.uin, packet.cmd, packet.hex_data); + }); + await nativePacketHandler.init(basicInfoWrapper.getFullQQVersion()); + // 在 init 之后注册监听器 + // 初始化 FFmpeg 服务 await FFmpegService.init(pathWrapper.binaryPath, logger); //直到登录成功后,执行下一步 @@ -65,7 +72,7 @@ export async function NCoreInitFramework( // 过早进入会导致addKernelMsgListener等Listener添加失败 // await sleep(2500); // 初始化 NapCatFramework - const loaderObject = new NapCatFramework(wrapper, session, logger, loginService, selfInfo, basicInfoWrapper, pathWrapper); + const loaderObject = new NapCatFramework(wrapper, session, logger, loginService, selfInfo, basicInfoWrapper, pathWrapper, nativePacketHandler); await loaderObject.core.initCore(); //启动WebUi @@ -86,8 +93,10 @@ export class NapCatFramework { selfInfo: SelfInfo, basicInfoWrapper: QQBasicInfoWrapper, pathWrapper: NapCatPathWrapper, + packetHandler: NativePacketHandler, ) { this.context = { + packetHandler, workingEnv: NapCatCoreWorkingEnv.Framework, wrapper, session, diff --git a/src/native/packet/MoeHoo.win.x64.node b/src/native/packet/MoeHoo.win32.x64.node similarity index 100% rename from src/native/packet/MoeHoo.win.x64.node rename to src/native/packet/MoeHoo.win32.x64.node diff --git a/src/onebot/index.ts b/src/onebot/index.ts index 02945ced..7c453487 100644 --- a/src/onebot/index.ts +++ b/src/onebot/index.ts @@ -102,7 +102,6 @@ export class NapCatOneBot11Adapter { async InitOneBot () { const selfInfo = this.core.selfInfo; const ob11Config = this.configLoader.configData; - this.core.apis.UserApi.getUserDetailInfo(selfInfo.uid, false) .then(async (user) => { selfInfo.nick = user.nick; diff --git a/src/shell/base.ts b/src/shell/base.ts index c6019aae..1d0238fe 100644 --- a/src/shell/base.ts +++ b/src/shell/base.ts @@ -33,6 +33,7 @@ import { NodeIO3MiscListener } from '@/core/listeners/NodeIO3MiscListener'; import { sleep } from '@/common/helper'; import { FFmpegService } from '@/common/ffmpeg'; import { connectToNamedPipe } from '@/shell/pipe'; +import { NativePacketHandler } from '@/core/packet/handler/client'; // NapCat Shell App ES 入口文件 async function handleUncaughtExceptions(logger: LogWrapper) { process.on('uncaughtException', (err) => { @@ -312,13 +313,19 @@ export async function NCoreInitShell() { const pathWrapper = new NapCatPathWrapper(); const logger = new LogWrapper(pathWrapper.logsPath); handleUncaughtExceptions(logger); - + // 初始化 FFmpeg 服务 await FFmpegService.init(pathWrapper.binaryPath, logger); - + await connectToNamedPipe(logger).catch(e => logger.logError('命名管道连接失败', e)); const basicInfoWrapper = new QQBasicInfoWrapper({ logger }); const wrapper = loadQQWrapper(basicInfoWrapper.getFullQQVersion()); + const nativePacketHandler = new NativePacketHandler({ logger }); // 初始化 NativePacketHandler 用于后续使用 + + nativePacketHandler.onAll((packet) => { + console.log('[Packet]', packet.uin, packet.cmd, packet.hex_data); + }); + await nativePacketHandler.init(basicInfoWrapper.getFullQQVersion()); const o3Service = wrapper.NodeIO3MiscService.get(); o3Service.addO3MiscListener(new NodeIO3MiscListener()); @@ -385,6 +392,7 @@ export async function NCoreInitShell() { selfInfo, basicInfoWrapper, pathWrapper, + nativePacketHandler ).InitNapCat(); } @@ -401,8 +409,10 @@ export class NapCatShell { selfInfo: SelfInfo, basicInfoWrapper: QQBasicInfoWrapper, pathWrapper: NapCatPathWrapper, + packetHandler: NativePacketHandler, ) { this.context = { + packetHandler, workingEnv: NapCatCoreWorkingEnv.Shell, wrapper, session,