@napuketto/kernel 0.0.3 → 0.0.5

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
@@ -518,24 +518,32 @@ var FriendApi = class {
518
518
  }
519
519
  /** 单项转换:DoubtBuddyReq → OB11 结构(可选字段条件赋值,exactOptionalPropertyTypes)。 */
520
520
  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;
521
+ return toDoubtFriendRequestInfo(item, uinMap);
533
522
  }
534
523
  /** 处理可疑好友申请(set_doubt_friends_add_request;uid=flag)。 */
535
524
  handleDoubtFriendRequest(uid) {
536
525
  this.service.approvalDoubtBuddyReq(uid, "", "");
537
526
  }
538
527
  };
528
+ /** 可选字段条件赋值(undefined 跳过,兼容 exactOptionalPropertyTypes)。 */
529
+ function assignOptional(info, key, value) {
530
+ if (value !== void 0) info[key] = value;
531
+ }
532
+ /** 单项转换:DoubtBuddyReq → OB11 结构(导出供单测,friend.test.ts)。 */
533
+ function toDoubtFriendRequestInfo(item, uinMap) {
534
+ const info = {
535
+ flag: item.uid,
536
+ uin: Number(uinMap.get(item.uid) ?? item.uid),
537
+ type: "doubt"
538
+ };
539
+ assignOptional(info, "nick", item.nick);
540
+ assignOptional(info, "source", item.source);
541
+ assignOptional(info, "reason", item.reason);
542
+ assignOptional(info, "msg", item.msg);
543
+ assignOptional(info, "group_code", item.groupCode);
544
+ assignOptional(info, "time", item.reqTime);
545
+ return info;
546
+ }
539
547
  //#endregion
540
548
  //#region src/types/entities.ts
541
549
  /**
@@ -943,11 +951,9 @@ var GroupApi = class {
943
951
  const raw = await this.service.getGroupDetailInfo(groupCode, 0);
944
952
  const { result, errMsg } = raw;
945
953
  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");
954
+ const detail = extractGroupDetail(result, raw);
955
+ if (detail === null) throw kernelError(`getGroupDetailInfo 返回形状异常(groupCode=${groupCode}): ${errMsg}`, "UNKNOWN");
956
+ return detail;
951
957
  }
952
958
  /** 群成员列表(forceFetch=true 强制拉取,默认缓存优先)。 */
953
959
  async getGroupMemberList(groupCode, forceFetch = false) {
@@ -1084,6 +1090,19 @@ var GroupApi = class {
1084
1090
  };
1085
1091
  }
1086
1092
  };
1093
+ /**
1094
+ * 从 getGroupDetailInfo 返回提取群详情(兼容三种形状):
1095
+ * - result 为对象 → 详情直接在其中(最常见)
1096
+ * - result 为 0/空 → 从 raw.groupInfo / detailInfo / info 提取
1097
+ * 提取不到返回 null(调用方抛 UNKNOWN)。导出供单测。
1098
+ */
1099
+ function extractGroupDetail(result, raw) {
1100
+ if (result !== null && typeof result === "object") return result;
1101
+ const detail = raw;
1102
+ const candidate = detail.groupInfo ?? detail.detailInfo ?? detail.info;
1103
+ if (candidate !== void 0 && candidate !== null && typeof candidate === "object") return candidate;
1104
+ return null;
1105
+ }
1087
1106
  //#endregion
1088
1107
  //#region src/apis/group-notify.ts
1089
1108
  /**
@@ -1219,14 +1238,10 @@ var MsgApi = class {
1219
1238
  * → 再拉一次消息读 pttElement.text。
1220
1239
  */
1221
1240
  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");
1241
+ const ptt = findPttElement(await this.fetchMsgsByMsgId(target, [msgId]));
1242
+ if (ptt === null) throw kernelError("消息中不包含语音", "NOT_FOUND");
1226
1243
  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;
1244
+ const text = findPttElement(await this.fetchMsgsByMsgId(target, [msgId]))?.pttElement?.text;
1230
1245
  if (text === void 0 || text === "") throw kernelError("获取语音转文字结果失败", "UNKNOWN");
1231
1246
  return text;
1232
1247
  }
@@ -1257,10 +1272,8 @@ var MsgApi = class {
1257
1272
  }
1258
1273
  /** 获取合并转发内容(get_forward_msg;resId 取自 multiForwardMsgElement)。 */
1259
1274
  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");
1275
+ const forward = findForwardElement(await this.fetchMsgsByMsgId(peer, [msgId]));
1276
+ if (forward === null || forward.resId === "") throw kernelError("消息不包含合并转发内容", "NOT_FOUND");
1264
1277
  const raw = await this.service.getMultiMsg(peer, msgId, forward.resId);
1265
1278
  unwrapResult("getMultiMsg", raw);
1266
1279
  return raw.msgList ?? [];
@@ -1275,6 +1288,18 @@ var MsgApi = class {
1275
1288
  unwrapResult("setStatus", await this.service.setStatus(opts));
1276
1289
  }
1277
1290
  };
1291
+ /** 在消息列表中找含 pttElement 的元素(找不到返回 null)。 */
1292
+ function findPttElement(msgs) {
1293
+ const [first] = msgs;
1294
+ if (first === void 0) return null;
1295
+ return first.elements.find((el) => el.pttElement !== void 0) ?? null;
1296
+ }
1297
+ /** 在消息列表中找含 multiForwardMsgElement 的元素(找不到返回 null)。 */
1298
+ function findForwardElement(msgs) {
1299
+ const [first] = msgs;
1300
+ if (first === void 0) return null;
1301
+ return first.elements.find((el) => el.multiForwardMsgElement !== void 0)?.multiForwardMsgElement ?? null;
1302
+ }
1278
1303
  //#endregion
1279
1304
  //#region src/apis/stranger-info.ts
1280
1305
  /** 宽松取值:undefined 时给默认值(null 也归默认)。 */
@@ -1286,13 +1311,19 @@ function mergeDetail(info, byUin) {
1286
1311
  const infoSimple = info.detail?.simpleInfo;
1287
1312
  const byUinSimple = byUin.detail?.simpleInfo;
1288
1313
  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
1314
+ coreInfo: pickField(infoSimple, byUinSimple, (s) => s?.coreInfo),
1315
+ baseInfo: pickField(infoSimple, byUinSimple, (s) => s?.baseInfo),
1316
+ vasInfo: pickField(infoSimple, byUinSimple, (s) => s?.vasInfo),
1317
+ commonExt: pickField(info.detail, byUin.detail, (d) => d?.commonExt),
1318
+ status: pickField(infoSimple, byUinSimple, (s) => s?.status?.status)
1294
1319
  };
1295
1320
  }
1321
+ /** 字段级回退:primary 的字段取不到回退 fallback 的同名字段(宽松取值)。 */
1322
+ function pickField(primary, fallback, get) {
1323
+ const fromPrimary = primary !== void 0 ? get(primary) : void 0;
1324
+ if (fromPrimary !== void 0) return fromPrimary;
1325
+ return fallback !== void 0 ? get(fallback) : void 0;
1326
+ }
1296
1327
  /** 扁平化两份详情 → StrangerInfo(宽松取值,字段缺失给默认)。 */
