@xmoxmo/bncr 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -34,7 +34,7 @@ openclaw plugins update bncr
34
34
  openclaw gateway restart
35
35
  ```
36
36
 
37
- > 兼容范围:`openclaw >= 2026.3.22`
37
+ > 兼容范围:`openclaw >= 2026.5.3-1`
38
38
  >
39
39
  > 如果你是从精确版本升级,或本地安装记录仍钉在旧版本,也可以显式执行:
40
40
  >
package/index.ts CHANGED
@@ -15,6 +15,7 @@ const linkType = process.platform === 'win32' ? 'junction' : 'dir';
15
15
  type ChannelModule = typeof import('./src/channel.ts');
16
16
  type OpenClawPluginApi = Parameters<ChannelModule['createBncrBridge']>[0];
17
17
  type BridgeSingleton = ReturnType<ChannelModule['createBncrBridge']>;
18
+ type ChannelPlugin = ReturnType<ChannelModule['createBncrChannelPlugin']>;
18
19
 
19
20
  type LoadedRuntime = {
20
21
  createBncrBridge: ChannelModule['createBncrBridge'];
@@ -99,9 +100,14 @@ type OpenClawPluginApiWithMeta = OpenClawPluginApi & {
99
100
  type BncrGatewayRuntime = {
100
101
  currentBridge?: BridgeSingletonWithOwner;
101
102
  registeredMethodsByRegistry: Map<string, Set<GatewayMethodName>>;
103
+ serviceRegistered?: boolean;
104
+ channelRegistered?: boolean;
105
+ serviceOwnerApiInstanceId?: string;
106
+ channelOwnerApiInstanceId?: string;
102
107
  };
103
108
 
104
109
  let runtime: LoadedRuntime | null = null;
110
+ let activeServiceStop: (() => Promise<void>) | null = null;
105
111
  const identityIds = new WeakMap<object, string>();
106
112
  let identitySeq = 0;
107
113
 
@@ -337,11 +343,49 @@ const getGatewayRuntime = (): BncrGatewayRuntime => {
337
343
  if (!p[BNCR_GATEWAY_RUNTIME]) {
338
344
  p[BNCR_GATEWAY_RUNTIME] = {
339
345
  registeredMethodsByRegistry: new Map<string, Set<GatewayMethodName>>(),
346
+ serviceRegistered: false,
347
+ channelRegistered: false,
340
348
  };
341
349
  }
342
350
  return p[BNCR_GATEWAY_RUNTIME]!;
343
351
  };
344
352
 
353
+ const getProcessOwnerApiInstanceId = (gatewayRuntime: BncrGatewayRuntime) =>
354
+ gatewayRuntime.serviceOwnerApiInstanceId ||
355
+ gatewayRuntime.channelOwnerApiInstanceId ||
356
+ undefined;
357
+
358
+ const shouldAdoptProcessOwner = (
359
+ apiInstanceId: string,
360
+ gatewayRuntime: BncrGatewayRuntime,
361
+ ) => {
362
+ const existingOwnerApiInstanceId = getProcessOwnerApiInstanceId(gatewayRuntime);
363
+ const hasSingletonOwner =
364
+ Boolean(gatewayRuntime.serviceRegistered) || Boolean(gatewayRuntime.channelRegistered);
365
+
366
+ if (!hasSingletonOwner) {
367
+ return {
368
+ adoptOwner: true,
369
+ existingOwnerApiInstanceId,
370
+ reason: 'no-singleton-owner',
371
+ };
372
+ }
373
+
374
+ if (existingOwnerApiInstanceId && existingOwnerApiInstanceId === apiInstanceId) {
375
+ return {
376
+ adoptOwner: true,
377
+ existingOwnerApiInstanceId,
378
+ reason: 'same-owner-api',
379
+ };
380
+ }
381
+
382
+ return {
383
+ adoptOwner: false,
384
+ existingOwnerApiInstanceId,
385
+ reason: 'singleton-owned-by-other-api',
386
+ };
387
+ };
388
+
345
389
  const gatewayMethodDispatchers: Record<
346
390
  GatewayMethodName,
347
391
  (bridge: BridgeSingletonWithOwner, opts: any) => any
@@ -528,9 +572,60 @@ const getBridgeSingleton = (api: OpenClawPluginApi) => {
528
572
  return { bridge: g.__bncrBridge, runtime: loaded, created, rebuilt, owner, previousOwner };
529
573
  };
530
574
 
575
+ const getExistingBridgeSingleton = (): BridgeSingletonWithOwner | undefined => {
576
+ const g = globalThis as typeof globalThis & { __bncrBridge?: BridgeSingletonWithOwner };
577
+ return g.__bncrBridge;
578
+ };
579
+
531
580
  const isPlainObject = (value: unknown): value is Record<string, unknown> =>
532
581
  typeof value === 'object' && value !== null && !Array.isArray(value);
533
582
 
583
+ const getCurrentBridge = (): BridgeSingletonWithOwner => {
584
+ const bridge = getGatewayRuntime().currentBridge;
585
+ if (!bridge) throw new Error('bncr current bridge unavailable');
586
+ return bridge;
587
+ };
588
+
589
+ const createDynamicChannelPlugin = (loaded: LoadedRuntime): ChannelPlugin => {
590
+ const base = loaded.createBncrChannelPlugin(() => getCurrentBridge());
591
+
592
+ return {
593
+ ...base,
594
+ outbound: {
595
+ ...base.outbound,
596
+ sendText: (ctx: any) => getCurrentBridge().channelSendText(ctx),
597
+ sendMedia: (ctx: any) => getCurrentBridge().channelSendMedia(ctx),
598
+ },
599
+ status: {
600
+ ...base.status,
601
+ buildChannelSummary: async ({ defaultAccountId }: any) =>
602
+ getCurrentBridge().getChannelSummary(defaultAccountId || 'Primary'),
603
+ buildAccountSnapshot: async ({ account, runtime }: any) => {
604
+ const bridgeNow = getCurrentBridge();
605
+ return base.status.buildAccountSnapshot({
606
+ account,
607
+ runtime: runtime || bridgeNow.getAccountRuntimeSnapshot(account?.accountId),
608
+ });
609
+ },
610
+ resolveAccountState: ({ enabled, configured, account, cfg, runtime }: any) => {
611
+ const bridgeNow = getCurrentBridge();
612
+ return base.status.resolveAccountState({
613
+ enabled,
614
+ configured,
615
+ account,
616
+ cfg,
617
+ runtime: runtime || bridgeNow.getAccountRuntimeSnapshot(account?.accountId),
618
+ });
619
+ },
620
+ },
621
+ gateway: {
622
+ ...base.gateway,
623
+ startAccount: (ctx: any) => getCurrentBridge().channelStartAccount(ctx),
624
+ stopAccount: (ctx: any) => getCurrentBridge().channelStopAccount(ctx),
625
+ },
626
+ };
627
+ };
628
+
534
629
  const registerBncrCli = (api: OpenClawPluginApi & { registerCli?: (...args: any[]) => void }) => {
535
630
  if (typeof api.registerCli !== 'function') return;
536
631
  api.registerCli(
@@ -591,6 +686,10 @@ const plugin = {
591
686
  registerBncrCli(api);
592
687
  if (shouldSkipNonRuntimeRegister(api.registrationMode)) return;
593
688
 
689
+ // 注意:OpenClaw 要求 plugin register 必须是同步函数;
690
+ // 不要在这里 await 停旧 service / 清理旧 runtime,否则 loader 会直接拒绝加载。
691
+ // 旧实例清理由 service stop / runtime 自愈逻辑兜底,这里只做同步声明式注册。
692
+
594
693
  const meta = getRegisterMeta(api);
595
694
  meta.registrationMode = api.registrationMode;
596
695
  const globalTrace = getGlobalRegisterTrace();
@@ -603,17 +702,43 @@ const plugin = {
603
702
  const firstSeenApi = !globalTrace.seenApiInstanceIds.has(apiInstanceId);
604
703
  const firstSeenRegistry = !globalTrace.seenRegistryFingerprints.has(registryFingerprint);
605
704
 
606
- const { bridge, runtime, created, rebuilt, owner, previousOwner } = getBridgeSingleton(api);
607
- getGatewayRuntime().currentBridge = bridge;
705
+ const gatewayRuntime = getGatewayRuntime();
706
+ const ownerDecision = shouldAdoptProcessOwner(apiInstanceId, gatewayRuntime);
707
+
708
+ let bridge: BridgeSingletonWithOwner | undefined;
709
+ let runtime: LoadedRuntime;
710
+ let created = false;
711
+ let rebuilt = false;
712
+ let owner: BridgeOwner | undefined;
713
+ let previousOwner: BridgeOwner | undefined;
714
+
715
+ if (ownerDecision.adoptOwner) {
716
+ const adopted = getBridgeSingleton(api);
717
+ bridge = adopted.bridge;
718
+ runtime = adopted.runtime;
719
+ created = adopted.created;
720
+ rebuilt = adopted.rebuilt;
721
+ owner = adopted.owner;
722
+ previousOwner = adopted.previousOwner;
723
+ gatewayRuntime.currentBridge = bridge;
724
+ } else {
725
+ runtime = loadRuntimeSync();
726
+ bridge = gatewayRuntime.currentBridge || getExistingBridgeSingleton();
727
+ previousOwner = getExistingBridgeSingleton()?.[BNCR_BRIDGE_OWNER];
728
+ owner = previousOwner;
729
+ if (bridge && !gatewayRuntime.currentBridge) {
730
+ gatewayRuntime.currentBridge = bridge;
731
+ }
732
+ }
608
733
 
609
734
  globalTrace.seenApiInstanceIds.add(apiInstanceId);
610
735
  globalTrace.seenRegistryFingerprints.add(registryFingerprint);
611
736
  globalTrace.lastApiInstanceId = apiInstanceId;
612
737
  globalTrace.lastRegistryFingerprint = registryFingerprint;
613
- bridge.noteRegister?.({
738
+ bridge?.noteRegister?.({
614
739
  source: '~/.openclaw/workspace/plugins/bncr/index.ts',
615
740
  pluginVersion,
616
- apiRebound: !created && !rebuilt,
741
+ apiRebound: ownerDecision.adoptOwner ? !created && !rebuilt : false,
617
742
  apiInstanceId: meta.apiInstanceId,
618
743
  registryFingerprint: meta.registryFingerprint,
619
744
  });
@@ -624,13 +749,13 @@ const plugin = {
624
749
  .trim();
625
750
  if (!rendered) return;
626
751
  emitBncrLogLine('info', `[bncr] debug ${rendered}`, { debugOnly: true }, () =>
627
- Boolean(bridge.isDebugEnabled?.()),
752
+ Boolean(bridge?.isDebugEnabled?.()),
628
753
  );
629
754
  };
630
755
 
631
756
  debugLog(
632
- `register begin bridge=${bridge.getBridgeId?.() || 'unknown'} created=${created} rebuilt=${rebuilt} ` +
633
- `ownerApi=${owner.apiInstanceId} ownerRegistry=${owner.registryFingerprint} ` +
757
+ `register begin bridge=${bridge?.getBridgeId?.() || 'unknown'} created=${created} rebuilt=${rebuilt} ` +
758
+ `ownerApi=${owner?.apiInstanceId || 'none'} ownerRegistry=${owner?.registryFingerprint || 'none'} ` +
634
759
  `previousOwnerApi=${previousOwner?.apiInstanceId || 'none'} previousOwnerRegistry=${previousOwner?.registryFingerprint || 'none'}`,
635
760
  );
636
761
  debugLog(
@@ -638,8 +763,18 @@ const plugin = {
638
763
  `sameApiAsPrevious=${sameApiAsPrevious} sameRegistryAsPrevious=${sameRegistryAsPrevious} ` +
639
764
  `firstSeenApi=${firstSeenApi} firstSeenRegistry=${firstSeenRegistry}`,
640
765
  );
641
- if (!created && !rebuilt) debugLog('bridge api rebound');
642
- if (rebuilt) debugLog('bridge rebuilt due to owner/runtime change');
766
+ debugLog(
767
+ `register owner adopt=${ownerDecision.adoptOwner} reason=${ownerDecision.reason} ` +
768
+ `existingOwnerApi=${ownerDecision.existingOwnerApiInstanceId || 'none'}`,
769
+ );
770
+ if (!ownerDecision.adoptOwner) {
771
+ debugLog(
772
+ `bridge rebuild suppressed due to existing singleton owner api ${ownerDecision.existingOwnerApiInstanceId || 'unknown'}`,
773
+ );
774
+ } else {
775
+ if (!created && !rebuilt) debugLog('bridge api rebound');
776
+ if (rebuilt) debugLog('bridge rebuilt due to owner/runtime change');
777
+ }
643
778
 
644
779
  const resolveDebug = async () => {
645
780
  try {
@@ -650,27 +785,41 @@ const plugin = {
650
785
  }
651
786
  };
652
787
 
653
- if (!meta.service) {
788
+ if (!gatewayRuntime.serviceRegistered) {
789
+ const serviceStopHandler = async () => {
790
+ await getCurrentBridge().stopService?.();
791
+ };
654
792
  api.registerService({
655
793
  id: 'bncr-bridge-service',
656
794
  start: async (ctx) => {
657
795
  const debug = await resolveDebug();
658
- await bridge.startService(ctx, debug);
796
+ await getCurrentBridge().startService(ctx, debug);
659
797
  },
660
- stop: bridge.stopService,
798
+ stop: serviceStopHandler,
661
799
  });
800
+ activeServiceStop = serviceStopHandler;
801
+ gatewayRuntime.serviceRegistered = true;
802
+ gatewayRuntime.serviceOwnerApiInstanceId = apiInstanceId;
662
803
  meta.service = true;
663
- debugLog('register service ok');
804
+ debugLog(`register service ok ownerApi=${apiInstanceId}`);
664
805
  } else {
665
- debugLog('register service skip (already registered on this api)');
806
+ meta.service = true;
807
+ debugLog(
808
+ `register service skip (process singleton already registered by api ${gatewayRuntime.serviceOwnerApiInstanceId || 'unknown'})`,
809
+ );
666
810
  }
667
811
 
668
- if (!meta.channel) {
669
- api.registerChannel({ plugin: runtime.createBncrChannelPlugin(bridge) });
812
+ if (!gatewayRuntime.channelRegistered) {
813
+ api.registerChannel({ plugin: createDynamicChannelPlugin(runtime) });
814
+ gatewayRuntime.channelRegistered = true;
815
+ gatewayRuntime.channelOwnerApiInstanceId = apiInstanceId;
670
816
  meta.channel = true;
671
- debugLog('register channel ok');
817
+ debugLog(`register channel ok ownerApi=${apiInstanceId}`);
672
818
  } else {
673
- debugLog('register channel skip (already registered on this api)');
819
+ meta.channel = true;
820
+ debugLog(
821
+ `register channel skip (process singleton already registered by api ${gatewayRuntime.channelOwnerApiInstanceId || 'unknown'})`,
822
+ );
674
823
  }
675
824
 
676
825
  ensureGatewayMethodRegistered(api, 'bncr.connect', debugLog);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmoxmo/bncr",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -33,11 +33,11 @@
33
33
  "check": "biome check ."
34
34
  },
35
35
  "peerDependencies": {
36
- "openclaw": ">=2026.3.22"
36
+ "openclaw": ">=2026.5.3-1"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@biomejs/biome": "^1.9.4",
40
- "openclaw": ">=2026.3.22"
40
+ "openclaw": ">=2026.5.3-1"
41
41
  },
42
42
  "openclaw": {
43
43
  "extensions": [