@napuketto/loader 0.0.13 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -21,6 +21,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
21
21
  enumerable: true
22
22
  }) : target, mod));
23
23
  //#endregion
24
+ let zod = require("zod");
24
25
  let node_fs = require("node:fs");
25
26
  let node_path = require("node:path");
26
27
  let node_process = require("node:process");
@@ -30,6 +31,171 @@ let node_url = require("node:url");
30
31
  let node_fs_promises = require("node:fs/promises");
31
32
  let node_crypto = require("node:crypto");
32
33
  let node_https = require("node:https");
34
+ //#region src/host/ipc/ipc-types.ts
35
+ /**
36
+ * ipc-types.ts:IPC 协议消息(koishi 插件 ↔ self-host 子进程,单一来源)。
37
+ *
38
+ * 协议契约(koishi 插件 design.md §5.6):`{ v, type, id?, payload }` JSON 行。
39
+ * 用 zod v4 定义单一 schema,两侧(loader host + koishi 插件)import 同一份:
40
+ * 类型经 z.infer 派生、运行时经 safeParse 校验,消除「两份手写 types.ts」漂移。
41
+ *
42
+ * 领域字段(LoginState / SelfInfo / QrCodeData)与 kernel 同名类型结构等价,
43
+ * 在协议边界内联为 zod schema(kernel 不依赖 zod,故不在 kernel 定义)。
44
+ */
45
+ /** IPC 协议版本(解码校验)。 */
46
+ const IPC_VERSION = 1;
47
+ /** 引导阶段(status 消息)。 */
48
+ const IpcBootPhaseSchema = zod.z.enum([
49
+ "booting",
50
+ "dlopening",
51
+ "logging",
52
+ "sessioning",
53
+ "ready",
54
+ "failed"
55
+ ]);
56
+ /** 结构化日志级别(pino level)。 */
57
+ const IpcLogLevelSchema = zod.z.enum([
58
+ "trace",
59
+ "debug",
60
+ "info",
61
+ "warn",
62
+ "error",
63
+ "fatal"
64
+ ]);
65
+ /** 登录状态(与 kernel LoginState 结构等价)。 */
66
+ const LoginStateSchema = zod.z.enum([
67
+ "idle",
68
+ "waiting_scan",
69
+ "scanned",
70
+ "logged_in",
71
+ "failed"
72
+ ]);
73
+ /** selfInfo(与 kernel SelfInfo 结构等价)。 */
74
+ const SelfInfoSchema = zod.z.object({
75
+ uin: zod.z.string(),
76
+ uid: zod.z.string(),
77
+ nick: zod.z.string()
78
+ });
79
+ /** status 消息 payload。 */
80
+ const IpcStatusPayloadSchema = zod.z.object({
81
+ phase: IpcBootPhaseSchema,
82
+ message: zod.z.string().optional(),
83
+ error: zod.z.object({
84
+ code: zod.z.string(),
85
+ message: zod.z.string()
86
+ }).optional()
87
+ });
88
+ /** login 消息 payload。 */
89
+ const IpcLoginPayloadSchema = zod.z.object({
90
+ state: LoginStateSchema,
91
+ selfInfo: SelfInfoSchema.optional(),
92
+ /** 失败原因(state=failed 时;如「登录超时,请刷新页面重试」)。 */
93
+ message: zod.z.string().optional()
94
+ });
95
+ /** event 消息 payload(kernel 事件通道形状,翻译层按 service/name 断言具体类型)。 */
96
+ const IpcEventPayloadSchema = zod.z.object({
97
+ service: zod.z.string(),
98
+ name: zod.z.string(),
99
+ args: zod.z.array(zod.z.unknown())
100
+ });
101
+ /** log 消息 payload。 */
102
+ const IpcLogPayloadSchema = zod.z.object({
103
+ level: IpcLogLevelSchema,
104
+ message: zod.z.string()
105
+ });
106
+ /** 动作响应 payload(result 消息)。 */
107
+ const IpcResultPayloadSchema = zod.z.union([zod.z.object({
108
+ ok: zod.z.literal(true),
109
+ value: zod.z.unknown().optional()
110
+ }), zod.z.object({
111
+ ok: zod.z.literal(false),
112
+ error: zod.z.object({
113
+ code: zod.z.string(),
114
+ message: zod.z.string()
115
+ })
116
+ })]);
117
+ /** 动作请求 payload(action 消息)。 */
118
+ const IpcActionPayloadSchema = zod.z.object({
119
+ action: zod.z.string(),
120
+ params: zod.z.record(zod.z.string(), zod.z.unknown()).optional()
121
+ });
122
+ /** 控制指令 payload(control 消息,父→子)。 */
123
+ const IpcControlPayloadSchema = zod.z.union([
124
+ zod.z.object({ command: zod.z.literal("stop") }),
125
+ zod.z.object({ command: zod.z.literal("restart") }),
126
+ zod.z.object({
127
+ command: zod.z.literal("login"),
128
+ uin: zod.z.string().optional(),
129
+ qr: zod.z.boolean().optional()
130
+ })
131
+ ]);
132
+ const IpcStatusMessageSchema = zod.z.object({
133
+ v: zod.z.literal(1),
134
+ type: zod.z.literal("status"),
135
+ payload: IpcStatusPayloadSchema
136
+ });
137
+ const IpcLoginMessageSchema = zod.z.object({
138
+ v: zod.z.literal(1),
139
+ type: zod.z.literal("login"),
140
+ payload: IpcLoginPayloadSchema
141
+ });
142
+ const IpcQrMessageSchema = zod.z.object({
143
+ v: zod.z.literal(1),
144
+ type: zod.z.literal("qr"),
145
+ payload: zod.z.object({
146
+ pngBase64: zod.z.string(),
147
+ qrcodeUrl: zod.z.string()
148
+ })
149
+ });
150
+ const IpcEventMessageSchema = zod.z.object({
151
+ v: zod.z.literal(1),
152
+ type: zod.z.literal("event"),
153
+ payload: IpcEventPayloadSchema
154
+ });
155
+ const IpcResultMessageSchema = zod.z.object({
156
+ v: zod.z.literal(1),
157
+ type: zod.z.literal("result"),
158
+ id: zod.z.number(),
159
+ payload: IpcResultPayloadSchema
160
+ });
161
+ const IpcLogMessageSchema = zod.z.object({
162
+ v: zod.z.literal(1),
163
+ type: zod.z.literal("log"),
164
+ payload: IpcLogPayloadSchema
165
+ });
166
+ const IpcPingMessageSchema = zod.z.object({
167
+ v: zod.z.literal(1),
168
+ type: zod.z.literal("ping")
169
+ });
170
+ const IpcPongMessageSchema = zod.z.object({
171
+ v: zod.z.literal(1),
172
+ type: zod.z.literal("pong")
173
+ });
174
+ const IpcActionMessageSchema = zod.z.object({
175
+ v: zod.z.literal(1),
176
+ type: zod.z.literal("action"),
177
+ id: zod.z.number(),
178
+ payload: IpcActionPayloadSchema
179
+ });
180
+ const IpcControlMessageSchema = zod.z.object({
181
+ v: zod.z.literal(1),
182
+ type: zod.z.literal("control"),
183
+ payload: IpcControlPayloadSchema
184
+ });
185
+ /** IPC 消息判别联合(协议边界,运行时经 safeParse 校验)。 */
186
+ const IpcMessageSchema = zod.z.discriminatedUnion("type", [
187
+ IpcStatusMessageSchema,
188
+ IpcLoginMessageSchema,
189
+ IpcQrMessageSchema,
190
+ IpcEventMessageSchema,
191
+ IpcResultMessageSchema,
192
+ IpcLogMessageSchema,
193
+ IpcPingMessageSchema,
194
+ IpcPongMessageSchema,
195
+ IpcActionMessageSchema,
196
+ IpcControlMessageSchema
197
+ ]);
198
+ //#endregion
33
199
  //#region src/instance-lock.ts