1297
1328
  function flattenStrangerInfo(uin, uid, byUin, info) {
1298
1329
  const d = mergeDetail(info, byUin);
@@ -1491,14 +1522,18 @@ var RichMediaApi = class {
1491
1522
  if (raw.result !== 0) unwrap("transGroupFile", raw.result, raw.errMsg);
1492
1523
  }
1493
1524
  };
1494
- /** 从 getGroupFileList 返回提取文件列表(兼容数组 / { items } / { fileList })。 */
1525
+ /** 从 getGroupFileList 返回提取文件列表(兼容数组 / { items } / { fileList })。导出供单测。 */
1495
1526
  function extractFileList(result) {
1496
1527
  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;
1528
+ if (result === null || typeof result !== "object") return [];
1529
+ const obj = result;
1530
+ for (const key of [
1531
+ "items",
1532
+ "fileList",
1533
+ "list"
1534
+ ]) {
1535
+ const value = obj[key];
1536
+ if (Array.isArray(value)) return value;
1502
1537
  }
1503
1538
  return [];
1504
1539
  }
@@ -1610,24 +1645,31 @@ var WebApi = class {
1610
1645
  constructor(opts) {
1611
1646
  this.getCookies = opts.getCookies;
1612
1647
  }
1648
+ /** 拉取单页精华消息(失败/retcode 非 0 返回 null)。 */
1649
+ async fetchEssencePage(groupCode, cookie, bkn, page) {
1650
+ const ret = await fetchQunJson("https://qun.qq.com/cgi-bin/group_digest/digest_list?" + new URLSearchParams({
1651
+ bkn,
1652
+ page_start: String(page * ESSENCE_PAGE_LIMIT),
1653
+ page_limit: String(ESSENCE_PAGE_LIMIT),
1654
+ group_code: groupCode
1655
+ }).toString(), cookie);
1656
+ if (ret === null || ret.retcode !== 0) return null;
1657
+ const list = ret.data?.msg_list;
1658
+ return {
1659
+ items: Array.isArray(list) ? list.filter((item) => item !== null) : [],
1660
+ isEnd: ret.data?.is_end === true
1661
+ };
1662
+ }
1613
1663
  /** 获取群精华消息列表(get_essence_msg_list;分页拉取至 is_end)。 */
1614
1664
  async getEssenceMsgList(groupCode) {
1615
1665
  const cookie = await this.getCookies(QUN_DOMAIN);
1616
1666
  const bkn = TicketApi.getBkn(cookie["skey"] ?? "");
1617
1667
  const out = [];
1618
1668
  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;
1669
+ const result = await this.fetchEssencePage(groupCode, cookie, bkn, page);
1670
+ if (result === null) break;
1671
+ out.push(...result.items);
1672
+ if (result.isEnd) break;
1631
1673
  }
1632
1674
  return out;
1633
1675
  }
@@ -1643,17 +1685,20 @@ var WebApi = class {
1643
1685
  emotion_list: [],
1644
1686
  strong_newbie_list: []
1645
1687
  };
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);
1688
+ await this.fillHonorLists(result, groupCode, cookie, type);
1655
1689
  return result;
1656
1690
  }
1691
+ /** 按 type 填充荣誉列表(ALL 拉全部,单类型只拉对应类)。 */
1692
+ async fillHonorLists(result, groupCode, cookie, type) {
1693
+ for (const target of honorTargets(type)) {
1694
+ const list = await this.fetchHonorList(groupCode, cookie, target.kind);
1695
+ result[target.listKey] = list;
1696
+ if (target.kind === WebHonorType.TALKATIVE) {
1697
+ const [first] = list;
1698
+ if (first !== void 0) result.current_talkative = first;
1699
+ }
1700
+ }
1701
+ }
1657
1702
  /** 拉取单类荣誉列表(honorlist 接口,正则提取 __INITIAL_STATE__)。 */
