@napuketto/loader 0.0.2 → 0.0.4

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.
@@ -24,6 +24,7 @@ let node_fs = require("node:fs");
24
24
  let node_path = require("node:path");
25
25
  let node_process = require("node:process");
26
26
  node_process = __toESM(node_process, 1);
27
+ let node_readline = require("node:readline");
27
28
  let node_os = require("node:os");
28
29
  //#region src/instance-lock.ts
29
30
  /**
@@ -160,6 +161,8 @@ const env = {
160
161
  NAPUTO_QUICK_UIN: process.env["NAPUTO_QUICK_UIN"],
161
162
  /** 收发消息冒烟自检开关(=1 触发)。 */
162
163
  NAPUTO_SMOKE: process.env["NAPUTO_SMOKE"],
164
+ /** IPC 子进程模式(=1:koishi 插件驱动——stdout 走 JSON 行协议,stdin 收 action/control)。 */
165
+ NAPUTO_IPC: process.env["NAPUTO_IPC"],
163
166
  /** 冒烟目标 peer(c2c:<uin> / group:<uin>,缺省发给自己)。 */
164
167
  NAPUTO_SMOKE_PEER: process.env["NAPUTO_SMOKE_PEER"],
165
168
  /** 运行时探测开关(=1 触发 probeRuntime)。 */
@@ -170,6 +173,293 @@ const env = {
170
173
  NAPKETTO_DATA: process.env["NAPKETTO_DATA"]
171
174
  };