34
200
  /**
35
201
  * instance-lock.ts:单实例锁(2026-08-07 根治「多实例抢数据目录锁挂起」)
@@ -134,6 +300,34 @@ function registerLockCleanup(dataDir) {
134
300
  });
135
301
  }
136
302
  //#endregion
303
+ //#region src/data-root.ts
304
+ /**
305
+ * data-root.ts:轻量数据根解析(loader 侧,避免新增 kernel 依赖)。
306
+ *
307
+ * 从 locate-qq.ts / win-node.ts 抽出的同构逻辑(2026-08 消重:两处
308
+ * 22 行重复 resolveDataRoot + findProjectRoot)。
309
+ *
310
+ * 优先级:显式参数 > NAPKETTO_DATA > 项目根/.napuketto > cwd 兜底。
311
+ * P2 若需完整语义(同 kernel resolveDataRoot)再切换。
312
+ */
313
+ /** 解析数据根(NAPKETTO_DATA 显式 > 项目根/.napuketto > cwd 兜底)。 */
314
+ function resolveDataRoot(dataRoot) {
315
+ const explicit = dataRoot ?? node_process.default.env["NAPKETTO_DATA"];
316
+ if (explicit !== void 0 && explicit !== "") return (0, node_path.resolve)(explicit);
317
+ const projectRoot = findProjectRoot(node_process.default.cwd()) ?? node_process.default.cwd();
318
+ return (0, node_path.join)(projectRoot, ".napuketto");
319
+ }
320
+ /** 向上探测项目根(含 pnpm-workspace.yaml 或 package.json 的目录)。 */
321
+ function findProjectRoot(start) {
322
+ let dir = (0, node_path.resolve)(start);
323
+ for (;;) {
324
+ if ((0, node_fs.existsSync)((0, node_path.join)(dir, "pnpm-workspace.yaml")) || (0, node_fs.existsSync)((0, node_path.join)(dir, "package.json"))) return dir;
325
+ const parent = (0, node_path.dirname)(dir);
326
+ if (parent === dir) return null;
327
+ dir = parent;
328
+ }
329
+ }
330
+ //#endregion
137
331
  //#region src/qq-download.ts
