@napuketto/kernel 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,12 +1,13 @@
1
1
  import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
2
- import { readFile, rename, writeFile } from "node:fs/promises";
3
- import { dirname, extname, join, resolve } from "node:path";
2
+ import { readFile, rename, stat, writeFile } from "node:fs/promises";
3
+ import { basename, dirname, extname, join, resolve } from "node:path";
4
4
  import { parse, stringify } from "smol-toml";
5
5
  import process from "node:process";
6
6
  import pino from "pino";
7
7
  import pinoPretty from "pino-pretty";
8
8
  import { homedir, hostname } from "node:os";
9
9
  import { fileURLToPath } from "node:url";
10
+ import { createHash } from "node:crypto";
10
11
  import { get } from "node:https";
11
12
  import { EventEmitter } from "node:events";
12
13
  //#region src/infra/errors.ts
@@ -518,24 +519,32 @@ var FriendApi = class {
518
519
  }
519
520
  /** 单项转换:DoubtBuddyReq → OB11 结构(可选字段条件赋值,exactOptionalPropertyTypes)。 */
520
521
  toDoubtFriendRequestInfo(item, uinMap) {
521
- const info = {
522
- flag: item.uid,
523
- uin: Number(uinMap.get(item.uid) ?? item.uid),
524
- type: "doubt"
525
- };
526
- if (item.nick !== void 0) info.nick = item.nick;
527
- if (item.source !== void 0) info.source = item.source;
528
- if (item.reason !== void 0) info.reason = item.reason;
529
- if (item.msg !== void 0) info.msg = item.msg;
530
- if (item.groupCode !== void 0) info.group_code = item.groupCode;
531
- if (item.reqTime !== void 0) info.time = item.reqTime;
532
- return info;
522
+ return toDoubtFriendRequestInfo(item, uinMap);
533
523
  }
534
524
  /** 处理可疑好友申请(set_doubt_friends_add_request;uid=flag)。 */
535
525
  handleDoubtFriendRequest(uid) {
536
526
  this.service.approvalDoubtBuddyReq(uid, "", "");
537
527
  }
538
528
  };
529
+ /** 可选字段条件赋值(undefined 跳过,兼容 exactOptionalPropertyTypes)。 */
530
+ function assignOptional(info, key, value) {
531
+ if (value !== void 0) info[key] = value;
532
+ }
533
+ /** 单项转换:DoubtBuddyReq → OB11 结构(导出供单测,friend.test.ts)。 */
534
+ function toDoubtFriendRequestInfo(item, uinMap) {
535
+ const info = {
536
+ flag: item.uid,
537
+ uin: Number(uinMap.get(item.uid) ?? item.uid),
538
+ type: "doubt"
539
+ };
540
+ assignOptional(info, "nick", item.nick);
541
+ assignOptional(info, "source", item.source);
542
+ assignOptional(info, "reason", item.reason);
543
+ assignOptional(info, "msg", item.msg);
544
+ assignOptional(info, "group_code", item.groupCode);
545
+ assignOptional(info, "time", item.reqTime);
546
+ return info;
547
+ }
539
548
  //#endregion
540
549
  //#region src/types/entities.ts
541
550
  /**
@@ -943,11 +952,9 @@ var GroupApi = class {
943
952
  const raw = await this.service.getGroupDetailInfo(groupCode, 0);
944
953
  const { result, errMsg } = raw;
945
954
  if (typeof result === "number" && result !== 0) unwrap("getGroupDetailInfo", result, errMsg);
946
- if (result !== null && typeof result === "object") return result;
947
- const detail = raw;
948
- const candidate = detail.groupInfo ?? detail.detailInfo ?? detail.info;
949
- if (candidate !== void 0 && candidate !== null && typeof candidate === "object") return candidate;
950
- throw kernelError(`getGroupDetailInfo 返回形状异常(groupCode=${groupCode}): ${errMsg}`, "UNKNOWN");
955
+ const detail = extractGroupDetail(result, raw);
956
+ if (detail === null) throw kernelError(`getGroupDetailInfo 返回形状异常(groupCode=${groupCode}): ${errMsg}`, "UNKNOWN");
957
+ return detail;
951
958
  }
952
959
  /** 群成员列表(forceFetch=true 强制拉取,默认缓存优先)。 */
953
960
  async getGroupMemberList(groupCode, forceFetch = false) {
@@ -1084,6 +1091,19 @@ var GroupApi = class {
1084
1091
  };
1085
1092
  }
1086
1093
  };
1094
+ /**
1095
+ * 从 getGroupDetailInfo 返回提取群详情(兼容三种形状):
1096
+ * - result 为对象 → 详情直接在其中(最常见)
1097
+ * - result 为 0/空 → 从 raw.groupInfo / detailInfo / info 提取
1098
+ * 提取不到返回 null(调用方抛 UNKNOWN)。导出供单测。
1099
+ */
1100
+ function extractGroupDetail(result, raw) {
1101
+ if (result !== null && typeof result === "object") return result;
1102
+ const detail = raw;
1103
+ const candidate = detail.groupInfo ?? detail.detailInfo ?? detail.info;
1104
+ if (candidate !== void 0 && candidate !== null && typeof candidate === "object") return candidate;
1105
+ return null;
1106
+ }
1087
1107
  //#endregion
1088
1108
  //#region src/apis/group-notify.ts
1089
1109
  /**
@@ -1159,15 +1179,47 @@ function extractMemberList(result) {
1159
1179
  *
1160
1180
  * 方法面(P2-1):发送 / 撤回 / 拉历史 / 标记已读。group/friend 等后续 apis 同构。
1161
1181
  */
1182
+ /** 发送状态(onMsgInfoListUpdate 事件 msg.sendStatus)。 */
1183
+ const SEND_STATUS = {
1184
+ FAILED: 0,
1185
+ SENDING: 1,
1186
+ SUCCESS: 2,
1187
+ SUCCESS_NO_SEQ: 3
1188
+ };
1189
+ /** 发送确认超时(毫秒)。 */
1190
+ const SEND_CONFIRM_TIMEOUT_MS = 15e3;
1191
+ /** 读文件 stat(富媒体发送预处理;文件不存在抛 INVALID_PARAM)。 */
1192
+ async function statFile(path) {
1193
+ try {
1194
+ return await stat(path);
1195
+ } catch {
1196
+ throw kernelError(`图片文件不存在: ${path}`, "INVALID_PARAM");
1197
+ }
1198
+ }
1199
+ /** 计算文件 md5(十六进制小写)。 */
1200
+ async function hashFile(path) {
1201
+ try {
1202
+ const buf = await readFile(path);
1203
+ return createHash("md5").update(buf).digest("hex");
1204
+ } catch {
1205
+ throw kernelError(`图片文件读取失败: ${path}`, "INVALID_PARAM");
1206
+ }
1207
+ }
1162
1208
  /** 消息 API:从 session 拿 msg service,包装成语义化方法。 */
