@napuketto/kernel 0.0.1

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 ADDED
@@ -0,0 +1,3377 @@
1
+ import { get } from "node:https";
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
3
+ import { dirname, extname, join, resolve } from "node:path";
4
+ import process from "node:process";
5
+ import pino from "pino";
6
+ import pinoPretty from "pino-pretty";
7
+ import { homedir, hostname } from "node:os";
8
+ import { fileURLToPath } from "node:url";
9
+ import { EventEmitter } from "node:events";
10
+ import { readFile, rename, writeFile } from "node:fs/promises";
11
+ import { parse, stringify } from "smol-toml";
12
+ //#region src/infra/errors.ts
13
+ /**
14
+ * 全部错误码集合。协议层映射表可据此推导:
15
+ * `Record<(typeof KERNEL_ERROR_CODES)[number], number>`。
16
+ */
17
+ const KERNEL_ERROR_CODES = [
18
+ "SEND_FAILED",
19
+ "PERMISSION_DENIED",
20
+ "NOT_FOUND",
21
+ "TIMEOUT",
22
+ "NOT_LOGIN",
23
+ "INVALID_PARAM",
24
+ "INVALID_STATE",
25
+ "UNKNOWN"
26
+ ];
27
+ /** kernel 类型化错误:apis / 基础设施层抛出的统一错误形态。 */
28
+ var KernelError = class extends Error {
29
+ code;
30
+ constructor(message, code = "UNKNOWN", options) {
31
+ super(message, options);
32
+ this.name = "KernelError";
33
+ this.code = code;
34
+ }
35
+ };
36
+ /** 便捷构造:`kernelError('消息不存在', 'NOT_FOUND', { cause })`。 */
37
+ function kernelError(message, code = "UNKNOWN", options) {
38
+ return new KernelError(message, code, options);
39
+ }
40
+ /** 类型守卫:协议层 catch 统一映射时区分 KernelError 与意外错误。 */
41
+ function isKernelError(err) {
42
+ return err instanceof KernelError;
43
+ }
44
+ //#endregion
45
+ //#region src/apis/friend.ts
46
+ /**
47
+ * FriendApi:好友语义化 API(ADR-009 统一错误语义)
48
+ *
49
+ * 内部解包原生返回:成功返回纯业务值,失败抛 KernelError。
50
+ * 方法面(P2-4 + P2-11):好友列表(uid + 昵称/备注)+ 好友请求
51
+ * (getBuddyReq / approvalFriendRequest / setBuddyRemark / delBuddy /
52
+ * getDoubtBuddyReq / approvalDoubtBuddyReq)。
53
+ */
54
+ /** 原生 result 解包(result 字段非 0 抛 KernelError)。 */
55
+ function unwrap$3(label, result, errMsg) {
56
+ if (result === 0) return;
57
+ throw kernelError(`${label} 失败: ${errMsg ?? "无错误详情"}`, "UNKNOWN");
58
+ }
59
+ /** 好友 API:从 session 拿 buddy service,包装成语义化方法。 */
60
+ var FriendApi = class {
61
+ service;
62
+ uidToUin;
63
+ constructor(session, opts = {}) {
64
+ const service = session.getBuddyService();
65
+ if (service === null || service === void 0) throw kernelError("getBuddyService() 返回空(session 未 init)", "INVALID_STATE");
66
+ this.service = service;
67
+ this.uidToUin = opts.uidToUin;
68
+ }
69
+ /** 好友列表(分类拍平为 Friend[];昵称/备注从 service 缓存读取)。 */
70
+ async getFriendList() {
71
+ let categories;
72
+ try {
73
+ categories = await this.service.getBuddyListFromCache(0);
74
+ } catch {
75
+ const raw = await this.service.getBuddyListV2("cli", true, 0);
76
+ if (raw.result !== 0) throw kernelError(`getBuddyListV2 失败: ${raw.errMsg}`, "UNKNOWN");
77
+ categories = raw.data;
78
+ }
79
+ const uids = categories.flatMap((cat) => cat.buddyUids);
80
+ let uinMap = /* @__PURE__ */ new Map();
81
+ if (this.uidToUin !== void 0) uinMap = await this.uidToUin(uids);
82
+ const out = [];
83
+ for (const uid of uids) out.push({
84
+ uid,
85
+ uin: uinMap.get(uid) ?? uid,
86
+ nickname: this.service.getBuddyNick(uid),
87
+ remark: this.service.getBuddyRemark(uid)
88
+ });
89
+ return out;
90
+ }
91
+ /** 带分类的好友列表(get_friends_with_category,分类保留)。 */
92
+ async getFriendCategories() {
93
+ const categories = await this.getBuddyCategories();
94
+ const friendMap = /* @__PURE__ */ new Map();
95
+ for (const friend of await this.getFriendList()) friendMap.set(friend.uid, friend);
96
+ return categories.map((cat) => {
97
+ const buddies = [];
98
+ for (const uid of cat.buddyUids) {
99
+ const friend = friendMap.get(uid);
100
+ if (friend !== void 0) buddies.push(friend);
101
+ }
102
+ return {
103
+ categoryId: cat.categoryId,
104
+ categorySortId: cat.categorySortId,
105
+ categoryName: cat.categroyName,
106
+ categoryMbCount: cat.categroyMbCount,
107
+ onlineCount: cat.onlineCount,
108
+ buddyList: buddies
109
+ };
110
+ });
111
+ }
112
+ /** 原始分类(getBuddyListV2 / 缓存)。 */
113
+ async getBuddyCategories() {
114
+ try {
115
+ return await this.service.getBuddyListFromCache(0);
116
+ } catch {
117
+ const raw = await this.service.getBuddyListV2("cli", true, 0);
118
+ if (raw.result !== 0) throw kernelError(`getBuddyListV2 失败: ${raw.errMsg}`, "UNKNOWN");
119
+ return raw.data;
120
+ }
121
+ }
122
+ /** 判断是否好友。 */
123
+ isBuddy(uid) {
124
+ return this.service.isBuddy(uid);
125
+ }
126
+ /** 好友申请列表(set_friend_add_request 应答前查找匹配项)。 */
127
+ async getBuddyReqList() {
128
+ const raw = await this.service.getBuddyReq();
129
+ unwrap$3("getBuddyReq", raw.result, raw.errMsg);
130
+ return raw.buddyReqs ?? [];
131
+ }
132
+ /** 应答好友申请(accept=true 同意;remark 由调用方另行 setFriendRemark)。 */
133
+ async handleFriendRequest(notify, accept) {
134
+ await this.service.approvalFriendRequest({
135
+ friendUid: notify.friendUid,
136
+ reqTime: notify.reqTime,
137
+ accept
138
+ });
139
+ }
140
+ /** 设置好友备注(void 语义乐观处理)。 */
141
+ setFriendRemark(uid, remark) {
142
+ this.service.setBuddyRemark({
143
+ uid,
144
+ remark
145
+ });
146
+ }
147
+ /** 删除好友(tempBlock=拉黑,tempBothDel=双向删除)。 */
148
+ async deleteFriend(uid, tempBlock = false, tempBothDel = false) {
149
+ const res = await this.service.delBuddy({
150
+ friendUid: uid,
151
+ tempBlock,
152
+ tempBothDel
153
+ });
154
+ if (res !== void 0 && res !== null && typeof res.result === "number" && res.result !== 0) throw kernelError(`delBuddy 失败: ${String(res.errMsg ?? "")}`, "UNKNOWN");
155
+ }
156
+ /** 可疑好友申请列表(get_doubt_friends_add_request;uin 经 uidToUin 转换)。 */
157
+ async getDoubtFriendRequest(count) {
158
+ const reqId = String(Date.now());
159
+ const raw = await this.service.getDoubtBuddyReq(reqId, count, "");
160
+ unwrap$3("getDoubtBuddyReq", raw.result, raw.errMsg);
161
+ const list = raw.doubtList ?? [];
162
+ let uinMap = /* @__PURE__ */ new Map();
163
+ if (this.uidToUin !== void 0 && list.length > 0) uinMap = await this.uidToUin(list.map((item) => item.uid));
164
+ const out = [];
165
+ for (const item of list) out.push(this.toDoubtFriendRequestInfo(item, uinMap));
166
+ return out;
167
+ }
168
+ /** 单项转换:DoubtBuddyReq → OB11 结构(可选字段条件赋值,exactOptionalPropertyTypes)。 */
169
+ toDoubtFriendRequestInfo(item, uinMap) {
170
+ const info = {
171
+ flag: item.uid,
172
+ uin: Number(uinMap.get(item.uid) ?? item.uid),
173
+ type: "doubt"
174
+ };
175
+ if (item.nick !== void 0) info.nick = item.nick;
176
+ if (item.source !== void 0) info.source = item.source;
177
+ if (item.reason !== void 0) info.reason = item.reason;
178
+ if (item.msg !== void 0) info.msg = item.msg;
179
+ if (item.groupCode !== void 0) info.group_code = item.groupCode;
180
+ if (item.reqTime !== void 0) info.time = item.reqTime;
181
+ return info;
182
+ }
183
+ /** 处理可疑好友申请(set_doubt_friends_add_request;uid=flag)。 */
184
+ handleDoubtFriendRequest(uid) {
185
+ this.service.approvalDoubtBuddyReq(uid, "", "");
186
+ }
187
+ };
188
+ //#endregion
189
+ //#region src/types/entities.ts
190
+ /**
191
+ * 实体类型层(运行时探测产物 + 公开资料作说明书,ADR-006)
192
+ *
193
+ * ⚠️ 部分字段按公开资料(wrapper 外部契约)自研描述,待下一次进程内
194
+ * 探测(probe.ts)校准——接口签名是外部系统事实,自研描述其形状。
195
+ */
196
+ /** 会话类型(QQ wrapper 外部契约)。 */
197
+ const ChatType = {
198
+ C2C: 1,
199
+ GROUP: 2,
200
+ DISC: 3,
201
+ GUILD: 4,
202
+ C2C_TEMP: 100
203
+ };
204
+ /** 灰色提示子类型(系统事件类型,QQ wrapper 契约)。 */
205
+ const GrayTipSubType = {
206
+ REVOKE: 1,
207
+ PROCLAMATION: 2,
208
+ GROUP: 4,
209
+ BUDDY: 5,
210
+ FEED: 6,
211
+ ESSENCE: 7,
212
+ GROUP_NOTIFY: 8,
213
+ BUDDY_NOTIFY: 9,
214
+ FILE: 10,
215
+ JSON: 17
216
+ };
217
+ /** 群成员变动类型(TipGroupElement.type)。 */
218
+ const TipGroupElementType = {
219
+ MEMBER_ADD: 1,
220
+ DISBANDED: 2,
221
+ QUIT: 3,
222
+ CREATED: 4,
223
+ GROUP_NAME_MODIFIED: 5,
224
+ BLOCK: 6,
225
+ UNBLOCK: 7,
226
+ SHUT_UP: 8
227
+ };
228
+ //#endregion
229
+ //#region src/types/services/group-service.ts
230
+ /** 群成员角色(QQ wrapper 契约)。 */
231
+ const NTGroupMemberRole = {
232
+ UNSPECIFIED: 0,
233
+ STRANGER: 1,
234
+ MEMBER: 2,
235
+ ADMIN: 3,
236
+ OWNER: 4
237
+ };
238
+ /** 群通知类型(GroupNotifyMsgType,说明书参考)。 */
239
+ const GroupNotifyMsgType = {
240
+ UN_SPECIFIED: 0,
241
+ INVITED_BY_MEMBER: 1,
242
+ REFUSE_INVITED: 2,
243
+ REFUSED_BY_ADMINI_STRATOR: 3,
244
+ AGREED_TOJOIN_DIRECT: 4,
245
+ INVITED_NEED_ADMINI_STRATOR_PASS: 5,
246
+ AGREED_TO_JOIN_BY_ADMINI_STRATOR: 6,
247
+ REQUEST_JOIN_NEED_ADMINI_STRATOR_PASS: 7,
248
+ SET_ADMIN: 8,
249
+ KICK_MEMBER_NOTIFY_ADMIN: 9,
250
+ KICK_MEMBER_NOTIFY_KICKED: 10,
251
+ MEMBER_LEAVE_NOTIFY_ADMIN: 11,
252
+ CANCEL_ADMIN_NOTIFY_CANCELED: 12,
253
+ CANCEL_ADMIN_NOTIFY_ADMIN: 13,
254
+ TRANSFER_GROUP_NOTIFY_OLDOWNER: 14,
255
+ TRANSFER_GROUP_NOTIFY_ADMIN: 15
256
+ };
257
+ /** 群通知处理状态(GroupNotifyMsgStatus,说明书参考)。 */
258
+ const GroupNotifyMsgStatus = {
259
+ KINIT: 0,
260
+ KUNHANDLE: 1,
261
+ KAGREED: 2,
262
+ KREFUSED: 3,
263
+ KIGNORED: 4
264
+ };
265
+ /** 群请求操作类型(NTGroupRequestOperateTypes,说明书参考)。 */
266
+ const NTGroupRequestOperateTypes = {
267
+ KUNSPECIFIED: 0,
268
+ KAGREE: 1,
269
+ KREFUSE: 2,
270
+ KIGNORE: 3,
271
+ KDELETE: 4
272
+ };
273
+ //#endregion
274
+ //#region src/apis/group.ts
275
+ /**
276
+ * GroupApi:群语义化 API(ADR-009 统一错误语义)
277
+ *
278
+ * 内部解包原生返回:成功返回纯业务值,失败抛 KernelError。
279
+ * 方法面(P2-4 + P2-10):群列表 / 群详情 / 成员列表 / 成员详情 / uin↔uid +
280
+ * 群管操作(踢人 / 禁言 / 管理员 / 名片 / 群名 / 退群 / 精华消息 / @all 剩余)。
281
+ */
282
+ /** 原生 result 解包(result 字段非 0 抛 KernelError)。 */
283
+ function unwrap$2(label, result, errMsg) {
284
+ if (result === 0) return;
285
+ throw kernelError(`${label} 失败: ${errMsg ?? "无错误详情"}`, "UNKNOWN");
286
+ }
287
+ /** 群 API:从 session 拿 group service,包装成语义化方法。 */
288
+ var GroupApi = class {
289
+ service;
290
+ /** 精华消息需要 msg service(按 msgId 拉消息取 msgSeq/msgRandom)。 */
291
+ msgService;
292
+ constructor(session) {
293
+ const service = session.getGroupService();
294
+ if (service === null || service === void 0) throw kernelError("getGroupService() 返回空(session 未 init)", "INVALID_STATE");
295
+ this.service = service;
296
+ const msgService = session.getMsgService();
297
+ if (msgService === null || msgService === void 0) throw kernelError("getMsgService() 返回空(session 未 init)", "INVALID_STATE");
298
+ this.msgService = msgService;
299
+ }
300
+ /** 群列表(force=true 强制刷新,默认缓存)。 */
301
+ async getGroupList(force = false) {
302
+ const raw = await this.service.getGroupList(force);
303
+ if (raw.result !== 0) unwrap$2("getGroupList", raw.result, raw.errMsg);
304
+ const list = raw.groupList;
305
+ if (Array.isArray(list)) return list;
306
+ return [];
307
+ }
308
+ /** 群详情(groupCode 为群号字符串)。
309
+ * getGroupDetailInfo 返回形状:失败 result=错误码;成功详情可能直接在 result
310
+ * 对象里,也可能在 groupInfo/detailInfo 字段——三种形状都兼容(探测校准)。
311
+ * 2026-08-07 修复:成功(result=0)时不再把 {result:0,errMsg} 原样当详情返回,
312
+ * 而是提取详情字段;形状异常抛 UNKNOWN 而非静默返回垃圾数据。 */
313
+ async getGroupInfo(groupCode) {
314
+ const raw = await this.service.getGroupDetailInfo(groupCode, 0);
315
+ const { result, errMsg } = raw;
316
+ if (typeof result === "number" && result !== 0) unwrap$2("getGroupDetailInfo", result, errMsg);
317
+ if (result !== null && typeof result === "object") return result;
318
+ const detail = raw;
319
+ const candidate = detail.groupInfo ?? detail.detailInfo ?? detail.info;
320
+ if (candidate !== void 0 && candidate !== null && typeof candidate === "object") return candidate;
321
+ throw kernelError(`getGroupDetailInfo 返回形状异常(groupCode=${groupCode}): ${errMsg}`, "UNKNOWN");
322
+ }
323
+ /** 群成员列表(forceFetch=true 强制拉取,默认缓存优先)。 */
324
+ async getGroupMemberList(groupCode, forceFetch = false) {
325
+ const raw = await this.service.getAllMemberList(groupCode, forceFetch);
326
+ if (raw.errCode !== 0) throw kernelError(`getAllMemberList 失败: ${raw.errMsg}`, "UNKNOWN");
327
+ return [...raw.result.infos.values()];
328
+ }
329
+ /** 群成员详情(uids 为空返回空数组)。 */
330
+ async getGroupMemberInfo(groupCode, uids) {
331
+ if (uids.length === 0) return [];
332
+ const raw = await this.service.getMemberInfo(groupCode, uids, true);
333
+ unwrap$2("getMemberInfo", raw.result, raw.errMsg);
334
+ const { infos } = raw;
335
+ if (infos instanceof Map) return [...infos.values()];
336
+ return [];
337
+ }
338
+ /** uin → uid(私聊发送等需要)。 */
339
+ async uinToUid(uins) {
340
+ if (uins.length === 0) return /* @__PURE__ */ new Map();
341
+ const raw = await this.service.getUidByUins(uins);
342
+ if (raw.errCode !== 0) throw kernelError(`getUidByUins 失败: ${raw.errMsg}`, "UNKNOWN");
343
+ return raw.uids;
344
+ }
345
+ /** uid → uin。 */
346
+ async uidToUin(uids) {
347
+ if (uids.length === 0) return /* @__PURE__ */ new Map();
348
+ const raw = await this.service.getUinByUids(uids);
349
+ if (raw.errCode !== 0) throw kernelError(`getUinByUids 失败: ${raw.errMsg}`, "UNKNOWN");
350
+ return raw.uins;
351
+ }
352
+ /** 群成员角色 → OB11 role 字符串。 */
353
+ static roleToString(role) {
354
+ if (role === NTGroupMemberRole.OWNER) return "owner";
355
+ if (role === NTGroupMemberRole.ADMIN) return "admin";
356
+ return "member";
357
+ }
358
+ /**
359
+ * 踢人(set_group_kick)。memberUids 为成员 uid;refuseForever=true 拉黑拒绝再次入群。
360
+ * kickMemberV2 的 kickFlag/kickList 字段值待探测校准(TODO P3 联调)。
361
+ */
362
+ async kickMember(groupCode, memberUids, refuseForever, reason = "") {
363
+ if (memberUids.length === 0) throw kernelError("kickMember 需要至少一个成员 uid", "INVALID_PARAM");
364
+ let optFlag = 1;
365
+ if (refuseForever) optFlag = 0;
366
+ const req = {
367
+ groupCode,
368
+ kickFlag: 0,
369
+ kickList: memberUids.map((uid) => ({
370
+ optFlag,
371
+ optOperate: 0,
372
+ optMemberUid: uid,
373
+ optBytesMsg: ""
374
+ })),
375
+ kickListUids: memberUids,
376
+ kickMsg: reason
377
+ };
378
+ const raw = await this.service.kickMemberV2(req);
379
+ unwrap$2("kickMemberV2", raw.result, raw.errMsg);
380
+ }
381
+ /** 成员禁言(duration 秒,0 解除禁言)。 */
382
+ async setMemberShutUp(groupCode, members) {
383
+ if (members.length === 0) throw kernelError("setMemberShutUp 需要至少一个成员", "INVALID_PARAM");
384
+ const raw = await this.service.setMemberShutUp(groupCode, members.map((m) => ({
385
+ uid: m.uid,
386
+ timeStamp: m.duration
387
+ })));
388
+ unwrap$2("setMemberShutUp", raw.result, raw.errMsg);
389
+ }
390
+ /** 全员禁言(enable=true 开启全群禁言)。 */
391
+ async setGroupShutUp(groupCode, enable) {
392
+ const raw = await this.service.setGroupShutUp(groupCode, enable);
393
+ unwrap$2("setGroupShutUp", raw.result, raw.errMsg);
394
+ }
395
+ /** 设置管理员(role=ADMIN 设管理,MEMBER 取消;void 语义乐观处理)。 */
396
+ setMemberRole(groupCode, uid, role) {
397
+ this.service.modifyMemberRole(groupCode, uid, role);
398
+ }
399
+ /** 设置群名片(void 语义乐观处理)。 */
400
+ setMemberCardName(groupCode, uid, cardName) {
401
+ this.service.modifyMemberCardName(groupCode, uid, cardName);
402
+ }
403
+ /** 修改群名。 */
404
+ async modifyGroupName(groupCode, groupName) {
405
+ const raw = await this.service.modifyGroupName(groupCode, groupName, false);
406
+ unwrap$2("modifyGroupName", raw.result, raw.errMsg);
407
+ }
408
+ /** 退群(needDeleteLocalMsg=is_dismiss:是否解散群,仅群主)。 */
409
+ async quitGroup(groupCode, needDeleteLocalMsg = false) {
410
+ const raw = await this.service.quitGroupV2({
411
+ groupCode,
412
+ needDeleteLocalMsg
413
+ });
414
+ unwrap$2("quitGroupV2", raw.result, raw.errMsg);
415
+ }
416
+ /** 设置精华消息(msgId 经 msgService 取 msgSeq/msgRandom)。 */
417
+ async addGroupEssence(groupCode, msgId) {
418
+ await this.setEssence(groupCode, msgId, true);
419
+ }
420
+ /** 取消精华消息。 */
421
+ async removeGroupEssence(groupCode, msgId) {
422
+ await this.setEssence(groupCode, msgId, false);
423
+ }
424
+ /** 精华消息公共流程:按 msgId 拉消息 → add/removeGroupEssence。 */
425
+ async setEssence(groupCode, msgId, add) {
426
+ const peer = {
427
+ chatType: ChatType.GROUP,
428
+ peerUid: groupCode
429
+ };
430
+ const raw = await this.msgService.getMsgsByMsgId(peer, [msgId]);
431
+ unwrap$2("getMsgsByMsgId", raw.result, raw.errMsg);
432
+ const msg = raw.msgList?.[0];
433
+ if (msg === void 0) throw kernelError(`消息 ${msgId} 不存在`, "NOT_FOUND");
434
+ const param = {
435
+ groupCode,
436
+ msgRandom: Number(msg.msgRandom ?? 0),
437
+ msgSeq: Number(msg.msgSeq)
438
+ };
439
+ let label = "removeGroupEssence";
440
+ let res;
441
+ if (add) {
442
+ label = "addGroupEssence";
443
+ res = await this.service.addGroupEssence(param);
444
+ } else res = await this.service.removeGroupEssence(param);
445
+ if (res !== void 0 && res !== null && typeof res.result === "number" && res.result !== 0) throw kernelError(`${label} 失败: ${String(res.errMsg ?? "")}`, "UNKNOWN");
446
+ }
447
+ /** @all 剩余次数(get_group_at_all_remain)。 */
448
+ async getGroupRemainAtTimes(groupCode) {
449
+ const raw = await this.service.getGroupRemainAtTimes(groupCode);
450
+ if (raw.errCode !== 0) throw kernelError(`getGroupRemainAtTimes 失败: ${raw.errMsg}`, "UNKNOWN");
451
+ return {
452
+ canAtAll: raw.atInfo.canAtAll,
453
+ remainAtAllCountForUin: raw.atInfo.remainAtAllCountForUin,
454
+ remainAtAllCountForGroup: raw.atInfo.remainAtAllCountForGroup
455
+ };
456
+ }
457
+ };
458
+ //#endregion
459
+ //#region src/apis/group-notify.ts
460
+ /**
461
+ * GroupNotifyApi:群通知/禁言列表语义化 API(ADR-009 统一错误语义,P2-13)
462
+ *
463
+ * get_group_system_msg / set_group_add_request / get_group_shut_list 共用。
464
+ * getSingleScreenNotifies / getGroupShutUpMemberList 的列表可能走 listener 回调,
465
+ * 直接调用返回形状待探测校准:兼容 result 数组 / 字段 / 直接数组。
466
+ */
467
+ /** 原生 result 解包(result 字段非 0 抛 KernelError)。 */
468
+ function unwrap$1(label, result, errMsg) {
469
+ if (result === 0) return;
470
+ throw kernelError(`${label} 失败: ${errMsg ?? "无错误详情"}`, "UNKNOWN");
471
+ }
472
+ /** 群通知 API:从 session 拿 group service,包装成语义化方法。 */
473
+ var GroupNotifyApi = class {
474
+ service;
475
+ constructor(session) {
476
+ const service = session.getGroupService();
477
+ if (service === null || service === void 0) throw kernelError("getGroupService() 返回空(session 未 init)", "INVALID_STATE");
478
+ this.service = service;
479
+ }
480
+ /** 单屏通知列表(get_group_system_msg / set_group_add_request 共用)。 */
481
+ async getSingleScreenNotifies(doubt, count) {
482
+ return extractNotifyList((await this.service.getSingleScreenNotifies(doubt, "", count)).result);
483
+ }
484
+ /** 禁言成员列表(get_group_shut_list)。 */
485
+ async getGroupShutUpMemberList(groupCode) {
486
+ const raw = await this.service.getGroupShutUpMemberList(groupCode);
487
+ if (raw.result !== 0) unwrap$1("getGroupShutUpMemberList", raw.result, raw.errMsg);
488
+ return extractMemberList(raw.result);
489
+ }
490
+ /** 处理群请求(set_group_add_request 应答;approve=true 同意 / false 拒绝)。 */
491
+ async handleGroupRequest(doubt, notify, operateType, reason) {
492
+ const { group, seq, type } = notify;
493
+ let groupCode = "";
494
+ if (group !== void 0) ({groupCode} = group);
495
+ let postscript = reason;
496
+ if (postscript === "") postscript = " ";
497
+ await this.service.operateSysNotify(doubt, {
498
+ operateType,
499
+ targetMsg: {
500
+ seq,
501
+ type,
502
+ groupCode,
503
+ postscript
504
+ }
505
+ });
506
+ }
507
+ };
508
+ /** 从 getSingleScreenNotifies 返回提取通知数组(兼容 result 数组 / notifies 字段)。 */
509
+ function extractNotifyList(result) {
510
+ if (Array.isArray(result)) return result;
511
+ if (result !== null && typeof result === "object") {
512
+ const obj = result;
513
+ if (Array.isArray(obj.notifies)) return obj.notifies;
514
+ if (Array.isArray(obj.groupNotifies)) return obj.groupNotifies;
515
+ }
516
+ return [];
517
+ }
518
+ /** 从 getGroupShutUpMemberList 返回提取成员数组(兼容 result 数组 / memberList 字段)。 */
519
+ function extractMemberList(result) {
520
+ if (Array.isArray(result)) return result;
521
+ if (result !== null && typeof result === "object") {
522
+ const obj = result;
523
+ if (Array.isArray(obj.memberList)) return obj.memberList;
524
+ if (Array.isArray(obj.list)) return obj.list;
525
+ }
526
+ return [];
527
+ }
528
+ //#endregion
529
+ //#region src/types/services/msg-service.ts
530
+ /** 消息元素类型枚举(QQ wrapper 外部契约,自研描述)。 */
531
+ const ElementType = {
532
+ UNKNOWN: 0,
533
+ TEXT: 1,
534
+ PIC: 2,
535
+ FILE: 3,
536
+ PTT: 4,
537
+ VIDEO: 5,
538
+ FACE: 6,
539
+ REPLY: 7,
540
+ GRAY_TIP: 8,
541
+ WALLET: 9,
542
+ ARK: 10,
543
+ MFACE: 11,
544
+ LIVE_GIFT: 12,
545
+ STRUCT_LONG_MSG: 13,
546
+ MARKDOWN: 14,
547
+ GIPHY: 15,
548
+ MULTI_FORWARD: 16,
549
+ INLINE_KEYBOARD: 17,
550
+ INTEXT_GIFT: 18,
551
+ CALENDAR: 19
552
+ };
553
+ //#endregion
554
+ //#region src/types/message-element.ts
555
+ /**
556
+ * canonical 消息元素模型(协议无关,ADR-008 延伸)
557
+ *
558
+ * 描述 QQ 消息的事实结构,是各协议翻译层的公共中间表示:
559
+ * kernel 只做一次 NT ↔ canonical(toCanonicalElements / toSendElements),
560
+ * 各协议(onebot11/satori)只写薄映射 canonical ↔ 协议格式。
561
+ */
562
+ /** 文本 at 类型(QQ wrapper 契约,自研描述)。 */
563
+ const AtType = {
564
+ ALL: 1,
565
+ ONE: 2,
566
+ ME: 4
567
+ };
568
+ /** TEXT 元素 → canonical(at 或纯文本)。 */
569
+ function textToCanonical(textEl) {
570
+ if (textEl === void 0) return {
571
+ type: "text",
572
+ text: ""
573
+ };
574
+ const { atType, atUid, content } = textEl;
575
+ if (atType === AtType.ALL) return {
576
+ type: "at",
577
+ target: "all"
578
+ };
579
+ if (atType === AtType.ONE || atType === AtType.ME) {
580
+ const base = {
581
+ type: "at",
582
+ target: atUid ?? content
583
+ };
584
+ const display = content.startsWith("@") ? content.slice(1) : content;
585
+ if (display === "") return base;
586
+ return {
587
+ ...base,
588
+ display
589
+ };
590
+ }
591
+ return {
592
+ type: "text",
593
+ text: content
594
+ };
595
+ }
596
+ /** PIC 元素 → canonical image。 */
597
+ function picToCanonical(pic) {
598
+ if (pic === void 0) return {
599
+ type: "image",
600
+ path: ""
601
+ };
602
+ const path = pic.picPath ?? pic.picUrl ?? "";
603
+ const url = pic.picUrl;
604
+ if (url === void 0) return {
605
+ type: "image",
606
+ path
607
+ };
608
+ return {
609
+ type: "image",
610
+ path,
611
+ url
612
+ };
613
+ }
614
+ /** FACE 元素 → canonical face。 */
615
+ function faceToCanonical(face) {
616
+ if (face === void 0) return {
617
+ type: "face",
618
+ id: "0"
619
+ };
620
+ return {
621
+ type: "face",
622
+ id: String(face.faceIndex)
623
+ };
624
+ }
625
+ /** PTT 元素 → canonical voice。 */
626
+ function pttToCanonical(ptt) {
627
+ if (ptt === void 0) return {
628
+ type: "voice",
629
+ path: ""
630
+ };
631
+ return {
632
+ type: "voice",
633
+ path: ptt.filePath ?? ""
634
+ };
635
+ }
636
+ /** VIDEO 元素 → canonical video。 */
637
+ function videoToCanonical(video) {
638
+ if (video === void 0) return {
639
+ type: "video",
640
+ path: ""
641
+ };
642
+ const path = video.filePath ?? video.videoUrl ?? "";
643
+ const url = video.videoUrl;
644
+ if (url === void 0) return {
645
+ type: "video",
646
+ path
647
+ };
648
+ return {
649
+ type: "video",
650
+ path,
651
+ url
652
+ };
653
+ }
654
+ /** FILE 元素 → canonical file。 */
655
+ function fileToCanonical(file) {
656
+ if (file === void 0) return {
657
+ type: "file",
658
+ path: ""
659
+ };
660
+ const path = file.filePath ?? "";
661
+ const name = file.fileName;
662
+ if (name === void 0) return {
663
+ type: "file",
664
+ path
665
+ };
666
+ return {
667
+ type: "file",
668
+ path,
669
+ name
670
+ };
671
+ }
672
+ /** REPLY 元素 → canonical reply。 */
673
+ function replyToCanonical(reply) {
674
+ if (reply === void 0) return {
675
+ type: "reply",
676
+ messageId: ""
677
+ };
678
+ return {
679
+ type: "reply",
680
+ messageId: reply.replayMsgId
681
+ };
682
+ }
683
+ /** 单个 NT 元素 → canonical(接收方向,宽容:无法表达回 unknown,不抛错)。 */
684
+ function toCanonicalElement(el) {
685
+ switch (el.elementType) {
686
+ case ElementType.TEXT: return textToCanonical(el.textElement);
687
+ case ElementType.PIC: return picToCanonical(el.picElement);
688
+ case ElementType.FACE: return faceToCanonical(el.faceElement);
689
+ case ElementType.PTT: return pttToCanonical(el.pttElement);
690
+ case ElementType.VIDEO: return videoToCanonical(el.videoElement);
691
+ case ElementType.FILE: return fileToCanonical(el.fileElement);
692
+ case ElementType.REPLY: return replyToCanonical(el.replyElement);
693
+ default: return {
694
+ type: "unknown",
695
+ raw: el
696
+ };
697
+ }
698
+ }
699
+ /** NT RawMessage → canonical 元素(与 toSendElements 对称,接收方向)。 */
700
+ function toCanonicalElements(msg) {
701
+ return msg.elements.map(toCanonicalElement);
702
+ }
703
+ /** canonical 元素 → NT 发送元素(text/at/face/image/voice/reply 核心五类)。 */
704
+ function toSendElements(elements) {
705
+ const out = [];
706
+ for (const el of elements) switch (el.type) {
707
+ case "text":
708
+ out.push({
709
+ elementType: ElementType.TEXT,
710
+ textElement: { content: el.text }
711
+ });
712
+ break;
713
+ case "at":
714
+ if (el.target === "all") out.push({
715
+ elementType: ElementType.TEXT,
716
+ textElement: {
717
+ content: el.display ?? "@",
718
+ atType: 1
719
+ }
720
+ });
721
+ else out.push({
722
+ elementType: ElementType.TEXT,
723
+ textElement: {
724
+ content: el.display ?? "@",
725
+ atType: 2,
726
+ atUid: el.target
727
+ }
728
+ });
729
+ break;
730
+ case "face":
731
+ out.push({
732
+ elementType: ElementType.FACE,
733
+ faceElement: { faceIndex: Number(el.id) }
734
+ });
735
+ break;
736
+ case "image":
737
+ out.push({
738
+ elementType: ElementType.PIC,
739
+ picElement: { picPath: el.path }
740
+ });
741
+ break;
742
+ case "voice":
743
+ out.push({
744
+ elementType: ElementType.PTT,
745
+ pttElement: { filePath: el.path }
746
+ });
747
+ break;
748
+ case "reply":
749
+ out.push({
750
+ elementType: ElementType.REPLY,
751
+ replyElement: {
752
+ replayMsgId: el.messageId,
753
+ replayMsgSeq: el.messageId,
754
+ replayMsgTime: "0"
755
+ }
756
+ });
757
+ break;
758
+ case "video":
759
+ case "file":
760
+ case "forward":
761
+ case "json":
762
+ case "xml":
763
+ case "unknown": throw kernelError(`发送元素 ${el.type} 暂不支持(P2-2 探测后补)`, "INVALID_PARAM");
764
+ }
765
+ return out;
766
+ }
767
+ //#endregion
768
+ //#region src/apis/msg.ts
769
+ /**
770
+ * MsgApi:消息语义化 API(ADR-009 统一错误语义)
771
+ *
772
+ * 内部解包原生 `{ result, errMsg }`:成功返回纯业务值,失败抛 KernelError。
773
+ * 协议层只维护 `KernelErrorCode → 协议错误码` 映射表,不解析错误逻辑。
774
+ *
775
+ * 方法面(P2-1):发送 / 撤回 / 拉历史 / 标记已读。group/friend 等后续 apis 同构。
776
+ */
777
+ /** 原生结果解包:result !== 0 抛 KernelError(errMsg 语义映射错误码)。 */
778
+ function unwrapResult(label, raw) {
779
+ if (raw.result === 0) return;
780
+ const msg = raw.errMsg || "无错误详情";
781
+ let code = "UNKNOWN";
782
+ if (msg.includes("未登录") || msg.includes("login")) code = "NOT_LOGIN";
783
+ else if (msg.includes("无权限") || msg.includes("permission")) code = "PERMISSION_DENIED";
784
+ else if (msg.includes("不存在") || msg.includes("not found")) code = "NOT_FOUND";
785
+ else if (label === "sendMsg") code = "SEND_FAILED";
786
+ throw kernelError(`${label} 失败: ${msg}`, code);
787
+ }
788
+ /** 消息 API:从 session 拿 msg service,包装成语义化方法。 */
789
+ var MsgApi = class {
790
+ service;
791
+ /** 上次生成 msgId 的时间(单调递增,2026-08-07 防同毫秒并发碰撞)。 */
792
+ lastMsgTime = 0;
793
+ constructor(session) {
794
+ const service = session.getMsgService();
795
+ if (service === null || service === void 0) throw kernelError("getMsgService() 返回空(session 未 init)", "INVALID_STATE");
796
+ this.service = service;
797
+ }
798
+ /**
799
+ * 生成 msgId 时间戳(单调递增)。
800
+ * generateMsgUniqueId(chatType, time) 以 time 区分消息——同毫秒并发发送
801
+ * (机器人群发/多会话同时回复)Date.now() 会碰撞,msgId 相同导致 wrapper
802
+ * 拒绝或覆盖。严格单调递增保证进程内唯一。
803
+ */
804
+ nextMsgTime() {
805
+ const now = Date.now();
806
+ if (now > this.lastMsgTime) this.lastMsgTime = now;
807
+ else this.lastMsgTime += 1;
808
+ return String(this.lastMsgTime);
809
+ }
810
+ /**
811
+ * 发送消息:canonical 元素 → NT 发送元素 → sendMsg。
812
+ * 返回 NT msgId(雪花 ID)。
813
+ */
814
+ async sendMessage(target, elements) {
815
+ const sendElements = toSendElements(elements);
816
+ const msgId = this.service.generateMsgUniqueId(target.chatType, this.nextMsgTime());
817
+ unwrapResult("sendMsg", await this.service.sendMsg(msgId, target, sendElements, /* @__PURE__ */ new Map()));
818
+ return { msgId };
819
+ }
820
+ /** 撤回消息(群聊管理员 / 私聊 2 分钟内)。 */
821
+ async recallMessage(target, msgIds) {
822
+ if (msgIds.length === 0) throw kernelError("recallMessage 需要至少一个 msgId", "INVALID_PARAM");
823
+ unwrapResult("recallMsg", await this.service.recallMsg(target, msgIds));
824
+ }
825
+ /** 拉取历史消息(msgId 为空从最新拉;count 条,时间倒序)。 */
826
+ async fetchMessages(target, opts) {
827
+ const raw = await this.service.getMsgs(target, opts.msgId ?? "", opts.count, false);
828
+ unwrapResult("getMsgs", raw);
829
+ return raw.msgList ?? [];
830
+ }
831
+ /** 按 msgId 批量拉取消息(get_msg / 精华消息 / ptt 转文字共用)。 */
832
+ async fetchMsgsByMsgId(target, ids) {
833
+ if (ids.length === 0) return [];
834
+ const raw = await this.service.getMsgsByMsgId(target, ids);
835
+ unwrapResult("getMsgsByMsgId", raw);
836
+ return raw.msgList ?? [];
837
+ }
838
+ /** 消息表情表态(set_msg_emoji_like;like=true 点赞,false 取消)。 */
839
+ async setMsgEmojiLike(target, opts) {
840
+ unwrapResult("setMsgEmojiLikes", await this.service.setMsgEmojiLikes(target, opts.msgSeq, opts.emojiId, opts.emojiType, opts.like));
841
+ }
842
+ /**
843
+ * 语音转文字(fetch_ptt_text)。
844
+ * 流程:按 msgId 拉消息 → 找 PTT 元素 → translatePtt2Text(异步转写)
845
+ * → 再拉一次消息读 pttElement.text。
846
+ */
847
+ async fetchPttText(msgId, target) {
848
+ const [first] = await this.fetchMsgsByMsgId(target, [msgId]);
849
+ if (first === void 0) throw kernelError("消息不存在", "NOT_FOUND");
850
+ const ptt = first.elements.find((el) => el.pttElement !== void 0);
851
+ if (ptt?.pttElement === void 0) throw kernelError("消息中不包含语音", "NOT_FOUND");
852
+ unwrapResult("translatePtt2Text", await this.service.translatePtt2Text(msgId, target, ptt));
853
+ const [afterFirst] = await this.fetchMsgsByMsgId(target, [msgId]);
854
+ if (afterFirst === void 0) throw kernelError("获取语音转文字结果失败", "UNKNOWN");
855
+ const text = afterFirst.elements.find((el) => el.pttElement !== void 0)?.pttElement?.text;
856
+ if (text === void 0 || text === "") throw kernelError("获取语音转文字结果失败", "UNKNOWN");
857
+ return text;
858
+ }
859
+ /** 标记会话已读。 */
860
+ async markRead(target) {
861
+ unwrapResult("setMsgRead", await this.service.setMsgRead(target));
862
+ }
863
+ /** 发送输入状态(set_input_status;eventType=1 输入中,0 停止)。 */
864
+ async setInputStatus(target, eventType) {
865
+ await this.service.sendShowInputStatusReq(target.chatType, eventType, target.peerUid);
866
+ }
867
+ /**
868
+ * 发送合并转发(send_group/private_forward_msg)。
869
+ * buildMultiForwardMsg 组装 MULTI_FORWARD 元素 → 直接作 sendMsg 元素发送。
870
+ */
871
+ async sendForwardMessage(target, sourcePeer, srcMsgIds) {
872
+ if (srcMsgIds.length === 0) throw kernelError("sendForwardMessage 需要至少一条源消息", "INVALID_PARAM");
873
+ const built = await this.service.buildMultiForwardMsg({
874
+ srcMsgIds,
875
+ srcContact: sourcePeer
876
+ });
877
+ unwrapResult("buildMultiForwardMsg", built);
878
+ const elements = built.rspInfo?.elements;
879
+ if (elements === void 0 || elements.length === 0) throw kernelError("合并转发组装失败:无元素", "UNKNOWN");
880
+ const msgId = this.service.generateMsgUniqueId(target.chatType, String(Date.now()));
881
+ unwrapResult("sendMsg", await this.service.sendMsg(msgId, target, elements, /* @__PURE__ */ new Map()));
882
+ return { msgId };
883
+ }
884
+ /** 获取合并转发内容(get_forward_msg;resId 取自 multiForwardMsgElement)。 */
885
+ async fetchForwardMessage(peer, msgId) {
886
+ const [first] = await this.fetchMsgsByMsgId(peer, [msgId]);
887
+ if (first === void 0) throw kernelError("消息不存在", "NOT_FOUND");
888
+ const forward = first.elements.find((el) => el.multiForwardMsgElement !== void 0)?.multiForwardMsgElement;
889
+ if (forward === void 0 || forward.resId === "") throw kernelError("消息不包含合并转发内容", "NOT_FOUND");
890
+ const raw = await this.service.getMultiMsg(peer, msgId, forward.resId);
891
+ unwrapResult("getMultiMsg", raw);
892
+ return raw.msgList ?? [];
893
+ }
894
+ /** 单条转发(forward_group/friend_single_msg;srcMsgIds 源、dstPeer 目标)。 */
895
+ async forwardSingleMessage(sourcePeer, srcMsgIds, dstPeer) {
896
+ if (srcMsgIds.length === 0) throw kernelError("forwardSingleMessage 需要至少一条源消息", "INVALID_PARAM");
897
+ unwrapResult("forwardMsg", await this.service.forwardMsg(srcMsgIds, sourcePeer, [dstPeer], void 0));
898
+ }
899
+ /** 设置在线状态(set_online_status;customStatus 为自定义状态)。 */
900
+ async setOnlineStatus(opts) {
901
+ unwrapResult("setStatus", await this.service.setStatus(opts));
902
+ }
903
+ };
904
+ //#endregion
905
+ //#region src/apis/ticket.ts
906
+ /**
907
+ * TicketApi:票据语义化 API(ADR-009 统一错误语义,P2-13)
908
+ *
909
+ * - getClientKey:TicketService.forceFetchClientKey 解包(get_clientkey)
910
+ * - getCookies:clientKey → ssl.ptlogin2.qq.com/jump 跳转 → 解析 set-cookie 头
911
+ * (fetch 会吞掉 Set-Cookie 头,故用 node:https 手动请求)
912
+ */
913
+ /** 跳转请求超时(毫秒)。 */
914
+ const JUMP_TIMEOUT_MS = 1e4;
915
+ /** 票据 API:从 session 拿 ticket service,包装成语义化方法。 */
916
+ var TicketApi = class {
917
+ service;
918
+ constructor(session) {
919
+ const service = session.getTicketService();
920
+ if (service === null || service === void 0) throw kernelError("getTicketService() 返回空(session 未 init)", "INVALID_STATE");
921
+ this.service = service;
922
+ }
923
+ /** 获取 clientKey(get_clientkey)。 */
924
+ async getClientKey() {
925
+ const raw = await this.service.forceFetchClientKey("");
926
+ if (raw.result !== 0 || raw.clientKey === "") throw kernelError(`forceFetchClientKey 失败: ${raw.errMsg}`, "UNKNOWN");
927
+ return raw;
928
+ }
929
+ /** 获取指定域名的 cookies(get_cookies;uin 为机器人 QQ 号)。 */
930
+ async getCookies(domain, uin) {
931
+ const { clientKey } = await this.getClientKey();
932
+ const u1 = `https://${domain}/${uin}/infocenter`;
933
+ return fetchJumpCookies(`https://ssl.ptlogin2.qq.com/jump?ptlang=1033&clientuin=${encodeURIComponent(uin)}&clientkey=${encodeURIComponent(clientKey)}&u1=${encodeURIComponent(u1)}&keyindex=19`);
934
+ }
935
+ /** 计算 bkn(get_csrf_token 与 web 接口共用;skey → bkn 哈希)。 */
936
+ static getBkn(skey) {
937
+ let hash = 5381;
938
+ for (let i = 0; i < skey.length; i += 1) {
939
+ const code = skey.charCodeAt(i);
940
+ hash = hash + (hash << 5) + code;
941
+ }
942
+ return (hash & 2147483647).toString();
943
+ }
944
+ };
945
+ /** 请求 jump 地址并解析 set-cookie → dict(fetch 吞 Set-Cookie,故用 node:https)。 */
946
+ function fetchJumpCookies(url) {
947
+ return new Promise((resolve, reject) => {
948
+ const req = get(url, { headers: { "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" } }, (res) => {
949
+ const cookies = parseSetCookieHeaders(res.headers["set-cookie"]);
950
+ res.resume();
951
+ res.on("end", () => resolve(cookies));
952
+ });
953
+ req.setTimeout(JUMP_TIMEOUT_MS, () => {
954
+ req.destroy();
955
+ reject(/* @__PURE__ */ new Error("获取 cookies 超时"));
956
+ });
957
+ req.on("error", (err) => reject(err));
958
+ });
959
+ }
960
+ /** 解析 set-cookie 头数组 → { name: value } dict。 */
961
+ function parseSetCookieHeaders(raw) {
962
+ const cookies = {};
963
+ if (raw === void 0) return cookies;
964
+ for (const line of raw) {
965
+ const [pair] = line.split(";");
966
+ if (pair !== void 0) {
967
+ const eq = pair.indexOf("=");
968
+ if (eq > 0) {
969
+ const name = pair.slice(0, eq).trim();
970
+ const value = pair.slice(eq + 1);
971
+ if (name !== "") cookies[name] = value;
972
+ }
973
+ }
974
+ }
975
+ return cookies;
976
+ }
977
+ //#endregion
978
+ //#region src/apis/profile.ts
979
+ /**
980
+ * ProfileApi:资料语义化 API(ADR-009 统一错误语义,P2-14/P2-15)
981
+ *
982
+ * - setLongNick:set_self_longnick
983
+ * - setNickName:set_qq_profile
984
+ * - setHeader:set_qq_avatar
985
+ * - getStrangerInfo:get_stranger_info(uin → uid → 详情扁平化)
986
+ */
987
+ /** 资料 API:从 session 拿 profile service,包装成语义化方法。 */
988
+ var ProfileApi = class {
989
+ service;
990
+ constructor(session) {
991
+ const service = session.getProfileService();
992
+ if (service === null || service === void 0) throw kernelError("getProfileService() 返回空(session 未 init)", "INVALID_STATE");
993
+ this.service = service;
994
+ }
995
+ /** 设置个性签名(set_self_longnick)。 */
996
+ async setLongNick(longNick) {
997
+ const res = await this.service.setLongNick(longNick);
998
+ if (res !== void 0 && res !== null && typeof res.result === "number" && res.result !== 0) throw kernelError(`setLongNick 失败: ${String(res.errMsg ?? "")}`, "UNKNOWN");
999
+ }
1000
+ /** 设置昵称(set_qq_profile)。 */
1001
+ async setNickName(nickName) {
1002
+ const res = await this.service.setNickName(nickName);
1003
+ if (res !== void 0 && res !== null && typeof res.result === "number" && res.result !== 0) throw kernelError(`setNickName 失败: ${String(res.errMsg ?? "")}`, "UNKNOWN");
1004
+ }
1005
+ /** 设置头像(set_qq_avatar;filePath 为本地路径)。 */
1006
+ async setHeader(filePath) {
1007
+ const raw = await this.service.setHeader(filePath);
1008
+ if (raw.result !== 0) throw kernelError(`setHeader 失败: ${raw.errMsg}`, "UNKNOWN");
1009
+ }
1010
+ /** 获取陌生人信息(get_stranger_info;uin 为用户 QQ 号)。 */
1011
+ async getStrangerInfo(uin) {
1012
+ const byUin = await this.service.getUserDetailInfoByUin(uin);
1013
+ const detailUid = byUin.detail?.uid;
1014
+ let uid = uin;
1015
+ if (detailUid !== void 0 && detailUid !== "") uid = detailUid;
1016
+ const info = await this.service.getUserDetailInfo(uid);
1017
+ return flattenStrangerInfo(uin, uid, byUin, info);
1018
+ }
1019
+ };
1020
+ /** 扁平化两份详情 → StrangerInfo(宽松取值,字段缺失给默认)。 */
1021
+ function flattenStrangerInfo(uin, uid, byUin, info) {
1022
+ const coreInfo = info.detail?.simpleInfo?.coreInfo ?? byUin.detail?.simpleInfo?.coreInfo;
1023
+ const baseInfo = info.detail?.simpleInfo?.baseInfo ?? byUin.detail?.simpleInfo?.baseInfo;
1024
+ const vasInfo = info.detail?.simpleInfo?.vasInfo ?? byUin.detail?.simpleInfo?.vasInfo;
1025
+ const commonExt = info.detail?.commonExt ?? byUin.detail?.commonExt;
1026
+ const status = info.detail?.simpleInfo?.status?.status ?? 0;
1027
+ return {
1028
+ user_id: Number(uin),
1029
+ uid,
1030
+ nickname: coreInfo?.nick ?? "",
1031
+ age: baseInfo?.age ?? 0,
1032
+ qid: baseInfo?.qid ?? "",
1033
+ qq_level: commonExt?.qqLevel ?? 0,
1034
+ sex: mapSex(baseInfo?.sex),
1035
+ long_nick: baseInfo?.longNick ?? "",
1036
+ reg_time: commonExt?.regTime ?? 0,
1037
+ is_vip: vasInfo?.svipFlag ?? false,
1038
+ is_years_vip: vasInfo?.yearVipFlag ?? false,
1039
+ vip_level: vasInfo?.vipLevel ?? 0,
1040
+ remark: coreInfo?.remark ?? "",
1041
+ status,
1042
+ login_days: 0
1043
+ };
1044
+ }
1045
+ /** 性别数值 → OB11 字符串(1=男 2=女 其余 unknown)。 */
1046
+ function mapSex(raw) {
1047
+ if (raw === 1) return "male";
1048
+ if (raw === 2) return "female";
1049
+ return "unknown";
1050
+ }
1051
+ //#endregion
1052
+ //#region src/apis/profile-like.ts
1053
+ /**
1054
+ * ProfileLikeApi:资料点赞语义化 API(ADR-009 统一错误语义,P2-14)
1055
+ *
1056
+ * sendLike:send_like(sourceId=71 赞来源,doLikeCount 次数)。
1057
+ */
1058
+ /** 点赞 API:从 session 拿 profile like service,包装成语义化方法。 */
1059
+ var ProfileLikeApi = class {
1060
+ service;
1061
+ constructor(session) {
1062
+ const service = session.getProfileLikeService();
1063
+ if (service === null || service === void 0) throw kernelError("getProfileLikeService() 返回空(session 未 init)", "INVALID_STATE");
1064
+ this.service = service;
1065
+ }
1066
+ /** 点赞(send_like;uid 为目标 uid,count 次数)。 */
1067
+ async sendLike(uid, count = 1) {
1068
+ const raw = await this.service.setBuddyProfileLike({
1069
+ friendUid: uid,
1070
+ sourceId: 71,
1071
+ doLikeCount: count,
1072
+ doLikeTollCount: 0
1073
+ });
1074
+ if (raw.result !== 0) throw kernelError(`点赞失败: ${raw.errMsg}`, "UNKNOWN");
1075
+ }
1076
+ };
1077
+ //#endregion
1078
+ //#region src/apis/richmedia.ts
1079
+ /**
1080
+ * RichMediaApi:富媒体语义化 API(ADR-009 统一错误语义,P2-14)
1081
+ *
1082
+ * - translateWords:translate_en2zh
1083
+ * - 群文件:列表 / 空间 / 建夹 / 删除文件 / 删除夹 / 重命名 / 移动 / 转存
1084
+ */
1085
+ /** 群文件业务类型(deleteGroupFile/moveGroupFile/renameGroupFile 用,说明书参考)。 */
1086
+ const GROUP_FILE_BIZ_TYPE = 102;
1087
+ /** 排序方式(按时间)。 */
1088
+ const SORT_TYPE_TIME = 1;
1089
+ /** 排序方向(倒序)。 */
1090
+ const SORT_ORDER_DESC = 2;
1091
+ /** 起始下标(根目录)。 */
1092
+ const START_INDEX_ROOT = 0;
1093
+ /** 不显示在线文档文件夹。 */
1094
+ const HIDE_ONLINEDOC_FOLDER = 0;
1095
+ /** 默认文件数量。 */
1096
+ const DEFAULT_FILE_COUNT = 50;
1097
+ /** 空间查询文件数量(单条即可)。 */
1098
+ const SPACE_QUERY_FILE_COUNT = 1;
1099
+ /** 原生 result 解包(result 字段非 0 抛 KernelError)。 */
1100
+ function unwrap(label, result, errMsg) {
1101
+ if (result === 0) return;
1102
+ throw kernelError(`${label} 失败: ${errMsg ?? "无错误详情"}`, "UNKNOWN");
1103
+ }
1104
+ /** 富媒体 API:从 session 拿 rich media service,包装成语义化方法。 */
1105
+ var RichMediaApi = class {
1106
+ service;
1107
+ constructor(session) {
1108
+ const service = session.getRichMediaService();
1109
+ if (service === null || service === void 0) throw kernelError("getRichMediaService() 返回空(session 未 init)", "INVALID_STATE");
1110
+ this.service = service;
1111
+ }
1112
+ /** 英文单词翻译(translate_en2zh)。 */
1113
+ async translateWords(words) {
1114
+ const raw = await this.service.translateEnWordToZn(words);
1115
+ unwrap("translateEnWordToZn", raw.result, raw.errMsg);
1116
+ return raw.words ?? [];
1117
+ }
1118
+ /** 群文件列表(get_group_root_files;parent 缺省根目录)。 */
1119
+ async getGroupFileList(groupCode, parentFolderId = "", fileCount = DEFAULT_FILE_COUNT) {
1120
+ const params = {
1121
+ sortType: SORT_TYPE_TIME,
1122
+ fileCount,
1123
+ startIndex: START_INDEX_ROOT,
1124
+ sortOrder: SORT_ORDER_DESC,
1125
+ showOnlinedocFolder: HIDE_ONLINEDOC_FOLDER
1126
+ };
1127
+ const raw = await this.service.getGroupFileList(groupCode, params);
1128
+ if (raw.result !== 0) unwrap("getGroupFileList", raw.result, raw.errMsg);
1129
+ const list = extractFileList(raw.result);
1130
+ if (parentFolderId === "") return list;
1131
+ return list.filter((item) => item.fileInfo !== void 0 && item.fileInfo.parent === parentFolderId || item.folderInfo !== void 0 && item.folderInfo.folderUid === parentFolderId);
1132
+ }
1133
+ /** 群文件系统信息(get_group_file_system_info;getGroupFileList 的 groupSpaceResult)。 */
1134
+ async getGroupFileSystemInfo(groupCode) {
1135
+ const params = {
1136
+ sortType: SORT_TYPE_TIME,
1137
+ fileCount: SPACE_QUERY_FILE_COUNT,
1138
+ startIndex: START_INDEX_ROOT,
1139
+ sortOrder: SORT_ORDER_DESC,
1140
+ showOnlinedocFolder: HIDE_ONLINEDOC_FOLDER
1141
+ };
1142
+ const raw = await this.service.getGroupFileList(groupCode, params);
1143
+ if (raw.result !== 0) unwrap("getGroupFileList", raw.result, raw.errMsg);
1144
+ const space = raw.groupSpaceResult;
1145
+ if (space === void 0) throw kernelError("getGroupFileList 无 groupSpaceResult", "UNKNOWN");
1146
+ return {
1147
+ fileCount: countGroupFiles(raw.result),
1148
+ limitCount: 0,
1149
+ usedSpace: space.usedSpace,
1150
+ totalSpace: space.totalSpace
1151
+ };
1152
+ }
1153
+ /** 创建群文件夹(create_group_file_folder)。 */
1154
+ async createGroupFolder(groupCode, folderName) {
1155
+ const raw = await this.service.createGroupFolder(groupCode, folderName);
1156
+ if (raw.result !== 0) unwrap("createGroupFolder", raw.result, raw.errMsg);
1157
+ const { result } = raw;
1158
+ if (typeof result === "string") return result;
1159
+ return "";
1160
+ }
1161
+ /** 删除群文件(delete_group_file)。 */
1162
+ async deleteGroupFile(groupCode, fileIds) {
1163
+ const raw = await this.service.deleteGroupFile(groupCode, [GROUP_FILE_BIZ_TYPE], fileIds);
1164
+ if (raw.result !== 0) unwrap("deleteGroupFile", raw.result, raw.errMsg);
1165
+ }
1166
+ /** 删除群文件夹(delete_group_folder)。 */
1167
+ async deleteGroupFolder(groupCode, folderId) {
1168
+ const raw = await this.service.deleteGroupFolder(groupCode, folderId);
1169
+ if (raw.result !== 0) unwrap("deleteGroupFolder", raw.result, raw.errMsg);
1170
+ }
1171
+ /** 重命名群文件(rename_group_file;参数语义待探测校准)。 */
1172
+ async renameGroupFile(groupCode, fileId, newName) {
1173
+ await this.service.renameGroupFile(groupCode, GROUP_FILE_BIZ_TYPE, fileId, newName, "");
1174
+ }
1175
+ /** 移动群文件(move_group_file;targetFolder 空为根目录)。 */
1176
+ async moveGroupFile(groupCode, fileIds, targetFolderId) {
1177
+ const raw = await this.service.moveGroupFile(groupCode, [GROUP_FILE_BIZ_TYPE], fileIds, "", targetFolderId);
1178
+ if (raw.result !== 0) unwrap("moveGroupFile", raw.result, raw.errMsg);
1179
+ }
1180
+ /** 转存群文件(trans_group_file)。 */
1181
+ async transGroupFile(groupCode, fileId) {
1182
+ const raw = await this.service.transGroupFile(groupCode, fileId);
1183
+ if (raw.result !== 0) unwrap("transGroupFile", raw.result, raw.errMsg);
1184
+ }
1185
+ };
1186
+ /** 从 getGroupFileList 返回提取文件列表(兼容数组 / { items } / { fileList })。 */
1187
+ function extractFileList(result) {
1188
+ if (Array.isArray(result)) return result;
1189
+ if (result !== null && typeof result === "object") {
1190
+ const obj = result;
1191
+ if (Array.isArray(obj.items)) return obj.items;
1192
+ if (Array.isArray(obj.fileList)) return obj.fileList;
1193
+ if (Array.isArray(obj.list)) return obj.list;
1194
+ }
1195
+ return [];
1196
+ }
1197
+ /** 统计文件数量(兼容数组 / { items })。 */
1198
+ function countGroupFiles(result) {
1199
+ if (Array.isArray(result)) return result.filter((item) => item.fileInfo !== void 0).length;
1200
+ return 0;
1201
+ }
1202
+ //#endregion
1203
+ //#region src/apis/webapi.ts
1204
+ /**
1205
+ * WebApi:群空间 web 接口(P2-15)
1206
+ *
1207
+ * qun.qq.com 群空间接口需 Cookie(p_skey/skey)+ bkn(skey 哈希)。
1208
+ * 复用 TicketApi.getCookies('qun.qq.com') 取 Cookie,TicketApi.getBkn 算 bkn。
1209
+ * 零 NapCat 代码:接口契约(URL/参数/返回形状)是 qun.qq.com 外部系统的事实,
1210
+ * 自研描述。
1211
+ */
1212
+ /** 群空间域名(Cookie 归属)。 */
1213
+ const QUN_DOMAIN = "qun.qq.com";
1214
+ /** 精华消息页大小(分页拉取)。 */
1215
+ const ESSENCE_PAGE_LIMIT = 50;
1216
+ /** 精华消息最大分页数。 */
1217
+ const ESSENCE_MAX_PAGES = 20;
1218
+ /** 群荣誉类型(1=龙王 2=群聊之火 3=群聊炽热 6=快乐源泉)。 */
1219
+ const WebHonorType = {
1220
+ ALL: 0,
1221
+ TALKATIVE: 1,
1222
+ PERFORMER: 2,
1223
+ LEGEND: 3,
1224
+ EMOTION: 6
1225
+ };
1226
+ /** 群空间 web API(Cookie 经 TicketApi 注入)。 */
1227
+ var WebApi = class {
1228
+ getCookies;
1229
+ constructor(opts) {
1230
+ this.getCookies = opts.getCookies;
1231
+ }
1232
+ /** 获取群精华消息列表(get_essence_msg_list;分页拉取至 is_end)。 */
1233
+ async getEssenceMsgList(groupCode) {
1234
+ const cookie = await this.getCookies(QUN_DOMAIN);
1235
+ const bkn = TicketApi.getBkn(cookie["skey"] ?? "");
1236
+ const out = [];
1237
+ for (let page = 0; page < ESSENCE_MAX_PAGES; page += 1) {
1238
+ const ret = await fetchQunJson("https://qun.qq.com/cgi-bin/group_digest/digest_list?" + new URLSearchParams({
1239
+ bkn,
1240
+ page_start: String(page * ESSENCE_PAGE_LIMIT),
1241
+ page_limit: String(ESSENCE_PAGE_LIMIT),
1242
+ group_code: groupCode
1243
+ }).toString(), cookie);
1244
+ if (ret === null || ret.retcode !== 0) break;
1245
+ const list = ret.data?.msg_list;
1246
+ if (Array.isArray(list)) {
1247
+ for (const item of list) if (item !== null) out.push(item);
1248
+ }
1249
+ if (ret.data?.is_end === true) break;
1250
+ }
1251
+ return out;
1252
+ }
1253
+ /** 获取群荣誉信息(get_group_honor_info;type 0=全部)。 */
1254
+ async getGroupHonorInfo(groupCode, type) {
1255
+ const cookie = await this.getCookies(QUN_DOMAIN);
1256
+ const result = {
1257
+ group_id: Number(groupCode),
1258
+ current_talkative: null,
1259
+ talkative_list: [],
1260
+ performer_list: [],
1261
+ legend_list: [],
1262
+ emotion_list: [],
1263
+ strong_newbie_list: []
1264
+ };
1265
+ if (type === WebHonorType.TALKATIVE || type === WebHonorType.ALL) {
1266
+ const talkative = await this.fetchHonorList(groupCode, cookie, WebHonorType.TALKATIVE);
1267
+ result.talkative_list = talkative;
1268
+ const [first] = talkative;
1269
+ if (first !== void 0) result.current_talkative = first;
1270
+ }
1271
+ if (type === WebHonorType.PERFORMER || type === WebHonorType.ALL) result.performer_list = await this.fetchHonorList(groupCode, cookie, WebHonorType.PERFORMER);
1272
+ if (type === WebHonorType.LEGEND || type === WebHonorType.ALL) result.legend_list = await this.fetchHonorList(groupCode, cookie, WebHonorType.LEGEND);
1273
+ if (type === WebHonorType.EMOTION || type === WebHonorType.ALL) result.emotion_list = await this.fetchHonorList(groupCode, cookie, WebHonorType.EMOTION);
1274
+ return result;
1275
+ }
1276
+ /** 拉取单类荣誉列表(honorlist 接口,正则提取 __INITIAL_STATE__)。 */
1277
+ async fetchHonorList(groupCode, cookie, type) {
1278
+ const text = await fetchQunText("https://qun.qq.com/interactive/honorlist?" + new URLSearchParams({
1279
+ gc: groupCode,
1280
+ type: String(type)
1281
+ }).toString(), cookie);
1282
+ if (text === null) return [];
1283
+ const match = /window\.__INITIAL_STATE__=(.*?);/.exec(text);
1284
+ if (match === null || match[1] === void 0) return [];
1285
+ let state;
1286
+ try {
1287
+ state = JSON.parse(match[1]);
1288
+ } catch {
1289
+ return [];
1290
+ }
1291
+ const raw = type === WebHonorType.TALKATIVE ? state.talkativeList : state.actorList;
1292
+ if (!Array.isArray(raw)) return [];
1293
+ const out = [];
1294
+ for (const item of raw) if (item !== null && typeof item === "object") {
1295
+ const obj = item;
1296
+ out.push({
1297
+ uin: String(obj.uin ?? ""),
1298
+ name: String(obj.name ?? ""),
1299
+ avatar: String(obj.avatar ?? ""),
1300
+ desc: String(obj.desc ?? "")
1301
+ });
1302
+ }
1303
+ return out;
1304
+ }
1305
+ };
1306
+ /** 请求 qun.qq.com JSON 接口(带 Cookie;失败返回 null)。 */
1307
+ async function fetchQunJson(url, cookie) {
1308
+ try {
1309
+ const res = await fetch(url, { headers: { Cookie: cookieToString(cookie) } });
1310
+ if (!res.ok) return null;
1311
+ const json = await res.json();
1312
+ return {
1313
+ retcode: json.retcode ?? -1,
1314
+ data: json.data
1315
+ };
1316
+ } catch (err) {
1317
+ throw kernelError(`qun.qq.com 请求失败: ${String(err)}`, "UNKNOWN");
1318
+ }
1319
+ }
1320
+ /** 请求 qun.qq.com 文本接口(带 Cookie;失败返回 null)。 */
1321
+ async function fetchQunText(url, cookie) {
1322
+ try {
1323
+ const res = await fetch(url, { headers: { Cookie: cookieToString(cookie) } });
1324
+ if (!res.ok) return null;
1325
+ return await res.text();
1326
+ } catch (err) {
1327
+ throw kernelError(`qun.qq.com 请求失败: ${String(err)}`, "UNKNOWN");
1328
+ }
1329
+ }
1330
+ /** Cookie dict → 头字符串。 */
1331
+ function cookieToString(cookie) {
1332
+ const parts = [];
1333
+ for (const [key, value] of Object.entries(cookie)) parts.push(`${key}=${value}`);
1334
+ return parts.join("; ");
1335
+ }
1336
+ //#endregion
1337
+ //#region src/bridge/group-bridge.ts
1338
+ /**
1339
+ * 群事件桥:注册原生监听 → 回调 emit 到 channel。
1340
+ * 无全局单例(ADR-015 推论)——每进程每 session 实例化一份。
1341
+ */
1342
+ var GroupBridge = class {
1343
+ service;
1344
+ channel;
1345
+ listenerId = null;
1346
+ constructor(session, channel) {
1347
+ const service = session.getGroupService();
1348
+ if (service === null || service === void 0) throw kernelError("getGroupService() 返回空(session 未 init)", "INVALID_STATE");
1349
+ this.service = service;
1350
+ this.channel = channel;
1351
+ }
1352
+ /** 注册原生监听(幂等)。 */
1353
+ register() {
1354
+ if (this.listenerId !== null) return;
1355
+ const listener = {
1356
+ onGroupListInited: (listEmpty) => this.channel.emit("Group/onGroupListInited", listEmpty),
1357
+ onGroupListUpdate: (updateType, groupList) => this.channel.emit("Group/onGroupListUpdate", updateType, groupList),
1358
+ onGroupDetailInfoChange: (detailInfo) => this.channel.emit("Group/onGroupDetailInfoChange", detailInfo),
1359
+ onMemberListChange: (arg) => this.channel.emit("Group/onMemberListChange", arg),
1360
+ onMemberInfoChange: (groupCode, dataSource, members) => this.channel.emit("Group/onMemberInfoChange", groupCode, dataSource, members),
1361
+ onGroupNotifiesUpdated: (doubt, notifies) => this.channel.emit("Group/onGroupNotifiesUpdated", doubt, notifies),
1362
+ onGroupSingleScreenNotifies: (doubt, seq, notifies) => this.channel.emit("Group/onGroupSingleScreenNotifies", doubt, seq, notifies),
1363
+ onShutUpMemberListChanged: (groupCode, members) => this.channel.emit("Group/onShutUpMemberListChanged", groupCode, members)
1364
+ };
1365
+ this.listenerId = this.service.addKernelGroupListener(listener);
1366
+ }
1367
+ /** 注销原生监听(幂等)。 */
1368
+ unregister() {
1369
+ if (this.listenerId === null) return;
1370
+ this.service.removeKernelGroupListener(this.listenerId);
1371
+ this.listenerId = null;
1372
+ }
1373
+ };
1374
+ //#endregion
1375
+ //#region src/bridge/msg-bridge.ts
1376
+ /**
1377
+ * 消息事件桥:注册原生监听 → 回调 emit 到 channel。
1378
+ * 无全局单例(ADR-015 推论)——每进程每 session 实例化一份。
1379
+ */
1380
+ var MsgBridge = class {
1381
+ service;
1382
+ channel;
1383
+ listenerId = null;
1384
+ constructor(session, channel) {
1385
+ const service = session.getMsgService();
1386
+ if (service === null || service === void 0) throw kernelError("getMsgService() 返回空(session 未 init)", "INVALID_STATE");
1387
+ this.service = service;
1388
+ this.channel = channel;
1389
+ }
1390
+ /** 注册原生监听(幂等)。 */
1391
+ register() {
1392
+ if (this.listenerId !== null) return;
1393
+ const listener = {
1394
+ onRecvMsg: (msgs) => this.channel.emit("Msg/onRecvMsg", msgs),
1395
+ onRecvMsgReadReport: (reports) => this.channel.emit("Msg/onRecvMsgReadReport", reports),
1396
+ onRecvMsgReceipt: (receipts) => this.channel.emit("Msg/onRecvMsgReceipt", receipts)
1397
+ };
1398
+ this.listenerId = this.service.addKernelMsgListener(listener);
1399
+ }
1400
+ /** 注销原生监听(幂等)。 */
1401
+ unregister() {
1402
+ if (this.listenerId === null) return;
1403
+ this.service.removeKernelMsgListener(this.listenerId);
1404
+ this.listenerId = null;
1405
+ }
1406
+ };
1407
+ //#endregion
1408
+ //#region src/types/listeners/group.ts
1409
+ /** 群列表更新类型(说明书参考,值待探测校准)。 */
1410
+ const GroupListUpdateType = {
1411
+ /** 全量更新(groupList 为完整列表)。 */
1412
+ ALL: 0,
1413
+ /** 增量更新(合并到现有缓存)。 */
1414
+ PARTIAL: 1
1415
+ };
1416
+ //#endregion
1417
+ //#region src/cache/group-cache.ts
1418
+ /**
1419
+ * 群/成员缓存:事件主动维护 + 查询惰性回填。
1420
+ * 只读接口 getGroupDetail / getMembers / getMember / getGroup(列表项)。
1421
+ */
1422
+ var GroupCache = class {
1423
+ channel;
1424
+ groupApi;
1425
+ /** 群列表项(onGroupListUpdate / getGroupList 回填)。 */
1426
+ groupList = /* @__PURE__ */ new Map();
1427
+ /** 群详情(onGroupDetailInfoChange / getGroupInfo 回填)。 */
1428
+ groupDetails = /* @__PURE__ */ new Map();
1429
+ /** 群成员:groupCode → (uid → GroupMember)。 */
1430
+ members = /* @__PURE__ */ new Map();
1431
+ /** 惰性回填 in-flight 去重。 */
1432
+ inFlight = /* @__PURE__ */ new Map();
1433
+ unsubscribes = null;
1434
+ constructor(opts) {
1435
+ this.channel = opts.channel;
1436
+ this.groupApi = opts.groupApi;
1437
+ }
1438
+ /** 订阅群事件通道(幂等)。 */
1439
+ register() {
1440
+ if (this.unsubscribes !== null) return;
1441
+ const unsubs = [];
1442
+ unsubs.push(this.channel.on("Group/onGroupListInited", (listEmpty) => {
1443
+ if (listEmpty) this.groupList.clear();
1444
+ }));
1445
+ unsubs.push(this.channel.on("Group/onGroupListUpdate", (updateType, groupList) => {
1446
+ if (updateType === GroupListUpdateType.ALL) this.groupList.clear();
1447
+ for (const group of groupList) this.groupList.set(group.groupCode, group);
1448
+ }));
1449
+ unsubs.push(this.channel.on("Group/onGroupDetailInfoChange", (detailInfo) => {
1450
+ this.groupDetails.set(detailInfo.groupCode, detailInfo);
1451
+ }));
1452
+ unsubs.push(this.channel.on("Group/onMemberListChange", (arg) => {
1453
+ this.upsertMembers(arg.sceneId, arg.infos);
1454
+ }));
1455
+ unsubs.push(this.channel.on("Group/onMemberInfoChange", (_groupCode, _dataSource, members) => {
1456
+ this.upsertMembers(_groupCode, members);
1457
+ }));
1458
+ this.unsubscribes = () => {
1459
+ for (const unsub of unsubs) unsub();
1460
+ };
1461
+ }
1462
+ /** 退订事件通道(幂等)。 */
1463
+ unregister() {
1464
+ this.unsubscribes?.();
1465
+ this.unsubscribes = null;
1466
+ this.groupList.clear();
1467
+ this.groupDetails.clear();
1468
+ this.members.clear();
1469
+ this.inFlight.clear();
1470
+ }
1471
+ /** 合并成员到该群缓存(uid → GroupMember)。 */
1472
+ upsertMembers(groupCode, infos) {
1473
+ let map = this.members.get(groupCode);
1474
+ if (map === void 0) {
1475
+ map = /* @__PURE__ */ new Map();
1476
+ this.members.set(groupCode, map);
1477
+ }
1478
+ for (const [uid, member] of infos) map.set(uid, member);
1479
+ }
1480
+ /** 群列表是否已有缓存(同步判断)。 */
1481
+ hasGroup(groupCode) {
1482
+ return this.groupList.has(groupCode) || this.groupDetails.has(groupCode);
1483
+ }
1484
+ /**
1485
+ * 群列表项(只读;缺失惰性回填 getGroupList)。
1486
+ * 返回 undefined 表示群不存在。
1487
+ */
1488
+ async getGroup(groupCode) {
1489
+ const cached = this.groupList.get(groupCode);
1490
+ if (cached !== void 0) return cached;
1491
+ await this.loadGroupList();
1492
+ return this.groupList.get(groupCode);
1493
+ }
1494
+ /**
1495
+ * 群详情(只读;缺失惰性回填 getGroupInfo)。
1496
+ * getGroupInfo 恒返回对象(失败抛 KernelError),直接缓存。
1497
+ */
1498
+ async getGroupDetail(groupCode) {
1499
+ const cached = this.groupDetails.get(groupCode);
1500
+ if (cached !== void 0) return cached;
1501
+ const detail = await this.dedupe(`detail:${groupCode}`, () => this.groupApi.getGroupInfo(groupCode));
1502
+ this.groupDetails.set(groupCode, detail);
1503
+ return detail;
1504
+ }
1505
+ /**
1506
+ * 群成员列表(只读;缺失惰性回填 getAllMemberList(forceFetch=true))。
1507
+ * 返回空数组表示该群无成员(或拉取失败已由 GroupApi 抛 KernelError)。
1508
+ */
1509
+ async getMembers(groupCode) {
1510
+ const map = this.members.get(groupCode);
1511
+ if (map !== void 0 && map.size > 0) return [...map.values()];
1512
+ const members = await this.dedupe(`members:${groupCode}`, () => this.groupApi.getGroupMemberList(groupCode, true));
1513
+ const merged = /* @__PURE__ */ new Map();
1514
+ for (const member of members) merged.set(member.uid, member);
1515
+ this.members.set(groupCode, merged);
1516
+ return members;
1517
+ }
1518
+ /**
1519
+ * 群成员(只读;缺失回填:先整群拉取,再按 uid 取;仍无则 getMemberInfo 单查)。
1520
+ * 返回 undefined 表示成员不在群。
1521
+ */
1522
+ async getMember(groupCode, uid) {
1523
+ let map = this.members.get(groupCode);
1524
+ if (map === void 0 || map.size === 0) {
1525
+ await this.getMembers(groupCode);
1526
+ map = this.members.get(groupCode);
1527
+ }
1528
+ const cached = map?.get(uid);
1529
+ if (cached !== void 0) return cached;
1530
+ const members = await this.dedupe(`member:${groupCode}:${uid}`, () => this.groupApi.getGroupMemberInfo(groupCode, [uid]));
1531
+ if (members.length === 0) return;
1532
+ const [member] = members;
1533
+ if (member !== void 0) {
1534
+ const target = this.members.get(groupCode);
1535
+ if (target !== void 0) target.set(member.uid, member);
1536
+ }
1537
+ return member;
1538
+ }
1539
+ /** 惰性回填全量群列表(getGroup 缺失时)。 */
1540
+ async loadGroupList() {
1541
+ const list = await this.dedupe("group-list", () => this.groupApi.getGroupList(false));
1542
+ for (const group of list) this.groupList.set(group.groupCode, group);
1543
+ }
1544
+ /** 惰性回填并发去重:同 key 并发只拉一次。 */
1545
+ async dedupe(key, loader) {
1546
+ const existing = this.inFlight.get(key);
1547
+ if (existing !== void 0) return await existing;
1548
+ const task = loader().catch((err) => {
1549
+ throw err;
1550
+ }).finally(() => {
1551
+ this.inFlight.delete(key);
1552
+ });
1553
+ this.inFlight.set(key, task);
1554
+ return await task;
1555
+ }
1556
+ };
1557
+ //#endregion
1558
+ //#region src/context.ts
1559
+ /** 创建装配根(初始 wrapper/login 为 null)。 */
1560
+ function createCoreContext(opts) {
1561
+ return {
1562
+ logger: opts.logger,
1563
+ paths: opts.paths,
1564
+ wrapper: null,
1565
+ login: null
1566
+ };
1567
+ }
1568
+ //#endregion
1569
+ //#region src/infra/logger.ts
1570
+ /**
1571
+ * pino 日志封装(ADR-007)
1572
+ *
1573
+ * - console(pino-pretty 可读输出)+ 可选文件双目的地
1574
+ * - 统一 level 控制
1575
+ * - 内置 redact(token / 票据等敏感字段不打日志),可追加
1576
+ * - 无全局单例(ADR-015 推论):每进程实例化一份,由装配层持有
1577
+ */
1578
+ /** 默认 redact 路径:顶层 token/票据 + 任意深度的密码/密钥。 */
1579
+ const DEFAULT_REDACT_PATHS = [
1580
+ "token",
1581
+ "ticket",
1582
+ "cookie",
1583
+ "password",
1584
+ "secret"
1585
+ ];
1586
+ function toSingle(streams) {
1587
+ const [only] = streams;
1588
+ if (only === void 0) throw new KernelError("logger 内部错误:目的地列表为空", "UNKNOWN");
1589
+ return only.stream;
1590
+ }
1591
+ /**
1592
+ * 创建 pino logger:console(pretty)+ 可选文件,统一 level / redact。
1593
+ *
1594
+ * 文件目的地用同步写入(`sync: true`):日志量级下性能可接受,且进程退出不丢日志,
1595
+ * 无需额外的 flush 步骤(对比异步 SonicBoom 的 flushSync 时序陷阱)。
1596
+ */
1597
+ function createLogger(opts = {}) {
1598
+ const level = opts.level ?? "info";
1599
+ const consoleEnabled = opts.console ?? true;
1600
+ const { file, base } = opts;
1601
+ const streams = [];
1602
+ if (consoleEnabled) streams.push({ stream: pinoPretty({
1603
+ colorize: true,
1604
+ translateTime: "SYS:HH:MM:ss.l",
1605
+ singleLine: false,
1606
+ destination: process.stdout
1607
+ }) });
1608
+ if (file) {
1609
+ mkdirSync(dirname(file), { recursive: true });
1610
+ streams.push({ stream: pino.destination({
1611
+ dest: file,
1612
+ sync: true
1613
+ }) });
1614
+ }
1615
+ if (streams.length === 0) throw new KernelError("logger 至少需要一个输出目标:console 或 file", "INVALID_PARAM");
1616
+ const pinoOptions = {
1617
+ level,
1618
+ redact: [...DEFAULT_REDACT_PATHS, ...opts.redact ?? []]
1619
+ };
1620
+ if (base) pinoOptions.base = {
1621
+ pid: process.pid,
1622
+ ...base
1623
+ };
1624
+ let stream;
1625
+ if (streams.length === 1) stream = toSingle(streams);
1626
+ else stream = pino.multistream(streams);
1627
+ return pino(pinoOptions, stream);
1628
+ }
1629
+ //#endregion
1630
+ //#region src/infra/paths.ts
1631
+ /**
1632
+ * 数据目录布局(ADR-016,2026-08-07 修订:配置文件独立于数据根)
1633
+ *
1634
+ * ```
1635
+ * <数据根>/<qq号>/ # 每账号独立目录(ADR-015 多账号前提)
1636
+ * ├── config/ # 各账号独立配置(历史产物,协议段已走 seed)
1637
+ * ├── logs/ # pino 文件日志
1638
+ * └── cache/ # 临时文件、媒体缓存
1639
+ *
1640
+ * <项目根>/napuketto.toml # 全局配置文件(2026-08-07 用户拍板:放项目根目录)
1641
+ * ```
1642
+ *
1643
+ * 数据根优先级:cli `--data-dir`(显式参数)> `NAPKETTO_DATA` 环境变量 > `~/.napuketto`(默认)。
1644
+ * 配置文件路径见 `resolveConfigPath`(项目根探测,与数据根解耦)。
1645
+ */
1646
+ /** 默认数据根目录名(用户目录下,程序目录可能只读,见 ADR-016)。 */
1647
+ const DEFAULT_DATA_ROOT_NAME = ".napuketto";
1648
+ /** 全局配置文件名。 */
1649
+ const MAIN_CONFIG_FILE = "napuketto.toml";
1650
+ /**
1651
+ * 解析数据根:显式参数 > 环境变量 > 用户目录默认。
1652
+ * 返回绝对路径。独立导出便于 cli / 探测脚本复用。
1653
+ */
1654
+ function resolveDataRoot(dataRoot) {
1655
+ const explicit = dataRoot ?? process.env["NAPKETTO_DATA"];
1656
+ if (explicit) return resolve(explicit);
1657
+ return join(homedir(), DEFAULT_DATA_ROOT_NAME);
1658
+ }
1659
+ /** 向上查找含标志文件的目录(项目根探测:monorepo 根 / 用户项目根)。 */
1660
+ function findProjectRoot(startDir, marker) {
1661
+ let dir = startDir;
1662
+ for (;;) {
1663
+ if (existsSync(join(dir, marker))) return dir;
1664
+ const parent = dirname(dir);
1665
+ if (parent === dir) return;
1666
+ dir = parent;
1667
+ }
1668
+ }
1669
+ /**
1670
+ * 全局配置文件路径解析(2026-08-07 用户拍板:**配置文件放项目根目录,
1671
+ * 数据仍按数据根组织**——数据根只承载账号目录/日志/缓存/QQ 数据)。
1672
+ *
1673
+ * 优先级:
1674
+ * 1. `NAPKETTO_CONFIG` 环境变量(显式指定完整路径,任意场景可用)
1675
+ * 2. 入口模块向上找 `pnpm-workspace.yaml`(monorepo 开发场景,即项目根)
1676
+ * 3. 运行目录(cwd)向上找 `pnpm-workspace.yaml` / `package.json`(发布后用户项目)
1677
+ * 4. 数据根兜底 `<dataRoot>/napuketto.toml`(旧行为兼容)
1678
+ *
1679
+ * 返回绝对路径。独立导出便于 cli / loader 装配链复用。
1680
+ */
1681
+ function resolveConfigPath(opts = {}) {
1682
+ const explicit = process.env["NAPKETTO_CONFIG"];
1683
+ if (explicit !== void 0 && explicit !== "") return resolve(explicit);
1684
+ const moduleRoot = findProjectRoot(dirname(fileURLToPath(import.meta.url)), "pnpm-workspace.yaml");
1685
+ if (moduleRoot !== void 0) return join(moduleRoot, MAIN_CONFIG_FILE);
1686
+ const cwdRoot = findProjectRoot(process.cwd(), "pnpm-workspace.yaml") ?? findProjectRoot(process.cwd(), "package.json");
1687
+ if (cwdRoot !== void 0) return join(cwdRoot, MAIN_CONFIG_FILE);
1688
+ return join(opts.dataRoot ?? resolveDataRoot(), MAIN_CONFIG_FILE);
1689
+ }
1690
+ /**
1691
+ * 路径装配器:持有账号数据目录下的全部子目录。
1692
+ * 无全局单例(ADR-015 推论)——每账号每进程实例化一份。
1693
+ */
1694
+ var PathWrapper = class {
1695
+ /** 数据根(绝对路径)。 */
1696
+ root;
1697
+ /** 账号数据目录(绝对路径),缺省 account 时等于 root。 */
1698
+ accountDir;
1699
+ /** config 目录(napuketto.json + 各协议配置)。 */
1700
+ configDir;
1701
+ /** logs 目录(pino 文件日志)。 */
1702
+ logsDir;
1703
+ /** cache 目录(临时文件、媒体缓存)。 */
1704
+ cacheDir;
1705
+ constructor(opts = {}) {
1706
+ const root = resolveDataRoot(opts.dataRoot);
1707
+ this.root = root;
1708
+ if (opts.account) this.accountDir = join(root, opts.account);
1709
+ else this.accountDir = root;
1710
+ this.configDir = join(this.accountDir, "config");
1711
+ this.logsDir = join(this.accountDir, "logs");
1712
+ this.cacheDir = join(this.accountDir, "cache");
1713
+ }
1714
+ /** 递归创建全部目录,启动装配时调用一次。 */
1715
+ ensure() {
1716
+ for (const dir of [
1717
+ this.configDir,
1718
+ this.logsDir,
1719
+ this.cacheDir
1720
+ ]) mkdirSync(dir, { recursive: true });
1721
+ }
1722
+ /** 账号目录内的文件路径(如 `file('config', 'napuketto.json')`)。 */
1723
+ file(...segments) {
1724
+ return join(this.accountDir, ...segments);
1725
+ }
1726
+ /**
1727
+ * 清空缓存目录(clean_cache 动作经注入回调消费)。
1728
+ * 只删除 cacheDir 下的条目,保留目录本身。
1729
+ */
1730
+ clearCache() {
1731
+ let entries;
1732
+ try {
1733
+ entries = readdirSync(this.cacheDir);
1734
+ } catch {
1735
+ return;
1736
+ }
1737
+ for (const entry of entries) rmSync(join(this.cacheDir, entry), {
1738
+ recursive: true,
1739
+ force: true
1740
+ });
1741
+ }
1742
+ };
1743
+ //#endregion
1744
+ //#region src/wrapper/wrapper-adapters.ts
1745
+ /** NAPI 回调占位(方法存在即可被反射调用,无副作用)。 */
1746
+ const noop = () => void 0;
1747
+ /** NodeIGlobalAdapter 实现(engine.initWithDeskTopConfig 第二参)。 */
1748
+ var GlobalAdapter = class {
1749
+ onLog = noop;
1750
+ onGetSrvCalTime = noop;
1751
+ onShowErrUITips = noop;
1752
+ fixPicImgType = noop;
1753
+ getAppSetting = noop;
1754
+ onInstallFinished = noop;
1755
+ onUpdateGeneralFlag = noop;
1756
+ onGetOfflineMsg = noop;
1757
+ };
1758
+ /** NodeIDependsAdapter 实现(session.init 第二参)。 */
1759
+ var DependsAdapter = class {
1760
+ onMSFStatusChange = noop;
1761
+ onMSFSsoError = noop;
1762
+ getGroupCode = noop;
1763
+ };
1764
+ /** NodeIDispatcherAdapter 实现(session.init 第三参)。 */
1765
+ var DispatcherAdapter = class {
1766
+ dispatchRequest = noop;
1767
+ dispatchCall = noop;
1768
+ dispatchCallWithJson = noop;
1769
+ };
1770
+ /**
1771
+ * 创建会话监听器(默认静默,boot 阶段默认;login 模块接入后可覆盖)。
1772
+ * 回调方法必须存在(NAPI 按方法名反射),但输出与否由调用方决定——
1773
+ * onNTSessionCreate / onOpentelemetryInit 等对用户无信息量,默认不打 stdout。
1774
+ */
1775
+ function createSessionListener(opts = {}) {
1776
+ const { onEvent } = opts;
1777
+ const emit = (name, ...args) => {
1778
+ onEvent?.(name, ...args);
1779
+ };
1780
+ return {
1781
+ onNTSessionCreate: (sessionId) => emit("onNTSessionCreate", sessionId),
1782
+ onGProSessionCreate: (sessionId) => emit("onGProSessionCreate", sessionId),
1783
+ onSessionInitComplete: (sessionId) => emit("onSessionInitComplete", sessionId),
1784
+ onOpentelemetryInit: (info) => emit("onOpentelemetryInit", info),
1785
+ onUserOnlineResult: (result) => emit("onUserOnlineResult", result),
1786
+ onGetSelfTinyId: (result) => emit("onGetSelfTinyId", result)
1787
+ };
1788
+ }
1789
+ /** 创建登录监听器(占位,登录器接入时填充)。 */
1790
+ function createLoginListener() {
1791
+ return {};
1792
+ }
1793
+ //#endregion
1794
+ //#region src/login/lifecycle.ts
1795
+ /**
1796
+ * 完整启动生命周期(wrapper.node 契约流程,2026-08-05 实测确认,自研实现)
1797
+ *
1798
+ * 启动顺序由 wrapper.node 外部契约决定(engine→login→session→startNT),
1799
+ * 运行时实测确认,自研实现:
1800
+ * 1. engine.initWithDeskTopConfig(appid/qua/版本)
1801
+ * 2. loginService.initConfig + addKernelLoginListener
1802
+ * 3. getLoginList() → quickLoginWithUin(uin)(或 QR 登录)
1803
+ * 4. 登录成功 → genSessionConfig → session.init(config, 3 adapter, listener) → startNT(0)
1804
+ * 5. 等 init 完成信号(onOpentelemetryInit(is_init) 为主,onSessionInitComplete 为辅)
1805
+ *
1806
+ * 关键(2026-08-05 实测修正):
1807
+ * - session 用 `new wrapper.NodeIQQNTWrapperSession()` 创建;exports 不含 NodeI*Adapter/Listener
1808
+ * 构造器(89 键实测无)——adapter 与 listener 一律传普通 JS 对象(NAPI 反射读取方法回调)。
1809
+ *
1810
+ * 模块边界(2026-08-05 解耦):
1811
+ * - 配置装配(buildEngineConfig / buildLoginConfig / buildSessionConfig)→ wrapper-config.ts
1812
+ * - NAPI 回调适配器(GlobalAdapter / DependsAdapter / DispatcherAdapter / listener 工厂)→ wrapper-adapters.ts
1813
+ * - 本文件只保留流程编排:快速登录 + session 初始化 + init 完成信号等待。
1814
+ */
1815
+ /** 等待条件满足(轮询,带超时)。 */
1816
+ function waitFor(predicate, timeoutMs, intervalMs = 500) {
1817
+ return new Promise((resolve) => {
1818
+ const started = Date.now();
1819
+ const tick = () => {
1820
+ if (predicate()) {
1821
+ resolve(true);
1822
+ return;
1823
+ }
1824
+ if (Date.now() - started > timeoutMs) {
1825
+ resolve(false);
1826
+ return;
1827
+ }
1828
+ setTimeout(tick, intervalMs);
1829
+ };
1830
+ tick();
1831
+ });
1832
+ }
1833
+ /** session init 默认超时(毫秒)。 */
1834
+ const DEFAULT_INIT_TIMEOUT_MS = 15e3;
1835
+ /** session 就绪等待默认超时(毫秒)。 */
1836
+ const SESSION_READY_TIMEOUT_MS = 3e4;
1837
+ /** session 就绪轮询间隔(毫秒)。 */
1838
+ const SESSION_READY_POLL_MS = 1e3;
1839
+ /**
1840
+ * 等待 QQ 自己完成 session 初始化(framework 复用模式,2026-08-05 修正)。
1841
+ *
1842
+ * 背景:QQ 9.9.31 实测——session 由 QQ 主进程自己创建并 init(登录后)。
1843
+ * 我们不自己 init(重复 init 断言 "implementation of IQQNTWrapperSession is not valid"),
1844
+ * 也不自己 create(登录前 create() 干扰 QQ,实测 QQ 退出 code=0)。
1845
+ * 正确做法:等 QQ 的 session 就绪(getMsgService 非 null = 核心 service 已挂载)。
1846
+ */
1847
+ async function waitSessionReady(ctx, opts) {
1848
+ const { session } = ctx;
1849
+ if (session === null || session === void 0) throw kernelError("session 未创建", "INVALID_STATE");
1850
+ if (!await waitFor(() => {
1851
+ try {
1852
+ const svc = session.getMsgService();
1853
+ return svc !== null && svc !== void 0;
1854
+ } catch {
1855
+ return false;
1856
+ }
1857
+ }, opts.timeoutMs ?? SESSION_READY_TIMEOUT_MS, SESSION_READY_POLL_MS)) throw kernelError("等待 QQ session 就绪超时(QQ 未完成 init)", "TIMEOUT");
1858
+ }
1859
+ /**
1860
+ * 网络状态(MSF)常量。
1861
+ * 3 = 已连接(NapCat waitForNetworkConnection 语义,自研描述)。
1862
+ */
1863
+ const MSF_STATUS_CONNECTED = 3;
1864
+ /** 快速登录网络异常错误码(1006511,P2-1 实测:登录前网络未就绪时报此错)。 */
1865
+ const NETWORK_ERROR_CODE = "1006511";
1866
+ /** 登录连接异常错误提示(自建宿主实测:connect 后未缓冲即 quickLogin 报此错)。 */
1867
+ const CONNECTION_ERROR_HINT = "登录系统连接异常";
1868
+ /** 网络重试最大次数(每次重试前等网络就绪)。 */
1869
+ const NETWORK_RETRY_MAX = 3;
1870
+ /** 网络就绪等待超时(毫秒)。 */
1871
+ const NETWORK_READY_TIMEOUT_MS = 15e3;
1872
+ /** 网络就绪轮询间隔(毫秒)。 */
1873
+ const NETWORK_READY_POLL_MS = 1e3;
1874
+ /**
1875
+ * 等待网络连接就绪(loginService.getMsfStatus() === 3)。
1876
+ * 参考 NapCat waitForNetworkConnection 思路(自研实现):快速登录在 QQ 刚拉起、
1877
+ * 网络栈未初始化时报 1006511 网络异常——等待 MSF 连接后再重试。
1878
+ * @returns 是否在超时前就绪。
1879
+ */
1880
+ function waitForNetworkConnection(ctx, opts = {}) {
1881
+ const raw = ctx.loginService;
1882
+ if (raw === null || typeof raw.getMsfStatus !== "function") return Promise.resolve(false);
1883
+ return waitFor(() => {
1884
+ try {
1885
+ return raw.getMsfStatus() === MSF_STATUS_CONNECTED;
1886
+ } catch {
1887
+ return false;
1888
+ }
1889
+ }, opts.timeoutMs ?? NETWORK_READY_TIMEOUT_MS, NETWORK_READY_POLL_MS);
1890
+ }
1891
+ /** 列出历史登录账号(boot.cjs 启动横幅用,「可用快速登录 of QQ」)。 */
1892
+ async function listLoginAccounts(ctx) {
1893
+ const raw = ctx.loginService;
1894
+ if (raw === null) throw kernelError("loginService 无效(缺 getLoginList)", "INVALID_STATE");
1895
+ return (await raw.getLoginList()).LocalLoginInfoList;
1896
+ }
1897
+ /**
1898
+ * 登录服务连接(自建宿主必需,HANDOVER-V6/V10 p0-kernel-flow 实证顺序):
1899
+ * connect() → 等 onLoginConnected。不 connect 则快速登录报「登录系统连接异常」。
1900
+ *
1901
+ * 对齐 p0-kernel-flow:initConfig → getLoginList → connect + onLoginConnected →
1902
+ * quickLoginWithUin。QQ 环境(worker/主进程)下可能已连接——connect 幂等,
1903
+ * 监听器超时兜底,不影响原流程。
1904
+ */
1905
+ async function ensureLoginConnected(ctx, opts) {
1906
+ const raw = ctx.loginService;
1907
+ if (raw === null || typeof raw.connect !== "function") return;
1908
+ const connect = raw.connect;
1909
+ const addListener = raw.addKernelLoginListener;
1910
+ await new Promise((resolve) => {
1911
+ let settled = false;
1912
+ const finish = () => {
1913
+ if (!settled) {
1914
+ settled = true;
1915
+ resolve();
1916
+ }
1917
+ };
1918
+ try {
1919
+ const listener = createLoginListener();
1920
+ listener.onLoginConnected = () => {
1921
+ finish();
1922
+ };
1923
+ if (typeof addListener === "function") addListener(listener);
1924
+ } catch {}
1925
+ setTimeout(finish, opts.timeoutMs ?? NETWORK_READY_TIMEOUT_MS);
1926
+ try {
1927
+ connect();
1928
+ } catch {
1929
+ finish();
1930
+ }
1931
+ });
1932
+ await sleep(CONNECTION_SETTLE_MS);
1933
+ }
1934
+ /** 连接稳定缓冲(毫秒,p0-kernel-flow 实证值)。 */
1935
+ const CONNECTION_SETTLE_MS = 3e3;
1936
+ /** 短延迟。 */
1937
+ function sleep(ms) {
1938
+ return new Promise((resolve) => {
1939
+ setTimeout(resolve, ms);
1940
+ });
1941
+ }
1942
+ /**
1943
+ * 快速登录:遍历历史登录列表尝试。
1944
+ * P2-1:失败且错误为网络异常(1006511)时,等网络就绪后重试(最多 NETWORK_RETRY_MAX 次)。
1945
+ */
1946
+ async function quickLogin(ctx, opts) {
1947
+ const raw = ctx.loginService;
1948
+ if (raw === null) throw kernelError("loginService 无效(缺 getLoginList)", "INVALID_STATE");
1949
+ const loginService = raw;
1950
+ await ensureLoginConnected(ctx, { ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {} });
1951
+ const items = (await loginService.getLoginList()).LocalLoginInfoList;
1952
+ if (items.length === 0) throw kernelError("无历史登录账号", "NOT_LOGIN");
1953
+ let target;
1954
+ if (opts.uin !== void 0) {
1955
+ target = items.find((i) => i.uin === opts.uin);
1956
+ if (target === void 0) {
1957
+ const available = items.map((i) => i.uin).join("、");
1958
+ throw kernelError(`账号 ${opts.uin} 不在登录列表(可用:${available})`, "NOT_FOUND");
1959
+ }
1960
+ } else {
1961
+ target = items.find((i) => i.isQuickLogin);
1962
+ if (target === void 0) {
1963
+ const [first] = items;
1964
+ target = first;
1965
+ }
1966
+ }
1967
+ if (target === void 0) throw kernelError("无可用登录账号", "NOT_LOGIN");
1968
+ return loginWithNetworkRetry(ctx, loginService, target, opts);
1969
+ }
1970
+ /**
1971
+ * 带网络重试的快速登录(P2-1)。
1972
+ * 重试语义:仅当失败为网络异常(1006511)且未达上限时,等网络就绪后重试。
1973
+ */
1974
+ async function loginWithNetworkRetry(ctx, loginService, target, opts) {
1975
+ let lastErrMsg = "";
1976
+ for (let attempt = 1; attempt <= NETWORK_RETRY_MAX; attempt += 1) {
1977
+ const { errMsg } = (await loginService.quickLoginWithUin(target.uin)).loginErrorInfo;
1978
+ if (!errMsg) return {
1979
+ uin: target.uin,
1980
+ uid: target.uid ?? "",
1981
+ nick: target.nickName ?? ""
1982
+ };
1983
+ lastErrMsg = errMsg;
1984
+ if (!(errMsg.includes(NETWORK_ERROR_CODE) || errMsg.includes(CONNECTION_ERROR_HINT)) || attempt >= NETWORK_RETRY_MAX) break;
1985
+ if (!await waitForNetworkConnection(ctx, { timeoutMs: opts.timeoutMs ?? NETWORK_READY_TIMEOUT_MS })) break;
1986
+ }
1987
+ throw kernelError(`快速登录失败: ${lastErrMsg}`, "NOT_LOGIN");
1988
+ }
1989
+ /** session 初始化(4 参全为普通 JS 对象,等 init 完成信号)。 */
1990
+ async function initAndStartSession(ctx, config, listener, opts) {
1991
+ const { session } = ctx;
1992
+ if (session === null || session === void 0) throw kernelError("session 未创建", "INVALID_STATE");
1993
+ const depends = new DependsAdapter();
1994
+ const dispatcher = new DispatcherAdapter();
1995
+ const initComplete = new Promise((resolve, reject) => {
1996
+ const onOpentelemetry = listener.onOpentelemetryInit;
1997
+ listener.onOpentelemetryInit = (info) => {
1998
+ if (info.is_init) resolve();
1999
+ if (typeof onOpentelemetry === "function") onOpentelemetry(info);
2000
+ };
2001
+ const onInitComplete = listener.onSessionInitComplete;
2002
+ listener.onSessionInitComplete = (r) => {
2003
+ if (r === 0 || r === "0") resolve();
2004
+ else reject(kernelError(`session init 失败: ${String(r)}`, "UNKNOWN"));
2005
+ if (typeof onInitComplete === "function") onInitComplete(r);
2006
+ };
2007
+ });
2008
+ session.init(config, depends, dispatcher, listener);
2009
+ const { startupSession } = ctx;
2010
+ if (startupSession !== null && typeof startupSession.start === "function") try {
2011
+ startupSession.start();
2012
+ } catch {}
2013
+ else try {
2014
+ session.startNT(0);
2015
+ } catch {
2016
+ try {
2017
+ session.startNT();
2018
+ } catch {}
2019
+ }
2020
+ if (!await Promise.race([initComplete.then(() => true), waitFor(() => false, opts.timeoutMs ?? DEFAULT_INIT_TIMEOUT_MS).then(() => false)])) throw kernelError("session init 超时", "TIMEOUT");
2021
+ }
2022
+ //#endregion
2023
+ //#region src/login/login.ts
2024
+ /**
2025
+ * login.ts:QR 登录流程编排 + 状态机 + selfInfo(ADR-010,2026-08-05)
2026
+ *
2027
+ * 流程(QQ wrapper 登录契约,自研实现):
2028
+ * loginService.initConfig(core.login 已做)→ addKernelLoginListener → connect()
2029
+ * → 指定账号:quickLoginWithUin;否则 getQRCodePicture() 触发二维码
2030
+ * → 回调驱动状态机:未登录 → 扫码中 → 已扫码 → 已登录(onQRCodeLoginSucceed)
2031
+ * → 二维码过期(errType=1 errCode=3)→ refresh()
2032
+ *
2033
+ * 不含 UI 渲染(cli 只做二维码渲染/URL 打印)。
2034
+ */
2035
+ /** 二维码过期错误码(errType=1 errCode=3,实测)。 */
2036
+ const QR_EXPIRED_ERR_TYPE = 1;
2037
+ const QR_EXPIRED_ERR_CODE = 3;
2038
+ /** 二维码过期判定。 */
2039
+ function isQrCodeExpired(errType, errCode) {
2040
+ return errType === QR_EXPIRED_ERR_TYPE && errCode === QR_EXPIRED_ERR_CODE;
2041
+ }
2042
+ /** 二维码 png data URI 前缀剥离正则。 */
2043
+ const DATA_URI_PREFIX_RE = /^data:image\/\w+;base64,/;
2044
+ /** QR 登录会话(状态机 + 回调订阅)。 */
2045
+ var QrLoginSession = class {
2046
+ loginService;
2047
+ state = "idle";
2048
+ listenerId = null;
2049
+ self = null;
2050
+ qrCodeHandlers = /* @__PURE__ */ new Set();
2051
+ stateHandlers = /* @__PURE__ */ new Set();
2052
+ constructor(loginService) {
2053
+ const svc = loginService;
2054
+ if (svc === null || typeof svc.addKernelLoginListener !== "function") throw kernelError("loginService 无效(缺 addKernelLoginListener)", "INVALID_STATE");
2055
+ this.loginService = svc;
2056
+ }
2057
+ /** 当前状态。 */
2058
+ get currentState() {
2059
+ return this.state;
2060
+ }
2061
+ /** selfInfo(登录成功后有值)。 */
2062
+ get selfInfo() {
2063
+ return this.self;
2064
+ }
2065
+ /** 订阅二维码(png base64 + url)。返回退订函数。 */
2066
+ onQrCode(cb) {
2067
+ this.qrCodeHandlers.add(cb);
2068
+ return () => this.qrCodeHandlers.delete(cb);
2069
+ }
2070
+ /** 订阅状态变化。返回退订函数。 */
2071
+ onStateChange(cb) {
2072
+ this.stateHandlers.add(cb);
2073
+ return () => this.stateHandlers.delete(cb);
2074
+ }
2075
+ /** 开始登录:注册监听 → connect → 快速登录(指定账号)或触发二维码。 */
2076
+ start(opts = {}) {
2077
+ if (this.state !== "idle") return;
2078
+ this.listenerId = this.loginService.addKernelLoginListener(this.buildListener());
2079
+ this.loginService.connect();
2080
+ if (opts.quickUin !== void 0 && opts.quickUin !== "") this.loginService.quickLoginWithUin(opts.quickUin).catch(() => {
2081
+ this.refresh();
2082
+ });
2083
+ else this.refresh();
2084
+ }
2085
+ /** 刷新二维码(手动 / 过期自动)。 */
2086
+ refresh() {
2087
+ this.setState("waiting_scan");
2088
+ this.loginService.getQRCodePicture();
2089
+ }
2090
+ /** 构建登录监听(普通 JS 对象,NAPI 反射)。 */
2091
+ buildListener() {
2092
+ const listener = createLoginListener();
2093
+ listener.onQRCodeGetPicture = ({ pngBase64QrcodeData, qrcodeUrl }) => {
2094
+ const realBase64 = pngBase64QrcodeData.replace(DATA_URI_PREFIX_RE, "");
2095
+ this.emitQrCode({
2096
+ pngBase64: realBase64,
2097
+ qrcodeUrl
2098
+ });
2099
+ };
2100
+ listener.onQRCodeLoginPollingStarted = () => {
2101
+ this.setState("waiting_scan");
2102
+ };
2103
+ listener.onQRCodeSessionUserScaned = () => {
2104
+ this.setState("scanned");
2105
+ };
2106
+ listener.onQRCodeLoginSucceed = (result) => {
2107
+ this.self = {
2108
+ uin: result.uin,
2109
+ uid: result.uid,
2110
+ nick: result.nick ?? ""
2111
+ };
2112
+ this.setState("logged_in");
2113
+ };
2114
+ listener.onQRCodeSessionFailed = (errType, errCode) => {
2115
+ if (isQrCodeExpired(errType, errCode)) {
2116
+ this.refresh();
2117
+ return;
2118
+ }
2119
+ this.setState("failed");
2120
+ };
2121
+ listener.onLoginFailed = () => {
2122
+ this.setState("failed");
2123
+ };
2124
+ return listener;
2125
+ }
2126
+ /** 停止:注销监听(登录成功后调用,防重复回调)。 */
2127
+ stop() {
2128
+ if (this.listenerId !== null) {
2129
+ this.loginService.removeKernelLoginListener(this.listenerId);
2130
+ this.listenerId = null;
2131
+ }
2132
+ }
2133
+ setState(next) {
2134
+ this.state = next;
2135
+ for (const cb of this.stateHandlers) cb(next);
2136
+ }
2137
+ emitQrCode(qr) {
2138
+ for (const cb of this.qrCodeHandlers) cb(qr);
2139
+ }
2140
+ };
2141
+ //#endregion
2142
+ //#region src/wrapper/qq-data-path.ts
2143
+ /**
2144
+ * QQ 数据根路径解析(P2-1,2026-08-06)
2145
+ *
2146
+ * 从 `getNTUserDataInfoConfig()` 的返回值中提取 QQ 真实数据根路径:
2147
+ * worker(utilityProcess)模式下 loginService 是 new 的,commonPath 必须指向
2148
+ * QQ 真实数据路径才能读到历史账号(cli 的 `.napuketto\default` 读不到,
2149
+ * getLoginList 空,P2-1 实测)。
2150
+ *
2151
+ * 返回值形态可能是 JSON 字符串(对象/数组)或纯路径字符串,此处做宽容解析。
2152
+ */
2153
+ /**
2154
+ * QQ 全局数据目录(commonPath / desktopGlobalPath = 数据根/nt_qq/global,
2155
+ * HANDOVER-V6 自建宿主三要素之三,p0-kernel-flow 实证)。
2156
+ *
2157
+ * 防御:已含 nt_qq/global 后缀(worker 环境下 getNTUserDataInfoConfig 可能
2158
+ * 返回完整路径)则不重复拼接,避免 `nt_qq/global/nt_qq/global` 错位。
2159
+ */
2160
+ function resolveQqGlobalPath(root) {
2161
+ const norm = root.replace(/\\/g, "/");
2162
+ if (norm.endsWith("/nt_qq/global")) return root;
2163
+ if (norm.endsWith("/nt_qq")) return join(root, "global");
2164
+ return join(root, "nt_qq", "global");
2165
+ }
2166
+ /** 从 getNTUserDataInfoConfig 返回值提取数据根(JSON 字符串解析或纯路径)。 */
2167
+ function extractDataRoot(raw) {
2168
+ if (raw.startsWith("{") || raw.startsWith("[")) try {
2169
+ return findPathInValue(JSON.parse(raw));
2170
+ } catch {
2171
+ return null;
2172
+ }
2173
+ return raw;
2174
+ }
2175
+ /** 递归在对象/数组里找含 QQ 数据路径特征的字符串值。 */
2176
+ function findPathInValue(value) {
2177
+ if (typeof value === "string") return isQqDataPath(value) ? value : null;
2178
+ if (Array.isArray(value)) return findInArray(value);
2179
+ if (typeof value === "object" && value !== null) return findInObject(value);
2180
+ return null;
2181
+ }
2182
+ /** 判断字符串是否含 QQ 数据路径特征。 */
2183
+ function isQqDataPath(value) {
2184
+ return value.includes("Tencent Files") || value.includes("nt_qq");
2185
+ }
2186
+ /** 在数组里递归查找。 */
2187
+ function findInArray(value) {
2188
+ for (const item of value) {
2189
+ const found = findPathInValue(item);
2190
+ if (found !== null) return found;
2191
+ }
2192
+ return null;
2193
+ }
2194
+ /** 在对象值里递归查找。 */
2195
+ function findInObject(value) {
2196
+ for (const v of Object.values(value)) {
2197
+ const found = findPathInValue(v);
2198
+ if (found !== null) return found;
2199
+ }
2200
+ return null;
2201
+ }
2202
+ //#endregion
2203
+ //#region src/types/wrapper.ts
2204
+ /**
2205
+ * wrapper.node 的 NAPI 接口面(自研描述,非移植)
2206
+ *
2207
+ * 来源:wrapper.node 在 QQ 定制版 Electron 里通过 napi_define_class / napi_set_named_property
2208
+ * 注册的 JS 类(2026-08-05 Ghidra 反编译 FUN_180c6274a / FUN_180c80224 确认类名)。
2209
+ * 这些接口是 wrapper.node 对外暴露的外部系统契约——我们自研描述其形状,未复制任何第三方代码。
2210
+ *
2211
+ * 注意:这里所有对象由 QQ 运行时的 NAPI 层自动构建/转换,业务层禁止触碰 C++ ABI。
2212
+ */
2213
+ /** 平台类型(与 wrapper.node 的 platform 字段对应)。 */
2214
+ const PlatformType = {
2215
+ KUNKNOWN: 0,
2216
+ KANDROID: 1,
2217
+ KIOS: 2,
2218
+ KWINDOWS: 3,
2219
+ KMAC: 4,
2220
+ KLINUX: 5
2221
+ };
2222
+ /** 厂商类型(deviceInfo.vendorType)。 */
2223
+ const VendorType = {
2224
+ KNOSETONIOS: 0,
2225
+ KUNSUPPORTANDROIDPUSH: 1,
2226
+ KSUPPORTTPNS: 2,
2227
+ KSUPPORTHMS: 3,
2228
+ KSUPPORTOPPOPUSH: 4,
2229
+ KSUPPORTVIVOPUSH: 5,
2230
+ KSUPPORTGOOGLEPUSH: 99
2231
+ };
2232
+ //#endregion
2233
+ //#region src/wrapper/wrapper-config.ts
2234
+ /**
2235
+ * wrapper 交互配置装配(纯函数,无状态)
2236
+ *
2237
+ * 生成 QQ wrapper 交互所需的三份配置:
2238
+ * - buildEngineConfig → engine.initWithDeskTopConfig(appid / qua / 版本 / 路径)
2239
+ * - buildLoginConfig → loginService.initConfig
2240
+ * - buildSessionConfig → session.init(登录成功后调用)
2241
+ *
2242
+ * 与 lifecycle.ts(流程编排)解耦:本模块只回答「配置长什么样」,不管流程怎么走。
2243
+ * 字段为 wrapper.node 外部契约(appid/qua/版本),运行时实测确认,自研描述。
2244
+ */
2245
+ /** 系统信息(先用 fixed 值,真实环境探测后补)。 */
2246
+ function systemInfo() {
2247
+ return {
2248
+ platVer: "Windows 10.0.22631",
2249
+ osVersion: "Windows 10 Pro",
2250
+ devType: "Windows"
2251
+ };
2252
+ }
2253
+ /** 纯数字串(major.node 提取的 appid 判定)。 */
2254
+ const DIGITS_ONLY_RE = /^\d+$/;
2255
+ /**
2256
+ * 从 major.node 提取 appid(NapCat parseAppidFromMajorV2 的自研等价实现)。
2257
+ * major.node 含 `QQAppId/` 标记后跟数字(腾讯工具链产物,实测确认)。
2258
+ * 返回 null 表示解析失败(调用方回退硬编码表)。
2259
+ */
2260
+ function parseAppidFromMajor(majorPath) {
2261
+ if (!existsSync(majorPath)) return null;
2262
+ let buf;
2263
+ try {
2264
+ buf = readFileSync(majorPath);
2265
+ } catch {
2266
+ return null;
2267
+ }
2268
+ const marker = Buffer.from("QQAppId/", "utf-8");
2269
+ let pos = 0;
2270
+ while (pos < buf.length) {
2271
+ const idx = buf.indexOf(marker, pos);
2272
+ if (idx === -1) return null;
2273
+ const start = idx + marker.length;
2274
+ let end = start;
2275
+ while (end < buf.length && buf[end] !== 0) end += 1;
2276
+ const str = buf.subarray(start, end).toString("utf-8");
2277
+ if (DIGITS_ONLY_RE.test(str)) return str;
2278
+ pos = end + 1;
2279
+ }
2280
+ return null;
2281
+ }
2282
+ /** Windows 兜底 appid / qua(major.node 解析失败时)。 */
2283
+ function fallbackAppidQua(fullVersion) {
2284
+ return {
2285
+ appid: "537237765",
2286
+ qua: `V1_WIN_NQ_${fullVersion}_${fullVersion.split("-")[1] ?? ""}_GW_B`
2287
+ };
2288
+ }
2289
+ /**
2290
+ * 解析 appid / qua(自研,参考 NapCat QQBasicInfoWrapper 思路但独立实现):
2291
+ * 1. 优先从 major.node 提取 appid(实测 9.9.33-51802 = 537376818)
2292
+ * 2. 回退硬编码表(旧版 537237765)
2293
+ * majorPath 传 wrapper.node 同目录 major.node;不传则跳过 major 解析。
2294
+ */
2295
+ function resolveAppidQua(fullVersion, majorPath) {
2296
+ if (majorPath !== void 0) {
2297
+ const appid = parseAppidFromMajor(majorPath);
2298
+ if (appid !== null) return {
2299
+ appid,
2300
+ qua: `V1_WIN_NQ_${fullVersion}_${fullVersion.split("-")[1] ?? ""}_GW_B`
2301
+ };
2302
+ }
2303
+ return fallbackAppidQua(fullVersion);
2304
+ }
2305
+ /** 生成 engine 桌面配置(wrapper 契约字段)。majorPath 可选(解析 appid/qua)。 */
2306
+ function buildEngineConfig(fullVersion, dataPathGlobal, majorPath) {
2307
+ const { osVersion } = systemInfo();
2308
+ const { qua } = resolveAppidQua(fullVersion, majorPath);
2309
+ return {
2310
+ base_path_prefix: "",
2311
+ platform_type: PlatformType.KWINDOWS,
2312
+ app_type: 4,
2313
+ app_version: fullVersion,
2314
+ os_version: osVersion,
2315
+ use_xlog: true,
2316
+ qua,
2317
+ global_path_config: { desktopGlobalPath: dataPathGlobal },
2318
+ thumb_config: {
2319
+ maxSide: 324,
2320
+ minSide: 48,
2321
+ longLimit: 6,
2322
+ density: 2
2323
+ }
2324
+ };
2325
+ }
2326
+ /** 生成登录初始化配置(externalVersion: false 与 NapCat 同款,扫码兼容关键)。 */
2327
+ function buildLoginConfig(appid, fullVersion, commonPath) {
2328
+ const { platVer } = systemInfo();
2329
+ return {
2330
+ machineId: "",
2331
+ appid,
2332
+ platVer,
2333
+ commonPath,
2334
+ clientVer: fullVersion,
2335
+ hostName: hostname(),
2336
+ externalVersion: false
2337
+ };
2338
+ }
2339
+ /** 生成 session 初始化配置(登录成功后调用)。 */
2340
+ function buildSessionConfig(options) {
2341
+ const { appid, fullVersion, selfUin, selfUid, accountPath, downloadPath } = options;
2342
+ const { platVer, osVersion, devType } = systemInfo();
2343
+ const deviceInfo = {
2344
+ guid: "",
2345
+ buildVer: fullVersion,
2346
+ localId: 2052,
2347
+ devName: hostname(),
2348
+ devType,
2349
+ vendorName: "",
2350
+ osVer: osVersion,
2351
+ vendorOsName: devType,
2352
+ setMute: false,
2353
+ vendorType: VendorType.KNOSETONIOS
2354
+ };
2355
+ return {
2356
+ selfUin,
2357
+ selfUid,
2358
+ desktopPathConfig: { account_path: accountPath },
2359
+ clientVer: fullVersion,
2360
+ a2: "",
2361
+ d2: "",
2362
+ d2Key: "",
2363
+ machineId: "",
2364
+ platform: PlatformType.KWINDOWS,
2365
+ platVer,
2366
+ appid,
2367
+ rdeliveryConfig: {
2368
+ appKey: "",
2369
+ systemId: 0,
2370
+ appId: "",
2371
+ logicEnvironment: "",
2372
+ platform: PlatformType.KWINDOWS,
2373
+ language: "",
2374
+ sdkVersion: "",
2375
+ userId: "",
2376
+ appVersion: "",
2377
+ osVersion: "",
2378
+ bundleId: "",
2379
+ serverUrl: "",
2380
+ fixedAfterHitKeys: [""]
2381
+ },
2382
+ defaultFileDownloadPath: downloadPath,
2383
+ deviceInfo,
2384
+ deviceConfig: "{\"appearance\":{\"isSplitViewMode\":true},\"msg\":{}}"
2385
+ };
2386
+ }
2387
+ //#endregion
2388
+ //#region src/wrapper/wrapper-loader.ts
2389
+ /**
2390
+ * wrapper 引导(NAPI 范式,2026-08-05 重构,替代旧 koffi 方案)
2391
+ *
2392
+ * 事实(实测):wrapper.node 不是标准 NAPI self-register 模块,只能在 QQ 定制版
2393
+ * Electron 主进程里由 preload 注册。@napuketto/loader 注入 hook DLL 后,boot.cjs
2394
+ * 在 QQ 主进程内截获 `process.dlopen` 的 `module.exports`,然后调用本模块初始化。
2395
+ *
2396
+ * 本模块**不再**使用 koffi / vtable / 内存偏移——所有对象由 QQ 的 NAPI 层自动构建。
2397
+ * 业务层拿到的都是真实 JS 对象:engine.initWithDeskTopConfig(config, adapter) 等。
2398
+ */
2399
+ /** 默认 engine 配置(KWINDOWS + 版本号,字段待首个真实联调确认)。 */
2400
+ function defaultEngineConfig(env, qqDataPath) {
2401
+ let osVersion = process.platform;
2402
+ if (process.platform === "win32") osVersion = "win32";
2403
+ return {
2404
+ base_path_prefix: env.dataDir ?? "",
2405
+ platform_type: PlatformType.KWINDOWS,
2406
+ app_type: 4,
2407
+ app_version: env.qqVersion ?? "",
2408
+ os_version: osVersion,
2409
+ use_xlog: false,
2410
+ qua: "",
2411
+ global_path_config: { desktopGlobalPath: qqDataPath ?? env.dataDir ?? "" },
2412
+ thumb_config: {
2413
+ maxSide: 324,
2414
+ minSide: 48,
2415
+ longLimit: 6,
2416
+ density: 2
2417
+ }
2418
+ };
2419
+ }
2420
+ /**
2421
+ * Electron 进程类型(Node 的 process 类型无此字段)。
2422
+ * - "browser":QQ 主进程(V1 模式)
2423
+ * - "utility":utilityProcess Worker(路线 B,2026-08-06)
2424
+ */
2425
+ function electronProcessType() {
2426
+ return process.type;
2427
+ }
2428
+ /**
2429
+ * 解析 QQ 用户数据根(登录数据目录,如 `C:\Users\<user>\Documents\Tencent Files`)。
2430
+ * 优先 NodeQQNTWrapperUtil.getNTUserDataInfoConfig()(QQ 官方 API),回退默认位置。
2431
+ *
2432
+ * 背景(2026-08-06 P2-1 实测):worker(utilityProcess)模式下 loginService 是
2433
+ * `new NodeIKernelLoginService()` 新建的,其 initConfig 的 commonPath 必须指向
2434
+ * QQ 真实数据路径才能读到历史账号——cli 的 `.napuketto\default` 数据目录读不到
2435
+ * (getLoginList 返回空 → 只能 QR 登录)。V1 主进程模式 QQ 自己 initConfig 过
2436
+ * 正确路径,无需此解析。
2437
+ */
2438
+ function resolveQqUserDataRoot(exports) {
2439
+ try {
2440
+ const raw = (exports.NodeQQNTWrapperUtil?.get?.())?.getNTUserDataInfoConfig?.();
2441
+ if (typeof raw === "string" && raw.trim() !== "") {
2442
+ const extracted = extractDataRoot(raw.trim());
2443
+ if (extracted !== null) return extracted;
2444
+ }
2445
+ } catch {}
2446
+ return join(homedir(), "Documents", "Tencent Files");
2447
+ }
2448
+ /**
2449
+ * 从 loader 截获的 NAPI exports 创建 wrapper 上下文。
2450
+ * 在 QQ 主进程内调用(boot.cjs → kernel 入口)。
2451
+ */
2452
+ function createWrapper(exports, versionInfo) {
2453
+ const engineMaybe = exports.NodeIQQNTWrapperEngine;
2454
+ if (engineMaybe === null || typeof engineMaybe.get !== "function") throw kernelError("wrapper.node exports 无效(缺少 NodeIQQNTWrapperEngine)", "INVALID_PARAM");
2455
+ const engine = engineMaybe.get();
2456
+ if (!engine || typeof engine.initWithDeskTopConfig !== "function") throw kernelError("NodeIQQNTWrapperEngine.get() 返回对象无效", "INVALID_PARAM");
2457
+ return {
2458
+ exports,
2459
+ engine,
2460
+ versionInfo,
2461
+ session: null,
2462
+ startupSession: null,
2463
+ loginService: null
2464
+ };
2465
+ }
2466
+ /** engine 初始化(wrapper 契约:先 engine 后 session)。config 为普通 JS 对象。 */
2467
+ function initEngine(ctx, config) {
2468
+ ctx.engine.initWithDeskTopConfig(config, new GlobalAdapter());
2469
+ }
2470
+ /**
2471
+ * 创建会话(2026-08-06 P2-0 实测修正——NapCat 方式自研等价):
2472
+ * 1. StartupSessionWrapper.create()(启动会话包装)
2473
+ * 2. getNTWrapperSession("nt_1")(QQ 主 session,带完整 cpp_impl)
2474
+ * 3. S.create() 回退
2475
+ *
2476
+ * 背景:`new NodeIQQNTWrapperSession()` 构造对象缺 cpp_impl,session.init 断言
2477
+ * "implementation of IQQNTWrapperSession is not valid"(P2-0 实测确认)。
2478
+ * NapCat 用 StartupSessionWrapper.create() + getNTWrapperSession('nt_1') 解决——
2479
+ * 返回带完整实现的实例。worker(utilityProcess)继承 QQ env 后同样有效。
2480
+ */
2481
+ function createSession(ctx) {
2482
+ const X = ctx.exports;
2483
+ const S = X.NodeIQQNTWrapperSession;
2484
+ let session = null;
2485
+ try {
2486
+ const Ssw = X.NodeIQQNTStartupSessionWrapper;
2487
+ if (Ssw !== void 0 && typeof Ssw.create === "function") ctx.startupSession = Ssw.create();
2488
+ } catch {}
2489
+ try {
2490
+ if (typeof S.getNTWrapperSession === "function") {
2491
+ const got = S.getNTWrapperSession("nt_1");
2492
+ if (got !== null && got !== void 0) session = got;
2493
+ }
2494
+ } catch {}
2495
+ if (session === null) try {
2496
+ if (typeof S.create === "function") session = S.create();
2497
+ } catch {
2498
+ session = null;
2499
+ }
2500
+ if (session === null) session = new S();
2501
+ ctx.session = session;
2502
+ return session;
2503
+ }
2504
+ /** session 初始化(4 参全为 JS 对象,NAPI 自动转换)。 */
2505
+ function initSession(ctx, config, listener) {
2506
+ (ctx.session ?? createSession(ctx)).init(config, new DependsAdapter(), new DispatcherAdapter(), listener);
2507
+ }
2508
+ /**
2509
+ * 启动会话(有 startupSession 用 start(),否则 startNT(0))。
2510
+ *
2511
+ * ⚠️ 2026-08-07 V9 修正:单纯 start() 只在「已 init」的 session 上有效。
2512
+ * 自建宿主必须走 initAndStartSession(先 session.init 再 startupSession.start())——
2513
+ * 先 start 后 init 业务 service 不挂载(HANDOVER-V9 实测)。本函数保留给
2514
+ * 已 init 的 session 补启动用;新路径请用 initAndStartSession。
2515
+ */
2516
+ function startSession(ctx) {
2517
+ const { session, startupSession } = ctx;
2518
+ if (session === null) throw kernelError("session 未创建,无法启动", "INVALID_STATE");
2519
+ if (startupSession !== null && typeof startupSession.start === "function") {
2520
+ startupSession.start();
2521
+ return;
2522
+ }
2523
+ session.startNT(0);
2524
+ }
2525
+ /**
2526
+ * boot 装配入口:createWrapper → initEngine → session。
2527
+ *
2528
+ * **session 来源(2026-08-05 修正)**:
2529
+ * 1. qqSession(boot.cjs 拦截 `new` 窃取的 QQ 已 init session)——尚未落地
2530
+ * 2. createSession(`new NodeIQQNTWrapperSession()`)——实测可用,默认路径
2531
+ * (getMainSession 实测为空壳:service 全 null + 缺 startNT,仅 probe 探测参考)
2532
+ *
2533
+ * 由 loader runtime/boot.cjs 在 QQ 主进程内调用(import kernel dist 后)。
2534
+ * 返回 WrapperContext;失败抛 KernelError。
2535
+ */
2536
+ function startNapuketto(options) {
2537
+ const { env, engineConfig, sessionConfig, wrapperExports, qqSession, qqLoginService } = options;
2538
+ const ctx = createWrapper(wrapperExports, {
2539
+ fullVersion: env?.qqVersion ?? "",
2540
+ buildVersion: env?.qqVersion ?? ""
2541
+ });
2542
+ const isSelfHost = electronProcessType() === void 0;
2543
+ if (qqSession !== void 0 && qqSession !== null) ctx.session = qqSession;
2544
+ else if (isSelfHost) createSession(ctx);
2545
+ let qqGlobalPath = null;
2546
+ if (electronProcessType() !== "browser") {
2547
+ const root = resolveQqUserDataRoot(wrapperExports);
2548
+ if (root !== null) qqGlobalPath = resolveQqGlobalPath(root);
2549
+ }
2550
+ initEngine(ctx, engineConfig ?? defaultEngineConfig(env ?? {}, qqGlobalPath ?? void 0));
2551
+ if (qqLoginService !== void 0 && qqLoginService !== null) ctx.loginService = qqLoginService;
2552
+ else try {
2553
+ const loginSvcCtor = ctx.exports.NodeIKernelLoginService;
2554
+ if (loginSvcCtor !== null && typeof loginSvcCtor.get === "function") ctx.loginService = loginSvcCtor.get();
2555
+ else ctx.loginService = new ctx.exports.NodeIKernelLoginService();
2556
+ } catch {
2557
+ ctx.loginService = null;
2558
+ }
2559
+ if (qqSession !== void 0 && qqSession !== null) ctx.session = qqSession;
2560
+ else if (!isSelfHost) createSession(ctx);
2561
+ if (sessionConfig !== void 0) {
2562
+ initSession(ctx, sessionConfig, createSessionListener());
2563
+ startSession(ctx);
2564
+ }
2565
+ return ctx;
2566
+ }
2567
+ //#endregion
2568
+ //#region src/core.ts
2569
+ /**
2570
+ * NapukettoCore:装配 + 启动(§9 第 6 项,2026-08-05 实现)
2571
+ *
2572
+ * 装配:paths(建目录)→ logger → CoreContext。
2573
+ * 启动:attachWrapper(wrapperExports → startNapuketto)→ login(快速登录 → session.init)。
2574
+ *
2575
+ * 典型用法(boot.cjs 截获 exports 后):
2576
+ * const core = await NapukettoCore.create({ dataRoot, account });
2577
+ * core.attachWrapper(wrapperExports, { qqVersion, dataDir });
2578
+ * await core.login({ appid: "537237765" });
2579
+ *
2580
+ * 与手动拼 startNapuketto/quickLogin/initAndStartSession 等价,但把装配状态
2581
+ * (ctx)集中管理,协议层直接消费 core.ctx。
2582
+ */
2583
+ /**
2584
+ * 核心装配器:持有 CoreContext,负责启动/停止编排。
2585
+ * 无全局单例(ADR-015 推论)——每账号每进程实例化一份。
2586
+ */
2587
+ var NapukettoCore = class NapukettoCore {
2588
+ /** 装配根(协议层 / apis / cache 的消费入口)。 */
2589
+ ctx;
2590
+ /** 由 create() 装配,不直接 new。 */
2591
+ constructor(ctx) {
2592
+ this.ctx = ctx;
2593
+ }
2594
+ /**
2595
+ * 装配:paths(建目录)→ logger → CoreContext。
2596
+ * 不加载 wrapper / 不登录——启动阶段由 attachWrapper + login 完成。
2597
+ */
2598
+ static create(opts = {}) {
2599
+ const paths = new PathWrapper(opts.paths);
2600
+ paths.ensure();
2601
+ const logger = createLogger({
2602
+ level: opts.logLevel ?? "info",
2603
+ console: opts.consoleLog ?? true,
2604
+ file: opts.logFile ?? paths.file("logs", "napuketto.log"),
2605
+ base: { name: "kernel" }
2606
+ });
2607
+ return new NapukettoCore(createCoreContext({
2608
+ logger,
2609
+ paths
2610
+ }));
2611
+ }
2612
+ /**
2613
+ * 装配 wrapper:wrapperExports → startNapuketto(engine.init + session 创建),
2614
+ * 结果挂到 ctx.wrapper。在 QQ 主进程内调用(boot.cjs 截获 exports 后)。
2615
+ * captured:boot.cjs 拦截 `new` 捕获的 QQ 实例(session/loginService,优先使用)。
2616
+ */
2617
+ attachWrapper(wrapperExports, env, captured) {
2618
+ let wrapper;
2619
+ if (env === void 0) wrapper = startNapuketto({
2620
+ wrapperExports,
2621
+ ...captured
2622
+ });
2623
+ else wrapper = startNapuketto({
2624
+ wrapperExports,
2625
+ env,
2626
+ ...captured
2627
+ });
2628
+ this.ctx.wrapper = wrapper;
2629
+ this.ctx.logger.info({ version: env?.qqVersion }, "wrapper 装配完成(engine + session)");
2630
+ return wrapper;
2631
+ }
2632
+ /**
2633
+ * 登录 + session 初始化(wrapper 契约,2026-08-05 实测确认):
2634
+ * loginService.initConfig → 登录(快速/QR)→ **登录成功后** create session + init
2635
+ * → 等 onOpentelemetryInit(is_init) 完成信号。成功后填 ctx.login。
2636
+ *
2637
+ * 关键(实测):登录前 create()/init 均断言失败(session 未 init / 干扰 QQ);
2638
+ * 登录后 QQ 自己 init 并触发 listener.onOpentelemetryInit——我们只需在登录后
2639
+ * create + init 并监听该信号。
2640
+ */
2641
+ async login(opts) {
2642
+ const { wrapper } = this.ctx;
2643
+ if (wrapper === null) throw kernelError("wrapper 未装配,无法登录", "INVALID_STATE");
2644
+ const loginService = wrapper.loginService;
2645
+ if (loginService !== null && typeof loginService.initConfig === "function") {
2646
+ let qqGlobalPath = null;
2647
+ if (electronProcessType() !== "browser") {
2648
+ const root = resolveQqUserDataRoot(wrapper.exports);
2649
+ if (root !== null) qqGlobalPath = resolveQqGlobalPath(root);
2650
+ }
2651
+ const commonPath = qqGlobalPath ?? this.ctx.paths.accountDir;
2652
+ const loginCfg = buildLoginConfig(opts.appid, wrapper.versionInfo.fullVersion, commonPath);
2653
+ loginService.initConfig(loginCfg);
2654
+ this.ctx.logger.info({ commonPath }, "loginService.initConfig OK");
2655
+ } else this.ctx.logger.warn("loginService 不可用,跳过 initConfig");
2656
+ let loginResult;
2657
+ try {
2658
+ const quickOpts = {};
2659
+ if (opts.quickUin !== void 0) quickOpts.uin = opts.quickUin;
2660
+ loginResult = await quickLogin(wrapper, quickOpts);
2661
+ this.ctx.logger.info({
2662
+ uin: loginResult.uin,
2663
+ uid: loginResult.uid
2664
+ }, "快速登录成功");
2665
+ } catch (err) {
2666
+ if (opts.qrFallback !== true) throw err;
2667
+ loginResult = await this.loginByQr(opts);
2668
+ }
2669
+ this.ctx.login = loginResult;
2670
+ return loginResult;
2671
+ }
2672
+ /**
2673
+ * 替换当前 session(登录后 Proxy 捕获的 QQ 新实例替换自己 create 的无效实例)。
2674
+ * 在 QQ 主进程内由 boot.cjs 调用(登录成功后)。
2675
+ */
2676
+ setSession(session) {
2677
+ if (this.ctx.wrapper === null) throw kernelError("wrapper 未装配,无法设置 session", "INVALID_STATE");
2678
+ this.ctx.wrapper.session = session;
2679
+ }
2680
+ /** QR 登录:QrLoginSession 状态机 + 二维码写缓存目录(无 UI,cli 可打印路径)。 */
2681
+ async loginByQr(opts) {
2682
+ const { wrapper } = this.ctx;
2683
+ if (wrapper === null || wrapper.loginService === null) throw kernelError("wrapper/loginService 不可用,无法 QR 登录", "INVALID_STATE");
2684
+ const session = new QrLoginSession(wrapper.loginService);
2685
+ const qrPath = opts.qrCodePath ?? this.ctx.paths.file("cache", "qrcode.png");
2686
+ session.onQrCode((qr) => {
2687
+ this.ctx.logger.warn(`请扫描二维码登录(保存: ${qrPath} | URL: ${qr.qrcodeUrl})`);
2688
+ if (qr.pngBase64 !== "") writeQrCodePng(qrPath, qr.pngBase64);
2689
+ });
2690
+ session.onStateChange((state) => {
2691
+ this.ctx.logger.info({ state }, "QR 登录状态");
2692
+ });
2693
+ const startOpts = {};
2694
+ if (opts.quickUin !== void 0) startOpts.quickUin = opts.quickUin;
2695
+ session.start(startOpts);
2696
+ await new Promise((resolve, reject) => {
2697
+ const offState = session.onStateChange((state) => {
2698
+ if (state === "logged_in") {
2699
+ offState();
2700
+ resolve();
2701
+ } else if (state === "failed") {
2702
+ offState();
2703
+ reject(kernelError("QR 登录失败", "NOT_LOGIN"));
2704
+ }
2705
+ });
2706
+ });
2707
+ const self = session.selfInfo;
2708
+ if (self === null) throw kernelError("QR 登录成功但 selfInfo 为空", "INVALID_STATE");
2709
+ session.stop();
2710
+ return {
2711
+ uin: self.uin,
2712
+ uid: self.uid,
2713
+ nick: self.nick
2714
+ };
2715
+ }
2716
+ /** 停止:日志收尾(资源清理留给 P2 常驻管理)。 */
2717
+ stop() {
2718
+ this.ctx.logger.info("NapukettoCore 停止");
2719
+ }
2720
+ };
2721
+ /** 写二维码 png 到磁盘(base64 解码,供 cli/用户扫码)。 */
2722
+ function writeQrCodePng(filePath, pngBase64) {
2723
+ try {
2724
+ mkdirSync(dirname(filePath), { recursive: true });
2725
+ writeFileSync(filePath, Buffer.from(pngBase64, "base64"));
2726
+ } catch {}
2727
+ }
2728
+ //#endregion
2729
+ //#region src/event-channel.ts
2730
+ /**
2731
+ * 类型化事件通道(ADR-003)
2732
+ *
2733
+ * - 事件名约定 `${Service}/${method}`(如 "Msg/onRecvMsg"),签名从 Listener 接口编译期推导
2734
+ * - 事件名写错 → 编译期报错(类型推导)
2735
+ * - `on` 返回 unsubscribe;`waitFor` 注册临时监听 → filter → 超时自动清理(替代魔法字符串方案)
2736
+ * - error 兜底:监听器抛异常不打断派发,统一经 `onError` 通知
2737
+ *
2738
+ * 组合实现而非 extends EventEmitter:EventEmitter.on 返回 `this`,与设计要求的
2739
+ * 返回 unsubscribe 冲突;组合更干净,且内核无全局单例(ADR-015 推论)。
2740
+ *
2741
+ * 类型推导说明:不直接 `ListenerEvents<L, Name>[E]` 索引(泛型下会扩宽到
2742
+ * string|number|symbol 索引,索引出 unknown),而是用 Extract 按事件名从
2743
+ * `{ name, fn }` 联合精确提取 handler / 参数类型,保证实例化后签名精确。
2744
+ */
2745
+ /** waitFor 默认超时(毫秒)。 */
2746
+ const DEFAULT_WAIT_TIMEOUT_MS = 3e4;
2747
+ /**
2748
+ * 类型化事件通道。
2749
+ *
2750
+ * @typeParam L - Listener 接口(探测产物,如 MsgListener)
2751
+ * @typeParam Name - Service 名(事件名前缀,如 "Msg")
2752
+ */
2753
+ var NTEventChannel = class {
2754
+ serviceName;
2755
+ emitter = new EventEmitter();
2756
+ errorHandlers = /* @__PURE__ */ new Set();
2757
+ constructor(serviceName) {
2758
+ this.serviceName = serviceName;
2759
+ this.emitter.setMaxListeners(0);
2760
+ }
2761
+ /**
2762
+ * 订阅事件:返回 unsubscribe(只注销该 handler,不影响其他订阅者)。
2763
+ * 每个 Service 只注册一次原生监听,缓存维护与协议翻译都订阅这里。
2764
+ */
2765
+ on(event, handler) {
2766
+ this.emitter.addListener(event, handler);
2767
+ return () => {
2768
+ this.emitter.removeListener(event, handler);
2769
+ };
2770
+ }
2771
+ /**
2772
+ * 请求-响应桥:注册临时监听 → filter 命中 → resolve;超时自动清理并 reject。
2773
+ * 替代魔法字符串事件名方案。
2774
+ */
2775
+ waitFor(event, opts = {}) {
2776
+ const timeoutMs = opts.timeout ?? DEFAULT_WAIT_TIMEOUT_MS;
2777
+ return new Promise((resolve, reject) => {
2778
+ const handler = (...args) => {
2779
+ if (opts.filter && !opts.filter(...args)) return;
2780
+ cleanup();
2781
+ resolve(args);
2782
+ };
2783
+ const timer = setTimeout(() => {
2784
+ cleanup();
2785
+ reject(new KernelError(`等待事件超时: ${event}`, "TIMEOUT"));
2786
+ }, timeoutMs);
2787
+ const cleanup = () => {
2788
+ clearTimeout(timer);
2789
+ this.emitter.removeListener(event, handler);
2790
+ };
2791
+ this.emitter.addListener(event, handler);
2792
+ });
2793
+ }
2794
+ /**
2795
+ * 类型化派发:wrapper-loader 的原生回调经此推入通道。
2796
+ * 2026-08-07 修复:原生 EventEmitter.emit 中一个 listener 抛异常会中断后续
2797
+ * listener 派发(Node 语义)——改为快照数组逐个 try/catch,保证「单个订阅者
2798
+ * 异常不打断派发」(协议翻译层异常不饿死缓存维护等其他订阅者)。
2799
+ */
2800
+ emit(event, ...args) {
2801
+ const listeners = this.emitter.listeners(event);
2802
+ for (const listener of listeners) try {
2803
+ listener(...args);
2804
+ } catch (err) {
2805
+ for (const handler of this.errorHandlers) handler(err);
2806
+ }
2807
+ }
2808
+ /** 订阅错误通知(监听器抛出的异常 / 其他内部错误)。 */
2809
+ onError(handler) {
2810
+ this.errorHandlers.add(handler);
2811
+ return () => {
2812
+ this.errorHandlers.delete(handler);
2813
+ };
2814
+ }
2815
+ };
2816
+ //#endregion
2817
+ //#region src/infra/config.ts
2818
+ /**
2819
+ * 配置基类(ADR-012)
2820
+ *
2821
+ * 读文件 → 校验 → 内存对象 → 变更写回。支持 **JSON / TOML** 两种文本格式
2822
+ * (按扩展名推断:.toml → TOML(smol-toml),其余 → JSON)。
2823
+ *
2824
+ * kernel 不依赖 zod:只依赖校验器的最小 `parse` 形状(`ConfigSchema`),
2825
+ * 协议包的 zod schema 天然满足;kernel 主配置用手写校验器包装。
2826
+ * 协议配置 schema 在各自协议包,见各包 docs/design.md。
2827
+ */
2828
+ /** 按扩展名推断格式(.toml → toml,其余 → json)。 */
2829
+ function inferFormat(path) {
2830
+ if (extname(path).toLowerCase() === ".toml") return "toml";
2831
+ return "json";
2832
+ }
2833
+ function isMissingFileError(err) {
2834
+ if (err instanceof Error) return err.code === "ENOENT";
2835
+ return false;
2836
+ }
2837
+ /**
2838
+ * 配置基类。每次 `load()` / `save()` 都经过 schema 校验,
2839
+ * 校验失败抛 `KernelError('INVALID_PARAM')`,不静默吞掉。
2840
+ * - `load()`:seed 存在 → 直接用 seed;否则读文件(缺失落默认值)。
2841
+ * - `save()`:按格式序列化(TOML/JSON)原子写。
2842
+ */
2843
+ var ConfigBase = class {
2844
+ path;
2845
+ defaults;
2846
+ schema;
2847
+ format;
2848
+ seed;
2849
+ value;
2850
+ constructor(opts) {
2851
+ this.path = opts.path;
2852
+ this.defaults = opts.defaults;
2853
+ this.schema = opts.schema;
2854
+ this.format = opts.format ?? inferFormat(opts.path);
2855
+ this.seed = opts.seed;
2856
+ this.value = opts.seed ?? opts.defaults;
2857
+ }
2858
+ /** 当前内存值(只读消费;外部不得直接修改返回对象)。 */
2859
+ get() {
2860
+ return this.value;
2861
+ }
2862
+ /**
2863
+ * 加载配置:
2864
+ * - seed 存在 → 直接用 seed(已校验,不读文件;全局 TOML 分段装配方式);
2865
+ * - 文件缺失 → 写入默认值并落盘(首次运行自动生成),返回默认值;
2866
+ * - 文件存在 → 读取 + 校验,更新内存,返回。
2867
+ */
2868
+ async load() {
2869
+ if (this.seed !== void 0) {
2870
+ this.value = this.seed;
2871
+ return this.value;
2872
+ }
2873
+ let raw;
2874
+ try {
2875
+ raw = await readFile(this.path, "utf8");
2876
+ } catch (err) {
2877
+ if (isMissingFileError(err)) {
2878
+ await this.save(this.defaults);
2879
+ return this.value;
2880
+ }
2881
+ throw new KernelError(`读取配置文件失败: ${this.path}`, "UNKNOWN", { cause: err });
2882
+ }
2883
+ this.value = this.parse(raw);
2884
+ return this.value;
2885
+ }
2886
+ /** 重新从磁盘加载(外部变更后的热更新入口,P2 配置热更新使用)。 */
2887
+ reload() {
2888
+ return this.load();
2889
+ }
2890
+ /** 校验并落盘(原子写:临时文件 + rename),随后更新内存。 */
2891
+ async save(next) {
2892
+ const parsed = this.parse(this.serialize(next));
2893
+ mkdirSync(dirname(this.path), { recursive: true });
2894
+ const tmpPath = `${this.path}.tmp`;
2895
+ await writeFile(tmpPath, `${this.serialize(parsed)}\n`, "utf8");
2896
+ await rename(tmpPath, this.path);
2897
+ this.value = parsed;
2898
+ }
2899
+ /** 对象 → 文本(TOML/JSON)。 */
2900
+ serialize(value) {
2901
+ if (this.format === "toml") return stringify(value);
2902
+ return JSON.stringify(value, null, 4);
2903
+ }
2904
+ /** 文本 → 对象(TOML/JSON)。 */
2905
+ deserialize(raw) {
2906
+ if (this.format === "toml") return parse(raw);
2907
+ return JSON.parse(raw);
2908
+ }
2909
+ /** 反序列化 + schema 校验,错误统一包装为 KernelError。 */
2910
+ parse(raw) {
2911
+ let input;
2912
+ try {
2913
+ input = this.deserialize(raw);
2914
+ } catch (err) {
2915
+ throw new KernelError(`配置文件 ${this.format.toUpperCase()} 解析失败: ${this.path}`, "INVALID_PARAM", { cause: err });
2916
+ }
2917
+ try {
2918
+ return this.schema.parse(input);
2919
+ } catch (err) {
2920
+ throw new KernelError(`配置文件校验失败: ${this.path}`, "INVALID_PARAM", { cause: err });
2921
+ }
2922
+ }
2923
+ };
2924
+ /** 解析 TOML 文本 → 对象(boot.cjs / 探测脚本等无 ConfigBase 场景复用)。 */
2925
+ function parseToml(text) {
2926
+ return parse(text);
2927
+ }
2928
+ /** 对象 → TOML 文本(config init 生成全局配置文件等场景)。 */
2929
+ function stringifyToml(value) {
2930
+ return stringify(value);
2931
+ }
2932
+ //#endregion
2933
+ //#region src/wrapper/session-resolver.ts
2934
+ /** 从 getSessionIdList 的 Map 提取主 sessionId(nt_ 前缀优先)。 */
2935
+ function findMainSessionId(ids) {
2936
+ for (const [k, v] of ids) if (typeof v === "string") {
2937
+ if (v.startsWith("nt_")) return v;
2938
+ } else if (typeof k === "string" && k.startsWith("nt_")) return k;
2939
+ for (const [, v] of ids) if (typeof v === "string") return v;
2940
+ for (const k of ids.keys()) if (typeof k === "string") return k;
2941
+ return null;
2942
+ }
2943
+ /** 通过 getNTWrapperSession 拿主 session(内部辅助)。 */
2944
+ function resolveMainSession(created, getNTWrapperSession) {
2945
+ if (typeof created.start === "function") created.start();
2946
+ if (typeof created.getSessionIdList !== "function") return null;
2947
+ const ids = created.getSessionIdList();
2948
+ if (!(ids instanceof Map)) return null;
2949
+ const mainId = findMainSessionId(ids);
2950
+ if (mainId === null) return null;
2951
+ const maybe = getNTWrapperSession(mainId);
2952
+ if (maybe !== null && maybe !== void 0 && typeof maybe.getMsgService === "function") return maybe;
2953
+ return null;
2954
+ }
2955
+ /**
2956
+ * 复用 QQ 已有 session(P1-4:QQ 已登录,直接拿单例避免重复 init)。
2957
+ * 优先 `NodeIQQNTWrapperSession.get()`;返回 null 时回退 createSession。
2958
+ */
2959
+ function getExistingSession(ctx) {
2960
+ try {
2961
+ const S = ctx.exports.NodeIQQNTWrapperSession;
2962
+ if (typeof S.get === "function") {
2963
+ const got = S.get();
2964
+ if (got && typeof got.getMsgService === "function") {
2965
+ ctx.session = got;
2966
+ return got;
2967
+ }
2968
+ }
2969
+ } catch {}
2970
+ return null;
2971
+ }
2972
+ /**
2973
+ * 复用 QQ 主 session(P1-4 实测链路):
2974
+ * `NodeIQQNTStartupSessionWrapper.create()` → `start()` → `getSessionIdList()`(Map
2975
+ * {nt:"nt_3", gpro:"gpro_3"})→ `NodeIQQNTWrapperSession.getNTWrapperSession("nt_3")`。
2976
+ * 注:实测这些 session 是空壳(核心 service null),真正可用的是 new + init(lifecycle.ts)。
2977
+ */
2978
+ function getMainSession(ctx) {
2979
+ try {
2980
+ const startupRaw = ctx.exports.NodeIQQNTStartupSessionWrapper;
2981
+ const sessionCtor = ctx.exports.NodeIQQNTWrapperSession;
2982
+ if (typeof startupRaw.create !== "function" || typeof sessionCtor.getNTWrapperSession !== "function") return null;
2983
+ const created = startupRaw.create();
2984
+ if (!created) return null;
2985
+ const getSession = (id) => sessionCtor.getNTWrapperSession(id);
2986
+ const session = resolveMainSession(created, getSession);
2987
+ if (session !== null) ctx.session = session;
2988
+ return session;
2989
+ } catch {}
2990
+ return null;
2991
+ }
2992
+ //#endregion
2993
+ //#region src/wrapper/probe.ts
2994
+ /**
2995
+ * 运行时反射探测(ADR-006 落地,P1-4)
2996
+ *
2997
+ * 注入链路打通后(2026-08-05),wrapper exports 是 QQ 已注册的单例。
2998
+ * 本模块在 **QQ 主进程内**反射枚举 session / service 的真实方法名与返回形状,
2999
+ * 产出 JSON 到 NAPUTO_CFG_DIR/napuketto-probe.json,作为 types/services/ 的权威来源。
3000
+ *
3001
+ * 用法:boot.cjs 在 NAPUTO_PROBE=1 时于 startNapuketto 后调用 probeRuntime(ctx)。
3002
+ * 产物是研究工具数据,类型层据此补全(自研描述,非移植)。
3003
+ */
3004
+ /** 反射枚举一个对象原型链上的方法名(去重、去构造器)。 */
3005
+ function listMethods(obj) {
3006
+ if (obj === null || obj === void 0) return [];
3007
+ const names = /* @__PURE__ */ new Set();
3008
+ let proto = Object.getPrototypeOf(obj);
3009
+ while (proto && proto !== Object.prototype && proto !== Function.prototype) {
3010
+ for (const name of Object.getOwnPropertyNames(proto)) if (name !== "constructor") names.add(name);
3011
+ proto = Object.getPrototypeOf(proto);
3012
+ }
3013
+ for (const name of Object.getOwnPropertyNames(obj)) if (name !== "constructor") names.add(name);
3014
+ return [...names].sort();
3015
+ }
3016
+ /** 安全调用:尝试调用方法拿返回值,失败记录错误(不中断探测)。 */
3017
+ function tryCall(obj, name) {
3018
+ try {
3019
+ const fn = obj[name];
3020
+ if (typeof fn !== "function") return {
3021
+ ok: false,
3022
+ error: `not a function (typeof=${typeof fn})`
3023
+ };
3024
+ return {
3025
+ ok: true,
3026
+ value: fn.call(obj)
3027
+ };
3028
+ } catch (e) {
3029
+ let message = String(e);
3030
+ if (e instanceof Error) message = e.message;
3031
+ return {
3032
+ ok: false,
3033
+ error: message
3034
+ };
3035
+ }
3036
+ }
3037
+ /** 序列化深度上限(循环引用/深层对象防护)。 */
3038
+ const MAX_SERIALIZE_DEPTH = 4;
3039
+ /** 数组序列化条数上限。 */
3040
+ const MAX_ARRAY_ITEMS = 20;
3041
+ /** 对象键序列化条数上限。 */
3042
+ const MAX_OBJECT_KEYS = 50;
3043
+ /** 序列化一个值(BigInt 转字符串,Map/Set/Promise 展开,循环引用防护)。 */
3044
+ function serialize(value, depth = 0) {
3045
+ if (value === null || value === void 0) return value;
3046
+ if (typeof value === "bigint") return value.toString();
3047
+ if (typeof value === "function") return `[function ${value.name ?? "anon"}]`;
3048
+ if (typeof value !== "object") return value;
3049
+ if (depth > MAX_SERIALIZE_DEPTH) return "[depth-limit]";
3050
+ try {
3051
+ if (Array.isArray(value)) return value.slice(0, MAX_ARRAY_ITEMS).map((v) => serialize(v, depth + 1));
3052
+ if (value instanceof Map) {
3053
+ const out = {};
3054
+ let i = 0;
3055
+ for (const [k, v] of value) {
3056
+ if (i >= MAX_OBJECT_KEYS) break;
3057
+ out[String(k)] = serialize(v, depth + 1);
3058
+ i += 1;
3059
+ }
3060
+ return {
3061
+ kind: "Map",
3062
+ size: value.size,
3063
+ entries: out
3064
+ };
3065
+ }
3066
+ if (value instanceof Set) return {
3067
+ kind: "Set",
3068
+ size: value.size,
3069
+ values: [...value].slice(0, MAX_ARRAY_ITEMS).map((v) => serialize(v, depth + 1))
3070
+ };
3071
+ if (typeof value.then === "function") return "[Promise]";
3072
+ const out = {};
3073
+ for (const key of Object.keys(value).slice(0, MAX_OBJECT_KEYS)) out[key] = serialize(value[key], depth + 1);
3074
+ return out;
3075
+ } catch {
3076
+ return "[unserializable]";
3077
+ }
3078
+ }
3079
+ /** 尝试解析一个 service getter 的返回值形状(深度受限)。 */
3080
+ function tryShape(session, getter) {
3081
+ try {
3082
+ const fn = session[getter];
3083
+ if (typeof fn !== "function") return null;
3084
+ const value = fn.call(session);
3085
+ if (value === null || value === void 0) return null;
3086
+ return serialize(value);
3087
+ } catch {
3088
+ return "[error]";
3089
+ }
3090
+ }
3091
+ /** 数据 getter 方法名模式(探测返回形状用)。 */
3092
+ const DATA_GETTER_RE = /^(get|query|fetch|load)/i;
3093
+ /** 探测 service 的关键数据 getter 返回形状(取前几个无参调用)。 */
3094
+ function shapeKeyGetters(session, serviceMethods) {
3095
+ const out = {};
3096
+ const candidates = serviceMethods.filter((m) => DATA_GETTER_RE.test(m) && !m.endsWith("Listener"));
3097
+ for (const m of candidates.slice(0, 8)) out[m] = tryShape(session, m);
3098
+ return out;
3099
+ }
3100
+ /** 探测关键 export 构造器方法(get/create/getNTWrapperSession 等)。 */
3101
+ function probeExportCtors(ctx) {
3102
+ const keys = [
3103
+ "NodeIQQNTWrapperSession",
3104
+ "NodeIQQNTStartupSessionWrapper",
3105
+ "NodeQQNTWrapperUtil",
3106
+ "NodeIKernelLoginService",
3107
+ "NodeIQQNTWrapperEngine"
3108
+ ];
3109
+ const out = {};
3110
+ for (const key of keys) {
3111
+ const ctor = ctx.exports[key];
3112
+ if (ctor !== null && ctor !== void 0) out[key] = {
3113
+ methods: listMethods(ctor),
3114
+ ownKeys: Object.getOwnPropertyNames(ctor)
3115
+ };
3116
+ }
3117
+ return out;
3118
+ }
3119
+ /** 探测 engine 关键调用(getDeviceInfo / getECDHService / getThirdPartySigService)。 */
3120
+ function probeEngineCalls(ctx) {
3121
+ const out = {};
3122
+ for (const m of [
3123
+ "getDeviceInfo",
3124
+ "getECDHService",
3125
+ "getThirdPartySigService",
3126
+ "readyToShow"
3127
+ ]) {
3128
+ const call = tryCall(ctx.engine, m);
3129
+ out[m] = call.ok ? {
3130
+ value: serialize(call.value),
3131
+ methods: call.value ? listMethods(call.value) : []
3132
+ } : { error: call.error ?? "null/undefined" };
3133
+ }
3134
+ return out;
3135
+ }
3136
+ /** 探测 LoginService 实例(QQ 已登录凭据入口)。 */
3137
+ function probeLoginService(ctx) {
3138
+ const ctor = ctx.exports.NodeIKernelLoginService;
3139
+ if (ctor === null || typeof ctor.get !== "function") return null;
3140
+ const inst = tryCall(ctor, "get");
3141
+ if (!(inst.ok && inst.value)) return { get: inst.error ?? "null/undefined" };
3142
+ const out = {
3143
+ methods: listMethods(inst.value),
3144
+ ownKeys: Object.getOwnPropertyNames(inst.value).slice(0, 30)
3145
+ };
3146
+ for (const m of [
3147
+ "getAccountInfo",
3148
+ "getLoginInfo",
3149
+ "getA2",
3150
+ "getTicket",
3151
+ "getUin",
3152
+ "getUid",
3153
+ "getSelfUin"
3154
+ ]) if (typeof inst.value[m] === "function") out[m] = tryShape(inst.value, m);
3155
+ return out;
3156
+ }
3157
+ /** 枚举候选 sessionId(nt_0..nt_9 / gpro_0..gpro_9)找 QQ 已 init 的 session。 */
3158
+ function enumerateSessionIds(ctx) {
3159
+ const S = ctx.exports.NodeIQQNTWrapperSession;
3160
+ const enumFn = S["getNTWrapperSession"];
3161
+ const out = {};
3162
+ if (typeof enumFn !== "function") return out;
3163
+ const ids = [];
3164
+ for (let i = 0; i < 10; i += 1) ids.push(`nt_${i}`, `gpro_${i}`);
3165
+ for (const id of ids) try {
3166
+ const value = enumFn.call(S, id);
3167
+ if (!value) continue;
3168
+ const rec = value;
3169
+ const hasGetter = typeof rec["getMsgService"] === "function";
3170
+ let msgService = null;
3171
+ let msgError = null;
3172
+ try {
3173
+ msgService = rec["getMsgService"].call(value);
3174
+ } catch (e) {
3175
+ msgError = e instanceof Error ? e.message : String(e);
3176
+ }
3177
+ out[id] = {
3178
+ hasMsgServiceGetter: hasGetter,
3179
+ msgServiceOk: msgService !== null && msgService !== void 0,
3180
+ msgServiceMethods: msgService ? listMethods(msgService).length : 0,
3181
+ msgError,
3182
+ methods: listMethods(value)
3183
+ };
3184
+ } catch {}
3185
+ return out;
3186
+ }
3187
+ /** 探测 startup session 链路:create() → start() → getSessionIdList → getNTWrapperSession。 */
3188
+ function probeStartup(ctx) {
3189
+ const startup = ctx.exports.NodeIQQNTStartupSessionWrapper;
3190
+ if (startup === null || typeof startup.create !== "function") return null;
3191
+ const out = { staticMethods: listMethods(startup) };
3192
+ const created = tryCall(startup, "create");
3193
+ out["create"] = created.ok ? {
3194
+ methods: created.value ? listMethods(created.value) : [],
3195
+ value: serialize(created.value)
3196
+ } : { error: created.error };
3197
+ if (created.ok && created.value) {
3198
+ const started = tryCall(created.value, "start");
3199
+ out["start"] = started.ok ? { ok: true } : { error: started.error };
3200
+ const ids = tryCall(created.value, "getSessionIdList");
3201
+ const idsValue = ids.ok ? serialize(ids.value) : null;
3202
+ out["createdGetSessionIdList"] = ids.ok ? { value: idsValue } : { error: ids.error };
3203
+ const foundIds = [];
3204
+ if (ids.ok && ids.value instanceof Map) for (const [k, v] of ids.value) {
3205
+ const id = typeof v === "string" ? v : String(k);
3206
+ foundIds.push(id);
3207
+ }
3208
+ out["sessionIds"] = foundIds;
3209
+ if (foundIds.length > 0) {
3210
+ const mainId = foundIds.find((id) => id.startsWith("nt_")) ?? foundIds[0];
3211
+ out["mainSessionId"] = mainId;
3212
+ const S = ctx.exports.NodeIQQNTWrapperSession;
3213
+ const fn = S["getNTWrapperSession"];
3214
+ if (typeof fn === "function" && mainId !== void 0) try {
3215
+ const value = fn.call(S, mainId);
3216
+ out["mainSession"] = value ? {
3217
+ methods: listMethods(value),
3218
+ ownKeys: Object.getOwnPropertyNames(value)
3219
+ } : null;
3220
+ } catch (e) {
3221
+ out["mainSession"] = { error: e instanceof Error ? e.message : String(e) };
3222
+ }
3223
+ out["enumSessionIds"] = enumerateSessionIds(ctx);
3224
+ return out;
3225
+ }
3226
+ }
3227
+ out["enumSessionIds"] = enumerateSessionIds(ctx);
3228
+ const S = ctx.exports.NodeIQQNTWrapperSession;
3229
+ const candidates = [
3230
+ "main",
3231
+ "primary",
3232
+ "session1",
3233
+ "default",
3234
+ ""
3235
+ ];
3236
+ const getResults = {};
3237
+ for (const name of candidates) try {
3238
+ const fn = S["getNTWrapperSession"];
3239
+ const value = typeof fn === "function" ? fn.call(S, name) : null;
3240
+ getResults[name === "" ? "<empty>" : name] = value ? {
3241
+ methods: listMethods(value),
3242
+ ownKeys: Object.getOwnPropertyNames(value)
3243
+ } : null;
3244
+ } catch (e) {
3245
+ getResults[name === "" ? "<empty>" : name] = { error: e instanceof Error ? e.message : String(e) };
3246
+ }
3247
+ out["getNTWrapperSession"] = getResults;
3248
+ return out;
3249
+ }
3250
+ /** 探测 session 与 service 方法(优先复用 QQ 主 session)。 */
3251
+ function probeSession(ctx) {
3252
+ const out = {};
3253
+ let session = null;
3254
+ try {
3255
+ const main = getMainSession(ctx);
3256
+ if (main !== null) {
3257
+ session = main;
3258
+ out["sessionSource"] = "mainSession(getNTWrapperSession)";
3259
+ }
3260
+ } catch {}
3261
+ if (session === null && ctx.session !== null) {
3262
+ session = ctx.session;
3263
+ out["sessionSource"] = "own-create";
3264
+ }
3265
+ out["session"] = session ? {
3266
+ methods: listMethods(session),
3267
+ ownKeys: Object.getOwnPropertyNames(session)
3268
+ } : null;
3269
+ if (session !== null && session !== void 0) {
3270
+ const getters = listMethods(session).filter((m) => m.startsWith("get") && m.endsWith("Service"));
3271
+ const services = {};
3272
+ for (const getter of getters) {
3273
+ const call = tryCall(session, getter);
3274
+ if (call.ok && call.value) {
3275
+ const methods = listMethods(call.value);
3276
+ services[getter] = {
3277
+ methods,
3278
+ ownKeys: Object.getOwnPropertyNames(call.value).slice(0, 20),
3279
+ shapes: shapeKeyGetters(call.value, methods)
3280
+ };
3281
+ } else services[getter] = {
3282
+ error: call.error ?? "null/undefined",
3283
+ ok: call.ok
3284
+ };
3285
+ }
3286
+ out["services"] = services;
3287
+ }
3288
+ return out;
3289
+ }
3290
+ /**
3291
+ * 探测 wrapper 运行时:dump session 方法与各 service 方法到 JSON。
3292
+ * 返回探测结果对象;同时写文件(NAPUTO_CFG_DIR/<file>,默认 napuketto-probe.json)。
3293
+ */
3294
+ function probeRuntime(ctx, file = "napuketto-probe.json") {
3295
+ const result = {
3296
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3297
+ exports: listMethods(ctx.exports),
3298
+ engineMethods: listMethods(ctx.engine),
3299
+ exportCtors: probeExportCtors(ctx),
3300
+ engineCalls: probeEngineCalls(ctx),
3301
+ loginService: probeLoginService(ctx),
3302
+ startup: probeStartup(ctx),
3303
+ ...probeSession(ctx)
3304
+ };
3305
+ try {
3306
+ const dir = process.env["NAPUTO_CFG_DIR"] ?? process.cwd();
3307
+ mkdirSync(dir, { recursive: true });
3308
+ const full = join(dir, file);
3309
+ writeFileSync(full, JSON.stringify(result, null, 2), "utf8");
3310
+ result["probeFile"] = full;
3311
+ } catch (e) {
3312
+ result["probeFileError"] = e instanceof Error ? e.message : String(e);
3313
+ }
3314
+ return result;
3315
+ }
3316
+ //#endregion
3317
+ //#region src/wrapper/wrapper-version.ts
3318
+ /**
3319
+ * QQ 安装目录探测(ADR-018)
3320
+ *
3321
+ * wrapper.node 路径随 QQ 版本变化:`<安装目录>/versions/<版本>/resources/app/wrapper.node`。
3322
+ * 加载前必须知道版本(登录握手参数 appid/qua 与版本强相关)。
3323
+ *
3324
+ * 已实测确认(2026-08-05,QQ 9.9.31-49919):
3325
+ * - 目录结构:versions/<9.9.31-49919>/resources/app/wrapper.node
3326
+ * - 版本信息在 versions/<版本>/resources/app/package.json(name=qq-chat / version / buildVersion)
3327
+ * - wrapper.node 是 C++ ABI 模块(非 N-API),导出 INTSessionShell 等符号
3328
+ */
3329
+ /** 从 versions 目录读取版本 package.json(拿 name/version/buildVersion)。 */
3330
+ function readVersionPackage(installDir, version) {
3331
+ const pkgPath = join(installDir, "versions", version, "resources", "app", "package.json");
3332
+ if (!existsSync(pkgPath)) throw kernelError(`QQ 版本目录缺少 package.json: ${pkgPath}`, "NOT_FOUND");
3333
+ try {
3334
+ const raw = JSON.parse(readFileSync(pkgPath, "utf8"));
3335
+ const result = {
3336
+ name: raw.name ?? "",
3337
+ version: raw.version ?? ""
3338
+ };
3339
+ if (raw.buildVersion !== void 0) result.buildVersion = raw.buildVersion;
3340
+ return result;
3341
+ } catch (cause) {
3342
+ throw kernelError(`QQ 版本 package.json 解析失败: ${pkgPath}`, "INVALID_PARAM", { cause });
3343
+ }
3344
+ }
3345
+ /** 解析 wrapper.node 绝对路径(按 ADR-018 目录约定)。 */
3346
+ function resolveWrapperPath(installDir, version) {
3347
+ return join(installDir, "versions", version, "resources", "app", "wrapper.node");
3348
+ }
3349
+ /**
3350
+ * 探测指定版本信息(校验 wrapper.node 存在)。
3351
+ * 版本号不匹配目录时抛 NOT_FOUND(明确失败而非静默)。
3352
+ */
3353
+ function resolveQQVersion(installDir, version) {
3354
+ const wrapperPath = resolveWrapperPath(installDir, version);
3355
+ if (!existsSync(wrapperPath)) throw kernelError(`未找到 wrapper.node: ${wrapperPath}`, "NOT_FOUND");
3356
+ const pkg = readVersionPackage(installDir, version);
3357
+ return {
3358
+ fullVersion: pkg.version || version,
3359
+ buildVersion: pkg.buildVersion ?? "",
3360
+ wrapperPath,
3361
+ appid: "",
3362
+ qua: ""
3363
+ };
3364
+ }
3365
+ /** 扫描 versions 目录,取可加载的版本列表(按目录名倒序,最新在前)。 */
3366
+ function listQQVersions(installDir) {
3367
+ const versionsDir = join(installDir, "versions");
3368
+ if (!existsSync(versionsDir)) return [];
3369
+ return readdirSync(versionsDir).filter((name) => {
3370
+ const full = join(versionsDir, name);
3371
+ return statSync(full).isDirectory() && existsSync(join(full, "resources", "app", "wrapper.node"));
3372
+ }).sort().reverse();
3373
+ }
3374
+ //#endregion
3375
+ export { ChatType, ConfigBase, DEFAULT_DATA_ROOT_NAME, ElementType, FriendApi, 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, RichMediaApi, TicketApi, TipGroupElementType, WebApi, WebHonorType, buildEngineConfig, buildLoginConfig, buildSessionConfig, createCoreContext, createSessionListener as createLifecycleSessionListener, createSessionListener, createLogger, createLoginListener, createSession, createWrapper, electronProcessType, getExistingSession, getMainSession, initAndStartSession, initEngine, initSession, isKernelError, kernelError, listLoginAccounts, listQQVersions, parseAppidFromMajor, parseToml, probeRuntime, quickLogin, resolveAppidQua, resolveConfigPath, resolveDataRoot, resolveQQVersion, resolveQqGlobalPath, resolveQqUserDataRoot, resolveWrapperPath, startNapuketto, startSession, stringifyToml, toCanonicalElements, toSendElements, waitForNetworkConnection, waitSessionReady };
3376
+
3377
+ //# sourceMappingURL=index.mjs.map