@napuketto/loader 0.0.10 → 0.0.12

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.
@@ -223,6 +223,14 @@ function createState() {
223
223
  * workspace 依赖——见 types.ts 注释)。错误码提取用宽松结构判断。
224
224
  */
225
225
  /**
226
+ * 注册 login.refreshQr 动作(登录期即可用——不依赖 session 服务,只依赖
227
+ * core.refreshQr)。bootstrap 提前启动 IPC 服务端时先注册本动作,登录完成后
228
+ * 再把 kernel 服务的完整动作并入同一张表。
229
+ */
230
+ function registerLoginRefreshAction(actions, refreshQr) {
231
+ actions.set("login.refreshQr", async () => refreshQr());
232
+ }
233
+ /**
226
234
  * 从宽松 params 构造 Peer(缺省 chatType=1 群聊)。
227
235
  * 优先 peerUid(uid 直通);缺省时 peerUin 经 uinToUid 转换。
228
236
  *
@@ -500,13 +508,14 @@ function sendStatus(phase, message, error) {
500
508
  });
501
509
  }
502
510
  /** 登录状态(QR 状态机 idle/waiting_scan/scanned/logged_in/failed)。 */
503
- function sendLogin(state, selfInfo) {
511
+ function sendLogin(state, selfInfo, message) {
504
512
  sendIpc({
505
513
  v: 1,
506
514
  type: "login",
507
515
  payload: {
508
516
  state,
509
- ...selfInfo !== void 0 ? { selfInfo } : {}
517
+ ...selfInfo !== void 0 ? { selfInfo } : {},
518
+ ...message !== void 0 ? { message } : {}
510
519
  }
511
520
  });
512
521
  }
@@ -557,6 +566,51 @@ function sendPing() {
557
566
  });
558
567
  }
559
568
  //#endregion
569
+ //#region src/host/ipc/ipc-bootstrap.ts
570
+ /** 订阅事件通道 → event 消息转发(事件名 "Service/method" 拆分)。 */
571
+ function forwardChannel(channel, stops) {
572
+ if (typeof channel.onAny !== "function") return;
573
+ const off = channel.onAny((event, ...args) => {
574
+ const slash = event.indexOf("/");
575
+ sendEvent(slash >= 0 ? event.slice(0, slash) : event, slash >= 0 ? event.slice(slash + 1) : "", args);
576
+ });
577
+ if (typeof off === "function") stops.push(off);
578
+ }
579
+ /**
580
+ * 登录期动作表:只含 login.refreshQr(依赖 core.refreshQr,不依赖 session 服务)。
581
+ * bootstrap-core 在登录前用它启动 IPC 服务端——登录中前端即可刷新二维码。
582
+ */
583
+ function createIpcActionsForCore(core) {
584
+ const actions = /* @__PURE__ */ new Map();
585
+ if (typeof core.refreshQr === "function") registerLoginRefreshAction(actions, () => core.refreshQr?.() ?? false);
586
+ return actions;
587
+ }
588
+ /**
589
+ * 把 kernel 服务动作并入共享动作表 + 事件通道转发(登录成功后调用)。
590
+ * 返回停止函数(清理事件转发订阅;stdin 服务端由 bootstrap-core 独立管理)。
591
+ */
592
+ function attachIpcServices(actions, services) {
593
+ const groupApi = services.groupApi;
594
+ const full = createIpcActions({
595
+ msgApi: services.msgApi,
596
+ groupApi: services.groupApi,
597
+ groupCache: services.groupCache,
598
+ friendApi: services.friendApi,
599
+ self: services.self,
600
+ session: services.session,
601
+ engine: services.engine,
602
+ util: services.util,
603
+ ...groupApi.uinToUid !== void 0 ? { uinToUid: groupApi.uinToUid } : {}
604
+ });
605
+ for (const [name, handler] of full) actions.set(name, handler);
606
+ const stops = [];
607
+ forwardChannel(services.channel, stops);
608
+ forwardChannel(services.groupChannel, stops);
609
+ return () => {
610
+ for (const stop of stops) stop();
611
+ };
612
+ }
613
+ //#endregion
560
614
  //#region src/host/ipc/ipc-codec.ts
