@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 +12 -0
- package/index.ts +166 -29
- package/package.json +8 -2
- package/scripts/check-register-drift.mjs +117 -0
- package/src/channel.ts +904 -211
- package/src/core/accounts.ts +7 -1
- package/src/core/permissions.ts +3 -1
- package/src/core/probe.ts +2 -1
- package/src/core/status.ts +9 -3
- package/src/core/targets.ts +151 -41
- package/src/messaging/inbound/commands.ts +34 -10
- package/src/messaging/inbound/dispatch.ts +40 -4
- package/src/messaging/inbound/gate.ts +1 -3
- package/src/messaging/inbound/parse.ts +5 -2
- package/src/messaging/outbound/media.ts +24 -5
- package/src/messaging/outbound/send.ts +25 -5
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
|
-
|
|
195
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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.
|
|
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));
|