1658
1703
  async fetchHonorList(groupCode, cookie, type) {
1659
1704
  const text = await fetchQunText("https://qun.qq.com/interactive/honorlist?" + new URLSearchParams({
@@ -1664,9 +1709,11 @@ var WebApi = class {
1664
1709
  return parseHonorList(text, type);
1665
1710
  }
1666
1711
  };
1712
+ /** honorlist 页面 __INITIAL_STATE__ 提取。 */
1713
+ const INITIAL_STATE_RE = /window\.__INITIAL_STATE__=(.*?);/;
1667
1714
  /** 从 honorlist 页面提取 __INITIAL_STATE__ → 荣誉列表(解析失败返回空)。 */
1668
1715
  function parseHonorList(text, type) {
1669
- const match = /window\.__INITIAL_STATE__=(.*?);/.exec(text);
1716
+ const match = INITIAL_STATE_RE.exec(text);
1670
1717
  if (match === null || match[1] === void 0) return [];
1671
1718
  let state;
1672
1719
  try {
@@ -1718,6 +1765,29 @@ function cookieToString(cookie) {
1718
1765
  for (const [key, value] of Object.entries(cookie)) parts.push(`${key}=${value}`);
1719
1766
  return parts.join("; ");
1720
1767
  }
1768
+ /** 荣誉类型 → 目标列表(ALL 拉全部,单类型只拉对应类)。 */
1769
+ function honorTargets(type) {
1770
+ const all = [
1771
+ {
1772
+ kind: WebHonorType.TALKATIVE,
1773
+ listKey: "talkative_list"
1774
+ },
1775
+ {
1776
+ kind: WebHonorType.PERFORMER,
1777
+ listKey: "performer_list"
1778
+ },
1779
+ {
1780
+ kind: WebHonorType.LEGEND,
1781
+ listKey: "legend_list"
1782
+ },
1783
+ {
1784
+ kind: WebHonorType.EMOTION,
1785
+ listKey: "emotion_list"
1786
+ }
1787
+ ];
1788
+ if (type === WebHonorType.ALL) return [...all];
1789
+ return all.filter((target) => target.kind === type);
1790
+ }
1721
1791
  //#endregion
1722
1792
  //#region src/bridge/group-bridge.ts
1723
1793
  /**
@@ -1947,21 +2017,22 @@ var GroupCache = class {
1947
2017
  * 返回 undefined 表示成员不在群。
1948
2018
  */
1949
2019
  async getMember(groupCode, uid) {
2020
+ const cached = await this.getCachedMember(groupCode, uid);
2021
+ if (cached !== void 0) return cached;
2022
+ const [member] = await this.dedupe(`member:${groupCode}:${uid}`, () => this.groupApi.getGroupMemberInfo(groupCode, [uid]));
2023
+ if (member === void 0) return;
2024
+ const target = this.members.get(groupCode);
2025
+ if (target !== void 0) target.set(member.uid, member);
2026
+ return member;
2027
+ }
2028
+ /** 读缓存成员(缺失先整群回填一次)。 */
2029
+ async getCachedMember(groupCode, uid) {
1950
2030
  let map = this.members.get(groupCode);
1951
2031
  if (map === void 0 || map.size === 0) {
1952
2032
  await this.getMembers(groupCode);
1953
2033
  map = this.members.get(groupCode);
1954
2034
  }
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;
2035
+ return map?.get(uid);
1965
2036
  }
1966
2037
  /** 惰性回填全量群列表(getGroup 缺失时)。 */
1967
2038
  async loadGroupList() {
@@ -2102,9 +2173,12 @@ async function listLoginAccounts(ctx) {
2102
2173
  async function ensureLoginConnected(ctx, opts) {
2103
2174
  const raw = ctx.loginService;
2104
2175
  if (raw === null || typeof raw.connect !== "function") return;
2105
- const connect = raw.connect;
2106
- const addListener = raw.addKernelLoginListener;
2107
- await new Promise((resolve) => {
2176
+ await waitLoginConnected(raw, opts.timeoutMs);
2177
+ await sleep(CONNECTION_SETTLE_MS);
2178
+ }
2179
+ /** 等 onLoginConnected(注册临时监听 + connect + 超时兜底)。 */
2180
+ function waitLoginConnected(raw, timeoutMs) {
2181
+ return new Promise((resolve) => {
2108
2182
  let settled = false;
2109
2183
  const finish = () => {
2110
2184
  if (!settled) {
@@ -2112,25 +2186,29 @@ async function ensureLoginConnected(ctx, opts) {
2112
2186
  resolve();
2113
2187
  }
2114
2188
  };
2189
+ tryRegisterConnectListener(raw, finish);
2190
+ setTimeout(finish, timeoutMs ?? NETWORK_READY_TIMEOUT_MS);
2115
2191
  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();
2192
+ raw.connect?.();
2125
2193
  } catch {
2126
2194
  finish();
2127
2195
  }
2128
2196
  });
2129
- await sleep(CONNECTION_SETTLE_MS);
2197
+ }
2198
+ /** 注册临时登录连接监听(失败不阻塞连接)。 */
2199
+ function tryRegisterConnectListener(raw, finish) {
2200
+ try {
2201
+ const listener = createLoginListener();
2202
+ listener.onLoginConnected = () => {
2203
+ finish();
2204
+ };
2205
+ if (typeof raw.addKernelLoginListener === "function") raw.addKernelLoginListener(listener);
2206
+ } catch {}
2130
2207
  }
2131
2208
  /**
2132
2209
  * 目标账号选择(2026-08-07 修复:显式 uin 不在列表时**报错**而非静默 fallback——
2133
2210
  * 之前会退到第一个可快速登录账号,导致 `-q <uin>` 实际登成别的号)。
2211
+ * 导出供单测(login-connect.test.ts)。
2134
2212
  */
2135
2213
  function pickLoginTarget(items, uin) {
2136
2214
  if (uin !== void 0) {
@@ -2151,18 +2229,34 @@ function pickLoginTarget(items, uin) {
2151
2229
  async function loginWithNetworkRetry(ctx, loginService, target, opts) {
2152
2230
  let lastErrMsg = "";
2153
2231
  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;
2232
+ const attemptResult = await attemptQuickLogin(loginService, target);
2233
+ if (attemptResult.ok) return attemptResult.result;
2234
+ lastErrMsg = attemptResult.errMsg;
2235
+ if (!(isNetworkError(lastErrMsg) && attempt < NETWORK_RETRY_MAX)) break;
2162
2236
  if (!await waitForNetworkConnection(ctx, { timeoutMs: opts.timeoutMs ?? NETWORK_READY_TIMEOUT_MS })) break;
2163
2237
  }
2164
2238
  throw kernelError(`快速登录失败: ${lastErrMsg}`, "NOT_LOGIN");
2165
2239
  }
2240
+ /** 单次快速登录尝试(成功带结果,失败带错误消息)。 */
2241
+ async function attemptQuickLogin(loginService, target) {
2242
+ const { errMsg } = (await loginService.quickLoginWithUin(target.uin)).loginErrorInfo;
2243
+ if (errMsg) return {
2244
+ ok: false,
2245
+ errMsg
2246
+ };
2247
+ return {
2248
+ ok: true,
2249
+ result: {
2250
+ uin: target.uin,
2251
+ uid: target.uid ?? "",
2252
+ nick: target.nickName ?? ""
2253
+ }
2254
+ };
2255
+ }
2256
+ /** 判定快速登录失败是否为网络异常(重试语义)。导出供单测。 */
2257
+ function isNetworkError(errMsg) {
2258
+ return errMsg.includes(NETWORK_ERROR_CODE) || errMsg.includes(CONNECTION_ERROR_HINT);
2259
+ }
2166
2260
  /**
2167
2261
  * 快速登录:遍历历史登录列表尝试。
2168
2262
  * P2-1:失败且错误为网络异常(1006511)时,等网络就绪后重试(最多 NETWORK_RETRY_MAX 次)。
@@ -2232,7 +2326,14 @@ async function initAndStartSession(ctx, config, listener, opts) {
2232
2326
  if (session === null || session === void 0) throw kernelError("session 未创建", "INVALID_STATE");
2233
2327
  const depends = new DependsAdapter();
2234
2328
  const dispatcher = new DispatcherAdapter();
2235
- const initComplete = new Promise((resolve, reject) => {
2329
+ const initComplete = watchInitSignal(listener);
2330
+ session.init(config, depends, dispatcher, listener);
2331
+ startSessionBestEffort(ctx, session);
2332
+ if (!await Promise.race([initComplete.then(() => true), waitFor(() => false, opts.timeoutMs ?? DEFAULT_INIT_TIMEOUT_MS).then(() => false)])) throw kernelError("session init 超时", "TIMEOUT");
2333
+ }
2334
+ /** 监听 init 完成信号:onOpentelemetryInit(is_init) 为主,onSessionInitComplete(0) 为辅。 */
2335
+ function watchInitSignal(listener) {
2336
+ return new Promise((resolve, reject) => {
2236
2337
  const onOpentelemetry = listener.onOpentelemetryInit;
2237
2338
  listener.onOpentelemetryInit = (info) => {
2238
2339
  if (info.is_init) resolve();
@@ -2245,19 +2346,33 @@ async function initAndStartSession(ctx, config, listener, opts) {
2245
2346
  if (typeof onInitComplete === "function") onInitComplete(r);
2246
2347
  };
2247
2348
  });
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 {
2349
+ }
2350
+ /**
2351
+ * 启动会话(best effort,失败不致命):
2352
+ * **先 init 后 start(2026-08-07 V9 决定性修正)**。
2353
+ * 自建宿主实测(HANDOVER-V9,p0-napcat-min):必须先 session.init 再
2354
+ * startupSession.start()——顺序颠倒(先 start 后 init)业务 service 不挂载
2355
+ * (getMsgService null);init 后用 startNT(非 startupSession.start)也失败。
2356
+ * NapCat initializeSession 同款:有 startupSession 用 start(),否则 startNT(0)。
2357
+ */
2358
+ function startSessionBestEffort(ctx, session) {
2359
+ if (startupSessionStart(ctx)) return;
2360
+ try {
2254
2361
  session.startNT(0);
2255
2362
  } catch {
2256
2363
  try {
2257
2364
  session.startNT();
2258
2365
  } catch {}
2259
2366
  }
2260
- if (!await Promise.race([initComplete.then(() => true), waitFor(() => false, opts.timeoutMs ?? DEFAULT_INIT_TIMEOUT_MS).then(() => false)])) throw kernelError("session init 超时", "TIMEOUT");
2367
+ }
2368
+ /** 有 startupSession 则 start()(失败不致命),返回是否已处理。 */
2369
+ function startupSessionStart(ctx) {
2370
+ const { startupSession } = ctx;
2371
+ if (startupSession === null || typeof startupSession.start !== "function") return false;
2372
+ try {
2373
+ startupSession.start();
2374
+ } catch {}
2375
+ return true;
2261
2376
  }
2262
2377
  //#endregion
2263
2378
  //#region src/login/login.ts
@@ -2475,20 +2590,27 @@ function parseAppidFromMajor(majorPath) {
2475
2590
  } catch {
2476
2591
  return null;
2477
2592
  }
2593
+ return scanAppidMarkers(buf);
2594
+ }
2595
+ /** 扫描 buf 中所有 QQAppId/ 标记,返回第一个纯数字串(无则 null)。 */
2596
+ function scanAppidMarkers(buf) {
2478
2597
  const marker = Buffer.from("QQAppId/", "utf-8");
2479
2598
  let pos = 0;
2480
2599
  while (pos < buf.length) {
2481
2600
  const idx = buf.indexOf(marker, pos);
2482
2601
  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");
2602
+ const str = readMarkerValue(buf, idx + marker.length);
2487
2603
  if (DIGITS_ONLY_RE.test(str)) return str;
2488
- pos = end + 1;
2604
+ pos = idx + marker.length + str.length + 1;
2489
2605
  }
2490
2606
  return null;
2491
2607
  }
2608
+ /** 读取标记后的 C 字符串(到首个 0 字节为止)。 */
2609
+ function readMarkerValue(buf, start) {
2610
+ let end = start;
2611
+ while (end < buf.length && buf[end] !== 0) end += 1;
2612
+ return buf.subarray(start, end).toString("utf-8");
2613
+ }
2492
2614
  /** Windows 兜底 appid / qua(major.node 解析失败时)。 */
2493
2615
  function fallbackAppidQua(fullVersion) {
2494
2616
  return {
@@ -2608,14 +2730,12 @@ function buildSessionConfig(options) {
2608
2730
  */
2609
2731
  /** 默认 engine 配置(KWINDOWS + 版本号,字段待首个真实联调确认)。 */
2610
2732
  function defaultEngineConfig(env, qqDataPath) {
2611
- let osVersion = process.platform;
2612
- if (process.platform === "win32") osVersion = "win32";
2613
2733
  return {
2614
2734
  base_path_prefix: env.dataDir ?? "",
2615
2735
  platform_type: PlatformType.KWINDOWS,
2616
2736
  app_type: 4,
2617
2737
  app_version: env.qqVersion ?? "",
2618
- os_version: osVersion,
2738
+ os_version: process.platform,
2619
2739
  use_xlog: false,
2620
2740
  qua: "",
2621
2741
  global_path_config: { desktopGlobalPath: qqDataPath ?? env.dataDir ?? "" },
@@ -2646,14 +2766,15 @@ function electronProcessType() {
2646
2766
  * 正确路径,无需此解析。
2647
2767
  */
2648
2768
  function resolveQqUserDataRoot(exports) {
2769
+ return extractUserDataRoot(exports) ?? join(homedir(), "Documents", "Tencent Files");
2770
+ }
2771
+ /** 从 QQ 官方 API 提取数据根(不可用返回 null,调用方回退默认)。 */
2772
+ function extractUserDataRoot(exports) {
2649
2773
  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
- }
2774
+ const raw = exports.NodeQQNTWrapperUtil?.get?.().getNTUserDataInfoConfig?.();
2775
+ if (typeof raw === "string" && raw.trim() !== "") return extractDataRoot(raw.trim());
2655
2776
  } catch {}
2656
- return join(homedir(), "Documents", "Tencent Files");
2777
+ return null;
2657
2778
  }
2658
2779
  /**
2659
2780
  * 从 loader 截获的 NAPI exports 创建 wrapper 上下文。
@@ -2689,27 +2810,37 @@ function initEngine(ctx, config) {
2689
2810
  * 返回带完整实现的实例。worker(utilityProcess)继承 QQ env 后同样有效。
2690
2811
  */
2691
2812
  function createSession(ctx) {
2692
- const X = ctx.exports;
2693
- const S = X.NodeIQQNTWrapperSession;
2694
- let session = null;
2813
+ const S = ctx.exports.NodeIQQNTWrapperSession;
2814
+ const session = createStartupSession(ctx) ?? getNTWrapperSession(S) ?? createNewSession(S) ?? new S();
2815
+ ctx.session = session;
2816
+ return session;
2817
+ }
2818
+ /** 步骤 1:StartupSessionWrapper.create()(建立启动 session,失败忽略)。 */
2819
+ function createStartupSession(ctx) {
2820
+ const Ssw = ctx.exports.NodeIQQNTStartupSessionWrapper;
2821
+ if (Ssw === void 0 || typeof Ssw.create !== "function") return null;
2695
2822
  try {
2696
- const Ssw = X.NodeIQQNTStartupSessionWrapper;
2697
- if (Ssw !== void 0 && typeof Ssw.create === "function") ctx.startupSession = Ssw.create();
2823
+ ctx.startupSession = Ssw.create();
2698
2824
  } catch {}
2825
+ return null;
2826
+ }
2827
+ /** 步骤 2:getNTWrapperSession("nt_1")(QQ 主 session,带 cpp_impl)。 */
2828
+ function getNTWrapperSession(S) {
2829
+ if (typeof S.getNTWrapperSession !== "function") return null;
2699
2830
  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();
2831
+ return S.getNTWrapperSession("nt_1");
2832
+ } catch {
2833
+ return null;
2834
+ }
2835
+ }
2836
+ /** 步骤 3:S.create() 回退。 */
2837
+ function createNewSession(S) {
2838
+ if (typeof S.create !== "function") return null;
2839
+ try {
2840
+ return S.create();
2707
2841
  } catch {
2708
- session = null;
2842
+ return null;
2709
2843
  }
2710
- if (session === null) session = new S();
2711
- ctx.session = session;
2712
- return session;
2713
2844
  }
2714
2845
  /** session 初始化(4 参全为 JS 对象,NAPI 自动转换)。 */
2715
2846
  function initSession(ctx, config, listener) {
@@ -2749,18 +2880,36 @@ function startNapuketto(options) {
2749
2880
  fullVersion: env?.qqVersion ?? "",
2750
2881
  buildVersion: env?.qqVersion ?? ""
2751
2882
  });
2883
+ const hasCapturedSession = captureSession(ctx, qqSession);
2752
2884
  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)));
2885
+ prepareSession(ctx, hasCapturedSession, isSelfHost);
2886
+ initEngine(ctx, resolveEngineConfig(ctx, env, engineConfig));
2756
2887
  resolveLoginService(ctx, qqLoginService);
2757
- if (qqSession !== void 0 && qqSession !== null) ctx.session = qqSession;
2758
- else if (!isSelfHost) createSession(ctx);
2888
+ finishSession(ctx, hasCapturedSession, isSelfHost, sessionConfig);
2889
+ return ctx;
2890
+ }
2891
+ /** engine 前 session 准备(自建宿主必须先建,顺序决定性)。 */
2892
+ function prepareSession(ctx, hasCaptured, isSelfHost) {
2893
+ if (!hasCaptured && isSelfHost) createSession(ctx);
2894
+ }
2895
+ /** engine 后收尾:非自建宿主补建 session;配置了 sessionConfig 则 init+start。 */
2896
+ function finishSession(ctx, hasCaptured, isSelfHost, sessionConfig) {
2897
+ if (!hasCaptured && !isSelfHost) createSession(ctx);
2759
2898
  if (sessionConfig !== void 0) {
2760
2899
  initSession(ctx, sessionConfig, createSessionListener());
2761
2900
  startSession(ctx);
2762
2901
  }
2763
- return ctx;
2902
+ }
2903
+ /** engine 配置:显式覆盖优先,否则默认(含 QQ 数据根 global 路径解析)。 */
2904
+ function resolveEngineConfig(ctx, env, engineConfig) {
2905
+ if (engineConfig !== void 0) return engineConfig;
2906
+ return defaultEngineConfig(env ?? {}, resolveBootQqGlobalPath(ctx));
2907
+ }
2908
+ /** 捕获 QQ 已 init 的 session(boot.cjs 拦截 `new` 窃取),返回是否捕获成功。 */
2909
+ function captureSession(ctx, qqSession) {
2910
+ if (qqSession === void 0 || qqSession === null) return false;
2911
+ ctx.session = qqSession;
2912
+ return true;
2764
2913
  }
2765
2914
  /**
2766
2915
  * 解析 QQ 真实数据目录的 global 路径(desktopGlobalPath 三要素之三)。
@@ -2785,12 +2934,16 @@ function resolveLoginService(ctx, qqLoginService) {
2785
2934
  ctx.loginService = qqLoginService;
2786
2935
  return;
2787
2936
  }
2937
+ ctx.loginService = getOrCreateLoginService(ctx.exports);
2938
+ }
2939
+ /** 单例 get() 优先,缺失回退 new(都失败返回 null)。 */
2940
+ function getOrCreateLoginService(exports) {
2788
2941
  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();
2942
+ const loginSvcCtor = exports.NodeIKernelLoginService;
2943
+ if (loginSvcCtor !== null && typeof loginSvcCtor.get === "function") return loginSvcCtor.get();
2944
+ return new exports.NodeIKernelLoginService();
2792
2945
  } catch {
2793
- ctx.loginService = null;
2946
+ return null;
2794
2947
  }
2795
2948
  }
2796
2949
  //#endregion
@@ -2870,18 +3023,7 @@ var NapukettoCore = class NapukettoCore {
2870
3023
  async login(opts) {
2871
3024
  const { wrapper } = this.ctx;
2872
3025
  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");
3026
+ this.initLoginConfig(wrapper, opts.appid);
2885
3027
  let loginResult;
2886
3028
  try {
2887
3029
  const quickOpts = {};
@@ -2899,6 +3041,25 @@ var NapukettoCore = class NapukettoCore {
2899
3041
  return loginResult;
2900
3042
  }
2901
3043
  /**
3044
+ * loginService.initConfig(worker/自建宿主需指定 QQ 真实数据路径)。
3045
+ * worker(utilityProcess)模式 + 自建宿主(标准 node)下 loginService 是
3046
+ * new 的,commonPath 必须指向 QQ 真实数据目录(数据根/nt_qq/global,
3047
+ * HANDOVER-V6 三要素之三)才能读到历史账号——cli 的 `.napuketto\default`
3048
+ * 读不到(getLoginList 空,P2-1 实测 2026-08-06)。
3049
+ * QQ 主进程(V1)模式 QQ 自己 initConfig 过正确路径,无需解析。
3050
+ */
3051
+ initLoginConfig(wrapper, appid) {
3052
+ const loginService = wrapper.loginService;
3053
+ if (loginService === null || typeof loginService.initConfig !== "function") {
3054
+ this.ctx.logger.warn("loginService 不可用,跳过 initConfig");
3055
+ return;
3056
+ }
3057
+ const commonPath = resolveCommonPath(this.ctx, wrapper);
3058
+ const loginCfg = buildLoginConfig(appid, wrapper.versionInfo.fullVersion, commonPath);
3059
+ loginService.initConfig(loginCfg);
3060
+ this.ctx.logger.info({ commonPath }, "loginService.initConfig OK");
3061
+ }
3062
+ /**
2902
3063
  * 替换当前 session(登录后 Proxy 捕获的 QQ 新实例替换自己 create 的无效实例)。
2903
3064
  * 在 QQ 主进程内由 boot.cjs 调用(登录成功后)。
2904
3065
  */
@@ -2912,6 +3073,22 @@ var NapukettoCore = class NapukettoCore {
2912
3073
  if (wrapper === null || wrapper.loginService === null) throw kernelError("wrapper/loginService 不可用,无法 QR 登录", "INVALID_STATE");
2913
3074
  const session = new QrLoginSession(wrapper.loginService);
2914
3075
  const qrPath = opts.qrCodePath ?? this.ctx.paths.file("cache", "qrcode.png");
3076
+ this.subscribeQrProgress(session, opts, qrPath);
3077
+ const startOpts = {};
3078
+ if (opts.quickUin !== void 0) startOpts.quickUin = opts.quickUin;
3079
+ session.start(startOpts);
3080
+ await waitQrLoggedIn(session);
3081
+ const self = session.selfInfo;
3082
+ if (self === null) throw kernelError("QR 登录成功但 selfInfo 为空", "INVALID_STATE");
3083
+ session.stop();
3084
+ return {
3085
+ uin: self.uin,
3086
+ uid: self.uid,
3087
+ nick: self.nick
3088
+ };
3089
+ }
3090
+ /** 订阅 QR 进度:二维码写盘 + 状态机经 onLoginProgress 转发。 */
3091
+ subscribeQrProgress(session, opts, qrPath) {
2915
3092
  session.onQrCode((qr) => {
2916
3093
  this.ctx.logger.warn(`请扫描二维码登录(保存: ${qrPath} | URL: ${qr.qrcodeUrl})`);
2917
3094
  if (qr.pngBase64 !== "") writeQrCodePng(qrPath, qr.pngBase64);
@@ -2924,28 +3101,6 @@ var NapukettoCore = class NapukettoCore {
2924
3101
  this.ctx.logger.info({ state }, "QR 登录状态");
2925
3102
  opts.onLoginProgress?.({ state });
2926
3103
  });
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
3104
  }
2950
3105
  /** 停止:日志收尾(资源清理留给 P2 常驻管理)。 */
2951
3106
  stop() {
@@ -2959,6 +3114,32 @@ function writeQrCodePng(filePath, pngBase64) {
2959
3114
  writeFileSync(filePath, Buffer.from(pngBase64, "base64"));
2960
3115
  } catch {}
2961
3116
  }
3117
+ /** 等待 QR 登录终态:logged_in resolve,failed reject。 */
3118
+ function waitQrLoggedIn(session) {
3119
+ return new Promise((resolve, reject) => {
3120
+ const offState = session.onStateChange((state) => {
3121
+ if (state === "logged_in") {
3122
+ offState();
3123
+ resolve();
3124
+ } else if (state === "failed") {
3125
+ offState();
3126
+ reject(kernelError("QR 登录失败", "NOT_LOGIN"));
3127
+ }
3128
+ });
3129
+ });
3130
+ }
3131
+ /**
3132
+ * loginService.initConfig 的 commonPath:
3133
+ * worker/自建宿主(非 browser)优先 QQ 真实数据目录(数据根/nt_qq/global),
3134
+ * 否则回退账号目录。
3135
+ */
3136
+ function resolveCommonPath(ctx, wrapper) {
3137
+ if (electronProcessType() !== "browser") {
3138
+ const root = resolveQqUserDataRoot(wrapper.exports);
3139
+ if (root !== null) return resolveQqGlobalPath(root);
3140
+ }
3141
+ return ctx.paths.accountDir;
3142
+ }
2962
3143
  //#endregion
2963
3144
  //#region src/event-channel.ts
2964
3145
  /**
@@ -3046,15 +3227,19 @@ var NTEventChannel = class {
3046
3227
  for (const anyHandler of this.anyHandlers) try {
3047
3228
  anyHandler(event, ...args);
3048
3229
  } catch (err) {
3049
- for (const errorHandler of this.errorHandlers) errorHandler(err);
3230
+ this.notifyError(err);
3050
3231
  }
3051
3232
  const listeners = this.emitter.listeners(event);
3052
3233
  for (const listener of listeners) try {
3053
3234
  listener(...args);
3054
3235
  } catch (err) {
3055
- for (const handler of this.errorHandlers) handler(err);
3236
+ this.notifyError(err);
3056
3237
  }
3057
3238
  }
3239
+ /** 通知所有错误处理器(单个异常不打断其他处理器)。 */
3240
+ notifyError(err) {
3241
+ for (const handler of this.errorHandlers) handler(err);
3242
+ }
3058
3243
  /** 订阅错误通知(监听器抛出的异常 / 其他内部错误)。 */
3059
3244
  onError(handler) {
3060
3245
  this.errorHandlers.add(handler);
@@ -3067,41 +3252,48 @@ var NTEventChannel = class {
3067
3252
  //#region src/wrapper/session-resolver.ts
3068
3253
  /** 从 getSessionIdList 的 Map 提取主 sessionId(nt_ 前缀优先)。 */
3069
3254
  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;
3255
+ return firstStringId(ids, (s) => s.startsWith("nt_")) ?? firstStringId(ids, () => true);
3256
+ }
3257
+ /** 遍历 Map 找第一个满足谓词的字符串 id(值优先,其次键)。 */
3258
+ function firstStringId(ids, pred) {
3259
+ for (const [k, v] of ids) {
3260
+ if (typeof v === "string" && pred(v)) return v;
3261
+ if (typeof k === "string" && pred(k)) return k;
3262
+ }
3075
3263
  return null;
3076
3264
  }
3077
3265
  /** 通过 getNTWrapperSession 拿主 session(内部辅助)。 */
3078
3266
  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();
3267
+ created.start?.();
3268
+ const ids = typeof created.getSessionIdList === "function" ? created.getSessionIdList() : null;
3082
3269
  if (!(ids instanceof Map)) return null;
3083
3270
  const mainId = findMainSessionId(ids);
3084
3271
  if (mainId === null) return null;
3085
3272
  const maybe = getNTWrapperSession(mainId);
3086
- if (maybe !== null && maybe !== void 0 && typeof maybe.getMsgService === "function") return maybe;
3087
- return null;
3273
+ return maybe !== null && maybe !== void 0 && typeof maybe.getMsgService === "function" ? maybe : null;
3088
3274
  }
3089
3275
  /**
3090
3276
  * 复用 QQ 已有 session(P1-4:QQ 已登录,直接拿单例避免重复 init)。
3091
3277
  * 优先 `NodeIQQNTWrapperSession.get()`;返回 null 时回退 createSession。
3092
3278
  */
3093
3279
  function getExistingSession(ctx) {
3280
+ const got = trySessionGet(ctx);
3281
+ if (got !== null) {
3282
+ ctx.session = got;
3283
+ return got;
3284
+ }
3285
+ return null;
3286
+ }
3287
+ /** NodeIQQNTWrapperSession.get() 单例(无效/异常返回 null)。 */
3288
+ function trySessionGet(ctx) {
3094
3289
  try {
3095
3290
  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;
3291
+ if (typeof S.get !== "function") return null;
3292
+ const got = S.get();
3293
+ return got && typeof got.getMsgService === "function" ? got : null;
3294
+ } catch {
3295
+ return null;
3296
+ }
3105
3297
  }
3106
3298
  /**
3107
3299
  * 复用 QQ 主 session(P1-4 实测链路):
@@ -3111,17 +3303,22 @@ function getExistingSession(ctx) {
3111
3303
  */
3112
3304
  function getMainSession(ctx) {
3113
3305
  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;
3306
+ return resolveMainFromStartup(ctx) ?? null;
3307
+ } catch {
3308
+ return null;
3309
+ }
3310
+ }
3311
+ /** startup.create → start → getSessionIdList → getNTWrapperSession 链路。 */
3312
+ function resolveMainFromStartup(ctx) {
3313
+ const startupRaw = ctx.exports.NodeIQQNTStartupSessionWrapper;
3314
+ const sessionCtor = ctx.exports.NodeIQQNTWrapperSession;
3315
+ if (typeof startupRaw.create !== "function" || typeof sessionCtor.getNTWrapperSession !== "function") return null;
3316
+ const created = startupRaw.create();
3317
+ if (!created) return null;
3318
+ const getSession = (id) => sessionCtor.getNTWrapperSession(id);
3319
+ const session = resolveMainSession(created, getSession);
3320
+ if (session !== null) ctx.session = session;
3321
+ return session;
3125
3322
  }
3126
3323
  //#endregion
3127
3324
  //#region src/wrapper/probe/probe-serialize.ts
@@ -3225,13 +3422,25 @@ function shapeKeyGetters(session, serviceMethods) {
3225
3422
  function listMethods(obj) {
3226
3423
  if (obj === null || obj === void 0) return [];
3227
3424
  const names = /* @__PURE__ */ new Set();
3425
+ collectPrototypeNames(obj, names);
3426
+ collectOwnNames(obj, names);
3427
+ return [...names].sort();
3428
+ }
3429
+ /** 收集原型链上的属性名(到 Object/Function.prototype 为止,去构造器)。 */
3430
+ function collectPrototypeNames(obj, names) {
3228
3431
  let proto = Object.getPrototypeOf(obj);
3229
- while (proto && proto !== Object.prototype && proto !== Function.prototype) {
3432
+ while (isProbeProto(proto)) {
3230
3433
  for (const name of Object.getOwnPropertyNames(proto)) if (name !== "constructor") names.add(name);
3231
3434
  proto = Object.getPrototypeOf(proto);
3232
3435
  }
3436
+ }
3437
+ /** 是否仍需遍历的原型(非 Object/Function.prototype 且非空)。 */
3438
+ function isProbeProto(proto) {
3439
+ return proto !== null && proto !== Object.prototype && proto !== Function.prototype;
3440
+ }
3441
+ /** 收集自有属性名(去构造器)。 */
3442
+ function collectOwnNames(obj, names) {
3233
3443
  for (const name of Object.getOwnPropertyNames(obj)) if (name !== "constructor") names.add(name);
3234
- return [...names].sort();
3235
3444
  }
3236
3445
  /** 安全调用:尝试调用方法拿返回值,失败记录错误(不中断探测)。 */
3237
3446
  function tryCall(obj, name) {
@@ -3278,41 +3487,52 @@ function probeExportCtors(ctx) {
3278
3487
  /** 探测 engine 关键调用(getDeviceInfo / getECDHService / getThirdPartySigService)。 */
3279
3488
  function probeEngineCalls(ctx) {
3280
3489
  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
- }
3490
+ for (const m of ENGINE_CALL_PROBES) out[m] = probeEngineCall(ctx, m);
3293
3491
  return out;
3294
3492
  }
3493
+ /** 探测单个 engine 方法调用(成功记录方法面/形状,失败记录错误)。 */
3494
+ function probeEngineCall(ctx, m) {
3495
+ const call = tryCall(ctx.engine, m);
3496
+ return call.ok ? {
3497
+ value: serialize(call.value),
3498
+ methods: call.value ? listMethods(call.value) : []
3499
+ } : { error: call.error ?? "null/undefined" };
3500
+ }
3501
+ /** engine 关键调用候选。 */
3502
+ const ENGINE_CALL_PROBES = [
3503
+ "getDeviceInfo",
3504
+ "getECDHService",
3505
+ "getThirdPartySigService",
3506
+ "readyToShow"
3507
+ ];
3295
3508
  /** 探测 LoginService 实例(QQ 已登录凭据入口)。 */
3296
3509
  function probeLoginService(ctx) {
3297
3510
  const ctor = ctx.exports.NodeIKernelLoginService;
3298
3511
  if (ctor === null || typeof ctor.get !== "function") return null;
3299
3512
  const inst = tryCall(ctor, "get");
3300
3513
  if (!(inst.ok && inst.value)) return { get: inst.error ?? "null/undefined" };
3301
- const out = {
3514
+ return {
3302
3515
  methods: listMethods(inst.value),
3303
- ownKeys: Object.getOwnPropertyNames(inst.value).slice(0, 30)
3516
+ ownKeys: Object.getOwnPropertyNames(inst.value).slice(0, 30),
3517
+ ...probeLoginGetters(inst.value)
3304
3518
  };
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);
3519
+ }
3520
+ /** 尝试关键登录信息 getter(无参调用,探测形状)。 */
3521
+ function probeLoginGetters(inst) {
3522
+ const out = {};
3523
+ for (const m of LOGIN_GETTER_PROBES) if (typeof inst[m] === "function") out[m] = tryShape(inst, m);
3314
3524
  return out;
3315
3525
  }
3526
+ /** 登录信息 getter 候选(无参调用,探测形状)。 */
3527
+ const LOGIN_GETTER_PROBES = [
3528
+ "getAccountInfo",
3529
+ "getLoginInfo",
3530
+ "getA2",
3531
+ "getTicket",
3532
+ "getUin",
3533
+ "getUid",
3534
+ "getSelfUin"
3535
+ ];
3316
3536
  //#endregion
3317
3537
  //#region src/wrapper/probe/probe.ts
3318
3538
  /**
@@ -3332,55 +3552,77 @@ function probeLoginService(ctx) {
3332
3552
  /** 枚举候选 sessionId(nt_0..nt_9 / gpro_0..gpro_9)找 QQ 已 init 的 session。 */
3333
3553
  function enumerateSessionIds(ctx) {
3334
3554
  const S = ctx.exports.NodeIQQNTWrapperSession;
3335
- const enumFn = S["getNTWrapperSession"];
3555
+ const enumFn = getNTWrapperSessionFn(ctx);
3556
+ if (enumFn === null) return {};
3336
3557
  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 {}
3558
+ for (let i = 0; i < 10; i += 1) {
3559
+ probeSessionId(enumFn, S, `nt_${i}`, out);
3560
+ probeSessionId(enumFn, S, `gpro_${i}`, out);
3561
+ }
3360
3562
  return out;
3361
3563
  }
3564
+ /** 探测单个 sessionId(getter 存在性 + getMsgService 调用结果)。 */
3565
+ function probeSessionId(enumFn, thisCtx, id, out) {
3566
+ try {
3567
+ const value = enumFn.call(thisCtx, id);
3568
+ if (!value) return;
3569
+ out[id] = probeSessionRecord(value);
3570
+ } catch {}
3571
+ }
3572
+ /** 探测单个 session 记录(getter 存在性 + msgService 调用结果 + 方法面)。 */
3573
+ function probeSessionRecord(value) {
3574
+ const rec = value;
3575
+ const msg = tryMsgService(rec, value);
3576
+ return {
3577
+ hasMsgServiceGetter: typeof rec["getMsgService"] === "function",
3578
+ msgServiceOk: msg.service !== null,
3579
+ msgServiceMethods: msg.service ? listMethods(msg.service).length : 0,
3580
+ msgError: msg.error,
3581
+ methods: listMethods(value)
3582
+ };
3583
+ }
3584
+ /** 尝试调用 getMsgService(失败记录错误,成功返回实例)。 */
3585
+ function tryMsgService(rec, thisCtx) {
3586
+ try {
3587
+ return {
3588
+ service: rec["getMsgService"].call(thisCtx) ?? null,
3589
+ error: null
3590
+ };
3591
+ } catch (e) {
3592
+ return {
3593
+ service: null,
3594
+ error: e instanceof Error ? e.message : String(e)
3595
+ };
3596
+ }
3597
+ }
3598
+ /** 取 NodeIQQNTWrapperSession.getNTWrapperSession 函数(无则 null)。 */
3599
+ function getNTWrapperSessionFn(ctx) {
3600
+ const fn = ctx.exports.NodeIQQNTWrapperSession["getNTWrapperSession"];
3601
+ return typeof fn === "function" ? fn : null;
3602
+ }
3362
3603
  /** 探测 startup 实例:start() + getSessionIdList(),提取 sessionIds(Map 值或键)。 */
3363
3604
  function probeStartupChain(created) {
3364
3605
  const started = tryCall(created, "start");
3365
3606
  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
3607
  return {
3372
3608
  start: started.ok ? { ok: true } : { error: started.error },
3373
3609
  createdGetSessionIdList: ids.ok ? { value: serialize(ids.value) } : { error: ids.error },
3374
- sessionIds
3610
+ sessionIds: extractSessionIds(ids)
3375
3611
  };
3376
3612
  }
3613
+ /** 从 getSessionIdList 结果提取 sessionIds(Map 值或键优先)。 */
3614
+ function extractSessionIds(ids) {
3615
+ if (!(ids.ok && ids.value instanceof Map)) return [];
3616
+ const out = [];
3617
+ for (const [k, v] of ids.value) out.push(typeof v === "string" ? v : String(k));
3618
+ return out;
3619
+ }
3377
3620
  /** 用真实 sessionId 取主 session(getNTWrapperSession(nt_x))并记录方法面。 */
3378
3621
  function probeSessionById(ctx, id) {
3379
- const S = ctx.exports.NodeIQQNTWrapperSession;
3380
- const fn = S["getNTWrapperSession"];
3381
- if (typeof fn !== "function") return null;
3622
+ const fn = getNTWrapperSessionFn(ctx);
3623
+ if (fn === null) return null;
3382
3624
  try {
3383
- const value = fn.call(S, id);
3625
+ const value = fn.call(ctx.exports.NodeIQQNTWrapperSession, id);
3384
3626
  return value ? {
3385
3627
  methods: listMethods(value),
3386
3628
  ownKeys: Object.getOwnPropertyNames(value)
@@ -3391,7 +3633,6 @@ function probeSessionById(ctx, id) {
3391
3633
  }
3392
3634
  /** 兜底:候选名字 getNTWrapperSession(无 Map 数据时)。 */
3393
3635
  function probeByNameFallback(ctx) {
3394
- const S = ctx.exports.NodeIQQNTWrapperSession;
3395
3636
  const candidates = [
3396
3637
  "main",
3397
3638
  "primary",
@@ -3400,17 +3641,22 @@ function probeByNameFallback(ctx) {
3400
3641
  ""
3401
3642
  ];
3402
3643
  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 ? {
3644
+ for (const name of candidates) getResults[name === "" ? "<empty>" : name] = probeSessionByName(ctx, name);
3645
+ return getResults;
3646
+ }
3647
+ /** 探测单个候选名(getNTWrapperSession(name),记录方法面或错误)。 */
3648
+ function probeSessionByName(ctx, name) {
3649
+ try {
3650
+ const fn = getNTWrapperSessionFn(ctx);
3651
+ if (fn === null) return null;
3652
+ const value = fn.call(ctx.exports.NodeIQQNTWrapperSession, name);
3653
+ return value ? {
3407
3654
  methods: listMethods(value),
3408
3655
  ownKeys: Object.getOwnPropertyNames(value)
3409
3656
  } : null;
3410
3657
  } catch (e) {
3411
- getResults[name === "" ? "<empty>" : name] = { error: e instanceof Error ? e.message : String(e) };
3658
+ return { error: e instanceof Error ? e.message : String(e) };
3412
3659
  }
3413
- return getResults;
3414
3660
  }
3415
3661
  /** 探测 startup session 链路:create() → start() → getSessionIdList → getNTWrapperSession。 */
3416
3662
  function probeStartup(ctx) {
@@ -3422,60 +3668,76 @@ function probeStartup(ctx) {
3422
3668
  methods: created.value ? listMethods(created.value) : [],
3423
3669
  value: serialize(created.value)
3424
3670
  } : { 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
- }
3671
+ if (created.ok && created.value) Object.assign(out, probeCreatedChain(ctx, created.value));
3436
3672
  out["enumSessionIds"] = enumerateSessionIds(ctx);
3437
3673
  if (out["sessionIds"] === void 0) out["getNTWrapperSession"] = probeByNameFallback(ctx);
3438
3674
  return out;
3439
3675
  }
3676
+ /** 探测 create 后的实例链:start / getSessionIdList / 主 session。 */
3677
+ function probeCreatedChain(ctx, created) {
3678
+ const chain = probeStartupChain(created);
3679
+ const out = {
3680
+ start: chain.start,
3681
+ createdGetSessionIdList: chain.createdGetSessionIdList,
3682
+ sessionIds: chain.sessionIds
3683
+ };
3684
+ const mainId = findMainProbeId(chain.sessionIds);
3685
+ if (mainId !== void 0) {
3686
+ out["mainSessionId"] = mainId;
3687
+ out["mainSession"] = probeSessionById(ctx, mainId);
3688
+ }
3689
+ return out;
3690
+ }
3691
+ /** 选主 sessionId(nt_ 前缀优先,否则第一个)。 */
3692
+ function findMainProbeId(sessionIds) {
3693
+ return sessionIds.find((id) => id.startsWith("nt_")) ?? sessionIds[0];
3694
+ }
3440
3695
  /** 探测 session 与 service 方法(优先复用 QQ 主 session)。 */
3441
3696
  function probeSession(ctx) {
3442
3697
  const out = {};
3443
- let session = null;
3698
+ const session = pickProbeSession(ctx, out);
3699
+ out["session"] = session ? {
3700
+ methods: listMethods(session),
3701
+ ownKeys: Object.getOwnPropertyNames(session)
3702
+ } : null;
3703
+ if (session !== null && session !== void 0) out["services"] = collectServiceGetters(session);
3704
+ return out;
3705
+ }
3706
+ /** 选探测目标 session:优先 QQ 主 session(getMainSession),回退 ctx.session。 */
3707
+ function pickProbeSession(ctx, out) {
3444
3708
  try {
3445
3709
  const main = getMainSession(ctx);
3446
3710
  if (main !== null) {
3447
- session = main;
3448
3711
  out["sessionSource"] = "mainSession(getNTWrapperSession)";
3712
+ return main;
3449
3713
  }
3450
3714
  } catch {}
3451
- if (session === null && ctx.session !== null) {
3452
- session = ctx.session;
3715
+ if (ctx.session !== null) {
3453
3716
  out["sessionSource"] = "own-create";
3717
+ return ctx.session;
3454
3718
  }
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;
3719
+ return null;
3720
+ }
3721
+ /** 收集 session 的 get*Service getter 方法面与返回形状。 */
3722
+ function collectServiceGetters(session) {
3723
+ const getters = listMethods(session).filter((m) => m.startsWith("get") && m.endsWith("Service"));
3724
+ const services = {};
3725
+ for (const getter of getters) services[getter] = probeServiceGetter(session, getter);
3726
+ return services;
3727
+ }
3728
+ /** 探测单个 getter:成功记录方法面/形状,失败记录错误。 */
3729
+ function probeServiceGetter(session, getter) {
3730
+ const call = tryCall(session, getter);
3731
+ if (!(call.ok && call.value)) return {
3732
+ error: call.error ?? "null/undefined",
3733
+ ok: call.ok
3734
+ };
3735
+ const methods = listMethods(call.value);
3736
+ return {
3737
+ methods,
3738
+ ownKeys: Object.getOwnPropertyNames(call.value).slice(0, 20),
3739
+ shapes: shapeKeyGetters(call.value, methods)
3740
+ };
3479
3741
  }
3480
3742
  /**
3481
3743
  * 探测 wrapper 运行时:dump session 方法与各 service 方法到 JSON。
@@ -3522,12 +3784,11 @@ function readVersionPackage(installDir, version) {
3522
3784
  if (!existsSync(pkgPath)) throw kernelError(`QQ 版本目录缺少 package.json: ${pkgPath}`, "NOT_FOUND");
3523
3785
  try {
3524
3786
  const raw = JSON.parse(readFileSync(pkgPath, "utf8"));
3525
- const result = {
3787
+ return {
3526
3788
  name: raw.name ?? "",
3527
- version: raw.version ?? ""
3789
+ version: raw.version ?? "",
3790
+ ...raw.buildVersion !== void 0 ? { buildVersion: raw.buildVersion } : {}
3528
3791
  };
3529
- if (raw.buildVersion !== void 0) result.buildVersion = raw.buildVersion;
3530
- return result;
3531
3792
  } catch (cause) {
3532
3793
  throw kernelError(`QQ 版本 package.json 解析失败: ${pkgPath}`, "INVALID_PARAM", { cause });
3533
3794
  }