@napuketto/kernel 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1515,6 +1515,72 @@ var GroupApi = class {
1515
1515
  } else res = await this.service.removeGroupEssence(param);
1516
1516
  checkLooseResult(label, res);
1517
1517
  }
1518
+ /**
1519
+ * 群公告列表(get_group_announcements;getGroupBulletin 返回形状待探测校准,
1520
+ * 兼容 result 数组 / result.bulletins 等宽松提取)。
1521
+ */
1522
+ async getGroupAnnouncements(groupCode) {
1523
+ let raw;
1524
+ try {
1525
+ raw = await this.service.getGroupBulletin(groupCode);
1526
+ } catch (e) {
1527
+ if (isKernelError(e)) throw e;
1528
+ throw kernelError(`getGroupBulletin 调用失败: ${errMsgString$3(e)}`, "UNKNOWN");
1529
+ }
1530
+ unwrap("getGroupBulletin", toResultNumber(raw.result), raw.errMsg);
1531
+ return extractBulletins(raw.result);
1532
+ }
1533
+ /**
1534
+ * 发群公告(send_group_announcement;imagePath 提供时先 uploadGroupBulletinPic
1535
+ * 再带 picId 发布——两步参数形状均待探测校准)。
1536
+ */
1537
+ async publishGroupAnnouncement(groupCode, content, imagePath) {
1538
+ let picId;
1539
+ if (imagePath !== void 0 && imagePath !== "") picId = await this.uploadBulletinPic(groupCode, imagePath);
1540
+ try {
1541
+ checkLooseResult("publishGroupBulletin", await this.service.publishGroupBulletin({
1542
+ groupCode,
1543
+ content,
1544
+ ...picId !== void 0 ? { picId } : {}
1545
+ }));
1546
+ } catch (e) {
1547
+ if (isKernelError(e)) throw e;
1548
+ throw kernelError(`publishGroupBulletin 调用失败: ${errMsgString$3(e)}`, "UNKNOWN");
1549
+ }
1550
+ }
1551
+ /** 删群公告(delete_group_announcement;参数待探测校准)。 */
1552
+ async deleteGroupAnnouncement(groupCode, announcementId) {
1553
+ try {
1554
+ checkLooseResult("deleteGroupBulletin", await this.service.deleteGroupBulletin(groupCode, announcementId));
1555
+ } catch (e) {
1556
+ if (isKernelError(e)) throw e;
1557
+ throw kernelError(`deleteGroupBulletin 调用失败: ${errMsgString$3(e)}`, "UNKNOWN");
1558
+ }
1559
+ }
1560
+ /** 专属头衔(set_group_member_special_title;setIdentityTitleInfo 参数待校准)。 */
1561
+ async setMemberSpecialTitle(groupCode, uid, title) {
1562
+ try {
1563
+ checkLooseResult("setIdentityTitleInfo", await this.service.setIdentityTitleInfo(groupCode, uid, title));
1564
+ } catch (e) {
1565
+ if (isKernelError(e)) throw e;
1566
+ throw kernelError(`setIdentityTitleInfo 调用失败: ${errMsgString$3(e)}`, "UNKNOWN");
1567
+ }
1568
+ }
1569
+ /** 群头像(set_group_avatar;GroupService.setHeader 参数待校准)。 */
1570
+ async setGroupAvatar(groupCode, imagePath) {
1571
+ try {
1572
+ checkLooseResult("setHeader", await this.service.setHeader(groupCode, imagePath));
1573
+ } catch (e) {
1574
+ if (isKernelError(e)) throw e;
1575
+ throw kernelError(`setHeader(group) 调用失败: ${errMsgString$3(e)}`, "UNKNOWN");
1576
+ }
1577
+ }
1578
+ /** 公告图片上传(uploadGroupBulletinPic 返回 picId 待校准,失败原样上抛)。 */
1579
+ async uploadBulletinPic(groupCode, imagePath) {
1580
+ const res = await this.service.uploadGroupBulletinPic(groupCode, imagePath);
1581
+ checkLooseResult("uploadGroupBulletinPic", res);
1582
+ return typeof res?.picId === "string" ? res.picId : "";
1583
+ }
1518
1584
  /** @all 剩余次数(get_group_at_all_remain)。 */
1519
1585
  async getGroupRemainAtTimes(groupCode) {
1520
1586
  const raw = await this.service.getGroupRemainAtTimes(groupCode);
@@ -1526,6 +1592,29 @@ var GroupApi = class {
1526
1592
  };
1527
1593
  }
1528
1594
  };
