@xmoxmo/bncr 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.ts +73 -16
- package/package.json +7 -2
- package/scripts/check-register-drift.mjs +17 -5
- package/src/channel.ts +589 -334
- 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 +42 -16
- package/src/messaging/inbound/dispatch.ts +48 -10
- package/src/messaging/inbound/gate.ts +1 -3
- package/src/messaging/inbound/parse.ts +5 -2
- package/src/messaging/inbound/reply-config.ts +50 -0
- package/src/messaging/outbound/media.ts +24 -5
- package/src/messaging/outbound/send.ts +25 -5
package/src/channel.ts
CHANGED
|
@@ -1,61 +1,76 @@
|
|
|
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;
|
|
57
71
|
const MAX_RETRY = 10;
|
|
58
72
|
const PUSH_DRAIN_INTERVAL_MS = 500;
|
|
73
|
+
const PUSH_ACK_TIMEOUT_MS = 30_000;
|
|
59
74
|
const FILE_FORCE_CHUNK = true; // 统一走 WS 分块,保留 base64 仅作兜底
|
|
60
75
|
const FILE_INLINE_THRESHOLD = 5 * 1024 * 1024; // fallback 阈值(仅 FILE_FORCE_CHUNK=false 时生效)
|
|
61
76
|
const FILE_CHUNK_SIZE = 256 * 1024; // 256KB
|
|
@@ -155,13 +170,11 @@ function asString(v: unknown, fallback = ''): string {
|
|
|
155
170
|
return String(v);
|
|
156
171
|
}
|
|
157
172
|
|
|
158
|
-
|
|
159
173
|
function backoffMs(retryCount: number): number {
|
|
160
174
|
// 1s,2s,4s,8s... capped by retry count checks
|
|
161
175
|
return Math.max(1_000, 1_000 * 2 ** Math.max(0, retryCount - 1));
|
|
162
176
|
}
|
|
163
177
|
|
|
164
|
-
|
|
165
178
|
function fileExtFromMime(mimeType?: string): string {
|
|
166
179
|
const mt = asString(mimeType || '').toLowerCase();
|
|
167
180
|
const map: Record<string, string> = {
|
|
@@ -184,7 +197,15 @@ function fileExtFromMime(mimeType?: string): string {
|
|
|
184
197
|
function sanitizeFileName(rawName?: string, fallback = 'file.bin'): string {
|
|
185
198
|
const name = asString(rawName || '').trim();
|
|
186
199
|
const base = name || fallback;
|
|
187
|
-
const cleaned =
|
|
200
|
+
const cleaned = Array.from(base, (ch) => {
|
|
201
|
+
const code = ch.charCodeAt(0);
|
|
202
|
+
if (code <= 0x1f) return '_';
|
|
203
|
+
if ('\\/:*?"<>|'.includes(ch)) return '_';
|
|
204
|
+
return ch;
|
|
205
|
+
})
|
|
206
|
+
.join('')
|
|
207
|
+
.replace(/\s+/g, ' ')
|
|
208
|
+
.trim();
|
|
188
209
|
return cleaned || fallback;
|
|
189
210
|
}
|
|
190
211
|
|
|
@@ -196,7 +217,11 @@ function buildTimestampFileName(mimeType?: string): string {
|
|
|
196
217
|
return `bncr_${ts}_${Math.random().toString(16).slice(2, 8)}${ext}`;
|
|
197
218
|
}
|
|
198
219
|
|
|
199
|
-
function resolveOutboundFileName(params: {
|
|
220
|
+
function resolveOutboundFileName(params: {
|
|
221
|
+
mediaUrl?: string;
|
|
222
|
+
fileName?: string;
|
|
223
|
+
mimeType?: string;
|
|
224
|
+
}): string {
|
|
200
225
|
const mediaUrl = asString(params.mediaUrl || '').trim();
|
|
201
226
|
const mimeType = asString(params.mimeType || '').trim();
|
|
202
227
|
|
|
@@ -234,12 +259,15 @@ class BncrBridgeRuntime {
|
|
|
234
259
|
private lastInboundAtGlobal: number | null = null;
|
|
235
260
|
private lastActivityAtGlobal: number | null = null;
|
|
236
261
|
private lastAckAtGlobal: number | null = null;
|
|
237
|
-
private recentConnections = new Map<
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
262
|
+
private recentConnections = new Map<
|
|
263
|
+
string,
|
|
264
|
+
{
|
|
265
|
+
epoch: number;
|
|
266
|
+
connectedAt: number;
|
|
267
|
+
lastActivityAt: number | null;
|
|
268
|
+
isPrimary: boolean;
|
|
269
|
+
}
|
|
270
|
+
>();
|
|
243
271
|
private staleCounters = {
|
|
244
272
|
staleConnect: 0,
|
|
245
273
|
staleInbound: 0,
|
|
@@ -274,14 +302,26 @@ class BncrBridgeRuntime {
|
|
|
274
302
|
private outbox = new Map<string, OutboxEntry>(); // messageId -> entry
|
|
275
303
|
private deadLetter: OutboxEntry[] = [];
|
|
276
304
|
|
|
277
|
-
private sessionRoutes = new Map<
|
|
278
|
-
|
|
305
|
+
private sessionRoutes = new Map<
|
|
306
|
+
string,
|
|
307
|
+
{ accountId: string; route: BncrRoute; updatedAt: number }
|
|
308
|
+
>();
|
|
309
|
+
private routeAliases = new Map<
|
|
310
|
+
string,
|
|
311
|
+
{ accountId: string; route: BncrRoute; updatedAt: number }
|
|
312
|
+
>();
|
|
279
313
|
|
|
280
314
|
private recentInbound = new Map<string, number>();
|
|
281
|
-
private lastSessionByAccount = new Map<
|
|
315
|
+
private lastSessionByAccount = new Map<
|
|
316
|
+
string,
|
|
317
|
+
{ sessionKey: string; scope: string; updatedAt: number }
|
|
318
|
+
>();
|
|
282
319
|
private lastActivityByAccount = new Map<string, number>();
|
|
283
320
|
private lastInboundByAccount = new Map<string, number>();
|
|
284
321
|
private lastOutboundByAccount = new Map<string, number>();
|
|
322
|
+
private canonicalAgentId: string | null = null;
|
|
323
|
+
private canonicalAgentSource: 'startup' | 'runtime' | 'fallback-main' | null = null;
|
|
324
|
+
private canonicalAgentResolvedAt: number | null = null;
|
|
285
325
|
|
|
286
326
|
// 内置健康/回归计数(替代独立脚本)
|
|
287
327
|
private startedAt = now();
|
|
@@ -299,11 +339,14 @@ class BncrBridgeRuntime {
|
|
|
299
339
|
// 文件互传状态(V1:尽力而为,重连不续传)
|
|
300
340
|
private fileSendTransfers = new Map<string, FileSendTransferState>(); // OpenClaw -> Bncr(服务端发起)
|
|
301
341
|
private fileRecvTransfers = new Map<string, FileRecvTransferState>(); // Bncr -> OpenClaw(客户端发起)
|
|
302
|
-
private fileAckWaiters = new Map<
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
342
|
+
private fileAckWaiters = new Map<
|
|
343
|
+
string,
|
|
344
|
+
{
|
|
345
|
+
resolve: (payload: Record<string, unknown>) => void;
|
|
346
|
+
reject: (err: Error) => void;
|
|
347
|
+
timer: NodeJS.Timeout;
|
|
348
|
+
}
|
|
349
|
+
>();
|
|
307
350
|
|
|
308
351
|
constructor(api: OpenClawPluginApi) {
|
|
309
352
|
this.api = api;
|
|
@@ -318,7 +361,11 @@ class BncrBridgeRuntime {
|
|
|
318
361
|
}
|
|
319
362
|
|
|
320
363
|
private classifyRegisterTrace(stack: string) {
|
|
321
|
-
if (
|
|
364
|
+
if (
|
|
365
|
+
stack.includes('prepareSecretsRuntimeSnapshot') ||
|
|
366
|
+
stack.includes('resolveRuntimeWebTools') ||
|
|
367
|
+
stack.includes('resolvePluginWebSearchProviders')
|
|
368
|
+
) {
|
|
322
369
|
return 'runtime/webtools';
|
|
323
370
|
}
|
|
324
371
|
if (stack.includes('startGatewayServer') || stack.includes('loadGatewayPlugins')) {
|
|
@@ -348,7 +395,9 @@ class BncrBridgeRuntime {
|
|
|
348
395
|
return winner;
|
|
349
396
|
}
|
|
350
397
|
|
|
351
|
-
private captureDriftSnapshot(
|
|
398
|
+
private captureDriftSnapshot(
|
|
399
|
+
summary: ReturnType<BncrBridgeRuntime['buildRegisterTraceSummary']>,
|
|
400
|
+
) {
|
|
352
401
|
this.lastDriftSnapshot = {
|
|
353
402
|
capturedAt: now(),
|
|
354
403
|
registerCount: this.registerCount,
|
|
@@ -374,7 +423,7 @@ class BncrBridgeRuntime {
|
|
|
374
423
|
|
|
375
424
|
for (const trace of this.registerTraceRecent) {
|
|
376
425
|
buckets[trace.stackBucket] = (buckets[trace.stackBucket] || 0) + 1;
|
|
377
|
-
const isWarmup = baseline != null &&
|
|
426
|
+
const isWarmup = baseline != null && trace.ts - baseline <= REGISTER_WARMUP_WINDOW_MS;
|
|
378
427
|
if (isWarmup) {
|
|
379
428
|
warmupCount += 1;
|
|
380
429
|
} else {
|
|
@@ -447,14 +496,13 @@ class BncrBridgeRuntime {
|
|
|
447
496
|
stackBucket,
|
|
448
497
|
};
|
|
449
498
|
this.registerTraceRecent.push(trace);
|
|
450
|
-
if (this.registerTraceRecent.length > 12)
|
|
499
|
+
if (this.registerTraceRecent.length > 12)
|
|
500
|
+
this.registerTraceRecent.splice(0, this.registerTraceRecent.length - 12);
|
|
451
501
|
|
|
452
502
|
const summary = this.buildRegisterTraceSummary();
|
|
453
503
|
if (summary.postWarmupRegisterCount > 0) this.captureDriftSnapshot(summary);
|
|
454
504
|
|
|
455
|
-
this.api.logger.info?.(
|
|
456
|
-
`[bncr-register-trace] ${JSON.stringify(trace)}`,
|
|
457
|
-
);
|
|
505
|
+
this.api.logger.info?.(`[bncr-register-trace] ${JSON.stringify(trace)}`);
|
|
458
506
|
}
|
|
459
507
|
|
|
460
508
|
private createLeaseId() {
|
|
@@ -488,26 +536,55 @@ class BncrBridgeRuntime {
|
|
|
488
536
|
}
|
|
489
537
|
|
|
490
538
|
private observeLease(
|
|
491
|
-
kind:
|
|
539
|
+
kind:
|
|
540
|
+
| 'connect'
|
|
541
|
+
| 'inbound'
|
|
542
|
+
| 'activity'
|
|
543
|
+
| 'ack'
|
|
544
|
+
| 'file.init'
|
|
545
|
+
| 'file.chunk'
|
|
546
|
+
| 'file.complete'
|
|
547
|
+
| 'file.abort',
|
|
492
548
|
params: { leaseId?: string; connectionEpoch?: number },
|
|
493
549
|
) {
|
|
494
550
|
const leaseId = typeof params.leaseId === 'string' ? params.leaseId.trim() : '';
|
|
495
|
-
const connectionEpoch =
|
|
551
|
+
const connectionEpoch =
|
|
552
|
+
typeof params.connectionEpoch === 'number' ? params.connectionEpoch : undefined;
|
|
496
553
|
if (!leaseId && connectionEpoch == null) return { stale: false, reason: 'missing' as const };
|
|
497
|
-
const staleByLease =
|
|
498
|
-
|
|
554
|
+
const staleByLease =
|
|
555
|
+
!!leaseId && this.primaryLeaseId != null && leaseId !== this.primaryLeaseId;
|
|
556
|
+
const staleByEpoch =
|
|
557
|
+
connectionEpoch != null &&
|
|
558
|
+
this.connectionEpoch > 0 &&
|
|
559
|
+
connectionEpoch !== this.connectionEpoch;
|
|
499
560
|
const stale = staleByLease || staleByEpoch;
|
|
500
561
|
if (!stale) return { stale: false, reason: 'ok' as const };
|
|
501
562
|
this.staleCounters.lastStaleAt = now();
|
|
502
563
|
switch (kind) {
|
|
503
|
-
case 'connect':
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
case '
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
case '
|
|
510
|
-
|
|
564
|
+
case 'connect':
|
|
565
|
+
this.staleCounters.staleConnect += 1;
|
|
566
|
+
break;
|
|
567
|
+
case 'inbound':
|
|
568
|
+
this.staleCounters.staleInbound += 1;
|
|
569
|
+
break;
|
|
570
|
+
case 'activity':
|
|
571
|
+
this.staleCounters.staleActivity += 1;
|
|
572
|
+
break;
|
|
573
|
+
case 'ack':
|
|
574
|
+
this.staleCounters.staleAck += 1;
|
|
575
|
+
break;
|
|
576
|
+
case 'file.init':
|
|
577
|
+
this.staleCounters.staleFileInit += 1;
|
|
578
|
+
break;
|
|
579
|
+
case 'file.chunk':
|
|
580
|
+
this.staleCounters.staleFileChunk += 1;
|
|
581
|
+
break;
|
|
582
|
+
case 'file.complete':
|
|
583
|
+
this.staleCounters.staleFileComplete += 1;
|
|
584
|
+
break;
|
|
585
|
+
case 'file.abort':
|
|
586
|
+
this.staleCounters.staleFileAbort += 1;
|
|
587
|
+
break;
|
|
511
588
|
}
|
|
512
589
|
this.api.logger.warn?.(
|
|
513
590
|
`[bncr] stale ${kind} observed lease=${leaseId || '-'} epoch=${connectionEpoch ?? '-'} currentLease=${this.primaryLeaseId || '-'} currentEpoch=${this.connectionEpoch}`,
|
|
@@ -581,10 +658,18 @@ class BncrBridgeRuntime {
|
|
|
581
658
|
startService = async (ctx: OpenClawPluginServiceContext, debug?: boolean) => {
|
|
582
659
|
this.statePath = path.join(ctx.stateDir, 'bncr-bridge-state.json');
|
|
583
660
|
await this.loadState();
|
|
661
|
+
try {
|
|
662
|
+
const cfg = await this.api.runtime.config.loadConfig();
|
|
663
|
+
this.initializeCanonicalAgentId(cfg);
|
|
664
|
+
} catch {
|
|
665
|
+
// ignore startup canonical agent initialization errors
|
|
666
|
+
}
|
|
584
667
|
if (typeof debug === 'boolean') BNCR_DEBUG_VERBOSE = debug;
|
|
585
668
|
const bootDiag = this.buildIntegratedDiagnostics(BNCR_DEFAULT_ACCOUNT_ID);
|
|
586
669
|
if (BNCR_DEBUG_VERBOSE) {
|
|
587
|
-
this.api.logger.info(
|
|
670
|
+
this.api.logger.info(
|
|
671
|
+
`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})`,
|
|
672
|
+
);
|
|
588
673
|
}
|
|
589
674
|
};
|
|
590
675
|
|
|
@@ -630,6 +715,65 @@ class BncrBridgeRuntime {
|
|
|
630
715
|
}
|
|
631
716
|
}
|
|
632
717
|
|
|
718
|
+
private tryResolveBindingAgentId(args: {
|
|
719
|
+
cfg: any;
|
|
720
|
+
accountId: string;
|
|
721
|
+
peer?: any;
|
|
722
|
+
channelId?: string;
|
|
723
|
+
}): string | null {
|
|
724
|
+
try {
|
|
725
|
+
const resolved = this.api.runtime.channel.routing.resolveAgentRoute({
|
|
726
|
+
cfg: args.cfg,
|
|
727
|
+
channel: args.channelId || CHANNEL_ID,
|
|
728
|
+
accountId: normalizeAccountId(args.accountId),
|
|
729
|
+
peer: args.peer,
|
|
730
|
+
});
|
|
731
|
+
const agentId = asString(resolved?.agentId || '').trim();
|
|
732
|
+
return agentId || null;
|
|
733
|
+
} catch {
|
|
734
|
+
return null;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
private initializeCanonicalAgentId(cfg: any) {
|
|
739
|
+
if (this.canonicalAgentId) return;
|
|
740
|
+
const agentId = this.tryResolveBindingAgentId({
|
|
741
|
+
cfg,
|
|
742
|
+
accountId: BNCR_DEFAULT_ACCOUNT_ID,
|
|
743
|
+
channelId: CHANNEL_ID,
|
|
744
|
+
peer: { kind: 'direct', id: 'bootstrap' },
|
|
745
|
+
});
|
|
746
|
+
if (!agentId) return;
|
|
747
|
+
this.canonicalAgentId = agentId;
|
|
748
|
+
this.canonicalAgentSource = 'startup';
|
|
749
|
+
this.canonicalAgentResolvedAt = now();
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
private ensureCanonicalAgentId(args: {
|
|
753
|
+
cfg: any;
|
|
754
|
+
accountId: string;
|
|
755
|
+
peer?: any;
|
|
756
|
+
channelId?: string;
|
|
757
|
+
}): string {
|
|
758
|
+
if (this.canonicalAgentId) return this.canonicalAgentId;
|
|
759
|
+
|
|
760
|
+
const agentId = this.tryResolveBindingAgentId(args);
|
|
761
|
+
if (agentId) {
|
|
762
|
+
this.canonicalAgentId = agentId;
|
|
763
|
+
this.canonicalAgentSource = 'runtime';
|
|
764
|
+
this.canonicalAgentResolvedAt = now();
|
|
765
|
+
return agentId;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
this.canonicalAgentId = 'main';
|
|
769
|
+
this.canonicalAgentSource = 'fallback-main';
|
|
770
|
+
this.canonicalAgentResolvedAt = now();
|
|
771
|
+
this.api.logger.warn?.(
|
|
772
|
+
'[bncr-canonical-agent] binding agent unresolved; fallback to main for current process lifetime',
|
|
773
|
+
);
|
|
774
|
+
return this.canonicalAgentId;
|
|
775
|
+
}
|
|
776
|
+
|
|
633
777
|
private countInvalidOutboxSessionKeys(accountId: string): number {
|
|
634
778
|
const acc = normalizeAccountId(accountId);
|
|
635
779
|
let count = 0;
|
|
@@ -642,7 +786,8 @@ class BncrBridgeRuntime {
|
|
|
642
786
|
|
|
643
787
|
private countLegacyAccountResidue(accountId: string): number {
|
|
644
788
|
const acc = normalizeAccountId(accountId);
|
|
645
|
-
const mismatched = (raw?: string | null) =>
|
|
789
|
+
const mismatched = (raw?: string | null) =>
|
|
790
|
+
asString(raw || '').trim() && normalizeAccountId(raw) !== acc;
|
|
646
791
|
|
|
647
792
|
let count = 0;
|
|
648
793
|
|
|
@@ -688,7 +833,8 @@ class BncrBridgeRuntime {
|
|
|
688
833
|
lastActivityAt: this.lastActivityByAccount.get(acc) || null,
|
|
689
834
|
lastInboundAt: this.lastInboundByAccount.get(acc) || null,
|
|
690
835
|
lastOutboundAt: this.lastOutboundByAccount.get(acc) || null,
|
|
691
|
-
sessionRoutesCount: Array.from(this.sessionRoutes.values()).filter((v) => v.accountId === acc)
|
|
836
|
+
sessionRoutesCount: Array.from(this.sessionRoutes.values()).filter((v) => v.accountId === acc)
|
|
837
|
+
.length,
|
|
692
838
|
invalidOutboxSessionKeys: this.countInvalidOutboxSessionKeys(acc),
|
|
693
839
|
legacyAccountResidue: this.countLegacyAccountResidue(acc),
|
|
694
840
|
channelRoot: path.join(process.cwd(), 'plugins', 'bncr'),
|
|
@@ -709,11 +855,12 @@ class BncrBridgeRuntime {
|
|
|
709
855
|
if (!entry?.messageId) continue;
|
|
710
856
|
const accountId = normalizeAccountId(entry.accountId);
|
|
711
857
|
const sessionKey = asString(entry.sessionKey || '').trim();
|
|
712
|
-
const normalized = normalizeStoredSessionKey(sessionKey);
|
|
858
|
+
const normalized = normalizeStoredSessionKey(sessionKey, this.canonicalAgentId);
|
|
713
859
|
if (!normalized) continue;
|
|
714
860
|
|
|
715
861
|
const route = parseRouteLike(entry.route) || normalized.route;
|
|
716
|
-
const payload =
|
|
862
|
+
const payload =
|
|
863
|
+
entry.payload && typeof entry.payload === 'object' ? { ...entry.payload } : {};
|
|
717
864
|
(payload as any).sessionKey = normalized.sessionKey;
|
|
718
865
|
(payload as any).platform = route.platform;
|
|
719
866
|
(payload as any).groupId = route.groupId;
|
|
@@ -740,11 +887,12 @@ class BncrBridgeRuntime {
|
|
|
740
887
|
if (!entry?.messageId) continue;
|
|
741
888
|
const accountId = normalizeAccountId(entry.accountId);
|
|
742
889
|
const sessionKey = asString(entry.sessionKey || '').trim();
|
|
743
|
-
const normalized = normalizeStoredSessionKey(sessionKey);
|
|
890
|
+
const normalized = normalizeStoredSessionKey(sessionKey, this.canonicalAgentId);
|
|
744
891
|
if (!normalized) continue;
|
|
745
892
|
|
|
746
893
|
const route = parseRouteLike(entry.route) || normalized.route;
|
|
747
|
-
const payload =
|
|
894
|
+
const payload =
|
|
895
|
+
entry.payload && typeof entry.payload === 'object' ? { ...entry.payload } : {};
|
|
748
896
|
(payload as any).sessionKey = normalized.sessionKey;
|
|
749
897
|
(payload as any).platform = route.platform;
|
|
750
898
|
(payload as any).groupId = route.groupId;
|
|
@@ -767,7 +915,10 @@ class BncrBridgeRuntime {
|
|
|
767
915
|
this.sessionRoutes.clear();
|
|
768
916
|
this.routeAliases.clear();
|
|
769
917
|
for (const item of data.sessionRoutes || []) {
|
|
770
|
-
const normalized = normalizeStoredSessionKey(
|
|
918
|
+
const normalized = normalizeStoredSessionKey(
|
|
919
|
+
asString(item?.sessionKey || ''),
|
|
920
|
+
this.canonicalAgentId,
|
|
921
|
+
);
|
|
771
922
|
if (!normalized) continue;
|
|
772
923
|
|
|
773
924
|
const route = parseRouteLike(item?.route) || normalized.route;
|
|
@@ -787,7 +938,10 @@ class BncrBridgeRuntime {
|
|
|
787
938
|
this.lastSessionByAccount.clear();
|
|
788
939
|
for (const item of data.lastSessionByAccount || []) {
|
|
789
940
|
const accountId = normalizeAccountId(item?.accountId);
|
|
790
|
-
const normalized = normalizeStoredSessionKey(
|
|
941
|
+
const normalized = normalizeStoredSessionKey(
|
|
942
|
+
asString(item?.sessionKey || ''),
|
|
943
|
+
this.canonicalAgentId,
|
|
944
|
+
);
|
|
791
945
|
const updatedAt = Number(item?.updatedAt || 0);
|
|
792
946
|
if (!normalized || !Number.isFinite(updatedAt) || updatedAt <= 0) continue;
|
|
793
947
|
|
|
@@ -823,24 +977,38 @@ class BncrBridgeRuntime {
|
|
|
823
977
|
this.lastOutboundByAccount.set(accountId, updatedAt);
|
|
824
978
|
}
|
|
825
979
|
|
|
826
|
-
this.lastDriftSnapshot =
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
:
|
|
842
|
-
|
|
843
|
-
|
|
980
|
+
this.lastDriftSnapshot =
|
|
981
|
+
data.lastDriftSnapshot && typeof data.lastDriftSnapshot === 'object'
|
|
982
|
+
? {
|
|
983
|
+
capturedAt: Number((data.lastDriftSnapshot as any).capturedAt || 0),
|
|
984
|
+
registerCount: Number.isFinite(Number((data.lastDriftSnapshot as any).registerCount))
|
|
985
|
+
? Number((data.lastDriftSnapshot as any).registerCount)
|
|
986
|
+
: null,
|
|
987
|
+
apiGeneration: Number.isFinite(Number((data.lastDriftSnapshot as any).apiGeneration))
|
|
988
|
+
? Number((data.lastDriftSnapshot as any).apiGeneration)
|
|
989
|
+
: null,
|
|
990
|
+
postWarmupRegisterCount: Number.isFinite(
|
|
991
|
+
Number((data.lastDriftSnapshot as any).postWarmupRegisterCount),
|
|
992
|
+
)
|
|
993
|
+
? Number((data.lastDriftSnapshot as any).postWarmupRegisterCount)
|
|
994
|
+
: null,
|
|
995
|
+
apiInstanceId:
|
|
996
|
+
asString((data.lastDriftSnapshot as any).apiInstanceId || '').trim() || null,
|
|
997
|
+
registryFingerprint:
|
|
998
|
+
asString((data.lastDriftSnapshot as any).registryFingerprint || '').trim() || null,
|
|
999
|
+
dominantBucket:
|
|
1000
|
+
asString((data.lastDriftSnapshot as any).dominantBucket || '').trim() || null,
|
|
1001
|
+
sourceBuckets:
|
|
1002
|
+
(data.lastDriftSnapshot as any).sourceBuckets &&
|
|
1003
|
+
typeof (data.lastDriftSnapshot as any).sourceBuckets === 'object'
|
|
1004
|
+
? { ...((data.lastDriftSnapshot as any).sourceBuckets as Record<string, number>) }
|
|
1005
|
+
: {},
|
|
1006
|
+
traceWindowSize: Number((data.lastDriftSnapshot as any).traceWindowSize || 0),
|
|
1007
|
+
traceRecent: Array.isArray((data.lastDriftSnapshot as any).traceRecent)
|
|
1008
|
+
? [...((data.lastDriftSnapshot as any).traceRecent as Array<Record<string, unknown>>)]
|
|
1009
|
+
: [],
|
|
1010
|
+
}
|
|
1011
|
+
: null;
|
|
844
1012
|
|
|
845
1013
|
// 兼容旧状态文件:若尚未持久化 lastSession*/lastActivity*,从 sessionRoutes 回填。
|
|
846
1014
|
if (this.lastSessionByAccount.size === 0 && this.sessionRoutes.size > 0) {
|
|
@@ -884,24 +1052,32 @@ class BncrBridgeRuntime {
|
|
|
884
1052
|
outbox: Array.from(this.outbox.values()),
|
|
885
1053
|
deadLetter: this.deadLetter.slice(-1000),
|
|
886
1054
|
sessionRoutes,
|
|
887
|
-
lastSessionByAccount: Array.from(this.lastSessionByAccount.entries()).map(
|
|
888
|
-
accountId,
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
accountId,
|
|
903
|
-
|
|
904
|
-
|
|
1055
|
+
lastSessionByAccount: Array.from(this.lastSessionByAccount.entries()).map(
|
|
1056
|
+
([accountId, v]) => ({
|
|
1057
|
+
accountId,
|
|
1058
|
+
sessionKey: v.sessionKey,
|
|
1059
|
+
scope: v.scope,
|
|
1060
|
+
updatedAt: v.updatedAt,
|
|
1061
|
+
}),
|
|
1062
|
+
),
|
|
1063
|
+
lastActivityByAccount: Array.from(this.lastActivityByAccount.entries()).map(
|
|
1064
|
+
([accountId, updatedAt]) => ({
|
|
1065
|
+
accountId,
|
|
1066
|
+
updatedAt,
|
|
1067
|
+
}),
|
|
1068
|
+
),
|
|
1069
|
+
lastInboundByAccount: Array.from(this.lastInboundByAccount.entries()).map(
|
|
1070
|
+
([accountId, updatedAt]) => ({
|
|
1071
|
+
accountId,
|
|
1072
|
+
updatedAt,
|
|
1073
|
+
}),
|
|
1074
|
+
),
|
|
1075
|
+
lastOutboundByAccount: Array.from(this.lastOutboundByAccount.entries()).map(
|
|
1076
|
+
([accountId, updatedAt]) => ({
|
|
1077
|
+
accountId,
|
|
1078
|
+
updatedAt,
|
|
1079
|
+
}),
|
|
1080
|
+
),
|
|
905
1081
|
lastDriftSnapshot: this.lastDriftSnapshot
|
|
906
1082
|
? {
|
|
907
1083
|
capturedAt: this.lastDriftSnapshot.capturedAt,
|
|
@@ -1004,7 +1180,6 @@ class BncrBridgeRuntime {
|
|
|
1004
1180
|
})}`,
|
|
1005
1181
|
);
|
|
1006
1182
|
}
|
|
1007
|
-
this.outbox.delete(entry.messageId);
|
|
1008
1183
|
this.lastOutboundByAccount.set(entry.accountId, now());
|
|
1009
1184
|
this.markActivity(entry.accountId);
|
|
1010
1185
|
this.scheduleSave();
|
|
@@ -1038,7 +1213,11 @@ class BncrBridgeRuntime {
|
|
|
1038
1213
|
const filterAcc = accountId ? normalizeAccountId(accountId) : null;
|
|
1039
1214
|
const targetAccounts = filterAcc
|
|
1040
1215
|
? [filterAcc]
|
|
1041
|
-
: Array.from(
|
|
1216
|
+
: Array.from(
|
|
1217
|
+
new Set(
|
|
1218
|
+
Array.from(this.outbox.values()).map((entry) => normalizeAccountId(entry.accountId)),
|
|
1219
|
+
),
|
|
1220
|
+
);
|
|
1042
1221
|
if (BNCR_DEBUG_VERBOSE) {
|
|
1043
1222
|
this.api.logger.info?.(
|
|
1044
1223
|
`[bncr-outbox-flush] ${JSON.stringify({
|
|
@@ -1070,88 +1249,6 @@ class BncrBridgeRuntime {
|
|
|
1070
1249
|
})}`,
|
|
1071
1250
|
);
|
|
1072
1251
|
}
|
|
1073
|
-
if (!online) {
|
|
1074
|
-
const ctx = this.gatewayContext;
|
|
1075
|
-
const directConnIds = Array.from(this.connections.values())
|
|
1076
|
-
.filter((c) => normalizeAccountId(c.accountId) === acc && c.connId)
|
|
1077
|
-
.map((c) => c.connId as string);
|
|
1078
|
-
|
|
1079
|
-
if (BNCR_DEBUG_VERBOSE) {
|
|
1080
|
-
this.api.logger.info?.(
|
|
1081
|
-
`[bncr-outbox-direct-push] ${JSON.stringify({
|
|
1082
|
-
bridge: this.bridgeId,
|
|
1083
|
-
accountId: acc,
|
|
1084
|
-
outboxSize: this.outbox.size,
|
|
1085
|
-
hasGatewayContext: Boolean(ctx),
|
|
1086
|
-
connCount: directConnIds.length,
|
|
1087
|
-
})}`,
|
|
1088
|
-
);
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
if (!ctx) {
|
|
1092
|
-
if (BNCR_DEBUG_VERBOSE) {
|
|
1093
|
-
this.api.logger.info?.(
|
|
1094
|
-
`[bncr-outbox-direct-push-skip] ${JSON.stringify({
|
|
1095
|
-
bridge: this.bridgeId,
|
|
1096
|
-
accountId: acc,
|
|
1097
|
-
reason: 'no-gateway-context',
|
|
1098
|
-
})}`,
|
|
1099
|
-
);
|
|
1100
|
-
}
|
|
1101
|
-
continue;
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
if (!directConnIds.length) {
|
|
1105
|
-
if (BNCR_DEBUG_VERBOSE) {
|
|
1106
|
-
this.api.logger.info?.(
|
|
1107
|
-
`[bncr-outbox-direct-push-skip] ${JSON.stringify({
|
|
1108
|
-
accountId: acc,
|
|
1109
|
-
reason: 'no-connection',
|
|
1110
|
-
})}`,
|
|
1111
|
-
);
|
|
1112
|
-
}
|
|
1113
|
-
continue;
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
const directPayloads = this.collectDue(acc, 50);
|
|
1117
|
-
if (!directPayloads.length) continue;
|
|
1118
|
-
|
|
1119
|
-
try {
|
|
1120
|
-
ctx.broadcastToConnIds(BNCR_PUSH_EVENT, {
|
|
1121
|
-
forcePush: true,
|
|
1122
|
-
items: directPayloads,
|
|
1123
|
-
}, new Set(directConnIds));
|
|
1124
|
-
|
|
1125
|
-
const pushedIds = directPayloads
|
|
1126
|
-
.map((item: any) => asString(item?.messageId || item?.idempotencyKey || '').trim())
|
|
1127
|
-
.filter(Boolean);
|
|
1128
|
-
for (const id of pushedIds) this.outbox.delete(id);
|
|
1129
|
-
if (pushedIds.length) this.scheduleSave();
|
|
1130
|
-
|
|
1131
|
-
if (BNCR_DEBUG_VERBOSE) {
|
|
1132
|
-
this.api.logger.info?.(
|
|
1133
|
-
`[bncr-outbox-direct-push-ok] ${JSON.stringify({
|
|
1134
|
-
bridge: this.bridgeId,
|
|
1135
|
-
accountId: acc,
|
|
1136
|
-
count: directPayloads.length,
|
|
1137
|
-
connCount: directConnIds.length,
|
|
1138
|
-
dropped: pushedIds.length,
|
|
1139
|
-
})}`,
|
|
1140
|
-
);
|
|
1141
|
-
}
|
|
1142
|
-
} catch (error) {
|
|
1143
|
-
if (BNCR_DEBUG_VERBOSE) {
|
|
1144
|
-
this.api.logger.info?.(
|
|
1145
|
-
`[bncr-outbox-direct-push-fail] ${JSON.stringify({
|
|
1146
|
-
accountId: acc,
|
|
1147
|
-
error: asString((error as any)?.message || error || 'direct-push-error'),
|
|
1148
|
-
})}`,
|
|
1149
|
-
);
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
continue;
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
1252
|
this.pushDrainRunningAccounts.add(acc);
|
|
1156
1253
|
try {
|
|
1157
1254
|
let localNextDelay: number | null = null;
|
|
@@ -1163,7 +1260,6 @@ class BncrBridgeRuntime {
|
|
|
1163
1260
|
.sort((a, b) => a.createdAt - b.createdAt);
|
|
1164
1261
|
|
|
1165
1262
|
if (!entries.length) break;
|
|
1166
|
-
if (!this.isOnline(acc)) break;
|
|
1167
1263
|
|
|
1168
1264
|
const entry = entries.find((item) => item.nextAttemptAt <= t);
|
|
1169
1265
|
if (!entry) {
|
|
@@ -1172,11 +1268,33 @@ class BncrBridgeRuntime {
|
|
|
1172
1268
|
break;
|
|
1173
1269
|
}
|
|
1174
1270
|
|
|
1271
|
+
const onlineNow = this.isOnline(acc);
|
|
1175
1272
|
const pushed = this.tryPushEntry(entry);
|
|
1176
1273
|
if (pushed) {
|
|
1274
|
+
if (onlineNow) {
|
|
1275
|
+
await this.waitForOutbound(acc, PUSH_ACK_TIMEOUT_MS);
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
if (!this.outbox.has(entry.messageId)) {
|
|
1279
|
+
await this.sleepMs(PUSH_DRAIN_INTERVAL_MS);
|
|
1280
|
+
continue;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
entry.retryCount += 1;
|
|
1284
|
+
entry.lastAttemptAt = now();
|
|
1285
|
+
if (entry.retryCount > MAX_RETRY) {
|
|
1286
|
+
this.moveToDeadLetter(entry, entry.lastError || 'push-ack-timeout');
|
|
1287
|
+
continue;
|
|
1288
|
+
}
|
|
1289
|
+
entry.nextAttemptAt = now() + backoffMs(entry.retryCount);
|
|
1290
|
+
entry.lastError = entry.lastError || 'push-ack-timeout';
|
|
1291
|
+
this.outbox.set(entry.messageId, entry);
|
|
1177
1292
|
this.scheduleSave();
|
|
1293
|
+
|
|
1294
|
+
const wait = Math.max(0, entry.nextAttemptAt - now());
|
|
1295
|
+
localNextDelay = localNextDelay == null ? wait : Math.min(localNextDelay, wait);
|
|
1178
1296
|
await this.sleepMs(PUSH_DRAIN_INTERVAL_MS);
|
|
1179
|
-
|
|
1297
|
+
break;
|
|
1180
1298
|
}
|
|
1181
1299
|
|
|
1182
1300
|
const nextAttempt = entry.retryCount + 1;
|
|
@@ -1198,7 +1316,8 @@ class BncrBridgeRuntime {
|
|
|
1198
1316
|
}
|
|
1199
1317
|
|
|
1200
1318
|
if (localNextDelay != null) {
|
|
1201
|
-
globalNextDelay =
|
|
1319
|
+
globalNextDelay =
|
|
1320
|
+
globalNextDelay == null ? localNextDelay : Math.min(globalNextDelay, localNextDelay);
|
|
1202
1321
|
}
|
|
1203
1322
|
} finally {
|
|
1204
1323
|
this.pushDrainRunningAccounts.delete(acc);
|
|
@@ -1320,7 +1439,11 @@ class BncrBridgeRuntime {
|
|
|
1320
1439
|
}
|
|
1321
1440
|
|
|
1322
1441
|
const curConn = this.connections.get(current);
|
|
1323
|
-
if (
|
|
1442
|
+
if (
|
|
1443
|
+
!curConn ||
|
|
1444
|
+
t - curConn.lastSeenAt > CONNECT_TTL_MS ||
|
|
1445
|
+
nextConn.connectedAt >= curConn.connectedAt
|
|
1446
|
+
) {
|
|
1324
1447
|
this.activeConnectionByAccount.set(acc, key);
|
|
1325
1448
|
}
|
|
1326
1449
|
}
|
|
@@ -1372,9 +1495,6 @@ class BncrBridgeRuntime {
|
|
|
1372
1495
|
const info = { accountId: acc, route, updatedAt: t };
|
|
1373
1496
|
|
|
1374
1497
|
this.sessionRoutes.set(key, info);
|
|
1375
|
-
// 同步维护旧格式与新格式,便于平滑切换
|
|
1376
|
-
this.sessionRoutes.set(buildFallbackSessionKey(route), info);
|
|
1377
|
-
|
|
1378
1498
|
this.routeAliases.set(routeKey(acc, route), info);
|
|
1379
1499
|
this.lastSessionByAccount.set(acc, {
|
|
1380
1500
|
sessionKey: key,
|
|
@@ -1404,7 +1524,10 @@ class BncrBridgeRuntime {
|
|
|
1404
1524
|
// 1) 标准 to 仅认 Bncr:<platform>:<groupId>:<userId> / Bncr:<platform>:<userId>
|
|
1405
1525
|
// 2) 仍接受 strict sessionKey 作为内部兼容输入
|
|
1406
1526
|
// 3) 其他旧格式直接失败,并输出标准格式提示日志
|
|
1407
|
-
private resolveVerifiedTarget(
|
|
1527
|
+
private resolveVerifiedTarget(
|
|
1528
|
+
rawTarget: string,
|
|
1529
|
+
accountId: string,
|
|
1530
|
+
): { sessionKey: string; route: BncrRoute; displayScope: string } {
|
|
1408
1531
|
const acc = normalizeAccountId(accountId);
|
|
1409
1532
|
const raw = asString(rawTarget).trim();
|
|
1410
1533
|
if (!raw) throw new Error('bncr invalid target(empty)');
|
|
@@ -1424,17 +1547,23 @@ class BncrBridgeRuntime {
|
|
|
1424
1547
|
|
|
1425
1548
|
if (!route) {
|
|
1426
1549
|
this.api.logger.warn?.(
|
|
1427
|
-
`[bncr-target-invalid] raw=${raw} accountId=${acc} reason=unparseable-or-unknown standardTo=Bncr:<platform>:<groupId>:<userId>|Bncr:<platform>:<userId> standardSessionKey=agent
|
|
1550
|
+
`[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)>`,
|
|
1551
|
+
);
|
|
1552
|
+
throw new Error(
|
|
1553
|
+
`bncr invalid target(standard: Bncr:<platform>:<groupId>:<userId> | Bncr:<platform>:<userId>): ${raw}`,
|
|
1428
1554
|
);
|
|
1429
|
-
throw new Error(`bncr invalid target(standard: Bncr:<platform>:<groupId>:<userId> | Bncr:<platform>:<userId>): ${raw}`);
|
|
1430
1555
|
}
|
|
1431
1556
|
|
|
1432
1557
|
const wantedRouteKey = routeKey(acc, route);
|
|
1433
1558
|
let best: { sessionKey: string; route: BncrRoute; updatedAt: number } | null = null;
|
|
1434
1559
|
|
|
1435
1560
|
if (BNCR_DEBUG_VERBOSE) {
|
|
1436
|
-
this.api.logger.info?.(
|
|
1437
|
-
|
|
1561
|
+
this.api.logger.info?.(
|
|
1562
|
+
`[bncr-target-incoming-route] raw=${raw} accountId=${acc} route=${JSON.stringify(route)}`,
|
|
1563
|
+
);
|
|
1564
|
+
this.api.logger.info?.(
|
|
1565
|
+
`[bncr-target-incoming-sessionRoutes] raw=${raw} accountId=${acc} sessionRoutes=${JSON.stringify(this.sessionRoutes.entries())}`,
|
|
1566
|
+
);
|
|
1438
1567
|
}
|
|
1439
1568
|
|
|
1440
1569
|
for (const [key, info] of this.sessionRoutes.entries()) {
|
|
@@ -1446,29 +1575,40 @@ class BncrBridgeRuntime {
|
|
|
1446
1575
|
const updatedAt = Number(info.updatedAt || 0);
|
|
1447
1576
|
if (!best || updatedAt >= best.updatedAt) {
|
|
1448
1577
|
best = {
|
|
1449
|
-
sessionKey:
|
|
1578
|
+
sessionKey: key,
|
|
1450
1579
|
route: parsed.route,
|
|
1451
1580
|
updatedAt,
|
|
1452
1581
|
};
|
|
1453
1582
|
}
|
|
1454
1583
|
}
|
|
1455
1584
|
|
|
1456
|
-
// 直接根据raw生成标准sessionkey
|
|
1457
1585
|
if (!best) {
|
|
1458
1586
|
const updatedAt = 0;
|
|
1587
|
+
const canonicalAgentId =
|
|
1588
|
+
this.canonicalAgentId ||
|
|
1589
|
+
this.ensureCanonicalAgentId({
|
|
1590
|
+
cfg: this.api.runtime.config?.get?.() || {},
|
|
1591
|
+
accountId: acc,
|
|
1592
|
+
channelId: CHANNEL_ID,
|
|
1593
|
+
peer: { kind: 'direct', id: route.groupId === '0' ? route.userId : route.groupId },
|
|
1594
|
+
});
|
|
1459
1595
|
best = {
|
|
1460
|
-
sessionKey:
|
|
1596
|
+
sessionKey: buildCanonicalBncrSessionKey(route, canonicalAgentId),
|
|
1461
1597
|
route,
|
|
1462
1598
|
updatedAt,
|
|
1463
1599
|
};
|
|
1464
1600
|
}
|
|
1465
1601
|
|
|
1466
1602
|
if (BNCR_DEBUG_VERBOSE) {
|
|
1467
|
-
this.api.logger.info?.(
|
|
1603
|
+
this.api.logger.info?.(
|
|
1604
|
+
`[bncr-target-incoming-best] raw=${raw} accountId=${acc} best=${JSON.stringify(best)}`,
|
|
1605
|
+
);
|
|
1468
1606
|
}
|
|
1469
1607
|
|
|
1470
1608
|
if (!best) {
|
|
1471
|
-
this.api.logger.warn?.(
|
|
1609
|
+
this.api.logger.warn?.(
|
|
1610
|
+
`[bncr-target-miss] raw=${raw} accountId=${acc} sessionRoutes=${this.sessionRoutes.size}`,
|
|
1611
|
+
);
|
|
1472
1612
|
throw new Error(`bncr target not found in known sessions: ${raw}`);
|
|
1473
1613
|
}
|
|
1474
1614
|
|
|
@@ -1496,11 +1636,19 @@ class BncrBridgeRuntime {
|
|
|
1496
1636
|
return `${transferId}|${stage}|${idx}`;
|
|
1497
1637
|
}
|
|
1498
1638
|
|
|
1499
|
-
private waitForFileAck(params: {
|
|
1639
|
+
private waitForFileAck(params: {
|
|
1640
|
+
transferId: string;
|
|
1641
|
+
stage: string;
|
|
1642
|
+
chunkIndex?: number;
|
|
1643
|
+
timeoutMs?: number;
|
|
1644
|
+
}) {
|
|
1500
1645
|
const transferId = asString(params.transferId).trim();
|
|
1501
1646
|
const stage = asString(params.stage).trim();
|
|
1502
1647
|
const key = this.fileAckKey(transferId, stage, params.chunkIndex);
|
|
1503
|
-
const timeoutMs = Math.max(
|
|
1648
|
+
const timeoutMs = Math.max(
|
|
1649
|
+
1_000,
|
|
1650
|
+
Math.min(Number(params.timeoutMs || FILE_ACK_TIMEOUT_MS), 120_000),
|
|
1651
|
+
);
|
|
1504
1652
|
|
|
1505
1653
|
return new Promise<Record<string, unknown>>((resolve, reject) => {
|
|
1506
1654
|
const timer = setTimeout(() => {
|
|
@@ -1511,7 +1659,13 @@ class BncrBridgeRuntime {
|
|
|
1511
1659
|
});
|
|
1512
1660
|
}
|
|
1513
1661
|
|
|
1514
|
-
private resolveFileAck(params: {
|
|
1662
|
+
private resolveFileAck(params: {
|
|
1663
|
+
transferId: string;
|
|
1664
|
+
stage: string;
|
|
1665
|
+
chunkIndex?: number;
|
|
1666
|
+
payload: Record<string, unknown>;
|
|
1667
|
+
ok: boolean;
|
|
1668
|
+
}) {
|
|
1515
1669
|
const transferId = asString(params.transferId).trim();
|
|
1516
1670
|
const stage = asString(params.stage).trim();
|
|
1517
1671
|
const key = this.fileAckKey(transferId, stage, params.chunkIndex);
|
|
@@ -1520,11 +1674,20 @@ class BncrBridgeRuntime {
|
|
|
1520
1674
|
this.fileAckWaiters.delete(key);
|
|
1521
1675
|
clearTimeout(waiter.timer);
|
|
1522
1676
|
if (params.ok) waiter.resolve(params.payload);
|
|
1523
|
-
else
|
|
1677
|
+
else
|
|
1678
|
+
waiter.reject(
|
|
1679
|
+
new Error(
|
|
1680
|
+
asString(params.payload?.errorMessage || params.payload?.error || 'file ack failed'),
|
|
1681
|
+
),
|
|
1682
|
+
);
|
|
1524
1683
|
return true;
|
|
1525
1684
|
}
|
|
1526
1685
|
|
|
1527
|
-
private pushFileEventToAccount(
|
|
1686
|
+
private pushFileEventToAccount(
|
|
1687
|
+
accountId: string,
|
|
1688
|
+
event: string,
|
|
1689
|
+
payload: Record<string, unknown>,
|
|
1690
|
+
) {
|
|
1528
1691
|
const connIds = this.resolvePushConnIds(accountId);
|
|
1529
1692
|
if (!connIds.size || !this.gatewayContext) {
|
|
1530
1693
|
throw new Error(`no active bncr connection for account=${accountId}`);
|
|
@@ -1547,7 +1710,9 @@ class BncrBridgeRuntime {
|
|
|
1547
1710
|
return dir;
|
|
1548
1711
|
}
|
|
1549
1712
|
|
|
1550
|
-
private async materializeRecvTransfer(
|
|
1713
|
+
private async materializeRecvTransfer(
|
|
1714
|
+
st: FileRecvTransferState,
|
|
1715
|
+
): Promise<{ path: string; fileSha256: string }> {
|
|
1551
1716
|
const dir = this.resolveInboundFilesDir();
|
|
1552
1717
|
const safeName = asString(st.fileName).trim() || `${st.transferId}.bin`;
|
|
1553
1718
|
const finalPath = path.join(dir, safeName);
|
|
@@ -1589,7 +1754,8 @@ class BncrBridgeRuntime {
|
|
|
1589
1754
|
lastActivityAt: this.lastActivityByAccount.get(acc) || null,
|
|
1590
1755
|
lastInboundAt: this.lastInboundByAccount.get(acc) || null,
|
|
1591
1756
|
lastOutboundAt: this.lastOutboundByAccount.get(acc) || null,
|
|
1592
|
-
sessionRoutesCount: Array.from(this.sessionRoutes.values()).filter((v) => v.accountId === acc)
|
|
1757
|
+
sessionRoutesCount: Array.from(this.sessionRoutes.values()).filter((v) => v.accountId === acc)
|
|
1758
|
+
.length,
|
|
1593
1759
|
invalidOutboxSessionKeys: this.countInvalidOutboxSessionKeys(acc),
|
|
1594
1760
|
legacyAccountResidue: this.countLegacyAccountResidue(acc),
|
|
1595
1761
|
channelRoot: path.join(process.cwd(), 'plugins', 'bncr'),
|
|
@@ -1613,7 +1779,8 @@ class BncrBridgeRuntime {
|
|
|
1613
1779
|
lastActivityAt: this.lastActivityByAccount.get(acc) || null,
|
|
1614
1780
|
lastInboundAt: this.lastInboundByAccount.get(acc) || null,
|
|
1615
1781
|
lastOutboundAt: this.lastOutboundByAccount.get(acc) || null,
|
|
1616
|
-
sessionRoutesCount: Array.from(this.sessionRoutes.values()).filter((v) => v.accountId === acc)
|
|
1782
|
+
sessionRoutesCount: Array.from(this.sessionRoutes.values()).filter((v) => v.accountId === acc)
|
|
1783
|
+
.length,
|
|
1617
1784
|
invalidOutboxSessionKeys: this.countInvalidOutboxSessionKeys(acc),
|
|
1618
1785
|
legacyAccountResidue: this.countLegacyAccountResidue(acc),
|
|
1619
1786
|
running: true,
|
|
@@ -1638,7 +1805,8 @@ class BncrBridgeRuntime {
|
|
|
1638
1805
|
lastActivityAt: this.lastActivityByAccount.get(acc) || null,
|
|
1639
1806
|
lastInboundAt: this.lastInboundByAccount.get(acc) || null,
|
|
1640
1807
|
lastOutboundAt: this.lastOutboundByAccount.get(acc) || null,
|
|
1641
|
-
sessionRoutesCount: Array.from(this.sessionRoutes.values()).filter((v) => v.accountId === acc)
|
|
1808
|
+
sessionRoutesCount: Array.from(this.sessionRoutes.values()).filter((v) => v.accountId === acc)
|
|
1809
|
+
.length,
|
|
1642
1810
|
invalidOutboxSessionKeys: this.countInvalidOutboxSessionKeys(acc),
|
|
1643
1811
|
legacyAccountResidue: this.countLegacyAccountResidue(acc),
|
|
1644
1812
|
channelRoot: path.join(process.cwd(), 'plugins', 'bncr'),
|
|
@@ -1763,7 +1931,10 @@ class BncrBridgeRuntime {
|
|
|
1763
1931
|
timeoutMs?: number;
|
|
1764
1932
|
}): Promise<void> {
|
|
1765
1933
|
const { transferId, chunkIndex } = params;
|
|
1766
|
-
const timeoutMs = Math.max(
|
|
1934
|
+
const timeoutMs = Math.max(
|
|
1935
|
+
1_000,
|
|
1936
|
+
Math.min(Number(params.timeoutMs || FILE_TRANSFER_ACK_TTL_MS), 60_000),
|
|
1937
|
+
);
|
|
1767
1938
|
const started = now();
|
|
1768
1939
|
|
|
1769
1940
|
return new Promise<void>((resolve, reject) => {
|
|
@@ -1832,7 +2003,13 @@ class BncrBridgeRuntime {
|
|
|
1832
2003
|
route: BncrRoute;
|
|
1833
2004
|
mediaUrl: string;
|
|
1834
2005
|
mediaLocalRoots?: readonly string[];
|
|
1835
|
-
}): Promise<{
|
|
2006
|
+
}): Promise<{
|
|
2007
|
+
mode: 'base64' | 'chunk';
|
|
2008
|
+
mimeType?: string;
|
|
2009
|
+
fileName?: string;
|
|
2010
|
+
mediaBase64?: string;
|
|
2011
|
+
path?: string;
|
|
2012
|
+
}> {
|
|
1836
2013
|
const loaded = await this.api.runtime.media.loadWebMedia(params.mediaUrl, {
|
|
1837
2014
|
localRoots: params.mediaLocalRoots,
|
|
1838
2015
|
maxBytes: 50 * 1024 * 1024,
|
|
@@ -1884,21 +2061,25 @@ class BncrBridgeRuntime {
|
|
|
1884
2061
|
};
|
|
1885
2062
|
this.fileSendTransfers.set(transferId, st);
|
|
1886
2063
|
|
|
1887
|
-
ctx.broadcastToConnIds(
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
2064
|
+
ctx.broadcastToConnIds(
|
|
2065
|
+
'bncr.file.init',
|
|
2066
|
+
{
|
|
2067
|
+
transferId,
|
|
2068
|
+
direction: 'oc2bncr',
|
|
2069
|
+
sessionKey: params.sessionKey,
|
|
2070
|
+
platform: params.route.platform,
|
|
2071
|
+
groupId: params.route.groupId,
|
|
2072
|
+
userId: params.route.userId,
|
|
2073
|
+
fileName,
|
|
2074
|
+
mimeType,
|
|
2075
|
+
fileSize: size,
|
|
2076
|
+
chunkSize,
|
|
2077
|
+
totalChunks,
|
|
2078
|
+
fileSha256,
|
|
2079
|
+
ts: now(),
|
|
2080
|
+
},
|
|
2081
|
+
connIds,
|
|
2082
|
+
);
|
|
1902
2083
|
|
|
1903
2084
|
// 逐块发送并等待 ACK
|
|
1904
2085
|
for (let idx = 0; idx < totalChunks; idx++) {
|
|
@@ -1910,18 +2091,26 @@ class BncrBridgeRuntime {
|
|
|
1910
2091
|
let ok = false;
|
|
1911
2092
|
let lastErr: unknown = null;
|
|
1912
2093
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
1913
|
-
ctx.broadcastToConnIds(
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
2094
|
+
ctx.broadcastToConnIds(
|
|
2095
|
+
'bncr.file.chunk',
|
|
2096
|
+
{
|
|
2097
|
+
transferId,
|
|
2098
|
+
chunkIndex: idx,
|
|
2099
|
+
offset: start,
|
|
2100
|
+
size: slice.byteLength,
|
|
2101
|
+
chunkSha256,
|
|
2102
|
+
base64: slice.toString('base64'),
|
|
2103
|
+
ts: now(),
|
|
2104
|
+
},
|
|
2105
|
+
connIds,
|
|
2106
|
+
);
|
|
1922
2107
|
|
|
1923
2108
|
try {
|
|
1924
|
-
await this.waitChunkAck({
|
|
2109
|
+
await this.waitChunkAck({
|
|
2110
|
+
transferId,
|
|
2111
|
+
chunkIndex: idx,
|
|
2112
|
+
timeoutMs: FILE_TRANSFER_ACK_TTL_MS,
|
|
2113
|
+
});
|
|
1925
2114
|
ok = true;
|
|
1926
2115
|
break;
|
|
1927
2116
|
} catch (err) {
|
|
@@ -1934,19 +2123,27 @@ class BncrBridgeRuntime {
|
|
|
1934
2123
|
st.status = 'aborted';
|
|
1935
2124
|
st.error = String((lastErr as any)?.message || lastErr || `chunk-${idx}-failed`);
|
|
1936
2125
|
this.fileSendTransfers.set(transferId, st);
|
|
1937
|
-
ctx.broadcastToConnIds(
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
2126
|
+
ctx.broadcastToConnIds(
|
|
2127
|
+
'bncr.file.abort',
|
|
2128
|
+
{
|
|
2129
|
+
transferId,
|
|
2130
|
+
reason: st.error,
|
|
2131
|
+
ts: now(),
|
|
2132
|
+
},
|
|
2133
|
+
connIds,
|
|
2134
|
+
);
|
|
1942
2135
|
throw new Error(st.error);
|
|
1943
2136
|
}
|
|
1944
2137
|
}
|
|
1945
2138
|
|
|
1946
|
-
ctx.broadcastToConnIds(
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
2139
|
+
ctx.broadcastToConnIds(
|
|
2140
|
+
'bncr.file.complete',
|
|
2141
|
+
{
|
|
2142
|
+
transferId,
|
|
2143
|
+
ts: now(),
|
|
2144
|
+
},
|
|
2145
|
+
connIds,
|
|
2146
|
+
);
|
|
1950
2147
|
|
|
1951
2148
|
const done = await this.waitCompleteAck({ transferId, timeoutMs: 60_000 });
|
|
1952
2149
|
|
|
@@ -1962,7 +2159,14 @@ class BncrBridgeRuntime {
|
|
|
1962
2159
|
accountId: string;
|
|
1963
2160
|
sessionKey: string;
|
|
1964
2161
|
route: BncrRoute;
|
|
1965
|
-
payload: {
|
|
2162
|
+
payload: {
|
|
2163
|
+
text?: string;
|
|
2164
|
+
mediaUrl?: string;
|
|
2165
|
+
mediaUrls?: string[];
|
|
2166
|
+
asVoice?: boolean;
|
|
2167
|
+
audioAsVoice?: boolean;
|
|
2168
|
+
kind?: 'tool' | 'block' | 'final';
|
|
2169
|
+
};
|
|
1966
2170
|
mediaLocalRoots?: readonly string[];
|
|
1967
2171
|
}) {
|
|
1968
2172
|
const { accountId, sessionKey, route, payload, mediaLocalRoots } = params;
|
|
@@ -2144,6 +2348,7 @@ class BncrBridgeRuntime {
|
|
|
2144
2348
|
if (ok) {
|
|
2145
2349
|
this.outbox.delete(messageId);
|
|
2146
2350
|
this.scheduleSave();
|
|
2351
|
+
this.wakeAccountWaiters(accountId);
|
|
2147
2352
|
respond(true, { ok: true });
|
|
2148
2353
|
return;
|
|
2149
2354
|
}
|
|
@@ -2270,11 +2475,12 @@ class BncrBridgeRuntime {
|
|
|
2270
2475
|
return;
|
|
2271
2476
|
}
|
|
2272
2477
|
|
|
2273
|
-
const route =
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2478
|
+
const route =
|
|
2479
|
+
parseRouteLike({
|
|
2480
|
+
platform: asString(params?.platform || normalized.route.platform),
|
|
2481
|
+
groupId: asString(params?.groupId || normalized.route.groupId),
|
|
2482
|
+
userId: asString(params?.userId || normalized.route.userId),
|
|
2483
|
+
}) || normalized.route;
|
|
2278
2484
|
|
|
2279
2485
|
this.fileRecvTransfers.set(transferId, {
|
|
2280
2486
|
transferId,
|
|
@@ -2354,7 +2560,12 @@ class BncrBridgeRuntime {
|
|
|
2354
2560
|
}
|
|
2355
2561
|
};
|
|
2356
2562
|
|
|
2357
|
-
handleFileComplete = async ({
|
|
2563
|
+
handleFileComplete = async ({
|
|
2564
|
+
params,
|
|
2565
|
+
respond,
|
|
2566
|
+
client,
|
|
2567
|
+
context,
|
|
2568
|
+
}: GatewayRequestHandlerOptions) => {
|
|
2358
2569
|
const accountId = normalizeAccountId(asString(params?.accountId || ''));
|
|
2359
2570
|
const connId = asString(client?.connId || '').trim() || `no-conn-${Date.now()}`;
|
|
2360
2571
|
const clientId = asString((params as any)?.clientId || '').trim() || undefined;
|
|
@@ -2377,10 +2588,14 @@ class BncrBridgeRuntime {
|
|
|
2377
2588
|
|
|
2378
2589
|
try {
|
|
2379
2590
|
if (st.receivedChunks.size < st.totalChunks) {
|
|
2380
|
-
throw new Error(
|
|
2591
|
+
throw new Error(
|
|
2592
|
+
`chunk not complete received=${st.receivedChunks.size} total=${st.totalChunks}`,
|
|
2593
|
+
);
|
|
2381
2594
|
}
|
|
2382
2595
|
|
|
2383
|
-
const ordered = Array.from(st.bufferByChunk.entries())
|
|
2596
|
+
const ordered = Array.from(st.bufferByChunk.entries())
|
|
2597
|
+
.sort((a, b) => a[0] - b[0])
|
|
2598
|
+
.map((x) => x[1]);
|
|
2384
2599
|
const merged = Buffer.concat(ordered);
|
|
2385
2600
|
if (st.fileSize > 0 && merged.length !== st.fileSize) {
|
|
2386
2601
|
throw new Error(`file size mismatch expected=${st.fileSize} got=${merged.length}`);
|
|
@@ -2515,7 +2730,24 @@ class BncrBridgeRuntime {
|
|
|
2515
2730
|
|
|
2516
2731
|
handleInbound = async ({ params, respond, client, context }: GatewayRequestHandlerOptions) => {
|
|
2517
2732
|
const parsed = parseBncrInboundParams(params);
|
|
2518
|
-
const {
|
|
2733
|
+
const {
|
|
2734
|
+
accountId,
|
|
2735
|
+
platform,
|
|
2736
|
+
groupId,
|
|
2737
|
+
userId,
|
|
2738
|
+
sessionKeyfromroute,
|
|
2739
|
+
route,
|
|
2740
|
+
text,
|
|
2741
|
+
msgType,
|
|
2742
|
+
mediaBase64,
|
|
2743
|
+
mediaPathFromTransfer,
|
|
2744
|
+
mimeType,
|
|
2745
|
+
fileName,
|
|
2746
|
+
msgId,
|
|
2747
|
+
dedupKey,
|
|
2748
|
+
peer,
|
|
2749
|
+
extracted,
|
|
2750
|
+
} = parsed;
|
|
2519
2751
|
const connId = asString(client?.connId || '').trim() || `no-conn-${Date.now()}`;
|
|
2520
2752
|
const clientId = asString((params as any)?.clientId || '').trim() || undefined;
|
|
2521
2753
|
this.rememberGatewayContext(context);
|
|
@@ -2555,13 +2787,21 @@ class BncrBridgeRuntime {
|
|
|
2555
2787
|
return;
|
|
2556
2788
|
}
|
|
2557
2789
|
|
|
2558
|
-
const
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2790
|
+
const canonicalAgentId = this.ensureCanonicalAgentId({
|
|
2791
|
+
cfg,
|
|
2792
|
+
accountId,
|
|
2793
|
+
peer,
|
|
2794
|
+
channelId: CHANNEL_ID,
|
|
2795
|
+
});
|
|
2796
|
+
const resolvedRoute = this.api.runtime.channel.routing.resolveAgentRoute({
|
|
2797
|
+
cfg,
|
|
2798
|
+
channel: CHANNEL_ID,
|
|
2799
|
+
accountId,
|
|
2800
|
+
peer,
|
|
2801
|
+
});
|
|
2802
|
+
const baseSessionKey =
|
|
2803
|
+
normalizeInboundSessionKey(sessionKeyfromroute, route, canonicalAgentId) ||
|
|
2804
|
+
resolvedRoute.sessionKey;
|
|
2565
2805
|
const taskSessionKey = withTaskSessionKey(baseSessionKey, extracted.taskKey);
|
|
2566
2806
|
const sessionKey = taskSessionKey || baseSessionKey;
|
|
2567
2807
|
|
|
@@ -2578,7 +2818,9 @@ class BncrBridgeRuntime {
|
|
|
2578
2818
|
channelId: CHANNEL_ID,
|
|
2579
2819
|
cfg,
|
|
2580
2820
|
parsed,
|
|
2581
|
-
|
|
2821
|
+
canonicalAgentId,
|
|
2822
|
+
rememberSessionRoute: (sessionKey, accountId, route) =>
|
|
2823
|
+
this.rememberSessionRoute(sessionKey, accountId, route),
|
|
2582
2824
|
enqueueFromReply: (args) => this.enqueueFromReply(args),
|
|
2583
2825
|
setInboundActivity: (accountId, at) => {
|
|
2584
2826
|
this.lastInboundByAccount.set(accountId, at);
|
|
@@ -2664,7 +2906,8 @@ class BncrBridgeRuntime {
|
|
|
2664
2906
|
text: asString(ctx.text || ''),
|
|
2665
2907
|
mediaLocalRoots: ctx.mediaLocalRoots,
|
|
2666
2908
|
resolveVerifiedTarget: (to, accountId) => this.resolveVerifiedTarget(to, accountId),
|
|
2667
|
-
rememberSessionRoute: (sessionKey, accountId, route) =>
|
|
2909
|
+
rememberSessionRoute: (sessionKey, accountId, route) =>
|
|
2910
|
+
this.rememberSessionRoute(sessionKey, accountId, route),
|
|
2668
2911
|
enqueueFromReply: (args) => this.enqueueFromReply(args),
|
|
2669
2912
|
createMessageId: () => randomUUID(),
|
|
2670
2913
|
});
|
|
@@ -2708,7 +2951,8 @@ class BncrBridgeRuntime {
|
|
|
2708
2951
|
audioAsVoice,
|
|
2709
2952
|
mediaLocalRoots: ctx.mediaLocalRoots,
|
|
2710
2953
|
resolveVerifiedTarget: (to, accountId) => this.resolveVerifiedTarget(to, accountId),
|
|
2711
|
-
rememberSessionRoute: (sessionKey, accountId, route) =>
|
|
2954
|
+
rememberSessionRoute: (sessionKey, accountId, route) =>
|
|
2955
|
+
this.rememberSessionRoute(sessionKey, accountId, route),
|
|
2712
2956
|
enqueueFromReply: (args) => this.enqueueFromReply(args),
|
|
2713
2957
|
createMessageId: () => randomUUID(),
|
|
2714
2958
|
});
|
|
@@ -2723,10 +2967,13 @@ export function createBncrChannelPlugin(bridge: BncrBridgeRuntime) {
|
|
|
2723
2967
|
const messageActions: ChannelMessageActionAdapter = {
|
|
2724
2968
|
describeMessageTool: ({ cfg }) => {
|
|
2725
2969
|
const channelCfg = cfg?.channels?.[CHANNEL_ID];
|
|
2726
|
-
const hasExplicitConfiguredAccount =
|
|
2727
|
-
&&
|
|
2728
|
-
|
|
2729
|
-
|
|
2970
|
+
const hasExplicitConfiguredAccount =
|
|
2971
|
+
Boolean(channelCfg && typeof channelCfg === 'object') &&
|
|
2972
|
+
resolveBncrChannelPolicy(channelCfg).enabled !== false &&
|
|
2973
|
+
Boolean(channelCfg.accounts && typeof channelCfg.accounts === 'object') &&
|
|
2974
|
+
Object.keys(channelCfg.accounts).some(
|
|
2975
|
+
(accountId) => resolveAccount(cfg, accountId).enabled !== false,
|
|
2976
|
+
);
|
|
2730
2977
|
|
|
2731
2978
|
const hasConnectedRuntime = listAccountIds(cfg).some((accountId) => {
|
|
2732
2979
|
const resolved = resolveAccount(cfg, accountId);
|
|
@@ -2746,7 +2993,8 @@ export function createBncrChannelPlugin(bridge: BncrBridgeRuntime) {
|
|
|
2746
2993
|
supportsAction: ({ action }) => action === 'send',
|
|
2747
2994
|
extractToolSend: ({ args }) => extractToolSend(args, 'sendMessage'),
|
|
2748
2995
|
handleAction: async ({ action, params, accountId, mediaLocalRoots }) => {
|
|
2749
|
-
if (action !== 'send')
|
|
2996
|
+
if (action !== 'send')
|
|
2997
|
+
throw new Error(`Action ${action} is not supported for provider ${CHANNEL_ID}.`);
|
|
2750
2998
|
const to = readStringParam(params, 'to', { required: true });
|
|
2751
2999
|
const message = readStringParam(params, 'message', { allowEmpty: true }) ?? '';
|
|
2752
3000
|
const caption = readStringParam(params, 'caption', { allowEmpty: true }) ?? '';
|
|
@@ -2758,36 +3006,40 @@ export function createBncrChannelPlugin(bridge: BncrBridgeRuntime) {
|
|
|
2758
3006
|
readStringParam(params, 'mediaUrl', { trim: false });
|
|
2759
3007
|
const asVoice = readBooleanParam(params, 'asVoice') ?? false;
|
|
2760
3008
|
const audioAsVoice = readBooleanParam(params, 'audioAsVoice') ?? false;
|
|
2761
|
-
const resolvedAccountId = normalizeAccountId(
|
|
3009
|
+
const resolvedAccountId = normalizeAccountId(
|
|
3010
|
+
readStringParam(params, 'accountId') ?? accountId,
|
|
3011
|
+
);
|
|
2762
3012
|
|
|
2763
3013
|
if (!content.trim() && !mediaUrl) throw new Error('send requires text or media');
|
|
2764
3014
|
|
|
2765
3015
|
const result = mediaUrl
|
|
2766
3016
|
? await sendBncrMedia({
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
3017
|
+
channelId: CHANNEL_ID,
|
|
3018
|
+
accountId: resolvedAccountId,
|
|
3019
|
+
to,
|
|
3020
|
+
text: content,
|
|
3021
|
+
mediaUrl,
|
|
3022
|
+
asVoice,
|
|
3023
|
+
audioAsVoice,
|
|
3024
|
+
mediaLocalRoots,
|
|
3025
|
+
resolveVerifiedTarget: (to, accountId) => bridge.resolveVerifiedTarget(to, accountId),
|
|
3026
|
+
rememberSessionRoute: (sessionKey, accountId, route) =>
|
|
3027
|
+
bridge.rememberSessionRoute(sessionKey, accountId, route),
|
|
3028
|
+
enqueueFromReply: (args) => bridge.enqueueFromReply(args as any),
|
|
3029
|
+
createMessageId: () => randomUUID(),
|
|
3030
|
+
})
|
|
2780
3031
|
: await sendBncrText({
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
3032
|
+
channelId: CHANNEL_ID,
|
|
3033
|
+
accountId: resolvedAccountId,
|
|
3034
|
+
to,
|
|
3035
|
+
text: content,
|
|
3036
|
+
mediaLocalRoots,
|
|
3037
|
+
resolveVerifiedTarget: (to, accountId) => bridge.resolveVerifiedTarget(to, accountId),
|
|
3038
|
+
rememberSessionRoute: (sessionKey, accountId, route) =>
|
|
3039
|
+
bridge.rememberSessionRoute(sessionKey, accountId, route),
|
|
3040
|
+
enqueueFromReply: (args) => bridge.enqueueFromReply(args as any),
|
|
3041
|
+
createMessageId: () => randomUUID(),
|
|
3042
|
+
});
|
|
2791
3043
|
|
|
2792
3044
|
return jsonResult({ ok: true, ...result });
|
|
2793
3045
|
},
|
|
@@ -2820,7 +3072,7 @@ export function createBncrChannelPlugin(bridge: BncrBridgeRuntime) {
|
|
|
2820
3072
|
looksLikeId: (raw: string, normalized?: string) => {
|
|
2821
3073
|
return Boolean(asString(normalized || raw).trim());
|
|
2822
3074
|
},
|
|
2823
|
-
hint: 'Standard to=Bncr:<platform>:<group>:<user> or Bncr:<platform>:<user>; sessionKey keeps existing strict/legacy compatibility, canonical sessionKey=agent
|
|
3075
|
+
hint: 'Standard to=Bncr:<platform>:<group>:<user> or Bncr:<platform>:<user>; sessionKey keeps existing strict/legacy compatibility, canonical sessionKey=agent:<agentId>:bncr:direct:<hex>',
|
|
2824
3076
|
},
|
|
2825
3077
|
},
|
|
2826
3078
|
configSchema: BncrConfigSchema,
|
|
@@ -2873,30 +3125,35 @@ export function createBncrChannelPlugin(bridge: BncrBridgeRuntime) {
|
|
|
2873
3125
|
},
|
|
2874
3126
|
outbound: {
|
|
2875
3127
|
deliveryMode: 'gateway' as const,
|
|
2876
|
-
textChunkLimit: 4000,
|
|
2877
3128
|
sendText: bridge.channelSendText,
|
|
2878
3129
|
sendMedia: bridge.channelSendMedia,
|
|
2879
|
-
replyAction: async (ctx: any) =>
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
3130
|
+
replyAction: async (ctx: any) =>
|
|
3131
|
+
sendBncrReplyAction({
|
|
3132
|
+
accountId: normalizeAccountId(ctx?.accountId),
|
|
3133
|
+
to: asString(ctx?.to || '').trim(),
|
|
3134
|
+
text: asString(ctx?.text || ''),
|
|
3135
|
+
replyToMessageId:
|
|
3136
|
+
asString(ctx?.replyToId || ctx?.replyToMessageId || '').trim() || undefined,
|
|
3137
|
+
sendText: async ({ accountId, to, text }) =>
|
|
3138
|
+
bridge.channelSendText({ accountId, to, text }),
|
|
3139
|
+
}),
|
|
3140
|
+
deleteAction: async (ctx: any) =>
|
|
3141
|
+
deleteBncrMessageAction({
|
|
3142
|
+
accountId: normalizeAccountId(ctx?.accountId),
|
|
3143
|
+
targetMessageId: asString(ctx?.messageId || ctx?.targetMessageId || '').trim(),
|
|
3144
|
+
}),
|
|
3145
|
+
reactAction: async (ctx: any) =>
|
|
3146
|
+
reactBncrMessageAction({
|
|
3147
|
+
accountId: normalizeAccountId(ctx?.accountId),
|
|
3148
|
+
targetMessageId: asString(ctx?.messageId || ctx?.targetMessageId || '').trim(),
|
|
3149
|
+
emoji: asString(ctx?.emoji || '').trim(),
|
|
3150
|
+
}),
|
|
3151
|
+
editAction: async (ctx: any) =>
|
|
3152
|
+
editBncrMessageAction({
|
|
3153
|
+
accountId: normalizeAccountId(ctx?.accountId),
|
|
3154
|
+
targetMessageId: asString(ctx?.messageId || ctx?.targetMessageId || '').trim(),
|
|
3155
|
+
text: asString(ctx?.text || ''),
|
|
3156
|
+
}),
|
|
2900
3157
|
},
|
|
2901
3158
|
status: {
|
|
2902
3159
|
defaultRuntime: createDefaultChannelRuntimeState(BNCR_DEFAULT_ACCOUNT_ID, {
|
|
@@ -2923,9 +3180,7 @@ export function createBncrChannelPlugin(bridge: BncrBridgeRuntime) {
|
|
|
2923
3180
|
const lastOutboundAgo = rt?.lastOutboundAgo ?? meta.lastOutboundAgo ?? '-';
|
|
2924
3181
|
const diagnostics = rt?.diagnostics ?? meta.diagnostics ?? null;
|
|
2925
3182
|
// 右侧状态字段统一:离线时也显示 Status(避免出现 configured 文案)
|
|
2926
|
-
const normalizedMode = rt?.mode === 'linked'
|
|
2927
|
-
? 'linked'
|
|
2928
|
-
: 'Status';
|
|
3183
|
+
const normalizedMode = rt?.mode === 'linked' ? 'linked' : 'Status';
|
|
2929
3184
|
|
|
2930
3185
|
const displayName = resolveDefaultDisplayName(account?.name, account?.accountId);
|
|
2931
3186
|
|