@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 +1 -1
- package/index.ts +167 -18
- package/package.json +3 -3
- package/src/channel.ts +766 -99
- package/src/core/types.ts +15 -1
- package/src/messaging/outbound/build-send-action.ts +115 -0
package/README.md
CHANGED
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
|
|
607
|
-
|
|
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
|
|
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
|
|
752
|
+
Boolean(bridge?.isDebugEnabled?.()),
|
|
628
753
|
);
|
|
629
754
|
};
|
|
630
755
|
|
|
631
756
|
debugLog(
|
|
632
|
-
`register begin bridge=${bridge
|
|
633
|
-
`ownerApi=${owner
|
|
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
|
-
|
|
642
|
-
|
|
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 (!
|
|
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
|
|
796
|
+
await getCurrentBridge().startService(ctx, debug);
|
|
659
797
|
},
|
|
660
|
-
stop:
|
|
798
|
+
stop: serviceStopHandler,
|
|
661
799
|
});
|
|
800
|
+
activeServiceStop = serviceStopHandler;
|
|
801
|
+
gatewayRuntime.serviceRegistered = true;
|
|
802
|
+
gatewayRuntime.serviceOwnerApiInstanceId = apiInstanceId;
|
|
662
803
|
meta.service = true;
|
|
663
|
-
debugLog(
|
|
804
|
+
debugLog(`register service ok ownerApi=${apiInstanceId}`);
|
|
664
805
|
} else {
|
|
665
|
-
|
|
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 (!
|
|
669
|
-
api.registerChannel({ plugin: runtime
|
|
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(
|
|
817
|
+
debugLog(`register channel ok ownerApi=${apiInstanceId}`);
|
|
672
818
|
} else {
|
|
673
|
-
|
|
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.
|
|
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
|
|
36
|
+
"openclaw": ">=2026.5.3-1"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@biomejs/biome": "^1.9.4",
|
|
40
|
-
"openclaw": ">=2026.3
|
|
40
|
+
"openclaw": ">=2026.5.3-1"
|
|
41
41
|
},
|
|
42
42
|
"openclaw": {
|
|
43
43
|
"extensions": [
|