1595
+ /** result 字段宽松取数(数字直接用,缺省 0 = 成功语义)。 */
1596
+ function toResultNumber(result) {
1597
+ return typeof result === "number" ? result : 0;
1598
+ }
1599
+ /** 公告列表宽松提取(result 数组 / { bulletins } / { result: 数组 } 兼容,形状待校准)。 */
1600
+ function extractBulletins(result) {
1601
+ const candidates = [result];
1602
+ if (result !== null && typeof result === "object") {
1603
+ const rec = result;
1604
+ for (const key of [
1605
+ "bulletins",
1606
+ "announcementList",
1607
+ "result",
1608
+ "list"
1609
+ ]) candidates.push(rec[key]);
1610
+ }
1611
+ for (const candidate of candidates) if (Array.isArray(candidate)) return candidate.filter((item) => item !== null && typeof item === "object");
1612
+ return [];
1613
+ }
1614
+ /** 未知异常 → 消息字符串。 */
1615
+ function errMsgString$3(e) {
1616
+ return e instanceof Error ? e.message : String(e);
1617
+ }
1529
1618
  /**
1530
1619
  * 从 getGroupDetailInfo 返回提取群详情(兼容三种形状):
1531
1620
  * - result 为对象 → 详情直接在其中(最常见)
@@ -1699,6 +1788,28 @@ function sleep$1(ms) {
1699
1788
  setTimeout(resolve, ms);
1700
1789
  });
1701
1790
  }
1791
+ /** 未知异常 → 消息字符串。 */
1792
+ function errMsgString$2(e) {
1793
+ return e instanceof Error ? e.message : String(e);
1794
+ }
1795
+ /** 语音下载完成判定:有路径且(传输完成或末次轮询)才返回元素,否则 null。 */
1796
+ function pttDownloadResult(latest, isLastPoll) {
1797
+ if (latest === void 0) return null;
1798
+ const hasPath = latest.filePath !== void 0 && latest.filePath !== "";
1799
+ const done = latest.transferStatus === 2 || latest.transferStatus === 4;
1800
+ return hasPath && (done || isLastPoll) ? latest : null;
1801
+ }
1802
+ /** 收藏表情项 → url(字段名待校准,url/imageUrl/emojiUrl 逐个兜底)。 */
1803
+ function extractFavEmojiUrls(list) {
1804
+ const out = [];
1805
+ for (const item of list) {
1806
+ if (item === null || typeof item !== "object") continue;
1807
+ const rec = item;
1808
+ const url = rec["url"] ?? rec["imageUrl"] ?? rec["emojiUrl"];
1809
+ if (typeof url === "string" && url !== "") out.push(url);
1810
+ }
1811
+ return out;
1812
+ }
1702
1813
  /** 读文件 stat(富媒体发送预处理;文件不存在抛 INVALID_PARAM)。 */