172
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 () => {
227
+ return ctx.groupCache.listGroupsRefreshed();
228
+ });
229
+ actions.set("friend.getFriendList", async () => ctx.friendApi.getFriendList());
230
+ return actions;
231
+ }
232
+ /** 统一动作调用:查找动作表 → 执行 → 返回结果或错误(不抛,调用方转 result)。 */
233
+ async function callIpcAction(actions, action, params) {
234
+ const handler = actions.get(action);
235
+ if (handler === void 0) return {
236
+ ok: false,
237
+ error: {
238
+ code: "NOT_FOUND",
239
+ message: `未知动作: ${action}`
240
+ }
241
+ };
242
+ try {
243
+ return {
244
+ ok: true,
245
+ value: await handler(params ?? {})
246
+ };
247
+ } catch (err) {
248
+ const message = err instanceof Error ? err.message : String(err);
249
+ return {
250
+ ok: false,
251
+ error: {
252
+ code: errorCodeOf(err),
253
+ message
254
+ }
255
+ };
256
+ }
257
+ }
258
+ /** 宽松提取错误码(KernelError 有 string code 字段;其他错误 UNKNOWN)。 */
259
+ function errorCodeOf(err) {
260
+ if (typeof err === "object" && err !== null) {
261
+ const code = err.code;
262
+ if (typeof code === "string" && code !== "") return code;
263
+ }
264
+ return "UNKNOWN";
265
+ }
266
+ //#endregion
267
+ //#region src/host/ipc/ipc-types.ts
268
+ /** 消息类型集合(解码校验用)。 */
269
+ const IPC_MESSAGE_TYPES = /* @__PURE__ */ new Set([
270
+ "status",
271
+ "login",
272
+ "qr",
273
+ "event",
274
+ "result",
275
+ "log",
276
+ "ping",
277
+ "pong",
278
+ "action",
279
+ "control"
280
+ ]);
281
+ //#endregion
282
+ //#region src/host/ipc/ipc-sender.ts
283
+ /**
284
+ * ipc-sender.ts:IPC 消息发送封装(self-host 子进程 → koishi 插件)。
285
+ *
286
+ * 仅 NAPUTO_IPC=1 启用:stdout 专用于 JSON 行协议(stdin 收 action/control)。
287
+ * 非 IPC 模式(cli pnpm start)stdout 不输出协议行,零干扰。
288
+ */
289
+ /** IPC 模式开关(self-host.ts 在 env.NAPUTO_IPC=1 时 enable)。 */
290
+ let enabled = false;
291
+ /** 启用 IPC 发送(幂等)。 */
292
+ function enableIpc() {
293
+ enabled = true;
294
+ }
295
+ /** 发送一条 IPC 消息(JSON 行)。失败静默(不阻塞引导)。 */
296
+ function sendIpc(message) {
297
+ if (!enabled) return;
298
+ try {
299
+ process.stdout.write(`${JSON.stringify(message)}\n`);
300
+ } catch {}
301
+ }
302
+ /** 引导状态(booting/dlopening/logging/sessioning/ready/failed)。 */
303
+ function sendStatus(phase, message, error) {
304
+ sendIpc({
305
+ v: 1,
306
+ type: "status",
307
+ payload: {
308
+ phase,
309
+ ...message !== void 0 ? { message } : {},
310
+ ...error !== void 0 ? { error } : {}
311
+ }
312
+ });
313
+ }
314
+ /** 登录状态(QR 状态机 idle/waiting_scan/scanned/logged_in/failed)。 */
315
+ function sendLogin(state, selfInfo) {
316
+ sendIpc({
317
+ v: 1,
318
+ type: "login",
319
+ payload: {
320
+ state,
321
+ ...selfInfo !== void 0 ? { selfInfo } : {}
322
+ }
323
+ });
324
+ }
325
+ /** 二维码数据(扫码登录展示用)。 */
326
+ function sendQr(pngBase64, qrcodeUrl) {
327
+ sendIpc({
328
+ v: 1,
329
+ type: "qr",
330
+ payload: {
331
+ pngBase64,
332
+ qrcodeUrl
333
+ }
334
+ });
335
+ }
336
+ /** kernel 事件转发(service/name/args 原样透传,koishi 插件翻译层消费)。 */
337
+ function sendEvent(service, name, args) {
338
+ sendIpc({
339
+ v: 1,
340
+ type: "event",
341
+ payload: {
342
+ service,
343
+ name,
344
+ args
345
+ }
346
+ });
347
+ }
348
+ /** 动作响应。 */
349
+ function sendResult(id, payload) {
350
+ sendIpc({
351
+ v: 1,
352
+ type: "result",
353
+ id,
354
+ payload
355
+ });
356
+ }
357
+ /** 心跳应答(收到 ping 时回)。 */
358
+ function sendPong() {
359
+ sendIpc({
360
+ v: 1,
361
+ type: "pong"
362
+ });
363
+ }
364
+ /** 心跳(定期发送,koishi 插件探活)。 */
365
+ function sendPing() {
366
+ sendIpc({
367
+ v: 1,
368
+ type: "ping"
369
+ });
370
+ }
371
+ //#endregion
372
+ //#region src/host/ipc/ipc-codec.ts
373
+ /**
374
+ * ipc-codec.ts:IPC 消息编解码(JSON 行协议,与 koishi 插件侧 src/ipc/codec.ts 对齐)。
375
+ *
376
+ * 每行一条 JSON 消息,`\n` 结尾。解码宽松:非法行 / 空行 / v 不匹配 /
377
+ * 未知 type → null,调用方忽略,不崩通道。
378
+ */
379
+ /** 类型守卫:v 匹配 + type 合法。 */
380
+ function isIpcMessage(value) {
381
+ if (typeof value !== "object" || value === null) return false;
382
+ const record = value;
383
+ return record["v"] === 1 && typeof record["type"] === "string" && IPC_MESSAGE_TYPES.has(record["type"]);
384
+ }
385
+ /** 解码一行 JSON;非法行/空行返回 null。 */
386
+ function decodeIpcMessage(line) {
387
+ const trimmed = line.trim();
388
+ if (trimmed === "") return null;
389
+ try {
390
+ const parsed = JSON.parse(trimmed);
391
+ return isIpcMessage(parsed) ? parsed : null;
392
+ } catch {
393
+ return null;
394
+ }
395
+ }
396
+ /** 启动 IPC 服务端(stdin 接收 + 心跳)。返回停止函数。 */
397
+ function startIpcServer(options) {
398
+ const heartbeatMs = options.heartbeatMs ?? 15e3;
399
+ const onExit = options.onExit ?? (() => process.exit(0));
400
+ const rl = (0, node_readline.createInterface)({
401
+ input: process.stdin,
402
+ crlfDelay: Infinity
403
+ });
404
+ rl.on("line", (line) => {
405
+ const message = decodeIpcMessage(line);
406
+ if (message === null) return;
407
+ switch (message.type) {
408
+ case "action":
409
+ handleAction(message.id, message.payload.action, message.payload.params, options.actions);
410
+ break;
411
+ case "control":
412
+ handleControl(message.payload, onExit);
413
+ break;
414
+ case "ping": sendPong();
415
+ }
416
+ });
417
+ const timer = setInterval(() => sendPing(), heartbeatMs);
418
+ return () => {
419
+ clearInterval(timer);
420
+ rl.close();
421
+ };
422
+ }
423
+ /** 动作执行 → result 响应(不抛:callIpcAction 内部兜底转错误)。 */
424
+ async function handleAction(id, action, params, actions) {
425
+ sendResult(id, await callIpcAction(actions, action, params));
426
+ }
427
+ /** 控制指令处理(stop/restart 退出;login 预留——引导期登录由 bootstrap 内部驱动)。 */
428
+ function handleControl(payload, onExit) {
429
+ if (payload.command === "stop" || payload.command === "restart") onExit();
430
+ }
431
+ //#endregion
432
+ //#region src/host/ipc/ipc-bootstrap.ts
433
+ /** 订阅事件通道 → event 消息转发(事件名 "Service/method" 拆分)。 */
434
+ function forwardChannel(channel, stops) {
435
+ if (typeof channel.onAny !== "function") return;
436
+ const off = channel.onAny((event, ...args) => {
437
+ const slash = event.indexOf("/");
438
+ sendEvent(slash >= 0 ? event.slice(0, slash) : event, slash >= 0 ? event.slice(slash + 1) : "", args);
439
+ });
440
+ if (typeof off === "function") stops.push(off);
441
+ }
442
+ /** IPC 模式装配(enableIpc + 动作表 + 服务端 + 事件转发)。返回停止函数。 */
443
+ function startIpcMode(services) {
444
+ enableIpc();
445
+ const groupApi = services.groupApi;
446
+ const stopServer = startIpcServer({ actions: createIpcActions({
447
+ msgApi: services.msgApi,
448
+ groupApi: services.groupApi,
449
+ groupCache: services.groupCache,
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
173
463
  //#region src/host/util.ts
174
464
  /**
175
465
  * util.ts:引导公共工具(日志 + 共享状态)。
@@ -208,7 +498,7 @@ function createState() {
208
498
  };
209
499
  }
210
500
  //#endregion
211
- //#region src/host/login.ts
501
+ //#region src/host/core/login.ts
212
502
  /** 打印可用快速登录账号 + 返回目标账号(启动横幅)。forcedUin(cli `-q` / NAPUTO_QUICK_UIN)优先。 */
213
503
  async function pickLoginAccount(kernel, ctx, loginResultRef, forcedUin) {
214
504
  try {
@@ -253,25 +543,37 @@ async function doLogin(core, opts) {
253
543
  //#endregion
254
544
  //#region src/host/load-config.ts
255
545
  /**
256
- * 协议配置段读取(从 protocols.ts 拆分,2026-08-08 FTA 优化)
546
+ * 协议配置段读取(从 protocols.ts 拆分,2026-08-08 FTA 优化;2026-08-08 结构拍板:按账号取段)
257
547
  *
258
- * 全局 TOML 配置(<项目根>/napuketto.toml,2026-08-07 用户拍板:配置文件放项目根):
259
- * 读 [onebot11] / [satori] 段,装配时 zod 校验后作 seed。路径优先级:NAPKETTO_CONFIG
260
- * (launcher 注入)> 装配链自身探测(kernel.resolveConfigPath)> NAPUTO_CFG_DIR 兜底
261
- * (旧行为兼容)。(ConfigBase seed 模式:load() 直接用内存值,不再读写独立协议文件)
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() 直接用内存值,不再读写独立协议文件)
262
553
  */
263
- /** 读取全局 TOML 的协议段(读取失败用默认空段,不阻塞装配)。 */
264
- function loadProtocolSections(kernel) {
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
+ }
559
+ /**
560
+ * 按登录账号 uin 读取全局 TOML 的协议段(读取失败 / 未找到账号 / 无协议段 → 空段,
561
+ * 不阻塞装配)。uin 为登录成功后实际账号(-q 指定账号与配置不一致时以 uin 为准)。
562
+ */
563
+ function loadProtocolSections(kernel, uin) {
265
564
  const cfgFile = env.NAPKETTO_CONFIG || (0, node_path.join)(env.NAPUTO_CFG_DIR || ".", "napuketto.toml");
266
565
  let ob11Section = {};
267
566
  let satoriSection = {};
268
567
  try {
269
568
  const raw = (0, node_fs.readFileSync)(cfgFile, "utf8");
270
- const parsed = kernel.parseToml(raw);
271
- const ob11 = parsed["onebot11"];
272
- const satori = parsed["satori"];
273
- if (ob11 !== void 0 && ob11 !== null && typeof ob11 === "object") ob11Section = ob11;
274
- if (satori !== void 0 && satori !== null && typeof satori === "object") satoriSection = satori;
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
+ }
275
577
  } catch (e) {
276
578
  log(`bootstrap: 全局配置读取失败(用默认 ob11/satori 配置): ${errMsg(e)}`);
277
579
  }
@@ -282,74 +584,13 @@ function loadProtocolSections(kernel) {
282
584
  };
283
585
  }
284
586
  //#endregion
285
- //#region src/host/msg-log.ts
286
- /** 终端 ANSI 颜色(消息日志颜色分层,2026-08-07 用户定稿)。 */
287
- const ANSI = {
288
- gray: "\x1B[90m",
289
- cyan: "\x1B[36m",
290
- green: "\x1B[32m",
291
- reset: "\x1B[0m"
292
- };
293
- /** canonical 元素 → 日志占位文本。 */
294
- function elementToText(el) {
295
- if (el.type === "text") return el.text ?? "";
296
- if (el.type === "at") return `@${el.display ?? (el.target === "all" ? "全体" : el.target)}`;
297
- if (el.type === "file") return `[文件${el.name ?? ""}]`;
298
- if (el.type === "forward") return "[合并转发]";
299
- return `[${el.type}]`;
300
- }
301
- /** 渲染消息为单行文本(text 拼接,at/媒体元素转占位)。 */
302
- function renderMessage(kernel, msg) {
303
- let rendered = "";
304
- for (const el of kernel.toCanonicalElements(msg)) rendered += elementToText(el);
305
- return rendered === "" ? "[空消息/媒体]" : rendered;
306
- }
307
- /**
308
- * 消息日志订阅(onRecvMsg 回调参数为消息数组——2026-08-07 运行时实证,遍历逐条打印)。
309
- */
310
- function setupMsgLogging(kernel, channel, logger) {
311
- channel.on("Msg/onRecvMsg", (msgs) => {
312
- forEachRawMessage(msgs, (m) => {
313
- try {
314
- const raw = m;
315
- const rendered = renderMessage(kernel, m);
316
- const isGroup = raw.chatType === kernel.ChatType.GROUP;
317
- const kind = isGroup ? "群聊" : "私聊";
318
- const peerUin = String(raw.peerUin ?? "");
319
- const senderUin = String(raw.senderUin ?? (peerUin || "未知"));
320
- const plainTags = isGroup ? `[群${peerUin}] [用户${senderUin}]` : `[用户${senderUin}]`;
321
- const coloredTags = isGroup ? `${ANSI.cyan}[群${peerUin}]${ANSI.reset} ${ANSI.green}[用户${senderUin}]${ANSI.reset}` : `${ANSI.green}[用户${senderUin}]${ANSI.reset}`;
322
- const content = rendered.replace(/\n/g, "\n ");
323
- const plain = `接收 <- ${kind} ${plainTags}: ${content}`;
324
- const colored = `${ANSI.gray}loader | 接收 <- ${kind}${ANSI.reset} ${coloredTags}: ${content}`;
325
- log(plain);
326
- logger?.info(colored);
327
- } catch (e) {
328
- const line2 = `接收消息(解析失败: ${errMsg(e)})`;
329
- log(line2);
330
- logger?.warn(line2);
331
- }
332
- });
333
- });
334
- }
335
- //#endregion
336
- //#region src/host/protocols.ts
337
- /**
338
- * protocols.ts:协议装配(登录成功后)。
339
- * 2026-08-07 阶段 2:由 runtime/boot-protocols.js TS 化(零语义改动)。
340
- * 动态 import adapter/network 入口,装配 OB11 / Satori 适配器。
341
- * 依赖 launcher 注入的 NAPUTO_ADAPTER_ENTRY / NAPUTO_NETWORK_ENTRY。
342
- *
343
- * 2026-08-08 FTA 优化:消息日志渲染 → msg-log.ts;TOML 配置段读取 → load-config.ts。
344
- */
587
+ //#region src/host/core/assemble-protocols.ts
345
588
  /**
346
- * 登录成功后装配协议:kernel 各 Api + network 广播 + OB11 / Satori 适配器。
589
+ * assemble-protocols.ts:OB11 / Satori 协议装配(非 IPC 模式,cli pnpm start)。
590
+ * 2026-08-08:从 protocols.ts 拆分——protocols.ts 只留入口 + IPC 分支。
347
591
  */
348
- async function startProtocols(kernel, ctx, loginResult) {
349
- const logger = kernel.createLogger?.({
350
- console: true,
351
- base: { name: "loader" }
352
- });
592
+ /** 装配 OB11 + Satori 适配器(登录成功后,非 IPC 模式)。 */
593
+ async function assembleOb11AndSatori(kernel, services, loginResult) {
353
594
  const adapterEntry = env.NAPUTO_ADAPTER_ENTRY;
354
595
  const networkEntry = env.NAPUTO_NETWORK_ENTRY;
355
596
  if (!adapterEntry || !networkEntry) {
@@ -364,32 +605,9 @@ async function startProtocols(kernel, ctx, loginResult) {
364
605
  const adapter = await import(`file://${onebot11Entry.replace(/\\/g, "/")}`);
365
606
  const satoriAdapter = await import(`file://${satoriEntry.replace(/\\/g, "/")}`);
366
607
  const adapterCore = await import(`file://${coreEntry.replace(/\\/g, "/")}`);
367
- const session = ctx.session;
368
- if (!session) {
369
- log("bootstrap: session 为空,无法装配协议");
370
- return;
371
- }
372
- const channel = new kernel.NTEventChannel("Msg");
373
- new kernel.MsgBridge(session, channel).register();
374
- setupMsgLogging(kernel, channel, logger);
375
- const groupApi = new kernel.GroupApi(session);
376
- const msgApi = new kernel.MsgApi(session);
377
- const friendApi = new kernel.FriendApi(session, { uidToUin: (uids) => groupApi.uidToUin(uids) });
378
- const groupChannel = new kernel.NTEventChannel("Group");
379
- new kernel.GroupBridge(session, groupChannel).register();
380
- const groupCache = new kernel.GroupCache({
381
- channel: groupChannel,
382
- groupApi
383
- });
384
- groupCache.register();
385
- const groupNotifyApi = new kernel.GroupNotifyApi(session);
386
- const ticketApi = new kernel.TicketApi(session);
387
- const richMediaApi = new kernel.RichMediaApi(session);
388
- const profileApi = new kernel.ProfileApi(session);
389
- const profileLikeApi = new kernel.ProfileLikeApi(session);
390
- const webApi = new kernel.WebApi({ getCookies: (domain) => ticketApi.getCookies(domain, loginResult.uin) });
608
+ const { channel, groupCache } = services;
391
609
  const broadcaster = new network.EventBroadcaster();
392
- const { cfgFile, ob11Section, satoriSection } = loadProtocolSections(kernel);
610
+ const { cfgFile, ob11Section, satoriSection } = loadProtocolSections(kernel, loginResult.uin);
393
611
  const ob11Config = new adapterCore.ProtocolConfig({
394
612
  path: cfgFile,
395
613
  schema: adapter.ob11ConfigSchema,
@@ -400,19 +618,16 @@ async function startProtocols(kernel, ctx, loginResult) {
400
618
  config: ob11Config,
401
619
  broadcaster,
402
620
  msgChannel: channel,
403
- msgApi,
404
- groupApi,
405
- groupNotifyApi,
406
- friendApi,
407
- ticketApi,
408
- richMediaApi,
409
- profileApi,
410
- profileLikeApi,
411
- webApi,
412
- self: {
413
- uin: loginResult.uin,
414
- nickname: loginResult.nick
415
- },
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,
416
631
  system: {
417
632
  appVersion: env.NAPUTO_QQ_VERSION || "unknown",
418
633
  cleanCache: async () => {
@@ -445,15 +660,12 @@ async function startProtocols(kernel, ctx, loginResult) {
445
660
  config: satoriConfig,
446
661
  broadcaster,
447
662
  msgChannel: channel,
448
- msgApi,
449
- groupApi,
450
- groupNotifyApi,
451
- friendApi,
452
- profileApi,
453
- self: {
454
- uin: loginResult.uin,
455
- nickname: loginResult.nick
456
- },
663
+ msgApi: services.msgApi,
664
+ groupApi: services.groupApi,
665
+ groupNotifyApi: services.groupNotifyApi,
666
+ friendApi: services.friendApi,
667
+ profileApi: services.profileApi,
668
+ self: services.self,
457
669
  cacheDir: satoriCacheDir,
458
670
  groupCache
459
671
  }).start();
@@ -463,7 +675,142 @@ async function startProtocols(kernel, ctx, loginResult) {
463
675
  }
464
676
  }
465
677
  //#endregion
466
- //#region src/host/session.ts
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);
807
+ } catch (e) {
808
+ log(`bootstrap: 协议装配失败: ${errMsg(e)}`);
809
+ }
810
+ return services;
811
+ }
812
+ //#endregion
813
+ //#region src/host/core/session.ts
467
814
  /** 对象有效性判断:getMsgService() 调用不抛断言(cpp_impl 已激活)。
468
815
  * 与 isSessionUsable 的区别:这里允许返回 null(未 init 但对象有效)。 */
469
816
  function isSessionObjectValid(s) {
@@ -588,7 +935,7 @@ function pickBestSession(candidates) {
588
935
  return bestValid;
589
936
  }
590
937
  //#endregion
591
- //#region src/host/smoke.ts
938
+ //#region src/host/core/smoke.ts
592
939
  /**
593
940
  * smoke.ts:P2-1 收发消息冒烟自检(NAPUTO_SMOKE=1 触发,2026-08-06)。
594
941
  * 2026-08-07 阶段 2:由 runtime/boot-smoke.js TS 化(零语义改动)。
@@ -714,7 +1061,7 @@ async function runSmokeTest(kernel, ctx, loginResult) {
714
1061
  return ok;
715
1062
  }
716
1063
  //#endregion
717
- //#region src/host/bootstrap-core.ts
1064
+ //#region src/host/core/bootstrap-core.ts
718
1065
  /**
719
1066
  * bootstrap-core.ts:NapukettoCore 装配路径(从 bootstrap.ts 拆分,2026-08-08 FTA 优化)。
720
1067
  *
@@ -735,14 +1082,31 @@ function probeCreatedSession(state, ctx) {
735
1082
  log(`BOOT: create() 探测失败: ${errMsg(e)}`);
736
1083
  }
737
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
+ }
738
1097
  /** 登录参数(NAPUTO_QUICK_UIN 强制指定 / ref 目标 / 默认)。 */
739
1098
  function buildLoginOpts(Appid, forcedUin, ref) {
740
1099
  const quickUin = forcedUin ?? ref.targetUin;
741
- return {
1100
+ const opts = {
742
1101
  appid: Appid,
743
1102
  initTimeoutMs: 2e4,
744
1103
  ...quickUin !== void 0 ? { quickUin } : {}
745
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;
746
1110
  }
747
1111
  /** V2 登录后替换 session:优先 QQ 主 session,其次 vehicle 单例表;自建宿主例外。 */
748
1112
  function replaceSession(kernel, core, state, ctx) {
@@ -841,20 +1205,32 @@ async function bootstrapWithCore(kernel, state, bootEnv, Appid) {
841
1205
  const forcedUin = env.NAPUTO_QUICK_UIN;
842
1206
  const ref = { targetUin: void 0 };
843
1207
  await pickLoginAccount(kernel, ctx, ref, forcedUin);
1208
+ if (env.NAPUTO_IPC === "1") sendStatus("logging");
844
1209
  const loginResult = await doLogin(core, buildLoginOpts(Appid, forcedUin, ref));
845
1210
  if (loginResult === null) {
846
1211
  log("bootstrap: 登录失败,引导中止");
1212
+ if (env.NAPUTO_IPC === "1") sendStatus("failed", "登录失败", {
1213
+ code: "NOT_LOGIN",
1214
+ message: "登录失败"
1215
+ });
847
1216
  return;
848
1217
  }
849
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
+ });
850
1224
  await activateSession(kernel, state, ctx, replaceSession(kernel, core, state, ctx), loginResult, Appid, bootEnv);
851
1225
  await waitSessionReady(kernel, ctx);
1226
+ if (env.NAPUTO_IPC === "1") sendStatus("sessioning");
852
1227
  await runSmokeIfEnabled(kernel, ctx, loginResult);
853
- await startProtocols(kernel, ctx, loginResult);
1228
+ const services = await startProtocols(kernel, ctx, loginResult);
1229
+ if (env.NAPUTO_IPC === "1" && services !== null) startIpcMode(services);
854
1230
  runProbePhase(kernel, ctx);
855
1231
  }
856
1232
  //#endregion
857
- //#region src/host/bootstrap.ts
1233
+ //#region src/host/core/bootstrap.ts
858
1234
  /**
859
1235
  * bootstrap.ts:kernel 引导核心(import kernel → 装配 → 登录 → session 替换 → 协议装配)。
860
1236
  * 2026-08-07 阶段 2:由 runtime/boot-bootstrap.js TS 化(零语义改动)。
@@ -951,7 +1327,7 @@ async function bootstrap(state) {
951
1327
  }
952
1328
  }
953
1329
  //#endregion
954
- //#region src/host/self-host.ts
1330
+ //#region src/host/core/self-host.ts
955
1331
  /**
956
1332
  * self-host.ts:自建宿主(路线 A,NAPUTO_SELF_HOST)入口——标准 Node 进程直接运行。
957
1333
  * 2026-08-07 阶段 2:由 runtime/self-host.cjs TS 化(零语义改动)。
@@ -976,6 +1352,11 @@ async function bootstrap(state) {
976
1352
  * → kernel 装配 → 登录(快速/QR)→ session(先 init 后 start)→ 冒烟 → 协议装配
977
1353
  */
978
1354
  const state = createState();
1355
+ const ipcMode = env.NAPUTO_IPC === "1";
1356
+ if (ipcMode) {
1357
+ enableIpc();
1358
+ sendStatus("booting");
1359
+ }
979
1360
  log(`[self-host] 启动 @ ${(/* @__PURE__ */ new Date()).toISOString()} pid=${process.pid} node=${process.version} type=${process.type ?? "(标准 node)"}`);
980
1361
  log(`[self-host] stub 目录: ${env.NAPUTO_STUB_DIR ?? "(PATH 前置,未记录)"}`);
981
1362
  const lockDataDir = env.NAPUTO_CFG_DIR;
@@ -983,6 +1364,10 @@ if (lockDataDir) {
983
1364
  if (!acquireInstanceLock(lockDataDir)) {
984
1365
  const { pid: holderPid } = checkInstanceLock(lockDataDir);
985
1366
  log(`[self-host] ❌ 数据目录已被其他实例占用(pid=${holderPid ?? "?"}),退出;同一账号数据目录仅允许一个实例,请先停止已有实例`);
1367
+ if (ipcMode) sendStatus("failed", "数据目录被占用", {
1368
+ code: "INVALID_STATE",
1369
+ message: `数据目录已被其他实例占用(pid=${holderPid ?? "?"})`
1370
+ });
986
1371
  process.exit(1);
987
1372
  }
988
1373
  registerLockCleanup(lockDataDir);
@@ -991,9 +1376,14 @@ if (lockDataDir) {
991
1376
  const wrapperPath = env.NAPUTO_WRAPPER_PATH;
992
1377
  if (!wrapperPath) {
993
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
+ });
994
1383
  process.exit(1);
995
1384
  }
996
1385
  log(`[self-host] dlopen wrapper.node: ${wrapperPath}`);
1386
+ if (ipcMode) sendStatus("dlopening");
997
1387
  try {
998
1388
  const m = { exports: {} };
999
1389
  process.dlopen(m, wrapperPath);
@@ -1001,11 +1391,19 @@ try {
1001
1391
  log(`[self-host] ✅ dlopen 成功,exports ${keys.length} 个`);
1002
1392
  if (!keys.includes("NodeIKernelLoginService")) {
1003
1393
  log("[self-host] ❌ exports 无 NodeIKernelLoginService(stub 环境异常?)");
1394
+ if (ipcMode) sendStatus("failed", "exports 无 NodeIKernelLoginService", {
1395
+ code: "INVALID_STATE",
1396
+ message: "exports 无 NodeIKernelLoginService(stub 环境异常?)"
1397
+ });
1004
1398
  process.exit(1);
1005
1399
  }
1006
1400
  state.wrapperExports = m.exports;
1007
1401
  } catch (e) {
1008
1402
  log(`[self-host] ❌ dlopen 失败: ${errMsg(e)}`);
1403
+ if (ipcMode) sendStatus("failed", "dlopen 失败", {
1404
+ code: "UNKNOWN",
1405
+ message: errMsg(e)
1406
+ });
1009
1407
  process.exit(1);
1010
1408
  }
1011
1409
  try {
@@ -1022,8 +1420,13 @@ log("[self-host] 调 bootstrap(state) ...");
1022
1420
  try {
1023
1421
  await bootstrap(state);
1024
1422
  log("[self-host] bootstrap 完成");
1423
+ if (ipcMode) sendStatus("ready");
1025
1424
  } catch (e) {
1026
1425
  log(`[self-host] bootstrap 失败: ${errMsg(e)}`);
1426
+ if (ipcMode) sendStatus("failed", "bootstrap 失败", {
1427
+ code: "UNKNOWN",
1428
+ message: errMsg(e)
1429
+ });
1027
1430
  process.exit(1);
1028
1431
  }
1029
1432
  })();
package/dist/index.d.mts CHANGED
@@ -97,6 +97,8 @@ interface LaunchOptions {
97
97
  stubDir?: string;
98
98
  /** 强制指定快速登录账号(注入 NAPUTO_QUICK_UIN;cli `-q <uin>` 透传,2026-08-07)。 */
99
99
  quickUin?: string;
100
+ /** IPC 子进程模式(=1:stdout 走 JSON 行协议 + stdin 收 action/control,koishi 插件驱动)。 */
101
+ ipc?: boolean;
100
102
  /** 自建宿主入口(默认 dist/host/self-host.cjs)。 */
101
103
  selfHostEntry?: string;
102
104
  /**
@@ -154,6 +156,8 @@ declare const ENV: {
154
156
  readonly CONFIG_PATH: "NAPKETTO_CONFIG";
155
157
  /** 强制指定快速登录账号(cli `-q <uin>` 透传,bootstrap 登录用)。 */
156
158
  readonly QUICK_UIN: "NAPUTO_QUICK_UIN";
159
+ /** IPC 子进程模式(koishi 插件驱动:stdout JSON 行 + stdin action/control)。 */
160
+ readonly IPC: "NAPUTO_IPC";
157
161
  };
158
162
  //#endregion
159
163
  export { ENV, INSTANCE_LOCK_FILE, type InstanceLockCheck, type InstanceLockInfo, type LaunchOptions, type LaunchResult, type QqInstallInfo, acquireInstanceLock, checkInstanceLock, defaultStubDir, launchSelfHost, locateQqPath, registerLockCleanup, releaseInstanceLock, resolveQqInstall };
@@ -1 +1 @@
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;;;;;;;;EAQA;;;;;;EAMA,QAAQ;;UAGK;;EAEb,OAAO,kBAAkB;;EAEzB;;;;;;;;;;;iBAYY,eAAe,SAAS,gBAAgB;;;;;iBAiDxC;;cAqCH;WACT;WACA;WACA;WACA;WACA;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA"}
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
@@ -174,6 +174,7 @@ function buildLaunchEnv(options) {
174
174
  if (options.networkEntry !== void 0) env[ENV.NETWORK_ENTRY] = resolve(options.networkEntry);
175
175
  if (options.configPath !== void 0) env[ENV.CONFIG_PATH] = resolve(options.configPath);
176
176
  if (options.quickUin !== void 0) env[ENV.QUICK_UIN] = options.quickUin;
177
+ if (options.ipc === true) env[ENV.IPC] = "1";
177
178
  return env;
178
179
  }
179
180
  /** 环境变量名(self-host.cjs 与 kernel 引导读取)。 */
@@ -194,7 +195,9 @@ const ENV = {
194
195
  /** 全局配置文件路径(项目根 napuketto.toml,boot-protocols 读取)。 */
195
196
  CONFIG_PATH: "NAPKETTO_CONFIG",
196
197
  /** 强制指定快速登录账号(cli `-q <uin>` 透传,bootstrap 登录用)。 */
197
- QUICK_UIN: "NAPUTO_QUICK_UIN"
198
+ QUICK_UIN: "NAPUTO_QUICK_UIN",
199
+ /** IPC 子进程模式(koishi 插件驱动:stdout JSON 行 + stdin action/control)。 */
200
+ IPC: "NAPUTO_IPC"
198
201
  };
199
202
  //#endregion
200
203
  //#region src/locate-qq.ts
@@ -1 +1 @@
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 /** 自建宿主入口(默认 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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@napuketto/loader",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "NapukettoQQ 引导组件:自建宿主(标准 Node + stub QQNT.dll)dlopen wrapper.node 并启动 kernel,不拉起 QQ / 不注入",
5
5
  "type": "module",
6
6
  "files": [