@xmoxmo/bncr 0.1.7 → 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 CHANGED
@@ -4,6 +4,7 @@ import { createRequire } from 'node:module';
4
4
  import path from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { BncrConfigSchema } from './src/core/config-schema.ts';
7
+ import { emitBncrLogLine } from './src/core/logging.ts';
7
8
 
8
9
  const pluginFile = fileURLToPath(import.meta.url);
9
10
  const pluginDir = path.dirname(pluginFile);
@@ -254,12 +255,12 @@ const ensureGatewayMethodRegistered = (
254
255
  ) => {
255
256
  const meta = getRegisterMeta(api);
256
257
  if (meta.methods?.has(name)) {
257
- debugLog(`bncr register method skip ${name} (already registered on this api)`);
258
+ debugLog(`register method skip ${name} (already registered on this api)`);
258
259
  return;
259
260
  }
260
261
  api.registerGatewayMethod(name, handler);
261
262
  meta.methods?.add(name);
262
- debugLog(`bncr register method ok ${name}`);
263
+ debugLog(`register method ok ${name}`);
263
264
  };
264
265
 
265
266
  const getBridgeSingleton = (api: OpenClawPluginApi) => {
@@ -291,14 +292,20 @@ const plugin = {
291
292
  registryFingerprint: meta.registryFingerprint,
292
293
  });
293
294
  const debugLog = (...args: any[]) => {
294
- if (!bridge.isDebugEnabled?.()) return;
295
- api.logger.info?.(...args);
295
+ const rendered = args
296
+ .map((arg) => (typeof arg === 'string' ? arg : JSON.stringify(arg)))
297
+ .join(' ')
298
+ .trim();
299
+ if (!rendered) return;
300
+ emitBncrLogLine('info', `[bncr] debug ${rendered}`, { debugOnly: true }, () =>
301
+ Boolean(bridge.isDebugEnabled?.()),
302
+ );
296
303
  };
297
304
 
298
305
  debugLog(
299
- `bncr plugin register begin bridge=${bridge.getBridgeId?.() || 'unknown'} created=${created}`,
306
+ `register begin bridge=${bridge.getBridgeId?.() || 'unknown'} created=${created}`,
300
307
  );
301
- if (!created) debugLog('bncr bridge api rebound');
308
+ if (!created) debugLog('bridge api rebound');
302
309
 
303
310
  const resolveDebug = async () => {
304
311
  try {
@@ -319,17 +326,17 @@ const plugin = {
319
326
  stop: bridge.stopService,
320
327
  });
321
328
  meta.service = true;
322
- debugLog('bncr register service ok');
329
+ debugLog('register service ok');
323
330
  } else {
324
- debugLog('bncr register service skip (already registered on this api)');
331
+ debugLog('register service skip (already registered on this api)');
325
332
  }
326
333
 
327
334
  if (!meta.channel) {
328
335
  api.registerChannel({ plugin: runtime.createBncrChannelPlugin(bridge) });
329
336
  meta.channel = true;
330
- debugLog('bncr register channel ok');
337
+ debugLog('register channel ok');
331
338
  } else {
332
- debugLog('bncr register channel skip (already registered on this api)');
339
+ debugLog('register channel skip (already registered on this api)');
333
340
  }
334
341
 
335
342
  ensureGatewayMethodRegistered(
@@ -387,7 +394,7 @@ const plugin = {
387
394
  (opts) => bridge.handleFileAck(opts),
388
395
  debugLog,
389
396
  );
390
- debugLog('bncr plugin register done');
397
+ debugLog('register done');
391
398
  },
392
399
  };
393
400
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmoxmo/bncr",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
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';
@@ -367,6 +371,54 @@ class BncrBridgeRuntime {
367
371
  return this.bridgeId;
368
372
  }