1163
1209
  var MsgApi = class {
1164
1210
  service;
1211
+ /** 消息事件通道(sendMsg 后等 onMsgInfoListUpdate 确认发送结果)。 */
1212
+ channel;
1213
+ /** NodeQQNTWrapperUtil(富媒体发送 copyFile 用;session 拿不到时可为 null)。 */
1214
+ util;
1165
1215
  /** 上次生成 msgId 的时间(单调递增,2026-08-07 防同毫秒并发碰撞)。 */
1166
1216
  lastMsgTime = 0;
1167
- constructor(session) {
1217
+ constructor(session, channel, util) {
1168
1218
  const service = session.getMsgService();
1169
1219
  if (service === null || service === void 0) throw kernelError("getMsgService() 返回空(session 未 init)", "INVALID_STATE");
1170
1220
  this.service = service;
1221
+ this.channel = channel ?? null;
1222
+ this.util = util !== null && util !== void 0 ? util : null;
1171
1223
  }
1172
1224
  /**
1173
1225
  * 生成 msgId 时间戳(单调递增)。
@@ -1182,15 +1234,114 @@ var MsgApi = class {
1182
1234
  return String(this.lastMsgTime);
1183
1235
  }
1184
1236
  /**
1185
- * 发送消息:canonical 元素 → NT 发送元素 → sendMsg。
1237
+ * 发送消息:canonical 元素 → NT 发送元素 → sendMsg(NapCat 式)。
1186
1238
  * 返回 NT msgId(雪花 ID)。
1239
+ *
1240
+ * 2026-08-11 修复(NapCat 式,实测):
1241
+ * - sendMsg 第一参传 '0'(固定),msgId 塞 peer.guildId —— 传 msgId 作第一参
1242
+ * 时 wrapper 返回 result=5(失败),NapCat 同款调用返回 result=0。
1243
+ * - 发送结果以 onMsgInfoListUpdate 事件确认(sendStatus===2 成功):sendMsg
1244
+ * 返回值 result 可能为 5 但事件仍成功(异步确认),反之亦然。
1245
+ * - ⚠️ 必须先注册事件监听再调 sendMsg(事件可能在 sendMsg 返回前就触发)。
1187
1246
  */
1188
1247
  async sendMessage(target, elements) {
1189
- const sendElements = toSendElements(elements);
1248
+ const sendElements = await this.prepareSendElements(elements);
1190
1249
  const msgId = this.service.generateMsgUniqueId(target.chatType, this.nextMsgTime());
1191
- unwrapResult("sendMsg", await this.service.sendMsg(msgId, target, sendElements, /* @__PURE__ */ new Map()));
1250
+ const sendPeer = {
1251
+ ...target,
1252
+ guildId: msgId
1253
+ };
1254
+ if (this.channel === null) {
1255
+ unwrapResult("sendMsg", await this.service.sendMsg("0", sendPeer, sendElements, /* @__PURE__ */ new Map()));
1256
+ return { msgId };
1257
+ }
1258
+ const confirm = this.confirmSend(msgId, target);
1259
+ await this.service.sendMsg("0", sendPeer, sendElements, /* @__PURE__ */ new Map());
1260
+ await confirm;
1192
1261
  return { msgId };
1193
1262
  }
1263
+ /**
1264
+ * canonical 元素 → 发送元素,PIC 做 NapCat 式预处理。
1265
+ * 2026-08-11 修复(实测):图片发送必须 elementType=2(PIC)+ 完整 picElement
1266
+ * (md5HexStr/fileSize/fileName/sourcePath),且文件须经 getRichMediaFilePathForGuild
1267
+ * 计算目标路径 + util.copyFile 放置——缺任一环节 sendMsg 返回 rich media transfer failed。
1268
+ */
1269
+ async prepareSendElements(elements) {
1270
+ const out = [];
1271
+ for (const el of elements) {
1272
+ if (el.type !== "image") {
1273
+ out.push(...toSendElements([el]));
1274
+ continue;
1275
+ }
1276
+ out.push(await this.prepareImageElement(el.path));
1277
+ }
1278
+ return out;
1279
+ }
1280
+ /** PIC 元素 NapCat 式预处理:md5 → 目标路径 → copyFile → 完整 picElement。 */
1281
+ async prepareImageElement(path) {
1282
+ const service = this.service;
1283
+ const util = this.util;
1284
+ const stat = await statFile(path);
1285
+ const md5 = await hashFile(path);
1286
+ const fileSize = stat.size;
1287
+ const fileName = basename(path);
1288
+ const relPath = service.getRichMediaFilePathForGuild({
1289
+ md5HexStr: md5,
1290
+ fileName,
1291
+ elementType: ElementType.PIC,
1292
+ elementSubType: 0,
1293
+ thumbSize: 0,
1294
+ needCreate: true,
1295
+ downloadType: 1,
1296
+ file_uuid: ""
1297
+ });
1298
+ if (util !== null) {
1299
+ const instance = typeof util.get === "function" ? util.get() : util;
1300
+ const copy = instance["copyFile"];
1301
+ if (typeof copy === "function") await copy.call(instance, path, relPath);
1302
+ }
1303
+ return {
1304
+ elementType: ElementType.PIC,
1305
+ elementId: "",
1306
+ picElement: {
1307
+ md5HexStr: md5,
1308
+ fileSize: String(fileSize),
1309
+ picWidth: 0,
1310
+ picHeight: 0,
1311
+ fileName,
1312
+ sourcePath: relPath,
1313
+ original: true,
1314
+ picType: 1e3,
1315
+ picSubType: 0,
1316
+ fileUuid: "",
1317
+ fileSubId: "",
1318
+ thumbFileSize: 0,
1319
+ summary: ""
1320
+ }
1321
+ };
1322
+ }
1323
+ /** 注册 onMsgInfoListUpdate 确认监听(返回 Promise,resolve 时发送成功)。 */
1324
+ confirmSend(msgId, target) {
1325
+ const channel = this.channel;
1326
+ if (channel === null) return Promise.resolve();
1327
+ return new Promise((resolve, reject) => {
1328
+ const off = channel.on("Msg/onMsgInfoListUpdate", (list) => {
1329
+ const mine = list.find((m) => m.guildId === msgId);
1330
+ if (mine === void 0) return;
1331
+ if (mine.sendStatus === SEND_STATUS.SUCCESS || mine.sendStatus === SEND_STATUS.SUCCESS_NO_SEQ) {
1332
+ off();
1333
+ resolve();
1334
+ } else if (mine.sendStatus === SEND_STATUS.FAILED) {
1335
+ off();
1336
+ reject(kernelError(`sendMsg 失败: sendStatus=${mine.sendStatus}`, "SEND_FAILED"));
1337
+ }
1338
+ });
1339
+ setTimeout(() => {
1340
+ off();
1341
+ reject(kernelError(`sendMsg 等待确认超时(msgId=${msgId}, target=${JSON.stringify(target)})`, "SEND_FAILED"));
1342
+ }, SEND_CONFIRM_TIMEOUT_MS);
1343
+ });
1344
+ }
1194
1345
  /** 撤回消息(群聊管理员 / 私聊 2 分钟内)。 */
1195
1346
  async recallMessage(target, msgIds) {
1196
1347
  if (msgIds.length === 0) throw kernelError("recallMessage 需要至少一个 msgId", "INVALID_PARAM");
@@ -1219,14 +1370,10 @@ var MsgApi = class {
1219
1370
  * → 再拉一次消息读 pttElement.text。
1220
1371
  */
1221
1372
  async fetchPttText(msgId, target) {
1222
- const [first] = await this.fetchMsgsByMsgId(target, [msgId]);
1223
- if (first === void 0) throw kernelError("消息不存在", "NOT_FOUND");
1224
- const ptt = first.elements.find((el) => el.pttElement !== void 0);
1225
- if (ptt?.pttElement === void 0) throw kernelError("消息中不包含语音", "NOT_FOUND");
1373
+ const ptt = findPttElement(await this.fetchMsgsByMsgId(target, [msgId]));
1374
+ if (ptt === null) throw kernelError("消息中不包含语音", "NOT_FOUND");
1226
1375
  unwrapResult("translatePtt2Text", await this.service.translatePtt2Text(msgId, target, ptt));
1227
- const [afterFirst] = await this.fetchMsgsByMsgId(target, [msgId]);
1228
- if (afterFirst === void 0) throw kernelError("获取语音转文字结果失败", "UNKNOWN");
1229
- const text = afterFirst.elements.find((el) => el.pttElement !== void 0)?.pttElement?.text;
1376
+ const text = findPttElement(await this.fetchMsgsByMsgId(target, [msgId]))?.pttElement?.text;
1230
1377
  if (text === void 0 || text === "") throw kernelError("获取语音转文字结果失败", "UNKNOWN");
1231
1378
  return text;
1232
1379
  }
@@ -1257,10 +1404,8 @@ var MsgApi = class {
1257
1404
  }
1258
1405
  /** 获取合并转发内容(get_forward_msg;resId 取自 multiForwardMsgElement)。 */
1259
1406
  async fetchForwardMessage(peer, msgId) {
1260
- const [first] = await this.fetchMsgsByMsgId(peer, [msgId]);
1261
- if (first === void 0) throw kernelError("消息不存在", "NOT_FOUND");
1262
- const forward = first.elements.find((el) => el.multiForwardMsgElement !== void 0)?.multiForwardMsgElement;
1263
- if (forward === void 0 || forward.resId === "") throw kernelError("消息不包含合并转发内容", "NOT_FOUND");
1407
+ const forward = findForwardElement(await this.fetchMsgsByMsgId(peer, [msgId]));
1408
+ if (forward === null || forward.resId === "") throw kernelError("消息不包含合并转发内容", "NOT_FOUND");
1264
1409
  const raw = await this.service.getMultiMsg(peer, msgId, forward.resId);
1265
1410
  unwrapResult("getMultiMsg", raw);
1266
1411
  return raw.msgList ?? [];
@@ -1275,6 +1420,18 @@ var MsgApi = class {
1275
1420
  unwrapResult("setStatus", await this.service.setStatus(opts));
1276
1421
  }
1277
1422
  };
1423
+ /** 在消息列表中找含 pttElement 的元素(找不到返回 null)。 */
1424
+ function findPttElement(msgs) {
1425
+ const [first] = msgs;
1426
+ if (first === void 0) return null;
1427
+ return first.elements.find((el) => el.pttElement !== void 0) ?? null;
1428
+ }
1429
+ /** 在消息列表中找含 multiForwardMsgElement 的元素(找不到返回 null)。 */
1430
+ function findForwardElement(msgs) {
1431
+ const [first] = msgs;
1432
+ if (first === void 0) return null;
1433
+ return first.elements.find((el) => el.multiForwardMsgElement !== void 0)?.multiForwardMsgElement ?? null;
1434
+ }
1278
1435
  //#endregion
1279
1436
  //#region src/apis/stranger-info.ts
1280
1437
  /** 宽松取值:undefined 时给默认值(null 也归默认)。 */
@@ -1286,13 +1443,19 @@ function mergeDetail(info, byUin) {
1286
1443
  const infoSimple = info.detail?.simpleInfo;
1287
1444
  const byUinSimple = byUin.detail?.simpleInfo;
1288
1445
  return {
1289
- coreInfo: infoSimple?.coreInfo ?? byUinSimple?.coreInfo,
1290
- baseInfo: infoSimple?.baseInfo ?? byUinSimple?.baseInfo,
1291
- vasInfo: infoSimple?.vasInfo ?? byUinSimple?.vasInfo,
1292
- commonExt: info.detail?.commonExt ?? byUin.detail?.commonExt,
1293
- status: infoSimple?.status?.status ?? byUinSimple?.status?.status
1446
+ coreInfo: pickField(infoSimple, byUinSimple, (s) => s?.coreInfo),
1447
+ baseInfo: pickField(infoSimple, byUinSimple, (s) => s?.baseInfo),
1448
+ vasInfo: pickField(infoSimple, byUinSimple, (s) => s?.vasInfo),
1449
+ commonExt: pickField(info.detail, byUin.detail, (d) => d?.commonExt),
1450
+ status: pickField(infoSimple, byUinSimple, (s) => s?.status?.status)
1294
1451
  };
1295
1452
  }
1453
+ /** 字段级回退:primary 的字段取不到回退 fallback 的同名字段(宽松取值)。 */
1454
+ function pickField(primary, fallback, get) {
1455
+ const fromPrimary = primary !== void 0 ? get(primary) : void 0;
1456
+ if (fromPrimary !== void 0) return fromPrimary;
1457
+ return fallback !== void 0 ? get(fallback) : void 0;
1458
+ }
1296
1459
  /** 扁平化两份详情 → StrangerInfo(宽松取值,字段缺失给默认)。 */
1297
1460
  function flattenStrangerInfo(uin, uid, byUin, info) {
1298
1461
  const d = mergeDetail(info, byUin);
@@ -1491,14 +1654,18 @@ var RichMediaApi = class {
1491
1654
  if (raw.result !== 0) unwrap("transGroupFile", raw.result, raw.errMsg);
1492
1655
  }
1493
1656
  };
1494
- /** 从 getGroupFileList 返回提取文件列表(兼容数组 / { items } / { fileList })。 */
1657
+ /** 从 getGroupFileList 返回提取文件列表(兼容数组 / { items } / { fileList })。导出供单测。 */
1495
1658
  function extractFileList(result) {
1496
1659
  if (Array.isArray(result)) return result;
1497
- if (result !== null && typeof result === "object") {
1498
- const obj = result;
1499
- if (Array.isArray(obj.items)) return obj.items;
1500
- if (Array.isArray(obj.fileList)) return obj.fileList;
1501
- if (Array.isArray(obj.list)) return obj.list;
1660
+ if (result === null || typeof result !== "object") return [];
1661
+ const obj = result;
1662
+ for (const key of [
1663
+ "items",
1664
+ "fileList",
1665
+ "list"
1666
+ ]) {
1667
+ const value = obj[key];
1668
+ if (Array.isArray(value)) return value;
1502
1669
  }
1503
1670
  return [];
1504
1671
  }
@@ -1610,24 +1777,31 @@ var WebApi = class {
1610
1777
  constructor(opts) {
1611
1778
  this.getCookies = opts.getCookies;
1612
1779
  }
1780
+ /** 拉取单页精华消息(失败/retcode 非 0 返回 null)。 */
1781
+ async fetchEssencePage(groupCode, cookie, bkn, page) {
1782
+ const ret = await fetchQunJson("https://qun.qq.com/cgi-bin/group_digest/digest_list?" + new URLSearchParams({
1783
+ bkn,
1784
+ page_start: String(page * ESSENCE_PAGE_LIMIT),
1785
+ page_limit: String(ESSENCE_PAGE_LIMIT),
1786
+ group_code: groupCode
1787
+ }).toString(), cookie);
1788
+ if (ret === null || ret.retcode !== 0) return null;
1789
+ const list = ret.data?.msg_list;
1790
+ return {
1791
+ items: Array.isArray(list) ? list.filter((item) => item !== null) : [],
1792
+ isEnd: ret.data?.is_end === true
1793
+ };
1794
+ }
1613
1795
  /** 获取群精华消息列表(get_essence_msg_list;分页拉取至 is_end)。 */
1614
1796
  async getEssenceMsgList(groupCode) {
1615
1797
  const cookie = await this.getCookies(QUN_DOMAIN);
1616
1798
  const bkn = TicketApi.getBkn(cookie["skey"] ?? "");
1617
1799
  const out = [];
1618
1800
  for (let page = 0; page < ESSENCE_MAX_PAGES; page += 1) {
1619
- const ret = await fetchQunJson("https://qun.qq.com/cgi-bin/group_digest/digest_list?" + new URLSearchParams({
1620
- bkn,
1621
- page_start: String(page * ESSENCE_PAGE_LIMIT),
1622
- page_limit: String(ESSENCE_PAGE_LIMIT),
1623
- group_code: groupCode
1624
- }).toString(), cookie);
1625
- if (ret === null || ret.retcode !== 0) break;
1626
- const list = ret.data?.msg_list;
1627
- if (Array.isArray(list)) {
1628
- for (const item of list) if (item !== null) out.push(item);
1629
- }
1630
- if (ret.data?.is_end === true) break;
1801
+ const result = await this.fetchEssencePage(groupCode, cookie, bkn, page);
1802
+ if (result === null) break;
1803
+ out.push(...result.items);
1804
+ if (result.isEnd) break;
1631
1805
  }
1632
1806
  return out;
1633
1807
  }
@@ -1643,17 +1817,20 @@ var WebApi = class {
1643
1817
  emotion_list: [],
1644
1818
  strong_newbie_list: []
1645
1819
  };
1646
- if (type === WebHonorType.TALKATIVE || type === WebHonorType.ALL) {
1647
- const talkative = await this.fetchHonorList(groupCode, cookie, WebHonorType.TALKATIVE);
1648
- result.talkative_list = talkative;
1649
- const [first] = talkative;
1650
- if (first !== void 0) result.current_talkative = first;
1651
- }
1652
- if (type === WebHonorType.PERFORMER || type === WebHonorType.ALL) result.performer_list = await this.fetchHonorList(groupCode, cookie, WebHonorType.PERFORMER);
1653
- if (type === WebHonorType.LEGEND || type === WebHonorType.ALL) result.legend_list = await this.fetchHonorList(groupCode, cookie, WebHonorType.LEGEND);
1654
- if (type === WebHonorType.EMOTION || type === WebHonorType.ALL) result.emotion_list = await this.fetchHonorList(groupCode, cookie, WebHonorType.EMOTION);
1820
+ await this.fillHonorLists(result, groupCode, cookie, type);
1655
1821
  return result;
1656
1822
  }
1823
+ /** 按 type 填充荣誉列表(ALL 拉全部,单类型只拉对应类)。 */
1824
+ async fillHonorLists(result, groupCode, cookie, type) {
1825
+ for (const target of honorTargets(type)) {
1826
+ const list = await this.fetchHonorList(groupCode, cookie, target.kind);
1827
+ result[target.listKey] = list;
1828
+ if (target.kind === WebHonorType.TALKATIVE) {
1829
+ const [first] = list;
1830
+ if (first !== void 0) result.current_talkative = first;
1831
+ }
1832
+ }
1833
+ }
1657
1834
  /** 拉取单类荣誉列表(honorlist 接口,正则提取 __INITIAL_STATE__)。 */
1658
1835
  async fetchHonorList(groupCode, cookie, type) {
1659
1836
  const text = await fetchQunText("https://qun.qq.com/interactive/honorlist?" + new URLSearchParams({
@@ -1664,9 +1841,11 @@ var WebApi = class {
1664
1841
  return parseHonorList(text, type);
1665
1842
  }
1666
1843
  };
1844
+ /** honorlist 页面 __INITIAL_STATE__ 提取。 */
1845
+ const INITIAL_STATE_RE = /window\.__INITIAL_STATE__=(.*?);/;
1667
1846
  /** 从 honorlist 页面提取 __INITIAL_STATE__ → 荣誉列表(解析失败返回空)。 */
1668
1847
  function parseHonorList(text, type) {
1669
- const match = /window\.__INITIAL_STATE__=(.*?);/.exec(text);
1848
+ const match = INITIAL_STATE_RE.exec(text);
1670
1849
  if (match === null || match[1] === void 0) return [];
1671
1850
  let state;
1672
1851
  try {
@@ -1718,6 +1897,29 @@ function cookieToString(cookie) {
1718
1897
  for (const [key, value] of Object.entries(cookie)) parts.push(`${key}=${value}`);
1719
1898
  return parts.join("; ");
1720
1899
  }
1900
+ /** 荣誉类型 → 目标列表(ALL 拉全部,单类型只拉对应类)。 */
1901
+ function honorTargets(type) {
1902
+ const all = [
1903
+ {
1904
+ kind: WebHonorType.TALKATIVE,
1905
+ listKey: "talkative_list"
1906
+ },
1907
+ {
1908
+ kind: WebHonorType.PERFORMER,
1909
+ listKey: "performer_list"
1910
+ },
1911
+ {
1912
+ kind: WebHonorType.LEGEND,
1913
+ listKey: "legend_list"
1914
+ },
1915
+ {
1916
+ kind: WebHonorType.EMOTION,
1917
+ listKey: "emotion_list"
1918
+ }
1919
+ ];
1920
+ if (type === WebHonorType.ALL) return [...all];
1921
+ return all.filter((target) => target.kind === type);
1922
+ }
1721
1923
  //#endregion
1722
1924
  //#region src/bridge/group-bridge.ts
1723
1925
  /**
@@ -1778,7 +1980,8 @@ var MsgBridge = class {
1778
1980
  const listener = {
1779
1981
  onRecvMsg: (msgs) => this.channel.emit("Msg/onRecvMsg", msgs),
1780
1982
  onRecvMsgReadReport: (reports) => this.channel.emit("Msg/onRecvMsgReadReport", reports),
1781
- onRecvMsgReceipt: (receipts) => this.channel.emit("Msg/onRecvMsgReceipt", receipts)
1983
+ onRecvMsgReceipt: (receipts) => this.channel.emit("Msg/onRecvMsgReceipt", receipts),
1984
+ onMsgInfoListUpdate: (msgs) => this.channel.emit("Msg/onMsgInfoListUpdate", msgs)
1782
1985
  };
1783
1986
  this.listenerId = this.service.addKernelMsgListener(listener);
1784
1987
  }
@@ -1947,21 +2150,22 @@ var GroupCache = class {
1947
2150
  * 返回 undefined 表示成员不在群。
1948
2151
  */
1949
2152
  async getMember(groupCode, uid) {
2153
+ const cached = await this.getCachedMember(groupCode, uid);
2154
+ if (cached !== void 0) return cached;
2155
+ const [member] = await this.dedupe(`member:${groupCode}:${uid}`, () => this.groupApi.getGroupMemberInfo(groupCode, [uid]));
2156
+ if (member === void 0) return;
2157
+ const target = this.members.get(groupCode);
2158
+ if (target !== void 0) target.set(member.uid, member);
2159
+ return member;
2160
+ }
2161
+ /** 读缓存成员(缺失先整群回填一次)。 */
2162
+ async getCachedMember(groupCode, uid) {
1950
2163
  let map = this.members.get(groupCode);
1951
2164
  if (map === void 0 || map.size === 0) {
1952
2165
  await this.getMembers(groupCode);
1953
2166
  map = this.members.get(groupCode);
1954
2167
  }
1955
- const cached = map?.get(uid);
1956
- if (cached !== void 0) return cached;
1957
- const members = await this.dedupe(`member:${groupCode}:${uid}`, () => this.groupApi.getGroupMemberInfo(groupCode, [uid]));
1958
- if (members.length === 0) return;
1959
- const [member] = members;
1960
- if (member !== void 0) {
1961
- const target = this.members.get(groupCode);
1962
- if (target !== void 0) target.set(member.uid, member);
1963
- }
1964
- return member;
2168
+ return map?.get(uid);
1965
2169
  }
1966
2170
  /** 惰性回填全量群列表(getGroup 缺失时)。 */
1967
2171
  async loadGroupList() {
@@ -2102,9 +2306,12 @@ async function listLoginAccounts(ctx) {
2102
2306
  async function ensureLoginConnected(ctx, opts) {
2103
2307
  const raw = ctx.loginService;
2104
2308
  if (raw === null || typeof raw.connect !== "function") return;
2105
- const connect = raw.connect;
2106
- const addListener = raw.addKernelLoginListener;
2107
- await new Promise((resolve) => {
2309
+ await waitLoginConnected(raw, opts.timeoutMs);
2310
+ await sleep(CONNECTION_SETTLE_MS);
2311
+ }
2312
+ /** 等 onLoginConnected(注册临时监听 + connect + 超时兜底)。 */
2313
+ function waitLoginConnected(raw, timeoutMs) {
2314
+ return new Promise((resolve) => {
2108
2315
  let settled = false;
2109
2316
  const finish = () => {
2110
2317
  if (!settled) {
@@ -2112,25 +2319,29 @@ async function ensureLoginConnected(ctx, opts) {
2112
2319
  resolve();
2113
2320
  }
2114
2321
  };
2322
+ tryRegisterConnectListener(raw, finish);
2323
+ setTimeout(finish, timeoutMs ?? NETWORK_READY_TIMEOUT_MS);
2115
2324
  try {
2116
- const listener = createLoginListener();
2117
- listener.onLoginConnected = () => {
2118
- finish();
2119
- };
2120
- if (typeof addListener === "function") addListener(listener);
2121
- } catch {}
2122
- setTimeout(finish, opts.timeoutMs ?? NETWORK_READY_TIMEOUT_MS);
2123
- try {
2124
- connect();
2325
+ raw.connect?.();
2125
2326
  } catch {
2126
2327
  finish();
2127
2328
  }
2128
2329
  });
2129
- await sleep(CONNECTION_SETTLE_MS);
2330
+ }
2331
+ /** 注册临时登录连接监听(失败不阻塞连接)。 */
2332
+ function tryRegisterConnectListener(raw, finish) {
2333
+ try {
2334
+ const listener = createLoginListener();
2335
+ listener.onLoginConnected = () => {
2336
+ finish();
2337
+ };
2338
+ if (typeof raw.addKernelLoginListener === "function") raw.addKernelLoginListener(listener);
2339
+ } catch {}
2130
2340
  }
2131
2341
  /**
2132
2342
  * 目标账号选择(2026-08-07 修复:显式 uin 不在列表时**报错**而非静默 fallback——
2133
2343
  * 之前会退到第一个可快速登录账号,导致 `-q <uin>` 实际登成别的号)。
2344
+ * 导出供单测(login-connect.test.ts)。
2134
2345
  */
2135
2346
  function pickLoginTarget(items, uin) {
2136
2347
  if (uin !== void 0) {
@@ -2151,18 +2362,34 @@ function pickLoginTarget(items, uin) {
2151
2362
  async function loginWithNetworkRetry(ctx, loginService, target, opts) {
2152
2363
  let lastErrMsg = "";
2153
2364
  for (let attempt = 1; attempt <= NETWORK_RETRY_MAX; attempt += 1) {
2154
- const { errMsg } = (await loginService.quickLoginWithUin(target.uin)).loginErrorInfo;
2155
- if (!errMsg) return {
2156
- uin: target.uin,
2157
- uid: target.uid ?? "",
2158
- nick: target.nickName ?? ""
2159
- };
2160
- lastErrMsg = errMsg;
2161
- if (!(errMsg.includes(NETWORK_ERROR_CODE) || errMsg.includes(CONNECTION_ERROR_HINT)) || attempt >= NETWORK_RETRY_MAX) break;
2365
+ const attemptResult = await attemptQuickLogin(loginService, target);
2366
+ if (attemptResult.ok) return attemptResult.result;
2367
+ lastErrMsg = attemptResult.errMsg;
2368
+ if (!(isNetworkError(lastErrMsg) && attempt < NETWORK_RETRY_MAX)) break;
2162
2369
  if (!await waitForNetworkConnection(ctx, { timeoutMs: opts.timeoutMs ?? NETWORK_READY_TIMEOUT_MS })) break;
2163
2370
  }
2164
2371
  throw kernelError(`快速登录失败: ${lastErrMsg}`, "NOT_LOGIN");
2165
2372
  }
2373
+ /** 单次快速登录尝试(成功带结果,失败带错误消息)。 */
2374
+ async function attemptQuickLogin(loginService, target) {
2375
+ const { errMsg } = (await loginService.quickLoginWithUin(target.uin)).loginErrorInfo;
2376
+ if (errMsg) return {
2377
+ ok: false,
2378
+ errMsg
2379
+ };
2380
+ return {
2381
+ ok: true,
2382
+ result: {
2383
+ uin: target.uin,
2384
+ uid: target.uid ?? "",
2385
+ nick: target.nickName ?? ""
2386
+ }
2387
+ };
2388
+ }
2389
+ /** 判定快速登录失败是否为网络异常(重试语义)。导出供单测。 */
2390
+ function isNetworkError(errMsg) {
2391
+ return errMsg.includes(NETWORK_ERROR_CODE) || errMsg.includes(CONNECTION_ERROR_HINT);
2392
+ }
2166
2393
  /**
2167
2394
  * 快速登录:遍历历史登录列表尝试。
2168
2395
  * P2-1:失败且错误为网络异常(1006511)时,等网络就绪后重试(最多 NETWORK_RETRY_MAX 次)。
@@ -2232,7 +2459,14 @@ async function initAndStartSession(ctx, config, listener, opts) {
2232
2459
  if (session === null || session === void 0) throw kernelError("session 未创建", "INVALID_STATE");
2233
2460
  const depends = new DependsAdapter();
2234
2461
  const dispatcher = new DispatcherAdapter();
2235
- const initComplete = new Promise((resolve, reject) => {
2462
+ const initComplete = watchInitSignal(listener);
2463
+ session.init(config, depends, dispatcher, listener);
2464
+ startSessionBestEffort(ctx, session);
2465
+ if (!await Promise.race([initComplete.then(() => true), waitFor(() => false, opts.timeoutMs ?? DEFAULT_INIT_TIMEOUT_MS).then(() => false)])) throw kernelError("session init 超时", "TIMEOUT");
2466
+ }
2467
+ /** 监听 init 完成信号:onOpentelemetryInit(is_init) 为主,onSessionInitComplete(0) 为辅。 */
2468
+ function watchInitSignal(listener) {
2469
+ return new Promise((resolve, reject) => {
2236
2470
  const onOpentelemetry = listener.onOpentelemetryInit;
2237
2471
  listener.onOpentelemetryInit = (info) => {
2238
2472
  if (info.is_init) resolve();
@@ -2245,19 +2479,33 @@ async function initAndStartSession(ctx, config, listener, opts) {
2245
2479
  if (typeof onInitComplete === "function") onInitComplete(r);
2246
2480
  };
2247
2481
  });
2248
- session.init(config, depends, dispatcher, listener);
2249
- const { startupSession } = ctx;
2250
- if (startupSession !== null && typeof startupSession.start === "function") try {
2251
- startupSession.start();
2252
- } catch {}
2253
- else try {
2482
+ }
2483
+ /**
2484
+ * 启动会话(best effort,失败不致命):
2485
+ * **先 init 后 start(2026-08-07 V9 决定性修正)**。
2486
+ * 自建宿主实测(HANDOVER-V9,p0-napcat-min):必须先 session.init 再
2487
+ * startupSession.start()——顺序颠倒(先 start 后 init)业务 service 不挂载
2488
+ * (getMsgService null);init 后用 startNT(非 startupSession.start)也失败。
2489
+ * NapCat initializeSession 同款:有 startupSession 用 start(),否则 startNT(0)。
2490
+ */
2491
+ function startSessionBestEffort(ctx, session) {
2492
+ if (startupSessionStart(ctx)) return;
2493
+ try {
2254
2494
  session.startNT(0);
2255
2495
  } catch {
2256
2496
  try {
2257
2497
  session.startNT();
2258
2498
  } catch {}
2259
2499
  }
2260
- if (!await Promise.race([initComplete.then(() => true), waitFor(() => false, opts.timeoutMs ?? DEFAULT_INIT_TIMEOUT_MS).then(() => false)])) throw kernelError("session init 超时", "TIMEOUT");
2500
+ }
2501
+ /** 有 startupSession 则 start()(失败不致命),返回是否已处理。 */
2502
+ function startupSessionStart(ctx) {
2503
+ const { startupSession } = ctx;
2504
+ if (startupSession === null || typeof startupSession.start !== "function") return false;
2505
+ try {
2506
+ startupSession.start();
2507
+ } catch {}
2508
+ return true;
2261
2509
  }
2262
2510
  //#endregion
2263
2511
  //#region src/login/login.ts
@@ -2475,20 +2723,27 @@ function parseAppidFromMajor(majorPath) {
2475
2723
  } catch {
2476
2724
  return null;
2477
2725
  }
2726
+ return scanAppidMarkers(buf);
2727
+ }
2728
+ /** 扫描 buf 中所有 QQAppId/ 标记,返回第一个纯数字串(无则 null)。 */
2729
+ function scanAppidMarkers(buf) {
2478
2730
  const marker = Buffer.from("QQAppId/", "utf-8");
2479
2731
  let pos = 0;
2480
2732
  while (pos < buf.length) {
2481
2733
  const idx = buf.indexOf(marker, pos);
2482
2734
  if (idx === -1) return null;
2483
- const start = idx + marker.length;
2484
- let end = start;
2485
- while (end < buf.length && buf[end] !== 0) end += 1;
2486
- const str = buf.subarray(start, end).toString("utf-8");
2735
+ const str = readMarkerValue(buf, idx + marker.length);
2487
2736
  if (DIGITS_ONLY_RE.test(str)) return str;
2488
- pos = end + 1;
2737
+ pos = idx + marker.length + str.length + 1;
2489
2738
  }
2490
2739
  return null;
2491
2740
  }
2741
+ /** 读取标记后的 C 字符串(到首个 0 字节为止)。 */
2742
+ function readMarkerValue(buf, start) {
2743
+ let end = start;
2744
+ while (end < buf.length && buf[end] !== 0) end += 1;
2745
+ return buf.subarray(start, end).toString("utf-8");
2746
+ }
2492
2747
  /** Windows 兜底 appid / qua(major.node 解析失败时)。 */
2493
2748
  function fallbackAppidQua(fullVersion) {
2494
2749
  return {
@@ -2608,14 +2863,12 @@ function buildSessionConfig(options) {
2608
2863
  */
2609
2864
  /** 默认 engine 配置(KWINDOWS + 版本号,字段待首个真实联调确认)。 */
2610
2865
  function defaultEngineConfig(env, qqDataPath) {
2611
- let osVersion = process.platform;
2612
- if (process.platform === "win32") osVersion = "win32";
2613
2866
  return {
2614
2867
  base_path_prefix: env.dataDir ?? "",
2615
2868
  platform_type: PlatformType.KWINDOWS,
2616
2869
  app_type: 4,
2617
2870
  app_version: env.qqVersion ?? "",
2618
- os_version: osVersion,
2871
+ os_version: process.platform,
2619
2872
  use_xlog: false,
2620
2873
  qua: "",
2621
2874
  global_path_config: { desktopGlobalPath: qqDataPath ?? env.dataDir ?? "" },
@@ -2646,14 +2899,15 @@ function electronProcessType() {
2646
2899
  * 正确路径,无需此解析。
2647
2900
  */
2648
2901
  function resolveQqUserDataRoot(exports) {
2902
+ return extractUserDataRoot(exports) ?? join(homedir(), "Documents", "Tencent Files");
2903
+ }
2904
+ /** 从 QQ 官方 API 提取数据根(不可用返回 null,调用方回退默认)。 */
2905
+ function extractUserDataRoot(exports) {
2649
2906
  try {
2650
- const raw = (exports.NodeQQNTWrapperUtil?.get?.())?.getNTUserDataInfoConfig?.();
2651
- if (typeof raw === "string" && raw.trim() !== "") {
2652
- const extracted = extractDataRoot(raw.trim());
2653
- if (extracted !== null) return extracted;
2654
- }
2907
+ const raw = exports.NodeQQNTWrapperUtil?.get?.().getNTUserDataInfoConfig?.();
2908
+ if (typeof raw === "string" && raw.trim() !== "") return extractDataRoot(raw.trim());
2655
2909
  } catch {}
2656
- return join(homedir(), "Documents", "Tencent Files");
2910
+ return null;
2657
2911
  }
2658
2912
  /**
2659
2913
  * 从 loader 截获的 NAPI exports 创建 wrapper 上下文。
@@ -2689,27 +2943,37 @@ function initEngine(ctx, config) {
2689
2943
  * 返回带完整实现的实例。worker(utilityProcess)继承 QQ env 后同样有效。
2690
2944
  */
2691
2945
  function createSession(ctx) {
2692
- const X = ctx.exports;
2693
- const S = X.NodeIQQNTWrapperSession;
2694
- let session = null;
2946
+ const S = ctx.exports.NodeIQQNTWrapperSession;
2947
+ const session = createStartupSession(ctx) ?? getNTWrapperSession(S) ?? createNewSession(S) ?? new S();
2948
+ ctx.session = session;
2949
+ return session;
2950
+ }
2951
+ /** 步骤 1:StartupSessionWrapper.create()(建立启动 session,失败忽略)。 */
2952
+ function createStartupSession(ctx) {
2953
+ const Ssw = ctx.exports.NodeIQQNTStartupSessionWrapper;
2954
+ if (Ssw === void 0 || typeof Ssw.create !== "function") return null;
2695
2955
  try {
2696
- const Ssw = X.NodeIQQNTStartupSessionWrapper;
2697
- if (Ssw !== void 0 && typeof Ssw.create === "function") ctx.startupSession = Ssw.create();
2956
+ ctx.startupSession = Ssw.create();
2698
2957
  } catch {}
2958
+ return null;
2959
+ }
2960
+ /** 步骤 2:getNTWrapperSession("nt_1")(QQ 主 session,带 cpp_impl)。 */
2961
+ function getNTWrapperSession(S) {
2962
+ if (typeof S.getNTWrapperSession !== "function") return null;
2699
2963
  try {
2700
- if (typeof S.getNTWrapperSession === "function") {
2701
- const got = S.getNTWrapperSession("nt_1");
2702
- if (got !== null && got !== void 0) session = got;
2703
- }
2704
- } catch {}
2705
- if (session === null) try {
2706
- if (typeof S.create === "function") session = S.create();
2964
+ return S.getNTWrapperSession("nt_1");
2707
2965
  } catch {
2708
- session = null;
2966
+ return null;
2967
+ }
2968
+ }
2969
+ /** 步骤 3:S.create() 回退。 */
2970
+ function createNewSession(S) {
2971
+ if (typeof S.create !== "function") return null;
2972
+ try {
2973
+ return S.create();
2974
+ } catch {
2975
+ return null;
2709
2976
  }
2710
- if (session === null) session = new S();
2711
- ctx.session = session;
2712
- return session;
2713
2977
  }
2714
2978
  /** session 初始化(4 参全为 JS 对象,NAPI 自动转换)。 */
2715
2979
  function initSession(ctx, config, listener) {
@@ -2749,18 +3013,36 @@ function startNapuketto(options) {
2749
3013
  fullVersion: env?.qqVersion ?? "",
2750
3014
  buildVersion: env?.qqVersion ?? ""
2751
3015
  });
3016
+ const hasCapturedSession = captureSession(ctx, qqSession);
2752
3017
  const isSelfHost = electronProcessType() === void 0;
2753
- if (qqSession !== void 0 && qqSession !== null) ctx.session = qqSession;
2754
- else if (isSelfHost) createSession(ctx);
2755
- initEngine(ctx, engineConfig ?? defaultEngineConfig(env ?? {}, resolveBootQqGlobalPath(ctx)));
3018
+ prepareSession(ctx, hasCapturedSession, isSelfHost);
3019
+ initEngine(ctx, resolveEngineConfig(ctx, env, engineConfig));
2756
3020
  resolveLoginService(ctx, qqLoginService);
2757
- if (qqSession !== void 0 && qqSession !== null) ctx.session = qqSession;
2758
- else if (!isSelfHost) createSession(ctx);
3021
+ finishSession(ctx, hasCapturedSession, isSelfHost, sessionConfig);
3022
+ return ctx;
3023
+ }
3024
+ /** engine 前 session 准备(自建宿主必须先建,顺序决定性)。 */
3025
+ function prepareSession(ctx, hasCaptured, isSelfHost) {
3026
+ if (!hasCaptured && isSelfHost) createSession(ctx);
3027
+ }
3028
+ /** engine 后收尾:非自建宿主补建 session;配置了 sessionConfig 则 init+start。 */
3029
+ function finishSession(ctx, hasCaptured, isSelfHost, sessionConfig) {
3030
+ if (!hasCaptured && !isSelfHost) createSession(ctx);
2759
3031
  if (sessionConfig !== void 0) {
2760
3032
  initSession(ctx, sessionConfig, createSessionListener());
2761
3033
  startSession(ctx);
2762
3034
  }
2763
- return ctx;
3035
+ }
3036
+ /** engine 配置:显式覆盖优先,否则默认(含 QQ 数据根 global 路径解析)。 */
3037
+ function resolveEngineConfig(ctx, env, engineConfig) {
3038
+ if (engineConfig !== void 0) return engineConfig;
3039
+ return defaultEngineConfig(env ?? {}, resolveBootQqGlobalPath(ctx));
3040
+ }
3041
+ /** 捕获 QQ 已 init 的 session(boot.cjs 拦截 `new` 窃取),返回是否捕获成功。 */
3042
+ function captureSession(ctx, qqSession) {
3043
+ if (qqSession === void 0 || qqSession === null) return false;
3044
+ ctx.session = qqSession;
3045
+ return true;
2764
3046
  }
2765
3047
  /**
2766
3048
  * 解析 QQ 真实数据目录的 global 路径(desktopGlobalPath 三要素之三)。
@@ -2785,12 +3067,16 @@ function resolveLoginService(ctx, qqLoginService) {
2785
3067
  ctx.loginService = qqLoginService;
2786
3068
  return;
2787
3069
  }
3070
+ ctx.loginService = getOrCreateLoginService(ctx.exports);
3071
+ }
3072
+ /** 单例 get() 优先,缺失回退 new(都失败返回 null)。 */
3073
+ function getOrCreateLoginService(exports) {
2788
3074
  try {
2789
- const loginSvcCtor = ctx.exports.NodeIKernelLoginService;
2790
- if (loginSvcCtor !== null && typeof loginSvcCtor.get === "function") ctx.loginService = loginSvcCtor.get();
2791
- else ctx.loginService = new ctx.exports.NodeIKernelLoginService();
3075
+ const loginSvcCtor = exports.NodeIKernelLoginService;
3076
+ if (loginSvcCtor !== null && typeof loginSvcCtor.get === "function") return loginSvcCtor.get();
3077
+ return new exports.NodeIKernelLoginService();
2792
3078
  } catch {
2793
- ctx.loginService = null;
3079
+ return null;
2794
3080
  }
2795
3081
  }
2796
3082
  //#endregion
@@ -2870,18 +3156,7 @@ var NapukettoCore = class NapukettoCore {
2870
3156
  async login(opts) {
2871
3157
  const { wrapper } = this.ctx;
2872
3158
  if (wrapper === null) throw kernelError("wrapper 未装配,无法登录", "INVALID_STATE");
2873
- const loginService = wrapper.loginService;
2874
- if (loginService !== null && typeof loginService.initConfig === "function") {
2875
- let qqGlobalPath = null;
2876
- if (electronProcessType() !== "browser") {
2877
- const root = resolveQqUserDataRoot(wrapper.exports);
2878
- if (root !== null) qqGlobalPath = resolveQqGlobalPath(root);
2879
- }
2880
- const commonPath = qqGlobalPath ?? this.ctx.paths.accountDir;
2881
- const loginCfg = buildLoginConfig(opts.appid, wrapper.versionInfo.fullVersion, commonPath);
2882
- loginService.initConfig(loginCfg);
2883
- this.ctx.logger.info({ commonPath }, "loginService.initConfig OK");
2884
- } else this.ctx.logger.warn("loginService 不可用,跳过 initConfig");
3159
+ this.initLoginConfig(wrapper, opts.appid);
2885
3160
  let loginResult;
2886
3161
  try {
2887
3162
  const quickOpts = {};
@@ -2899,6 +3174,25 @@ var NapukettoCore = class NapukettoCore {
2899
3174
  return loginResult;
2900
3175
  }
2901
3176
  /**
3177
+ * loginService.initConfig(worker/自建宿主需指定 QQ 真实数据路径)。
3178
+ * worker(utilityProcess)模式 + 自建宿主(标准 node)下 loginService 是
3179
+ * new 的,commonPath 必须指向 QQ 真实数据目录(数据根/nt_qq/global,
3180
+ * HANDOVER-V6 三要素之三)才能读到历史账号——cli 的 `.napuketto\default`
3181
+ * 读不到(getLoginList 空,P2-1 实测 2026-08-06)。
3182
+ * QQ 主进程(V1)模式 QQ 自己 initConfig 过正确路径,无需解析。
3183
+ */
3184
+ initLoginConfig(wrapper, appid) {
3185
+ const loginService = wrapper.loginService;
3186
+ if (loginService === null || typeof loginService.initConfig !== "function") {
3187
+ this.ctx.logger.warn("loginService 不可用,跳过 initConfig");
3188
+ return;
3189
+ }
3190
+ const commonPath = resolveCommonPath(this.ctx, wrapper);
3191
+ const loginCfg = buildLoginConfig(appid, wrapper.versionInfo.fullVersion, commonPath);
3192
+ loginService.initConfig(loginCfg);
3193
+ this.ctx.logger.info({ commonPath }, "loginService.initConfig OK");
3194
+ }
3195
+ /**
2902
3196
  * 替换当前 session(登录后 Proxy 捕获的 QQ 新实例替换自己 create 的无效实例)。
2903
3197
  * 在 QQ 主进程内由 boot.cjs 调用(登录成功后)。
2904
3198
  */
@@ -2912,6 +3206,22 @@ var NapukettoCore = class NapukettoCore {
2912
3206
  if (wrapper === null || wrapper.loginService === null) throw kernelError("wrapper/loginService 不可用,无法 QR 登录", "INVALID_STATE");
2913
3207
  const session = new QrLoginSession(wrapper.loginService);
2914
3208
  const qrPath = opts.qrCodePath ?? this.ctx.paths.file("cache", "qrcode.png");
3209
+ this.subscribeQrProgress(session, opts, qrPath);
3210
+ const startOpts = {};
3211
+ if (opts.quickUin !== void 0) startOpts.quickUin = opts.quickUin;
3212
+ session.start(startOpts);
3213
+ await waitQrLoggedIn(session);
3214
+ const self = session.selfInfo;
3215
+ if (self === null) throw kernelError("QR 登录成功但 selfInfo 为空", "INVALID_STATE");
3216
+ session.stop();
3217
+ return {
3218
+ uin: self.uin,
3219
+ uid: self.uid,
3220
+ nick: self.nick
3221
+ };
3222
+ }
3223
+ /** 订阅 QR 进度:二维码写盘 + 状态机经 onLoginProgress 转发。 */
3224
+ subscribeQrProgress(session, opts, qrPath) {
2915
3225
  session.onQrCode((qr) => {
2916
3226
  this.ctx.logger.warn(`请扫描二维码登录(保存: ${qrPath} | URL: ${qr.qrcodeUrl})`);
2917
3227
  if (qr.pngBase64 !== "") writeQrCodePng(qrPath, qr.pngBase64);
@@ -2924,28 +3234,6 @@ var NapukettoCore = class NapukettoCore {
2924
3234
  this.ctx.logger.info({ state }, "QR 登录状态");
2925
3235
  opts.onLoginProgress?.({ state });
2926
3236
  });
2927
- const startOpts = {};
2928
- if (opts.quickUin !== void 0) startOpts.quickUin = opts.quickUin;
2929
- session.start(startOpts);
2930
- await new Promise((resolve, reject) => {
2931
- const offState = session.onStateChange((state) => {
2932
- if (state === "logged_in") {
2933
- offState();
2934
- resolve();
2935
- } else if (state === "failed") {
2936
- offState();
2937
- reject(kernelError("QR 登录失败", "NOT_LOGIN"));
2938
- }
2939
- });
2940
- });
2941
- const self = session.selfInfo;
2942
- if (self === null) throw kernelError("QR 登录成功但 selfInfo 为空", "INVALID_STATE");
2943
- session.stop();
2944
- return {
2945
- uin: self.uin,
2946
- uid: self.uid,
2947
- nick: self.nick
2948
- };
2949
3237
  }
2950
3238
  /** 停止:日志收尾(资源清理留给 P2 常驻管理)。 */
2951
3239
  stop() {
@@ -2959,6 +3247,32 @@ function writeQrCodePng(filePath, pngBase64) {
2959
3247
  writeFileSync(filePath, Buffer.from(pngBase64, "base64"));
2960
3248
  } catch {}
2961
3249
  }
3250
+ /** 等待 QR 登录终态:logged_in resolve,failed reject。 */
3251
+ function waitQrLoggedIn(session) {
3252
+ return new Promise((resolve, reject) => {
3253
+ const offState = session.onStateChange((state) => {
3254
+ if (state === "logged_in") {
3255
+ offState();
3256
+ resolve();
3257
+ } else if (state === "failed") {
3258
+ offState();
3259
+ reject(kernelError("QR 登录失败", "NOT_LOGIN"));
3260
+ }
3261
+ });
3262
+ });
3263
+ }
3264
+ /**
3265
+ * loginService.initConfig 的 commonPath:
3266
+ * worker/自建宿主(非 browser)优先 QQ 真实数据目录(数据根/nt_qq/global),
3267
+ * 否则回退账号目录。
3268
+ */
3269
+ function resolveCommonPath(ctx, wrapper) {
3270
+ if (electronProcessType() !== "browser") {
3271
+ const root = resolveQqUserDataRoot(wrapper.exports);
3272
+ if (root !== null) return resolveQqGlobalPath(root);
3273
+ }
3274
+ return ctx.paths.accountDir;
3275
+ }
2962
3276
  //#endregion
2963
3277
  //#region src/event-channel.ts
2964
3278
  /**
@@ -3046,15 +3360,19 @@ var NTEventChannel = class {
3046
3360
  for (const anyHandler of this.anyHandlers) try {
3047
3361
  anyHandler(event, ...args);
3048
3362
  } catch (err) {
3049
- for (const errorHandler of this.errorHandlers) errorHandler(err);
3363
+ this.notifyError(err);
3050
3364
  }
3051
3365
  const listeners = this.emitter.listeners(event);
3052
3366
  for (const listener of listeners) try {
3053
3367
  listener(...args);
3054
3368
  } catch (err) {
3055
- for (const handler of this.errorHandlers) handler(err);
3369
+ this.notifyError(err);
3056
3370
  }
3057
3371
  }
3372
+ /** 通知所有错误处理器(单个异常不打断其他处理器)。 */
3373
+ notifyError(err) {
3374
+ for (const handler of this.errorHandlers) handler(err);
3375
+ }
3058
3376
  /** 订阅错误通知(监听器抛出的异常 / 其他内部错误)。 */
3059
3377
  onError(handler) {
3060
3378
  this.errorHandlers.add(handler);
@@ -3067,41 +3385,48 @@ var NTEventChannel = class {
3067
3385
  //#region src/wrapper/session-resolver.ts
3068
3386
  /** 从 getSessionIdList 的 Map 提取主 sessionId(nt_ 前缀优先)。 */
3069
3387
  function findMainSessionId(ids) {
3070
- for (const [k, v] of ids) if (typeof v === "string") {
3071
- if (v.startsWith("nt_")) return v;
3072
- } else if (typeof k === "string" && k.startsWith("nt_")) return k;
3073
- for (const [, v] of ids) if (typeof v === "string") return v;
3074
- for (const k of ids.keys()) if (typeof k === "string") return k;
3388
+ return firstStringId(ids, (s) => s.startsWith("nt_")) ?? firstStringId(ids, () => true);
3389
+ }
3390
+ /** 遍历 Map 找第一个满足谓词的字符串 id(值优先,其次键)。 */
3391
+ function firstStringId(ids, pred) {
3392
+ for (const [k, v] of ids) {
3393
+ if (typeof v === "string" && pred(v)) return v;
3394
+ if (typeof k === "string" && pred(k)) return k;
3395
+ }
3075
3396
  return null;
3076
3397
  }
3077
3398
  /** 通过 getNTWrapperSession 拿主 session(内部辅助)。 */
3078
3399
  function resolveMainSession(created, getNTWrapperSession) {
3079
- if (typeof created.start === "function") created.start();
3080
- if (typeof created.getSessionIdList !== "function") return null;
3081
- const ids = created.getSessionIdList();
3400
+ created.start?.();
3401
+ const ids = typeof created.getSessionIdList === "function" ? created.getSessionIdList() : null;
3082
3402
  if (!(ids instanceof Map)) return null;
3083
3403
  const mainId = findMainSessionId(ids);
3084
3404
  if (mainId === null) return null;
3085
3405
  const maybe = getNTWrapperSession(mainId);
3086
- if (maybe !== null && maybe !== void 0 && typeof maybe.getMsgService === "function") return maybe;
3087
- return null;
3406
+ return maybe !== null && maybe !== void 0 && typeof maybe.getMsgService === "function" ? maybe : null;
3088
3407
  }
3089
3408
  /**
3090
3409
  * 复用 QQ 已有 session(P1-4:QQ 已登录,直接拿单例避免重复 init)。
3091
3410
  * 优先 `NodeIQQNTWrapperSession.get()`;返回 null 时回退 createSession。
3092
3411
  */
3093
3412
  function getExistingSession(ctx) {
3413
+ const got = trySessionGet(ctx);
3414
+ if (got !== null) {
3415
+ ctx.session = got;
3416
+ return got;
3417
+ }
3418
+ return null;
3419
+ }
3420
+ /** NodeIQQNTWrapperSession.get() 单例(无效/异常返回 null)。 */
3421
+ function trySessionGet(ctx) {
3094
3422
  try {
3095
3423
  const S = ctx.exports.NodeIQQNTWrapperSession;
3096
- if (typeof S.get === "function") {
3097
- const got = S.get();
3098
- if (got && typeof got.getMsgService === "function") {
3099
- ctx.session = got;
3100
- return got;
3101
- }
3102
- }
3103
- } catch {}
3104
- return null;
3424
+ if (typeof S.get !== "function") return null;
3425
+ const got = S.get();
3426
+ return got && typeof got.getMsgService === "function" ? got : null;
3427
+ } catch {
3428
+ return null;
3429
+ }
3105
3430
  }
3106
3431
  /**
3107
3432
  * 复用 QQ 主 session(P1-4 实测链路):
@@ -3111,17 +3436,22 @@ function getExistingSession(ctx) {
3111
3436
  */
3112
3437
  function getMainSession(ctx) {
3113
3438
  try {
3114
- const startupRaw = ctx.exports.NodeIQQNTStartupSessionWrapper;
3115
- const sessionCtor = ctx.exports.NodeIQQNTWrapperSession;
3116
- if (typeof startupRaw.create !== "function" || typeof sessionCtor.getNTWrapperSession !== "function") return null;
3117
- const created = startupRaw.create();
3118
- if (!created) return null;
3119
- const getSession = (id) => sessionCtor.getNTWrapperSession(id);
3120
- const session = resolveMainSession(created, getSession);
3121
- if (session !== null) ctx.session = session;
3122
- return session;
3123
- } catch {}
3124
- return null;
3439
+ return resolveMainFromStartup(ctx) ?? null;
3440
+ } catch {
3441
+ return null;
3442
+ }
3443
+ }
3444
+ /** startup.create → start → getSessionIdList → getNTWrapperSession 链路。 */
3445
+ function resolveMainFromStartup(ctx) {
3446
+ const startupRaw = ctx.exports.NodeIQQNTStartupSessionWrapper;
3447
+ const sessionCtor = ctx.exports.NodeIQQNTWrapperSession;
3448
+ if (typeof startupRaw.create !== "function" || typeof sessionCtor.getNTWrapperSession !== "function") return null;
3449
+ const created = startupRaw.create();
3450
+ if (!created) return null;
3451
+ const getSession = (id) => sessionCtor.getNTWrapperSession(id);
3452
+ const session = resolveMainSession(created, getSession);
3453
+ if (session !== null) ctx.session = session;
3454
+ return session;
3125
3455
  }
3126
3456
  //#endregion
3127
3457
  //#region src/wrapper/probe/probe-serialize.ts
@@ -3225,13 +3555,25 @@ function shapeKeyGetters(session, serviceMethods) {
3225
3555
  function listMethods(obj) {
3226
3556
  if (obj === null || obj === void 0) return [];
3227
3557
  const names = /* @__PURE__ */ new Set();
3558
+ collectPrototypeNames(obj, names);
3559
+ collectOwnNames(obj, names);
3560
+ return [...names].sort();
3561
+ }
3562
+ /** 收集原型链上的属性名(到 Object/Function.prototype 为止,去构造器)。 */
3563
+ function collectPrototypeNames(obj, names) {
3228
3564
  let proto = Object.getPrototypeOf(obj);
3229
- while (proto && proto !== Object.prototype && proto !== Function.prototype) {
3565
+ while (isProbeProto(proto)) {
3230
3566
  for (const name of Object.getOwnPropertyNames(proto)) if (name !== "constructor") names.add(name);
3231
3567
  proto = Object.getPrototypeOf(proto);
3232
3568
  }
3569
+ }
3570
+ /** 是否仍需遍历的原型(非 Object/Function.prototype 且非空)。 */
3571
+ function isProbeProto(proto) {
3572
+ return proto !== null && proto !== Object.prototype && proto !== Function.prototype;
3573
+ }
3574
+ /** 收集自有属性名(去构造器)。 */
3575
+ function collectOwnNames(obj, names) {
3233
3576
  for (const name of Object.getOwnPropertyNames(obj)) if (name !== "constructor") names.add(name);
3234
- return [...names].sort();
3235
3577
  }
3236
3578
  /** 安全调用:尝试调用方法拿返回值,失败记录错误(不中断探测)。 */
3237
3579
  function tryCall(obj, name) {
@@ -3278,41 +3620,52 @@ function probeExportCtors(ctx) {
3278
3620
  /** 探测 engine 关键调用(getDeviceInfo / getECDHService / getThirdPartySigService)。 */
3279
3621
  function probeEngineCalls(ctx) {
3280
3622
  const out = {};
3281
- for (const m of [
3282
- "getDeviceInfo",
3283
- "getECDHService",
3284
- "getThirdPartySigService",
3285
- "readyToShow"
3286
- ]) {
3287
- const call = tryCall(ctx.engine, m);
3288
- out[m] = call.ok ? {
3289
- value: serialize(call.value),
3290
- methods: call.value ? listMethods(call.value) : []
3291
- } : { error: call.error ?? "null/undefined" };
3292
- }
3623
+ for (const m of ENGINE_CALL_PROBES) out[m] = probeEngineCall(ctx, m);
3293
3624
  return out;
3294
3625
  }
3626
+ /** 探测单个 engine 方法调用(成功记录方法面/形状,失败记录错误)。 */
3627
+ function probeEngineCall(ctx, m) {
3628
+ const call = tryCall(ctx.engine, m);
3629
+ return call.ok ? {
3630
+ value: serialize(call.value),
3631
+ methods: call.value ? listMethods(call.value) : []
3632
+ } : { error: call.error ?? "null/undefined" };
3633
+ }
3634
+ /** engine 关键调用候选。 */
3635
+ const ENGINE_CALL_PROBES = [
3636
+ "getDeviceInfo",
3637
+ "getECDHService",
3638
+ "getThirdPartySigService",
3639
+ "readyToShow"
3640
+ ];
3295
3641
  /** 探测 LoginService 实例(QQ 已登录凭据入口)。 */
3296
3642
  function probeLoginService(ctx) {
3297
3643
  const ctor = ctx.exports.NodeIKernelLoginService;
3298
3644
  if (ctor === null || typeof ctor.get !== "function") return null;
3299
3645
  const inst = tryCall(ctor, "get");
3300
3646
  if (!(inst.ok && inst.value)) return { get: inst.error ?? "null/undefined" };
3301
- const out = {
3647
+ return {
3302
3648
  methods: listMethods(inst.value),
3303
- ownKeys: Object.getOwnPropertyNames(inst.value).slice(0, 30)
3649
+ ownKeys: Object.getOwnPropertyNames(inst.value).slice(0, 30),
3650
+ ...probeLoginGetters(inst.value)
3304
3651
  };
3305
- for (const m of [
3306
- "getAccountInfo",
3307
- "getLoginInfo",
3308
- "getA2",
3309
- "getTicket",
3310
- "getUin",
3311
- "getUid",
3312
- "getSelfUin"
3313
- ]) if (typeof inst.value[m] === "function") out[m] = tryShape(inst.value, m);
3652
+ }
3653
+ /** 尝试关键登录信息 getter(无参调用,探测形状)。 */
3654
+ function probeLoginGetters(inst) {
3655
+ const out = {};
3656
+ for (const m of LOGIN_GETTER_PROBES) if (typeof inst[m] === "function") out[m] = tryShape(inst, m);
3314
3657
  return out;
3315
3658
  }
3659
+ /** 登录信息 getter 候选(无参调用,探测形状)。 */
3660
+ const LOGIN_GETTER_PROBES = [
3661
+ "getAccountInfo",
3662
+ "getLoginInfo",
3663
+ "getA2",
3664
+ "getTicket",
3665
+ "getUin",
3666
+ "getUid",
3667
+ "getSelfUin"
3668
+ ];
3316
3669
  //#endregion
3317
3670
  //#region src/wrapper/probe/probe.ts
3318
3671
  /**
@@ -3332,55 +3685,77 @@ function probeLoginService(ctx) {
3332
3685
  /** 枚举候选 sessionId(nt_0..nt_9 / gpro_0..gpro_9)找 QQ 已 init 的 session。 */
3333
3686
  function enumerateSessionIds(ctx) {
3334
3687
  const S = ctx.exports.NodeIQQNTWrapperSession;
3335
- const enumFn = S["getNTWrapperSession"];
3688
+ const enumFn = getNTWrapperSessionFn(ctx);
3689
+ if (enumFn === null) return {};
3336
3690
  const out = {};
3337
- if (typeof enumFn !== "function") return out;
3338
- const ids = [];
3339
- for (let i = 0; i < 10; i += 1) ids.push(`nt_${i}`, `gpro_${i}`);
3340
- for (const id of ids) try {
3341
- const value = enumFn.call(S, id);
3342
- if (!value) continue;
3343
- const rec = value;
3344
- const hasGetter = typeof rec["getMsgService"] === "function";
3345
- let msgService = null;
3346
- let msgError = null;
3347
- try {
3348
- msgService = rec["getMsgService"].call(value);
3349
- } catch (e) {
3350
- msgError = e instanceof Error ? e.message : String(e);
3351
- }
3352
- out[id] = {
3353
- hasMsgServiceGetter: hasGetter,
3354
- msgServiceOk: msgService !== null && msgService !== void 0,
3355
- msgServiceMethods: msgService ? listMethods(msgService).length : 0,
3356
- msgError,
3357
- methods: listMethods(value)
3358
- };
3359
- } catch {}
3691
+ for (let i = 0; i < 10; i += 1) {
3692
+ probeSessionId(enumFn, S, `nt_${i}`, out);
3693
+ probeSessionId(enumFn, S, `gpro_${i}`, out);
3694
+ }
3360
3695
  return out;
3361
3696
  }
3697
+ /** 探测单个 sessionId(getter 存在性 + getMsgService 调用结果)。 */
3698
+ function probeSessionId(enumFn, thisCtx, id, out) {
3699
+ try {
3700
+ const value = enumFn.call(thisCtx, id);
3701
+ if (!value) return;
3702
+ out[id] = probeSessionRecord(value);
3703
+ } catch {}
3704
+ }
3705
+ /** 探测单个 session 记录(getter 存在性 + msgService 调用结果 + 方法面)。 */
3706
+ function probeSessionRecord(value) {
3707
+ const rec = value;
3708
+ const msg = tryMsgService(rec, value);
3709
+ return {
3710
+ hasMsgServiceGetter: typeof rec["getMsgService"] === "function",
3711
+ msgServiceOk: msg.service !== null,
3712
+ msgServiceMethods: msg.service ? listMethods(msg.service).length : 0,
3713
+ msgError: msg.error,
3714
+ methods: listMethods(value)
3715
+ };
3716
+ }
3717
+ /** 尝试调用 getMsgService(失败记录错误,成功返回实例)。 */
3718
+ function tryMsgService(rec, thisCtx) {
3719
+ try {
3720
+ return {
3721
+ service: rec["getMsgService"].call(thisCtx) ?? null,
3722
+ error: null
3723
+ };
3724
+ } catch (e) {
3725
+ return {
3726
+ service: null,
3727
+ error: e instanceof Error ? e.message : String(e)
3728
+ };
3729
+ }
3730
+ }
3731
+ /** 取 NodeIQQNTWrapperSession.getNTWrapperSession 函数(无则 null)。 */
3732
+ function getNTWrapperSessionFn(ctx) {
3733
+ const fn = ctx.exports.NodeIQQNTWrapperSession["getNTWrapperSession"];
3734
+ return typeof fn === "function" ? fn : null;
3735
+ }
3362
3736
  /** 探测 startup 实例:start() + getSessionIdList(),提取 sessionIds(Map 值或键)。 */
3363
3737
  function probeStartupChain(created) {
3364
3738
  const started = tryCall(created, "start");
3365
3739
  const ids = tryCall(created, "getSessionIdList");
3366
- const sessionIds = [];
3367
- if (ids.ok && ids.value instanceof Map) for (const [k, v] of ids.value) {
3368
- const id = typeof v === "string" ? v : String(k);
3369
- sessionIds.push(id);
3370
- }
3371
3740
  return {
3372
3741
  start: started.ok ? { ok: true } : { error: started.error },
3373
3742
  createdGetSessionIdList: ids.ok ? { value: serialize(ids.value) } : { error: ids.error },
3374
- sessionIds
3743
+ sessionIds: extractSessionIds(ids)
3375
3744
  };
3376
3745
  }
3746
+ /** 从 getSessionIdList 结果提取 sessionIds(Map 值或键优先)。 */
3747
+ function extractSessionIds(ids) {
3748
+ if (!(ids.ok && ids.value instanceof Map)) return [];
3749
+ const out = [];
3750
+ for (const [k, v] of ids.value) out.push(typeof v === "string" ? v : String(k));
3751
+ return out;
3752
+ }
3377
3753
  /** 用真实 sessionId 取主 session(getNTWrapperSession(nt_x))并记录方法面。 */
3378
3754
  function probeSessionById(ctx, id) {
3379
- const S = ctx.exports.NodeIQQNTWrapperSession;
3380
- const fn = S["getNTWrapperSession"];
3381
- if (typeof fn !== "function") return null;
3755
+ const fn = getNTWrapperSessionFn(ctx);
3756
+ if (fn === null) return null;
3382
3757
  try {
3383
- const value = fn.call(S, id);
3758
+ const value = fn.call(ctx.exports.NodeIQQNTWrapperSession, id);
3384
3759
  return value ? {
3385
3760
  methods: listMethods(value),
3386
3761
  ownKeys: Object.getOwnPropertyNames(value)
@@ -3391,7 +3766,6 @@ function probeSessionById(ctx, id) {
3391
3766
  }
3392
3767
  /** 兜底:候选名字 getNTWrapperSession(无 Map 数据时)。 */
3393
3768
  function probeByNameFallback(ctx) {
3394
- const S = ctx.exports.NodeIQQNTWrapperSession;
3395
3769
  const candidates = [
3396
3770
  "main",
3397
3771
  "primary",
@@ -3400,17 +3774,22 @@ function probeByNameFallback(ctx) {
3400
3774
  ""
3401
3775
  ];
3402
3776
  const getResults = {};
3403
- for (const name of candidates) try {
3404
- const fn = S["getNTWrapperSession"];
3405
- const value = typeof fn === "function" ? fn.call(S, name) : null;
3406
- getResults[name === "" ? "<empty>" : name] = value ? {
3777
+ for (const name of candidates) getResults[name === "" ? "<empty>" : name] = probeSessionByName(ctx, name);
3778
+ return getResults;
3779
+ }
3780
+ /** 探测单个候选名(getNTWrapperSession(name),记录方法面或错误)。 */
3781
+ function probeSessionByName(ctx, name) {
3782
+ try {
3783
+ const fn = getNTWrapperSessionFn(ctx);
3784
+ if (fn === null) return null;
3785
+ const value = fn.call(ctx.exports.NodeIQQNTWrapperSession, name);
3786
+ return value ? {
3407
3787
  methods: listMethods(value),
3408
3788
  ownKeys: Object.getOwnPropertyNames(value)
3409
3789
  } : null;
3410
3790
  } catch (e) {
3411
- getResults[name === "" ? "<empty>" : name] = { error: e instanceof Error ? e.message : String(e) };
3791
+ return { error: e instanceof Error ? e.message : String(e) };
3412
3792
  }
3413
- return getResults;
3414
3793
  }
3415
3794
  /** 探测 startup session 链路:create() → start() → getSessionIdList → getNTWrapperSession。 */
3416
3795
  function probeStartup(ctx) {
@@ -3422,60 +3801,76 @@ function probeStartup(ctx) {
3422
3801
  methods: created.value ? listMethods(created.value) : [],
3423
3802
  value: serialize(created.value)
3424
3803
  } : { error: created.error };
3425
- if (created.ok && created.value) {
3426
- const chain = probeStartupChain(created.value);
3427
- out["start"] = chain.start;
3428
- out["createdGetSessionIdList"] = chain.createdGetSessionIdList;
3429
- out["sessionIds"] = chain.sessionIds;
3430
- const mainId = chain.sessionIds.find((id) => id.startsWith("nt_")) ?? chain.sessionIds[0];
3431
- if (mainId !== void 0) {
3432
- out["mainSessionId"] = mainId;
3433
- out["mainSession"] = probeSessionById(ctx, mainId);
3434
- }
3435
- }
3804
+ if (created.ok && created.value) Object.assign(out, probeCreatedChain(ctx, created.value));
3436
3805
  out["enumSessionIds"] = enumerateSessionIds(ctx);
3437
3806
  if (out["sessionIds"] === void 0) out["getNTWrapperSession"] = probeByNameFallback(ctx);
3438
3807
  return out;
3439
3808
  }
3809
+ /** 探测 create 后的实例链:start / getSessionIdList / 主 session。 */
3810
+ function probeCreatedChain(ctx, created) {
3811
+ const chain = probeStartupChain(created);
3812
+ const out = {
3813
+ start: chain.start,
3814
+ createdGetSessionIdList: chain.createdGetSessionIdList,
3815
+ sessionIds: chain.sessionIds
3816
+ };
3817
+ const mainId = findMainProbeId(chain.sessionIds);
3818
+ if (mainId !== void 0) {
3819
+ out["mainSessionId"] = mainId;
3820
+ out["mainSession"] = probeSessionById(ctx, mainId);
3821
+ }
3822
+ return out;
3823
+ }
3824
+ /** 选主 sessionId(nt_ 前缀优先,否则第一个)。 */
3825
+ function findMainProbeId(sessionIds) {
3826
+ return sessionIds.find((id) => id.startsWith("nt_")) ?? sessionIds[0];
3827
+ }
3440
3828
  /** 探测 session 与 service 方法(优先复用 QQ 主 session)。 */
3441
3829
  function probeSession(ctx) {
3442
3830
  const out = {};
3443
- let session = null;
3831
+ const session = pickProbeSession(ctx, out);
3832
+ out["session"] = session ? {
3833
+ methods: listMethods(session),
3834
+ ownKeys: Object.getOwnPropertyNames(session)
3835
+ } : null;
3836
+ if (session !== null && session !== void 0) out["services"] = collectServiceGetters(session);
3837
+ return out;
3838
+ }
3839
+ /** 选探测目标 session:优先 QQ 主 session(getMainSession),回退 ctx.session。 */
3840
+ function pickProbeSession(ctx, out) {
3444
3841
  try {
3445
3842
  const main = getMainSession(ctx);
3446
3843
  if (main !== null) {
3447
- session = main;
3448
3844
  out["sessionSource"] = "mainSession(getNTWrapperSession)";
3845
+ return main;
3449
3846
  }
3450
3847
  } catch {}
3451
- if (session === null && ctx.session !== null) {
3452
- session = ctx.session;
3848
+ if (ctx.session !== null) {
3453
3849
  out["sessionSource"] = "own-create";
3850
+ return ctx.session;
3454
3851
  }
3455
- out["session"] = session ? {
3456
- methods: listMethods(session),
3457
- ownKeys: Object.getOwnPropertyNames(session)
3458
- } : null;
3459
- if (session !== null && session !== void 0) {
3460
- const getters = listMethods(session).filter((m) => m.startsWith("get") && m.endsWith("Service"));
3461
- const services = {};
3462
- for (const getter of getters) {
3463
- const call = tryCall(session, getter);
3464
- if (call.ok && call.value) {
3465
- const methods = listMethods(call.value);
3466
- services[getter] = {
3467
- methods,
3468
- ownKeys: Object.getOwnPropertyNames(call.value).slice(0, 20),
3469
- shapes: shapeKeyGetters(call.value, methods)
3470
- };
3471
- } else services[getter] = {
3472
- error: call.error ?? "null/undefined",
3473
- ok: call.ok
3474
- };
3475
- }
3476
- out["services"] = services;
3477
- }
3478
- return out;
3852
+ return null;
3853
+ }
3854
+ /** 收集 session 的 get*Service getter 方法面与返回形状。 */
3855
+ function collectServiceGetters(session) {
3856
+ const getters = listMethods(session).filter((m) => m.startsWith("get") && m.endsWith("Service"));
3857
+ const services = {};
3858
+ for (const getter of getters) services[getter] = probeServiceGetter(session, getter);
3859
+ return services;
3860
+ }
3861
+ /** 探测单个 getter:成功记录方法面/形状,失败记录错误。 */
3862
+ function probeServiceGetter(session, getter) {
3863
+ const call = tryCall(session, getter);
3864
+ if (!(call.ok && call.value)) return {
3865
+ error: call.error ?? "null/undefined",
3866
+ ok: call.ok
3867
+ };
3868
+ const methods = listMethods(call.value);
3869
+ return {
3870
+ methods,
3871
+ ownKeys: Object.getOwnPropertyNames(call.value).slice(0, 20),
3872
+ shapes: shapeKeyGetters(call.value, methods)
3873
+ };
3479
3874
  }
3480
3875
  /**
3481
3876
  * 探测 wrapper 运行时:dump session 方法与各 service 方法到 JSON。
@@ -3522,12 +3917,11 @@ function readVersionPackage(installDir, version) {
3522
3917
  if (!existsSync(pkgPath)) throw kernelError(`QQ 版本目录缺少 package.json: ${pkgPath}`, "NOT_FOUND");
3523
3918
  try {
3524
3919
  const raw = JSON.parse(readFileSync(pkgPath, "utf8"));
3525
- const result = {
3920
+ return {
3526
3921
  name: raw.name ?? "",
3527
- version: raw.version ?? ""
3922
+ version: raw.version ?? "",
3923
+ ...raw.buildVersion !== void 0 ? { buildVersion: raw.buildVersion } : {}
3528
3924
  };
3529
- if (raw.buildVersion !== void 0) result.buildVersion = raw.buildVersion;
3530
- return result;
3531
3925
  } catch (cause) {
3532
3926
  throw kernelError(`QQ 版本 package.json 解析失败: ${pkgPath}`, "INVALID_PARAM", { cause });
3533
3927
  }