@napuketto/loader 0.0.8 → 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/assets/7zip/7z.dll +0 -0
- package/assets/7zip/7z.exe +0 -0
- package/assets/7zip/License.txt +146 -0
- package/dist/index.cjs +849 -0
- package/dist/index.d.cts +365 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +208 -7
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +575 -43
- package/dist/index.mjs.map +1 -1
- package/package.json +35 -29
- package/qq-releases.json +13 -0
- package/LICENSE +0 -21
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
|
-
import process from "node:process";
|
|
4
|
-
import { execFileSync, spawn } from "node:child_process";
|
|
3
|
+
import process$1 from "node:process";
|
|
4
|
+
import { execFile, execFileSync, spawn } from "node:child_process";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { copyFile, mkdir, readdir, rm } from "node:fs/promises";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import { get } from "node:https";
|
|
6
9
|
//#region src/instance-lock.ts
|
|
7
10
|
/**
|
|
8
11
|
* instance-lock.ts:单实例锁(2026-08-07 根治「多实例抢数据目录锁挂起」)
|
|
@@ -51,7 +54,7 @@ function checkInstanceLock(dataDir) {
|
|
|
51
54
|
};
|
|
52
55
|
let alive = false;
|
|
53
56
|
try {
|
|
54
|
-
process.kill(info.pid, 0);
|
|
57
|
+
process$1.kill(info.pid, 0);
|
|
55
58
|
alive = true;
|
|
56
59
|
} catch {
|
|
57
60
|
alive = false;
|
|
@@ -74,11 +77,11 @@ function checkInstanceLock(dataDir) {
|
|
|
74
77
|
function acquireInstanceLock(dataDir, cmdline) {
|
|
75
78
|
const { occupied, pid, lockPath } = checkInstanceLock(dataDir);
|
|
76
79
|
if (occupied) {
|
|
77
|
-
if (pid === process.pid) return true;
|
|
80
|
+
if (pid === process$1.pid) return true;
|
|
78
81
|
return false;
|
|
79
82
|
}
|
|
80
83
|
const info = {
|
|
81
|
-
pid: process.pid,
|
|
84
|
+
pid: process$1.pid,
|
|
82
85
|
startedAt: Date.now(),
|
|
83
86
|
...cmdline !== void 0 && cmdline !== "" ? { cmdline } : {}
|
|
84
87
|
};
|
|
@@ -97,16 +100,351 @@ function acquireInstanceLock(dataDir, cmdline) {
|
|
|
97
100
|
function releaseInstanceLock(dataDir) {
|
|
98
101
|
const lockPath = join(dataDir, INSTANCE_LOCK_FILE);
|
|
99
102
|
try {
|
|
100
|
-
if (JSON.parse(readFileSync(lockPath, "utf-8")).pid === process.pid) rmSync(lockPath, { force: true });
|
|
103
|
+
if (JSON.parse(readFileSync(lockPath, "utf-8")).pid === process$1.pid) rmSync(lockPath, { force: true });
|
|
101
104
|
} catch {}
|
|
102
105
|
}
|
|
103
106
|
/** 注册进程退出清理(SIGINT/SIGTERM/exit 均触发;崩溃强杀由残留检测兜底)。 */
|
|
104
107
|
function registerLockCleanup(dataDir) {
|
|
105
|
-
process.on("exit", () => {
|
|
108
|
+
process$1.on("exit", () => {
|
|
106
109
|
releaseInstanceLock(dataDir);
|
|
107
110
|
});
|
|
108
111
|
}
|
|
109
112
|
//#endregion
|
|
113
|
+
//#region src/qq-download.ts
|
|
114
|
+
/**
|
|
115
|
+
* qq-download.ts:QQ 官方安装包下载器(P1)。
|
|
116
|
+
*
|
|
117
|
+
* 无第三方依赖:Node 内置 https + crypto(sha256)。设计文档 §2.2。
|
|
118
|
+
* - https GET 流式写文件(低内存占用,313MB 安装包不整载入内存)
|
|
119
|
+
* - 下载同时流式累加 sha256,完成后与清单比对(防链接漂移劫持 / 下载不完整)
|
|
120
|
+
* - 支持 302 重定向(QQ CDN 可能跳转)
|
|
121
|
+
* - NAPUTO_QQ_URL 环境变量可覆盖下载地址(用户拿到新链接时用)
|
|
122
|
+
*/
|
|
123
|
+
/** 下载中断(超时 / 非 2xx / 网络错误)抛出的错误。 */
|
|
124
|
+
var DownloadError = class extends Error {
|
|
125
|
+
constructor(message) {
|
|
126
|
+
super(message);
|
|
127
|
+
this.name = "DownloadError";
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
/** 发起一次 https GET,返回响应(跟随重定向,最多 5 跳)。 */
|
|
131
|
+
function httpsGet(url, timeoutMs) {
|
|
132
|
+
return new Promise((resolve, reject) => {
|
|
133
|
+
const request = (currentUrl, redirects) => {
|
|
134
|
+
const req = get(currentUrl, { headers: { "user-agent": "napuketto-loader/0.0.x" } }, (res) => {
|
|
135
|
+
const status = res.statusCode ?? 0;
|
|
136
|
+
if (status >= 300 && status < 400 && res.headers["location"] !== void 0) {
|
|
137
|
+
res.resume();
|
|
138
|
+
if (redirects >= 5) {
|
|
139
|
+
reject(new DownloadError(`重定向次数过多: ${currentUrl}`));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
request(new URL(res.headers["location"], currentUrl).toString(), redirects + 1);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (status !== 200) {
|
|
146
|
+
res.resume();
|
|
147
|
+
reject(new DownloadError(`HTTP ${status}: ${currentUrl}`));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
resolve(res);
|
|
151
|
+
});
|
|
152
|
+
req.setTimeout(timeoutMs, () => {
|
|
153
|
+
req.destroy(new DownloadError(`下载超时(${timeoutMs}ms): ${currentUrl}`));
|
|
154
|
+
});
|
|
155
|
+
req.on("error", (err) => {
|
|
156
|
+
reject(err instanceof Error ? err : new DownloadError(String(err)));
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
request(url, 0);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* 下载文件并(可选)校验 sha256。失败时清理半成品文件。
|
|
164
|
+
* 返回实际 sha256(无论是否校验),便于调用方回填清单。
|
|
165
|
+
*/
|
|
166
|
+
async function downloadFile(options) {
|
|
167
|
+
const url = options.url ?? process$1.env["NAPUTO_QQ_URL"];
|
|
168
|
+
if (url === void 0 || url === "") throw new DownloadError("下载地址为空:请设置 NAPUTO_QQ_URL 或提供清单 URL");
|
|
169
|
+
const timeoutMs = options.timeoutMs ?? 6e4;
|
|
170
|
+
mkdirSync(dirname(options.dest), { recursive: true });
|
|
171
|
+
const hash = createHash("sha256");
|
|
172
|
+
const fileStream = createWriteStream(options.dest);
|
|
173
|
+
let fileFailed = false;
|
|
174
|
+
try {
|
|
175
|
+
const res = await httpsGet(url, timeoutMs);
|
|
176
|
+
await new Promise((resolve, reject) => {
|
|
177
|
+
res.on("data", (chunk) => {
|
|
178
|
+
hash.update(chunk);
|
|
179
|
+
});
|
|
180
|
+
res.on("error", (err) => {
|
|
181
|
+
fileFailed = true;
|
|
182
|
+
reject(err instanceof Error ? err : new DownloadError(String(err)));
|
|
183
|
+
});
|
|
184
|
+
fileStream.on("error", (err) => {
|
|
185
|
+
fileFailed = true;
|
|
186
|
+
reject(err);
|
|
187
|
+
});
|
|
188
|
+
fileStream.on("finish", () => {
|
|
189
|
+
resolve();
|
|
190
|
+
});
|
|
191
|
+
res.pipe(fileStream);
|
|
192
|
+
});
|
|
193
|
+
} catch (err) {
|
|
194
|
+
fileStream.destroy();
|
|
195
|
+
if (!fileFailed) await rm(options.dest, { force: true });
|
|
196
|
+
throw err instanceof Error ? err : new DownloadError(String(err));
|
|
197
|
+
}
|
|
198
|
+
const sha256 = hash.digest("hex");
|
|
199
|
+
const expected = options.expectedSha256;
|
|
200
|
+
const verified = expected === void 0 || expected === "" ? true : sha256.toLowerCase() === expected.toLowerCase();
|
|
201
|
+
if (!verified) {
|
|
202
|
+
await rm(options.dest, { force: true });
|
|
203
|
+
throw new DownloadError(`sha256 校验失败: ${options.dest}\n期望 ${expected}\n实际 ${sha256}`);
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
sha256,
|
|
207
|
+
verified
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
//#endregion
|
|
211
|
+
//#region src/qq-extract.ts
|
|
212
|
+
/**
|
|
213
|
+
* qq-extract.ts:QQ 官方安装包解包与提取(P1)。
|
|
214
|
+
*
|
|
215
|
+
* ⚠️ 格式事实(2026-08-12 实测修正):QQ 官方安装包是 NSIS 自解压格式,
|
|
216
|
+
* **7za standalone 不支持 NSIS**(只支持 7z/zip/tar 等)——必须用完整版
|
|
217
|
+
* 7z(7z.exe + 7z.dll)。设计文档 §2.3 原「内置 7za.exe」修正为:
|
|
218
|
+
* - Windows:内置 assets/7zip/7z.exe + 7z.dll(7-Zip 官方,LGPL 合规可分发)
|
|
219
|
+
* - Linux/Docker:系统 p7zip-full(7z 完整版)
|
|
220
|
+
*
|
|
221
|
+
* ⚠️ 提取策略(2026-08-12 实测修正,与设计 §2.3 不同):
|
|
222
|
+
* 1. NSIS 解包产物在 <extracted>/Files/(NSIS 内部布局),Files/ = 安装根
|
|
223
|
+
* 2. 真实 QQNT.dll(214MB)位于 versions/<v>/QQNT.dll,**自建宿主不需要**
|
|
224
|
+
* ——stub QQNT.dll 已替代其宿主符号职责(wrapper.node 的 v8/node/napi/qq_magic
|
|
225
|
+
* 符号由 stub 转发到 node.exe)
|
|
226
|
+
* 3. 只提取 resources/app 顶层 *.node + *.dll(wrapper.node 的直接/传递/delay-load
|
|
227
|
+
* 依赖全覆盖,版本无关;跳过 wmpfsdk/avsdk 等大子目录,共约 235MB)
|
|
228
|
+
*
|
|
229
|
+
* 提取目标(与安装目录同构,resolveFromRoot 可直接消费):
|
|
230
|
+
* <数据根>/qq-files/<版本>/versions/<版本>/resources/app/ ← *.node + *.dll
|
|
231
|
+
*/
|
|
232
|
+
/** resources/app 下需要提取的文件扩展名(.node 原生模块 + .dll 依赖)。 */
|
|
233
|
+
const EXTRACT_EXTENSIONS = /* @__PURE__ */ new Set([".node", ".dll"]);
|
|
234
|
+
/**
|
|
235
|
+
* 探测 7z 可执行文件:
|
|
236
|
+
* NAPUTO_7Z_PATH 环境变量 > 内置资产(assets/7zip/7z.exe,Windows)> 系统 PATH 7z。
|
|
237
|
+
* Linux 上用 p7zip 提供的 7z 命令(系统 PATH)。
|
|
238
|
+
*/
|
|
239
|
+
function findSevenZip() {
|
|
240
|
+
const envPath = process$1.env["NAPUTO_7Z_PATH"];
|
|
241
|
+
if (envPath !== void 0 && envPath !== "" && existsSync(envPath)) return {
|
|
242
|
+
exe: envPath,
|
|
243
|
+
source: "环境变量 NAPUTO_7Z_PATH"
|
|
244
|
+
};
|
|
245
|
+
if (process$1.platform === "win32") {
|
|
246
|
+
const bundled = join(dirname(fileURLToPath(import.meta.url)), "..", "assets", "7zip", "7z.exe");
|
|
247
|
+
if (existsSync(bundled)) return {
|
|
248
|
+
exe: bundled,
|
|
249
|
+
source: "内置资产 assets/7zip"
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
exe: "7z",
|
|
254
|
+
source: "系统 PATH"
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
/** 解包安装包到指定目录(spawn 7z x,失败抛错)。 */
|
|
258
|
+
async function extractInstaller(installerPath, destDir, sevenZipPath) {
|
|
259
|
+
const sevenZip = sevenZipPath ?? findSevenZip().exe;
|
|
260
|
+
await mkdir(destDir, { recursive: true });
|
|
261
|
+
await new Promise((resolve, reject) => {
|
|
262
|
+
execFile(sevenZip, [
|
|
263
|
+
"x",
|
|
264
|
+
installerPath,
|
|
265
|
+
`-o${destDir}`,
|
|
266
|
+
"-y"
|
|
267
|
+
], { maxBuffer: 67108864 }, (err, _stdout, stderr) => {
|
|
268
|
+
if (err !== null) {
|
|
269
|
+
const hint = err.code === "ENOENT" ? `未找到 7z 可执行文件: ${sevenZip}\n(Windows 请安装 7-Zip 或用 NAPUTO_7Z_PATH 指定;Linux 请安装 p7zip-full)` : "(QQ 安装包是 NSIS 格式,需完整版 7z 而非 7za)";
|
|
270
|
+
reject(/* @__PURE__ */ new Error(`7z 解包失败: ${installerPath}\n${stderr.trim()}\n${hint}`));
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
resolve();
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* 从解包目录提取运行所需文件到缓存:
|
|
279
|
+
* resources/app 顶层全部 *.node + *.dll(保持同目录结构,PATH 前置即可解析依赖)。
|
|
280
|
+
* 幂等:目标 wrapper.node 已存在则跳过。
|
|
281
|
+
*/
|
|
282
|
+
async function extractWrapperFiles(extractedDir, version, cacheRoot) {
|
|
283
|
+
const appDir = join(extractedDir, "Files", "versions", version, "resources", "app");
|
|
284
|
+
if (!existsSync(join(appDir, "wrapper.node"))) throw new Error(`解包产物缺少 wrapper.node: ${appDir}`);
|
|
285
|
+
const destAppDir = join(cacheRoot, version, "versions", version, "resources", "app");
|
|
286
|
+
const destWrapper = join(destAppDir, "wrapper.node");
|
|
287
|
+
if (existsSync(destWrapper)) return buildCachedInfo(cacheRoot, version);
|
|
288
|
+
await mkdir(destAppDir, { recursive: true });
|
|
289
|
+
const entries = await readdir(appDir, { withFileTypes: true });
|
|
290
|
+
for (const entry of entries) {
|
|
291
|
+
if (!entry.isFile()) continue;
|
|
292
|
+
const dot = entry.name.lastIndexOf(".");
|
|
293
|
+
const ext = dot >= 0 ? entry.name.slice(dot).toLowerCase() : "";
|
|
294
|
+
if (!EXTRACT_EXTENSIONS.has(ext)) continue;
|
|
295
|
+
await copyFile(join(appDir, entry.name), join(destAppDir, entry.name));
|
|
296
|
+
}
|
|
297
|
+
if (!existsSync(destWrapper)) throw new Error(`提取失败:wrapper.node 未复制到 ${destWrapper}`);
|
|
298
|
+
return buildCachedInfo(cacheRoot, version);
|
|
299
|
+
}
|
|
300
|
+
/** 构造 cached 来源的 QqInstallInfo(wrapperPath 指向缓存中 wrapper.node)。 */
|
|
301
|
+
function buildCachedInfo(cacheRoot, version) {
|
|
302
|
+
const versionDir = join(cacheRoot, version);
|
|
303
|
+
return {
|
|
304
|
+
qqPath: join(versionDir, "QQ.exe"),
|
|
305
|
+
installDir: versionDir,
|
|
306
|
+
version,
|
|
307
|
+
wrapperPath: join(versionDir, "versions", version, "resources", "app", "wrapper.node"),
|
|
308
|
+
source: "cached"
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
/** 清理缓存版本目录(下载/解包失败残留,供调用方兜底)。 */
|
|
312
|
+
async function clearCacheVersion(cacheRoot, version) {
|
|
313
|
+
await rm(join(cacheRoot, version), {
|
|
314
|
+
recursive: true,
|
|
315
|
+
force: true
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
//#endregion
|
|
319
|
+
//#region src/win-node.ts
|
|
320
|
+
/**
|
|
321
|
+
* win-node.ts:Windows 版 node.exe 获取(P2,Linux/wine 场景用)。
|
|
322
|
+
*
|
|
323
|
+
* wine 只能跑 Windows PE 程序——自建宿主在 Linux 上必须用 Windows 版 node.exe
|
|
324
|
+
* 作为宿主(wrapper.node 是 PE 原生模块,只能被 Windows node.exe dlopen)。
|
|
325
|
+
* 来源:nodejs.org 官方 zip(开源软件官方发行版,但为保守起见同样不内置,
|
|
326
|
+
* 运行时下载到 <数据根>/runtime/win-node/,可被 NAPUTO_WIN_NODE_PATH 覆盖)。
|
|
327
|
+
*
|
|
328
|
+
* 设计文档 §3.1。与 qq-download.ts 同构:https 下载 + 解压 + 缓存幂等。
|
|
329
|
+
*/
|
|
330
|
+
/** 目标 Windows node.exe 可执行文件名(zip 内)。 */
|
|
331
|
+
const WIN_NODE_EXE = "node.exe";
|
|
332
|
+
/** 运行时缓存子目录名(<数据根>/runtime/win-node/)。 */
|
|
333
|
+
const WIN_NODE_DIR_NAME = "win-node";
|
|
334
|
+
/** 解析 nodejs.org zip URL(与 dist 目录命名一致)。 */
|
|
335
|
+
function nodeZipUrl(version) {
|
|
336
|
+
return `https://nodejs.org/dist/${version}/node-${version}-win-x64.zip`;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* 确保 Windows 版 node.exe 就绪(下载 + 解压 + 缓存)。
|
|
340
|
+
* 幂等:<数据根>/runtime/win-node/<version>/node.exe 已存在则直接返回。
|
|
341
|
+
*/
|
|
342
|
+
async function ensureWinNode(options = {}) {
|
|
343
|
+
const explicit = options.exePath ?? process$1.env["NAPUTO_WIN_NODE_PATH"];
|
|
344
|
+
if (explicit !== void 0 && explicit !== "" && existsSync(explicit)) return {
|
|
345
|
+
exePath: resolve(explicit),
|
|
346
|
+
version: "explicit"
|
|
347
|
+
};
|
|
348
|
+
const version = options.version ?? process$1.env["NAPUTO_WIN_NODE_VERSION"] ?? DEFAULT_WIN_NODE_VERSION;
|
|
349
|
+
const dataRoot = resolveDataRoot(options.dataRoot);
|
|
350
|
+
const cacheDir = join(dataRoot, "runtime", WIN_NODE_DIR_NAME, version);
|
|
351
|
+
const exePath = join(cacheDir, WIN_NODE_EXE);
|
|
352
|
+
if (existsSync(exePath)) return {
|
|
353
|
+
exePath,
|
|
354
|
+
version
|
|
355
|
+
};
|
|
356
|
+
const zipPath = join(cacheDir, `node-${version}-win-x64.zip`);
|
|
357
|
+
const url = process$1.env["NAPUTO_WIN_NODE_URL"] ?? nodeZipUrl(version);
|
|
358
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
359
|
+
try {
|
|
360
|
+
await downloadFile({
|
|
361
|
+
dest: zipPath,
|
|
362
|
+
url
|
|
363
|
+
});
|
|
364
|
+
await unzipNode(zipPath, cacheDir);
|
|
365
|
+
} finally {
|
|
366
|
+
await rm(zipPath, { force: true });
|
|
367
|
+
}
|
|
368
|
+
if (!existsSync(exePath)) throw new Error(`解压后未找到 node.exe: ${exePath}`);
|
|
369
|
+
return {
|
|
370
|
+
exePath,
|
|
371
|
+
version
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
/** 解压 node zip(zip 内为 node-<version>-win-x64/ 目录,取其中 node.exe 移到缓存根)。 */
|
|
375
|
+
async function unzipNode(zipPath, cacheDir) {
|
|
376
|
+
const tmpDir = join(cacheDir, "_zip");
|
|
377
|
+
await extractInstaller(zipPath, tmpDir);
|
|
378
|
+
const nodeDir = (await readdir(tmpDir)).find((n) => n.startsWith("node-"));
|
|
379
|
+
const src = nodeDir === void 0 ? join(tmpDir, WIN_NODE_EXE) : join(tmpDir, nodeDir, WIN_NODE_EXE);
|
|
380
|
+
if (!existsSync(src)) throw new Error(`node zip 解压后未找到 node.exe: ${src}`);
|
|
381
|
+
await copyFile(src, join(cacheDir, WIN_NODE_EXE));
|
|
382
|
+
await rm(tmpDir, {
|
|
383
|
+
recursive: true,
|
|
384
|
+
force: true
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
/** 默认 Windows node 版本(latest v24,LTS 线;可被环境变量/参数覆盖)。 */
|
|
388
|
+
const DEFAULT_WIN_NODE_VERSION = "v24.16.0";
|
|
389
|
+
/** 轻量数据根解析(与 locate-qq 内同构,避免循环依赖)。 */
|
|
390
|
+
function resolveDataRoot(dataRoot) {
|
|
391
|
+
const explicit = dataRoot ?? process$1.env["NAPKETTO_DATA"];
|
|
392
|
+
if (explicit !== void 0 && explicit !== "") return resolve(explicit);
|
|
393
|
+
const projectRoot = findProjectRoot$1(process$1.cwd()) ?? process$1.cwd();
|
|
394
|
+
return join(projectRoot, ".napuketto");
|
|
395
|
+
}
|
|
396
|
+
/** 向上探测项目根(含 pnpm-workspace.yaml 或 package.json 的目录)。 */
|
|
397
|
+
function findProjectRoot$1(start) {
|
|
398
|
+
let dir = resolve(start);
|
|
399
|
+
for (;;) {
|
|
400
|
+
if (existsSync(join(dir, "pnpm-workspace.yaml")) || existsSync(join(dir, "package.json"))) return dir;
|
|
401
|
+
const parent = dirname(dir);
|
|
402
|
+
if (parent === dir) return null;
|
|
403
|
+
dir = parent;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
//#endregion
|
|
407
|
+
//#region src/wine.ts
|
|
408
|
+
/**
|
|
409
|
+
* wine.ts:Linux 上经 wine 运行 Windows 版 node.exe 的路径与 spawn 组装(P2)。
|
|
410
|
+
*
|
|
411
|
+
* 设计文档 §3.2(2026-08-12 拍板确认)。核心认知:
|
|
412
|
+
* - Windows 版 node.exe 在 wine 内把 Linux 根目录挂载在 `Z:\`
|
|
413
|
+
* - process.dlopen / 环境变量路径都是 wine 视角的 Windows 路径
|
|
414
|
+
* - 传给 wine 子进程的**所有路径**(wrapperPath / cfgDir / stubDir / self-host.cjs / PATH)
|
|
415
|
+
* 都要过 toWinePath 转成 `Z:\...`
|
|
416
|
+
*
|
|
417
|
+
* 纯函数设计:不 import node:child_process,方便 Windows 上单测断言(P2 纯逻辑层)。
|
|
418
|
+
*/
|
|
419
|
+
/** Linux 绝对路径 → wine `Z:\` 路径(Windows 版 node.exe 视角)。幂等:已是 Windows 路径(含盘符)原样返回。 */
|
|
420
|
+
function toWinePath(linuxPath) {
|
|
421
|
+
if (DRIVE_LETTER_RE.test(linuxPath)) return linuxPath;
|
|
422
|
+
return `Z:\\${linuxPath.replaceAll("/", "\\").replace(LEADING_SLASH_RE, "")}`;
|
|
423
|
+
}
|
|
424
|
+
/** 前导斜杠(toWinePath 去根用,顶层常量避免每次调用重建)。 */
|
|
425
|
+
const LEADING_SLASH_RE = /^\\/;
|
|
426
|
+
/** 盘符前缀(C: / Z: 等,识别已转换的 Windows 路径)。 */
|
|
427
|
+
const DRIVE_LETTER_RE = /^[A-Za-z]:/;
|
|
428
|
+
/** 判断当前是否 Linux(需 wine 的场景)。 */
|
|
429
|
+
function isLinux() {
|
|
430
|
+
return process.platform === "linux";
|
|
431
|
+
}
|
|
432
|
+
/** wine 可执行文件(Linux 上 spawn 用;可被 NAPUTO_WINE 覆盖)。 */
|
|
433
|
+
function wineBinary() {
|
|
434
|
+
return process.env["NAPUTO_WINE"] ?? "wine";
|
|
435
|
+
}
|
|
436
|
+
/** 组装 spawn 命令(win32 vs linux 分支)。 */
|
|
437
|
+
function buildSpawnCommand(options) {
|
|
438
|
+
if ((options.platform ?? process.platform) === "win32") return {
|
|
439
|
+
command: options.winNodePath ?? process.execPath,
|
|
440
|
+
args: [options.selfHostPath]
|
|
441
|
+
};
|
|
442
|
+
return {
|
|
443
|
+
command: options.wine ?? wineBinary(),
|
|
444
|
+
args: [options.winNodePath ?? "node.exe", options.selfHostPath]
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
//#endregion
|
|
110
448
|
//#region src/launcher.ts
|
|
111
449
|
/**
|
|
112
450
|
* launcher:自建宿主引导(2026-08-07 唯一路线)——spawn 标准 node 跑 self-host.cjs。
|
|
@@ -116,32 +454,42 @@ function registerLockCleanup(dataDir) {
|
|
|
116
454
|
* 2. QQ 是打包应用,禁 NODE_OPTIONS(实测 stderr)
|
|
117
455
|
* → 旧方案:NapukettoBootMain.exe 启动 QQ + 注入 NapukettoWinBootHook.dll(已废弃)
|
|
118
456
|
* → 现方案:stub QQNT.dll 转发宿主符号 → 标准 node 直接 dlopen wrapper.node(自建宿主)
|
|
457
|
+
*
|
|
458
|
+
* P2(2026-08-12):平台分支——win32 本机 node;linux 经 wine 跑 Windows 版 node.exe
|
|
459
|
+
* (ensureWinNode 下载)。所有传给 wine 子进程的路径过 toWinePath(Z:\)。
|
|
119
460
|
*/
|
|
120
461
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
121
462
|
/**
|
|
122
|
-
* 启动自建宿主(路线 A
|
|
123
|
-
* 不拉起 QQ / 不注入。
|
|
463
|
+
* 启动自建宿主(路线 A):spawn 标准 node 直接跑 self-host.cjs,不拉起 QQ / 不注入。
|
|
124
464
|
*
|
|
125
465
|
* 关键(HANDOVER-V7 技术数据):wrapper.node 从 QQNT.dll 导入 99 符号
|
|
126
466
|
* (napi_*×40 + uv_*×56 + qq_magic×1 + v8/node mangled×2)——标准 node 无这些宿主
|
|
127
467
|
* 符号,必须 PATH 前置 stub QQNT.dll 目录(转发到 node.exe)+ QQ resources\app
|
|
128
468
|
* (wrapper.node 同目录依赖 DLL)才能 dlopen 成功。
|
|
469
|
+
*
|
|
470
|
+
* P2 平台分支:win32 用本机 node;linux 用 wine + Windows 版 node.exe
|
|
471
|
+
* (ensureWinNode 自动下载,路径过 toWinePath)。async(win-node 可能需下载)。
|
|
129
472
|
*/
|
|
130
|
-
function launchSelfHost(options) {
|
|
473
|
+
async function launchSelfHost(options) {
|
|
131
474
|
const selfHostPath = options.selfHostEntry ?? join(__dirname, "..", "dist", "host", "self-host.cjs");
|
|
132
475
|
if (!existsSync(selfHostPath)) throw new Error(`self-host.cjs 缺失: ${selfHostPath}(先运行 pnpm --filter @napuketto/loader build)`);
|
|
133
476
|
const stub = options.stubDir ?? defaultStubDir();
|
|
134
477
|
if (!existsSync(join(stub, "QQNT.dll"))) throw new Error(`stub QQNT.dll 未找到: ${stub}(自建宿主必需:stub 转发 QQNT.dll 符号到 node.exe;请用 --stub-dir 或环境变量 NAPUTO_STUB_DIR 指定 stub 目录)`);
|
|
135
|
-
const
|
|
136
|
-
env
|
|
478
|
+
const { useWine, winNodePath } = await resolveNodeExecutable(options);
|
|
479
|
+
const env = buildLaunchEnv(options, useWine);
|
|
480
|
+
applyPathEnv(env, {
|
|
137
481
|
stub,
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
env[ENV.STUB_DIR] = stub;
|
|
482
|
+
wrapperPath: options.qq.wrapperPath,
|
|
483
|
+
useWine
|
|
484
|
+
});
|
|
142
485
|
if (options.cwd !== void 0) mkdirSync(options.cwd, { recursive: true });
|
|
486
|
+
const { command, args } = buildSpawnCommand({
|
|
487
|
+
...useWine && winNodePath !== void 0 ? { winNodePath } : { winNodePath: process$1.execPath },
|
|
488
|
+
selfHostPath: useWine ? toWinePath(selfHostPath) : selfHostPath,
|
|
489
|
+
wine: wineBinary$1()
|
|
490
|
+
});
|
|
143
491
|
return {
|
|
144
|
-
child: spawn(
|
|
492
|
+
child: spawn(command, args, {
|
|
145
493
|
...options.cwd !== void 0 ? { cwd: options.cwd } : {},
|
|
146
494
|
env,
|
|
147
495
|
stdio: options.stdio ?? "inherit",
|
|
@@ -150,6 +498,30 @@ function launchSelfHost(options) {
|
|
|
150
498
|
bootJsPath: selfHostPath
|
|
151
499
|
};
|
|
152
500
|
}
|
|
501
|
+
/** 平台分支解析 node 可执行:win32 本机;linux wine + Windows 版 node.exe。 */
|
|
502
|
+
async function resolveNodeExecutable(options) {
|
|
503
|
+
if (!isLinux()) return {
|
|
504
|
+
useWine: false,
|
|
505
|
+
winNodePath: void 0
|
|
506
|
+
};
|
|
507
|
+
return {
|
|
508
|
+
useWine: true,
|
|
509
|
+
winNodePath: (await ensureWinNode({
|
|
510
|
+
...options.cwd !== void 0 ? { dataRoot: options.cwd } : {},
|
|
511
|
+
...options.winNodePath !== void 0 ? { exePath: options.winNodePath } : {}
|
|
512
|
+
})).exePath
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
/** 注入 PATH(stub 目录 + wrapper.node 目录前置)与 STUB_DIR(wine 场景转 Z:\)。 */
|
|
516
|
+
function applyPathEnv(env, opts) {
|
|
517
|
+
const p = (linuxPath) => opts.useWine ? toWinePath(linuxPath) : linuxPath;
|
|
518
|
+
env["PATH"] = [
|
|
519
|
+
p(opts.stub),
|
|
520
|
+
p(dirname(opts.wrapperPath)),
|
|
521
|
+
process$1.env["PATH"] ?? ""
|
|
522
|
+
].filter((entry) => entry !== void 0 && entry !== "").join(";");
|
|
523
|
+
env[ENV.STUB_DIR] = p(opts.stub);
|
|
524
|
+
}
|
|
153
525
|
/**
|
|
154
526
|
* 默认 stub QQNT.dll 目录(loader 包内闭源 native/build/stub-test-env,开发机默认)。
|
|
155
527
|
* src 与 native 同层,构建后 dist 与 native 同层——`../native` 恒正确。
|
|
@@ -157,25 +529,35 @@ function launchSelfHost(options) {
|
|
|
157
529
|
function defaultStubDir() {
|
|
158
530
|
return join(__dirname, "..", "native", "build", "stub-test-env");
|
|
159
531
|
}
|
|
160
|
-
/**
|
|
161
|
-
function buildLaunchEnv(options) {
|
|
532
|
+
/** 装配自建宿主环境变量(wine 场景路径转 Z:\;win32 原样)。 */
|
|
533
|
+
function buildLaunchEnv(options, useWine) {
|
|
162
534
|
const cfg = resolve(options.cfgDir);
|
|
163
535
|
mkdirSync(cfg, { recursive: true });
|
|
536
|
+
const p = (linuxPath) => useWine ? toWinePath(linuxPath) : linuxPath;
|
|
164
537
|
const env = {
|
|
165
|
-
...process.env,
|
|
166
|
-
[ENV.QQ_PATH]: options.qq.qqPath,
|
|
167
|
-
[ENV.KERNEL_ENTRY]: resolve(options.kernelEntry),
|
|
168
|
-
[ENV.CFG_DIR]: cfg,
|
|
538
|
+
...process$1.env,
|
|
539
|
+
[ENV.QQ_PATH]: p(options.qq.qqPath),
|
|
540
|
+
[ENV.KERNEL_ENTRY]: p(resolve(options.kernelEntry)),
|
|
541
|
+
[ENV.CFG_DIR]: p(cfg),
|
|
169
542
|
[ENV.QQ_VERSION]: options.qq.version,
|
|
170
|
-
[ENV.WRAPPER_PATH]: options.qq.wrapperPath
|
|
543
|
+
[ENV.WRAPPER_PATH]: p(options.qq.wrapperPath)
|
|
544
|
+
};
|
|
545
|
+
const optional = {
|
|
546
|
+
...options.selfHost === true ? { [ENV.SELF_HOST]: "1" } : {},
|
|
547
|
+
...options.adapterEntry !== void 0 ? { [ENV.ADAPTER_ENTRY]: p(resolve(options.adapterEntry)) } : {},
|
|
548
|
+
...options.networkEntry !== void 0 ? { [ENV.NETWORK_ENTRY]: p(resolve(options.networkEntry)) } : {},
|
|
549
|
+
...options.configPath !== void 0 ? { [ENV.CONFIG_PATH]: p(resolve(options.configPath)) } : {},
|
|
550
|
+
...options.quickUin !== void 0 ? { [ENV.QUICK_UIN]: options.quickUin } : {},
|
|
551
|
+
...options.ipc === true ? { [ENV.IPC]: "1" } : {}
|
|
552
|
+
};
|
|
553
|
+
return {
|
|
554
|
+
...env,
|
|
555
|
+
...optional
|
|
171
556
|
};
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if (options.quickUin !== void 0) env[ENV.QUICK_UIN] = options.quickUin;
|
|
177
|
-
if (options.ipc === true) env[ENV.IPC] = "1";
|
|
178
|
-
return env;
|
|
557
|
+
}
|
|
558
|
+
/** wine 可执行文件(linux 场景;NAPUTO_WINE 可覆盖)。 */
|
|
559
|
+
function wineBinary$1() {
|
|
560
|
+
return process$1.env["NAPUTO_WINE"] ?? "wine";
|
|
179
561
|
}
|
|
180
562
|
/** 环境变量名(self-host.cjs 与 kernel 引导读取)。 */
|
|
181
563
|
const ENV = {
|
|
@@ -200,10 +582,68 @@ const ENV = {
|
|
|
200
582
|
IPC: "NAPUTO_IPC"
|
|
201
583
|
};
|
|
202
584
|
//#endregion
|
|
585
|
+
//#region src/qq-releases.ts
|
|
586
|
+
/**
|
|
587
|
+
* qq-releases.ts:版本清单读取(P1)。
|
|
588
|
+
*
|
|
589
|
+
* 运行时从包根 qq-releases.json 读取(发布时 files 含它,安装后位于
|
|
590
|
+
* node_modules/@napuketto/loader/qq-releases.json)。清单由 CI/社区 PR 维护
|
|
591
|
+
* (设计文档 §2.2)。NAPUTO_QQ_URL 可运行时覆盖下载地址。
|
|
592
|
+
*/
|
|
593
|
+
/** 清单解析失败。 */
|
|
594
|
+
var QqReleasesError = class extends Error {
|
|
595
|
+
constructor(message) {
|
|
596
|
+
super(message);
|
|
597
|
+
this.name = "QqReleasesError";
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
/** 包根路径(dist 与包根同层:dist/index.mjs → ../qq-releases.json)。 */
|
|
601
|
+
function packageRoot() {
|
|
602
|
+
return dirname(dirname(fileURLToPath(import.meta.url)));
|
|
603
|
+
}
|
|
604
|
+
/** 读版本清单(同步,启动期调用一次即可)。 */
|
|
605
|
+
function loadQqReleases() {
|
|
606
|
+
const file = join(packageRoot(), "qq-releases.json");
|
|
607
|
+
let raw;
|
|
608
|
+
try {
|
|
609
|
+
raw = readFileSync(file, "utf8");
|
|
610
|
+
} catch {
|
|
611
|
+
throw new QqReleasesError(`版本清单缺失: ${file}(@napuketto/loader 包安装不完整?)`);
|
|
612
|
+
}
|
|
613
|
+
try {
|
|
614
|
+
const parsed = JSON.parse(raw);
|
|
615
|
+
if (parsed.schema !== 1 || !Array.isArray(parsed.known) || parsed.known.length === 0) throw new Error("结构不合法");
|
|
616
|
+
return parsed;
|
|
617
|
+
} catch {
|
|
618
|
+
throw new QqReleasesError(`版本清单解析失败: ${file}`);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
/** 取最新已知可用版本(known 数组按录入顺序,取最后一项 = 最新)。 */
|
|
622
|
+
function latestRelease(releases) {
|
|
623
|
+
const [latest] = [...releases.known].reverse();
|
|
624
|
+
if (latest === void 0) throw new QqReleasesError("版本清单为空");
|
|
625
|
+
return latest;
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* 解析实际下载 URL:NAPUTO_QQ_URL 环境变量覆盖 > 清单 URL。
|
|
629
|
+
* 供 downloadFile 使用(也支持按版本查询,当前固定取最新)。
|
|
630
|
+
*/
|
|
631
|
+
function resolveDownloadUrl(release) {
|
|
632
|
+
const override = process$1.env["NAPUTO_QQ_URL"];
|
|
633
|
+
return override !== void 0 && override !== "" ? override : release.url;
|
|
634
|
+
}
|
|
635
|
+
//#endregion
|
|
203
636
|
//#region src/locate-qq.ts
|
|
204
637
|
/**
|
|
205
|
-
* 定位 QQ
|
|
638
|
+
* 定位 QQ 原生文件(wrapper.node)来源(P0:多级来源纯定位,不下载)。
|
|
639
|
+
*
|
|
640
|
+
* 来源优先级(resolveQqFiles):
|
|
641
|
+
* L0: NAPUTO_QQ_FILES / qqFilesDir —— 显式指定含 versions/ 结构的文件根(缓存/拷贝)
|
|
642
|
+
* L1: 本机 QQ 安装 —— NAPUTO_QQ_PATH / 注册表 / 常见路径(既有逻辑保留)
|
|
643
|
+
* L2: 数据根缓存 <数据根>/qq-files/<版本> —— 下载解包产物(P1 落地后才有)
|
|
206
644
|
*/
|
|
645
|
+
/** 数据根下 QQ 文件缓存目录名(<数据根>/qq-files/<版本>/,P1 下载解包产物)。 */
|
|
646
|
+
const QQ_FILES_DIR_NAME = "qq-files";
|
|
207
647
|
/** 注册表 UninstallString 查询(QQ 官方安装路径)。 */
|
|
208
648
|
const REG_QUERY_RE = /"([^"]+)"/;
|
|
209
649
|
/** 注册表 UninstallString 查询(QQ 官方安装路径)。 */
|
|
@@ -225,7 +665,7 @@ function findQqViaRegistry() {
|
|
|
225
665
|
}
|
|
226
666
|
/** 常见安装路径探测。 */
|
|
227
667
|
function findQqViaCommonPaths() {
|
|
228
|
-
const localAppData = process.env["LOCALAPPDATA"] ?? "";
|
|
668
|
+
const localAppData = process$1.env["LOCALAPPDATA"] ?? "";
|
|
229
669
|
const candidates = [
|
|
230
670
|
"C:/Program Files/Tencent/QQNT/QQ.exe",
|
|
231
671
|
"C:/Program Files (x86)/Tencent/QQNT/QQ.exe",
|
|
@@ -243,24 +683,116 @@ function locateQqPath() {
|
|
|
243
683
|
if (viaCommon) return viaCommon;
|
|
244
684
|
throw new Error("未找到 QQ.exe(请先安装 QQ,或通过 NAPUTO_QQ_PATH 指定)");
|
|
245
685
|
}
|
|
246
|
-
/**
|
|
247
|
-
function
|
|
248
|
-
const
|
|
249
|
-
const installDir = qq.slice(0, qq.lastIndexOf("\\"));
|
|
250
|
-
const versionsDir = join(installDir, "versions");
|
|
686
|
+
/** 从文件根定位最新版本 wrapper.node(local/cached 共用)。 */
|
|
687
|
+
function resolveFromRoot(rootDir, source) {
|
|
688
|
+
const versionsDir = join(rootDir, "versions");
|
|
251
689
|
if (!existsSync(versionsDir)) throw new Error(`QQ 版本目录不存在: ${versionsDir}`);
|
|
252
690
|
const [first] = readdirSync(versionsDir).filter((v) => statSync(join(versionsDir, v)).isDirectory()).sort().reverse();
|
|
253
691
|
if (first === void 0) throw new Error(`QQ 版本目录为空: ${versionsDir}`);
|
|
254
692
|
const wrapperPath = join(versionsDir, first, "resources", "app", "wrapper.node");
|
|
255
693
|
if (!existsSync(wrapperPath)) throw new Error(`未找到 wrapper.node: ${wrapperPath}`);
|
|
256
694
|
return {
|
|
257
|
-
qqPath:
|
|
258
|
-
installDir,
|
|
695
|
+
qqPath: join(rootDir, "QQ.exe"),
|
|
696
|
+
installDir: rootDir,
|
|
259
697
|
version: first,
|
|
260
|
-
wrapperPath
|
|
698
|
+
wrapperPath,
|
|
699
|
+
source
|
|
261
700
|
};
|
|
262
701
|
}
|
|
702
|
+
/** 从 QQ.exe 路径推导安装目录,并探测当前版本(L1:本机安装)。 */
|
|
703
|
+
function resolveQqInstall(qqPath) {
|
|
704
|
+
const qq = qqPath ?? process$1.env["NAPUTO_QQ_PATH"] ?? locateQqPath();
|
|
705
|
+
return resolveFromRoot(qq.slice(0, qq.lastIndexOf("\\")), "local");
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* 多级来源定位 QQ 原生文件:
|
|
709
|
+
* L0 → L1 → L2 依次尝试,全部失败且 autoDownload 时自动进入下载流程(ensureQqFiles)。
|
|
710
|
+
* ⚠️ async:自动下载为异步流程(P1 起);纯定位场景用 resolveQqInstall(同步)。
|
|
711
|
+
*/
|
|
712
|
+
async function resolveQqFiles(options = {}) {
|
|
713
|
+
const explicitDir = options.qqFilesDir ?? process$1.env["NAPUTO_QQ_FILES"];
|
|
714
|
+
if (explicitDir !== void 0 && explicitDir !== "") return resolveFromRoot(resolve(explicitDir), "cached");
|
|
715
|
+
const qqPath = options.qqPath ?? process$1.env["NAPUTO_QQ_PATH"];
|
|
716
|
+
if (qqPath !== void 0 && qqPath !== "") return resolveQqInstall(qqPath);
|
|
717
|
+
try {
|
|
718
|
+
return resolveQqInstall();
|
|
719
|
+
} catch {}
|
|
720
|
+
const dataRoot = resolveDataRootLight(options.dataRoot);
|
|
721
|
+
const cached = tryResolveCached(dataRoot);
|
|
722
|
+
if (cached !== null) return cached;
|
|
723
|
+
if (options.autoDownload !== false) return ensureQqFiles({
|
|
724
|
+
dataRoot,
|
|
725
|
+
...options.sevenZipPath !== void 0 ? { sevenZipPath: options.sevenZipPath } : {}
|
|
726
|
+
});
|
|
727
|
+
throw new Error("未找到 QQ 原生文件(wrapper.node):请安装 QQ、设置 NAPUTO_QQ_PATH/NAPUTO_QQ_FILES,或使用下载流程(ensureQqFiles)自动获取");
|
|
728
|
+
}
|
|
729
|
+
/** 扫描数据根缓存(L2);命中返回,未命中返回 null。 */
|
|
730
|
+
function tryResolveCached(dataRoot) {
|
|
731
|
+
const cacheRoot = join(dataRoot, QQ_FILES_DIR_NAME);
|
|
732
|
+
if (!existsSync(cacheRoot)) return null;
|
|
733
|
+
const versionDirs = readdirSync(cacheRoot).filter((v) => statSync(join(cacheRoot, v)).isDirectory()).sort().reverse();
|
|
734
|
+
for (const versionDir of versionDirs) try {
|
|
735
|
+
return resolveFromRoot(join(cacheRoot, versionDir), "cached");
|
|
736
|
+
} catch {}
|
|
737
|
+
return null;
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* 确保 QQ 原生文件就绪(P1):幂等缓存检查 → 下载官方安装包 → sha256 校验
|
|
741
|
+
* → 7z 解包 → 提取 wrapper.node/QQNT.dll → 缓存。返回缓存版本 QqInstallInfo。
|
|
742
|
+
*/
|
|
743
|
+
async function ensureQqFiles(options = {}) {
|
|
744
|
+
const dataRoot = resolveDataRootLight(options.dataRoot);
|
|
745
|
+
const cacheRoot = join(dataRoot, QQ_FILES_DIR_NAME);
|
|
746
|
+
mkdirSync(cacheRoot, { recursive: true });
|
|
747
|
+
const existing = tryResolveCached(dataRoot);
|
|
748
|
+
if (existing !== null) return existing;
|
|
749
|
+
const release = latestRelease(loadQqReleases());
|
|
750
|
+
const url = resolveDownloadUrl(release);
|
|
751
|
+
const version = release.version;
|
|
752
|
+
const tmpDir = join(dataRoot, "tmp");
|
|
753
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
754
|
+
const installerPath = join(tmpDir, `qq-installer-${version}.exe`);
|
|
755
|
+
await downloadFile({
|
|
756
|
+
dest: installerPath,
|
|
757
|
+
url,
|
|
758
|
+
expectedSha256: release.sha256
|
|
759
|
+
});
|
|
760
|
+
const extractedDir = join(tmpDir, `qq-extracted-${version}`);
|
|
761
|
+
try {
|
|
762
|
+
await extractInstaller(installerPath, extractedDir, options.sevenZipPath);
|
|
763
|
+
return await extractWrapperFiles(extractedDir, version, cacheRoot);
|
|
764
|
+
} catch (err) {
|
|
765
|
+
await clearCacheVersion(cacheRoot, version);
|
|
766
|
+
throw err;
|
|
767
|
+
} finally {
|
|
768
|
+
await rm(installerPath, { force: true });
|
|
769
|
+
await rm(extractedDir, {
|
|
770
|
+
recursive: true,
|
|
771
|
+
force: true
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* 轻量数据根解析(loader 侧,避免新增 kernel 依赖;P2 若需完整语义再切换)。
|
|
777
|
+
* 优先级:显式参数 > NAPKETTO_DATA > 项目根/.napuketto > cwd 兜底。
|
|
778
|
+
*/
|
|
779
|
+
function resolveDataRootLight(dataRoot) {
|
|
780
|
+
const explicit = dataRoot ?? process$1.env["NAPKETTO_DATA"];
|
|
781
|
+
if (explicit !== void 0 && explicit !== "") return resolve(explicit);
|
|
782
|
+
const projectRoot = findProjectRoot(process$1.cwd()) ?? process$1.cwd();
|
|
783
|
+
return join(projectRoot, ".napuketto");
|
|
784
|
+
}
|
|
785
|
+
/** 向上探测项目根(含 pnpm-workspace.yaml 或 package.json 的目录)。 */
|
|
786
|
+
function findProjectRoot(start) {
|
|
787
|
+
let dir = resolve(start);
|
|
788
|
+
for (;;) {
|
|
789
|
+
if (existsSync(join(dir, "pnpm-workspace.yaml")) || existsSync(join(dir, "package.json"))) return dir;
|
|
790
|
+
const parent = dirname(dir);
|
|
791
|
+
if (parent === dir) return null;
|
|
792
|
+
dir = parent;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
263
795
|
//#endregion
|
|
264
|
-
export { ENV, INSTANCE_LOCK_FILE, acquireInstanceLock, checkInstanceLock, defaultStubDir, launchSelfHost, locateQqPath, registerLockCleanup, releaseInstanceLock, resolveQqInstall };
|
|
796
|
+
export { DownloadError, ENV, INSTANCE_LOCK_FILE, QQ_FILES_DIR_NAME, QqReleasesError, WIN_NODE_DIR_NAME, acquireInstanceLock, buildSpawnCommand, checkInstanceLock, clearCacheVersion, defaultStubDir, downloadFile, ensureQqFiles, ensureWinNode, extractInstaller, extractWrapperFiles, findSevenZip, isLinux, latestRelease, launchSelfHost, loadQqReleases, locateQqPath, nodeZipUrl, registerLockCleanup, releaseInstanceLock, resolveDownloadUrl, resolveQqFiles, resolveQqInstall, toWinePath, wineBinary };
|
|
265
797
|
|
|
266
798
|
//# sourceMappingURL=index.mjs.map
|