369
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
+
370
422
  private classifyRegisterTrace(stack: string) {
371
423
  if (
372
424
  stack.includes('prepareSecretsRuntimeSnapshot') ||
@@ -509,9 +561,7 @@ class BncrBridgeRuntime {
509
561
  const summary = this.buildRegisterTraceSummary();
510
562
  if (summary.postWarmupRegisterCount > 0) this.captureDriftSnapshot(summary);
511
563
 
512
- if (BNCR_DEBUG_VERBOSE) {
513
- this.api.logger.info?.(`[bncr-register-trace] ${JSON.stringify(trace)}`);
514
- }
564
+ this.logInfo('debug', `register-trace ${JSON.stringify(trace)}`, { debugOnly: true });
515
565
  }
516
566
 
517
567
  private createLeaseId() {
@@ -595,8 +645,9 @@ class BncrBridgeRuntime {
595
645
  this.staleCounters.staleFileAbort += 1;
596
646
  break;
597
647
  }
598
- this.api.logger.warn?.(
599
- `[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}`,
600
651
  );
601
652
  return { stale: true, reason: 'mismatch' as const };
602
653
  }
@@ -674,12 +725,13 @@ class BncrBridgeRuntime {
674
725
  // ignore startup canonical agent initialization errors
675
726
  }
676
727
  if (typeof debug === 'boolean') BNCR_DEBUG_VERBOSE = debug;
728
+ await this.refreshDebugFlagFromConfig({ forceLog: true });
677
729
  const bootDiag = this.buildIntegratedDiagnostics(BNCR_DEFAULT_ACCOUNT_ID);
678
- if (BNCR_DEBUG_VERBOSE) {
679
- this.api.logger.info(
680
- `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})`,
681
- );
682
- }
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
+ );
683
735
  };
684
736
 
685
737
  stopService = async () => {
@@ -688,9 +740,7 @@ class BncrBridgeRuntime {
688
740
  this.pushTimer = null;
689
741
  }
690
742
  await this.flushState();
691
- if (BNCR_DEBUG_VERBOSE) {
692
- this.api.logger.info('bncr-channel service stopped');
693
- }
743
+ this.logInfo('debug', 'service stopped', { debugOnly: true });
694
744
  };
695
745
 
