@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.d.mts +19 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +576 -315
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -3
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
|
-
|
|
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
|
-
|
|
947
|
-
|
|
948
|
-
|
|
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
|
|
1223
|
-
if (
|
|
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
|
|
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
|
|
1261
|
-
if (
|
|
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
|
|
1290
|
-
baseInfo: infoSimple
|
|
1291
|
-
vasInfo: infoSimple
|
|
1292
|
-
commonExt: info.detail
|
|
1293
|
-
status: infoSimple
|
|
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
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
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
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2155
|
-
if (
|
|
2156
|
-
|
|
2157
|
-
|
|
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 =
|
|
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
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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:
|
|
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 =
|
|
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
|
|
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
|
|
2693
|
-
const
|
|
2694
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2754
|
-
|
|
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
|
-
|
|
2758
|
-
|
|
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
|
-
|
|
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 =
|
|
2790
|
-
if (loginSvcCtor !== null && typeof loginSvcCtor.get === "function")
|
|
2791
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
for (const k of ids
|
|
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
|
-
|
|
3080
|
-
|
|
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
|
-
|
|
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
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
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
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
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 =
|
|
3555
|
+
const enumFn = getNTWrapperSessionFn(ctx);
|
|
3556
|
+
if (enumFn === null) return {};
|
|
3336
3557
|
const out = {};
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
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
|
|
3380
|
-
|
|
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(
|
|
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)
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
3452
|
-
session = ctx.session;
|
|
3715
|
+
if (ctx.session !== null) {
|
|
3453
3716
|
out["sessionSource"] = "own-create";
|
|
3717
|
+
return ctx.session;
|
|
3454
3718
|
}
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
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
|
-
|
|
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
|
}
|