1703
1814
  async function statFile(path, label = "图片") {
1704
1815
  try {
@@ -1808,6 +1919,9 @@ var MsgApi = class {
1808
1919
  case "video":
1809
1920
  out.push(await this.prepareVideoElement(el));
1810
1921
  break;
1922
+ case "file":
1923
+ out.push(await this.prepareFileElement(el));
1924
+ break;
1811
1925
  default: out.push(...toSendElements([el]));
1812
1926
  }
1813
1927
  return out;
@@ -1928,6 +2042,28 @@ var MsgApi = class {
1928
2042
  };
1929
2043
  }
1930
2044
  /**
2045
+ * FILE 元素预处理(P2-2 预留口子,upload_private/group_file 落地):
2046
+ * pic/ptt/video 同款 NapCat 式链路——md5 → getRichMediaFilePathForGuild(FILE)
2047
+ * → copyFile → 完整 fileElement。链路模式已验证(三类媒体同构);fileElement
2048
+ * 字段值(fileSize 字符串形态、fileUuid 空串等)待探测校准。
2049
+ */
2050
+ async prepareFileElement(el) {
2051
+ const file = await statFile(el.path, "文件");
2052
+ const md5 = await hashFile(el.path, "文件");
2053
+ const fileName = el.name !== void 0 && el.name !== "" ? el.name : basename(el.path);
2054
+ const relPath = await this.placeMediaFile(el.path, ElementType.FILE, md5, fileName);
2055
+ return {
2056
+ elementType: ElementType.FILE,
2057
+ elementId: "",
2058
+ fileElement: {
2059
+ fileName,
2060
+ filePath: relPath,
2061
+ fileSize: String(file.size),
2062
+ fileUuid: ""
2063
+ }
2064
+ };
2065
+ }
2066
+ /**
1931
2067
  * 富媒体文件放置(PIC/PTT/VIDEO 共用):md5 + 文件名 → QQ 内部目标路径
1932
2068
  * (getRichMediaFilePathForGuild)→ util.copyFile 放置 → 返回目标路径。
1933
2069
  *
@@ -2021,31 +2157,72 @@ var MsgApi = class {
2021
2157
  * 调用方),完全无路径抛 NOT_FOUND。
2022
2158
  */
2023
2159
  async downloadPtt(msgId, target) {
2024
- const el = findPttElement(await this.fetchMsgsByMsgId(target, [msgId]));
2025
- const ptt = el?.pttElement;
2026
- if (el === null || ptt === void 0) throw kernelError("消息中不包含语音", "NOT_FOUND");
2027
- const elemId = el.elementId ?? "";
2160
+ const found = await this.pttElementOf(msgId, target);
2161
+ if (found === null) throw kernelError("消息中不包含语音", "NOT_FOUND");
2028
2162
  await this.service.downloadRichMedia({
2029
2163
  msgId,
2030
- elemId,
2164
+ elemId: found.elemId,
2031
2165
  chatType: target.chatType,
2032
2166
  downloadType: DOWNLOAD_TYPE_COMMON,
2033
2167
  thumbSize: THUMB_SIZE_NONE
2034
2168
  });
2035
2169
  for (let i = 0; i < PTT_POLL_TIMES; i++) {
2036
2170
  await sleep$1(PTT_POLL_INTERVAL_MS);
2037
- const latest = findPttElement(await this.fetchMsgsByMsgId(target, [msgId]))?.pttElement;
2038
- if (latest === void 0) continue;
2039
- const done = latest.transferStatus === 2 || latest.transferStatus === 4;
2040
- if (latest.filePath !== void 0 && latest.filePath !== "" && done) return latest;
2041
- if (i === 33 && latest.filePath !== void 0 && latest.filePath !== "") return latest;
2171
+ const result = pttDownloadResult(findPttElement(await this.fetchMsgsByMsgId(target, [msgId]))?.pttElement, i === 33);
2172
+ if (result !== null) return result;
2042
2173
  }
2043
2174
  throw kernelError("语音下载未完成(超时)", "TIMEOUT");
2044
2175
  }
2176
+ /** 取消息内语音元素(无则 null;附 elemId 供主动下载)。 */
2177
+ async pttElementOf(msgId, target) {
2178
+ const el = findPttElement(await this.fetchMsgsByMsgId(target, [msgId]));
2179
+ const ptt = el?.pttElement;
2180
+ if (el === null || ptt === void 0) return null;
2181
+ return {
2182
+ elemId: el.elementId ?? "",
2183
+ ptt
2184
+ };
2185
+ }
2045
2186
  /** 发送输入状态(set_input_status;eventType=1 输入中,0 停止)。 */
2046
2187
  async setInputStatus(target, eventType) {
2047
2188
  await this.service.sendShowInputStatusReq(target.chatType, eventType, target.peerUid);
2048
2189
  }
2190
+ /** 戳一戳发送(send_friend/group_nudge;参数形状待探测校准,失败抛 UNKNOWN)。 */
2191
+ async sendNudge(target, opts) {
2192
+ const param = {
2193
+ receiverUid: opts.receiverUid,
2194
+ callerUid: opts.callerUid,
2195
+ ...target.chatType === ChatType.GROUP ? { confId: target.peerUid } : {}
2196
+ };
2197
+ try {
2198
+ checkLooseResult("sendNudge", await this.service.sendNudge(param));
2199
+ } catch (e) {
2200
+ if (isKernelError(e)) throw e;
2201
+ throw kernelError(`sendNudge 调用失败: ${errMsgString$2(e)}`, "UNKNOWN");
2202
+ }
2203
+ }
2204
+ /** 会话置顶(set_peer_pin;setContactLocalTop 参数待探测校准)。 */
2205
+ async setContactTop(target, isTop) {
2206
+ try {
2207
+ checkLooseResult("setContactLocalTop", await this.service.setContactLocalTop(target, isTop));
2208
+ } catch (e) {
2209
+ if (isKernelError(e)) throw e;
2210
+ throw kernelError(`setContactLocalTop 调用失败: ${errMsgString$2(e)}`, "UNKNOWN");
2211
+ }
2212
+ }
2213
+ /** 收藏表情列表(get_custom_face_url_list;queryFavEmojiByDesc 形状待校准)。 */
2214
+ async queryFavEmojis() {
2215
+ let raw;
2216
+ try {
2217
+ raw = await this.service.queryFavEmojiByDesc("");
2218
+ } catch (e) {
2219
+ throw kernelError(`queryFavEmojiByDesc 调用失败: ${errMsgString$2(e)}`, "UNKNOWN");
2220
+ }
2221
+ checkLooseResult("queryFavEmojiByDesc", raw);
2222
+ const list = raw.emojiInfoList;
2223
+ if (!Array.isArray(list)) return [];
2224
+ return extractFavEmojiUrls(list);
2225
+ }
2049
2226
  /**
2050
2227
  * 发送合并转发(send_group/private_forward_msg)。
2051
2228
  * buildMultiForwardMsg 组装 MULTI_FORWARD 元素 → 直接作 sendMsg 元素发送。
@@ -2212,6 +2389,76 @@ var ProfileLikeApi = class {
2212
2389
  }
2213
2390
  };
2214
2391
  //#endregion
2392
+ //#region src/apis/recent-contact.ts
2393
+ /**
2394
+ * RecentContactApi:会话列表语义化 API(P2 批次二,get_peer_pins 数据源)
2395
+ *
2396
+ * 2026-10-31 实机探测确认 getRecentContactService 有 getRecentContactList /
2397
+ * setContactListTop 等方法;返回形状待探测校准(宽松提取)。
2398
+ * 置顶数据源:isTop 过滤(ChatType 区分 friend/group 场景)。
2399
+ */
2400
+ /** 会话列表拉取条数(覆盖置顶集合足够大;参数值待探测校准)。 */
2401
+ const RECENT_CONTACT_FETCH_COUNT = 200;
2402
+ /** 会话列表 API:从 session 拿 recent contact service,包装成语义化方法。 */
2403
+ var RecentContactApi = class {
2404
+ service;
2405
+ constructor(session) {
2406
+ const service = session.getRecentContactService();
2407
+ if (service === null || service === void 0) throw kernelError("getRecentContactService() 返回空(session 未 init)", "INVALID_STATE");
2408
+ this.service = service;
2409
+ }
2410
+ /** 置顶会话列表(get_peer_pins;getRecentContactList 返回形状待探测校准)。 */
2411
+ async getPinnedContacts() {
2412
+ let raw;
2413
+ try {
2414
+ raw = await this.service.getRecentContactList({ count: RECENT_CONTACT_FETCH_COUNT });
2415
+ } catch (e) {
2416
+ if (isKernelError(e)) throw e;
2417
+ throw kernelError(`getRecentContactList 调用失败: ${errMsgString$1(e)}`, "UNKNOWN");
2418
+ }
2419
+ checkLooseResult("getRecentContactList", raw);
2420
+ return extractPinnedContacts(raw);
2421
+ }
2422
+ };
2423
+ /** 从返回形状提取置顶会话(result 数组 / 顶层 contacts / 嵌套容器兼容,待校准)。 */
2424
+ function extractPinnedContacts(raw) {
2425
+ const candidates = [raw.result, raw];
2426
+ for (const container of [raw.result, raw]) {
2427
+ if (container === null || typeof container !== "object") continue;
2428
+ const rec = container;
2429
+ for (const key of [
2430
+ "contacts",
2431
+ "data",
2432
+ "result",
2433
+ "list"
2434
+ ]) candidates.push(rec[key]);
2435
+ }
2436
+ for (const candidate of candidates) {
2437
+ if (!Array.isArray(candidate)) continue;
2438
+ return candidate.flatMap((item) => pinnedOf(item));
2439
+ }
2440
+ return [];
2441
+ }
2442
+ /** 单条会话 → 置顶项(isTop 为真且场景可判定才产出)。 */
2443
+ function pinnedOf(item) {
2444
+ if (item === null || typeof item !== "object") return [];
2445
+ const contact = item;
2446
+ if (contact.isTop !== true || typeof contact.peerUid !== "string") return [];
2447
+ if (contact.chatType === ChatType.GROUP) return [{
2448
+ peerUid: contact.peerUid,
2449
+ scene: "group"
2450
+ }];
2451
+ if (contact.chatType === ChatType.C2C) return [{
2452
+ peerUid: contact.peerUid,
2453
+ scene: "friend"
2454
+ }];
2455
+ return [];
2456
+ }
2457
+ /** 未知异常 → 消息字符串。 */
2458
+ function errMsgString$1(e) {
2459
+ return e instanceof Error ? e.message : String(e);
2460
+ }
2461
+ //#endregion
2215
2462
  //#region src/apis/richmedia.ts
2216
2463
  /**
2217
2464
  * RichMediaApi:富媒体语义化 API(ADR-009 统一错误语义,P2-14)
@@ -2257,10 +2504,11 @@ var RichMediaApi = class {
2257
2504
  showOnlinedocFolder: HIDE_ONLINEDOC_FOLDER
2258
2505
  };
2259
2506
  const raw = await this.service.getGroupFileList(groupCode, params);
2260
- if (raw.result !== 0) unwrap("getGroupFileList", raw.result, raw.errMsg);
2507
+ checkLooseResult("getGroupFileList", raw);
2261
2508
  const list = extractFileList(raw.result);
2262
- if (parentFolderId === "") return list;
2263
- return list.filter((item) => item.fileInfo !== void 0 && item.fileInfo.parent === parentFolderId || item.folderInfo !== void 0 && item.folderInfo.folderUid === parentFolderId);
2509
+ const items = list.length > 0 ? list : extractFileList(raw);
2510
+ if (parentFolderId === "") return items;
2511
+ return items.filter((item) => item.fileInfo !== void 0 && item.fileInfo.parent === parentFolderId || item.folderInfo !== void 0 && item.folderInfo.folderUid === parentFolderId);
2264
2512
  }
2265
2513
  /** 群文件系统信息(get_group_file_system_info;getGroupFileList 的 groupSpaceResult)。 */
2266
2514
  async getGroupFileSystemInfo(groupCode) {
@@ -2309,6 +2557,26 @@ var RichMediaApi = class {
2309
2557
  const raw = await this.service.moveGroupFile(groupCode, [GROUP_FILE_BIZ_TYPE], fileIds, "", targetFolderId);
2310
2558
  if (raw.result !== 0) unwrap("moveGroupFile", raw.result, raw.errMsg);
2311
2559
  }
2560
+ /** 重命名群文件夹(rename_group_folder;形态镜像 renameGroupFile,待校准)。 */
2561
+ async renameGroupFolder(groupCode, folderId, newName) {
2562
+ await this.service.renameGroupFolder(groupCode, GROUP_FILE_BIZ_TYPE, folderId, newName, "");
2563
+ }
2564
+ /**
2565
+ * 群文件下载链接(get_group_file_download_url):getGroupFileList 根列表按
2566
+ * fileId 查找取 url 字段(字段待实机确认)。已知差异:仅根目录可达——
2567
+ * 子目录文件需 parentFolderId 逐层定位,暂不支持。
2568
+ */
2569
+ async getGroupFileDownloadUrl(groupCode, fileId) {
2570
+ const items = await this.getGroupFileList(groupCode);
2571
+ for (const item of items) {
2572
+ const info = item.fileInfo;
2573
+ if (info !== void 0 && info.fileId === fileId) {
2574
+ if (typeof info.url === "string" && info.url !== "") return info.url;
2575
+ throw kernelError(`群文件 ${fileId} 无下载链接(url 字段缺失,待实机校准)`, "NOT_FOUND");
2576
+ }
2577
+ }
2578
+ throw kernelError(`群文件 ${fileId} 不在根目录或不存在(仅支持根目录查找)`, "NOT_FOUND");
2579
+ }
2312
2580
  /** 转存群文件(trans_group_file)。 */
2313
2581
  async transGroupFile(groupCode, fileId) {
2314
2582
  const raw = await this.service.transGroupFile(groupCode, fileId);
@@ -4420,7 +4688,6 @@ function resolveMainFromStartup(ctx) {
4420
4688
  *
4421
4689
  * - serialize:深度受限序列化(BigInt 转字符串,Map/Set/Promise 展开,循环引用防护)
4422
4690
  * - tryShape:解析 service getter 返回形状
4423
- * - shapeKeyGetters:探测 service 关键数据 getter 返回形状
4424
4691
  */
4425
4692
  /** 序列化深度上限(循环引用/深层对象防护)。 */
4426
4693
  const MAX_SERIALIZE_DEPTH = 4;
@@ -4494,15 +4761,6 @@ function tryShape(session, getter) {
4494
4761
  return "[error]";
4495
4762
  }
4496
4763
  }
4497
- /** 数据 getter 方法名模式(探测返回形状用)。 */
4498
- const DATA_GETTER_RE = /^(get|query|fetch|load)/i;
4499
- /** 探测 service 的关键数据 getter 返回形状(取前几个无参调用)。 */
4500
- function shapeKeyGetters(session, serviceMethods) {
4501
- const out = {};
4502
- const candidates = serviceMethods.filter((m) => DATA_GETTER_RE.test(m) && !m.endsWith("Listener"));
4503
- for (const m of candidates.slice(0, 8)) out[m] = tryShape(session, m);
4504
- return out;
4505
- }
4506
4764
  //#endregion
4507
4765
  //#region src/wrapper/probe/probe-utils.ts
4508
4766
  /**
@@ -4560,29 +4818,6 @@ function tryCall(obj, name) {
4560
4818
  function isServiceGetter(name) {
4561
4819
  return name.startsWith("get") && name.endsWith("Service");
4562
4820
  }
4563
- /** 非空服务实例计数(逐个调用 get*Service 数非空返回;候选可用度评分)。
4564
- * 自建宿主下 QQ 主 session 未被 init,get*Service 恒 null(2026-10-31 实测
4565
- * napuketto-probe-late.json),kernel 自建 session 服务可用——主目标按此评分选取。 */
4566
- function countNonNullServices(session) {
4567
- if (session === null || session === void 0) return 0;
4568
- let count = 0;
4569
- for (const name of listMethods(session)) {
4570
- if (!isServiceGetter(name)) continue;
4571
- const call = tryCall(session, name);
4572
- if (call.ok && call.value !== null && call.value !== void 0) count += 1;
4573
- }
4574
- return count;
4575
- }
4576
- /** 主探测目标选取:服务实例最多者优先,评分并列保持候选顺序(主 session 在前)。 */
4577
- function pickPrimaryCandidate(candidates) {
4578
- let best = candidates[0];
4579
- for (const candidate of candidates.slice(1)) if (best !== void 0 && candidate.serviceNonNull > best.serviceNonNull) best = candidate;
4580
- return best ?? {
4581
- source: "own-create",
4582
- session: null,
4583
- serviceNonNull: 0
4584
- };
4585
- }
4586
4821
  //#endregion
4587
4822
  //#region src/wrapper/probe/probe-services.ts
4588
4823
  /** 探测关键 export 构造器方法(get/create/getNTWrapperSession 等)。 */
@@ -4833,89 +5068,76 @@ function probeCreatedChain(ctx, created) {
4833
5068
  function findMainProbeId(sessionIds) {
4834
5069
  return sessionIds.find((id) => id.startsWith("nt_")) ?? sessionIds[0];
4835
5070
  }
4836
- /** 探测 session 与 service 方法(候选中选服务实例最多者为主目标)。
4837
- * 自建宿主下 QQ 主 session 未被 init(get*Service 恒 null,2026-10-31 实测
4838
- * napuketto-probe-late.json 全服务空),kernel 自建 session 已带 adapter init
4839
- * 服务可用——主目标按 countNonNullServices 评分选取,并列保持主 session 优先。 */
4840
- function probeSession(ctx) {
4841
- const out = {};
4842
- const candidates = collectProbeCandidates(ctx).map(scoreCandidate);
4843
- const primary = pickPrimaryCandidate(candidates);
4844
- out["sessionSource"] = primary.source;
4845
- out["session"] = sessionFace(primary.session);
4846
- if (primary.session !== null && primary.session !== void 0) out["services"] = collectServiceGetters(primary.session);
4847
- if (candidates.length > 1) out["sessionAlternates"] = alternateFaces(candidates, primary);
4848
- return out;
4849
- }
4850
- /** 候选评分(非空服务实例数)。 */
4851
- function scoreCandidate(candidate) {
4852
- return {
4853
- ...candidate,
4854
- serviceNonNull: countNonNullServices(candidate.session)
4855
- };
4856
- }
4857
- /** 落选候选的概要面(方法数 + 评分,诊断用)。 */
4858
- function alternateFaces(candidates, primary) {
4859
- return candidates.filter((candidate) => candidate !== primary).map((candidate) => ({
4860
- source: candidate.source,
4861
- methodCount: listMethods(candidate.session).length,
4862
- serviceNonNull: candidate.serviceNonNull
4863
- }));
4864
- }
4865
- /** 收集候选 session:kernel 自建 session + QQ 主 session(若可得)。
5071
+ /** 探测 session 与 service 方法名面。
5072
+ * 主目标 = ctx.session 快照(kernel 自建就绪 session),缺省回退 QQ 主 session;
5073
+ * 自建宿主下主 session 未被 init(get*Service 恒 null,2026-10-31 实测)。
4866
5074
  * ⚠️ 先快照 ctx.session 再枚举主 session——getMainSession 有 P1-4 遗留副作用
4867
- * (resolveMainFromStartup 把空壳主 session 写回 ctx.session,session-resolver.ts),
4868
- * 后读会把就绪的自建 session 读成空壳(2026-10-31 实测:候选退化为单主 session)。 */
4869
- function collectProbeCandidates(ctx) {
5075
+ * (resolveMainFromStartup 把空壳主 session 写回 ctx.session,session-resolver.ts)。
5076
+ * ⚠️ names-only:不对服务实例做无参 shape 调用——活 session 上无参调原生方法
5077
+ * = 访问违例 0xC0000005(2026-10-31 实测崩溃,tryShape 捕不住原生段错误)。 */
5078
+ /** 非空判定(null / undefined 均视为缺省)。 */
5079
+ function hasValue(value) {
5080
+ return value !== null && value !== void 0;
5081
+ }
5082
+ function probeSession(ctx, persist) {
5083
+ const out = {};
4870
5084
  const own = ctx.session;
4871
- const candidates = [];
4872
- pushMainCandidate(ctx, candidates);
4873
- pushOwnCandidate(own, candidates);
4874
- return candidates;
5085
+ const main = tryMainSession(ctx);
5086
+ const primary = hasValue(own) ? own : main;
5087
+ out["sessionSource"] = hasValue(own) ? "own-create" : "mainSession";
5088
+ out["session"] = sessionFace(primary);
5089
+ assignServices(out, primary, persist);
5090
+ assignAlternates(out, own, main);
5091
+ return out;
4875
5092
  }
4876
- /** 并入 QQ 主 session 候选(枚举失败静默跳过)。 */
4877
- function pushMainCandidate(ctx, out) {
5093
+ /** 并入主目标的服务方法名面(主目标缺省时跳过)。 */
5094
+ function assignServices(out, primary, persist) {
5095
+ if (!hasValue(primary)) return;
5096
+ out["services"] = collectServiceGetters(primary, persist);
5097
+ }
5098
+ /** 并入落选候选概要(仅主目标为自建 session 时记录主 session 方法数,诊断用)。 */
5099
+ function assignAlternates(out, own, main) {
5100
+ if (!hasValue(own) || !hasValue(main)) return;
5101
+ out["sessionAlternates"] = [{
5102
+ source: "mainSession(getNTWrapperSession)",
5103
+ methodCount: listMethods(main).length
5104
+ }];
5105
+ }
5106
+ /** QQ 主 session(getMainSession 有写回 ctx.session 的副作用——调用前须已快照)。 */
5107
+ function tryMainSession(ctx) {
4878
5108
  try {
4879
- const main = getMainSession(ctx);
4880
- if (main !== null) out.push({
4881
- source: "mainSession(getNTWrapperSession)",
4882
- session: main
4883
- });
4884
- } catch {}
4885
- }
4886
- /** 并入 kernel 自建 session 候选(与已有候选同一实例时去重)。 */
4887
- function pushOwnCandidate(own, out) {
4888
- if (own === null || own === void 0) return;
4889
- if (out.some((candidate) => candidate.session === own)) return;
4890
- out.push({
4891
- source: "own-create",
4892
- session: own
4893
- });
5109
+ return getMainSession(ctx);
5110
+ } catch {
5111
+ return null;
5112
+ }
4894
5113
  }
4895
- /** 收集 session 的 get*Service getter 方法面与返回形状。 */
4896
- function collectServiceGetters(session) {
4897
- const getters = listMethods(session).filter((m) => m.startsWith("get") && m.endsWith("Service"));
5114
+ /** 收集 session 的 get*Service 方法名面(names-only,不做 shape 无参调用)。
5115
+ * 每个服务完成即 persist 一次——活 session 上个别 getter 若触发原生崩溃,
5116
+ * 文件里留下最后一个成功项,精确定位肇事 getter。 */
5117
+ function collectServiceGetters(session, persist) {
4898
5118
  const services = {};
4899
- for (const getter of getters) services[getter] = probeServiceGetter(session, getter);
5119
+ for (const getter of listMethods(session).filter(isServiceGetter)) {
5120
+ services[getter] = probeServiceGetter(session, getter);
5121
+ persist?.();
5122
+ }
4900
5123
  return services;
4901
5124
  }
4902
- /** 探测单个 getter:成功记录方法面/形状,失败记录错误。 */
5125
+ /** 探测单个 getter:成功记录方法名面(不调用服务方法),失败记录错误。 */
4903
5126
  function probeServiceGetter(session, getter) {
4904
5127
  const call = tryCall(session, getter);
4905
5128
  if (!(call.ok && call.value)) return {
4906
5129
  error: call.error ?? "null/undefined",
4907
5130
  ok: call.ok
4908
5131
  };
4909
- const methods = listMethods(call.value);
4910
5132
  return {
4911
- methods,
4912
- ownKeys: Object.getOwnPropertyNames(call.value).slice(0, 20),
4913
- shapes: shapeKeyGetters(call.value, methods)
5133
+ methods: listMethods(call.value),
5134
+ ownKeys: Object.getOwnPropertyNames(call.value).slice(0, 20)
4914
5135
  };
4915
5136
  }
4916
5137
  /**
4917
- * 探测 wrapper 运行时:dump session 方法与各 service 方法到 JSON。
5138
+ * 探测 wrapper 运行时:dump session 方法与各 service 方法名到 JSON。
4918
5139
  * 返回探测结果对象;同时写文件(NAPUTO_CFG_DIR/<file>,默认 napuketto-probe.json)。
5140
+ * 服务枚举逐项增量落盘(persist)——活 session 上原生崩溃时文件保留断点。
4919
5141
  */
4920
5142
  function probeRuntime(ctx, file = "napuketto-probe.json") {
4921
5143
  const result = {
@@ -4925,9 +5147,15 @@ function probeRuntime(ctx, file = "napuketto-probe.json") {
4925
5147
  exportCtors: probeExportCtors(ctx),
4926
5148
  engineCalls: probeEngineCalls(ctx),
4927
5149
  loginService: probeLoginService(ctx),
4928
- startup: probeStartup(ctx),
4929
- ...probeSession(ctx)
5150
+ startup: probeStartup(ctx)
4930
5151
  };
5152
+ const persist = () => writeProbeFile(result, file);
5153
+ Object.assign(result, probeSession(ctx, persist));
5154
+ writeProbeFile(result, file);
5155
+ return result;
5156
+ }
5157
+ /** 探测结果落盘(增量与最终共用;最终写入后记 probeFile 路径)。 */
5158
+ function writeProbeFile(result, file) {
4931
5159
  try {
4932
5160
  const dir = process.env["NAPUTO_CFG_DIR"] ?? process.cwd();
4933
5161
  mkdirSync(dir, { recursive: true });
@@ -4935,9 +5163,8 @@ function probeRuntime(ctx, file = "napuketto-probe.json") {
4935
5163
  writeFileSync(full, JSON.stringify(result, null, 2), "utf8");
4936
5164
  result["probeFile"] = full;
4937
5165
  } catch (e) {
4938
- result["probeFileError"] = e instanceof Error ? e.message : String(e);
5166
+ result["probeFileError"] = errMsgString(e);
4939
5167
  }
4940
- return result;
4941
5168
  }
4942
5169
  //#endregion
4943
5170
  //#region src/wrapper/wrapper-version.ts
@@ -4997,6 +5224,6 @@ function listQQVersions(installDir) {
4997
5224
  }).sort().reverse();
4998
5225
  }
4999
5226
  //#endregion
5000
- export { BuddyCache, ChatType, ConfigBase, ConfigError, DEFAULT_DATA_ROOT_NAME, EXIT_CONFIG_INVALID, ElementType, FriendApi, FriendBridge, GrayTipSubType, GroupApi, GroupBridge, GroupCache, GroupListUpdateType, GroupNotifyApi, GroupNotifyMsgStatus, GroupNotifyMsgType, KERNEL_ERROR_CODES, KernelError, MAIN_CONFIG_FILE, MsgApi, MsgBridge, NTEventChannel, NTGroupMemberRole, NTGroupRequestOperateTypes, NapukettoCore, PathWrapper, ProfileApi, ProfileLikeApi, QrLoginSession, RichMediaApi, TicketApi, TipGroupElementType, WebApi, WebHonorType, buildEngineConfig, buildLoginConfig, buildSessionConfig, configError, createCoreContext, createSessionListener as createLifecycleSessionListener, createSessionListener, createLogger, createLoginListener, createSession, createWrapper, electronProcessType, getExistingSession, getMainSession, initAndStartSession, initEngine, initSession, isKernelError, kernelError, listLoginAccounts, listQQVersions, narrowBuddyCategories, parseAppidFromMajor, parseLogConfig, parseToml, probeRuntime, quickLogin, readMachineGuid, resolveAppidQua, resolveConfigPath, resolveDataRoot, resolveQQVersion, resolveQqGlobalPath, resolveQqUserDataRoot, resolveWrapperPath, startNapuketto, startSession, stringifyToml, toCanonicalElements, toSendElements, waitForNetworkConnection, waitSessionReady };
5227
+ export { BuddyCache, ChatType, ConfigBase, ConfigError, DEFAULT_DATA_ROOT_NAME, EXIT_CONFIG_INVALID, ElementType, FriendApi, FriendBridge, GrayTipSubType, GroupApi, GroupBridge, GroupCache, GroupListUpdateType, GroupNotifyApi, GroupNotifyMsgStatus, GroupNotifyMsgType, KERNEL_ERROR_CODES, KernelError, MAIN_CONFIG_FILE, MsgApi, MsgBridge, NTEventChannel, NTGroupMemberRole, NTGroupRequestOperateTypes, NapukettoCore, PathWrapper, ProfileApi, ProfileLikeApi, QrLoginSession, RecentContactApi, RichMediaApi, TicketApi, TipGroupElementType, WebApi, WebHonorType, buildEngineConfig, buildLoginConfig, buildSessionConfig, configError, createCoreContext, createSessionListener as createLifecycleSessionListener, createSessionListener, createLogger, createLoginListener, createSession, createWrapper, electronProcessType, getExistingSession, getMainSession, initAndStartSession, initEngine, initSession, isKernelError, kernelError, listLoginAccounts, listQQVersions, narrowBuddyCategories, parseAppidFromMajor, parseLogConfig, parseToml, probeRuntime, quickLogin, readMachineGuid, resolveAppidQua, resolveConfigPath, resolveDataRoot, resolveQQVersion, resolveQqGlobalPath, resolveQqUserDataRoot, resolveWrapperPath, startNapuketto, startSession, stringifyToml, toCanonicalElements, toSendElements, waitForNetworkConnection, waitSessionReady };
5001
5228
 
5002
5229
  //# sourceMappingURL=index.mjs.map