@napuketto/loader 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 睦 <oppenheymu@gmail.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,774 @@
1
+ let node_path = require("node:path");
2
+ let node_fs = require("node:fs");
3
+ let node_os = require("node:os");
4
+ //#region src/host/env.ts
5
+ /**
6
+ * env.ts:引导进程环境变量访问层(2026-08-07 阶段 2)。
7
+ *
8
+ * 根 tsconfig 开启 noPropertyAccessFromIndexSignature——process.env 是索引签名
9
+ * 类型(Record<string, string | undefined>),点访问会报 TS4111;且 useNamingConvention
10
+ * 不允许 typeMember 用 CONSTANT_CASE。故改为「对象字面量快照」:
11
+ * - 对象字面量成员允许 CONSTANT_CASE(biome 配置),键是字面量 → env.XXX 点访问合法
12
+ * - 快照语义:引导进程内环境变量不变(launcher spawn 前注入完毕),取一次即可
13
+ */
14
+ const env = {
15
+ /** kernel 入口(.mjs,bootstrap 动态 import)。 */
16
+ NAPUTO_KERNEL_ENTRY: process.env["NAPUTO_KERNEL_ENTRY"],
17
+ /** QQ 版本号(appid 解析 / 协议装配用)。 */
18
+ NAPUTO_QQ_VERSION: process.env["NAPUTO_QQ_VERSION"],
19
+ /** 配置目录(数据根,日志 / 协议配置兜底)。 */
20
+ NAPUTO_CFG_DIR: process.env["NAPUTO_CFG_DIR"],
21
+ /** wrapper.node 绝对路径(dlopen 目标)。 */
22
+ NAPUTO_WRAPPER_PATH: process.env["NAPUTO_WRAPPER_PATH"],
23
+ /** adapter 包入口(协议装配用)。 */
24
+ NAPUTO_ADAPTER_ENTRY: process.env["NAPUTO_ADAPTER_ENTRY"],
25
+ /** network 包入口(协议装配用)。 */
26
+ NAPUTO_NETWORK_ENTRY: process.env["NAPUTO_NETWORK_ENTRY"],
27
+ /** 自建宿主标记(=1:标准 node + stub 引导,launcher 注入)。 */
28
+ NAPUTO_SELF_HOST: process.env["NAPUTO_SELF_HOST"],
29
+ /** stub QQNT.dll 目录(launcher 注入,self-host 诊断用)。 */
30
+ NAPUTO_STUB_DIR: process.env["NAPUTO_STUB_DIR"],
31
+ /** 强制指定快速登录账号(防风控账号挂起)。 */
32
+ NAPUTO_QUICK_UIN: process.env["NAPUTO_QUICK_UIN"],
33
+ /** 收发消息冒烟自检开关(=1 触发)。 */
34
+ NAPUTO_SMOKE: process.env["NAPUTO_SMOKE"],
35
+ /** 冒烟目标 peer(c2c:<uin> / group:<uin>,缺省发给自己)。 */
36
+ NAPUTO_SMOKE_PEER: process.env["NAPUTO_SMOKE_PEER"],
37
+ /** 运行时探测开关(=1 触发 probeRuntime)。 */
38
+ NAPUTO_PROBE: process.env["NAPUTO_PROBE"],
39
+ /** 全局 TOML 配置显式路径(优先级最高)。 */
40
+ NAPKETTO_CONFIG: process.env["NAPKETTO_CONFIG"],
41
+ /** 数据根(kernel 数据目录,cleanCache 用)。 */
42
+ NAPKETTO_DATA: process.env["NAPKETTO_DATA"]
43
+ };
44
+ //#endregion
45
+ //#region src/host/util.ts
46
+ /**
47
+ * util.ts:引导公共工具(日志 + 共享状态)。
48
+ * 2026-08-07 阶段 2:由 runtime/boot-util.js TS 化(零语义改动)。
49
+ * 由 self-host.ts 及各拆分模块 import(CJS bundle,运行在自建宿主引导进程内)。
50
+ */
51
+ /** boot 日志路径(NAPUTO_CFG_DIR 下,与 stub 验证日志同目录)。 */
52
+ const LOG_PATH = env.NAPUTO_CFG_DIR ? (0, node_path.join)(env.NAPUTO_CFG_DIR, "napuketto-boot.log") : (0, node_path.join)((0, node_os.tmpdir)(), "napuketto-boot.log");
53
+ /** 追加一行 boot 日志(失败静默,不阻塞引导)。 */
54
+ function log(msg) {
55
+ try {
56
+ (0, node_fs.appendFileSync)(LOG_PATH, `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}\n`);
57
+ } catch {}
58
+ }
59
+ /** 错误信息提取(保留 JS 原语义:message 存在用 message,否则用错误对象本身)。 */
60
+ function errMsg(e) {
61
+ return `${e?.message ?? e}`;
62
+ }
63
+ function createState() {
64
+ return {
65
+ wrapperExports: null,
66
+ qqSession: null,
67
+ qqLoginService: null,
68
+ bootstrapped: false
69
+ };
70
+ }
71
+ //#endregion
72
+ //#region src/host/login.ts
73
+ /** 打印可用快速登录账号 + 返回目标账号(启动横幅)。forcedUin(cli `-q` / NAPUTO_QUICK_UIN)优先。 */
74
+ async function pickLoginAccount(kernel, ctx, loginResultRef, forcedUin) {
75
+ try {
76
+ const accounts = await kernel.listLoginAccounts(ctx);
77
+ if (accounts.length > 0) {
78
+ log(`可用于快速登录 of QQ(${accounts.length} 个):`);
79
+ accounts.forEach((acct, idx) => {
80
+ const nick = acct.nickName || acct.uin;
81
+ const marker = acct.isQuickLogin ? "(默认)" : "";
82
+ log(`${idx + 1}. ${acct.uin} ${nick}${marker}`);
83
+ });
84
+ const target = forcedUin !== void 0 ? accounts.find((a) => a.uin === forcedUin) ?? void 0 : accounts.find((a) => a.isQuickLogin) ?? accounts[0];
85
+ if (target !== void 0) {
86
+ loginResultRef.targetUin = target.uin;
87
+ log(`正在快速登录 ${target.uin}`);
88
+ } else if (forcedUin !== void 0) log(`指定账号 ${forcedUin} 不在登录列表(登录流程将报错)`);
89
+ } else log("登录列表暂不可用(boot 阶段未 initConfig),由登录流程自动选择账号");
90
+ } catch (listErr) {
91
+ log(`bootstrap: 获取登录列表失败: ${errMsg(listErr)}`);
92
+ }
93
+ }
94
+ /**
95
+ * 登录(快速登录 → QR 回退)。
96
+ * @returns LoginResult | null(失败返回 null,不抛出——QR 兜底在内部)
97
+ */
98
+ async function doLogin(core, opts) {
99
+ try {
100
+ return await core.login(opts);
101
+ } catch (loginErr) {
102
+ log(`bootstrap: 快速登录失败(${errMsg(loginErr)}),尝试 QR 登录`);
103
+ try {
104
+ return await core.login({
105
+ ...opts,
106
+ qrFallback: true
107
+ });
108
+ } catch (qrErr) {
109
+ log(`bootstrap: QR 登录也失败: ${errMsg(qrErr)}`);
110
+ return null;
111
+ }
112
+ }
113
+ }
114
+ //#endregion
115
+ //#region src/host/protocols.ts
116
+ /**
117
+ * protocols.ts:协议装配(登录成功后)。
118
+ * 2026-08-07 阶段 2:由 runtime/boot-protocols.js TS 化(零语义改动)。
119
+ * 动态 import adapter/network 入口,装配 OB11 适配器。
120
+ * 依赖 launcher 注入的 NAPUTO_ADAPTER_ENTRY / NAPUTO_NETWORK_ENTRY。
121
+ */
122
+ /**
123
+ * 终端 ANSI 颜色(消息日志颜色分层,2026-08-07 用户定稿):
124
+ * `loader | 接收 <- 群聊` 灰色(背景噪音)/ `[会话]` 青色 / `[发送者]` 绿色 / 内容默认色。
125
+ * 仅 console 版嵌入;boot 文件日志用纯文本版(log),不污染文件。
126
+ */
127
+ const ANSI = {
128
+ gray: "\x1B[90m",
129
+ cyan: "\x1B[36m",
130
+ green: "\x1B[32m",
131
+ reset: "\x1B[0m"
132
+ };
133
+ /**
134
+ * 登录成功后装配协议:kernel 各 Api + network 广播 + OB11 适配器。
135
+ */
136
+ async function startProtocols(kernel, ctx, loginResult) {
137
+ const logger = kernel.createLogger?.({
138
+ console: true,
139
+ base: { name: "loader" }
140
+ });
141
+ const adapterEntry = env.NAPUTO_ADAPTER_ENTRY;
142
+ const networkEntry = env.NAPUTO_NETWORK_ENTRY;
143
+ if (!adapterEntry || !networkEntry) {
144
+ log("bootstrap: NAPUTO_ADAPTER_ENTRY/NETWORK_ENTRY 未设置,跳过协议装配");
145
+ return;
146
+ }
147
+ try {
148
+ const network = await import(`file://${networkEntry.replace(/\\/g, "/")}`);
149
+ const onebot11Entry = adapterEntry.replace(/index\.mjs$/, "onebot11/index.mjs");
150
+ const coreEntry = adapterEntry.replace(/index\.mjs$/, "core/index.mjs");
151
+ const adapter = await import(`file://${onebot11Entry.replace(/\\/g, "/")}`);
152
+ const adapterCore = await import(`file://${coreEntry.replace(/\\/g, "/")}`);
153
+ const session = ctx.session;
154
+ if (!session) {
155
+ log("bootstrap: session 为空,无法装配协议");
156
+ return;
157
+ }
158
+ const channel = new kernel.NTEventChannel("Msg");
159
+ new kernel.MsgBridge(session, channel).register();
160
+ channel.on("Msg/onRecvMsg", (msgs) => {
161
+ const list = Array.isArray(msgs) ? msgs : [msgs];
162
+ for (const msg of list) {
163
+ if (!msg || typeof msg !== "object") continue;
164
+ try {
165
+ let rendered = "";
166
+ for (const el of kernel.toCanonicalElements(msg)) if (el.type === "text") rendered += el.text;
167
+ else if (el.type === "at") {
168
+ const name = el.display ?? (el.target === "all" ? "全体" : el.target);
169
+ rendered += `@${name}`;
170
+ } else if (el.type === "face") rendered += "[表情]";
171
+ else if (el.type === "image") rendered += "[图片]";
172
+ else if (el.type === "voice") rendered += "[语音]";
173
+ else if (el.type === "video") rendered += "[视频]";
174
+ else if (el.type === "reply") rendered += "[回复]";
175
+ else if (el.type === "file") rendered += `[文件${el.name ?? ""}]`;
176
+ else if (el.type === "forward") rendered += "[合并转发]";
177
+ else if (el.type === "json") rendered += "[json]";
178
+ else if (el.type === "xml") rendered += "[xml]";
179
+ if (rendered === "") rendered = "[空消息/媒体]";
180
+ const isGroup = msg.chatType === kernel.ChatType.GROUP;
181
+ const kind = isGroup ? "群聊" : "私聊";
182
+ const peerUin = String(msg.peerUin ?? "");
183
+ const senderUin = String(msg.senderUin ?? (peerUin || "未知"));
184
+ const plainTags = isGroup ? `[群${peerUin}] [用户${senderUin}]` : `[用户${senderUin}]`;
185
+ const coloredTags = isGroup ? `${ANSI.cyan}[群${peerUin}]${ANSI.reset} ${ANSI.green}[用户${senderUin}]${ANSI.reset}` : `${ANSI.green}[用户${senderUin}]${ANSI.reset}`;
186
+ const content = rendered.replace(/\n/g, "\n ");
187
+ const plain = `接收 <- ${kind} ${plainTags}: ${content}`;
188
+ const colored = `${ANSI.gray}loader | 接收 <- ${kind}${ANSI.reset} ${coloredTags}: ${content}`;
189
+ log(plain);
190
+ logger?.info(colored);
191
+ } catch (e) {
192
+ const line2 = `接收消息(解析失败: ${errMsg(e)})`;
193
+ log(line2);
194
+ logger?.warn(line2);
195
+ }
196
+ }
197
+ });
198
+ const groupApi = new kernel.GroupApi(session);
199
+ const msgApi = new kernel.MsgApi(session);
200
+ const friendApi = new kernel.FriendApi(session, { uidToUin: (uids) => groupApi.uidToUin(uids) });
201
+ const groupChannel = new kernel.NTEventChannel("Group");
202
+ new kernel.GroupBridge(session, groupChannel).register();
203
+ const groupCache = new kernel.GroupCache({
204
+ channel: groupChannel,
205
+ groupApi
206
+ });
207
+ groupCache.register();
208
+ const groupNotifyApi = new kernel.GroupNotifyApi(session);
209
+ const ticketApi = new kernel.TicketApi(session);
210
+ const richMediaApi = new kernel.RichMediaApi(session);
211
+ const profileApi = new kernel.ProfileApi(session);
212
+ const profileLikeApi = new kernel.ProfileLikeApi(session);
213
+ const webApi = new kernel.WebApi({ getCookies: (domain) => ticketApi.getCookies(domain, loginResult.uin) });
214
+ const broadcaster = new network.EventBroadcaster();
215
+ let ob11Section = {};
216
+ const cfgFile = env.NAPKETTO_CONFIG || (0, node_path.join)(env.NAPUTO_CFG_DIR || ".", "napuketto.toml");
217
+ try {
218
+ const raw = (0, node_fs.readFileSync)(cfgFile, "utf8");
219
+ const parsed = kernel.parseToml(raw);
220
+ if (parsed && typeof parsed["onebot11"] === "object" && parsed["onebot11"] !== null) ob11Section = parsed["onebot11"];
221
+ } catch (e) {
222
+ log(`bootstrap: 全局配置读取失败(用默认 ob11 配置): ${errMsg(e)}`);
223
+ }
224
+ const ob11Config = new adapterCore.ProtocolConfig({
225
+ path: cfgFile,
226
+ schema: adapter.ob11ConfigSchema,
227
+ defaults: adapter.ob11ConfigSchema.parse({}),
228
+ seed: adapter.ob11ConfigSchema.parse(ob11Section)
229
+ });
230
+ await new adapter.NapukettoOneBot11Adapter({
231
+ config: ob11Config,
232
+ broadcaster,
233
+ msgChannel: channel,
234
+ msgApi,
235
+ groupApi,
236
+ groupNotifyApi,
237
+ friendApi,
238
+ ticketApi,
239
+ richMediaApi,
240
+ profileApi,
241
+ profileLikeApi,
242
+ webApi,
243
+ self: {
244
+ uin: loginResult.uin,
245
+ nickname: loginResult.nick
246
+ },
247
+ system: {
248
+ appVersion: env.NAPUTO_QQ_VERSION || "unknown",
249
+ cleanCache: async () => {
250
+ new kernel.PathWrapper({
251
+ dataRoot: env.NAPKETTO_DATA,
252
+ account: loginResult.uin
253
+ }).clearCache();
254
+ },
255
+ cacheDir: (0, node_path.join)(env.NAPUTO_CFG_DIR || ".", "cache"),
256
+ exit: async () => {
257
+ log("bootstrap: bot_exit 触发,退出 QQ 主进程");
258
+ process.exit(0);
259
+ },
260
+ restart: async () => {
261
+ log("bootstrap: set_restart 触发,退出 QQ 主进程(由 launcher 重启)");
262
+ process.exit(0);
263
+ }
264
+ },
265
+ groupCache
266
+ }).start();
267
+ log("bootstrap: onebot11 adapter started");
268
+ } catch (e) {
269
+ log(`bootstrap: 协议装配失败: ${errMsg(e)}`);
270
+ }
271
+ }
272
+ //#endregion
273
+ //#region src/host/session.ts
274
+ /** 对象有效性判断:getMsgService() 调用不抛断言(cpp_impl 已激活)。
275
+ * 与 isSessionUsable 的区别:这里允许返回 null(未 init 但对象有效)。 */
276
+ function isSessionObjectValid(s) {
277
+ if (!s) return false;
278
+ try {
279
+ s.getMsgService();
280
+ return true;
281
+ } catch {
282
+ return false;
283
+ }
284
+ }
285
+ /** session 可用性判断:getMsgService() 可调且非 null(核心 service 已挂载)。 */
286
+ function isSessionUsable(s) {
287
+ if (!s) return false;
288
+ try {
289
+ const svc = s.getMsgService();
290
+ return svc !== null && svc !== void 0;
291
+ } catch {
292
+ return false;
293
+ }
294
+ }
295
+ /** session 就绪探测(5s 间隔,60s 上限)——观察 qqSession / get() 状态。 */
296
+ function startSessionProbe(state, _ctx, durationMs = 6e4) {
297
+ const sessionProbe = setInterval(() => {
298
+ try {
299
+ const S2 = state.wrapperExports?.["NodeIQQNTWrapperSession"];
300
+ const out = [];
301
+ const describe = (s) => {
302
+ if (!s) return "null";
303
+ try {
304
+ const sess = s;
305
+ const id = typeof sess.getSessionId === "function" ? String(sess.getSessionId()) : "?";
306
+ const svc = typeof sess.getMsgService === "function" ? sess.getMsgService() : null;
307
+ return `id=${id} msgSvc=${svc !== null && svc !== void 0 ? "READY" : "null"}`;
308
+ } catch (e) {
309
+ return `id=? msgSvc=断言(${errMsg(e).slice(0, 60)})`;
310
+ }
311
+ };
312
+ if (state.qqSession && typeof state.qqSession.getMsgService === "function") out.push(`qqSession[${describe(state.qqSession)}]`);
313
+ if (S2 && typeof S2.get === "function") {
314
+ const got = S2.get();
315
+ out.push(`get()[${describe(got)}]`);
316
+ }
317
+ if (S2 && typeof S2.getNTWrapperSession === "function") {
318
+ const gotNT = S2.getNTWrapperSession("Session");
319
+ if (gotNT) out.push(`getNT("Session")[${describe(gotNT)}]`);
320
+ }
321
+ log(`BOOT: session 探测: ${out.join(" | ")}`);
322
+ } catch (e) {
323
+ log(`BOOT: session 探测失败: ${errMsg(e)}`);
324
+ }
325
+ }, 5e3);
326
+ setTimeout(() => clearInterval(sessionProbe), durationMs);
327
+ return sessionProbe;
328
+ }
329
+ /** 探测 session 方法面(NAPI 反射,验证 startNT/init 等关键方法)。 */
330
+ function probeSessionMethods(ctx) {
331
+ try {
332
+ const s = ctx.session;
333
+ if (s) {
334
+ const names = [...Object.getOwnPropertyNames(Object.getPrototypeOf(s) ?? {}), ...Object.keys(s)];
335
+ log(`bootstrap: session methods(${names.length}): ${[...new Set(names)].join(", ")}`);
336
+ log(`bootstrap: session.init=${typeof s["init"]} startNT=${typeof s["startNT"]} getMsgService=${typeof s["getMsgService"]}`);
337
+ }
338
+ } catch (e) {
339
+ log(`bootstrap: session 探测失败: ${errMsg(e)}`);
340
+ }
341
+ }
342
+ /** 收集候选 session(按优先级:QQ 主 session → vehicle 激活 session → get() → qqSession)。 */
343
+ function collectCandidateSessions(state, kernel, ctx) {
344
+ const candidates = [];
345
+ const S2 = state.wrapperExports?.["NodeIQQNTWrapperSession"];
346
+ try {
347
+ if (typeof kernel.getMainSession === "function") {
348
+ const ms = kernel.getMainSession(ctx);
349
+ if (ms) candidates.push({
350
+ s: ms,
351
+ tag: "getMainSession(nt_x)"
352
+ });
353
+ }
354
+ } catch (e) {
355
+ log(`bootstrap: getMainSession 失败: ${errMsg(e)}`);
356
+ }
357
+ try {
358
+ if (S2 && typeof S2.getNTWrapperSession === "function") {
359
+ const got = S2.getNTWrapperSession("Session");
360
+ if (got) candidates.push({
361
+ s: got,
362
+ tag: "getNT(Session)"
363
+ });
364
+ }
365
+ } catch (e) {
366
+ log(`bootstrap: getNTWrapperSession 失败: ${errMsg(e)}`);
367
+ }
368
+ try {
369
+ if (S2 && typeof S2.get === "function") {
370
+ const got = S2.get();
371
+ if (got) candidates.push({
372
+ s: got,
373
+ tag: "get()"
374
+ });
375
+ }
376
+ } catch (e) {
377
+ log(`bootstrap: get() 失败: ${errMsg(e)}`);
378
+ }
379
+ if (state.qqSession) candidates.push({
380
+ s: state.qqSession,
381
+ tag: "qqSession"
382
+ });
383
+ return candidates;
384
+ }
385
+ /** 从候选里选最佳 session:优先有效对象,且 READY(msgSvc 非 null)优先。 */
386
+ function pickBestSession(candidates) {
387
+ let bestValid = null;
388
+ for (const c of candidates) {
389
+ const ready = isSessionUsable(c.s);
390
+ const valid = isSessionObjectValid(c.s);
391
+ log(`bootstrap: 候选 ${c.tag}: valid=${valid} msgSvc=${ready ? "READY" : "null/断言"}`);
392
+ if (valid && bestValid === null) bestValid = c;
393
+ if (ready) return c;
394
+ }
395
+ return bestValid;
396
+ }
397
+ //#endregion
398
+ //#region src/host/smoke.ts
399
+ /**
400
+ * smoke.ts:P2-1 收发消息冒烟自检(NAPUTO_SMOKE=1 触发,2026-08-06)。
401
+ * 2026-08-07 阶段 2:由 runtime/boot-smoke.js TS 化(零语义改动)。
402
+ *
403
+ * 目的:业务层最后试金石——登录 + session 就绪后,真正用 kernel MsgBridge
404
+ * (addKernelMsgListener)+ MsgApi(sendMsg)发/收一条消息,验证:
405
+ * ① 发送方向:MsgApi.sendMessage 成功返回 msgId(NT 雪花 ID)
406
+ * ② 接收方向:MsgBridge 订阅 onRecvMsg 收到消息事件
407
+ * ③ 落库验证:fetchMessages 拉取最近消息,确认刚发的消息在列表里
408
+ *
409
+ * 目标 peer 由环境变量 NAPUTO_SMOKE_PEER 指定(格式:c2c:<uin> 或 group:<uin>),
410
+ * 缺省发给自己的 uin(C2C 我的设备)。发送内容固定带时间戳,便于日志核对。
411
+ *
412
+ * 由 bootstrap.ts 在登录成功、session 就绪后调用(协议装配前——不依赖
413
+ * adapter/network,单独验证 kernel 业务链路)。
414
+ */
415
+ /** 冒烟测试默认等待(毫秒)。 */
416
+ const SMOKE_SETTLE_MS = 5e3;
417
+ /** 解析冒烟目标:NAPUTO_SMOKE_PEER(c2c:<uin> / group:<uin>),缺省发给自己。 */
418
+ function parseSmokeTarget(envPeer, selfUin) {
419
+ if (!envPeer) return {
420
+ kind: "c2c",
421
+ uin: selfUin
422
+ };
423
+ const [kind, uin = ""] = envPeer.split(":");
424
+ if (kind === "group" && uin !== "") return {
425
+ kind: "group",
426
+ uin
427
+ };
428
+ if (kind === "c2c" && uin !== "") return {
429
+ kind: "c2c",
430
+ uin
431
+ };
432
+ return {
433
+ kind: "c2c",
434
+ uin: selfUin
435
+ };
436
+ }
437
+ /** uin → uid(c2c 目标用;group 直接拿群号作 peerUid)。 */
438
+ async function resolvePeerUid(kernel, ctx, target, selfUid) {
439
+ const groupApi = new kernel.GroupApi(ctx.session);
440
+ if (target.kind === "group") return {
441
+ chatType: kernel.ChatType.GROUP,
442
+ peerUid: target.uin
443
+ };
444
+ if (target.uin === String(ctx.login?.uin ?? "")) return {
445
+ chatType: kernel.ChatType.C2C,
446
+ peerUid: selfUid
447
+ };
448
+ const uid = (await groupApi.uinToUid([target.uin])).get(target.uin);
449
+ if (!uid) throw new Error(`uinToUid 解析失败: ${target.uin}`);
450
+ return {
451
+ chatType: kernel.ChatType.C2C,
452
+ peerUid: uid
453
+ };
454
+ }
455
+ /**
456
+ * 执行冒烟自检:注册 MsgBridge → 订阅 onRecvMsg → 发一条消息 → 拉历史核对。
457
+ * @returns 是否全部通过(发送成功 + 收到事件 + 落库验证)
458
+ */
459
+ async function runSmokeTest(kernel, ctx, loginResult) {
460
+ const session = ctx.session;
461
+ if (!session) {
462
+ log("smoke: ❌ session 为空,跳过");
463
+ return false;
464
+ }
465
+ const target = parseSmokeTarget(env.NAPUTO_SMOKE_PEER, loginResult.uin);
466
+ log(`smoke: 目标=${target.kind}:${target.uin}(peer env=${env.NAPUTO_SMOKE_PEER ?? "(缺省=自己)"})`);
467
+ const channel = new kernel.NTEventChannel("Msg");
468
+ const bridge = new kernel.MsgBridge(session, channel);
469
+ bridge.register();
470
+ log("smoke: ✅ MsgBridge 注册完成(addKernelMsgListener)");
471
+ let received = 0;
472
+ let receivedText = "";
473
+ channel.on("Msg/onRecvMsg", (msgs) => {
474
+ const list = Array.isArray(msgs) ? msgs : [msgs];
475
+ for (const msg of list) {
476
+ if (!msg || typeof msg !== "object") continue;
477
+ received += 1;
478
+ try {
479
+ const texts = kernel.toCanonicalElements(msg).filter((el) => el.type === "text").map((el) => el.text).join("");
480
+ if (texts) receivedText = texts;
481
+ } catch (e) {
482
+ log(`smoke: onRecvMsg 解析失败: ${errMsg(e)}`);
483
+ }
484
+ log(`smoke: 📥 onRecvMsg 收到 #${received}(msgId=${msg.msgId ?? "?"} seq=${msg.msgSeq ?? "?"} text="${receivedText}")`);
485
+ }
486
+ });
487
+ const msgApi = new kernel.MsgApi(session);
488
+ const peer = await resolvePeerUid(kernel, ctx, target, loginResult.uid);
489
+ const content = `NapukettoQQ smoke test ${Date.now()}`;
490
+ let sentMsgId = "";
491
+ try {
492
+ const { msgId } = await msgApi.sendMessage(peer, [{
493
+ type: "text",
494
+ text: content
495
+ }]);
496
+ sentMsgId = msgId;
497
+ log(`smoke: ✅ 发送成功 msgId=${msgId}(content="${content}")`);
498
+ } catch (e) {
499
+ log(`smoke: ❌ 发送失败: ${errMsg(e)}`);
500
+ }
501
+ await new Promise((r) => setTimeout(r, SMOKE_SETTLE_MS));
502
+ let landed = false;
503
+ try {
504
+ const recent = await msgApi.fetchMessages(peer, { count: 10 });
505
+ landed = recent.some((m) => m.msgId === sentMsgId || m.msgSeq === sentMsgId);
506
+ log(`smoke: 落库验证: 最近 ${recent.length} 条${landed ? " ✅ 含刚发消息" : " ❌ 未找到(可能同步延迟)"}`);
507
+ if (!landed) for (const m of recent.slice(0, 3)) {
508
+ const texts = kernel.toCanonicalElements(m).filter((el) => el.type === "text").map((el) => el.text).join("");
509
+ log(`smoke: 最近消息 msgId=${m.msgId} seq=${m.msgSeq} text="${texts}"`);
510
+ }
511
+ } catch (e) {
512
+ log(`smoke: 落库验证失败: ${errMsg(e)}`);
513
+ }
514
+ try {
515
+ bridge.unregister();
516
+ log("smoke: ✅ MsgBridge 已注销");
517
+ } catch (e) {
518
+ log(`smoke: 注销失败(忽略): ${errMsg(e)}`);
519
+ }
520
+ const ok = sentMsgId !== "" && (received > 0 || landed);
521
+ log(`smoke: ===== 冒烟${ok ? "✅ 通过" : "❌ 未完全通过"}(发送=${sentMsgId !== "" ? "OK" : "FAIL"},收到事件=${received},落库=${landed})=====`);
522
+ return ok;
523
+ }
524
+ //#endregion
525
+ //#region src/host/bootstrap.ts
526
+ /**
527
+ * bootstrap.ts:kernel 引导核心(import kernel → 装配 → 登录 → session 替换 → 协议装配)。
528
+ * 2026-08-07 阶段 2:由 runtime/boot-bootstrap.js TS 化(零语义改动)。
529
+ * 由 self-host.ts(自建宿主,2026-08-07 唯一路线)在 dlopen wrapper.node 后调用。
530
+ *
531
+ * 2026-08-07 阶段 1 拆分(零语义改动):
532
+ * - 登录流程(选账号 / 快速登录 / QR 回退)→ login.ts
533
+ * - session 选择 / 探测 / 就绪 → session.ts
534
+ * - 协议装配 → protocols.ts;冒烟自检 → smoke.ts
535
+ *
536
+ * session 来源(2026-08-07 实测结论;V1 Proxy 捕获与 vehicle 载具已归档 archive/):
537
+ * 登录成功后按优先级取 session:
538
+ * ① kernel.getMainSession(ctx)(startup 链路 getNTWrapperSession(nt_x)——QQ 主 session,
539
+ * 渲染进程已完成 init → getMsgService READY)
540
+ * ② get() / qqSession(V1 兼容)
541
+ * 取到后 core.setSession 替换 → waitSessionReady 确认 getMsgService READY。
542
+ */
543
+ /** 核心引导:import kernel → 装配 → 登录 → 替换 session → 等待就绪 → 协议装配。 */
544
+ async function bootstrap(state) {
545
+ const kernelEntry = env.NAPUTO_KERNEL_ENTRY;
546
+ if (!kernelEntry) {
547
+ log("bootstrap: NAPUTO_KERNEL_ENTRY not set");
548
+ return;
549
+ }
550
+ try {
551
+ const kernel = await import(`file://${kernelEntry.replace(/\\/g, "/")}`);
552
+ log(`bootstrap: kernel imported, keys: ${Object.keys(kernel).join(", ")}`);
553
+ if (typeof kernel.startNapuketto !== "function" && typeof kernel.NapukettoCore !== "function") {
554
+ log("bootstrap: kernel 无 startNapuketto/NapukettoCore 导出");
555
+ return;
556
+ }
557
+ const bootEnv = {
558
+ qqVersion: env.NAPUTO_QQ_VERSION,
559
+ dataDir: env.NAPUTO_CFG_DIR,
560
+ wrapperPath: env.NAPUTO_WRAPPER_PATH
561
+ };
562
+ const Appid = typeof kernel.parseAppidFromMajor === "function" && bootEnv.wrapperPath && kernel.parseAppidFromMajor((0, node_path.join)((0, node_path.dirname)(bootEnv.wrapperPath), "major.node")) || typeof kernel.resolveAppidQua === "function" && kernel.resolveAppidQua(bootEnv.qqVersion || "").appid || "537237765";
563
+ log(`bootstrap: appid=${Appid}(wrapper=${bootEnv.wrapperPath})`);
564
+ try {
565
+ if (kernel.NapukettoCore !== void 0) {
566
+ const core = kernel.NapukettoCore.create({
567
+ paths: { dataRoot: bootEnv.dataDir },
568
+ logLevel: "info"
569
+ });
570
+ const ctx = core.attachWrapper(state.wrapperExports, bootEnv);
571
+ log(`bootstrap: attachWrapper OK, engine=${typeof ctx.engine}, session=${ctx.session !== null}`);
572
+ try {
573
+ const S2 = state.wrapperExports?.["NodeIQQNTWrapperSession"];
574
+ const created = S2 && typeof S2.create === "function" ? S2.create() : null;
575
+ log(`BOOT: create()===qqSession? ${created === state.qqSession} | create()===ctx.session? ${created === ctx.session} | qqSession===ctx.session? ${state.qqSession === ctx.session}`);
576
+ const sess = created;
577
+ const svc = sess && typeof sess.getMsgService === "function" ? sess.getMsgService() : null;
578
+ const qs = state.qqSession;
579
+ log(`BOOT: create().getMsgService=${svc !== null && svc !== void 0 ? "ready" : "null"} qqSession.getMsgService=${qs && typeof qs.getMsgService === "function" && qs.getMsgService() !== null && qs.getMsgService() !== void 0 ? "ready" : "null"}`);
580
+ } catch (e) {
581
+ log(`BOOT: create() 探测失败: ${errMsg(e)}`);
582
+ }
583
+ startSessionProbe(state, ctx);
584
+ probeSessionMethods(ctx);
585
+ let loginResult = null;
586
+ if (typeof core.login === "function") {
587
+ const forcedUin = env.NAPUTO_QUICK_UIN;
588
+ const ref = { targetUin: void 0 };
589
+ await pickLoginAccount(kernel, ctx, ref, forcedUin);
590
+ loginResult = await doLogin(core, {
591
+ appid: Appid,
592
+ initTimeoutMs: 2e4,
593
+ ...forcedUin ? { quickUin: forcedUin } : ref.targetUin !== void 0 ? { quickUin: ref.targetUin } : {}
594
+ });
595
+ if (loginResult === null) {
596
+ log("bootstrap: 登录失败,引导中止");
597
+ return;
598
+ }
599
+ log(`bootstrap: 登录成功 uin=${loginResult.uin} uid=${loginResult.uid} nick=${loginResult.nick}`);
600
+ const isSelfHost = env.NAPUTO_SELF_HOST === "1";
601
+ let chosen = null;
602
+ if (!isSelfHost) chosen = pickBestSession(collectCandidateSessions(state, kernel, ctx));
603
+ if (chosen && chosen.s !== ctx.session) {
604
+ core.setSession(chosen.s);
605
+ log(`bootstrap: 已替换 session(来源=${chosen.tag},msgSvc=${isSessionUsable(chosen.s) ? "READY" : "null"})`);
606
+ } else if (chosen) log(`bootstrap: 候选 session 与 ctx.session 相同(${chosen.tag})`);
607
+ else log("bootstrap: 无有效候选 session(保留 kernel 自建 session)");
608
+ const activateTarget = chosen ?? {
609
+ s: ctx.session,
610
+ tag: "kernel 自建 session"
611
+ };
612
+ if (activateTarget.s && !isSessionUsable(activateTarget.s)) {
613
+ log(`bootstrap: 激活 session(${activateTarget.tag},先 init 后 startupSession.start)...`);
614
+ try {
615
+ const qqDataRoot = typeof kernel.resolveQqUserDataRoot === "function" ? kernel.resolveQqUserDataRoot(state.wrapperExports) : null;
616
+ const accountPath = qqDataRoot && typeof kernel.resolveQqGlobalPath === "function" ? kernel.resolveQqGlobalPath(qqDataRoot) : bootEnv.dataDir || ".";
617
+ const sessionConfig = kernel.buildSessionConfig({
618
+ appid: Appid,
619
+ fullVersion: bootEnv.qqVersion || "",
620
+ selfUin: loginResult.uin,
621
+ selfUid: loginResult.uid,
622
+ accountPath,
623
+ downloadPath: (0, node_path.join)(accountPath, "NapCat", "temp")
624
+ });
625
+ const listener = kernel.createLifecycleSessionListener();
626
+ await kernel.initAndStartSession(ctx, sessionConfig, listener, { timeoutMs: 2e4 });
627
+ log("bootstrap: 激活 session init + start 完成");
628
+ } catch (initErr) {
629
+ log(`bootstrap: 激活 session init 失败: ${errMsg(initErr)}`);
630
+ }
631
+ }
632
+ try {
633
+ await kernel.waitSessionReady(ctx, { timeoutMs: 3e4 });
634
+ log("bootstrap: QQ session 就绪(getMsgService 可用)");
635
+ } catch (readyErr) {
636
+ log(`bootstrap: 等待 session 就绪失败: ${errMsg(readyErr)}`);
637
+ }
638
+ if (env.NAPUTO_SMOKE === "1" && loginResult !== null) try {
639
+ log(`bootstrap: 冒烟自检${await runSmokeTest(kernel, ctx, loginResult) ? "通过" : "未完全通过"}(见上文 smoke 日志)`);
640
+ } catch (smokeErr) {
641
+ log(`bootstrap: 冒烟自检异常: ${errMsg(smokeErr)}`);
642
+ }
643
+ } else log("bootstrap: kernel core missing login fn");
644
+ if (loginResult !== null) await startProtocols(kernel, ctx, loginResult);
645
+ if (env.NAPUTO_PROBE === "1" && typeof kernel.probeRuntime === "function") try {
646
+ const probe = kernel.probeRuntime(ctx);
647
+ log(`bootstrap: probe done, session=${probe.session ? "ok" : "null"}, services=${Object.keys(probe.services ?? {}).length}`);
648
+ setTimeout(() => {
649
+ try {
650
+ if (typeof kernel.probeRuntime !== "function") return;
651
+ const late = kernel.probeRuntime(ctx, "napuketto-probe-late.json");
652
+ log(`bootstrap: probe-late done, session=${late.session ? "ok" : "null"}, services=${Object.keys(late.services ?? {}).length}`);
653
+ } catch (e2) {
654
+ log(`bootstrap: probe-late error: ${errMsg(e2)}`);
655
+ }
656
+ }, 35e3);
657
+ } catch (e) {
658
+ log(`bootstrap: probe error: ${errMsg(e)}`);
659
+ }
660
+ } else {
661
+ log("bootstrap: kernel has no NapukettoCore, falling back");
662
+ const ctx = kernel.startNapuketto?.({
663
+ wrapperExports: state.wrapperExports,
664
+ env: bootEnv
665
+ });
666
+ if (ctx === void 0) {
667
+ log("bootstrap: startNapuketto 不可用");
668
+ return;
669
+ }
670
+ log(`bootstrap: startNapuketto OK, engine=${typeof ctx.engine}, session=${ctx.session !== null}`);
671
+ if (typeof kernel.quickLogin === "function" && typeof kernel.initAndStartSession === "function") {
672
+ if (typeof kernel.buildLoginConfig === "function" && ctx.loginService) {
673
+ const loginCfg = kernel.buildLoginConfig(Appid, bootEnv.qqVersion || "", bootEnv.dataDir || ".");
674
+ if (typeof ctx.loginService.initConfig === "function") {
675
+ ctx.loginService.initConfig(loginCfg);
676
+ log("bootstrap: loginService.initConfig OK");
677
+ }
678
+ }
679
+ const loginResult = await kernel.quickLogin(ctx, {});
680
+ log(`bootstrap: quickLogin OK, uin=${loginResult.uin}, uid=${loginResult.uid}`);
681
+ const sessionConfig = kernel.buildSessionConfig({
682
+ appid: Appid,
683
+ fullVersion: bootEnv.qqVersion || "",
684
+ selfUin: loginResult.uin,
685
+ selfUid: loginResult.uid,
686
+ accountPath: bootEnv.dataDir || ".",
687
+ downloadPath: (0, node_path.join)(bootEnv.dataDir || ".", "temp")
688
+ });
689
+ const listener = kernel.createLifecycleSessionListener();
690
+ await kernel.initAndStartSession(ctx, sessionConfig, listener, { timeoutMs: 2e4 });
691
+ log("bootstrap: session init + startNT OK!");
692
+ } else log("bootstrap: kernel missing lifecycle fns (quickLogin/initAndStartSession)");
693
+ if (env.NAPUTO_PROBE === "1" && typeof kernel.probeRuntime === "function") try {
694
+ const probe = kernel.probeRuntime(ctx);
695
+ log(`bootstrap: probe done, session=${probe.session ? "ok" : "null"}, services=${Object.keys(probe.services ?? {}).length}`);
696
+ } catch (e) {
697
+ log(`bootstrap: probe error: ${errMsg(e)}`);
698
+ }
699
+ }
700
+ } catch (e) {
701
+ log(`bootstrap: lifecycle error: ${errMsg(e)}`);
702
+ }
703
+ } catch (e) {
704
+ log(`bootstrap: import kernel failed: ${errMsg(e)}`);
705
+ }
706
+ }
707
+ //#endregion
708
+ //#region src/host/self-host.ts
709
+ /**
710
+ * self-host.ts:自建宿主(路线 A,NAPUTO_SELF_HOST)入口——标准 Node 进程直接运行。
711
+ * 2026-08-07 阶段 2:由 runtime/self-host.cjs TS 化(零语义改动)。
712
+ * 由 loader launcher.launchSelfHost() spawn 标准 node 启动;环境变量继承。
713
+ *
714
+ * 背景(2026-08-07 HANDOVER-V6/V9 实测,勿重复探索):
715
+ * - 纯 Node(系统 node)+ stub QQNT.dll 转发 + 9.9.33 官方 wrapper.node 可完整登录,
716
+ * 且 session 业务 service 全部 READY(getMsgService 298 方法)。
717
+ * - 与路线 B(注入 QQ worker,继承 QQ env)不同:标准 node 没有 QQ 的环境与事件分发
718
+ * 对象,必须手动完成三要素:
719
+ * ① 加载 = stub QQNT.dll 转发(napi_* → node.exe,PATH 前置 stub 目录,
720
+ * launcher.launchSelfHost 负责装配 PATH)
721
+ * ② `NodeIO3MiscService.get()` + `addO3MiscListener` 激活事件分发
722
+ * (否则 getLoginList 永不 resolve——挂起,V6 实测)
723
+ * ③ commonPath/desktopGlobalPath = 数据根/nt_qq/global(kernel 已按
724
+ * electronProcessType() !== "browser" 自动解析,见 wrapper-loader.ts)
725
+ *
726
+ * 职责(本文件只做自建宿主特有的引导前置,业务链路完全复用):
727
+ * 1. process.dlopen(wrapper.node) → exports(98 个,stub 提供宿主符号)
728
+ * 2. O3MiscService 激活事件分发
729
+ * 3. 构造 state(wrapperExports)→ 调 bootstrap(state)
730
+ * → kernel 装配 → 登录(快速/QR)→ session(先 init 后 start)→ 冒烟 → 协议装配
731
+ */
732
+ const state = createState();
733
+ log(`[self-host] 启动 @ ${(/* @__PURE__ */ new Date()).toISOString()} pid=${process.pid} node=${process.version} type=${process.type ?? "(标准 node)"}`);
734
+ log(`[self-host] stub 目录: ${env.NAPUTO_STUB_DIR ?? "(PATH 前置,未记录)"}`);
735
+ const wrapperPath = env.NAPUTO_WRAPPER_PATH;
736
+ if (!wrapperPath) {
737
+ log("[self-host] NAPUTO_WRAPPER_PATH 未设置,退出");
738
+ process.exit(1);
739
+ }
740
+ log(`[self-host] dlopen wrapper.node: ${wrapperPath}`);
741
+ try {
742
+ const m = { exports: {} };
743
+ process.dlopen(m, wrapperPath);
744
+ const keys = Object.keys(m.exports ?? {});
745
+ log(`[self-host] ✅ dlopen 成功,exports ${keys.length} 个`);
746
+ if (!keys.includes("NodeIKernelLoginService")) {
747
+ log("[self-host] ❌ exports 无 NodeIKernelLoginService(stub 环境异常?)");
748
+ process.exit(1);
749
+ }
750
+ state.wrapperExports = m.exports;
751
+ } catch (e) {
752
+ log(`[self-host] ❌ dlopen 失败: ${errMsg(e)}`);
753
+ process.exit(1);
754
+ }
755
+ try {
756
+ const O3 = state.wrapperExports?.["NodeIO3MiscService"];
757
+ if (O3 && typeof O3.get === "function") {
758
+ O3.get().addO3MiscListener({ getOnAmgomDataPiece() {} });
759
+ log("[self-host] ✅ O3MiscService 激活事件分发");
760
+ } else log("[self-host] ⚠️ NodeIO3MiscService 缺失(版本变化?继续尝试)");
761
+ } catch (e) {
762
+ log(`[self-host] ⚠️ O3MiscService 激活失败: ${errMsg(e)}`);
763
+ }
764
+ log("[self-host] 调 bootstrap(state) ...");
765
+ (async () => {
766
+ try {
767
+ await bootstrap(state);
768
+ log("[self-host] bootstrap 完成");
769
+ } catch (e) {
770
+ log(`[self-host] bootstrap 失败: ${errMsg(e)}`);
771
+ process.exit(1);
772
+ }
773
+ })();
774
+ //#endregion
@@ -0,0 +1,99 @@
1
+ import { StdioOptions, spawn } from "node:child_process";
2
+ //#region src/locate-qq.d.ts
3
+ /** QQ 安装目录探测结果。 */
4
+ interface QqInstallInfo {
5
+ /** QQ.exe 绝对路径。 */
6
+ qqPath: string;
7
+ /** 安装根目录。 */
8
+ installDir: string;
9
+ /** 当前版本目录名(如 9.9.31-49919)。 */
10
+ version: string;
11
+ /** wrapper.node 绝对路径。 */
12
+ wrapperPath: string;
13
+ }
14
+ /** 找 QQ.exe 路径。 */
15
+ declare function locateQqPath(): string;
16
+ /** 从 QQ.exe 路径推导安装目录,并探测当前版本。 */
17
+ declare function resolveQqInstall(qqPath?: string): QqInstallInfo;
18
+ //#endregion
19
+ //#region src/launcher.d.ts
20
+ interface LaunchOptions {
21
+ /** QQ 安装信息(qqPath/version/wrapperPath)。 */
22
+ qq: QqInstallInfo;
23
+ /** kernel 入口(.mjs,self-host.cjs 里 import)。 */
24
+ kernelEntry: string;
25
+ /** 配置目录(账号数据目录,NAPUTO_CFG_DIR)。 */
26
+ cfgDir: string;
27
+ /** 全局配置文件路径(项目根 napuketto.toml,注入 NAPKETTO_CONFIG 供装配链读取)。 */
28
+ configPath?: string;
29
+ /** adapter 入口(.mjs,协议装配用)。 */
30
+ adapterEntry?: string;
31
+ /** network 入口(.mjs,协议装配用)。 */
32
+ networkEntry?: string;
33
+ /** 自建宿主:标准 node + stub QQNT.dll 直接引导,不拉起 QQ(2026-08-07 唯一路线)。 */
34
+ selfHost?: boolean;
35
+ /** stub QQNT.dll 目录(PATH 前置,转发 napi_*、uv_* 符号到 node.exe)。 */
36
+ stubDir?: string;
37
+ /** 强制指定快速登录账号(注入 NAPUTO_QUICK_UIN;cli `-q <uin>` 透传,2026-08-07)。 */
38
+ quickUin?: string;
39
+ /** 自建宿主入口(默认 dist/host/self-host.cjs)。 */
40
+ selfHostEntry?: string;
41
+ /**
42
+ * 子进程工作目录(缺省继承父进程 cwd)。
43
+ *
44
+ * ⚠️ 必须指向数据根(~/.napuketto):QQ 原生层在账号上下文就绪前会
45
+ * fallback 到进程 cwd 落盘(如频道库 guild1.db),不指定则污染调用方
46
+ * 目录(实测写入项目根,guild1.db 08-07)。cli 传 dataRoot 根治。
47
+ */
48
+ cwd?: string;
49
+ /**
50
+ * 子进程 stdio(默认 "inherit")。cli 传 ["inherit","pipe","pipe"] 并接管
51
+ * 子进程 stdout/stderr:逐行过滤 wrapper 原生噪音(MMKV 刷屏、Node 符号
52
+ * 查找失败警告),其余转发——原生 printf 直写 fd 的字节无法从 JS 层拦截。
53
+ */
54
+ stdio?: StdioOptions;
55
+ }
56
+ interface LaunchResult {
57
+ /** 子进程句柄(自建宿主 node 进程)。 */
58
+ child: ReturnType<typeof spawn>;
59
+ /** 自建宿主入口路径(self-host.cjs)。 */
60
+ bootJsPath: string;
61
+ }
62
+ /**
63
+ * 启动自建宿主(路线 A,2026-08-07 产品化):spawn 标准 node 直接跑 self-host.cjs,
64
+ * 不拉起 QQ / 不注入。
65
+ *
66
+ * 关键(HANDOVER-V7 技术数据):wrapper.node 从 QQNT.dll 导入 99 符号
67
+ * (napi_*×40 + uv_*×56 + qq_magic×1 + v8/node mangled×2)——标准 node 无这些宿主
68
+ * 符号,必须 PATH 前置 stub QQNT.dll 目录(转发到 node.exe)+ QQ resources\app
69
+ * (wrapper.node 同目录依赖 DLL)才能 dlopen 成功。
70
+ */
71
+ declare function launchSelfHost(options: LaunchOptions): LaunchResult;
72
+ /**
73
+ * 默认 stub QQNT.dll 目录(loader 包内闭源 native/build/stub-test-env,开发机默认)。
74
+ * src 与 native 同层,构建后 dist 与 native 同层——`../native` 恒正确。
75
+ */
76
+ declare function defaultStubDir(): string;
77
+ /** 环境变量名(self-host.cjs 与 kernel 引导读取)。 */
78
+ declare const ENV: {
79
+ readonly QQ_PATH: "NAPUTO_QQ_PATH";
80
+ readonly KERNEL_ENTRY: "NAPUTO_KERNEL_ENTRY";
81
+ readonly CFG_DIR: "NAPUTO_CFG_DIR";
82
+ readonly QQ_VERSION: "NAPUTO_QQ_VERSION";
83
+ readonly WRAPPER_PATH: "NAPUTO_WRAPPER_PATH";
84
+ /** 自建宿主:标准 node + stub QQNT.dll 引导(self-host.cjs 分支)。 */
85
+ readonly SELF_HOST: "NAPUTO_SELF_HOST";
86
+ /** stub QQNT.dll 目录(launchSelfHost 注入,self-host.cjs 诊断用)。 */
87
+ readonly STUB_DIR: "NAPUTO_STUB_DIR";
88
+ /** adapter 包入口(协议装配用)。 */
89
+ readonly ADAPTER_ENTRY: "NAPUTO_ADAPTER_ENTRY";
90
+ /** network 包入口(协议装配用)。 */
91
+ readonly NETWORK_ENTRY: "NAPUTO_NETWORK_ENTRY";
92
+ /** 全局配置文件路径(项目根 napuketto.toml,boot-protocols 读取)。 */
93
+ readonly CONFIG_PATH: "NAPKETTO_CONFIG";
94
+ /** 强制指定快速登录账号(cli `-q <uin>` 透传,bootstrap 登录用)。 */
95
+ readonly QUICK_UIN: "NAPUTO_QUICK_UIN";
96
+ };
97
+ //#endregion
98
+ export { ENV, type LaunchOptions, type LaunchResult, type QqInstallInfo, defaultStubDir, launchSelfHost, locateQqPath, resolveQqInstall };
99
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/locate-qq.ts","../src/launcher.ts"],"mappings":";;;UAuDiB;;EAEb;;EAEA;;EAEA;;EAEA;;;iBAIY;;iBAaA,iBAAiB,kBAAkB;;;UC9DlC;;EAEb,IAAI;;EAEJ;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;;;;;;;EAQA;;;;;;EAMA,QAAQ;;UAGK;;EAEb,OAAO,kBAAkB;;EAEzB;;;;;;;;;;;iBAYY,eAAe,SAAS,gBAAgB;;;;;iBAiDxC;;cAqCH;WACT;WACA;WACA;WACA;WACA;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA"}
package/dist/index.mjs ADDED
@@ -0,0 +1,159 @@
1
+ import { execFileSync, spawn } from "node:child_process";
2
+ import { existsSync, mkdirSync, readdirSync, statSync } from "node:fs";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import process from "node:process";
5
+ import { fileURLToPath } from "node:url";
6
+ //#region src/launcher.ts
7
+ /**
8
+ * launcher:自建宿主引导(2026-08-07 唯一路线)——spawn 标准 node 跑 self-host.cjs。
9
+ *
10
+ * 历史(V1/路线 B 事实链,已归档 archive/):
11
+ * 1. wrapper.node 只能在 QQ 定制版 Electron 里注册(实测纯 Node/普通 Electron 均 self-register 失败)
12
+ * 2. QQ 是打包应用,禁 NODE_OPTIONS(实测 stderr)
13
+ * → 旧方案:NapukettoBootMain.exe 启动 QQ + 注入 NapukettoWinBootHook.dll(已废弃)
14
+ * → 现方案:stub QQNT.dll 转发宿主符号 → 标准 node 直接 dlopen wrapper.node(自建宿主)
15
+ */
16
+ const __dirname = dirname(fileURLToPath(import.meta.url));
17
+ /**
18
+ * 启动自建宿主(路线 A,2026-08-07 产品化):spawn 标准 node 直接跑 self-host.cjs,
19
+ * 不拉起 QQ / 不注入。
20
+ *
21
+ * 关键(HANDOVER-V7 技术数据):wrapper.node 从 QQNT.dll 导入 99 符号
22
+ * (napi_*×40 + uv_*×56 + qq_magic×1 + v8/node mangled×2)——标准 node 无这些宿主
23
+ * 符号,必须 PATH 前置 stub QQNT.dll 目录(转发到 node.exe)+ QQ resources\app
24
+ * (wrapper.node 同目录依赖 DLL)才能 dlopen 成功。
25
+ */
26
+ function launchSelfHost(options) {
27
+ const selfHostPath = options.selfHostEntry ?? join(__dirname, "..", "dist", "host", "self-host.cjs");
28
+ if (!existsSync(selfHostPath)) throw new Error(`self-host.cjs 缺失: ${selfHostPath}(先运行 pnpm --filter @napuketto/loader build)`);
29
+ const stub = options.stubDir ?? defaultStubDir();
30
+ if (!existsSync(join(stub, "QQNT.dll"))) throw new Error(`stub QQNT.dll 未找到: ${stub}(自建宿主必需:stub 转发 QQNT.dll 符号到 node.exe;请用 --stub-dir 或环境变量 NAPUTO_STUB_DIR 指定 stub 目录)`);
31
+ const env = buildLaunchEnv(options);
32
+ env["PATH"] = [
33
+ stub,
34
+ dirname(options.qq.wrapperPath),
35
+ process.env["PATH"] ?? ""
36
+ ].filter((p) => p !== void 0 && p !== "").join(";");
37
+ env[ENV.STUB_DIR] = stub;
38
+ if (options.cwd !== void 0) mkdirSync(options.cwd, { recursive: true });
39
+ return {
40
+ child: spawn(process.execPath, [selfHostPath], {
41
+ ...options.cwd !== void 0 ? { cwd: options.cwd } : {},
42
+ env,
43
+ stdio: options.stdio ?? "inherit",
44
+ windowsHide: false
45
+ }),
46
+ bootJsPath: selfHostPath
47
+ };
48
+ }
49
+ /**
50
+ * 默认 stub QQNT.dll 目录(loader 包内闭源 native/build/stub-test-env,开发机默认)。
51
+ * src 与 native 同层,构建后 dist 与 native 同层——`../native` 恒正确。
52
+ */
53
+ function defaultStubDir() {
54
+ return join(__dirname, "..", "native", "build", "stub-test-env");
55
+ }
56
+ /** 装配自建宿主环境变量。 */
57
+ function buildLaunchEnv(options) {
58
+ const cfg = resolve(options.cfgDir);
59
+ mkdirSync(cfg, { recursive: true });
60
+ const env = {
61
+ ...process.env,
62
+ [ENV.QQ_PATH]: options.qq.qqPath,
63
+ [ENV.KERNEL_ENTRY]: resolve(options.kernelEntry),
64
+ [ENV.CFG_DIR]: cfg,
65
+ [ENV.QQ_VERSION]: options.qq.version,
66
+ [ENV.WRAPPER_PATH]: options.qq.wrapperPath
67
+ };
68
+ if (options.selfHost === true) env[ENV.SELF_HOST] = "1";
69
+ if (options.adapterEntry !== void 0) env[ENV.ADAPTER_ENTRY] = resolve(options.adapterEntry);
70
+ if (options.networkEntry !== void 0) env[ENV.NETWORK_ENTRY] = resolve(options.networkEntry);
71
+ if (options.configPath !== void 0) env[ENV.CONFIG_PATH] = resolve(options.configPath);
72
+ if (options.quickUin !== void 0) env[ENV.QUICK_UIN] = options.quickUin;
73
+ return env;
74
+ }
75
+ /** 环境变量名(self-host.cjs 与 kernel 引导读取)。 */
76
+ const ENV = {
77
+ QQ_PATH: "NAPUTO_QQ_PATH",
78
+ KERNEL_ENTRY: "NAPUTO_KERNEL_ENTRY",
79
+ CFG_DIR: "NAPUTO_CFG_DIR",
80
+ QQ_VERSION: "NAPUTO_QQ_VERSION",
81
+ WRAPPER_PATH: "NAPUTO_WRAPPER_PATH",
82
+ /** 自建宿主:标准 node + stub QQNT.dll 引导(self-host.cjs 分支)。 */
83
+ SELF_HOST: "NAPUTO_SELF_HOST",
84
+ /** stub QQNT.dll 目录(launchSelfHost 注入,self-host.cjs 诊断用)。 */
85
+ STUB_DIR: "NAPUTO_STUB_DIR",
86
+ /** adapter 包入口(协议装配用)。 */
87
+ ADAPTER_ENTRY: "NAPUTO_ADAPTER_ENTRY",
88
+ /** network 包入口(协议装配用)。 */
89
+ NETWORK_ENTRY: "NAPUTO_NETWORK_ENTRY",
90
+ /** 全局配置文件路径(项目根 napuketto.toml,boot-protocols 读取)。 */
91
+ CONFIG_PATH: "NAPKETTO_CONFIG",
92
+ /** 强制指定快速登录账号(cli `-q <uin>` 透传,bootstrap 登录用)。 */
93
+ QUICK_UIN: "NAPUTO_QUICK_UIN"
94
+ };
95
+ //#endregion
96
+ //#region src/locate-qq.ts
97
+ /**
98
+ * 定位 QQ 安装目录与版本(复用 kernel wrapper-version 的探测逻辑)
99
+ */
100
+ /** 注册表 UninstallString 查询(QQ 官方安装路径)。 */
101
+ const REG_QUERY_RE = /"([^"]+)"/;
102
+ /** 注册表 UninstallString 查询(QQ 官方安装路径)。 */
103
+ function findQqViaRegistry() {
104
+ try {
105
+ const exe = execFileSync("reg", [
106
+ "query",
107
+ "HKLM\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\QQ",
108
+ "/v",
109
+ "UninstallString"
110
+ ]).toString().match(REG_QUERY_RE)?.[1];
111
+ if (exe !== void 0) {
112
+ const dir = exe.replaceAll("\"", "").slice(0, exe.lastIndexOf("\\"));
113
+ const qq = join(dir, "QQ.exe");
114
+ if (existsSync(qq)) return qq;
115
+ }
116
+ } catch {}
117
+ return null;
118
+ }
119
+ /** 常见安装路径探测。 */
120
+ function findQqViaCommonPaths() {
121
+ const localAppData = process.env["LOCALAPPDATA"] ?? "";
122
+ const candidates = [
123
+ "C:/Program Files/Tencent/QQNT/QQ.exe",
124
+ "C:/Program Files (x86)/Tencent/QQNT/QQ.exe",
125
+ join(localAppData, "Programs", "Tencent", "QQNT", "QQ.exe"),
126
+ "C:/Dev/QQBot-Dev/QQNT/QQ.exe"
127
+ ];
128
+ for (const p of candidates) if (existsSync(p)) return p;
129
+ return null;
130
+ }
131
+ /** 找 QQ.exe 路径。 */
132
+ function locateQqPath() {
133
+ const viaReg = findQqViaRegistry();
134
+ if (viaReg) return viaReg;
135
+ const viaCommon = findQqViaCommonPaths();
136
+ if (viaCommon) return viaCommon;
137
+ throw new Error("未找到 QQ.exe(请先安装 QQ,或通过 NAPUTO_QQ_PATH 指定)");
138
+ }
139
+ /** 从 QQ.exe 路径推导安装目录,并探测当前版本。 */
140
+ function resolveQqInstall(qqPath) {
141
+ const qq = qqPath ?? process.env["NAPUTO_QQ_PATH"] ?? locateQqPath();
142
+ const installDir = qq.slice(0, qq.lastIndexOf("\\"));
143
+ const versionsDir = join(installDir, "versions");
144
+ if (!existsSync(versionsDir)) throw new Error(`QQ 版本目录不存在: ${versionsDir}`);
145
+ const [first] = readdirSync(versionsDir).filter((v) => statSync(join(versionsDir, v)).isDirectory()).sort().reverse();
146
+ if (first === void 0) throw new Error(`QQ 版本目录为空: ${versionsDir}`);
147
+ const wrapperPath = join(versionsDir, first, "resources", "app", "wrapper.node");
148
+ if (!existsSync(wrapperPath)) throw new Error(`未找到 wrapper.node: ${wrapperPath}`);
149
+ return {
150
+ qqPath: qq,
151
+ installDir,
152
+ version: first,
153
+ wrapperPath
154
+ };
155
+ }
156
+ //#endregion
157
+ export { ENV, defaultStubDir, launchSelfHost, locateQqPath, resolveQqInstall };
158
+
159
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/launcher.ts","../src/locate-qq.ts"],"sourcesContent":["/**\n * launcher:自建宿主引导(2026-08-07 唯一路线)——spawn 标准 node 跑 self-host.cjs。\n *\n * 历史(V1/路线 B 事实链,已归档 archive/):\n * 1. wrapper.node 只能在 QQ 定制版 Electron 里注册(实测纯 Node/普通 Electron 均 self-register 失败)\n * 2. QQ 是打包应用,禁 NODE_OPTIONS(实测 stderr)\n * → 旧方案:NapukettoBootMain.exe 启动 QQ + 注入 NapukettoWinBootHook.dll(已废弃)\n * → 现方案:stub QQNT.dll 转发宿主符号 → 标准 node 直接 dlopen wrapper.node(自建宿主)\n */\nimport { type StdioOptions, spawn } from \"node:child_process\";\nimport { existsSync, mkdirSync } from \"node:fs\";\nimport { dirname, join, resolve } from \"node:path\";\nimport process from \"node:process\";\nimport { fileURLToPath } from \"node:url\";\nimport type { QqInstallInfo } from \"./locate-qq.js\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\nexport interface LaunchOptions {\n /** QQ 安装信息(qqPath/version/wrapperPath)。 */\n qq: QqInstallInfo;\n /** kernel 入口(.mjs,self-host.cjs 里 import)。 */\n kernelEntry: string;\n /** 配置目录(账号数据目录,NAPUTO_CFG_DIR)。 */\n cfgDir: string;\n /** 全局配置文件路径(项目根 napuketto.toml,注入 NAPKETTO_CONFIG 供装配链读取)。 */\n configPath?: string;\n /** adapter 入口(.mjs,协议装配用)。 */\n adapterEntry?: string;\n /** network 入口(.mjs,协议装配用)。 */\n networkEntry?: string;\n /** 自建宿主:标准 node + stub QQNT.dll 直接引导,不拉起 QQ(2026-08-07 唯一路线)。 */\n selfHost?: boolean;\n /** stub QQNT.dll 目录(PATH 前置,转发 napi_*、uv_* 符号到 node.exe)。 */\n stubDir?: string;\n /** 强制指定快速登录账号(注入 NAPUTO_QUICK_UIN;cli `-q <uin>` 透传,2026-08-07)。 */\n quickUin?: string;\n /** 自建宿主入口(默认 dist/host/self-host.cjs)。 */\n selfHostEntry?: string;\n /**\n * 子进程工作目录(缺省继承父进程 cwd)。\n *\n * ⚠️ 必须指向数据根(~/.napuketto):QQ 原生层在账号上下文就绪前会\n * fallback 到进程 cwd 落盘(如频道库 guild1.db),不指定则污染调用方\n * 目录(实测写入项目根,guild1.db 08-07)。cli 传 dataRoot 根治。\n */\n cwd?: string;\n /**\n * 子进程 stdio(默认 \"inherit\")。cli 传 [\"inherit\",\"pipe\",\"pipe\"] 并接管\n * 子进程 stdout/stderr:逐行过滤 wrapper 原生噪音(MMKV 刷屏、Node 符号\n * 查找失败警告),其余转发——原生 printf 直写 fd 的字节无法从 JS 层拦截。\n */\n stdio?: StdioOptions;\n}\n\nexport interface LaunchResult {\n /** 子进程句柄(自建宿主 node 进程)。 */\n child: ReturnType<typeof spawn>;\n /** 自建宿主入口路径(self-host.cjs)。 */\n bootJsPath: string;\n}\n\n/**\n * 启动自建宿主(路线 A,2026-08-07 产品化):spawn 标准 node 直接跑 self-host.cjs,\n * 不拉起 QQ / 不注入。\n *\n * 关键(HANDOVER-V7 技术数据):wrapper.node 从 QQNT.dll 导入 99 符号\n * (napi_*×40 + uv_*×56 + qq_magic×1 + v8/node mangled×2)——标准 node 无这些宿主\n * 符号,必须 PATH 前置 stub QQNT.dll 目录(转发到 node.exe)+ QQ resources\\app\n * (wrapper.node 同目录依赖 DLL)才能 dlopen 成功。\n */\nexport function launchSelfHost(options: LaunchOptions): LaunchResult {\n const selfHostPath =\n options.selfHostEntry ?? join(__dirname, \"..\", \"dist\", \"host\", \"self-host.cjs\");\n if (!existsSync(selfHostPath)) {\n throw new Error(\n `self-host.cjs 缺失: ${selfHostPath}(先运行 pnpm --filter @napuketto/loader build)`,\n );\n }\n\n // stub QQNT.dll 校验(自建宿主必需:PATH 前置 stub 转发 napi_* 到 node.exe,\n // 否则 wrapper.node dlopen 失败)。默认 loader 包内闭源 submodule native/stub-test-env,\n // 缺失时提示 --stub-dir / NAPUTO_STUB_DIR 指定。\n const stub = options.stubDir ?? defaultStubDir();\n if (!existsSync(join(stub, \"QQNT.dll\"))) {\n throw new Error(\n `stub QQNT.dll 未找到: ${stub}(自建宿主必需:stub 转发 QQNT.dll 符号到 node.exe;` +\n \"请用 --stub-dir 或环境变量 NAPUTO_STUB_DIR 指定 stub 目录)\",\n );\n }\n\n const env = buildLaunchEnv(options);\n // PATH 前置 stub 目录(stub QQNT.dll 转发)+ QQ resources\\app(wrapper.node 依赖 DLL)\n const pathEntries = [stub, dirname(options.qq.wrapperPath), process.env[\"PATH\"] ?? \"\"]\n .filter((p) => p !== undefined && p !== \"\")\n .join(\";\");\n env[\"PATH\"] = pathEntries;\n env[ENV.STUB_DIR] = stub;\n\n // 标准 node 直接跑入口(不拉起 QQ,不注入)\n // cwd 显式指向数据根(cli 传入):QQ 原生层 fallback 落盘(guild1.db 等)\n // 不再污染项目根目录(实测 08-07,GPro 模块初始化早于账号上下文)。\n if (options.cwd !== undefined) {\n mkdirSync(options.cwd, { recursive: true });\n }\n const child = spawn(process.execPath, [selfHostPath], {\n // exactOptionalPropertyTypes:未传 cwd 时不显式写入 undefined(继承父进程)\n ...(options.cwd !== undefined ? { cwd: options.cwd } : {}),\n env,\n stdio: options.stdio ?? \"inherit\",\n windowsHide: false,\n });\n\n return { child, bootJsPath: selfHostPath };\n}\n\n/**\n * 默认 stub QQNT.dll 目录(loader 包内闭源 native/build/stub-test-env,开发机默认)。\n * src 与 native 同层,构建后 dist 与 native 同层——`../native` 恒正确。\n */\nexport function defaultStubDir(): string {\n return join(__dirname, \"..\", \"native\", \"build\", \"stub-test-env\");\n}\n\n/** 装配自建宿主环境变量。 */\nfunction buildLaunchEnv(options: LaunchOptions): Record<string, string> {\n // 配置目录兜底\n const cfg = resolve(options.cfgDir);\n mkdirSync(cfg, { recursive: true });\n\n const env: Record<string, string> = {\n ...process.env,\n [ENV.QQ_PATH]: options.qq.qqPath,\n [ENV.KERNEL_ENTRY]: resolve(options.kernelEntry),\n [ENV.CFG_DIR]: cfg,\n [ENV.QQ_VERSION]: options.qq.version,\n [ENV.WRAPPER_PATH]: options.qq.wrapperPath,\n };\n if (options.selfHost === true) {\n env[ENV.SELF_HOST] = \"1\";\n }\n if (options.adapterEntry !== undefined) {\n env[ENV.ADAPTER_ENTRY] = resolve(options.adapterEntry);\n }\n if (options.networkEntry !== undefined) {\n env[ENV.NETWORK_ENTRY] = resolve(options.networkEntry);\n }\n if (options.configPath !== undefined) {\n env[ENV.CONFIG_PATH] = resolve(options.configPath);\n }\n if (options.quickUin !== undefined) {\n env[ENV.QUICK_UIN] = options.quickUin;\n }\n return env;\n}\n\n/** 环境变量名(self-host.cjs 与 kernel 引导读取)。 */\nexport const ENV = {\n QQ_PATH: \"NAPUTO_QQ_PATH\",\n KERNEL_ENTRY: \"NAPUTO_KERNEL_ENTRY\",\n CFG_DIR: \"NAPUTO_CFG_DIR\",\n QQ_VERSION: \"NAPUTO_QQ_VERSION\",\n WRAPPER_PATH: \"NAPUTO_WRAPPER_PATH\",\n /** 自建宿主:标准 node + stub QQNT.dll 引导(self-host.cjs 分支)。 */\n SELF_HOST: \"NAPUTO_SELF_HOST\",\n /** stub QQNT.dll 目录(launchSelfHost 注入,self-host.cjs 诊断用)。 */\n STUB_DIR: \"NAPUTO_STUB_DIR\",\n /** adapter 包入口(协议装配用)。 */\n ADAPTER_ENTRY: \"NAPUTO_ADAPTER_ENTRY\",\n /** network 包入口(协议装配用)。 */\n NETWORK_ENTRY: \"NAPUTO_NETWORK_ENTRY\",\n /** 全局配置文件路径(项目根 napuketto.toml,boot-protocols 读取)。 */\n CONFIG_PATH: \"NAPKETTO_CONFIG\",\n /** 强制指定快速登录账号(cli `-q <uin>` 透传,bootstrap 登录用)。 */\n QUICK_UIN: \"NAPUTO_QUICK_UIN\",\n} as const;\n","/**\n * 定位 QQ 安装目录与版本(复用 kernel wrapper-version 的探测逻辑)\n */\nimport { execFileSync } from \"node:child_process\";\nimport { existsSync, readdirSync, statSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport process from \"node:process\";\n\n/** 注册表 UninstallString 查询(QQ 官方安装路径)。 */\nconst REG_QUERY_RE = /\"([^\"]+)\"/;\n\n/** 注册表 UninstallString 查询(QQ 官方安装路径)。 */\nfunction findQqViaRegistry(): string | null {\n try {\n const out = execFileSync(\"reg\", [\n \"query\",\n \"HKLM\\\\SOFTWARE\\\\WOW6432Node\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Uninstall\\\\QQ\",\n \"/v\",\n \"UninstallString\",\n ]).toString();\n const m = out.match(REG_QUERY_RE);\n const exe = m?.[1];\n if (exe !== undefined) {\n // UninstallString 形如 \"C:\\...\\uninst.exe\" 或带参数\n const dir = exe.replaceAll('\"', \"\").slice(0, exe.lastIndexOf(\"\\\\\"));\n const qq = join(dir, \"QQ.exe\");\n if (existsSync(qq)) {\n return qq;\n }\n }\n } catch {\n // 注册表查询失败,走常见路径\n }\n return null;\n}\n\n/** 常见安装路径探测。 */\nfunction findQqViaCommonPaths(): string | null {\n const localAppData = process.env[\"LOCALAPPDATA\"] ?? \"\";\n const candidates = [\n \"C:/Program Files/Tencent/QQNT/QQ.exe\",\n \"C:/Program Files (x86)/Tencent/QQNT/QQ.exe\",\n join(localAppData, \"Programs\", \"Tencent\", \"QQNT\", \"QQ.exe\"),\n // 开发机目录(2026-08-07 环境事实:QQ 9.9.33-51802 在 C:\\Dev\\QQBot-Dev\\QQNT)\n \"C:/Dev/QQBot-Dev/QQNT/QQ.exe\",\n ];\n for (const p of candidates) {\n if (existsSync(p)) {\n return p;\n }\n }\n return null;\n}\n\n/** QQ 安装目录探测结果。 */\nexport interface QqInstallInfo {\n /** QQ.exe 绝对路径。 */\n qqPath: string;\n /** 安装根目录。 */\n installDir: string;\n /** 当前版本目录名(如 9.9.31-49919)。 */\n version: string;\n /** wrapper.node 绝对路径。 */\n wrapperPath: string;\n}\n\n/** 找 QQ.exe 路径。 */\nexport function locateQqPath(): string {\n const viaReg = findQqViaRegistry();\n if (viaReg) {\n return viaReg;\n }\n const viaCommon = findQqViaCommonPaths();\n if (viaCommon) {\n return viaCommon;\n }\n throw new Error(\"未找到 QQ.exe(请先安装 QQ,或通过 NAPUTO_QQ_PATH 指定)\");\n}\n\n/** 从 QQ.exe 路径推导安装目录,并探测当前版本。 */\nexport function resolveQqInstall(qqPath?: string): QqInstallInfo {\n const qq = qqPath ?? process.env[\"NAPUTO_QQ_PATH\"] ?? locateQqPath();\n const installDir = qq.slice(0, qq.lastIndexOf(\"\\\\\"));\n const versionsDir = join(installDir, \"versions\");\n if (!existsSync(versionsDir)) {\n throw new Error(`QQ 版本目录不存在: ${versionsDir}`);\n }\n const versions = readdirSync(versionsDir)\n .filter((v) => statSync(join(versionsDir, v)).isDirectory())\n .sort()\n .reverse();\n const [first] = versions;\n if (first === undefined) {\n throw new Error(`QQ 版本目录为空: ${versionsDir}`);\n }\n const wrapperPath = join(versionsDir, first, \"resources\", \"app\", \"wrapper.node\");\n if (!existsSync(wrapperPath)) {\n throw new Error(`未找到 wrapper.node: ${wrapperPath}`);\n }\n return { qqPath: qq, installDir, version: first, wrapperPath };\n}\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@napuketto/loader",
3
+ "version": "0.0.1",
4
+ "description": "NapukettoQQ 引导组件:自建宿主(标准 Node + stub QQNT.dll)dlopen wrapper.node 并启动 kernel,不拉起 QQ / 不注入",
5
+ "type": "module",
6
+ "files": [
7
+ "dist",
8
+ "native/build/stub-test-env"
9
+ ],
10
+ "main": "./dist/index.mjs",
11
+ "module": "./dist/index.mjs",
12
+ "types": "./dist/index.d.mts",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.mts",
16
+ "import": "./dist/index.mjs"
17
+ }
18
+ },
19
+ "devDependencies": {
20
+ "@types/node": "^24.0.0",
21
+ "tsdown": "^0.22.14"
22
+ },
23
+ "license": "MIT",
24
+ "scripts": {
25
+ "dev": "tsdown --watch",
26
+ "build": "tsdown"
27
+ }
28
+ }