@xmoxmo/bncr 0.1.7 → 0.1.9

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,18 @@ 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
- debugLog(
299
- `bncr plugin register begin bridge=${bridge.getBridgeId?.() || 'unknown'} created=${created}`,
300
- );
301
- if (!created) debugLog('bncr bridge api rebound');
305
+ debugLog(`register begin bridge=${bridge.getBridgeId?.() || 'unknown'} created=${created}`);
306
+ if (!created) debugLog('bridge api rebound');
302
307
 
303
308
  const resolveDebug = async () => {
304
309
  try {
@@ -319,17 +324,17 @@ const plugin = {
319
324
  stop: bridge.stopService,
320
325
  });
321
326
  meta.service = true;
322
- debugLog('bncr register service ok');
327
+ debugLog('register service ok');
323
328
  } else {
324
- debugLog('bncr register service skip (already registered on this api)');
329
+ debugLog('register service skip (already registered on this api)');
325
330
  }
326
331
 
327
332
  if (!meta.channel) {
328
333
  api.registerChannel({ plugin: runtime.createBncrChannelPlugin(bridge) });
329
334
  meta.channel = true;
330
- debugLog('bncr register channel ok');
335
+ debugLog('register channel ok');
331
336
  } else {
332
- debugLog('bncr register channel skip (already registered on this api)');
337
+ debugLog('register channel skip (already registered on this api)');
333
338
  }
334
339
 
335
340
  ensureGatewayMethodRegistered(
@@ -387,7 +392,7 @@ const plugin = {
387
392
  (opts) => bridge.handleFileAck(opts),
388
393
  debugLog,
389
394
  );
390
- debugLog('bncr plugin register done');
395
+ debugLog('register done');
391
396
  },
392
397
  };
393
398
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmoxmo/bncr",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/channel.ts CHANGED
@@ -26,6 +26,7 @@ import {
26
26
  resolveDefaultDisplayName,
27
27
  } from './core/accounts.ts';
28
28
  import { BncrConfigSchema } from './core/config-schema.ts';
29
+ import { emitBncrLog, emitBncrLogLine } from './core/logging.ts';
29
30
  import { buildBncrPermissionSummary } from './core/permissions.ts';
30
31
  import { resolveBncrChannelPolicy } from './core/policy.ts';
31
32
  import { probeBncrAccount } from './core/probe.ts';
@@ -367,6 +368,51 @@ class BncrBridgeRuntime {
367
368
  return this.bridgeId;
368
369
  }
369
370
 
