@xmoxmo/bncr 0.1.2 → 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/index.ts +60 -15
- package/package.json +7 -2
- package/scripts/check-register-drift.mjs +17 -5
- package/src/channel.ts +572 -252
- 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;
|
|
@@ -155,13 +169,11 @@ function asString(v: unknown, fallback = ''): string {
|
|
|
155
169
|
return String(v);
|
|
156
170
|
}
|
|
157
171
|
|
|
158
|
-
|
|
159
172
|
function backoffMs(retryCount: number): number {
|
|
160
173
|
// 1s,2s,4s,8s... capped by retry count checks
|
|
161
174
|
return Math.max(1_000, 1_000 * 2 ** Math.max(0, retryCount - 1));
|
|
162
175
|
}
|
|
163
176
|
|
|
164
|
-
|
|
165
177
|
function fileExtFromMime(mimeType?: string): string {
|
|
166
178
|
const mt = asString(mimeType || '').toLowerCase();
|
|
167
179
|
const map: Record<string, string> = {
|
|
@@ -184,7 +196,15 @@ function fileExtFromMime(mimeType?: string): string {
|
|
|
184
196
|
function sanitizeFileName(rawName?: string, fallback = 'file.bin'): string {
|
|
185
197
|
const name = asString(rawName || '').trim();
|
|
186
198
|
const base = name || fallback;
|
|
187
|
-
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();
|
|
188
208
|
return cleaned || fallback;
|
|
189
209
|
}
|
|
190
210
|
|
|
@@ -196,7 +216,11 @@ function buildTimestampFileName(mimeType?: string): string {
|
|
|
196
216
|
return `bncr_${ts}_${Math.random().toString(16).slice(2, 8)}${ext}`;
|
|
197
217
|
}
|
|
198
218
|
|
|
199
|
-
function resolveOutboundFileName(params: {
|
|
219
|
+
function resolveOutboundFileName(params: {
|
|
220
|
+
mediaUrl?: string;
|
|
221
|
+
fileName?: string;
|
|
222
|
+
mimeType?: string;
|
|
223
|
+
}): string {
|
|
200
224
|
const mediaUrl = asString(params.mediaUrl || '').trim();
|
|
201
225
|
const mimeType = asString(params.mimeType || '').trim();
|
|
202
226
|
|
|
@@ -234,12 +258,15 @@ class BncrBridgeRuntime {
|
|
|
234
258
|
private lastInboundAtGlobal: number | null = null;
|
|
235
259
|
private lastActivityAtGlobal: number | null = null;
|
|
236
260
|
private lastAckAtGlobal: number | null = null;
|
|
237
|
-
private recentConnections = new Map<
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
261
|
+
private recentConnections = new Map<
|
|
262
|
+
string,
|
|
263
|
+
{
|
|
264
|
+
epoch: number;
|
|
265
|
+
connectedAt: number;
|
|
266
|
+
lastActivityAt: number | null;
|
|
267
|
+
isPrimary: boolean;
|
|
268
|
+
}
|
|
269
|
+
>();
|
|
243
270
|
private staleCounters = {
|
|
244
271
|
staleConnect: 0,
|
|
245
272
|
staleInbound: 0,
|
|
@@ -274,14 +301,26 @@ class BncrBridgeRuntime {
|
|
|
274
301
|
private outbox = new Map<string, OutboxEntry>(); // messageId -> entry
|
|
275
302
|
private deadLetter: OutboxEntry[] = [];
|
|
276
303
|
|
|
277
|
-
private sessionRoutes = new Map<
|
|
278
|
-
|
|
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
|
+
>();
|
|
279
312
|
|
|
280
313
|
private recentInbound = new Map<string, number>();
|
|
281
|
-
private lastSessionByAccount = new Map<
|
|
314
|
+
private lastSessionByAccount = new Map<
|
|
315
|
+
string,
|
|
316
|
+
{ sessionKey: string; scope: string; updatedAt: number }
|
|
317
|
+
>();
|
|
282
318
|
private lastActivityByAccount = new Map<string, number>();
|
|
283
319
|
private lastInboundByAccount = new Map<string, number>();
|
|
284
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;
|
|
285
324
|
|
|
286
325
|
// 内置健康/回归计数(替代独立脚本)
|
|
287
326
|
private startedAt = now();
|
|
@@ -299,11 +338,14 @@ class BncrBridgeRuntime {
|
|
|
299
338
|
// 文件互传状态(V1:尽力而为,重连不续传)
|
|
300
339
|
private fileSendTransfers = new Map<string, FileSendTransferState>(); // OpenClaw -> Bncr(服务端发起)
|
|
301
340
|
private fileRecvTransfers = new Map<string, FileRecvTransferState>(); // Bncr -> OpenClaw(客户端发起)
|
|
302
|
-
private fileAckWaiters = new Map<
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
+
>();
|
|
307
349
|
|
|
308
350
|
constructor(api: OpenClawPluginApi) {
|
|
309
351
|
this.api = api;
|
|
@@ -318,7 +360,11 @@ class BncrBridgeRuntime {
|
|
|
318
360
|
}
|
|
319
361
|
|
|
320
362
|
private classifyRegisterTrace(stack: string) {
|
|
321
|
-
if (
|
|
363
|
+
if (
|
|
364
|
+
stack.includes('prepareSecretsRuntimeSnapshot') ||
|
|
365
|
+
stack.includes('resolveRuntimeWebTools') ||
|
|
366
|
+
stack.includes('resolvePluginWebSearchProviders')
|
|
367
|
+
) {
|
|
322
368
|
return 'runtime/webtools';
|
|
323
369
|
}
|
|
324
370
|
if (stack.includes('startGatewayServer') || stack.includes('loadGatewayPlugins')) {
|
|
@@ -348,7 +394,9 @@ class BncrBridgeRuntime {
|
|
|
348
394
|
return winner;
|
|
349
395
|
}
|
|
350
396
|
|
|
351
|
-
private captureDriftSnapshot(
|
|
397
|
+
private captureDriftSnapshot(
|
|
398
|
+
summary: ReturnType<BncrBridgeRuntime['buildRegisterTraceSummary']>,
|
|
399
|
+
) {
|
|
352
400
|
this.lastDriftSnapshot = {
|
|
353
401
|
capturedAt: now(),
|
|
354
402
|
registerCount: this.registerCount,
|
|
@@ -374,7 +422,7 @@ class BncrBridgeRuntime {
|
|
|
374
422
|
|
|
375
423
|
for (const trace of this.registerTraceRecent) {
|
|
376
424
|
buckets[trace.stackBucket] = (buckets[trace.stackBucket] || 0) + 1;
|
|
377
|
-
const isWarmup = baseline != null &&
|
|
425
|
+
const isWarmup = baseline != null && trace.ts - baseline <= REGISTER_WARMUP_WINDOW_MS;
|
|
378
426
|
if (isWarmup) {
|
|
379
427
|
warmupCount += 1;
|
|
380
428
|
} else {
|
|
@@ -447,14 +495,13 @@ class BncrBridgeRuntime {
|
|
|
447
495
|
stackBucket,
|
|
448
496
|
};
|
|
449
497
|
this.registerTraceRecent.push(trace);
|
|
450
|
-
if (this.registerTraceRecent.length > 12)
|
|
498
|
+
if (this.registerTraceRecent.length > 12)
|
|
499
|
+
this.registerTraceRecent.splice(0, this.registerTraceRecent.length - 12);
|
|
451
500
|
|
|
452
501
|
const summary = this.buildRegisterTraceSummary();
|
|
453
502
|
if (summary.postWarmupRegisterCount > 0) this.captureDriftSnapshot(summary);
|
|
454
503
|
|
|
455
|
-
this.api.logger.info?.(
|
|
456
|
-
`[bncr-register-trace] ${JSON.stringify(trace)}`,
|
|
457
|
-
);
|
|
504
|
+
this.api.logger.info?.(`[bncr-register-trace] ${JSON.stringify(trace)}`);
|
|
458
505
|
}
|
|
459
506
|
|
|
460
507
|
private createLeaseId() {
|
|
@@ -488,26 +535,55 @@ class BncrBridgeRuntime {
|
|
|
488
535
|
}
|
|
489
536
|
|
|
490
537
|
private observeLease(
|
|
491
|
-
kind:
|
|
538
|
+
kind:
|
|
539
|
+
| 'connect'
|
|
540
|
+
| 'inbound'
|
|
541
|
+
| 'activity'
|
|
542
|
+
| 'ack'
|
|
543
|
+
| 'file.init'
|
|
544
|
+
| 'file.chunk'
|
|
545
|
+
| 'file.complete'
|
|
546
|
+
| 'file.abort',
|
|
492
547
|
params: { leaseId?: string; connectionEpoch?: number },
|
|
493
548
|
) {
|
|
494
549
|
const leaseId = typeof params.leaseId === 'string' ? params.leaseId.trim() : '';
|
|
495
|
-
const connectionEpoch =
|
|
550
|
+
const connectionEpoch =
|
|
551
|
+
typeof params.connectionEpoch === 'number' ? params.connectionEpoch : undefined;
|
|
496
552
|
if (!leaseId && connectionEpoch == null) return { stale: false, reason: 'missing' as const };
|
|
497
|
-
const staleByLease =
|
|
498
|
-
|
|
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;
|
|
499
559
|
const stale = staleByLease || staleByEpoch;
|
|
500
560
|
if (!stale) return { stale: false, reason: 'ok' as const };
|
|
501
561
|
this.staleCounters.lastStaleAt = now();
|
|
502
562
|
switch (kind) {
|
|
503
|
-
case 'connect':
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
case '
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
case '
|
|
510
|
-
|
|
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;
|
|
511
587
|
}
|
|
512
588
|
this.api.logger.warn?.(
|
|
513
589
|
`[bncr] stale ${kind} observed lease=${leaseId || '-'} epoch=${connectionEpoch ?? '-'} currentLease=${this.primaryLeaseId || '-'} currentEpoch=${this.connectionEpoch}`,
|
|
@@ -581,10 +657,18 @@ class BncrBridgeRuntime {
|
|
|
581
657
|
startService = async (ctx: OpenClawPluginServiceContext, debug?: boolean) => {
|
|
582
658
|
this.statePath = path.join(ctx.stateDir, 'bncr-bridge-state.json');
|
|
583
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
|
+
}
|
|
584
666
|
if (typeof debug === 'boolean') BNCR_DEBUG_VERBOSE = debug;
|
|
585
667
|
const bootDiag = this.buildIntegratedDiagnostics(BNCR_DEFAULT_ACCOUNT_ID);
|
|
586
668
|
if (BNCR_DEBUG_VERBOSE) {
|
|
587
|
-
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
|
+
);
|
|
588
672
|
}
|
|
589
673
|
};
|
|
590
674
|
|
|
@@ -630,6 +714,65 @@ class BncrBridgeRuntime {
|
|
|
630
714
|
}
|
|
631
715
|
}
|
|
632
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
|
+
|
|
633
776
|
private countInvalidOutboxSessionKeys(accountId: string): number {
|
|
634
777
|
const acc = normalizeAccountId(accountId);
|
|
635
778
|
let count = 0;
|
|
@@ -642,7 +785,8 @@ class BncrBridgeRuntime {
|
|
|
642
785
|
|
|
643
786
|
private countLegacyAccountResidue(accountId: string): number {
|
|
644
787
|
const acc = normalizeAccountId(accountId);
|
|
645
|
-
const mismatched = (raw?: string | null) =>
|
|
788
|
+
const mismatched = (raw?: string | null) =>
|
|
789
|
+
asString(raw || '').trim() && normalizeAccountId(raw) !== acc;
|
|
646
790
|
|
|
647
791
|
let count = 0;
|
|
648
792
|
|
|
@@ -688,7 +832,8 @@ class BncrBridgeRuntime {
|
|
|
688
832
|
lastActivityAt: this.lastActivityByAccount.get(acc) || null,
|
|
689
833
|
lastInboundAt: this.lastInboundByAccount.get(acc) || null,
|
|
690
834
|
lastOutboundAt: this.lastOutboundByAccount.get(acc) || null,
|
|
691
|
-
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,
|
|
692
837
|
invalidOutboxSessionKeys: this.countInvalidOutboxSessionKeys(acc),
|
|
693
838
|
legacyAccountResidue: this.countLegacyAccountResidue(acc),
|
|
694
839
|
channelRoot: path.join(process.cwd(), 'plugins', 'bncr'),
|
|
@@ -709,11 +854,12 @@ class BncrBridgeRuntime {
|
|
|
709
854
|
if (!entry?.messageId) continue;
|
|
710
855
|
const accountId = normalizeAccountId(entry.accountId);
|
|
711
856
|
const sessionKey = asString(entry.sessionKey || '').trim();
|
|
712
|
-
const normalized = normalizeStoredSessionKey(sessionKey);
|
|
857
|
+
const normalized = normalizeStoredSessionKey(sessionKey, this.canonicalAgentId);
|
|
713
858
|
if (!normalized) continue;
|
|
714
859
|
|
|
715
860
|
const route = parseRouteLike(entry.route) || normalized.route;
|
|
716
|
-
const payload =
|
|
861
|
+
const payload =
|
|
862
|
+
entry.payload && typeof entry.payload === 'object' ? { ...entry.payload } : {};
|
|
717
863
|
(payload as any).sessionKey = normalized.sessionKey;
|
|
718
864
|
(payload as any).platform = route.platform;
|
|
719
865
|
(payload as any).groupId = route.groupId;
|
|
@@ -740,11 +886,12 @@ class BncrBridgeRuntime {
|
|
|
740
886
|
if (!entry?.messageId) continue;
|
|
741
887
|
const accountId = normalizeAccountId(entry.accountId);
|
|
742
888
|
const sessionKey = asString(entry.sessionKey || '').trim();
|
|
743
|
-
const normalized = normalizeStoredSessionKey(sessionKey);
|
|
889
|
+
const normalized = normalizeStoredSessionKey(sessionKey, this.canonicalAgentId);
|
|
744
890
|
if (!normalized) continue;
|
|
745
891
|
|
|
746
892
|
const route = parseRouteLike(entry.route) || normalized.route;
|
|
747
|
-
const payload =
|
|
893
|
+
const payload =
|
|
894
|
+
entry.payload && typeof entry.payload === 'object' ? { ...entry.payload } : {};
|
|
748
895
|
(payload as any).sessionKey = normalized.sessionKey;
|
|
749
896
|
(payload as any).platform = route.platform;
|
|
750
897
|
(payload as any).groupId = route.groupId;
|
|
@@ -767,7 +914,10 @@ class BncrBridgeRuntime {
|
|
|
767
914
|
this.sessionRoutes.clear();
|
|
768
915
|
this.routeAliases.clear();
|
|
769
916
|
for (const item of data.sessionRoutes || []) {
|
|
770
|
-
const normalized = normalizeStoredSessionKey(
|
|
917
|
+
const normalized = normalizeStoredSessionKey(
|
|
918
|
+
asString(item?.sessionKey || ''),
|
|
919
|
+
this.canonicalAgentId,
|
|
920
|
+
);
|
|
771
921
|
if (!normalized) continue;
|
|
772
922
|
|
|
773
923
|
const route = parseRouteLike(item?.route) || normalized.route;
|
|
@@ -787,7 +937,10 @@ class BncrBridgeRuntime {
|
|
|
787
937
|
this.lastSessionByAccount.clear();
|
|
788
938
|
for (const item of data.lastSessionByAccount || []) {
|
|
789
939
|
const accountId = normalizeAccountId(item?.accountId);
|
|
790
|
-
const normalized = normalizeStoredSessionKey(
|
|
940
|
+
const normalized = normalizeStoredSessionKey(
|
|
941
|
+
asString(item?.sessionKey || ''),
|
|
942
|
+
this.canonicalAgentId,
|
|
943
|
+
);
|
|
791
944
|
const updatedAt = Number(item?.updatedAt || 0);
|
|
792
945
|
if (!normalized || !Number.isFinite(updatedAt) || updatedAt <= 0) continue;
|
|
793
946
|
|
|
@@ -823,24 +976,38 @@ class BncrBridgeRuntime {
|
|
|
823
976
|
this.lastOutboundByAccount.set(accountId, updatedAt);
|
|
824
977
|
}
|
|
825
978
|
|
|
826
|
-
this.lastDriftSnapshot =
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
:
|
|
842
|
-
|
|
843
|
-
|
|
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;
|
|
844
1011
|
|
|
845
1012
|
// 兼容旧状态文件:若尚未持久化 lastSession*/lastActivity*,从 sessionRoutes 回填。
|
|
846
1013
|
if (this.lastSessionByAccount.size === 0 && this.sessionRoutes.size > 0) {
|
|
@@ -884,24 +1051,32 @@ class BncrBridgeRuntime {
|
|
|
884
1051
|
outbox: Array.from(this.outbox.values()),
|
|
885
1052
|
deadLetter: this.deadLetter.slice(-1000),
|
|
886
1053
|
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
|
-
|
|
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
|
+
),
|
|
905
1080
|
lastDriftSnapshot: this.lastDriftSnapshot
|
|
906
1081
|
? {
|
|
907
1082
|
capturedAt: this.lastDriftSnapshot.capturedAt,
|
|
@@ -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({
|
|
@@ -1117,10 +1296,14 @@ class BncrBridgeRuntime {
|
|
|
1117
1296
|
if (!directPayloads.length) continue;
|
|
1118
1297
|
|
|
1119
1298
|
try {
|
|
1120
|
-
ctx.broadcastToConnIds(
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1299
|
+
ctx.broadcastToConnIds(
|
|
1300
|
+
BNCR_PUSH_EVENT,
|
|
1301
|
+
{
|
|
1302
|
+
forcePush: true,
|
|
1303
|
+
items: directPayloads,
|
|
1304
|
+
},
|
|
1305
|
+
new Set(directConnIds),
|
|
1306
|
+
);
|
|
1124
1307
|
|
|
1125
1308
|
const pushedIds = directPayloads
|
|
1126
1309
|
.map((item: any) => asString(item?.messageId || item?.idempotencyKey || '').trim())
|
|
@@ -1198,7 +1381,8 @@ class BncrBridgeRuntime {
|
|
|
1198
1381
|
}
|
|
1199
1382
|
|
|
1200
1383
|
if (localNextDelay != null) {
|
|
1201
|
-
globalNextDelay =
|
|
1384
|
+
globalNextDelay =
|
|
1385
|
+
globalNextDelay == null ? localNextDelay : Math.min(globalNextDelay, localNextDelay);
|
|
1202
1386
|
}
|
|
1203
1387
|
} finally {
|
|
1204
1388
|
this.pushDrainRunningAccounts.delete(acc);
|
|
@@ -1320,7 +1504,11 @@ class BncrBridgeRuntime {
|
|
|
1320
1504
|
}
|
|
1321
1505
|
|
|
1322
1506
|
const curConn = this.connections.get(current);
|
|
1323
|
-
if (
|
|
1507
|
+
if (
|
|
1508
|
+
!curConn ||
|
|
1509
|
+
t - curConn.lastSeenAt > CONNECT_TTL_MS ||
|
|
1510
|
+
nextConn.connectedAt >= curConn.connectedAt
|
|
1511
|
+
) {
|
|
1324
1512
|
this.activeConnectionByAccount.set(acc, key);
|
|
1325
1513
|
}
|
|
1326
1514
|
}
|
|
@@ -1372,9 +1560,6 @@ class BncrBridgeRuntime {
|
|
|
1372
1560
|
const info = { accountId: acc, route, updatedAt: t };
|
|
1373
1561
|
|
|
1374
1562
|
this.sessionRoutes.set(key, info);
|
|
1375
|
-
// 同步维护旧格式与新格式,便于平滑切换
|
|
1376
|
-
this.sessionRoutes.set(buildFallbackSessionKey(route), info);
|
|
1377
|
-
|
|
1378
1563
|
this.routeAliases.set(routeKey(acc, route), info);
|
|
1379
1564
|
this.lastSessionByAccount.set(acc, {
|
|
1380
1565
|
sessionKey: key,
|
|
@@ -1404,7 +1589,10 @@ class BncrBridgeRuntime {
|
|
|
1404
1589
|
// 1) 标准 to 仅认 Bncr:<platform>:<groupId>:<userId> / Bncr:<platform>:<userId>
|
|
1405
1590
|
// 2) 仍接受 strict sessionKey 作为内部兼容输入
|
|
1406
1591
|
// 3) 其他旧格式直接失败,并输出标准格式提示日志
|
|
1407
|
-
private resolveVerifiedTarget(
|
|
1592
|
+
private resolveVerifiedTarget(
|
|
1593
|
+
rawTarget: string,
|
|
1594
|
+
accountId: string,
|
|
1595
|
+
): { sessionKey: string; route: BncrRoute; displayScope: string } {
|
|
1408
1596
|
const acc = normalizeAccountId(accountId);
|
|
1409
1597
|
const raw = asString(rawTarget).trim();
|
|
1410
1598
|
if (!raw) throw new Error('bncr invalid target(empty)');
|
|
@@ -1424,17 +1612,23 @@ class BncrBridgeRuntime {
|
|
|
1424
1612
|
|
|
1425
1613
|
if (!route) {
|
|
1426
1614
|
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
|
|
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}`,
|
|
1428
1619
|
);
|
|
1429
|
-
throw new Error(`bncr invalid target(standard: Bncr:<platform>:<groupId>:<userId> | Bncr:<platform>:<userId>): ${raw}`);
|
|
1430
1620
|
}
|
|
1431
1621
|
|
|
1432
1622
|
const wantedRouteKey = routeKey(acc, route);
|
|
1433
1623
|
let best: { sessionKey: string; route: BncrRoute; updatedAt: number } | null = null;
|
|
1434
1624
|
|
|
1435
1625
|
if (BNCR_DEBUG_VERBOSE) {
|
|
1436
|
-
this.api.logger.info?.(
|
|
1437
|
-
|
|
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
|
+
);
|
|
1438
1632
|
}
|
|
1439
1633
|
|
|
1440
1634
|
for (const [key, info] of this.sessionRoutes.entries()) {
|
|
@@ -1446,29 +1640,40 @@ class BncrBridgeRuntime {
|
|
|
1446
1640
|
const updatedAt = Number(info.updatedAt || 0);
|
|
1447
1641
|
if (!best || updatedAt >= best.updatedAt) {
|
|
1448
1642
|
best = {
|
|
1449
|
-
sessionKey:
|
|
1643
|
+
sessionKey: key,
|
|
1450
1644
|
route: parsed.route,
|
|
1451
1645
|
updatedAt,
|
|
1452
1646
|
};
|
|
1453
1647
|
}
|
|
1454
1648
|
}
|
|
1455
1649
|
|
|
1456
|
-
// 直接根据raw生成标准sessionkey
|
|
1457
1650
|
if (!best) {
|
|
1458
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
|
+
});
|
|
1459
1660
|
best = {
|
|
1460
|
-
sessionKey:
|
|
1661
|
+
sessionKey: buildCanonicalBncrSessionKey(route, canonicalAgentId),
|
|
1461
1662
|
route,
|
|
1462
1663
|
updatedAt,
|
|
1463
1664
|
};
|
|
1464
1665
|
}
|
|
1465
1666
|
|
|
1466
1667
|
if (BNCR_DEBUG_VERBOSE) {
|
|
1467
|
-
this.api.logger.info?.(
|
|
1668
|
+
this.api.logger.info?.(
|
|
1669
|
+
`[bncr-target-incoming-best] raw=${raw} accountId=${acc} best=${JSON.stringify(best)}`,
|
|
1670
|
+
);
|
|
1468
1671
|
}
|
|
1469
1672
|
|
|
1470
1673
|
if (!best) {
|
|
1471
|
-
this.api.logger.warn?.(
|
|
1674
|
+
this.api.logger.warn?.(
|
|
1675
|
+
`[bncr-target-miss] raw=${raw} accountId=${acc} sessionRoutes=${this.sessionRoutes.size}`,
|
|
1676
|
+
);
|
|
1472
1677
|
throw new Error(`bncr target not found in known sessions: ${raw}`);
|
|
1473
1678
|
}
|
|
1474
1679
|
|
|
@@ -1496,11 +1701,19 @@ class BncrBridgeRuntime {
|
|
|
1496
1701
|
return `${transferId}|${stage}|${idx}`;
|
|
1497
1702
|
}
|
|
1498
1703
|
|
|
1499
|
-
private waitForFileAck(params: {
|
|
1704
|
+
private waitForFileAck(params: {
|
|
1705
|
+
transferId: string;
|
|
1706
|
+
stage: string;
|
|
1707
|
+
chunkIndex?: number;
|
|
1708
|
+
timeoutMs?: number;
|
|
1709
|
+
}) {
|
|
1500
1710
|
const transferId = asString(params.transferId).trim();
|
|
1501
1711
|
const stage = asString(params.stage).trim();
|
|
1502
1712
|
const key = this.fileAckKey(transferId, stage, params.chunkIndex);
|
|
1503
|
-
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
|
+
);
|
|
1504
1717
|
|
|
1505
1718
|
return new Promise<Record<string, unknown>>((resolve, reject) => {
|
|
1506
1719
|
const timer = setTimeout(() => {
|
|
@@ -1511,7 +1724,13 @@ class BncrBridgeRuntime {
|
|
|
1511
1724
|
});
|
|
1512
1725
|
}
|
|
1513
1726
|
|
|
1514
|
-
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
|
+
}) {
|
|
1515
1734
|
const transferId = asString(params.transferId).trim();
|
|
1516
1735
|
const stage = asString(params.stage).trim();
|
|
1517
1736
|
const key = this.fileAckKey(transferId, stage, params.chunkIndex);
|
|
@@ -1520,11 +1739,20 @@ class BncrBridgeRuntime {
|
|
|
1520
1739
|
this.fileAckWaiters.delete(key);
|
|
1521
1740
|
clearTimeout(waiter.timer);
|
|
1522
1741
|
if (params.ok) waiter.resolve(params.payload);
|
|
1523
|
-
else
|
|
1742
|
+
else
|
|
1743
|
+
waiter.reject(
|
|
1744
|
+
new Error(
|
|
1745
|
+
asString(params.payload?.errorMessage || params.payload?.error || 'file ack failed'),
|
|
1746
|
+
),
|
|
1747
|
+
);
|
|
1524
1748
|
return true;
|
|
1525
1749
|
}
|
|
1526
1750
|
|
|
1527
|
-
private pushFileEventToAccount(
|
|
1751
|
+
private pushFileEventToAccount(
|
|
1752
|
+
accountId: string,
|
|
1753
|
+
event: string,
|
|
1754
|
+
payload: Record<string, unknown>,
|
|
1755
|
+
) {
|
|
1528
1756
|
const connIds = this.resolvePushConnIds(accountId);
|
|
1529
1757
|
if (!connIds.size || !this.gatewayContext) {
|
|
1530
1758
|
throw new Error(`no active bncr connection for account=${accountId}`);
|
|
@@ -1547,7 +1775,9 @@ class BncrBridgeRuntime {
|
|
|
1547
1775
|
return dir;
|
|
1548
1776
|
}
|
|
1549
1777
|
|
|
1550
|
-
private async materializeRecvTransfer(
|
|
1778
|
+
private async materializeRecvTransfer(
|
|
1779
|
+
st: FileRecvTransferState,
|
|
1780
|
+
): Promise<{ path: string; fileSha256: string }> {
|
|
1551
1781
|
const dir = this.resolveInboundFilesDir();
|
|
1552
1782
|
const safeName = asString(st.fileName).trim() || `${st.transferId}.bin`;
|
|
1553
1783
|
const finalPath = path.join(dir, safeName);
|
|
@@ -1589,7 +1819,8 @@ class BncrBridgeRuntime {
|
|
|
1589
1819
|
lastActivityAt: this.lastActivityByAccount.get(acc) || null,
|
|
1590
1820
|
lastInboundAt: this.lastInboundByAccount.get(acc) || null,
|
|
1591
1821
|
lastOutboundAt: this.lastOutboundByAccount.get(acc) || null,
|
|
1592
|
-
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,
|
|
1593
1824
|
invalidOutboxSessionKeys: this.countInvalidOutboxSessionKeys(acc),
|
|
1594
1825
|
legacyAccountResidue: this.countLegacyAccountResidue(acc),
|
|
1595
1826
|
channelRoot: path.join(process.cwd(), 'plugins', 'bncr'),
|
|
@@ -1613,7 +1844,8 @@ class BncrBridgeRuntime {
|
|
|
1613
1844
|
lastActivityAt: this.lastActivityByAccount.get(acc) || null,
|
|
1614
1845
|
lastInboundAt: this.lastInboundByAccount.get(acc) || null,
|
|
1615
1846
|
lastOutboundAt: this.lastOutboundByAccount.get(acc) || null,
|
|
1616
|
-
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,
|
|
1617
1849
|
invalidOutboxSessionKeys: this.countInvalidOutboxSessionKeys(acc),
|
|
1618
1850
|
legacyAccountResidue: this.countLegacyAccountResidue(acc),
|
|
1619
1851
|
running: true,
|
|
@@ -1638,7 +1870,8 @@ class BncrBridgeRuntime {
|
|
|
1638
1870
|
lastActivityAt: this.lastActivityByAccount.get(acc) || null,
|
|
1639
1871
|
lastInboundAt: this.lastInboundByAccount.get(acc) || null,
|
|
1640
1872
|
lastOutboundAt: this.lastOutboundByAccount.get(acc) || null,
|
|
1641
|
-
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,
|
|
1642
1875
|
invalidOutboxSessionKeys: this.countInvalidOutboxSessionKeys(acc),
|
|
1643
1876
|
legacyAccountResidue: this.countLegacyAccountResidue(acc),
|
|
1644
1877
|
channelRoot: path.join(process.cwd(), 'plugins', 'bncr'),
|
|
@@ -1763,7 +1996,10 @@ class BncrBridgeRuntime {
|
|
|
1763
1996
|
timeoutMs?: number;
|
|
1764
1997
|
}): Promise<void> {
|
|
1765
1998
|
const { transferId, chunkIndex } = params;
|
|
1766
|
-
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
|
+
);
|
|
1767
2003
|
const started = now();
|
|
1768
2004
|
|
|
1769
2005
|
return new Promise<void>((resolve, reject) => {
|
|
@@ -1832,7 +2068,13 @@ class BncrBridgeRuntime {
|
|
|
1832
2068
|
route: BncrRoute;
|
|
1833
2069
|
mediaUrl: string;
|
|
1834
2070
|
mediaLocalRoots?: readonly string[];
|
|
1835
|
-
}): Promise<{
|
|
2071
|
+
}): Promise<{
|
|
2072
|
+
mode: 'base64' | 'chunk';
|
|
2073
|
+
mimeType?: string;
|
|
2074
|
+
fileName?: string;
|
|
2075
|
+
mediaBase64?: string;
|
|
2076
|
+
path?: string;
|
|
2077
|
+
}> {
|
|
1836
2078
|
const loaded = await this.api.runtime.media.loadWebMedia(params.mediaUrl, {
|
|
1837
2079
|
localRoots: params.mediaLocalRoots,
|
|
1838
2080
|
maxBytes: 50 * 1024 * 1024,
|
|
@@ -1884,21 +2126,25 @@ class BncrBridgeRuntime {
|
|
|
1884
2126
|
};
|
|
1885
2127
|
this.fileSendTransfers.set(transferId, st);
|
|
1886
2128
|
|
|
1887
|
-
ctx.broadcastToConnIds(
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
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
|
+
);
|
|
1902
2148
|
|
|
1903
2149
|
// 逐块发送并等待 ACK
|
|
1904
2150
|
for (let idx = 0; idx < totalChunks; idx++) {
|
|
@@ -1910,18 +2156,26 @@ class BncrBridgeRuntime {
|
|
|
1910
2156
|
let ok = false;
|
|
1911
2157
|
let lastErr: unknown = null;
|
|
1912
2158
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
1913
|
-
ctx.broadcastToConnIds(
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
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
|
+
);
|
|
1922
2172
|
|
|
1923
2173
|
try {
|
|
1924
|
-
await this.waitChunkAck({
|
|
2174
|
+
await this.waitChunkAck({
|
|
2175
|
+
transferId,
|
|
2176
|
+
chunkIndex: idx,
|
|
2177
|
+
timeoutMs: FILE_TRANSFER_ACK_TTL_MS,
|
|
2178
|
+
});
|
|
1925
2179
|
ok = true;
|
|
1926
2180
|
break;
|
|
1927
2181
|
} catch (err) {
|
|
@@ -1934,19 +2188,27 @@ class BncrBridgeRuntime {
|
|
|
1934
2188
|
st.status = 'aborted';
|
|
1935
2189
|
st.error = String((lastErr as any)?.message || lastErr || `chunk-${idx}-failed`);
|
|
1936
2190
|
this.fileSendTransfers.set(transferId, st);
|
|
1937
|
-
ctx.broadcastToConnIds(
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
2191
|
+
ctx.broadcastToConnIds(
|
|
2192
|
+
'bncr.file.abort',
|
|
2193
|
+
{
|
|
2194
|
+
transferId,
|
|
2195
|
+
reason: st.error,
|
|
2196
|
+
ts: now(),
|
|
2197
|
+
},
|
|
2198
|
+
connIds,
|
|
2199
|
+
);
|
|
1942
2200
|
throw new Error(st.error);
|
|
1943
2201
|
}
|
|
1944
2202
|
}
|
|
1945
2203
|
|
|
1946
|
-
ctx.broadcastToConnIds(
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
2204
|
+
ctx.broadcastToConnIds(
|
|
2205
|
+
'bncr.file.complete',
|
|
2206
|
+
{
|
|
2207
|
+
transferId,
|
|
2208
|
+
ts: now(),
|
|
2209
|
+
},
|
|
2210
|
+
connIds,
|
|
2211
|
+
);
|
|
1950
2212
|
|
|
1951
2213
|
const done = await this.waitCompleteAck({ transferId, timeoutMs: 60_000 });
|
|
1952
2214
|
|
|
@@ -1962,7 +2224,14 @@ class BncrBridgeRuntime {
|
|
|
1962
2224
|
accountId: string;
|
|
1963
2225
|
sessionKey: string;
|
|
1964
2226
|
route: BncrRoute;
|
|
1965
|
-
payload: {
|
|
2227
|
+
payload: {
|
|
2228
|
+
text?: string;
|
|
2229
|
+
mediaUrl?: string;
|
|
2230
|
+
mediaUrls?: string[];
|
|
2231
|
+
asVoice?: boolean;
|
|
2232
|
+
audioAsVoice?: boolean;
|
|
2233
|
+
kind?: 'block' | 'final';
|
|
2234
|
+
};
|
|
1966
2235
|
mediaLocalRoots?: readonly string[];
|
|
1967
2236
|
}) {
|
|
1968
2237
|
const { accountId, sessionKey, route, payload, mediaLocalRoots } = params;
|
|
@@ -2270,11 +2539,12 @@ class BncrBridgeRuntime {
|
|
|
2270
2539
|
return;
|
|
2271
2540
|
}
|
|
2272
2541
|
|
|
2273
|
-
const route =
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
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;
|
|
2278
2548
|
|
|
2279
2549
|
this.fileRecvTransfers.set(transferId, {
|
|
2280
2550
|
transferId,
|
|
@@ -2354,7 +2624,12 @@ class BncrBridgeRuntime {
|
|
|
2354
2624
|
}
|
|
2355
2625
|
};
|
|
2356
2626
|
|
|
2357
|
-
handleFileComplete = async ({
|
|
2627
|
+
handleFileComplete = async ({
|
|
2628
|
+
params,
|
|
2629
|
+
respond,
|
|
2630
|
+
client,
|
|
2631
|
+
context,
|
|
2632
|
+
}: GatewayRequestHandlerOptions) => {
|
|
2358
2633
|
const accountId = normalizeAccountId(asString(params?.accountId || ''));
|
|
2359
2634
|
const connId = asString(client?.connId || '').trim() || `no-conn-${Date.now()}`;
|
|
2360
2635
|
const clientId = asString((params as any)?.clientId || '').trim() || undefined;
|
|
@@ -2377,10 +2652,14 @@ class BncrBridgeRuntime {
|
|
|
2377
2652
|
|
|
2378
2653
|
try {
|
|
2379
2654
|
if (st.receivedChunks.size < st.totalChunks) {
|
|
2380
|
-
throw new Error(
|
|
2655
|
+
throw new Error(
|
|
2656
|
+
`chunk not complete received=${st.receivedChunks.size} total=${st.totalChunks}`,
|
|
2657
|
+
);
|
|
2381
2658
|
}
|
|
2382
2659
|
|
|
2383
|
-
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]);
|
|
2384
2663
|
const merged = Buffer.concat(ordered);
|
|
2385
2664
|
if (st.fileSize > 0 && merged.length !== st.fileSize) {
|
|
2386
2665
|
throw new Error(`file size mismatch expected=${st.fileSize} got=${merged.length}`);
|
|
@@ -2515,7 +2794,24 @@ class BncrBridgeRuntime {
|
|
|
2515
2794
|
|
|
2516
2795
|
handleInbound = async ({ params, respond, client, context }: GatewayRequestHandlerOptions) => {
|
|
2517
2796
|
const parsed = parseBncrInboundParams(params);
|
|
2518
|
-
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;
|
|
2519
2815
|
const connId = asString(client?.connId || '').trim() || `no-conn-${Date.now()}`;
|
|
2520
2816
|
const clientId = asString((params as any)?.clientId || '').trim() || undefined;
|
|
2521
2817
|
this.rememberGatewayContext(context);
|
|
@@ -2555,13 +2851,21 @@ class BncrBridgeRuntime {
|
|
|
2555
2851
|
return;
|
|
2556
2852
|
}
|
|
2557
2853
|
|
|
2558
|
-
const
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
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;
|
|
2565
2869
|
const taskSessionKey = withTaskSessionKey(baseSessionKey, extracted.taskKey);
|
|
2566
2870
|
const sessionKey = taskSessionKey || baseSessionKey;
|
|
2567
2871
|
|
|
@@ -2578,7 +2882,9 @@ class BncrBridgeRuntime {
|
|
|
2578
2882
|
channelId: CHANNEL_ID,
|
|
2579
2883
|
cfg,
|
|
2580
2884
|
parsed,
|
|
2581
|
-
|
|
2885
|
+
canonicalAgentId,
|
|
2886
|
+
rememberSessionRoute: (sessionKey, accountId, route) =>
|
|
2887
|
+
this.rememberSessionRoute(sessionKey, accountId, route),
|
|
2582
2888
|
enqueueFromReply: (args) => this.enqueueFromReply(args),
|
|
2583
2889
|
setInboundActivity: (accountId, at) => {
|
|
2584
2890
|
this.lastInboundByAccount.set(accountId, at);
|
|
@@ -2664,7 +2970,8 @@ class BncrBridgeRuntime {
|
|
|
2664
2970
|
text: asString(ctx.text || ''),
|
|
2665
2971
|
mediaLocalRoots: ctx.mediaLocalRoots,
|
|
2666
2972
|
resolveVerifiedTarget: (to, accountId) => this.resolveVerifiedTarget(to, accountId),
|
|
2667
|
-
rememberSessionRoute: (sessionKey, accountId, route) =>
|
|
2973
|
+
rememberSessionRoute: (sessionKey, accountId, route) =>
|
|
2974
|
+
this.rememberSessionRoute(sessionKey, accountId, route),
|
|
2668
2975
|
enqueueFromReply: (args) => this.enqueueFromReply(args),
|
|
2669
2976
|
createMessageId: () => randomUUID(),
|
|
2670
2977
|
});
|
|
@@ -2708,7 +3015,8 @@ class BncrBridgeRuntime {
|
|
|
2708
3015
|
audioAsVoice,
|
|
2709
3016
|
mediaLocalRoots: ctx.mediaLocalRoots,
|
|
2710
3017
|
resolveVerifiedTarget: (to, accountId) => this.resolveVerifiedTarget(to, accountId),
|
|
2711
|
-
rememberSessionRoute: (sessionKey, accountId, route) =>
|
|
3018
|
+
rememberSessionRoute: (sessionKey, accountId, route) =>
|
|
3019
|
+
this.rememberSessionRoute(sessionKey, accountId, route),
|
|
2712
3020
|
enqueueFromReply: (args) => this.enqueueFromReply(args),
|
|
2713
3021
|
createMessageId: () => randomUUID(),
|
|
2714
3022
|
});
|
|
@@ -2723,10 +3031,13 @@ export function createBncrChannelPlugin(bridge: BncrBridgeRuntime) {
|
|
|
2723
3031
|
const messageActions: ChannelMessageActionAdapter = {
|
|
2724
3032
|
describeMessageTool: ({ cfg }) => {
|
|
2725
3033
|
const channelCfg = cfg?.channels?.[CHANNEL_ID];
|
|
2726
|
-
const hasExplicitConfiguredAccount =
|
|
2727
|
-
&&
|
|
2728
|
-
|
|
2729
|
-
|
|
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
|
+
);
|
|
2730
3041
|
|
|
2731
3042
|
const hasConnectedRuntime = listAccountIds(cfg).some((accountId) => {
|
|
2732
3043
|
const resolved = resolveAccount(cfg, accountId);
|
|
@@ -2746,7 +3057,8 @@ export function createBncrChannelPlugin(bridge: BncrBridgeRuntime) {
|
|
|
2746
3057
|
supportsAction: ({ action }) => action === 'send',
|
|
2747
3058
|
extractToolSend: ({ args }) => extractToolSend(args, 'sendMessage'),
|
|
2748
3059
|
handleAction: async ({ action, params, accountId, mediaLocalRoots }) => {
|
|
2749
|
-
if (action !== 'send')
|
|
3060
|
+
if (action !== 'send')
|
|
3061
|
+
throw new Error(`Action ${action} is not supported for provider ${CHANNEL_ID}.`);
|
|
2750
3062
|
const to = readStringParam(params, 'to', { required: true });
|
|
2751
3063
|
const message = readStringParam(params, 'message', { allowEmpty: true }) ?? '';
|
|
2752
3064
|
const caption = readStringParam(params, 'caption', { allowEmpty: true }) ?? '';
|
|
@@ -2758,36 +3070,40 @@ export function createBncrChannelPlugin(bridge: BncrBridgeRuntime) {
|
|
|
2758
3070
|
readStringParam(params, 'mediaUrl', { trim: false });
|
|
2759
3071
|
const asVoice = readBooleanParam(params, 'asVoice') ?? false;
|
|
2760
3072
|
const audioAsVoice = readBooleanParam(params, 'audioAsVoice') ?? false;
|
|
2761
|
-
const resolvedAccountId = normalizeAccountId(
|
|
3073
|
+
const resolvedAccountId = normalizeAccountId(
|
|
3074
|
+
readStringParam(params, 'accountId') ?? accountId,
|
|
3075
|
+
);
|
|
2762
3076
|
|
|
2763
3077
|
if (!content.trim() && !mediaUrl) throw new Error('send requires text or media');
|
|
2764
3078
|
|
|
2765
3079
|
const result = mediaUrl
|
|
2766
3080
|
? await sendBncrMedia({
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
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
|
+
})
|
|
2780
3095
|
: await sendBncrText({
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
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
|
+
});
|
|
2791
3107
|
|
|
2792
3108
|
return jsonResult({ ok: true, ...result });
|
|
2793
3109
|
},
|
|
@@ -2820,7 +3136,7 @@ export function createBncrChannelPlugin(bridge: BncrBridgeRuntime) {
|
|
|
2820
3136
|
looksLikeId: (raw: string, normalized?: string) => {
|
|
2821
3137
|
return Boolean(asString(normalized || raw).trim());
|
|
2822
3138
|
},
|
|
2823
|
-
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>',
|
|
2824
3140
|
},
|
|
2825
3141
|
},
|
|
2826
3142
|
configSchema: BncrConfigSchema,
|
|
@@ -2876,27 +3192,33 @@ export function createBncrChannelPlugin(bridge: BncrBridgeRuntime) {
|
|
|
2876
3192
|
textChunkLimit: 4000,
|
|
2877
3193
|
sendText: bridge.channelSendText,
|
|
2878
3194
|
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
|
-
|
|
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
|
+
}),
|
|
2900
3222
|
},
|
|
2901
3223
|
status: {
|
|
2902
3224
|
defaultRuntime: createDefaultChannelRuntimeState(BNCR_DEFAULT_ACCOUNT_ID, {
|
|
@@ -2923,9 +3245,7 @@ export function createBncrChannelPlugin(bridge: BncrBridgeRuntime) {
|
|
|
2923
3245
|
const lastOutboundAgo = rt?.lastOutboundAgo ?? meta.lastOutboundAgo ?? '-';
|
|
2924
3246
|
const diagnostics = rt?.diagnostics ?? meta.diagnostics ?? null;
|
|
2925
3247
|
// 右侧状态字段统一:离线时也显示 Status(避免出现 configured 文案)
|
|
2926
|
-
const normalizedMode = rt?.mode === 'linked'
|
|
2927
|
-
? 'linked'
|
|
2928
|
-
: 'Status';
|
|
3248
|
+
const normalizedMode = rt?.mode === 'linked' ? 'linked' : 'Status';
|
|
2929
3249
|
|
|
2930
3250
|
const displayName = resolveDefaultDisplayName(account?.name, account?.accountId);
|
|
2931
3251
|
|