@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.
@@ -1,6 +1,135 @@
1
- let node_path = require("node:path");
1
+ //#region \0rolldown/runtime.js
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
+ key = keys[i];
11
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
+ get: ((k) => from[k]).bind(null, key),
13
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
+ });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
19
+ value: mod,
20
+ enumerable: true
21
+ }) : target, mod));
22
+ //#endregion
2
23
  let node_fs = require("node:fs");
24
+ let node_path = require("node:path");
25
+ let node_process = require("node:process");
26
+ node_process = __toESM(node_process, 1);
27
+ let node_readline = require("node:readline");
3
28
  let node_os = require("node:os");
29
+ //#region src/instance-lock.ts
30
+ /**
31
+ * instance-lock.ts:单实例锁(2026-08-07 根治「多实例抢数据目录锁挂起」)
32
+ *
33
+ * 背景(实测):同一账号数据目录(~/.napuketto/<uin>)只能被一个实例持有——
34
+ * QQ 原生层(MMKV / 登录单例 / 数据库)有锁,第二个实例抢不到锁会卡在
35
+ * loginService.initConfig 之后无响应(挂起)。此前 create-napukettoqq 生成的
36
+ * 副本实例常驻占锁,导致用户后续启动全部卡死。
37
+ *
38
+ * 方案:以数据目录为粒度加锁文件 `<dataDir>/instance.lock`(JSON 内容:
39
+ * pid / startedAt / cmdline 摘要)。启动前 checkInstanceLock:
40
+ * - 锁不存在 → 可启动(然后 acquireInstanceLock 写入)
41
+ * - 锁存在但 PID 已死 → 崩溃残留,自动清理(rm + 重新获取)
42
+ * - 锁存在且 PID 存活 → 已被占用,拒绝启动(返回占用 PID 供提示)
43
+ *
44
+ * 退出清理:进程正常退出时 releaseInstanceLock 删除锁文件;崩溃/强杀残留
45
+ * 由下次启动的「PID 存活检测」兜底(PID 已死 → 残留视为过期)。
46
+ *
47
+ * 放置说明:放 loader 而非 kernel——self-host.cjs 是 rolldown 内联 bundle
48
+ * (只内联 src/host/ 依赖树),静态 import kernel 会留 external 缺口;
49
+ * 放 loader 则 self-host(内联)与 cli(@napuketto/loader 导入)双入口可用。
50
+ */
51
+ /** 锁文件名(置于数据目录内)。 */
52
+ const INSTANCE_LOCK_FILE = "instance.lock";
53
+ /**
54
+ * 检测指定数据目录是否已被其他实例占用。
55
+ *
56
+ * @param dataDir 数据目录(账号级,如 ~/.napuketto/3567141148)
57
+ */
58
+ function checkInstanceLock(dataDir) {
59
+ const lockPath = (0, node_path.join)(dataDir, INSTANCE_LOCK_FILE);
60
+ let info = null;
61
+ try {
62
+ info = JSON.parse((0, node_fs.readFileSync)(lockPath, "utf-8"));
63
+ } catch {
64
+ return {
65
+ occupied: false,
66
+ pid: null,
67
+ lockPath
68
+ };
69
+ }
70
+ if (typeof info.pid !== "number" || Number.isNaN(info.pid) || info.pid <= 0) return {
71
+ occupied: false,
72
+ pid: null,
73
+ lockPath
74
+ };
75
+ let alive = false;
76
+ try {
77
+ node_process.default.kill(info.pid, 0);
78
+ alive = true;
79
+ } catch {
80
+ alive = false;
81
+ }
82
+ if (!alive) return {
83
+ occupied: false,
84
+ pid: null,
85
+ lockPath
86
+ };
87
+ return {
88
+ occupied: true,
89
+ pid: info.pid,
90
+ lockPath
91
+ };
92
+ }
93
+ /**
94
+ * 获取数据目录锁。占用(其他实例存活)返回 false;成功或清理残留后返回 true。
95
+ * 幂等:本进程已持有则直接返回 true(不重复写)。
96
+ */
97
+ function acquireInstanceLock(dataDir, cmdline) {
98
+ const { occupied, pid, lockPath } = checkInstanceLock(dataDir);
99
+ if (occupied) {
100
+ if (pid === node_process.default.pid) return true;
101
+ return false;
102
+ }
103
+ const info = {
104
+ pid: node_process.default.pid,
105
+ startedAt: Date.now(),
106
+ ...cmdline !== void 0 && cmdline !== "" ? { cmdline } : {}
107
+ };
108
+ try {
109
+ (0, node_fs.rmSync)(lockPath, { force: true });
110
+ (0, node_fs.writeFileSync)(lockPath, JSON.stringify(info), "utf-8");
111
+ return true;
112
+ } catch {
113
+ return false;
114
+ }
115
+ }
116
+ /**
117
+ * 释放数据目录锁(正常退出时调用)。仅当持有者是本进程时才删除,
118
+ * 避免误删新持有者的锁(PID 复用 / 快速重启场景)。
119
+ */
120
+ function releaseInstanceLock(dataDir) {
121
+ const lockPath = (0, node_path.join)(dataDir, INSTANCE_LOCK_FILE);
122
+ try {
123
+ if (JSON.parse((0, node_fs.readFileSync)(lockPath, "utf-8")).pid === node_process.default.pid) (0, node_fs.rmSync)(lockPath, { force: true });
124
+ } catch {}
125
+ }
126
+ /** 注册进程退出清理(SIGINT/SIGTERM/exit 均触发;崩溃强杀由残留检测兜底)。 */
127
+ function registerLockCleanup(dataDir) {
128
+ node_process.default.on("exit", () => {
129
+ releaseInstanceLock(dataDir);
130
+ });
131
+ }
132
+ //#endregion
4
133
  //#region src/host/env.ts
5
134
  /**
6
135
  * env.ts:引导进程环境变量访问层(2026-08-07 阶段 2)。
@@ -32,6 +161,8 @@ const env = {
32
161
  NAPUTO_QUICK_UIN: process.env["NAPUTO_QUICK_UIN"],
33
162
  /** 收发消息冒烟自检开关(=1 触发)。 */
34
163
  NAPUTO_SMOKE: process.env["NAPUTO_SMOKE"],
164
+ /** IPC 子进程模式(=1:koishi 插件驱动——stdout 走 JSON 行协议,stdin 收 action/control)。 */
165
+ NAPUTO_IPC: process.env["NAPUTO_IPC"],
35
166
  /** 冒烟目标 peer(c2c:<uin> / group:<uin>,缺省发给自己)。 */
36
167
  NAPUTO_SMOKE_PEER: process.env["NAPUTO_SMOKE_PEER"],
37
168
  /** 运行时探测开关(=1 触发 probeRuntime)。 */
@@ -42,6 +173,293 @@ const env = {
42
173
  NAPKETTO_DATA: process.env["NAPKETTO_DATA"]
43
174
  };
44
175
  //#endregion
