@napuketto/loader 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/dist/host/self-host.cjs +917 -258
- package/dist/index.d.mts +66 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +111 -4
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,65 @@
|
|
|
1
1
|
import { StdioOptions, spawn } from "node:child_process";
|
|
2
|
+
//#region src/instance-lock.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* instance-lock.ts:单实例锁(2026-08-07 根治「多实例抢数据目录锁挂起」)
|
|
5
|
+
*
|
|
6
|
+
* 背景(实测):同一账号数据目录(~/.napuketto/<uin>)只能被一个实例持有——
|
|
7
|
+
* QQ 原生层(MMKV / 登录单例 / 数据库)有锁,第二个实例抢不到锁会卡在
|
|
8
|
+
* loginService.initConfig 之后无响应(挂起)。此前 create-napukettoqq 生成的
|
|
9
|
+
* 副本实例常驻占锁,导致用户后续启动全部卡死。
|
|
10
|
+
*
|
|
11
|
+
* 方案:以数据目录为粒度加锁文件 `<dataDir>/instance.lock`(JSON 内容:
|
|
12
|
+
* pid / startedAt / cmdline 摘要)。启动前 checkInstanceLock:
|
|
13
|
+
* - 锁不存在 → 可启动(然后 acquireInstanceLock 写入)
|
|
14
|
+
* - 锁存在但 PID 已死 → 崩溃残留,自动清理(rm + 重新获取)
|
|
15
|
+
* - 锁存在且 PID 存活 → 已被占用,拒绝启动(返回占用 PID 供提示)
|
|
16
|
+
*
|
|
17
|
+
* 退出清理:进程正常退出时 releaseInstanceLock 删除锁文件;崩溃/强杀残留
|
|
18
|
+
* 由下次启动的「PID 存活检测」兜底(PID 已死 → 残留视为过期)。
|
|
19
|
+
*
|
|
20
|
+
* 放置说明:放 loader 而非 kernel——self-host.cjs 是 rolldown 内联 bundle
|
|
21
|
+
* (只内联 src/host/ 依赖树),静态 import kernel 会留 external 缺口;
|
|
22
|
+
* 放 loader 则 self-host(内联)与 cli(@napuketto/loader 导入)双入口可用。
|
|
23
|
+
*/
|
|
24
|
+
/** 锁文件名(置于数据目录内)。 */
|
|
25
|
+
declare const INSTANCE_LOCK_FILE = "instance.lock";
|
|
26
|
+
/** 锁文件内容。 */
|
|
27
|
+
interface InstanceLockInfo {
|
|
28
|
+
/** 持有实例的进程 PID。 */
|
|
29
|
+
pid: number;
|
|
30
|
+
/** 获取锁的时间(epoch ms)。 */
|
|
31
|
+
startedAt: number;
|
|
32
|
+
/** 命令行摘要(诊断用,可选)。 */
|
|
33
|
+
cmdline?: string;
|
|
34
|
+
}
|
|
35
|
+
/** 数据目录锁检查结果。 */
|
|
36
|
+
interface InstanceLockCheck {
|
|
37
|
+
/** 是否被其他实例占用。 */
|
|
38
|
+
occupied: boolean;
|
|
39
|
+
/** 占用进程 PID(occupied=true 时有效)。 */
|
|
40
|
+
pid: number | null;
|
|
41
|
+
/** 锁文件完整路径(提示/清理用)。 */
|
|
42
|
+
lockPath: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 检测指定数据目录是否已被其他实例占用。
|
|
46
|
+
*
|
|
47
|
+
* @param dataDir 数据目录(账号级,如 ~/.napuketto/3567141148)
|
|
48
|
+
*/
|
|
49
|
+
declare function checkInstanceLock(dataDir: string): InstanceLockCheck;
|
|
50
|
+
/**
|
|
51
|
+
* 获取数据目录锁。占用(其他实例存活)返回 false;成功或清理残留后返回 true。
|
|
52
|
+
* 幂等:本进程已持有则直接返回 true(不重复写)。
|
|
53
|
+
*/
|
|
54
|
+
declare function acquireInstanceLock(dataDir: string, cmdline?: string): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* 释放数据目录锁(正常退出时调用)。仅当持有者是本进程时才删除,
|
|
57
|
+
* 避免误删新持有者的锁(PID 复用 / 快速重启场景)。
|
|
58
|
+
*/
|
|
59
|
+
declare function releaseInstanceLock(dataDir: string): void;
|
|
60
|
+
/** 注册进程退出清理(SIGINT/SIGTERM/exit 均触发;崩溃强杀由残留检测兜底)。 */
|
|
61
|
+
declare function registerLockCleanup(dataDir: string): void;
|
|
62
|
+
//#endregion
|
|
2
63
|
//#region src/locate-qq.d.ts
|
|
3
64
|
/** QQ 安装目录探测结果。 */
|
|
4
65
|
interface QqInstallInfo {
|
|
@@ -36,6 +97,8 @@ interface LaunchOptions {
|
|
|
36
97
|
stubDir?: string;
|
|
37
98
|
/** 强制指定快速登录账号(注入 NAPUTO_QUICK_UIN;cli `-q <uin>` 透传,2026-08-07)。 */
|
|
38
99
|
quickUin?: string;
|
|
100
|
+
/** IPC 子进程模式(=1:stdout 走 JSON 行协议 + stdin 收 action/control,koishi 插件驱动)。 */
|
|
101
|
+
ipc?: boolean;
|
|
39
102
|
/** 自建宿主入口(默认 dist/host/self-host.cjs)。 */
|
|
40
103
|
selfHostEntry?: string;
|
|
41
104
|
/**
|
|
@@ -93,7 +156,9 @@ declare const ENV: {
|
|
|
93
156
|
readonly CONFIG_PATH: "NAPKETTO_CONFIG";
|
|
94
157
|
/** 强制指定快速登录账号(cli `-q <uin>` 透传,bootstrap 登录用)。 */
|
|
95
158
|
readonly QUICK_UIN: "NAPUTO_QUICK_UIN";
|
|
159
|
+
/** IPC 子进程模式(koishi 插件驱动:stdout JSON 行 + stdin action/control)。 */
|
|
160
|
+
readonly IPC: "NAPUTO_IPC";
|
|
96
161
|
};
|
|
97
162
|
//#endregion
|
|
98
|
-
export { ENV, type LaunchOptions, type LaunchResult, type QqInstallInfo, defaultStubDir, launchSelfHost, locateQqPath, resolveQqInstall };
|
|
163
|
+
export { ENV, INSTANCE_LOCK_FILE, type InstanceLockCheck, type InstanceLockInfo, type LaunchOptions, type LaunchResult, type QqInstallInfo, acquireInstanceLock, checkInstanceLock, defaultStubDir, launchSelfHost, locateQqPath, registerLockCleanup, releaseInstanceLock, resolveQqInstall };
|
|
99
164
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/locate-qq.ts","../src/launcher.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/instance-lock.ts","../src/locate-qq.ts","../src/launcher.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;cA2Ba;;UAGI;;EAEb;;EAEA;;EAEA;;;UAIa;;EAEb;;EAEA;;EAEA;;;;;;;iBAQY,kBAAkB,kBAAkB;;;;;iBA+BpC,oBAAoB,iBAAiB;;;;;iBA4BrC,oBAAoB;;iBAapB,oBAAoB;;;;UCvEnB;;EAEb;;EAEA;;EAEA;;EAEA;;;iBAIY;;iBAaA,iBAAiB,kBAAkB;;;UC9DlC;;EAEb,IAAI;;EAEJ;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;;;;;;;EAQA;;;;;;EAMA,QAAQ;;UAGK;;EAEb,OAAO,kBAAkB;;EAEzB;;;;;;;;;;;iBAYY,eAAe,SAAS,gBAAgB;;;;;iBAiDxC;;cAwCH;WACT;WACA;WACA;WACA;WACA;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,112 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { existsSync, mkdirSync, readdirSync, statSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
2
|
import { dirname, join, resolve } from "node:path";
|
|
4
3
|
import process from "node:process";
|
|
4
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
//#region src/instance-lock.ts
|
|
7
|
+
/**
|
|
8
|
+
* instance-lock.ts:单实例锁(2026-08-07 根治「多实例抢数据目录锁挂起」)
|
|
9
|
+
*
|
|
10
|
+
* 背景(实测):同一账号数据目录(~/.napuketto/<uin>)只能被一个实例持有——
|
|
11
|
+
* QQ 原生层(MMKV / 登录单例 / 数据库)有锁,第二个实例抢不到锁会卡在
|
|
12
|
+
* loginService.initConfig 之后无响应(挂起)。此前 create-napukettoqq 生成的
|
|
13
|
+
* 副本实例常驻占锁,导致用户后续启动全部卡死。
|
|
14
|
+
*
|
|
15
|
+
* 方案:以数据目录为粒度加锁文件 `<dataDir>/instance.lock`(JSON 内容:
|
|
16
|
+
* pid / startedAt / cmdline 摘要)。启动前 checkInstanceLock:
|
|
17
|
+
* - 锁不存在 → 可启动(然后 acquireInstanceLock 写入)
|
|
18
|
+
* - 锁存在但 PID 已死 → 崩溃残留,自动清理(rm + 重新获取)
|
|
19
|
+
* - 锁存在且 PID 存活 → 已被占用,拒绝启动(返回占用 PID 供提示)
|
|
20
|
+
*
|
|
21
|
+
* 退出清理:进程正常退出时 releaseInstanceLock 删除锁文件;崩溃/强杀残留
|
|
22
|
+
* 由下次启动的「PID 存活检测」兜底(PID 已死 → 残留视为过期)。
|
|
23
|
+
*
|
|
24
|
+
* 放置说明:放 loader 而非 kernel——self-host.cjs 是 rolldown 内联 bundle
|
|
25
|
+
* (只内联 src/host/ 依赖树),静态 import kernel 会留 external 缺口;
|
|
26
|
+
* 放 loader 则 self-host(内联)与 cli(@napuketto/loader 导入)双入口可用。
|
|
27
|
+
*/
|
|
28
|
+
/** 锁文件名(置于数据目录内)。 */
|
|
29
|
+
const INSTANCE_LOCK_FILE = "instance.lock";
|
|
30
|
+
/**
|
|
31
|
+
* 检测指定数据目录是否已被其他实例占用。
|
|
32
|
+
*
|
|
33
|
+
* @param dataDir 数据目录(账号级,如 ~/.napuketto/3567141148)
|
|
34
|
+
*/
|
|
35
|
+
function checkInstanceLock(dataDir) {
|
|
36
|
+
const lockPath = join(dataDir, INSTANCE_LOCK_FILE);
|
|
37
|
+
let info = null;
|
|
38
|
+
try {
|
|
39
|
+
info = JSON.parse(readFileSync(lockPath, "utf-8"));
|
|
40
|
+
} catch {
|
|
41
|
+
return {
|
|
42
|
+
occupied: false,
|
|
43
|
+
pid: null,
|
|
44
|
+
lockPath
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (typeof info.pid !== "number" || Number.isNaN(info.pid) || info.pid <= 0) return {
|
|
48
|
+
occupied: false,
|
|
49
|
+
pid: null,
|
|
50
|
+
lockPath
|
|
51
|
+
};
|
|
52
|
+
let alive = false;
|
|
53
|
+
try {
|
|
54
|
+
process.kill(info.pid, 0);
|
|
55
|
+
alive = true;
|
|
56
|
+
} catch {
|
|
57
|
+
alive = false;
|
|
58
|
+
}
|
|
59
|
+
if (!alive) return {
|
|
60
|
+
occupied: false,
|
|
61
|
+
pid: null,
|
|
62
|
+
lockPath
|
|
63
|
+
};
|
|
64
|
+
return {
|
|
65
|
+
occupied: true,
|
|
66
|
+
pid: info.pid,
|
|
67
|
+
lockPath
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* 获取数据目录锁。占用(其他实例存活)返回 false;成功或清理残留后返回 true。
|
|
72
|
+
* 幂等:本进程已持有则直接返回 true(不重复写)。
|
|
73
|
+
*/
|
|
74
|
+
function acquireInstanceLock(dataDir, cmdline) {
|
|
75
|
+
const { occupied, pid, lockPath } = checkInstanceLock(dataDir);
|
|
76
|
+
if (occupied) {
|
|
77
|
+
if (pid === process.pid) return true;
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
const info = {
|
|
81
|
+
pid: process.pid,
|
|
82
|
+
startedAt: Date.now(),
|
|
83
|
+
...cmdline !== void 0 && cmdline !== "" ? { cmdline } : {}
|
|
84
|
+
};
|
|
85
|
+
try {
|
|
86
|
+
rmSync(lockPath, { force: true });
|
|
87
|
+
writeFileSync(lockPath, JSON.stringify(info), "utf-8");
|
|
88
|
+
return true;
|
|
89
|
+
} catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* 释放数据目录锁(正常退出时调用)。仅当持有者是本进程时才删除,
|
|
95
|
+
* 避免误删新持有者的锁(PID 复用 / 快速重启场景)。
|
|
96
|
+
*/
|
|
97
|
+
function releaseInstanceLock(dataDir) {
|
|
98
|
+
const lockPath = join(dataDir, INSTANCE_LOCK_FILE);
|
|
99
|
+
try {
|
|
100
|
+
if (JSON.parse(readFileSync(lockPath, "utf-8")).pid === process.pid) rmSync(lockPath, { force: true });
|
|
101
|
+
} catch {}
|
|
102
|
+
}
|
|
103
|
+
/** 注册进程退出清理(SIGINT/SIGTERM/exit 均触发;崩溃强杀由残留检测兜底)。 */
|
|
104
|
+
function registerLockCleanup(dataDir) {
|
|
105
|
+
process.on("exit", () => {
|
|
106
|
+
releaseInstanceLock(dataDir);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
//#endregion
|
|
6
110
|
//#region src/launcher.ts
|
|
7
111
|
/**
|
|
8
112
|
* launcher:自建宿主引导(2026-08-07 唯一路线)——spawn 标准 node 跑 self-host.cjs。
|
|
@@ -70,6 +174,7 @@ function buildLaunchEnv(options) {
|
|
|
70
174
|
if (options.networkEntry !== void 0) env[ENV.NETWORK_ENTRY] = resolve(options.networkEntry);
|
|
71
175
|
if (options.configPath !== void 0) env[ENV.CONFIG_PATH] = resolve(options.configPath);
|
|
72
176
|
if (options.quickUin !== void 0) env[ENV.QUICK_UIN] = options.quickUin;
|
|
177
|
+
if (options.ipc === true) env[ENV.IPC] = "1";
|
|
73
178
|
return env;
|
|
74
179
|
}
|
|
75
180
|
/** 环境变量名(self-host.cjs 与 kernel 引导读取)。 */
|
|
@@ -90,7 +195,9 @@ const ENV = {
|
|
|
90
195
|
/** 全局配置文件路径(项目根 napuketto.toml,boot-protocols 读取)。 */
|
|
91
196
|
CONFIG_PATH: "NAPKETTO_CONFIG",
|
|
92
197
|
/** 强制指定快速登录账号(cli `-q <uin>` 透传,bootstrap 登录用)。 */
|
|
93
|
-
QUICK_UIN: "NAPUTO_QUICK_UIN"
|
|
198
|
+
QUICK_UIN: "NAPUTO_QUICK_UIN",
|
|
199
|
+
/** IPC 子进程模式(koishi 插件驱动:stdout JSON 行 + stdin action/control)。 */
|
|
200
|
+
IPC: "NAPUTO_IPC"
|
|
94
201
|
};
|
|
95
202
|
//#endregion
|
|
96
203
|
//#region src/locate-qq.ts
|
|
@@ -154,6 +261,6 @@ function resolveQqInstall(qqPath) {
|
|
|
154
261
|
};
|
|
155
262
|
}
|
|
156
263
|
//#endregion
|
|
157
|
-
export { ENV, defaultStubDir, launchSelfHost, locateQqPath, resolveQqInstall };
|
|
264
|
+
export { ENV, INSTANCE_LOCK_FILE, acquireInstanceLock, checkInstanceLock, defaultStubDir, launchSelfHost, locateQqPath, registerLockCleanup, releaseInstanceLock, resolveQqInstall };
|
|
158
265
|
|
|
159
266
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/launcher.ts","../src/locate-qq.ts"],"sourcesContent":["/**\n * launcher:自建宿主引导(2026-08-07 唯一路线)——spawn 标准 node 跑 self-host.cjs。\n *\n * 历史(V1/路线 B 事实链,已归档 archive/):\n * 1. wrapper.node 只能在 QQ 定制版 Electron 里注册(实测纯 Node/普通 Electron 均 self-register 失败)\n * 2. QQ 是打包应用,禁 NODE_OPTIONS(实测 stderr)\n * → 旧方案:NapukettoBootMain.exe 启动 QQ + 注入 NapukettoWinBootHook.dll(已废弃)\n * → 现方案:stub QQNT.dll 转发宿主符号 → 标准 node 直接 dlopen wrapper.node(自建宿主)\n */\nimport { type StdioOptions, spawn } from \"node:child_process\";\nimport { existsSync, mkdirSync } from \"node:fs\";\nimport { dirname, join, resolve } from \"node:path\";\nimport process from \"node:process\";\nimport { fileURLToPath } from \"node:url\";\nimport type { QqInstallInfo } from \"./locate-qq.js\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\nexport interface LaunchOptions {\n /** QQ 安装信息(qqPath/version/wrapperPath)。 */\n qq: QqInstallInfo;\n /** kernel 入口(.mjs,self-host.cjs 里 import)。 */\n kernelEntry: string;\n /** 配置目录(账号数据目录,NAPUTO_CFG_DIR)。 */\n cfgDir: string;\n /** 全局配置文件路径(项目根 napuketto.toml,注入 NAPKETTO_CONFIG 供装配链读取)。 */\n configPath?: string;\n /** adapter 入口(.mjs,协议装配用)。 */\n adapterEntry?: string;\n /** network 入口(.mjs,协议装配用)。 */\n networkEntry?: string;\n /** 自建宿主:标准 node + stub QQNT.dll 直接引导,不拉起 QQ(2026-08-07 唯一路线)。 */\n selfHost?: boolean;\n /** stub QQNT.dll 目录(PATH 前置,转发 napi_*、uv_* 符号到 node.exe)。 */\n stubDir?: string;\n /** 强制指定快速登录账号(注入 NAPUTO_QUICK_UIN;cli `-q <uin>` 透传,2026-08-07)。 */\n quickUin?: string;\n /** 自建宿主入口(默认 dist/host/self-host.cjs)。 */\n selfHostEntry?: string;\n /**\n * 子进程工作目录(缺省继承父进程 cwd)。\n *\n * ⚠️ 必须指向数据根(~/.napuketto):QQ 原生层在账号上下文就绪前会\n * fallback 到进程 cwd 落盘(如频道库 guild1.db),不指定则污染调用方\n * 目录(实测写入项目根,guild1.db 08-07)。cli 传 dataRoot 根治。\n */\n cwd?: string;\n /**\n * 子进程 stdio(默认 \"inherit\")。cli 传 [\"inherit\",\"pipe\",\"pipe\"] 并接管\n * 子进程 stdout/stderr:逐行过滤 wrapper 原生噪音(MMKV 刷屏、Node 符号\n * 查找失败警告),其余转发——原生 printf 直写 fd 的字节无法从 JS 层拦截。\n */\n stdio?: StdioOptions;\n}\n\nexport interface LaunchResult {\n /** 子进程句柄(自建宿主 node 进程)。 */\n child: ReturnType<typeof spawn>;\n /** 自建宿主入口路径(self-host.cjs)。 */\n bootJsPath: string;\n}\n\n/**\n * 启动自建宿主(路线 A,2026-08-07 产品化):spawn 标准 node 直接跑 self-host.cjs,\n * 不拉起 QQ / 不注入。\n *\n * 关键(HANDOVER-V7 技术数据):wrapper.node 从 QQNT.dll 导入 99 符号\n * (napi_*×40 + uv_*×56 + qq_magic×1 + v8/node mangled×2)——标准 node 无这些宿主\n * 符号,必须 PATH 前置 stub QQNT.dll 目录(转发到 node.exe)+ QQ resources\\app\n * (wrapper.node 同目录依赖 DLL)才能 dlopen 成功。\n */\nexport function launchSelfHost(options: LaunchOptions): LaunchResult {\n const selfHostPath =\n options.selfHostEntry ?? join(__dirname, \"..\", \"dist\", \"host\", \"self-host.cjs\");\n if (!existsSync(selfHostPath)) {\n throw new Error(\n `self-host.cjs 缺失: ${selfHostPath}(先运行 pnpm --filter @napuketto/loader build)`,\n );\n }\n\n // stub QQNT.dll 校验(自建宿主必需:PATH 前置 stub 转发 napi_* 到 node.exe,\n // 否则 wrapper.node dlopen 失败)。默认 loader 包内闭源 submodule native/stub-test-env,\n // 缺失时提示 --stub-dir / NAPUTO_STUB_DIR 指定。\n const stub = options.stubDir ?? defaultStubDir();\n if (!existsSync(join(stub, \"QQNT.dll\"))) {\n throw new Error(\n `stub QQNT.dll 未找到: ${stub}(自建宿主必需:stub 转发 QQNT.dll 符号到 node.exe;` +\n \"请用 --stub-dir 或环境变量 NAPUTO_STUB_DIR 指定 stub 目录)\",\n );\n }\n\n const env = buildLaunchEnv(options);\n // PATH 前置 stub 目录(stub QQNT.dll 转发)+ QQ resources\\app(wrapper.node 依赖 DLL)\n const pathEntries = [stub, dirname(options.qq.wrapperPath), process.env[\"PATH\"] ?? \"\"]\n .filter((p) => p !== undefined && p !== \"\")\n .join(\";\");\n env[\"PATH\"] = pathEntries;\n env[ENV.STUB_DIR] = stub;\n\n // 标准 node 直接跑入口(不拉起 QQ,不注入)\n // cwd 显式指向数据根(cli 传入):QQ 原生层 fallback 落盘(guild1.db 等)\n // 不再污染项目根目录(实测 08-07,GPro 模块初始化早于账号上下文)。\n if (options.cwd !== undefined) {\n mkdirSync(options.cwd, { recursive: true });\n }\n const child = spawn(process.execPath, [selfHostPath], {\n // exactOptionalPropertyTypes:未传 cwd 时不显式写入 undefined(继承父进程)\n ...(options.cwd !== undefined ? { cwd: options.cwd } : {}),\n env,\n stdio: options.stdio ?? \"inherit\",\n windowsHide: false,\n });\n\n return { child, bootJsPath: selfHostPath };\n}\n\n/**\n * 默认 stub QQNT.dll 目录(loader 包内闭源 native/build/stub-test-env,开发机默认)。\n * src 与 native 同层,构建后 dist 与 native 同层——`../native` 恒正确。\n */\nexport function defaultStubDir(): string {\n return join(__dirname, \"..\", \"native\", \"build\", \"stub-test-env\");\n}\n\n/** 装配自建宿主环境变量。 */\nfunction buildLaunchEnv(options: LaunchOptions): Record<string, string> {\n // 配置目录兜底\n const cfg = resolve(options.cfgDir);\n mkdirSync(cfg, { recursive: true });\n\n const env: Record<string, string> = {\n ...process.env,\n [ENV.QQ_PATH]: options.qq.qqPath,\n [ENV.KERNEL_ENTRY]: resolve(options.kernelEntry),\n [ENV.CFG_DIR]: cfg,\n [ENV.QQ_VERSION]: options.qq.version,\n [ENV.WRAPPER_PATH]: options.qq.wrapperPath,\n };\n if (options.selfHost === true) {\n env[ENV.SELF_HOST] = \"1\";\n }\n if (options.adapterEntry !== undefined) {\n env[ENV.ADAPTER_ENTRY] = resolve(options.adapterEntry);\n }\n if (options.networkEntry !== undefined) {\n env[ENV.NETWORK_ENTRY] = resolve(options.networkEntry);\n }\n if (options.configPath !== undefined) {\n env[ENV.CONFIG_PATH] = resolve(options.configPath);\n }\n if (options.quickUin !== undefined) {\n env[ENV.QUICK_UIN] = options.quickUin;\n }\n return env;\n}\n\n/** 环境变量名(self-host.cjs 与 kernel 引导读取)。 */\nexport const ENV = {\n QQ_PATH: \"NAPUTO_QQ_PATH\",\n KERNEL_ENTRY: \"NAPUTO_KERNEL_ENTRY\",\n CFG_DIR: \"NAPUTO_CFG_DIR\",\n QQ_VERSION: \"NAPUTO_QQ_VERSION\",\n WRAPPER_PATH: \"NAPUTO_WRAPPER_PATH\",\n /** 自建宿主:标准 node + stub QQNT.dll 引导(self-host.cjs 分支)。 */\n SELF_HOST: \"NAPUTO_SELF_HOST\",\n /** stub QQNT.dll 目录(launchSelfHost 注入,self-host.cjs 诊断用)。 */\n STUB_DIR: \"NAPUTO_STUB_DIR\",\n /** adapter 包入口(协议装配用)。 */\n ADAPTER_ENTRY: \"NAPUTO_ADAPTER_ENTRY\",\n /** network 包入口(协议装配用)。 */\n NETWORK_ENTRY: \"NAPUTO_NETWORK_ENTRY\",\n /** 全局配置文件路径(项目根 napuketto.toml,boot-protocols 读取)。 */\n CONFIG_PATH: \"NAPKETTO_CONFIG\",\n /** 强制指定快速登录账号(cli `-q <uin>` 透传,bootstrap 登录用)。 */\n QUICK_UIN: \"NAPUTO_QUICK_UIN\",\n} as const;\n","/**\n * 定位 QQ 安装目录与版本(复用 kernel wrapper-version 的探测逻辑)\n */\nimport { execFileSync } from \"node:child_process\";\nimport { existsSync, readdirSync, statSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport process from \"node:process\";\n\n/** 注册表 UninstallString 查询(QQ 官方安装路径)。 */\nconst REG_QUERY_RE = /\"([^\"]+)\"/;\n\n/** 注册表 UninstallString 查询(QQ 官方安装路径)。 */\nfunction findQqViaRegistry(): string | null {\n try {\n const out = execFileSync(\"reg\", [\n \"query\",\n \"HKLM\\\\SOFTWARE\\\\WOW6432Node\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Uninstall\\\\QQ\",\n \"/v\",\n \"UninstallString\",\n ]).toString();\n const m = out.match(REG_QUERY_RE);\n const exe = m?.[1];\n if (exe !== undefined) {\n // UninstallString 形如 \"C:\\...\\uninst.exe\" 或带参数\n const dir = exe.replaceAll('\"', \"\").slice(0, exe.lastIndexOf(\"\\\\\"));\n const qq = join(dir, \"QQ.exe\");\n if (existsSync(qq)) {\n return qq;\n }\n }\n } catch {\n // 注册表查询失败,走常见路径\n }\n return null;\n}\n\n/** 常见安装路径探测。 */\nfunction findQqViaCommonPaths(): string | null {\n const localAppData = process.env[\"LOCALAPPDATA\"] ?? \"\";\n const candidates = [\n \"C:/Program Files/Tencent/QQNT/QQ.exe\",\n \"C:/Program Files (x86)/Tencent/QQNT/QQ.exe\",\n join(localAppData, \"Programs\", \"Tencent\", \"QQNT\", \"QQ.exe\"),\n // 开发机目录(2026-08-07 环境事实:QQ 9.9.33-51802 在 C:\\Dev\\QQBot-Dev\\QQNT)\n \"C:/Dev/QQBot-Dev/QQNT/QQ.exe\",\n ];\n for (const p of candidates) {\n if (existsSync(p)) {\n return p;\n }\n }\n return null;\n}\n\n/** QQ 安装目录探测结果。 */\nexport interface QqInstallInfo {\n /** QQ.exe 绝对路径。 */\n qqPath: string;\n /** 安装根目录。 */\n installDir: string;\n /** 当前版本目录名(如 9.9.31-49919)。 */\n version: string;\n /** wrapper.node 绝对路径。 */\n wrapperPath: string;\n}\n\n/** 找 QQ.exe 路径。 */\nexport function locateQqPath(): string {\n const viaReg = findQqViaRegistry();\n if (viaReg) {\n return viaReg;\n }\n const viaCommon = findQqViaCommonPaths();\n if (viaCommon) {\n return viaCommon;\n }\n throw new Error(\"未找到 QQ.exe(请先安装 QQ,或通过 NAPUTO_QQ_PATH 指定)\");\n}\n\n/** 从 QQ.exe 路径推导安装目录,并探测当前版本。 */\nexport function resolveQqInstall(qqPath?: string): QqInstallInfo {\n const qq = qqPath ?? process.env[\"NAPUTO_QQ_PATH\"] ?? locateQqPath();\n const installDir = qq.slice(0, qq.lastIndexOf(\"\\\\\"));\n const versionsDir = join(installDir, \"versions\");\n if (!existsSync(versionsDir)) {\n throw new Error(`QQ 版本目录不存在: ${versionsDir}`);\n }\n const versions = readdirSync(versionsDir)\n .filter((v) => statSync(join(versionsDir, v)).isDirectory())\n .sort()\n .reverse();\n const [first] = versions;\n if (first === undefined) {\n throw new Error(`QQ 版本目录为空: ${versionsDir}`);\n }\n const wrapperPath = join(versionsDir, first, \"resources\", \"app\", \"wrapper.node\");\n if (!existsSync(wrapperPath)) {\n throw new Error(`未找到 wrapper.node: ${wrapperPath}`);\n }\n return { qqPath: qq, installDir, version: first, wrapperPath };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/instance-lock.ts","../src/launcher.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 * launcher:自建宿主引导(2026-08-07 唯一路线)——spawn 标准 node 跑 self-host.cjs。\n *\n * 历史(V1/路线 B 事实链,已归档 archive/):\n * 1. wrapper.node 只能在 QQ 定制版 Electron 里注册(实测纯 Node/普通 Electron 均 self-register 失败)\n * 2. QQ 是打包应用,禁 NODE_OPTIONS(实测 stderr)\n * → 旧方案:NapukettoBootMain.exe 启动 QQ + 注入 NapukettoWinBootHook.dll(已废弃)\n * → 现方案:stub QQNT.dll 转发宿主符号 → 标准 node 直接 dlopen wrapper.node(自建宿主)\n */\nimport { type StdioOptions, spawn } from \"node:child_process\";\nimport { existsSync, mkdirSync } from \"node:fs\";\nimport { dirname, join, resolve } from \"node:path\";\nimport process from \"node:process\";\nimport { fileURLToPath } from \"node:url\";\nimport type { QqInstallInfo } from \"./locate-qq.js\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\nexport interface LaunchOptions {\n /** QQ 安装信息(qqPath/version/wrapperPath)。 */\n qq: QqInstallInfo;\n /** kernel 入口(.mjs,self-host.cjs 里 import)。 */\n kernelEntry: string;\n /** 配置目录(账号数据目录,NAPUTO_CFG_DIR)。 */\n cfgDir: string;\n /** 全局配置文件路径(项目根 napuketto.toml,注入 NAPKETTO_CONFIG 供装配链读取)。 */\n configPath?: string;\n /** adapter 入口(.mjs,协议装配用)。 */\n adapterEntry?: string;\n /** network 入口(.mjs,协议装配用)。 */\n networkEntry?: string;\n /** 自建宿主:标准 node + stub QQNT.dll 直接引导,不拉起 QQ(2026-08-07 唯一路线)。 */\n selfHost?: boolean;\n /** stub QQNT.dll 目录(PATH 前置,转发 napi_*、uv_* 符号到 node.exe)。 */\n stubDir?: string;\n /** 强制指定快速登录账号(注入 NAPUTO_QUICK_UIN;cli `-q <uin>` 透传,2026-08-07)。 */\n quickUin?: string;\n /** IPC 子进程模式(=1:stdout 走 JSON 行协议 + stdin 收 action/control,koishi 插件驱动)。 */\n ipc?: boolean;\n /** 自建宿主入口(默认 dist/host/self-host.cjs)。 */\n selfHostEntry?: string;\n /**\n * 子进程工作目录(缺省继承父进程 cwd)。\n *\n * ⚠️ 必须指向数据根(~/.napuketto):QQ 原生层在账号上下文就绪前会\n * fallback 到进程 cwd 落盘(如频道库 guild1.db),不指定则污染调用方\n * 目录(实测写入项目根,guild1.db 08-07)。cli 传 dataRoot 根治。\n */\n cwd?: string;\n /**\n * 子进程 stdio(默认 \"inherit\")。cli 传 [\"inherit\",\"pipe\",\"pipe\"] 并接管\n * 子进程 stdout/stderr:逐行过滤 wrapper 原生噪音(MMKV 刷屏、Node 符号\n * 查找失败警告),其余转发——原生 printf 直写 fd 的字节无法从 JS 层拦截。\n */\n stdio?: StdioOptions;\n}\n\nexport interface LaunchResult {\n /** 子进程句柄(自建宿主 node 进程)。 */\n child: ReturnType<typeof spawn>;\n /** 自建宿主入口路径(self-host.cjs)。 */\n bootJsPath: string;\n}\n\n/**\n * 启动自建宿主(路线 A,2026-08-07 产品化):spawn 标准 node 直接跑 self-host.cjs,\n * 不拉起 QQ / 不注入。\n *\n * 关键(HANDOVER-V7 技术数据):wrapper.node 从 QQNT.dll 导入 99 符号\n * (napi_*×40 + uv_*×56 + qq_magic×1 + v8/node mangled×2)——标准 node 无这些宿主\n * 符号,必须 PATH 前置 stub QQNT.dll 目录(转发到 node.exe)+ QQ resources\\app\n * (wrapper.node 同目录依赖 DLL)才能 dlopen 成功。\n */\nexport function launchSelfHost(options: LaunchOptions): LaunchResult {\n const selfHostPath =\n options.selfHostEntry ?? join(__dirname, \"..\", \"dist\", \"host\", \"self-host.cjs\");\n if (!existsSync(selfHostPath)) {\n throw new Error(\n `self-host.cjs 缺失: ${selfHostPath}(先运行 pnpm --filter @napuketto/loader build)`,\n );\n }\n\n // stub QQNT.dll 校验(自建宿主必需:PATH 前置 stub 转发 napi_* 到 node.exe,\n // 否则 wrapper.node dlopen 失败)。默认 loader 包内闭源 submodule native/stub-test-env,\n // 缺失时提示 --stub-dir / NAPUTO_STUB_DIR 指定。\n const stub = options.stubDir ?? defaultStubDir();\n if (!existsSync(join(stub, \"QQNT.dll\"))) {\n throw new Error(\n `stub QQNT.dll 未找到: ${stub}(自建宿主必需:stub 转发 QQNT.dll 符号到 node.exe;` +\n \"请用 --stub-dir 或环境变量 NAPUTO_STUB_DIR 指定 stub 目录)\",\n );\n }\n\n const env = buildLaunchEnv(options);\n // PATH 前置 stub 目录(stub QQNT.dll 转发)+ QQ resources\\app(wrapper.node 依赖 DLL)\n const pathEntries = [stub, dirname(options.qq.wrapperPath), process.env[\"PATH\"] ?? \"\"]\n .filter((p) => p !== undefined && p !== \"\")\n .join(\";\");\n env[\"PATH\"] = pathEntries;\n env[ENV.STUB_DIR] = stub;\n\n // 标准 node 直接跑入口(不拉起 QQ,不注入)\n // cwd 显式指向数据根(cli 传入):QQ 原生层 fallback 落盘(guild1.db 等)\n // 不再污染项目根目录(实测 08-07,GPro 模块初始化早于账号上下文)。\n if (options.cwd !== undefined) {\n mkdirSync(options.cwd, { recursive: true });\n }\n const child = spawn(process.execPath, [selfHostPath], {\n // exactOptionalPropertyTypes:未传 cwd 时不显式写入 undefined(继承父进程)\n ...(options.cwd !== undefined ? { cwd: options.cwd } : {}),\n env,\n stdio: options.stdio ?? \"inherit\",\n windowsHide: false,\n });\n\n return { child, bootJsPath: selfHostPath };\n}\n\n/**\n * 默认 stub QQNT.dll 目录(loader 包内闭源 native/build/stub-test-env,开发机默认)。\n * src 与 native 同层,构建后 dist 与 native 同层——`../native` 恒正确。\n */\nexport function defaultStubDir(): string {\n return join(__dirname, \"..\", \"native\", \"build\", \"stub-test-env\");\n}\n\n/** 装配自建宿主环境变量。 */\nfunction buildLaunchEnv(options: LaunchOptions): Record<string, string> {\n // 配置目录兜底\n const cfg = resolve(options.cfgDir);\n mkdirSync(cfg, { recursive: true });\n\n const env: Record<string, string> = {\n ...process.env,\n [ENV.QQ_PATH]: options.qq.qqPath,\n [ENV.KERNEL_ENTRY]: resolve(options.kernelEntry),\n [ENV.CFG_DIR]: cfg,\n [ENV.QQ_VERSION]: options.qq.version,\n [ENV.WRAPPER_PATH]: options.qq.wrapperPath,\n };\n if (options.selfHost === true) {\n env[ENV.SELF_HOST] = \"1\";\n }\n if (options.adapterEntry !== undefined) {\n env[ENV.ADAPTER_ENTRY] = resolve(options.adapterEntry);\n }\n if (options.networkEntry !== undefined) {\n env[ENV.NETWORK_ENTRY] = resolve(options.networkEntry);\n }\n if (options.configPath !== undefined) {\n env[ENV.CONFIG_PATH] = resolve(options.configPath);\n }\n if (options.quickUin !== undefined) {\n env[ENV.QUICK_UIN] = options.quickUin;\n }\n if (options.ipc === true) {\n env[ENV.IPC] = \"1\";\n }\n return env;\n}\n\n/** 环境变量名(self-host.cjs 与 kernel 引导读取)。 */\nexport const ENV = {\n QQ_PATH: \"NAPUTO_QQ_PATH\",\n KERNEL_ENTRY: \"NAPUTO_KERNEL_ENTRY\",\n CFG_DIR: \"NAPUTO_CFG_DIR\",\n QQ_VERSION: \"NAPUTO_QQ_VERSION\",\n WRAPPER_PATH: \"NAPUTO_WRAPPER_PATH\",\n /** 自建宿主:标准 node + stub QQNT.dll 引导(self-host.cjs 分支)。 */\n SELF_HOST: \"NAPUTO_SELF_HOST\",\n /** stub QQNT.dll 目录(launchSelfHost 注入,self-host.cjs 诊断用)。 */\n STUB_DIR: \"NAPUTO_STUB_DIR\",\n /** adapter 包入口(协议装配用)。 */\n ADAPTER_ENTRY: \"NAPUTO_ADAPTER_ENTRY\",\n /** network 包入口(协议装配用)。 */\n NETWORK_ENTRY: \"NAPUTO_NETWORK_ENTRY\",\n /** 全局配置文件路径(项目根 napuketto.toml,boot-protocols 读取)。 */\n CONFIG_PATH: \"NAPKETTO_CONFIG\",\n /** 强制指定快速登录账号(cli `-q <uin>` 透传,bootstrap 登录用)。 */\n QUICK_UIN: \"NAPUTO_QUICK_UIN\",\n /** IPC 子进程模式(koishi 插件驱动:stdout JSON 行 + stdin action/control)。 */\n IPC: \"NAPUTO_IPC\",\n} as const;\n","/**\n * 定位 QQ 安装目录与版本(复用 kernel wrapper-version 的探测逻辑)\n */\nimport { execFileSync } from \"node:child_process\";\nimport { existsSync, readdirSync, statSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport process from \"node:process\";\n\n/** 注册表 UninstallString 查询(QQ 官方安装路径)。 */\nconst REG_QUERY_RE = /\"([^\"]+)\"/;\n\n/** 注册表 UninstallString 查询(QQ 官方安装路径)。 */\nfunction findQqViaRegistry(): string | null {\n try {\n const out = execFileSync(\"reg\", [\n \"query\",\n \"HKLM\\\\SOFTWARE\\\\WOW6432Node\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Uninstall\\\\QQ\",\n \"/v\",\n \"UninstallString\",\n ]).toString();\n const m = out.match(REG_QUERY_RE);\n const exe = m?.[1];\n if (exe !== undefined) {\n // UninstallString 形如 \"C:\\...\\uninst.exe\" 或带参数\n const dir = exe.replaceAll('\"', \"\").slice(0, exe.lastIndexOf(\"\\\\\"));\n const qq = join(dir, \"QQ.exe\");\n if (existsSync(qq)) {\n return qq;\n }\n }\n } catch {\n // 注册表查询失败,走常见路径\n }\n return null;\n}\n\n/** 常见安装路径探测。 */\nfunction findQqViaCommonPaths(): string | null {\n const localAppData = process.env[\"LOCALAPPDATA\"] ?? \"\";\n const candidates = [\n \"C:/Program Files/Tencent/QQNT/QQ.exe\",\n \"C:/Program Files (x86)/Tencent/QQNT/QQ.exe\",\n join(localAppData, \"Programs\", \"Tencent\", \"QQNT\", \"QQ.exe\"),\n // 开发机目录(2026-08-07 环境事实:QQ 9.9.33-51802 在 C:\\Dev\\QQBot-Dev\\QQNT)\n \"C:/Dev/QQBot-Dev/QQNT/QQ.exe\",\n ];\n for (const p of candidates) {\n if (existsSync(p)) {\n return p;\n }\n }\n return null;\n}\n\n/** QQ 安装目录探测结果。 */\nexport interface QqInstallInfo {\n /** QQ.exe 绝对路径。 */\n qqPath: string;\n /** 安装根目录。 */\n installDir: string;\n /** 当前版本目录名(如 9.9.31-49919)。 */\n version: string;\n /** wrapper.node 绝对路径。 */\n wrapperPath: string;\n}\n\n/** 找 QQ.exe 路径。 */\nexport function locateQqPath(): string {\n const viaReg = findQqViaRegistry();\n if (viaReg) {\n return viaReg;\n }\n const viaCommon = findQqViaCommonPaths();\n if (viaCommon) {\n return viaCommon;\n }\n throw new Error(\"未找到 QQ.exe(请先安装 QQ,或通过 NAPUTO_QQ_PATH 指定)\");\n}\n\n/** 从 QQ.exe 路径推导安装目录,并探测当前版本。 */\nexport function resolveQqInstall(qqPath?: string): QqInstallInfo {\n const qq = qqPath ?? process.env[\"NAPUTO_QQ_PATH\"] ?? locateQqPath();\n const installDir = qq.slice(0, qq.lastIndexOf(\"\\\\\"));\n const versionsDir = join(installDir, \"versions\");\n if (!existsSync(versionsDir)) {\n throw new Error(`QQ 版本目录不存在: ${versionsDir}`);\n }\n const versions = readdirSync(versionsDir)\n .filter((v) => statSync(join(versionsDir, v)).isDirectory())\n .sort()\n .reverse();\n const [first] = versions;\n if (first === undefined) {\n throw new Error(`QQ 版本目录为空: ${versionsDir}`);\n }\n const wrapperPath = join(versionsDir, first, \"resources\", \"app\", \"wrapper.node\");\n if (!existsSync(wrapperPath)) {\n throw new Error(`未找到 wrapper.node: ${wrapperPath}`);\n }\n return { qqPath: qq, installDir, version: first, wrapperPath };\n}\n"],"mappings":"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"}
|