@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/src/channel.ts
CHANGED
|
@@ -1,56 +1,70 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
4
|
+
import { readBooleanParam } from 'openclaw/plugin-sdk/boolean-param';
|
|
4
5
|
import type {
|
|
6
|
+
GatewayRequestHandlerOptions,
|
|
5
7
|
OpenClawPluginApi,
|
|
6
8
|
OpenClawPluginServiceContext,
|
|
7
|
-
GatewayRequestHandlerOptions,
|
|
8
9
|
} from 'openclaw/plugin-sdk/core';
|
|
9
10
|
import {
|
|
10
|
-
setAccountEnabledInConfigSection,
|
|
11
11
|
applyAccountNameToChannelSection,
|
|
12
|
+
setAccountEnabledInConfigSection,
|
|
12
13
|
} from 'openclaw/plugin-sdk/core';
|
|
13
|
-
import
|
|
14
|
-
import {
|
|
15
|
-
import { writeJsonFileAtomically, readJsonFileWithFallback } from 'openclaw/plugin-sdk/json-store';
|
|
14
|
+
import { readJsonFileWithFallback, writeJsonFileAtomically } from 'openclaw/plugin-sdk/json-store';
|
|
15
|
+
import type { ChannelMessageActionAdapter, ChatType } from 'openclaw/plugin-sdk/mattermost';
|
|
16
16
|
import { readStringParam } from 'openclaw/plugin-sdk/param-readers';
|
|
17
|
-
import {
|
|
18
|
-
import { extractToolSend } from 'openclaw/plugin-sdk/tool-send';
|
|
17
|
+
import { createDefaultChannelRuntimeState } from 'openclaw/plugin-sdk/status-helpers';
|
|
19
18
|
import { jsonResult } from 'openclaw/plugin-sdk/telegram-core';
|
|
20
|
-
import {
|
|
21
|
-
import type { BncrRoute, BncrConnection, OutboxEntry } from './core/types.ts';
|
|
19
|
+
import { extractToolSend } from 'openclaw/plugin-sdk/tool-send';
|
|
22
20
|
import {
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
BNCR_DEFAULT_ACCOUNT_ID,
|
|
22
|
+
CHANNEL_ID,
|
|
23
|
+
listAccountIds,
|
|
24
|
+
normalizeAccountId,
|
|
25
|
+
resolveAccount,
|
|
26
|
+
resolveDefaultDisplayName,
|
|
27
|
+
} from './core/accounts.ts';
|
|
28
|
+
import { BncrConfigSchema } from './core/config-schema.ts';
|
|
29
|
+
import { buildBncrPermissionSummary } from './core/permissions.ts';
|
|
30
|
+
import { resolveBncrChannelPolicy } from './core/policy.ts';
|
|
31
|
+
import { probeBncrAccount } from './core/probe.ts';
|
|
32
|
+
import {
|
|
33
|
+
buildAccountRuntimeSnapshot,
|
|
34
|
+
buildIntegratedDiagnostics as buildIntegratedDiagnosticsFromRuntime,
|
|
35
|
+
buildStatusHeadlineFromRuntime,
|
|
36
|
+
buildStatusMetaFromRuntime,
|
|
37
|
+
} from './core/status.ts';
|
|
38
|
+
import {
|
|
39
|
+
buildCanonicalBncrSessionKey,
|
|
25
40
|
formatDisplayScope,
|
|
26
41
|
isLowerHex,
|
|
27
|
-
|
|
42
|
+
normalizeInboundSessionKey,
|
|
43
|
+
normalizeStoredSessionKey,
|
|
44
|
+
parseRouteFromDisplayScope,
|
|
28
45
|
parseRouteFromHexScope,
|
|
46
|
+
parseRouteFromScope,
|
|
29
47
|
parseRouteLike,
|
|
30
|
-
parseLegacySessionKeyToStrict,
|
|
31
|
-
normalizeStoredSessionKey,
|
|
32
48
|
parseStrictBncrSessionKey,
|
|
33
|
-
normalizeInboundSessionKey,
|
|
34
|
-
withTaskSessionKey,
|
|
35
|
-
buildFallbackSessionKey,
|
|
36
49
|
routeKey,
|
|
50
|
+
routeScopeToHex,
|
|
51
|
+
withTaskSessionKey,
|
|
37
52
|
} from './core/targets.ts';
|
|
38
|
-
import {
|
|
53
|
+
import type { BncrConnection, BncrRoute, OutboxEntry } from './core/types.ts';
|
|
39
54
|
import { dispatchBncrInbound } from './messaging/inbound/dispatch.ts';
|
|
40
55
|
import { checkBncrMessageGate } from './messaging/inbound/gate.ts';
|
|
41
|
-
import {
|
|
42
|
-
import { buildBncrMediaOutboundFrame, resolveBncrOutboundMessageType } from './messaging/outbound/media.ts';
|
|
43
|
-
import { sendBncrReplyAction, deleteBncrMessageAction, reactBncrMessageAction, editBncrMessageAction } from './messaging/outbound/actions.ts';
|
|
56
|
+
import { parseBncrInboundParams } from './messaging/inbound/parse.ts';
|
|
44
57
|
import {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
} from './
|
|
50
|
-
import {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
58
|
+
deleteBncrMessageAction,
|
|
59
|
+
editBncrMessageAction,
|
|
60
|
+
reactBncrMessageAction,
|
|
61
|
+
sendBncrReplyAction,
|
|
62
|
+
} from './messaging/outbound/actions.ts';
|
|
63
|
+
import {
|
|
64
|
+
buildBncrMediaOutboundFrame,
|
|
65
|
+
resolveBncrOutboundMessageType,
|
|
66
|
+
} from './messaging/outbound/media.ts';
|
|
67
|
+
import { sendBncrMedia, sendBncrText } from './messaging/outbound/send.ts';
|
|
54
68
|
const BRIDGE_VERSION = 2;
|
|
55
69
|
const BNCR_PUSH_EVENT = 'bncr.push';
|
|
56
70
|
const CONNECT_TTL_MS = 120_000;
|
|
@@ -63,6 +77,7 @@ const FILE_CHUNK_RETRY = 3;
|
|
|
63
77
|
const FILE_ACK_TIMEOUT_MS = 30_000;
|
|
64
78
|
const FILE_TRANSFER_ACK_TTL_MS = 30_000;
|
|
65
79
|
const FILE_TRANSFER_KEEP_MS = 6 * 60 * 60 * 1000;
|
|
80
|
+
const REGISTER_WARMUP_WINDOW_MS = 30_000;
|
|
66
81
|
let BNCR_DEBUG_VERBOSE = false; // 全局调试日志开关(默认关闭)
|
|
67
82
|
|
|
68
83
|
type FileSendTransferState = {
|
|
@@ -130,6 +145,18 @@ type PersistedState = {
|
|
|
130
145
|
accountId: string;
|
|
131
146
|
updatedAt: number;
|
|
132
147
|
}>;
|
|
148
|
+
lastDriftSnapshot?: {
|
|
149
|
+
capturedAt: number;
|
|
150
|
+
registerCount: number | null;
|
|
151
|
+
apiGeneration: number | null;
|
|
152
|
+
postWarmupRegisterCount: number | null;
|
|
153
|
+
apiInstanceId: string | null;
|
|
154
|
+
registryFingerprint: string | null;
|
|
155
|
+
dominantBucket: string | null;
|
|
156
|
+
sourceBuckets: Record<string, number>;
|
|
157
|
+
traceWindowSize: number;
|
|
158
|
+
traceRecent: Array<Record<string, unknown>>;
|
|
159
|
+
} | null;
|
|
133
160
|
};
|
|
134
161
|
|
|
135
162
|
function now() {
|
|
@@ -142,13 +169,11 @@ function asString(v: unknown, fallback = ''): string {
|
|
|
142
169
|
return String(v);
|
|
143
170
|
}
|
|
144
171
|
|
|
145
|
-
|
|
146
172
|
function backoffMs(retryCount: number): number {
|
|
147
173
|
// 1s,2s,4s,8s... capped by retry count checks
|
|
148
174
|
return Math.max(1_000, 1_000 * 2 ** Math.max(0, retryCount - 1));
|
|
149
175
|
}
|
|
150
176
|
|
|
151
|
-
|
|
152
177
|
function fileExtFromMime(mimeType?: string): string {
|
|
153
178
|
const mt = asString(mimeType || '').toLowerCase();
|
|
154
179
|
const map: Record<string, string> = {
|
|
@@ -171,7 +196,15 @@ function fileExtFromMime(mimeType?: string): string {
|
|
|
171
196
|
function sanitizeFileName(rawName?: string, fallback = 'file.bin'): string {
|
|
172
197
|
const name = asString(rawName || '').trim();
|
|
173
198
|
const base = name || fallback;
|
|
174
|
-
const cleaned =
|
|
199
|
+
const cleaned = Array.from(base, (ch) => {
|
|
200
|
+
const code = ch.charCodeAt(0);
|
|
201
|
+
if (code <= 0x1f) return '_';
|
|
202
|
+
if ('\\/:*?"<>|'.includes(ch)) return '_';
|
|
203
|
+
return ch;
|
|
204
|
+
})
|
|
205
|
+
.join('')
|
|
206
|
+
.replace(/\s+/g, ' ')
|
|
207
|
+
.trim();
|
|
175
208
|
return cleaned || fallback;
|
|
176
209
|
}
|
|
177
210
|
|
|
@@ -183,7 +216,11 @@ function buildTimestampFileName(mimeType?: string): string {
|
|
|
183
216
|
return `bncr_${ts}_${Math.random().toString(16).slice(2, 8)}${ext}`;
|
|
184
217
|
}
|
|
185
218
|
|
|
186
|
-
function resolveOutboundFileName(params: {
|
|
219
|
+
function resolveOutboundFileName(params: {
|
|
220
|
+
mediaUrl?: string;
|
|
221
|
+
fileName?: string;
|
|
222
|
+
mimeType?: string;
|
|
223
|
+
}): string {
|
|
187
224
|
const mediaUrl = asString(params.mediaUrl || '').trim();
|
|
188
225
|
const mimeType = asString(params.mimeType || '').trim();
|
|
189
226
|
|
|
@@ -205,20 +242,85 @@ class BncrBridgeRuntime {
|
|
|
205
242
|
private api: OpenClawPluginApi;
|
|
206
243
|
private statePath: string | null = null;
|
|
207
244
|
private bridgeId = `${process.pid}-${Math.random().toString(16).slice(2, 8)}`;
|
|
245
|
+
private gatewayPid = process.pid;
|
|
246
|
+
private registerCount = 0;
|
|
247
|
+
private apiGeneration = 0;
|
|
248
|
+
private firstRegisterAt: number | null = null;
|
|
249
|
+
private lastRegisterAt: number | null = null;
|
|
250
|
+
private lastApiRebindAt: number | null = null;
|
|
251
|
+
private pluginSource: string | null = null;
|
|
252
|
+
private pluginVersion: string | null = null;
|
|
253
|
+
private connectionEpoch = 0;
|
|
254
|
+
private primaryLeaseId: string | null = null;
|
|
255
|
+
private acceptedConnections = 0;
|
|
256
|
+
private lastConnectAt: number | null = null;
|
|
257
|
+
private lastDisconnectAt: number | null = null;
|
|
258
|
+
private lastInboundAtGlobal: number | null = null;
|
|
259
|
+
private lastActivityAtGlobal: number | null = null;
|
|
260
|
+
private lastAckAtGlobal: number | null = null;
|
|
261
|
+
private recentConnections = new Map<
|
|
262
|
+
string,
|
|
263
|
+
{
|
|
264
|
+
epoch: number;
|
|
265
|
+
connectedAt: number;
|
|
266
|
+
lastActivityAt: number | null;
|
|
267
|
+
isPrimary: boolean;
|
|
268
|
+
}
|
|
269
|
+
>();
|
|
270
|
+
private staleCounters = {
|
|
271
|
+
staleConnect: 0,
|
|
272
|
+
staleInbound: 0,
|
|
273
|
+
staleActivity: 0,
|
|
274
|
+
staleAck: 0,
|
|
275
|
+
staleFileInit: 0,
|
|
276
|
+
staleFileChunk: 0,
|
|
277
|
+
staleFileComplete: 0,
|
|
278
|
+
staleFileAbort: 0,
|
|
279
|
+
lastStaleAt: null as number | null,
|
|
280
|
+
};
|
|
281
|
+
private lastApiInstanceId: string | null = null;
|
|
282
|
+
private lastRegistryFingerprint: string | null = null;
|
|
283
|
+
private lastDriftSnapshot: PersistedState['lastDriftSnapshot'] = null;
|
|
284
|
+
private registerTraceRecent: Array<{
|
|
285
|
+
ts: number;
|
|
286
|
+
bridgeId: string;
|
|
287
|
+
gatewayPid: number;
|
|
288
|
+
registerCount: number;
|
|
289
|
+
apiGeneration: number;
|
|
290
|
+
apiRebound: boolean;
|
|
291
|
+
apiInstanceId: string | null;
|
|
292
|
+
registryFingerprint: string | null;
|
|
293
|
+
source: string | null;
|
|
294
|
+
pluginVersion: string | null;
|
|
295
|
+
stack: string;
|
|
296
|
+
stackBucket: string;
|
|
297
|
+
}> = [];
|
|
208
298
|
|
|
209
299
|
private connections = new Map<string, BncrConnection>(); // connectionKey -> connection
|
|
210
300
|
private activeConnectionByAccount = new Map<string, string>(); // accountId -> connectionKey
|
|
211
301
|
private outbox = new Map<string, OutboxEntry>(); // messageId -> entry
|
|
212
302
|
private deadLetter: OutboxEntry[] = [];
|
|
213
303
|
|
|
214
|
-
private sessionRoutes = new Map<
|
|
215
|
-
|
|
304
|
+
private sessionRoutes = new Map<
|
|
305
|
+
string,
|
|
306
|
+
{ accountId: string; route: BncrRoute; updatedAt: number }
|
|
307
|
+
>();
|
|
308
|
+
private routeAliases = new Map<
|
|
309
|
+
string,
|
|
310
|
+
{ accountId: string; route: BncrRoute; updatedAt: number }
|
|
311
|
+
>();
|
|
216
312
|
|
|
217
313
|
private recentInbound = new Map<string, number>();
|
|
218
|
-
private lastSessionByAccount = new Map<
|
|
314
|
+
private lastSessionByAccount = new Map<
|
|
315
|
+
string,
|
|
316
|
+
{ sessionKey: string; scope: string; updatedAt: number }
|
|
317
|
+
>();
|
|
219
318
|
private lastActivityByAccount = new Map<string, number>();
|
|
220
319
|
private lastInboundByAccount = new Map<string, number>();
|
|
221
320
|
private lastOutboundByAccount = new Map<string, number>();
|
|
321
|
+
private canonicalAgentId: string | null = null;
|
|
322
|
+
private canonicalAgentSource: 'startup' | 'runtime' | 'fallback-main' | null = null;
|
|
323
|
+
private canonicalAgentResolvedAt: number | null = null;
|
|
222
324
|
|
|
223
325
|
// 内置健康/回归计数(替代独立脚本)
|
|
224
326
|
private startedAt = now();
|
|
@@ -236,16 +338,313 @@ class BncrBridgeRuntime {
|
|
|
236
338
|
// 文件互传状态(V1:尽力而为,重连不续传)
|
|
237
339
|
private fileSendTransfers = new Map<string, FileSendTransferState>(); // OpenClaw -> Bncr(服务端发起)
|
|
238
340
|
private fileRecvTransfers = new Map<string, FileRecvTransferState>(); // Bncr -> OpenClaw(客户端发起)
|
|
239
|
-
private fileAckWaiters = new Map<
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
341
|
+
private fileAckWaiters = new Map<
|
|
342
|
+
string,
|
|
343
|
+
{
|
|
344
|
+
resolve: (payload: Record<string, unknown>) => void;
|
|
345
|
+
reject: (err: Error) => void;
|
|
346
|
+
timer: NodeJS.Timeout;
|
|
347
|
+
}
|
|
348
|
+
>();
|
|
244
349
|
|
|
245
350
|
constructor(api: OpenClawPluginApi) {
|
|
246
351
|
this.api = api;
|
|
247
352
|
}
|
|
248
353
|
|
|
354
|
+
bindApi(api: OpenClawPluginApi) {
|
|
355
|
+
this.api = api;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
getBridgeId() {
|
|
359
|
+
return this.bridgeId;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private classifyRegisterTrace(stack: string) {
|
|
363
|
+
if (
|
|
364
|
+
stack.includes('prepareSecretsRuntimeSnapshot') ||
|
|
365
|
+
stack.includes('resolveRuntimeWebTools') ||
|
|
366
|
+
stack.includes('resolvePluginWebSearchProviders')
|
|
367
|
+
) {
|
|
368
|
+
return 'runtime/webtools';
|
|
369
|
+
}
|
|
370
|
+
if (stack.includes('startGatewayServer') || stack.includes('loadGatewayPlugins')) {
|
|
371
|
+
return 'gateway/startup';
|
|
372
|
+
}
|
|
373
|
+
if (stack.includes('resolvePluginImplicitProviders')) {
|
|
374
|
+
return 'provider/discovery/implicit';
|
|
375
|
+
}
|
|
376
|
+
if (stack.includes('resolvePluginDiscoveryProviders')) {
|
|
377
|
+
return 'provider/discovery/discovery';
|
|
378
|
+
}
|
|
379
|
+
if (stack.includes('resolvePluginProviders')) {
|
|
380
|
+
return 'provider/discovery/providers';
|
|
381
|
+
}
|
|
382
|
+
return 'other';
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
private dominantRegisterBucket(sourceBuckets: Record<string, number>) {
|
|
386
|
+
let winner: string | null = null;
|
|
387
|
+
let winnerCount = -1;
|
|
388
|
+
for (const [bucket, count] of Object.entries(sourceBuckets)) {
|
|
389
|
+
if (count > winnerCount) {
|
|
390
|
+
winner = bucket;
|
|
391
|
+
winnerCount = count;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return winner;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
private captureDriftSnapshot(
|
|
398
|
+
summary: ReturnType<BncrBridgeRuntime['buildRegisterTraceSummary']>,
|
|
399
|
+
) {
|
|
400
|
+
this.lastDriftSnapshot = {
|
|
401
|
+
capturedAt: now(),
|
|
402
|
+
registerCount: this.registerCount,
|
|
403
|
+
apiGeneration: this.apiGeneration,
|
|
404
|
+
postWarmupRegisterCount: summary.postWarmupRegisterCount,
|
|
405
|
+
apiInstanceId: this.lastApiInstanceId,
|
|
406
|
+
registryFingerprint: this.lastRegistryFingerprint,
|
|
407
|
+
dominantBucket: summary.dominantBucket,
|
|
408
|
+
sourceBuckets: { ...summary.sourceBuckets },
|
|
409
|
+
traceWindowSize: this.registerTraceRecent.length,
|
|
410
|
+
traceRecent: this.registerTraceRecent.map((trace) => ({ ...trace })),
|
|
411
|
+
};
|
|
412
|
+
this.scheduleSave();
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
private buildRegisterTraceSummary() {
|
|
416
|
+
const buckets: Record<string, number> = {};
|
|
417
|
+
let warmupCount = 0;
|
|
418
|
+
let postWarmupCount = 0;
|
|
419
|
+
let unexpectedRegisterAfterWarmup = false;
|
|
420
|
+
let lastUnexpectedRegisterAt: number | null = null;
|
|
421
|
+
const baseline = this.firstRegisterAt;
|
|
422
|
+
|
|
423
|
+
for (const trace of this.registerTraceRecent) {
|
|
424
|
+
buckets[trace.stackBucket] = (buckets[trace.stackBucket] || 0) + 1;
|
|
425
|
+
const isWarmup = baseline != null && trace.ts - baseline <= REGISTER_WARMUP_WINDOW_MS;
|
|
426
|
+
if (isWarmup) {
|
|
427
|
+
warmupCount += 1;
|
|
428
|
+
} else {
|
|
429
|
+
postWarmupCount += 1;
|
|
430
|
+
unexpectedRegisterAfterWarmup = true;
|
|
431
|
+
lastUnexpectedRegisterAt = trace.ts;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const dominantBucket = this.dominantRegisterBucket(buckets);
|
|
436
|
+
const likelyRuntimeRegistryDrift = postWarmupCount > 0;
|
|
437
|
+
const likelyStartupFanoutOnly = warmupCount > 0 && postWarmupCount === 0;
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
startupWindowMs: REGISTER_WARMUP_WINDOW_MS,
|
|
441
|
+
traceWindowSize: this.registerTraceRecent.length,
|
|
442
|
+
sourceBuckets: buckets,
|
|
443
|
+
dominantBucket,
|
|
444
|
+
warmupRegisterCount: warmupCount,
|
|
445
|
+
postWarmupRegisterCount: postWarmupCount,
|
|
446
|
+
unexpectedRegisterAfterWarmup,
|
|
447
|
+
lastUnexpectedRegisterAt,
|
|
448
|
+
likelyRuntimeRegistryDrift,
|
|
449
|
+
likelyStartupFanoutOnly,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
noteRegister(meta: {
|
|
454
|
+
source?: string;
|
|
455
|
+
pluginVersion?: string;
|
|
456
|
+
apiRebound?: boolean;
|
|
457
|
+
apiInstanceId?: string;
|
|
458
|
+
registryFingerprint?: string;
|
|
459
|
+
}) {
|
|
460
|
+
const ts = now();
|
|
461
|
+
this.registerCount += 1;
|
|
462
|
+
if (this.firstRegisterAt == null) this.firstRegisterAt = ts;
|
|
463
|
+
this.lastRegisterAt = ts;
|
|
464
|
+
if (meta.apiRebound) {
|
|
465
|
+
this.apiGeneration += 1;
|
|
466
|
+
this.lastApiRebindAt = ts;
|
|
467
|
+
} else if (this.registerCount === 1 && this.apiGeneration === 0) {
|
|
468
|
+
this.apiGeneration = 1;
|
|
469
|
+
}
|
|
470
|
+
if (meta.source) this.pluginSource = meta.source;
|
|
471
|
+
if (meta.pluginVersion) this.pluginVersion = meta.pluginVersion;
|
|
472
|
+
if (meta.apiInstanceId) this.lastApiInstanceId = meta.apiInstanceId;
|
|
473
|
+
if (meta.registryFingerprint) this.lastRegistryFingerprint = meta.registryFingerprint;
|
|
474
|
+
|
|
475
|
+
const stack = String(new Error().stack || '')
|
|
476
|
+
.split('\n')
|
|
477
|
+
.slice(2, 7)
|
|
478
|
+
.map((line) => line.trim())
|
|
479
|
+
.filter(Boolean)
|
|
480
|
+
.join(' <- ');
|
|
481
|
+
const stackBucket = this.classifyRegisterTrace(stack);
|
|
482
|
+
|
|
483
|
+
const trace = {
|
|
484
|
+
ts,
|
|
485
|
+
bridgeId: this.bridgeId,
|
|
486
|
+
gatewayPid: this.gatewayPid,
|
|
487
|
+
registerCount: this.registerCount,
|
|
488
|
+
apiGeneration: this.apiGeneration,
|
|
489
|
+
apiRebound: meta.apiRebound === true,
|
|
490
|
+
apiInstanceId: this.lastApiInstanceId,
|
|
491
|
+
registryFingerprint: this.lastRegistryFingerprint,
|
|
492
|
+
source: this.pluginSource,
|
|
493
|
+
pluginVersion: this.pluginVersion,
|
|
494
|
+
stack,
|
|
495
|
+
stackBucket,
|
|
496
|
+
};
|
|
497
|
+
this.registerTraceRecent.push(trace);
|
|
498
|
+
if (this.registerTraceRecent.length > 12)
|
|
499
|
+
this.registerTraceRecent.splice(0, this.registerTraceRecent.length - 12);
|
|
500
|
+
|
|
501
|
+
const summary = this.buildRegisterTraceSummary();
|
|
502
|
+
if (summary.postWarmupRegisterCount > 0) this.captureDriftSnapshot(summary);
|
|
503
|
+
|
|
504
|
+
this.api.logger.info?.(`[bncr-register-trace] ${JSON.stringify(trace)}`);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
private createLeaseId() {
|
|
508
|
+
return typeof crypto?.randomUUID === 'function'
|
|
509
|
+
? `lease_${crypto.randomUUID()}`
|
|
510
|
+
: `lease_${Math.random().toString(16).slice(2)}${Date.now().toString(16)}`;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
private acceptConnection() {
|
|
514
|
+
const ts = now();
|
|
515
|
+
const leaseId = this.createLeaseId();
|
|
516
|
+
const connectionEpoch = ++this.connectionEpoch;
|
|
517
|
+
this.primaryLeaseId = leaseId;
|
|
518
|
+
this.acceptedConnections += 1;
|
|
519
|
+
this.lastConnectAt = ts;
|
|
520
|
+
this.recentConnections.set(leaseId, {
|
|
521
|
+
epoch: connectionEpoch,
|
|
522
|
+
connectedAt: ts,
|
|
523
|
+
lastActivityAt: null,
|
|
524
|
+
isPrimary: true,
|
|
525
|
+
});
|
|
526
|
+
for (const [id, entry] of this.recentConnections.entries()) {
|
|
527
|
+
if (id !== leaseId) entry.isPrimary = false;
|
|
528
|
+
}
|
|
529
|
+
while (this.recentConnections.size > 8) {
|
|
530
|
+
const oldest = this.recentConnections.keys().next().value;
|
|
531
|
+
if (!oldest) break;
|
|
532
|
+
this.recentConnections.delete(oldest);
|
|
533
|
+
}
|
|
534
|
+
return { leaseId, connectionEpoch, acceptedAt: ts };
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
private observeLease(
|
|
538
|
+
kind:
|
|
539
|
+
| 'connect'
|
|
540
|
+
| 'inbound'
|
|
541
|
+
| 'activity'
|
|
542
|
+
| 'ack'
|
|
543
|
+
| 'file.init'
|
|
544
|
+
| 'file.chunk'
|
|
545
|
+
| 'file.complete'
|
|
546
|
+
| 'file.abort',
|
|
547
|
+
params: { leaseId?: string; connectionEpoch?: number },
|
|
548
|
+
) {
|
|
549
|
+
const leaseId = typeof params.leaseId === 'string' ? params.leaseId.trim() : '';
|
|
550
|
+
const connectionEpoch =
|
|
551
|
+
typeof params.connectionEpoch === 'number' ? params.connectionEpoch : undefined;
|
|
552
|
+
if (!leaseId && connectionEpoch == null) return { stale: false, reason: 'missing' as const };
|
|
553
|
+
const staleByLease =
|
|
554
|
+
!!leaseId && this.primaryLeaseId != null && leaseId !== this.primaryLeaseId;
|
|
555
|
+
const staleByEpoch =
|
|
556
|
+
connectionEpoch != null &&
|
|
557
|
+
this.connectionEpoch > 0 &&
|
|
558
|
+
connectionEpoch !== this.connectionEpoch;
|
|
559
|
+
const stale = staleByLease || staleByEpoch;
|
|
560
|
+
if (!stale) return { stale: false, reason: 'ok' as const };
|
|
561
|
+
this.staleCounters.lastStaleAt = now();
|
|
562
|
+
switch (kind) {
|
|
563
|
+
case 'connect':
|
|
564
|
+
this.staleCounters.staleConnect += 1;
|
|
565
|
+
break;
|
|
566
|
+
case 'inbound':
|
|
567
|
+
this.staleCounters.staleInbound += 1;
|
|
568
|
+
break;
|
|
569
|
+
case 'activity':
|
|
570
|
+
this.staleCounters.staleActivity += 1;
|
|
571
|
+
break;
|
|
572
|
+
case 'ack':
|
|
573
|
+
this.staleCounters.staleAck += 1;
|
|
574
|
+
break;
|
|
575
|
+
case 'file.init':
|
|
576
|
+
this.staleCounters.staleFileInit += 1;
|
|
577
|
+
break;
|
|
578
|
+
case 'file.chunk':
|
|
579
|
+
this.staleCounters.staleFileChunk += 1;
|
|
580
|
+
break;
|
|
581
|
+
case 'file.complete':
|
|
582
|
+
this.staleCounters.staleFileComplete += 1;
|
|
583
|
+
break;
|
|
584
|
+
case 'file.abort':
|
|
585
|
+
this.staleCounters.staleFileAbort += 1;
|
|
586
|
+
break;
|
|
587
|
+
}
|
|
588
|
+
this.api.logger.warn?.(
|
|
589
|
+
`[bncr] stale ${kind} observed lease=${leaseId || '-'} epoch=${connectionEpoch ?? '-'} currentLease=${this.primaryLeaseId || '-'} currentEpoch=${this.connectionEpoch}`,
|
|
590
|
+
);
|
|
591
|
+
return { stale: true, reason: 'mismatch' as const };
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
private buildExtendedDiagnostics(accountId: string) {
|
|
595
|
+
const diagnostics = this.buildIntegratedDiagnostics(accountId) as Record<string, any>;
|
|
596
|
+
return {
|
|
597
|
+
...diagnostics,
|
|
598
|
+
register: {
|
|
599
|
+
bridgeId: this.bridgeId,
|
|
600
|
+
gatewayPid: this.gatewayPid,
|
|
601
|
+
pluginVersion: this.pluginVersion,
|
|
602
|
+
source: this.pluginSource,
|
|
603
|
+
apiInstanceId: this.lastApiInstanceId,
|
|
604
|
+
registryFingerprint: this.lastRegistryFingerprint,
|
|
605
|
+
registerCount: this.registerCount,
|
|
606
|
+
firstRegisterAt: this.firstRegisterAt,
|
|
607
|
+
lastRegisterAt: this.lastRegisterAt,
|
|
608
|
+
lastApiRebindAt: this.lastApiRebindAt,
|
|
609
|
+
apiGeneration: this.apiGeneration,
|
|
610
|
+
traceRecent: this.registerTraceRecent.slice(),
|
|
611
|
+
traceSummary: this.buildRegisterTraceSummary(),
|
|
612
|
+
lastDriftSnapshot: this.lastDriftSnapshot,
|
|
613
|
+
},
|
|
614
|
+
connection: {
|
|
615
|
+
active: this.activeConnectionCount(accountId),
|
|
616
|
+
primaryLeaseId: this.primaryLeaseId,
|
|
617
|
+
primaryEpoch: this.connectionEpoch || null,
|
|
618
|
+
acceptedConnections: this.acceptedConnections,
|
|
619
|
+
lastConnectAt: this.lastConnectAt,
|
|
620
|
+
lastDisconnectAt: this.lastDisconnectAt,
|
|
621
|
+
lastActivityAt: this.lastActivityAtGlobal,
|
|
622
|
+
lastInboundAt: this.lastInboundAtGlobal,
|
|
623
|
+
lastAckAt: this.lastAckAtGlobal,
|
|
624
|
+
recent: Array.from(this.recentConnections.entries()).map(([leaseId, entry]) => ({
|
|
625
|
+
leaseId,
|
|
626
|
+
epoch: entry.epoch,
|
|
627
|
+
connectedAt: entry.connectedAt,
|
|
628
|
+
lastActivityAt: entry.lastActivityAt,
|
|
629
|
+
isPrimary: entry.isPrimary,
|
|
630
|
+
})),
|
|
631
|
+
},
|
|
632
|
+
protocol: {
|
|
633
|
+
bridgeVersion: BRIDGE_VERSION,
|
|
634
|
+
protocolVersion: 2,
|
|
635
|
+
minClientProtocol: 1,
|
|
636
|
+
features: {
|
|
637
|
+
leaseId: true,
|
|
638
|
+
connectionEpoch: true,
|
|
639
|
+
staleObserveOnly: true,
|
|
640
|
+
staleRejectAck: false,
|
|
641
|
+
staleRejectFile: false,
|
|
642
|
+
},
|
|
643
|
+
},
|
|
644
|
+
stale: { ...this.staleCounters },
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
|
|
249
648
|
isDebugEnabled(): boolean {
|
|
250
649
|
try {
|
|
251
650
|
const cfg = (this.api.runtime.config?.get?.() as any) || {};
|
|
@@ -258,10 +657,18 @@ class BncrBridgeRuntime {
|
|
|
258
657
|
startService = async (ctx: OpenClawPluginServiceContext, debug?: boolean) => {
|
|
259
658
|
this.statePath = path.join(ctx.stateDir, 'bncr-bridge-state.json');
|
|
260
659
|
await this.loadState();
|
|
660
|
+
try {
|
|
661
|
+
const cfg = await this.api.runtime.config.loadConfig();
|
|
662
|
+
this.initializeCanonicalAgentId(cfg);
|
|
663
|
+
} catch {
|
|
664
|
+
// ignore startup canonical agent initialization errors
|
|
665
|
+
}
|
|
261
666
|
if (typeof debug === 'boolean') BNCR_DEBUG_VERBOSE = debug;
|
|
262
667
|
const bootDiag = this.buildIntegratedDiagnostics(BNCR_DEFAULT_ACCOUNT_ID);
|
|
263
668
|
if (BNCR_DEBUG_VERBOSE) {
|
|
264
|
-
this.api.logger.info(
|
|
669
|
+
this.api.logger.info(
|
|
670
|
+
`bncr-channel service started (bridge=${this.bridgeId} diag.ok=${bootDiag.regression.ok} routes=${bootDiag.regression.totalKnownRoutes} pending=${bootDiag.health.pending} dead=${bootDiag.health.deadLetter} debug=${BNCR_DEBUG_VERBOSE})`,
|
|
671
|
+
);
|
|
265
672
|
}
|
|
266
673
|
};
|
|
267
674
|
|
|
@@ -307,6 +714,65 @@ class BncrBridgeRuntime {
|
|
|
307
714
|
}
|
|
308
715
|
}
|
|
309
716
|
|
|
717
|
+
private tryResolveBindingAgentId(args: {
|
|
718
|
+
cfg: any;
|
|
719
|
+
accountId: string;
|
|
720
|
+
peer?: any;
|
|
721
|
+
channelId?: string;
|
|
722
|
+
}): string | null {
|
|
723
|
+
try {
|
|
724
|
+
const resolved = this.api.runtime.channel.routing.resolveAgentRoute({
|
|
725
|
+
cfg: args.cfg,
|
|
726
|
+
channel: args.channelId || CHANNEL_ID,
|
|
727
|
+
accountId: normalizeAccountId(args.accountId),
|
|
728
|
+
peer: args.peer,
|
|
729
|
+
});
|
|
730
|
+
const agentId = asString(resolved?.agentId || '').trim();
|
|
731
|
+
return agentId || null;
|
|
732
|
+
} catch {
|
|
733
|
+
return null;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
private initializeCanonicalAgentId(cfg: any) {
|
|
738
|
+
if (this.canonicalAgentId) return;
|
|
739
|
+
const agentId = this.tryResolveBindingAgentId({
|
|
740
|
+
cfg,
|
|
741
|
+
accountId: BNCR_DEFAULT_ACCOUNT_ID,
|
|
742
|
+
channelId: CHANNEL_ID,
|
|
743
|
+
peer: { kind: 'direct', id: 'bootstrap' },
|
|
744
|
+
});
|
|
745
|
+
if (!agentId) return;
|
|
746
|
+
this.canonicalAgentId = agentId;
|
|
747
|
+
this.canonicalAgentSource = 'startup';
|
|
748
|
+
this.canonicalAgentResolvedAt = now();
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
private ensureCanonicalAgentId(args: {
|
|
752
|
+
cfg: any;
|
|
753
|
+
accountId: string;
|
|
754
|
+
peer?: any;
|
|
755
|
+
channelId?: string;
|
|
756
|
+
}): string {
|
|
757
|
+
if (this.canonicalAgentId) return this.canonicalAgentId;
|
|
758
|
+
|
|
759
|
+
const agentId = this.tryResolveBindingAgentId(args);
|
|
760
|
+
if (agentId) {
|
|
761
|
+
this.canonicalAgentId = agentId;
|
|
762
|
+
this.canonicalAgentSource = 'runtime';
|
|
763
|
+
this.canonicalAgentResolvedAt = now();
|
|
764
|
+
return agentId;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
this.canonicalAgentId = 'main';
|
|
768
|
+
this.canonicalAgentSource = 'fallback-main';
|
|
769
|
+
this.canonicalAgentResolvedAt = now();
|
|
770
|
+
this.api.logger.warn?.(
|
|
771
|
+
'[bncr-canonical-agent] binding agent unresolved; fallback to main for current process lifetime',
|
|
772
|
+
);
|
|
773
|
+
return this.canonicalAgentId;
|
|
774
|
+
}
|
|
775
|
+
|
|
310
776
|
private countInvalidOutboxSessionKeys(accountId: string): number {
|
|
311
777
|
const acc = normalizeAccountId(accountId);
|
|
312
778
|
let count = 0;
|
|
@@ -319,7 +785,8 @@ class BncrBridgeRuntime {
|
|
|
319
785
|
|
|
320
786
|
private countLegacyAccountResidue(accountId: string): number {
|
|
321
787
|
const acc = normalizeAccountId(accountId);
|
|
322
|
-
const mismatched = (raw?: string | null) =>
|
|
788
|
+
const mismatched = (raw?: string | null) =>
|
|
789
|
+
asString(raw || '').trim() && normalizeAccountId(raw) !== acc;
|
|
323
790
|
|
|
324
791
|
let count = 0;
|
|
325
792
|
|
|
@@ -365,7 +832,8 @@ class BncrBridgeRuntime {
|
|
|
365
832
|
lastActivityAt: this.lastActivityByAccount.get(acc) || null,
|
|
366
833
|
lastInboundAt: this.lastInboundByAccount.get(acc) || null,
|
|
367
834
|
lastOutboundAt: this.lastOutboundByAccount.get(acc) || null,
|
|
368
|
-
sessionRoutesCount: Array.from(this.sessionRoutes.values()).filter((v) => v.accountId === acc)
|
|
835
|
+
sessionRoutesCount: Array.from(this.sessionRoutes.values()).filter((v) => v.accountId === acc)
|
|
836
|
+
.length,
|
|
369
837
|
invalidOutboxSessionKeys: this.countInvalidOutboxSessionKeys(acc),
|
|
370
838
|
legacyAccountResidue: this.countLegacyAccountResidue(acc),
|
|
371
839
|
channelRoot: path.join(process.cwd(), 'plugins', 'bncr'),
|
|
@@ -386,11 +854,12 @@ class BncrBridgeRuntime {
|
|
|
386
854
|
if (!entry?.messageId) continue;
|
|
387
855
|
const accountId = normalizeAccountId(entry.accountId);
|
|
388
856
|
const sessionKey = asString(entry.sessionKey || '').trim();
|
|
389
|
-
const normalized = normalizeStoredSessionKey(sessionKey);
|
|
857
|
+
const normalized = normalizeStoredSessionKey(sessionKey, this.canonicalAgentId);
|
|
390
858
|
if (!normalized) continue;
|
|
391
859
|
|
|
392
860
|
const route = parseRouteLike(entry.route) || normalized.route;
|
|
393
|
-
const payload =
|
|
861
|
+
const payload =
|
|
862
|
+
entry.payload && typeof entry.payload === 'object' ? { ...entry.payload } : {};
|
|
394
863
|
(payload as any).sessionKey = normalized.sessionKey;
|
|
395
864
|
(payload as any).platform = route.platform;
|
|
396
865
|
(payload as any).groupId = route.groupId;
|
|
@@ -417,11 +886,12 @@ class BncrBridgeRuntime {
|
|
|
417
886
|
if (!entry?.messageId) continue;
|
|
418
887
|
const accountId = normalizeAccountId(entry.accountId);
|
|
419
888
|
const sessionKey = asString(entry.sessionKey || '').trim();
|
|
420
|
-
const normalized = normalizeStoredSessionKey(sessionKey);
|
|
889
|
+
const normalized = normalizeStoredSessionKey(sessionKey, this.canonicalAgentId);
|
|
421
890
|
if (!normalized) continue;
|
|
422
891
|
|
|
423
892
|
const route = parseRouteLike(entry.route) || normalized.route;
|
|
424
|
-
const payload =
|
|
893
|
+
const payload =
|
|
894
|
+
entry.payload && typeof entry.payload === 'object' ? { ...entry.payload } : {};
|
|
425
895
|
(payload as any).sessionKey = normalized.sessionKey;
|
|
426
896
|
(payload as any).platform = route.platform;
|
|
427
897
|
(payload as any).groupId = route.groupId;
|
|
@@ -444,7 +914,10 @@ class BncrBridgeRuntime {
|
|
|
444
914
|
this.sessionRoutes.clear();
|
|
445
915
|
this.routeAliases.clear();
|
|
446
916
|
for (const item of data.sessionRoutes || []) {
|
|
447
|
-
const normalized = normalizeStoredSessionKey(
|
|
917
|
+
const normalized = normalizeStoredSessionKey(
|
|
918
|
+
asString(item?.sessionKey || ''),
|
|
919
|
+
this.canonicalAgentId,
|
|
920
|
+
);
|
|
448
921
|
if (!normalized) continue;
|
|
449
922
|
|
|
450
923
|
const route = parseRouteLike(item?.route) || normalized.route;
|
|
@@ -464,7 +937,10 @@ class BncrBridgeRuntime {
|
|
|
464
937
|
this.lastSessionByAccount.clear();
|
|
465
938
|
for (const item of data.lastSessionByAccount || []) {
|
|
466
939
|
const accountId = normalizeAccountId(item?.accountId);
|
|
467
|
-
const normalized = normalizeStoredSessionKey(
|
|
940
|
+
const normalized = normalizeStoredSessionKey(
|
|
941
|
+
asString(item?.sessionKey || ''),
|
|
942
|
+
this.canonicalAgentId,
|
|
943
|
+
);
|
|
468
944
|
const updatedAt = Number(item?.updatedAt || 0);
|
|
469
945
|
if (!normalized || !Number.isFinite(updatedAt) || updatedAt <= 0) continue;
|
|
470
946
|
|
|
@@ -500,6 +976,39 @@ class BncrBridgeRuntime {
|
|
|
500
976
|
this.lastOutboundByAccount.set(accountId, updatedAt);
|
|
501
977
|
}
|
|
502
978
|
|
|
979
|
+
this.lastDriftSnapshot =
|
|
980
|
+
data.lastDriftSnapshot && typeof data.lastDriftSnapshot === 'object'
|
|
981
|
+
? {
|
|
982
|
+
capturedAt: Number((data.lastDriftSnapshot as any).capturedAt || 0),
|
|
983
|
+
registerCount: Number.isFinite(Number((data.lastDriftSnapshot as any).registerCount))
|
|
984
|
+
? Number((data.lastDriftSnapshot as any).registerCount)
|
|
985
|
+
: null,
|
|
986
|
+
apiGeneration: Number.isFinite(Number((data.lastDriftSnapshot as any).apiGeneration))
|
|
987
|
+
? Number((data.lastDriftSnapshot as any).apiGeneration)
|
|
988
|
+
: null,
|
|
989
|
+
postWarmupRegisterCount: Number.isFinite(
|
|
990
|
+
Number((data.lastDriftSnapshot as any).postWarmupRegisterCount),
|
|
991
|
+
)
|
|
992
|
+
? Number((data.lastDriftSnapshot as any).postWarmupRegisterCount)
|
|
993
|
+
: null,
|
|
994
|
+
apiInstanceId:
|
|
995
|
+
asString((data.lastDriftSnapshot as any).apiInstanceId || '').trim() || null,
|
|
996
|
+
registryFingerprint:
|
|
997
|
+
asString((data.lastDriftSnapshot as any).registryFingerprint || '').trim() || null,
|
|
998
|
+
dominantBucket:
|
|
999
|
+
asString((data.lastDriftSnapshot as any).dominantBucket || '').trim() || null,
|
|
1000
|
+
sourceBuckets:
|
|
1001
|
+
(data.lastDriftSnapshot as any).sourceBuckets &&
|
|
1002
|
+
typeof (data.lastDriftSnapshot as any).sourceBuckets === 'object'
|
|
1003
|
+
? { ...((data.lastDriftSnapshot as any).sourceBuckets as Record<string, number>) }
|
|
1004
|
+
: {},
|
|
1005
|
+
traceWindowSize: Number((data.lastDriftSnapshot as any).traceWindowSize || 0),
|
|
1006
|
+
traceRecent: Array.isArray((data.lastDriftSnapshot as any).traceRecent)
|
|
1007
|
+
? [...((data.lastDriftSnapshot as any).traceRecent as Array<Record<string, unknown>>)]
|
|
1008
|
+
: [],
|
|
1009
|
+
}
|
|
1010
|
+
: null;
|
|
1011
|
+
|
|
503
1012
|
// 兼容旧状态文件:若尚未持久化 lastSession*/lastActivity*,从 sessionRoutes 回填。
|
|
504
1013
|
if (this.lastSessionByAccount.size === 0 && this.sessionRoutes.size > 0) {
|
|
505
1014
|
for (const [sessionKey, info] of this.sessionRoutes.entries()) {
|
|
@@ -542,24 +1051,46 @@ class BncrBridgeRuntime {
|
|
|
542
1051
|
outbox: Array.from(this.outbox.values()),
|
|
543
1052
|
deadLetter: this.deadLetter.slice(-1000),
|
|
544
1053
|
sessionRoutes,
|
|
545
|
-
lastSessionByAccount: Array.from(this.lastSessionByAccount.entries()).map(
|
|
546
|
-
accountId,
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
accountId,
|
|
561
|
-
|
|
562
|
-
|
|
1054
|
+
lastSessionByAccount: Array.from(this.lastSessionByAccount.entries()).map(
|
|
1055
|
+
([accountId, v]) => ({
|
|
1056
|
+
accountId,
|
|
1057
|
+
sessionKey: v.sessionKey,
|
|
1058
|
+
scope: v.scope,
|
|
1059
|
+
updatedAt: v.updatedAt,
|
|
1060
|
+
}),
|
|
1061
|
+
),
|
|
1062
|
+
lastActivityByAccount: Array.from(this.lastActivityByAccount.entries()).map(
|
|
1063
|
+
([accountId, updatedAt]) => ({
|
|
1064
|
+
accountId,
|
|
1065
|
+
updatedAt,
|
|
1066
|
+
}),
|
|
1067
|
+
),
|
|
1068
|
+
lastInboundByAccount: Array.from(this.lastInboundByAccount.entries()).map(
|
|
1069
|
+
([accountId, updatedAt]) => ({
|
|
1070
|
+
accountId,
|
|
1071
|
+
updatedAt,
|
|
1072
|
+
}),
|
|
1073
|
+
),
|
|
1074
|
+
lastOutboundByAccount: Array.from(this.lastOutboundByAccount.entries()).map(
|
|
1075
|
+
([accountId, updatedAt]) => ({
|
|
1076
|
+
accountId,
|
|
1077
|
+
updatedAt,
|
|
1078
|
+
}),
|
|
1079
|
+
),
|
|
1080
|
+
lastDriftSnapshot: this.lastDriftSnapshot
|
|
1081
|
+
? {
|
|
1082
|
+
capturedAt: this.lastDriftSnapshot.capturedAt,
|
|
1083
|
+
registerCount: this.lastDriftSnapshot.registerCount,
|
|
1084
|
+
apiGeneration: this.lastDriftSnapshot.apiGeneration,
|
|
1085
|
+
postWarmupRegisterCount: this.lastDriftSnapshot.postWarmupRegisterCount,
|
|
1086
|
+
apiInstanceId: this.lastDriftSnapshot.apiInstanceId,
|
|
1087
|
+
registryFingerprint: this.lastDriftSnapshot.registryFingerprint,
|
|
1088
|
+
dominantBucket: this.lastDriftSnapshot.dominantBucket,
|
|
1089
|
+
sourceBuckets: { ...this.lastDriftSnapshot.sourceBuckets },
|
|
1090
|
+
traceWindowSize: this.lastDriftSnapshot.traceWindowSize,
|
|
1091
|
+
traceRecent: this.lastDriftSnapshot.traceRecent.map((trace) => ({ ...trace })),
|
|
1092
|
+
}
|
|
1093
|
+
: null,
|
|
563
1094
|
};
|
|
564
1095
|
|
|
565
1096
|
await writeJsonFileAtomically(this.statePath, data);
|
|
@@ -682,7 +1213,11 @@ class BncrBridgeRuntime {
|
|
|
682
1213
|
const filterAcc = accountId ? normalizeAccountId(accountId) : null;
|
|
683
1214
|
const targetAccounts = filterAcc
|
|
684
1215
|
? [filterAcc]
|
|
685
|
-
: Array.from(
|
|
1216
|
+
: Array.from(
|
|
1217
|
+
new Set(
|
|
1218
|
+
Array.from(this.outbox.values()).map((entry) => normalizeAccountId(entry.accountId)),
|
|
1219
|
+
),
|
|
1220
|
+
);
|
|
686
1221
|
if (BNCR_DEBUG_VERBOSE) {
|
|
687
1222
|
this.api.logger.info?.(
|
|
688
1223
|
`[bncr-outbox-flush] ${JSON.stringify({
|
|
@@ -761,10 +1296,14 @@ class BncrBridgeRuntime {
|
|
|
761
1296
|
if (!directPayloads.length) continue;
|
|
762
1297
|
|
|
763
1298
|
try {
|
|
764
|
-
ctx.broadcastToConnIds(
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
1299
|
+
ctx.broadcastToConnIds(
|
|
1300
|
+
BNCR_PUSH_EVENT,
|
|
1301
|
+
{
|
|
1302
|
+
forcePush: true,
|
|
1303
|
+
items: directPayloads,
|
|
1304
|
+
},
|
|
1305
|
+
new Set(directConnIds),
|
|
1306
|
+
);
|
|
768
1307
|
|
|
769
1308
|
const pushedIds = directPayloads
|
|
770
1309
|
.map((item: any) => asString(item?.messageId || item?.idempotencyKey || '').trim())
|
|
@@ -842,7 +1381,8 @@ class BncrBridgeRuntime {
|
|
|
842
1381
|
}
|
|
843
1382
|
|
|
844
1383
|
if (localNextDelay != null) {
|
|
845
|
-
globalNextDelay =
|
|
1384
|
+
globalNextDelay =
|
|
1385
|
+
globalNextDelay == null ? localNextDelay : Math.min(globalNextDelay, localNextDelay);
|
|
846
1386
|
}
|
|
847
1387
|
} finally {
|
|
848
1388
|
this.pushDrainRunningAccounts.delete(acc);
|
|
@@ -964,7 +1504,11 @@ class BncrBridgeRuntime {
|
|
|
964
1504
|
}
|
|
965
1505
|
|
|
966
1506
|
const curConn = this.connections.get(current);
|
|
967
|
-
if (
|
|
1507
|
+
if (
|
|
1508
|
+
!curConn ||
|
|
1509
|
+
t - curConn.lastSeenAt > CONNECT_TTL_MS ||
|
|
1510
|
+
nextConn.connectedAt >= curConn.connectedAt
|
|
1511
|
+
) {
|
|
968
1512
|
this.activeConnectionByAccount.set(acc, key);
|
|
969
1513
|
}
|
|
970
1514
|
}
|
|
@@ -1016,9 +1560,6 @@ class BncrBridgeRuntime {
|
|
|
1016
1560
|
const info = { accountId: acc, route, updatedAt: t };
|
|
1017
1561
|
|
|
1018
1562
|
this.sessionRoutes.set(key, info);
|
|
1019
|
-
// 同步维护旧格式与新格式,便于平滑切换
|
|
1020
|
-
this.sessionRoutes.set(buildFallbackSessionKey(route), info);
|
|
1021
|
-
|
|
1022
1563
|
this.routeAliases.set(routeKey(acc, route), info);
|
|
1023
1564
|
this.lastSessionByAccount.set(acc, {
|
|
1024
1565
|
sessionKey: key,
|
|
@@ -1048,7 +1589,10 @@ class BncrBridgeRuntime {
|
|
|
1048
1589
|
// 1) 标准 to 仅认 Bncr:<platform>:<groupId>:<userId> / Bncr:<platform>:<userId>
|
|
1049
1590
|
// 2) 仍接受 strict sessionKey 作为内部兼容输入
|
|
1050
1591
|
// 3) 其他旧格式直接失败,并输出标准格式提示日志
|
|
1051
|
-
private resolveVerifiedTarget(
|
|
1592
|
+
private resolveVerifiedTarget(
|
|
1593
|
+
rawTarget: string,
|
|
1594
|
+
accountId: string,
|
|
1595
|
+
): { sessionKey: string; route: BncrRoute; displayScope: string } {
|
|
1052
1596
|
const acc = normalizeAccountId(accountId);
|
|
1053
1597
|
const raw = asString(rawTarget).trim();
|
|
1054
1598
|
if (!raw) throw new Error('bncr invalid target(empty)');
|
|
@@ -1068,17 +1612,23 @@ class BncrBridgeRuntime {
|
|
|
1068
1612
|
|
|
1069
1613
|
if (!route) {
|
|
1070
1614
|
this.api.logger.warn?.(
|
|
1071
|
-
`[bncr-target-invalid] raw=${raw} accountId=${acc} reason=unparseable-or-unknown standardTo=Bncr:<platform>:<groupId>:<userId>|Bncr:<platform>:<userId> standardSessionKey=agent
|
|
1615
|
+
`[bncr-target-invalid] raw=${raw} accountId=${acc} reason=unparseable-or-unknown standardTo=Bncr:<platform>:<groupId>:<userId>|Bncr:<platform>:<userId> standardSessionKey=agent:<agentId>:bncr:direct:<hex(scope)>`,
|
|
1616
|
+
);
|
|
1617
|
+
throw new Error(
|
|
1618
|
+
`bncr invalid target(standard: Bncr:<platform>:<groupId>:<userId> | Bncr:<platform>:<userId>): ${raw}`,
|
|
1072
1619
|
);
|
|
1073
|
-
throw new Error(`bncr invalid target(standard: Bncr:<platform>:<groupId>:<userId> | Bncr:<platform>:<userId>): ${raw}`);
|
|
1074
1620
|
}
|
|
1075
1621
|
|
|
1076
1622
|
const wantedRouteKey = routeKey(acc, route);
|
|
1077
1623
|
let best: { sessionKey: string; route: BncrRoute; updatedAt: number } | null = null;
|
|
1078
1624
|
|
|
1079
1625
|
if (BNCR_DEBUG_VERBOSE) {
|
|
1080
|
-
this.api.logger.info?.(
|
|
1081
|
-
|
|
1626
|
+
this.api.logger.info?.(
|
|
1627
|
+
`[bncr-target-incoming-route] raw=${raw} accountId=${acc} route=${JSON.stringify(route)}`,
|
|
1628
|
+
);
|
|
1629
|
+
this.api.logger.info?.(
|
|
1630
|
+
`[bncr-target-incoming-sessionRoutes] raw=${raw} accountId=${acc} sessionRoutes=${JSON.stringify(this.sessionRoutes.entries())}`,
|
|
1631
|
+
);
|
|
1082
1632
|
}
|
|
1083
1633
|
|
|
1084
1634
|
for (const [key, info] of this.sessionRoutes.entries()) {
|
|
@@ -1090,29 +1640,40 @@ class BncrBridgeRuntime {
|
|
|
1090
1640
|
const updatedAt = Number(info.updatedAt || 0);
|
|
1091
1641
|
if (!best || updatedAt >= best.updatedAt) {
|
|
1092
1642
|
best = {
|
|
1093
|
-
sessionKey:
|
|
1643
|
+
sessionKey: key,
|
|
1094
1644
|
route: parsed.route,
|
|
1095
1645
|
updatedAt,
|
|
1096
1646
|
};
|
|
1097
1647
|
}
|
|
1098
1648
|
}
|
|
1099
1649
|
|
|
1100
|
-
// 直接根据raw生成标准sessionkey
|
|
1101
1650
|
if (!best) {
|
|
1102
1651
|
const updatedAt = 0;
|
|
1652
|
+
const canonicalAgentId =
|
|
1653
|
+
this.canonicalAgentId ||
|
|
1654
|
+
this.ensureCanonicalAgentId({
|
|
1655
|
+
cfg: this.api.runtime.config?.get?.() || {},
|
|
1656
|
+
accountId: acc,
|
|
1657
|
+
channelId: CHANNEL_ID,
|
|
1658
|
+
peer: { kind: 'direct', id: route.groupId === '0' ? route.userId : route.groupId },
|
|
1659
|
+
});
|
|
1103
1660
|
best = {
|
|
1104
|
-
sessionKey:
|
|
1661
|
+
sessionKey: buildCanonicalBncrSessionKey(route, canonicalAgentId),
|
|
1105
1662
|
route,
|
|
1106
1663
|
updatedAt,
|
|
1107
1664
|
};
|
|
1108
1665
|
}
|
|
1109
1666
|
|
|
1110
1667
|
if (BNCR_DEBUG_VERBOSE) {
|
|
1111
|
-
this.api.logger.info?.(
|
|
1668
|
+
this.api.logger.info?.(
|
|
1669
|
+
`[bncr-target-incoming-best] raw=${raw} accountId=${acc} best=${JSON.stringify(best)}`,
|
|
1670
|
+
);
|
|
1112
1671
|
}
|
|
1113
1672
|
|
|
1114
1673
|
if (!best) {
|
|
1115
|
-
this.api.logger.warn?.(
|
|
1674
|
+
this.api.logger.warn?.(
|
|
1675
|
+
`[bncr-target-miss] raw=${raw} accountId=${acc} sessionRoutes=${this.sessionRoutes.size}`,
|
|
1676
|
+
);
|
|
1116
1677
|
throw new Error(`bncr target not found in known sessions: ${raw}`);
|
|
1117
1678
|
}
|
|
1118
1679
|
|
|
@@ -1140,11 +1701,19 @@ class BncrBridgeRuntime {
|
|
|
1140
1701
|
return `${transferId}|${stage}|${idx}`;
|
|
1141
1702
|
}
|
|
1142
1703
|
|
|
1143
|
-
private waitForFileAck(params: {
|
|
1704
|
+
private waitForFileAck(params: {
|
|
1705
|
+
transferId: string;
|
|
1706
|
+
stage: string;
|
|
1707
|
+
chunkIndex?: number;
|
|
1708
|
+
timeoutMs?: number;
|
|
1709
|
+
}) {
|
|
1144
1710
|
const transferId = asString(params.transferId).trim();
|
|
1145
1711
|
const stage = asString(params.stage).trim();
|
|
1146
1712
|
const key = this.fileAckKey(transferId, stage, params.chunkIndex);
|
|
1147
|
-
const timeoutMs = Math.max(
|
|
1713
|
+
const timeoutMs = Math.max(
|
|
1714
|
+
1_000,
|
|
1715
|
+
Math.min(Number(params.timeoutMs || FILE_ACK_TIMEOUT_MS), 120_000),
|
|
1716
|
+
);
|
|
1148
1717
|
|
|
1149
1718
|
return new Promise<Record<string, unknown>>((resolve, reject) => {
|
|
1150
1719
|
const timer = setTimeout(() => {
|
|
@@ -1155,7 +1724,13 @@ class BncrBridgeRuntime {
|
|
|
1155
1724
|
});
|
|
1156
1725
|
}
|
|
1157
1726
|
|
|
1158
|
-
private resolveFileAck(params: {
|
|
1727
|
+
private resolveFileAck(params: {
|
|
1728
|
+
transferId: string;
|
|
1729
|
+
stage: string;
|
|
1730
|
+
chunkIndex?: number;
|
|
1731
|
+
payload: Record<string, unknown>;
|
|
1732
|
+
ok: boolean;
|
|
1733
|
+
}) {
|
|
1159
1734
|
const transferId = asString(params.transferId).trim();
|
|
1160
1735
|
const stage = asString(params.stage).trim();
|
|
1161
1736
|
const key = this.fileAckKey(transferId, stage, params.chunkIndex);
|
|
@@ -1164,11 +1739,20 @@ class BncrBridgeRuntime {
|
|
|
1164
1739
|
this.fileAckWaiters.delete(key);
|
|
1165
1740
|
clearTimeout(waiter.timer);
|
|
1166
1741
|
if (params.ok) waiter.resolve(params.payload);
|
|
1167
|
-
else
|
|
1742
|
+
else
|
|
1743
|
+
waiter.reject(
|
|
1744
|
+
new Error(
|
|
1745
|
+
asString(params.payload?.errorMessage || params.payload?.error || 'file ack failed'),
|
|
1746
|
+
),
|
|
1747
|
+
);
|
|
1168
1748
|
return true;
|
|
1169
1749
|
}
|
|
1170
1750
|
|
|
1171
|
-
private pushFileEventToAccount(
|
|
1751
|
+
private pushFileEventToAccount(
|
|
1752
|
+
accountId: string,
|
|
1753
|
+
event: string,
|
|
1754
|
+
payload: Record<string, unknown>,
|
|
1755
|
+
) {
|
|
1172
1756
|
const connIds = this.resolvePushConnIds(accountId);
|
|
1173
1757
|
if (!connIds.size || !this.gatewayContext) {
|
|
1174
1758
|
throw new Error(`no active bncr connection for account=${accountId}`);
|
|
@@ -1191,7 +1775,9 @@ class BncrBridgeRuntime {
|
|
|
1191
1775
|
return dir;
|
|
1192
1776
|
}
|
|
1193
1777
|
|
|
1194
|
-
private async materializeRecvTransfer(
|
|
1778
|
+
private async materializeRecvTransfer(
|
|
1779
|
+
st: FileRecvTransferState,
|
|
1780
|
+
): Promise<{ path: string; fileSha256: string }> {
|
|
1195
1781
|
const dir = this.resolveInboundFilesDir();
|
|
1196
1782
|
const safeName = asString(st.fileName).trim() || `${st.transferId}.bin`;
|
|
1197
1783
|
const finalPath = path.join(dir, safeName);
|
|
@@ -1233,7 +1819,8 @@ class BncrBridgeRuntime {
|
|
|
1233
1819
|
lastActivityAt: this.lastActivityByAccount.get(acc) || null,
|
|
1234
1820
|
lastInboundAt: this.lastInboundByAccount.get(acc) || null,
|
|
1235
1821
|
lastOutboundAt: this.lastOutboundByAccount.get(acc) || null,
|
|
1236
|
-
sessionRoutesCount: Array.from(this.sessionRoutes.values()).filter((v) => v.accountId === acc)
|
|
1822
|
+
sessionRoutesCount: Array.from(this.sessionRoutes.values()).filter((v) => v.accountId === acc)
|
|
1823
|
+
.length,
|
|
1237
1824
|
invalidOutboxSessionKeys: this.countInvalidOutboxSessionKeys(acc),
|
|
1238
1825
|
legacyAccountResidue: this.countLegacyAccountResidue(acc),
|
|
1239
1826
|
channelRoot: path.join(process.cwd(), 'plugins', 'bncr'),
|
|
@@ -1257,7 +1844,8 @@ class BncrBridgeRuntime {
|
|
|
1257
1844
|
lastActivityAt: this.lastActivityByAccount.get(acc) || null,
|
|
1258
1845
|
lastInboundAt: this.lastInboundByAccount.get(acc) || null,
|
|
1259
1846
|
lastOutboundAt: this.lastOutboundByAccount.get(acc) || null,
|
|
1260
|
-
sessionRoutesCount: Array.from(this.sessionRoutes.values()).filter((v) => v.accountId === acc)
|
|
1847
|
+
sessionRoutesCount: Array.from(this.sessionRoutes.values()).filter((v) => v.accountId === acc)
|
|
1848
|
+
.length,
|
|
1261
1849
|
invalidOutboxSessionKeys: this.countInvalidOutboxSessionKeys(acc),
|
|
1262
1850
|
legacyAccountResidue: this.countLegacyAccountResidue(acc),
|
|
1263
1851
|
running: true,
|
|
@@ -1282,7 +1870,8 @@ class BncrBridgeRuntime {
|
|
|
1282
1870
|
lastActivityAt: this.lastActivityByAccount.get(acc) || null,
|
|
1283
1871
|
lastInboundAt: this.lastInboundByAccount.get(acc) || null,
|
|
1284
1872
|
lastOutboundAt: this.lastOutboundByAccount.get(acc) || null,
|
|
1285
|
-
sessionRoutesCount: Array.from(this.sessionRoutes.values()).filter((v) => v.accountId === acc)
|
|
1873
|
+
sessionRoutesCount: Array.from(this.sessionRoutes.values()).filter((v) => v.accountId === acc)
|
|
1874
|
+
.length,
|
|
1286
1875
|
invalidOutboxSessionKeys: this.countInvalidOutboxSessionKeys(acc),
|
|
1287
1876
|
legacyAccountResidue: this.countLegacyAccountResidue(acc),
|
|
1288
1877
|
channelRoot: path.join(process.cwd(), 'plugins', 'bncr'),
|
|
@@ -1407,7 +1996,10 @@ class BncrBridgeRuntime {
|
|
|
1407
1996
|
timeoutMs?: number;
|
|
1408
1997
|
}): Promise<void> {
|
|
1409
1998
|
const { transferId, chunkIndex } = params;
|
|
1410
|
-
const timeoutMs = Math.max(
|
|
1999
|
+
const timeoutMs = Math.max(
|
|
2000
|
+
1_000,
|
|
2001
|
+
Math.min(Number(params.timeoutMs || FILE_TRANSFER_ACK_TTL_MS), 60_000),
|
|
2002
|
+
);
|
|
1411
2003
|
const started = now();
|
|
1412
2004
|
|
|
1413
2005
|
return new Promise<void>((resolve, reject) => {
|
|
@@ -1476,7 +2068,13 @@ class BncrBridgeRuntime {
|
|
|
1476
2068
|
route: BncrRoute;
|
|
1477
2069
|
mediaUrl: string;
|
|
1478
2070
|
mediaLocalRoots?: readonly string[];
|
|
1479
|
-
}): Promise<{
|
|
2071
|
+
}): Promise<{
|
|
2072
|
+
mode: 'base64' | 'chunk';
|
|
2073
|
+
mimeType?: string;
|
|
2074
|
+
fileName?: string;
|
|
2075
|
+
mediaBase64?: string;
|
|
2076
|
+
path?: string;
|
|
2077
|
+
}> {
|
|
1480
2078
|
const loaded = await this.api.runtime.media.loadWebMedia(params.mediaUrl, {
|
|
1481
2079
|
localRoots: params.mediaLocalRoots,
|
|
1482
2080
|
maxBytes: 50 * 1024 * 1024,
|
|
@@ -1528,21 +2126,25 @@ class BncrBridgeRuntime {
|
|
|
1528
2126
|
};
|
|
1529
2127
|
this.fileSendTransfers.set(transferId, st);
|
|
1530
2128
|
|
|
1531
|
-
ctx.broadcastToConnIds(
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
2129
|
+
ctx.broadcastToConnIds(
|
|
2130
|
+
'bncr.file.init',
|
|
2131
|
+
{
|
|
2132
|
+
transferId,
|
|
2133
|
+
direction: 'oc2bncr',
|
|
2134
|
+
sessionKey: params.sessionKey,
|
|
2135
|
+
platform: params.route.platform,
|
|
2136
|
+
groupId: params.route.groupId,
|
|
2137
|
+
userId: params.route.userId,
|
|
2138
|
+
fileName,
|
|
2139
|
+
mimeType,
|
|
2140
|
+
fileSize: size,
|
|
2141
|
+
chunkSize,
|
|
2142
|
+
totalChunks,
|
|
2143
|
+
fileSha256,
|
|
2144
|
+
ts: now(),
|
|
2145
|
+
},
|
|
2146
|
+
connIds,
|
|
2147
|
+
);
|
|
1546
2148
|
|
|
1547
2149
|
// 逐块发送并等待 ACK
|
|
1548
2150
|
for (let idx = 0; idx < totalChunks; idx++) {
|
|
@@ -1554,18 +2156,26 @@ class BncrBridgeRuntime {
|
|
|
1554
2156
|
let ok = false;
|
|
1555
2157
|
let lastErr: unknown = null;
|
|
1556
2158
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
1557
|
-
ctx.broadcastToConnIds(
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
2159
|
+
ctx.broadcastToConnIds(
|
|
2160
|
+
'bncr.file.chunk',
|
|
2161
|
+
{
|
|
2162
|
+
transferId,
|
|
2163
|
+
chunkIndex: idx,
|
|
2164
|
+
offset: start,
|
|
2165
|
+
size: slice.byteLength,
|
|
2166
|
+
chunkSha256,
|
|
2167
|
+
base64: slice.toString('base64'),
|
|
2168
|
+
ts: now(),
|
|
2169
|
+
},
|
|
2170
|
+
connIds,
|
|
2171
|
+
);
|
|
1566
2172
|
|
|
1567
2173
|
try {
|
|
1568
|
-
await this.waitChunkAck({
|
|
2174
|
+
await this.waitChunkAck({
|
|
2175
|
+
transferId,
|
|
2176
|
+
chunkIndex: idx,
|
|
2177
|
+
timeoutMs: FILE_TRANSFER_ACK_TTL_MS,
|
|
2178
|
+
});
|
|
1569
2179
|
ok = true;
|
|
1570
2180
|
break;
|
|
1571
2181
|
} catch (err) {
|
|
@@ -1578,19 +2188,27 @@ class BncrBridgeRuntime {
|
|
|
1578
2188
|
st.status = 'aborted';
|
|
1579
2189
|
st.error = String((lastErr as any)?.message || lastErr || `chunk-${idx}-failed`);
|
|
1580
2190
|
this.fileSendTransfers.set(transferId, st);
|
|
1581
|
-
ctx.broadcastToConnIds(
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
2191
|
+
ctx.broadcastToConnIds(
|
|
2192
|
+
'bncr.file.abort',
|
|
2193
|
+
{
|
|
2194
|
+
transferId,
|
|
2195
|
+
reason: st.error,
|
|
2196
|
+
ts: now(),
|
|
2197
|
+
},
|
|
2198
|
+
connIds,
|
|
2199
|
+
);
|
|
1586
2200
|
throw new Error(st.error);
|
|
1587
2201
|
}
|
|
1588
2202
|
}
|
|
1589
2203
|
|
|
1590
|
-
ctx.broadcastToConnIds(
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
2204
|
+
ctx.broadcastToConnIds(
|
|
2205
|
+
'bncr.file.complete',
|
|
2206
|
+
{
|
|
2207
|
+
transferId,
|
|
2208
|
+
ts: now(),
|
|
2209
|
+
},
|
|
2210
|
+
connIds,
|
|
2211
|
+
);
|
|
1594
2212
|
|
|
1595
2213
|
const done = await this.waitCompleteAck({ transferId, timeoutMs: 60_000 });
|
|
1596
2214
|
|
|
@@ -1606,7 +2224,14 @@ class BncrBridgeRuntime {
|
|
|
1606
2224
|
accountId: string;
|
|
1607
2225
|
sessionKey: string;
|
|
1608
2226
|
route: BncrRoute;
|
|
1609
|
-
payload: {
|
|
2227
|
+
payload: {
|
|
2228
|
+
text?: string;
|
|
2229
|
+
mediaUrl?: string;
|
|
2230
|
+
mediaUrls?: string[];
|
|
2231
|
+
asVoice?: boolean;
|
|
2232
|
+
audioAsVoice?: boolean;
|
|
2233
|
+
kind?: 'block' | 'final';
|
|
2234
|
+
};
|
|
1610
2235
|
mediaLocalRoots?: readonly string[];
|
|
1611
2236
|
}) {
|
|
1612
2237
|
const { accountId, sessionKey, route, payload, mediaLocalRoots } = params;
|
|
@@ -1718,6 +2343,7 @@ class BncrBridgeRuntime {
|
|
|
1718
2343
|
this.markSeen(accountId, connId, clientId);
|
|
1719
2344
|
this.markActivity(accountId);
|
|
1720
2345
|
this.incrementCounter(this.connectEventsByAccount, accountId);
|
|
2346
|
+
const lease = this.acceptConnection();
|
|
1721
2347
|
|
|
1722
2348
|
respond(true, {
|
|
1723
2349
|
channel: CHANNEL_ID,
|
|
@@ -1729,7 +2355,13 @@ class BncrBridgeRuntime {
|
|
|
1729
2355
|
activeConnections: this.activeConnectionCount(accountId),
|
|
1730
2356
|
pending: Array.from(this.outbox.values()).filter((v) => v.accountId === accountId).length,
|
|
1731
2357
|
deadLetter: this.deadLetter.filter((v) => v.accountId === accountId).length,
|
|
1732
|
-
diagnostics: this.
|
|
2358
|
+
diagnostics: this.buildExtendedDiagnostics(accountId),
|
|
2359
|
+
leaseId: lease.leaseId,
|
|
2360
|
+
connectionEpoch: lease.connectionEpoch,
|
|
2361
|
+
protocolVersion: 2,
|
|
2362
|
+
acceptedAt: lease.acceptedAt,
|
|
2363
|
+
serverPid: this.gatewayPid,
|
|
2364
|
+
bridgeId: this.bridgeId,
|
|
1733
2365
|
now: now(),
|
|
1734
2366
|
});
|
|
1735
2367
|
|
|
@@ -1743,6 +2375,8 @@ class BncrBridgeRuntime {
|
|
|
1743
2375
|
const clientId = asString((params as any)?.clientId || '').trim() || undefined;
|
|
1744
2376
|
this.rememberGatewayContext(context);
|
|
1745
2377
|
this.markSeen(accountId, connId, clientId);
|
|
2378
|
+
this.observeLease('ack', params ?? {});
|
|
2379
|
+
this.lastAckAtGlobal = now();
|
|
1746
2380
|
this.incrementCounter(this.ackEventsByAccount, accountId);
|
|
1747
2381
|
|
|
1748
2382
|
const messageId = asString(params?.messageId || '').trim();
|
|
@@ -1802,6 +2436,8 @@ class BncrBridgeRuntime {
|
|
|
1802
2436
|
const accountId = normalizeAccountId(asString(params?.accountId || ''));
|
|
1803
2437
|
const connId = asString(client?.connId || '').trim() || `no-conn-${Date.now()}`;
|
|
1804
2438
|
const clientId = asString((params as any)?.clientId || '').trim() || undefined;
|
|
2439
|
+
this.observeLease('activity', params ?? {});
|
|
2440
|
+
this.lastActivityAtGlobal = now();
|
|
1805
2441
|
if (BNCR_DEBUG_VERBOSE) {
|
|
1806
2442
|
this.api.logger.info?.(
|
|
1807
2443
|
`[bncr-activity] ${JSON.stringify({
|
|
@@ -1834,7 +2470,7 @@ class BncrBridgeRuntime {
|
|
|
1834
2470
|
const accountId = normalizeAccountId(asString(params?.accountId || ''));
|
|
1835
2471
|
const cfg = await this.api.runtime.config.loadConfig();
|
|
1836
2472
|
const runtime = this.getAccountRuntimeSnapshot(accountId);
|
|
1837
|
-
const diagnostics = this.
|
|
2473
|
+
const diagnostics = this.buildExtendedDiagnostics(accountId);
|
|
1838
2474
|
const permissions = buildBncrPermissionSummary(cfg ?? {});
|
|
1839
2475
|
const probe = probeBncrAccount({
|
|
1840
2476
|
accountId,
|
|
@@ -1869,6 +2505,7 @@ class BncrBridgeRuntime {
|
|
|
1869
2505
|
const clientId = asString((params as any)?.clientId || '').trim() || undefined;
|
|
1870
2506
|
this.rememberGatewayContext(context);
|
|
1871
2507
|
this.markSeen(accountId, connId, clientId);
|
|
2508
|
+
this.observeLease('file.init', params ?? {});
|
|
1872
2509
|
this.markActivity(accountId);
|
|
1873
2510
|
|
|
1874
2511
|
const transferId = asString(params?.transferId || '').trim();
|
|
@@ -1902,11 +2539,12 @@ class BncrBridgeRuntime {
|
|
|
1902
2539
|
return;
|
|
1903
2540
|
}
|
|
1904
2541
|
|
|
1905
|
-
const route =
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
2542
|
+
const route =
|
|
2543
|
+
parseRouteLike({
|
|
2544
|
+
platform: asString(params?.platform || normalized.route.platform),
|
|
2545
|
+
groupId: asString(params?.groupId || normalized.route.groupId),
|
|
2546
|
+
userId: asString(params?.userId || normalized.route.userId),
|
|
2547
|
+
}) || normalized.route;
|
|
1910
2548
|
|
|
1911
2549
|
this.fileRecvTransfers.set(transferId, {
|
|
1912
2550
|
transferId,
|
|
@@ -1938,6 +2576,7 @@ class BncrBridgeRuntime {
|
|
|
1938
2576
|
const clientId = asString((params as any)?.clientId || '').trim() || undefined;
|
|
1939
2577
|
this.rememberGatewayContext(context);
|
|
1940
2578
|
this.markSeen(accountId, connId, clientId);
|
|
2579
|
+
this.observeLease('file.chunk', params ?? {});
|
|
1941
2580
|
this.markActivity(accountId);
|
|
1942
2581
|
|
|
1943
2582
|
const transferId = asString(params?.transferId || '').trim();
|
|
@@ -1985,12 +2624,18 @@ class BncrBridgeRuntime {
|
|
|
1985
2624
|
}
|
|
1986
2625
|
};
|
|
1987
2626
|
|
|
1988
|
-
handleFileComplete = async ({
|
|
2627
|
+
handleFileComplete = async ({
|
|
2628
|
+
params,
|
|
2629
|
+
respond,
|
|
2630
|
+
client,
|
|
2631
|
+
context,
|
|
2632
|
+
}: GatewayRequestHandlerOptions) => {
|
|
1989
2633
|
const accountId = normalizeAccountId(asString(params?.accountId || ''));
|
|
1990
2634
|
const connId = asString(client?.connId || '').trim() || `no-conn-${Date.now()}`;
|
|
1991
2635
|
const clientId = asString((params as any)?.clientId || '').trim() || undefined;
|
|
1992
2636
|
this.rememberGatewayContext(context);
|
|
1993
2637
|
this.markSeen(accountId, connId, clientId);
|
|
2638
|
+
this.observeLease('file.complete', params ?? {});
|
|
1994
2639
|
this.markActivity(accountId);
|
|
1995
2640
|
|
|
1996
2641
|
const transferId = asString(params?.transferId || '').trim();
|
|
@@ -2007,10 +2652,14 @@ class BncrBridgeRuntime {
|
|
|
2007
2652
|
|
|
2008
2653
|
try {
|
|
2009
2654
|
if (st.receivedChunks.size < st.totalChunks) {
|
|
2010
|
-
throw new Error(
|
|
2655
|
+
throw new Error(
|
|
2656
|
+
`chunk not complete received=${st.receivedChunks.size} total=${st.totalChunks}`,
|
|
2657
|
+
);
|
|
2011
2658
|
}
|
|
2012
2659
|
|
|
2013
|
-
const ordered = Array.from(st.bufferByChunk.entries())
|
|
2660
|
+
const ordered = Array.from(st.bufferByChunk.entries())
|
|
2661
|
+
.sort((a, b) => a[0] - b[0])
|
|
2662
|
+
.map((x) => x[1]);
|
|
2014
2663
|
const merged = Buffer.concat(ordered);
|
|
2015
2664
|
if (st.fileSize > 0 && merged.length !== st.fileSize) {
|
|
2016
2665
|
throw new Error(`file size mismatch expected=${st.fileSize} got=${merged.length}`);
|
|
@@ -2054,6 +2703,7 @@ class BncrBridgeRuntime {
|
|
|
2054
2703
|
const clientId = asString((params as any)?.clientId || '').trim() || undefined;
|
|
2055
2704
|
this.rememberGatewayContext(context);
|
|
2056
2705
|
this.markSeen(accountId, connId, clientId);
|
|
2706
|
+
this.observeLease('file.abort', params ?? {});
|
|
2057
2707
|
this.markActivity(accountId);
|
|
2058
2708
|
|
|
2059
2709
|
const transferId = asString(params?.transferId || '').trim();
|
|
@@ -2144,12 +2794,31 @@ class BncrBridgeRuntime {
|
|
|
2144
2794
|
|
|
2145
2795
|
handleInbound = async ({ params, respond, client, context }: GatewayRequestHandlerOptions) => {
|
|
2146
2796
|
const parsed = parseBncrInboundParams(params);
|
|
2147
|
-
const {
|
|
2797
|
+
const {
|
|
2798
|
+
accountId,
|
|
2799
|
+
platform,
|
|
2800
|
+
groupId,
|
|
2801
|
+
userId,
|
|
2802
|
+
sessionKeyfromroute,
|
|
2803
|
+
route,
|
|
2804
|
+
text,
|
|
2805
|
+
msgType,
|
|
2806
|
+
mediaBase64,
|
|
2807
|
+
mediaPathFromTransfer,
|
|
2808
|
+
mimeType,
|
|
2809
|
+
fileName,
|
|
2810
|
+
msgId,
|
|
2811
|
+
dedupKey,
|
|
2812
|
+
peer,
|
|
2813
|
+
extracted,
|
|
2814
|
+
} = parsed;
|
|
2148
2815
|
const connId = asString(client?.connId || '').trim() || `no-conn-${Date.now()}`;
|
|
2149
2816
|
const clientId = asString((params as any)?.clientId || '').trim() || undefined;
|
|
2150
2817
|
this.rememberGatewayContext(context);
|
|
2151
2818
|
this.markSeen(accountId, connId, clientId);
|
|
2819
|
+
this.observeLease('inbound', params ?? {});
|
|
2152
2820
|
this.markActivity(accountId);
|
|
2821
|
+
this.lastInboundAtGlobal = now();
|
|
2153
2822
|
this.incrementCounter(this.inboundEventsByAccount, accountId);
|
|
2154
2823
|
|
|
2155
2824
|
if (!platform || (!userId && !groupId)) {
|
|
@@ -2182,13 +2851,21 @@ class BncrBridgeRuntime {
|
|
|
2182
2851
|
return;
|
|
2183
2852
|
}
|
|
2184
2853
|
|
|
2185
|
-
const
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2854
|
+
const canonicalAgentId = this.ensureCanonicalAgentId({
|
|
2855
|
+
cfg,
|
|
2856
|
+
accountId,
|
|
2857
|
+
peer,
|
|
2858
|
+
channelId: CHANNEL_ID,
|
|
2859
|
+
});
|
|
2860
|
+
const resolvedRoute = this.api.runtime.channel.routing.resolveAgentRoute({
|
|
2861
|
+
cfg,
|
|
2862
|
+
channel: CHANNEL_ID,
|
|
2863
|
+
accountId,
|
|
2864
|
+
peer,
|
|
2865
|
+
});
|
|
2866
|
+
const baseSessionKey =
|
|
2867
|
+
normalizeInboundSessionKey(sessionKeyfromroute, route, canonicalAgentId) ||
|
|
2868
|
+
resolvedRoute.sessionKey;
|
|
2192
2869
|
const taskSessionKey = withTaskSessionKey(baseSessionKey, extracted.taskKey);
|
|
2193
2870
|
const sessionKey = taskSessionKey || baseSessionKey;
|
|
2194
2871
|
|
|
@@ -2205,7 +2882,9 @@ class BncrBridgeRuntime {
|
|
|
2205
2882
|
channelId: CHANNEL_ID,
|
|
2206
2883
|
cfg,
|
|
2207
2884
|
parsed,
|
|
2208
|
-
|
|
2885
|
+
canonicalAgentId,
|
|
2886
|
+
rememberSessionRoute: (sessionKey, accountId, route) =>
|
|
2887
|
+
this.rememberSessionRoute(sessionKey, accountId, route),
|
|
2209
2888
|
enqueueFromReply: (args) => this.enqueueFromReply(args),
|
|
2210
2889
|
setInboundActivity: (accountId, at) => {
|
|
2211
2890
|
this.lastInboundByAccount.set(accountId, at);
|
|
@@ -2291,7 +2970,8 @@ class BncrBridgeRuntime {
|
|
|
2291
2970
|
text: asString(ctx.text || ''),
|
|
2292
2971
|
mediaLocalRoots: ctx.mediaLocalRoots,
|
|
2293
2972
|
resolveVerifiedTarget: (to, accountId) => this.resolveVerifiedTarget(to, accountId),
|
|
2294
|
-
rememberSessionRoute: (sessionKey, accountId, route) =>
|
|
2973
|
+
rememberSessionRoute: (sessionKey, accountId, route) =>
|
|
2974
|
+
this.rememberSessionRoute(sessionKey, accountId, route),
|
|
2295
2975
|
enqueueFromReply: (args) => this.enqueueFromReply(args),
|
|
2296
2976
|
createMessageId: () => randomUUID(),
|
|
2297
2977
|
});
|
|
@@ -2335,7 +3015,8 @@ class BncrBridgeRuntime {
|
|
|
2335
3015
|
audioAsVoice,
|
|
2336
3016
|
mediaLocalRoots: ctx.mediaLocalRoots,
|
|
2337
3017
|
resolveVerifiedTarget: (to, accountId) => this.resolveVerifiedTarget(to, accountId),
|
|
2338
|
-
rememberSessionRoute: (sessionKey, accountId, route) =>
|
|
3018
|
+
rememberSessionRoute: (sessionKey, accountId, route) =>
|
|
3019
|
+
this.rememberSessionRoute(sessionKey, accountId, route),
|
|
2339
3020
|
enqueueFromReply: (args) => this.enqueueFromReply(args),
|
|
2340
3021
|
createMessageId: () => randomUUID(),
|
|
2341
3022
|
});
|
|
@@ -2350,10 +3031,13 @@ export function createBncrChannelPlugin(bridge: BncrBridgeRuntime) {
|
|
|
2350
3031
|
const messageActions: ChannelMessageActionAdapter = {
|
|
2351
3032
|
describeMessageTool: ({ cfg }) => {
|
|
2352
3033
|
const channelCfg = cfg?.channels?.[CHANNEL_ID];
|
|
2353
|
-
const hasExplicitConfiguredAccount =
|
|
2354
|
-
&&
|
|
2355
|
-
|
|
2356
|
-
|
|
3034
|
+
const hasExplicitConfiguredAccount =
|
|
3035
|
+
Boolean(channelCfg && typeof channelCfg === 'object') &&
|
|
3036
|
+
resolveBncrChannelPolicy(channelCfg).enabled !== false &&
|
|
3037
|
+
Boolean(channelCfg.accounts && typeof channelCfg.accounts === 'object') &&
|
|
3038
|
+
Object.keys(channelCfg.accounts).some(
|
|
3039
|
+
(accountId) => resolveAccount(cfg, accountId).enabled !== false,
|
|
3040
|
+
);
|
|
2357
3041
|
|
|
2358
3042
|
const hasConnectedRuntime = listAccountIds(cfg).some((accountId) => {
|
|
2359
3043
|
const resolved = resolveAccount(cfg, accountId);
|
|
@@ -2373,7 +3057,8 @@ export function createBncrChannelPlugin(bridge: BncrBridgeRuntime) {
|
|
|
2373
3057
|
supportsAction: ({ action }) => action === 'send',
|
|
2374
3058
|
extractToolSend: ({ args }) => extractToolSend(args, 'sendMessage'),
|
|
2375
3059
|
handleAction: async ({ action, params, accountId, mediaLocalRoots }) => {
|
|
2376
|
-
if (action !== 'send')
|
|
3060
|
+
if (action !== 'send')
|
|
3061
|
+
throw new Error(`Action ${action} is not supported for provider ${CHANNEL_ID}.`);
|
|
2377
3062
|
const to = readStringParam(params, 'to', { required: true });
|
|
2378
3063
|
const message = readStringParam(params, 'message', { allowEmpty: true }) ?? '';
|
|
2379
3064
|
const caption = readStringParam(params, 'caption', { allowEmpty: true }) ?? '';
|
|
@@ -2385,36 +3070,40 @@ export function createBncrChannelPlugin(bridge: BncrBridgeRuntime) {
|
|
|
2385
3070
|
readStringParam(params, 'mediaUrl', { trim: false });
|
|
2386
3071
|
const asVoice = readBooleanParam(params, 'asVoice') ?? false;
|
|
2387
3072
|
const audioAsVoice = readBooleanParam(params, 'audioAsVoice') ?? false;
|
|
2388
|
-
const resolvedAccountId = normalizeAccountId(
|
|
3073
|
+
const resolvedAccountId = normalizeAccountId(
|
|
3074
|
+
readStringParam(params, 'accountId') ?? accountId,
|
|
3075
|
+
);
|
|
2389
3076
|
|
|
2390
3077
|
if (!content.trim() && !mediaUrl) throw new Error('send requires text or media');
|
|
2391
3078
|
|
|
2392
3079
|
const result = mediaUrl
|
|
2393
3080
|
? await sendBncrMedia({
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
3081
|
+
channelId: CHANNEL_ID,
|
|
3082
|
+
accountId: resolvedAccountId,
|
|
3083
|
+
to,
|
|
3084
|
+
text: content,
|
|
3085
|
+
mediaUrl,
|
|
3086
|
+
asVoice,
|
|
3087
|
+
audioAsVoice,
|
|
3088
|
+
mediaLocalRoots,
|
|
3089
|
+
resolveVerifiedTarget: (to, accountId) => bridge.resolveVerifiedTarget(to, accountId),
|
|
3090
|
+
rememberSessionRoute: (sessionKey, accountId, route) =>
|
|
3091
|
+
bridge.rememberSessionRoute(sessionKey, accountId, route),
|
|
3092
|
+
enqueueFromReply: (args) => bridge.enqueueFromReply(args as any),
|
|
3093
|
+
createMessageId: () => randomUUID(),
|
|
3094
|
+
})
|
|
2407
3095
|
: await sendBncrText({
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
3096
|
+
channelId: CHANNEL_ID,
|
|
3097
|
+
accountId: resolvedAccountId,
|
|
3098
|
+
to,
|
|
3099
|
+
text: content,
|
|
3100
|
+
mediaLocalRoots,
|
|
3101
|
+
resolveVerifiedTarget: (to, accountId) => bridge.resolveVerifiedTarget(to, accountId),
|
|
3102
|
+
rememberSessionRoute: (sessionKey, accountId, route) =>
|
|
3103
|
+
bridge.rememberSessionRoute(sessionKey, accountId, route),
|
|
3104
|
+
enqueueFromReply: (args) => bridge.enqueueFromReply(args as any),
|
|
3105
|
+
createMessageId: () => randomUUID(),
|
|
3106
|
+
});
|
|
2418
3107
|
|
|
2419
3108
|
return jsonResult({ ok: true, ...result });
|
|
2420
3109
|
},
|
|
@@ -2447,7 +3136,7 @@ export function createBncrChannelPlugin(bridge: BncrBridgeRuntime) {
|
|
|
2447
3136
|
looksLikeId: (raw: string, normalized?: string) => {
|
|
2448
3137
|
return Boolean(asString(normalized || raw).trim());
|
|
2449
3138
|
},
|
|
2450
|
-
hint: 'Standard to=Bncr:<platform>:<group>:<user> or Bncr:<platform>:<user>; sessionKey keeps existing strict/legacy compatibility, canonical sessionKey=agent
|
|
3139
|
+
hint: 'Standard to=Bncr:<platform>:<group>:<user> or Bncr:<platform>:<user>; sessionKey keeps existing strict/legacy compatibility, canonical sessionKey=agent:<agentId>:bncr:direct:<hex>',
|
|
2451
3140
|
},
|
|
2452
3141
|
},
|
|
2453
3142
|
configSchema: BncrConfigSchema,
|
|
@@ -2503,27 +3192,33 @@ export function createBncrChannelPlugin(bridge: BncrBridgeRuntime) {
|
|
|
2503
3192
|
textChunkLimit: 4000,
|
|
2504
3193
|
sendText: bridge.channelSendText,
|
|
2505
3194
|
sendMedia: bridge.channelSendMedia,
|
|
2506
|
-
replyAction: async (ctx: any) =>
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
3195
|
+
replyAction: async (ctx: any) =>
|
|
3196
|
+
sendBncrReplyAction({
|
|
3197
|
+
accountId: normalizeAccountId(ctx?.accountId),
|
|
3198
|
+
to: asString(ctx?.to || '').trim(),
|
|
3199
|
+
text: asString(ctx?.text || ''),
|
|
3200
|
+
replyToMessageId:
|
|
3201
|
+
asString(ctx?.replyToId || ctx?.replyToMessageId || '').trim() || undefined,
|
|
3202
|
+
sendText: async ({ accountId, to, text }) =>
|
|
3203
|
+
bridge.channelSendText({ accountId, to, text }),
|
|
3204
|
+
}),
|
|
3205
|
+
deleteAction: async (ctx: any) =>
|
|
3206
|
+
deleteBncrMessageAction({
|
|
3207
|
+
accountId: normalizeAccountId(ctx?.accountId),
|
|
3208
|
+
targetMessageId: asString(ctx?.messageId || ctx?.targetMessageId || '').trim(),
|
|
3209
|
+
}),
|
|
3210
|
+
reactAction: async (ctx: any) =>
|
|
3211
|
+
reactBncrMessageAction({
|
|
3212
|
+
accountId: normalizeAccountId(ctx?.accountId),
|
|
3213
|
+
targetMessageId: asString(ctx?.messageId || ctx?.targetMessageId || '').trim(),
|
|
3214
|
+
emoji: asString(ctx?.emoji || '').trim(),
|
|
3215
|
+
}),
|
|
3216
|
+
editAction: async (ctx: any) =>
|
|
3217
|
+
editBncrMessageAction({
|
|
3218
|
+
accountId: normalizeAccountId(ctx?.accountId),
|
|
3219
|
+
targetMessageId: asString(ctx?.messageId || ctx?.targetMessageId || '').trim(),
|
|
3220
|
+
text: asString(ctx?.text || ''),
|
|
3221
|
+
}),
|
|
2527
3222
|
},
|
|
2528
3223
|
status: {
|
|
2529
3224
|
defaultRuntime: createDefaultChannelRuntimeState(BNCR_DEFAULT_ACCOUNT_ID, {
|
|
@@ -2550,9 +3245,7 @@ export function createBncrChannelPlugin(bridge: BncrBridgeRuntime) {
|
|
|
2550
3245
|
const lastOutboundAgo = rt?.lastOutboundAgo ?? meta.lastOutboundAgo ?? '-';
|
|
2551
3246
|
const diagnostics = rt?.diagnostics ?? meta.diagnostics ?? null;
|
|
2552
3247
|
// 右侧状态字段统一:离线时也显示 Status(避免出现 configured 文案)
|
|
2553
|
-
const normalizedMode = rt?.mode === 'linked'
|
|
2554
|
-
? 'linked'
|
|
2555
|
-
: 'Status';
|
|
3248
|
+
const normalizedMode = rt?.mode === 'linked' ? 'linked' : 'Status';
|
|
2556
3249
|
|
|
2557
3250
|
const displayName = resolveDefaultDisplayName(account?.name, account?.accountId);
|
|
2558
3251
|
|