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