176
+ //#region src/host/ipc/ipc-actions.ts
177
+ /**
178
+ * 从宽松 params 构造 Peer(缺省 chatType=1 群聊)。
179
+ * 优先 peerUid(uid 直通);缺省时 peerUin(QQ 号)经 uinToUid 转换。
180
+ */
181
+ async function toPeer(params, uinToUid) {
182
+ const chatType = typeof params["chatType"] === "number" ? params["chatType"] : 1;
183
+ const uidParam = params["peerUid"];
184
+ if (typeof uidParam === "string" && uidParam !== "") return {
185
+ chatType,
186
+ peerUid: uidParam
187
+ };
188
+ const uinParam = params["peerUin"];
189
+ if (typeof uinParam === "string" && uinParam !== "" && uinToUid !== void 0) {
190
+ const uid = (await uinToUid([uinParam])).get(uinParam);
191
+ if (uid !== void 0 && uid !== "") return {
192
+ chatType,
193
+ peerUid: uid
194
+ };
195
+ throw new Error(`uin 转 uid 失败: ${uinParam}`);
196
+ }
197
+ throw new Error("缺 peerUid(或 peerUin 且未注入 uinToUid)");
198
+ }
199
+ /** 构造动作表(map 顺序即注册顺序,重复动作名后注册覆盖)。 */
200
+ function createIpcActions(ctx) {
201
+ const actions = /* @__PURE__ */ new Map();
202
+ actions.set("login.getSelf", async () => ctx.self);
203
+ actions.set("msg.sendMessage", async (params) => {
204
+ const peer = await toPeer(params, ctx.uinToUid);
205
+ const elements = Array.isArray(params["elements"]) ? params["elements"] : [];
206
+ return ctx.msgApi.sendMessage(peer, elements);
207
+ });
208
+ actions.set("msg.recallMessage", async (params) => {
209
+ const peer = await toPeer(params, ctx.uinToUid);
210
+ const msgIds = Array.isArray(params["msgIds"]) ? params["msgIds"].map(String) : [];
211
+ await ctx.msgApi.recallMessage(peer, msgIds);
212
+ });
213
+ actions.set("msg.fetchMessages", async (params) => {
214
+ const peer = await toPeer(params, ctx.uinToUid);
215
+ const count = typeof params["count"] === "number" ? params["count"] : 20;
216
+ const msgId = typeof params["msgId"] === "string" ? params["msgId"] : void 0;
217
+ return ctx.msgApi.fetchMessages(peer, {
218
+ count,
219
+ ...msgId !== void 0 ? { msgId } : {}
220
+ });
221
+ });
222
+ actions.set("msg.markRead", async (params) => {
223
+ const peer = await toPeer(params, ctx.uinToUid);
224
+ await ctx.msgApi.markRead(peer);
225
+ });
226
+ actions.set("group.getGroupList", async (params) => {
227
+ const force = params["force"] === true;
228
+ return ctx.groupApi.getGroupList(force);
229
+ });
230
+ actions.set("friend.getFriendList", async () => ctx.friendApi.getFriendList());
231
+ return actions;
232
+ }
233
+ /** 统一动作调用:查找动作表 → 执行 → 返回结果或错误(不抛,调用方转 result)。 */
234
+ async function callIpcAction(actions, action, params) {
235
+ const handler = actions.get(action);
236
+ if (handler === void 0) return {
237
+ ok: false,
238
+ error: {
239
+ code: "NOT_FOUND",
240
+ message: `未知动作: ${action}`
241
+ }
242
+ };
243
+ try {
244
+ return {
245
+ ok: true,
246
+ value: await handler(params ?? {})
247
+ };
248
+ } catch (err) {
249
+ const message = err instanceof Error ? err.message : String(err);
250
+ return {
251
+ ok: false,
252
+ error: {
253
+ code: errorCodeOf(err),
254
+ message
255
+ }
256
+ };
257
+ }
258
+ }
259
+ /** 宽松提取错误码(KernelError 有 string code 字段;其他错误 UNKNOWN)。 */
260
+ function errorCodeOf(err) {
261
+ if (typeof err === "object" && err !== null) {
262
+ const code = err.code;
263
+ if (typeof code === "string" && code !== "") return code;
264
+ }
265
+ return "UNKNOWN";
266
+ }
267
+ //#endregion
268
+ //#region src/host/ipc/ipc-types.ts
269
+ /** 消息类型集合(解码校验用)。 */
270
+ const IPC_MESSAGE_TYPES = /* @__PURE__ */ new Set([
271
+ "status",
272
+ "login",
273
+ "qr",
274
+ "event",
275
+ "result",
276
+ "log",
277
+ "ping",
278
+ "pong",
279
+ "action",
280
+ "control"
281
+ ]);
282
+ //#endregion
283
+ //#region src/host/ipc/ipc-sender.ts
284
+ /**
285
+ * ipc-sender.ts:IPC 消息发送封装(self-host 子进程 → koishi 插件)。
286
+ *
287
+ * 仅 NAPUTO_IPC=1 启用:stdout 专用于 JSON 行协议(stdin 收 action/control)。
288
+ * 非 IPC 模式(cli pnpm start)stdout 不输出协议行,零干扰。
289
+ */
290
+ /** IPC 模式开关(self-host.ts 在 env.NAPUTO_IPC=1 时 enable)。 */
291
+ let enabled = false;
292
+ /** 启用 IPC 发送(幂等)。 */
293
+ function enableIpc() {
294
+ enabled = true;
295
+ }
296
+ /** 发送一条 IPC 消息(JSON 行)。失败静默(不阻塞引导)。 */
297
+ function sendIpc(message) {
298
+ if (!enabled) return;
299
+ try {
300
+ process.stdout.write(`${JSON.stringify(message)}\n`);
301
+ } catch {}
302
+ }
303
+ /** 引导状态(booting/dlopening/logging/sessioning/ready/failed)。 */
304
+ function sendStatus(phase, message, error) {
305
+ sendIpc({
306
+ v: 1,
307
+ type: "status",
308
+ payload: {
309
+ phase,
310
+ ...message !== void 0 ? { message } : {},
311
+ ...error !== void 0 ? { error } : {}
312
+ }
313
+ });
314
+ }
315
+ /** 登录状态(QR 状态机 idle/waiting_scan/scanned/logged_in/failed)。 */
316
+ function sendLogin(state, selfInfo) {
317
+ sendIpc({
318
+ v: 1,
319
+ type: "login",
320
+ payload: {
321
+ state,
322
+ ...selfInfo !== void 0 ? { selfInfo } : {}
323
+ }
324
+ });
325
+ }
326
+ /** 二维码数据(扫码登录展示用)。 */
327
+ function sendQr(pngBase64, qrcodeUrl) {
328
+ sendIpc({
329
+ v: 1,
330
+ type: "qr",
331
+ payload: {
332
+ pngBase64,
333
+ qrcodeUrl
334
+ }
335
+ });
336
+ }
337
+ /** kernel 事件转发(service/name/args 原样透传,koishi 插件翻译层消费)。 */
338
+ function sendEvent(service, name, args) {
339
+ sendIpc({
340
+ v: 1,
341
+ type: "event",
342
+ payload: {
343
+ service,
344
+ name,
345
+ args
346
+ }
347
+ });
348
+ }
349
+ /** 动作响应。 */
350
+ function sendResult(id, payload) {
351
+ sendIpc({
352
+ v: 1,
353
+ type: "result",
354
+ id,
355
+ payload
356
+ });
357
+ }
358
+ /** 心跳应答(收到 ping 时回)。 */
359
+ function sendPong() {
360
+ sendIpc({
361
+ v: 1,
362
+ type: "pong"
363
+ });
364
+ }
365
+ /** 心跳(定期发送,koishi 插件探活)。 */
366
+ function sendPing() {
367
+ sendIpc({
368
+ v: 1,
369
+ type: "ping"
370
+ });
371
+ }
372
+ //#endregion
373
+ //#region src/host/ipc/ipc-codec.ts
374
+ /**
375
+ * ipc-codec.ts:IPC 消息编解码(JSON 行协议,与 koishi 插件侧 src/ipc/codec.ts 对齐)。
376
+ *
377
+ * 每行一条 JSON 消息,`\n` 结尾。解码宽松:非法行 / 空行 / v 不匹配 /
378
+ * 未知 type → null,调用方忽略,不崩通道。
379
+ */
380
+ /** 类型守卫:v 匹配 + type 合法。 */
381
+ function isIpcMessage(value) {
382
+ if (typeof value !== "object" || value === null) return false;
383
+ const record = value;
384
+ return record["v"] === 1 && typeof record["type"] === "string" && IPC_MESSAGE_TYPES.has(record["type"]);
385
+ }
386
+ /** 解码一行 JSON;非法行/空行返回 null。 */
387
+ function decodeIpcMessage(line) {
388
+ const trimmed = line.trim();
389
+ if (trimmed === "") return null;
390
+ try {
391
+ const parsed = JSON.parse(trimmed);
392
+ return isIpcMessage(parsed) ? parsed : null;
393
+ } catch {
394
+ return null;
395
+ }
396
+ }
397
+ /** 启动 IPC 服务端(stdin 接收 + 心跳)。返回停止函数。 */
398
+ function startIpcServer(options) {
399
+ const heartbeatMs = options.heartbeatMs ?? 15e3;
400
+ const onExit = options.onExit ?? (() => process.exit(0));
401
+ const rl = (0, node_readline.createInterface)({
402
+ input: process.stdin,
403
+ crlfDelay: Infinity
404
+ });
405
+ rl.on("line", (line) => {
406
+ const message = decodeIpcMessage(line);
407
+ if (message === null) return;
408
+ switch (message.type) {
409
+ case "action":
410
+ handleAction(message.id, message.payload.action, message.payload.params, options.actions);
411
+ break;
412
+ case "control":
413
+ handleControl(message.payload, onExit);
414
+ break;
415
+ case "ping": sendPong();
416
+ }
417
+ });
418
+ const timer = setInterval(() => sendPing(), heartbeatMs);
419
+ return () => {
420
+ clearInterval(timer);
421
+ rl.close();
422
+ };
423
+ }
424
+ /** 动作执行 → result 响应(不抛:callIpcAction 内部兜底转错误)。 */
425
+ async function handleAction(id, action, params, actions) {
426
+ sendResult(id, await callIpcAction(actions, action, params));
427
+ }
428
+ /** 控制指令处理(stop/restart 退出;login 预留——引导期登录由 bootstrap 内部驱动)。 */
429
+ function handleControl(payload, onExit) {
430
+ if (payload.command === "stop" || payload.command === "restart") onExit();
431
+ }
432
+ //#endregion
433
+ //#region src/host/ipc/ipc-bootstrap.ts
434
+ /** 订阅事件通道 → event 消息转发(事件名 "Service/method" 拆分)。 */
435
+ function forwardChannel(channel, stops) {
436
+ if (typeof channel.onAny !== "function") return;
437
+ const off = channel.onAny((event, ...args) => {
438
+ const slash = event.indexOf("/");
439
+ sendEvent(slash >= 0 ? event.slice(0, slash) : event, slash >= 0 ? event.slice(slash + 1) : "", args);
440
+ });
441
+ if (typeof off === "function") stops.push(off);
442
+ }
443
+ /** IPC 模式装配(enableIpc + 动作表 + 服务端 + 事件转发)。返回停止函数。 */
444
+ function startIpcMode(services) {
445
+ enableIpc();
446
+ const groupApi = services.groupApi;
447
+ const stopServer = startIpcServer({ actions: createIpcActions({
448
+ msgApi: services.msgApi,
449
+ groupApi: services.groupApi,
450
+ friendApi: services.friendApi,
451
+ self: services.self,
452
+ ...groupApi.uinToUid !== void 0 ? { uinToUid: groupApi.uinToUid } : {}
453
+ }) });
454
+ const stops = [];
455
+ forwardChannel(services.channel, stops);
456
+ forwardChannel(services.groupChannel, stops);
457
+ return () => {
458
+ stopServer();
459
+ for (const stop of stops) stop();
460
+ };
461
+ }
462
+ //#endregion
45
463
  //#region src/host/util.ts
