@xmoxmo/bncr 0.1.6 → 0.1.8
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 +18 -11
- package/package.json +1 -1
- package/src/channel.ts +382 -268
- package/src/core/logging.ts +65 -0
- package/src/core/targets.ts +97 -0
- package/src/messaging/inbound/commands.ts +4 -2
- package/src/messaging/inbound/dispatch.ts +4 -3
- package/src/messaging/outbound/media.ts +3 -1
- package/src/messaging/outbound/send.ts +11 -1
- package/src/messaging/outbound/session-route.ts +73 -0
- package/src/messaging/outbound/target-resolver.ts +56 -0
package/src/channel.ts
CHANGED
|
@@ -25,6 +25,10 @@ import {
|
|
|
25
25
|
resolveAccount,
|
|
26
26
|
resolveDefaultDisplayName,
|
|
27
27
|
} from './core/accounts.ts';
|
|
28
|
+
import {
|
|
29
|
+
emitBncrLog,
|
|
30
|
+
emitBncrLogLine,
|
|
31
|
+
} from './core/logging.ts';
|
|
28
32
|
import { BncrConfigSchema } from './core/config-schema.ts';
|
|
29
33
|
import { buildBncrPermissionSummary } from './core/permissions.ts';
|
|
30
34
|
import { resolveBncrChannelPolicy } from './core/policy.ts';
|
|
@@ -38,9 +42,11 @@ import {
|
|
|
38
42
|
import {
|
|
39
43
|
buildCanonicalBncrSessionKey,
|
|
40
44
|
formatDisplayScope,
|
|
45
|
+
formatTargetDisplay,
|
|
41
46
|
isLowerHex,
|
|
42
47
|
normalizeInboundSessionKey,
|
|
43
48
|
normalizeStoredSessionKey,
|
|
49
|
+
parseExplicitTarget,
|
|
44
50
|
parseRouteFromDisplayScope,
|
|
45
51
|
parseRouteFromHexScope,
|
|
46
52
|
parseRouteFromScope,
|
|
@@ -65,6 +71,11 @@ import {
|
|
|
65
71
|
resolveBncrOutboundMessageType,
|
|
66
72
|
} from './messaging/outbound/media.ts';
|
|
67
73
|
import { sendBncrMedia, sendBncrText } from './messaging/outbound/send.ts';
|
|
74
|
+
import { resolveBncrOutboundSessionRoute } from './messaging/outbound/session-route.ts';
|
|
75
|
+
import {
|
|
76
|
+
looksLikeBncrExplicitTarget,
|
|
77
|
+
resolveBncrOutboundTarget,
|
|
78
|
+
} from './messaging/outbound/target-resolver.ts';
|
|
68
79
|
const BRIDGE_VERSION = 2;
|
|
69
80
|
const BNCR_PUSH_EVENT = 'bncr.push';
|
|
70
81
|
const CONNECT_TTL_MS = 120_000;
|
|
@@ -360,6 +371,54 @@ class BncrBridgeRuntime {
|
|
|
360
371
|
return this.bridgeId;
|
|
361
372
|
}
|
|
362
373
|
|
|
374
|
+
private isDebugEnabled() {
|
|
375
|
+
return BNCR_DEBUG_VERBOSE;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
private logInfo(scope: string | undefined, message: string, options?: { debugOnly?: boolean }) {
|
|
379
|
+
emitBncrLog('info', scope, message, options, () => this.isDebugEnabled());
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
private logWarn(scope: string | undefined, message: string, options?: { debugOnly?: boolean }) {
|
|
383
|
+
emitBncrLog('warn', scope, message, options, () => this.isDebugEnabled());
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
private logError(scope: string | undefined, message: string, options?: { debugOnly?: boolean }) {
|
|
387
|
+
emitBncrLog('error', scope, message, options, () => this.isDebugEnabled());
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
private summarizeTextPreview(raw: string, limit = 8) {
|
|
392
|
+
const compact = asString(raw || '').replace(/\s+/g, ' ').trim();
|
|
393
|
+
if (!compact) return '-';
|
|
394
|
+
const chars = Array.from(compact);
|
|
395
|
+
return chars.length > limit ? `${chars.slice(0, Math.max(1, limit)).join('')}…` : compact;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private summarizeScope(route: BncrRoute) {
|
|
399
|
+
return formatDisplayScope(route);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private logInboundSummary(params: {
|
|
403
|
+
accountId: string;
|
|
404
|
+
route: BncrRoute;
|
|
405
|
+
msgType: string;
|
|
406
|
+
text: string;
|
|
407
|
+
hasMedia: boolean;
|
|
408
|
+
}) {
|
|
409
|
+
const type = params.hasMedia ? `${params.msgType}+media` : params.msgType;
|
|
410
|
+
const preview = this.summarizeTextPreview(params.text);
|
|
411
|
+
this.logInfo('inbound', [type, this.summarizeScope(params.route), preview].join('|'));
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
private logOutboundSummary(entry: OutboxEntry) {
|
|
415
|
+
const msg = (entry.payload as any)?.message || {};
|
|
416
|
+
const type = asString(msg.type || (entry.payload as any)?.type || 'unknown');
|
|
417
|
+
const text = asString(msg.msg || '');
|
|
418
|
+
const preview = this.summarizeTextPreview(text);
|
|
419
|
+
this.logInfo('outbound', [type, this.summarizeScope(entry.route), preview].join('|'));
|
|
420
|
+
}
|
|
421
|
+
|
|
363
422
|
private classifyRegisterTrace(stack: string) {
|
|
364
423
|
if (
|
|
365
424
|
stack.includes('prepareSecretsRuntimeSnapshot') ||
|
|
@@ -502,9 +561,7 @@ class BncrBridgeRuntime {
|
|
|
502
561
|
const summary = this.buildRegisterTraceSummary();
|
|
503
562
|
if (summary.postWarmupRegisterCount > 0) this.captureDriftSnapshot(summary);
|
|
504
563
|
|
|
505
|
-
|
|
506
|
-
this.api.logger.info?.(`[bncr-register-trace] ${JSON.stringify(trace)}`);
|
|
507
|
-
}
|
|
564
|
+
this.logInfo('debug', `register-trace ${JSON.stringify(trace)}`, { debugOnly: true });
|
|
508
565
|
}
|
|
509
566
|
|
|
510
567
|
private createLeaseId() {
|
|
@@ -588,8 +645,9 @@ class BncrBridgeRuntime {
|
|
|
588
645
|
this.staleCounters.staleFileAbort += 1;
|
|
589
646
|
break;
|
|
590
647
|
}
|
|
591
|
-
this.
|
|
592
|
-
|
|
648
|
+
this.logWarn(
|
|
649
|
+
'stale',
|
|
650
|
+
`observed kind=${kind} lease=${leaseId || '-'} epoch=${connectionEpoch ?? '-'} currentLease=${this.primaryLeaseId || '-'} currentEpoch=${this.connectionEpoch}`,
|
|
593
651
|
);
|
|
594
652
|
return { stale: true, reason: 'mismatch' as const };
|
|
595
653
|
}
|
|
@@ -667,12 +725,13 @@ class BncrBridgeRuntime {
|
|
|
667
725
|
// ignore startup canonical agent initialization errors
|
|
668
726
|
}
|
|
669
727
|
if (typeof debug === 'boolean') BNCR_DEBUG_VERBOSE = debug;
|
|
728
|
+
await this.refreshDebugFlagFromConfig({ forceLog: true });
|
|
670
729
|
const bootDiag = this.buildIntegratedDiagnostics(BNCR_DEFAULT_ACCOUNT_ID);
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
730
|
+
this.logInfo(
|
|
731
|
+
'debug',
|
|
732
|
+
`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}`,
|
|
733
|
+
{ debugOnly: true },
|
|
734
|
+
);
|
|
676
735
|
};
|
|
677
736
|
|
|
678
737
|
stopService = async () => {
|
|
@@ -681,9 +740,7 @@ class BncrBridgeRuntime {
|
|
|
681
740
|
this.pushTimer = null;
|
|
682
741
|
}
|
|
683
742
|
await this.flushState();
|
|
684
|
-
|
|
685
|
-
this.api.logger.info('bncr-channel service stopped');
|
|
686
|
-
}
|
|
743
|
+
this.logInfo('debug', 'service stopped', { debugOnly: true });
|
|
687
744
|
};
|
|
688
745
|
|
|
689
746
|
private scheduleSave() {
|
|
@@ -703,20 +760,25 @@ class BncrBridgeRuntime {
|
|
|
703
760
|
return map.get(normalizeAccountId(accountId)) || 0;
|
|
704
761
|
}
|
|
705
762
|
|
|
706
|
-
private async
|
|
763
|
+
private async refreshDebugFlagFromConfig(options?: { forceLog?: boolean }) {
|
|
707
764
|
try {
|
|
708
765
|
const cfg = await this.api.runtime.config.loadConfig();
|
|
709
766
|
const raw = (cfg as any)?.channels?.[CHANNEL_ID]?.debug?.verbose;
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
767
|
+
const next = typeof raw === 'boolean' ? raw : false;
|
|
768
|
+
const changed = next !== BNCR_DEBUG_VERBOSE;
|
|
769
|
+
BNCR_DEBUG_VERBOSE = next;
|
|
770
|
+
if (changed || options?.forceLog) {
|
|
771
|
+
this.logInfo('debug', `verbose=${BNCR_DEBUG_VERBOSE}`);
|
|
714
772
|
}
|
|
715
773
|
} catch {
|
|
716
774
|
// ignore config read errors
|
|
717
775
|
}
|
|
718
776
|
}
|
|
719
777
|
|
|
778
|
+
private syncDebugFlag() {
|
|
779
|
+
void this.refreshDebugFlagFromConfig();
|
|
780
|
+
}
|
|
781
|
+
|
|
720
782
|
private tryResolveBindingAgentId(args: {
|
|
721
783
|
cfg: any;
|
|
722
784
|
accountId: string;
|
|
@@ -770,9 +832,7 @@ class BncrBridgeRuntime {
|
|
|
770
832
|
this.canonicalAgentId = 'main';
|
|
771
833
|
this.canonicalAgentSource = 'fallback-main';
|
|
772
834
|
this.canonicalAgentResolvedAt = now();
|
|
773
|
-
this.
|
|
774
|
-
'[bncr-canonical-agent] binding agent unresolved; fallback to main for current process lifetime',
|
|
775
|
-
);
|
|
835
|
+
this.logWarn('target', 'binding agent unresolved; fallback to main for current process lifetime');
|
|
776
836
|
return this.canonicalAgentId;
|
|
777
837
|
}
|
|
778
838
|
|
|
@@ -1139,29 +1199,29 @@ class BncrBridgeRuntime {
|
|
|
1139
1199
|
private tryPushEntry(entry: OutboxEntry): boolean {
|
|
1140
1200
|
const ctx = this.gatewayContext;
|
|
1141
1201
|
if (!ctx) {
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1202
|
+
this.logInfo(
|
|
1203
|
+
'outbox',
|
|
1204
|
+
`push-skip ${JSON.stringify({
|
|
1205
|
+
messageId: entry.messageId,
|
|
1206
|
+
accountId: entry.accountId,
|
|
1207
|
+
reason: 'no-gateway-context',
|
|
1208
|
+
})}`,
|
|
1209
|
+
{ debugOnly: true },
|
|
1210
|
+
);
|
|
1151
1211
|
return false;
|
|
1152
1212
|
}
|
|
1153
1213
|
|
|
1154
1214
|
const connIds = this.resolvePushConnIds(entry.accountId);
|
|
1155
1215
|
if (!connIds.size) {
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1216
|
+
this.logInfo(
|
|
1217
|
+
'outbox',
|
|
1218
|
+
`push-skip ${JSON.stringify({
|
|
1219
|
+
messageId: entry.messageId,
|
|
1220
|
+
accountId: entry.accountId,
|
|
1221
|
+
reason: 'no-active-connection',
|
|
1222
|
+
})}`,
|
|
1223
|
+
{ debugOnly: true },
|
|
1224
|
+
);
|
|
1165
1225
|
return false;
|
|
1166
1226
|
}
|
|
1167
1227
|
|
|
@@ -1172,16 +1232,16 @@ class BncrBridgeRuntime {
|
|
|
1172
1232
|
};
|
|
1173
1233
|
|
|
1174
1234
|
ctx.broadcastToConnIds(BNCR_PUSH_EVENT, payload, connIds);
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1235
|
+
this.logInfo(
|
|
1236
|
+
'outbox',
|
|
1237
|
+
`push-ok ${JSON.stringify({
|
|
1238
|
+
messageId: entry.messageId,
|
|
1239
|
+
accountId: entry.accountId,
|
|
1240
|
+
connIds: Array.from(connIds),
|
|
1241
|
+
event: BNCR_PUSH_EVENT,
|
|
1242
|
+
})}`,
|
|
1243
|
+
{ debugOnly: true },
|
|
1244
|
+
);
|
|
1185
1245
|
this.lastOutboundByAccount.set(entry.accountId, now());
|
|
1186
1246
|
this.markActivity(entry.accountId);
|
|
1187
1247
|
this.scheduleSave();
|
|
@@ -1189,15 +1249,15 @@ class BncrBridgeRuntime {
|
|
|
1189
1249
|
} catch (error) {
|
|
1190
1250
|
entry.lastError = asString((error as any)?.message || error || 'push-error');
|
|
1191
1251
|
this.outbox.set(entry.messageId, entry);
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1252
|
+
this.logInfo(
|
|
1253
|
+
'outbox',
|
|
1254
|
+
`push-fail ${JSON.stringify({
|
|
1255
|
+
messageId: entry.messageId,
|
|
1256
|
+
accountId: entry.accountId,
|
|
1257
|
+
error: entry.lastError,
|
|
1258
|
+
})}`,
|
|
1259
|
+
{ debugOnly: true },
|
|
1260
|
+
);
|
|
1201
1261
|
return false;
|
|
1202
1262
|
}
|
|
1203
1263
|
}
|
|
@@ -1220,37 +1280,37 @@ class BncrBridgeRuntime {
|
|
|
1220
1280
|
Array.from(this.outbox.values()).map((entry) => normalizeAccountId(entry.accountId)),
|
|
1221
1281
|
),
|
|
1222
1282
|
);
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1283
|
+
this.logInfo(
|
|
1284
|
+
'outbox',
|
|
1285
|
+
`flush ${JSON.stringify({
|
|
1286
|
+
bridge: this.bridgeId,
|
|
1287
|
+
accountId: filterAcc,
|
|
1288
|
+
targetAccounts,
|
|
1289
|
+
outboxSize: this.outbox.size,
|
|
1290
|
+
})}`,
|
|
1291
|
+
{ debugOnly: true },
|
|
1292
|
+
);
|
|
1233
1293
|
|
|
1234
1294
|
let globalNextDelay: number | null = null;
|
|
1235
1295
|
|
|
1236
1296
|
for (const acc of targetAccounts) {
|
|
1237
1297
|
if (!acc || this.pushDrainRunningAccounts.has(acc)) continue;
|
|
1238
1298
|
const online = this.isOnline(acc);
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1299
|
+
this.logInfo(
|
|
1300
|
+
'outbox',
|
|
1301
|
+
`online ${JSON.stringify({
|
|
1302
|
+
bridge: this.bridgeId,
|
|
1303
|
+
accountId: acc,
|
|
1304
|
+
online,
|
|
1305
|
+
connections: Array.from(this.connections.values()).map((c) => ({
|
|
1306
|
+
accountId: c.accountId,
|
|
1307
|
+
connId: c.connId,
|
|
1308
|
+
clientId: c.clientId,
|
|
1309
|
+
lastSeenAt: c.lastSeenAt,
|
|
1310
|
+
})),
|
|
1311
|
+
})}`,
|
|
1312
|
+
{ debugOnly: true },
|
|
1313
|
+
);
|
|
1254
1314
|
this.pushDrainRunningAccounts.add(acc);
|
|
1255
1315
|
try {
|
|
1256
1316
|
let localNextDelay: number | null = null;
|
|
@@ -1368,19 +1428,19 @@ class BncrBridgeRuntime {
|
|
|
1368
1428
|
const staleBefore = t - CONNECT_TTL_MS * 2;
|
|
1369
1429
|
for (const [key, c] of this.connections.entries()) {
|
|
1370
1430
|
if (c.lastSeenAt < staleBefore) {
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1431
|
+
this.logInfo(
|
|
1432
|
+
'connection',
|
|
1433
|
+
`gc ${JSON.stringify({
|
|
1434
|
+
bridge: this.bridgeId,
|
|
1435
|
+
key,
|
|
1436
|
+
accountId: c.accountId,
|
|
1437
|
+
connId: c.connId,
|
|
1438
|
+
clientId: c.clientId,
|
|
1439
|
+
lastSeenAt: c.lastSeenAt,
|
|
1440
|
+
staleBefore,
|
|
1441
|
+
})}`,
|
|
1442
|
+
{ debugOnly: true },
|
|
1443
|
+
);
|
|
1384
1444
|
this.connections.delete(key);
|
|
1385
1445
|
}
|
|
1386
1446
|
}
|
|
@@ -1421,18 +1481,18 @@ class BncrBridgeRuntime {
|
|
|
1421
1481
|
};
|
|
1422
1482
|
|
|
1423
1483
|
this.connections.set(key, nextConn);
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1484
|
+
this.logInfo(
|
|
1485
|
+
'connection',
|
|
1486
|
+
`seen ${JSON.stringify({
|
|
1487
|
+
bridge: this.bridgeId,
|
|
1488
|
+
accountId: acc,
|
|
1489
|
+
connId,
|
|
1490
|
+
clientId: nextConn.clientId,
|
|
1491
|
+
connectedAt: nextConn.connectedAt,
|
|
1492
|
+
lastSeenAt: nextConn.lastSeenAt,
|
|
1493
|
+
})}`,
|
|
1494
|
+
{ debugOnly: true },
|
|
1495
|
+
);
|
|
1436
1496
|
|
|
1437
1497
|
const current = this.activeConnectionByAccount.get(acc);
|
|
1438
1498
|
if (!current) {
|
|
@@ -1534,9 +1594,7 @@ class BncrBridgeRuntime {
|
|
|
1534
1594
|
const raw = asString(rawTarget).trim();
|
|
1535
1595
|
if (!raw) throw new Error('bncr invalid target(empty)');
|
|
1536
1596
|
|
|
1537
|
-
|
|
1538
|
-
this.api.logger.info?.(`[bncr-target-incoming] raw=${raw} accountId=${acc}`);
|
|
1539
|
-
}
|
|
1597
|
+
this.logInfo('target', `incoming raw=${raw} accountId=${acc}`, { debugOnly: true });
|
|
1540
1598
|
|
|
1541
1599
|
let route: BncrRoute | null = null;
|
|
1542
1600
|
|
|
@@ -1548,85 +1606,44 @@ class BncrBridgeRuntime {
|
|
|
1548
1606
|
}
|
|
1549
1607
|
|
|
1550
1608
|
if (!route) {
|
|
1551
|
-
this.
|
|
1552
|
-
|
|
1609
|
+
this.logWarn(
|
|
1610
|
+
'target',
|
|
1611
|
+
`invalid raw=${raw} accountId=${acc} reason=unparseable-or-unknown standardTo=Bncr:<platform>:<groupId>:<userId>|Bncr:<platform>:<userId> standardSessionKey=agent:<agentId>:bncr:direct:<hex(scope)>`,
|
|
1553
1612
|
);
|
|
1554
1613
|
throw new Error(
|
|
1555
1614
|
`bncr invalid target(standard: Bncr:<platform>:<groupId>:<userId> | Bncr:<platform>:<userId>): ${raw}`,
|
|
1556
1615
|
);
|
|
1557
1616
|
}
|
|
1558
1617
|
|
|
1559
|
-
const
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
)
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
if (normalizeAccountId(info.accountId) !== acc) continue;
|
|
1573
|
-
const parsed = parseStrictBncrSessionKey(key);
|
|
1574
|
-
if (!parsed) continue;
|
|
1575
|
-
if (routeKey(acc, parsed.route) !== wantedRouteKey) continue;
|
|
1576
|
-
|
|
1577
|
-
const updatedAt = Number(info.updatedAt || 0);
|
|
1578
|
-
if (!best || updatedAt >= best.updatedAt) {
|
|
1579
|
-
best = {
|
|
1580
|
-
sessionKey: key,
|
|
1581
|
-
route: parsed.route,
|
|
1582
|
-
updatedAt,
|
|
1583
|
-
};
|
|
1584
|
-
}
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
if (!best) {
|
|
1588
|
-
const updatedAt = 0;
|
|
1589
|
-
const canonicalAgentId =
|
|
1590
|
-
this.canonicalAgentId ||
|
|
1591
|
-
this.ensureCanonicalAgentId({
|
|
1592
|
-
cfg: this.api.runtime.config?.get?.() || {},
|
|
1593
|
-
accountId: acc,
|
|
1594
|
-
channelId: CHANNEL_ID,
|
|
1595
|
-
peer: { kind: 'direct', id: route.groupId === '0' ? route.userId : route.groupId },
|
|
1596
|
-
});
|
|
1597
|
-
best = {
|
|
1598
|
-
sessionKey: buildCanonicalBncrSessionKey(route, canonicalAgentId),
|
|
1599
|
-
route,
|
|
1600
|
-
updatedAt,
|
|
1601
|
-
};
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1604
|
-
if (BNCR_DEBUG_VERBOSE) {
|
|
1605
|
-
this.api.logger.info?.(
|
|
1606
|
-
`[bncr-target-incoming-best] raw=${raw} accountId=${acc} best=${JSON.stringify(best)}`,
|
|
1607
|
-
);
|
|
1608
|
-
}
|
|
1618
|
+
const canonicalAgentId =
|
|
1619
|
+
this.canonicalAgentId ||
|
|
1620
|
+
this.ensureCanonicalAgentId({
|
|
1621
|
+
cfg: this.api.runtime.config?.get?.() || {},
|
|
1622
|
+
accountId: acc,
|
|
1623
|
+
channelId: CHANNEL_ID,
|
|
1624
|
+
peer: { kind: 'direct', id: route.groupId === '0' ? route.userId : route.groupId },
|
|
1625
|
+
});
|
|
1626
|
+
const verified = {
|
|
1627
|
+
sessionKey: buildCanonicalBncrSessionKey(route, canonicalAgentId),
|
|
1628
|
+
route,
|
|
1629
|
+
displayScope: formatDisplayScope(route),
|
|
1630
|
+
};
|
|
1609
1631
|
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
}
|
|
1632
|
+
this.logInfo(
|
|
1633
|
+
'target',
|
|
1634
|
+
`canonical raw=${raw} accountId=${acc} verified=${JSON.stringify(verified)}`,
|
|
1635
|
+
{ debugOnly: true },
|
|
1636
|
+
);
|
|
1616
1637
|
|
|
1617
1638
|
// 发送链路命中目标时,同步刷新 lastSession,避免状态页显示过期会话。
|
|
1618
1639
|
this.lastSessionByAccount.set(acc, {
|
|
1619
|
-
sessionKey:
|
|
1620
|
-
scope:
|
|
1640
|
+
sessionKey: verified.sessionKey,
|
|
1641
|
+
scope: verified.displayScope,
|
|
1621
1642
|
updatedAt: now(),
|
|
1622
1643
|
});
|
|
1623
1644
|
this.scheduleSave();
|
|
1624
1645
|
|
|
1625
|
-
return
|
|
1626
|
-
sessionKey: best.sessionKey,
|
|
1627
|
-
route: best.route,
|
|
1628
|
-
displayScope: formatDisplayScope(best.route),
|
|
1629
|
-
};
|
|
1646
|
+
return verified;
|
|
1630
1647
|
}
|
|
1631
1648
|
|
|
1632
1649
|
private markActivity(accountId: string, at = now()) {
|
|
@@ -1840,22 +1857,25 @@ class BncrBridgeRuntime {
|
|
|
1840
1857
|
}
|
|
1841
1858
|
|
|
1842
1859
|
private enqueueOutbound(entry: OutboxEntry) {
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1860
|
+
const msg = (entry.payload as any)?.message || {};
|
|
1861
|
+
const type = asString(msg.type || (entry.payload as any)?.type || 'unknown');
|
|
1862
|
+
const text = asString(msg.msg || '');
|
|
1863
|
+
const displayScope = formatDisplayScope(entry.route);
|
|
1864
|
+
this.logInfo(
|
|
1865
|
+
'outbound',
|
|
1866
|
+
JSON.stringify({
|
|
1867
|
+
bridge: this.bridgeId,
|
|
1868
|
+
messageId: entry.messageId,
|
|
1869
|
+
accountId: entry.accountId,
|
|
1870
|
+
sessionKey: entry.sessionKey,
|
|
1871
|
+
scope: displayScope,
|
|
1872
|
+
type,
|
|
1873
|
+
textLen: text.length,
|
|
1874
|
+
textPreview: text.slice(0, 120),
|
|
1875
|
+
}),
|
|
1876
|
+
{ debugOnly: true },
|
|
1877
|
+
);
|
|
1878
|
+
this.logOutboundSummary(entry);
|
|
1859
1879
|
this.outbox.set(entry.messageId, entry);
|
|
1860
1880
|
this.scheduleSave();
|
|
1861
1881
|
this.wakeAccountWaiters(entry.accountId);
|
|
@@ -2168,6 +2188,7 @@ class BncrBridgeRuntime {
|
|
|
2168
2188
|
asVoice?: boolean;
|
|
2169
2189
|
audioAsVoice?: boolean;
|
|
2170
2190
|
kind?: 'tool' | 'block' | 'final';
|
|
2191
|
+
replyToId?: string;
|
|
2171
2192
|
};
|
|
2172
2193
|
mediaLocalRoots?: readonly string[];
|
|
2173
2194
|
}) {
|
|
@@ -2206,6 +2227,7 @@ class BncrBridgeRuntime {
|
|
|
2206
2227
|
}),
|
|
2207
2228
|
hintedType: wantsVoice ? 'voice' : undefined,
|
|
2208
2229
|
kind: payload.kind,
|
|
2230
|
+
replyToId: asString(payload.replyToId || '').trim() || undefined,
|
|
2209
2231
|
now: now(),
|
|
2210
2232
|
});
|
|
2211
2233
|
|
|
@@ -2233,6 +2255,7 @@ class BncrBridgeRuntime {
|
|
|
2233
2255
|
messageId,
|
|
2234
2256
|
idempotencyKey: messageId,
|
|
2235
2257
|
sessionKey,
|
|
2258
|
+
replyToId: asString(payload.replyToId || '').trim() || undefined,
|
|
2236
2259
|
message: {
|
|
2237
2260
|
platform: route.platform,
|
|
2238
2261
|
groupId: route.groupId,
|
|
@@ -2260,21 +2283,22 @@ class BncrBridgeRuntime {
|
|
|
2260
2283
|
}
|
|
2261
2284
|
|
|
2262
2285
|
handleConnect = async ({ params, respond, client, context }: GatewayRequestHandlerOptions) => {
|
|
2286
|
+
this.syncDebugFlag();
|
|
2263
2287
|
const accountId = normalizeAccountId(asString(params?.accountId || ''));
|
|
2264
2288
|
const connId = asString(client?.connId || '').trim() || `no-conn-${Date.now()}`;
|
|
2265
2289
|
const clientId = asString((params as any)?.clientId || '').trim() || undefined;
|
|
2266
2290
|
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2291
|
+
this.logInfo(
|
|
2292
|
+
'connection',
|
|
2293
|
+
`connect ${JSON.stringify({
|
|
2294
|
+
bridge: this.bridgeId,
|
|
2295
|
+
accountId,
|
|
2296
|
+
connId,
|
|
2297
|
+
clientId,
|
|
2298
|
+
hasContext: Boolean(context),
|
|
2299
|
+
})}`,
|
|
2300
|
+
{ debugOnly: true },
|
|
2301
|
+
);
|
|
2278
2302
|
|
|
2279
2303
|
this.rememberGatewayContext(context);
|
|
2280
2304
|
this.markSeen(accountId, connId, clientId);
|
|
@@ -2307,6 +2331,7 @@ class BncrBridgeRuntime {
|
|
|
2307
2331
|
};
|
|
2308
2332
|
|
|
2309
2333
|
handleAck = async ({ params, respond, client, context }: GatewayRequestHandlerOptions) => {
|
|
2334
|
+
this.syncDebugFlag();
|
|
2310
2335
|
const accountId = normalizeAccountId(asString(params?.accountId || ''));
|
|
2311
2336
|
const connId = asString(client?.connId || '').trim() || `no-conn-${Date.now()}`;
|
|
2312
2337
|
const clientId = asString((params as any)?.clientId || '').trim() || undefined;
|
|
@@ -2317,17 +2342,17 @@ class BncrBridgeRuntime {
|
|
|
2317
2342
|
this.incrementCounter(this.ackEventsByAccount, accountId);
|
|
2318
2343
|
|
|
2319
2344
|
const messageId = asString(params?.messageId || '').trim();
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2345
|
+
this.logInfo(
|
|
2346
|
+
'outbox',
|
|
2347
|
+
`ack ${JSON.stringify({
|
|
2348
|
+
accountId,
|
|
2349
|
+
messageId,
|
|
2350
|
+
ok: params?.ok !== false,
|
|
2351
|
+
fatal: params?.fatal === true,
|
|
2352
|
+
error: asString(params?.error || ''),
|
|
2353
|
+
})}`,
|
|
2354
|
+
{ debugOnly: true },
|
|
2355
|
+
);
|
|
2331
2356
|
if (!messageId) {
|
|
2332
2357
|
respond(false, { error: 'messageId required' });
|
|
2333
2358
|
return;
|
|
@@ -2370,23 +2395,23 @@ class BncrBridgeRuntime {
|
|
|
2370
2395
|
};
|
|
2371
2396
|
|
|
2372
2397
|
handleActivity = async ({ params, respond, client, context }: GatewayRequestHandlerOptions) => {
|
|
2373
|
-
|
|
2398
|
+
this.syncDebugFlag();
|
|
2374
2399
|
const accountId = normalizeAccountId(asString(params?.accountId || ''));
|
|
2375
2400
|
const connId = asString(client?.connId || '').trim() || `no-conn-${Date.now()}`;
|
|
2376
2401
|
const clientId = asString((params as any)?.clientId || '').trim() || undefined;
|
|
2377
2402
|
this.observeLease('activity', params ?? {});
|
|
2378
2403
|
this.lastActivityAtGlobal = now();
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2404
|
+
this.logInfo(
|
|
2405
|
+
'activity',
|
|
2406
|
+
`event ${JSON.stringify({
|
|
2407
|
+
bridge: this.bridgeId,
|
|
2408
|
+
accountId,
|
|
2409
|
+
connId,
|
|
2410
|
+
clientId,
|
|
2411
|
+
hasContext: Boolean(context),
|
|
2412
|
+
})}`,
|
|
2413
|
+
{ debugOnly: true },
|
|
2414
|
+
);
|
|
2390
2415
|
this.rememberGatewayContext(context);
|
|
2391
2416
|
this.markSeen(accountId, connId, clientId);
|
|
2392
2417
|
this.markActivity(accountId);
|
|
@@ -2731,6 +2756,7 @@ class BncrBridgeRuntime {
|
|
|
2731
2756
|
};
|
|
2732
2757
|
|
|
2733
2758
|
handleInbound = async ({ params, respond, client, context }: GatewayRequestHandlerOptions) => {
|
|
2759
|
+
this.syncDebugFlag();
|
|
2734
2760
|
const parsed = parseBncrInboundParams(params);
|
|
2735
2761
|
const {
|
|
2736
2762
|
accountId,
|
|
@@ -2806,6 +2832,30 @@ class BncrBridgeRuntime {
|
|
|
2806
2832
|
resolvedRoute.sessionKey;
|
|
2807
2833
|
const taskSessionKey = withTaskSessionKey(baseSessionKey, extracted.taskKey);
|
|
2808
2834
|
const sessionKey = taskSessionKey || baseSessionKey;
|
|
2835
|
+
const inboundText = asString(extracted.text || text || '');
|
|
2836
|
+
this.logInfo(
|
|
2837
|
+
'inbound',
|
|
2838
|
+
JSON.stringify({
|
|
2839
|
+
accountId,
|
|
2840
|
+
msgId: msgId ?? null,
|
|
2841
|
+
platform,
|
|
2842
|
+
chatType: peer.kind,
|
|
2843
|
+
scope: formatDisplayScope(route),
|
|
2844
|
+
sessionKey,
|
|
2845
|
+
msgType,
|
|
2846
|
+
textLen: inboundText.length,
|
|
2847
|
+
textPreview: inboundText.slice(0, 120),
|
|
2848
|
+
hasMedia: Boolean(mediaBase64 || mediaPathFromTransfer),
|
|
2849
|
+
}),
|
|
2850
|
+
{ debugOnly: true },
|
|
2851
|
+
);
|
|
2852
|
+
this.logInboundSummary({
|
|
2853
|
+
accountId,
|
|
2854
|
+
route,
|
|
2855
|
+
msgType,
|
|
2856
|
+
text: inboundText,
|
|
2857
|
+
hasMedia: Boolean(mediaBase64 || mediaPathFromTransfer),
|
|
2858
|
+
});
|
|
2809
2859
|
|
|
2810
2860
|
respond(true, {
|
|
2811
2861
|
accepted: true,
|
|
@@ -2829,9 +2879,12 @@ class BncrBridgeRuntime {
|
|
|
2829
2879
|
this.markActivity(accountId, at);
|
|
2830
2880
|
},
|
|
2831
2881
|
scheduleSave: () => this.scheduleSave(),
|
|
2832
|
-
logger:
|
|
2882
|
+
logger: {
|
|
2883
|
+
warn: (msg: string) => emitBncrLogLine('warn', msg),
|
|
2884
|
+
error: (msg: string) => emitBncrLogLine('error', msg),
|
|
2885
|
+
},
|
|
2833
2886
|
}).catch((err) => {
|
|
2834
|
-
this.
|
|
2887
|
+
this.logError('inbound', `process failed: ${String(err)}`);
|
|
2835
2888
|
});
|
|
2836
2889
|
};
|
|
2837
2890
|
|
|
@@ -2882,30 +2935,31 @@ class BncrBridgeRuntime {
|
|
|
2882
2935
|
const accountId = normalizeAccountId(ctx.accountId);
|
|
2883
2936
|
const to = asString(ctx.to || '').trim();
|
|
2884
2937
|
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2938
|
+
this.logInfo(
|
|
2939
|
+
'outbound',
|
|
2940
|
+
`send-entry:text ${JSON.stringify({
|
|
2941
|
+
accountId,
|
|
2942
|
+
to,
|
|
2943
|
+
text: asString(ctx?.text || ''),
|
|
2944
|
+
mediaUrl: asString(ctx?.mediaUrl || ''),
|
|
2945
|
+
sessionKey: asString(ctx?.sessionKey || ''),
|
|
2946
|
+
mirrorSessionKey: asString(ctx?.mirror?.sessionKey || ''),
|
|
2947
|
+
rawCtx: {
|
|
2948
|
+
to: ctx?.to,
|
|
2949
|
+
accountId: ctx?.accountId,
|
|
2950
|
+
threadId: ctx?.threadId,
|
|
2951
|
+
replyToId: ctx?.replyToId,
|
|
2952
|
+
},
|
|
2953
|
+
})}`,
|
|
2954
|
+
{ debugOnly: true },
|
|
2955
|
+
);
|
|
2903
2956
|
|
|
2904
2957
|
return sendBncrText({
|
|
2905
2958
|
channelId: CHANNEL_ID,
|
|
2906
2959
|
accountId,
|
|
2907
2960
|
to,
|
|
2908
2961
|
text: asString(ctx.text || ''),
|
|
2962
|
+
replyToId: asString(ctx?.replyToId || ctx?.replyToMessageId || '').trim() || undefined,
|
|
2909
2963
|
mediaLocalRoots: ctx.mediaLocalRoots,
|
|
2910
2964
|
resolveVerifiedTarget: (to, accountId) => this.resolveVerifiedTarget(to, accountId),
|
|
2911
2965
|
rememberSessionRoute: (sessionKey, accountId, route) =>
|
|
@@ -2921,27 +2975,27 @@ class BncrBridgeRuntime {
|
|
|
2921
2975
|
const asVoice = ctx?.asVoice === true;
|
|
2922
2976
|
const audioAsVoice = ctx?.audioAsVoice === true;
|
|
2923
2977
|
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2978
|
+
this.logInfo(
|
|
2979
|
+
'outbound',
|
|
2980
|
+
`send-entry:media ${JSON.stringify({
|
|
2981
|
+
accountId,
|
|
2982
|
+
to,
|
|
2983
|
+
text: asString(ctx?.text || ''),
|
|
2984
|
+
mediaUrl: asString(ctx?.mediaUrl || ''),
|
|
2985
|
+
mediaUrls: Array.isArray(ctx?.mediaUrls) ? ctx.mediaUrls : undefined,
|
|
2986
|
+
asVoice,
|
|
2987
|
+
audioAsVoice,
|
|
2988
|
+
sessionKey: asString(ctx?.sessionKey || ''),
|
|
2989
|
+
mirrorSessionKey: asString(ctx?.mirror?.sessionKey || ''),
|
|
2990
|
+
rawCtx: {
|
|
2991
|
+
to: ctx?.to,
|
|
2992
|
+
accountId: ctx?.accountId,
|
|
2993
|
+
threadId: ctx?.threadId,
|
|
2994
|
+
replyToId: ctx?.replyToId,
|
|
2995
|
+
},
|
|
2996
|
+
})}`,
|
|
2997
|
+
{ debugOnly: true },
|
|
2998
|
+
);
|
|
2945
2999
|
|
|
2946
3000
|
return sendBncrMedia({
|
|
2947
3001
|
channelId: CHANNEL_ID,
|
|
@@ -2951,6 +3005,7 @@ class BncrBridgeRuntime {
|
|
|
2951
3005
|
mediaUrl: asString(ctx.mediaUrl || ''),
|
|
2952
3006
|
asVoice,
|
|
2953
3007
|
audioAsVoice,
|
|
3008
|
+
replyToId: asString(ctx?.replyToId || ctx?.replyToMessageId || '').trim() || undefined,
|
|
2954
3009
|
mediaLocalRoots: ctx.mediaLocalRoots,
|
|
2955
3010
|
resolveVerifiedTarget: (to, accountId) => this.resolveVerifiedTarget(to, accountId),
|
|
2956
3011
|
rememberSessionRoute: (sessionKey, accountId, route) =>
|
|
@@ -3070,9 +3125,68 @@ export function createBncrChannelPlugin(bridge: BncrBridgeRuntime) {
|
|
|
3070
3125
|
const input = asString(raw).trim();
|
|
3071
3126
|
return input || undefined;
|
|
3072
3127
|
},
|
|
3128
|
+
parseExplicitTarget: ({ raw, accountId, cfg }: any) => {
|
|
3129
|
+
const resolvedAccountId = normalizeAccountId(
|
|
3130
|
+
asString(accountId || BNCR_DEFAULT_ACCOUNT_ID),
|
|
3131
|
+
);
|
|
3132
|
+
const canonicalAgentId =
|
|
3133
|
+
bridge.canonicalAgentId ||
|
|
3134
|
+
bridge.ensureCanonicalAgentId({ cfg, accountId: resolvedAccountId });
|
|
3135
|
+
return parseExplicitTarget(asString(raw).trim(), { canonicalAgentId });
|
|
3136
|
+
},
|
|
3137
|
+
formatTargetDisplay: ({ target }: any) => {
|
|
3138
|
+
return formatTargetDisplay(target);
|
|
3139
|
+
},
|
|
3140
|
+
resolveSessionTarget: ({ id, accountId, cfg }: any) => {
|
|
3141
|
+
const raw = asString(id).trim();
|
|
3142
|
+
if (!raw) return undefined;
|
|
3143
|
+
const resolvedAccountId = normalizeAccountId(
|
|
3144
|
+
asString(accountId || BNCR_DEFAULT_ACCOUNT_ID),
|
|
3145
|
+
);
|
|
3146
|
+
const canonicalAgentId =
|
|
3147
|
+
bridge.canonicalAgentId ||
|
|
3148
|
+
bridge.ensureCanonicalAgentId({ cfg, accountId: resolvedAccountId });
|
|
3149
|
+
|
|
3150
|
+
let parsed = parseExplicitTarget(raw, { canonicalAgentId });
|
|
3151
|
+
if (!parsed) {
|
|
3152
|
+
const route = bridge.resolveRouteBySession(raw, resolvedAccountId);
|
|
3153
|
+
if (route) {
|
|
3154
|
+
parsed = parseExplicitTarget(formatDisplayScope(route), { canonicalAgentId });
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
return parsed?.displayScope || undefined;
|
|
3158
|
+
},
|
|
3159
|
+
resolveOutboundSessionRoute: (params: any) => {
|
|
3160
|
+
const accountId = normalizeAccountId(
|
|
3161
|
+
asString(params?.accountId || BNCR_DEFAULT_ACCOUNT_ID),
|
|
3162
|
+
);
|
|
3163
|
+
const canonicalAgentId =
|
|
3164
|
+
bridge.canonicalAgentId || bridge.ensureCanonicalAgentId({ cfg: params?.cfg, accountId });
|
|
3165
|
+
return resolveBncrOutboundSessionRoute({
|
|
3166
|
+
...params,
|
|
3167
|
+
canonicalAgentId,
|
|
3168
|
+
resolveRouteBySession: (raw: string, acc: string) =>
|
|
3169
|
+
bridge.resolveRouteBySession(raw, acc),
|
|
3170
|
+
});
|
|
3171
|
+
},
|
|
3073
3172
|
targetResolver: {
|
|
3074
3173
|
looksLikeId: (raw: string, normalized?: string) => {
|
|
3075
|
-
return
|
|
3174
|
+
return looksLikeBncrExplicitTarget(asString(normalized || raw).trim());
|
|
3175
|
+
},
|
|
3176
|
+
resolveTarget: async ({ accountId, input, normalized }) => {
|
|
3177
|
+
const resolved = resolveBncrOutboundTarget({
|
|
3178
|
+
target: asString(normalized || input).trim(),
|
|
3179
|
+
accountId: normalizeAccountId(asString(accountId || BNCR_DEFAULT_ACCOUNT_ID)),
|
|
3180
|
+
resolveRouteBySession: (raw: string, acc: string) =>
|
|
3181
|
+
this.resolveRouteBySession(raw, acc),
|
|
3182
|
+
});
|
|
3183
|
+
if (!resolved) return null;
|
|
3184
|
+
return {
|
|
3185
|
+
to: resolved.displayScope,
|
|
3186
|
+
kind: resolved.kind,
|
|
3187
|
+
display: resolved.displayScope,
|
|
3188
|
+
source: 'normalized' as const,
|
|
3189
|
+
};
|
|
3076
3190
|
},
|
|
3077
3191
|
hint: 'Standard to=Bncr:<platform>:<group>:<user> or Bncr:<platform>:<user>; sessionKey keeps existing strict/legacy compatibility, canonical sessionKey=agent:<agentId>:bncr:direct:<hex>',
|
|
3078
3192
|
},
|