561
615
  /**
562
616
  * ipc-codec.ts:IPC 消息编解码(JSON 行协议,与 koishi 插件侧 src/ipc/codec.ts 对齐)。
@@ -617,40 +671,6 @@ function handleControl(payload, onExit) {
617
671
  if (payload.command === "stop" || payload.command === "restart") onExit();
618
672
  }
619
673
  //#endregion
620
- //#region src/host/ipc/ipc-bootstrap.ts
621
- /** 订阅事件通道 → event 消息转发(事件名 "Service/method" 拆分)。 */
622
- function forwardChannel(channel, stops) {
623
- if (typeof channel.onAny !== "function") return;
624
- const off = channel.onAny((event, ...args) => {
625
- const slash = event.indexOf("/");
626
- sendEvent(slash >= 0 ? event.slice(0, slash) : event, slash >= 0 ? event.slice(slash + 1) : "", args);
627
- });
628
- if (typeof off === "function") stops.push(off);
629
- }
630
- /** IPC 模式装配(enableIpc + 动作表 + 服务端 + 事件转发)。返回停止函数。 */
631
- function startIpcMode(services) {
632
- enableIpc();
633
- const groupApi = services.groupApi;
634
- const stopServer = startIpcServer({ actions: createIpcActions({
635
- msgApi: services.msgApi,
636
- groupApi: services.groupApi,
637
- groupCache: services.groupCache,
638
- friendApi: services.friendApi,
639
- self: services.self,
640
- session: services.session,
641
- engine: services.engine,
642
- util: services.util,
643
- ...groupApi.uinToUid !== void 0 ? { uinToUid: groupApi.uinToUid } : {}
644
- }) });
645
- const stops = [];
646
- forwardChannel(services.channel, stops);
647
- forwardChannel(services.groupChannel, stops);
648
- return () => {
649
- stopServer();
650
- for (const stop of stops) stop();
651
- };
652
- }
653
- //#endregion
654
674
  //#region src/host/core/login.ts
655
675
  /** 打印可用快速登录账号 + 返回目标账号(启动横幅)。forcedUin(cli `-q` / NAPUTO_QUICK_UIN)优先。 */
