@napuketto/loader 0.0.9 → 0.0.10

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.cjs ADDED
@@ -0,0 +1,849 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region \0rolldown/runtime.js
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
+ get: ((k) => from[k]).bind(null, key),
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
20
+ value: mod,
21
+ enumerable: true
22
+ }) : target, mod));
23
+ //#endregion
24
+ let node_fs = require("node:fs");
25
+ let node_path = require("node:path");
26
+ let node_process = require("node:process");
27
+ node_process = __toESM(node_process, 1);
28
+ let node_child_process = require("node:child_process");
29
+ let node_url = require("node:url");
30
+ let node_fs_promises = require("node:fs/promises");
31
+ let node_crypto = require("node:crypto");
32
+ let node_https = require("node:https");
33
+ //#region src/instance-lock.ts
34
+ /**
35
+ * instance-lock.ts:单实例锁(2026-08-07 根治「多实例抢数据目录锁挂起」)
36
+ *
37
+ * 背景(实测):同一账号数据目录(~/.napuketto/<uin>)只能被一个实例持有——
38
+ * QQ 原生层(MMKV / 登录单例 / 数据库)有锁,第二个实例抢不到锁会卡在
39
+ * loginService.initConfig 之后无响应(挂起)。此前 create-napukettoqq 生成的
40
+ * 副本实例常驻占锁,导致用户后续启动全部卡死。
41
+ *
42
+ * 方案:以数据目录为粒度加锁文件 `<dataDir>/instance.lock`(JSON 内容:
43
+ * pid / startedAt / cmdline 摘要)。启动前 checkInstanceLock:
44
+ * - 锁不存在 → 可启动(然后 acquireInstanceLock 写入)
45
+ * - 锁存在但 PID 已死 → 崩溃残留,自动清理(rm + 重新获取)
46
+ * - 锁存在且 PID 存活 → 已被占用,拒绝启动(返回占用 PID 供提示)
47
+ *
48
+ * 退出清理:进程正常退出时 releaseInstanceLock 删除锁文件;崩溃/强杀残留
49
+ * 由下次启动的「PID 存活检测」兜底(PID 已死 → 残留视为过期)。
50
+ *
51
+ * 放置说明:放 loader 而非 kernel——self-host.cjs 是 rolldown 内联 bundle
52
+ * (只内联 src/host/ 依赖树),静态 import kernel 会留 external 缺口;
53
+ * 放 loader 则 self-host(内联)与 cli(@napuketto/loader 导入)双入口可用。
54
+ */
55
+ /** 锁文件名(置于数据目录内)。 */
56
+ const INSTANCE_LOCK_FILE = "instance.lock";
57
+ /**
58
+ * 检测指定数据目录是否已被其他实例占用。
59
+ *
60
+ * @param dataDir 数据目录(账号级,如 ~/.napuketto/3567141148)
61
+ */
62
+ function checkInstanceLock(dataDir) {
63
+ const lockPath = (0, node_path.join)(dataDir, INSTANCE_LOCK_FILE);
64
+ let info = null;
65
+ try {
66
+ info = JSON.parse((0, node_fs.readFileSync)(lockPath, "utf-8"));
67
+ } catch {
68
+ return {
69
+ occupied: false,
70
+ pid: null,
71
+ lockPath
72
+ };
73
+ }
74
+ if (typeof info.pid !== "number" || Number.isNaN(info.pid) || info.pid <= 0) return {
75
+ occupied: false,
76
+ pid: null,
77
+ lockPath
78
+ };
79
+ let alive = false;
80
+ try {
81
+ node_process.default.kill(info.pid, 0);
82
+ alive = true;
83
+ } catch {
84
+ alive = false;
85
+ }
86
+ if (!alive) return {
87
+ occupied: false,
88
+ pid: null,
89
+ lockPath
90
+ };
91
+ return {
92
+ occupied: true,
93
+ pid: info.pid,
94
+ lockPath
95
+ };
96
+ }
97
+ /**
98
+ * 获取数据目录锁。占用(其他实例存活)返回 false;成功或清理残留后返回 true。
99
+ * 幂等:本进程已持有则直接返回 true(不重复写)。
100
+ */
101
+ function acquireInstanceLock(dataDir, cmdline) {
102
+ const { occupied, pid, lockPath } = checkInstanceLock(dataDir);
103
+ if (occupied) {
104
+ if (pid === node_process.default.pid) return true;
105
+ return false;
106
+ }
107
+ const info = {
108
+ pid: node_process.default.pid,
109
+ startedAt: Date.now(),
110
+ ...cmdline !== void 0 && cmdline !== "" ? { cmdline } : {}
111
+ };
112
+ try {
113
+ (0, node_fs.rmSync)(lockPath, { force: true });
114
+ (0, node_fs.writeFileSync)(lockPath, JSON.stringify(info), "utf-8");
115
+ return true;
116
+ } catch {
117
+ return false;
118
+ }
119
+ }
120
+ /**
121
+ * 释放数据目录锁(正常退出时调用)。仅当持有者是本进程时才删除,
122
+ * 避免误删新持有者的锁(PID 复用 / 快速重启场景)。
123
+ */
124
+ function releaseInstanceLock(dataDir) {
125
+ const lockPath = (0, node_path.join)(dataDir, INSTANCE_LOCK_FILE);
126
+ try {
127
+ if (JSON.parse((0, node_fs.readFileSync)(lockPath, "utf-8")).pid === node_process.default.pid) (0, node_fs.rmSync)(lockPath, { force: true });
128
+ } catch {}
129
+ }
130
+ /** 注册进程退出清理(SIGINT/SIGTERM/exit 均触发;崩溃强杀由残留检测兜底)。 */
131
+ function registerLockCleanup(dataDir) {
132
+ node_process.default.on("exit", () => {
133
+ releaseInstanceLock(dataDir);
134
+ });
135
+ }
136
+ //#endregion
137
+ //#region src/qq-download.ts
138
+ /**
139
+ * qq-download.ts:QQ 官方安装包下载器(P1)。
140
+ *
141
+ * 无第三方依赖:Node 内置 https + crypto(sha256)。设计文档 §2.2。
142
+ * - https GET 流式写文件(低内存占用,313MB 安装包不整载入内存)
143
+ * - 下载同时流式累加 sha256,完成后与清单比对(防链接漂移劫持 / 下载不完整)
144
+ * - 支持 302 重定向(QQ CDN 可能跳转)
145
+ * - NAPUTO_QQ_URL 环境变量可覆盖下载地址(用户拿到新链接时用)
146
+ */
147
+ /** 下载中断(超时 / 非 2xx / 网络错误)抛出的错误。 */
148
+ var DownloadError = class extends Error {
149
+ constructor(message) {
150
+ super(message);
151
+ this.name = "DownloadError";
152
+ }
153
+ };
154
+ /** 发起一次 https GET,返回响应(跟随重定向,最多 5 跳)。 */
155
+ function httpsGet(url, timeoutMs) {
156
+ return new Promise((resolve, reject) => {
157
+ const request = (currentUrl, redirects) => {
158
+ const req = (0, node_https.get)(currentUrl, { headers: { "user-agent": "napuketto-loader/0.0.x" } }, (res) => {
159
+ const status = res.statusCode ?? 0;
160
+ if (status >= 300 && status < 400 && res.headers["location"] !== void 0) {
161
+ res.resume();
162
+ if (redirects >= 5) {
163
+ reject(new DownloadError(`重定向次数过多: ${currentUrl}`));
164
+ return;
165
+ }
166
+ request(new URL(res.headers["location"], currentUrl).toString(), redirects + 1);
167
+ return;
168
+ }
169
+ if (status !== 200) {
170
+ res.resume();
171
+ reject(new DownloadError(`HTTP ${status}: ${currentUrl}`));
172
+ return;
173
+ }
174
+ resolve(res);
175
+ });
176
+ req.setTimeout(timeoutMs, () => {
177
+ req.destroy(new DownloadError(`下载超时(${timeoutMs}ms): ${currentUrl}`));
178
+ });
179
+ req.on("error", (err) => {
180
+ reject(err instanceof Error ? err : new DownloadError(String(err)));
181
+ });
182
+ };
183
+ request(url, 0);
184
+ });
185
+ }
186
+ /**
187
+ * 下载文件并(可选)校验 sha256。失败时清理半成品文件。
188
+ * 返回实际 sha256(无论是否校验),便于调用方回填清单。
189
+ */
190
+ async function downloadFile(options) {
191
+ const url = options.url ?? node_process.default.env["NAPUTO_QQ_URL"];
192
+ if (url === void 0 || url === "") throw new DownloadError("下载地址为空:请设置 NAPUTO_QQ_URL 或提供清单 URL");
193
+ const timeoutMs = options.timeoutMs ?? 6e4;
194
+ (0, node_fs.mkdirSync)((0, node_path.dirname)(options.dest), { recursive: true });
195
+ const hash = (0, node_crypto.createHash)("sha256");
196
+ const fileStream = (0, node_fs.createWriteStream)(options.dest);
197
+ let fileFailed = false;
198
+ try {
199
+ const res = await httpsGet(url, timeoutMs);
200
+ await new Promise((resolve, reject) => {
201
+ res.on("data", (chunk) => {
202
+ hash.update(chunk);
203
+ });
204
+ res.on("error", (err) => {
205
+ fileFailed = true;
206
+ reject(err instanceof Error ? err : new DownloadError(String(err)));
207
+ });
208
+ fileStream.on("error", (err) => {
209
+ fileFailed = true;
210
+ reject(err);
211
+ });
212
+ fileStream.on("finish", () => {
213
+ resolve();
214
+ });
215
+ res.pipe(fileStream);
216
+ });
217
+ } catch (err) {
218
+ fileStream.destroy();
219
+ if (!fileFailed) await (0, node_fs_promises.rm)(options.dest, { force: true });
220
+ throw err instanceof Error ? err : new DownloadError(String(err));
221
+ }
222
+ const sha256 = hash.digest("hex");
223
+ const expected = options.expectedSha256;
224
+ const verified = expected === void 0 || expected === "" ? true : sha256.toLowerCase() === expected.toLowerCase();
225
+ if (!verified) {
226
+ await (0, node_fs_promises.rm)(options.dest, { force: true });
227
+ throw new DownloadError(`sha256 校验失败: ${options.dest}\n期望 ${expected}\n实际 ${sha256}`);
228
+ }
229
+ return {
230
+ sha256,
231
+ verified
232
+ };
233
+ }
234
+ //#endregion
235
+ //#region src/qq-extract.ts
236
+ /**
237
+ * qq-extract.ts:QQ 官方安装包解包与提取(P1)。
238
+ *
239
+ * ⚠️ 格式事实(2026-08-12 实测修正):QQ 官方安装包是 NSIS 自解压格式,
240
+ * **7za standalone 不支持 NSIS**(只支持 7z/zip/tar 等)——必须用完整版
241
+ * 7z(7z.exe + 7z.dll)。设计文档 §2.3 原「内置 7za.exe」修正为:
242
+ * - Windows:内置 assets/7zip/7z.exe + 7z.dll(7-Zip 官方,LGPL 合规可分发)
243
+ * - Linux/Docker:系统 p7zip-full(7z 完整版)
244
+ *
245
+ * ⚠️ 提取策略(2026-08-12 实测修正,与设计 §2.3 不同):
246
+ * 1. NSIS 解包产物在 <extracted>/Files/(NSIS 内部布局),Files/ = 安装根
247
+ * 2. 真实 QQNT.dll(214MB)位于 versions/<v>/QQNT.dll,**自建宿主不需要**
248
+ * ——stub QQNT.dll 已替代其宿主符号职责(wrapper.node 的 v8/node/napi/qq_magic
249
+ * 符号由 stub 转发到 node.exe)
250
+ * 3. 只提取 resources/app 顶层 *.node + *.dll(wrapper.node 的直接/传递/delay-load
251
+ * 依赖全覆盖,版本无关;跳过 wmpfsdk/avsdk 等大子目录,共约 235MB)
252
+ *
253
+ * 提取目标(与安装目录同构,resolveFromRoot 可直接消费):
254
+ * <数据根>/qq-files/<版本>/versions/<版本>/resources/app/ ← *.node + *.dll
255
+ */
256
+ /** resources/app 下需要提取的文件扩展名(.node 原生模块 + .dll 依赖)。 */
257
+ const EXTRACT_EXTENSIONS = /* @__PURE__ */ new Set([".node", ".dll"]);
258
+ /**
259
+ * 探测 7z 可执行文件:
260
+ * NAPUTO_7Z_PATH 环境变量 > 内置资产(assets/7zip/7z.exe,Windows)> 系统 PATH 7z。
261
+ * Linux 上用 p7zip 提供的 7z 命令(系统 PATH)。
262
+ */
263
+ function findSevenZip() {
264
+ const envPath = node_process.default.env["NAPUTO_7Z_PATH"];
265
+ if (envPath !== void 0 && envPath !== "" && (0, node_fs.existsSync)(envPath)) return {
266
+ exe: envPath,
267
+ source: "环境变量 NAPUTO_7Z_PATH"
268
+ };
269
+ if (node_process.default.platform === "win32") {
270
+ const bundled = (0, node_path.join)((0, node_path.dirname)((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href)), "..", "assets", "7zip", "7z.exe");
271
+ if ((0, node_fs.existsSync)(bundled)) return {
272
+ exe: bundled,
273
+ source: "内置资产 assets/7zip"
274
+ };
275
+ }
276
+ return {
277
+ exe: "7z",
278
+ source: "系统 PATH"
279
+ };
280
+ }
281
+ /** 解包安装包到指定目录(spawn 7z x,失败抛错)。 */
282
+ async function extractInstaller(installerPath, destDir, sevenZipPath) {
283
+ const sevenZip = sevenZipPath ?? findSevenZip().exe;
284
+ await (0, node_fs_promises.mkdir)(destDir, { recursive: true });
285
+ await new Promise((resolve, reject) => {
286
+ (0, node_child_process.execFile)(sevenZip, [
287
+ "x",
288
+ installerPath,
289
+ `-o${destDir}`,
290
+ "-y"
291
+ ], { maxBuffer: 67108864 }, (err, _stdout, stderr) => {
292
+ if (err !== null) {
293
+ const hint = err.code === "ENOENT" ? `未找到 7z 可执行文件: ${sevenZip}\n(Windows 请安装 7-Zip 或用 NAPUTO_7Z_PATH 指定;Linux 请安装 p7zip-full)` : "(QQ 安装包是 NSIS 格式,需完整版 7z 而非 7za)";
294
+ reject(/* @__PURE__ */ new Error(`7z 解包失败: ${installerPath}\n${stderr.trim()}\n${hint}`));
295
+ return;
296
+ }
297
+ resolve();
298
+ });
299
+ });
300
+ }
301
+ /**
302
+ * 从解包目录提取运行所需文件到缓存:
303
+ * resources/app 顶层全部 *.node + *.dll(保持同目录结构,PATH 前置即可解析依赖)。
304
+ * 幂等:目标 wrapper.node 已存在则跳过。
305
+ */
306
+ async function extractWrapperFiles(extractedDir, version, cacheRoot) {
307
+ const appDir = (0, node_path.join)(extractedDir, "Files", "versions", version, "resources", "app");
308
+ if (!(0, node_fs.existsSync)((0, node_path.join)(appDir, "wrapper.node"))) throw new Error(`解包产物缺少 wrapper.node: ${appDir}`);
309
+ const destAppDir = (0, node_path.join)(cacheRoot, version, "versions", version, "resources", "app");
310
+ const destWrapper = (0, node_path.join)(destAppDir, "wrapper.node");
311
+ if ((0, node_fs.existsSync)(destWrapper)) return buildCachedInfo(cacheRoot, version);
312
+ await (0, node_fs_promises.mkdir)(destAppDir, { recursive: true });
313
+ const entries = await (0, node_fs_promises.readdir)(appDir, { withFileTypes: true });
314
+ for (const entry of entries) {
315
+ if (!entry.isFile()) continue;
316
+ const dot = entry.name.lastIndexOf(".");
317
+ const ext = dot >= 0 ? entry.name.slice(dot).toLowerCase() : "";
318
+ if (!EXTRACT_EXTENSIONS.has(ext)) continue;
319
+ await (0, node_fs_promises.copyFile)((0, node_path.join)(appDir, entry.name), (0, node_path.join)(destAppDir, entry.name));
320
+ }
321
+ if (!(0, node_fs.existsSync)(destWrapper)) throw new Error(`提取失败:wrapper.node 未复制到 ${destWrapper}`);
322
+ return buildCachedInfo(cacheRoot, version);
323
+ }
324
+ /** 构造 cached 来源的 QqInstallInfo(wrapperPath 指向缓存中 wrapper.node)。 */
325
+ function buildCachedInfo(cacheRoot, version) {
326
+ const versionDir = (0, node_path.join)(cacheRoot, version);
327
+ return {
328
+ qqPath: (0, node_path.join)(versionDir, "QQ.exe"),
329
+ installDir: versionDir,
330
+ version,
331
+ wrapperPath: (0, node_path.join)(versionDir, "versions", version, "resources", "app", "wrapper.node"),
332
+ source: "cached"
333
+ };
334
+ }
335
+ /** 清理缓存版本目录(下载/解包失败残留,供调用方兜底)。 */
336
+ async function clearCacheVersion(cacheRoot, version) {
337
+ await (0, node_fs_promises.rm)((0, node_path.join)(cacheRoot, version), {
338
+ recursive: true,
339
+ force: true
340
+ });
341
+ }
342
+ //#endregion
343
+ //#region src/win-node.ts
344
+ /**
345
+ * win-node.ts:Windows 版 node.exe 获取(P2,Linux/wine 场景用)。
346
+ *
347
+ * wine 只能跑 Windows PE 程序——自建宿主在 Linux 上必须用 Windows 版 node.exe
348
+ * 作为宿主(wrapper.node 是 PE 原生模块,只能被 Windows node.exe dlopen)。
349
+ * 来源:nodejs.org 官方 zip(开源软件官方发行版,但为保守起见同样不内置,
350
+ * 运行时下载到 <数据根>/runtime/win-node/,可被 NAPUTO_WIN_NODE_PATH 覆盖)。
351
+ *
352
+ * 设计文档 §3.1。与 qq-download.ts 同构:https 下载 + 解压 + 缓存幂等。
353
+ */
354
+ /** 目标 Windows node.exe 可执行文件名(zip 内)。 */
355
+ const WIN_NODE_EXE = "node.exe";
356
+ /** 运行时缓存子目录名(<数据根>/runtime/win-node/)。 */
357
+ const WIN_NODE_DIR_NAME = "win-node";
358
+ /** 解析 nodejs.org zip URL(与 dist 目录命名一致)。 */
359
+ function nodeZipUrl(version) {
360
+ return `https://nodejs.org/dist/${version}/node-${version}-win-x64.zip`;
361
+ }
362
+ /**
363
+ * 确保 Windows 版 node.exe 就绪(下载 + 解压 + 缓存)。
364
+ * 幂等:<数据根>/runtime/win-node/<version>/node.exe 已存在则直接返回。
365
+ */
366
+ async function ensureWinNode(options = {}) {
367
+ const explicit = options.exePath ?? node_process.default.env["NAPUTO_WIN_NODE_PATH"];
368
+ if (explicit !== void 0 && explicit !== "" && (0, node_fs.existsSync)(explicit)) return {
369
+ exePath: (0, node_path.resolve)(explicit),
370
+ version: "explicit"
371
+ };
372
+ const version = options.version ?? node_process.default.env["NAPUTO_WIN_NODE_VERSION"] ?? DEFAULT_WIN_NODE_VERSION;
373
+ const dataRoot = resolveDataRoot(options.dataRoot);
374
+ const cacheDir = (0, node_path.join)(dataRoot, "runtime", WIN_NODE_DIR_NAME, version);
375
+ const exePath = (0, node_path.join)(cacheDir, WIN_NODE_EXE);
376
+ if ((0, node_fs.existsSync)(exePath)) return {
377
+ exePath,
378
+ version
379
+ };
380
+ const zipPath = (0, node_path.join)(cacheDir, `node-${version}-win-x64.zip`);
381
+ const url = node_process.default.env["NAPUTO_WIN_NODE_URL"] ?? nodeZipUrl(version);
382
+ (0, node_fs.mkdirSync)(cacheDir, { recursive: true });
383
+ try {
384
+ await downloadFile({
385
+ dest: zipPath,
386
+ url
387
+ });
388
+ await unzipNode(zipPath, cacheDir);
389
+ } finally {
390
+ await (0, node_fs_promises.rm)(zipPath, { force: true });
391
+ }
392
+ if (!(0, node_fs.existsSync)(exePath)) throw new Error(`解压后未找到 node.exe: ${exePath}`);
393
+ return {
394
+ exePath,
395
+ version
396
+ };
397
+ }
398
+ /** 解压 node zip(zip 内为 node-<version>-win-x64/ 目录,取其中 node.exe 移到缓存根)。 */
399
+ async function unzipNode(zipPath, cacheDir) {
400
+ const tmpDir = (0, node_path.join)(cacheDir, "_zip");
401
+ await extractInstaller(zipPath, tmpDir);
402
+ const nodeDir = (await (0, node_fs_promises.readdir)(tmpDir)).find((n) => n.startsWith("node-"));
403
+ const src = nodeDir === void 0 ? (0, node_path.join)(tmpDir, WIN_NODE_EXE) : (0, node_path.join)(tmpDir, nodeDir, WIN_NODE_EXE);
404
+ if (!(0, node_fs.existsSync)(src)) throw new Error(`node zip 解压后未找到 node.exe: ${src}`);
405
+ await (0, node_fs_promises.copyFile)(src, (0, node_path.join)(cacheDir, WIN_NODE_EXE));
406
+ await (0, node_fs_promises.rm)(tmpDir, {
407
+ recursive: true,
408
+ force: true
409
+ });
410
+ }
411
+ /** 默认 Windows node 版本(latest v24,LTS 线;可被环境变量/参数覆盖)。 */
412
+ const DEFAULT_WIN_NODE_VERSION = "v24.16.0";
413
+ /** 轻量数据根解析(与 locate-qq 内同构,避免循环依赖)。 */
414
+ function resolveDataRoot(dataRoot) {
415
+ const explicit = dataRoot ?? node_process.default.env["NAPKETTO_DATA"];
416
+ if (explicit !== void 0 && explicit !== "") return (0, node_path.resolve)(explicit);
417
+ const projectRoot = findProjectRoot$1(node_process.default.cwd()) ?? node_process.default.cwd();
418
+ return (0, node_path.join)(projectRoot, ".napuketto");
419
+ }
420
+ /** 向上探测项目根(含 pnpm-workspace.yaml 或 package.json 的目录)。 */
421
+ function findProjectRoot$1(start) {
422
+ let dir = (0, node_path.resolve)(start);
423
+ for (;;) {
424
+ if ((0, node_fs.existsSync)((0, node_path.join)(dir, "pnpm-workspace.yaml")) || (0, node_fs.existsSync)((0, node_path.join)(dir, "package.json"))) return dir;
425
+ const parent = (0, node_path.dirname)(dir);
426
+ if (parent === dir) return null;
427
+ dir = parent;
428
+ }
429
+ }
430
+ //#endregion
431
+ //#region src/wine.ts
432
+ /**
433
+ * wine.ts:Linux 上经 wine 运行 Windows 版 node.exe 的路径与 spawn 组装(P2)。
434
+ *
435
+ * 设计文档 §3.2(2026-08-12 拍板确认)。核心认知:
436
+ * - Windows 版 node.exe 在 wine 内把 Linux 根目录挂载在 `Z:\`
437
+ * - process.dlopen / 环境变量路径都是 wine 视角的 Windows 路径
438
+ * - 传给 wine 子进程的**所有路径**(wrapperPath / cfgDir / stubDir / self-host.cjs / PATH)
439
+ * 都要过 toWinePath 转成 `Z:\...`
440
+ *
441
+ * 纯函数设计:不 import node:child_process,方便 Windows 上单测断言(P2 纯逻辑层)。
442
+ */
443
+ /** Linux 绝对路径 → wine `Z:\` 路径(Windows 版 node.exe 视角)。幂等:已是 Windows 路径(含盘符)原样返回。 */
444
+ function toWinePath(linuxPath) {
445
+ if (DRIVE_LETTER_RE.test(linuxPath)) return linuxPath;
446
+ return `Z:\\${linuxPath.replaceAll("/", "\\").replace(LEADING_SLASH_RE, "")}`;
447
+ }
448
+ /** 前导斜杠(toWinePath 去根用,顶层常量避免每次调用重建)。 */
449
+ const LEADING_SLASH_RE = /^\\/;
450
+ /** 盘符前缀(C: / Z: 等,识别已转换的 Windows 路径)。 */
451
+ const DRIVE_LETTER_RE = /^[A-Za-z]:/;
452
+ /** 判断当前是否 Linux(需 wine 的场景)。 */
453
+ function isLinux() {
454
+ return process.platform === "linux";
455
+ }
456
+ /** wine 可执行文件(Linux 上 spawn 用;可被 NAPUTO_WINE 覆盖)。 */
457
+ function wineBinary() {
458
+ return process.env["NAPUTO_WINE"] ?? "wine";
459
+ }
460
+ /** 组装 spawn 命令(win32 vs linux 分支)。 */
461
+ function buildSpawnCommand(options) {
462
+ if ((options.platform ?? process.platform) === "win32") return {
463
+ command: options.winNodePath ?? process.execPath,
464
+ args: [options.selfHostPath]
465
+ };
466
+ return {
467
+ command: options.wine ?? wineBinary(),
468
+ args: [options.winNodePath ?? "node.exe", options.selfHostPath]
469
+ };
470
+ }
471
+ //#endregion
472
+ //#region src/launcher.ts
473
+ /**
474
+ * launcher:自建宿主引导(2026-08-07 唯一路线)——spawn 标准 node 跑 self-host.cjs。
475
+ *
476
+ * 历史(V1/路线 B 事实链,已归档 archive/):
477
+ * 1. wrapper.node 只能在 QQ 定制版 Electron 里注册(实测纯 Node/普通 Electron 均 self-register 失败)
478
+ * 2. QQ 是打包应用,禁 NODE_OPTIONS(实测 stderr)
479
+ * → 旧方案:NapukettoBootMain.exe 启动 QQ + 注入 NapukettoWinBootHook.dll(已废弃)
480
+ * → 现方案:stub QQNT.dll 转发宿主符号 → 标准 node 直接 dlopen wrapper.node(自建宿主)
481
+ *
482
+ * P2(2026-08-12):平台分支——win32 本机 node;linux 经 wine 跑 Windows 版 node.exe
483
+ * (ensureWinNode 下载)。所有传给 wine 子进程的路径过 toWinePath(Z:\)。
484
+ */
485
+ const __dirname$1 = (0, node_path.dirname)((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href));
486
+ /**
487
+ * 启动自建宿主(路线 A):spawn 标准 node 直接跑 self-host.cjs,不拉起 QQ / 不注入。
488
+ *
489
+ * 关键(HANDOVER-V7 技术数据):wrapper.node 从 QQNT.dll 导入 99 符号
490
+ * (napi_*×40 + uv_*×56 + qq_magic×1 + v8/node mangled×2)——标准 node 无这些宿主
491
+ * 符号,必须 PATH 前置 stub QQNT.dll 目录(转发到 node.exe)+ QQ resources\app
492
+ * (wrapper.node 同目录依赖 DLL)才能 dlopen 成功。
493
+ *
494
+ * P2 平台分支:win32 用本机 node;linux 用 wine + Windows 版 node.exe
495
+ * (ensureWinNode 自动下载,路径过 toWinePath)。async(win-node 可能需下载)。
496
+ */
497
+ async function launchSelfHost(options) {
498
+ const selfHostPath = options.selfHostEntry ?? (0, node_path.join)(__dirname$1, "..", "dist", "host", "self-host.cjs");
499
+ if (!(0, node_fs.existsSync)(selfHostPath)) throw new Error(`self-host.cjs 缺失: ${selfHostPath}(先运行 pnpm --filter @napuketto/loader build)`);
500
+ const stub = options.stubDir ?? defaultStubDir();
501
+ if (!(0, node_fs.existsSync)((0, node_path.join)(stub, "QQNT.dll"))) throw new Error(`stub QQNT.dll 未找到: ${stub}(自建宿主必需:stub 转发 QQNT.dll 符号到 node.exe;请用 --stub-dir 或环境变量 NAPUTO_STUB_DIR 指定 stub 目录)`);
502
+ const { useWine, winNodePath } = await resolveNodeExecutable(options);
503
+ const env = buildLaunchEnv(options, useWine);
504
+ applyPathEnv(env, {
505
+ stub,
506
+ wrapperPath: options.qq.wrapperPath,
507
+ useWine
508
+ });
509
+ if (options.cwd !== void 0) (0, node_fs.mkdirSync)(options.cwd, { recursive: true });
510
+ const { command, args } = buildSpawnCommand({
511
+ ...useWine && winNodePath !== void 0 ? { winNodePath } : { winNodePath: node_process.default.execPath },
512
+ selfHostPath: useWine ? toWinePath(selfHostPath) : selfHostPath,
513
+ wine: wineBinary$1()
514
+ });
515
+ return {
516
+ child: (0, node_child_process.spawn)(command, args, {
517
+ ...options.cwd !== void 0 ? { cwd: options.cwd } : {},
518
+ env,
519
+ stdio: options.stdio ?? "inherit",
520
+ windowsHide: false
521
+ }),
522
+ bootJsPath: selfHostPath
523
+ };
524
+ }
525
+ /** 平台分支解析 node 可执行:win32 本机;linux wine + Windows 版 node.exe。 */
526
+ async function resolveNodeExecutable(options) {
527
+ if (!isLinux()) return {
528
+ useWine: false,
529
+ winNodePath: void 0
530
+ };
531
+ return {
532
+ useWine: true,
533
+ winNodePath: (await ensureWinNode({
534
+ ...options.cwd !== void 0 ? { dataRoot: options.cwd } : {},
535
+ ...options.winNodePath !== void 0 ? { exePath: options.winNodePath } : {}
536
+ })).exePath
537
+ };
538
+ }
539
+ /** 注入 PATH(stub 目录 + wrapper.node 目录前置)与 STUB_DIR(wine 场景转 Z:\)。 */
540
+ function applyPathEnv(env, opts) {
541
+ const p = (linuxPath) => opts.useWine ? toWinePath(linuxPath) : linuxPath;
542
+ env["PATH"] = [
543
+ p(opts.stub),
544
+ p((0, node_path.dirname)(opts.wrapperPath)),
545
+ node_process.default.env["PATH"] ?? ""
546
+ ].filter((entry) => entry !== void 0 && entry !== "").join(";");
547
+ env[ENV.STUB_DIR] = p(opts.stub);
548
+ }
549
+ /**
550
+ * 默认 stub QQNT.dll 目录(loader 包内闭源 native/build/stub-test-env,开发机默认)。
551
+ * src 与 native 同层,构建后 dist 与 native 同层——`../native` 恒正确。
552
+ */
553
+ function defaultStubDir() {
554
+ return (0, node_path.join)(__dirname$1, "..", "native", "build", "stub-test-env");
555
+ }
556
+ /** 装配自建宿主环境变量(wine 场景路径转 Z:\;win32 原样)。 */
557
+ function buildLaunchEnv(options, useWine) {
558
+ const cfg = (0, node_path.resolve)(options.cfgDir);
559
+ (0, node_fs.mkdirSync)(cfg, { recursive: true });
560
+ const p = (linuxPath) => useWine ? toWinePath(linuxPath) : linuxPath;
561
+ const env = {
562
+ ...node_process.default.env,
563
+ [ENV.QQ_PATH]: p(options.qq.qqPath),
564
+ [ENV.KERNEL_ENTRY]: p((0, node_path.resolve)(options.kernelEntry)),
565
+ [ENV.CFG_DIR]: p(cfg),
566
+ [ENV.QQ_VERSION]: options.qq.version,
567
+ [ENV.WRAPPER_PATH]: p(options.qq.wrapperPath)
568
+ };
569
+ const optional = {
570
+ ...options.selfHost === true ? { [ENV.SELF_HOST]: "1" } : {},
571
+ ...options.adapterEntry !== void 0 ? { [ENV.ADAPTER_ENTRY]: p((0, node_path.resolve)(options.adapterEntry)) } : {},
572
+ ...options.networkEntry !== void 0 ? { [ENV.NETWORK_ENTRY]: p((0, node_path.resolve)(options.networkEntry)) } : {},
573
+ ...options.configPath !== void 0 ? { [ENV.CONFIG_PATH]: p((0, node_path.resolve)(options.configPath)) } : {},
574
+ ...options.quickUin !== void 0 ? { [ENV.QUICK_UIN]: options.quickUin } : {},
575
+ ...options.ipc === true ? { [ENV.IPC]: "1" } : {}
576
+ };
577
+ return {
578
+ ...env,
579
+ ...optional
580
+ };
581
+ }
582
+ /** wine 可执行文件(linux 场景;NAPUTO_WINE 可覆盖)。 */
583
+ function wineBinary$1() {
584
+ return node_process.default.env["NAPUTO_WINE"] ?? "wine";
585
+ }
586
+ /** 环境变量名(self-host.cjs 与 kernel 引导读取)。 */
587
+ const ENV = {
588
+ QQ_PATH: "NAPUTO_QQ_PATH",
589
+ KERNEL_ENTRY: "NAPUTO_KERNEL_ENTRY",
590
+ CFG_DIR: "NAPUTO_CFG_DIR",
591
+ QQ_VERSION: "NAPUTO_QQ_VERSION",
592
+ WRAPPER_PATH: "NAPUTO_WRAPPER_PATH",
593
+ /** 自建宿主:标准 node + stub QQNT.dll 引导(self-host.cjs 分支)。 */
594
+ SELF_HOST: "NAPUTO_SELF_HOST",
595
+ /** stub QQNT.dll 目录(launchSelfHost 注入,self-host.cjs 诊断用)。 */
596
+ STUB_DIR: "NAPUTO_STUB_DIR",
597
+ /** adapter 包入口(协议装配用)。 */
598
+ ADAPTER_ENTRY: "NAPUTO_ADAPTER_ENTRY",
599
+ /** network 包入口(协议装配用)。 */
600
+ NETWORK_ENTRY: "NAPUTO_NETWORK_ENTRY",
601
+ /** 全局配置文件路径(项目根 napuketto.toml,boot-protocols 读取)。 */
602
+ CONFIG_PATH: "NAPKETTO_CONFIG",
603
+ /** 强制指定快速登录账号(cli `-q <uin>` 透传,bootstrap 登录用)。 */
604
+ QUICK_UIN: "NAPUTO_QUICK_UIN",
605
+ /** IPC 子进程模式(koishi 插件驱动:stdout JSON 行 + stdin action/control)。 */
606
+ IPC: "NAPUTO_IPC"
607
+ };
608
+ //#endregion
609
+ //#region src/qq-releases.ts
610
+ /**
611
+ * qq-releases.ts:版本清单读取(P1)。
612
+ *
613
+ * 运行时从包根 qq-releases.json 读取(发布时 files 含它,安装后位于
614
+ * node_modules/@napuketto/loader/qq-releases.json)。清单由 CI/社区 PR 维护
615
+ * (设计文档 §2.2)。NAPUTO_QQ_URL 可运行时覆盖下载地址。
616
+ */
617
+ /** 清单解析失败。 */
618
+ var QqReleasesError = class extends Error {
619
+ constructor(message) {
620
+ super(message);
621
+ this.name = "QqReleasesError";
622
+ }
623
+ };
624
+ /** 包根路径(dist 与包根同层:dist/index.mjs → ../qq-releases.json)。 */
625
+ function packageRoot() {
626
+ return (0, node_path.dirname)((0, node_path.dirname)((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href)));
627
+ }
628
+ /** 读版本清单(同步,启动期调用一次即可)。 */
629
+ function loadQqReleases() {
630
+ const file = (0, node_path.join)(packageRoot(), "qq-releases.json");
631
+ let raw;
632
+ try {
633
+ raw = (0, node_fs.readFileSync)(file, "utf8");
634
+ } catch {
635
+ throw new QqReleasesError(`版本清单缺失: ${file}(@napuketto/loader 包安装不完整?)`);
636
+ }
637
+ try {
638
+ const parsed = JSON.parse(raw);
639
+ if (parsed.schema !== 1 || !Array.isArray(parsed.known) || parsed.known.length === 0) throw new Error("结构不合法");
640
+ return parsed;
641
+ } catch {
642
+ throw new QqReleasesError(`版本清单解析失败: ${file}`);
643
+ }
644
+ }
645
+ /** 取最新已知可用版本(known 数组按录入顺序,取最后一项 = 最新)。 */
646
+ function latestRelease(releases) {
647
+ const [latest] = [...releases.known].reverse();
648
+ if (latest === void 0) throw new QqReleasesError("版本清单为空");
649
+ return latest;
650
+ }
651
+ /**
652
+ * 解析实际下载 URL:NAPUTO_QQ_URL 环境变量覆盖 > 清单 URL。
653
+ * 供 downloadFile 使用(也支持按版本查询,当前固定取最新)。
654
+ */
655
+ function resolveDownloadUrl(release) {
656
+ const override = node_process.default.env["NAPUTO_QQ_URL"];
657
+ return override !== void 0 && override !== "" ? override : release.url;
658
+ }
659
+ //#endregion
660
+ //#region src/locate-qq.ts
661
+ /**
662
+ * 定位 QQ 原生文件(wrapper.node)来源(P0:多级来源纯定位,不下载)。
663
+ *
664
+ * 来源优先级(resolveQqFiles):
665
+ * L0: NAPUTO_QQ_FILES / qqFilesDir —— 显式指定含 versions/ 结构的文件根(缓存/拷贝)
666
+ * L1: 本机 QQ 安装 —— NAPUTO_QQ_PATH / 注册表 / 常见路径(既有逻辑保留)
667
+ * L2: 数据根缓存 <数据根>/qq-files/<版本> —— 下载解包产物(P1 落地后才有)
668
+ */
669
+ /** 数据根下 QQ 文件缓存目录名(<数据根>/qq-files/<版本>/,P1 下载解包产物)。 */
670
+ const QQ_FILES_DIR_NAME = "qq-files";
671
+ /** 注册表 UninstallString 查询(QQ 官方安装路径)。 */
672
+ const REG_QUERY_RE = /"([^"]+)"/;
673
+ /** 注册表 UninstallString 查询(QQ 官方安装路径)。 */
674
+ function findQqViaRegistry() {
675
+ try {
676
+ const exe = (0, node_child_process.execFileSync)("reg", [
677
+ "query",
678
+ "HKLM\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\QQ",
679
+ "/v",
680
+ "UninstallString"
681
+ ]).toString().match(REG_QUERY_RE)?.[1];
682
+ if (exe !== void 0) {
683
+ const dir = exe.replaceAll("\"", "").slice(0, exe.lastIndexOf("\\"));
684
+ const qq = (0, node_path.join)(dir, "QQ.exe");
685
+ if ((0, node_fs.existsSync)(qq)) return qq;
686
+ }
687
+ } catch {}
688
+ return null;
689
+ }
690
+ /** 常见安装路径探测。 */
691
+ function findQqViaCommonPaths() {
692
+ const localAppData = node_process.default.env["LOCALAPPDATA"] ?? "";
693
+ const candidates = [
694
+ "C:/Program Files/Tencent/QQNT/QQ.exe",
695
+ "C:/Program Files (x86)/Tencent/QQNT/QQ.exe",
696
+ (0, node_path.join)(localAppData, "Programs", "Tencent", "QQNT", "QQ.exe"),
697
+ "C:/Dev/QQBot-Dev/QQNT/QQ.exe"
698
+ ];
699
+ for (const p of candidates) if ((0, node_fs.existsSync)(p)) return p;
700
+ return null;
701
+ }
702
+ /** 找 QQ.exe 路径。 */
703
+ function locateQqPath() {
704
+ const viaReg = findQqViaRegistry();
705
+ if (viaReg) return viaReg;
706
+ const viaCommon = findQqViaCommonPaths();
707
+ if (viaCommon) return viaCommon;
708
+ throw new Error("未找到 QQ.exe(请先安装 QQ,或通过 NAPUTO_QQ_PATH 指定)");
709
+ }
710
+ /** 从文件根定位最新版本 wrapper.node(local/cached 共用)。 */
711
+ function resolveFromRoot(rootDir, source) {
712
+ const versionsDir = (0, node_path.join)(rootDir, "versions");
713
+ if (!(0, node_fs.existsSync)(versionsDir)) throw new Error(`QQ 版本目录不存在: ${versionsDir}`);
714
+ const [first] = (0, node_fs.readdirSync)(versionsDir).filter((v) => (0, node_fs.statSync)((0, node_path.join)(versionsDir, v)).isDirectory()).sort().reverse();
715
+ if (first === void 0) throw new Error(`QQ 版本目录为空: ${versionsDir}`);
716
+ const wrapperPath = (0, node_path.join)(versionsDir, first, "resources", "app", "wrapper.node");
717
+ if (!(0, node_fs.existsSync)(wrapperPath)) throw new Error(`未找到 wrapper.node: ${wrapperPath}`);
718
+ return {
719
+ qqPath: (0, node_path.join)(rootDir, "QQ.exe"),
720
+ installDir: rootDir,
721
+ version: first,
722
+ wrapperPath,
723
+ source
724
+ };
725
+ }
726
+ /** 从 QQ.exe 路径推导安装目录,并探测当前版本(L1:本机安装)。 */
727
+ function resolveQqInstall(qqPath) {
728
+ const qq = qqPath ?? node_process.default.env["NAPUTO_QQ_PATH"] ?? locateQqPath();
729
+ return resolveFromRoot(qq.slice(0, qq.lastIndexOf("\\")), "local");
730
+ }
731
+ /**
732
+ * 多级来源定位 QQ 原生文件:
733
+ * L0 → L1 → L2 依次尝试,全部失败且 autoDownload 时自动进入下载流程(ensureQqFiles)。
734
+ * ⚠️ async:自动下载为异步流程(P1 起);纯定位场景用 resolveQqInstall(同步)。
735
+ */
736
+ async function resolveQqFiles(options = {}) {
737
+ const explicitDir = options.qqFilesDir ?? node_process.default.env["NAPUTO_QQ_FILES"];
738
+ if (explicitDir !== void 0 && explicitDir !== "") return resolveFromRoot((0, node_path.resolve)(explicitDir), "cached");
739
+ const qqPath = options.qqPath ?? node_process.default.env["NAPUTO_QQ_PATH"];
740
+ if (qqPath !== void 0 && qqPath !== "") return resolveQqInstall(qqPath);
741
+ try {
742
+ return resolveQqInstall();
743
+ } catch {}
744
+ const dataRoot = resolveDataRootLight(options.dataRoot);
745
+ const cached = tryResolveCached(dataRoot);
746
+ if (cached !== null) return cached;
747
+ if (options.autoDownload !== false) return ensureQqFiles({
748
+ dataRoot,
749
+ ...options.sevenZipPath !== void 0 ? { sevenZipPath: options.sevenZipPath } : {}
750
+ });
751
+ throw new Error("未找到 QQ 原生文件(wrapper.node):请安装 QQ、设置 NAPUTO_QQ_PATH/NAPUTO_QQ_FILES,或使用下载流程(ensureQqFiles)自动获取");
752
+ }
753
+ /** 扫描数据根缓存(L2);命中返回,未命中返回 null。 */
754
+ function tryResolveCached(dataRoot) {
755
+ const cacheRoot = (0, node_path.join)(dataRoot, QQ_FILES_DIR_NAME);
756
+ if (!(0, node_fs.existsSync)(cacheRoot)) return null;
757
+ const versionDirs = (0, node_fs.readdirSync)(cacheRoot).filter((v) => (0, node_fs.statSync)((0, node_path.join)(cacheRoot, v)).isDirectory()).sort().reverse();
758
+ for (const versionDir of versionDirs) try {
759
+ return resolveFromRoot((0, node_path.join)(cacheRoot, versionDir), "cached");
760
+ } catch {}
761
+ return null;
762
+ }
763
+ /**
764
+ * 确保 QQ 原生文件就绪(P1):幂等缓存检查 → 下载官方安装包 → sha256 校验
765
+ * → 7z 解包 → 提取 wrapper.node/QQNT.dll → 缓存。返回缓存版本 QqInstallInfo。
766
+ */
767
+ async function ensureQqFiles(options = {}) {
768
+ const dataRoot = resolveDataRootLight(options.dataRoot);
769
+ const cacheRoot = (0, node_path.join)(dataRoot, QQ_FILES_DIR_NAME);
770
+ (0, node_fs.mkdirSync)(cacheRoot, { recursive: true });
771
+ const existing = tryResolveCached(dataRoot);
772
+ if (existing !== null) return existing;
773
+ const release = latestRelease(loadQqReleases());
774
+ const url = resolveDownloadUrl(release);
775
+ const version = release.version;
776
+ const tmpDir = (0, node_path.join)(dataRoot, "tmp");
777
+ (0, node_fs.mkdirSync)(tmpDir, { recursive: true });
778
+ const installerPath = (0, node_path.join)(tmpDir, `qq-installer-${version}.exe`);
779
+ await downloadFile({
780
+ dest: installerPath,
781
+ url,
782
+ expectedSha256: release.sha256
783
+ });
784
+ const extractedDir = (0, node_path.join)(tmpDir, `qq-extracted-${version}`);
785
+ try {
786
+ await extractInstaller(installerPath, extractedDir, options.sevenZipPath);
787
+ return await extractWrapperFiles(extractedDir, version, cacheRoot);
788
+ } catch (err) {
789
+ await clearCacheVersion(cacheRoot, version);
790
+ throw err;
791
+ } finally {
792
+ await (0, node_fs_promises.rm)(installerPath, { force: true });
793
+ await (0, node_fs_promises.rm)(extractedDir, {
794
+ recursive: true,
795
+ force: true
796
+ });
797
+ }
798
+ }
799
+ /**
800
+ * 轻量数据根解析(loader 侧,避免新增 kernel 依赖;P2 若需完整语义再切换)。
801
+ * 优先级:显式参数 > NAPKETTO_DATA > 项目根/.napuketto > cwd 兜底。
802
+ */
803
+ function resolveDataRootLight(dataRoot) {
804
+ const explicit = dataRoot ?? node_process.default.env["NAPKETTO_DATA"];
805
+ if (explicit !== void 0 && explicit !== "") return (0, node_path.resolve)(explicit);
806
+ const projectRoot = findProjectRoot(node_process.default.cwd()) ?? node_process.default.cwd();
807
+ return (0, node_path.join)(projectRoot, ".napuketto");
808
+ }
809
+ /** 向上探测项目根(含 pnpm-workspace.yaml 或 package.json 的目录)。 */
810
+ function findProjectRoot(start) {
811
+ let dir = (0, node_path.resolve)(start);
812
+ for (;;) {
813
+ if ((0, node_fs.existsSync)((0, node_path.join)(dir, "pnpm-workspace.yaml")) || (0, node_fs.existsSync)((0, node_path.join)(dir, "package.json"))) return dir;
814
+ const parent = (0, node_path.dirname)(dir);
815
+ if (parent === dir) return null;
816
+ dir = parent;
817
+ }
818
+ }
819
+ //#endregion
820
+ exports.DownloadError = DownloadError;
821
+ exports.ENV = ENV;
822
+ exports.INSTANCE_LOCK_FILE = INSTANCE_LOCK_FILE;
823
+ exports.QQ_FILES_DIR_NAME = QQ_FILES_DIR_NAME;
824
+ exports.QqReleasesError = QqReleasesError;
825
+ exports.WIN_NODE_DIR_NAME = WIN_NODE_DIR_NAME;
826
+ exports.acquireInstanceLock = acquireInstanceLock;
827
+ exports.buildSpawnCommand = buildSpawnCommand;
828
+ exports.checkInstanceLock = checkInstanceLock;
829
+ exports.clearCacheVersion = clearCacheVersion;
830
+ exports.defaultStubDir = defaultStubDir;
831
+ exports.downloadFile = downloadFile;
832
+ exports.ensureQqFiles = ensureQqFiles;
833
+ exports.ensureWinNode = ensureWinNode;
834
+ exports.extractInstaller = extractInstaller;
835
+ exports.extractWrapperFiles = extractWrapperFiles;
836
+ exports.findSevenZip = findSevenZip;
837
+ exports.isLinux = isLinux;
838
+ exports.latestRelease = latestRelease;
839
+ exports.launchSelfHost = launchSelfHost;
840
+ exports.loadQqReleases = loadQqReleases;
841
+ exports.locateQqPath = locateQqPath;
842
+ exports.nodeZipUrl = nodeZipUrl;
843
+ exports.registerLockCleanup = registerLockCleanup;
844
+ exports.releaseInstanceLock = releaseInstanceLock;
845
+ exports.resolveDownloadUrl = resolveDownloadUrl;
846
+ exports.resolveQqFiles = resolveQqFiles;
847
+ exports.resolveQqInstall = resolveQqInstall;
848
+ exports.toWinePath = toWinePath;
849
+ exports.wineBinary = wineBinary;
@@ -0,0 +1,365 @@
1
+ import { StdioOptions, spawn } from "node:child_process";
2
+ //#region src/instance-lock.d.ts
3
+ /**
4
+ * instance-lock.ts:单实例锁(2026-08-07 根治「多实例抢数据目录锁挂起」)
5
+ *
6
+ * 背景(实测):同一账号数据目录(~/.napuketto/<uin>)只能被一个实例持有——
7
+ * QQ 原生层(MMKV / 登录单例 / 数据库)有锁,第二个实例抢不到锁会卡在
8
+ * loginService.initConfig 之后无响应(挂起)。此前 create-napukettoqq 生成的
9
+ * 副本实例常驻占锁,导致用户后续启动全部卡死。
10
+ *
11
+ * 方案:以数据目录为粒度加锁文件 `<dataDir>/instance.lock`(JSON 内容:
12
+ * pid / startedAt / cmdline 摘要)。启动前 checkInstanceLock:
13
+ * - 锁不存在 → 可启动(然后 acquireInstanceLock 写入)
14
+ * - 锁存在但 PID 已死 → 崩溃残留,自动清理(rm + 重新获取)
15
+ * - 锁存在且 PID 存活 → 已被占用,拒绝启动(返回占用 PID 供提示)
16
+ *
17
+ * 退出清理:进程正常退出时 releaseInstanceLock 删除锁文件;崩溃/强杀残留
18
+ * 由下次启动的「PID 存活检测」兜底(PID 已死 → 残留视为过期)。
19
+ *
20
+ * 放置说明:放 loader 而非 kernel——self-host.cjs 是 rolldown 内联 bundle
21
+ * (只内联 src/host/ 依赖树),静态 import kernel 会留 external 缺口;
22
+ * 放 loader 则 self-host(内联)与 cli(@napuketto/loader 导入)双入口可用。
23
+ */
24
+ /** 锁文件名(置于数据目录内)。 */
25
+ declare const INSTANCE_LOCK_FILE = "instance.lock";
26
+ /** 锁文件内容。 */
27
+ interface InstanceLockInfo {
28
+ /** 持有实例的进程 PID。 */
29
+ pid: number;
30
+ /** 获取锁的时间(epoch ms)。 */
31
+ startedAt: number;
32
+ /** 命令行摘要(诊断用,可选)。 */
33
+ cmdline?: string;
34
+ }
35
+ /** 数据目录锁检查结果。 */
36
+ interface InstanceLockCheck {
37
+ /** 是否被其他实例占用。 */
38
+ occupied: boolean;
39
+ /** 占用进程 PID(occupied=true 时有效)。 */
40
+ pid: number | null;
41
+ /** 锁文件完整路径(提示/清理用)。 */
42
+ lockPath: string;
43
+ }
44
+ /**
45
+ * 检测指定数据目录是否已被其他实例占用。
46
+ *
47
+ * @param dataDir 数据目录(账号级,如 ~/.napuketto/3567141148)
48
+ */
49
+ declare function checkInstanceLock(dataDir: string): InstanceLockCheck;
50
+ /**
51
+ * 获取数据目录锁。占用(其他实例存活)返回 false;成功或清理残留后返回 true。
52
+ * 幂等:本进程已持有则直接返回 true(不重复写)。
53
+ */
54
+ declare function acquireInstanceLock(dataDir: string, cmdline?: string): boolean;
55
+ /**
56
+ * 释放数据目录锁(正常退出时调用)。仅当持有者是本进程时才删除,
57
+ * 避免误删新持有者的锁(PID 复用 / 快速重启场景)。
58
+ */
59
+ declare function releaseInstanceLock(dataDir: string): void;
60
+ /** 注册进程退出清理(SIGINT/SIGTERM/exit 均触发;崩溃强杀由残留检测兜底)。 */
61
+ declare function registerLockCleanup(dataDir: string): void;
62
+ //#endregion
63
+ //#region src/locate-qq.d.ts
64
+ /** 数据根下 QQ 文件缓存目录名(<数据根>/qq-files/<版本>/,P1 下载解包产物)。 */
65
+ declare const QQ_FILES_DIR_NAME = "qq-files";
66
+ /** QQ 文件来源。 */
67
+ type QqFileSource = "local" | "cached";
68
+ /** QQ 安装目录探测结果。 */
69
+ interface QqInstallInfo {
70
+ /** QQ.exe 绝对路径(cached 来源为语义占位——缓存目录无 QQ.exe,当前无消费方)。 */
71
+ qqPath: string;
72
+ /** 安装根目录 / 文件根目录。 */
73
+ installDir: string;
74
+ /** 当前版本目录名(如 9.9.31-49919)。 */
75
+ version: string;
76
+ /** wrapper.node 绝对路径。 */
77
+ wrapperPath: string;
78
+ /** 来源:local = 本机安装;cached = NAPUTO_QQ_FILES / 数据根缓存。 */
79
+ source: QqFileSource;
80
+ }
81
+ /** resolveQqFiles 选项。 */
82
+ interface ResolveQqFilesOptions {
83
+ /** 显式文件根(L0;缺省读 NAPUTO_QQ_FILES)。 */
84
+ qqFilesDir?: string;
85
+ /** 显式 QQ.exe 路径(L1;缺省读 NAPUTO_QQ_PATH)。 */
86
+ qqPath?: string;
87
+ /** 数据根(L2 缓存扫描;缺省轻量解析 NAPKETTO_DATA ?? 项目根/.napuketto)。 */
88
+ dataRoot?: string;
89
+ /** L0/L1/L2 全部失败时自动进入下载流程(P1;默认 true,测试可关)。 */
90
+ autoDownload?: boolean;
91
+ /** 7z 可执行文件路径(解包用;缺省自动探测)。 */
92
+ sevenZipPath?: string;
93
+ }
94
+ /** 找 QQ.exe 路径。 */
95
+ declare function locateQqPath(): string;
96
+ /** 从 QQ.exe 路径推导安装目录,并探测当前版本(L1:本机安装)。 */
97
+ declare function resolveQqInstall(qqPath?: string): QqInstallInfo;
98
+ /**
99
+ * 多级来源定位 QQ 原生文件:
100
+ * L0 → L1 → L2 依次尝试,全部失败且 autoDownload 时自动进入下载流程(ensureQqFiles)。
101
+ * ⚠️ async:自动下载为异步流程(P1 起);纯定位场景用 resolveQqInstall(同步)。
102
+ */
103
+ declare function resolveQqFiles(options?: ResolveQqFilesOptions): Promise<QqInstallInfo>;
104
+ /**
105
+ * 确保 QQ 原生文件就绪(P1):幂等缓存检查 → 下载官方安装包 → sha256 校验
106
+ * → 7z 解包 → 提取 wrapper.node/QQNT.dll → 缓存。返回缓存版本 QqInstallInfo。
107
+ */
108
+ declare function ensureQqFiles(options?: {
109
+ /** 数据根(缓存 <数据根>/qq-files/)。 */
110
+ dataRoot?: string;
111
+ /** 7z 路径(缺省自动探测)。 */
112
+ sevenZipPath?: string;
113
+ }): Promise<QqInstallInfo>;
114
+ //#endregion
115
+ //#region src/launcher.d.ts
116
+ interface LaunchOptions {
117
+ /** QQ 安装信息(qqPath/version/wrapperPath)。 */
118
+ qq: QqInstallInfo;
119
+ /** kernel 入口(.mjs,self-host.cjs 里 import)。 */
120
+ kernelEntry: string;
121
+ /** 配置目录(账号数据目录,NAPUTO_CFG_DIR)。 */
122
+ cfgDir: string;
123
+ /** 全局配置文件路径(项目根 napuketto.toml,注入 NAPKETTO_CONFIG 供装配链读取)。 */
124
+ configPath?: string;
125
+ /** adapter 入口(.mjs,协议装配用)。 */
126
+ adapterEntry?: string;
127
+ /** network 入口(.mjs,协议装配用)。 */
128
+ networkEntry?: string;
129
+ /** 自建宿主:标准 node + stub QQNT.dll 直接引导,不拉起 QQ(2026-08-07 唯一路线)。 */
130
+ selfHost?: boolean;
131
+ /** stub QQNT.dll 目录(PATH 前置,转发 napi_*、uv_* 符号到 node.exe)。 */
132
+ stubDir?: string;
133
+ /** 强制指定快速登录账号(注入 NAPUTO_QUICK_UIN;cli `-q <uin>` 透传,2026-08-07)。 */
134
+ quickUin?: string;
135
+ /** IPC 子进程模式(=1:stdout 走 JSON 行协议 + stdin 收 action/control,koishi 插件驱动)。 */
136
+ ipc?: boolean;
137
+ /** 自建宿主入口(默认 dist/host/self-host.cjs)。 */
138
+ selfHostEntry?: string;
139
+ /** Windows 版 node.exe 路径(linux 场景覆盖;缺省 ensureWinNode 下载)。 */
140
+ winNodePath?: string;
141
+ /**
142
+ * 子进程工作目录(缺省继承父进程 cwd)。
143
+ *
144
+ * ⚠️ 必须指向数据根(~/.napuketto):QQ 原生层在账号上下文就绪前会
145
+ * fallback 到进程 cwd 落盘(如频道库 guild1.db),不指定则污染调用方
146
+ * 目录(实测写入项目根,guild1.db 08-07)。cli 传 dataRoot 根治。
147
+ */
148
+ cwd?: string;
149
+ /**
150
+ * 子进程 stdio(默认 "inherit")。cli 传 ["inherit","pipe","pipe"] 并接管
151
+ * 子进程 stdout/stderr:逐行过滤 wrapper 原生噪音(MMKV 刷屏、Node 符号
152
+ * 查找失败警告),其余转发——原生 printf 直写 fd 的字节无法从 JS 层拦截。
153
+ */
154
+ stdio?: StdioOptions;
155
+ }
156
+ interface LaunchResult {
157
+ /** 子进程句柄(自建宿主 node 进程)。 */
158
+ child: ReturnType<typeof spawn>;
159
+ /** 自建宿主入口路径(self-host.cjs)。 */
160
+ bootJsPath: string;
161
+ }
162
+ /**
163
+ * 启动自建宿主(路线 A):spawn 标准 node 直接跑 self-host.cjs,不拉起 QQ / 不注入。
164
+ *
165
+ * 关键(HANDOVER-V7 技术数据):wrapper.node 从 QQNT.dll 导入 99 符号
166
+ * (napi_*×40 + uv_*×56 + qq_magic×1 + v8/node mangled×2)——标准 node 无这些宿主
167
+ * 符号,必须 PATH 前置 stub QQNT.dll 目录(转发到 node.exe)+ QQ resources\app
168
+ * (wrapper.node 同目录依赖 DLL)才能 dlopen 成功。
169
+ *
170
+ * P2 平台分支:win32 用本机 node;linux 用 wine + Windows 版 node.exe
171
+ * (ensureWinNode 自动下载,路径过 toWinePath)。async(win-node 可能需下载)。
172
+ */
173
+ declare function launchSelfHost(options: LaunchOptions): Promise<LaunchResult>;
174
+ /**
175
+ * 默认 stub QQNT.dll 目录(loader 包内闭源 native/build/stub-test-env,开发机默认)。
176
+ * src 与 native 同层,构建后 dist 与 native 同层——`../native` 恒正确。
177
+ */
178
+ declare function defaultStubDir(): string;
179
+ /** 环境变量名(self-host.cjs 与 kernel 引导读取)。 */
180
+ declare const ENV: {
181
+ readonly QQ_PATH: "NAPUTO_QQ_PATH";
182
+ readonly KERNEL_ENTRY: "NAPUTO_KERNEL_ENTRY";
183
+ readonly CFG_DIR: "NAPUTO_CFG_DIR";
184
+ readonly QQ_VERSION: "NAPUTO_QQ_VERSION";
185
+ readonly WRAPPER_PATH: "NAPUTO_WRAPPER_PATH";
186
+ /** 自建宿主:标准 node + stub QQNT.dll 引导(self-host.cjs 分支)。 */
187
+ readonly SELF_HOST: "NAPUTO_SELF_HOST";
188
+ /** stub QQNT.dll 目录(launchSelfHost 注入,self-host.cjs 诊断用)。 */
189
+ readonly STUB_DIR: "NAPUTO_STUB_DIR";
190
+ /** adapter 包入口(协议装配用)。 */
191
+ readonly ADAPTER_ENTRY: "NAPUTO_ADAPTER_ENTRY";
192
+ /** network 包入口(协议装配用)。 */
193
+ readonly NETWORK_ENTRY: "NAPUTO_NETWORK_ENTRY";
194
+ /** 全局配置文件路径(项目根 napuketto.toml,boot-protocols 读取)。 */
195
+ readonly CONFIG_PATH: "NAPKETTO_CONFIG";
196
+ /** 强制指定快速登录账号(cli `-q <uin>` 透传,bootstrap 登录用)。 */
197
+ readonly QUICK_UIN: "NAPUTO_QUICK_UIN";
198
+ /** IPC 子进程模式(koishi 插件驱动:stdout JSON 行 + stdin action/control)。 */
199
+ readonly IPC: "NAPUTO_IPC";
200
+ };
201
+ //#endregion
202
+ //#region src/qq-download.d.ts
203
+ /** 下载选项。 */
204
+ interface DownloadOptions {
205
+ /** 目标文件绝对路径。 */
206
+ dest: string;
207
+ /** 期望 sha256(十六进制小写);缺省/空串 = 不校验(首次下载尚无参考值)。 */
208
+ expectedSha256?: string;
209
+ /** 覆盖下载地址(缺省读 NAPUTO_QQ_URL)。 */
210
+ url?: string;
211
+ /** 超时(毫秒,默认 60s 无数据则中断)。 */
212
+ timeoutMs?: number;
213
+ }
214
+ /** 下载结果。 */
215
+ interface DownloadResult {
216
+ /** 实际 sha256(十六进制小写)。 */
217
+ sha256: string;
218
+ /** 是否通过期望值校验(无期望值时恒 true)。 */
219
+ verified: boolean;
220
+ }
221
+ /** 下载中断(超时 / 非 2xx / 网络错误)抛出的错误。 */
222
+ declare class DownloadError extends Error {
223
+ constructor(message: string);
224
+ }
225
+ /**
226
+ * 下载文件并(可选)校验 sha256。失败时清理半成品文件。
227
+ * 返回实际 sha256(无论是否校验),便于调用方回填清单。
228
+ */
229
+ declare function downloadFile(options: DownloadOptions): Promise<DownloadResult>;
230
+ //#endregion
231
+ //#region src/qq-extract.d.ts
232
+ /** 7-Zip 可执行文件来源。 */
233
+ interface SevenZipResult {
234
+ /** 7z 可执行文件绝对路径。 */
235
+ exe: string;
236
+ /** 来源说明(内置资产 / 系统 / 环境变量)。 */
237
+ source: string;
238
+ }
239
+ /** 提取选项。 */
240
+ interface ExtractOptions {
241
+ /** 安装包绝对路径。 */
242
+ installerPath: string;
243
+ /** 目标版本目录(如 9.9.33-51802)。 */
244
+ version: string;
245
+ /** 缓存根目录(<数据根>/qq-files;版本子目录将建在其下)。 */
246
+ cacheRoot: string;
247
+ /** 7z 可执行文件路径(缺省自动探测)。 */
248
+ sevenZipPath?: string;
249
+ }
250
+ /**
251
+ * 探测 7z 可执行文件:
252
+ * NAPUTO_7Z_PATH 环境变量 > 内置资产(assets/7zip/7z.exe,Windows)> 系统 PATH 7z。
253
+ * Linux 上用 p7zip 提供的 7z 命令(系统 PATH)。
254
+ */
255
+ declare function findSevenZip(): SevenZipResult;
256
+ /** 解包安装包到指定目录(spawn 7z x,失败抛错)。 */
257
+ declare function extractInstaller(installerPath: string, destDir: string, sevenZipPath?: string): Promise<void>;
258
+ /**
259
+ * 从解包目录提取运行所需文件到缓存:
260
+ * resources/app 顶层全部 *.node + *.dll(保持同目录结构,PATH 前置即可解析依赖)。
261
+ * 幂等:目标 wrapper.node 已存在则跳过。
262
+ */
263
+ declare function extractWrapperFiles(extractedDir: string, version: string, cacheRoot: string): Promise<QqInstallInfo>;
264
+ /** 清理缓存版本目录(下载/解包失败残留,供调用方兜底)。 */
265
+ declare function clearCacheVersion(cacheRoot: string, version: string): Promise<void>;
266
+ //#endregion
267
+ //#region src/qq-releases.d.ts
268
+ /** 清单中单个版本条目。 */
269
+ interface QqReleaseEntry {
270
+ /** 版本目录名(如 9.9.33-51802)。 */
271
+ version: string;
272
+ /** 官方安装包下载 URL。 */
273
+ url: string;
274
+ /** sha256(十六进制小写);空串 = 尚未校验(首次录入时 CI 计算后填入)。 */
275
+ sha256: string;
276
+ /** appid(预知信息,运行时仍从 major.node 解析)。 */
277
+ appid: number;
278
+ /** 来源标记(official = 官方)。 */
279
+ source: string;
280
+ /** 构建日期(YYYY-MM-DD,可选)。 */
281
+ buildDate?: string;
282
+ }
283
+ /** 清单文件结构。 */
284
+ interface QqReleasesFile {
285
+ schema: number;
286
+ known: QqReleaseEntry[];
287
+ }
288
+ /** 清单解析失败。 */
289
+ declare class QqReleasesError extends Error {
290
+ constructor(message: string);
291
+ }
292
+ /** 读版本清单(同步,启动期调用一次即可)。 */
293
+ declare function loadQqReleases(): QqReleasesFile;
294
+ /** 取最新已知可用版本(known 数组按录入顺序,取最后一项 = 最新)。 */
295
+ declare function latestRelease(releases: QqReleasesFile): QqReleaseEntry;
296
+ /**
297
+ * 解析实际下载 URL:NAPUTO_QQ_URL 环境变量覆盖 > 清单 URL。
298
+ * 供 downloadFile 使用(也支持按版本查询,当前固定取最新)。
299
+ */
300
+ declare function resolveDownloadUrl(release: QqReleaseEntry): string;
301
+ //#endregion
302
+ //#region src/win-node.d.ts
303
+ /** 运行时缓存子目录名(<数据根>/runtime/win-node/)。 */
304
+ declare const WIN_NODE_DIR_NAME = "win-node";
305
+ /** Windows node.exe 定位结果。 */
306
+ interface WinNodeInfo {
307
+ /** node.exe 绝对路径(Windows 视角,供 wine 调用)。 */
308
+ exePath: string;
309
+ /** 已下载的完整版 node zip 版本号(如 v24.16.0)。 */
310
+ version: string;
311
+ }
312
+ /** 解析 nodejs.org zip URL(与 dist 目录命名一致)。 */
313
+ declare function nodeZipUrl(version: string): string;
314
+ /**
315
+ * 确保 Windows 版 node.exe 就绪(下载 + 解压 + 缓存)。
316
+ * 幂等:<数据根>/runtime/win-node/<version>/node.exe 已存在则直接返回。
317
+ */
318
+ declare function ensureWinNode(options?: {
319
+ /** 数据根(缓存 <数据根>/runtime/win-node/)。 */
320
+ dataRoot?: string;
321
+ /** node 版本(缺省读 NAPUTO_WIN_NODE_VERSION,再兜底 latest v24)。 */
322
+ version?: string;
323
+ /** 显式 node.exe 路径(覆盖缓存逻辑,读 NAPUTO_WIN_NODE_PATH)。 */
324
+ exePath?: string;
325
+ }): Promise<WinNodeInfo>;
326
+ //#endregion
327
+ //#region src/wine.d.ts
328
+ /**
329
+ * wine.ts:Linux 上经 wine 运行 Windows 版 node.exe 的路径与 spawn 组装(P2)。
330
+ *
331
+ * 设计文档 §3.2(2026-08-12 拍板确认)。核心认知:
332
+ * - Windows 版 node.exe 在 wine 内把 Linux 根目录挂载在 `Z:\`
333
+ * - process.dlopen / 环境变量路径都是 wine 视角的 Windows 路径
334
+ * - 传给 wine 子进程的**所有路径**(wrapperPath / cfgDir / stubDir / self-host.cjs / PATH)
335
+ * 都要过 toWinePath 转成 `Z:\...`
336
+ *
337
+ * 纯函数设计:不 import node:child_process,方便 Windows 上单测断言(P2 纯逻辑层)。
338
+ */
339
+ /** Linux 绝对路径 → wine `Z:\` 路径(Windows 版 node.exe 视角)。幂等:已是 Windows 路径(含盘符)原样返回。 */
340
+ declare function toWinePath(linuxPath: string): string;
341
+ /** 判断当前是否 Linux(需 wine 的场景)。 */
342
+ declare function isLinux(): boolean;
343
+ /** wine 可执行文件(Linux 上 spawn 用;可被 NAPUTO_WINE 覆盖)。 */
344
+ declare function wineBinary(): string;
345
+ /** spawn 命令与参数(平台分支;纯函数,可单测)。 */
346
+ interface SpawnCommand {
347
+ /** 命令(win32 = node.exe;linux = wine)。 */
348
+ command: string;
349
+ /** 参数(win32 = [selfHostPath];linux = [winNodePath, selfHostPath])。 */
350
+ args: string[];
351
+ }
352
+ /** 组装 spawn 命令(win32 vs linux 分支)。 */
353
+ declare function buildSpawnCommand(options: {
354
+ /** 平台(缺省 process.platform,测试注入)。 */
355
+ platform?: NodeJS.Platform;
356
+ /** Windows 本机 node 路径(win32 用,缺省 process.execPath)。 */
357
+ winNodePath?: string;
358
+ /** wine 命令(linux 用,缺省 wineBinary())。 */
359
+ wine?: string;
360
+ /** 自建宿主入口(self-host.cjs,传给 wine/win node)。 */
361
+ selfHostPath: string;
362
+ }): SpawnCommand;
363
+ //#endregion
364
+ export { DownloadError, type DownloadOptions, type DownloadResult, ENV, type ExtractOptions, INSTANCE_LOCK_FILE, type InstanceLockCheck, type InstanceLockInfo, type LaunchOptions, type LaunchResult, QQ_FILES_DIR_NAME, type QqFileSource, type QqInstallInfo, type QqReleaseEntry, QqReleasesError, type QqReleasesFile, type ResolveQqFilesOptions, type SevenZipResult, type SpawnCommand, WIN_NODE_DIR_NAME, type WinNodeInfo, acquireInstanceLock, buildSpawnCommand, checkInstanceLock, clearCacheVersion, defaultStubDir, downloadFile, ensureQqFiles, ensureWinNode, extractInstaller, extractWrapperFiles, findSevenZip, isLinux, latestRelease, launchSelfHost, loadQqReleases, locateQqPath, nodeZipUrl, registerLockCleanup, releaseInstanceLock, resolveDownloadUrl, resolveQqFiles, resolveQqInstall, toWinePath, wineBinary };
365
+ //# sourceMappingURL=index.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../src/instance-lock.ts","../src/locate-qq.ts","../src/launcher.ts","../src/qq-download.ts","../src/qq-extract.ts","../src/qq-releases.ts","../src/win-node.ts","../src/wine.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;cA2Ba;;UAGI;;EAEb;;EAEA;;EAEA;;;UAIa;;EAEb;;EAEA;;EAEA;;;;;;;iBAQY,kBAAkB,kBAAkB;;;;;iBA+BpC,oBAAoB,iBAAiB;;;;;iBA4BrC,oBAAoB;;iBAapB,oBAAoB;;;;cC5GvB;;KAiDD;;UAGK;;EAEb;;EAEA;;EAEA;;EAEA;;EAEA,QAAQ;;;UAIK;;EAEb;;EAEA;;EAEA;;EAEA;;EAEA;;;iBAIY;;iBAwCA,iBAAiB,kBAAkB;;;;;;iBAY7B,eAAe,UAAS,wBAA6B,QAAQ;;;;;iBA0D7D,cAClB;;EAEI;;EAEA;IAEL,QAAQ;;;UChMM;;EAEb,IAAI;;EAEJ;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;;;;;;;EAQA;;;;;;EAMA,QAAQ;;UAGK;;EAEb,OAAO,kBAAkB;;EAEzB;;;;;;;;;;;;;iBAckB,eAAe,SAAS,gBAAgB,QAAQ;;;;;iBAoFtD;;cA4CH;WACT;WACA;WACA;WACA;WACA;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;;;;UCnNa;;EAEb;;EAEA;;EAEA;;EAEA;;;UAIa;;EAEb;;EAEA;;;cAIS,sBAAsB;EAC/B,YAAY;;;;;;iBA8CM,aAAa,SAAS,kBAAkB,QAAQ;;;;UCrDrD;;EAEb;;EAEA;;;UAIa;;EAEb;;EAEA;;EAEA;;EAEA;;;;;;;iBAQY,gBAAgB;;iBAuBV,iBAClB,uBACA,iBACA,wBACD;;;;;;iBA8BmB,oBAClB,sBACA,iBACA,oBACD,QAAQ;;iBAkDW,kBAAkB,mBAAmB,kBAAkB;;;;UC1J5D;;EAEb;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;;UAIa;EACb;EACA,OAAO;;;cAIE,wBAAwB;EACjC,YAAY;;;iBAYA,kBAAkB;;iBAoBlB,cAAc,UAAU,iBAAiB;;;;;iBAYzC,mBAAmB,SAAS;;;;cC3D/B;;UAGI;;EAEb;;EAEA;;;iBAIY,WAAW;;;;;iBAQL,cAClB;;EAEI;;EAEA;;EAEA;IAEL,QAAQ;;;;;;;;;;;;;;;iBCpCK,WAAW;;iBAiBX;;iBAKA;;UAKC;;EAEb;;EAEA;;;iBAIY,kBAAkB;;EAE9B,WAAW,OAAO;;EAElB;;EAEA;;EAEA;IACA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@napuketto/loader",
3
- "version": "0.0.9",
3
+ "version": "0.0.10",
4
4
  "description": "NapukettoQQ 引导组件:自建宿主(标准 Node + stub QQNT.dll)dlopen wrapper.node 并启动 kernel,不拉起 QQ / 不注入",
5
5
  "type": "module",
6
6
  "files": [
@@ -9,15 +9,19 @@
9
9
  "qq-releases.json",
10
10
  "assets/7zip"
11
11
  ],
12
- "main": "./dist/index.mjs",
12
+ "main": "./dist/index.cjs",
13
13
  "module": "./dist/index.mjs",
14
14
  "types": "./dist/index.d.mts",
15
15
  "exports": {
16
16
  ".": {
17
- "types": "./dist/index.d.mts",
18
- "import": "./dist/index.mjs",
19
- "require": "./dist/index.mjs",
20
- "default": "./dist/index.mjs"
17
+ "import": {
18
+ "types": "./dist/index.d.mts",
19
+ "default": "./dist/index.mjs"
20
+ },
21
+ "require": {
22
+ "types": "./dist/index.d.cts",
23
+ "default": "./dist/index.cjs"
24
+ }
21
25
  }
22
26
  },
23
27
  "scripts": {