696
746
  private scheduleSave() {
@@ -710,20 +760,25 @@ class BncrBridgeRuntime {
710
760
  return map.get(normalizeAccountId(accountId)) || 0;
711
761
  }
712
762
 
713
- private async syncDebugFlag() {
763
+ private async refreshDebugFlagFromConfig(options?: { forceLog?: boolean }) {
714
764
  try {
715
765
  const cfg = await this.api.runtime.config.loadConfig();
716
766
  const raw = (cfg as any)?.channels?.[CHANNEL_ID]?.debug?.verbose;
717
- if (typeof raw !== 'boolean') return;
718
- if (raw !== BNCR_DEBUG_VERBOSE) {
719
- BNCR_DEBUG_VERBOSE = raw;
720
- 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}`);
721
772
  }
722
773
  } catch {
723
774
  // ignore config read errors
724
775
  }
725
776
  }
726
777
 
778
+ private syncDebugFlag() {
779
+ void this.refreshDebugFlagFromConfig();
780
+ }
781
+
727
782
  private tryResolveBindingAgentId(args: {
728
783
  cfg: any;
729
784
  accountId: string;
@@ -777,9 +832,7 @@ class BncrBridgeRuntime {
777
832
  this.canonicalAgentId = 'main';
778
833
  this.canonicalAgentSource = 'fallback-main';
779
834
  this.canonicalAgentResolvedAt = now();
780
- this.api.logger.warn?.(
781
- '[bncr-canonical-agent] binding agent unresolved; fallback to main for current process lifetime',
782
- );
835
+ this.logWarn('target', 'binding agent unresolved; fallback to main for current process lifetime');
783
836
  return this.canonicalAgentId;
784
837
  }
785
838
 
@@ -1146,29 +1199,29 @@ class BncrBridgeRuntime {
1146
1199
  private tryPushEntry(entry: OutboxEntry): boolean {
1147
1200
  const ctx = this.gatewayContext;
1148
1201
  if (!ctx) {
1149
- if (BNCR_DEBUG_VERBOSE) {
1150
- this.api.logger.info?.(
1151
- `[bncr-outbox-push-skip] ${JSON.stringify({
1152
- messageId: entry.messageId,
1153
- accountId: entry.accountId,
1154
- reason: 'no-gateway-context',
1155
- })}`,
1156
- );
1157
- }
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
+ );
1158
1211
  return false;
1159
1212
  }
1160
1213
 
1161
1214
  const connIds = this.resolvePushConnIds(entry.accountId);
1162
1215
  if (!connIds.size) {
1163
- if (BNCR_DEBUG_VERBOSE) {
1164
- this.api.logger.info?.(
1165
- `[bncr-outbox-push-skip] ${JSON.stringify({
1166
- messageId: entry.messageId,
1167
- accountId: entry.accountId,
1168
- reason: 'no-active-connection',
1169
- })}`,
1170
- );
1171
- }
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
+ );
1172
1225
  return false;
1173
1226
  }
1174
1227
 
@@ -1179,16 +1232,16 @@ class BncrBridgeRuntime {
1179
1232
  };
1180
1233
 
1181
1234
  ctx.broadcastToConnIds(BNCR_PUSH_EVENT, payload, connIds);
1182
- if (BNCR_DEBUG_VERBOSE) {
1183
- this.api.logger.info?.(
1184
- `[bncr-outbox-push-ok] ${JSON.stringify({
1185
- messageId: entry.messageId,
1186
- accountId: entry.accountId,
1187
- connIds: Array.from(connIds),
1188
- event: BNCR_PUSH_EVENT,
1189
- })}`,
1190
- );
1191
- }
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
+ );
1192
1245
  this.lastOutboundByAccount.set(entry.accountId, now());
1193
1246
  this.markActivity(entry.accountId);
1194
1247
  this.scheduleSave();
@@ -1196,15 +1249,15 @@ class BncrBridgeRuntime {
1196
1249
  } catch (error) {
1197
1250
  entry.lastError = asString((error as any)?.message || error || 'push-error');
1198
1251
  this.outbox.set(entry.messageId, entry);
1199
- if (BNCR_DEBUG_VERBOSE) {
1200
- this.api.logger.info?.(
1201
- `[bncr-outbox-push-fail] ${JSON.stringify({
1202
- messageId: entry.messageId,
1203
- accountId: entry.accountId,
1204
- error: entry.lastError,
1205
- })}`,
1206
- );
1207
- }
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
+ );
1208
1261
  return false;
1209
1262
  }
1210
1263
  }
@@ -1227,37 +1280,37 @@ class BncrBridgeRuntime {
1227
1280
  Array.from(this.outbox.values()).map((entry) => normalizeAccountId(entry.accountId)),
1228
1281
  ),
1229
1282
  );
1230
- if (BNCR_DEBUG_VERBOSE) {
1231
- this.api.logger.info?.(
1232
- `[bncr-outbox-flush] ${JSON.stringify({
1233
- bridge: this.bridgeId,
1234
- accountId: filterAcc,
1235
- targetAccounts,
1236
- outboxSize: this.outbox.size,
1237
- })}`,
1238
- );
1239
- }
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
+ );
1240
1293
 
1241
1294
  let globalNextDelay: number | null = null;
1242
1295
 
1243
1296
  for (const acc of targetAccounts) {
1244
1297
  if (!acc || this.pushDrainRunningAccounts.has(acc)) continue;
1245
1298
  const online = this.isOnline(acc);
1246
- if (BNCR_DEBUG_VERBOSE) {
1247
- this.api.logger.info?.(
1248
- `[bncr-outbox-online] ${JSON.stringify({
1249
- bridge: this.bridgeId,
1250
- accountId: acc,
1251
- online,
1252
- connections: Array.from(this.connections.values()).map((c) => ({
1253
- accountId: c.accountId,
1254
- connId: c.connId,
1255
- clientId: c.clientId,
1256
- lastSeenAt: c.lastSeenAt,
1257
- })),
1258
- })}`,
1259
- );
1260
- }
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
+ );
1261
1314
  this.pushDrainRunningAccounts.add(acc);
1262
1315
  try {
1263
1316
  let localNextDelay: number | null = null;
@@ -1375,19 +1428,19 @@ class BncrBridgeRuntime {
1375
1428
  const staleBefore = t - CONNECT_TTL_MS * 2;
1376
1429
  for (const [key, c] of this.connections.entries()) {
1377
1430
  if (c.lastSeenAt < staleBefore) {
1378
- if (BNCR_DEBUG_VERBOSE) {
1379
- this.api.logger.info?.(
1380
- `[bncr-conn-gc] ${JSON.stringify({
1381
- bridge: this.bridgeId,
1382
- key,
1383
- accountId: c.accountId,
1384
- connId: c.connId,
1385
- clientId: c.clientId,
1386
- lastSeenAt: c.lastSeenAt,
1387
- staleBefore,
1388
- })}`,
1389
- );
1390
- }
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
+ );
1391
1444
  this.connections.delete(key);
1392
1445
  }
1393
1446
  }
@@ -1428,18 +1481,18 @@ class BncrBridgeRuntime {
1428
1481
  };
1429
1482
 
1430
1483
  this.connections.set(key, nextConn);
1431
- if (BNCR_DEBUG_VERBOSE) {
1432
- this.api.logger.info?.(
1433
- `[bncr-conn-seen] ${JSON.stringify({
1434
- bridge: this.bridgeId,
1435
- accountId: acc,
1436
- connId,
1437
- clientId: nextConn.clientId,
1438
- connectedAt: nextConn.connectedAt,
1439
- lastSeenAt: nextConn.lastSeenAt,
1440
- })}`,
1441
- );
1442
- }
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
+ );
1443
1496
 
1444
1497
  const current = this.activeConnectionByAccount.get(acc);
1445
1498
  if (!current) {
@@ -1541,9 +1594,7 @@ class BncrBridgeRuntime {
1541
1594
  const raw = asString(rawTarget).trim();
1542
1595
  if (!raw) throw new Error('bncr invalid target(empty)');
1543
1596
 
1544
- if (BNCR_DEBUG_VERBOSE) {
1545
- this.api.logger.info?.(`[bncr-target-incoming] raw=${raw} accountId=${acc}`);
1546
- }
1597
+ this.logInfo('target', `incoming raw=${raw} accountId=${acc}`, { debugOnly: true });
1547
1598
 
1548
1599
  let route: BncrRoute | null = null;
1549
1600
 
@@ -1555,8 +1606,9 @@ class BncrBridgeRuntime {
1555
1606
  }
1556
1607
 
1557
1608
  if (!route) {
1558
- this.api.logger.warn?.(
1559
- `[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)>`,
1560
1612
  );
1561
1613
  throw new Error(
1562
1614
  `bncr invalid target(standard: Bncr:<platform>:<groupId>:<userId> | Bncr:<platform>:<userId>): ${raw}`,
@@ -1577,11 +1629,11 @@ class BncrBridgeRuntime {
1577
1629
  displayScope: formatDisplayScope(route),
1578
1630
  };
1579
1631
 
1580
- if (BNCR_DEBUG_VERBOSE) {
1581
- this.api.logger.info?.(
1582
- `[bncr-target-incoming-canonical] raw=${raw} accountId=${acc} verified=${JSON.stringify(verified)}`,
1583
- );
1584
- }
1632
+ this.logInfo(
1633
+ 'target',
1634
+ `canonical raw=${raw} accountId=${acc} verified=${JSON.stringify(verified)}`,
1635
+ { debugOnly: true },
1636
+ );
1585
1637
 
1586
1638
  // 发送链路命中目标时,同步刷新 lastSession,避免状态页显示过期会话。
1587
1639
  this.lastSessionByAccount.set(acc, {
@@ -1805,22 +1857,25 @@ class BncrBridgeRuntime {
1805
1857
  }
1806
1858
 
1807
1859
  private enqueueOutbound(entry: OutboxEntry) {
1808
- if (BNCR_DEBUG_VERBOSE) {
1809
- const msg = (entry.payload as any)?.message || {};
1810
- const type = asString(msg.type || (entry.payload as any)?.type || 'unknown');
1811
- const text = asString(msg.msg || '');
1812
- this.api.logger.info?.(
1813
- `[bncr-outbox-enqueue] ${JSON.stringify({
1814
- bridge: this.bridgeId,
1815
- messageId: entry.messageId,
1816
- accountId: entry.accountId,
1817
- sessionKey: entry.sessionKey,
1818
- route: entry.route,
1819
- type,
1820
- textLen: text.length,
1821
- })}`,
1822
- );
1823
- }
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);
1824
1879
  this.outbox.set(entry.messageId, entry);
1825
1880
  this.scheduleSave();
1826
1881
  this.wakeAccountWaiters(entry.accountId);
@@ -2133,6 +2188,7 @@ class BncrBridgeRuntime {
2133
2188
  asVoice?: boolean;
2134
2189
  audioAsVoice?: boolean;
2135
2190
  kind?: 'tool' | 'block' | 'final';
2191
+ replyToId?: string;
2136
2192
  };
2137
2193
  mediaLocalRoots?: readonly string[];
2138
2194
  }) {
@@ -2171,6 +2227,7 @@ class BncrBridgeRuntime {
2171
2227
  }),
2172
2228
  hintedType: wantsVoice ? 'voice' : undefined,
2173
2229
  kind: payload.kind,
2230
+ replyToId: asString(payload.replyToId || '').trim() || undefined,
2174
2231
  now: now(),
2175
2232
  });
2176
2233
 
@@ -2198,6 +2255,7 @@ class BncrBridgeRuntime {
2198
2255
  messageId,
2199
2256
  idempotencyKey: messageId,
2200
2257
  sessionKey,
2258
+ replyToId: asString(payload.replyToId || '').trim() || undefined,
2201
2259
  message: {
2202
2260
  platform: route.platform,
2203
2261
  groupId: route.groupId,
@@ -2225,21 +2283,22 @@ class BncrBridgeRuntime {
2225
2283
  }
2226
2284
 
2227
2285
  handleConnect = async ({ params, respond, client, context }: GatewayRequestHandlerOptions) => {
2286
+ this.syncDebugFlag();
2228
2287
  const accountId = normalizeAccountId(asString(params?.accountId || ''));
2229
2288
  const connId = asString(client?.connId || '').trim() || `no-conn-${Date.now()}`;
2230
2289
  const clientId = asString((params as any)?.clientId || '').trim() || undefined;
2231
2290
 
2232
- if (BNCR_DEBUG_VERBOSE) {
2233
- this.api.logger.info?.(
2234
- `[bncr-connect] ${JSON.stringify({
2235
- bridge: this.bridgeId,
2236
- accountId,
2237
- connId,
2238
- clientId,
2239
- hasContext: Boolean(context),
2240
- })}`,
2241
- );
2242
- }
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
+ );
2243
2302
 
2244
2303
  this.rememberGatewayContext(context);
2245
2304
  this.markSeen(accountId, connId, clientId);
@@ -2272,6 +2331,7 @@ class BncrBridgeRuntime {
2272
2331
  };
2273
2332
 
2274
2333
  handleAck = async ({ params, respond, client, context }: GatewayRequestHandlerOptions) => {
2334
+ this.syncDebugFlag();
2275
2335
  const accountId = normalizeAccountId(asString(params?.accountId || ''));
2276
2336
  const connId = asString(client?.connId || '').trim() || `no-conn-${Date.now()}`;
2277
2337
  const clientId = asString((params as any)?.clientId || '').trim() || undefined;
@@ -2282,17 +2342,17 @@ class BncrBridgeRuntime {
2282
2342
  this.incrementCounter(this.ackEventsByAccount, accountId);
2283
2343
 
2284
2344
  const messageId = asString(params?.messageId || '').trim();
2285
- if (BNCR_DEBUG_VERBOSE) {
2286
- this.api.logger.info?.(
2287
- `[bncr-outbox-ack] ${JSON.stringify({
2288
- accountId,
2289
- messageId,
2290
- ok: params?.ok !== false,
2291
- fatal: params?.fatal === true,
2292
- error: asString(params?.error || ''),
2293
- })}`,
2294
- );
2295
- }
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
+ );
2296
2356
  if (!messageId) {
2297
2357
  respond(false, { error: 'messageId required' });
2298
2358
  return;
@@ -2335,23 +2395,23 @@ class BncrBridgeRuntime {
2335
2395
  };
2336
2396
 
2337
2397
  handleActivity = async ({ params, respond, client, context }: GatewayRequestHandlerOptions) => {
2338
- void this.syncDebugFlag();
2398
+ this.syncDebugFlag();
2339
2399
  const accountId = normalizeAccountId(asString(params?.accountId || ''));
2340
2400
  const connId = asString(client?.connId || '').trim() || `no-conn-${Date.now()}`;
2341
2401
  const clientId = asString((params as any)?.clientId || '').trim() || undefined;
2342
2402
  this.observeLease('activity', params ?? {});
2343
2403
  this.lastActivityAtGlobal = now();
2344
- if (BNCR_DEBUG_VERBOSE) {
2345
- this.api.logger.info?.(
2346
- `[bncr-activity] ${JSON.stringify({
2347
- bridge: this.bridgeId,
2348
- accountId,
2349
- connId,
2350
- clientId,
2351
- hasContext: Boolean(context),
2352
- })}`,
2353
- );
2354
- }
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
+ );
2355
2415
  this.rememberGatewayContext(context);
2356
2416
  this.markSeen(accountId, connId, clientId);
2357
2417
  this.markActivity(accountId);
@@ -2696,6 +2756,7 @@ class BncrBridgeRuntime {
2696
2756
  };
2697
2757
 
2698
2758
  handleInbound = async ({ params, respond, client, context }: GatewayRequestHandlerOptions) => {
2759
+ this.syncDebugFlag();
2699
2760
  const parsed = parseBncrInboundParams(params);
2700
2761
  const {
2701
2762
  accountId,
@@ -2771,6 +2832,30 @@ class BncrBridgeRuntime {
2771
2832
  resolvedRoute.sessionKey;
2772
2833
  const taskSessionKey = withTaskSessionKey(baseSessionKey, extracted.taskKey);
2773
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
+ });
2774
2859
 
2775
2860
  respond(true, {
2776
2861
  accepted: true,
@@ -2794,9 +2879,12 @@ class BncrBridgeRuntime {
2794
2879
  this.markActivity(accountId, at);
2795
2880
  },
2796
2881
  scheduleSave: () => this.scheduleSave(),
2797
- logger: this.api.logger,
2882
+ logger: {
2883
+ warn: (msg: string) => emitBncrLogLine('warn', msg),
2884
+ error: (msg: string) => emitBncrLogLine('error', msg),
2885
+ },
2798
2886
  }).catch((err) => {
2799
- this.api.logger.error?.(`bncr inbound process failed: ${String(err)}`);
2887
+ this.logError('inbound', `process failed: ${String(err)}`);
2800
2888
  });
2801
2889
  };
2802
2890
 
@@ -2847,30 +2935,31 @@ class BncrBridgeRuntime {
2847
2935
  const accountId = normalizeAccountId(ctx.accountId);
2848
2936
  const to = asString(ctx.to || '').trim();
2849
2937
 
2850
- if (BNCR_DEBUG_VERBOSE) {
2851
- this.api.logger.info?.(
2852
- `[bncr-send-entry:text] ${JSON.stringify({
2853
- accountId,
2854
- to,
2855
- text: asString(ctx?.text || ''),
2856
- mediaUrl: asString(ctx?.mediaUrl || ''),
2857
- sessionKey: asString(ctx?.sessionKey || ''),
2858
- mirrorSessionKey: asString(ctx?.mirror?.sessionKey || ''),
2859
- rawCtx: {
2860
- to: ctx?.to,
2861
- accountId: ctx?.accountId,
2862
- threadId: ctx?.threadId,
2863
- replyToId: ctx?.replyToId,
2864
- },
2865
- })}`,
2866
- );
2867
- }
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
+ );
2868
2956
 
2869
2957
  return sendBncrText({
2870
2958
  channelId: CHANNEL_ID,
2871
2959
  accountId,
2872
2960
  to,
2873
2961
  text: asString(ctx.text || ''),
2962
+ replyToId: asString(ctx?.replyToId || ctx?.replyToMessageId || '').trim() || undefined,
2874
2963
  mediaLocalRoots: ctx.mediaLocalRoots,
2875
2964
  resolveVerifiedTarget: (to, accountId) => this.resolveVerifiedTarget(to, accountId),
2876
2965
  rememberSessionRoute: (sessionKey, accountId, route) =>
@@ -2886,27 +2975,27 @@ class BncrBridgeRuntime {
2886
2975
  const asVoice = ctx?.asVoice === true;
2887
2976
  const audioAsVoice = ctx?.audioAsVoice === true;
2888
2977
 
2889
- if (BNCR_DEBUG_VERBOSE) {
2890
- this.api.logger.info?.(
2891
- `[bncr-send-entry:media] ${JSON.stringify({
2892
- accountId,
2893
- to,
2894
- text: asString(ctx?.text || ''),
2895
- mediaUrl: asString(ctx?.mediaUrl || ''),
2896
- mediaUrls: Array.isArray(ctx?.mediaUrls) ? ctx.mediaUrls : undefined,
2897
- asVoice,
2898
- audioAsVoice,
2899
- sessionKey: asString(ctx?.sessionKey || ''),
2900
- mirrorSessionKey: asString(ctx?.mirror?.sessionKey || ''),
2901
- rawCtx: {
2902
- to: ctx?.to,
2903
- accountId: ctx?.accountId,
2904
- threadId: ctx?.threadId,
2905
- replyToId: ctx?.replyToId,
2906
- },
2907
- })}`,
2908
- );
2909
- }
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
+ );
2910
2999
 
2911
3000
  return sendBncrMedia({
2912
3001
  channelId: CHANNEL_ID,
@@ -2916,6 +3005,7 @@ class BncrBridgeRuntime {
2916
3005
  mediaUrl: asString(ctx.mediaUrl || ''),
2917
3006
  asVoice,
2918
3007
  audioAsVoice,
3008
+ replyToId: asString(ctx?.replyToId || ctx?.replyToMessageId || '').trim() || undefined,
2919
3009
  mediaLocalRoots: ctx.mediaLocalRoots,
2920
3010
  resolveVerifiedTarget: (to, accountId) => this.resolveVerifiedTarget(to, accountId),
2921
3011
  rememberSessionRoute: (sessionKey, accountId, route) =>
@@ -0,0 +1,65 @@
1
+ export type BncrLogLevel = 'info' | 'warn' | 'error';
2
+ export type BncrLogOptions = { debugOnly?: boolean };
3
+
4
+ const BNCR_PREFIX = '[bncr]';
5
+
6
+ type DebugGate = () => boolean;
7
+
8
+ type ConsoleMethod = 'log' | 'warn' | 'error';
9
+
10
+ function resolveConsoleMethod(level: BncrLogLevel): ConsoleMethod {
11
+ switch (level) {
12
+ case 'warn':
13
+ return 'warn';
14
+ case 'error':
15
+ return 'error';
16
+ default:
17
+ return 'log';
18
+ }
19
+ }
20
+
21
+ function emitConsole(method: ConsoleMethod, line: string) {
22
+ if (method === 'warn') {
23
+ console.warn(line);
24
+ return;
25
+ }
26
+ if (method === 'error') {
27
+ console.error(line);
28
+ return;
29
+ }
30
+ console.log(line);
31
+ }
32
+
33
+ export function normalizeBncrLogLine(raw: string | undefined) {
34
+ const text = String(raw || '').trim();
35
+ if (!text) return BNCR_PREFIX;
36
+ return text.startsWith(BNCR_PREFIX) ? text : `${BNCR_PREFIX} ${text}`;
37
+ }
38
+
39
+ export function formatBncrLogLine(scope: string | undefined, message: string | undefined) {
40
+ const normalizedScope = String(scope || '').trim();
41
+ const normalizedMessage = String(message || '').trim();
42
+ const prefix = normalizedScope ? `${BNCR_PREFIX} ${normalizedScope}` : BNCR_PREFIX;
43
+ return normalizedMessage ? `${prefix} ${normalizedMessage}` : prefix;
44
+ }
45
+
46
+ export function emitBncrLog(
47
+ level: BncrLogLevel,
48
+ scope: string | undefined,
49
+ message: string | undefined,
50
+ options?: BncrLogOptions,
51
+ isDebugEnabled?: DebugGate,
52
+ ) {
53
+ if (options?.debugOnly && !(isDebugEnabled?.() ?? false)) return;
54
+ emitConsole(resolveConsoleMethod(level), formatBncrLogLine(scope, message));
55
+ }
56
+
57
+ export function emitBncrLogLine(
58
+ level: BncrLogLevel,
59
+ line: string | undefined,
60
+ options?: BncrLogOptions,
61
+ isDebugEnabled?: DebugGate,
62
+ ) {
63
+ if (options?.debugOnly && !(isDebugEnabled?.() ?? false)) return;
64
+ emitConsole(resolveConsoleMethod(level), normalizeBncrLogLine(line));
65
+ }
@@ -1,3 +1,4 @@
1
+ import { emitBncrLogLine } from '../../core/logging.ts';
1
2
  import {
2
3
  formatDisplayScope,
3
4
  normalizeInboundSessionKey,
@@ -78,7 +79,7 @@ export async function handleBncrNativeCommand(params: {
78
79
  const displayTo = formatDisplayScope(route);
79
80
  const body = command.body;
80
81
  if (!clientId) {
81
- logger?.warn?.('bncr: missing clientId for inbound command identity');
82
+ emitBncrLogLine('warn', '[bncr] inbound missing clientId for native command identity');
82
83
  return { handled: false };
83
84
  }
84
85
  const senderIdForContext = clientId;
@@ -119,7 +120,7 @@ export async function handleBncrNativeCommand(params: {
119
120
  sessionKey,
120
121
  ctx: ctxPayload,
121
122
  onRecordError: (err: unknown) => {
122
- logger?.warn?.(`bncr: record native command session failed: ${String(err)}`);
123
+ emitBncrLogLine('warn', `[bncr] inbound record native command session failed: ${String(err)}`);
123
124
  },
124
125
  });
125
126
 
@@ -155,6 +156,7 @@ export async function handleBncrNativeCommand(params: {
155
156
  payload: {
156
157
  ...payload,
157
158
  kind: kind as 'tool' | 'block' | 'final' | undefined,
159
+ replyToId: msgId || undefined,
158
160
  },
159
161
  });
160
162
  },
@@ -1,3 +1,4 @@
1
+ import { emitBncrLogLine } from '../../core/logging.ts';
1
2
  import {
2
3
  formatDisplayScope,
3
4
  normalizeInboundSessionKey,
@@ -132,7 +133,7 @@ export async function dispatchBncrInbound(params: {
132
133
 
133
134
  const displayTo = formatDisplayScope(route);
134
135
  if (!clientId) {
135
- logger?.warn?.('bncr: missing clientId for inbound chat identity');
136
+ emitBncrLogLine('warn', '[bncr] inbound missing clientId for chat identity');
136
137
  return {
137
138
  accountId,
138
139
  sessionKey,
@@ -171,7 +172,7 @@ export async function dispatchBncrInbound(params: {
171
172
  sessionKey,
172
173
  ctx: ctxPayload,
173
174
  onRecordError: (err: unknown) => {
174
- logger?.warn?.(`bncr: record session failed: ${String(err)}`);
175
+ emitBncrLogLine('warn', `[bncr] inbound record session failed: ${String(err)}`);
175
176
  },
176
177
  });
177
178
 
@@ -207,7 +208,7 @@ export async function dispatchBncrInbound(params: {
207
208
  });
208
209
  },
209
210
  onError: (err: unknown) => {
210
- logger?.error?.(`bncr reply failed: ${String(err)}`);
211
+ emitBncrLogLine('error', `[bncr] outbound reply failed: ${String(err)}`);
211
212
  },
212
213
  },
213
214
  replyOptions: {
@@ -56,6 +56,7 @@ export function buildBncrMediaOutboundFrame(params: {
56
56
  fileName: string;
57
57
  hintedType?: string;
58
58
  kind?: 'tool' | 'block' | 'final';
59
+ replyToId?: string;
59
60
  now: number;
60
61
  }) {
61
62
  return {
@@ -63,6 +64,7 @@ export function buildBncrMediaOutboundFrame(params: {
63
64
  messageId: params.messageId,
64
65
  idempotencyKey: params.messageId,
65
66
  sessionKey: params.sessionKey,
67
+ replyToId: asString(params.replyToId || '').trim() || undefined,
66
68
  message: {
67
69
  platform: params.route.platform,
68
70
  groupId: params.route.groupId,
@@ -3,6 +3,7 @@ export async function sendBncrText(params: {
3
3
  accountId: string;
4
4
  to: string;
5
5
  text: string;
6
+ replyToId?: string;
6
7
  mediaLocalRoots?: readonly string[];
7
8
  resolveVerifiedTarget: (
8
9
  to: string,
@@ -13,7 +14,12 @@ export async function sendBncrText(params: {
13
14
  accountId: string;
14
15
  sessionKey: string;
15
16
  route: any;
16
- payload: { text?: string; mediaUrl?: string; mediaUrls?: string[] };
17
+ payload: {
18
+ text?: string;
19
+ mediaUrl?: string;
20
+ mediaUrls?: string[];
21
+ replyToId?: string;
22
+ };
17
23
  mediaLocalRoots?: readonly string[];
18
24
  }) => Promise<void>;
19
25
  createMessageId: () => string;
@@ -27,6 +33,7 @@ export async function sendBncrText(params: {
27
33
  route: verified.route,
28
34
  payload: {
29
35
  text: params.text,
36
+ replyToId: params.replyToId,
30
37
  },
31
38
  mediaLocalRoots: params.mediaLocalRoots,
32
39
  });
@@ -46,6 +53,7 @@ export async function sendBncrMedia(params: {
46
53
  mediaUrl?: string;
47
54
  asVoice?: boolean;
48
55
  audioAsVoice?: boolean;
56
+ replyToId?: string;
49
57
  mediaLocalRoots?: readonly string[];
50
58
  resolveVerifiedTarget: (
51
59
  to: string,
@@ -62,6 +70,7 @@ export async function sendBncrMedia(params: {
62
70
  mediaUrls?: string[];
63
71
  asVoice?: boolean;
64
72
  audioAsVoice?: boolean;
73
+ replyToId?: string;
65
74
  };
66
75
  mediaLocalRoots?: readonly string[];
67
76
  }) => Promise<void>;
@@ -79,6 +88,7 @@ export async function sendBncrMedia(params: {
79
88
  mediaUrl: params.mediaUrl || '',
80
89
  asVoice: params.asVoice === true ? true : undefined,
81
90
  audioAsVoice: params.audioAsVoice === true ? true : undefined,
91
+ replyToId: params.replyToId,
82
92
  },
83
93
  mediaLocalRoots: params.mediaLocalRoots,
84
94
  });