@xmoxmo/bncr 0.5.6 → 0.5.7

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.
Files changed (73) hide show
  1. package/dist/index.js +1 -9
  2. package/index.ts +1 -9
  3. package/package.json +1 -1
  4. package/scripts/check-pack.mjs +0 -6
  5. package/scripts/selfcheck.mjs +0 -7
  6. package/src/channel.ts +79 -43
  7. package/src/core/accounts.ts +1 -6
  8. package/src/core/connection-reachability.ts +1 -5
  9. package/src/core/dead-letter-diagnostics.ts +4 -10
  10. package/src/core/outbox-entry-builders.ts +34 -18
  11. package/src/core/outbox-file-transfer-failure.ts +5 -0
  12. package/src/core/outbox-file-transfer-guards.ts +6 -0
  13. package/src/core/outbox-file-transfer-prep.ts +8 -5
  14. package/src/core/outbox-file-transfer-success.ts +5 -0
  15. package/src/core/outbox-queue.ts +1 -7
  16. package/src/core/outbox-summary.ts +15 -27
  17. package/src/core/outbox-text-push-prep.ts +1 -36
  18. package/src/core/permissions.ts +1 -9
  19. package/src/core/persisted-outbox-entry.ts +54 -11
  20. package/src/core/policy.ts +1 -11
  21. package/src/core/register-trace.ts +2 -5
  22. package/src/core/status-meta.ts +1 -9
  23. package/src/core/status.ts +1 -21
  24. package/src/core/targets.ts +52 -70
  25. package/src/core/types.ts +1 -17
  26. package/src/core/value-sanitize.ts +38 -1
  27. package/src/messaging/inbound/commands.ts +2 -24
  28. package/src/messaging/inbound/dispatch-prep.ts +1 -1
  29. package/src/messaging/inbound/gate.ts +2 -31
  30. package/src/messaging/inbound/group-history.ts +0 -75
  31. package/src/messaging/inbound/parse.ts +1 -31
  32. package/src/messaging/inbound/scene-admin.ts +8 -8
  33. package/src/messaging/outbound/diagnostics.ts +1 -6
  34. package/src/messaging/outbound/durable-queue-adapter.ts +17 -11
  35. package/src/messaging/outbound/marker-parser.ts +62 -28
  36. package/src/messaging/outbound/media-dedupe.ts +2 -6
  37. package/src/messaging/outbound/media.ts +1 -6
  38. package/src/messaging/outbound/normalize-outbound-send.ts +327 -0
  39. package/src/messaging/outbound/queue-selectors.ts +1 -5
  40. package/src/messaging/outbound/reply-enqueue.ts +121 -70
  41. package/src/messaging/outbound/session-route.ts +6 -11
  42. package/src/messaging/outbound/target-resolver.ts +1 -6
  43. package/src/openclaw/routing-runtime.ts +0 -11
  44. package/src/openclaw/sdk-helpers.ts +0 -2
  45. package/src/plugin/bridge-file-transfer-push-facade.ts +6 -6
  46. package/src/plugin/bridge-media-facade.ts +0 -1
  47. package/src/plugin/channel-components.ts +0 -13
  48. package/src/plugin/channel-plugin-surface-group.ts +52 -65
  49. package/src/plugin/channel-runtime-builders-delivery.ts +1 -0
  50. package/src/plugin/channel-runtime-types.ts +2 -24
  51. package/src/plugin/channel-send.ts +205 -221
  52. package/src/plugin/channel-utils.ts +8 -29
  53. package/src/plugin/connection-handlers-helpers.ts +0 -15
  54. package/src/plugin/connection-handlers.ts +6 -5
  55. package/src/plugin/file-inbound-abort.ts +5 -0
  56. package/src/plugin/file-inbound-chunk.ts +5 -0
  57. package/src/plugin/file-inbound-complete.ts +5 -0
  58. package/src/plugin/media-dedupe-runtime.ts +1 -6
  59. package/src/plugin/message-send.ts +7 -8
  60. package/src/plugin/outbound.ts +6 -26
  61. package/src/plugin/outbox-drain-loop.ts +2 -2
  62. package/src/plugin/outbox-file-push-flow.ts +7 -7
  63. package/src/plugin/outbox-text-push-flow.ts +1 -1
  64. package/src/plugin/target-runtime.ts +1 -6
  65. package/src/runtime/outbound-ack-timeout.ts +1 -8
  66. package/src/runtime/outbox-transitions.ts +1 -4
  67. package/src/runtime/status-worker.ts +1 -9
  68. package/src/messaging/outbound/build-send-action.ts +0 -130
  69. package/src/messaging/outbound/durable-message-adapter.ts +0 -121
  70. package/src/messaging/outbound/send-params.ts +0 -114
  71. package/src/messaging/outbound/send.ts +0 -127
  72. package/src/openclaw/ingress-runtime.ts +0 -25
  73. package/src/plugin/channel-send-runtime-group.ts +0 -37
