@xmoxmo/bncr 0.1.1 → 0.1.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
@@ -187,6 +187,18 @@ npm pack
187
187
  - `npm test`:跑回归测试
188
188
  - `npm run selfcheck`:检查插件骨架是否完整
189
189
  - `npm pack`:确认当前版本可正常打包
190
+ - `npm run check-register-drift -- --duration-sec 300 --interval-sec 15`:静置采样 `bncr.diagnostics`,观察 `registerCount / apiGeneration / postWarmupRegisterCount` 是否在 warmup 后继续增长
191
+
192
+ 示例输出重点:
193
+
194
+ - `delta.registerCount`
195
+ - `delta.apiGeneration`
196
+ - `delta.postWarmupRegisterCount`
197
+ - `historicalWarmupExternalDrift`
198
+ - `newDriftDuringWindow`
199
+ - `last.postWarmupRegisterCount`
200
+ - `last.unexpectedRegisterAfterWarmup`
201
+ - `driftDetected`
190
202
 
191
203
  ---
192
204
 
package/index.ts CHANGED
@@ -1,7 +1,7 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
1
  import { execFileSync } from 'node:child_process';
2
+ import fs from 'node:fs';
4
3
  import { createRequire } from 'node:module';
4
+ import path from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { BncrConfigSchema } from './src/core/config-schema.ts';
7
7
 
@@ -20,7 +20,23 @@ type LoadedRuntime = {
20
20
  createBncrChannelPlugin: ChannelModule['createBncrChannelPlugin'];
21
21
  };
22
22
 
23
+ const BNCR_REGISTER_META = Symbol.for('bncr.register.meta');
24
+
25
+ type RegisterMeta = {
26
+ service?: boolean;
27
+ channel?: boolean;
28
+ methods?: Set<string>;
29
+ apiInstanceId?: string;
30
+ registryFingerprint?: string;
31
+ };
32
+
33
+ type OpenClawPluginApiWithMeta = OpenClawPluginApi & {
34
+ [BNCR_REGISTER_META]?: RegisterMeta;
35
+ };
36
+
23
37
  let runtime: LoadedRuntime | null = null;
38
+ const identityIds = new WeakMap<object, string>();
39
+ let identitySeq = 0;
24
40
 