656
676
  async function pickLoginAccount(kernel, ctx, loginResultRef, forcedUin) {
@@ -1274,7 +1294,7 @@ function buildLoginOpts(Appid, forcedUin, ref) {
1274
1294
  qrcodeUrl: progress.qr.qrcodeUrl
1275
1295
  })}\n`);
1276
1296
  }
1277
- if (ipcMode && isLoginState(progress.state)) sendLogin(progress.state, progress.selfInfo);
1297
+ if (ipcMode && isLoginState(progress.state)) sendLogin(progress.state, progress.selfInfo, progress.message);
1278
1298
  };
1279
1299
  return opts;
1280
1300
  }
@@ -1363,6 +1383,11 @@ async function bootstrapWithCore(kernel, state, bootEnv, Appid) {
1363
1383
  paths: { dataRoot: bootEnv.dataDir },
1364
1384
  logLevel: "info"
1365
1385
  });
1386
+ let ipcActions = null;
1387
+ if (env.NAPUTO_IPC === "1") {
1388
+ ipcActions = createIpcActionsForCore(core);
1389
+ startIpcServer({ actions: ipcActions });
1390
+ }
1366
1391
  const ctx = core.attachWrapper(state.wrapperExports, { ...bootEnv });
1367
1392
  log(`bootstrap: attachWrapper OK, engine=${typeof ctx.engine}, session=${ctx.session !== null}`);
1368
1393
  probeCreatedSession(state, ctx);
@@ -1396,7 +1421,7 @@ async function bootstrapWithCore(kernel, state, bootEnv, Appid) {
1396
1421
  if (env.NAPUTO_IPC === "1") sendStatus("sessioning");
1397
1422
  await runSmokeIfEnabled(kernel, ctx, loginResult);
1398
1423
  const services = await startProtocols(kernel, ctx, loginResult);
1399
- if (env.NAPUTO_IPC === "1" && services !== null) startIpcMode(services);
1424
+ if (env.NAPUTO_IPC === "1" && services !== null && ipcActions !== null) attachIpcServices(ipcActions, services);
1400
1425
  runProbePhase(kernel, ctx);
1401
1426
  }
1402
1427
  //#endregion
package/dist/index.cjs CHANGED
@@ -670,6 +670,8 @@ function resolveDownloadUrl(release) {
670
670
  const QQ_FILES_DIR_NAME = "qq-files";
671
671
  /** 注册表 UninstallString 查询(QQ 官方安装路径)。 */
672
672
  const REG_QUERY_RE = /"([^"]+)"/;
673
+ /** Windows 盘符路径正则(`C:/...` / `C:\...`,wslMappedPath 映射用,模块级常量)。 */
674
+ const DRIVE_PATH_RE = /^([A-Za-z]):[\\/](.*)$/;
673
675
  /** 注册表 UninstallString 查询(QQ 官方安装路径)。 */
674
676
  function findQqViaRegistry() {
675
677
  try {
@@ -696,9 +698,27 @@ function findQqViaCommonPaths() {
696
698
  (0, node_path.join)(localAppData, "Programs", "Tencent", "QQNT", "QQ.exe"),
697
699
  "C:/Dev/QQBot-Dev/QQNT/QQ.exe"
698
700
  ];
699
- for (const p of candidates) if ((0, node_fs.existsSync)(p)) return p;
701
+ for (const p of candidates) {
702
+ const wsl = wslMappedPath(p);
703
+ if ((0, node_fs.existsSync)(wsl)) return wsl;
704
+ if ((0, node_fs.existsSync)(p)) return p;
705
+ }
700
706
  return null;
701
707
  }
708
+ /**
709
+ * Windows 路径 → WSL 挂载路径(`C:/...` → `/mnt/c/...`;非 Linux 或非盘符路径原样)。
710
+ * WSL 默认把 Windows 盘符挂载到 `/mnt/<小写盘符>/`,Linux 侧 `existsSync` 必须用映射路径。
711
+ * platform 参数可注入(测试用;缺省 process.platform)。
712
+ * 导出便于单测(locate-qq.test.ts)。
713
+ * ⚠️ 不用 join 拼 `/mnt/...`:测试在 Windows 跑,win32 join 会产生反斜杠;目标路径
714
+ * 是 Linux 侧路径(WSL 挂载点),必须恒正斜杠。
715
+ */
716
+ function wslMappedPath(winPath, platform = node_process.default.platform) {
717
+ if (platform !== "linux") return winPath;
718
+ const [, drive, rest] = DRIVE_PATH_RE.exec(winPath) ?? [];
719
+ if (drive === void 0 || rest === void 0) return winPath;
720
+ return `/mnt/${drive.toLowerCase()}/${rest.replaceAll("\\", "/")}`;
721
+ }
702
722
  /** 找 QQ.exe 路径。 */
703
723
  function locateQqPath() {
704
724
  const viaReg = findQqViaRegistry();
@@ -726,7 +746,7 @@ function resolveFromRoot(rootDir, source) {
726
746
  /** 从 QQ.exe 路径推导安装目录,并探测当前版本(L1:本机安装)。 */
727
747
  function resolveQqInstall(qqPath) {
728
748
  const qq = qqPath ?? node_process.default.env["NAPUTO_QQ_PATH"] ?? locateQqPath();
729
- return resolveFromRoot(qq.slice(0, qq.lastIndexOf("\\")), "local");
749
+ return resolveFromRoot((0, node_path.dirname)(qq), "local");
730
750
  }
731
751
  /**
732
752
  * 多级来源定位 QQ 原生文件:
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.cts","names":[],"sources":["../src/instance-lock.ts","../src/locate-qq.ts","../src/launcher.ts","../src/qq-download.ts","../src/qq-extract.ts","../src/qq-releases.ts","../src/win-node.ts","../src/wine.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;cA2Ba;;UAGI;;EAEb;;EAEA;;EAEA;;;UAIa;;EAEb;;EAEA;;EAEA;;;;;;;iBAQY,kBAAkB,kBAAkB;;;;;iBA+BpC,oBAAoB,iBAAiB;;;;;iBA4BrC,oBAAoB;;iBAapB,oBAAoB;;;;cC5GvB;;KAiDD;;UAGK;;EAEb;;EAEA;;EAEA;;EAEA;;EAEA,QAAQ;;;UAIK;;EAEb;;EAEA;;EAEA;;EAEA;;EAEA;;;iBAIY;;iBAwCA,iBAAiB,kBAAkB;;;;;;iBAY7B,eAAe,UAAS,wBAA6B,QAAQ;;;;;iBA0D7D,cAClB;;EAEI;;EAEA;IAEL,QAAQ;;;UChMM;;EAEb,IAAI;;EAEJ;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;;;;;;;EAQA;;;;;;EAMA,QAAQ;;UAGK;;EAEb,OAAO,kBAAkB;;EAEzB;;;;;;;;;;;;;iBAckB,eAAe,SAAS,gBAAgB,QAAQ;;;;;iBAoFtD;;cA4CH;WACT;WACA;WACA;WACA;WACA;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;;;;UCnNa;;EAEb;;EAEA;;EAEA;;EAEA;;;UAIa;;EAEb;;EAEA;;;cAIS,sBAAsB;EAC/B,YAAY;;;;;;iBA8CM,aAAa,SAAS,kBAAkB,QAAQ;;;;UCrDrD;;EAEb;;EAEA;;;UAIa;;EAEb;;EAEA;;EAEA;;EAEA;;;;;;;iBAQY,gBAAgB;;iBAuBV,iBAClB,uBACA,iBACA,wBACD;;;;;;iBA8BmB,oBAClB,sBACA,iBACA,oBACD,QAAQ;;iBAkDW,kBAAkB,mBAAmB,kBAAkB;;;;UC1J5D;;EAEb;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;;UAIa;EACb;EACA,OAAO;;;cAIE,wBAAwB;EACjC,YAAY;;;iBAYA,kBAAkB;;iBAoBlB,cAAc,UAAU,iBAAiB;;;;;iBAYzC,mBAAmB,SAAS;;;;cC3D/B;;UAGI;;EAEb;;EAEA;;;iBAIY,WAAW;;;;;iBAQL,cAClB;;EAEI;;EAEA;;EAEA;IAEL,QAAQ;;;;;;;;;;;;;;;iBCpCK,WAAW;;iBAiBX;;iBAKA;;UAKC;;EAEb;;EAEA;;;iBAIY,kBAAkB;;EAE9B,WAAW,OAAO;;EAElB;;EAEA;;EAEA;IACA"}
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../src/instance-lock.ts","../src/locate-qq.ts","../src/launcher.ts","../src/qq-download.ts","../src/qq-extract.ts","../src/qq-releases.ts","../src/win-node.ts","../src/wine.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;cA2Ba;;UAGI;;EAEb;;EAEA;;EAEA;;;UAIa;;EAEb;;EAEA;;EAEA;;;;;;;iBAQY,kBAAkB,kBAAkB;;;;;iBA+BpC,oBAAoB,iBAAiB;;;;;iBA4BrC,oBAAoB;;iBAapB,oBAAoB;;;;cC5GvB;;KAgFD;;UAGK;;EAEb;;EAEA;;EAEA;;EAEA;;EAEA,QAAQ;;;UAIK;;EAEb;;EAEA;;EAEA;;EAEA;;EAEA;;;iBAIY;;iBAwCA,iBAAiB,kBAAkB;;;;;;iBAa7B,eAAe,UAAS,wBAA6B,QAAQ;;;;;iBA0D7D,cAClB;;EAEI;;EAEA;IAEL,QAAQ;;;UChOM;;EAEb,IAAI;;EAEJ;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;;;;;;;EAQA;;;;;;EAMA,QAAQ;;UAGK;;EAEb,OAAO,kBAAkB;;EAEzB;;;;;;;;;;;;;iBAckB,eAAe,SAAS,gBAAgB,QAAQ;;;;;iBAoFtD;;cA4CH;WACT;WACA;WACA;WACA;WACA;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;;;;UCnNa;;EAEb;;EAEA;;EAEA;;EAEA;;;UAIa;;EAEb;;EAEA;;;cAIS,sBAAsB;EAC/B,YAAY;;;;;;iBA8CM,aAAa,SAAS,kBAAkB,QAAQ;;;;UCrDrD;;EAEb;;EAEA;;;UAIa;;EAEb;;EAEA;;EAEA;;EAEA;;;;;;;iBAQY,gBAAgB;;iBAuBV,iBAClB,uBACA,iBACA,wBACD;;;;;;iBA8BmB,oBAClB,sBACA,iBACA,oBACD,QAAQ;;iBAkDW,kBAAkB,mBAAmB,kBAAkB;;;;UC1J5D;;EAEb;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;;UAIa;EACb;EACA,OAAO;;;cAIE,wBAAwB;EACjC,YAAY;;;iBAYA,kBAAkB;;iBAoBlB,cAAc,UAAU,iBAAiB;;;;;iBAYzC,mBAAmB,SAAS;;;;cC3D/B;;UAGI;;EAEb;;EAEA;;;iBAIY,WAAW;;;;;iBAQL,cAClB;;EAEI;;EAEA;;EAEA;IAEL,QAAQ;;;;;;;;;;;;;;;iBCpCK,WAAW;;iBAiBX;;iBAKA;;UAKC;;EAEb;;EAEA;;;iBAIY,kBAAkB;;EAE9B,WAAW,OAAO;;EAElB;;EAEA;;EAEA;IACA"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/instance-lock.ts","../src/locate-qq.ts","../src/launcher.ts","../src/qq-download.ts","../src/qq-extract.ts","../src/qq-releases.ts","../src/win-node.ts","../src/wine.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;cA2Ba;;UAGI;;EAEb;;EAEA;;EAEA;;;UAIa;;EAEb;;EAEA;;EAEA;;;;;;;iBAQY,kBAAkB,kBAAkB;;;;;iBA+BpC,oBAAoB,iBAAiB;;;;;iBA4BrC,oBAAoB;;iBAapB,oBAAoB;;;;cC5GvB;;KAiDD;;UAGK;;EAEb;;EAEA;;EAEA;;EAEA;;EAEA,QAAQ;;;UAIK;;EAEb;;EAEA;;EAEA;;EAEA;;EAEA;;;iBAIY;;iBAwCA,iBAAiB,kBAAkB;;;;;;iBAY7B,eAAe,UAAS,wBAA6B,QAAQ;;;;;iBA0D7D,cAClB;;EAEI;;EAEA;IAEL,QAAQ;;;UChMM;;EAEb,IAAI;;EAEJ;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;;;;;;;EAQA;;;;;;EAMA,QAAQ;;UAGK;;EAEb,OAAO,kBAAkB;;EAEzB;;;;;;;;;;;;;iBAckB,eAAe,SAAS,gBAAgB,QAAQ;;;;;iBAoFtD;;cA4CH;WACT;WACA;WACA;WACA;WACA;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;;;;UCnNa;;EAEb;;EAEA;;EAEA;;EAEA;;;UAIa;;EAEb;;EAEA;;;cAIS,sBAAsB;EAC/B,YAAY;;;;;;iBA8CM,aAAa,SAAS,kBAAkB,QAAQ;;;;UCrDrD;;EAEb;;EAEA;;;UAIa;;EAEb;;EAEA;;EAEA;;EAEA;;;;;;;iBAQY,gBAAgB;;iBAuBV,iBAClB,uBACA,iBACA,wBACD;;;;;;iBA8BmB,oBAClB,sBACA,iBACA,oBACD,QAAQ;;iBAkDW,kBAAkB,mBAAmB,kBAAkB;;;;UC1J5D;;EAEb;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;;UAIa;EACb;EACA,OAAO;;;cAIE,wBAAwB;EACjC,YAAY;;;iBAYA,kBAAkB;;iBAoBlB,cAAc,UAAU,iBAAiB;;;;;iBAYzC,mBAAmB,SAAS;;;;cC3D/B;;UAGI;;EAEb;;EAEA;;;iBAIY,WAAW;;;;;iBAQL,cAClB;;EAEI;;EAEA;;EAEA;IAEL,QAAQ;;;;;;;;;;;;;;;iBCpCK,WAAW;;iBAiBX;;iBAKA;;UAKC;;EAEb;;EAEA;;;iBAIY,kBAAkB;;EAE9B,WAAW,OAAO;;EAElB;;EAEA;;EAEA;IACA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/instance-lock.ts","../src/locate-qq.ts","../src/launcher.ts","../src/qq-download.ts","../src/qq-extract.ts","../src/qq-releases.ts","../src/win-node.ts","../src/wine.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;cA2Ba;;UAGI;;EAEb;;EAEA;;EAEA;;;UAIa;;EAEb;;EAEA;;EAEA;;;;;;;iBAQY,kBAAkB,kBAAkB;;;;;iBA+BpC,oBAAoB,iBAAiB;;;;;iBA4BrC,oBAAoB;;iBAapB,oBAAoB;;;;cC5GvB;;KAgFD;;UAGK;;EAEb;;EAEA;;EAEA;;EAEA;;EAEA,QAAQ;;;UAIK;;EAEb;;EAEA;;EAEA;;EAEA;;EAEA;;;iBAIY;;iBAwCA,iBAAiB,kBAAkB;;;;;;iBAa7B,eAAe,UAAS,wBAA6B,QAAQ;;;;;iBA0D7D,cAClB;;EAEI;;EAEA;IAEL,QAAQ;;;UChOM;;EAEb,IAAI;;EAEJ;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;;;;;;;EAQA;;;;;;EAMA,QAAQ;;UAGK;;EAEb,OAAO,kBAAkB;;EAEzB;;;;;;;;;;;;;iBAckB,eAAe,SAAS,gBAAgB,QAAQ;;;;;iBAoFtD;;cA4CH;WACT;WACA;WACA;WACA;WACA;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;;;;UCnNa;;EAEb;;EAEA;;EAEA;;EAEA;;;UAIa;;EAEb;;EAEA;;;cAIS,sBAAsB;EAC/B,YAAY;;;;;;iBA8CM,aAAa,SAAS,kBAAkB,QAAQ;;;;UCrDrD;;EAEb;;EAEA;;;UAIa;;EAEb;;EAEA;;EAEA;;EAEA;;;;;;;iBAQY,gBAAgB;;iBAuBV,iBAClB,uBACA,iBACA,wBACD;;;;;;iBA8BmB,oBAClB,sBACA,iBACA,oBACD,QAAQ;;iBAkDW,kBAAkB,mBAAmB,kBAAkB;;;;UC1J5D;;EAEb;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;;UAIa;EACb;EACA,OAAO;;;cAIE,wBAAwB;EACjC,YAAY;;;iBAYA,kBAAkB;;iBAoBlB,cAAc,UAAU,iBAAiB;;;;;iBAYzC,mBAAmB,SAAS;;;;cC3D/B;;UAGI;;EAEb;;EAEA;;;iBAIY,WAAW;;;;;iBAQL,cAClB;;EAEI;;EAEA;;EAEA;IAEL,QAAQ;;;;;;;;;;;;;;;iBCpCK,WAAW;;iBAiBX;;iBAKA;;UAKC;;EAEb;;EAEA;;;iBAIY,kBAAkB;;EAE9B,WAAW,OAAO;;EAElB;;EAEA;;EAEA;IACA"}
package/dist/index.mjs CHANGED
@@ -646,6 +646,8 @@ function resolveDownloadUrl(release) {
646
646
  const QQ_FILES_DIR_NAME = "qq-files";
647
647
  /** 注册表 UninstallString 查询(QQ 官方安装路径)。 */
648
648
  const REG_QUERY_RE = /"([^"]+)"/;
649
+ /** Windows 盘符路径正则(`C:/...` / `C:\...`,wslMappedPath 映射用,模块级常量)。 */
650
+ const DRIVE_PATH_RE = /^([A-Za-z]):[\\/](.*)$/;
649
651
  /** 注册表 UninstallString 查询(QQ 官方安装路径)。 */
650
652
  function findQqViaRegistry() {
651
653
  try {
@@ -672,9 +674,27 @@ function findQqViaCommonPaths() {
672
674
  join(localAppData, "Programs", "Tencent", "QQNT", "QQ.exe"),
673
675
  "C:/Dev/QQBot-Dev/QQNT/QQ.exe"
674
676
  ];
675
- for (const p of candidates) if (existsSync(p)) return p;
677
+ for (const p of candidates) {
678
+ const wsl = wslMappedPath(p);
679
+ if (existsSync(wsl)) return wsl;
680
+ if (existsSync(p)) return p;
681
+ }
676
682
  return null;
677
683
  }
684
+ /**
685
+ * Windows 路径 → WSL 挂载路径(`C:/...` → `/mnt/c/...`;非 Linux 或非盘符路径原样)。
686
+ * WSL 默认把 Windows 盘符挂载到 `/mnt/<小写盘符>/`,Linux 侧 `existsSync` 必须用映射路径。
687
+ * platform 参数可注入(测试用;缺省 process.platform)。
688
+ * 导出便于单测(locate-qq.test.ts)。
689
+ * ⚠️ 不用 join 拼 `/mnt/...`:测试在 Windows 跑,win32 join 会产生反斜杠;目标路径
690
+ * 是 Linux 侧路径(WSL 挂载点),必须恒正斜杠。
691
+ */
692
+ function wslMappedPath(winPath, platform = process$1.platform) {
693
+ if (platform !== "linux") return winPath;
694
+ const [, drive, rest] = DRIVE_PATH_RE.exec(winPath) ?? [];
695
+ if (drive === void 0 || rest === void 0) return winPath;
696
+ return `/mnt/${drive.toLowerCase()}/${rest.replaceAll("\\", "/")}`;
697
+ }
678
698
  /** 找 QQ.exe 路径。 */
679
699
  function locateQqPath() {
680
700
  const viaReg = findQqViaRegistry();
@@ -702,7 +722,7 @@ function resolveFromRoot(rootDir, source) {
702
722
  /** 从 QQ.exe 路径推导安装目录,并探测当前版本(L1:本机安装)。 */
703
723
  function resolveQqInstall(qqPath) {
704
724
  const qq = qqPath ?? process$1.env["NAPUTO_QQ_PATH"] ?? locateQqPath();
705
- return resolveFromRoot(qq.slice(0, qq.lastIndexOf("\\")), "local");
725
+ return resolveFromRoot(dirname(qq), "local");
706
726
  }
707
727
  /**
708
728
  * 多级来源定位 QQ 原生文件:
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["process","process","process","process","findProjectRoot","process","wineBinary","process","process"],"sources":["../src/instance-lock.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 * 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 * 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 } 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\";\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\": \"napuketto-loader/0.0.x\" },\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 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 环境变量 > 内置资产(assets/7zip/7z.exe,Windows)> 系统 PATH 7z。\n * Linux 上用 p7zip 提供的 7z 命令(系统 PATH)。\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\") {\n const bundled = join(\n dirname(fileURLToPath(import.meta.url)),\n \"..\",\n \"assets\",\n \"7zip\",\n \"7z.exe\",\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 指定;Linux 请安装 p7zip-full)\"\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 { dirname, join, resolve } from \"node:path\";\nimport process from \"node:process\";\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/** 默认 Windows node 版本(latest v24,LTS 线;可被环境变量/参数覆盖)。 */\nconst DEFAULT_WIN_NODE_VERSION = \"v24.16.0\";\n\n/** 轻量数据根解析(与 locate-qq 内同构,避免循环依赖)。 */\nfunction 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 * 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/** wine 可执行文件(Linux 上 spawn 用;可被 NAPUTO_WINE 覆盖)。 */\nexport function wineBinary(): string {\n return process.env[\"NAPUTO_WINE\"] ?? \"wine\";\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 } from \"node:child_process\";\nimport { existsSync, mkdirSync } from \"node:fs\";\nimport { dirname, join, resolve } from \"node:path\";\nimport process from \"node:process\";\nimport { fileURLToPath } from \"node:url\";\nimport type { QqInstallInfo } from \"./locate-qq.js\";\nimport { ensureWinNode } from \"./win-node.js\";\nimport { buildSpawnCommand, isLinux, toWinePath } 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 /** 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 场景覆盖;缺省 ensureWinNode 下载)。 */\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\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\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 const { command, args } = buildSpawnCommand({\n ...(useWine && winNodePath !== undefined\n ? { winNodePath }\n : { winNodePath: process.execPath }),\n selfHostPath: useWine ? toWinePath(selfHostPath) : selfHostPath,\n wine: wineBinary(),\n });\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 return { child, bootJsPath: selfHostPath };\n}\n\n/** 平台分支解析 node 可执行:win32 本机;linux wine + Windows 版 node.exe。 */\nasync function resolveNodeExecutable(options: LaunchOptions): Promise<{\n useWine: boolean;\n winNodePath: string | undefined;\n}> {\n if (!isLinux()) {\n return { useWine: false, winNodePath: undefined };\n }\n const winNode = await ensureWinNode({\n ...(options.cwd !== undefined ? { dataRoot: options.cwd } : {}),\n ...(options.winNodePath !== undefined ? { exePath: options.winNodePath } : {}),\n });\n return { useWine: true, winNodePath: 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 const pathEntries = [p(opts.stub), p(dirname(opts.wrapperPath)), process.env[\"PATH\"] ?? \"\"]\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.ipc === true ? { [ENV.IPC]: \"1\" } : {}),\n };\n return { ...env, ...optional };\n}\n\n/** wine 可执行文件(linux 场景;NAPUTO_WINE 可覆盖)。 */\nfunction wineBinary(): string {\n return process.env[\"NAPUTO_WINE\"] ?? \"wine\";\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 /** 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)。清单由 CI/社区 PR 维护\n * (设计文档 §2.2)。NAPUTO_QQ_URL 可运行时覆盖下载地址。\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 解析)。 */\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 { rm } from \"node:fs/promises\";\nimport { dirname, join, resolve } from \"node:path\";\nimport process from \"node:process\";\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/** 注册表 UninstallString 查询(QQ 官方安装路径)。 */\nconst REG_QUERY_RE = /\"([^\"]+)\"/;\n\n/** 注册表 UninstallString 查询(QQ 官方安装路径)。 */\nfunction findQqViaRegistry(): string | null {\n try {\n const out = execFileSync(\"reg\", [\n \"query\",\n \"HKLM\\\\SOFTWARE\\\\WOW6432Node\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Uninstall\\\\QQ\",\n \"/v\",\n \"UninstallString\",\n ]).toString();\n const m = out.match(REG_QUERY_RE);\n const exe = m?.[1];\n if (exe !== undefined) {\n // UninstallString 形如 \"C:\\...\\uninst.exe\" 或带参数\n const dir = exe.replaceAll('\"', \"\").slice(0, exe.lastIndexOf(\"\\\\\"));\n const qq = join(dir, \"QQ.exe\");\n if (existsSync(qq)) {\n return qq;\n }\n }\n } catch {\n // 注册表查询失败,走常见路径\n }\n return null;\n}\n\n/** 常见安装路径探测。 */\nfunction findQqViaCommonPaths(): string | null {\n const localAppData = process.env[\"LOCALAPPDATA\"] ?? \"\";\n const candidates = [\n \"C:/Program Files/Tencent/QQNT/QQ.exe\",\n \"C:/Program Files (x86)/Tencent/QQNT/QQ.exe\",\n join(localAppData, \"Programs\", \"Tencent\", \"QQNT\", \"QQ.exe\"),\n // 开发机目录(2026-08-07 环境事实:QQ 9.9.33-51802 在 C:\\Dev\\QQBot-Dev\\QQNT)\n \"C:/Dev/QQBot-Dev/QQNT/QQ.exe\",\n ];\n for (const p of candidates) {\n if (existsSync(p)) {\n return p;\n }\n }\n return null;\n}\n\n/** QQ 文件来源。 */\nexport 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\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 const installDir = qq.slice(0, qq.lastIndexOf(\"\\\\\"));\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 = resolveDataRootLight(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({ dataRoot, ...sevenZipPath });\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 * 确保 QQ 原生文件就绪(P1):幂等缓存检查 → 下载官方安装包 → sha256 校验\n * → 7z 解包 → 提取 wrapper.node/QQNT.dll → 缓存。返回缓存版本 QqInstallInfo。\n */\nexport async function ensureQqFiles(\n options: {\n /** 数据根(缓存 <数据根>/qq-files/)。 */\n dataRoot?: string;\n /** 7z 路径(缺省自动探测)。 */\n sevenZipPath?: string;\n } = {},\n): Promise<QqInstallInfo> {\n const dataRoot = resolveDataRootLight(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 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 const installerPath = join(tmpDir, `qq-installer-${version}.exe`);\n await downloadFile({ dest: installerPath, url, expectedSha256: release.sha256 });\n\n // 3. 解包 + 提取(失败清理缓存残留;临时文件无论如何清理)\n const extractedDir = join(tmpDir, `qq-extracted-${version}`);\n try {\n await extractInstaller(installerPath, extractedDir, options.sevenZipPath);\n const info = await extractWrapperFiles(extractedDir, version, cacheRoot);\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\n/**\n * 轻量数据根解析(loader 侧,避免新增 kernel 依赖;P2 若需完整语义再切换)。\n * 优先级:显式参数 > NAPKETTO_DATA > 项目根/.napuketto > cwd 兜底。\n */\nfunction resolveDataRootLight(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"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["process","process","process","process","findProjectRoot","process","wineBinary","process","process"],"sources":["../src/instance-lock.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 * 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 * 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 } 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\";\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\": \"napuketto-loader/0.0.x\" },\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 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 环境变量 > 内置资产(assets/7zip/7z.exe,Windows)> 系统 PATH 7z。\n * Linux 上用 p7zip 提供的 7z 命令(系统 PATH)。\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\") {\n const bundled = join(\n dirname(fileURLToPath(import.meta.url)),\n \"..\",\n \"assets\",\n \"7zip\",\n \"7z.exe\",\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 指定;Linux 请安装 p7zip-full)\"\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 { dirname, join, resolve } from \"node:path\";\nimport process from \"node:process\";\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/** 默认 Windows node 版本(latest v24,LTS 线;可被环境变量/参数覆盖)。 */\nconst DEFAULT_WIN_NODE_VERSION = \"v24.16.0\";\n\n/** 轻量数据根解析(与 locate-qq 内同构,避免循环依赖)。 */\nfunction 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 * 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/** wine 可执行文件(Linux 上 spawn 用;可被 NAPUTO_WINE 覆盖)。 */\nexport function wineBinary(): string {\n return process.env[\"NAPUTO_WINE\"] ?? \"wine\";\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 } from \"node:child_process\";\nimport { existsSync, mkdirSync } from \"node:fs\";\nimport { dirname, join, resolve } from \"node:path\";\nimport process from \"node:process\";\nimport { fileURLToPath } from \"node:url\";\nimport type { QqInstallInfo } from \"./locate-qq.js\";\nimport { ensureWinNode } from \"./win-node.js\";\nimport { buildSpawnCommand, isLinux, toWinePath } 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 /** 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 场景覆盖;缺省 ensureWinNode 下载)。 */\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\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\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 const { command, args } = buildSpawnCommand({\n ...(useWine && winNodePath !== undefined\n ? { winNodePath }\n : { winNodePath: process.execPath }),\n selfHostPath: useWine ? toWinePath(selfHostPath) : selfHostPath,\n wine: wineBinary(),\n });\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 return { child, bootJsPath: selfHostPath };\n}\n\n/** 平台分支解析 node 可执行:win32 本机;linux wine + Windows 版 node.exe。 */\nasync function resolveNodeExecutable(options: LaunchOptions): Promise<{\n useWine: boolean;\n winNodePath: string | undefined;\n}> {\n if (!isLinux()) {\n return { useWine: false, winNodePath: undefined };\n }\n const winNode = await ensureWinNode({\n ...(options.cwd !== undefined ? { dataRoot: options.cwd } : {}),\n ...(options.winNodePath !== undefined ? { exePath: options.winNodePath } : {}),\n });\n return { useWine: true, winNodePath: 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 const pathEntries = [p(opts.stub), p(dirname(opts.wrapperPath)), process.env[\"PATH\"] ?? \"\"]\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.ipc === true ? { [ENV.IPC]: \"1\" } : {}),\n };\n return { ...env, ...optional };\n}\n\n/** wine 可执行文件(linux 场景;NAPUTO_WINE 可覆盖)。 */\nfunction wineBinary(): string {\n return process.env[\"NAPUTO_WINE\"] ?? \"wine\";\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 /** 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)。清单由 CI/社区 PR 维护\n * (设计文档 §2.2)。NAPUTO_QQ_URL 可运行时覆盖下载地址。\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 解析)。 */\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 { rm } from \"node:fs/promises\";\nimport { dirname, join, resolve } from \"node:path\";\nimport process from \"node:process\";\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/** 注册表 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 // 开发机目录(2026-08-07 环境事实:QQ 9.9.33-51802 在 C:\\Dev\\QQBot-Dev\\QQNT)\n \"C:/Dev/QQBot-Dev/QQNT/QQ.exe\",\n ];\n for (const p of candidates) {\n // 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\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 = resolveDataRootLight(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({ dataRoot, ...sevenZipPath });\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 * 确保 QQ 原生文件就绪(P1):幂等缓存检查 → 下载官方安装包 → sha256 校验\n * → 7z 解包 → 提取 wrapper.node/QQNT.dll → 缓存。返回缓存版本 QqInstallInfo。\n */\nexport async function ensureQqFiles(\n options: {\n /** 数据根(缓存 <数据根>/qq-files/)。 */\n dataRoot?: string;\n /** 7z 路径(缺省自动探测)。 */\n sevenZipPath?: string;\n } = {},\n): Promise<QqInstallInfo> {\n const dataRoot = resolveDataRootLight(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 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 const installerPath = join(tmpDir, `qq-installer-${version}.exe`);\n await downloadFile({ dest: installerPath, url, expectedSha256: release.sha256 });\n\n // 3. 解包 + 提取(失败清理缓存残留;临时文件无论如何清理)\n const extractedDir = join(tmpDir, `qq-extracted-${version}`);\n try {\n await extractInstaller(installerPath, extractedDir, options.sevenZipPath);\n const info = await extractWrapperFiles(extractedDir, version, cacheRoot);\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\n/**\n * 轻量数据根解析(loader 侧,避免新增 kernel 依赖;P2 若需完整语义再切换)。\n * 优先级:显式参数 > NAPKETTO_DATA > 项目根/.napuketto > cwd 兜底。\n */\nfunction resolveDataRootLight(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"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@napuketto/loader",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "description": "NapukettoQQ 引导组件:自建宿主(标准 Node + stub QQNT.dll)dlopen wrapper.node 并启动 kernel,不拉起 QQ / 不注入",
5
5
  "type": "module",
6
6
  "files": [