@napuketto/loader 0.0.30 → 0.0.32
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/host/self-host.cjs +126 -4
- package/dist/index.cjs +97 -1
- package/dist/index.d.cts +16 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +16 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +97 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/host/self-host.cjs
CHANGED
|
@@ -24,9 +24,89 @@ let node_fs = require("node:fs");
|
|
|
24
24
|
let node_path = require("node:path");
|
|
25
25
|
let node_process = require("node:process");
|
|
26
26
|
node_process = __toESM(node_process, 1);
|
|
27
|
+
let node_child_process = require("node:child_process");
|
|
27
28
|
let node_os = require("node:os");
|
|
28
29
|
let zod = require("zod");
|
|
29
30
|
let node_readline = require("node:readline");
|
|
31
|
+
//#region src/pid-cmdline.ts
|
|
32
|
+
/**
|
|
33
|
+
* pid-cmdline.ts:跨进程命令行查询(instance-lock pid 复用根治,2026-09-10)。
|
|
34
|
+
*
|
|
35
|
+
* 背景(2026-09-08 实测事故):锁内 pid 的持有者已死、pid 被 Windows 复用给
|
|
36
|
+
* 无关进程时,process.kill(pid, 0) 探活成功 → 误判存活 → 新实例永久拒绝启动
|
|
37
|
+
* (只能手动删锁恢复)。根治:探活通过后再查该 pid 当前的真实命令行,与锁内
|
|
38
|
+
* cmdline 摘要比对——查不到(进程已退/查询失败)或不一致(pid 复用)均视为
|
|
39
|
+
* 持有者已死,允许接管。误判方向取舍:误判「死亡」→ 双实例抢 QQ 原生锁,
|
|
40
|
+
* 后启动者挂起退出(可恢复);误判「存活」→ 永久锁死需人工干预——前者代价
|
|
41
|
+
* 远低,故查询失败宁可接管。
|
|
42
|
+
*
|
|
43
|
+
* 平台实现:Windows 走 PowerShell Get-CimInstance Win32_Process 跨进程查询
|
|
44
|
+
* (仅在启动判活时调用一次,冷启动 ~0.5-2s 可接受;wmic 在新 Win11 已移除,
|
|
45
|
+
* 不可用)。Linux 走 /proc/<pid>/cmdline(\0 分隔)。其余平台(darwin 等)
|
|
46
|
+
* 不支持——判活退回纯 pid 探测(旧行为)。
|
|
47
|
+
*
|
|
48
|
+
* 比对不要求逐字符相等:锁内摘要来自 process.execArgv/argv join,查询侧
|
|
49
|
+
* 拿到的是操作系统记录的原始命令行,两者存在引号 / 大小写 / 路径分隔符差异
|
|
50
|
+
* ——统一经 normalizeCmdline 规范化后比对。残余差异源(8.3 短路径、相对
|
|
51
|
+
* 启动路径)极罕见,误判后果同上(可恢复),接受。
|
|
52
|
+
*/
|
|
53
|
+
/** 当前平台是否支持跨进程 cmdline 查询(不支持则判活退回纯 pid 探测)。 */
|
|
54
|
+
const CMDLINE_PROBE_SUPPORTED = node_process.default.platform === "win32" || node_process.default.platform === "linux";
|
|
55
|
+
/**
|
|
56
|
+
* 查询指定 pid 的当前命令行。
|
|
57
|
+
*
|
|
58
|
+
* @returns 命令行原文;进程不存在 / 命令行不可得(权限、系统进程 null)/
|
|
59
|
+
* 查询机制失败(超时、PowerShell 缺失)均返回 null(调用方视为持有者已死)
|
|
60
|
+
*/
|
|
61
|
+
function readCmdlineForPid(pid) {
|
|
62
|
+
if (node_process.default.platform === "win32") return readWindowsCmdline(pid);
|
|
63
|
+
if (node_process.default.platform === "linux") return readLinuxCmdline(pid);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
/** Windows:PowerShell Get-CimInstance 查询(进程不存在时输出空 → null)。 */
|
|
67
|
+
function readWindowsCmdline(pid) {
|
|
68
|
+
try {
|
|
69
|
+
const cmdline = (0, node_child_process.execFileSync)("powershell.exe", [
|
|
70
|
+
"-NoProfile",
|
|
71
|
+
"-NonInteractive",
|
|
72
|
+
"-ExecutionPolicy",
|
|
73
|
+
"Bypass",
|
|
74
|
+
"-Command",
|
|
75
|
+
`[Console]::OutputEncoding=[System.Text.Encoding]::UTF8;(Get-CimInstance Win32_Process -Filter 'ProcessId=${pid}').CommandLine`
|
|
76
|
+
], {
|
|
77
|
+
encoding: "utf-8",
|
|
78
|
+
timeout: 1e4,
|
|
79
|
+
windowsHide: true
|
|
80
|
+
}).trim();
|
|
81
|
+
return cmdline === "" ? null : cmdline;
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Linux:/proc/<pid>/cmdline(\0 分隔转空格;进程不存在抛错 → null)。 */
|
|
87
|
+
function readLinuxCmdline(pid) {
|
|
88
|
+
try {
|
|
89
|
+
const cmdline = (0, node_fs.readFileSync)(`/proc/${pid}/cmdline`, "utf-8").replaceAll("\0", " ").trim();
|
|
90
|
+
return cmdline === "" ? null : cmdline;
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* 命令行规范化(比对用):统一大小写 / 去引号 / 路径分隔符归一 / 折叠空白。
|
|
97
|
+
* Windows 路径大小写不敏感,lowercase 安全。
|
|
98
|
+
*/
|
|
99
|
+
function normalizeCmdline(cmdline) {
|
|
100
|
+
return cmdline.toLowerCase().replaceAll("\"", "").replaceAll("\\", "/").replace(/\s+/g, " ").trim();
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* 本进程命令行摘要(写锁用):execArgv(node 选项)+ argv(脚本与参数),
|
|
104
|
+
* 与操作系统记录的完整命令行结构一致(规范化后可比对)。
|
|
105
|
+
*/
|
|
106
|
+
function selfCmdline() {
|
|
107
|
+
return [...node_process.default.execArgv, ...node_process.default.argv].join(" ").trim();
|
|
108
|
+
}
|
|
109
|
+
//#endregion
|
|
30
110
|
//#region src/instance-lock.ts
|
|
31
111
|
/**
|
|
32
112
|
* instance-lock.ts:单实例锁(2026-08-07 根治「多实例抢数据目录锁挂起」)
|
|
@@ -42,6 +122,12 @@ let node_readline = require("node:readline");
|
|
|
42
122
|
* - 锁存在但 PID 已死 → 崩溃残留,自动清理(rm + 重新获取)
|
|
43
123
|
* - 锁存在且 PID 存活 → 已被占用,拒绝启动(返回占用 PID 供提示)
|
|
44
124
|
*
|
|
125
|
+
* PID 复用根治(2026-09-10):PID 探活通过后再比对锁内 cmdline 摘要与该
|
|
126
|
+
* PID 当前真实命令行(pid-cmdline.ts)——查不到或不一致 → PID 已被无关
|
|
127
|
+
* 进程复用(持有者已死),视为残留允许接管。旧锁(无 cmdline 字段)无法
|
|
128
|
+
* 校验,保守维持「占用」判定(一次性过渡:新锁写入时自动带摘要)。详见
|
|
129
|
+
* pid-cmdline.ts 头注释的误判代价取舍。
|
|
130
|
+
*
|
|
45
131
|
* 退出清理:进程正常退出时 releaseInstanceLock 删除锁文件;崩溃/强杀残留
|
|
46
132
|
* 由下次启动的「PID 存活检测」兜底(PID 已死 → 残留视为过期)。
|
|
47
133
|
*
|
|
@@ -85,6 +171,15 @@ function checkInstanceLock(dataDir) {
|
|
|
85
171
|
pid: null,
|
|
86
172
|
lockPath
|
|
87
173
|
};
|
|
174
|
+
const lockCmdline = typeof info.cmdline === "string" && info.cmdline !== "" ? info.cmdline : void 0;
|
|
175
|
+
if (lockCmdline !== void 0 && CMDLINE_PROBE_SUPPORTED) {
|
|
176
|
+
const current = readCmdlineForPid(info.pid);
|
|
177
|
+
if (current === null || normalizeCmdline(current) !== normalizeCmdline(lockCmdline)) return {
|
|
178
|
+
occupied: false,
|
|
179
|
+
pid: null,
|
|
180
|
+
lockPath
|
|
181
|
+
};
|
|
182
|
+
}
|
|
88
183
|
return {
|
|
89
184
|
occupied: true,
|
|
90
185
|
pid: info.pid,
|
|
@@ -101,10 +196,11 @@ function acquireInstanceLock(dataDir, cmdline) {
|
|
|
101
196
|
if (pid === node_process.default.pid) return true;
|
|
102
197
|
return false;
|
|
103
198
|
}
|
|
199
|
+
const effectiveCmdline = cmdline !== void 0 && cmdline !== "" ? cmdline : selfCmdline();
|
|
104
200
|
const info = {
|
|
105
201
|
pid: node_process.default.pid,
|
|
106
202
|
startedAt: Date.now(),
|
|
107
|
-
...
|
|
203
|
+
...effectiveCmdline !== "" ? { cmdline: effectiveCmdline } : {}
|
|
108
204
|
};
|
|
109
205
|
try {
|
|
110
206
|
(0, node_fs.rmSync)(lockPath, { force: true });
|
|
@@ -1140,6 +1236,7 @@ function loadProtocolSections(kernel, uin) {
|
|
|
1140
1236
|
const cfgFile = env.NAPKETTO_CONFIG || (0, node_path.join)(env.NAPUTO_CFG_DIR || ".", "napuketto.toml");
|
|
1141
1237
|
let ob11Section = {};
|
|
1142
1238
|
let satoriSection = {};
|
|
1239
|
+
let kurobotSection = {};
|
|
1143
1240
|
try {
|
|
1144
1241
|
const raw = (0, node_fs.readFileSync)(cfgFile, "utf8");
|
|
1145
1242
|
const accounts = kernel.parseToml(raw)["accounts"];
|
|
@@ -1148,6 +1245,7 @@ function loadProtocolSections(kernel, uin) {
|
|
|
1148
1245
|
if (mine !== void 0) {
|
|
1149
1246
|
ob11Section = sectionOf(mine, "onebot11") ?? {};
|
|
1150
1247
|
satoriSection = sectionOf(mine, "satori") ?? {};
|
|
1248
|
+
kurobotSection = sectionOf(mine, "kurobot") ?? {};
|
|
1151
1249
|
}
|
|
1152
1250
|
}
|
|
1153
1251
|
} catch (e) {
|
|
@@ -1156,7 +1254,8 @@ function loadProtocolSections(kernel, uin) {
|
|
|
1156
1254
|
return {
|
|
1157
1255
|
cfgFile,
|
|
1158
1256
|
ob11Section,
|
|
1159
|
-
satoriSection
|
|
1257
|
+
satoriSection,
|
|
1258
|
+
kurobotSection
|
|
1160
1259
|
};
|
|
1161
1260
|
}
|
|
1162
1261
|
//#endregion
|
|
@@ -1191,13 +1290,15 @@ async function assembleOb11AndSatori(kernel, services, loginResult) {
|
|
|
1191
1290
|
const network = await import(`file://${networkEntry.replace(BACKSLASH_RE, "/")}`);
|
|
1192
1291
|
const onebot11Entry = adapterEntry.replace(INDEX_MJS_RE, "onebot11/index.mjs");
|
|
1193
1292
|
const satoriEntry = adapterEntry.replace(INDEX_MJS_RE, "satori/index.mjs");
|
|
1293
|
+
const kurobotEntry = adapterEntry.replace(INDEX_MJS_RE, "kurobot/index.mjs");
|
|
1194
1294
|
const coreEntry = adapterEntry.replace(INDEX_MJS_RE, "core/index.mjs");
|
|
1195
1295
|
const adapter = await import(`file://${onebot11Entry.replace(BACKSLASH_RE, "/")}`);
|
|
1196
1296
|
const satoriAdapter = await import(`file://${satoriEntry.replace(BACKSLASH_RE, "/")}`);
|
|
1297
|
+
const kurobotAdapter = await import(`file://${kurobotEntry.replace(BACKSLASH_RE, "/")}`);
|
|
1197
1298
|
const adapterCore = await import(`file://${coreEntry.replace(BACKSLASH_RE, "/")}`);
|
|
1198
1299
|
const { channel, groupCache } = services;
|
|
1199
1300
|
const broadcaster = new network.EventBroadcaster();
|
|
1200
|
-
const { cfgFile, ob11Section, satoriSection } = loadProtocolSections(kernel, loginResult.uin);
|
|
1301
|
+
const { cfgFile, ob11Section, satoriSection, kurobotSection } = loadProtocolSections(kernel, loginResult.uin);
|
|
1201
1302
|
const ob11Config = new adapterCore.ProtocolConfig({
|
|
1202
1303
|
path: cfgFile,
|
|
1203
1304
|
schema: adapter.ob11ConfigSchema,
|
|
@@ -1270,9 +1371,28 @@ async function assembleOb11AndSatori(kernel, services, loginResult) {
|
|
|
1270
1371
|
});
|
|
1271
1372
|
await satori.start();
|
|
1272
1373
|
log("bootstrap: satori adapter started");
|
|
1374
|
+
let kurobot = null;
|
|
1375
|
+
if (Object.keys(kurobotSection).length > 0) {
|
|
1376
|
+
const kurobotConfig = new adapterCore.ProtocolConfig({
|
|
1377
|
+
path: cfgFile,
|
|
1378
|
+
schema: kurobotAdapter.kurobotConfigSchema,
|
|
1379
|
+
defaults: kurobotAdapter.kurobotConfigDefaults(),
|
|
1380
|
+
seed: kurobotAdapter.kurobotConfigSchema.parse(kurobotSection)
|
|
1381
|
+
});
|
|
1382
|
+
kurobot = new kurobotAdapter.NapukettoKurobotAdapter({
|
|
1383
|
+
config: kurobotConfig,
|
|
1384
|
+
msgChannel: channel,
|
|
1385
|
+
msgApi: services.msgApi,
|
|
1386
|
+
self: services.self,
|
|
1387
|
+
logger: services.logger
|
|
1388
|
+
});
|
|
1389
|
+
await kurobot.start();
|
|
1390
|
+
log("bootstrap: kurobot adapter started");
|
|
1391
|
+
}
|
|
1273
1392
|
return async () => {
|
|
1274
1393
|
await ob11.stop();
|
|
1275
1394
|
await satori.stop();
|
|
1395
|
+
if (kurobot !== null) await kurobot.stop();
|
|
1276
1396
|
};
|
|
1277
1397
|
} catch (e) {
|
|
1278
1398
|
log(`bootstrap: 协议装配失败: ${errMsg(e)}`);
|
|
@@ -1351,9 +1471,10 @@ function setupMsgLogging(kernel, channel, logger, colorize) {
|
|
|
1351
1471
|
/** 登录成功后创建 kernel 业务服务(channel/bridge/cache/apis)。失败返回 null。 */
|
|
1352
1472
|
async function createKernelServices(kernel, ctx, loginResult) {
|
|
1353
1473
|
const ipcMode = env.NAPUTO_IPC === "1";
|
|
1474
|
+
const logFile = env.NAPUTO_CFG_DIR !== void 0 ? (0, node_path.join)(env.NAPUTO_CFG_DIR, "logs", "loader.log") : (0, node_path.join)((0, node_os.tmpdir)(), "napuketto-loader.log");
|
|
1354
1475
|
const logger = kernel.createLogger?.({
|
|
1355
1476
|
console: !ipcMode,
|
|
1356
|
-
|
|
1477
|
+
file: logFile,
|
|
1357
1478
|
base: { name: "loader" }
|
|
1358
1479
|
});
|
|
1359
1480
|
const session = ctx.session;
|
|
@@ -1584,6 +1705,7 @@ function createLoginControlHandler(core, Appid, control, deps) {
|
|
|
1584
1705
|
const opts = {
|
|
1585
1706
|
appid: String(Appid),
|
|
1586
1707
|
initTimeoutMs: 2e4,
|
|
1708
|
+
quickLoginTimeoutMs: 2e4,
|
|
1587
1709
|
qrFallback: true,
|
|
1588
1710
|
...payload.uin !== void 0 ? { quickUin: payload.uin } : {},
|
|
1589
1711
|
...payload.qr === true ? { qrOnly: true } : {},
|
package/dist/index.cjs
CHANGED
|
@@ -227,6 +227,85 @@ function isNativeNoiseLine(line) {
|
|
|
227
227
|
return NATIVE_NOISE.test(line);
|
|
228
228
|
}
|
|
229
229
|
//#endregion
|
|
230
|
+
//#region src/pid-cmdline.ts
|
|
231
|
+
/**
|
|
232
|
+
* pid-cmdline.ts:跨进程命令行查询(instance-lock pid 复用根治,2026-09-10)。
|
|
233
|
+
*
|
|
234
|
+
* 背景(2026-09-08 实测事故):锁内 pid 的持有者已死、pid 被 Windows 复用给
|
|
235
|
+
* 无关进程时,process.kill(pid, 0) 探活成功 → 误判存活 → 新实例永久拒绝启动
|
|
236
|
+
* (只能手动删锁恢复)。根治:探活通过后再查该 pid 当前的真实命令行,与锁内
|
|
237
|
+
* cmdline 摘要比对——查不到(进程已退/查询失败)或不一致(pid 复用)均视为
|
|
238
|
+
* 持有者已死,允许接管。误判方向取舍:误判「死亡」→ 双实例抢 QQ 原生锁,
|
|
239
|
+
* 后启动者挂起退出(可恢复);误判「存活」→ 永久锁死需人工干预——前者代价
|
|
240
|
+
* 远低,故查询失败宁可接管。
|
|
241
|
+
*
|
|
242
|
+
* 平台实现:Windows 走 PowerShell Get-CimInstance Win32_Process 跨进程查询
|
|
243
|
+
* (仅在启动判活时调用一次,冷启动 ~0.5-2s 可接受;wmic 在新 Win11 已移除,
|
|
244
|
+
* 不可用)。Linux 走 /proc/<pid>/cmdline(\0 分隔)。其余平台(darwin 等)
|
|
245
|
+
* 不支持——判活退回纯 pid 探测(旧行为)。
|
|
246
|
+
*
|
|
247
|
+
* 比对不要求逐字符相等:锁内摘要来自 process.execArgv/argv join,查询侧
|
|
248
|
+
* 拿到的是操作系统记录的原始命令行,两者存在引号 / 大小写 / 路径分隔符差异
|
|
249
|
+
* ——统一经 normalizeCmdline 规范化后比对。残余差异源(8.3 短路径、相对
|
|
250
|
+
* 启动路径)极罕见,误判后果同上(可恢复),接受。
|
|
251
|
+
*/
|
|
252
|
+
/** 当前平台是否支持跨进程 cmdline 查询(不支持则判活退回纯 pid 探测)。 */
|
|
253
|
+
const CMDLINE_PROBE_SUPPORTED = node_process.default.platform === "win32" || node_process.default.platform === "linux";
|
|
254
|
+
/**
|
|
255
|
+
* 查询指定 pid 的当前命令行。
|
|
256
|
+
*
|
|
257
|
+
* @returns 命令行原文;进程不存在 / 命令行不可得(权限、系统进程 null)/
|
|
258
|
+
* 查询机制失败(超时、PowerShell 缺失)均返回 null(调用方视为持有者已死)
|
|
259
|
+
*/
|
|
260
|
+
function readCmdlineForPid(pid) {
|
|
261
|
+
if (node_process.default.platform === "win32") return readWindowsCmdline(pid);
|
|
262
|
+
if (node_process.default.platform === "linux") return readLinuxCmdline(pid);
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
/** Windows:PowerShell Get-CimInstance 查询(进程不存在时输出空 → null)。 */
|
|
266
|
+
function readWindowsCmdline(pid) {
|
|
267
|
+
try {
|
|
268
|
+
const cmdline = (0, node_child_process.execFileSync)("powershell.exe", [
|
|
269
|
+
"-NoProfile",
|
|
270
|
+
"-NonInteractive",
|
|
271
|
+
"-ExecutionPolicy",
|
|
272
|
+
"Bypass",
|
|
273
|
+
"-Command",
|
|
274
|
+
`[Console]::OutputEncoding=[System.Text.Encoding]::UTF8;(Get-CimInstance Win32_Process -Filter 'ProcessId=${pid}').CommandLine`
|
|
275
|
+
], {
|
|
276
|
+
encoding: "utf-8",
|
|
277
|
+
timeout: 1e4,
|
|
278
|
+
windowsHide: true
|
|
279
|
+
}).trim();
|
|
280
|
+
return cmdline === "" ? null : cmdline;
|
|
281
|
+
} catch {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/** Linux:/proc/<pid>/cmdline(\0 分隔转空格;进程不存在抛错 → null)。 */
|
|
286
|
+
function readLinuxCmdline(pid) {
|
|
287
|
+
try {
|
|
288
|
+
const cmdline = (0, node_fs.readFileSync)(`/proc/${pid}/cmdline`, "utf-8").replaceAll("\0", " ").trim();
|
|
289
|
+
return cmdline === "" ? null : cmdline;
|
|
290
|
+
} catch {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* 命令行规范化(比对用):统一大小写 / 去引号 / 路径分隔符归一 / 折叠空白。
|
|
296
|
+
* Windows 路径大小写不敏感,lowercase 安全。
|
|
297
|
+
*/
|
|
298
|
+
function normalizeCmdline(cmdline) {
|
|
299
|
+
return cmdline.toLowerCase().replaceAll("\"", "").replaceAll("\\", "/").replace(/\s+/g, " ").trim();
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* 本进程命令行摘要(写锁用):execArgv(node 选项)+ argv(脚本与参数),
|
|
303
|
+
* 与操作系统记录的完整命令行结构一致(规范化后可比对)。
|
|
304
|
+
*/
|
|
305
|
+
function selfCmdline() {
|
|
306
|
+
return [...node_process.default.execArgv, ...node_process.default.argv].join(" ").trim();
|
|
307
|
+
}
|
|
308
|
+
//#endregion
|
|
230
309
|
//#region src/instance-lock.ts
|
|
231
310
|
/**
|
|
232
311
|
* instance-lock.ts:单实例锁(2026-08-07 根治「多实例抢数据目录锁挂起」)
|
|
@@ -242,6 +321,12 @@ function isNativeNoiseLine(line) {
|
|
|
242
321
|
* - 锁存在但 PID 已死 → 崩溃残留,自动清理(rm + 重新获取)
|
|
243
322
|
* - 锁存在且 PID 存活 → 已被占用,拒绝启动(返回占用 PID 供提示)
|
|
244
323
|
*
|
|
324
|
+
* PID 复用根治(2026-09-10):PID 探活通过后再比对锁内 cmdline 摘要与该
|
|
325
|
+
* PID 当前真实命令行(pid-cmdline.ts)——查不到或不一致 → PID 已被无关
|
|
326
|
+
* 进程复用(持有者已死),视为残留允许接管。旧锁(无 cmdline 字段)无法
|
|
327
|
+
* 校验,保守维持「占用」判定(一次性过渡:新锁写入时自动带摘要)。详见
|
|
328
|
+
* pid-cmdline.ts 头注释的误判代价取舍。
|
|
329
|
+
*
|
|
245
330
|
* 退出清理:进程正常退出时 releaseInstanceLock 删除锁文件;崩溃/强杀残留
|
|
246
331
|
* 由下次启动的「PID 存活检测」兜底(PID 已死 → 残留视为过期)。
|
|
247
332
|
*
|
|
@@ -285,6 +370,15 @@ function checkInstanceLock(dataDir) {
|
|
|
285
370
|
pid: null,
|
|
286
371
|
lockPath
|
|
287
372
|
};
|
|
373
|
+
const lockCmdline = typeof info.cmdline === "string" && info.cmdline !== "" ? info.cmdline : void 0;
|
|
374
|
+
if (lockCmdline !== void 0 && CMDLINE_PROBE_SUPPORTED) {
|
|
375
|
+
const current = readCmdlineForPid(info.pid);
|
|
376
|
+
if (current === null || normalizeCmdline(current) !== normalizeCmdline(lockCmdline)) return {
|
|
377
|
+
occupied: false,
|
|
378
|
+
pid: null,
|
|
379
|
+
lockPath
|
|
380
|
+
};
|
|
381
|
+
}
|
|
288
382
|
return {
|
|
289
383
|
occupied: true,
|
|
290
384
|
pid: info.pid,
|
|
@@ -301,10 +395,11 @@ function acquireInstanceLock(dataDir, cmdline) {
|
|
|
301
395
|
if (pid === node_process.default.pid) return true;
|
|
302
396
|
return false;
|
|
303
397
|
}
|
|
398
|
+
const effectiveCmdline = cmdline !== void 0 && cmdline !== "" ? cmdline : selfCmdline();
|
|
304
399
|
const info = {
|
|
305
400
|
pid: node_process.default.pid,
|
|
306
401
|
startedAt: Date.now(),
|
|
307
|
-
...
|
|
402
|
+
...effectiveCmdline !== "" ? { cmdline: effectiveCmdline } : {}
|
|
308
403
|
};
|
|
309
404
|
try {
|
|
310
405
|
(0, node_fs.rmSync)(lockPath, { force: true });
|
|
@@ -1297,6 +1392,7 @@ exports.linuxSevenZipUrl = linuxSevenZipUrl;
|
|
|
1297
1392
|
exports.loadQqReleases = loadQqReleases;
|
|
1298
1393
|
exports.locateQqPath = locateQqPath;
|
|
1299
1394
|
exports.nodeZipUrl = nodeZipUrl;
|
|
1395
|
+
exports.readCmdlineForPid = readCmdlineForPid;
|
|
1300
1396
|
exports.registerLockCleanup = registerLockCleanup;
|
|
1301
1397
|
exports.releaseInstanceLock = releaseInstanceLock;
|
|
1302
1398
|
exports.resolveDownloadUrl = resolveDownloadUrl;
|
package/dist/index.d.cts
CHANGED
|
@@ -370,6 +370,12 @@ declare function isNativeNoiseLine(line: string): boolean;
|
|
|
370
370
|
* - 锁存在但 PID 已死 → 崩溃残留,自动清理(rm + 重新获取)
|
|
371
371
|
* - 锁存在且 PID 存活 → 已被占用,拒绝启动(返回占用 PID 供提示)
|
|
372
372
|
*
|
|
373
|
+
* PID 复用根治(2026-09-10):PID 探活通过后再比对锁内 cmdline 摘要与该
|
|
374
|
+
* PID 当前真实命令行(pid-cmdline.ts)——查不到或不一致 → PID 已被无关
|
|
375
|
+
* 进程复用(持有者已死),视为残留允许接管。旧锁(无 cmdline 字段)无法
|
|
376
|
+
* 校验,保守维持「占用」判定(一次性过渡:新锁写入时自动带摘要)。详见
|
|
377
|
+
* pid-cmdline.ts 头注释的误判代价取舍。
|
|
378
|
+
*
|
|
373
379
|
* 退出清理:进程正常退出时 releaseInstanceLock 删除锁文件;崩溃/强杀残留
|
|
374
380
|
* 由下次启动的「PID 存活检测」兜底(PID 已死 → 残留视为过期)。
|
|
375
381
|
*
|
|
@@ -620,6 +626,15 @@ declare const ENV: {
|
|
|
620
626
|
readonly IPC: "NAPUTO_IPC";
|
|
621
627
|
};
|
|
622
628
|
//#endregion
|
|
629
|
+
//#region src/pid-cmdline.d.ts
|
|
630
|
+
/**
|
|
631
|
+
* 查询指定 pid 的当前命令行。
|
|
632
|
+
*
|
|
633
|
+
* @returns 命令行原文;进程不存在 / 命令行不可得(权限、系统进程 null)/
|
|
634
|
+
* 查询机制失败(超时、PowerShell 缺失)均返回 null(调用方视为持有者已死)
|
|
635
|
+
*/
|
|
636
|
+
declare function readCmdlineForPid(pid: number): string | null;
|
|
637
|
+
//#endregion
|
|
623
638
|
//#region src/qq-download.d.ts
|
|
624
639
|
/** 下载选项。 */
|
|
625
640
|
interface DownloadOptions {
|
|
@@ -757,5 +772,5 @@ declare function buildSpawnCommand(options: {
|
|
|
757
772
|
selfHostPath: string;
|
|
758
773
|
}): SpawnCommand;
|
|
759
774
|
//#endregion
|
|
760
|
-
export { DownloadError, type DownloadOptions, type DownloadResult, ENV, type ExtractOptions, INSTANCE_LOCK_FILE, IPC_VERSION, type InstanceLockCheck, type InstanceLockInfo, type IpcActionMessage, type IpcActionPayload, IpcActionPayloadSchema, type IpcBootPhase, IpcBootPhaseSchema, type IpcControlMessage, type IpcControlPayload, IpcControlPayloadSchema, type IpcEventMessage, type IpcEventPayload, IpcEventPayloadSchema, type IpcLogLevel, IpcLogLevelSchema, type IpcLogMessage, type IpcLogPayload, IpcLogPayloadSchema, type IpcLoginMessage, type IpcLoginPayload, IpcLoginPayloadSchema, type IpcMessage, IpcMessageSchema, type IpcPingMessage, type IpcPongMessage, type IpcQrMessage, type IpcResultMessage, type IpcResultPayload, IpcResultPayloadSchema, type IpcStatusMessage, type IpcStatusPayload, IpcStatusPayloadSchema, 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, ensureLinuxSevenZip, ensureQqFiles, ensureWinNode, extractInstaller, extractWrapperFiles, findSevenZip, isLinux, isNativeNoiseLine, latestRelease, launchSelfHost, linuxSevenZipUrl, loadQqReleases, locateQqPath, nodeZipUrl, registerLockCleanup, releaseInstanceLock, resolveDownloadUrl, resolveQqFiles, resolveQqInstall, toWinePath, wineBinary };
|
|
775
|
+
export { DownloadError, type DownloadOptions, type DownloadResult, ENV, type ExtractOptions, INSTANCE_LOCK_FILE, IPC_VERSION, type InstanceLockCheck, type InstanceLockInfo, type IpcActionMessage, type IpcActionPayload, IpcActionPayloadSchema, type IpcBootPhase, IpcBootPhaseSchema, type IpcControlMessage, type IpcControlPayload, IpcControlPayloadSchema, type IpcEventMessage, type IpcEventPayload, IpcEventPayloadSchema, type IpcLogLevel, IpcLogLevelSchema, type IpcLogMessage, type IpcLogPayload, IpcLogPayloadSchema, type IpcLoginMessage, type IpcLoginPayload, IpcLoginPayloadSchema, type IpcMessage, IpcMessageSchema, type IpcPingMessage, type IpcPongMessage, type IpcQrMessage, type IpcResultMessage, type IpcResultPayload, IpcResultPayloadSchema, type IpcStatusMessage, type IpcStatusPayload, IpcStatusPayloadSchema, 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, ensureLinuxSevenZip, ensureQqFiles, ensureWinNode, extractInstaller, extractWrapperFiles, findSevenZip, isLinux, isNativeNoiseLine, latestRelease, launchSelfHost, linuxSevenZipUrl, loadQqReleases, locateQqPath, nodeZipUrl, readCmdlineForPid, registerLockCleanup, releaseInstanceLock, resolveDownloadUrl, resolveQqFiles, resolveQqInstall, toWinePath, wineBinary };
|
|
761
776
|
//# sourceMappingURL=index.d.cts.map
|
package/dist/index.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/host/ipc/ipc-types.ts","../src/host/ipc/native-noise.ts","../src/instance-lock.ts","../src/locate-qq.ts","../src/win-node.ts","../src/launcher.ts","../src/qq-download.ts","../src/qq-extract.ts","../src/qq-releases.ts","../src/wine.ts"],"mappings":";;;;cAaa;;cAGA,oBAAkB,EAAA;;;;;;;;;cAUlB,mBAAiB,EAAA;;;;;;;;;cAajB,wBAAsB,EAAA;;;;;;;;;;;;;;GAIjC,EAAA,KAAA;;cAGW,uBAAqB,EAAA;;;;;;;;;;;;;;GAKhC,EAAA,KAAA;;cAGW,uBAAqB,EAAA;;;;GAIhC,EAAA,KAAA;;cAGW,qBAAmB,EAAA;;;;;;;;;;GAG9B,EAAA,KAAA;;cAGW,wBAAsB,EAAA,mBAAA,EAAA;;;;;;;;;GAGjC,EAAA,KAAA;;cAGW,wBAAsB,EAAA;;;GAGjC,EAAA,KAAA;;cAGW,yBAAuB,EAAA,mBAAA,EAAA;;;;;;;;;;GASlC,EAAA,KAAA;cAII,wBAAsB,EAAA;;;;;;;;;;;;;;;;;;GAI1B,EAAA,KAAA;cAEI,uBAAqB,EAAA;;;;;;;;;;;;;;;;;;GAIzB,EAAA,KAAA;cAEI,oBAAkB,EAAA;;;;;;;GAItB,EAAA,KAAA;cAEI,uBAAqB,EAAA;;;;;;;;GAIzB,EAAA,KAAA;cAEI,wBAAsB,EAAA;;;;;;;;;;;;;;GAK1B,EAAA,KAAA;cAEI,qBAAmB,EAAA;;;;;;;;;;;;;;GAIvB,EAAA,KAAA;cAEI,sBAAoB,EAAA;;;GAGxB,EAAA,KAAA;cAEI,sBAAoB,EAAA;;;GAGxB,EAAA,KAAA;cAEI,wBAAsB,EAAA;;;;;;;;GAK1B,EAAA,KAAA;cAEI,yBAAuB,EAAA;;;;;;;;;;;;;;GAI3B,EAAA,KAAA;;cAGW,kBAAgB,EAAA,uBAAA,EAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAW3B,EAAA,KAAA;KAIU,eAAe,EAAE,aAAa;KAC9B,cAAc,EAAE,aAAa;KAC7B,mBAAmB,EAAE,aAAa;KAClC,kBAAkB,EAAE,aAAa;KACjC,kBAAkB,EAAE,aAAa;KACjC,gBAAgB,EAAE,aAAa;KAC/B,mBAAmB,EAAE,aAAa;KAClC,mBAAmB,EAAE,aAAa;KAClC,oBAAoB,EAAE,aAAa;KACnC,mBAAmB,EAAE,aAAa;KAClC,kBAAkB,EAAE,aAAa;KACjC,eAAe,EAAE,aAAa;KAC9B,kBAAkB,EAAE,aAAa;KACjC,mBAAmB,EAAE,aAAa;KAClC,gBAAgB,EAAE,aAAa;KAC/B,iBAAiB,EAAE,aAAa;KAChC,iBAAiB,EAAE,aAAa;KAChC,mBAAmB,EAAE,aAAa;KAClC,oBAAoB,EAAE,aAAa;KACnC,aAAa,EAAE,aAAa;;;;;;;;;iBC9JxB,kBAAkB
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/host/ipc/ipc-types.ts","../src/host/ipc/native-noise.ts","../src/instance-lock.ts","../src/locate-qq.ts","../src/win-node.ts","../src/launcher.ts","../src/pid-cmdline.ts","../src/qq-download.ts","../src/qq-extract.ts","../src/qq-releases.ts","../src/wine.ts"],"mappings":"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"}
|
package/dist/index.d.mts
CHANGED
|
@@ -370,6 +370,12 @@ declare function isNativeNoiseLine(line: string): boolean;
|
|
|
370
370
|
* - 锁存在但 PID 已死 → 崩溃残留,自动清理(rm + 重新获取)
|
|
371
371
|
* - 锁存在且 PID 存活 → 已被占用,拒绝启动(返回占用 PID 供提示)
|
|
372
372
|
*
|
|
373
|
+
* PID 复用根治(2026-09-10):PID 探活通过后再比对锁内 cmdline 摘要与该
|
|
374
|
+
* PID 当前真实命令行(pid-cmdline.ts)——查不到或不一致 → PID 已被无关
|
|
375
|
+
* 进程复用(持有者已死),视为残留允许接管。旧锁(无 cmdline 字段)无法
|
|
376
|
+
* 校验,保守维持「占用」判定(一次性过渡:新锁写入时自动带摘要)。详见
|
|
377
|
+
* pid-cmdline.ts 头注释的误判代价取舍。
|
|
378
|
+
*
|
|
373
379
|
* 退出清理:进程正常退出时 releaseInstanceLock 删除锁文件;崩溃/强杀残留
|
|
374
380
|
* 由下次启动的「PID 存活检测」兜底(PID 已死 → 残留视为过期)。
|
|
375
381
|
*
|
|
@@ -620,6 +626,15 @@ declare const ENV: {
|
|
|
620
626
|
readonly IPC: "NAPUTO_IPC";
|
|
621
627
|
};
|
|
622
628
|
//#endregion
|
|
629
|
+
//#region src/pid-cmdline.d.ts
|
|
630
|
+
/**
|
|
631
|
+
* 查询指定 pid 的当前命令行。
|
|
632
|
+
*
|
|
633
|
+
* @returns 命令行原文;进程不存在 / 命令行不可得(权限、系统进程 null)/
|
|
634
|
+
* 查询机制失败(超时、PowerShell 缺失)均返回 null(调用方视为持有者已死)
|
|
635
|
+
*/
|
|
636
|
+
declare function readCmdlineForPid(pid: number): string | null;
|
|
637
|
+
//#endregion
|
|
623
638
|
//#region src/qq-download.d.ts
|
|
624
639
|
/** 下载选项。 */
|
|
625
640
|
interface DownloadOptions {
|
|
@@ -757,5 +772,5 @@ declare function buildSpawnCommand(options: {
|
|
|
757
772
|
selfHostPath: string;
|
|
758
773
|
}): SpawnCommand;
|
|
759
774
|
//#endregion
|
|
760
|
-
export { DownloadError, type DownloadOptions, type DownloadResult, ENV, type ExtractOptions, INSTANCE_LOCK_FILE, IPC_VERSION, type InstanceLockCheck, type InstanceLockInfo, type IpcActionMessage, type IpcActionPayload, IpcActionPayloadSchema, type IpcBootPhase, IpcBootPhaseSchema, type IpcControlMessage, type IpcControlPayload, IpcControlPayloadSchema, type IpcEventMessage, type IpcEventPayload, IpcEventPayloadSchema, type IpcLogLevel, IpcLogLevelSchema, type IpcLogMessage, type IpcLogPayload, IpcLogPayloadSchema, type IpcLoginMessage, type IpcLoginPayload, IpcLoginPayloadSchema, type IpcMessage, IpcMessageSchema, type IpcPingMessage, type IpcPongMessage, type IpcQrMessage, type IpcResultMessage, type IpcResultPayload, IpcResultPayloadSchema, type IpcStatusMessage, type IpcStatusPayload, IpcStatusPayloadSchema, 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, ensureLinuxSevenZip, ensureQqFiles, ensureWinNode, extractInstaller, extractWrapperFiles, findSevenZip, isLinux, isNativeNoiseLine, latestRelease, launchSelfHost, linuxSevenZipUrl, loadQqReleases, locateQqPath, nodeZipUrl, registerLockCleanup, releaseInstanceLock, resolveDownloadUrl, resolveQqFiles, resolveQqInstall, toWinePath, wineBinary };
|
|
775
|
+
export { DownloadError, type DownloadOptions, type DownloadResult, ENV, type ExtractOptions, INSTANCE_LOCK_FILE, IPC_VERSION, type InstanceLockCheck, type InstanceLockInfo, type IpcActionMessage, type IpcActionPayload, IpcActionPayloadSchema, type IpcBootPhase, IpcBootPhaseSchema, type IpcControlMessage, type IpcControlPayload, IpcControlPayloadSchema, type IpcEventMessage, type IpcEventPayload, IpcEventPayloadSchema, type IpcLogLevel, IpcLogLevelSchema, type IpcLogMessage, type IpcLogPayload, IpcLogPayloadSchema, type IpcLoginMessage, type IpcLoginPayload, IpcLoginPayloadSchema, type IpcMessage, IpcMessageSchema, type IpcPingMessage, type IpcPongMessage, type IpcQrMessage, type IpcResultMessage, type IpcResultPayload, IpcResultPayloadSchema, type IpcStatusMessage, type IpcStatusPayload, IpcStatusPayloadSchema, 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, ensureLinuxSevenZip, ensureQqFiles, ensureWinNode, extractInstaller, extractWrapperFiles, findSevenZip, isLinux, isNativeNoiseLine, latestRelease, launchSelfHost, linuxSevenZipUrl, loadQqReleases, locateQqPath, nodeZipUrl, readCmdlineForPid, registerLockCleanup, releaseInstanceLock, resolveDownloadUrl, resolveQqFiles, resolveQqInstall, toWinePath, wineBinary };
|
|
761
776
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/host/ipc/ipc-types.ts","../src/host/ipc/native-noise.ts","../src/instance-lock.ts","../src/locate-qq.ts","../src/win-node.ts","../src/launcher.ts","../src/qq-download.ts","../src/qq-extract.ts","../src/qq-releases.ts","../src/wine.ts"],"mappings":";;;;cAaa;;cAGA,oBAAkB,EAAA;;;;;;;;;cAUlB,mBAAiB,EAAA;;;;;;;;;cAajB,wBAAsB,EAAA;;;;;;;;;;;;;;GAIjC,EAAA,KAAA;;cAGW,uBAAqB,EAAA;;;;;;;;;;;;;;GAKhC,EAAA,KAAA;;cAGW,uBAAqB,EAAA;;;;GAIhC,EAAA,KAAA;;cAGW,qBAAmB,EAAA;;;;;;;;;;GAG9B,EAAA,KAAA;;cAGW,wBAAsB,EAAA,mBAAA,EAAA;;;;;;;;;GAGjC,EAAA,KAAA;;cAGW,wBAAsB,EAAA;;;GAGjC,EAAA,KAAA;;cAGW,yBAAuB,EAAA,mBAAA,EAAA;;;;;;;;;;GASlC,EAAA,KAAA;cAII,wBAAsB,EAAA;;;;;;;;;;;;;;;;;;GAI1B,EAAA,KAAA;cAEI,uBAAqB,EAAA;;;;;;;;;;;;;;;;;;GAIzB,EAAA,KAAA;cAEI,oBAAkB,EAAA;;;;;;;GAItB,EAAA,KAAA;cAEI,uBAAqB,EAAA;;;;;;;;GAIzB,EAAA,KAAA;cAEI,wBAAsB,EAAA;;;;;;;;;;;;;;GAK1B,EAAA,KAAA;cAEI,qBAAmB,EAAA;;;;;;;;;;;;;;GAIvB,EAAA,KAAA;cAEI,sBAAoB,EAAA;;;GAGxB,EAAA,KAAA;cAEI,sBAAoB,EAAA;;;GAGxB,EAAA,KAAA;cAEI,wBAAsB,EAAA;;;;;;;;GAK1B,EAAA,KAAA;cAEI,yBAAuB,EAAA;;;;;;;;;;;;;;GAI3B,EAAA,KAAA;;cAGW,kBAAgB,EAAA,uBAAA,EAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAW3B,EAAA,KAAA;KAIU,eAAe,EAAE,aAAa;KAC9B,cAAc,EAAE,aAAa;KAC7B,mBAAmB,EAAE,aAAa;KAClC,kBAAkB,EAAE,aAAa;KACjC,kBAAkB,EAAE,aAAa;KACjC,gBAAgB,EAAE,aAAa;KAC/B,mBAAmB,EAAE,aAAa;KAClC,mBAAmB,EAAE,aAAa;KAClC,oBAAoB,EAAE,aAAa;KACnC,mBAAmB,EAAE,aAAa;KAClC,kBAAkB,EAAE,aAAa;KACjC,eAAe,EAAE,aAAa;KAC9B,kBAAkB,EAAE,aAAa;KACjC,mBAAmB,EAAE,aAAa;KAClC,gBAAgB,EAAE,aAAa;KAC/B,iBAAiB,EAAE,aAAa;KAChC,iBAAiB,EAAE,aAAa;KAChC,mBAAmB,EAAE,aAAa;KAClC,oBAAoB,EAAE,aAAa;KACnC,aAAa,EAAE,aAAa;;;;;;;;;iBC9JxB,kBAAkB
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/host/ipc/ipc-types.ts","../src/host/ipc/native-noise.ts","../src/instance-lock.ts","../src/locate-qq.ts","../src/win-node.ts","../src/launcher.ts","../src/pid-cmdline.ts","../src/qq-download.ts","../src/qq-extract.ts","../src/qq-releases.ts","../src/wine.ts"],"mappings":"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"}
|
package/dist/index.mjs
CHANGED
|
@@ -203,6 +203,85 @@ function isNativeNoiseLine(line) {
|
|
|
203
203
|
return NATIVE_NOISE.test(line);
|
|
204
204
|
}
|
|
205
205
|
//#endregion
|
|
206
|
+
//#region src/pid-cmdline.ts
|
|
207
|
+
/**
|
|
208
|
+
* pid-cmdline.ts:跨进程命令行查询(instance-lock pid 复用根治,2026-09-10)。
|
|
209
|
+
*
|
|
210
|
+
* 背景(2026-09-08 实测事故):锁内 pid 的持有者已死、pid 被 Windows 复用给
|
|
211
|
+
* 无关进程时,process.kill(pid, 0) 探活成功 → 误判存活 → 新实例永久拒绝启动
|
|
212
|
+
* (只能手动删锁恢复)。根治:探活通过后再查该 pid 当前的真实命令行,与锁内
|
|
213
|
+
* cmdline 摘要比对——查不到(进程已退/查询失败)或不一致(pid 复用)均视为
|
|
214
|
+
* 持有者已死,允许接管。误判方向取舍:误判「死亡」→ 双实例抢 QQ 原生锁,
|
|
215
|
+
* 后启动者挂起退出(可恢复);误判「存活」→ 永久锁死需人工干预——前者代价
|
|
216
|
+
* 远低,故查询失败宁可接管。
|
|
217
|
+
*
|
|
218
|
+
* 平台实现:Windows 走 PowerShell Get-CimInstance Win32_Process 跨进程查询
|
|
219
|
+
* (仅在启动判活时调用一次,冷启动 ~0.5-2s 可接受;wmic 在新 Win11 已移除,
|
|
220
|
+
* 不可用)。Linux 走 /proc/<pid>/cmdline(\0 分隔)。其余平台(darwin 等)
|
|
221
|
+
* 不支持——判活退回纯 pid 探测(旧行为)。
|
|
222
|
+
*
|
|
223
|
+
* 比对不要求逐字符相等:锁内摘要来自 process.execArgv/argv join,查询侧
|
|
224
|
+
* 拿到的是操作系统记录的原始命令行,两者存在引号 / 大小写 / 路径分隔符差异
|
|
225
|
+
* ——统一经 normalizeCmdline 规范化后比对。残余差异源(8.3 短路径、相对
|
|
226
|
+
* 启动路径)极罕见,误判后果同上(可恢复),接受。
|
|
227
|
+
*/
|
|
228
|
+
/** 当前平台是否支持跨进程 cmdline 查询(不支持则判活退回纯 pid 探测)。 */
|
|
229
|
+
const CMDLINE_PROBE_SUPPORTED = process$1.platform === "win32" || process$1.platform === "linux";
|
|
230
|
+
/**
|
|
231
|
+
* 查询指定 pid 的当前命令行。
|
|
232
|
+
*
|
|
233
|
+
* @returns 命令行原文;进程不存在 / 命令行不可得(权限、系统进程 null)/
|
|
234
|
+
* 查询机制失败(超时、PowerShell 缺失)均返回 null(调用方视为持有者已死)
|
|
235
|
+
*/
|
|
236
|
+
function readCmdlineForPid(pid) {
|
|
237
|
+
if (process$1.platform === "win32") return readWindowsCmdline(pid);
|
|
238
|
+
if (process$1.platform === "linux") return readLinuxCmdline(pid);
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
/** Windows:PowerShell Get-CimInstance 查询(进程不存在时输出空 → null)。 */
|
|
242
|
+
function readWindowsCmdline(pid) {
|
|
243
|
+
try {
|
|
244
|
+
const cmdline = execFileSync("powershell.exe", [
|
|
245
|
+
"-NoProfile",
|
|
246
|
+
"-NonInteractive",
|
|
247
|
+
"-ExecutionPolicy",
|
|
248
|
+
"Bypass",
|
|
249
|
+
"-Command",
|
|
250
|
+
`[Console]::OutputEncoding=[System.Text.Encoding]::UTF8;(Get-CimInstance Win32_Process -Filter 'ProcessId=${pid}').CommandLine`
|
|
251
|
+
], {
|
|
252
|
+
encoding: "utf-8",
|
|
253
|
+
timeout: 1e4,
|
|
254
|
+
windowsHide: true
|
|
255
|
+
}).trim();
|
|
256
|
+
return cmdline === "" ? null : cmdline;
|
|
257
|
+
} catch {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/** Linux:/proc/<pid>/cmdline(\0 分隔转空格;进程不存在抛错 → null)。 */
|
|
262
|
+
function readLinuxCmdline(pid) {
|
|
263
|
+
try {
|
|
264
|
+
const cmdline = readFileSync(`/proc/${pid}/cmdline`, "utf-8").replaceAll("\0", " ").trim();
|
|
265
|
+
return cmdline === "" ? null : cmdline;
|
|
266
|
+
} catch {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* 命令行规范化(比对用):统一大小写 / 去引号 / 路径分隔符归一 / 折叠空白。
|
|
272
|
+
* Windows 路径大小写不敏感,lowercase 安全。
|
|
273
|
+
*/
|
|
274
|
+
function normalizeCmdline(cmdline) {
|
|
275
|
+
return cmdline.toLowerCase().replaceAll("\"", "").replaceAll("\\", "/").replace(/\s+/g, " ").trim();
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* 本进程命令行摘要(写锁用):execArgv(node 选项)+ argv(脚本与参数),
|
|
279
|
+
* 与操作系统记录的完整命令行结构一致(规范化后可比对)。
|
|
280
|
+
*/
|
|
281
|
+
function selfCmdline() {
|
|
282
|
+
return [...process$1.execArgv, ...process$1.argv].join(" ").trim();
|
|
283
|
+
}
|
|
284
|
+
//#endregion
|
|
206
285
|
//#region src/instance-lock.ts
|
|
207
286
|
/**
|
|
208
287
|
* instance-lock.ts:单实例锁(2026-08-07 根治「多实例抢数据目录锁挂起」)
|
|
@@ -218,6 +297,12 @@ function isNativeNoiseLine(line) {
|
|
|
218
297
|
* - 锁存在但 PID 已死 → 崩溃残留,自动清理(rm + 重新获取)
|
|
219
298
|
* - 锁存在且 PID 存活 → 已被占用,拒绝启动(返回占用 PID 供提示)
|
|
220
299
|
*
|
|
300
|
+
* PID 复用根治(2026-09-10):PID 探活通过后再比对锁内 cmdline 摘要与该
|
|
301
|
+
* PID 当前真实命令行(pid-cmdline.ts)——查不到或不一致 → PID 已被无关
|
|
302
|
+
* 进程复用(持有者已死),视为残留允许接管。旧锁(无 cmdline 字段)无法
|
|
303
|
+
* 校验,保守维持「占用」判定(一次性过渡:新锁写入时自动带摘要)。详见
|
|
304
|
+
* pid-cmdline.ts 头注释的误判代价取舍。
|
|
305
|
+
*
|
|
221
306
|
* 退出清理:进程正常退出时 releaseInstanceLock 删除锁文件;崩溃/强杀残留
|
|
222
307
|
* 由下次启动的「PID 存活检测」兜底(PID 已死 → 残留视为过期)。
|
|
223
308
|
*
|
|
@@ -261,6 +346,15 @@ function checkInstanceLock(dataDir) {
|
|
|
261
346
|
pid: null,
|
|
262
347
|
lockPath
|
|
263
348
|
};
|
|
349
|
+
const lockCmdline = typeof info.cmdline === "string" && info.cmdline !== "" ? info.cmdline : void 0;
|
|
350
|
+
if (lockCmdline !== void 0 && CMDLINE_PROBE_SUPPORTED) {
|
|
351
|
+
const current = readCmdlineForPid(info.pid);
|
|
352
|
+
if (current === null || normalizeCmdline(current) !== normalizeCmdline(lockCmdline)) return {
|
|
353
|
+
occupied: false,
|
|
354
|
+
pid: null,
|
|
355
|
+
lockPath
|
|
356
|
+
};
|
|
357
|
+
}
|
|
264
358
|
return {
|
|
265
359
|
occupied: true,
|
|
266
360
|
pid: info.pid,
|
|
@@ -277,10 +371,11 @@ function acquireInstanceLock(dataDir, cmdline) {
|
|
|
277
371
|
if (pid === process$1.pid) return true;
|
|
278
372
|
return false;
|
|
279
373
|
}
|
|
374
|
+
const effectiveCmdline = cmdline !== void 0 && cmdline !== "" ? cmdline : selfCmdline();
|
|
280
375
|
const info = {
|
|
281
376
|
pid: process$1.pid,
|
|
282
377
|
startedAt: Date.now(),
|
|
283
|
-
...
|
|
378
|
+
...effectiveCmdline !== "" ? { cmdline: effectiveCmdline } : {}
|
|
284
379
|
};
|
|
285
380
|
try {
|
|
286
381
|
rmSync(lockPath, { force: true });
|
|
@@ -1236,6 +1331,6 @@ async function ensureQqFiles(options = {}) {
|
|
|
1236
1331
|
}
|
|
1237
1332
|
}
|
|
1238
1333
|
//#endregion
|
|
1239
|
-
export { DownloadError, ENV, INSTANCE_LOCK_FILE, IPC_VERSION, IpcActionPayloadSchema, IpcBootPhaseSchema, IpcControlPayloadSchema, IpcEventPayloadSchema, IpcLogLevelSchema, IpcLogPayloadSchema, IpcLoginPayloadSchema, IpcMessageSchema, IpcResultPayloadSchema, IpcStatusPayloadSchema, QQ_FILES_DIR_NAME, QqReleasesError, WIN_NODE_DIR_NAME, acquireInstanceLock, buildSpawnCommand, checkInstanceLock, clearCacheVersion, defaultStubDir, downloadFile, ensureLinuxSevenZip, ensureQqFiles, ensureWinNode, extractInstaller, extractWrapperFiles, findSevenZip, isLinux, isNativeNoiseLine, latestRelease, launchSelfHost, linuxSevenZipUrl, loadQqReleases, locateQqPath, nodeZipUrl, registerLockCleanup, releaseInstanceLock, resolveDownloadUrl, resolveQqFiles, resolveQqInstall, toWinePath, wineBinary };
|
|
1334
|
+
export { DownloadError, ENV, INSTANCE_LOCK_FILE, IPC_VERSION, IpcActionPayloadSchema, IpcBootPhaseSchema, IpcControlPayloadSchema, IpcEventPayloadSchema, IpcLogLevelSchema, IpcLogPayloadSchema, IpcLoginPayloadSchema, IpcMessageSchema, IpcResultPayloadSchema, IpcStatusPayloadSchema, QQ_FILES_DIR_NAME, QqReleasesError, WIN_NODE_DIR_NAME, acquireInstanceLock, buildSpawnCommand, checkInstanceLock, clearCacheVersion, defaultStubDir, downloadFile, ensureLinuxSevenZip, ensureQqFiles, ensureWinNode, extractInstaller, extractWrapperFiles, findSevenZip, isLinux, isNativeNoiseLine, latestRelease, launchSelfHost, linuxSevenZipUrl, loadQqReleases, locateQqPath, nodeZipUrl, readCmdlineForPid, registerLockCleanup, releaseInstanceLock, resolveDownloadUrl, resolveQqFiles, resolveQqInstall, toWinePath, wineBinary };
|
|
1240
1335
|
|
|
1241
1336
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["process","process","packageRoot","process","process","process","process","process","process"],"sources":["../src/host/ipc/ipc-types.ts","../src/host/ipc/native-noise.ts","../src/instance-lock.ts","../src/data-root.ts","../src/package-info.ts","../src/qq-download.ts","../src/qq-extract.ts","../src/win-node.ts","../src/wine.ts","../src/launcher.ts","../src/qq-releases.ts","../src/locate-qq.ts"],"sourcesContent":["/**\n * ipc-types.ts:IPC 协议消息(koishi 插件 ↔ self-host 子进程,单一来源)。\n *\n * 协议契约(koishi 插件 design.md §5.6):`{ v, type, id?, payload }` JSON 行。\n * 用 zod v4 定义单一 schema,两侧(loader host + koishi 插件)import 同一份:\n * 类型经 z.infer 派生、运行时经 safeParse 校验,消除「两份手写 types.ts」漂移。\n *\n * 领域字段(LoginState / SelfInfo / QrCodeData)与 kernel 同名类型结构等价,\n * 在协议边界内联为 zod schema(kernel 不依赖 zod,故不在 kernel 定义)。\n */\nimport { z } from \"zod\";\n\n/** IPC 协议版本(解码校验)。 */\nexport const IPC_VERSION = 1;\n\n/** 引导阶段(status 消息)。 */\nexport const IpcBootPhaseSchema = z.enum([\n \"booting\", // 子进程已启动(spawn 成功)\n \"dlopening\", // dlopen wrapper.node\n \"logging\", // 登录流程进行中\n \"sessioning\", // session 初始化中\n \"ready\", // session READY + 业务 service 装配完成\n \"failed\", // 引导失败(携带错误)\n]);\n\n/** 结构化日志级别(pino level)。 */\nexport const IpcLogLevelSchema = z.enum([\"trace\", \"debug\", \"info\", \"warn\", \"error\", \"fatal\"]);\n\n/** 登录状态(与 kernel LoginState 结构等价)。 */\nconst LoginStateSchema = z.enum([\"idle\", \"waiting_scan\", \"scanned\", \"logged_in\", \"failed\"]);\n\n/** selfInfo(与 kernel SelfInfo 结构等价)。 */\nconst SelfInfoSchema = z.object({\n uin: z.string(),\n uid: z.string(),\n nick: z.string(),\n});\n\n/** status 消息 payload。 */\nexport const IpcStatusPayloadSchema = z.object({\n phase: IpcBootPhaseSchema,\n message: z.string().optional(),\n error: z.object({ code: z.string(), message: z.string() }).optional(),\n});\n\n/** login 消息 payload。 */\nexport const IpcLoginPayloadSchema = z.object({\n state: LoginStateSchema,\n selfInfo: SelfInfoSchema.optional(),\n /** 失败原因(state=failed 时;如「登录超时,请刷新页面重试」)。 */\n message: z.string().optional(),\n});\n\n/** event 消息 payload(kernel 事件通道形状,翻译层按 service/name 断言具体类型)。 */\nexport const IpcEventPayloadSchema = z.object({\n service: z.string(),\n name: z.string(),\n args: z.array(z.unknown()),\n});\n\n/** log 消息 payload。 */\nexport const IpcLogPayloadSchema = z.object({\n level: IpcLogLevelSchema,\n message: z.string(),\n});\n\n/** 动作响应 payload(result 消息)。 */\nexport const IpcResultPayloadSchema = z.union([\n z.object({ ok: z.literal(true), value: z.unknown().optional() }),\n z.object({ ok: z.literal(false), error: z.object({ code: z.string(), message: z.string() }) }),\n]);\n\n/** 动作请求 payload(action 消息)。 */\nexport const IpcActionPayloadSchema = z.object({\n action: z.string(),\n params: z.record(z.string(), z.unknown()).optional(),\n});\n\n/** 控制指令 payload(control 消息,父→子)。 */\nexport const IpcControlPayloadSchema = z.union([\n z.object({ command: z.literal(\"stop\") }), // 优雅退出\n z.object({ command: z.literal(\"restart\") }), // 退出并期望驱动层重启\n z.object({\n command: z.literal(\"login\"),\n uin: z.string().optional(),\n qr: z.boolean().optional(),\n }), // 触发登录(uin 指定账号 / qr=true 强制扫码;登录期成功结果抢占初始登录竞速,2026-09-08)\n z.object({ command: z.literal(\"status\") }), // 状态查询:重播最近一条 status(丢失重同步,2026-09-06)\n]);\n\n// ── 消息联合 ──\n\nconst IpcStatusMessageSchema = z.object({\n v: z.literal(IPC_VERSION),\n type: z.literal(\"status\"),\n payload: IpcStatusPayloadSchema,\n});\n\nconst IpcLoginMessageSchema = z.object({\n v: z.literal(IPC_VERSION),\n type: z.literal(\"login\"),\n payload: IpcLoginPayloadSchema,\n});\n\nconst IpcQrMessageSchema = z.object({\n v: z.literal(IPC_VERSION),\n type: z.literal(\"qr\"),\n payload: z.object({ pngBase64: z.string(), qrcodeUrl: z.string() }),\n});\n\nconst IpcEventMessageSchema = z.object({\n v: z.literal(IPC_VERSION),\n type: z.literal(\"event\"),\n payload: IpcEventPayloadSchema,\n});\n\nconst IpcResultMessageSchema = z.object({\n v: z.literal(IPC_VERSION),\n type: z.literal(\"result\"),\n id: z.number(),\n payload: IpcResultPayloadSchema,\n});\n\nconst IpcLogMessageSchema = z.object({\n v: z.literal(IPC_VERSION),\n type: z.literal(\"log\"),\n payload: IpcLogPayloadSchema,\n});\n\nconst IpcPingMessageSchema = z.object({\n v: z.literal(IPC_VERSION),\n type: z.literal(\"ping\"),\n});\n\nconst IpcPongMessageSchema = z.object({\n v: z.literal(IPC_VERSION),\n type: z.literal(\"pong\"),\n});\n\nconst IpcActionMessageSchema = z.object({\n v: z.literal(IPC_VERSION),\n type: z.literal(\"action\"),\n id: z.number(),\n payload: IpcActionPayloadSchema,\n});\n\nconst IpcControlMessageSchema = z.object({\n v: z.literal(IPC_VERSION),\n type: z.literal(\"control\"),\n payload: IpcControlPayloadSchema,\n});\n\n/** IPC 消息判别联合(协议边界,运行时经 safeParse 校验)。 */\nexport const IpcMessageSchema = z.discriminatedUnion(\"type\", [\n IpcStatusMessageSchema,\n IpcLoginMessageSchema,\n IpcQrMessageSchema,\n IpcEventMessageSchema,\n IpcResultMessageSchema,\n IpcLogMessageSchema,\n IpcPingMessageSchema,\n IpcPongMessageSchema,\n IpcActionMessageSchema,\n IpcControlMessageSchema,\n]);\n\n// ── 派生类型(z.infer,两侧消费同名类型,无镜像漂移) ──\n\nexport type IpcBootPhase = z.infer<typeof IpcBootPhaseSchema>;\nexport type IpcLogLevel = z.infer<typeof IpcLogLevelSchema>;\nexport type IpcStatusPayload = z.infer<typeof IpcStatusPayloadSchema>;\nexport type IpcLoginPayload = z.infer<typeof IpcLoginPayloadSchema>;\nexport type IpcEventPayload = z.infer<typeof IpcEventPayloadSchema>;\nexport type IpcLogPayload = z.infer<typeof IpcLogPayloadSchema>;\nexport type IpcResultPayload = z.infer<typeof IpcResultPayloadSchema>;\nexport type IpcActionPayload = z.infer<typeof IpcActionPayloadSchema>;\nexport type IpcControlPayload = z.infer<typeof IpcControlPayloadSchema>;\nexport type IpcStatusMessage = z.infer<typeof IpcStatusMessageSchema>;\nexport type IpcLoginMessage = z.infer<typeof IpcLoginMessageSchema>;\nexport type IpcQrMessage = z.infer<typeof IpcQrMessageSchema>;\nexport type IpcEventMessage = z.infer<typeof IpcEventMessageSchema>;\nexport type IpcResultMessage = z.infer<typeof IpcResultMessageSchema>;\nexport type IpcLogMessage = z.infer<typeof IpcLogMessageSchema>;\nexport type IpcPingMessage = z.infer<typeof IpcPingMessageSchema>;\nexport type IpcPongMessage = z.infer<typeof IpcPongMessageSchema>;\nexport type IpcActionMessage = z.infer<typeof IpcActionMessageSchema>;\nexport type IpcControlMessage = z.infer<typeof IpcControlMessageSchema>;\nexport type IpcMessage = z.infer<typeof IpcMessageSchema>;\n","/**\n * native-noise.ts:wrapper 原生噪音行判定(cli 转发与 koishi IPC 两条路径共享单一来源)。\n *\n * wrapper.node 加载后原生 printf 直写 fd 的 C++ 日志,JS 层无法拦截,只能在消费端\n * 逐行过滤:\n * - `<MMKV` / `<MemoryFile_Win32` / `<MMKV_IO`:MMKV 存储库刷屏(每次初始化打 ~6 行)\n * - `loadSymbolFromShell` / `getNodeGetJsListApi` / `get symbol failed`:\n * 标准 node 无腾讯私有符号(NodeContextifyContextMetrics 等),GetProcAddress\n * 失败的加载警告(无害,纯噪音)\n * - `loaded [mmkv.*] with N key-values`:MMKV 初始化完成行(野生日志,风格三,\n * 无统一前缀/时间戳,过滤)\n *\n * 边界:只处理「整条」噪音行;原生输出插进 JSON 协议行字节中间的撕裂不归这里管\n * (IPC 路径由 status 重播与脏行诊断兜底,见 ipc-sender.ts / koishi 插件 client)。\n */\nimport { IPC_VERSION } from \"./ipc-types.js\";\n\nconst NATIVE_NOISE =\n /<MMKV|<MemoryFile_Win32|<MMKV_IO|loadSymbolFromShell|getNodeGetJsListApi|get symbol failed|loaded \\[mmkv/i;\n\n/** IPC 协议行前缀(encodeIpcMessage 走 JSON.stringify 无空白,前缀恒定)。 */\nconst IPC_LINE_PREFIX = `{\"v\":${IPC_VERSION}`;\n\n/**\n * 判定一行子进程输出是否为 wrapper 原生噪音(命中即整行静默丢弃,不进协议解析)。\n *\n * IPC 协议行构造性豁免:载荷可能嵌入噪音样文本(如用户消息含 `<MMKV>` 字样),\n * 以协议前缀开头的行永不判噪——过滤与协议必须无交集,防误滤丢消息事件。\n */\nexport function isNativeNoiseLine(line: string): boolean {\n if (line.startsWith(IPC_LINE_PREFIX)) {\n return false;\n }\n return NATIVE_NOISE.test(line);\n}\n","/**\n * instance-lock.ts:单实例锁(2026-08-07 根治「多实例抢数据目录锁挂起」)\n *\n * 背景(实测):同一账号数据目录(~/.napuketto/<uin>)只能被一个实例持有——\n * QQ 原生层(MMKV / 登录单例 / 数据库)有锁,第二个实例抢不到锁会卡在\n * loginService.initConfig 之后无响应(挂起)。此前 create-napukettoqq 生成的\n * 副本实例常驻占锁,导致用户后续启动全部卡死。\n *\n * 方案:以数据目录为粒度加锁文件 `<dataDir>/instance.lock`(JSON 内容:\n * pid / startedAt / cmdline 摘要)。启动前 checkInstanceLock:\n * - 锁不存在 → 可启动(然后 acquireInstanceLock 写入)\n * - 锁存在但 PID 已死 → 崩溃残留,自动清理(rm + 重新获取)\n * - 锁存在且 PID 存活 → 已被占用,拒绝启动(返回占用 PID 供提示)\n *\n * 退出清理:进程正常退出时 releaseInstanceLock 删除锁文件;崩溃/强杀残留\n * 由下次启动的「PID 存活检测」兜底(PID 已死 → 残留视为过期)。\n *\n * 放置说明:放 loader 而非 kernel——self-host.cjs 是 rolldown 内联 bundle\n * (只内联 src/host/ 依赖树),静态 import kernel 会留 external 缺口;\n * 放 loader 则 self-host(内联)与 cli(@napuketto/loader 导入)双入口可用。\n */\n\nimport { readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport process from \"node:process\";\n\n/** 锁文件名(置于数据目录内)。 */\nexport const INSTANCE_LOCK_FILE = \"instance.lock\";\n\n/** 锁文件内容。 */\nexport interface InstanceLockInfo {\n /** 持有实例的进程 PID。 */\n pid: number;\n /** 获取锁的时间(epoch ms)。 */\n startedAt: number;\n /** 命令行摘要(诊断用,可选)。 */\n cmdline?: string;\n}\n\n/** 数据目录锁检查结果。 */\nexport interface InstanceLockCheck {\n /** 是否被其他实例占用。 */\n occupied: boolean;\n /** 占用进程 PID(occupied=true 时有效)。 */\n pid: number | null;\n /** 锁文件完整路径(提示/清理用)。 */\n lockPath: string;\n}\n\n/**\n * 检测指定数据目录是否已被其他实例占用。\n *\n * @param dataDir 数据目录(账号级,如 ~/.napuketto/3567141148)\n */\nexport function checkInstanceLock(dataDir: string): InstanceLockCheck {\n const lockPath = join(dataDir, INSTANCE_LOCK_FILE);\n let info: InstanceLockInfo | null = null;\n try {\n info = JSON.parse(readFileSync(lockPath, \"utf-8\")) as InstanceLockInfo;\n } catch {\n // 文件不存在 / 内容损坏 → 视为可启动(损坏锁由 acquire 覆盖)\n return { occupied: false, pid: null, lockPath };\n }\n if (typeof info.pid !== \"number\" || Number.isNaN(info.pid) || info.pid <= 0) {\n return { occupied: false, pid: null, lockPath };\n }\n // PID 存活判定(Windows 下 process.kill(pid, 0) 探测)\n let alive = false;\n try {\n process.kill(info.pid, 0);\n alive = true;\n } catch {\n alive = false;\n }\n if (!alive) {\n // 崩溃残留:持有者已死,可安全接管\n return { occupied: false, pid: null, lockPath };\n }\n return { occupied: true, pid: info.pid, lockPath };\n}\n\n/**\n * 获取数据目录锁。占用(其他实例存活)返回 false;成功或清理残留后返回 true。\n * 幂等:本进程已持有则直接返回 true(不重复写)。\n */\nexport function acquireInstanceLock(dataDir: string, cmdline?: string): boolean {\n const { occupied, pid, lockPath } = checkInstanceLock(dataDir);\n if (occupied) {\n // 已占用:除非占用者是自己(PID 复用保护),否则拒绝\n if (pid === process.pid) {\n return true;\n }\n return false;\n }\n // 残留锁(持有者已死 / 无锁):写入\n const info: InstanceLockInfo = {\n pid: process.pid,\n startedAt: Date.now(),\n ...(cmdline !== undefined && cmdline !== \"\" ? { cmdline } : {}),\n };\n try {\n rmSync(lockPath, { force: true });\n writeFileSync(lockPath, JSON.stringify(info), \"utf-8\");\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * 释放数据目录锁(正常退出时调用)。仅当持有者是本进程时才删除,\n * 避免误删新持有者的锁(PID 复用 / 快速重启场景)。\n */\nexport function releaseInstanceLock(dataDir: string): void {\n const lockPath = join(dataDir, INSTANCE_LOCK_FILE);\n try {\n const info = JSON.parse(readFileSync(lockPath, \"utf-8\")) as InstanceLockInfo;\n if (info.pid === process.pid) {\n rmSync(lockPath, { force: true });\n }\n } catch {\n // 文件不存在 / 内容损坏 → 无需清理\n }\n}\n\n/** 注册进程退出清理(SIGINT/SIGTERM/exit 均触发;崩溃强杀由残留检测兜底)。 */\nexport function registerLockCleanup(dataDir: string): void {\n process.on(\"exit\", () => {\n releaseInstanceLock(dataDir);\n });\n}\n","/**\n * data-root.ts:轻量数据根解析(loader 侧,避免新增 kernel 依赖)。\n *\n * 从 locate-qq.ts / win-node.ts 抽出的同构逻辑(2026-08 消重:两处\n * 22 行重复 resolveDataRoot + findProjectRoot)。\n *\n * 优先级:显式参数 > NAPKETTO_DATA > 项目根/.napuketto > cwd 兜底。\n * P2 若需完整语义(同 kernel resolveDataRoot)再切换。\n */\nimport { existsSync } from \"node:fs\";\nimport { dirname, join, resolve } from \"node:path\";\nimport process from \"node:process\";\n\n/** 解析数据根(NAPKETTO_DATA 显式 > 项目根/.napuketto > cwd 兜底)。 */\nexport function resolveDataRoot(dataRoot?: string): string {\n const explicit = dataRoot ?? process.env[\"NAPKETTO_DATA\"];\n if (explicit !== undefined && explicit !== \"\") {\n return resolve(explicit);\n }\n const projectRoot = findProjectRoot(process.cwd()) ?? process.cwd();\n return join(projectRoot, \".napuketto\");\n}\n\n/** 向上探测项目根(含 pnpm-workspace.yaml 或 package.json 的目录)。 */\nfunction findProjectRoot(start: string): string | null {\n let dir = resolve(start);\n for (;;) {\n if (existsSync(join(dir, \"pnpm-workspace.yaml\")) || existsSync(join(dir, \"package.json\"))) {\n return dir;\n }\n const parent = dirname(dir);\n if (parent === dir) {\n return null;\n }\n dir = parent;\n }\n}\n","/**\n * package-info.ts:运行时读取本包 package.json 元数据。\n *\n * ESM 下用 import.meta.url 定位包根(dist/ 与包根同层:dist/index.mjs → ../package.json);\n * CJS 输出由 rolldown 将 import.meta.url 转为 pathToFileURL(__filename) 等价形式,\n * 定位行为不变(设计文档 §发布形态)。\n */\nimport { readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\n/** 包根路径(dist 与包根同层:dist/index.mjs → ../package.json)。 */\nfunction packageRoot(): string {\n return dirname(dirname(fileURLToPath(import.meta.url)));\n}\n\n/**\n * 读取本包版本号;失败(发布产物缺 package.json / 解析异常)兜底 \"0.0.0\"。\n * 仅用于下载 User-Agent 等非关键元数据,失败不阻塞主流程。\n */\nexport function loaderVersion(): string {\n try {\n const raw = readFileSync(join(packageRoot(), \"package.json\"), \"utf8\");\n const parsed = JSON.parse(raw) as { version?: unknown };\n if (typeof parsed.version === \"string\" && parsed.version !== \"\") {\n return parsed.version;\n }\n } catch {\n // 读取/解析失败——仅影响 UA 与日志标识,兜底即可\n }\n return \"0.0.0\";\n}\n","/**\n * qq-download.ts:QQ 官方安装包下载器(P1)。\n *\n * 无第三方依赖:Node 内置 https + crypto(sha256)。设计文档 §2.2。\n * - https GET 流式写文件(低内存占用,313MB 安装包不整载入内存)\n * - 下载同时流式累加 sha256,完成后与清单比对(防链接漂移劫持 / 下载不完整)\n * - 支持 302 重定向(QQ CDN 可能跳转)\n * - NAPUTO_QQ_URL 环境变量可覆盖下载地址(用户拿到新链接时用)\n */\nimport { createHash } from \"node:crypto\";\nimport { createWriteStream, mkdirSync, statSync } from \"node:fs\";\nimport { rm } from \"node:fs/promises\";\nimport type { IncomingMessage } from \"node:http\";\nimport { get, type RequestOptions } from \"node:https\";\nimport { dirname } from \"node:path\";\nimport process from \"node:process\";\nimport { loaderVersion } from \"./package-info.js\";\n\n/** 下载请求 User-Agent(运行时读本包版本,失败兜底 0.0.0)。 */\nconst USER_AGENT = `napuketto-loader/${loaderVersion()}`;\n\n/** 下载选项。 */\nexport interface DownloadOptions {\n /** 目标文件绝对路径。 */\n dest: string;\n /** 期望 sha256(十六进制小写);缺省/空串 = 不校验(首次下载尚无参考值)。 */\n expectedSha256?: string;\n /** 覆盖下载地址(缺省读 NAPUTO_QQ_URL)。 */\n url?: string;\n /** 超时(毫秒,默认 60s 无数据则中断)。 */\n timeoutMs?: number;\n}\n\n/** 下载结果。 */\nexport interface DownloadResult {\n /** 实际 sha256(十六进制小写)。 */\n sha256: string;\n /** 是否通过期望值校验(无期望值时恒 true)。 */\n verified: boolean;\n}\n\n/** 下载中断(超时 / 非 2xx / 网络错误)抛出的错误。 */\nexport class DownloadError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"DownloadError\";\n }\n}\n\n/** 发起一次 https GET,返回响应(跟随重定向,最多 5 跳)。 */\nfunction httpsGet(url: string, timeoutMs: number): Promise<IncomingMessage> {\n return new Promise((resolve, reject) => {\n const request = (currentUrl: string, redirects: number): void => {\n const opts: RequestOptions = {\n headers: { \"user-agent\": USER_AGENT },\n };\n const req = get(currentUrl, opts, (res) => {\n const status = res.statusCode ?? 0;\n if (status >= 300 && status < 400 && res.headers[\"location\"] !== undefined) {\n res.resume(); // 释放连接\n if (redirects >= 5) {\n reject(new DownloadError(`重定向次数过多: ${currentUrl}`));\n return;\n }\n request(new URL(res.headers[\"location\"], currentUrl).toString(), redirects + 1);\n return;\n }\n if (status !== 200) {\n res.resume();\n reject(new DownloadError(`HTTP ${status}: ${currentUrl}`));\n return;\n }\n resolve(res);\n });\n req.setTimeout(timeoutMs, () => {\n req.destroy(new DownloadError(`下载超时(${timeoutMs}ms): ${currentUrl}`));\n });\n req.on(\"error\", (err) => {\n reject(err instanceof Error ? err : new DownloadError(String(err)));\n });\n };\n request(url, 0);\n });\n}\n\n/**\n * 下载文件并(可选)校验 sha256。失败时清理半成品文件。\n * 返回实际 sha256(无论是否校验),便于调用方回填清单。\n */\nexport async function downloadFile(options: DownloadOptions): Promise<DownloadResult> {\n const url = options.url ?? process.env[\"NAPUTO_QQ_URL\"];\n if (url === undefined || url === \"\") {\n throw new DownloadError(\"下载地址为空:请设置 NAPUTO_QQ_URL 或提供清单 URL\");\n }\n const timeoutMs = options.timeoutMs ?? 60_000;\n // 目标目录必须提前建好(createWriteStream 不自动建目录)\n mkdirSync(dirname(options.dest), { recursive: true });\n const hash = createHash(\"sha256\");\n const fileStream = createWriteStream(options.dest);\n let fileFailed = false;\n\n try {\n const res = await httpsGet(url, timeoutMs);\n await new Promise<void>((resolve, reject) => {\n res.on(\"data\", (chunk: Buffer) => {\n hash.update(chunk);\n });\n res.on(\"error\", (err: Error) => {\n fileFailed = true;\n reject(err instanceof Error ? err : new DownloadError(String(err)));\n });\n fileStream.on(\"error\", (err) => {\n fileFailed = true;\n reject(err);\n });\n fileStream.on(\"finish\", () => {\n resolve();\n });\n res.pipe(fileStream);\n });\n } catch (err) {\n fileStream.destroy();\n if (!fileFailed) {\n await rm(options.dest, { force: true });\n }\n throw err instanceof Error ? err : new DownloadError(String(err));\n }\n\n // ⚠️ 完成态校验(2026-08-23 WSL 生产事故):Promise 正常 resolve 但文件\n // 缺失/为空是真实发生过的(koishi 场景 7z 解包报「No such file or\n // directory」,疑为并发实例清理 tmp 或写入中断)。resolve 不代表落盘完整,\n // 这里 stat 兜底,缺失/空文件立即报错而不是把坏文件交给下游解包。\n let size: number;\n try {\n size = statSync(options.dest).size;\n } catch {\n throw new DownloadError(`下载完成但文件缺失: ${options.dest}`);\n }\n if (size <= 0) {\n await rm(options.dest, { force: true });\n throw new DownloadError(`下载完成但文件为空: ${options.dest}`);\n }\n\n const sha256 = hash.digest(\"hex\");\n const expected = options.expectedSha256;\n const verified =\n expected === undefined || expected === \"\"\n ? true\n : sha256.toLowerCase() === expected.toLowerCase();\n if (!verified) {\n await rm(options.dest, { force: true });\n throw new DownloadError(\n `sha256 校验失败: ${options.dest}\\n期望 ${expected}\\n实际 ${sha256}`,\n );\n }\n return { sha256, verified };\n}\n","/**\n * qq-extract.ts:QQ 官方安装包解包与提取(P1)。\n *\n * ⚠️ 格式事实(2026-08-12 实测修正):QQ 官方安装包是 NSIS 自解压格式,\n * **7za standalone 不支持 NSIS**(只支持 7z/zip/tar 等)——必须用完整版\n * 7z(7z.exe + 7z.dll)。设计文档 §2.3 原「内置 7za.exe」修正为:\n * - Windows:内置 assets/7zip/7z.exe + 7z.dll(7-Zip 官方,LGPL 合规可分发)\n * - Linux/Docker:系统 p7zip-full(7z 完整版)\n *\n * ⚠️ 提取策略(2026-08-12 实测修正,与设计 §2.3 不同):\n * 1. NSIS 解包产物在 <extracted>/Files/(NSIS 内部布局),Files/ = 安装根\n * 2. 真实 QQNT.dll(214MB)位于 versions/<v>/QQNT.dll,**自建宿主不需要**\n * ——stub QQNT.dll 已替代其宿主符号职责(wrapper.node 的 v8/node/napi/qq_magic\n * 符号由 stub 转发到 node.exe)\n * 3. 只提取 resources/app 顶层 *.node + *.dll(wrapper.node 的直接/传递/delay-load\n * 依赖全覆盖,版本无关;跳过 wmpfsdk/avsdk 等大子目录,共约 235MB)\n *\n * 提取目标(与安装目录同构,resolveFromRoot 可直接消费):\n * <数据根>/qq-files/<版本>/versions/<版本>/resources/app/ ← *.node + *.dll\n */\nimport { execFile } from \"node:child_process\";\nimport { existsSync } from \"node:fs\";\nimport { copyFile, mkdir, readdir, rm } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport process from \"node:process\";\nimport { fileURLToPath } from \"node:url\";\nimport type { QqInstallInfo } from \"./locate-qq.js\";\n\n/** resources/app 下需要提取的文件扩展名(.node 原生模块 + .dll 依赖)。 */\nconst EXTRACT_EXTENSIONS = new Set([\".node\", \".dll\"]);\n\n/** 7-Zip 可执行文件来源。 */\nexport interface SevenZipResult {\n /** 7z 可执行文件绝对路径。 */\n exe: string;\n /** 来源说明(内置资产 / 系统 / 环境变量)。 */\n source: string;\n}\n\n/** 提取选项。 */\nexport interface ExtractOptions {\n /** 安装包绝对路径。 */\n installerPath: string;\n /** 目标版本目录(如 9.9.33-51802)。 */\n version: string;\n /** 缓存根目录(<数据根>/qq-files;版本子目录将建在其下)。 */\n cacheRoot: string;\n /** 7z 可执行文件路径(缺省自动探测)。 */\n sevenZipPath?: string;\n}\n\n/**\n * 探测 7z 可执行文件:\n * NAPUTO_7Z_PATH 环境变量 > 内置资产(Windows assets/7zip/7z.exe、Linux\n * assets/7zip/7zz——2026-08-14 治本,静态二进制已打进发布包)> 系统 PATH 7z。\n */\nexport function findSevenZip(): SevenZipResult {\n const envPath = process.env[\"NAPUTO_7Z_PATH\"];\n if (envPath !== undefined && envPath !== \"\" && existsSync(envPath)) {\n return { exe: envPath, source: \"环境变量 NAPUTO_7Z_PATH\" };\n }\n // 内置资产:dist 与 assets 同层(tsdown 构建后 dist/,资产在包根 assets/)\n if (process.platform === \"win32\" || process.platform === \"linux\") {\n const exeName = process.platform === \"win32\" ? \"7z.exe\" : \"7zz\";\n const bundled = join(\n dirname(fileURLToPath(import.meta.url)),\n \"..\",\n \"assets\",\n \"7zip\",\n exeName,\n );\n if (existsSync(bundled)) {\n return { exe: bundled, source: \"内置资产 assets/7zip\" };\n }\n }\n // 系统 PATH(Windows 装了 7-Zip 有 7z.exe;Linux p7zip-full 提供 7z 命令)\n return { exe: \"7z\", source: \"系统 PATH\" };\n}\n\n/** 解包安装包到指定目录(spawn 7z x,失败抛错)。 */\nexport async function extractInstaller(\n installerPath: string,\n destDir: string,\n sevenZipPath?: string,\n): Promise<void> {\n const sevenZip = sevenZipPath ?? findSevenZip().exe;\n await mkdir(destDir, { recursive: true });\n await new Promise<void>((resolve, reject) => {\n execFile(\n sevenZip,\n [\"x\", installerPath, `-o${destDir}`, \"-y\"],\n { maxBuffer: 64 * 1024 * 1024 },\n (err, _stdout, stderr) => {\n if (err !== null) {\n const code = (err as NodeJS.ErrnoException).code;\n const hint =\n code === \"ENOENT\"\n ? `未找到 7z 可执行文件: ${sevenZip}\\n` +\n \"(Windows 请安装 7-Zip 或用 NAPUTO_7Z_PATH 指定;\" +\n \"Linux 自动下载失败时请安装 p7zip-full 或用 NAPUTO_7Z_PATH 指定)\"\n : \"(QQ 安装包是 NSIS 格式,需完整版 7z 而非 7za)\";\n reject(new Error(`7z 解包失败: ${installerPath}\\n${stderr.trim()}\\n${hint}`));\n return;\n }\n resolve();\n },\n );\n });\n}\n\n/**\n * 从解包目录提取运行所需文件到缓存:\n * resources/app 顶层全部 *.node + *.dll(保持同目录结构,PATH 前置即可解析依赖)。\n * 幂等:目标 wrapper.node 已存在则跳过。\n */\nexport async function extractWrapperFiles(\n extractedDir: string,\n version: string,\n cacheRoot: string,\n): Promise<QqInstallInfo> {\n // NSIS 解包产物在 <extracted>/Files/(安装根),resources/app 在其下\n const appDir = join(extractedDir, \"Files\", \"versions\", version, \"resources\", \"app\");\n if (!existsSync(join(appDir, \"wrapper.node\"))) {\n throw new Error(`解包产物缺少 wrapper.node: ${appDir}`);\n }\n\n // 目标结构(与安装目录同构):\n // <cacheRoot>/<version>/versions/<version>/resources/app/*.{node,dll}\n const destAppDir = join(cacheRoot, version, \"versions\", version, \"resources\", \"app\");\n const destWrapper = join(destAppDir, \"wrapper.node\");\n\n // 幂等:已提取过则跳过\n if (existsSync(destWrapper)) {\n return buildCachedInfo(cacheRoot, version);\n }\n\n await mkdir(destAppDir, { recursive: true });\n const entries = await readdir(appDir, { withFileTypes: true });\n for (const entry of entries) {\n if (!entry.isFile()) {\n continue; // 跳过子目录(wmpfsdk/avsdk 等大目录不需要)\n }\n const dot = entry.name.lastIndexOf(\".\");\n const ext = dot >= 0 ? entry.name.slice(dot).toLowerCase() : \"\";\n if (!EXTRACT_EXTENSIONS.has(ext)) {\n continue; // 只提取 .node + .dll\n }\n await copyFile(join(appDir, entry.name), join(destAppDir, entry.name));\n }\n if (!existsSync(destWrapper)) {\n throw new Error(`提取失败:wrapper.node 未复制到 ${destWrapper}`);\n }\n\n return buildCachedInfo(cacheRoot, version);\n}\n\n/** 构造 cached 来源的 QqInstallInfo(wrapperPath 指向缓存中 wrapper.node)。 */\nfunction buildCachedInfo(cacheRoot: string, version: string): QqInstallInfo {\n const versionDir = join(cacheRoot, version);\n return {\n qqPath: join(versionDir, \"QQ.exe\"), // 语义占位(缓存目录无 QQ.exe)\n installDir: versionDir,\n version,\n wrapperPath: join(versionDir, \"versions\", version, \"resources\", \"app\", \"wrapper.node\"),\n source: \"cached\",\n };\n}\n\n/** 清理缓存版本目录(下载/解包失败残留,供调用方兜底)。 */\nexport async function clearCacheVersion(cacheRoot: string, version: string): Promise<void> {\n await rm(join(cacheRoot, version), { recursive: true, force: true });\n}\n","/**\n * win-node.ts:Windows 版 node.exe 获取(P2,Linux/wine 场景用)。\n *\n * wine 只能跑 Windows PE 程序——自建宿主在 Linux 上必须用 Windows 版 node.exe\n * 作为宿主(wrapper.node 是 PE 原生模块,只能被 Windows node.exe dlopen)。\n * 来源:nodejs.org 官方 zip(开源软件官方发行版,但为保守起见同样不内置,\n * 运行时下载到 <数据根>/runtime/win-node/,可被 NAPUTO_WIN_NODE_PATH 覆盖)。\n *\n * 设计文档 §3.1。与 qq-download.ts 同构:https 下载 + 解压 + 缓存幂等。\n */\nimport { existsSync, mkdirSync } from \"node:fs\";\nimport { copyFile, readdir, rm } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\nimport process from \"node:process\";\nimport { resolveDataRoot } from \"./data-root.js\";\nimport { downloadFile } from \"./qq-download.js\";\nimport { extractInstaller } from \"./qq-extract.js\";\n\n/** 目标 Windows node.exe 可执行文件名(zip 内)。 */\nconst WIN_NODE_EXE = \"node.exe\";\n\n/** 运行时缓存子目录名(<数据根>/runtime/win-node/)。 */\nexport const WIN_NODE_DIR_NAME = \"win-node\";\n\n/** Windows node.exe 定位结果。 */\nexport interface WinNodeInfo {\n /** node.exe 绝对路径(Windows 视角,供 wine 调用)。 */\n exePath: string;\n /** 已下载的完整版 node zip 版本号(如 v24.16.0)。 */\n version: string;\n}\n\n/** 解析 nodejs.org zip URL(与 dist 目录命名一致)。 */\nexport function nodeZipUrl(version: string): string {\n return `https://nodejs.org/dist/${version}/node-${version}-win-x64.zip`;\n}\n\n/**\n * 确保 Windows 版 node.exe 就绪(下载 + 解压 + 缓存)。\n * 幂等:<数据根>/runtime/win-node/<version>/node.exe 已存在则直接返回。\n */\nexport async function ensureWinNode(\n options: {\n /** 数据根(缓存 <数据根>/runtime/win-node/)。 */\n dataRoot?: string;\n /** node 版本(缺省读 NAPUTO_WIN_NODE_VERSION,再兜底 latest v24)。 */\n version?: string;\n /** 显式 node.exe 路径(覆盖缓存逻辑,读 NAPUTO_WIN_NODE_PATH)。 */\n exePath?: string;\n } = {},\n): Promise<WinNodeInfo> {\n // 显式路径覆盖(NAPUTO_WIN_NODE_PATH / exePath 参数)\n const explicit = options.exePath ?? process.env[\"NAPUTO_WIN_NODE_PATH\"];\n if (explicit !== undefined && explicit !== \"\" && existsSync(explicit)) {\n return { exePath: resolve(explicit), version: \"explicit\" };\n }\n\n const version =\n options.version ?? process.env[\"NAPUTO_WIN_NODE_VERSION\"] ?? DEFAULT_WIN_NODE_VERSION;\n const dataRoot = resolveDataRoot(options.dataRoot);\n const cacheDir = join(dataRoot, \"runtime\", WIN_NODE_DIR_NAME, version);\n const exePath = join(cacheDir, WIN_NODE_EXE);\n if (existsSync(exePath)) {\n return { exePath, version };\n }\n\n // 下载 zip + 解压(用 P1 的 7z 解 zip;7z 支持 zip)\n const zipPath = join(cacheDir, `node-${version}-win-x64.zip`);\n const url = process.env[\"NAPUTO_WIN_NODE_URL\"] ?? nodeZipUrl(version);\n mkdirSync(cacheDir, { recursive: true });\n try {\n await downloadFile({ dest: zipPath, url });\n await unzipNode(zipPath, cacheDir);\n } finally {\n await rm(zipPath, { force: true });\n }\n if (!existsSync(exePath)) {\n throw new Error(`解压后未找到 node.exe: ${exePath}`);\n }\n return { exePath, version };\n}\n\n/** 解压 node zip(zip 内为 node-<version>-win-x64/ 目录,取其中 node.exe 移到缓存根)。 */\nasync function unzipNode(zipPath: string, cacheDir: string): Promise<void> {\n const tmpDir = join(cacheDir, \"_zip\");\n await extractInstaller(zipPath, tmpDir);\n // zip 顶层是 node-<version>-win-x64/,找其中 node.exe\n const top = await readdir(tmpDir);\n const nodeDir = top.find((n) => n.startsWith(\"node-\"));\n const src =\n nodeDir === undefined ? join(tmpDir, WIN_NODE_EXE) : join(tmpDir, nodeDir, WIN_NODE_EXE);\n if (!existsSync(src)) {\n throw new Error(`node zip 解压后未找到 node.exe: ${src}`);\n }\n await copyFile(src, join(cacheDir, WIN_NODE_EXE));\n await rm(tmpDir, { recursive: true, force: true });\n}\n\n/**\n * 默认 Windows node 版本(latest v24,LTS 线;可被环境变量/参数覆盖)。\n * ⚠️ nodejs.org/dist 保留历史版本不会 404,但 pin 死旧 patch 收不到安全更新——\n * 升级 Windows node 时记得同步 bump 此常量(或经 NAPUTO_WIN_NODE_VERSION 覆盖)。\n */\nconst DEFAULT_WIN_NODE_VERSION = \"v24.16.0\";\n","/**\n * wine.ts:Linux 上经 wine 运行 Windows 版 node.exe 的路径与 spawn 组装(P2)。\n *\n * 设计文档 §3.2(2026-08-12 拍板确认)。核心认知:\n * - Windows 版 node.exe 在 wine 内把 Linux 根目录挂载在 `Z:\\`\n * - process.dlopen / 环境变量路径都是 wine 视角的 Windows 路径\n * - 传给 wine 子进程的**所有路径**(wrapperPath / cfgDir / stubDir / self-host.cjs / PATH)\n * 都要过 toWinePath 转成 `Z:\\...`\n *\n * 纯函数设计:不 import node:child_process,方便 Windows 上单测断言(P2 纯逻辑层)。\n */\n\n/** Linux 绝对路径 → wine `Z:\\` 路径(Windows 版 node.exe 视角)。幂等:已是 Windows 路径(含盘符)原样返回。 */\nexport function toWinePath(linuxPath: string): string {\n // 已是 Windows 风格(盘符 + 反斜杠)→ 原样(幂等保护,调用方不应传这种但防御性保留)\n if (DRIVE_LETTER_RE.test(linuxPath)) {\n return linuxPath;\n }\n // /app/.napuketto/qq-files/9.9.33/wrapper.node → Z:\\app\\.napuketto\\qq-files\\9.9.33\\wrapper.node\n // 去前导 /,正斜杠转反斜杠\n return `Z:\\\\${linuxPath.replaceAll(\"/\", \"\\\\\").replace(LEADING_SLASH_RE, \"\")}`;\n}\n\n/** 前导斜杠(toWinePath 去根用,顶层常量避免每次调用重建)。 */\nconst LEADING_SLASH_RE = /^\\\\/;\n\n/** 盘符前缀(C: / Z: 等,识别已转换的 Windows 路径)。 */\nconst DRIVE_LETTER_RE = /^[A-Za-z]:/;\n\n/** 判断当前是否 Linux(需 wine 的场景)。 */\nexport function isLinux(): boolean {\n return process.platform === \"linux\";\n}\n\n/**\n * Unix PATH(冒号分隔)→ wine Windows PATH(分号分隔 + 全部 Z:\\ 条目)。\n *\n * ⚠️ 混入 Unix PATH 会让 wine 按错误分隔符拆分盘符路径(Z:\\...),stub 目录\n * 丢失 → wrapper.node 依赖的 QQNT.dll 找不到(2026-08-14 生产实测\n * \"import_dll Library QQNT.dll not found\")。wine 场景 PATH 必须纯 Windows 风格。\n */\nexport function unixPathToWinePath(unixPath: string): string {\n return unixPath\n .split(\":\")\n .filter((entry) => entry !== \"\")\n .map((entry) => toWinePath(entry))\n .join(\";\");\n}\n\n/** wine 可执行文件(Linux 上 spawn 用;可被 NAPUTO_WINE 覆盖)。 */\nexport function wineBinary(): string {\n return process.env[\"NAPUTO_WINE\"] ?? \"wine\";\n}\n\n/** wine 安装指引(缺失时提示用户如何准备,纯字符串可单测)。 */\nexport function wineInstallHint(): string {\n return (\n \"Linux/WSL 自建宿主需要 wine 运行 Windows 版 node.exe(wrapper.node 是\" +\n \"Windows PE 原生模块,只能被 Windows node.exe dlopen)。\\n\" +\n \" 安装:sudo apt-get install wine64(或其他发行版对应 wine 包)\\n\" +\n \" 覆盖:设置环境变量 NAPUTO_WINE 指向 wine 可执行文件路径\"\n );\n}\n\n/**\n * wine 可执行性探测结果 → 可读错误(null = 可用)。纯函数,可单测。\n * 由 launcher 在 spawn 前用 spawnSync 探测 wine(缺失时 throw 可读错误,\n * 避免 spawn 异步 emit 'error' 无监听者导致宿主进程崩溃,2026-08-23 WSL 实测)。\n */\nexport function wineCheckError(probe: {\n /** spawnSync 退出码(null = spawn 本身失败,如命令不存在)。 */\n status: number | null;\n /** spawn 错误(ENOENT 等;可选)。 */\n error?: unknown;\n}): string | null {\n if (probe.status === 0) {\n return null;\n }\n const err = probe.error as NodeJS.ErrnoException | undefined;\n if (err !== undefined && err.code === \"ENOENT\") {\n return `未检测到 wine 可执行文件。${wineInstallHint()}`;\n }\n if (err !== undefined) {\n return `wine 探测失败: ${err.message ?? String(err)}。${wineInstallHint()}`;\n }\n return `wine --version 退出码非 0(status=${String(probe.status)}),wine 可能异常。${wineInstallHint()}`;\n}\n\n/** spawn 命令与参数(平台分支;纯函数,可单测)。 */\nexport interface SpawnCommand {\n /** 命令(win32 = node.exe;linux = wine)。 */\n command: string;\n /** 参数(win32 = [selfHostPath];linux = [winNodePath, selfHostPath])。 */\n args: string[];\n}\n\n/** 组装 spawn 命令(win32 vs linux 分支)。 */\nexport function buildSpawnCommand(options: {\n /** 平台(缺省 process.platform,测试注入)。 */\n platform?: NodeJS.Platform;\n /** Windows 本机 node 路径(win32 用,缺省 process.execPath)。 */\n winNodePath?: string;\n /** wine 命令(linux 用,缺省 wineBinary())。 */\n wine?: string;\n /** 自建宿主入口(self-host.cjs,传给 wine/win node)。 */\n selfHostPath: string;\n}): SpawnCommand {\n const platform = options.platform ?? process.platform;\n if (platform === \"win32\") {\n return {\n command: options.winNodePath ?? process.execPath,\n args: [options.selfHostPath],\n };\n }\n // linux 及其他(wine 场景)\n return {\n command: options.wine ?? wineBinary(),\n args: [options.winNodePath ?? \"node.exe\", options.selfHostPath],\n };\n}\n","/**\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 *\n * P2(2026-08-12):平台分支——win32 本机 node;linux 经 wine 跑 Windows 版 node.exe\n * (ensureWinNode 下载)。所有传给 wine 子进程的路径过 toWinePath(Z:\\)。\n */\nimport { type StdioOptions, spawn, spawnSync } from \"node:child_process\";\nimport { existsSync, mkdirSync } from \"node:fs\";\nimport { basename, dirname, join, resolve } from \"node:path\";\nimport process from \"node:process\";\nimport { fileURLToPath } from \"node:url\";\nimport type { QqInstallInfo } from \"./locate-qq.js\";\nimport { ensureWinNode } from \"./win-node.js\";\nimport {\n buildSpawnCommand,\n isLinux,\n toWinePath,\n unixPathToWinePath,\n wineBinary,\n wineCheckError,\n wineInstallHint,\n} from \"./wine.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 /** 强制扫码登录(注入 NAPUTO_QR_ONLY;跳过快速登录直接 QR,koishi 面板「扫码登录」用)。 */\n qrOnly?: boolean;\n /** IPC 子进程模式(=1:stdout 走 JSON 行协议 + stdin 收 action/control,koishi 插件驱动)。 */\n ipc?: boolean;\n /** 自建宿主入口(默认 dist/host/self-host.cjs)。 */\n selfHostEntry?: string;\n /** Windows 版 node.exe 路径(linux 下载覆盖 / win32 非 node 宿主时显式指定;缺省自动解析)。 */\n winNodePath?: 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 * 阶段回调(stub 校验/win-node 下载/spawn 提示,2026-08-23 起——下载\n * 313MB 安装包与 win-node 全程静默的坑)。调用方接 logger.info。\n */\n onStage?: (message: string) => void;\n}\n\nexport interface LaunchResult {\n /** 子进程句柄(自建宿主 node 进程)。 */\n child: ReturnType<typeof spawn>;\n /** 自建宿主入口路径(self-host.cjs)。 */\n bootJsPath: string;\n}\n\n/**\n * 启动自建宿主(路线 A):spawn 标准 node 直接跑 self-host.cjs,不拉起 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 *\n * P2 平台分支:win32 用本机 node;linux 用 wine + Windows 版 node.exe\n * (ensureWinNode 自动下载,路径过 toWinePath)。async(win-node 可能需下载)。\n */\nexport async function launchSelfHost(options: LaunchOptions): Promise<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 options.onStage?.(`stub QQNT.dll 就绪:${stub}`);\n\n // 平台分支:win32 本机 node;linux wine + win-node(下载)\n const { useWine, winNodePath } = await resolveNodeExecutable(options);\n\n const env = buildLaunchEnv(options, useWine);\n applyPathEnv(env, { stub, wrapperPath: options.qq.wrapperPath, useWine });\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 // spawn 命令:win32 = node.exe selfHostPath;linux = wine winNodePath selfHostPath\n // wine 场景 selfHostPath 也需转 Windows 路径(wine 内 node 读参数)\n // winNodePath:resolveNodeExecutable 解析出的宿主 node(wine 下载 / win32 非\n // node 宿主解析);未解析(win32 标准 node 环境,execPath 即 node 自身)回退。\n const { command, args } = buildSpawnCommand({\n ...(winNodePath !== undefined ? { winNodePath } : { winNodePath: process.execPath }),\n selfHostPath: useWine ? toWinePath(selfHostPath) : selfHostPath,\n wine: wineBinary(),\n });\n options.onStage?.(`启动自建宿主子进程:${command} ${args.join(\" \")}`);\n const child = spawn(command, args, {\n // exactOptionalPropertyTypes:未传 cwd 时不显式写入 undefined(继承父进程)\n ...(options.cwd !== undefined ? { cwd: options.cwd } : {}),\n env,\n stdio: options.stdio ?? \"inherit\",\n windowsHide: false,\n });\n\n // ⚠️ spawn error 兜底(2026-08-23 WSL 生产事故):wine 未安装时 Node 异步\n // emit 'error'(ENOENT),无监听者 = EventEmitter 默认 throw →\n // uncaughtException → 整个宿主进程(koishi)崩溃。预检(assertWineAvailable)\n // 已覆盖常见场景,这里防漏网并输出可读信息,绝不让宿主进程崩掉。\n child.once(\"error\", (err) => {\n const code = (err as NodeJS.ErrnoException).code;\n const message =\n code === \"ENOENT\"\n ? `无法启动子进程(命令不存在): ${command}\\n${isLinux() ? wineInstallHint() : \"\"}`\n : `子进程启动失败: ${err.message}`;\n // 防崩溃兜底输出(driver 侧另有 error 监听转 onError 友好流程)\n process.stderr.write(`[napuketto-loader] ${message}\\n`);\n });\n\n return { child, bootJsPath: selfHostPath };\n}\n\n/**\n * 平台分支解析 node 可执行:win32 本机(非 node 宿主时解析真 node);linux wine +\n * Windows 版 node.exe(ensureWinNode 下载)。\n */\nasync function resolveNodeExecutable(options: LaunchOptions): Promise<{\n useWine: boolean;\n winNodePath: string | undefined;\n}> {\n if (!isLinux()) {\n // win32:标准 node 宿主(execPath 即 node)直接用自身;非 node 宿主\n // (koishi CE 等 Bun 运行时,execPath=bun.exe)必须解析真 node——\n // wrapper.node 的符号由 stub QQNT.dll 绑定到 node.exe,bun.exe 进程内\n // 无此模块,LoadLibrary 报 1114「DLL 初始化例程失败」(2026-09-05 实测)。\n if (isNodeExecutable(process.execPath)) {\n return { useWine: false, winNodePath: undefined };\n }\n const hostNode = await resolveWinHostNode({\n ...(options.winNodePath !== undefined ? { explicitPath: options.winNodePath } : {}),\n ...(options.cwd !== undefined ? { dataRoot: options.cwd } : {}),\n ...(options.onStage !== undefined ? { onStage: options.onStage } : {}),\n });\n return { useWine: false, winNodePath: hostNode };\n }\n // ⚠️ wine 预检(2026-08-23 WSL 生产事故):spawn 前确认 wine 可执行——\n // 干净 WSL 环境默认没有 wine,缺失时给出安装指引(throw 可读错误,由\n // 调用方 driver 捕获转 [W] 驱动错误),而不是 spawn 后异步 'error' 崩进程。\n const wine = wineBinary();\n const probe = spawnSync(wine, [\"--version\"], { stdio: \"ignore\" });\n const hint = wineCheckError(probe);\n if (hint !== null) {\n throw new Error(hint);\n }\n options.onStage?.(\"wine 就绪,获取 Windows 版 node.exe(Linux/wine 场景)…\");\n const winNode = await ensureWinNode({\n ...(options.cwd !== undefined ? { dataRoot: options.cwd } : {}),\n ...(options.winNodePath !== undefined ? { exePath: options.winNodePath } : {}),\n });\n options.onStage?.(`Windows 版 node.exe 就绪:${winNode.exePath}(${winNode.version})`);\n return { useWine: true, winNodePath: winNode.exePath };\n}\n\n/** 可执行文件名是否 node(win32 宿主判定;node.exe / node 两种形态)。 */\nexport function isNodeExecutable(execPath: string): boolean {\n const name = basename(execPath).toLowerCase();\n return name === \"node.exe\" || name === \"node\";\n}\n\n/** 系统 node 探测结果(win32,PATH 解析 + 可执行校验)。 */\nexport interface SystemNodeInfo {\n exePath: string;\n version: string;\n}\n\n/** 行拆分(where 输出 \\r\\n / \\n 兼容)。 */\nconst LINE_SPLIT = /\\r?\\n/;\n\n/** 探测系统 PATH 上的 node(win32;where node 首条 + --version 校验可执行)。 */\nexport function findSystemNode(): SystemNodeInfo | undefined {\n const probe = spawnSync(\"where\", [\"node\"], {\n stdio: \"pipe\",\n encoding: \"utf8\",\n windowsHide: true,\n });\n if (probe.status !== 0 || !probe.stdout) {\n return undefined;\n }\n const first = probe.stdout\n .split(LINE_SPLIT)\n .map((line) => line.trim())\n .find((line) => line !== \"\");\n if (first === undefined || !existsSync(first)) {\n return undefined;\n }\n const ver = spawnSync(first, [\"--version\"], {\n stdio: \"pipe\",\n encoding: \"utf8\",\n windowsHide: true,\n });\n const version = ver.status === 0 ? ver.stdout.trim() : \"\";\n if (!version.startsWith(\"v\")) {\n return undefined;\n }\n return { exePath: resolve(first), version };\n}\n\n/** resolveWinHostNode 测试注入面(探测/下载替换为假实现)。 */\nexport interface WinHostNodeResolvers {\n findSystemNode?: () => SystemNodeInfo | undefined;\n ensureWinNode?: typeof ensureWinNode;\n}\n\n/**\n * win32 非 node 宿主时解析真 node.exe:显式覆盖(winNodePath 参数 /\n * NAPUTO_WIN_NODE_PATH)> 系统 PATH node(离线可用,免下载)> ensureWinNode\n * 下载(缓存幂等)。显式路径不存在时静默跳过(与 ensureWinNode 行为一致)。\n */\nexport async function resolveWinHostNode(\n options: {\n explicitPath?: string;\n dataRoot?: string;\n onStage?: (message: string) => void;\n },\n resolvers: WinHostNodeResolvers = {},\n): Promise<string | undefined> {\n const findSystem = resolvers.findSystemNode ?? findSystemNode;\n const ensure = resolvers.ensureWinNode ?? ensureWinNode;\n\n const explicit = options.explicitPath ?? process.env[\"NAPUTO_WIN_NODE_PATH\"];\n if (explicit !== undefined && explicit !== \"\" && existsSync(explicit)) {\n const exePath = resolve(explicit);\n options.onStage?.(`使用显式指定 node:${exePath}`);\n return exePath;\n }\n\n options.onStage?.(\n `当前宿主非 node(${basename(process.execPath)}),解析 Windows 版 node.exe…`,\n );\n\n const sys = findSystem();\n if (sys !== undefined) {\n options.onStage?.(`系统 node ${sys.version}:${sys.exePath}`);\n return sys.exePath;\n }\n\n const winNode = await ensure({\n ...(options.dataRoot !== undefined ? { dataRoot: options.dataRoot } : {}),\n });\n options.onStage?.(`Windows 版 node.exe 就绪:${winNode.exePath}(${winNode.version})`);\n return winNode.exePath;\n}\n\n/** 注入 PATH(stub 目录 + wrapper.node 目录前置)与 STUB_DIR(wine 场景转 Z:\\)。 */\nfunction applyPathEnv(\n env: Record<string, string>,\n opts: { stub: string; wrapperPath: string; useWine: boolean },\n): void {\n const p = (linuxPath: string): string => (opts.useWine ? toWinePath(linuxPath) : linuxPath);\n // ⚠️ wine 场景 PATH 必须纯 Windows 风格(全部 Z:\\ 条目 + 分号分隔):混入\n // Unix PATH(冒号分隔)会让 wine 按错误分隔符拆分,盘符路径(Z:\\...)被拆散,\n // stub 目录丢失 → wrapper.node 依赖的 QQNT.dll 找不到(2026-08-14 生产实测\n // \"import_dll Library QQNT.dll not found\")。Unix PATH 逐条转 Z:\\ 再并入。\n const unixPath = process.env[\"PATH\"] ?? \"\";\n const winPath = opts.useWine ? unixPathToWinePath(unixPath) : unixPath;\n const pathEntries = [p(opts.stub), p(dirname(opts.wrapperPath)), winPath]\n .filter((entry) => entry !== undefined && entry !== \"\")\n .join(\";\");\n env[\"PATH\"] = pathEntries;\n env[ENV.STUB_DIR] = p(opts.stub);\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/** 装配自建宿主环境变量(wine 场景路径转 Z:\\;win32 原样)。 */\nfunction buildLaunchEnv(options: LaunchOptions, useWine: boolean): Record<string, string> {\n // 配置目录兜底\n const cfg = resolve(options.cfgDir);\n mkdirSync(cfg, { recursive: true });\n\n // wine 场景:传给子进程的路径全转 Z:\\(node.exe 在 wine 内按 Windows 路径读)\n const p = (linuxPath: string): string => (useWine ? toWinePath(linuxPath) : linuxPath);\n const env: Record<string, string> = {\n ...process.env,\n [ENV.QQ_PATH]: p(options.qq.qqPath),\n [ENV.KERNEL_ENTRY]: p(resolve(options.kernelEntry)),\n [ENV.CFG_DIR]: p(cfg),\n [ENV.QQ_VERSION]: options.qq.version,\n [ENV.WRAPPER_PATH]: p(options.qq.wrapperPath),\n };\n // 可选注入(对象展开,保持低复杂度)\n const optional: Record<string, string> = {\n ...(options.selfHost === true ? { [ENV.SELF_HOST]: \"1\" } : {}),\n ...(options.adapterEntry !== undefined\n ? { [ENV.ADAPTER_ENTRY]: p(resolve(options.adapterEntry)) }\n : {}),\n ...(options.networkEntry !== undefined\n ? { [ENV.NETWORK_ENTRY]: p(resolve(options.networkEntry)) }\n : {}),\n ...(options.configPath !== undefined\n ? { [ENV.CONFIG_PATH]: p(resolve(options.configPath)) }\n : {}),\n ...(options.quickUin !== undefined ? { [ENV.QUICK_UIN]: options.quickUin } : {}),\n ...(options.qrOnly === true ? { [ENV.QR_ONLY]: \"1\" } : {}),\n ...(options.ipc === true ? { [ENV.IPC]: \"1\" } : {}),\n };\n return { ...env, ...optional };\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 /** 强制扫码登录(跳过快速登录直接 QR;koishi 面板「扫码登录」重启路径用)。 */\n QR_ONLY: \"NAPUTO_QR_ONLY\",\n /** IPC 子进程模式(koishi 插件驱动:stdout JSON 行 + stdin action/control)。 */\n IPC: \"NAPUTO_IPC\",\n} as const;\n","/**\n * qq-releases.ts:版本清单读取(P1)。\n *\n * 运行时从包根 qq-releases.json 读取(发布时 files 含它,安装后位于\n * node_modules/@napuketto/loader/qq-releases.json)。\n *\n * 维护机制(真实存在,非「声称」):清单由 CI 自动维护——\n * .github/workflows/update-qq-releases.yml(每日 cron + 手动触发)调用\n * scripts/qq-releases/update-qq-releases.ts(纯 Node,与本地共用),抓取 QQ 官方下载配置\n * 解析最新版 + 下载安装包计算 sha256 + 解析安装包内部版本目录后更新本清单\n * (保留历史条目、known 按版本递增)。社区 PR 仍可手动补充。NAPUTO_QQ_URL\n * 可运行时覆盖下载地址(用户拿到新链接时用)。\n */\nimport { readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport process from \"node:process\";\nimport { fileURLToPath } from \"node:url\";\n\n/** 清单中单个版本条目。 */\nexport interface QqReleaseEntry {\n /** 版本目录名(如 9.9.33-51802)。 */\n version: string;\n /** 官方安装包下载 URL。 */\n url: string;\n /** sha256(十六进制小写);空串 = 尚未校验(首次录入时 CI 计算后填入)。 */\n sha256: string;\n /** appid(预知信息,运行时仍从 major.node 解析;0 = 尚未解析/待运行时补)。 */\n appid: number;\n /** 来源标记(official = 官方)。 */\n source: string;\n /** 构建日期(YYYY-MM-DD,可选)。 */\n buildDate?: string;\n}\n\n/** 清单文件结构。 */\nexport interface QqReleasesFile {\n schema: number;\n known: QqReleaseEntry[];\n}\n\n/** 清单解析失败。 */\nexport class QqReleasesError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"QqReleasesError\";\n }\n}\n\n/** 包根路径(dist 与包根同层:dist/index.mjs → ../qq-releases.json)。 */\nfunction packageRoot(): string {\n return dirname(dirname(fileURLToPath(import.meta.url)));\n}\n\n/** 读版本清单(同步,启动期调用一次即可)。 */\nexport function loadQqReleases(): QqReleasesFile {\n const file = join(packageRoot(), \"qq-releases.json\");\n let raw: string;\n try {\n raw = readFileSync(file, \"utf8\");\n } catch {\n throw new QqReleasesError(`版本清单缺失: ${file}(@napuketto/loader 包安装不完整?)`);\n }\n try {\n const parsed = JSON.parse(raw) as Partial<QqReleasesFile>;\n if (parsed.schema !== 1 || !Array.isArray(parsed.known) || parsed.known.length === 0) {\n throw new Error(\"结构不合法\");\n }\n return parsed as QqReleasesFile;\n } catch {\n throw new QqReleasesError(`版本清单解析失败: ${file}`);\n }\n}\n\n/** 取最新已知可用版本(known 数组按录入顺序,取最后一项 = 最新)。 */\nexport function latestRelease(releases: QqReleasesFile): QqReleaseEntry {\n const [latest] = [...releases.known].reverse();\n if (latest === undefined) {\n throw new QqReleasesError(\"版本清单为空\");\n }\n return latest;\n}\n\n/**\n * 解析实际下载 URL:NAPUTO_QQ_URL 环境变量覆盖 > 清单 URL。\n * 供 downloadFile 使用(也支持按版本查询,当前固定取最新)。\n */\nexport function resolveDownloadUrl(release: QqReleaseEntry): string {\n const override = process.env[\"NAPUTO_QQ_URL\"];\n return override !== undefined && override !== \"\" ? override : release.url;\n}\n","/**\n * 定位 QQ 原生文件(wrapper.node)来源(P0:多级来源纯定位,不下载)。\n *\n * 来源优先级(resolveQqFiles):\n * L0: NAPUTO_QQ_FILES / qqFilesDir —— 显式指定含 versions/ 结构的文件根(缓存/拷贝)\n * L1: 本机 QQ 安装 —— NAPUTO_QQ_PATH / 注册表 / 常见路径(既有逻辑保留)\n * L2: 数据根缓存 <数据根>/qq-files/<版本> —— 下载解包产物(P1 落地后才有)\n */\nimport { execFileSync } from \"node:child_process\";\nimport { existsSync, mkdirSync, readdirSync, statSync } from \"node:fs\";\nimport { chmod, rm } from \"node:fs/promises\";\nimport { dirname, join, resolve } from \"node:path\";\nimport process from \"node:process\";\nimport { fileURLToPath } from \"node:url\";\nimport { resolveDataRoot } from \"./data-root.js\";\nimport { downloadFile } from \"./qq-download.js\";\nimport { clearCacheVersion, extractInstaller, extractWrapperFiles } from \"./qq-extract.js\";\nimport { latestRelease, loadQqReleases, resolveDownloadUrl } from \"./qq-releases.js\";\n\n/** 数据根下 QQ 文件缓存目录名(<数据根>/qq-files/<版本>/,P1 下载解包产物)。 */\nexport const QQ_FILES_DIR_NAME = \"qq-files\";\n\n/**\n * 7-Zip 官方 Linux 版版本号(tar.xz 文件名组成部分;兜底下载用,内置资产优先)。\n * ⚠️ 此值为最后兜底下载用,7-Zip 官网只留最新版,旧版号会 404;内置资产失败才触发。\n */\nconst SEVEN_ZIP_LINUX_VERSION = \"2501\";\n\n/** 注册表 UninstallString 查询(QQ 官方安装路径)。 */\nconst REG_QUERY_RE = /\"([^\"]+)\"/;\n\n/** Windows 盘符路径正则(`C:/...` / `C:\\...`,wslMappedPath 映射用,模块级常量)。 */\nconst DRIVE_PATH_RE = /^([A-Za-z]):[\\\\/](.*)$/;\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 ];\n for (const p of candidates) {\n // WSL 场景:Windows 盘挂载在 /mnt/<盘符>/(如 C:\\ → /mnt/c/),\n // 同一候选同时探测 Linux 侧映射路径(2026-08-13 koishi-app 实测报「未找到 QQ.exe」)。\n const wsl = wslMappedPath(p);\n if (existsSync(wsl)) {\n return wsl;\n }\n if (existsSync(p)) {\n return p;\n }\n }\n return null;\n}\n\n/**\n * Windows 路径 → WSL 挂载路径(`C:/...` → `/mnt/c/...`;非 Linux 或非盘符路径原样)。\n * WSL 默认把 Windows 盘符挂载到 `/mnt/<小写盘符>/`,Linux 侧 `existsSync` 必须用映射路径。\n * platform 参数可注入(测试用;缺省 process.platform)。\n * 导出便于单测(locate-qq.test.ts)。\n * ⚠️ 不用 join 拼 `/mnt/...`:测试在 Windows 跑,win32 join 会产生反斜杠;目标路径\n * 是 Linux 侧路径(WSL 挂载点),必须恒正斜杠。\n */\nexport function wslMappedPath(\n winPath: string,\n platform: NodeJS.Platform = process.platform,\n): string {\n if (platform !== \"linux\") {\n return winPath;\n }\n const [, drive, rest] = DRIVE_PATH_RE.exec(winPath) ?? [];\n if (drive === undefined || rest === undefined) {\n return winPath;\n }\n return `/mnt/${drive.toLowerCase()}/${rest.replaceAll(\"\\\\\", \"/\")}`;\n}\n\n/** QQ 文件来源。 */\nexport type QqFileSource = \"local\" | \"cached\";\n\n/** QQ 安装目录探测结果。 */\nexport interface QqInstallInfo {\n /** QQ.exe 绝对路径(cached 来源为语义占位——缓存目录无 QQ.exe,当前无消费方)。 */\n qqPath: string;\n /** 安装根目录 / 文件根目录。 */\n installDir: string;\n /** 当前版本目录名(如 9.9.31-49919)。 */\n version: string;\n /** wrapper.node 绝对路径。 */\n wrapperPath: string;\n /** 来源:local = 本机安装;cached = NAPUTO_QQ_FILES / 数据根缓存。 */\n source: QqFileSource;\n}\n\n/** resolveQqFiles 选项。 */\nexport interface ResolveQqFilesOptions {\n /** 显式文件根(L0;缺省读 NAPUTO_QQ_FILES)。 */\n qqFilesDir?: string;\n /** 显式 QQ.exe 路径(L1;缺省读 NAPUTO_QQ_PATH)。 */\n qqPath?: string;\n /** 数据根(L2 缓存扫描;缺省轻量解析 NAPKETTO_DATA ?? 项目根/.napuketto)。 */\n dataRoot?: string;\n /** L0/L1/L2 全部失败时自动进入下载流程(P1;默认 true,测试可关)。 */\n autoDownload?: boolean;\n /** 7z 可执行文件路径(解包用;缺省自动探测)。 */\n sevenZipPath?: string;\n /**\n * 阶段回调(下载/校验/解包/提取各阶段提示,2026-08-23 WSL 事故后加:\n * 下载 313MB 全程静默,用户以为流程没生效)。调用方接 logger.info。\n */\n onStage?: (message: string) => void;\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/** 从文件根定位最新版本 wrapper.node(local/cached 共用)。 */\nfunction resolveFromRoot(rootDir: string, source: QqFileSource): QqInstallInfo {\n const versionsDir = join(rootDir, \"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 {\n qqPath: join(rootDir, \"QQ.exe\"),\n installDir: rootDir,\n version: first,\n wrapperPath,\n source,\n };\n}\n\n/** 从 QQ.exe 路径推导安装目录,并探测当前版本(L1:本机安装)。 */\nexport function resolveQqInstall(qqPath?: string): QqInstallInfo {\n const qq = qqPath ?? process.env[\"NAPUTO_QQ_PATH\"] ?? locateQqPath();\n // Windows 安装结构:<installDir>/versions/<版本>/resources/app/wrapper.node\n // ⚠️ dirname 跨平台:Linux/WSL 路径用正斜杠(lastIndexOf(\"\\\\\") 会切错,2026-08-13 实测)\n const installDir = dirname(qq);\n return resolveFromRoot(installDir, \"local\");\n}\n\n/**\n * 多级来源定位 QQ 原生文件:\n * L0 → L1 → L2 依次尝试,全部失败且 autoDownload 时自动进入下载流程(ensureQqFiles)。\n * ⚠️ async:自动下载为异步流程(P1 起);纯定位场景用 resolveQqInstall(同步)。\n */\nexport async function resolveQqFiles(options: ResolveQqFilesOptions = {}): Promise<QqInstallInfo> {\n // L0:显式文件根(参数 > 环境变量)\n const explicitDir = options.qqFilesDir ?? process.env[\"NAPUTO_QQ_FILES\"];\n if (explicitDir !== undefined && explicitDir !== \"\") {\n return resolveFromRoot(resolve(explicitDir), \"cached\");\n }\n // L1:本机安装(参数 > 环境变量 > 注册表/常见路径)\n const qqPath = options.qqPath ?? process.env[\"NAPUTO_QQ_PATH\"];\n if (qqPath !== undefined && qqPath !== \"\") {\n return resolveQqInstall(qqPath);\n }\n try {\n return resolveQqInstall();\n } catch {\n // 本机未安装 → 落 L2\n }\n // L2:数据根缓存(P1 下载解包产物;结构 <数据根>/qq-files/<版本>/)\n const dataRoot = resolveDataRoot(options.dataRoot);\n const cached = tryResolveCached(dataRoot);\n if (cached !== null) {\n return cached;\n }\n // 全部缺失 → 自动下载(P1)\n if (options.autoDownload !== false) {\n const sevenZipPath =\n options.sevenZipPath !== undefined ? { sevenZipPath: options.sevenZipPath } : {};\n return ensureQqFiles({\n dataRoot,\n ...sevenZipPath,\n ...(options.onStage !== undefined ? { onStage: options.onStage } : {}),\n });\n }\n throw new Error(\n \"未找到 QQ 原生文件(wrapper.node):请安装 QQ、设置 NAPUTO_QQ_PATH/NAPUTO_QQ_FILES,\" +\n \"或使用下载流程(ensureQqFiles)自动获取\",\n );\n}\n\n/** 扫描数据根缓存(L2);命中返回,未命中返回 null。 */\nfunction tryResolveCached(dataRoot: string): QqInstallInfo | null {\n const cacheRoot = join(dataRoot, QQ_FILES_DIR_NAME);\n if (!existsSync(cacheRoot)) {\n return null;\n }\n const versionDirs = readdirSync(cacheRoot)\n .filter((v) => statSync(join(cacheRoot, v)).isDirectory())\n .sort()\n .reverse();\n for (const versionDir of versionDirs) {\n try {\n return resolveFromRoot(join(cacheRoot, versionDir), \"cached\");\n } catch {\n // 该版本目录结构不完整(如下载中断残留),尝试下一个\n }\n }\n return null;\n}\n\n/**\n * 7-Zip 官方 Linux 版下载地址(纯函数,可单测)。\n * 环境变量 NAPUTO_7Z_URL 可覆盖(同 QQ 下载 NAPUTO_QQ_URL 模式)。\n */\nexport function linuxSevenZipUrl(): string {\n return (\n process.env[\"NAPUTO_7Z_URL\"] ??\n `https://www.7-zip.org/a/7z${SEVEN_ZIP_LINUX_VERSION}-linux-x64.tar.xz`\n );\n}\n\n/**\n * 确保 Linux 7zz 就绪(治本:内置资产优先,下载兜底)。\n *\n * 优先级:内置资产 assets/7zip/7zz(2026-08-14 治本——与 Windows 7z.exe 同模式,\n * 7zz 静态二进制已打进发布包,LGPL 合规可分发,零运行时下载)> 数据根缓存\n * runtime/7zip/7zz(历史下载产物复用)> 官网下载(兜底,NAPUTO_7Z_URL 可覆盖)。\n *\n * 背景:7-Zip 官网只保留最新版 tar.xz,硬编码旧版本号(如 2409)会被删档 404,\n * 运行时下载既脆弱又依赖外网。内置资产彻底消灭该依赖。\n */\nexport async function ensureLinuxSevenZip(\n options: { dataRoot?: string } = {},\n): Promise<{ exe: string; source: string }> {\n // 1. 内置资产(治本):dist 与 assets 同层(tsdown 构建后 dist/,资产在包根 assets/)\n const bundled = join(dirname(fileURLToPath(import.meta.url)), \"..\", \"assets\", \"7zip\", \"7zz\");\n if (existsSync(bundled)) {\n // npm 安装可能丢失执行位,保险设置(失败不阻塞:execFile 报 EACCES 时由调用方提示)\n await chmod(bundled, 0o755).catch(() => {\n // 忽略:无执行位场景后续解包会明确报错,此处不静默吞掉主流程\n });\n return { exe: bundled, source: \"内置资产 assets/7zip\" };\n }\n\n // 2. 数据根缓存(历史下载产物复用)\n const dataRoot = resolveDataRoot(options.dataRoot);\n const cacheDir = join(dataRoot, \"runtime\", \"7zip\");\n const exe = join(cacheDir, \"7zz\");\n if (existsSync(exe)) {\n return { exe, source: \"数据根缓存 runtime/7zip\" };\n }\n\n // 3. 官网下载兜底(NAPUTO_7Z_URL 覆盖;仅当发布包缺失旧形态时触发)\n const archive = join(cacheDir, `7z${SEVEN_ZIP_LINUX_VERSION}-linux-x64.tar.xz`);\n mkdirSync(cacheDir, { recursive: true });\n await downloadFile({ dest: archive, url: linuxSevenZipUrl() });\n try {\n // tar -xJf 解压(Linux 自带 tar;7-Zip 官方 tar.xz 根目录含 7zz)\n try {\n execFileSync(\"tar\", [\"-xJf\", archive, \"-C\", cacheDir], { stdio: \"pipe\" });\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n throw new Error(\n `7z linux 解压失败: ${message}(需 tar + xz-utils;或设 NAPUTO_7Z_PATH 指定系统 7z)`,\n );\n }\n } finally {\n await rm(archive, { force: true });\n }\n if (!existsSync(exe)) {\n throw new Error(`7z linux 解压后未找到 7zz: ${exe}`);\n }\n await chmod(exe, 0o755); // 官方 tar.xz 内已有执行位,保险设置\n return { exe, source: \"自动下载(7-Zip 官方 linux 版)\" };\n}\n\n/**\n * 确保 QQ 原生文件就绪(P1):幂等缓存检查 → 下载官方安装包 → sha256 校验\n * → 7z 解包 → 提取 wrapper.node/QQNT.dll → 缓存。返回缓存版本 QqInstallInfo。\n *\n * onStage:阶段回调(下载/解包等提示,2026-08-23 起——下载 313MB 全程静默\n * 的坑)。调用方接 logger.info。\n */\nexport async function ensureQqFiles(\n options: {\n /** 数据根(缓存 <数据根>/qq-files/)。 */\n dataRoot?: string;\n /** 7z 路径(缺省自动探测)。 */\n sevenZipPath?: string;\n /** 阶段回调(下载/校验/解包/提取,接 logger.info)。 */\n onStage?: (message: string) => void;\n } = {},\n): Promise<QqInstallInfo> {\n const dataRoot = resolveDataRoot(options.dataRoot);\n const cacheRoot = join(dataRoot, QQ_FILES_DIR_NAME);\n mkdirSync(cacheRoot, { recursive: true });\n\n // 幂等:缓存已有完整版本 → 直接返回\n const existing = tryResolveCached(dataRoot);\n if (existing !== null) {\n options.onStage?.(`QQ 原生文件缓存命中:版本 ${existing.version}`);\n return existing;\n }\n\n // 1. 版本清单 → 最新可用版本\n const releases = loadQqReleases();\n const release = latestRelease(releases);\n const url = resolveDownloadUrl(release);\n const version = release.version;\n\n // 2. 下载 + sha256 校验(清单无参考值时下载器跳过校验,完整性由解包/加载兜底)\n const tmpDir = join(dataRoot, \"tmp\");\n mkdirSync(tmpDir, { recursive: true });\n // ⚠️ tmp 文件名唯一化(2026-08-23 WSL 事故):固定名 qq-installer-<v>.exe 在\n // 多实例/重试并发时互相覆盖,且一个实例 finally 清理会删掉另一个实例刚下载\n // 的文件——表现就是「下载成功但 7z 解包报 No such file or directory」。\n // 加 pid + 时间戳后缀,实例间天然隔离。\n const installerPath = join(\n tmpDir,\n `qq-installer-${version}-${process.pid}-${Date.now().toString(36)}.exe`,\n );\n options.onStage?.(`下载 QQ 官方安装包 ${version}(首次使用需下载,约 300MB)…`);\n await downloadFile({ dest: installerPath, url, expectedSha256: release.sha256 });\n options.onStage?.(`QQ 安装包下载完成,sha256 校验通过(${release.sha256.slice(0, 12)}…)`);\n\n // 3. 解包 + 提取(失败清理缓存残留;临时文件无论如何清理)\n // Linux 分支:确保 7zz(自动下载 7-Zip 官方 linux 版,2026-08-14 生产修复——\n // 此前依赖系统 p7zip-full,未安装则 7z 解包失败、QQ 文件无法就绪)\n let sevenZipPath = options.sevenZipPath;\n if (sevenZipPath === undefined && process.platform === \"linux\") {\n sevenZipPath = (await ensureLinuxSevenZip({ dataRoot })).exe;\n }\n const extractedDir = join(tmpDir, `qq-extracted-${version}-${process.pid}`);\n try {\n // 解包前再次确认安装包真实存在(downloadFile 已 stat 兜底;防御并发清理竞态)\n if (!existsSync(installerPath)) {\n throw new Error(\n `下载产物在解包前丢失: ${installerPath}(疑为并发实例清理 tmp,` +\n \"已用唯一文件名规避,请重试)\",\n );\n }\n options.onStage?.(`7z 解包安装包(${version},需数分钟)…`);\n await extractInstaller(installerPath, extractedDir, sevenZipPath);\n options.onStage?.(\"提取 wrapper.node 及原生依赖 DLL…\");\n const info = await extractWrapperFiles(extractedDir, version, cacheRoot);\n options.onStage?.(`QQ 原生文件就绪:缓存版本 ${info.version}`);\n return info;\n } catch (err) {\n await clearCacheVersion(cacheRoot, version);\n throw err;\n } finally {\n await rm(installerPath, { force: true });\n await rm(extractedDir, { recursive: true, force: true });\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["process","process","process","packageRoot","process","process","process","process","process","process"],"sources":["../src/host/ipc/ipc-types.ts","../src/host/ipc/native-noise.ts","../src/pid-cmdline.ts","../src/instance-lock.ts","../src/data-root.ts","../src/package-info.ts","../src/qq-download.ts","../src/qq-extract.ts","../src/win-node.ts","../src/wine.ts","../src/launcher.ts","../src/qq-releases.ts","../src/locate-qq.ts"],"sourcesContent":["/**\n * ipc-types.ts:IPC 协议消息(koishi 插件 ↔ self-host 子进程,单一来源)。\n *\n * 协议契约(koishi 插件 design.md §5.6):`{ v, type, id?, payload }` JSON 行。\n * 用 zod v4 定义单一 schema,两侧(loader host + koishi 插件)import 同一份:\n * 类型经 z.infer 派生、运行时经 safeParse 校验,消除「两份手写 types.ts」漂移。\n *\n * 领域字段(LoginState / SelfInfo / QrCodeData)与 kernel 同名类型结构等价,\n * 在协议边界内联为 zod schema(kernel 不依赖 zod,故不在 kernel 定义)。\n */\nimport { z } from \"zod\";\n\n/** IPC 协议版本(解码校验)。 */\nexport const IPC_VERSION = 1;\n\n/** 引导阶段(status 消息)。 */\nexport const IpcBootPhaseSchema = z.enum([\n \"booting\", // 子进程已启动(spawn 成功)\n \"dlopening\", // dlopen wrapper.node\n \"logging\", // 登录流程进行中\n \"sessioning\", // session 初始化中\n \"ready\", // session READY + 业务 service 装配完成\n \"failed\", // 引导失败(携带错误)\n]);\n\n/** 结构化日志级别(pino level)。 */\nexport const IpcLogLevelSchema = z.enum([\"trace\", \"debug\", \"info\", \"warn\", \"error\", \"fatal\"]);\n\n/** 登录状态(与 kernel LoginState 结构等价)。 */\nconst LoginStateSchema = z.enum([\"idle\", \"waiting_scan\", \"scanned\", \"logged_in\", \"failed\"]);\n\n/** selfInfo(与 kernel SelfInfo 结构等价)。 */\nconst SelfInfoSchema = z.object({\n uin: z.string(),\n uid: z.string(),\n nick: z.string(),\n});\n\n/** status 消息 payload。 */\nexport const IpcStatusPayloadSchema = z.object({\n phase: IpcBootPhaseSchema,\n message: z.string().optional(),\n error: z.object({ code: z.string(), message: z.string() }).optional(),\n});\n\n/** login 消息 payload。 */\nexport const IpcLoginPayloadSchema = z.object({\n state: LoginStateSchema,\n selfInfo: SelfInfoSchema.optional(),\n /** 失败原因(state=failed 时;如「登录超时,请刷新页面重试」)。 */\n message: z.string().optional(),\n});\n\n/** event 消息 payload(kernel 事件通道形状,翻译层按 service/name 断言具体类型)。 */\nexport const IpcEventPayloadSchema = z.object({\n service: z.string(),\n name: z.string(),\n args: z.array(z.unknown()),\n});\n\n/** log 消息 payload。 */\nexport const IpcLogPayloadSchema = z.object({\n level: IpcLogLevelSchema,\n message: z.string(),\n});\n\n/** 动作响应 payload(result 消息)。 */\nexport const IpcResultPayloadSchema = z.union([\n z.object({ ok: z.literal(true), value: z.unknown().optional() }),\n z.object({ ok: z.literal(false), error: z.object({ code: z.string(), message: z.string() }) }),\n]);\n\n/** 动作请求 payload(action 消息)。 */\nexport const IpcActionPayloadSchema = z.object({\n action: z.string(),\n params: z.record(z.string(), z.unknown()).optional(),\n});\n\n/** 控制指令 payload(control 消息,父→子)。 */\nexport const IpcControlPayloadSchema = z.union([\n z.object({ command: z.literal(\"stop\") }), // 优雅退出\n z.object({ command: z.literal(\"restart\") }), // 退出并期望驱动层重启\n z.object({\n command: z.literal(\"login\"),\n uin: z.string().optional(),\n qr: z.boolean().optional(),\n }), // 触发登录(uin 指定账号 / qr=true 强制扫码;登录期成功结果抢占初始登录竞速,2026-09-08)\n z.object({ command: z.literal(\"status\") }), // 状态查询:重播最近一条 status(丢失重同步,2026-09-06)\n]);\n\n// ── 消息联合 ──\n\nconst IpcStatusMessageSchema = z.object({\n v: z.literal(IPC_VERSION),\n type: z.literal(\"status\"),\n payload: IpcStatusPayloadSchema,\n});\n\nconst IpcLoginMessageSchema = z.object({\n v: z.literal(IPC_VERSION),\n type: z.literal(\"login\"),\n payload: IpcLoginPayloadSchema,\n});\n\nconst IpcQrMessageSchema = z.object({\n v: z.literal(IPC_VERSION),\n type: z.literal(\"qr\"),\n payload: z.object({ pngBase64: z.string(), qrcodeUrl: z.string() }),\n});\n\nconst IpcEventMessageSchema = z.object({\n v: z.literal(IPC_VERSION),\n type: z.literal(\"event\"),\n payload: IpcEventPayloadSchema,\n});\n\nconst IpcResultMessageSchema = z.object({\n v: z.literal(IPC_VERSION),\n type: z.literal(\"result\"),\n id: z.number(),\n payload: IpcResultPayloadSchema,\n});\n\nconst IpcLogMessageSchema = z.object({\n v: z.literal(IPC_VERSION),\n type: z.literal(\"log\"),\n payload: IpcLogPayloadSchema,\n});\n\nconst IpcPingMessageSchema = z.object({\n v: z.literal(IPC_VERSION),\n type: z.literal(\"ping\"),\n});\n\nconst IpcPongMessageSchema = z.object({\n v: z.literal(IPC_VERSION),\n type: z.literal(\"pong\"),\n});\n\nconst IpcActionMessageSchema = z.object({\n v: z.literal(IPC_VERSION),\n type: z.literal(\"action\"),\n id: z.number(),\n payload: IpcActionPayloadSchema,\n});\n\nconst IpcControlMessageSchema = z.object({\n v: z.literal(IPC_VERSION),\n type: z.literal(\"control\"),\n payload: IpcControlPayloadSchema,\n});\n\n/** IPC 消息判别联合(协议边界,运行时经 safeParse 校验)。 */\nexport const IpcMessageSchema = z.discriminatedUnion(\"type\", [\n IpcStatusMessageSchema,\n IpcLoginMessageSchema,\n IpcQrMessageSchema,\n IpcEventMessageSchema,\n IpcResultMessageSchema,\n IpcLogMessageSchema,\n IpcPingMessageSchema,\n IpcPongMessageSchema,\n IpcActionMessageSchema,\n IpcControlMessageSchema,\n]);\n\n// ── 派生类型(z.infer,两侧消费同名类型,无镜像漂移) ──\n\nexport type IpcBootPhase = z.infer<typeof IpcBootPhaseSchema>;\nexport type IpcLogLevel = z.infer<typeof IpcLogLevelSchema>;\nexport type IpcStatusPayload = z.infer<typeof IpcStatusPayloadSchema>;\nexport type IpcLoginPayload = z.infer<typeof IpcLoginPayloadSchema>;\nexport type IpcEventPayload = z.infer<typeof IpcEventPayloadSchema>;\nexport type IpcLogPayload = z.infer<typeof IpcLogPayloadSchema>;\nexport type IpcResultPayload = z.infer<typeof IpcResultPayloadSchema>;\nexport type IpcActionPayload = z.infer<typeof IpcActionPayloadSchema>;\nexport type IpcControlPayload = z.infer<typeof IpcControlPayloadSchema>;\nexport type IpcStatusMessage = z.infer<typeof IpcStatusMessageSchema>;\nexport type IpcLoginMessage = z.infer<typeof IpcLoginMessageSchema>;\nexport type IpcQrMessage = z.infer<typeof IpcQrMessageSchema>;\nexport type IpcEventMessage = z.infer<typeof IpcEventMessageSchema>;\nexport type IpcResultMessage = z.infer<typeof IpcResultMessageSchema>;\nexport type IpcLogMessage = z.infer<typeof IpcLogMessageSchema>;\nexport type IpcPingMessage = z.infer<typeof IpcPingMessageSchema>;\nexport type IpcPongMessage = z.infer<typeof IpcPongMessageSchema>;\nexport type IpcActionMessage = z.infer<typeof IpcActionMessageSchema>;\nexport type IpcControlMessage = z.infer<typeof IpcControlMessageSchema>;\nexport type IpcMessage = z.infer<typeof IpcMessageSchema>;\n","/**\n * native-noise.ts:wrapper 原生噪音行判定(cli 转发与 koishi IPC 两条路径共享单一来源)。\n *\n * wrapper.node 加载后原生 printf 直写 fd 的 C++ 日志,JS 层无法拦截,只能在消费端\n * 逐行过滤:\n * - `<MMKV` / `<MemoryFile_Win32` / `<MMKV_IO`:MMKV 存储库刷屏(每次初始化打 ~6 行)\n * - `loadSymbolFromShell` / `getNodeGetJsListApi` / `get symbol failed`:\n * 标准 node 无腾讯私有符号(NodeContextifyContextMetrics 等),GetProcAddress\n * 失败的加载警告(无害,纯噪音)\n * - `loaded [mmkv.*] with N key-values`:MMKV 初始化完成行(野生日志,风格三,\n * 无统一前缀/时间戳,过滤)\n *\n * 边界:只处理「整条」噪音行;原生输出插进 JSON 协议行字节中间的撕裂不归这里管\n * (IPC 路径由 status 重播与脏行诊断兜底,见 ipc-sender.ts / koishi 插件 client)。\n */\nimport { IPC_VERSION } from \"./ipc-types.js\";\n\nconst NATIVE_NOISE =\n /<MMKV|<MemoryFile_Win32|<MMKV_IO|loadSymbolFromShell|getNodeGetJsListApi|get symbol failed|loaded \\[mmkv/i;\n\n/** IPC 协议行前缀(encodeIpcMessage 走 JSON.stringify 无空白,前缀恒定)。 */\nconst IPC_LINE_PREFIX = `{\"v\":${IPC_VERSION}`;\n\n/**\n * 判定一行子进程输出是否为 wrapper 原生噪音(命中即整行静默丢弃,不进协议解析)。\n *\n * IPC 协议行构造性豁免:载荷可能嵌入噪音样文本(如用户消息含 `<MMKV>` 字样),\n * 以协议前缀开头的行永不判噪——过滤与协议必须无交集,防误滤丢消息事件。\n */\nexport function isNativeNoiseLine(line: string): boolean {\n if (line.startsWith(IPC_LINE_PREFIX)) {\n return false;\n }\n return NATIVE_NOISE.test(line);\n}\n","/**\n * pid-cmdline.ts:跨进程命令行查询(instance-lock pid 复用根治,2026-09-10)。\n *\n * 背景(2026-09-08 实测事故):锁内 pid 的持有者已死、pid 被 Windows 复用给\n * 无关进程时,process.kill(pid, 0) 探活成功 → 误判存活 → 新实例永久拒绝启动\n * (只能手动删锁恢复)。根治:探活通过后再查该 pid 当前的真实命令行,与锁内\n * cmdline 摘要比对——查不到(进程已退/查询失败)或不一致(pid 复用)均视为\n * 持有者已死,允许接管。误判方向取舍:误判「死亡」→ 双实例抢 QQ 原生锁,\n * 后启动者挂起退出(可恢复);误判「存活」→ 永久锁死需人工干预——前者代价\n * 远低,故查询失败宁可接管。\n *\n * 平台实现:Windows 走 PowerShell Get-CimInstance Win32_Process 跨进程查询\n * (仅在启动判活时调用一次,冷启动 ~0.5-2s 可接受;wmic 在新 Win11 已移除,\n * 不可用)。Linux 走 /proc/<pid>/cmdline(\\0 分隔)。其余平台(darwin 等)\n * 不支持——判活退回纯 pid 探测(旧行为)。\n *\n * 比对不要求逐字符相等:锁内摘要来自 process.execArgv/argv join,查询侧\n * 拿到的是操作系统记录的原始命令行,两者存在引号 / 大小写 / 路径分隔符差异\n * ——统一经 normalizeCmdline 规范化后比对。残余差异源(8.3 短路径、相对\n * 启动路径)极罕见,误判后果同上(可恢复),接受。\n */\n\nimport { execFileSync } from \"node:child_process\";\nimport { readFileSync } from \"node:fs\";\nimport process from \"node:process\";\n\n/** 当前平台是否支持跨进程 cmdline 查询(不支持则判活退回纯 pid 探测)。 */\nexport const CMDLINE_PROBE_SUPPORTED = process.platform === \"win32\" || process.platform === \"linux\";\n\n/**\n * 查询指定 pid 的当前命令行。\n *\n * @returns 命令行原文;进程不存在 / 命令行不可得(权限、系统进程 null)/\n * 查询机制失败(超时、PowerShell 缺失)均返回 null(调用方视为持有者已死)\n */\nexport function readCmdlineForPid(pid: number): string | null {\n if (process.platform === \"win32\") {\n return readWindowsCmdline(pid);\n }\n if (process.platform === \"linux\") {\n return readLinuxCmdline(pid);\n }\n return null;\n}\n\n/** Windows:PowerShell Get-CimInstance 查询(进程不存在时输出空 → null)。 */\nfunction readWindowsCmdline(pid: number): string | null {\n try {\n const out = execFileSync(\n \"powershell.exe\",\n [\n \"-NoProfile\",\n \"-NonInteractive\",\n \"-ExecutionPolicy\",\n \"Bypass\",\n \"-Command\",\n // UTF-8 输出避免中文路径被 OEM 代码页转码\n `[Console]::OutputEncoding=[System.Text.Encoding]::UTF8;` +\n `(Get-CimInstance Win32_Process -Filter 'ProcessId=${pid}').CommandLine`,\n ],\n { encoding: \"utf-8\", timeout: 10_000, windowsHide: true },\n );\n const cmdline = out.trim();\n return cmdline === \"\" ? null : cmdline;\n } catch {\n return null;\n }\n}\n\n/** Linux:/proc/<pid>/cmdline(\\0 分隔转空格;进程不存在抛错 → null)。 */\nfunction readLinuxCmdline(pid: number): string | null {\n try {\n const raw = readFileSync(`/proc/${pid}/cmdline`, \"utf-8\");\n const cmdline = raw.replaceAll(\"\\0\", \" \").trim();\n return cmdline === \"\" ? null : cmdline;\n } catch {\n return null;\n }\n}\n\n/**\n * 命令行规范化(比对用):统一大小写 / 去引号 / 路径分隔符归一 / 折叠空白。\n * Windows 路径大小写不敏感,lowercase 安全。\n */\nexport function normalizeCmdline(cmdline: string): string {\n return cmdline\n .toLowerCase()\n .replaceAll('\"', \"\")\n .replaceAll(\"\\\\\", \"/\")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\n/**\n * 本进程命令行摘要(写锁用):execArgv(node 选项)+ argv(脚本与参数),\n * 与操作系统记录的完整命令行结构一致(规范化后可比对)。\n */\nexport function selfCmdline(): string {\n return [...process.execArgv, ...process.argv].join(\" \").trim();\n}\n","/**\n * instance-lock.ts:单实例锁(2026-08-07 根治「多实例抢数据目录锁挂起」)\n *\n * 背景(实测):同一账号数据目录(~/.napuketto/<uin>)只能被一个实例持有——\n * QQ 原生层(MMKV / 登录单例 / 数据库)有锁,第二个实例抢不到锁会卡在\n * loginService.initConfig 之后无响应(挂起)。此前 create-napukettoqq 生成的\n * 副本实例常驻占锁,导致用户后续启动全部卡死。\n *\n * 方案:以数据目录为粒度加锁文件 `<dataDir>/instance.lock`(JSON 内容:\n * pid / startedAt / cmdline 摘要)。启动前 checkInstanceLock:\n * - 锁不存在 → 可启动(然后 acquireInstanceLock 写入)\n * - 锁存在但 PID 已死 → 崩溃残留,自动清理(rm + 重新获取)\n * - 锁存在且 PID 存活 → 已被占用,拒绝启动(返回占用 PID 供提示)\n *\n * PID 复用根治(2026-09-10):PID 探活通过后再比对锁内 cmdline 摘要与该\n * PID 当前真实命令行(pid-cmdline.ts)——查不到或不一致 → PID 已被无关\n * 进程复用(持有者已死),视为残留允许接管。旧锁(无 cmdline 字段)无法\n * 校验,保守维持「占用」判定(一次性过渡:新锁写入时自动带摘要)。详见\n * pid-cmdline.ts 头注释的误判代价取舍。\n *\n * 退出清理:进程正常退出时 releaseInstanceLock 删除锁文件;崩溃/强杀残留\n * 由下次启动的「PID 存活检测」兜底(PID 已死 → 残留视为过期)。\n *\n * 放置说明:放 loader 而非 kernel——self-host.cjs 是 rolldown 内联 bundle\n * (只内联 src/host/ 依赖树),静态 import kernel 会留 external 缺口;\n * 放 loader 则 self-host(内联)与 cli(@napuketto/loader 导入)双入口可用。\n */\n\nimport { readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport process from \"node:process\";\nimport {\n CMDLINE_PROBE_SUPPORTED,\n normalizeCmdline,\n readCmdlineForPid,\n selfCmdline,\n} from \"./pid-cmdline.js\";\n\n/** 锁文件名(置于数据目录内)。 */\nexport const INSTANCE_LOCK_FILE = \"instance.lock\";\n\n/** 锁文件内容。 */\nexport interface InstanceLockInfo {\n /** 持有实例的进程 PID。 */\n pid: number;\n /** 获取锁的时间(epoch ms)。 */\n startedAt: number;\n /** 命令行摘要(诊断用,可选)。 */\n cmdline?: string;\n}\n\n/** 数据目录锁检查结果。 */\nexport interface InstanceLockCheck {\n /** 是否被其他实例占用。 */\n occupied: boolean;\n /** 占用进程 PID(occupied=true 时有效)。 */\n pid: number | null;\n /** 锁文件完整路径(提示/清理用)。 */\n lockPath: string;\n}\n\n/**\n * 检测指定数据目录是否已被其他实例占用。\n *\n * @param dataDir 数据目录(账号级,如 ~/.napuketto/3567141148)\n */\nexport function checkInstanceLock(dataDir: string): InstanceLockCheck {\n const lockPath = join(dataDir, INSTANCE_LOCK_FILE);\n let info: InstanceLockInfo | null = null;\n try {\n info = JSON.parse(readFileSync(lockPath, \"utf-8\")) as InstanceLockInfo;\n } catch {\n // 文件不存在 / 内容损坏 → 视为可启动(损坏锁由 acquire 覆盖)\n return { occupied: false, pid: null, lockPath };\n }\n if (typeof info.pid !== \"number\" || Number.isNaN(info.pid) || info.pid <= 0) {\n return { occupied: false, pid: null, lockPath };\n }\n // PID 存活判定(Windows 下 process.kill(pid, 0) 探测)\n let alive = false;\n try {\n process.kill(info.pid, 0);\n alive = true;\n } catch {\n alive = false;\n }\n if (!alive) {\n // 崩溃残留:持有者已死,可安全接管\n return { occupied: false, pid: null, lockPath };\n }\n // PID 复用校验(2026-09-10):探活成功≠持有者存活——pid 可能已被无关进程\n // 复用。锁内有 cmdline 摘要且平台支持查询时,比对当前真实命令行:\n // - 查不到(进程刚退/查询失败)→ 视为死亡,接管(误判代价远低于锁死)\n // - 不一致 → pid 复用,接管\n // 旧锁无摘要 / 平台不支持 → 无法校验,保守维持占用(见头注释)。\n const lockCmdline =\n typeof info.cmdline === \"string\" && info.cmdline !== \"\" ? info.cmdline : undefined;\n if (lockCmdline !== undefined && CMDLINE_PROBE_SUPPORTED) {\n const current = readCmdlineForPid(info.pid);\n if (current === null || normalizeCmdline(current) !== normalizeCmdline(lockCmdline)) {\n return { occupied: false, pid: null, lockPath };\n }\n }\n return { occupied: true, pid: info.pid, lockPath };\n}\n\n/**\n * 获取数据目录锁。占用(其他实例存活)返回 false;成功或清理残留后返回 true。\n * 幂等:本进程已持有则直接返回 true(不重复写)。\n */\nexport function acquireInstanceLock(dataDir: string, cmdline?: string): boolean {\n const { occupied, pid, lockPath } = checkInstanceLock(dataDir);\n if (occupied) {\n // 已占用:除非占用者是自己(PID 复用保护),否则拒绝\n if (pid === process.pid) {\n return true;\n }\n return false;\n }\n // 残留锁(持有者已死 / pid 复用 / 无锁):写入。\n // cmdline 摘要缺省自动填本进程命令行(调用方无需显式传,全部消费点自动受益)\n const effectiveCmdline = cmdline !== undefined && cmdline !== \"\" ? cmdline : selfCmdline();\n const info: InstanceLockInfo = {\n pid: process.pid,\n startedAt: Date.now(),\n ...(effectiveCmdline !== \"\" ? { cmdline: effectiveCmdline } : {}),\n };\n try {\n rmSync(lockPath, { force: true });\n writeFileSync(lockPath, JSON.stringify(info), \"utf-8\");\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * 释放数据目录锁(正常退出时调用)。仅当持有者是本进程时才删除,\n * 避免误删新持有者的锁(PID 复用 / 快速重启场景)。\n */\nexport function releaseInstanceLock(dataDir: string): void {\n const lockPath = join(dataDir, INSTANCE_LOCK_FILE);\n try {\n const info = JSON.parse(readFileSync(lockPath, \"utf-8\")) as InstanceLockInfo;\n if (info.pid === process.pid) {\n rmSync(lockPath, { force: true });\n }\n } catch {\n // 文件不存在 / 内容损坏 → 无需清理\n }\n}\n\n/** 注册进程退出清理(SIGINT/SIGTERM/exit 均触发;崩溃强杀由残留检测兜底)。 */\nexport function registerLockCleanup(dataDir: string): void {\n process.on(\"exit\", () => {\n releaseInstanceLock(dataDir);\n });\n}\n","/**\n * data-root.ts:轻量数据根解析(loader 侧,避免新增 kernel 依赖)。\n *\n * 从 locate-qq.ts / win-node.ts 抽出的同构逻辑(2026-08 消重:两处\n * 22 行重复 resolveDataRoot + findProjectRoot)。\n *\n * 优先级:显式参数 > NAPKETTO_DATA > 项目根/.napuketto > cwd 兜底。\n * P2 若需完整语义(同 kernel resolveDataRoot)再切换。\n */\nimport { existsSync } from \"node:fs\";\nimport { dirname, join, resolve } from \"node:path\";\nimport process from \"node:process\";\n\n/** 解析数据根(NAPKETTO_DATA 显式 > 项目根/.napuketto > cwd 兜底)。 */\nexport function resolveDataRoot(dataRoot?: string): string {\n const explicit = dataRoot ?? process.env[\"NAPKETTO_DATA\"];\n if (explicit !== undefined && explicit !== \"\") {\n return resolve(explicit);\n }\n const projectRoot = findProjectRoot(process.cwd()) ?? process.cwd();\n return join(projectRoot, \".napuketto\");\n}\n\n/** 向上探测项目根(含 pnpm-workspace.yaml 或 package.json 的目录)。 */\nfunction findProjectRoot(start: string): string | null {\n let dir = resolve(start);\n for (;;) {\n if (existsSync(join(dir, \"pnpm-workspace.yaml\")) || existsSync(join(dir, \"package.json\"))) {\n return dir;\n }\n const parent = dirname(dir);\n if (parent === dir) {\n return null;\n }\n dir = parent;\n }\n}\n","/**\n * package-info.ts:运行时读取本包 package.json 元数据。\n *\n * ESM 下用 import.meta.url 定位包根(dist/ 与包根同层:dist/index.mjs → ../package.json);\n * CJS 输出由 rolldown 将 import.meta.url 转为 pathToFileURL(__filename) 等价形式,\n * 定位行为不变(设计文档 §发布形态)。\n */\nimport { readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\n/** 包根路径(dist 与包根同层:dist/index.mjs → ../package.json)。 */\nfunction packageRoot(): string {\n return dirname(dirname(fileURLToPath(import.meta.url)));\n}\n\n/**\n * 读取本包版本号;失败(发布产物缺 package.json / 解析异常)兜底 \"0.0.0\"。\n * 仅用于下载 User-Agent 等非关键元数据,失败不阻塞主流程。\n */\nexport function loaderVersion(): string {\n try {\n const raw = readFileSync(join(packageRoot(), \"package.json\"), \"utf8\");\n const parsed = JSON.parse(raw) as { version?: unknown };\n if (typeof parsed.version === \"string\" && parsed.version !== \"\") {\n return parsed.version;\n }\n } catch {\n // 读取/解析失败——仅影响 UA 与日志标识,兜底即可\n }\n return \"0.0.0\";\n}\n","/**\n * qq-download.ts:QQ 官方安装包下载器(P1)。\n *\n * 无第三方依赖:Node 内置 https + crypto(sha256)。设计文档 §2.2。\n * - https GET 流式写文件(低内存占用,313MB 安装包不整载入内存)\n * - 下载同时流式累加 sha256,完成后与清单比对(防链接漂移劫持 / 下载不完整)\n * - 支持 302 重定向(QQ CDN 可能跳转)\n * - NAPUTO_QQ_URL 环境变量可覆盖下载地址(用户拿到新链接时用)\n */\nimport { createHash } from \"node:crypto\";\nimport { createWriteStream, mkdirSync, statSync } from \"node:fs\";\nimport { rm } from \"node:fs/promises\";\nimport type { IncomingMessage } from \"node:http\";\nimport { get, type RequestOptions } from \"node:https\";\nimport { dirname } from \"node:path\";\nimport process from \"node:process\";\nimport { loaderVersion } from \"./package-info.js\";\n\n/** 下载请求 User-Agent(运行时读本包版本,失败兜底 0.0.0)。 */\nconst USER_AGENT = `napuketto-loader/${loaderVersion()}`;\n\n/** 下载选项。 */\nexport interface DownloadOptions {\n /** 目标文件绝对路径。 */\n dest: string;\n /** 期望 sha256(十六进制小写);缺省/空串 = 不校验(首次下载尚无参考值)。 */\n expectedSha256?: string;\n /** 覆盖下载地址(缺省读 NAPUTO_QQ_URL)。 */\n url?: string;\n /** 超时(毫秒,默认 60s 无数据则中断)。 */\n timeoutMs?: number;\n}\n\n/** 下载结果。 */\nexport interface DownloadResult {\n /** 实际 sha256(十六进制小写)。 */\n sha256: string;\n /** 是否通过期望值校验(无期望值时恒 true)。 */\n verified: boolean;\n}\n\n/** 下载中断(超时 / 非 2xx / 网络错误)抛出的错误。 */\nexport class DownloadError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"DownloadError\";\n }\n}\n\n/** 发起一次 https GET,返回响应(跟随重定向,最多 5 跳)。 */\nfunction httpsGet(url: string, timeoutMs: number): Promise<IncomingMessage> {\n return new Promise((resolve, reject) => {\n const request = (currentUrl: string, redirects: number): void => {\n const opts: RequestOptions = {\n headers: { \"user-agent\": USER_AGENT },\n };\n const req = get(currentUrl, opts, (res) => {\n const status = res.statusCode ?? 0;\n if (status >= 300 && status < 400 && res.headers[\"location\"] !== undefined) {\n res.resume(); // 释放连接\n if (redirects >= 5) {\n reject(new DownloadError(`重定向次数过多: ${currentUrl}`));\n return;\n }\n request(new URL(res.headers[\"location\"], currentUrl).toString(), redirects + 1);\n return;\n }\n if (status !== 200) {\n res.resume();\n reject(new DownloadError(`HTTP ${status}: ${currentUrl}`));\n return;\n }\n resolve(res);\n });\n req.setTimeout(timeoutMs, () => {\n req.destroy(new DownloadError(`下载超时(${timeoutMs}ms): ${currentUrl}`));\n });\n req.on(\"error\", (err) => {\n reject(err instanceof Error ? err : new DownloadError(String(err)));\n });\n };\n request(url, 0);\n });\n}\n\n/**\n * 下载文件并(可选)校验 sha256。失败时清理半成品文件。\n * 返回实际 sha256(无论是否校验),便于调用方回填清单。\n */\nexport async function downloadFile(options: DownloadOptions): Promise<DownloadResult> {\n const url = options.url ?? process.env[\"NAPUTO_QQ_URL\"];\n if (url === undefined || url === \"\") {\n throw new DownloadError(\"下载地址为空:请设置 NAPUTO_QQ_URL 或提供清单 URL\");\n }\n const timeoutMs = options.timeoutMs ?? 60_000;\n // 目标目录必须提前建好(createWriteStream 不自动建目录)\n mkdirSync(dirname(options.dest), { recursive: true });\n const hash = createHash(\"sha256\");\n const fileStream = createWriteStream(options.dest);\n let fileFailed = false;\n\n try {\n const res = await httpsGet(url, timeoutMs);\n await new Promise<void>((resolve, reject) => {\n res.on(\"data\", (chunk: Buffer) => {\n hash.update(chunk);\n });\n res.on(\"error\", (err: Error) => {\n fileFailed = true;\n reject(err instanceof Error ? err : new DownloadError(String(err)));\n });\n fileStream.on(\"error\", (err) => {\n fileFailed = true;\n reject(err);\n });\n fileStream.on(\"finish\", () => {\n resolve();\n });\n res.pipe(fileStream);\n });\n } catch (err) {\n fileStream.destroy();\n if (!fileFailed) {\n await rm(options.dest, { force: true });\n }\n throw err instanceof Error ? err : new DownloadError(String(err));\n }\n\n // ⚠️ 完成态校验(2026-08-23 WSL 生产事故):Promise 正常 resolve 但文件\n // 缺失/为空是真实发生过的(koishi 场景 7z 解包报「No such file or\n // directory」,疑为并发实例清理 tmp 或写入中断)。resolve 不代表落盘完整,\n // 这里 stat 兜底,缺失/空文件立即报错而不是把坏文件交给下游解包。\n let size: number;\n try {\n size = statSync(options.dest).size;\n } catch {\n throw new DownloadError(`下载完成但文件缺失: ${options.dest}`);\n }\n if (size <= 0) {\n await rm(options.dest, { force: true });\n throw new DownloadError(`下载完成但文件为空: ${options.dest}`);\n }\n\n const sha256 = hash.digest(\"hex\");\n const expected = options.expectedSha256;\n const verified =\n expected === undefined || expected === \"\"\n ? true\n : sha256.toLowerCase() === expected.toLowerCase();\n if (!verified) {\n await rm(options.dest, { force: true });\n throw new DownloadError(\n `sha256 校验失败: ${options.dest}\\n期望 ${expected}\\n实际 ${sha256}`,\n );\n }\n return { sha256, verified };\n}\n","/**\n * qq-extract.ts:QQ 官方安装包解包与提取(P1)。\n *\n * ⚠️ 格式事实(2026-08-12 实测修正):QQ 官方安装包是 NSIS 自解压格式,\n * **7za standalone 不支持 NSIS**(只支持 7z/zip/tar 等)——必须用完整版\n * 7z(7z.exe + 7z.dll)。设计文档 §2.3 原「内置 7za.exe」修正为:\n * - Windows:内置 assets/7zip/7z.exe + 7z.dll(7-Zip 官方,LGPL 合规可分发)\n * - Linux/Docker:系统 p7zip-full(7z 完整版)\n *\n * ⚠️ 提取策略(2026-08-12 实测修正,与设计 §2.3 不同):\n * 1. NSIS 解包产物在 <extracted>/Files/(NSIS 内部布局),Files/ = 安装根\n * 2. 真实 QQNT.dll(214MB)位于 versions/<v>/QQNT.dll,**自建宿主不需要**\n * ——stub QQNT.dll 已替代其宿主符号职责(wrapper.node 的 v8/node/napi/qq_magic\n * 符号由 stub 转发到 node.exe)\n * 3. 只提取 resources/app 顶层 *.node + *.dll(wrapper.node 的直接/传递/delay-load\n * 依赖全覆盖,版本无关;跳过 wmpfsdk/avsdk 等大子目录,共约 235MB)\n *\n * 提取目标(与安装目录同构,resolveFromRoot 可直接消费):\n * <数据根>/qq-files/<版本>/versions/<版本>/resources/app/ ← *.node + *.dll\n */\nimport { execFile } from \"node:child_process\";\nimport { existsSync } from \"node:fs\";\nimport { copyFile, mkdir, readdir, rm } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport process from \"node:process\";\nimport { fileURLToPath } from \"node:url\";\nimport type { QqInstallInfo } from \"./locate-qq.js\";\n\n/** resources/app 下需要提取的文件扩展名(.node 原生模块 + .dll 依赖)。 */\nconst EXTRACT_EXTENSIONS = new Set([\".node\", \".dll\"]);\n\n/** 7-Zip 可执行文件来源。 */\nexport interface SevenZipResult {\n /** 7z 可执行文件绝对路径。 */\n exe: string;\n /** 来源说明(内置资产 / 系统 / 环境变量)。 */\n source: string;\n}\n\n/** 提取选项。 */\nexport interface ExtractOptions {\n /** 安装包绝对路径。 */\n installerPath: string;\n /** 目标版本目录(如 9.9.33-51802)。 */\n version: string;\n /** 缓存根目录(<数据根>/qq-files;版本子目录将建在其下)。 */\n cacheRoot: string;\n /** 7z 可执行文件路径(缺省自动探测)。 */\n sevenZipPath?: string;\n}\n\n/**\n * 探测 7z 可执行文件:\n * NAPUTO_7Z_PATH 环境变量 > 内置资产(Windows assets/7zip/7z.exe、Linux\n * assets/7zip/7zz——2026-08-14 治本,静态二进制已打进发布包)> 系统 PATH 7z。\n */\nexport function findSevenZip(): SevenZipResult {\n const envPath = process.env[\"NAPUTO_7Z_PATH\"];\n if (envPath !== undefined && envPath !== \"\" && existsSync(envPath)) {\n return { exe: envPath, source: \"环境变量 NAPUTO_7Z_PATH\" };\n }\n // 内置资产:dist 与 assets 同层(tsdown 构建后 dist/,资产在包根 assets/)\n if (process.platform === \"win32\" || process.platform === \"linux\") {\n const exeName = process.platform === \"win32\" ? \"7z.exe\" : \"7zz\";\n const bundled = join(\n dirname(fileURLToPath(import.meta.url)),\n \"..\",\n \"assets\",\n \"7zip\",\n exeName,\n );\n if (existsSync(bundled)) {\n return { exe: bundled, source: \"内置资产 assets/7zip\" };\n }\n }\n // 系统 PATH(Windows 装了 7-Zip 有 7z.exe;Linux p7zip-full 提供 7z 命令)\n return { exe: \"7z\", source: \"系统 PATH\" };\n}\n\n/** 解包安装包到指定目录(spawn 7z x,失败抛错)。 */\nexport async function extractInstaller(\n installerPath: string,\n destDir: string,\n sevenZipPath?: string,\n): Promise<void> {\n const sevenZip = sevenZipPath ?? findSevenZip().exe;\n await mkdir(destDir, { recursive: true });\n await new Promise<void>((resolve, reject) => {\n execFile(\n sevenZip,\n [\"x\", installerPath, `-o${destDir}`, \"-y\"],\n { maxBuffer: 64 * 1024 * 1024 },\n (err, _stdout, stderr) => {\n if (err !== null) {\n const code = (err as NodeJS.ErrnoException).code;\n const hint =\n code === \"ENOENT\"\n ? `未找到 7z 可执行文件: ${sevenZip}\\n` +\n \"(Windows 请安装 7-Zip 或用 NAPUTO_7Z_PATH 指定;\" +\n \"Linux 自动下载失败时请安装 p7zip-full 或用 NAPUTO_7Z_PATH 指定)\"\n : \"(QQ 安装包是 NSIS 格式,需完整版 7z 而非 7za)\";\n reject(new Error(`7z 解包失败: ${installerPath}\\n${stderr.trim()}\\n${hint}`));\n return;\n }\n resolve();\n },\n );\n });\n}\n\n/**\n * 从解包目录提取运行所需文件到缓存:\n * resources/app 顶层全部 *.node + *.dll(保持同目录结构,PATH 前置即可解析依赖)。\n * 幂等:目标 wrapper.node 已存在则跳过。\n */\nexport async function extractWrapperFiles(\n extractedDir: string,\n version: string,\n cacheRoot: string,\n): Promise<QqInstallInfo> {\n // NSIS 解包产物在 <extracted>/Files/(安装根),resources/app 在其下\n const appDir = join(extractedDir, \"Files\", \"versions\", version, \"resources\", \"app\");\n if (!existsSync(join(appDir, \"wrapper.node\"))) {\n throw new Error(`解包产物缺少 wrapper.node: ${appDir}`);\n }\n\n // 目标结构(与安装目录同构):\n // <cacheRoot>/<version>/versions/<version>/resources/app/*.{node,dll}\n const destAppDir = join(cacheRoot, version, \"versions\", version, \"resources\", \"app\");\n const destWrapper = join(destAppDir, \"wrapper.node\");\n\n // 幂等:已提取过则跳过\n if (existsSync(destWrapper)) {\n return buildCachedInfo(cacheRoot, version);\n }\n\n await mkdir(destAppDir, { recursive: true });\n const entries = await readdir(appDir, { withFileTypes: true });\n for (const entry of entries) {\n if (!entry.isFile()) {\n continue; // 跳过子目录(wmpfsdk/avsdk 等大目录不需要)\n }\n const dot = entry.name.lastIndexOf(\".\");\n const ext = dot >= 0 ? entry.name.slice(dot).toLowerCase() : \"\";\n if (!EXTRACT_EXTENSIONS.has(ext)) {\n continue; // 只提取 .node + .dll\n }\n await copyFile(join(appDir, entry.name), join(destAppDir, entry.name));\n }\n if (!existsSync(destWrapper)) {\n throw new Error(`提取失败:wrapper.node 未复制到 ${destWrapper}`);\n }\n\n return buildCachedInfo(cacheRoot, version);\n}\n\n/** 构造 cached 来源的 QqInstallInfo(wrapperPath 指向缓存中 wrapper.node)。 */\nfunction buildCachedInfo(cacheRoot: string, version: string): QqInstallInfo {\n const versionDir = join(cacheRoot, version);\n return {\n qqPath: join(versionDir, \"QQ.exe\"), // 语义占位(缓存目录无 QQ.exe)\n installDir: versionDir,\n version,\n wrapperPath: join(versionDir, \"versions\", version, \"resources\", \"app\", \"wrapper.node\"),\n source: \"cached\",\n };\n}\n\n/** 清理缓存版本目录(下载/解包失败残留,供调用方兜底)。 */\nexport async function clearCacheVersion(cacheRoot: string, version: string): Promise<void> {\n await rm(join(cacheRoot, version), { recursive: true, force: true });\n}\n","/**\n * win-node.ts:Windows 版 node.exe 获取(P2,Linux/wine 场景用)。\n *\n * wine 只能跑 Windows PE 程序——自建宿主在 Linux 上必须用 Windows 版 node.exe\n * 作为宿主(wrapper.node 是 PE 原生模块,只能被 Windows node.exe dlopen)。\n * 来源:nodejs.org 官方 zip(开源软件官方发行版,但为保守起见同样不内置,\n * 运行时下载到 <数据根>/runtime/win-node/,可被 NAPUTO_WIN_NODE_PATH 覆盖)。\n *\n * 设计文档 §3.1。与 qq-download.ts 同构:https 下载 + 解压 + 缓存幂等。\n */\nimport { existsSync, mkdirSync } from \"node:fs\";\nimport { copyFile, readdir, rm } from \"node:fs/promises\";\nimport { join, resolve } from \"node:path\";\nimport process from \"node:process\";\nimport { resolveDataRoot } from \"./data-root.js\";\nimport { downloadFile } from \"./qq-download.js\";\nimport { extractInstaller } from \"./qq-extract.js\";\n\n/** 目标 Windows node.exe 可执行文件名(zip 内)。 */\nconst WIN_NODE_EXE = \"node.exe\";\n\n/** 运行时缓存子目录名(<数据根>/runtime/win-node/)。 */\nexport const WIN_NODE_DIR_NAME = \"win-node\";\n\n/** Windows node.exe 定位结果。 */\nexport interface WinNodeInfo {\n /** node.exe 绝对路径(Windows 视角,供 wine 调用)。 */\n exePath: string;\n /** 已下载的完整版 node zip 版本号(如 v24.16.0)。 */\n version: string;\n}\n\n/** 解析 nodejs.org zip URL(与 dist 目录命名一致)。 */\nexport function nodeZipUrl(version: string): string {\n return `https://nodejs.org/dist/${version}/node-${version}-win-x64.zip`;\n}\n\n/**\n * 确保 Windows 版 node.exe 就绪(下载 + 解压 + 缓存)。\n * 幂等:<数据根>/runtime/win-node/<version>/node.exe 已存在则直接返回。\n */\nexport async function ensureWinNode(\n options: {\n /** 数据根(缓存 <数据根>/runtime/win-node/)。 */\n dataRoot?: string;\n /** node 版本(缺省读 NAPUTO_WIN_NODE_VERSION,再兜底 latest v24)。 */\n version?: string;\n /** 显式 node.exe 路径(覆盖缓存逻辑,读 NAPUTO_WIN_NODE_PATH)。 */\n exePath?: string;\n } = {},\n): Promise<WinNodeInfo> {\n // 显式路径覆盖(NAPUTO_WIN_NODE_PATH / exePath 参数)\n const explicit = options.exePath ?? process.env[\"NAPUTO_WIN_NODE_PATH\"];\n if (explicit !== undefined && explicit !== \"\" && existsSync(explicit)) {\n return { exePath: resolve(explicit), version: \"explicit\" };\n }\n\n const version =\n options.version ?? process.env[\"NAPUTO_WIN_NODE_VERSION\"] ?? DEFAULT_WIN_NODE_VERSION;\n const dataRoot = resolveDataRoot(options.dataRoot);\n const cacheDir = join(dataRoot, \"runtime\", WIN_NODE_DIR_NAME, version);\n const exePath = join(cacheDir, WIN_NODE_EXE);\n if (existsSync(exePath)) {\n return { exePath, version };\n }\n\n // 下载 zip + 解压(用 P1 的 7z 解 zip;7z 支持 zip)\n const zipPath = join(cacheDir, `node-${version}-win-x64.zip`);\n const url = process.env[\"NAPUTO_WIN_NODE_URL\"] ?? nodeZipUrl(version);\n mkdirSync(cacheDir, { recursive: true });\n try {\n await downloadFile({ dest: zipPath, url });\n await unzipNode(zipPath, cacheDir);\n } finally {\n await rm(zipPath, { force: true });\n }\n if (!existsSync(exePath)) {\n throw new Error(`解压后未找到 node.exe: ${exePath}`);\n }\n return { exePath, version };\n}\n\n/** 解压 node zip(zip 内为 node-<version>-win-x64/ 目录,取其中 node.exe 移到缓存根)。 */\nasync function unzipNode(zipPath: string, cacheDir: string): Promise<void> {\n const tmpDir = join(cacheDir, \"_zip\");\n await extractInstaller(zipPath, tmpDir);\n // zip 顶层是 node-<version>-win-x64/,找其中 node.exe\n const top = await readdir(tmpDir);\n const nodeDir = top.find((n) => n.startsWith(\"node-\"));\n const src =\n nodeDir === undefined ? join(tmpDir, WIN_NODE_EXE) : join(tmpDir, nodeDir, WIN_NODE_EXE);\n if (!existsSync(src)) {\n throw new Error(`node zip 解压后未找到 node.exe: ${src}`);\n }\n await copyFile(src, join(cacheDir, WIN_NODE_EXE));\n await rm(tmpDir, { recursive: true, force: true });\n}\n\n/**\n * 默认 Windows node 版本(latest v24,LTS 线;可被环境变量/参数覆盖)。\n * ⚠️ nodejs.org/dist 保留历史版本不会 404,但 pin 死旧 patch 收不到安全更新——\n * 升级 Windows node 时记得同步 bump 此常量(或经 NAPUTO_WIN_NODE_VERSION 覆盖)。\n */\nconst DEFAULT_WIN_NODE_VERSION = \"v24.16.0\";\n","/**\n * wine.ts:Linux 上经 wine 运行 Windows 版 node.exe 的路径与 spawn 组装(P2)。\n *\n * 设计文档 §3.2(2026-08-12 拍板确认)。核心认知:\n * - Windows 版 node.exe 在 wine 内把 Linux 根目录挂载在 `Z:\\`\n * - process.dlopen / 环境变量路径都是 wine 视角的 Windows 路径\n * - 传给 wine 子进程的**所有路径**(wrapperPath / cfgDir / stubDir / self-host.cjs / PATH)\n * 都要过 toWinePath 转成 `Z:\\...`\n *\n * 纯函数设计:不 import node:child_process,方便 Windows 上单测断言(P2 纯逻辑层)。\n */\n\n/** Linux 绝对路径 → wine `Z:\\` 路径(Windows 版 node.exe 视角)。幂等:已是 Windows 路径(含盘符)原样返回。 */\nexport function toWinePath(linuxPath: string): string {\n // 已是 Windows 风格(盘符 + 反斜杠)→ 原样(幂等保护,调用方不应传这种但防御性保留)\n if (DRIVE_LETTER_RE.test(linuxPath)) {\n return linuxPath;\n }\n // /app/.napuketto/qq-files/9.9.33/wrapper.node → Z:\\app\\.napuketto\\qq-files\\9.9.33\\wrapper.node\n // 去前导 /,正斜杠转反斜杠\n return `Z:\\\\${linuxPath.replaceAll(\"/\", \"\\\\\").replace(LEADING_SLASH_RE, \"\")}`;\n}\n\n/** 前导斜杠(toWinePath 去根用,顶层常量避免每次调用重建)。 */\nconst LEADING_SLASH_RE = /^\\\\/;\n\n/** 盘符前缀(C: / Z: 等,识别已转换的 Windows 路径)。 */\nconst DRIVE_LETTER_RE = /^[A-Za-z]:/;\n\n/** 判断当前是否 Linux(需 wine 的场景)。 */\nexport function isLinux(): boolean {\n return process.platform === \"linux\";\n}\n\n/**\n * Unix PATH(冒号分隔)→ wine Windows PATH(分号分隔 + 全部 Z:\\ 条目)。\n *\n * ⚠️ 混入 Unix PATH 会让 wine 按错误分隔符拆分盘符路径(Z:\\...),stub 目录\n * 丢失 → wrapper.node 依赖的 QQNT.dll 找不到(2026-08-14 生产实测\n * \"import_dll Library QQNT.dll not found\")。wine 场景 PATH 必须纯 Windows 风格。\n */\nexport function unixPathToWinePath(unixPath: string): string {\n return unixPath\n .split(\":\")\n .filter((entry) => entry !== \"\")\n .map((entry) => toWinePath(entry))\n .join(\";\");\n}\n\n/** wine 可执行文件(Linux 上 spawn 用;可被 NAPUTO_WINE 覆盖)。 */\nexport function wineBinary(): string {\n return process.env[\"NAPUTO_WINE\"] ?? \"wine\";\n}\n\n/** wine 安装指引(缺失时提示用户如何准备,纯字符串可单测)。 */\nexport function wineInstallHint(): string {\n return (\n \"Linux/WSL 自建宿主需要 wine 运行 Windows 版 node.exe(wrapper.node 是\" +\n \"Windows PE 原生模块,只能被 Windows node.exe dlopen)。\\n\" +\n \" 安装:sudo apt-get install wine64(或其他发行版对应 wine 包)\\n\" +\n \" 覆盖:设置环境变量 NAPUTO_WINE 指向 wine 可执行文件路径\"\n );\n}\n\n/**\n * wine 可执行性探测结果 → 可读错误(null = 可用)。纯函数,可单测。\n * 由 launcher 在 spawn 前用 spawnSync 探测 wine(缺失时 throw 可读错误,\n * 避免 spawn 异步 emit 'error' 无监听者导致宿主进程崩溃,2026-08-23 WSL 实测)。\n */\nexport function wineCheckError(probe: {\n /** spawnSync 退出码(null = spawn 本身失败,如命令不存在)。 */\n status: number | null;\n /** spawn 错误(ENOENT 等;可选)。 */\n error?: unknown;\n}): string | null {\n if (probe.status === 0) {\n return null;\n }\n const err = probe.error as NodeJS.ErrnoException | undefined;\n if (err !== undefined && err.code === \"ENOENT\") {\n return `未检测到 wine 可执行文件。${wineInstallHint()}`;\n }\n if (err !== undefined) {\n return `wine 探测失败: ${err.message ?? String(err)}。${wineInstallHint()}`;\n }\n return `wine --version 退出码非 0(status=${String(probe.status)}),wine 可能异常。${wineInstallHint()}`;\n}\n\n/** spawn 命令与参数(平台分支;纯函数,可单测)。 */\nexport interface SpawnCommand {\n /** 命令(win32 = node.exe;linux = wine)。 */\n command: string;\n /** 参数(win32 = [selfHostPath];linux = [winNodePath, selfHostPath])。 */\n args: string[];\n}\n\n/** 组装 spawn 命令(win32 vs linux 分支)。 */\nexport function buildSpawnCommand(options: {\n /** 平台(缺省 process.platform,测试注入)。 */\n platform?: NodeJS.Platform;\n /** Windows 本机 node 路径(win32 用,缺省 process.execPath)。 */\n winNodePath?: string;\n /** wine 命令(linux 用,缺省 wineBinary())。 */\n wine?: string;\n /** 自建宿主入口(self-host.cjs,传给 wine/win node)。 */\n selfHostPath: string;\n}): SpawnCommand {\n const platform = options.platform ?? process.platform;\n if (platform === \"win32\") {\n return {\n command: options.winNodePath ?? process.execPath,\n args: [options.selfHostPath],\n };\n }\n // linux 及其他(wine 场景)\n return {\n command: options.wine ?? wineBinary(),\n args: [options.winNodePath ?? \"node.exe\", options.selfHostPath],\n };\n}\n","/**\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 *\n * P2(2026-08-12):平台分支——win32 本机 node;linux 经 wine 跑 Windows 版 node.exe\n * (ensureWinNode 下载)。所有传给 wine 子进程的路径过 toWinePath(Z:\\)。\n */\nimport { type StdioOptions, spawn, spawnSync } from \"node:child_process\";\nimport { existsSync, mkdirSync } from \"node:fs\";\nimport { basename, dirname, join, resolve } from \"node:path\";\nimport process from \"node:process\";\nimport { fileURLToPath } from \"node:url\";\nimport type { QqInstallInfo } from \"./locate-qq.js\";\nimport { ensureWinNode } from \"./win-node.js\";\nimport {\n buildSpawnCommand,\n isLinux,\n toWinePath,\n unixPathToWinePath,\n wineBinary,\n wineCheckError,\n wineInstallHint,\n} from \"./wine.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 /** 强制扫码登录(注入 NAPUTO_QR_ONLY;跳过快速登录直接 QR,koishi 面板「扫码登录」用)。 */\n qrOnly?: boolean;\n /** IPC 子进程模式(=1:stdout 走 JSON 行协议 + stdin 收 action/control,koishi 插件驱动)。 */\n ipc?: boolean;\n /** 自建宿主入口(默认 dist/host/self-host.cjs)。 */\n selfHostEntry?: string;\n /** Windows 版 node.exe 路径(linux 下载覆盖 / win32 非 node 宿主时显式指定;缺省自动解析)。 */\n winNodePath?: 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 * 阶段回调(stub 校验/win-node 下载/spawn 提示,2026-08-23 起——下载\n * 313MB 安装包与 win-node 全程静默的坑)。调用方接 logger.info。\n */\n onStage?: (message: string) => void;\n}\n\nexport interface LaunchResult {\n /** 子进程句柄(自建宿主 node 进程)。 */\n child: ReturnType<typeof spawn>;\n /** 自建宿主入口路径(self-host.cjs)。 */\n bootJsPath: string;\n}\n\n/**\n * 启动自建宿主(路线 A):spawn 标准 node 直接跑 self-host.cjs,不拉起 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 *\n * P2 平台分支:win32 用本机 node;linux 用 wine + Windows 版 node.exe\n * (ensureWinNode 自动下载,路径过 toWinePath)。async(win-node 可能需下载)。\n */\nexport async function launchSelfHost(options: LaunchOptions): Promise<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 options.onStage?.(`stub QQNT.dll 就绪:${stub}`);\n\n // 平台分支:win32 本机 node;linux wine + win-node(下载)\n const { useWine, winNodePath } = await resolveNodeExecutable(options);\n\n const env = buildLaunchEnv(options, useWine);\n applyPathEnv(env, { stub, wrapperPath: options.qq.wrapperPath, useWine });\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 // spawn 命令:win32 = node.exe selfHostPath;linux = wine winNodePath selfHostPath\n // wine 场景 selfHostPath 也需转 Windows 路径(wine 内 node 读参数)\n // winNodePath:resolveNodeExecutable 解析出的宿主 node(wine 下载 / win32 非\n // node 宿主解析);未解析(win32 标准 node 环境,execPath 即 node 自身)回退。\n const { command, args } = buildSpawnCommand({\n ...(winNodePath !== undefined ? { winNodePath } : { winNodePath: process.execPath }),\n selfHostPath: useWine ? toWinePath(selfHostPath) : selfHostPath,\n wine: wineBinary(),\n });\n options.onStage?.(`启动自建宿主子进程:${command} ${args.join(\" \")}`);\n const child = spawn(command, args, {\n // exactOptionalPropertyTypes:未传 cwd 时不显式写入 undefined(继承父进程)\n ...(options.cwd !== undefined ? { cwd: options.cwd } : {}),\n env,\n stdio: options.stdio ?? \"inherit\",\n windowsHide: false,\n });\n\n // ⚠️ spawn error 兜底(2026-08-23 WSL 生产事故):wine 未安装时 Node 异步\n // emit 'error'(ENOENT),无监听者 = EventEmitter 默认 throw →\n // uncaughtException → 整个宿主进程(koishi)崩溃。预检(assertWineAvailable)\n // 已覆盖常见场景,这里防漏网并输出可读信息,绝不让宿主进程崩掉。\n child.once(\"error\", (err) => {\n const code = (err as NodeJS.ErrnoException).code;\n const message =\n code === \"ENOENT\"\n ? `无法启动子进程(命令不存在): ${command}\\n${isLinux() ? wineInstallHint() : \"\"}`\n : `子进程启动失败: ${err.message}`;\n // 防崩溃兜底输出(driver 侧另有 error 监听转 onError 友好流程)\n process.stderr.write(`[napuketto-loader] ${message}\\n`);\n });\n\n return { child, bootJsPath: selfHostPath };\n}\n\n/**\n * 平台分支解析 node 可执行:win32 本机(非 node 宿主时解析真 node);linux wine +\n * Windows 版 node.exe(ensureWinNode 下载)。\n */\nasync function resolveNodeExecutable(options: LaunchOptions): Promise<{\n useWine: boolean;\n winNodePath: string | undefined;\n}> {\n if (!isLinux()) {\n // win32:标准 node 宿主(execPath 即 node)直接用自身;非 node 宿主\n // (koishi CE 等 Bun 运行时,execPath=bun.exe)必须解析真 node——\n // wrapper.node 的符号由 stub QQNT.dll 绑定到 node.exe,bun.exe 进程内\n // 无此模块,LoadLibrary 报 1114「DLL 初始化例程失败」(2026-09-05 实测)。\n if (isNodeExecutable(process.execPath)) {\n return { useWine: false, winNodePath: undefined };\n }\n const hostNode = await resolveWinHostNode({\n ...(options.winNodePath !== undefined ? { explicitPath: options.winNodePath } : {}),\n ...(options.cwd !== undefined ? { dataRoot: options.cwd } : {}),\n ...(options.onStage !== undefined ? { onStage: options.onStage } : {}),\n });\n return { useWine: false, winNodePath: hostNode };\n }\n // ⚠️ wine 预检(2026-08-23 WSL 生产事故):spawn 前确认 wine 可执行——\n // 干净 WSL 环境默认没有 wine,缺失时给出安装指引(throw 可读错误,由\n // 调用方 driver 捕获转 [W] 驱动错误),而不是 spawn 后异步 'error' 崩进程。\n const wine = wineBinary();\n const probe = spawnSync(wine, [\"--version\"], { stdio: \"ignore\" });\n const hint = wineCheckError(probe);\n if (hint !== null) {\n throw new Error(hint);\n }\n options.onStage?.(\"wine 就绪,获取 Windows 版 node.exe(Linux/wine 场景)…\");\n const winNode = await ensureWinNode({\n ...(options.cwd !== undefined ? { dataRoot: options.cwd } : {}),\n ...(options.winNodePath !== undefined ? { exePath: options.winNodePath } : {}),\n });\n options.onStage?.(`Windows 版 node.exe 就绪:${winNode.exePath}(${winNode.version})`);\n return { useWine: true, winNodePath: winNode.exePath };\n}\n\n/** 可执行文件名是否 node(win32 宿主判定;node.exe / node 两种形态)。 */\nexport function isNodeExecutable(execPath: string): boolean {\n const name = basename(execPath).toLowerCase();\n return name === \"node.exe\" || name === \"node\";\n}\n\n/** 系统 node 探测结果(win32,PATH 解析 + 可执行校验)。 */\nexport interface SystemNodeInfo {\n exePath: string;\n version: string;\n}\n\n/** 行拆分(where 输出 \\r\\n / \\n 兼容)。 */\nconst LINE_SPLIT = /\\r?\\n/;\n\n/** 探测系统 PATH 上的 node(win32;where node 首条 + --version 校验可执行)。 */\nexport function findSystemNode(): SystemNodeInfo | undefined {\n const probe = spawnSync(\"where\", [\"node\"], {\n stdio: \"pipe\",\n encoding: \"utf8\",\n windowsHide: true,\n });\n if (probe.status !== 0 || !probe.stdout) {\n return undefined;\n }\n const first = probe.stdout\n .split(LINE_SPLIT)\n .map((line) => line.trim())\n .find((line) => line !== \"\");\n if (first === undefined || !existsSync(first)) {\n return undefined;\n }\n const ver = spawnSync(first, [\"--version\"], {\n stdio: \"pipe\",\n encoding: \"utf8\",\n windowsHide: true,\n });\n const version = ver.status === 0 ? ver.stdout.trim() : \"\";\n if (!version.startsWith(\"v\")) {\n return undefined;\n }\n return { exePath: resolve(first), version };\n}\n\n/** resolveWinHostNode 测试注入面(探测/下载替换为假实现)。 */\nexport interface WinHostNodeResolvers {\n findSystemNode?: () => SystemNodeInfo | undefined;\n ensureWinNode?: typeof ensureWinNode;\n}\n\n/**\n * win32 非 node 宿主时解析真 node.exe:显式覆盖(winNodePath 参数 /\n * NAPUTO_WIN_NODE_PATH)> 系统 PATH node(离线可用,免下载)> ensureWinNode\n * 下载(缓存幂等)。显式路径不存在时静默跳过(与 ensureWinNode 行为一致)。\n */\nexport async function resolveWinHostNode(\n options: {\n explicitPath?: string;\n dataRoot?: string;\n onStage?: (message: string) => void;\n },\n resolvers: WinHostNodeResolvers = {},\n): Promise<string | undefined> {\n const findSystem = resolvers.findSystemNode ?? findSystemNode;\n const ensure = resolvers.ensureWinNode ?? ensureWinNode;\n\n const explicit = options.explicitPath ?? process.env[\"NAPUTO_WIN_NODE_PATH\"];\n if (explicit !== undefined && explicit !== \"\" && existsSync(explicit)) {\n const exePath = resolve(explicit);\n options.onStage?.(`使用显式指定 node:${exePath}`);\n return exePath;\n }\n\n options.onStage?.(\n `当前宿主非 node(${basename(process.execPath)}),解析 Windows 版 node.exe…`,\n );\n\n const sys = findSystem();\n if (sys !== undefined) {\n options.onStage?.(`系统 node ${sys.version}:${sys.exePath}`);\n return sys.exePath;\n }\n\n const winNode = await ensure({\n ...(options.dataRoot !== undefined ? { dataRoot: options.dataRoot } : {}),\n });\n options.onStage?.(`Windows 版 node.exe 就绪:${winNode.exePath}(${winNode.version})`);\n return winNode.exePath;\n}\n\n/** 注入 PATH(stub 目录 + wrapper.node 目录前置)与 STUB_DIR(wine 场景转 Z:\\)。 */\nfunction applyPathEnv(\n env: Record<string, string>,\n opts: { stub: string; wrapperPath: string; useWine: boolean },\n): void {\n const p = (linuxPath: string): string => (opts.useWine ? toWinePath(linuxPath) : linuxPath);\n // ⚠️ wine 场景 PATH 必须纯 Windows 风格(全部 Z:\\ 条目 + 分号分隔):混入\n // Unix PATH(冒号分隔)会让 wine 按错误分隔符拆分,盘符路径(Z:\\...)被拆散,\n // stub 目录丢失 → wrapper.node 依赖的 QQNT.dll 找不到(2026-08-14 生产实测\n // \"import_dll Library QQNT.dll not found\")。Unix PATH 逐条转 Z:\\ 再并入。\n const unixPath = process.env[\"PATH\"] ?? \"\";\n const winPath = opts.useWine ? unixPathToWinePath(unixPath) : unixPath;\n const pathEntries = [p(opts.stub), p(dirname(opts.wrapperPath)), winPath]\n .filter((entry) => entry !== undefined && entry !== \"\")\n .join(\";\");\n env[\"PATH\"] = pathEntries;\n env[ENV.STUB_DIR] = p(opts.stub);\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/** 装配自建宿主环境变量(wine 场景路径转 Z:\\;win32 原样)。 */\nfunction buildLaunchEnv(options: LaunchOptions, useWine: boolean): Record<string, string> {\n // 配置目录兜底\n const cfg = resolve(options.cfgDir);\n mkdirSync(cfg, { recursive: true });\n\n // wine 场景:传给子进程的路径全转 Z:\\(node.exe 在 wine 内按 Windows 路径读)\n const p = (linuxPath: string): string => (useWine ? toWinePath(linuxPath) : linuxPath);\n const env: Record<string, string> = {\n ...process.env,\n [ENV.QQ_PATH]: p(options.qq.qqPath),\n [ENV.KERNEL_ENTRY]: p(resolve(options.kernelEntry)),\n [ENV.CFG_DIR]: p(cfg),\n [ENV.QQ_VERSION]: options.qq.version,\n [ENV.WRAPPER_PATH]: p(options.qq.wrapperPath),\n };\n // 可选注入(对象展开,保持低复杂度)\n const optional: Record<string, string> = {\n ...(options.selfHost === true ? { [ENV.SELF_HOST]: \"1\" } : {}),\n ...(options.adapterEntry !== undefined\n ? { [ENV.ADAPTER_ENTRY]: p(resolve(options.adapterEntry)) }\n : {}),\n ...(options.networkEntry !== undefined\n ? { [ENV.NETWORK_ENTRY]: p(resolve(options.networkEntry)) }\n : {}),\n ...(options.configPath !== undefined\n ? { [ENV.CONFIG_PATH]: p(resolve(options.configPath)) }\n : {}),\n ...(options.quickUin !== undefined ? { [ENV.QUICK_UIN]: options.quickUin } : {}),\n ...(options.qrOnly === true ? { [ENV.QR_ONLY]: \"1\" } : {}),\n ...(options.ipc === true ? { [ENV.IPC]: \"1\" } : {}),\n };\n return { ...env, ...optional };\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 /** 强制扫码登录(跳过快速登录直接 QR;koishi 面板「扫码登录」重启路径用)。 */\n QR_ONLY: \"NAPUTO_QR_ONLY\",\n /** IPC 子进程模式(koishi 插件驱动:stdout JSON 行 + stdin action/control)。 */\n IPC: \"NAPUTO_IPC\",\n} as const;\n","/**\n * qq-releases.ts:版本清单读取(P1)。\n *\n * 运行时从包根 qq-releases.json 读取(发布时 files 含它,安装后位于\n * node_modules/@napuketto/loader/qq-releases.json)。\n *\n * 维护机制(真实存在,非「声称」):清单由 CI 自动维护——\n * .github/workflows/update-qq-releases.yml(每日 cron + 手动触发)调用\n * scripts/qq-releases/update-qq-releases.ts(纯 Node,与本地共用),抓取 QQ 官方下载配置\n * 解析最新版 + 下载安装包计算 sha256 + 解析安装包内部版本目录后更新本清单\n * (保留历史条目、known 按版本递增)。社区 PR 仍可手动补充。NAPUTO_QQ_URL\n * 可运行时覆盖下载地址(用户拿到新链接时用)。\n */\nimport { readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport process from \"node:process\";\nimport { fileURLToPath } from \"node:url\";\n\n/** 清单中单个版本条目。 */\nexport interface QqReleaseEntry {\n /** 版本目录名(如 9.9.33-51802)。 */\n version: string;\n /** 官方安装包下载 URL。 */\n url: string;\n /** sha256(十六进制小写);空串 = 尚未校验(首次录入时 CI 计算后填入)。 */\n sha256: string;\n /** appid(预知信息,运行时仍从 major.node 解析;0 = 尚未解析/待运行时补)。 */\n appid: number;\n /** 来源标记(official = 官方)。 */\n source: string;\n /** 构建日期(YYYY-MM-DD,可选)。 */\n buildDate?: string;\n}\n\n/** 清单文件结构。 */\nexport interface QqReleasesFile {\n schema: number;\n known: QqReleaseEntry[];\n}\n\n/** 清单解析失败。 */\nexport class QqReleasesError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"QqReleasesError\";\n }\n}\n\n/** 包根路径(dist 与包根同层:dist/index.mjs → ../qq-releases.json)。 */\nfunction packageRoot(): string {\n return dirname(dirname(fileURLToPath(import.meta.url)));\n}\n\n/** 读版本清单(同步,启动期调用一次即可)。 */\nexport function loadQqReleases(): QqReleasesFile {\n const file = join(packageRoot(), \"qq-releases.json\");\n let raw: string;\n try {\n raw = readFileSync(file, \"utf8\");\n } catch {\n throw new QqReleasesError(`版本清单缺失: ${file}(@napuketto/loader 包安装不完整?)`);\n }\n try {\n const parsed = JSON.parse(raw) as Partial<QqReleasesFile>;\n if (parsed.schema !== 1 || !Array.isArray(parsed.known) || parsed.known.length === 0) {\n throw new Error(\"结构不合法\");\n }\n return parsed as QqReleasesFile;\n } catch {\n throw new QqReleasesError(`版本清单解析失败: ${file}`);\n }\n}\n\n/** 取最新已知可用版本(known 数组按录入顺序,取最后一项 = 最新)。 */\nexport function latestRelease(releases: QqReleasesFile): QqReleaseEntry {\n const [latest] = [...releases.known].reverse();\n if (latest === undefined) {\n throw new QqReleasesError(\"版本清单为空\");\n }\n return latest;\n}\n\n/**\n * 解析实际下载 URL:NAPUTO_QQ_URL 环境变量覆盖 > 清单 URL。\n * 供 downloadFile 使用(也支持按版本查询,当前固定取最新)。\n */\nexport function resolveDownloadUrl(release: QqReleaseEntry): string {\n const override = process.env[\"NAPUTO_QQ_URL\"];\n return override !== undefined && override !== \"\" ? override : release.url;\n}\n","/**\n * 定位 QQ 原生文件(wrapper.node)来源(P0:多级来源纯定位,不下载)。\n *\n * 来源优先级(resolveQqFiles):\n * L0: NAPUTO_QQ_FILES / qqFilesDir —— 显式指定含 versions/ 结构的文件根(缓存/拷贝)\n * L1: 本机 QQ 安装 —— NAPUTO_QQ_PATH / 注册表 / 常见路径(既有逻辑保留)\n * L2: 数据根缓存 <数据根>/qq-files/<版本> —— 下载解包产物(P1 落地后才有)\n */\nimport { execFileSync } from \"node:child_process\";\nimport { existsSync, mkdirSync, readdirSync, statSync } from \"node:fs\";\nimport { chmod, rm } from \"node:fs/promises\";\nimport { dirname, join, resolve } from \"node:path\";\nimport process from \"node:process\";\nimport { fileURLToPath } from \"node:url\";\nimport { resolveDataRoot } from \"./data-root.js\";\nimport { downloadFile } from \"./qq-download.js\";\nimport { clearCacheVersion, extractInstaller, extractWrapperFiles } from \"./qq-extract.js\";\nimport { latestRelease, loadQqReleases, resolveDownloadUrl } from \"./qq-releases.js\";\n\n/** 数据根下 QQ 文件缓存目录名(<数据根>/qq-files/<版本>/,P1 下载解包产物)。 */\nexport const QQ_FILES_DIR_NAME = \"qq-files\";\n\n/**\n * 7-Zip 官方 Linux 版版本号(tar.xz 文件名组成部分;兜底下载用,内置资产优先)。\n * ⚠️ 此值为最后兜底下载用,7-Zip 官网只留最新版,旧版号会 404;内置资产失败才触发。\n */\nconst SEVEN_ZIP_LINUX_VERSION = \"2501\";\n\n/** 注册表 UninstallString 查询(QQ 官方安装路径)。 */\nconst REG_QUERY_RE = /\"([^\"]+)\"/;\n\n/** Windows 盘符路径正则(`C:/...` / `C:\\...`,wslMappedPath 映射用,模块级常量)。 */\nconst DRIVE_PATH_RE = /^([A-Za-z]):[\\\\/](.*)$/;\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 ];\n for (const p of candidates) {\n // WSL 场景:Windows 盘挂载在 /mnt/<盘符>/(如 C:\\ → /mnt/c/),\n // 同一候选同时探测 Linux 侧映射路径(2026-08-13 koishi-app 实测报「未找到 QQ.exe」)。\n const wsl = wslMappedPath(p);\n if (existsSync(wsl)) {\n return wsl;\n }\n if (existsSync(p)) {\n return p;\n }\n }\n return null;\n}\n\n/**\n * Windows 路径 → WSL 挂载路径(`C:/...` → `/mnt/c/...`;非 Linux 或非盘符路径原样)。\n * WSL 默认把 Windows 盘符挂载到 `/mnt/<小写盘符>/`,Linux 侧 `existsSync` 必须用映射路径。\n * platform 参数可注入(测试用;缺省 process.platform)。\n * 导出便于单测(locate-qq.test.ts)。\n * ⚠️ 不用 join 拼 `/mnt/...`:测试在 Windows 跑,win32 join 会产生反斜杠;目标路径\n * 是 Linux 侧路径(WSL 挂载点),必须恒正斜杠。\n */\nexport function wslMappedPath(\n winPath: string,\n platform: NodeJS.Platform = process.platform,\n): string {\n if (platform !== \"linux\") {\n return winPath;\n }\n const [, drive, rest] = DRIVE_PATH_RE.exec(winPath) ?? [];\n if (drive === undefined || rest === undefined) {\n return winPath;\n }\n return `/mnt/${drive.toLowerCase()}/${rest.replaceAll(\"\\\\\", \"/\")}`;\n}\n\n/** QQ 文件来源。 */\nexport type QqFileSource = \"local\" | \"cached\";\n\n/** QQ 安装目录探测结果。 */\nexport interface QqInstallInfo {\n /** QQ.exe 绝对路径(cached 来源为语义占位——缓存目录无 QQ.exe,当前无消费方)。 */\n qqPath: string;\n /** 安装根目录 / 文件根目录。 */\n installDir: string;\n /** 当前版本目录名(如 9.9.31-49919)。 */\n version: string;\n /** wrapper.node 绝对路径。 */\n wrapperPath: string;\n /** 来源:local = 本机安装;cached = NAPUTO_QQ_FILES / 数据根缓存。 */\n source: QqFileSource;\n}\n\n/** resolveQqFiles 选项。 */\nexport interface ResolveQqFilesOptions {\n /** 显式文件根(L0;缺省读 NAPUTO_QQ_FILES)。 */\n qqFilesDir?: string;\n /** 显式 QQ.exe 路径(L1;缺省读 NAPUTO_QQ_PATH)。 */\n qqPath?: string;\n /** 数据根(L2 缓存扫描;缺省轻量解析 NAPKETTO_DATA ?? 项目根/.napuketto)。 */\n dataRoot?: string;\n /** L0/L1/L2 全部失败时自动进入下载流程(P1;默认 true,测试可关)。 */\n autoDownload?: boolean;\n /** 7z 可执行文件路径(解包用;缺省自动探测)。 */\n sevenZipPath?: string;\n /**\n * 阶段回调(下载/校验/解包/提取各阶段提示,2026-08-23 WSL 事故后加:\n * 下载 313MB 全程静默,用户以为流程没生效)。调用方接 logger.info。\n */\n onStage?: (message: string) => void;\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/** 从文件根定位最新版本 wrapper.node(local/cached 共用)。 */\nfunction resolveFromRoot(rootDir: string, source: QqFileSource): QqInstallInfo {\n const versionsDir = join(rootDir, \"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 {\n qqPath: join(rootDir, \"QQ.exe\"),\n installDir: rootDir,\n version: first,\n wrapperPath,\n source,\n };\n}\n\n/** 从 QQ.exe 路径推导安装目录,并探测当前版本(L1:本机安装)。 */\nexport function resolveQqInstall(qqPath?: string): QqInstallInfo {\n const qq = qqPath ?? process.env[\"NAPUTO_QQ_PATH\"] ?? locateQqPath();\n // Windows 安装结构:<installDir>/versions/<版本>/resources/app/wrapper.node\n // ⚠️ dirname 跨平台:Linux/WSL 路径用正斜杠(lastIndexOf(\"\\\\\") 会切错,2026-08-13 实测)\n const installDir = dirname(qq);\n return resolveFromRoot(installDir, \"local\");\n}\n\n/**\n * 多级来源定位 QQ 原生文件:\n * L0 → L1 → L2 依次尝试,全部失败且 autoDownload 时自动进入下载流程(ensureQqFiles)。\n * ⚠️ async:自动下载为异步流程(P1 起);纯定位场景用 resolveQqInstall(同步)。\n */\nexport async function resolveQqFiles(options: ResolveQqFilesOptions = {}): Promise<QqInstallInfo> {\n // L0:显式文件根(参数 > 环境变量)\n const explicitDir = options.qqFilesDir ?? process.env[\"NAPUTO_QQ_FILES\"];\n if (explicitDir !== undefined && explicitDir !== \"\") {\n return resolveFromRoot(resolve(explicitDir), \"cached\");\n }\n // L1:本机安装(参数 > 环境变量 > 注册表/常见路径)\n const qqPath = options.qqPath ?? process.env[\"NAPUTO_QQ_PATH\"];\n if (qqPath !== undefined && qqPath !== \"\") {\n return resolveQqInstall(qqPath);\n }\n try {\n return resolveQqInstall();\n } catch {\n // 本机未安装 → 落 L2\n }\n // L2:数据根缓存(P1 下载解包产物;结构 <数据根>/qq-files/<版本>/)\n const dataRoot = resolveDataRoot(options.dataRoot);\n const cached = tryResolveCached(dataRoot);\n if (cached !== null) {\n return cached;\n }\n // 全部缺失 → 自动下载(P1)\n if (options.autoDownload !== false) {\n const sevenZipPath =\n options.sevenZipPath !== undefined ? { sevenZipPath: options.sevenZipPath } : {};\n return ensureQqFiles({\n dataRoot,\n ...sevenZipPath,\n ...(options.onStage !== undefined ? { onStage: options.onStage } : {}),\n });\n }\n throw new Error(\n \"未找到 QQ 原生文件(wrapper.node):请安装 QQ、设置 NAPUTO_QQ_PATH/NAPUTO_QQ_FILES,\" +\n \"或使用下载流程(ensureQqFiles)自动获取\",\n );\n}\n\n/** 扫描数据根缓存(L2);命中返回,未命中返回 null。 */\nfunction tryResolveCached(dataRoot: string): QqInstallInfo | null {\n const cacheRoot = join(dataRoot, QQ_FILES_DIR_NAME);\n if (!existsSync(cacheRoot)) {\n return null;\n }\n const versionDirs = readdirSync(cacheRoot)\n .filter((v) => statSync(join(cacheRoot, v)).isDirectory())\n .sort()\n .reverse();\n for (const versionDir of versionDirs) {\n try {\n return resolveFromRoot(join(cacheRoot, versionDir), \"cached\");\n } catch {\n // 该版本目录结构不完整(如下载中断残留),尝试下一个\n }\n }\n return null;\n}\n\n/**\n * 7-Zip 官方 Linux 版下载地址(纯函数,可单测)。\n * 环境变量 NAPUTO_7Z_URL 可覆盖(同 QQ 下载 NAPUTO_QQ_URL 模式)。\n */\nexport function linuxSevenZipUrl(): string {\n return (\n process.env[\"NAPUTO_7Z_URL\"] ??\n `https://www.7-zip.org/a/7z${SEVEN_ZIP_LINUX_VERSION}-linux-x64.tar.xz`\n );\n}\n\n/**\n * 确保 Linux 7zz 就绪(治本:内置资产优先,下载兜底)。\n *\n * 优先级:内置资产 assets/7zip/7zz(2026-08-14 治本——与 Windows 7z.exe 同模式,\n * 7zz 静态二进制已打进发布包,LGPL 合规可分发,零运行时下载)> 数据根缓存\n * runtime/7zip/7zz(历史下载产物复用)> 官网下载(兜底,NAPUTO_7Z_URL 可覆盖)。\n *\n * 背景:7-Zip 官网只保留最新版 tar.xz,硬编码旧版本号(如 2409)会被删档 404,\n * 运行时下载既脆弱又依赖外网。内置资产彻底消灭该依赖。\n */\nexport async function ensureLinuxSevenZip(\n options: { dataRoot?: string } = {},\n): Promise<{ exe: string; source: string }> {\n // 1. 内置资产(治本):dist 与 assets 同层(tsdown 构建后 dist/,资产在包根 assets/)\n const bundled = join(dirname(fileURLToPath(import.meta.url)), \"..\", \"assets\", \"7zip\", \"7zz\");\n if (existsSync(bundled)) {\n // npm 安装可能丢失执行位,保险设置(失败不阻塞:execFile 报 EACCES 时由调用方提示)\n await chmod(bundled, 0o755).catch(() => {\n // 忽略:无执行位场景后续解包会明确报错,此处不静默吞掉主流程\n });\n return { exe: bundled, source: \"内置资产 assets/7zip\" };\n }\n\n // 2. 数据根缓存(历史下载产物复用)\n const dataRoot = resolveDataRoot(options.dataRoot);\n const cacheDir = join(dataRoot, \"runtime\", \"7zip\");\n const exe = join(cacheDir, \"7zz\");\n if (existsSync(exe)) {\n return { exe, source: \"数据根缓存 runtime/7zip\" };\n }\n\n // 3. 官网下载兜底(NAPUTO_7Z_URL 覆盖;仅当发布包缺失旧形态时触发)\n const archive = join(cacheDir, `7z${SEVEN_ZIP_LINUX_VERSION}-linux-x64.tar.xz`);\n mkdirSync(cacheDir, { recursive: true });\n await downloadFile({ dest: archive, url: linuxSevenZipUrl() });\n try {\n // tar -xJf 解压(Linux 自带 tar;7-Zip 官方 tar.xz 根目录含 7zz)\n try {\n execFileSync(\"tar\", [\"-xJf\", archive, \"-C\", cacheDir], { stdio: \"pipe\" });\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n throw new Error(\n `7z linux 解压失败: ${message}(需 tar + xz-utils;或设 NAPUTO_7Z_PATH 指定系统 7z)`,\n );\n }\n } finally {\n await rm(archive, { force: true });\n }\n if (!existsSync(exe)) {\n throw new Error(`7z linux 解压后未找到 7zz: ${exe}`);\n }\n await chmod(exe, 0o755); // 官方 tar.xz 内已有执行位,保险设置\n return { exe, source: \"自动下载(7-Zip 官方 linux 版)\" };\n}\n\n/**\n * 确保 QQ 原生文件就绪(P1):幂等缓存检查 → 下载官方安装包 → sha256 校验\n * → 7z 解包 → 提取 wrapper.node/QQNT.dll → 缓存。返回缓存版本 QqInstallInfo。\n *\n * onStage:阶段回调(下载/解包等提示,2026-08-23 起——下载 313MB 全程静默\n * 的坑)。调用方接 logger.info。\n */\nexport async function ensureQqFiles(\n options: {\n /** 数据根(缓存 <数据根>/qq-files/)。 */\n dataRoot?: string;\n /** 7z 路径(缺省自动探测)。 */\n sevenZipPath?: string;\n /** 阶段回调(下载/校验/解包/提取,接 logger.info)。 */\n onStage?: (message: string) => void;\n } = {},\n): Promise<QqInstallInfo> {\n const dataRoot = resolveDataRoot(options.dataRoot);\n const cacheRoot = join(dataRoot, QQ_FILES_DIR_NAME);\n mkdirSync(cacheRoot, { recursive: true });\n\n // 幂等:缓存已有完整版本 → 直接返回\n const existing = tryResolveCached(dataRoot);\n if (existing !== null) {\n options.onStage?.(`QQ 原生文件缓存命中:版本 ${existing.version}`);\n return existing;\n }\n\n // 1. 版本清单 → 最新可用版本\n const releases = loadQqReleases();\n const release = latestRelease(releases);\n const url = resolveDownloadUrl(release);\n const version = release.version;\n\n // 2. 下载 + sha256 校验(清单无参考值时下载器跳过校验,完整性由解包/加载兜底)\n const tmpDir = join(dataRoot, \"tmp\");\n mkdirSync(tmpDir, { recursive: true });\n // ⚠️ tmp 文件名唯一化(2026-08-23 WSL 事故):固定名 qq-installer-<v>.exe 在\n // 多实例/重试并发时互相覆盖,且一个实例 finally 清理会删掉另一个实例刚下载\n // 的文件——表现就是「下载成功但 7z 解包报 No such file or directory」。\n // 加 pid + 时间戳后缀,实例间天然隔离。\n const installerPath = join(\n tmpDir,\n `qq-installer-${version}-${process.pid}-${Date.now().toString(36)}.exe`,\n );\n options.onStage?.(`下载 QQ 官方安装包 ${version}(首次使用需下载,约 300MB)…`);\n await downloadFile({ dest: installerPath, url, expectedSha256: release.sha256 });\n options.onStage?.(`QQ 安装包下载完成,sha256 校验通过(${release.sha256.slice(0, 12)}…)`);\n\n // 3. 解包 + 提取(失败清理缓存残留;临时文件无论如何清理)\n // Linux 分支:确保 7zz(自动下载 7-Zip 官方 linux 版,2026-08-14 生产修复——\n // 此前依赖系统 p7zip-full,未安装则 7z 解包失败、QQ 文件无法就绪)\n let sevenZipPath = options.sevenZipPath;\n if (sevenZipPath === undefined && process.platform === \"linux\") {\n sevenZipPath = (await ensureLinuxSevenZip({ dataRoot })).exe;\n }\n const extractedDir = join(tmpDir, `qq-extracted-${version}-${process.pid}`);\n try {\n // 解包前再次确认安装包真实存在(downloadFile 已 stat 兜底;防御并发清理竞态)\n if (!existsSync(installerPath)) {\n throw new Error(\n `下载产物在解包前丢失: ${installerPath}(疑为并发实例清理 tmp,` +\n \"已用唯一文件名规避,请重试)\",\n );\n }\n options.onStage?.(`7z 解包安装包(${version},需数分钟)…`);\n await extractInstaller(installerPath, extractedDir, sevenZipPath);\n options.onStage?.(\"提取 wrapper.node 及原生依赖 DLL…\");\n const info = await extractWrapperFiles(extractedDir, version, cacheRoot);\n options.onStage?.(`QQ 原生文件就绪:缓存版本 ${info.version}`);\n return info;\n } catch (err) {\n await clearCacheVersion(cacheRoot, version);\n throw err;\n } finally {\n await rm(installerPath, { force: true });\n await rm(extractedDir, { recursive: true, force: true });\n }\n}\n"],"mappings":"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"}
|