package/dist/index.js CHANGED
@@ -1,6 +1,3 @@
1
- // index.ts
2
- import path3 from "node:path";
3
-
4
1
  // src/bootstrap/channel-plugin-runtime.ts
5
2
  function createDynamicChannelPlugin(args) {
6
3
  const { loaded, getCurrentBridge: getCurrentBridge2 } = args;
@@ -786,12 +783,7 @@ var BncrConfigSchema = {
786
783
  };
787
784
 
788
785
  // index.ts
789
- var readPluginVersion2 = (rootDir = pluginRoot) => {
790
- const packageJsonPath = path3.join(rootDir, "package.json");
791
- void packageJsonPath;
792
- return pluginVersion;
793
- };
794
- var pluginVersion2 = readPluginVersion2();
786
+ var pluginVersion2 = pluginVersion;
795
787
  var registerRuntime = createBncrRegisterRuntime();
796
788
  var {
797
789
  ensureGatewayMethodRegistered,
package/index.ts CHANGED
@@ -1,4 +1,3 @@
1
- import path from 'node:path';
2
1
  import { createDynamicChannelPlugin } from './src/bootstrap/channel-plugin-runtime.ts';
3
2
  import {
4
3
  type BncrRegistrationApi,
@@ -10,7 +9,6 @@ import {
10
9
  type ChannelModule,
11
10
  type LoadedRuntime,
12
11
  loadBncrRuntimeSync,
13
- pluginRoot,
14
12
  pluginVersion as runtimePluginVersion,
15
13
  } from './src/bootstrap/runtime-loader.ts';
16
14
  import { BncrConfigSchema } from './src/core/config-schema.ts';
@@ -19,13 +17,7 @@ import { getOpenClawRuntimeConfig } from './src/openclaw/config-runtime.ts';
19
17
 
20
18
  type ChannelPlugin = ReturnType<ChannelModule['createBncrChannelPlugin']>;
21
19
 
22
- const readPluginVersion = (rootDir = pluginRoot) => {
23
- const packageJsonPath = path.join(rootDir, 'package.json');
24
- void packageJsonPath;
25
- return runtimePluginVersion;
26
- };
27
-
28
- const pluginVersion = readPluginVersion();
20
+ const pluginVersion = runtimePluginVersion;
29
21
 
30
22
  const registerRuntime = createBncrRegisterRuntime();
31
23
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmoxmo/bncr",
3
- "version": "0.5.6",
3
+ "version": "0.5.7",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -76,10 +76,8 @@ const requiredSourceFiles = [
76
76
  'src/messaging/inbound/session-label.ts',
77
77
  'src/messaging/inbound/turn-context.ts',
78
78
  'src/messaging/outbound/actions.ts',
79
- 'src/messaging/outbound/build-send-action.ts',
80
79
  'src/messaging/outbound/diagnostics.ts',
81
80
  'src/messaging/outbound/diagnostics-debug-builders.ts',
82
- 'src/messaging/outbound/durable-message-adapter.ts',
83
81
  'src/messaging/outbound/durable-queue-adapter.ts',
84
82
  'src/messaging/outbound/media-dedupe.ts',
85
83
  'src/messaging/outbound/media.ts',
@@ -88,13 +86,10 @@ const requiredSourceFiles = [
88
86
  'src/messaging/outbound/reply-enqueue.ts',
89
87
  'src/messaging/outbound/reply-target-policy.ts',
90
88
  'src/messaging/outbound/retry-policy.ts',
91
- 'src/messaging/outbound/send-params.ts',
92
- 'src/messaging/outbound/send.ts',
93
89
  'src/messaging/outbound/session-route.ts',
94
90
  'src/messaging/outbound/target-resolver.ts',
95
91
  'src/openclaw/config-runtime.ts',
96
92
  'src/openclaw/inbound-session-runtime.ts',
97
- 'src/openclaw/ingress-runtime.ts',
98
93
  'src/openclaw/media-runtime.ts',
99
94
  'src/openclaw/reply-runtime.ts',
100
95
  'src/openclaw/routing-runtime.ts',
@@ -111,7 +106,6 @@ const requiredSourceFiles = [
111
106
  'src/plugin/channel-plugin-surface-group.ts',
112
107
  'src/plugin/channel-runtime-constants.ts',
113
108
  'src/plugin/channel-runtime-builders.ts',
114
- 'src/plugin/channel-send-runtime-group.ts',
115
109
  'src/plugin/channel-runtime-types.ts',
116
110
  'src/plugin/channel-send.ts',
117
111
  'src/plugin/channel-utils.ts',
@@ -72,10 +72,8 @@ const requiredSourceFiles = [
72
72
  'src/messaging/inbound/session-label.ts',
73
73
  'src/messaging/inbound/turn-context.ts',
74
74
  'src/messaging/outbound/actions.ts',
75
- 'src/messaging/outbound/build-send-action.ts',
76
75
  'src/messaging/outbound/diagnostics.ts',
77
76
  'src/messaging/outbound/diagnostics-debug-builders.ts',
78
- 'src/messaging/outbound/durable-message-adapter.ts',
79
77
  'src/messaging/outbound/durable-queue-adapter.ts',
80
78
  'src/messaging/outbound/media-dedupe.ts',
81
79
  'src/messaging/outbound/media.ts',
@@ -84,13 +82,10 @@ const requiredSourceFiles = [
84
82
  'src/messaging/outbound/reply-enqueue.ts',
85
83
  'src/messaging/outbound/reply-target-policy.ts',
86
84
  'src/messaging/outbound/retry-policy.ts',
87
- 'src/messaging/outbound/send-params.ts',
88
- 'src/messaging/outbound/send.ts',
89
85
  'src/messaging/outbound/session-route.ts',
90
86
  'src/messaging/outbound/target-resolver.ts',
91
87
  'src/openclaw/config-runtime.ts',
92
88
  'src/openclaw/inbound-session-runtime.ts',
93
- 'src/openclaw/ingress-runtime.ts',
94
89
  'src/openclaw/media-runtime.ts',
95
90
  'src/openclaw/reply-runtime.ts',
96
91
  'src/openclaw/routing-runtime.ts',
@@ -107,7 +102,6 @@ const requiredSourceFiles = [
107
102
  'src/plugin/channel-plugin-surface-group.ts',
108
103
  'src/plugin/channel-runtime-constants.ts',
109
104
  'src/plugin/channel-runtime-builders.ts',
110
- 'src/plugin/channel-send-runtime-group.ts',
111
105
  'src/plugin/channel-runtime-types.ts',
112
106
  'src/plugin/channel-send.ts',
113
107
  'src/plugin/channel-utils.ts',
@@ -198,7 +192,6 @@ const readNpmLatestVersion = (packageName) => {
198
192
 
199
193
  const requiredOpenClawSdkSubpaths = [
200
194
  'openclaw/plugin-sdk',
201
- 'openclaw/plugin-sdk/boolean-param',
202
195
  'openclaw/plugin-sdk/channel-outbound',
203
196
  'openclaw/plugin-sdk/channel-ingress-runtime',
204
197
  'openclaw/plugin-sdk/channel-message',
package/src/channel.ts CHANGED
@@ -152,10 +152,9 @@ import type {
152
152
  BncrChannelConfigRoot,
153
153
  BncrChannelSendContext,
154
154
  BncrSceneRecord,
155
- FileAckPayloadState,
156
155
  PersistedState,
157
156
  } from './plugin/channel-runtime-types.ts';
158
- import { createBncrChannelSendRuntimeGroup } from './plugin/channel-send-runtime-group.ts';
157
+ import { createBncrChannelSendRuntime } from './plugin/channel-send.ts';
159
158
  import {
160
159
  asString,
161
160
  backoffMs,
@@ -168,6 +167,7 @@ import {
168
167
  } from './plugin/channel-utils.ts';
169
168
  import { BNCR_CONFIG_SURFACE } from './plugin/config.ts';
170
169
  import { createBncrConnectionStateRuntimeGroup } from './plugin/connection-state-runtime-group.ts';
170
+ import type { FileAckPayloadState } from './plugin/file-ack-runtime.ts';
171
171
  import { createBncrFileTransferRuntimeGroup } from './plugin/file-transfer-runtime-group.ts';
172
172
  import { BNCR_GATEWAY_METHODS } from './plugin/gateway-methods.ts';
173
173
  import { prepareBncrInboundAcceptance } from './plugin/inbound-acceptance.ts';
@@ -1133,7 +1133,7 @@ class BncrBridgeRuntime {
1133
1133
 
1134
1134
  private async pushFileTransferSuccessPath(args: {
1135
1135
  entry: OutboxEntry;
1136
- meta: Record<string, unknown>;
1136
+ msg: Record<string, unknown>;
1137
1137
  owner: ReturnType<BncrBridgeRuntime['resolveOutboxPushOwner']>;
1138
1138
  connIds: Iterable<string>;
1139
1139
  recentInboundReachable: boolean;
@@ -1157,11 +1157,11 @@ class BncrBridgeRuntime {
1157
1157
 
1158
1158
  private async tryPushFileTransferEntry(
1159
1159
  entry: OutboxEntry,
1160
- meta: Record<string, unknown>,
1160
+ msg: Record<string, unknown>,
1161
1161
  ): Promise<boolean> {
1162
1162
  return await runBncrFileTransferOutboxPush({
1163
1163
  entry,
1164
- meta,
1164
+ msg,
1165
1165
  gatewayContext: this.gatewayContext,
1166
1166
  owner: this.resolveOutboxPushOwner(entry.accountId),
1167
1167
  resolvePushConnIds: (accountId) => this.resolvePushConnIds(accountId),
@@ -1195,7 +1195,6 @@ class BncrBridgeRuntime {
1195
1195
  createMessageId: () => randomUUID(),
1196
1196
  now,
1197
1197
  normalizeAccountId,
1198
- pushEvent: BNCR_PUSH_EVENT,
1199
1198
  accountId: params.accountId,
1200
1199
  sessionKey: params.sessionKey,
1201
1200
  route: params.route,
@@ -1209,7 +1208,7 @@ class BncrBridgeRuntime {
1209
1208
  kind: params.kind,
1210
1209
  replyToId: asString(params.replyToId || '').trim() || undefined,
1211
1210
  replyTargetPolicy: params.replyTargetPolicy,
1212
- downloadMedia: params.downloadMedia === true ? true : undefined,
1211
+ downloadMedia: params.downloadMedia,
1213
1212
  });
1214
1213
  }
1215
1214
 
@@ -1243,29 +1242,55 @@ class BncrBridgeRuntime {
1243
1242
 
1244
1243
  private buildFileTransferOutboundFrame(params: {
1245
1244
  entry: OutboxEntry;
1246
- meta: Record<string, unknown>;
1245
+ msg: Record<string, unknown>;
1247
1246
  media: { fileName?: string; mimeType?: string; path?: string; base64?: string; type?: string };
1248
1247
  mediaUrl: string;
1249
1248
  }) {
1250
- const wantsVoice = params.meta.asVoice === true || params.meta.audioAsVoice === true;
1251
- const messageKind =
1252
- params.meta.messageKind === 'tool' ||
1253
- params.meta.messageKind === 'block' ||
1254
- params.meta.messageKind === 'final'
1255
- ? params.meta.messageKind
1249
+ const wantsVoice = params.msg.asVoice === true || params.msg.audioAsVoice === true;
1250
+ const rawKind = params.msg.kind ?? (params.msg as Record<string, unknown>).messageKind;
1251
+ const messageKind: 'tool' | 'block' | 'final' | undefined =
1252
+ rawKind === 'tool' || rawKind === 'block' || rawKind === 'final'
1253
+ ? (rawKind as 'tool' | 'block' | 'final')
1256
1254
  : undefined;
1257
1255
 
1258
1256
  const hasPayload = !!(params.media.base64 || params.media.path);
1259
- if (hasPayload) {
1260
- console.log(
1261
- '[bncr] outbound-media download=true|media=' +
1262
- (params.media.path || `base64:${(params.media.base64 || '').substring(0, 12)}...`) +
1263
- '|url=' +
1264
- params.mediaUrl.split('?')[0],
1265
- );
1266
- } else {
1267
- console.log(`[bncr] outbound-media pass-through|url=${params.mediaUrl.split('?')[0]}`);
1257
+ this.logInfoJson(
1258
+ 'outbound',
1259
+ 'media-frame',
1260
+ {
1261
+ mid: params.entry.messageId,
1262
+ mode: hasPayload ? (params.media.path ? 'chunk' : 'base64') : 'pass-through',
1263
+ type: wantsVoice ? 'voice' : asString(params.msg.type || '') || undefined,
1264
+ url: params.mediaUrl.split('?')[0],
1265
+ ...(params.media.path ? { path: params.media.path } : {}),
1266
+ ...(params.media.base64 ? { base64: true } : {}),
1267
+ },
1268
+ { debugOnly: true },
1269
+ );
1270
+ // Collect non-control fields from msg for passthrough into message.*
1271
+ const MSG_PASSTHROUGH_KEYS = new Set([
1272
+ 'platform',
1273
+ 'groupId',
1274
+ 'userId',
1275
+ 'mediaUrl',
1276
+ 'mediaLocalRoots',
1277
+ 'asVoice',
1278
+ 'audioAsVoice',
1279
+ 'downloadMedia',
1280
+ 'transferMode',
1281
+ 'path',
1282
+ 'base64',
1283
+ 'fileName',
1284
+ 'kind',
1285
+ 'replyToId',
1286
+ ]);
1287
+ const passthroughExtra: Record<string, unknown> = {};
1288
+ for (const [key, value] of Object.entries(params.msg)) {
1289
+ if (!MSG_PASSTHROUGH_KEYS.has(key) && value !== undefined) {
1290
+ passthroughExtra[key] = value;
1291
+ }
1268
1292
  }
1293
+
1269
1294
  return buildBncrMediaOutboundFrame({
1270
1295
  messageId: params.entry.messageId,
1271
1296
  sessionKey: params.entry.sessionKey,
@@ -1278,16 +1303,16 @@ class BncrBridgeRuntime {
1278
1303
  path: params.media.path,
1279
1304
  },
1280
1305
  mediaUrl: params.mediaUrl,
1281
- mediaMsg: asString(params.meta.text || ''),
1306
+ mediaMsg: asString(params.msg.msg ?? ''),
1282
1307
  fileName: resolveOutboundFileName({
1283
1308
  mediaUrl: params.mediaUrl,
1284
1309
  fileName: params.media.fileName,
1285
1310
  mimeType: params.media.mimeType,
1286
1311
  }),
1287
- hintedType: wantsVoice ? 'voice' : asString(params.meta.type || '') || undefined,
1288
- extra: params.meta.extra as Record<string, unknown> | undefined,
1312
+ hintedType: wantsVoice ? 'voice' : asString(params.msg.type || '') || undefined,
1313
+ extra: Object.keys(passthroughExtra).length > 0 ? passthroughExtra : undefined,
1289
1314
  kind: messageKind,
1290
- replyToId: normalizeReplyToId(params.meta.replyToId) || undefined,
1315
+ replyToId: normalizeReplyToId(asString(params.msg.replyToId || '')) || undefined,
1291
1316
  now: now(),
1292
1317
  });
1293
1318
  }
@@ -1305,13 +1330,11 @@ class BncrBridgeRuntime {
1305
1330
  kind?: 'tool' | 'block' | 'final';
1306
1331
  replyToId?: string;
1307
1332
  replyTargetPolicy?: OutboundReplyTargetPolicy;
1308
- downloadMedia?: boolean;
1309
1333
  }): OutboxEntry {
1310
1334
  return buildTextOutboxEntryFromRuntime({
1311
1335
  createMessageId: () => randomUUID(),
1312
1336
  now,
1313
1337
  normalizeAccountId,
1314
- normalizeReplyToId,
1315
1338
  accountId: params.accountId,
1316
1339
  sessionKey: params.sessionKey,
1317
1340
  route: params.route,
@@ -1320,16 +1343,18 @@ class BncrBridgeRuntime {
1320
1343
  kind: params.kind,
1321
1344
  replyToId: params.replyToId,
1322
1345
  replyTargetPolicy: params.replyTargetPolicy,
1323
- downloadMedia: params.downloadMedia === true ? true : undefined,
1324
1346
  });
1325
1347
  }
1326
1348
 
1327
1349
  private async tryPushEntry(entry: OutboxEntry): Promise<boolean> {
1328
- const meta = isPlainObject(entry.payload?._meta) ? entry.payload._meta : null;
1329
- if (meta?.kind === 'file-transfer') {
1330
- return this.tryPushFileTransferEntry(entry, meta);
1350
+ const msg =
1351
+ isPlainObject(entry.payload) &&
1352
+ isPlainObject((entry.payload as Record<string, unknown>).message)
1353
+ ? ((entry.payload as Record<string, unknown>).message as Record<string, unknown>)
1354
+ : null;
1355
+ if (msg?.transferMode === 'media') {
1356
+ return this.tryPushFileTransferEntry(entry, msg);
1331
1357
  }
1332
-
1333
1358
  return this.tryPushTextEntry(entry);
1334
1359
  }
1335
1360
 
@@ -1753,8 +1778,12 @@ class BncrBridgeRuntime {
1753
1778
  clampFiniteNumber(value, fallback, min ?? fallback, max ?? fallback),
1754
1779
  normalizeAccountId,
1755
1780
  formatDisplayScope,
1756
- isFileTransferEntry: (entry) =>
1757
- isPlainObject(entry.payload?._meta) && entry.payload?._meta?.kind === 'file-transfer',
1781
+ isFileTransferEntry: (entry) => {
1782
+ const msg = isPlainObject(entry.payload)
1783
+ ? (entry.payload as Record<string, unknown>).message
1784
+ : null;
1785
+ return isPlainObject(msg) && (msg as Record<string, unknown>).transferMode === 'media';
1786
+ },
1758
1787
  recommendedAckTimeoutMaxMs: RECOMMENDED_ACK_TIMEOUT_MAX_MS,
1759
1788
  adaptiveAckTimeoutEnabled: ADAPTIVE_ACK_TIMEOUT_DEFAULT_ENABLED,
1760
1789
  defaultAckTimeoutMs: PUSH_ACK_TIMEOUT_MS,
@@ -2937,6 +2966,18 @@ class BncrBridgeRuntime {
2937
2966
  logInfo: (scope, message, options) => this.logInfo(scope, message, options),
2938
2967
  ...targetRuntime,
2939
2968
  listOutboxEntries: () => Array.from(this.outbox.values()),
2969
+ resolveSceneDownloadMedia: (to: string) => {
2970
+ const parts = to.split(':');
2971
+ if (parts.length >= 3) {
2972
+ const key = `${parts[1]}:${parts[2]}`;
2973
+ const sc = this.sceneRegistry.get(key);
2974
+ // Scene boolean wins (including explicit false); else fall to global.
2975
+ if (typeof sc?.downloadMedia === 'boolean') return sc.downloadMedia;
2976
+ const g = this.sceneRegistry.get('__global__');
2977
+ if (typeof g?.downloadMedia === 'boolean') return g.downloadMedia;
2978
+ }
2979
+ return undefined;
2980
+ },
2940
2981
  });
2941
2982
  }
2942
2983
 
@@ -3021,20 +3062,15 @@ class BncrBridgeRuntime {
3021
3062
  if (ctx.forceDocument === true) extra.forceDocument = true;
3022
3063
  if (ctx.gifPlayback === true) extra.gifPlayback = true;
3023
3064
  if (ctx.silent === true) extra.silent = true;
3024
- // Clear original fields so resolveUnifiedOutboundExtra doesn't re-add and override marker values
3065
+ // Promote host flags into extra so marker params can override them via mergeMarkerAndHostFields
3025
3066
  return { ...ctx, extra, forceDocument: undefined, gifPlayback: undefined, silent: undefined };
3026
3067
  }
3027
3068
 
3028
3069
  // Channel send surface assembly ------------------------------------------
3029
- // This group backs the public channel send APIs and should stay adjacent to
3030
- // the final exposed channel* methods for quick top-down scanning.
3031
-
3032
- private readonly channelSendRuntimeGroup = createBncrChannelSendRuntimeGroup(
3070
+ private readonly channelSendRuntime = createBncrChannelSendRuntime(
3033
3071
  this.buildChannelSendRuntime(),
3034
3072
  );
3035
3073
 
3036
- private readonly channelSendRuntime = this.channelSendRuntimeGroup.channelSendRuntime;
3037
-
3038
3074
  channelSendText = async (ctx: BncrChannelSendContext) =>
3039
3075
  this.channelSendRuntime.channelSendText(this.mergeHostFields(ctx));
3040
3076
 
@@ -3,16 +3,11 @@ import type {
3
3
  BncrChannelConfigRoot,
4
4
  BncrChannelConfigSection,
5
5
  } from '../plugin/channel-runtime-types.ts';
6
+ import { asString } from './value-sanitize.ts';
6
7
 
7
8
  const CHANNEL_ID = 'bncr';
8
9
  const BNCR_DEFAULT_ACCOUNT_ID = 'Primary';
9
10
 
10
- function asString(v: unknown, fallback = ''): string {
11
- if (typeof v === 'string') return v;
12
- if (v == null) return fallback;
13
- return String(v);
14
- }
15
-
16
11
  export function normalizeAccountId(accountId?: string | null): string {
17
12
  const v = asString(accountId || '').trim();
18
13
  if (!v) return BNCR_DEFAULT_ACCOUNT_ID;
@@ -1,9 +1,5 @@
1
1
  import type { BncrConnection, OutboxEntry } from './types.ts';
2
-
3
- function finiteNumberOr(value: unknown, fallback: number): number {
4
- const n = Number(value);
5
- return Number.isFinite(n) ? n : fallback;
6
- }
2
+ import { finiteNumberOr } from './value-sanitize.ts';
7
3
 
8
4
  export function hasRecentInboundReachability(args: {
9
5
  now: number;
@@ -5,6 +5,7 @@ import type {
5
5
  BncrDeadLetterEntrySummary,
6
6
  OutboxEntry,
7
7
  } from './types.ts';
8
+ import { asString } from './value-sanitize.ts';
8
9
 
9
10
  export type DeadLetterTopReason = { reason: string; count: number };
10
11
 
@@ -15,12 +16,6 @@ export type BuildDeadLetterDiagnosticsOptions = {
15
16
  cappedAt: number;
16
17
  };
17
18
 
18
- function asString(value: unknown, fallback = ''): string {
19
- if (typeof value === 'string') return value;
20
- if (value == null) return fallback;
21
- return String(value);
22
- }
23
-
24
19
  function asPayloadMessage(payload: OutboxEntry['payload']): {
25
20
  msg?: string;
26
21
  type?: string;
@@ -87,15 +82,14 @@ export function parseDeadLetterOlderThan(raw: unknown): number | null {
87
82
  }
88
83
 
89
84
  export function summarizeDeadLetterEntry(entry: OutboxEntry) {
90
- const meta = entry.payload?._meta || {};
91
- const msg = asPayloadMessage(entry.payload);
92
- const text = asString(meta.text || msg.msg || '');
85
+ const msg = asPayloadMessage(entry.payload) || {};
86
+ const text = asString(msg.msg || '');
93
87
  return {
94
88
  messageId: entry.messageId,
95
89
  accountId: entry.accountId,
96
90
  sessionKey: entry.sessionKey,
97
91
  route: formatDisplayScope(entry.route),
98
- kind: asString(meta.kind || msg.type || 'message'),
92
+ kind: asString(msg.type || 'message'),
99
93
  createdAt: Number.isFinite(Number(entry.createdAt)) ? Number(entry.createdAt) : null,
100
94
  retryCount: Number.isFinite(Number(entry.retryCount)) ? Number(entry.retryCount) : 0,
101
95
  lastError: entry.lastError || null,
@@ -2,11 +2,20 @@ import type { OutboundReplyTargetPolicy } from '../messaging/outbound/reply-targ
2
2
  import { normalizeOutboundReplyToId } from '../messaging/outbound/reply-target-policy.ts';
3
3
  import type { BncrRoute, OutboxEntry } from './types.ts';
4
4
 
5
+ /** Strip empty/falsy values from extra so they don't override existing message defaults. */
6
+ function sanitizeExtraSpread(extra: Record<string, unknown>): Record<string, unknown> {
7
+ const out: Record<string, unknown> = {};
8
+ for (const [key, value] of Object.entries(extra)) {
9
+ if (value === '' || value === null || value === undefined) continue;
10
+ out[key] = value;
11
+ }
12
+ return out;
13
+ }
14
+
5
15
  export function buildFileTransferOutboxEntry(args: {
6
16
  createMessageId: () => string;
7
17
  now: () => number;
8
18
  normalizeAccountId: (accountId?: string | null) => string;
9
- pushEvent: string;
10
19
  accountId: string;
11
20
  sessionKey: string;
12
21
  route: BncrRoute;
@@ -31,25 +40,34 @@ export function buildFileTransferOutboxEntry(args: {
31
40
  route: args.route,
32
41
  payload: {
33
42
  type: 'message.outbound',
43
+ messageId,
44
+ idempotencyKey: messageId,
34
45
  sessionKey: args.sessionKey,
35
- _meta: {
36
- kind: 'file-transfer',
46
+ replyToId:
47
+ normalizeOutboundReplyToId({
48
+ kind: args.kind,
49
+ replyToId: args.replyToId,
50
+ replyTargetPolicy: args.replyTargetPolicy,
51
+ }) || undefined,
52
+ message: {
53
+ platform: args.route.platform,
54
+ groupId: args.route.groupId,
55
+ userId: args.route.userId,
56
+ type: args.type,
57
+ // No fallback: when type is not provided the downstream adapter
58
+ // (tgBot / GewePlus) infers from mediaUrl extension.
59
+ kind: args.kind,
60
+ msg: args.text,
37
61
  mediaUrl: args.mediaUrl,
38
62
  mediaLocalRoots: args.mediaLocalRoots ? Array.from(args.mediaLocalRoots) : undefined,
39
- text: args.text,
40
63
  asVoice: args.asVoice === true,
41
64
  audioAsVoice: args.audioAsVoice === true,
42
- type: args.type,
43
- downloadMedia: args.downloadMedia === true,
44
- ...(args.extra ? { extra: { ...args.extra } } : {}),
45
- finalEvent: args.pushEvent,
46
- replyToId:
47
- normalizeOutboundReplyToId({
48
- kind: args.kind,
49
- replyToId: args.replyToId,
50
- replyTargetPolicy: args.replyTargetPolicy,
51
- }) || undefined,
52
- messageKind: args.kind,
65
+ downloadMedia: args.downloadMedia,
66
+ transferMode: 'media',
67
+ path: '',
68
+ base64: '',
69
+ fileName: '',
70
+ ...(args.extra ? sanitizeExtraSpread(args.extra) : {}),
53
71
  },
54
72
  },
55
73
  createdAt,
@@ -62,7 +80,6 @@ export function buildTextOutboxEntry(args: {
62
80
  createMessageId: () => string;
63
81
  now: () => number;
64
82
  normalizeAccountId: (accountId?: string | null) => string;
65
- normalizeReplyToId: (value?: string | null) => string;
66
83
  accountId: string;
67
84
  extra?: Record<string, unknown>;
68
85
  sessionKey: string;
@@ -71,7 +88,6 @@ export function buildTextOutboxEntry(args: {
71
88
  kind?: 'tool' | 'block' | 'final';
72
89
  replyToId?: string;
73
90
  replyTargetPolicy?: OutboundReplyTargetPolicy;
74
- downloadMedia?: boolean;
75
91
  }): OutboxEntry {
76
92
  const messageId = args.createMessageId();
77
93
  const createdAt = args.now();
@@ -98,7 +114,7 @@ export function buildTextOutboxEntry(args: {
98
114
  fileName: '',
99
115
  // Extra fields from marker/pipeline spread for adapter-specific data
100
116
  // (e.g. type="appmsg", msg="<appmsg>..." from [BncrParam:...])
101
- ...(args.extra ? { ...args.extra } : {}),
117
+ ...(args.extra ? sanitizeExtraSpread(args.extra) : {}),
102
118
  },
103
119
  ts: createdAt,
104
120
  };
@@ -1,3 +1,8 @@
1
+ /*
2
+ * Intentionally separate from outbox-text-push-failure.ts.
3
+ * File transfer failure adds retryable-state resolution logic
4
+ * that text push doesn't need (no chunk-level retry semantics).
5
+ */
1
6
  import { buildPushFailureArgs } from './outbox-push-args.ts';
2
7
  import type { OutboxEntry } from './types.ts';
3
8
 
@@ -1,3 +1,9 @@
1
+ /*
2
+ * Intentionally separate from outbox-text-push-guards.ts.
3
+ * File transfer guard has distinct output type (includes owner/mediaUrl)
4
+ * and extra mediaUrl validation that text push doesn't need.
5
+ * Merging would add unnecessary conditional branches.
6
+ */
1
7
  import type { BncrConnection, OutboxEntry } from './types.ts';
2
8
 
3
9
  export type FileTransferGuardResult =
@@ -1,26 +1,29 @@
1
1
  import type { BncrConnection, OutboxEntry } from './types.ts';
2
2
 
3
- export function prepareFileTransferRouteSelection(args: {
3
+ type PushRouteSelectionArgs<C> = {
4
4
  entry: OutboxEntry;
5
5
  owner: BncrConnection | null;
6
6
  resolvePushConnIds: (accountId: string) => Iterable<string>;
7
7
  resolveRecentInboundConnIds: (accountId: string) => Iterable<string>;
8
8
  hasRecentInboundReachability: (accountId: string) => boolean;
9
9
  isRevalidatedAttemptedConn: (connId: string) => boolean;
10
- selectOutboxFileTransferRouteCandidates: (args: {
10
+ selectRouteCandidates: (args: {
11
11
  routeCandidates: Iterable<string>;
12
12
  attemptedConnIds: string[];
13
13
  recentInboundConnIds: Iterable<string>;
14
14
  ownerConnId?: string;
15
15
  recentInboundReachable: boolean;
16
16
  isRevalidatedAttemptedConn: (connId: string) => boolean;
17
- }) => { connIds: Iterable<string>; recentInboundReachable: boolean; routeReason: string };
18
- }) {
17
+ }) => C;
18
+ };
19
+
20
+ /** Shared route selection prep for both text and file push flows. */
21
+ export function prepareRouteSelection<C>(args: PushRouteSelectionArgs<C>): C {
19
22
  const attemptedConnIds = Array.isArray(args.entry.routeAttemptConnIds)
20
23
  ? args.entry.routeAttemptConnIds.filter((v): v is string => typeof v === 'string' && !!v)
21
24
  : [];
22
25
 
23
- return args.selectOutboxFileTransferRouteCandidates({
26
+ return args.selectRouteCandidates({
24
27
  routeCandidates: args.resolvePushConnIds(args.entry.accountId),
25
28
  attemptedConnIds,
26
29
  recentInboundConnIds: args.resolveRecentInboundConnIds(args.entry.accountId),
@@ -1,3 +1,8 @@
1
+ /*
2
+ * Intentionally separate from outbox-text-push-success.ts.
3
+ * File transfer success uses kind:'file-transfer' tag and different
4
+ * argument shapes (owner vs connId). Separate callers, separate files.
5
+ */
1
6
  import { buildPushBroadcastPayload, buildPushRouteSelectArgs } from './outbox-push-args.ts';
2
7
  import type { BncrConnection, OutboxEntry } from './types.ts';
3
8
 
@@ -50,13 +50,7 @@ export function collectDueOutboxEntries(args: {
50
50
  nextAttemptAt: args.now + args.backoffMs(nextAttempt),
51
51
  };
52
52
  updatedEntries.push(updatedEntry);
53
- duePayloads.push({
54
- ...updatedEntry.payload,
55
- _meta: {
56
- retryCount: updatedEntry.retryCount,
57
- nextAttemptAt: updatedEntry.nextAttemptAt,
58
- },
59
- });
53
+ duePayloads.push(updatedEntry.payload);
60
54
 
61
55
  if (duePayloads.length >= args.maxBatch) break;
62
56
  }