@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/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
- if (BNCR_DEBUG_VERBOSE) {
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.api.logger.warn?.(
592
- `[bncr] stale ${kind} observed lease=${leaseId || '-'} epoch=${connectionEpoch ?? '-'} currentLease=${this.primaryLeaseId || '-'} currentEpoch=${this.connectionEpoch}`,
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
- if (BNCR_DEBUG_VERBOSE) {
672
- this.api.logger.info(
673
- `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})`,
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
- if (BNCR_DEBUG_VERBOSE) {
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 syncDebugFlag() {
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
- if (typeof raw !== 'boolean') return;
711
- if (raw !== BNCR_DEBUG_VERBOSE) {
712
- BNCR_DEBUG_VERBOSE = raw;
713
- this.api.logger.info?.(`[bncr-debug] verbose=${BNCR_DEBUG_VERBOSE}`);
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.api.logger.warn?.(
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
- if (BNCR_DEBUG_VERBOSE) {
1143
- this.api.logger.info?.(
1144
- `[bncr-outbox-push-skip] ${JSON.stringify({
1145
- messageId: entry.messageId,
1146
- accountId: entry.accountId,
1147
- reason: 'no-gateway-context',
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
- if (BNCR_DEBUG_VERBOSE) {
1157
- this.api.logger.info?.(
1158
- `[bncr-outbox-push-skip] ${JSON.stringify({
1159
- messageId: entry.messageId,
1160
- accountId: entry.accountId,
1161
- reason: 'no-active-connection',
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
- if (BNCR_DEBUG_VERBOSE) {
1176
- this.api.logger.info?.(
1177
- `[bncr-outbox-push-ok] ${JSON.stringify({
1178
- messageId: entry.messageId,
1179
- accountId: entry.accountId,
1180
- connIds: Array.from(connIds),
1181
- event: BNCR_PUSH_EVENT,
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
- if (BNCR_DEBUG_VERBOSE) {
1193
- this.api.logger.info?.(
1194
- `[bncr-outbox-push-fail] ${JSON.stringify({
1195
- messageId: entry.messageId,
1196
- accountId: entry.accountId,
1197
- error: entry.lastError,
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
- if (BNCR_DEBUG_VERBOSE) {
1224
- this.api.logger.info?.(
1225
- `[bncr-outbox-flush] ${JSON.stringify({
1226
- bridge: this.bridgeId,
1227
- accountId: filterAcc,
1228
- targetAccounts,
1229
- outboxSize: this.outbox.size,
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
- if (BNCR_DEBUG_VERBOSE) {
1240
- this.api.logger.info?.(
1241
- `[bncr-outbox-online] ${JSON.stringify({
1242
- bridge: this.bridgeId,
1243
- accountId: acc,
1244
- online,
1245
- connections: Array.from(this.connections.values()).map((c) => ({
1246
- accountId: c.accountId,
1247
- connId: c.connId,
1248
- clientId: c.clientId,
1249
- lastSeenAt: c.lastSeenAt,
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
- if (BNCR_DEBUG_VERBOSE) {
1372
- this.api.logger.info?.(
1373
- `[bncr-conn-gc] ${JSON.stringify({
1374
- bridge: this.bridgeId,
1375
- key,
1376
- accountId: c.accountId,
1377
- connId: c.connId,
1378
- clientId: c.clientId,
1379
- lastSeenAt: c.lastSeenAt,
1380
- staleBefore,
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
- if (BNCR_DEBUG_VERBOSE) {
1425
- this.api.logger.info?.(
1426
- `[bncr-conn-seen] ${JSON.stringify({
1427
- bridge: this.bridgeId,
1428
- accountId: acc,
1429
- connId,
1430
- clientId: nextConn.clientId,
1431
- connectedAt: nextConn.connectedAt,
1432
- lastSeenAt: nextConn.lastSeenAt,
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
- if (BNCR_DEBUG_VERBOSE) {
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.api.logger.warn?.(
1552
- `[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)>`,
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 wantedRouteKey = routeKey(acc, route);
1560
- let best: { sessionKey: string; route: BncrRoute; updatedAt: number } | null = null;
1561
-
1562
- if (BNCR_DEBUG_VERBOSE) {
1563
- this.api.logger.info?.(
1564
- `[bncr-target-incoming-route] raw=${raw} accountId=${acc} route=${JSON.stringify(route)}`,
1565
- );
1566
- this.api.logger.info?.(
1567
- `[bncr-target-incoming-sessionRoutes] raw=${raw} accountId=${acc} sessionRoutes=${JSON.stringify(this.sessionRoutes.entries())}`,
1568
- );
1569
- }
1570
-
1571
- for (const [key, info] of this.sessionRoutes.entries()) {
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
- if (!best) {
1611
- this.api.logger.warn?.(
1612
- `[bncr-target-miss] raw=${raw} accountId=${acc} sessionRoutes=${this.sessionRoutes.size}`,
1613
- );
1614
- throw new Error(`bncr target not found in known sessions: ${raw}`);
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: best.sessionKey,
1620
- scope: formatDisplayScope(best.route),
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
- if (BNCR_DEBUG_VERBOSE) {
1844
- const msg = (entry.payload as any)?.message || {};
1845
- const type = asString(msg.type || (entry.payload as any)?.type || 'unknown');
1846
- const text = asString(msg.msg || '');
1847
- this.api.logger.info?.(
1848
- `[bncr-outbox-enqueue] ${JSON.stringify({
1849
- bridge: this.bridgeId,
1850
- messageId: entry.messageId,
1851
- accountId: entry.accountId,
1852
- sessionKey: entry.sessionKey,
1853
- route: entry.route,
1854
- type,
1855
- textLen: text.length,
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
- if (BNCR_DEBUG_VERBOSE) {
2268
- this.api.logger.info?.(
2269
- `[bncr-connect] ${JSON.stringify({
2270
- bridge: this.bridgeId,
2271
- accountId,
2272
- connId,
2273
- clientId,
2274
- hasContext: Boolean(context),
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
- if (BNCR_DEBUG_VERBOSE) {
2321
- this.api.logger.info?.(
2322
- `[bncr-outbox-ack] ${JSON.stringify({
2323
- accountId,
2324
- messageId,
2325
- ok: params?.ok !== false,
2326
- fatal: params?.fatal === true,
2327
- error: asString(params?.error || ''),
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
- void this.syncDebugFlag();
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
- if (BNCR_DEBUG_VERBOSE) {
2380
- this.api.logger.info?.(
2381
- `[bncr-activity] ${JSON.stringify({
2382
- bridge: this.bridgeId,
2383
- accountId,
2384
- connId,
2385
- clientId,
2386
- hasContext: Boolean(context),
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: this.api.logger,
2882
+ logger: {
2883
+ warn: (msg: string) => emitBncrLogLine('warn', msg),
2884
+ error: (msg: string) => emitBncrLogLine('error', msg),
2885
+ },
2833
2886
  }).catch((err) => {
2834
- this.api.logger.error?.(`bncr inbound process failed: ${String(err)}`);
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
- if (BNCR_DEBUG_VERBOSE) {
2886
- this.api.logger.info?.(
2887
- `[bncr-send-entry:text] ${JSON.stringify({
2888
- accountId,
2889
- to,
2890
- text: asString(ctx?.text || ''),
2891
- mediaUrl: asString(ctx?.mediaUrl || ''),
2892
- sessionKey: asString(ctx?.sessionKey || ''),
2893
- mirrorSessionKey: asString(ctx?.mirror?.sessionKey || ''),
2894
- rawCtx: {
2895
- to: ctx?.to,
2896
- accountId: ctx?.accountId,
2897
- threadId: ctx?.threadId,
2898
- replyToId: ctx?.replyToId,
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
- if (BNCR_DEBUG_VERBOSE) {
2925
- this.api.logger.info?.(
2926
- `[bncr-send-entry:media] ${JSON.stringify({
2927
- accountId,
2928
- to,
2929
- text: asString(ctx?.text || ''),
2930
- mediaUrl: asString(ctx?.mediaUrl || ''),
2931
- mediaUrls: Array.isArray(ctx?.mediaUrls) ? ctx.mediaUrls : undefined,
2932
- asVoice,
2933
- audioAsVoice,
2934
- sessionKey: asString(ctx?.sessionKey || ''),
2935
- mirrorSessionKey: asString(ctx?.mirror?.sessionKey || ''),
2936
- rawCtx: {
2937
- to: ctx?.to,
2938
- accountId: ctx?.accountId,
2939
- threadId: ctx?.threadId,
2940
- replyToId: ctx?.replyToId,
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 Boolean(asString(normalized || raw).trim());
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
  },