diff --git a/packages/napcat-framework/napcat.cjs b/packages/napcat-framework/napcat.cjs index dbfdaba7..9d992aaa 100644 --- a/packages/napcat-framework/napcat.cjs +++ b/packages/napcat-framework/napcat.cjs @@ -10,37 +10,126 @@ let wrapperSession; let wrapperNodeApi; let wrapperLoginService; let getWebUiUrlFunc; -let ncCallback = () => { }; -let napCatInitialized = false; // 添加一个标志 +let wrapperHooked = false; +let sessionReady = false; +let warnedMissingSession = false; +const initCallbacks = new Set(); +const sessionProxies = new WeakMap(); +let resolveSession; +const sessionPromise = new Promise(resolve => { resolveSession = resolve; }); -function createServiceProxy (ServiceName) { - return new Proxy(() => { }, { - get: (target, FunctionName) => { - if (ServiceName === 'NodeIQQNTWrapperSession' && FunctionName === 'create') { - return () => new Proxy({}, { - get: function (target, ClassFunName, receiver) { - return function () { - if (ClassFunName === 'init') { - const origin = arguments[3].onOpentelemetryInit; - arguments[3].onOpentelemetryInit = function (result) { - origin(...arguments); - if (result.is_init) { - ncCallback(); - } - }; - } - const ret = wrapperSession[ClassFunName](...arguments); - return ret; - }; - }, - }); - } - if (ServiceName === 'NodeIKernelLoginService' && FunctionName === 'get') { - return () => wrapperLoginService; - } - return wrapperNodeApi[ServiceName][FunctionName]; +// 原生导出可能含只读、不可配置的属性,使用独立 target 避免替换方法时违反 Proxy 约束。 +// 同时保留导出枚举、prototype 和属性探测,不能把 wrapper 伪装成空对象。 +function nativeProxy (target, get, construct) { + // bound function 没有不可配置的 prototype/caller 属性,可转发原生 class 的属性。 + // eslint-disable-next-line no-extra-bind + const facade = typeof target === 'function' ? function () {}.bind(null) : {}; + const proxy = new Proxy(facade, { + get: (_, key) => get(key), + has: (_, key) => Reflect.has(target, key), + ownKeys: () => Reflect.ownKeys(target), + getOwnPropertyDescriptor: (_, key) => { + const descriptor = Reflect.getOwnPropertyDescriptor(target, key); + return descriptor ? { ...descriptor, configurable: true } : undefined; + }, + getPrototypeOf: () => Reflect.getPrototypeOf(target), + set: (_, key, value) => Reflect.set(target, key, value, target), + apply: (_, thisArg, args) => Reflect.apply(target, thisArg, args), + construct: (_, args, newTarget) => { + const instance = Reflect.construct(target, args, newTarget === proxy ? target : newTarget); + return construct ? construct(instance) : instance; }, }); + return proxy; +} + +function callInitCallback (callback) { + try { + callback(); + } catch (error) { + // NapCat 的回调不能中断 QQ 自己的初始化回调。 + console.log('[NapCat] [Error] 初始化回调失败', error); + } +} + +function registerInitCallback (callback) { + if (sessionReady) callInitCallback(callback); + else initCallbacks.add(callback); +} + +function markSessionReady (session) { + if (session !== wrapperSession || sessionReady) return; + sessionReady = true; + for (const callback of initCallbacks) callInitCallback(callback); + initCallbacks.clear(); +} + +function wrapSessionListener (listener, session) { + const target = listener && typeof listener === 'object' ? listener : {}; + const methods = new Map(); + // 缺失第四参/原方法时也提供监听器,不修改 QQ 的原对象(可能已冻结)。 + const telemetryCallback = (...args) => { + const origin = target.onOpentelemetryInit; + const ret = typeof origin === 'function' ? Reflect.apply(origin, target, args) : undefined; + if (args[0]?.is_init) markSessionReady(session); + return ret; + }; + const listenerTarget = Object.create(Object.getPrototypeOf(target), { + ...Object.getOwnPropertyDescriptors(target), + onOpentelemetryInit: { value: telemetryCallback, enumerable: true, configurable: true }, + }); + return nativeProxy(listenerTarget, key => { + if (key === 'onOpentelemetryInit') return telemetryCallback; + const value = Reflect.get(target, key, target); + if (typeof value !== 'function' || key === 'constructor') return value; + if (!methods.has(value)) methods.set(value, value.bind(target)); + return methods.get(value); + }); +} + +function captureSession (session) { + if (!session || typeof session.init !== 'function') { + if (!warnedMissingSession) { + warnedMissingSession = true; + console.log('[NapCat] [Warn] QQ 尚未返回有效 Session,等待 QQ 后续创建会话'); + } + return session; + } + if (!wrapperSession) { + wrapperSession = session; + resolveSession(session); + } + if (sessionProxies.has(session)) return sessionProxies.get(session); + const methods = new Map(); + const proxy = nativeProxy(session, key => { + const value = Reflect.get(session, key, session); + if (typeof value !== 'function' || key === 'constructor') return value; + if (!methods.has(key)) { + methods.set(key, (...args) => { + if (key === 'init') args[3] = wrapSessionListener(args[3], session); + return Reflect.apply(value, session, args); + }); + } + return methods.get(key); + }); + sessionProxies.set(session, proxy); + return proxy; +} + +function createSessionServiceProxy (service) { + const methods = new Map(); + return nativeProxy(service, key => { + const value = Reflect.get(service, key, service); + if (typeof value !== 'function' || key === 'constructor') return value; + if (!methods.has(key)) { + methods.set(key, (...args) => { + // 使用原生 class 作为 this;把 QQ 的参数原样传递,不能提前固定调用 nt_1。 + const ret = Reflect.apply(value, service, args); + return key === 'create' || key === 'getNTWrapperSession' ? captureSession(ret) : ret; + }); + } + return methods.get(key); + }, captureSession); } function clearHook () { @@ -53,7 +142,7 @@ async function initializeNapCat () { const currentPath = path.dirname(__filename); const { NCoreInitFramework, getWebUiUrl } = await import(pathToFileURL(path.join(currentPath, './napcat.mjs')).href); getWebUiUrlFunc = getWebUiUrl; - await NCoreInitFramework(wrapperSession, wrapperLoginService, (callback) => { ncCallback = callback; }); + await NCoreInitFramework(sessionPromise, wrapperLoginService, registerInitCallback); } catch (error) { console.log('[NapCat] [Error] 初始化NapCat', error); } @@ -61,24 +150,20 @@ async function initializeNapCat () { process.dlopen = function (module, filename, flags = os.constants.dlopen.RTLD_LAZY) { const dlopenRet = dlopenOrig(module, filename, flags); - if (!filename.includes('wrapper.node') || napCatInitialized) return dlopenRet; - napCatInitialized = true; // 初始化完成后设置标志 + if (path.basename(filename) !== 'wrapper.node' || wrapperHooked) return dlopenRet; + wrapperHooked = true; clearHook(); wrapperNodeApi = module.exports; wrapperLoginService = wrapperNodeApi.NodeIKernelLoginService.get(); - wrapperSession = wrapperNodeApi.NodeIQQNTWrapperSession.create(); - - initializeNapCat().then().catch(); - - module.exports = new Proxy({}, { - get: (target, ServiceName) => { - if (ServiceName === 'NodeIKernelLoginService' || ServiceName === 'NodeIQQNTWrapperSession') { - return createServiceProxy(ServiceName); - } - return wrapperNodeApi[ServiceName]; - }, + const sessionService = createSessionServiceProxy(wrapperNodeApi.NodeIQQNTWrapperSession); + module.exports = nativeProxy(wrapperNodeApi, key => { + if (key === 'NodeIQQNTWrapperSession') return sessionService; + return Reflect.get(wrapperNodeApi, key, wrapperNodeApi); }); + // 先安装拦截,再异步加载 Framework。会话由 QQ 创建,不调用已移除的工厂或抢跑初始化。 + initializeNapCat().catch(error => console.log('[NapCat] [Error] 初始化NapCat', error)); + return dlopenRet; }; diff --git a/packages/napcat-framework/napcat.ts b/packages/napcat-framework/napcat.ts index 29604e77..e6519253 100644 --- a/packages/napcat-framework/napcat.ts +++ b/packages/napcat-framework/napcat.ts @@ -23,7 +23,7 @@ export async function getWebUiUrl () { } export async function NCoreInitFramework ( - session: NodeIQQNTWrapperSession, + session: NodeIQQNTWrapperSession | Promise, loginService: NodeIKernelLoginService, registerInitCallback: (callback: () => void) => void ) { @@ -176,7 +176,10 @@ export async function NCoreInitFramework ( // 过早进入会导致addKernelMsgListener等Listener添加失败 // await sleep(2500); // 初始化 NapCatFramework - const loaderObject = new NapCatFramework(wrapper, session, logger, selfInfo, basicInfoWrapper, pathWrapper, nativePacketHandler, napi2nativeLoader); + // 新版 wrapper 的会话由 QQ 在启动过程中创建,dlopen 时可能尚不存在。 + const wrapperSession = await session; + if (!wrapperSession) throw new Error('QQ 未提供有效的 Framework Session'); + const loaderObject = new NapCatFramework(wrapper, wrapperSession, logger, selfInfo, basicInfoWrapper, pathWrapper, nativePacketHandler, napi2nativeLoader); // 将捕获的 passphrase 设置到 core 上,供 DatabaseApi 使用 if (dbPassphrase) { loaderObject.core.dbPassphrase = dbPassphrase;