@xmoxmo/bncr 0.2.1 → 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 +6 -2
- package/index.ts +474 -87
- package/openclaw.plugin.json +59 -0
- package/package.json +5 -5
- package/src/channel.ts +1279 -198
- package/src/core/config-schema.ts +6 -0
- package/src/core/types.ts +18 -1
- package/src/messaging/outbound/build-send-action.ts +115 -0
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'];
|
|
@@ -22,6 +23,10 @@ type LoadedRuntime = {
|
|
|
22
23
|
};
|
|
23
24
|
|
|
24
25
|
const BNCR_REGISTER_META = Symbol.for('bncr.register.meta');
|
|
26
|
+
const BNCR_GLOBAL_REGISTER_TRACE = Symbol.for('bncr.global.register.trace');
|
|
27
|
+
const BNCR_BRIDGE_OWNER = Symbol.for('bncr.bridge.owner');
|
|
28
|
+
const BNCR_GATEWAY_RUNTIME = Symbol.for('bncr.gateway.runtime');
|
|
29
|
+
const MODULE_EPOCH = `${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
25
30
|
|
|
26
31
|
type RegisterMeta = {
|
|
27
32
|
service?: boolean;
|
|
@@ -29,13 +34,80 @@ type RegisterMeta = {
|
|
|
29
34
|
methods?: Set<string>;
|
|
30
35
|
apiInstanceId?: string;
|
|
31
36
|
registryFingerprint?: string;
|
|
37
|
+
registrationMode?: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type GlobalRegisterTrace = {
|
|
41
|
+
lastApiInstanceId?: string;
|
|
42
|
+
lastRegistryFingerprint?: string;
|
|
43
|
+
seenRegistryFingerprints: Set<string>;
|
|
44
|
+
seenApiInstanceIds: Set<string>;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
type BridgeOwner = {
|
|
48
|
+
moduleEpoch: string;
|
|
49
|
+
bridgeFactoryId: string;
|
|
50
|
+
apiInstanceId: string;
|
|
51
|
+
registryFingerprint: string;
|
|
52
|
+
registrationMode?: string;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
type BridgeRegisterStateSnapshot = {
|
|
56
|
+
registerCount: number;
|
|
57
|
+
apiGeneration: number;
|
|
58
|
+
firstRegisterAt: number | null;
|
|
59
|
+
lastRegisterAt: number | null;
|
|
60
|
+
lastApiRebindAt: number | null;
|
|
61
|
+
pluginSource: string | null;
|
|
62
|
+
pluginVersion: string | null;
|
|
63
|
+
lastApiInstanceId: string | null;
|
|
64
|
+
lastRegistryFingerprint: string | null;
|
|
65
|
+
lastDriftSnapshot: unknown;
|
|
66
|
+
registerTraceRecent: Array<Record<string, unknown>>;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
type GatewayMethodName =
|
|
70
|
+
| 'bncr.connect'
|
|
71
|
+
| 'bncr.inbound'
|
|
72
|
+
| 'bncr.activity'
|
|
73
|
+
| 'bncr.ack'
|
|
74
|
+
| 'bncr.diagnostics'
|
|
75
|
+
| 'bncr.file.init'
|
|
76
|
+
| 'bncr.file.chunk'
|
|
77
|
+
| 'bncr.file.complete'
|
|
78
|
+
| 'bncr.file.abort'
|
|
79
|
+
| 'bncr.file.ack';
|
|
80
|
+
|
|
81
|
+
type BridgeSingletonWithOwner = BridgeSingleton & {
|
|
82
|
+
[BNCR_BRIDGE_OWNER]?: BridgeOwner;
|
|
83
|
+
registerCount?: number;
|
|
84
|
+
apiGeneration?: number;
|
|
85
|
+
firstRegisterAt?: number | null;
|
|
86
|
+
lastRegisterAt?: number | null;
|
|
87
|
+
lastApiRebindAt?: number | null;
|
|
88
|
+
pluginSource?: string | null;
|
|
89
|
+
pluginVersion?: string | null;
|
|
90
|
+
lastApiInstanceId?: string | null;
|
|
91
|
+
lastRegistryFingerprint?: string | null;
|
|
92
|
+
lastDriftSnapshot?: unknown;
|
|
93
|
+
registerTraceRecent?: Array<Record<string, unknown>>;
|
|
32
94
|
};
|
|
33
95
|
|
|
34
96
|
type OpenClawPluginApiWithMeta = OpenClawPluginApi & {
|
|
35
97
|
[BNCR_REGISTER_META]?: RegisterMeta;
|
|
36
98
|
};
|
|
37
99
|
|
|
100
|
+
type BncrGatewayRuntime = {
|
|
101
|
+
currentBridge?: BridgeSingletonWithOwner;
|
|
102
|
+
registeredMethodsByRegistry: Map<string, Set<GatewayMethodName>>;
|
|
103
|
+
serviceRegistered?: boolean;
|
|
104
|
+
channelRegistered?: boolean;
|
|
105
|
+
serviceOwnerApiInstanceId?: string;
|
|
106
|
+
channelOwnerApiInstanceId?: string;
|
|
107
|
+
};
|
|
108
|
+
|
|
38
109
|
let runtime: LoadedRuntime | null = null;
|
|
110
|
+
let activeServiceStop: (() => Promise<void>) | null = null;
|
|
39
111
|
const identityIds = new WeakMap<object, string>();
|
|
40
112
|
let identitySeq = 0;
|
|
41
113
|
|
|
@@ -218,7 +290,7 @@ const loadRuntimeSync = (): LoadedRuntime => {
|
|
|
218
290
|
const getIdentityId = (obj: object, prefix: string) => {
|
|
219
291
|
const existing = identityIds.get(obj);
|
|
220
292
|
if (existing) return existing;
|
|
221
|
-
const next = `${prefix}_${++identitySeq}`;
|
|
293
|
+
const next = `${prefix}_${MODULE_EPOCH}_${++identitySeq}`;
|
|
222
294
|
identityIds.set(obj, next);
|
|
223
295
|
return next;
|
|
224
296
|
};
|
|
@@ -247,38 +319,313 @@ const getRegisterMeta = (api: OpenClawPluginApi): RegisterMeta => {
|
|
|
247
319
|
return host[BNCR_REGISTER_META]!;
|
|
248
320
|
};
|
|
249
321
|
|
|
322
|
+
const getProcessStore = () => {
|
|
323
|
+
const p = process as NodeJS.Process & {
|
|
324
|
+
[BNCR_GLOBAL_REGISTER_TRACE]?: GlobalRegisterTrace;
|
|
325
|
+
[BNCR_GATEWAY_RUNTIME]?: BncrGatewayRuntime;
|
|
326
|
+
};
|
|
327
|
+
return p;
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const getGlobalRegisterTrace = () => {
|
|
331
|
+
const p = getProcessStore();
|
|
332
|
+
if (!p[BNCR_GLOBAL_REGISTER_TRACE]) {
|
|
333
|
+
p[BNCR_GLOBAL_REGISTER_TRACE] = {
|
|
334
|
+
seenRegistryFingerprints: new Set<string>(),
|
|
335
|
+
seenApiInstanceIds: new Set<string>(),
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
return p[BNCR_GLOBAL_REGISTER_TRACE]!;
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const getGatewayRuntime = (): BncrGatewayRuntime => {
|
|
342
|
+
const p = getProcessStore();
|
|
343
|
+
if (!p[BNCR_GATEWAY_RUNTIME]) {
|
|
344
|
+
p[BNCR_GATEWAY_RUNTIME] = {
|
|
345
|
+
registeredMethodsByRegistry: new Map<string, Set<GatewayMethodName>>(),
|
|
346
|
+
serviceRegistered: false,
|
|
347
|
+
channelRegistered: false,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
return p[BNCR_GATEWAY_RUNTIME]!;
|
|
351
|
+
};
|
|
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
|
+
|
|
389
|
+
const gatewayMethodDispatchers: Record<
|
|
390
|
+
GatewayMethodName,
|
|
391
|
+
(bridge: BridgeSingletonWithOwner, opts: any) => any
|
|
392
|
+
> = {
|
|
393
|
+
'bncr.connect': (bridge, opts) => bridge.handleConnect(opts),
|
|
394
|
+
'bncr.inbound': (bridge, opts) => bridge.handleInbound(opts),
|
|
395
|
+
'bncr.activity': (bridge, opts) => bridge.handleActivity(opts),
|
|
396
|
+
'bncr.ack': (bridge, opts) => bridge.handleAck(opts),
|
|
397
|
+
'bncr.diagnostics': (bridge, opts) => bridge.handleDiagnostics(opts),
|
|
398
|
+
'bncr.file.init': (bridge, opts) => bridge.handleFileInit(opts),
|
|
399
|
+
'bncr.file.chunk': (bridge, opts) => bridge.handleFileChunk(opts),
|
|
400
|
+
'bncr.file.complete': (bridge, opts) => bridge.handleFileComplete(opts),
|
|
401
|
+
'bncr.file.abort': (bridge, opts) => bridge.handleFileAbort(opts),
|
|
402
|
+
'bncr.file.ack': (bridge, opts) => bridge.handleFileAck(opts),
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const dispatchGatewayMethod = (name: GatewayMethodName, opts: any) => {
|
|
406
|
+
const gatewayRuntime = getGatewayRuntime();
|
|
407
|
+
const bridge = gatewayRuntime.currentBridge;
|
|
408
|
+
if (!bridge) {
|
|
409
|
+
throw new Error(`bncr gateway runtime unavailable for ${name}`);
|
|
410
|
+
}
|
|
411
|
+
return gatewayMethodDispatchers[name](bridge, opts);
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
const mirrorGatewayMethodForMockApi = (api: OpenClawPluginApi, name: GatewayMethodName) => {
|
|
415
|
+
const host = api as OpenClawPluginApi & {
|
|
416
|
+
methods?: Array<{ name: string; handler: (opts: any) => any }>;
|
|
417
|
+
};
|
|
418
|
+
if (!Array.isArray(host.methods)) return;
|
|
419
|
+
if (host.methods.some((item) => item?.name === name)) return;
|
|
420
|
+
host.methods.push({ name, handler: (opts) => dispatchGatewayMethod(name, opts) });
|
|
421
|
+
};
|
|
422
|
+
|
|
250
423
|
const ensureGatewayMethodRegistered = (
|
|
251
424
|
api: OpenClawPluginApi,
|
|
252
|
-
name:
|
|
253
|
-
handler: (opts: any) => any,
|
|
425
|
+
name: GatewayMethodName,
|
|
254
426
|
debugLog: (...args: any[]) => void,
|
|
255
427
|
) => {
|
|
256
428
|
const meta = getRegisterMeta(api);
|
|
429
|
+
const gatewayRuntime = getGatewayRuntime();
|
|
430
|
+
const registryFingerprint = meta.registryFingerprint || getRegistryFingerprint(api);
|
|
431
|
+
let registryMethods = gatewayRuntime.registeredMethodsByRegistry.get(registryFingerprint);
|
|
432
|
+
if (!registryMethods) {
|
|
433
|
+
registryMethods = new Set<GatewayMethodName>();
|
|
434
|
+
gatewayRuntime.registeredMethodsByRegistry.set(registryFingerprint, registryMethods);
|
|
435
|
+
}
|
|
257
436
|
if (meta.methods?.has(name)) {
|
|
258
437
|
debugLog(`register method skip ${name} (already registered on this api)`);
|
|
259
438
|
return;
|
|
260
439
|
}
|
|
261
|
-
|
|
440
|
+
if (registryMethods.has(name)) {
|
|
441
|
+
mirrorGatewayMethodForMockApi(api, name);
|
|
442
|
+
meta.methods?.add(name);
|
|
443
|
+
debugLog(`register method reuse ${name} (already registered in registry)`);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
api.registerGatewayMethod(name, (opts) => dispatchGatewayMethod(name, opts));
|
|
447
|
+
mirrorGatewayMethodForMockApi(api, name);
|
|
448
|
+
registryMethods.add(name);
|
|
262
449
|
meta.methods?.add(name);
|
|
263
450
|
debugLog(`register method ok ${name}`);
|
|
264
451
|
};
|
|
265
452
|
|
|
453
|
+
const getBridgeOwner = (api: OpenClawPluginApi, loaded: LoadedRuntime): BridgeOwner => {
|
|
454
|
+
const meta = getRegisterMeta(api);
|
|
455
|
+
return {
|
|
456
|
+
moduleEpoch: MODULE_EPOCH,
|
|
457
|
+
bridgeFactoryId: getIdentityId(loaded.createBncrBridge as object, 'bridgeFactory'),
|
|
458
|
+
apiInstanceId: meta.apiInstanceId || 'unknown',
|
|
459
|
+
registryFingerprint: meta.registryFingerprint || 'unknown',
|
|
460
|
+
registrationMode: meta.registrationMode,
|
|
461
|
+
};
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
const sameBridgeOwner = (left?: BridgeOwner, right?: BridgeOwner) => {
|
|
465
|
+
if (!left || !right) return false;
|
|
466
|
+
return (
|
|
467
|
+
left.moduleEpoch === right.moduleEpoch &&
|
|
468
|
+
left.bridgeFactoryId === right.bridgeFactoryId &&
|
|
469
|
+
left.apiInstanceId === right.apiInstanceId &&
|
|
470
|
+
left.registryFingerprint === right.registryFingerprint
|
|
471
|
+
);
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
const snapshotBridgeRegisterState = (
|
|
475
|
+
bridge?: BridgeSingletonWithOwner,
|
|
476
|
+
): BridgeRegisterStateSnapshot | null => {
|
|
477
|
+
if (!bridge) return null;
|
|
478
|
+
return {
|
|
479
|
+
registerCount: Number(bridge.registerCount || 0),
|
|
480
|
+
apiGeneration: Number(bridge.apiGeneration || 0),
|
|
481
|
+
firstRegisterAt:
|
|
482
|
+
typeof bridge.firstRegisterAt === 'number'
|
|
483
|
+
? bridge.firstRegisterAt
|
|
484
|
+
: (bridge.firstRegisterAt ?? null),
|
|
485
|
+
lastRegisterAt:
|
|
486
|
+
typeof bridge.lastRegisterAt === 'number'
|
|
487
|
+
? bridge.lastRegisterAt
|
|
488
|
+
: (bridge.lastRegisterAt ?? null),
|
|
489
|
+
lastApiRebindAt:
|
|
490
|
+
typeof bridge.lastApiRebindAt === 'number'
|
|
491
|
+
? bridge.lastApiRebindAt
|
|
492
|
+
: (bridge.lastApiRebindAt ?? null),
|
|
493
|
+
pluginSource: typeof bridge.pluginSource === 'string' ? bridge.pluginSource : null,
|
|
494
|
+
pluginVersion: typeof bridge.pluginVersion === 'string' ? bridge.pluginVersion : null,
|
|
495
|
+
lastApiInstanceId:
|
|
496
|
+
typeof bridge.lastApiInstanceId === 'string' ? bridge.lastApiInstanceId : null,
|
|
497
|
+
lastRegistryFingerprint:
|
|
498
|
+
typeof bridge.lastRegistryFingerprint === 'string' ? bridge.lastRegistryFingerprint : null,
|
|
499
|
+
lastDriftSnapshot: bridge.lastDriftSnapshot ?? null,
|
|
500
|
+
registerTraceRecent: Array.isArray(bridge.registerTraceRecent)
|
|
501
|
+
? bridge.registerTraceRecent.map((trace) => ({ ...trace }))
|
|
502
|
+
: [],
|
|
503
|
+
};
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
const hydrateBridgeRegisterState = (
|
|
507
|
+
bridge: BridgeSingletonWithOwner,
|
|
508
|
+
snapshot: BridgeRegisterStateSnapshot | null,
|
|
509
|
+
) => {
|
|
510
|
+
if (!snapshot) return bridge;
|
|
511
|
+
bridge.registerCount = snapshot.registerCount;
|
|
512
|
+
bridge.apiGeneration = snapshot.apiGeneration;
|
|
513
|
+
bridge.firstRegisterAt = snapshot.firstRegisterAt;
|
|
514
|
+
bridge.lastRegisterAt = snapshot.lastRegisterAt;
|
|
515
|
+
bridge.lastApiRebindAt = snapshot.lastApiRebindAt;
|
|
516
|
+
bridge.pluginSource = snapshot.pluginSource;
|
|
517
|
+
bridge.pluginVersion = snapshot.pluginVersion;
|
|
518
|
+
bridge.lastApiInstanceId = snapshot.lastApiInstanceId;
|
|
519
|
+
bridge.lastRegistryFingerprint = snapshot.lastRegistryFingerprint;
|
|
520
|
+
bridge.lastDriftSnapshot = snapshot.lastDriftSnapshot;
|
|
521
|
+
bridge.registerTraceRecent = snapshot.registerTraceRecent.map((trace) => ({ ...trace }));
|
|
522
|
+
return bridge;
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
const assignBridgeOwner = (bridge: BridgeSingleton, owner: BridgeOwner) => {
|
|
526
|
+
(bridge as BridgeSingletonWithOwner)[BNCR_BRIDGE_OWNER] = owner;
|
|
527
|
+
return bridge as BridgeSingletonWithOwner;
|
|
528
|
+
};
|
|
529
|
+
|
|
266
530
|
const getBridgeSingleton = (api: OpenClawPluginApi) => {
|
|
267
531
|
const loaded = loadRuntimeSync();
|
|
268
|
-
const g = globalThis as typeof globalThis & { __bncrBridge?:
|
|
532
|
+
const g = globalThis as typeof globalThis & { __bncrBridge?: BridgeSingletonWithOwner };
|
|
533
|
+
const owner = getBridgeOwner(api, loaded);
|
|
534
|
+
const previousOwner = g.__bncrBridge?.[BNCR_BRIDGE_OWNER];
|
|
535
|
+
|
|
269
536
|
let created = false;
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
537
|
+
let rebuilt = false;
|
|
538
|
+
|
|
539
|
+
if (g.__bncrBridge) {
|
|
540
|
+
const mustRebuild =
|
|
541
|
+
!sameBridgeOwner(previousOwner, owner) &&
|
|
542
|
+
(previousOwner?.moduleEpoch !== owner.moduleEpoch ||
|
|
543
|
+
previousOwner?.bridgeFactoryId !== owner.bridgeFactoryId ||
|
|
544
|
+
previousOwner?.registrationMode !== owner.registrationMode ||
|
|
545
|
+
previousOwner?.apiInstanceId !== owner.apiInstanceId ||
|
|
546
|
+
previousOwner?.registryFingerprint !== owner.registryFingerprint);
|
|
547
|
+
|
|
548
|
+
if (mustRebuild) {
|
|
549
|
+
const registerState = snapshotBridgeRegisterState(g.__bncrBridge);
|
|
550
|
+
try {
|
|
551
|
+
g.__bncrBridge.stopService?.();
|
|
552
|
+
} catch {
|
|
553
|
+
// ignore stop errors during hot-restart recovery
|
|
554
|
+
}
|
|
555
|
+
g.__bncrBridge = hydrateBridgeRegisterState(
|
|
556
|
+
assignBridgeOwner(loaded.createBncrBridge(api), owner),
|
|
557
|
+
registerState,
|
|
558
|
+
);
|
|
559
|
+
created = true;
|
|
560
|
+
rebuilt = true;
|
|
561
|
+
} else {
|
|
562
|
+
g.__bncrBridge.bindApi?.(api);
|
|
563
|
+
assignBridgeOwner(g.__bncrBridge, owner);
|
|
564
|
+
created = false;
|
|
565
|
+
rebuilt = false;
|
|
566
|
+
}
|
|
273
567
|
} else {
|
|
274
|
-
g.__bncrBridge.
|
|
568
|
+
g.__bncrBridge = assignBridgeOwner(loaded.createBncrBridge(api), owner);
|
|
569
|
+
created = true;
|
|
275
570
|
}
|
|
276
|
-
|
|
571
|
+
|
|
572
|
+
return { bridge: g.__bncrBridge, runtime: loaded, created, rebuilt, owner, previousOwner };
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
const getExistingBridgeSingleton = (): BridgeSingletonWithOwner | undefined => {
|
|
576
|
+
const g = globalThis as typeof globalThis & { __bncrBridge?: BridgeSingletonWithOwner };
|
|
577
|
+
return g.__bncrBridge;
|
|
277
578
|
};
|
|
278
579
|
|
|
279
580
|
const isPlainObject = (value: unknown): value is Record<string, unknown> =>
|
|
280
581
|
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
281
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
|
+
|
|
282
629
|
const registerBncrCli = (api: OpenClawPluginApi & { registerCli?: (...args: any[]) => void }) => {
|
|
283
630
|
if (typeof api.registerCli !== 'function') return;
|
|
284
631
|
api.registerCli(
|
|
@@ -290,10 +637,11 @@ const registerBncrCli = (api: OpenClawPluginApi & { registerCli?: (...args: any[
|
|
|
290
637
|
'Seed minimal channels.bncr config (adds enabled=true and allowTool=false only when missing)',
|
|
291
638
|
)
|
|
292
639
|
.action(async () => {
|
|
293
|
-
const cfg =
|
|
294
|
-
|
|
640
|
+
const cfg = api.runtime.config.current() as Record<string, unknown>;
|
|
641
|
+
const next = structuredClone(cfg);
|
|
642
|
+
if (!isPlainObject(next.channels)) next.channels = {};
|
|
295
643
|
|
|
296
|
-
const existing = isPlainObject(
|
|
644
|
+
const existing = isPlainObject(next.channels.bncr) ? next.channels.bncr : {};
|
|
297
645
|
const bncrCfg: Record<string, unknown> = { ...existing };
|
|
298
646
|
const added: string[] = [];
|
|
299
647
|
|
|
@@ -307,14 +655,14 @@ const registerBncrCli = (api: OpenClawPluginApi & { registerCli?: (...args: any[
|
|
|
307
655
|
added.push('allowTool=false');
|
|
308
656
|
}
|
|
309
657
|
|
|
310
|
-
|
|
658
|
+
next.channels.bncr = bncrCfg;
|
|
311
659
|
|
|
312
660
|
if (added.length === 0) {
|
|
313
661
|
console.log('Minimal bncr config already present. No changes made.');
|
|
314
662
|
return;
|
|
315
663
|
}
|
|
316
664
|
|
|
317
|
-
await api.runtime.config.writeConfigFile(
|
|
665
|
+
await api.runtime.config.writeConfigFile(next);
|
|
318
666
|
console.log('Seeded minimal bncr config at channels.bncr.');
|
|
319
667
|
console.log(`Added missing fields: ${added.join(', ')}`);
|
|
320
668
|
console.log('Restart the gateway to apply changes.');
|
|
@@ -324,6 +672,9 @@ const registerBncrCli = (api: OpenClawPluginApi & { registerCli?: (...args: any[
|
|
|
324
672
|
);
|
|
325
673
|
};
|
|
326
674
|
|
|
675
|
+
const shouldSkipNonRuntimeRegister = (mode?: string) =>
|
|
676
|
+
mode === 'cli-metadata' || mode === 'discovery';
|
|
677
|
+
|
|
327
678
|
const plugin = {
|
|
328
679
|
id: 'bncr',
|
|
329
680
|
name: 'Bncr',
|
|
@@ -333,14 +684,61 @@ const plugin = {
|
|
|
333
684
|
api: OpenClawPluginApi & { registerCli?: (...args: any[]) => void; registrationMode?: string },
|
|
334
685
|
) {
|
|
335
686
|
registerBncrCli(api);
|
|
336
|
-
if (api.registrationMode
|
|
687
|
+
if (shouldSkipNonRuntimeRegister(api.registrationMode)) return;
|
|
688
|
+
|
|
689
|
+
// 注意:OpenClaw 要求 plugin register 必须是同步函数;
|
|
690
|
+
// 不要在这里 await 停旧 service / 清理旧 runtime,否则 loader 会直接拒绝加载。
|
|
691
|
+
// 旧实例清理由 service stop / runtime 自愈逻辑兜底,这里只做同步声明式注册。
|
|
337
692
|
|
|
338
693
|
const meta = getRegisterMeta(api);
|
|
339
|
-
|
|
340
|
-
|
|
694
|
+
meta.registrationMode = api.registrationMode;
|
|
695
|
+
const globalTrace = getGlobalRegisterTrace();
|
|
696
|
+
const previousApiInstanceId = globalTrace.lastApiInstanceId;
|
|
697
|
+
const previousRegistryFingerprint = globalTrace.lastRegistryFingerprint;
|
|
698
|
+
const apiInstanceId = meta.apiInstanceId || 'unknown';
|
|
699
|
+
const registryFingerprint = meta.registryFingerprint || 'unknown';
|
|
700
|
+
const sameApiAsPrevious = previousApiInstanceId === apiInstanceId;
|
|
701
|
+
const sameRegistryAsPrevious = previousRegistryFingerprint === registryFingerprint;
|
|
702
|
+
const firstSeenApi = !globalTrace.seenApiInstanceIds.has(apiInstanceId);
|
|
703
|
+
const firstSeenRegistry = !globalTrace.seenRegistryFingerprints.has(registryFingerprint);
|
|
704
|
+
|
|
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
|
+
}
|
|
733
|
+
|
|
734
|
+
globalTrace.seenApiInstanceIds.add(apiInstanceId);
|
|
735
|
+
globalTrace.seenRegistryFingerprints.add(registryFingerprint);
|
|
736
|
+
globalTrace.lastApiInstanceId = apiInstanceId;
|
|
737
|
+
globalTrace.lastRegistryFingerprint = registryFingerprint;
|
|
738
|
+
bridge?.noteRegister?.({
|
|
341
739
|
source: '~/.openclaw/workspace/plugins/bncr/index.ts',
|
|
342
740
|
pluginVersion,
|
|
343
|
-
apiRebound: !created,
|
|
741
|
+
apiRebound: ownerDecision.adoptOwner ? !created && !rebuilt : false,
|
|
344
742
|
apiInstanceId: meta.apiInstanceId,
|
|
345
743
|
registryFingerprint: meta.registryFingerprint,
|
|
346
744
|
});
|
|
@@ -351,100 +749,89 @@ const plugin = {
|
|
|
351
749
|
.trim();
|
|
352
750
|
if (!rendered) return;
|
|
353
751
|
emitBncrLogLine('info', `[bncr] debug ${rendered}`, { debugOnly: true }, () =>
|
|
354
|
-
Boolean(bridge
|
|
752
|
+
Boolean(bridge?.isDebugEnabled?.()),
|
|
355
753
|
);
|
|
356
754
|
};
|
|
357
755
|
|
|
358
|
-
debugLog(
|
|
359
|
-
|
|
756
|
+
debugLog(
|
|
757
|
+
`register begin bridge=${bridge?.getBridgeId?.() || 'unknown'} created=${created} rebuilt=${rebuilt} ` +
|
|
758
|
+
`ownerApi=${owner?.apiInstanceId || 'none'} ownerRegistry=${owner?.registryFingerprint || 'none'} ` +
|
|
759
|
+
`previousOwnerApi=${previousOwner?.apiInstanceId || 'none'} previousOwnerRegistry=${previousOwner?.registryFingerprint || 'none'}`,
|
|
760
|
+
);
|
|
761
|
+
debugLog(
|
|
762
|
+
`register classify mode=${meta.registrationMode || 'unknown'} api=${apiInstanceId} registry=${registryFingerprint} ` +
|
|
763
|
+
`sameApiAsPrevious=${sameApiAsPrevious} sameRegistryAsPrevious=${sameRegistryAsPrevious} ` +
|
|
764
|
+
`firstSeenApi=${firstSeenApi} firstSeenRegistry=${firstSeenRegistry}`,
|
|
765
|
+
);
|
|
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
|
+
}
|
|
360
778
|
|
|
361
779
|
const resolveDebug = async () => {
|
|
362
780
|
try {
|
|
363
|
-
const cfg =
|
|
781
|
+
const cfg = api.runtime.config.current();
|
|
364
782
|
return Boolean((cfg as any)?.channels?.bncr?.debug?.verbose);
|
|
365
783
|
} catch {
|
|
366
784
|
return false;
|
|
367
785
|
}
|
|
368
786
|
};
|
|
369
787
|
|
|
370
|
-
if (!
|
|
788
|
+
if (!gatewayRuntime.serviceRegistered) {
|
|
789
|
+
const serviceStopHandler = async () => {
|
|
790
|
+
await getCurrentBridge().stopService?.();
|
|
791
|
+
};
|
|
371
792
|
api.registerService({
|
|
372
793
|
id: 'bncr-bridge-service',
|
|
373
794
|
start: async (ctx) => {
|
|
374
795
|
const debug = await resolveDebug();
|
|
375
|
-
await
|
|
796
|
+
await getCurrentBridge().startService(ctx, debug);
|
|
376
797
|
},
|
|
377
|
-
stop:
|
|
798
|
+
stop: serviceStopHandler,
|
|
378
799
|
});
|
|
800
|
+
activeServiceStop = serviceStopHandler;
|
|
801
|
+
gatewayRuntime.serviceRegistered = true;
|
|
802
|
+
gatewayRuntime.serviceOwnerApiInstanceId = apiInstanceId;
|
|
379
803
|
meta.service = true;
|
|
380
|
-
debugLog(
|
|
804
|
+
debugLog(`register service ok ownerApi=${apiInstanceId}`);
|
|
381
805
|
} else {
|
|
382
|
-
|
|
806
|
+
meta.service = true;
|
|
807
|
+
debugLog(
|
|
808
|
+
`register service skip (process singleton already registered by api ${gatewayRuntime.serviceOwnerApiInstanceId || 'unknown'})`,
|
|
809
|
+
);
|
|
383
810
|
}
|
|
384
811
|
|
|
385
|
-
if (!
|
|
386
|
-
api.registerChannel({ plugin: runtime
|
|
812
|
+
if (!gatewayRuntime.channelRegistered) {
|
|
813
|
+
api.registerChannel({ plugin: createDynamicChannelPlugin(runtime) });
|
|
814
|
+
gatewayRuntime.channelRegistered = true;
|
|
815
|
+
gatewayRuntime.channelOwnerApiInstanceId = apiInstanceId;
|
|
387
816
|
meta.channel = true;
|
|
388
|
-
debugLog(
|
|
817
|
+
debugLog(`register channel ok ownerApi=${apiInstanceId}`);
|
|
389
818
|
} else {
|
|
390
|
-
|
|
819
|
+
meta.channel = true;
|
|
820
|
+
debugLog(
|
|
821
|
+
`register channel skip (process singleton already registered by api ${gatewayRuntime.channelOwnerApiInstanceId || 'unknown'})`,
|
|
822
|
+
);
|
|
391
823
|
}
|
|
392
824
|
|
|
393
|
-
ensureGatewayMethodRegistered(
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
);
|
|
399
|
-
ensureGatewayMethodRegistered(
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
debugLog,
|
|
404
|
-
);
|
|
405
|
-
ensureGatewayMethodRegistered(
|
|
406
|
-
api,
|
|
407
|
-
'bncr.activity',
|
|
408
|
-
(opts) => bridge.handleActivity(opts),
|
|
409
|
-
debugLog,
|
|
410
|
-
);
|
|
411
|
-
ensureGatewayMethodRegistered(api, 'bncr.ack', (opts) => bridge.handleAck(opts), debugLog);
|
|
412
|
-
ensureGatewayMethodRegistered(
|
|
413
|
-
api,
|
|
414
|
-
'bncr.diagnostics',
|
|
415
|
-
(opts) => bridge.handleDiagnostics(opts),
|
|
416
|
-
debugLog,
|
|
417
|
-
);
|
|
418
|
-
ensureGatewayMethodRegistered(
|
|
419
|
-
api,
|
|
420
|
-
'bncr.file.init',
|
|
421
|
-
(opts) => bridge.handleFileInit(opts),
|
|
422
|
-
debugLog,
|
|
423
|
-
);
|
|
424
|
-
ensureGatewayMethodRegistered(
|
|
425
|
-
api,
|
|
426
|
-
'bncr.file.chunk',
|
|
427
|
-
(opts) => bridge.handleFileChunk(opts),
|
|
428
|
-
debugLog,
|
|
429
|
-
);
|
|
430
|
-
ensureGatewayMethodRegistered(
|
|
431
|
-
api,
|
|
432
|
-
'bncr.file.complete',
|
|
433
|
-
(opts) => bridge.handleFileComplete(opts),
|
|
434
|
-
debugLog,
|
|
435
|
-
);
|
|
436
|
-
ensureGatewayMethodRegistered(
|
|
437
|
-
api,
|
|
438
|
-
'bncr.file.abort',
|
|
439
|
-
(opts) => bridge.handleFileAbort(opts),
|
|
440
|
-
debugLog,
|
|
441
|
-
);
|
|
442
|
-
ensureGatewayMethodRegistered(
|
|
443
|
-
api,
|
|
444
|
-
'bncr.file.ack',
|
|
445
|
-
(opts) => bridge.handleFileAck(opts),
|
|
446
|
-
debugLog,
|
|
447
|
-
);
|
|
825
|
+
ensureGatewayMethodRegistered(api, 'bncr.connect', debugLog);
|
|
826
|
+
ensureGatewayMethodRegistered(api, 'bncr.inbound', debugLog);
|
|
827
|
+
ensureGatewayMethodRegistered(api, 'bncr.activity', debugLog);
|
|
828
|
+
ensureGatewayMethodRegistered(api, 'bncr.ack', debugLog);
|
|
829
|
+
ensureGatewayMethodRegistered(api, 'bncr.diagnostics', debugLog);
|
|
830
|
+
ensureGatewayMethodRegistered(api, 'bncr.file.init', debugLog);
|
|
831
|
+
ensureGatewayMethodRegistered(api, 'bncr.file.chunk', debugLog);
|
|
832
|
+
ensureGatewayMethodRegistered(api, 'bncr.file.complete', debugLog);
|
|
833
|
+
ensureGatewayMethodRegistered(api, 'bncr.file.abort', debugLog);
|
|
834
|
+
ensureGatewayMethodRegistered(api, 'bncr.file.ack', debugLog);
|
|
448
835
|
debugLog('register done');
|
|
449
836
|
},
|
|
450
837
|
};
|