25
41
  const tryExec = (command: string, args: string[]) => {
26
42
  try {
@@ -105,9 +121,7 @@ const collectOpenClawCandidates = () => {
105
121
  }
106
122
 
107
123
  const packageRoots = unique(
108
- directCandidates
109
- .map((candidate) => findOpenClawPackageRoot(candidate))
110
- .filter(Boolean),
124
+ directCandidates.map((candidate) => findOpenClawPackageRoot(candidate)).filter(Boolean),
111
125
  );
112
126
 
113
127
  return packageRoots.filter((candidate) => {
@@ -188,11 +202,65 @@ const loadRuntimeSync = (): LoadedRuntime => {
188
202
  }
189
203
  };
190
204
 
205
+ const getIdentityId = (obj: object, prefix: string) => {
206
+ const existing = identityIds.get(obj);
207
+ if (existing) return existing;
208
+ const next = `${prefix}_${++identitySeq}`;
209
+ identityIds.set(obj, next);
210
+ return next;
211
+ };
212
+
213
+ const getRegistryFingerprint = (api: OpenClawPluginApi) => {
214
+ const serviceId = getIdentityId(api.registerService as object, 'svc');
215
+ const channelId = getIdentityId(api.registerChannel as object, 'chn');
216
+ const methodId = getIdentityId(api.registerGatewayMethod as object, 'mth');
217
+ return `${serviceId}:${channelId}:${methodId}`;
218
+ };
219
+
220
+ const getRegisterMeta = (api: OpenClawPluginApi): RegisterMeta => {
221
+ const host = api as OpenClawPluginApiWithMeta;
222
+ if (!host[BNCR_REGISTER_META]) {
223
+ host[BNCR_REGISTER_META] = { methods: new Set<string>() };
224
+ }
225
+ if (!host[BNCR_REGISTER_META]!.methods) {
226
+ host[BNCR_REGISTER_META]!.methods = new Set<string>();
227
+ }
228
+ if (!host[BNCR_REGISTER_META]!.apiInstanceId) {
229
+ host[BNCR_REGISTER_META]!.apiInstanceId = getIdentityId(api as object, 'api');
230
+ }
231
+ if (!host[BNCR_REGISTER_META]!.registryFingerprint) {
232
+ host[BNCR_REGISTER_META]!.registryFingerprint = getRegistryFingerprint(api);
233
+ }
234
+ return host[BNCR_REGISTER_META]!;
235
+ };
236
+
237
+ const ensureGatewayMethodRegistered = (
238
+ api: OpenClawPluginApi,
239
+ name: string,
240
+ handler: (opts: any) => any,
241
+ debugLog: (...args: any[]) => void,
242
+ ) => {
243
+ const meta = getRegisterMeta(api);
244
+ if (meta.methods?.has(name)) {
245
+ debugLog(`bncr register method skip ${name} (already registered on this api)`);
246
+ return;
247
+ }
248
+ api.registerGatewayMethod(name, handler);
249
+ meta.methods?.add(name);
250
+ debugLog(`bncr register method ok ${name}`);
251
+ };
252
+
191
253
  const getBridgeSingleton = (api: OpenClawPluginApi) => {
192
254
  const loaded = loadRuntimeSync();
193
255
  const g = globalThis as typeof globalThis & { __bncrBridge?: BridgeSingleton };
194
- if (!g.__bncrBridge) g.__bncrBridge = loaded.createBncrBridge(api);
195
- return { bridge: g.__bncrBridge, runtime: loaded };
256
+ let created = false;
257
+ if (!g.__bncrBridge) {
258
+ g.__bncrBridge = loaded.createBncrBridge(api);
259
+ created = true;
260
+ } else {
261
+ g.__bncrBridge.bindApi?.(api);
262
+ }
263
+ return { bridge: g.__bncrBridge, runtime: loaded, created };
196
264
  };
197
265
 
198
266
  const plugin = {
@@ -201,13 +269,24 @@ const plugin = {
201
269
  description: 'Bncr channel plugin',
202
270
  configSchema: BncrConfigSchema,
203
271
  register(api: OpenClawPluginApi) {
204
- const { bridge, runtime } = getBridgeSingleton(api);
272
+ const meta = getRegisterMeta(api);
273
+ const { bridge, runtime, created } = getBridgeSingleton(api);
274
+ bridge.noteRegister?.({
275
+ source: '~/.openclaw/workspace/plugins/bncr/index.ts',
276
+ pluginVersion: '0.1.1',
277
+ apiRebound: !created,
278
+ apiInstanceId: meta.apiInstanceId,
279
+ registryFingerprint: meta.registryFingerprint,
280
+ });
205
281
  const debugLog = (...args: any[]) => {
206
282
  if (!bridge.isDebugEnabled?.()) return;
207
283
  api.logger.info?.(...args);
208
284
  };
209
285
 
210
- debugLog(`bncr plugin register bridge=${(bridge as any)?.bridgeId || 'unknown'}`);
286
+ debugLog(
287
+ `bncr plugin register begin bridge=${bridge.getBridgeId?.() || 'unknown'} created=${created}`,
288
+ );
289
+ if (!created) debugLog('bncr bridge api rebound');
211
290
 
212
291
  const resolveDebug = async () => {
213
292
  try {
@@ -218,27 +297,85 @@ const plugin = {
218
297
  }
219
298
  };
220
299
 
221
- api.registerService({
222
- id: 'bncr-bridge-service',
223
- start: async (ctx) => {
224
- const debug = await resolveDebug();
225
- await bridge.startService(ctx, debug);
226
- },
227
- stop: bridge.stopService,
228
- });
300
+ if (!meta.service) {
301
+ api.registerService({
302
+ id: 'bncr-bridge-service',
303
+ start: async (ctx) => {
304
+ const debug = await resolveDebug();
305
+ await bridge.startService(ctx, debug);
306
+ },
307
+ stop: bridge.stopService,
308
+ });
309
+ meta.service = true;
310
+ debugLog('bncr register service ok');
311
+ } else {
312
+ debugLog('bncr register service skip (already registered on this api)');
313
+ }
314
+
315
+ if (!meta.channel) {
316
+ api.registerChannel({ plugin: runtime.createBncrChannelPlugin(bridge) });
317
+ meta.channel = true;
318
+ debugLog('bncr register channel ok');
319
+ } else {
320
+ debugLog('bncr register channel skip (already registered on this api)');
321
+ }
229
322
 
230
- api.registerChannel({ plugin: runtime.createBncrChannelPlugin(bridge) });
231
-
232
- api.registerGatewayMethod('bncr.connect', (opts) => bridge.handleConnect(opts));
233
- api.registerGatewayMethod('bncr.inbound', (opts) => bridge.handleInbound(opts));
234
- api.registerGatewayMethod('bncr.activity', (opts) => bridge.handleActivity(opts));
235
- api.registerGatewayMethod('bncr.ack', (opts) => bridge.handleAck(opts));
236
- api.registerGatewayMethod('bncr.diagnostics', (opts) => bridge.handleDiagnostics(opts));
237
- api.registerGatewayMethod('bncr.file.init', (opts) => bridge.handleFileInit(opts));
238
- api.registerGatewayMethod('bncr.file.chunk', (opts) => bridge.handleFileChunk(opts));
239
- api.registerGatewayMethod('bncr.file.complete', (opts) => bridge.handleFileComplete(opts));
240
- api.registerGatewayMethod('bncr.file.abort', (opts) => bridge.handleFileAbort(opts));
241
- api.registerGatewayMethod('bncr.file.ack', (opts) => bridge.handleFileAck(opts));
323
+ ensureGatewayMethodRegistered(
324
+ api,
325
+ 'bncr.connect',
326
+ (opts) => bridge.handleConnect(opts),
327
+ debugLog,
328
+ );
329
+ ensureGatewayMethodRegistered(
330
+ api,
331
+ 'bncr.inbound',
332
+ (opts) => bridge.handleInbound(opts),
333
+ debugLog,
334
+ );
335
+ ensureGatewayMethodRegistered(
336
+ api,
337
+ 'bncr.activity',
338
+ (opts) => bridge.handleActivity(opts),
339
+ debugLog,
340
+ );
341
+ ensureGatewayMethodRegistered(api, 'bncr.ack', (opts) => bridge.handleAck(opts), debugLog);
342
+ ensureGatewayMethodRegistered(
343
+ api,
344
+ 'bncr.diagnostics',
345
+ (opts) => bridge.handleDiagnostics(opts),
346
+ debugLog,
347
+ );
348
+ ensureGatewayMethodRegistered(
349
+ api,
350
+ 'bncr.file.init',
351
+ (opts) => bridge.handleFileInit(opts),
352
+ debugLog,
353
+ );
354
+ ensureGatewayMethodRegistered(
355
+ api,
356
+ 'bncr.file.chunk',
357
+ (opts) => bridge.handleFileChunk(opts),
358
+ debugLog,
359
+ );
360
+ ensureGatewayMethodRegistered(
361
+ api,
362
+ 'bncr.file.complete',
363
+ (opts) => bridge.handleFileComplete(opts),
364
+ debugLog,
365
+ );
366
+ ensureGatewayMethodRegistered(
367
+ api,
368
+ 'bncr.file.abort',
369
+ (opts) => bridge.handleFileAbort(opts),
370
+ debugLog,
371
+ );
372
+ ensureGatewayMethodRegistered(
373
+ api,
374
+ 'bncr.file.ack',
375
+ (opts) => bridge.handleFileAck(opts),
376
+ debugLog,
377
+ );
378
+ debugLog('bncr plugin register done');
242
379
  },
243
380
  };
244
381
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmoxmo/bncr",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -25,12 +25,18 @@
25
25
  ],
26
26
  "scripts": {
27
27
  "selfcheck": "node ./scripts/selfcheck.mjs",
28
- "test": "node --import ./tests/register-ts-hooks.mjs --test ./tests/*.test.mjs"
28
+ "test": "node --import ./tests/register-ts-hooks.mjs --test ./tests/*.test.mjs",
29
+ "check-register-drift": "node ./scripts/check-register-drift.mjs",
30
+ "format:check": "biome format --check .",
31
+ "format": "biome format --write .",
32
+ "lint": "biome lint .",
33
+ "check": "biome check ."
29
34
  },
30
35
  "peerDependencies": {
31
36
  "openclaw": ">=2026.3.22"
32
37
  },
33
38
  "devDependencies": {
39
+ "@biomejs/biome": "^1.9.4",
34
40
  "openclaw": ">=2026.3.22"
35
41
  },
36
42
  "openclaw": {
@@ -0,0 +1,117 @@
1
+ import { execFileSync } from 'node:child_process';
2
+
3
+ const readNumber = (value, fallback) => {
4
+ const n = Number(value);
5
+ return Number.isFinite(n) ? n : fallback;
6
+ };
7
+
8
+ const args = process.argv.slice(2);
9
+ const options = {
10
+ durationSec: 300,
11
+ intervalSec: 15,
12
+ accountId: 'Primary',
13
+ gatewayBin: 'openclaw',
14
+ };
15
+
16
+ for (let i = 0; i < args.length; i += 1) {
17
+ const arg = args[i];
18
+ if (arg === '--duration-sec') options.durationSec = readNumber(args[++i], options.durationSec);
19
+ else if (arg === '--interval-sec')
20
+ options.intervalSec = readNumber(args[++i], options.intervalSec);
21
+ else if (arg === '--account-id') options.accountId = args[++i] || options.accountId;
22
+ else if (arg === '--gateway-bin') options.gatewayBin = args[++i] || options.gatewayBin;
23
+ else if (arg === '--help' || arg === '-h') {
24
+ console.log(
25
+ 'Usage: node ./scripts/check-register-drift.mjs [--duration-sec 300] [--interval-sec 15] [--account-id Primary] [--gateway-bin openclaw]\n\nSamples bncr.diagnostics over time and reports whether register counters drift after warmup.',
26
+ );
27
+ process.exit(0);
28
+ }
29
+ }
30
+
31
+ if (options.durationSec <= 0) throw new Error('durationSec must be > 0');
32
+ if (options.intervalSec <= 0) throw new Error('intervalSec must be > 0');
33
+
34
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
35
+
36
+ const fetchDiagnostics = () => {
37
+ const raw = execFileSync(
38
+ options.gatewayBin,
39
+ [
40
+ 'gateway',
41
+ 'call',
42
+ 'bncr.diagnostics',
43
+ '--json',
44
+ '--params',
45
+ JSON.stringify({ accountId: options.accountId }),
46
+ ],
47
+ { encoding: 'utf8' },
48
+ );
49
+ const parsed = JSON.parse(raw);
50
+ const reg = parsed?.diagnostics?.register || {};
51
+ const summary = reg?.traceSummary || {};
52
+ return {
53
+ now: parsed?.now ?? Date.now(),
54
+ registerCount: reg?.registerCount ?? null,
55
+ apiGeneration: reg?.apiGeneration ?? null,
56
+ apiInstanceId: reg?.apiInstanceId ?? null,
57
+ registryFingerprint: reg?.registryFingerprint ?? null,
58
+ warmupRegisterCount: summary?.warmupRegisterCount ?? null,
59
+ postWarmupRegisterCount: summary?.postWarmupRegisterCount ?? null,
60
+ unexpectedRegisterAfterWarmup: summary?.unexpectedRegisterAfterWarmup ?? null,
61
+ lastUnexpectedRegisterAt: summary?.lastUnexpectedRegisterAt ?? null,
62
+ sourceBuckets: summary?.sourceBuckets ?? null,
63
+ };
64
+ };
65
+
66
+ const startedAt = Date.now();
67
+ const samples = [];
68
+ const deadline = startedAt + options.durationSec * 1000;
69
+
70
+ while (true) {
71
+ const sample = fetchDiagnostics();
72
+ samples.push(sample);
73
+ const nextAt = Date.now() + options.intervalSec * 1000;
74
+ if (nextAt > deadline) break;
75
+ await sleep(Math.max(0, nextAt - Date.now()));
76
+ }
77
+
78
+ const first = samples[0] || {};
79
+ const last = samples[samples.length - 1] || {};
80
+ const deltaRegisterCount = (last.registerCount ?? 0) - (first.registerCount ?? 0);
81
+ const deltaApiGeneration = (last.apiGeneration ?? 0) - (first.apiGeneration ?? 0);
82
+ const deltaPostWarmupRegisterCount =
83
+ (last.postWarmupRegisterCount ?? 0) - (first.postWarmupRegisterCount ?? 0);
84
+ const historicalWarmupExternalDrift = Boolean(first.unexpectedRegisterAfterWarmup);
85
+ const newWarmupExternalDriftDuringWindow = deltaPostWarmupRegisterCount > 0;
86
+ const newDriftDuringWindow =
87
+ deltaRegisterCount > 0 || deltaApiGeneration > 0 || newWarmupExternalDriftDuringWindow;
88
+ const driftDetected = historicalWarmupExternalDrift || newDriftDuringWindow;
89
+
90
+ const result = {
91
+ ok: true,
92
+ accountId: options.accountId,
93
+ durationSec: options.durationSec,
94
+ intervalSec: options.intervalSec,
95
+ startedAt,
96
+ endedAt: Date.now(),
97
+ sampleCount: samples.length,
98
+ first,
99
+ last,
100
+ delta: {
101
+ registerCount: deltaRegisterCount,
102
+ apiGeneration: deltaApiGeneration,
103
+ postWarmupRegisterCount: deltaPostWarmupRegisterCount,
104
+ },
105
+ historicalWarmupExternalDrift,
106
+ newWarmupExternalDriftDuringWindow,
107
+ newDriftDuringWindow,
108
+ driftDetected,
109
+ conclusion: newDriftDuringWindow
110
+ ? 'new register drift was observed during this sampling window'
111
+ : historicalWarmupExternalDrift
112
+ ? 'no new drift during this window, but warmup-external drift had already happened before sampling began'
113
+ : 'register counters stayed stable during this window and no warmup-external drift was flagged',
114
+ samples,
115
+ };
116
+
117
+ console.log(JSON.stringify(result, null, 2));