46
464
  /**
47
465
  * util.ts:引导公共工具(日志 + 共享状态)。
@@ -60,6 +478,17 @@ function log(msg) {
60
478
  function errMsg(e) {
61
479
  return `${e?.message ?? e}`;
62
480
  }
481
+ /**
482
+ * 逐条处理消息回调参数(onRecvMsg 为消息数组——2026-08-07 运行时实证;
483
+ * 兼容单条对象,过滤无效项)。
484
+ */
485
+ function forEachRawMessage(msgs, handler) {
486
+ const list = Array.isArray(msgs) ? msgs : [msgs];
487
+ for (const msg of list) {
488
+ if (!msg || typeof msg !== "object") continue;
489
+ handler(msg);
490
+ }
491
+ }
63
492
  function createState() {
64
493
  return {
65
494
  wrapperExports: null,
@@ -69,7 +498,7 @@ function createState() {
69
498
  };
70
499
  }
71
500
  //#endregion
72
- //#region src/host/login.ts
501
+ //#region src/host/core/login.ts
73
502
  /** 打印可用快速登录账号 + 返回目标账号(启动横幅)。forcedUin(cli `-q` / NAPUTO_QUICK_UIN)优先。 */
74
503
  async function pickLoginAccount(kernel, ctx, loginResultRef, forcedUin) {
75
504
  try {
@@ -112,32 +541,56 @@ async function doLogin(core, opts) {
112
541
  }
113
542
  }
114
543
  //#endregion
115
- //#region src/host/protocols.ts
544
+ //#region src/host/load-config.ts
116
545
  /**
117
- * protocols.ts:协议装配(登录成功后)。
118
- * 2026-08-07 阶段 2:由 runtime/boot-protocols.js TS 化(零语义改动)。
119
- * 动态 import adapter/network 入口,装配 OB11 适配器。
120
- * 依赖 launcher 注入的 NAPUTO_ADAPTER_ENTRY / NAPUTO_NETWORK_ENTRY。
546
+ * 协议配置段读取(从 protocols.ts 拆分,2026-08-08 FTA 优化;2026-08-08 结构拍板:按账号取段)
547
+ *
548
+ * 全局 TOML 配置(<项目根>/napuketto.toml):**协议配置嵌在账号内**——
549
+ * accounts 数组每项含 [onebot11] / [satori] 段,登录成功后按账号 uin 找到对应项,
550
+ * 取其段作 seed(zod 校验)。路径优先级:NAPKETTO_CONFIG(launcher 注入)>
551
+ * 装配链自身探测(kernel.resolveConfigPath)> NAPUTO_CFG_DIR 兜底(旧行为兼容)。
552
+ * (ConfigBase seed 模式:load() 直接用内存值,不再读写独立协议文件)
121
553
  */
554
+ /** 账号内取协议段(宽松对象,装配时 zod 校验)。 */
555
+ function sectionOf(account, key) {
556
+ const section = account[key];
557
+ if (section !== void 0 && section !== null && typeof section === "object") return section;
558
+ }
122
559
  /**
123
- * 终端 ANSI 颜色(消息日志颜色分层,2026-08-07 用户定稿):
124
- * `loader | 接收 <- 群聊` 灰色(背景噪音)/ `[会话]` 青色 / `[发送者]` 绿色 / 内容默认色。
125
- * 仅 console 版嵌入;boot 文件日志用纯文本版(log),不污染文件。
560
+ * 按登录账号 uin 读取全局 TOML 的协议段(读取失败 / 未找到账号 / 无协议段 → 空段,
561
+ * 不阻塞装配)。uin 为登录成功后实际账号(-q 指定账号与配置不一致时以 uin 为准)。
126
562
  */
127
- const ANSI = {
128
- gray: "\x1B[90m",
129
- cyan: "\x1B[36m",
130
- green: "\x1B[32m",
131
- reset: "\x1B[0m"
132
- };
563
+ function loadProtocolSections(kernel, uin) {
564
+ const cfgFile = env.NAPKETTO_CONFIG || (0, node_path.join)(env.NAPUTO_CFG_DIR || ".", "napuketto.toml");
565
+ let ob11Section = {};
566
+ let satoriSection = {};
567
+ try {
568
+ const raw = (0, node_fs.readFileSync)(cfgFile, "utf8");
569
+ const accounts = kernel.parseToml(raw)["accounts"];
570
+ if (Array.isArray(accounts)) {
571
+ const mine = accounts.find((item) => typeof item === "object" && item !== null && String(item["qq"]) === uin);
572
+ if (mine !== void 0) {
573
+ ob11Section = sectionOf(mine, "onebot11") ?? {};
574
+ satoriSection = sectionOf(mine, "satori") ?? {};
575
+ }
576
+ }
577
+ } catch (e) {
578
+ log(`bootstrap: 全局配置读取失败(用默认 ob11/satori 配置): ${errMsg(e)}`);
579
+ }
580
+ return {
581
+ cfgFile,
582
+ ob11Section,
583
+ satoriSection
584
+ };
585
+ }
586
+ //#endregion
587
+ //#region src/host/core/assemble-protocols.ts
133
588
  /**
134
- * 登录成功后装配协议:kernel 各 Api + network 广播 + OB11 适配器。
589
+ * assemble-protocols.ts:OB11 / Satori 协议装配(非 IPC 模式,cli pnpm start)。
590
+ * 2026-08-08:从 protocols.ts 拆分——protocols.ts 只留入口 + IPC 分支。
135
591
  */
136
- async function startProtocols(kernel, ctx, loginResult) {
137
- const logger = kernel.createLogger?.({
138
- console: true,
139
- base: { name: "loader" }
140
- });
592
+ /** 装配 OB11 + Satori 适配器(登录成功后,非 IPC 模式)。 */
593
+ async function assembleOb11AndSatori(kernel, services, loginResult) {
141
594
  const adapterEntry = env.NAPUTO_ADAPTER_ENTRY;
142
595
  const networkEntry = env.NAPUTO_NETWORK_ENTRY;
143
596
  if (!adapterEntry || !networkEntry) {
@@ -147,80 +600,14 @@ async function startProtocols(kernel, ctx, loginResult) {
147
600
  try {
148
601
  const network = await import(`file://${networkEntry.replace(/\\/g, "/")}`);
149
602
  const onebot11Entry = adapterEntry.replace(/index\.mjs$/, "onebot11/index.mjs");
603
+ const satoriEntry = adapterEntry.replace(/index\.mjs$/, "satori/index.mjs");
150
604
  const coreEntry = adapterEntry.replace(/index\.mjs$/, "core/index.mjs");
151
605
  const adapter = await import(`file://${onebot11Entry.replace(/\\/g, "/")}`);
606
+ const satoriAdapter = await import(`file://${satoriEntry.replace(/\\/g, "/")}`);
152
607
  const adapterCore = await import(`file://${coreEntry.replace(/\\/g, "/")}`);
153
- const session = ctx.session;
154
- if (!session) {
155
- log("bootstrap: session 为空,无法装配协议");
156
- return;
157
- }
158
- const channel = new kernel.NTEventChannel("Msg");
159
- new kernel.MsgBridge(session, channel).register();
160
- channel.on("Msg/onRecvMsg", (msgs) => {
161
- const list = Array.isArray(msgs) ? msgs : [msgs];
162
- for (const msg of list) {
163
- if (!msg || typeof msg !== "object") continue;
164
- try {
165
- let rendered = "";
166
- for (const el of kernel.toCanonicalElements(msg)) if (el.type === "text") rendered += el.text;
167
- else if (el.type === "at") {
168
- const name = el.display ?? (el.target === "all" ? "全体" : el.target);
169
- rendered += `@${name}`;
170
- } else if (el.type === "face") rendered += "[表情]";
171
- else if (el.type === "image") rendered += "[图片]";
172
- else if (el.type === "voice") rendered += "[语音]";
173
- else if (el.type === "video") rendered += "[视频]";
174
- else if (el.type === "reply") rendered += "[回复]";
175
- else if (el.type === "file") rendered += `[文件${el.name ?? ""}]`;
176
- else if (el.type === "forward") rendered += "[合并转发]";
177
- else if (el.type === "json") rendered += "[json]";
178
- else if (el.type === "xml") rendered += "[xml]";
179
- if (rendered === "") rendered = "[空消息/媒体]";
180
- const isGroup = msg.chatType === kernel.ChatType.GROUP;
181
- const kind = isGroup ? "群聊" : "私聊";
182
- const peerUin = String(msg.peerUin ?? "");
183
- const senderUin = String(msg.senderUin ?? (peerUin || "未知"));
184
- const plainTags = isGroup ? `[群${peerUin}] [用户${senderUin}]` : `[用户${senderUin}]`;
185
- const coloredTags = isGroup ? `${ANSI.cyan}[群${peerUin}]${ANSI.reset} ${ANSI.green}[用户${senderUin}]${ANSI.reset}` : `${ANSI.green}[用户${senderUin}]${ANSI.reset}`;
186
- const content = rendered.replace(/\n/g, "\n ");
187
- const plain = `接收 <- ${kind} ${plainTags}: ${content}`;
188
- const colored = `${ANSI.gray}loader | 接收 <- ${kind}${ANSI.reset} ${coloredTags}: ${content}`;
189
- log(plain);
190
- logger?.info(colored);
191
- } catch (e) {
192
- const line2 = `接收消息(解析失败: ${errMsg(e)})`;
193
- log(line2);
194
- logger?.warn(line2);
195
- }
196
- }
197
- });
198
- const groupApi = new kernel.GroupApi(session);
199
- const msgApi = new kernel.MsgApi(session);
200
- const friendApi = new kernel.FriendApi(session, { uidToUin: (uids) => groupApi.uidToUin(uids) });
201
- const groupChannel = new kernel.NTEventChannel("Group");
202
- new kernel.GroupBridge(session, groupChannel).register();
203
- const groupCache = new kernel.GroupCache({
204
- channel: groupChannel,
205
- groupApi
206
- });
207
- groupCache.register();
208
- const groupNotifyApi = new kernel.GroupNotifyApi(session);
209
- const ticketApi = new kernel.TicketApi(session);
210
- const richMediaApi = new kernel.RichMediaApi(session);
211
- const profileApi = new kernel.ProfileApi(session);
212
- const profileLikeApi = new kernel.ProfileLikeApi(session);
213
- const webApi = new kernel.WebApi({ getCookies: (domain) => ticketApi.getCookies(domain, loginResult.uin) });
608
+ const { channel, groupCache } = services;
214
609
  const broadcaster = new network.EventBroadcaster();
215
- let ob11Section = {};
216
- const cfgFile = env.NAPKETTO_CONFIG || (0, node_path.join)(env.NAPUTO_CFG_DIR || ".", "napuketto.toml");
217
- try {
218
- const raw = (0, node_fs.readFileSync)(cfgFile, "utf8");
219
- const parsed = kernel.parseToml(raw);
220
- if (parsed && typeof parsed["onebot11"] === "object" && parsed["onebot11"] !== null) ob11Section = parsed["onebot11"];
221
- } catch (e) {
222
- log(`bootstrap: 全局配置读取失败(用默认 ob11 配置): ${errMsg(e)}`);
223
- }
610
+ const { cfgFile, ob11Section, satoriSection } = loadProtocolSections(kernel, loginResult.uin);
224
611
  const ob11Config = new adapterCore.ProtocolConfig({
225
612
  path: cfgFile,
226
613
  schema: adapter.ob11ConfigSchema,
@@ -231,19 +618,16 @@ async function startProtocols(kernel, ctx, loginResult) {
231
618
  config: ob11Config,
232
619
  broadcaster,
233
620
  msgChannel: channel,
234
- msgApi,
235
- groupApi,
236
- groupNotifyApi,
237
- friendApi,
238
- ticketApi,
239
- richMediaApi,
240
- profileApi,
241
- profileLikeApi,
242
- webApi,
243
- self: {
244
- uin: loginResult.uin,
245
- nickname: loginResult.nick
246
- },
621
+ msgApi: services.msgApi,
622
+ groupApi: services.groupApi,
623
+ groupNotifyApi: services.groupNotifyApi,
624
+ friendApi: services.friendApi,
625
+ ticketApi: services.ticketApi,
626
+ richMediaApi: services.richMediaApi,
627
+ profileApi: services.profileApi,
628
+ profileLikeApi: services.profileLikeApi,
629
+ webApi: services.webApi,
630
+ self: services.self,
247
631
  system: {
248
632
  appVersion: env.NAPUTO_QQ_VERSION || "unknown",
249
633
  cleanCache: async () => {
@@ -265,12 +649,168 @@ async function startProtocols(kernel, ctx, loginResult) {
265
649
  groupCache
266
650
  }).start();
267
651
  log("bootstrap: onebot11 adapter started");
652
+ const satoriCacheDir = typeof satoriSection["cacheDir"] === "string" && satoriSection["cacheDir"] !== "" ? satoriSection["cacheDir"] : (0, node_path.join)(env.NAPUTO_CFG_DIR || ".", "cache");
653
+ const satoriConfig = new adapterCore.ProtocolConfig({
654
+ path: cfgFile,
655
+ schema: satoriAdapter.satoriConfigSchema,
656
+ defaults: satoriAdapter.satoriConfigSchema.parse({}),
657
+ seed: satoriAdapter.satoriConfigSchema.parse(satoriSection)
658
+ });
659
+ await new satoriAdapter.NapukettoSatoriAdapter({
660
+ config: satoriConfig,
661
+ broadcaster,
662
+ msgChannel: channel,
663
+ msgApi: services.msgApi,
664
+ groupApi: services.groupApi,
665
+ groupNotifyApi: services.groupNotifyApi,
666
+ friendApi: services.friendApi,
667
+ profileApi: services.profileApi,
668
+ self: services.self,
669
+ cacheDir: satoriCacheDir,
670
+ groupCache
671
+ }).start();
672
+ log("bootstrap: satori adapter started");
673
+ } catch (e) {
674
+ log(`bootstrap: 协议装配失败: ${errMsg(e)}`);
675
+ }
676
+ }
677
+ //#endregion
678
+ //#region src/host/msg-log.ts
679
+ /** 终端 ANSI 颜色(消息日志颜色分层,2026-08-07 用户定稿)。 */
680
+ const ANSI = {
681
+ gray: "\x1B[90m",
682
+ cyan: "\x1B[36m",
683
+ green: "\x1B[32m",
684
+ reset: "\x1B[0m"
685
+ };
686
+ /** canonical 元素 → 日志占位文本。 */
687
+ function elementToText(el) {
688
+ if (el.type === "text") return el.text ?? "";
689
+ if (el.type === "at") return `@${el.display ?? (el.target === "all" ? "全体" : el.target)}`;
690
+ if (el.type === "file") return `[文件${el.name ?? ""}]`;
691
+ if (el.type === "forward") return "[合并转发]";
692
+ return `[${el.type}]`;
693
+ }
694
+ /** 渲染消息为单行文本(text 拼接,at/媒体元素转占位)。 */
695
+ function renderMessage(kernel, msg) {
696
+ let rendered = "";
697
+ for (const el of kernel.toCanonicalElements(msg)) rendered += elementToText(el);
698
+ return rendered === "" ? "[空消息/媒体]" : rendered;
699
+ }
700
+ /**
701
+ * 消息日志订阅(onRecvMsg 回调参数为消息数组——2026-08-07 运行时实证,遍历逐条打印)。
702
+ */
703
+ function setupMsgLogging(kernel, channel, logger) {
704
+ channel.on("Msg/onRecvMsg", (msgs) => {
705
+ forEachRawMessage(msgs, (m) => {
706
+ try {
707
+ const raw = m;
708
+ const rendered = renderMessage(kernel, m);
709
+ const isGroup = raw.chatType === kernel.ChatType.GROUP;
710
+ const kind = isGroup ? "群聊" : "私聊";
711
+ const peerUin = String(raw.peerUin ?? "");
712
+ const senderUin = String(raw.senderUin ?? (peerUin || "未知"));
713
+ const plainTags = isGroup ? `[群${peerUin}] [用户${senderUin}]` : `[用户${senderUin}]`;
714
+ const coloredTags = isGroup ? `${ANSI.cyan}[群${peerUin}]${ANSI.reset} ${ANSI.green}[用户${senderUin}]${ANSI.reset}` : `${ANSI.green}[用户${senderUin}]${ANSI.reset}`;
715
+ const content = rendered.replace(/\n/g, "\n ");
716
+ const plain = `接收 <- ${kind} ${plainTags}: ${content}`;
717
+ const colored = `${ANSI.gray}loader | 接收 <- ${kind}${ANSI.reset} ${coloredTags}: ${content}`;
718
+ log(plain);
719
+ logger?.info(colored);
720
+ } catch (e) {
721
+ const line2 = `接收消息(解析失败: ${errMsg(e)})`;
722
+ log(line2);
723
+ logger?.warn(line2);
724
+ }
725
+ });
726
+ });
727
+ }
728
+ //#endregion
729
+ //#region src/host/core/kernel-services.ts
730
+ /**
731
+ * kernel-services.ts:kernel 业务服务装配(IPC 模式与协议模式共用)。
732
+ *
733
+ * 登录成功后创建:消息/群事件通道 + 桥 + 缓存 + kernel 各 Api + 自身信息。
734
+ * IPC 模式(koishi 插件)经此拿服务装配 ipc-server;非 IPC(cli)经此拿服务
735
+ * 装配 OB11/Satori 适配器。
736
+ */
737
+ /** 登录成功后创建 kernel 业务服务(channel/bridge/cache/apis)。失败返回 null。 */
738
+ async function createKernelServices(kernel, ctx, loginResult) {
739
+ const logger = kernel.createLogger?.({
740
+ console: true,
741
+ base: { name: "loader" }
742
+ });
743
+ const session = ctx.session;
744
+ if (!session) {
745
+ log("bootstrap: session 为空,无法创建 kernel 服务");
746
+ return null;
747
+ }
748
+ const channel = new kernel.NTEventChannel("Msg");
749
+ new kernel.MsgBridge(session, channel).register();
750
+ setupMsgLogging(kernel, channel, logger);
751
+ const groupApi = new kernel.GroupApi(session);
752
+ const msgApi = new kernel.MsgApi(session);
753
+ const friendApi = new kernel.FriendApi(session, { uidToUin: (uids) => groupApi.uidToUin(uids) });
754
+ const groupChannel = new kernel.NTEventChannel("Group");
755
+ new kernel.GroupBridge(session, groupChannel).register();
756
+ const groupCache = new kernel.GroupCache({
757
+ channel: groupChannel,
758
+ groupApi
759
+ });
760
+ groupCache.register();
761
+ const groupNotifyApi = new kernel.GroupNotifyApi(session);
762
+ const ticketApi = new kernel.TicketApi(session);
763
+ return {
764
+ kernel,
765
+ ctx,
766
+ logger,
767
+ channel,
768
+ groupChannel,
769
+ msgApi,
770
+ groupApi,
771
+ friendApi,
772
+ groupCache,
773
+ groupNotifyApi,
774
+ ticketApi,
775
+ richMediaApi: new kernel.RichMediaApi(session),
776
+ profileApi: new kernel.ProfileApi(session),
777
+ profileLikeApi: new kernel.ProfileLikeApi(session),
778
+ webApi: new kernel.WebApi({ getCookies: (domain) => ticketApi.getCookies(domain, loginResult.uin) }),
779
+ self: {
780
+ uin: loginResult.uin,
781
+ nickname: loginResult.nick ?? ""
782
+ }
783
+ };
784
+ }
785
+ //#endregion
786
+ //#region src/host/core/protocols.ts
787
+ /**
788
+ * protocols.ts:协议装配入口(登录成功后)。
789
+ * 2026-08-08 重构:kernel 服务创建 → kernel-services.ts;OB11/Satori 装配 →
790
+ * assemble-protocols.ts。本文件只留入口 + IPC 模式分支。
791
+ *
792
+ * 分支:
793
+ * - IPC 模式(NAPUTO_IPC=1,koishi 插件驱动):跳过 OB11/Satori(插件直接消费
794
+ * kernel 服务),返回 KernelServices 由 bootstrap 装配 ipc-server。
795
+ * - 非 IPC(cli pnpm start):照旧装配 OB11/Satori 网络传输。
796
+ */
797
+ /** 登录成功后装配协议:IPC 模式返回 kernel 服务(供 ipc-server),否则装配 OB11/Satori。 */
798
+ async function startProtocols(kernel, ctx, loginResult) {
799
+ const services = await createKernelServices(kernel, ctx, loginResult);
800
+ if (services === null) return null;
801
+ if (env.NAPUTO_IPC === "1") {
802
+ log("bootstrap: IPC 模式,跳过 OB11/Satori 装配(koishi 插件直接消费 kernel 服务)");
803
+ return services;
804
+ }
805
+ try {
806
+ await assembleOb11AndSatori(kernel, services, loginResult);
268
807
  } catch (e) {
269
808
  log(`bootstrap: 协议装配失败: ${errMsg(e)}`);
270
809
  }
810
+ return services;
271
811
  }
272
812
  //#endregion
273
- //#region src/host/session.ts
813
+ //#region src/host/core/session.ts
274
814
  /** 对象有效性判断:getMsgService() 调用不抛断言(cpp_impl 已激活)。
275
815
  * 与 isSessionUsable 的区别:这里允许返回 null(未 init 但对象有效)。 */
276
816
  function isSessionObjectValid(s) {
@@ -395,7 +935,7 @@ function pickBestSession(candidates) {
395
935
  return bestValid;
396
936
  }
397
937
  //#endregion
398
- //#region src/host/smoke.ts
938
+ //#region src/host/core/smoke.ts
399
939
  /**
400
940
  * smoke.ts:P2-1 收发消息冒烟自检(NAPUTO_SMOKE=1 触发,2026-08-06)。
401
941
  * 2026-08-07 阶段 2:由 runtime/boot-smoke.js TS 化(零语义改动)。
@@ -434,6 +974,10 @@ function parseSmokeTarget(envPeer, selfUin) {
434
974
  uin: selfUin
435
975
  };
436
976
  }
977
+ /** 提取消息纯文本(text 元素拼接,onRecvMsg 与落库核对共用)。 */
978
+ function extractTexts(kernel, msg) {
979
+ return kernel.toCanonicalElements(msg).filter((el) => el.type === "text").map((el) => el.text).join("");
980
+ }
437
981
  /** uin → uid(c2c 目标用;group 直接拿群号作 peerUid)。 */
438
982
  async function resolvePeerUid(kernel, ctx, target, selfUid) {
439
983
  const groupApi = new kernel.GroupApi(ctx.session);
@@ -471,18 +1015,16 @@ async function runSmokeTest(kernel, ctx, loginResult) {
471
1015
  let received = 0;
472
1016
  let receivedText = "";
473
1017
  channel.on("Msg/onRecvMsg", (msgs) => {
474
- const list = Array.isArray(msgs) ? msgs : [msgs];
475
- for (const msg of list) {
476
- if (!msg || typeof msg !== "object") continue;
1018
+ forEachRawMessage(msgs, (m) => {
477
1019
  received += 1;
478
1020
  try {
479
- const texts = kernel.toCanonicalElements(msg).filter((el) => el.type === "text").map((el) => el.text).join("");
1021
+ const texts = extractTexts(kernel, m);
480
1022
  if (texts) receivedText = texts;
481
1023
  } catch (e) {
482
1024
  log(`smoke: onRecvMsg 解析失败: ${errMsg(e)}`);
483
1025
  }
484
- log(`smoke: 📥 onRecvMsg 收到 #${received}(msgId=${msg.msgId ?? "?"} seq=${msg.msgSeq ?? "?"} text="${receivedText}")`);
485
- }
1026
+ log(`smoke: 📥 onRecvMsg 收到 #${received}(msgId=${m.msgId ?? "?"} seq=${m.msgSeq ?? "?"} text="${receivedText}")`);
1027
+ });
486
1028
  });
487
1029
  const msgApi = new kernel.MsgApi(session);
488
1030
  const peer = await resolvePeerUid(kernel, ctx, target, loginResult.uid);
@@ -504,10 +1046,7 @@ async function runSmokeTest(kernel, ctx, loginResult) {
504
1046
  const recent = await msgApi.fetchMessages(peer, { count: 10 });
505
1047
  landed = recent.some((m) => m.msgId === sentMsgId || m.msgSeq === sentMsgId);
506
1048
  log(`smoke: 落库验证: 最近 ${recent.length} 条${landed ? " ✅ 含刚发消息" : " ❌ 未找到(可能同步延迟)"}`);
507
- if (!landed) for (const m of recent.slice(0, 3)) {
508
- const texts = kernel.toCanonicalElements(m).filter((el) => el.type === "text").map((el) => el.text).join("");
509
- log(`smoke: 最近消息 msgId=${m.msgId} seq=${m.msgSeq} text="${texts}"`);
510
- }
1049
+ if (!landed) for (const m of recent.slice(0, 3)) log(`smoke: 最近消息 msgId=${m.msgId} seq=${m.msgSeq} text="${extractTexts(kernel, m)}"`);
511
1050
  } catch (e) {
512
1051
  log(`smoke: 落库验证失败: ${errMsg(e)}`);
513
1052
  }
@@ -522,7 +1061,176 @@ async function runSmokeTest(kernel, ctx, loginResult) {
522
1061
  return ok;
523
1062
  }
524
1063
  //#endregion
525
- //#region src/host/bootstrap.ts
1064
+ //#region src/host/core/bootstrap-core.ts
1065
+ /**
1066
+ * bootstrap-core.ts:NapukettoCore 装配路径(从 bootstrap.ts 拆分,2026-08-08 FTA 优化)。
1067
+ *
1068
+ * NapukettoCore.create → attachWrapper → 登录(pickLoginAccount + doLogin)→
1069
+ * session 替换/激活 → 就绪等待 → 冒烟自检 → 协议装配 → 探测模式。
1070
+ */
1071
+ /** 探测:create() 与捕获的 QQ session 关系(确认 create() 是否干扰 QQ)。 */
1072
+ function probeCreatedSession(state, ctx) {
1073
+ try {
1074
+ const S2 = state.wrapperExports?.["NodeIQQNTWrapperSession"];
1075
+ const created = S2 && typeof S2.create === "function" ? S2.create() : null;
1076
+ log(`BOOT: create()===qqSession? ${created === state.qqSession} | create()===ctx.session? ${created === ctx.session} | qqSession===ctx.session? ${state.qqSession === ctx.session}`);
1077
+ const sess = created;
1078
+ const svc = sess && typeof sess.getMsgService === "function" ? sess.getMsgService() : null;
1079
+ const qs = state.qqSession;
1080
+ log(`BOOT: create().getMsgService=${svc !== null && svc !== void 0 ? "ready" : "null"} qqSession.getMsgService=${qs && typeof qs.getMsgService === "function" && qs.getMsgService() !== null && qs.getMsgService() !== void 0 ? "ready" : "null"}`);
1081
+ } catch (e) {
1082
+ log(`BOOT: create() 探测失败: ${errMsg(e)}`);
1083
+ }
1084
+ }
1085
+ /** 登录状态字面量(IPC login 消息,与 kernel LoginState 对齐)。 */
1086
+ const LOGIN_STATES = [
1087
+ "idle",
1088
+ "waiting_scan",
1089
+ "scanned",
1090
+ "logged_in",
1091
+ "failed"
1092
+ ];
1093
+ /** 宽松收窄(kernel onLoginProgress.state 是 string)。 */
1094
+ function isLoginState(value) {
1095
+ return LOGIN_STATES.includes(value);
1096
+ }
1097
+ /** 登录参数(NAPUTO_QUICK_UIN 强制指定 / ref 目标 / 默认)。 */
1098
+ function buildLoginOpts(Appid, forcedUin, ref) {
1099
+ const quickUin = forcedUin ?? ref.targetUin;
1100
+ const opts = {
1101
+ appid: Appid,
1102
+ initTimeoutMs: 2e4,
1103
+ ...quickUin !== void 0 ? { quickUin } : {}
1104
+ };
1105
+ if (env.NAPUTO_IPC === "1") opts["onLoginProgress"] = (progress) => {
1106
+ if (progress.qr !== void 0) sendQr(progress.qr.pngBase64, progress.qr.qrcodeUrl);
1107
+ if (isLoginState(progress.state)) sendLogin(progress.state, progress.selfInfo);
1108
+ };
1109
+ return opts;
1110
+ }
1111
+ /** V2 登录后替换 session:优先 QQ 主 session,其次 vehicle 单例表;自建宿主例外。 */
1112
+ function replaceSession(kernel, core, state, ctx) {
1113
+ if (env.NAPUTO_SELF_HOST === "1") return null;
1114
+ const chosen = pickBestSession(collectCandidateSessions(state, kernel, ctx));
1115
+ if (chosen && chosen.s !== ctx.session) {
1116
+ core.setSession(chosen.s);
1117
+ log(`bootstrap: 已替换 session(来源=${chosen.tag},msgSvc=${isSessionUsable(chosen.s) ? "READY" : "null"})`);
1118
+ } else if (chosen) log(`bootstrap: 候选 session 与 ctx.session 相同(${chosen.tag})`);
1119
+ else log("bootstrap: 无有效候选 session(保留 kernel 自建 session)");
1120
+ return chosen;
1121
+ }
1122
+ /**
1123
+ * 激活目标 session(先 init 后 startupSession.start,HANDOVER-V6 三要素)。
1124
+ * 自建宿主无候选时对 kernel 自建 session 走 NapCat 激活。
1125
+ */
1126
+ async function activateSession(kernel, state, ctx, chosen, loginResult, Appid, bootEnv) {
1127
+ const activateTarget = chosen ?? {
1128
+ s: ctx.session,
1129
+ tag: "kernel 自建 session"
1130
+ };
1131
+ if (!activateTarget.s || isSessionUsable(activateTarget.s)) return;
1132
+ log(`bootstrap: 激活 session(${activateTarget.tag},先 init 后 startupSession.start)...`);
1133
+ try {
1134
+ const qqDataRoot = typeof kernel.resolveQqUserDataRoot === "function" ? kernel.resolveQqUserDataRoot(state.wrapperExports) : null;
1135
+ const accountPath = qqDataRoot && typeof kernel.resolveQqGlobalPath === "function" ? kernel.resolveQqGlobalPath(qqDataRoot) : bootEnv.dataDir || ".";
1136
+ const sessionConfig = kernel.buildSessionConfig({
1137
+ appid: Appid,
1138
+ fullVersion: bootEnv.qqVersion || "",
1139
+ selfUin: loginResult.uin,
1140
+ selfUid: loginResult.uid,
1141
+ accountPath,
1142
+ downloadPath: (0, node_path.join)(accountPath, "NapCat", "temp")
1143
+ });
1144
+ const listener = kernel.createLifecycleSessionListener();
1145
+ await kernel.initAndStartSession(ctx, sessionConfig, listener, { timeoutMs: 2e4 });
1146
+ log("bootstrap: 激活 session init + start 完成");
1147
+ } catch (initErr) {
1148
+ log(`bootstrap: 激活 session init 失败: ${errMsg(initErr)}`);
1149
+ }
1150
+ }
1151
+ /** 等 session 就绪(getMsgService 非 null)——init 完成后才有。 */
1152
+ async function waitSessionReady(kernel, ctx) {
1153
+ try {
1154
+ await kernel.waitSessionReady(ctx, { timeoutMs: 3e4 });
1155
+ log("bootstrap: QQ session 就绪(getMsgService 可用)");
1156
+ } catch (readyErr) {
1157
+ log(`bootstrap: 等待 session 就绪失败: ${errMsg(readyErr)}`);
1158
+ }
1159
+ }
1160
+ /** P2-1 收发消息冒烟自检(NAPUTO_SMOKE=1):MsgBridge + MsgApi 真发/收一条。 */
1161
+ async function runSmokeIfEnabled(kernel, ctx, loginResult) {
1162
+ if (env.NAPUTO_SMOKE !== "1") return;
1163
+ try {
1164
+ log(`bootstrap: 冒烟自检${await runSmokeTest(kernel, ctx, loginResult) ? "通过" : "未完全通过"}(见上文 smoke 日志)`);
1165
+ } catch (smokeErr) {
1166
+ log(`bootstrap: 冒烟自检异常: ${errMsg(smokeErr)}`);
1167
+ }
1168
+ }
1169
+ /** 探测模式(NAPUTO_PROBE=1):立即探测 + 35s 后晚探测(napuketto-probe-late.json)。 */
1170
+ function runProbePhase(kernel, ctx) {
1171
+ if (env.NAPUTO_PROBE !== "1" || typeof kernel.probeRuntime !== "function") return;
1172
+ try {
1173
+ const probe = kernel.probeRuntime(ctx);
1174
+ log(`bootstrap: probe done, session=${probe.session ? "ok" : "null"}, services=${Object.keys(probe.services ?? {}).length}`);
1175
+ setTimeout(() => {
1176
+ try {
1177
+ if (typeof kernel.probeRuntime !== "function") return;
1178
+ const late = kernel.probeRuntime(ctx, "napuketto-probe-late.json");
1179
+ log(`bootstrap: probe-late done, session=${late.session ? "ok" : "null"}, services=${Object.keys(late.services ?? {}).length}`);
1180
+ } catch (e2) {
1181
+ log(`bootstrap: probe-late error: ${errMsg(e2)}`);
1182
+ }
1183
+ }, 35e3);
1184
+ } catch (e) {
1185
+ log(`bootstrap: probe error: ${errMsg(e)}`);
1186
+ }
1187
+ }
1188
+ /** NapukettoCore 装配路径:create → attachWrapper → 登录 → 激活 → 协议 → 探测。 */
1189
+ async function bootstrapWithCore(kernel, state, bootEnv, Appid) {
1190
+ const coreCtor = kernel.NapukettoCore;
1191
+ if (coreCtor === void 0) return;
1192
+ const core = coreCtor.create({
1193
+ paths: { dataRoot: bootEnv.dataDir },
1194
+ logLevel: "info"
1195
+ });
1196
+ const ctx = core.attachWrapper(state.wrapperExports, { ...bootEnv });
1197
+ log(`bootstrap: attachWrapper OK, engine=${typeof ctx.engine}, session=${ctx.session !== null}`);
1198
+ probeCreatedSession(state, ctx);
1199
+ startSessionProbe(state, ctx);
1200
+ probeSessionMethods(ctx);
1201
+ if (typeof core.login !== "function") {
1202
+ log("bootstrap: kernel core missing login fn");
1203
+ return;
1204
+ }
1205
+ const forcedUin = env.NAPUTO_QUICK_UIN;
1206
+ const ref = { targetUin: void 0 };
1207
+ await pickLoginAccount(kernel, ctx, ref, forcedUin);
1208
+ if (env.NAPUTO_IPC === "1") sendStatus("logging");
1209
+ const loginResult = await doLogin(core, buildLoginOpts(Appid, forcedUin, ref));
1210
+ if (loginResult === null) {
1211
+ log("bootstrap: 登录失败,引导中止");
1212
+ if (env.NAPUTO_IPC === "1") sendStatus("failed", "登录失败", {
1213
+ code: "NOT_LOGIN",
1214
+ message: "登录失败"
1215
+ });
1216
+ return;
1217
+ }
1218
+ log(`bootstrap: 登录成功 uin=${loginResult.uin} uid=${loginResult.uid} nick=${loginResult.nick}`);
1219
+ if (env.NAPUTO_IPC === "1") sendLogin("logged_in", {
1220
+ uin: loginResult.uin,
1221
+ uid: loginResult.uid,
1222
+ nick: loginResult.nick ?? ""
1223
+ });
1224
+ await activateSession(kernel, state, ctx, replaceSession(kernel, core, state, ctx), loginResult, Appid, bootEnv);
1225
+ await waitSessionReady(kernel, ctx);
1226
+ if (env.NAPUTO_IPC === "1") sendStatus("sessioning");
1227
+ await runSmokeIfEnabled(kernel, ctx, loginResult);
1228
+ const services = await startProtocols(kernel, ctx, loginResult);
1229
+ if (env.NAPUTO_IPC === "1" && services !== null) startIpcMode(services);
1230
+ runProbePhase(kernel, ctx);
1231
+ }
1232
+ //#endregion
1233
+ //#region src/host/core/bootstrap.ts
526
1234
  /**
527
1235
  * bootstrap.ts:kernel 引导核心(import kernel → 装配 → 登录 → session 替换 → 协议装配)。
528
1236
  * 2026-08-07 阶段 2:由 runtime/boot-bootstrap.js TS 化(零语义改动)。
@@ -532,6 +1240,7 @@ async function runSmokeTest(kernel, ctx, loginResult) {
532
1240
  * - 登录流程(选账号 / 快速登录 / QR 回退)→ login.ts
533
1241
  * - session 选择 / 探测 / 就绪 → session.ts
534
1242
  * - 协议装配 → protocols.ts;冒烟自检 → smoke.ts
1243
+ * 2026-08-08 FTA 优化:NapukettoCore 装配路径 → bootstrap-core.ts(本文件留入口 + fallback)。
535
1244
  *
536
1245
  * session 来源(2026-08-07 实测结论;V1 Proxy 捕获与 vehicle 载具已归档 archive/):
537
1246
  * 登录成功后按优先级取 session:
@@ -540,6 +1249,52 @@ async function runSmokeTest(kernel, ctx, loginResult) {
540
1249
  * ② get() / qqSession(V1 兼容)
541
1250
  * 取到后 core.setSession 替换 → waitSessionReady 确认 getMsgService READY。
542
1251
  */
1252
+ /** ⭐ appid 动态解析(2026-08-06 P2-0 实测:硬编码 537237765 在 9.9.33 扫码失败
1253
+ * 「请下载最新版」;major.node 解析的 537376818 成功)。自研,参考 NapCat 思路。 */
1254
+ function resolveAppid(kernel, bootEnv) {
1255
+ return typeof kernel.parseAppidFromMajor === "function" && bootEnv.wrapperPath && kernel.parseAppidFromMajor((0, node_path.join)((0, node_path.dirname)(bootEnv.wrapperPath), "major.node")) || typeof kernel.resolveAppidQua === "function" && kernel.resolveAppidQua(bootEnv.qqVersion || "").appid || "537237765";
1256
+ }
1257
+ /** 回退:旧装配路径(startNapuketto + 手工 lifecycle)。 */
1258
+ async function bootstrapFallback(kernel, state, bootEnv, Appid) {
1259
+ log("bootstrap: kernel has no NapukettoCore, falling back");
1260
+ const ctx = kernel.startNapuketto?.({
1261
+ wrapperExports: state.wrapperExports,
1262
+ env: { ...bootEnv }
1263
+ });
1264
+ if (ctx === void 0) {
1265
+ log("bootstrap: startNapuketto 不可用");
1266
+ return;
1267
+ }
1268
+ log(`bootstrap: startNapuketto OK, engine=${typeof ctx.engine}, session=${ctx.session !== null}`);
1269
+ if (typeof kernel.quickLogin === "function" && typeof kernel.initAndStartSession === "function") {
1270
+ if (typeof kernel.buildLoginConfig === "function" && ctx.loginService) {
1271
+ const loginCfg = kernel.buildLoginConfig(Appid, bootEnv.qqVersion || "", bootEnv.dataDir || ".");
1272
+ if (typeof ctx.loginService.initConfig === "function") {
1273
+ ctx.loginService.initConfig(loginCfg);
1274
+ log("bootstrap: loginService.initConfig OK");
1275
+ }
1276
+ }
1277
+ const loginResult = await kernel.quickLogin(ctx, {});
1278
+ log(`bootstrap: quickLogin OK, uin=${loginResult.uin}, uid=${loginResult.uid}`);
1279
+ const sessionConfig = kernel.buildSessionConfig({
1280
+ appid: Appid,
1281
+ fullVersion: bootEnv.qqVersion || "",
1282
+ selfUin: loginResult.uin,
1283
+ selfUid: loginResult.uid,
1284
+ accountPath: bootEnv.dataDir || ".",
1285
+ downloadPath: (0, node_path.join)(bootEnv.dataDir || ".", "temp")
1286
+ });
1287
+ const listener = kernel.createLifecycleSessionListener();
1288
+ await kernel.initAndStartSession(ctx, sessionConfig, listener, { timeoutMs: 2e4 });
1289
+ log("bootstrap: session init + startNT OK!");
1290
+ } else log("bootstrap: kernel missing lifecycle fns (quickLogin/initAndStartSession)");
1291
+ if (env.NAPUTO_PROBE === "1" && typeof kernel.probeRuntime === "function") try {
1292
+ const probe = kernel.probeRuntime(ctx);
1293
+ log(`bootstrap: probe done, session=${probe.session ? "ok" : "null"}, services=${Object.keys(probe.services ?? {}).length}`);
1294
+ } catch (e) {
1295
+ log(`bootstrap: probe error: ${errMsg(e)}`);
1296
+ }
1297
+ }
543
1298
  /** 核心引导:import kernel → 装配 → 登录 → 替换 session → 等待就绪 → 协议装配。 */
544
1299
  async function bootstrap(state) {
545
1300
  const kernelEntry = env.NAPUTO_KERNEL_ENTRY;
@@ -555,148 +1310,15 @@ async function bootstrap(state) {
555
1310
  return;
556
1311
  }
557
1312
  const bootEnv = {
558
- qqVersion: env.NAPUTO_QQ_VERSION,
559
- dataDir: env.NAPUTO_CFG_DIR,
560
- wrapperPath: env.NAPUTO_WRAPPER_PATH
1313
+ qqVersion: env.NAPUTO_QQ_VERSION || "",
1314
+ dataDir: env.NAPUTO_CFG_DIR || "",
1315
+ wrapperPath: env.NAPUTO_WRAPPER_PATH || ""
561
1316
  };
562
- const Appid = typeof kernel.parseAppidFromMajor === "function" && bootEnv.wrapperPath && kernel.parseAppidFromMajor((0, node_path.join)((0, node_path.dirname)(bootEnv.wrapperPath), "major.node")) || typeof kernel.resolveAppidQua === "function" && kernel.resolveAppidQua(bootEnv.qqVersion || "").appid || "537237765";
1317
+ const Appid = resolveAppid(kernel, bootEnv);
563
1318
  log(`bootstrap: appid=${Appid}(wrapper=${bootEnv.wrapperPath})`);
564
1319
  try {
565
- if (kernel.NapukettoCore !== void 0) {
566
- const core = kernel.NapukettoCore.create({
567
- paths: { dataRoot: bootEnv.dataDir },
568
- logLevel: "info"
569
- });
570
- const ctx = core.attachWrapper(state.wrapperExports, bootEnv);
571
- log(`bootstrap: attachWrapper OK, engine=${typeof ctx.engine}, session=${ctx.session !== null}`);
572
- try {
573
- const S2 = state.wrapperExports?.["NodeIQQNTWrapperSession"];
574
- const created = S2 && typeof S2.create === "function" ? S2.create() : null;
575
- log(`BOOT: create()===qqSession? ${created === state.qqSession} | create()===ctx.session? ${created === ctx.session} | qqSession===ctx.session? ${state.qqSession === ctx.session}`);
576
- const sess = created;
577
- const svc = sess && typeof sess.getMsgService === "function" ? sess.getMsgService() : null;
578
- const qs = state.qqSession;
579
- log(`BOOT: create().getMsgService=${svc !== null && svc !== void 0 ? "ready" : "null"} qqSession.getMsgService=${qs && typeof qs.getMsgService === "function" && qs.getMsgService() !== null && qs.getMsgService() !== void 0 ? "ready" : "null"}`);
580
- } catch (e) {
581
- log(`BOOT: create() 探测失败: ${errMsg(e)}`);
582
- }
583
- startSessionProbe(state, ctx);
584
- probeSessionMethods(ctx);
585
- let loginResult = null;
586
- if (typeof core.login === "function") {
587
- const forcedUin = env.NAPUTO_QUICK_UIN;
588
- const ref = { targetUin: void 0 };
589
- await pickLoginAccount(kernel, ctx, ref, forcedUin);
590
- loginResult = await doLogin(core, {
591
- appid: Appid,
592
- initTimeoutMs: 2e4,
593
- ...forcedUin ? { quickUin: forcedUin } : ref.targetUin !== void 0 ? { quickUin: ref.targetUin } : {}
594
- });
595
- if (loginResult === null) {
596
- log("bootstrap: 登录失败,引导中止");
597
- return;
598
- }
599
- log(`bootstrap: 登录成功 uin=${loginResult.uin} uid=${loginResult.uid} nick=${loginResult.nick}`);
600
- const isSelfHost = env.NAPUTO_SELF_HOST === "1";
601
- let chosen = null;
602
- if (!isSelfHost) chosen = pickBestSession(collectCandidateSessions(state, kernel, ctx));
603
- if (chosen && chosen.s !== ctx.session) {
604
- core.setSession(chosen.s);
605
- log(`bootstrap: 已替换 session(来源=${chosen.tag},msgSvc=${isSessionUsable(chosen.s) ? "READY" : "null"})`);
606
- } else if (chosen) log(`bootstrap: 候选 session 与 ctx.session 相同(${chosen.tag})`);
607
- else log("bootstrap: 无有效候选 session(保留 kernel 自建 session)");
608
- const activateTarget = chosen ?? {
609
- s: ctx.session,
610
- tag: "kernel 自建 session"
611
- };
612
- if (activateTarget.s && !isSessionUsable(activateTarget.s)) {
613
- log(`bootstrap: 激活 session(${activateTarget.tag},先 init 后 startupSession.start)...`);
614
- try {
615
- const qqDataRoot = typeof kernel.resolveQqUserDataRoot === "function" ? kernel.resolveQqUserDataRoot(state.wrapperExports) : null;
616
- const accountPath = qqDataRoot && typeof kernel.resolveQqGlobalPath === "function" ? kernel.resolveQqGlobalPath(qqDataRoot) : bootEnv.dataDir || ".";
617
- const sessionConfig = kernel.buildSessionConfig({
618
- appid: Appid,
619
- fullVersion: bootEnv.qqVersion || "",
620
- selfUin: loginResult.uin,
621
- selfUid: loginResult.uid,
622
- accountPath,
623
- downloadPath: (0, node_path.join)(accountPath, "NapCat", "temp")
624
- });
625
- const listener = kernel.createLifecycleSessionListener();
626
- await kernel.initAndStartSession(ctx, sessionConfig, listener, { timeoutMs: 2e4 });
627
- log("bootstrap: 激活 session init + start 完成");
628
- } catch (initErr) {
629
- log(`bootstrap: 激活 session init 失败: ${errMsg(initErr)}`);
630
- }
631
- }
632
- try {
633
- await kernel.waitSessionReady(ctx, { timeoutMs: 3e4 });
634
- log("bootstrap: QQ session 就绪(getMsgService 可用)");
635
- } catch (readyErr) {
636
- log(`bootstrap: 等待 session 就绪失败: ${errMsg(readyErr)}`);
637
- }
638
- if (env.NAPUTO_SMOKE === "1" && loginResult !== null) try {
639
- log(`bootstrap: 冒烟自检${await runSmokeTest(kernel, ctx, loginResult) ? "通过" : "未完全通过"}(见上文 smoke 日志)`);
640
- } catch (smokeErr) {
641
- log(`bootstrap: 冒烟自检异常: ${errMsg(smokeErr)}`);
642
- }
643
- } else log("bootstrap: kernel core missing login fn");
644
- if (loginResult !== null) await startProtocols(kernel, ctx, loginResult);
645
- if (env.NAPUTO_PROBE === "1" && typeof kernel.probeRuntime === "function") try {
646
- const probe = kernel.probeRuntime(ctx);
647
- log(`bootstrap: probe done, session=${probe.session ? "ok" : "null"}, services=${Object.keys(probe.services ?? {}).length}`);
648
- setTimeout(() => {
649
- try {
650
- if (typeof kernel.probeRuntime !== "function") return;
651
- const late = kernel.probeRuntime(ctx, "napuketto-probe-late.json");
652
- log(`bootstrap: probe-late done, session=${late.session ? "ok" : "null"}, services=${Object.keys(late.services ?? {}).length}`);
653
- } catch (e2) {
654
- log(`bootstrap: probe-late error: ${errMsg(e2)}`);
655
- }
656
- }, 35e3);
657
- } catch (e) {
658
- log(`bootstrap: probe error: ${errMsg(e)}`);
659
- }
660
- } else {
661
- log("bootstrap: kernel has no NapukettoCore, falling back");
662
- const ctx = kernel.startNapuketto?.({
663
- wrapperExports: state.wrapperExports,
664
- env: bootEnv
665
- });
666
- if (ctx === void 0) {
667
- log("bootstrap: startNapuketto 不可用");
668
- return;
669
- }
670
- log(`bootstrap: startNapuketto OK, engine=${typeof ctx.engine}, session=${ctx.session !== null}`);
671
- if (typeof kernel.quickLogin === "function" && typeof kernel.initAndStartSession === "function") {
672
- if (typeof kernel.buildLoginConfig === "function" && ctx.loginService) {
673
- const loginCfg = kernel.buildLoginConfig(Appid, bootEnv.qqVersion || "", bootEnv.dataDir || ".");
674
- if (typeof ctx.loginService.initConfig === "function") {
675
- ctx.loginService.initConfig(loginCfg);
676
- log("bootstrap: loginService.initConfig OK");
677
- }
678
- }
679
- const loginResult = await kernel.quickLogin(ctx, {});
680
- log(`bootstrap: quickLogin OK, uin=${loginResult.uin}, uid=${loginResult.uid}`);
681
- const sessionConfig = kernel.buildSessionConfig({
682
- appid: Appid,
683
- fullVersion: bootEnv.qqVersion || "",
684
- selfUin: loginResult.uin,
685
- selfUid: loginResult.uid,
686
- accountPath: bootEnv.dataDir || ".",
687
- downloadPath: (0, node_path.join)(bootEnv.dataDir || ".", "temp")
688
- });
689
- const listener = kernel.createLifecycleSessionListener();
690
- await kernel.initAndStartSession(ctx, sessionConfig, listener, { timeoutMs: 2e4 });
691
- log("bootstrap: session init + startNT OK!");
692
- } else log("bootstrap: kernel missing lifecycle fns (quickLogin/initAndStartSession)");
693
- if (env.NAPUTO_PROBE === "1" && typeof kernel.probeRuntime === "function") try {
694
- const probe = kernel.probeRuntime(ctx);
695
- log(`bootstrap: probe done, session=${probe.session ? "ok" : "null"}, services=${Object.keys(probe.services ?? {}).length}`);
696
- } catch (e) {
697
- log(`bootstrap: probe error: ${errMsg(e)}`);
698
- }
699
- }
1320
+ if (kernel.NapukettoCore !== void 0) await bootstrapWithCore(kernel, state, bootEnv, Appid);
1321
+ else await bootstrapFallback(kernel, state, bootEnv, Appid);
700
1322
  } catch (e) {
701
1323
  log(`bootstrap: lifecycle error: ${errMsg(e)}`);
702
1324
  }
@@ -705,7 +1327,7 @@ async function bootstrap(state) {
705
1327
  }
706
1328
  }
707
1329
  //#endregion
708
- //#region src/host/self-host.ts
1330
+ //#region src/host/core/self-host.ts
709
1331
  /**
710
1332
  * self-host.ts:自建宿主(路线 A,NAPUTO_SELF_HOST)入口——标准 Node 进程直接运行。
711
1333
  * 2026-08-07 阶段 2:由 runtime/self-host.cjs TS 化(零语义改动)。
@@ -730,14 +1352,38 @@ async function bootstrap(state) {
730
1352
  * → kernel 装配 → 登录(快速/QR)→ session(先 init 后 start)→ 冒烟 → 协议装配
731
1353
  */
732
1354
  const state = createState();
1355
+ const ipcMode = env.NAPUTO_IPC === "1";
1356
+ if (ipcMode) {
1357
+ enableIpc();
1358
+ sendStatus("booting");
1359
+ }
733
1360
  log(`[self-host] 启动 @ ${(/* @__PURE__ */ new Date()).toISOString()} pid=${process.pid} node=${process.version} type=${process.type ?? "(标准 node)"}`);
734
1361
  log(`[self-host] stub 目录: ${env.NAPUTO_STUB_DIR ?? "(PATH 前置,未记录)"}`);
1362
+ const lockDataDir = env.NAPUTO_CFG_DIR;
1363
+ if (lockDataDir) {
1364
+ if (!acquireInstanceLock(lockDataDir)) {
1365
+ const { pid: holderPid } = checkInstanceLock(lockDataDir);
1366
+ log(`[self-host] ❌ 数据目录已被其他实例占用(pid=${holderPid ?? "?"}),退出;同一账号数据目录仅允许一个实例,请先停止已有实例`);
1367
+ if (ipcMode) sendStatus("failed", "数据目录被占用", {
1368
+ code: "INVALID_STATE",
1369
+ message: `数据目录已被其他实例占用(pid=${holderPid ?? "?"})`
1370
+ });
1371
+ process.exit(1);
1372
+ }
1373
+ registerLockCleanup(lockDataDir);
1374
+ log(`[self-host] ✅ 已获取数据目录锁(${lockDataDir})`);
1375
+ } else log("[self-host] ⚠️ NAPUTO_CFG_DIR 未设置,跳过单实例锁");
735
1376
  const wrapperPath = env.NAPUTO_WRAPPER_PATH;
736
1377
  if (!wrapperPath) {
737
1378
  log("[self-host] NAPUTO_WRAPPER_PATH 未设置,退出");
1379
+ if (ipcMode) sendStatus("failed", "NAPUTO_WRAPPER_PATH 未设置", {
1380
+ code: "INVALID_STATE",
1381
+ message: "NAPUTO_WRAPPER_PATH 未设置,退出"
1382
+ });
738
1383
  process.exit(1);
739
1384
  }
740
1385
  log(`[self-host] dlopen wrapper.node: ${wrapperPath}`);
1386
+ if (ipcMode) sendStatus("dlopening");
741
1387
  try {
742
1388
  const m = { exports: {} };
743
1389
  process.dlopen(m, wrapperPath);
@@ -745,11 +1391,19 @@ try {
745
1391
  log(`[self-host] ✅ dlopen 成功,exports ${keys.length} 个`);
746
1392
  if (!keys.includes("NodeIKernelLoginService")) {
747
1393
  log("[self-host] ❌ exports 无 NodeIKernelLoginService(stub 环境异常?)");
1394
+ if (ipcMode) sendStatus("failed", "exports 无 NodeIKernelLoginService", {
1395
+ code: "INVALID_STATE",
1396
+ message: "exports 无 NodeIKernelLoginService(stub 环境异常?)"
1397
+ });
748
1398
  process.exit(1);
749
1399
  }
750
1400
  state.wrapperExports = m.exports;
751
1401
  } catch (e) {
752
1402
  log(`[self-host] ❌ dlopen 失败: ${errMsg(e)}`);
1403
+ if (ipcMode) sendStatus("failed", "dlopen 失败", {
1404
+ code: "UNKNOWN",
1405
+ message: errMsg(e)
1406
+ });
753
1407
  process.exit(1);
754
1408
  }
755
1409
  try {
@@ -766,8 +1420,13 @@ log("[self-host] 调 bootstrap(state) ...");
766
1420
  try {
767
1421
  await bootstrap(state);
768
1422
  log("[self-host] bootstrap 完成");
1423
+ if (ipcMode) sendStatus("ready");
769
1424
  } catch (e) {
770
1425
  log(`[self-host] bootstrap 失败: ${errMsg(e)}`);
1426
+ if (ipcMode) sendStatus("failed", "bootstrap 失败", {
1427
+ code: "UNKNOWN",
1428
+ message: errMsg(e)
1429
+ });
771
1430
  process.exit(1);
772
1431
  }
773
1432
  })();