@napuketto/kernel 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
1
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
2
2
  import { readFile, rename, stat, writeFile } from "node:fs/promises";
3
3
  import { basename, dirname, extname, join, resolve } from "node:path";
4
4
  import { parse, stringify } from "smol-toml";
@@ -5167,6 +5167,185 @@ function writeProbeFile(result, file) {
5167
5167
  }
5168
5168
  }
5169
5169
  //#endregion
5170
+ //#region src/wrapper/probe/probe-listener.ts
5171
+ /**
5172
+ * listener 录制器(批次三前置探测,docs/protocols/milky.md §0.8.1、本包 design.md §10)
5173
+ *
5174
+ * 动机:无源事件缺的不是方法名而是回调证据——现有 bridge 只注册已知回调名,
5175
+ * 原生 listener 回调面之外的派发行为从未被观测。原理:原生 listener 是普通
5176
+ * JS 对象 + NAPI 按名反射回调(MsgBridge 模式),用 Proxy get 陷阱构造万能
5177
+ * 监听器,原生调什么记什么,不需要预先知道回调名。
5178
+ *
5179
+ * 开关:NAPUTO_LISTENER_RECORD=1(loader bootstrap 挂起,位置与 runProbePhase 相邻)。
5180
+ * 产物:NAPUTO_CFG_DIR/napuketto-listener-record.jsonl(JSONL 追加,注册过程
5181
+ * 逐行落盘,原生崩溃保留断点定位肇事服务)。
5182
+ *
5183
+ * 安全边界:记录函数只序列化入参后回调 onEvent,绝不反向调用原生;录制器与
5184
+ * 业务 bridge 并存(addKernelXListener 多监听各自独立 id),不干扰协议链路。
5185
+ */
5186
+ /** 默认仪器面:与 5 个无源事件 + sysmsg 样本采集相关的 listener 服务(design.md §10)。 */
5187
+ const INSTRUMENTED_SERVICE_GETTERS = [
5188
+ "getMsgService",
5189
+ "getGroupService",
5190
+ "getRecentContactService",
5191
+ "getBdhUploadService",
5192
+ "getOnlineStatusService",
5193
+ "getLockService",
5194
+ "getFileAssistantService",
5195
+ "getBuddyService",
5196
+ "getProfileLikeService",
5197
+ "getNodeMiscService"
5198
+ ];
5199
+ /** 服务 getter 前缀 / 后缀(getMsgService → Msg)。 */
5200
+ const GETTER_PREFIX_RE = /^get/;
5201
+ const SERVICE_SUFFIX_RE = /Service$/;
5202
+ /** listener 注册 / 注销方法名(addKernelMsgListener / 裸 addListener 同规则)。 */
5203
+ const ADD_LISTENER_RE = /^add(Kernel)?\w*Listener$/;
5204
+ const REMOVE_LISTENER_RE = /^remove(Kernel)?\w*Listener$/;
5205
+ /** 服务简称(getMsgService → Msg;getBdhUploadService → BdhUpload)。 */
5206
+ function serviceShortName(getter) {
5207
+ return getter.replace(GETTER_PREFIX_RE, "").replace(SERVICE_SUFFIX_RE, "");
5208
+ }
5209
+ /** 从服务方法面选 listener 注册方法(addKernelMsgListener / 裸 addListener 同规则)。 */
5210
+ function findAddMethod(service) {
5211
+ return listMethods(service).find((m) => ADD_LISTENER_RE.test(m)) ?? null;
5212
+ }
5213
+ /** 从服务方法面选 listener 注销方法(缺失允许——录制器不主动注销)。 */
5214
+ function findRemoveMethod(service) {
5215
+ return listMethods(service).find((m) => REMOVE_LISTENER_RE.test(m)) ?? null;
5216
+ }
5217
+ /**
5218
+ * 构造万能监听器:Proxy get 陷阱对任意字符串属性返回记录函数(同时记录原生
5219
+ * 查过的名字,即使从未调用);has / ownKeys 也记录(判定原生按名 Get 还是
5220
+ * 枚举发现回调);非字符串键(Symbol,如 util.inspect 触发)放行默认行为。
5221
+ */
5222
+ function createRecordingListener(service, onEvent) {
5223
+ const record = (event, args) => {
5224
+ onEvent({
5225
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
5226
+ service,
5227
+ event,
5228
+ args
5229
+ });
5230
+ };
5231
+ return new Proxy({}, {
5232
+ get(target, prop, receiver) {
5233
+ if (typeof prop !== "string") return Reflect.get(target, prop, receiver);
5234
+ record("<get>", [prop]);
5235
+ return (...args) => {
5236
+ record(prop, [...args].map((a) => serialize(a)));
5237
+ };
5238
+ },
5239
+ has(_target, prop) {
5240
+ if (typeof prop === "string") record("<has>", [prop]);
5241
+ return true;
5242
+ },
5243
+ ownKeys() {
5244
+ record("<ownKeys>", []);
5245
+ return [];
5246
+ }
5247
+ });
5248
+ }
5249
+ /**
5250
+ * 启动 listener 录制:session 十服务 + LoginService(exports 单例)逐服务注册
5251
+ * 万能监听器,JSONL 逐行追加。返回摘要;文件路径写入结果(bootstrap 日志用)。
5252
+ *
5253
+ * ⚠️ 调用时序约束:必须在任何探测(probeRuntime / getMainSession)之前调用——
5254
+ * resolveMainFromStartup 会把空壳主 session 写回 ctx.session(session-resolver.ts),
5255
+ * 空壳的 service getter 函数仍在但调用返回 null(late probe 61/63 null 的根因)。
5256
+ */
5257
+ function startListenerRecording(ctx, file = "napuketto-listener-record.jsonl") {
5258
+ const dir = process.env["NAPUTO_CFG_DIR"] ?? process.cwd();
5259
+ mkdirSync(dir, { recursive: true });
5260
+ const full = join(dir, file);
5261
+ writeFileSync(full, "", "utf8");
5262
+ const emit = (record) => {
5263
+ try {
5264
+ appendFileSync(full, `${JSON.stringify(record)}\n`, "utf8");
5265
+ } catch {}
5266
+ };
5267
+ const registered = {};
5268
+ const session = ctx.session;
5269
+ if (session !== null && session !== void 0) for (const getter of INSTRUMENTED_SERVICE_GETTERS) registered[serviceShortName(getter)] = recordService(session, getter, emit);
5270
+ registered["Login"] = recordLoginService(ctx, emit);
5271
+ return {
5272
+ file: full,
5273
+ registered
5274
+ };
5275
+ }
5276
+ /** 对单个服务注册万能监听器(getter 失败/null/无 add 方法均记录原因)。 */
5277
+ function recordService(session, getter, emit) {
5278
+ const name = serviceShortName(getter);
5279
+ try {
5280
+ const fn = session[getter];
5281
+ if (typeof fn !== "function") return {
5282
+ ok: false,
5283
+ error: `getter 缺失(typeof=${typeof fn})`
5284
+ };
5285
+ const service = fn.call(session);
5286
+ if (service === null || service === void 0) return {
5287
+ ok: false,
5288
+ error: "null/undefined"
5289
+ };
5290
+ return attachRecordingListener(name, service, emit);
5291
+ } catch (e) {
5292
+ return {
5293
+ ok: false,
5294
+ error: e instanceof Error ? e.message : String(e)
5295
+ };
5296
+ }
5297
+ }
5298
+ /** LoginService 单例录制(NodeIKernelLoginService.get(),bot_offline 候选源)。 */
5299
+ function recordLoginService(ctx, emit) {
5300
+ const ctor = ctx.exports["NodeIKernelLoginService"];
5301
+ try {
5302
+ if (ctor === null || ctor === void 0 || typeof ctor.get !== "function") return {
5303
+ ok: false,
5304
+ error: "NodeIKernelLoginService.get 缺失"
5305
+ };
5306
+ const service = ctor.get();
5307
+ if (service === null || service === void 0) return {
5308
+ ok: false,
5309
+ error: "null/undefined"
5310
+ };
5311
+ return attachRecordingListener("Login", service, emit);
5312
+ } catch (e) {
5313
+ return {
5314
+ ok: false,
5315
+ error: e instanceof Error ? e.message : String(e)
5316
+ };
5317
+ }
5318
+ }
5319
+ /** 在服务实例上挂万能监听器(add 方法自动选取;注册事件即行落盘)。 */
5320
+ function attachRecordingListener(name, service, emit) {
5321
+ const addMethod = findAddMethod(service);
5322
+ if (addMethod === null) return {
5323
+ ok: false,
5324
+ error: "无 add*Listener 方法"
5325
+ };
5326
+ const add = service[addMethod];
5327
+ if (typeof add !== "function") return {
5328
+ ok: false,
5329
+ error: `${addMethod} 非函数`
5330
+ };
5331
+ const listener = createRecordingListener(name, emit);
5332
+ const listenerId = add.call(service, listener);
5333
+ emit({
5334
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
5335
+ service: name,
5336
+ event: "<registered>",
5337
+ args: [{
5338
+ addMethod,
5339
+ removeMethod: findRemoveMethod(service),
5340
+ listenerId: serialize(listenerId)
5341
+ }]
5342
+ });
5343
+ return {
5344
+ ok: true,
5345
+ ...typeof listenerId === "number" ? { listenerId } : {}
5346
+ };
5347
+ }
5348
+ //#endregion
5170
5349
  //#region src/wrapper/wrapper-version.ts
