@xmoxmo/bncr 0.2.2 → 0.2.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.
- package/README.md +1 -1
- package/index.ts +191 -19
- package/package.json +3 -3
- package/src/channel.ts +1758 -172
- package/src/core/status.ts +10 -10
- package/src/core/types.ts +18 -1
- package/src/messaging/outbound/build-send-action.ts +115 -0
- package/src/messaging/outbound/send.ts +2 -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
|
|
@@ -364,7 +408,30 @@ const dispatchGatewayMethod = (name: GatewayMethodName, opts: any) => {
|
|
|
364
408
|
if (!bridge) {
|
|
365
409
|
throw new Error(`bncr gateway runtime unavailable for ${name}`);
|
|
366
410
|
}
|
|
367
|
-
|
|
411
|
+
try {
|
|
412
|
+
return gatewayMethodDispatchers[name](bridge, opts);
|
|
413
|
+
} catch (error) {
|
|
414
|
+
const detail =
|
|
415
|
+
error instanceof Error
|
|
416
|
+
? {
|
|
417
|
+
name: error.name,
|
|
418
|
+
message: error.message,
|
|
419
|
+
stack: error.stack || null,
|
|
420
|
+
}
|
|
421
|
+
: { name: 'NonError', message: String(error), stack: null };
|
|
422
|
+
emitBncrLogLine(
|
|
423
|
+
'error',
|
|
424
|
+
`[bncr] gateway method error ${JSON.stringify({
|
|
425
|
+
method: name,
|
|
426
|
+
bridgeId: bridge.getBridgeId?.() || null,
|
|
427
|
+
gatewayPid: bridge.gatewayPid || null,
|
|
428
|
+
detail,
|
|
429
|
+
})}`,
|
|
430
|
+
{ debugOnly: true },
|
|
431
|
+
() => true,
|
|
432
|
+
);
|
|
433
|
+
throw error;
|
|
434
|
+
}
|
|
368
435
|
};
|
|
369
436
|
|
|
370
437
|
const mirrorGatewayMethodForMockApi = (api: OpenClawPluginApi, name: GatewayMethodName) => {
|
|
@@ -528,9 +595,60 @@ const getBridgeSingleton = (api: OpenClawPluginApi) => {
|
|
|
528
595
|
return { bridge: g.__bncrBridge, runtime: loaded, created, rebuilt, owner, previousOwner };
|
|
529
596
|
};
|
|
530
597
|
|
|
598
|
+
const getExistingBridgeSingleton = (): BridgeSingletonWithOwner | undefined => {
|
|
599
|
+
const g = globalThis as typeof globalThis & { __bncrBridge?: BridgeSingletonWithOwner };
|
|
600
|
+
return g.__bncrBridge;
|
|
601
|
+
};
|
|
602
|
+
|
|
531
603
|
const isPlainObject = (value: unknown): value is Record<string, unknown> =>
|
|
532
604
|
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
533
605
|
|
|
606
|
+
const getCurrentBridge = (): BridgeSingletonWithOwner => {
|
|
607
|
+
const bridge = getGatewayRuntime().currentBridge;
|
|
608
|
+
if (!bridge) throw new Error('bncr current bridge unavailable');
|
|
609
|
+
return bridge;
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
const createDynamicChannelPlugin = (loaded: LoadedRuntime): ChannelPlugin => {
|
|
613
|
+
const base = loaded.createBncrChannelPlugin(() => getCurrentBridge());
|
|
614
|
+
|
|
615
|
+
return {
|
|
616
|
+
...base,
|
|
617
|
+
outbound: {
|
|
618
|
+
...base.outbound,
|
|
619
|
+
sendText: (ctx: any) => getCurrentBridge().channelSendText(ctx),
|
|
620
|
+
sendMedia: (ctx: any) => getCurrentBridge().channelSendMedia(ctx),
|
|
621
|
+
},
|
|
622
|
+
status: {
|
|
623
|
+
...base.status,
|
|
624
|
+
buildChannelSummary: async ({ defaultAccountId }: any) =>
|
|
625
|
+
getCurrentBridge().getChannelSummary(defaultAccountId || 'Primary'),
|
|
626
|
+
buildAccountSnapshot: async ({ account, runtime }: any) => {
|
|
627
|
+
const bridgeNow = getCurrentBridge();
|
|
628
|
+
return base.status.buildAccountSnapshot({
|
|
629
|
+
account,
|
|
630
|
+
runtime: runtime || bridgeNow.getAccountRuntimeSnapshot(account?.accountId),
|
|
631
|
+
});
|
|
632
|
+
},
|
|
633
|
+
resolveAccountState: ({ enabled, configured, account, cfg, runtime }: any) => {
|
|
634
|
+
const bridgeNow = getCurrentBridge();
|
|
635
|
+
return base.status.resolveAccountState({
|
|
636
|
+
enabled,
|
|
637
|
+
configured,
|
|
638
|
+
account,
|
|
639
|
+
cfg,
|
|
640
|
+
runtime: runtime || bridgeNow.getAccountRuntimeSnapshot(account?.accountId),
|
|
641
|
+
});
|
|
642
|
+
},
|
|
643
|
+
},
|
|
644
|
+
gateway: {
|
|
645
|
+
...base.gateway,
|
|
646
|
+
startAccount: (ctx: any) => getCurrentBridge().channelStartAccount(ctx),
|
|
647
|
+
stopAccount: (ctx: any) => getCurrentBridge().channelStopAccount(ctx),
|
|
648
|
+
},
|
|
649
|
+
};
|
|
650
|
+
};
|
|
651
|
+
|
|
534
652
|
const registerBncrCli = (api: OpenClawPluginApi & { registerCli?: (...args: any[]) => void }) => {
|
|
535
653
|
if (typeof api.registerCli !== 'function') return;
|
|
536
654
|
api.registerCli(
|
|
@@ -591,6 +709,10 @@ const plugin = {
|
|
|
591
709
|
registerBncrCli(api);
|
|
592
710
|
if (shouldSkipNonRuntimeRegister(api.registrationMode)) return;
|
|
593
711
|
|
|
712
|
+
// 注意:OpenClaw 要求 plugin register 必须是同步函数;
|
|
713
|
+
// 不要在这里 await 停旧 service / 清理旧 runtime,否则 loader 会直接拒绝加载。
|
|
714
|
+
// 旧实例清理由 service stop / runtime 自愈逻辑兜底,这里只做同步声明式注册。
|
|
715
|
+
|
|
594
716
|
const meta = getRegisterMeta(api);
|
|
595
717
|
meta.registrationMode = api.registrationMode;
|
|
596
718
|
const globalTrace = getGlobalRegisterTrace();
|
|
@@ -603,17 +725,43 @@ const plugin = {
|
|
|
603
725
|
const firstSeenApi = !globalTrace.seenApiInstanceIds.has(apiInstanceId);
|
|
604
726
|
const firstSeenRegistry = !globalTrace.seenRegistryFingerprints.has(registryFingerprint);
|
|
605
727
|
|
|
606
|
-
const
|
|
607
|
-
|
|
728
|
+
const gatewayRuntime = getGatewayRuntime();
|
|
729
|
+
const ownerDecision = shouldAdoptProcessOwner(apiInstanceId, gatewayRuntime);
|
|
730
|
+
|
|
731
|
+
let bridge: BridgeSingletonWithOwner | undefined;
|
|
732
|
+
let runtime: LoadedRuntime;
|
|
733
|
+
let created = false;
|
|
734
|
+
let rebuilt = false;
|
|
735
|
+
let owner: BridgeOwner | undefined;
|
|
736
|
+
let previousOwner: BridgeOwner | undefined;
|
|
737
|
+
|
|
738
|
+
if (ownerDecision.adoptOwner) {
|
|
739
|
+
const adopted = getBridgeSingleton(api);
|
|
740
|
+
bridge = adopted.bridge;
|
|
741
|
+
runtime = adopted.runtime;
|
|
742
|
+
created = adopted.created;
|
|
743
|
+
rebuilt = adopted.rebuilt;
|
|
744
|
+
owner = adopted.owner;
|
|
745
|
+
previousOwner = adopted.previousOwner;
|
|
746
|
+
gatewayRuntime.currentBridge = bridge;
|
|
747
|
+
} else {
|
|
748
|
+
runtime = loadRuntimeSync();
|
|
749
|
+
bridge = gatewayRuntime.currentBridge || getExistingBridgeSingleton();
|
|
750
|
+
previousOwner = getExistingBridgeSingleton()?.[BNCR_BRIDGE_OWNER];
|
|
751
|
+
owner = previousOwner;
|
|
752
|
+
if (bridge && !gatewayRuntime.currentBridge) {
|
|
753
|
+
gatewayRuntime.currentBridge = bridge;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
608
756
|
|
|
609
757
|
globalTrace.seenApiInstanceIds.add(apiInstanceId);
|
|
610
758
|
globalTrace.seenRegistryFingerprints.add(registryFingerprint);
|
|
611
759
|
globalTrace.lastApiInstanceId = apiInstanceId;
|
|
612
760
|
globalTrace.lastRegistryFingerprint = registryFingerprint;
|
|
613
|
-
bridge
|
|
761
|
+
bridge?.noteRegister?.({
|
|
614
762
|
source: '~/.openclaw/workspace/plugins/bncr/index.ts',
|
|
615
763
|
pluginVersion,
|
|
616
|
-
apiRebound: !created && !rebuilt,
|
|
764
|
+
apiRebound: ownerDecision.adoptOwner ? !created && !rebuilt : false,
|
|
617
765
|
apiInstanceId: meta.apiInstanceId,
|
|
618
766
|
registryFingerprint: meta.registryFingerprint,
|
|
619
767
|
});
|
|
@@ -624,13 +772,13 @@ const plugin = {
|
|
|
624
772
|
.trim();
|
|
625
773
|
if (!rendered) return;
|
|
626
774
|
emitBncrLogLine('info', `[bncr] debug ${rendered}`, { debugOnly: true }, () =>
|
|
627
|
-
Boolean(bridge
|
|
775
|
+
Boolean(bridge?.isDebugEnabled?.()),
|
|
628
776
|
);
|
|
629
777
|
};
|
|
630
778
|
|
|
631
779
|
debugLog(
|
|
632
|
-
`register begin bridge=${bridge
|
|
633
|
-
`ownerApi=${owner
|
|
780
|
+
`register begin bridge=${bridge?.getBridgeId?.() || 'unknown'} created=${created} rebuilt=${rebuilt} ` +
|
|
781
|
+
`ownerApi=${owner?.apiInstanceId || 'none'} ownerRegistry=${owner?.registryFingerprint || 'none'} ` +
|
|
634
782
|
`previousOwnerApi=${previousOwner?.apiInstanceId || 'none'} previousOwnerRegistry=${previousOwner?.registryFingerprint || 'none'}`,
|
|
635
783
|
);
|
|
636
784
|
debugLog(
|
|
@@ -638,8 +786,18 @@ const plugin = {
|
|
|
638
786
|
`sameApiAsPrevious=${sameApiAsPrevious} sameRegistryAsPrevious=${sameRegistryAsPrevious} ` +
|
|
639
787
|
`firstSeenApi=${firstSeenApi} firstSeenRegistry=${firstSeenRegistry}`,
|
|
640
788
|
);
|
|
641
|
-
|
|
642
|
-
|
|
789
|
+
debugLog(
|
|
790
|
+
`register owner adopt=${ownerDecision.adoptOwner} reason=${ownerDecision.reason} ` +
|
|
791
|
+
`existingOwnerApi=${ownerDecision.existingOwnerApiInstanceId || 'none'}`,
|
|
792
|
+
);
|
|
793
|
+
if (!ownerDecision.adoptOwner) {
|
|
794
|
+
debugLog(
|
|
795
|
+
`bridge rebuild suppressed due to existing singleton owner api ${ownerDecision.existingOwnerApiInstanceId || 'unknown'}`,
|
|
796
|
+
);
|
|
797
|
+
} else {
|
|
798
|
+
if (!created && !rebuilt) debugLog('bridge api rebound');
|
|
799
|
+
if (rebuilt) debugLog('bridge rebuilt due to owner/runtime change');
|
|
800
|
+
}
|
|
643
801
|
|
|
644
802
|
const resolveDebug = async () => {
|
|
645
803
|
try {
|
|
@@ -650,27 +808,41 @@ const plugin = {
|
|
|
650
808
|
}
|
|
651
809
|
};
|
|
652
810
|
|
|
653
|
-
if (!
|
|
811
|
+
if (!gatewayRuntime.serviceRegistered) {
|
|
812
|
+
const serviceStopHandler = async () => {
|
|
813
|
+
await getCurrentBridge().stopService?.();
|
|
814
|
+
};
|
|
654
815
|
api.registerService({
|
|
655
816
|
id: 'bncr-bridge-service',
|
|
656
817
|
start: async (ctx) => {
|
|
657
818
|
const debug = await resolveDebug();
|
|
658
|
-
await
|
|
819
|
+
await getCurrentBridge().startService(ctx, debug);
|
|
659
820
|
},
|
|
660
|
-
stop:
|
|
821
|
+
stop: serviceStopHandler,
|
|
661
822
|
});
|
|
823
|
+
activeServiceStop = serviceStopHandler;
|
|
824
|
+
gatewayRuntime.serviceRegistered = true;
|
|
825
|
+
gatewayRuntime.serviceOwnerApiInstanceId = apiInstanceId;
|
|
662
826
|
meta.service = true;
|
|
663
|
-
debugLog(
|
|
827
|
+
debugLog(`register service ok ownerApi=${apiInstanceId}`);
|
|
664
828
|
} else {
|
|
665
|
-
|
|
829
|
+
meta.service = true;
|
|
830
|
+
debugLog(
|
|
831
|
+
`register service skip (process singleton already registered by api ${gatewayRuntime.serviceOwnerApiInstanceId || 'unknown'})`,
|
|
832
|
+
);
|
|
666
833
|
}
|
|
667
834
|
|
|
668
|
-
if (!
|
|
669
|
-
api.registerChannel({ plugin: runtime
|
|
835
|
+
if (!gatewayRuntime.channelRegistered) {
|
|
836
|
+
api.registerChannel({ plugin: createDynamicChannelPlugin(runtime) });
|
|
837
|
+
gatewayRuntime.channelRegistered = true;
|
|
838
|
+
gatewayRuntime.channelOwnerApiInstanceId = apiInstanceId;
|
|
670
839
|
meta.channel = true;
|
|
671
|
-
debugLog(
|
|
840
|
+
debugLog(`register channel ok ownerApi=${apiInstanceId}`);
|
|
672
841
|
} else {
|
|
673
|
-
|
|
842
|
+
meta.channel = true;
|
|
843
|
+
debugLog(
|
|
844
|
+
`register channel skip (process singleton already registered by api ${gatewayRuntime.channelOwnerApiInstanceId || 'unknown'})`,
|
|
845
|
+
);
|
|
674
846
|
}
|
|
675
847
|
|
|
676
848
|
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.4",
|
|
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": [
|