@napuketto/loader 0.0.29 → 0.0.30

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.
@@ -856,6 +856,7 @@ function attachIpcServices(actions, services) {
856
856
  forwardChannel(services.friendChannel, stops);
857
857
  return () => {
858
858
  for (const stop of stops) stop();
859
+ for (const name of full.keys()) actions.delete(name);
859
860
  };
860
861
  }
861
862
  //#endregion
@@ -950,6 +951,7 @@ async function attachOb11IpcBridge(actions, services, deps = {}) {
950
951
  msgChannel: services.channel,
951
952
  groupChannel: services.groupChannel,
952
953
  friendChannel: services.friendChannel,
954
+ buddyCacheEvents: services.buddyCache?.events,
953
955
  logger: services.logger,
954
956
  msgApi: services.msgApi,
955
957
  groupApi: services.groupApi,
@@ -999,6 +1001,7 @@ async function attachOb11IpcBridge(actions, services, deps = {}) {
999
1001
  return () => {
1000
1002
  broadcaster.unregister(bridgeAdapter);
1001
1003
  ob11.unsubscribeOnly();
1004
+ for (const name of ob11.registry.names) actions.delete(name);
1002
1005
  };
1003
1006
  } catch (err) {
1004
1007
  log(`ipc-ob11: OB11 动作桥装配失败(降级纯 kernel 动作面): ${errMsg(err)}`);
@@ -1166,13 +1169,23 @@ function loadProtocolSections(kernel, uin) {
1166
1169
  const BACKSLASH_RE = /\\/g;
1167
1170
  /** adapter 入口 index.mjs 后缀(子路径导出替换用)。 */
1168
1171
  const INDEX_MJS_RE = /index\.mjs$/;
1169
- /** 装配 OB11 + Satori 适配器(登录成功后,非 IPC 模式)。 */
1172
+ /**
1173
+ * 装配 OB11 + Satori 适配器(登录成功后,非 IPC 模式)。
1174
+ *
1175
+ * 返回停止函数(ob11/satori 适配器 stop:传输关闭 + 订阅清理;装配跳过时
1176
+ * no-op)——startProtocols 存入 services.stopAdapters,供未来非 IPC 软重登
1177
+ * 清理(当前软重登仅 IPC 模式有触发通道,见 relogin.ts)。
1178
+ *
1179
+ * ⚠️ 装配失败**抛出**(2026-09-08 修复):cli 模式跑宿主就是为了 OB11/Satori
1180
+ * 服务,此处吞错会让 startProtocols 误判成功、以假 ready 留驻(内部 catch
1181
+ * 曾把 protocols.ts「装配失败返回 null 判引导失败」的意图整个短路)。
1182
+ */
1170
1183
  async function assembleOb11AndSatori(kernel, services, loginResult) {
1171
1184
  const adapterEntry = env.NAPUTO_ADAPTER_ENTRY;
1172
1185
  const networkEntry = env.NAPUTO_NETWORK_ENTRY;
1173
1186
  if (!adapterEntry || !networkEntry) {
1174
1187
  log("bootstrap: NAPUTO_ADAPTER_ENTRY/NETWORK_ENTRY 未设置,跳过协议装配");
1175
- return;
1188
+ return async () => {};
1176
1189
  }
1177
1190
  try {
1178
1191
  const network = await import(`file://${networkEntry.replace(BACKSLASH_RE, "/")}`);
@@ -1194,12 +1207,13 @@ async function assembleOb11AndSatori(kernel, services, loginResult) {
1194
1207
  const wrapperExports = services.ctx.wrapper?.exports;
1195
1208
  const qqRoot = kernel.resolveQqUserDataRoot?.(wrapperExports) ?? null;
1196
1209
  const mediaBaseDir = qqRoot !== null ? kernel.resolveQqGlobalPath?.(qqRoot) : void 0;
1197
- await new adapter.NapukettoOneBot11Adapter({
1210
+ const ob11 = new adapter.NapukettoOneBot11Adapter({
1198
1211
  config: ob11Config,
1199
1212
  broadcaster,
1200
1213
  msgChannel: channel,
1201
1214
  groupChannel: services.groupChannel,
1202
1215
  friendChannel: services.friendChannel,
1216
+ buddyCacheEvents: services.buddyCache?.events,
1203
1217
  logger: services.logger,
1204
1218
  msgApi: services.msgApi,
1205
1219
  groupApi: services.groupApi,
@@ -1231,7 +1245,8 @@ async function assembleOb11AndSatori(kernel, services, loginResult) {
1231
1245
  }
1232
1246
  },
1233
1247
  groupCache
1234
- }).start();
1248
+ });
1249
+ await ob11.start();
1235
1250
  log("bootstrap: onebot11 adapter started");
1236
1251
  const satoriCacheDir = typeof satoriSection["cacheDir"] === "string" && satoriSection["cacheDir"] !== "" ? satoriSection["cacheDir"] : (0, node_path.join)(env.NAPUTO_CFG_DIR || ".", "cache");
1237
1252
  const satoriConfig = new adapterCore.ProtocolConfig({
@@ -1240,7 +1255,7 @@ async function assembleOb11AndSatori(kernel, services, loginResult) {
1240
1255
  defaults: satoriAdapter.satoriConfigSchema.parse({}),
1241
1256
  seed: satoriAdapter.satoriConfigSchema.parse(satoriSection)
1242
1257
  });
1243
- await new satoriAdapter.NapukettoSatoriAdapter({
1258
+ const satori = new satoriAdapter.NapukettoSatoriAdapter({
1244
1259
  config: satoriConfig,
1245
1260
  broadcaster,
1246
1261
  msgChannel: channel,
@@ -1252,10 +1267,16 @@ async function assembleOb11AndSatori(kernel, services, loginResult) {
1252
1267
  self: services.self,
1253
1268
  cacheDir: satoriCacheDir,
1254
1269
  groupCache
1255
- }).start();
1270
+ });
1271
+ await satori.start();
1256
1272
  log("bootstrap: satori adapter started");
1273
+ return async () => {
1274
+ await ob11.stop();
1275
+ await satori.stop();
1276
+ };
1257
1277
  } catch (e) {
1258
1278
  log(`bootstrap: 协议装配失败: ${errMsg(e)}`);
1279
+ throw e;
1259
1280
  }
1260
1281
  }
1261
1282
  //#endregion
@@ -1299,9 +1320,11 @@ function formatMessageLog(kernel, msg) {
1299
1320
  }
1300
1321
  /**
1301
1322
  * 消息日志订阅(onRecvMsg 回调参数为消息数组——2026-08-07 运行时实证,遍历逐条打印)。
1323
+ * 返回退订函数(软重登重装配时清理旧订阅,2026-09-08);通道返回面宽松
1324
+ * (EventChannelLike.on 返回 unknown),非函数时退化为 no-op。
1302
1325
  */
1303
1326
  function setupMsgLogging(kernel, channel, logger, colorize) {
1304
- channel.on("Msg/onRecvMsg", (msgs) => {
1327
+ const off = channel.on("Msg/onRecvMsg", (msgs) => {
1305
1328
  forEachRawMessage(msgs, (m) => {
1306
1329
  try {
1307
1330
  const { plain, colored } = formatMessageLog(kernel, m);
@@ -1314,6 +1337,7 @@ function setupMsgLogging(kernel, channel, logger, colorize) {
1314
1337
  }
1315
1338
  });
1316
1339
  });
1340
+ return typeof off === "function" ? off : () => {};
1317
1341
  }
1318
1342
  //#endregion
1319
1343
  //#region src/host/core/kernel-services.ts
@@ -1338,21 +1362,29 @@ async function createKernelServices(kernel, ctx, loginResult) {
1338
1362
  return null;
1339
1363
  }
1340
1364
  const channel = new kernel.NTEventChannel("Msg");
1341
- new kernel.MsgBridge(session, channel).register();
1342
- setupMsgLogging(kernel, channel, logger, !ipcMode);
1365
+ const bridge = new kernel.MsgBridge(session, channel);
1366
+ bridge.register();
1367
+ const offMsgLogging = setupMsgLogging(kernel, channel, logger, !ipcMode);
1343
1368
  const groupApi = new kernel.GroupApi(session);
1344
1369
  const util = ctx.exports?.NodeQQNTWrapperUtil;
1345
1370
  const msgApi = new kernel.MsgApi(session, channel, util);
1346
1371
  const friendApi = new kernel.FriendApi(session, { uidToUin: (uids) => groupApi.uidToUin(uids) });
1347
1372
  const groupChannel = new kernel.NTEventChannel("Group");
1348
- new kernel.GroupBridge(session, groupChannel).register();
1373
+ const groupBridge = new kernel.GroupBridge(session, groupChannel);
1374
+ groupBridge.register();
1349
1375
  const friendChannel = new kernel.NTEventChannel("Buddy");
1350
- new kernel.FriendBridge(session, friendChannel).register();
1376
+ const friendBridge = new kernel.FriendBridge(session, friendChannel);
1377
+ friendBridge.register();
1351
1378
  const groupCache = new kernel.GroupCache({
1352
1379
  channel: groupChannel,
1353
1380
  groupApi
1354
1381
  });
1355
1382
  groupCache.register();
1383
+ const buddyCache = new kernel.BuddyCache({
1384
+ channel: friendChannel,
1385
+ logger
1386
+ });
1387
+ buddyCache.register();
1356
1388
  const groupNotifyApi = new kernel.GroupNotifyApi(session);
1357
1389
  const ticketApi = new kernel.TicketApi(session);
1358
1390
  return {
@@ -1366,6 +1398,7 @@ async function createKernelServices(kernel, ctx, loginResult) {
1366
1398
  groupApi,
1367
1399
  friendApi,
1368
1400
  groupCache,
1401
+ buddyCache,
1369
1402
  groupNotifyApi,
1370
1403
  ticketApi,
1371
1404
  richMediaApi: new kernel.RichMediaApi(session),
@@ -1378,7 +1411,15 @@ async function createKernelServices(kernel, ctx, loginResult) {
1378
1411
  },
1379
1412
  session,
1380
1413
  engine: ctx.engine,
1381
- util: ctx.exports?.NodeQQNTWrapperUtil
1414
+ util: ctx.exports?.NodeQQNTWrapperUtil,
1415
+ dispose: () => {
1416
+ bridge.unregister();
1417
+ groupBridge.unregister();
1418
+ friendBridge.unregister();
1419
+ groupCache.unregister();
1420
+ buddyCache.unregister();
1421
+ offMsgLogging();
1422
+ }
1382
1423
  };
1383
1424
  }
1384
1425
  //#endregion
@@ -1408,7 +1449,7 @@ async function startProtocols(kernel, ctx, loginResult) {
1408
1449
  return services;
1409
1450
  }
1410
1451
  try {
1411
- await assembleOb11AndSatori(kernel, services, loginResult);
1452
+ services.stopAdapters = await assembleOb11AndSatori(kernel, services, loginResult);
1412
1453
  } catch (e) {
1413
1454
  log(`bootstrap: 协议装配失败: ${errMsg(e)}`);
1414
1455
  return null;
@@ -1416,6 +1457,156 @@ async function startProtocols(kernel, ctx, loginResult) {
1416
1457
  return services;
1417
1458
  }
1418
1459
  //#endregion
1460
+ //#region src/host/core/relogin.ts
1461
+ /**
1462
+ * relogin.ts:登录控制相位机 + control login 指令处理(2026-09-08 A1/A2)。
1463
+ *
1464
+ * control login(koishi 面板「重新登录/扫码登录」→ IPC control login 指令)
1465
+ * 的成功结果按引导相位分派三条路径:
1466
+ * ① login-race(初始登录竞速中):结果经 preemptRef 接管引导链——快速登录
1467
+ * 风控挂起时强制扫码也能走完装配链(2026-09-08 T2 既有语义)。
1468
+ * ② ready(引导已完成):**软重登**——重装配(清理旧服务 + 用新结果重跑
1469
+ * 装配链),不再整进程重启;完成后重播 ready + logged_in。
1470
+ * ③ assembling/aborted:迟到结果**不上报**(A1 修复)——引导失败进程将退出,
1471
+ * 无条件 sendLogin("logged_in") 会制造 failed→logged_in 的误导序列
1472
+ * (koishi driver 收到 logged_in 但子进程无协议装配,上线后所有请求失败);
1473
+ * 装配进行中的并发结果同样忽略(不干扰初次装配链)。
1474
+ *
1475
+ * 防重入(2026-09-08 拍板):control login 全程互斥(claim/release)——在途
1476
+ * 登录/重装配期间的新指令直接忽略(仅留日志,不发登录消息,面板随首个流程
1477
+ * 的真实状态收敛)。
1478
+ */
1479
+ /** 登录控制相位机(bootstrapWithCore 持有,handler 与引导链共享)。 */
1480
+ var LoginControl = class {
1481
+ phase = "login-race";
1482
+ claimed = false;
1483
+ preempt;
1484
+ constructor(preempt) {
1485
+ this.preempt = preempt;
1486
+ }
1487
+ /** 当前相位。 */
1488
+ get currentPhase() {
1489
+ return this.phase;
1490
+ }
1491
+ /** 取走竞速抢占口(登录期 control login 成功接管引导链用);竞速已结束返回 null。 */
1492
+ takePreempt() {
1493
+ const resolve = this.preempt.resolve;
1494
+ if (resolve === null) return null;
1495
+ this.preempt.resolve = null;
1496
+ return resolve;
1497
+ }
1498
+ /** 竞速结束(初始登录 settle):关抢占口,进入装配期。 */
1499
+ beginAssembly() {
1500
+ this.preempt.resolve = null;
1501
+ this.phase = "assembling";
1502
+ }
1503
+ /** 装配完成:开启软重登窗口。 */
1504
+ markReady() {
1505
+ this.phase = "ready";
1506
+ }
1507
+ /** 引导失败(进程将退出):关抢占口,迟到 control login 结果不再上报。 */
1508
+ abort() {
1509
+ this.preempt.resolve = null;
1510
+ this.phase = "aborted";
1511
+ }
1512
+ /** control login 互斥(在途防重入):同一时刻只允许一个 control login 流程。 */
1513
+ claim() {
1514
+ if (this.claimed) return false;
1515
+ this.claimed = true;
1516
+ return true;
1517
+ }
1518
+ /** 释放互斥(core.login settle 后调用,含软重登重装配完成)。 */
1519
+ release() {
1520
+ this.claimed = false;
1521
+ }
1522
+ };
1523
+ /** 登录状态字面量(IPC login 消息,与 kernel LoginState 对齐)。 */
1524
+ const LOGIN_STATES = [
1525
+ "idle",
1526
+ "waiting_scan",
1527
+ "scanned",
1528
+ "logged_in",
1529
+ "failed"
1530
+ ];
1531
+ /** 宽松收窄(kernel onLoginProgress.state 是 string;bootstrap-core 登录回调共用)。 */
1532
+ function isLoginState(value) {
1533
+ return LOGIN_STATES.includes(value);
1534
+ }
1535
+ /** ready 态软重登:重装配 → 成功重播 ready/logged_in,失败交失败处理。 */
1536
+ async function softReloginAndReport(loginResult, selfInfo, deps) {
1537
+ if (!await deps.reassemble(loginResult)) {
1538
+ (deps.onReassembleFailed ?? defaultReassembleFailed)();
1539
+ return;
1540
+ }
1541
+ sendStatus("ready");
1542
+ sendLogin("logged_in", selfInfo);
1543
+ }
1544
+ /** control login 成功结果按相位分派(抽取自 handler,控制认知复杂度)。 */
1545
+ async function dispatchLoginSuccess(result, control, deps) {
1546
+ const selfInfo = {
1547
+ uin: result.uin,
1548
+ uid: result.uid,
1549
+ nick: result.nick ?? ""
1550
+ };
1551
+ const preemptResolve = control.takePreempt();
1552
+ if (preemptResolve !== null) {
1553
+ sendLogin("logged_in", selfInfo);
1554
+ preemptResolve(result);
1555
+ return;
1556
+ }
1557
+ if (control.currentPhase === "ready") {
1558
+ await softReloginAndReport(result, selfInfo, deps);
1559
+ return;
1560
+ }
1561
+ log(`relogin: control login 成功但引导相位=${control.currentPhase},忽略结果`);
1562
+ }
1563
+ /** 重装配失败默认处理:上报 failed + 退出进程(koishi driver 重启循环回收)。 */
1564
+ function defaultReassembleFailed() {
1565
+ log("relogin: 软重登重装配失败,上报 failed 并退出(driver 重启循环接管)");
1566
+ sendStatus("failed", "软重登重装配失败", {
1567
+ code: "UNKNOWN",
1568
+ message: "软重登重装配失败,详见 napuketto-boot.log"
1569
+ });
1570
+ node_process.default.exit(1);
1571
+ }
1572
+ /**
1573
+ * control login 指令 → 重新登录(qr=true 强制扫码跳过快速登录,uin 指定账号)。
1574
+ * 成功结果按 LoginControl 相位分派(登录期抢占 / ready 软重登 / 忽略迟到)。
1575
+ * 该 handler 仅在 IPC 模式经 startIpcServer 的 onLogin 注册(见 bootstrapWithCore),
1576
+ * 非 IPC 模式无 control 通道不会触达,故无条件走 JSON 行协议、无需再判 ipcMode。
1577
+ */
1578
+ function createLoginControlHandler(core, Appid, control, deps) {
1579
+ return (payload) => {
1580
+ if (!control.claim()) {
1581
+ log("relogin: control login 在途(登录/软重登进行中),忽略本次请求");
1582
+ return;
1583
+ }
1584
+ const opts = {
1585
+ appid: String(Appid),
1586
+ initTimeoutMs: 2e4,
1587
+ qrFallback: true,
1588
+ ...payload.uin !== void 0 ? { quickUin: payload.uin } : {},
1589
+ ...payload.qr === true ? { qrOnly: true } : {},
1590
+ onLoginProgress: (progress) => {
1591
+ if (progress.qr !== void 0) sendQr(progress.qr.pngBase64, progress.qr.qrcodeUrl);
1592
+ if (isLoginState(progress.state)) sendLogin(progress.state, progress.selfInfo, progress.message);
1593
+ }
1594
+ };
1595
+ core.login(opts).then(async (result) => {
1596
+ if (result === null) {
1597
+ sendLogin("failed", void 0, "登录返回空结果");
1598
+ return;
1599
+ }
1600
+ await dispatchLoginSuccess(result, control, deps);
1601
+ }).catch((err) => {
1602
+ log(`bootstrap: control login 失败: ${errMsg(err)}`);
1603
+ sendLogin("failed", void 0, errMsg(err));
1604
+ }).finally(() => {
1605
+ control.release();
1606
+ });
1607
+ };
1608
+ }
1609
+ //#endregion
1419
1610
  //#region src/host/core/session.ts
1420
1611
  /** 对象有效性判断:getMsgService() 调用不抛断言(cpp_impl 已激活)。
1421
1612
  * 与 isSessionUsable 的区别:这里允许返回 null(未 init 但对象有效)。 */
@@ -1688,18 +1879,6 @@ function probeCreatedSession(state, ctx) {
1688
1879
  log(`BOOT: create() 探测失败: ${errMsg(e)}`);
1689
1880
  }
1690
1881
  }
1691
- /** 登录状态字面量(IPC login 消息,与 kernel LoginState 对齐)。 */
1692
- const LOGIN_STATES = [
1693
- "idle",
1694
- "waiting_scan",
1695
- "scanned",
1696
- "logged_in",
1697
- "failed"
1698
- ];
1699
- /** 宽松收窄(kernel onLoginProgress.state 是 string)。 */
1700
- function isLoginState(value) {
1701
- return LOGIN_STATES.includes(value);
1702
- }
1703
1882
  /** 非 IPC 模式 QR 透出标记行前缀(cli forwardFiltered 解析后终端渲染)。 */
1704
1883
  const QR_LINE_PREFIX = "NAPUTO_QR ";
1705
1884
  /** 登录参数(NAPUTO_QUICK_UIN 强制指定 / ref 目标 / qrOnly 强制扫码 / 默认)。 */
@@ -1724,36 +1903,6 @@ function buildLoginOpts(Appid, forcedUin, ref, qrOnly = false) {
1724
1903
  };
1725
1904
  return opts;
1726
1905
  }
1727
- /** control login 指令 → 重新登录(qr=true 强制扫码跳过快速登录,uin 指定账号)。
1728
- * 登录期(初始 doLogin 竞速中)成功结果经 preemptRef 接管引导链。 */
1729
- function createLoginControlHandler(core, Appid, preemptRef) {
1730
- return (payload) => {
1731
- const opts = {
1732
- appid: String(Appid),
1733
- initTimeoutMs: 2e4,
1734
- qrFallback: true,
1735
- ...payload.uin !== void 0 ? { quickUin: payload.uin } : {},
1736
- ...payload.qr === true ? { qrOnly: true } : {},
1737
- onLoginProgress: (progress) => {
1738
- if (progress.qr !== void 0) sendQr(progress.qr.pngBase64, progress.qr.qrcodeUrl);
1739
- if (isLoginState(progress.state)) sendLogin(progress.state, progress.selfInfo, progress.message);
1740
- }
1741
- };
1742
- core.login(opts).then((result) => {
1743
- if (result !== null) {
1744
- sendLogin("logged_in", {
1745
- uin: result.uin,
1746
- uid: result.uid,
1747
- nick: result.nick ?? ""
1748
- });
1749
- preemptRef?.resolve?.(result);
1750
- } else sendLogin("failed", void 0, "登录返回空结果");
1751
- }).catch((err) => {
1752
- log(`bootstrap: control login 失败: ${errMsg(err)}`);
1753
- sendLogin("failed", void 0, errMsg(err));
1754
- });
1755
- };
1756
- }
1757
1906
  /** V2 登录后替换 session:优先 QQ 主 session,其次 vehicle 单例表;自建宿主例外。 */
1758
1907
  function replaceSession(kernel, core, state, ctx) {
1759
1908
  if (env.NAPUTO_SELF_HOST === "1") return null;
@@ -1849,20 +1998,72 @@ async function bootstrapWithCore(kernel, state, bootEnv, Appid) {
1849
1998
  const preemptLogin = new Promise((resolve) => {
1850
1999
  preempt.resolve = resolve;
1851
2000
  });
2001
+ const control = new LoginControl(preempt);
2002
+ const reassembleRef = { current: null };
1852
2003
  if (env.NAPUTO_IPC === "1") {
1853
2004
  ipcActions = createIpcActionsForCore(core);
1854
2005
  startIpcServer({
1855
2006
  actions: ipcActions,
1856
- onLogin: createLoginControlHandler(core, Appid, preempt)
2007
+ onLogin: createLoginControlHandler(core, Appid, control, { reassemble: (loginResult) => {
2008
+ const fn = reassembleRef.current;
2009
+ if (fn === null) {
2010
+ log("bootstrap: softRelogin 未就绪(理论不可达:相位机 ready 守卫)");
2011
+ return Promise.resolve(false);
2012
+ }
2013
+ return fn(loginResult);
2014
+ } })
1857
2015
  });
1858
2016
  }
1859
2017
  const ctx = core.attachWrapper(state.wrapperExports, { ...bootEnv });
1860
2018
  log(`bootstrap: attachWrapper OK, engine=${typeof ctx.engine}, session=${ctx.session !== null}`);
2019
+ let services = null;
2020
+ let stopIpcServices = null;
2021
+ let stopOb11Bridge = null;
2022
+ /**
2023
+ * 装配链尾部(登录成功后;初次引导与软重登共用)。
2024
+ * @returns 装配是否成功(协议服务未装配 = false,调用方判引导失败)。
2025
+ */
2026
+ const runAssembly = async (loginResult, initial) => {
2027
+ const chosen = replaceSession(kernel, core, state, ctx);
2028
+ await activateSession(kernel, state, ctx, chosen, loginResult, Appid, bootEnv);
2029
+ await waitSessionReady(kernel, ctx);
2030
+ if (env.NAPUTO_IPC === "1") sendStatus("sessioning");
2031
+ if (initial) await runSmokeIfEnabled(kernel, ctx, loginResult);
2032
+ const newServices = await startProtocols(kernel, ctx, loginResult);
2033
+ if (newServices === null) {
2034
+ log("bootstrap: 协议服务装配失败,引导中止");
2035
+ return false;
2036
+ }
2037
+ if (env.NAPUTO_IPC === "1" && ipcActions !== null) {
2038
+ stopIpcServices = attachIpcServices(ipcActions, newServices);
2039
+ stopOb11Bridge = await attachOb11IpcBridge(ipcActions, newServices);
2040
+ }
2041
+ services = newServices;
2042
+ return true;
2043
+ };
2044
+ /**
2045
+ * 软重登重装配(ready 态 control login 成功,relogin.ts 经 deps 调用):
2046
+ * 清理旧装配面 → 用新登录结果重跑装配链(A2,2026-09-08)。
2047
+ * 清理顺序:协议消费方(OB11 桥退订+动作表移除)→ IPC 事件转发/动作表
2048
+ * → kernel 桥/缓存/消息日志(dispose,幂等)。失败返回 false(上报退出)。
2049
+ */
2050
+ const softRelogin = async (loginResult) => {
2051
+ log(`bootstrap: 软重登 uin=${loginResult.uin}——清理旧装配并重跑装配链`);
2052
+ stopOb11Bridge?.();
2053
+ stopOb11Bridge = null;
2054
+ stopIpcServices?.();
2055
+ stopIpcServices = null;
2056
+ services?.dispose();
2057
+ services = null;
2058
+ return await runAssembly(loginResult, false);
2059
+ };
2060
+ reassembleRef.current = softRelogin;
1861
2061
  probeCreatedSession(state, ctx);
1862
2062
  startSessionProbe(state, ctx);
1863
2063
  probeSessionMethods(ctx);
1864
2064
  if (typeof core.login !== "function") {
1865
2065
  log("bootstrap: kernel core missing login fn");
2066
+ control.abort();
1866
2067
  return false;
1867
2068
  }
1868
2069
  const forcedUin = env.NAPUTO_QUICK_UIN;
@@ -1871,8 +2072,9 @@ async function bootstrapWithCore(kernel, state, bootEnv, Appid) {
1871
2072
  await pickLoginAccount(kernel, ctx, ref, forcedUin);
1872
2073
  if (env.NAPUTO_IPC === "1") sendStatus("logging");
1873
2074
  const loginResult = await Promise.race([doLogin(core, buildLoginOpts(Appid, forcedUin, ref, qrOnly)), preemptLogin]);
1874
- preempt.resolve = null;
2075
+ control.beginAssembly();
1875
2076
  if (loginResult === null) {
2077
+ control.abort();
1876
2078
  log("bootstrap: 登录失败,引导中止");
1877
2079
  if (env.NAPUTO_IPC === "1") sendStatus("failed", "登录失败", {
1878
2080
  code: "NOT_LOGIN",
@@ -1886,19 +2088,11 @@ async function bootstrapWithCore(kernel, state, bootEnv, Appid) {
1886
2088
  uid: loginResult.uid,
1887
2089
  nick: loginResult.nick ?? ""
1888
2090
  });
1889
- await activateSession(kernel, state, ctx, replaceSession(kernel, core, state, ctx), loginResult, Appid, bootEnv);
1890
- await waitSessionReady(kernel, ctx);
1891
- if (env.NAPUTO_IPC === "1") sendStatus("sessioning");
1892
- await runSmokeIfEnabled(kernel, ctx, loginResult);
1893
- const services = await startProtocols(kernel, ctx, loginResult);
1894
- if (services === null) {
1895
- log("bootstrap: 协议服务装配失败,引导中止");
2091
+ if (!await runAssembly(loginResult, true)) {
2092
+ control.abort();
1896
2093
  return false;
1897
2094
  }
1898
- if (env.NAPUTO_IPC === "1" && ipcActions !== null) {
1899
- attachIpcServices(ipcActions, services);
1900
- await attachOb11IpcBridge(ipcActions, services);
1901
- }
2095
+ control.markReady();
1902
2096
  runProbePhase(kernel, ctx);
1903
2097
  return true;
1904
2098
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@napuketto/loader",
3
- "version": "0.0.29",
3
+ "version": "0.0.30",
4
4
  "description": "NapukettoQQ 引导组件:自建宿主(标准 Node + stub QQNT.dll)dlopen wrapper.node 并启动 kernel,不拉起 QQ / 不注入",
5
5
  "type": "module",
6
6
  "files": [