138
332
  /**
139
333
  * qq-download.ts:QQ 官方安装包下载器(P1)。
@@ -257,8 +451,8 @@ async function downloadFile(options) {
257
451
  const EXTRACT_EXTENSIONS = /* @__PURE__ */ new Set([".node", ".dll"]);
258
452
  /**
259
453
  * 探测 7z 可执行文件:
260
- * NAPUTO_7Z_PATH 环境变量 > 内置资产(assets/7zip/7z.exe,Windows)> 系统 PATH 7z。
261
- * Linux 上用 p7zip 提供的 7z 命令(系统 PATH)。
454
+ * NAPUTO_7Z_PATH 环境变量 > 内置资产(Windows assets/7zip/7z.exe、Linux
455
+ * assets/7zip/7zz——2026-08-14 治本,静态二进制已打进发布包)> 系统 PATH 7z。
262
456
  */
263
457
  function findSevenZip() {
264
458
  const envPath = node_process.default.env["NAPUTO_7Z_PATH"];
@@ -266,8 +460,9 @@ function findSevenZip() {
266
460
  exe: envPath,
267
461
  source: "环境变量 NAPUTO_7Z_PATH"
268
462
  };
269
- if (node_process.default.platform === "win32") {
270
- const bundled = (0, node_path.join)((0, node_path.dirname)((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href)), "..", "assets", "7zip", "7z.exe");
463
+ if (node_process.default.platform === "win32" || node_process.default.platform === "linux") {
464
+ const exeName = node_process.default.platform === "win32" ? "7z.exe" : "7zz";
465
+ const bundled = (0, node_path.join)((0, node_path.dirname)((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href)), "..", "assets", "7zip", exeName);
271
466
  if ((0, node_fs.existsSync)(bundled)) return {
272
467
  exe: bundled,
273
468
  source: "内置资产 assets/7zip"
@@ -410,23 +605,6 @@ async function unzipNode(zipPath, cacheDir) {
410
605
  }
411
606
  /** 默认 Windows node 版本(latest v24,LTS 线;可被环境变量/参数覆盖)。 */
412
607
  const DEFAULT_WIN_NODE_VERSION = "v24.16.0";
413
- /** 轻量数据根解析(与 locate-qq 内同构,避免循环依赖)。 */
414
- function resolveDataRoot(dataRoot) {
415
- const explicit = dataRoot ?? node_process.default.env["NAPKETTO_DATA"];
416
- if (explicit !== void 0 && explicit !== "") return (0, node_path.resolve)(explicit);
417
- const projectRoot = findProjectRoot$1(node_process.default.cwd()) ?? node_process.default.cwd();
418
- return (0, node_path.join)(projectRoot, ".napuketto");
419
- }
420
- /** 向上探测项目根(含 pnpm-workspace.yaml 或 package.json 的目录)。 */
421
- function findProjectRoot$1(start) {
422
- let dir = (0, node_path.resolve)(start);
423
- for (;;) {
424
- if ((0, node_fs.existsSync)((0, node_path.join)(dir, "pnpm-workspace.yaml")) || (0, node_fs.existsSync)((0, node_path.join)(dir, "package.json"))) return dir;
425
- const parent = (0, node_path.dirname)(dir);
426
- if (parent === dir) return null;
427
- dir = parent;
428
- }
429
- }
430
608
  //#endregion
431
609
  //#region src/wine.ts
432
610
  /**
@@ -680,8 +858,8 @@ function resolveDownloadUrl(release) {
680
858
  */
681
859
  /** 数据根下 QQ 文件缓存目录名(<数据根>/qq-files/<版本>/,P1 下载解包产物)。 */
682
860
  const QQ_FILES_DIR_NAME = "qq-files";
683
- /** 7-Zip 官方 Linux 版版本号(tar.xz 文件名组成部分,2026-08-14 起自动下载)。 */
684
- const SEVEN_ZIP_LINUX_VERSION = "2409";
861
+ /** 7-Zip 官方 Linux 版版本号(tar.xz 文件名组成部分;兜底下载用,内置资产优先)。 */
862
+ const SEVEN_ZIP_LINUX_VERSION = "2501";
685
863
  /** 注册表 UninstallString 查询(QQ 官方安装路径)。 */
686
864
  const REG_QUERY_RE = /"([^"]+)"/;
687
865
  /** Windows 盘符路径正则(`C:/...` / `C:\...`,wslMappedPath 映射用,模块级常量)。 */
@@ -775,7 +953,7 @@ async function resolveQqFiles(options = {}) {
775
953
  try {
776
954
  return resolveQqInstall();
777
955
  } catch {}
778
- const dataRoot = resolveDataRootLight(options.dataRoot);
956
+ const dataRoot = resolveDataRoot(options.dataRoot);
779
957
  const cached = tryResolveCached(dataRoot);
780
958
  if (cached !== null) return cached;
781
959
  if (options.autoDownload !== false) return ensureQqFiles({
@@ -802,17 +980,25 @@ function linuxSevenZipUrl() {
802
980
  return node_process.default.env["NAPUTO_7Z_URL"] ?? `https://www.7-zip.org/a/7z${SEVEN_ZIP_LINUX_VERSION}-linux-x64.tar.xz`;
803
981
  }
804
982
  /**
805
- * 确保 Linux 7zz 就绪(2026-08-14 生产修复):自动下载 7-Zip 官方 Linux 版。
983
+ * 确保 Linux 7zz 就绪(治本:内置资产优先,下载兜底)。
806
984
  *
807
- * 背景:内置 assets/7zip 的 7z.exe 是 Windows PE(Linux 不可用),此前 Linux 依赖
808
- * 系统 p7zip-full——生产环境未安装 → `7z 解包失败: No such file or directory`。
809
- * 7zz 是 7-Zip 完整版(支持 NSIS 解包,同 7z.exe 能力),官方静态二进制,
810
- * 免 root 免系统包,下载到 <数据根>/runtime/7zip/7zz(与 win-node 同模式)。
985
+ * 优先级:内置资产 assets/7zip/7zz(2026-08-14 治本——与 Windows 7z.exe 同模式,
986
+ * 7zz 静态二进制已打进发布包,LGPL 合规可分发,零运行时下载)> 数据根缓存
987
+ * runtime/7zip/7zz(历史下载产物复用)> 官网下载(兜底,NAPUTO_7Z_URL 可覆盖)。
811
988
  *
812
- * 幂等:缓存已存在直接返回;tar -xJf 解压(Linux 自带 tar;需 xz-utils)。
989
+ * 背景:7-Zip 官网只保留最新版 tar.xz,硬编码旧版本号(如 2409)会被删档 404,
990
+ * 运行时下载既脆弱又依赖外网。内置资产彻底消灭该依赖。
813
991
  */
814
992
  async function ensureLinuxSevenZip(options = {}) {
815
- const dataRoot = resolveDataRootLight(options.dataRoot);
993
+ const bundled = (0, node_path.join)((0, node_path.dirname)((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href)), "..", "assets", "7zip", "7zz");
994
+ if ((0, node_fs.existsSync)(bundled)) {
995
+ await (0, node_fs_promises.chmod)(bundled, 493).catch(() => {});
996
+ return {
997
+ exe: bundled,
998
+ source: "内置资产 assets/7zip"
999
+ };
1000
+ }
1001
+ const dataRoot = resolveDataRoot(options.dataRoot);
816
1002
  const cacheDir = (0, node_path.join)(dataRoot, "runtime", "7zip");
817
1003
  const exe = (0, node_path.join)(cacheDir, "7zz");
818
1004
  if ((0, node_fs.existsSync)(exe)) return {
@@ -852,7 +1038,7 @@ async function ensureLinuxSevenZip(options = {}) {
852
1038
  * → 7z 解包 → 提取 wrapper.node/QQNT.dll → 缓存。返回缓存版本 QqInstallInfo。
853
1039
  */
854
1040
  async function ensureQqFiles(options = {}) {
855
- const dataRoot = resolveDataRootLight(options.dataRoot);
1041
+ const dataRoot = resolveDataRoot(options.dataRoot);
856
1042
  const cacheRoot = (0, node_path.join)(dataRoot, QQ_FILES_DIR_NAME);
857
1043
  (0, node_fs.mkdirSync)(cacheRoot, { recursive: true });
858
1044
  const existing = tryResolveCached(dataRoot);
@@ -885,30 +1071,21 @@ async function ensureQqFiles(options = {}) {
885
1071
  });
886
1072
  }
887
1073
  }
888
- /**
889
- * 轻量数据根解析(loader 侧,避免新增 kernel 依赖;P2 若需完整语义再切换)。
890
- * 优先级:显式参数 > NAPKETTO_DATA > 项目根/.napuketto > cwd 兜底。
891
- */
892
- function resolveDataRootLight(dataRoot) {
893
- const explicit = dataRoot ?? node_process.default.env["NAPKETTO_DATA"];
894
- if (explicit !== void 0 && explicit !== "") return (0, node_path.resolve)(explicit);
895
- const projectRoot = findProjectRoot(node_process.default.cwd()) ?? node_process.default.cwd();
896
- return (0, node_path.join)(projectRoot, ".napuketto");
897
- }
898
- /** 向上探测项目根(含 pnpm-workspace.yaml 或 package.json 的目录)。 */
899
- function findProjectRoot(start) {
900
- let dir = (0, node_path.resolve)(start);
901
- for (;;) {
902
- if ((0, node_fs.existsSync)((0, node_path.join)(dir, "pnpm-workspace.yaml")) || (0, node_fs.existsSync)((0, node_path.join)(dir, "package.json"))) return dir;
903
- const parent = (0, node_path.dirname)(dir);
904
- if (parent === dir) return null;
905
- dir = parent;
906
- }
907
- }
908
1074
  //#endregion
909
1075
  exports.DownloadError = DownloadError;
910
1076
  exports.ENV = ENV;
911
1077
  exports.INSTANCE_LOCK_FILE = INSTANCE_LOCK_FILE;
1078
+ exports.IPC_VERSION = IPC_VERSION;
1079
+ exports.IpcActionPayloadSchema = IpcActionPayloadSchema;
1080
+ exports.IpcBootPhaseSchema = IpcBootPhaseSchema;
1081
+ exports.IpcControlPayloadSchema = IpcControlPayloadSchema;
1082
+ exports.IpcEventPayloadSchema = IpcEventPayloadSchema;
1083
+ exports.IpcLogLevelSchema = IpcLogLevelSchema;
1084
+ exports.IpcLogPayloadSchema = IpcLogPayloadSchema;
1085
+ exports.IpcLoginPayloadSchema = IpcLoginPayloadSchema;
1086
+ exports.IpcMessageSchema = IpcMessageSchema;
1087
+ exports.IpcResultPayloadSchema = IpcResultPayloadSchema;
1088
+ exports.IpcStatusPayloadSchema = IpcStatusPayloadSchema;
912
1089
  exports.QQ_FILES_DIR_NAME = QQ_FILES_DIR_NAME;
913
1090
  exports.QqReleasesError = QqReleasesError;
914
1091
  exports.WIN_NODE_DIR_NAME = WIN_NODE_DIR_NAME;