5171
5350
  /**
5172
5351
  * QQ 安装目录探测(ADR-018)
@@ -5224,6 +5403,6 @@ function listQQVersions(installDir) {
5224
5403
  }).sort().reverse();
5225
5404
  }
5226
5405
  //#endregion
5227
- export { BuddyCache, ChatType, ConfigBase, ConfigError, DEFAULT_DATA_ROOT_NAME, EXIT_CONFIG_INVALID, ElementType, FriendApi, FriendBridge, GrayTipSubType, GroupApi, GroupBridge, GroupCache, GroupListUpdateType, GroupNotifyApi, GroupNotifyMsgStatus, GroupNotifyMsgType, KERNEL_ERROR_CODES, KernelError, MAIN_CONFIG_FILE, MsgApi, MsgBridge, NTEventChannel, NTGroupMemberRole, NTGroupRequestOperateTypes, NapukettoCore, PathWrapper, ProfileApi, ProfileLikeApi, QrLoginSession, RecentContactApi, RichMediaApi, TicketApi, TipGroupElementType, WebApi, WebHonorType, buildEngineConfig, buildLoginConfig, buildSessionConfig, configError, createCoreContext, createSessionListener as createLifecycleSessionListener, createSessionListener, createLogger, createLoginListener, createSession, createWrapper, electronProcessType, getExistingSession, getMainSession, initAndStartSession, initEngine, initSession, isKernelError, kernelError, listLoginAccounts, listQQVersions, narrowBuddyCategories, parseAppidFromMajor, parseLogConfig, parseToml, probeRuntime, quickLogin, readMachineGuid, resolveAppidQua, resolveConfigPath, resolveDataRoot, resolveQQVersion, resolveQqGlobalPath, resolveQqUserDataRoot, resolveWrapperPath, startNapuketto, startSession, stringifyToml, toCanonicalElements, toSendElements, waitForNetworkConnection, waitSessionReady };
5406
+ export { BuddyCache, ChatType, ConfigBase, ConfigError, DEFAULT_DATA_ROOT_NAME, EXIT_CONFIG_INVALID, ElementType, FriendApi, FriendBridge, GrayTipSubType, GroupApi, GroupBridge, GroupCache, GroupListUpdateType, GroupNotifyApi, GroupNotifyMsgStatus, GroupNotifyMsgType, KERNEL_ERROR_CODES, KernelError, MAIN_CONFIG_FILE, MsgApi, MsgBridge, NTEventChannel, NTGroupMemberRole, NTGroupRequestOperateTypes, NapukettoCore, PathWrapper, ProfileApi, ProfileLikeApi, QrLoginSession, RecentContactApi, RichMediaApi, TicketApi, TipGroupElementType, WebApi, WebHonorType, buildEngineConfig, buildLoginConfig, buildSessionConfig, configError, createCoreContext, createSessionListener as createLifecycleSessionListener, createSessionListener, createLogger, createLoginListener, createRecordingListener, createSession, createWrapper, electronProcessType, getExistingSession, getMainSession, initAndStartSession, initEngine, initSession, isKernelError, kernelError, listLoginAccounts, listQQVersions, narrowBuddyCategories, parseAppidFromMajor, parseLogConfig, parseToml, probeRuntime, quickLogin, readMachineGuid, resolveAppidQua, resolveConfigPath, resolveDataRoot, resolveQQVersion, resolveQqGlobalPath, resolveQqUserDataRoot, resolveWrapperPath, startListenerRecording, startNapuketto, startSession, stringifyToml, toCanonicalElements, toSendElements, waitForNetworkConnection, waitSessionReady };
5228
5407
 
5229
5408
  //# sourceMappingURL=index.mjs.map