371
+ private logInfo(scope: string | undefined, message: string, options?: { debugOnly?: boolean }) {
372
+ emitBncrLog('info', scope, message, options, () => this.isDebugEnabled());
373
+ }
374
+
375
+ private logWarn(scope: string | undefined, message: string, options?: { debugOnly?: boolean }) {
376
+ emitBncrLog('warn', scope, message, options, () => this.isDebugEnabled());
377
+ }
378
+
379
+ private logError(scope: string | undefined, message: string, options?: { debugOnly?: boolean }) {
380
+ emitBncrLog('error', scope, message, options, () => this.isDebugEnabled());
381
+ }
382
+
383
+ private summarizeTextPreview(raw: string, limit = 8) {
384
+ const compact = asString(raw || '')
385
+ .replace(/\s+/g, ' ')
386
+ .trim();
387
+ if (!compact) return '-';
388
+ const chars = Array.from(compact);
389
+ return chars.length > limit ? `${chars.slice(0, Math.max(1, limit)).join('')}…` : compact;
390
+ }
391
+
392
+ private summarizeScope(route: BncrRoute) {
393
+ return formatDisplayScope(route);
394
+ }
395
+
396
+ private logInboundSummary(params: {
397
+ accountId: string;
398
+ route: BncrRoute;
399
+ msgType: string;
400
+ text: string;
401
+ hasMedia: boolean;
402
+ }) {
403
+ const type = params.hasMedia ? `${params.msgType}+media` : params.msgType;
404
+ const preview = this.summarizeTextPreview(params.text);
405
+ this.logInfo('inbound', [type, this.summarizeScope(params.route), preview].join('|'));
406
+ }
407
+
408
+ private logOutboundSummary(entry: OutboxEntry) {
409
+ const msg = (entry.payload as any)?.message || {};
410
+ const type = asString(msg.type || (entry.payload as any)?.type || 'unknown');
411
+ const text = asString(msg.msg || '');
412
+ const preview = this.summarizeTextPreview(text);
413
+ this.logInfo('outbound', [type, this.summarizeScope(entry.route), preview].join('|'));
414
+ }
415
+
370
416
  private classifyRegisterTrace(stack: string) {
371
417
  if (
372
418
  stack.includes('prepareSecretsRuntimeSnapshot') ||
@@ -509,9 +555,7 @@ class BncrBridgeRuntime {
509
555
  const summary = this.buildRegisterTraceSummary();
510
556
  if (summary.postWarmupRegisterCount > 0) this.captureDriftSnapshot(summary);
511
557
 
512
- if (BNCR_DEBUG_VERBOSE) {
513
- this.api.logger.info?.(`[bncr-register-trace] ${JSON.stringify(trace)}`);
514
- }
558
+ this.logInfo('debug', `register-trace ${JSON.stringify(trace)}`, { debugOnly: true });
515
559
  }
516
560
 
517
561
  private createLeaseId() {
@@ -595,8 +639,10 @@ class BncrBridgeRuntime {
595
639
  this.staleCounters.staleFileAbort += 1;
596
640
  break;
597
641
  }
598
- this.api.logger.warn?.(
599
- `[bncr] stale ${kind} observed lease=${leaseId || '-'} epoch=${connectionEpoch ?? '-'} currentLease=${this.primaryLeaseId || '-'} currentEpoch=${this.connectionEpoch}`,
642
+ this.logWarn(
643
+ 'stale',
644
+ `observed kind=${kind} lease=${leaseId || '-'} epoch=${connectionEpoch ?? '-'} currentLease=${this.primaryLeaseId || '-'} currentEpoch=${this.connectionEpoch}`,
645
+ { debugOnly: true },
600
646
  );
601
647
  return { stale: true, reason: 'mismatch' as const };
602
648
  }
@@ -656,12 +702,7 @@ class BncrBridgeRuntime {
656
702
  }
657
703
 
658
704
  isDebugEnabled(): boolean {
659
- try {
660
- const cfg = (this.api.runtime.config?.get?.() as any) || {};
661
- return Boolean(cfg?.channels?.[CHANNEL_ID]?.debug?.verbose);
662
- } catch {
663
- return false;
664
- }
705
+ return BNCR_DEBUG_VERBOSE;
665
706
  }
666
707
 
667
708
  startService = async (ctx: OpenClawPluginServiceContext, debug?: boolean) => {
@@ -674,12 +715,17 @@ class BncrBridgeRuntime {
674
715
  // ignore startup canonical agent initialization errors
675
716
  }
676
717
  if (typeof debug === 'boolean') BNCR_DEBUG_VERBOSE = debug;
718
+ await this.refreshDebugFlagFromConfig({ forceLog: true });
677
719
  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
- }
720
+ this.logInfo(
721
+ 'startup',
722
+ `bridge=${this.bridgeId} routes=${bootDiag.regression.totalKnownRoutes}`,
723
+ );
724
+ this.logInfo(
725
+ 'debug',
726
+ `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}`,
727
+ { debugOnly: true },
728
+ );
683
729
  };
684
730
 
685
731
  stopService = async () => {
@@ -688,9 +734,7 @@ class BncrBridgeRuntime {
688
734
  this.pushTimer = null;
689
735
  }
690
736
  await this.flushState();
691
- if (BNCR_DEBUG_VERBOSE) {
692
- this.api.logger.info('bncr-channel service stopped');
693
- }
737
+ this.logInfo('debug', 'service stopped', { debugOnly: true });
694
738
  };
695
739
 
696
740
  private scheduleSave() {
@@ -710,20 +754,25 @@ class BncrBridgeRuntime {
710
754
  return map.get(normalizeAccountId(accountId)) || 0;
711
755
  }
712
756
 
713
- private async syncDebugFlag() {
757
+ private async refreshDebugFlagFromConfig(options?: { forceLog?: boolean }) {
714
758
  try {
715
759
  const cfg = await this.api.runtime.config.loadConfig();
716
760
  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}`);
761
+ const next = typeof raw === 'boolean' ? raw : false;
762
+ const changed = next !== BNCR_DEBUG_VERBOSE;
763
+ BNCR_DEBUG_VERBOSE = next;
764
+ if (changed || options?.forceLog) {
765
+ this.logInfo('debug', `verbose=${BNCR_DEBUG_VERBOSE}`, { debugOnly: true });
721
766
  }
722
767
  } catch {
723
768
  // ignore config read errors
724
769
  }
725
770
  }
726
771
 
772
+ private async syncDebugFlag() {
773
+ await this.refreshDebugFlagFromConfig();
774
+ }
775
+
727
776
  private tryResolveBindingAgentId(args: {
728
777
  cfg: any;
729
778
  accountId: string;
@@ -777,8 +826,10 @@ class BncrBridgeRuntime {
777
826
  this.canonicalAgentId = 'main';
778
827
  this.canonicalAgentSource = 'fallback-main';
779
828
  this.canonicalAgentResolvedAt = now();
780
- this.api.logger.warn?.(
781
- '[bncr-canonical-agent] binding agent unresolved; fallback to main for current process lifetime',
829
+ this.logWarn(
830
+ 'target',
831
+ 'binding agent unresolved; fallback to main for current process lifetime',
832
+ { debugOnly: true },
782
833
  );
783
834
  return this.canonicalAgentId;
784
835
  }
@@ -1146,29 +1197,29 @@ class BncrBridgeRuntime {
1146
1197
  private tryPushEntry(entry: OutboxEntry): boolean {
1147
1198
  const ctx = this.gatewayContext;
1148
1199
  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
- }
1200
+ this.logInfo(
1201
+ 'outbox',
1202
+ `push-skip ${JSON.stringify({
1203
+ messageId: entry.messageId,
1204
+ accountId: entry.accountId,
1205
+ reason: 'no-gateway-context',
1206
+ })}`,
1207
+ { debugOnly: true },
1208
+ );
1158
1209
  return false;
1159
1210
  }
1160
1211
 
1161
1212
  const connIds = this.resolvePushConnIds(entry.accountId);
1162
1213
  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
- }
1214
+ this.logInfo(
1215
+ 'outbox',
1216
+ `push-skip ${JSON.stringify({
1217
+ messageId: entry.messageId,
1218
+ accountId: entry.accountId,
1219
+ reason: 'no-active-connection',
1220
+ })}`,
1221
+ { debugOnly: true },
1222
+ );
1172
1223
  return false;
1173
1224
  }
1174
1225
 
@@ -1179,16 +1230,16 @@ class BncrBridgeRuntime {
1179
1230
  };
1180
1231
 
1181
1232
  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
- }
1233
+ this.logInfo(
1234
+ 'outbox',
1235
+ `push-ok ${JSON.stringify({
1236
+ messageId: entry.messageId,
1237
+ accountId: entry.accountId,
1238
+ connIds: Array.from(connIds),
1239
+ event: BNCR_PUSH_EVENT,
1240
+ })}`,
1241
+ { debugOnly: true },
1242
+ );
1192
1243
  this.lastOutboundByAccount.set(entry.accountId, now());
1193
1244
  this.markActivity(entry.accountId);
1194
1245
  this.scheduleSave();
@@ -1196,15 +1247,15 @@ class BncrBridgeRuntime {
1196
1247
  } catch (error) {
1197
1248
  entry.lastError = asString((error as any)?.message || error || 'push-error');
1198
1249
  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
- }
1250
+ this.logInfo(
1251
+ 'outbox',
1252
+ `push-fail ${JSON.stringify({
1253
+ messageId: entry.messageId,
1254
+ accountId: entry.accountId,
1255
+ error: entry.lastError,
1256
+ })}`,
1257
+ { debugOnly: true },
1258
+ );
1208
1259
  return false;
1209
1260
  }
1210
1261
  }
@@ -1227,37 +1278,37 @@ class BncrBridgeRuntime {
1227
1278
  Array.from(this.outbox.values()).map((entry) => normalizeAccountId(entry.accountId)),
1228
1279
  ),
1229
1280
  );
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
- }
1281
+ this.logInfo(
1282
+ 'outbox',
1283
+ `flush ${JSON.stringify({
1284
+ bridge: this.bridgeId,
1285
+ accountId: filterAcc,
1286
+ targetAccounts,
1287
+ outboxSize: this.outbox.size,
1288
+ })}`,
1289
+ { debugOnly: true },
1290
+ );
1240
1291
 
1241
1292
  let globalNextDelay: number | null = null;
1242
1293
 
1243
1294
  for (const acc of targetAccounts) {
1244
1295
  if (!acc || this.pushDrainRunningAccounts.has(acc)) continue;
1245
1296
  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
- }
1297
+ this.logInfo(
1298
+ 'outbox',
1299
+ `online ${JSON.stringify({
1300
+ bridge: this.bridgeId,
1301
+ accountId: acc,
1302
+ online,
1303
+ connections: Array.from(this.connections.values()).map((c) => ({
1304
+ accountId: c.accountId,
1305
+ connId: c.connId,
1306
+ clientId: c.clientId,
1307
+ lastSeenAt: c.lastSeenAt,
1308
+ })),
1309
+ })}`,
1310
+ { debugOnly: true },
1311
+ );
1261
1312
  this.pushDrainRunningAccounts.add(acc);
1262
1313
  try {
1263
1314
  let localNextDelay: number | null = null;
@@ -1375,19 +1426,19 @@ class BncrBridgeRuntime {
1375
1426
  const staleBefore = t - CONNECT_TTL_MS * 2;
1376
1427
  for (const [key, c] of this.connections.entries()) {
1377
1428
  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
- }
1429
+ this.logInfo(
1430
+ 'connection',
1431
+ `gc ${JSON.stringify({
1432
+ bridge: this.bridgeId,
1433
+ key,
1434
+ accountId: c.accountId,
1435
+ connId: c.connId,
1436
+ clientId: c.clientId,
1437
+ lastSeenAt: c.lastSeenAt,
1438
+ staleBefore,
1439
+ })}`,
1440
+ { debugOnly: true },
1441
+ );
1391
1442
  this.connections.delete(key);
1392
1443
  }
1393
1444
  }
@@ -1428,18 +1479,18 @@ class BncrBridgeRuntime {
1428
1479
  };
1429
1480
 
1430
1481
  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
- }
1482
+ this.logInfo(
1483
+ 'connection',
1484
+ `seen ${JSON.stringify({
1485
+ bridge: this.bridgeId,
1486
+ accountId: acc,
1487
+ connId,
1488
+ clientId: nextConn.clientId,
1489
+ connectedAt: nextConn.connectedAt,
1490
+ lastSeenAt: nextConn.lastSeenAt,
1491
+ })}`,
1492
+ { debugOnly: true },
1493
+ );
1443
1494
 
1444
1495
  const current = this.activeConnectionByAccount.get(acc);
1445
1496
  if (!current) {
@@ -1541,9 +1592,7 @@ class BncrBridgeRuntime {
1541
1592
  const raw = asString(rawTarget).trim();
1542
1593
  if (!raw) throw new Error('bncr invalid target(empty)');
1543
1594
 
1544
- if (BNCR_DEBUG_VERBOSE) {
1545
- this.api.logger.info?.(`[bncr-target-incoming] raw=${raw} accountId=${acc}`);
1546
- }
1595
+ this.logInfo('target', `incoming raw=${raw} accountId=${acc}`, { debugOnly: true });
1547
1596
 
1548
1597
  let route: BncrRoute | null = null;
1549
1598
 
@@ -1555,8 +1604,10 @@ class BncrBridgeRuntime {
1555
1604
  }
1556
1605
 
1557
1606
  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)>`,
1607
+ this.logWarn(
1608
+ 'target',
1609
+ `invalid raw=${raw} accountId=${acc} reason=unparseable-or-unknown standardTo=Bncr:<platform>:<groupId>:<userId>|Bncr:<platform>:<userId> standardSessionKey=agent:<agentId>:bncr:direct:<hex(scope)>`,
1610
+ { debugOnly: true },
1560
1611
  );
1561
1612
  throw new Error(
1562
1613
  `bncr invalid target(standard: Bncr:<platform>:<groupId>:<userId> | Bncr:<platform>:<userId>): ${raw}`,
@@ -1577,11 +1628,11 @@ class BncrBridgeRuntime {
1577
1628
  displayScope: formatDisplayScope(route),
1578
1629
  };
1579
1630
 
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
- }
1631
+ this.logInfo(
1632
+ 'target',
1633
+ `canonical raw=${raw} accountId=${acc} verified=${JSON.stringify(verified)}`,
1634
+ { debugOnly: true },
1635
+ );
1585
1636
 
1586
1637
  // 发送链路命中目标时,同步刷新 lastSession,避免状态页显示过期会话。
1587
1638
  this.lastSessionByAccount.set(acc, {
@@ -1805,22 +1856,25 @@ class BncrBridgeRuntime {
1805
1856
  }
1806
1857
 
1807
1858
  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
- }
1859
+ const msg = (entry.payload as any)?.message || {};
1860
+ const type = asString(msg.type || (entry.payload as any)?.type || 'unknown');
1861
+ const text = asString(msg.msg || '');
1862
+ const displayScope = formatDisplayScope(entry.route);
1863
+ this.logInfo(
1864
+ 'outbound',
1865
+ JSON.stringify({
1866
+ bridge: this.bridgeId,
1867
+ messageId: entry.messageId,
1868
+ accountId: entry.accountId,
1869
+ sessionKey: entry.sessionKey,
1870
+ scope: displayScope,
1871
+ type,
1872
+ textLen: text.length,
1873
+ textPreview: text.slice(0, 120),
1874
+ }),
1875
+ { debugOnly: true },
1876
+ );
1877
+ this.logOutboundSummary(entry);
1824
1878
  this.outbox.set(entry.messageId, entry);
1825
1879
  this.scheduleSave();
1826
1880
  this.wakeAccountWaiters(entry.accountId);
@@ -2133,6 +2187,7 @@ class BncrBridgeRuntime {
2133
2187
  asVoice?: boolean;
2134
2188
  audioAsVoice?: boolean;
2135
2189
  kind?: 'tool' | 'block' | 'final';
2190
+ replyToId?: string;
2136
2191
  };
2137
2192
  mediaLocalRoots?: readonly string[];
2138
2193
  }) {
@@ -2171,6 +2226,7 @@ class BncrBridgeRuntime {
2171
2226
  }),
2172
2227
  hintedType: wantsVoice ? 'voice' : undefined,
2173
2228
  kind: payload.kind,
2229
+ replyToId: asString(payload.replyToId || '').trim() || undefined,
2174
2230
  now: now(),
2175
2231
  });
2176
2232
 
@@ -2198,6 +2254,7 @@ class BncrBridgeRuntime {
2198
2254
  messageId,
2199
2255
  idempotencyKey: messageId,
2200
2256
  sessionKey,
2257
+ replyToId: asString(payload.replyToId || '').trim() || undefined,
2201
2258
  message: {
2202
2259
  platform: route.platform,
2203
2260
  groupId: route.groupId,
@@ -2225,21 +2282,22 @@ class BncrBridgeRuntime {
2225
2282
  }
2226
2283
 
2227
2284
  handleConnect = async ({ params, respond, client, context }: GatewayRequestHandlerOptions) => {
2285
+ await this.syncDebugFlag();
2228
2286
  const accountId = normalizeAccountId(asString(params?.accountId || ''));
2229
2287
  const connId = asString(client?.connId || '').trim() || `no-conn-${Date.now()}`;
2230
2288
  const clientId = asString((params as any)?.clientId || '').trim() || undefined;
2231
2289
 
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
- }
2290
+ this.logInfo(
2291
+ 'connection',
2292
+ `connect ${JSON.stringify({
2293
+ bridge: this.bridgeId,
2294
+ accountId,
2295
+ connId,
2296
+ clientId,
2297
+ hasContext: Boolean(context),
2298
+ })}`,
2299
+ { debugOnly: true },
2300
+ );
2243
2301
 
2244
2302
  this.rememberGatewayContext(context);
2245
2303
  this.markSeen(accountId, connId, clientId);
@@ -2272,6 +2330,7 @@ class BncrBridgeRuntime {
2272
2330
  };
2273
2331
 
2274
2332
  handleAck = async ({ params, respond, client, context }: GatewayRequestHandlerOptions) => {
2333
+ await this.syncDebugFlag();
2275
2334
  const accountId = normalizeAccountId(asString(params?.accountId || ''));
2276
2335
  const connId = asString(client?.connId || '').trim() || `no-conn-${Date.now()}`;
2277
2336
  const clientId = asString((params as any)?.clientId || '').trim() || undefined;
@@ -2282,17 +2341,17 @@ class BncrBridgeRuntime {
2282
2341
  this.incrementCounter(this.ackEventsByAccount, accountId);
2283
2342
 
2284
2343
  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
- }
2344
+ this.logInfo(
2345
+ 'outbox',
2346
+ `ack ${JSON.stringify({
2347
+ accountId,
2348
+ messageId,
2349
+ ok: params?.ok !== false,
2350
+ fatal: params?.fatal === true,
2351
+ error: asString(params?.error || ''),
2352
+ })}`,
2353
+ { debugOnly: true },
2354
+ );
2296
2355
  if (!messageId) {
2297
2356
  respond(false, { error: 'messageId required' });
2298
2357
  return;
@@ -2335,23 +2394,23 @@ class BncrBridgeRuntime {
2335
2394
  };
2336
2395
 
2337
2396
  handleActivity = async ({ params, respond, client, context }: GatewayRequestHandlerOptions) => {
2338
- void this.syncDebugFlag();
2397
+ await this.syncDebugFlag();
2339
2398
  const accountId = normalizeAccountId(asString(params?.accountId || ''));
2340
2399
  const connId = asString(client?.connId || '').trim() || `no-conn-${Date.now()}`;
2341
2400
  const clientId = asString((params as any)?.clientId || '').trim() || undefined;
2342
2401
  this.observeLease('activity', params ?? {});
2343
2402
  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
- }
2403
+ this.logInfo(
2404
+ 'activity',
2405
+ `event ${JSON.stringify({
2406
+ bridge: this.bridgeId,
2407
+ accountId,
2408
+ connId,
2409
+ clientId,
2410
+ hasContext: Boolean(context),
2411
+ })}`,
2412
+ { debugOnly: true },
2413
+ );
2355
2414
  this.rememberGatewayContext(context);
2356
2415
  this.markSeen(accountId, connId, clientId);
2357
2416
  this.markActivity(accountId);
@@ -2696,6 +2755,7 @@ class BncrBridgeRuntime {
2696
2755
  };
2697
2756
 
2698
2757
  handleInbound = async ({ params, respond, client, context }: GatewayRequestHandlerOptions) => {
2758
+ await this.syncDebugFlag();
2699
2759
  const parsed = parseBncrInboundParams(params);
2700
2760
  const {
2701
2761
  accountId,
@@ -2771,6 +2831,30 @@ class BncrBridgeRuntime {
2771
2831
  resolvedRoute.sessionKey;
2772
2832
  const taskSessionKey = withTaskSessionKey(baseSessionKey, extracted.taskKey);
2773
2833
  const sessionKey = taskSessionKey || baseSessionKey;
2834
+ const inboundText = asString(extracted.text || text || '');
2835
+ this.logInfo(
2836
+ 'inbound',
2837
+ JSON.stringify({
2838
+ accountId,
2839
+ msgId: msgId ?? null,
2840
+ platform,
2841
+ chatType: peer.kind,
2842
+ scope: formatDisplayScope(route),
2843
+ sessionKey,
2844
+ msgType,
2845
+ textLen: inboundText.length,
2846
+ textPreview: inboundText.slice(0, 120),
2847
+ hasMedia: Boolean(mediaBase64 || mediaPathFromTransfer),
2848
+ }),
2849
+ { debugOnly: true },
2850
+ );
2851
+ this.logInboundSummary({
2852
+ accountId,
2853
+ route,
2854
+ msgType,
2855
+ text: inboundText,
2856
+ hasMedia: Boolean(mediaBase64 || mediaPathFromTransfer),
2857
+ });
2774
2858
 
2775
2859
  respond(true, {
2776
2860
  accepted: true,
@@ -2794,9 +2878,12 @@ class BncrBridgeRuntime {
2794
2878
  this.markActivity(accountId, at);
2795
2879
  },
2796
2880
  scheduleSave: () => this.scheduleSave(),
2797
- logger: this.api.logger,
2881
+ logger: {
2882
+ warn: (msg: string) => emitBncrLogLine('warn', msg),
2883
+ error: (msg: string) => emitBncrLogLine('error', msg),
2884
+ },
2798
2885
  }).catch((err) => {
2799
- this.api.logger.error?.(`bncr inbound process failed: ${String(err)}`);
2886
+ this.logError('inbound', `process failed: ${String(err)}`, { debugOnly: true });
2800
2887
  });
2801
2888
  };
2802
2889
 
@@ -2844,33 +2931,35 @@ class BncrBridgeRuntime {
2844
2931
  };
2845
2932
 
2846
2933
  channelSendText = async (ctx: any) => {
2934
+ await this.syncDebugFlag();
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) =>
@@ -2881,32 +2970,33 @@ class BncrBridgeRuntime {
2881
2970
  };
2882
2971
 
2883
2972
  channelSendMedia = async (ctx: any) => {
2973
+ await this.syncDebugFlag();
2884
2974
  const accountId = normalizeAccountId(ctx.accountId);
2885
2975
  const to = asString(ctx.to || '').trim();
2886
2976
  const asVoice = ctx?.asVoice === true;
2887
2977
  const audioAsVoice = ctx?.audioAsVoice === true;
2888
2978
 
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
- }
2979
+ this.logInfo(
2980
+ 'outbound',
2981
+ `send-entry:media ${JSON.stringify({
2982
+ accountId,
2983
+ to,
2984
+ text: asString(ctx?.text || ''),
2985
+ mediaUrl: asString(ctx?.mediaUrl || ''),
2986
+ mediaUrls: Array.isArray(ctx?.mediaUrls) ? ctx.mediaUrls : undefined,
2987
+ asVoice,
2988
+ audioAsVoice,
2989
+ sessionKey: asString(ctx?.sessionKey || ''),
2990
+ mirrorSessionKey: asString(ctx?.mirror?.sessionKey || ''),
2991
+ rawCtx: {
2992
+ to: ctx?.to,
2993
+ accountId: ctx?.accountId,
2994
+ threadId: ctx?.threadId,
2995
+ replyToId: ctx?.replyToId,
2996
+ },
2997
+ })}`,
2998
+ { debugOnly: true },
2999
+ );
2910
3000
 
2911
3001
  return sendBncrMedia({
2912
3002
  channelId: CHANNEL_ID,
@@ -2916,6 +3006,7 @@ class BncrBridgeRuntime {
2916
3006
  mediaUrl: asString(ctx.mediaUrl || ''),
2917
3007
  asVoice,
2918
3008
  audioAsVoice,
3009
+ replyToId: asString(ctx?.replyToId || ctx?.replyToMessageId || '').trim() || undefined,
2919
3010
  mediaLocalRoots: ctx.mediaLocalRoots,
2920
3011
  resolveVerifiedTarget: (to, accountId) => this.resolveVerifiedTarget(to, accountId),
2921
3012
  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,10 @@ 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(
124
+ 'warn',
125
+ `[bncr] inbound record native command session failed: ${String(err)}`,
126
+ );
123
127
  },
124
128
  });
125
129
 
@@ -155,6 +159,7 @@ export async function handleBncrNativeCommand(params: {
155
159
  payload: {
156
160
  ...payload,
157
161
  kind: kind as 'tool' | 'block' | 'final' | undefined,
162
+ replyToId: msgId || undefined,
158
163
  },
159
164
  });
160
165
  },
@@ -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
  });