@xmoxmo/bncr 0.5.5 → 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 (78) 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 +114 -37
  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 +38 -15
  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/native-command.ts +7 -3
  32. package/src/messaging/inbound/parse.ts +1 -31
  33. package/src/messaging/inbound/scene-admin.ts +181 -5
  34. package/src/messaging/outbound/diagnostics.ts +1 -6
  35. package/src/messaging/outbound/durable-queue-adapter.ts +17 -11
  36. package/src/messaging/outbound/marker-parser.ts +188 -0
  37. package/src/messaging/outbound/media-dedupe.ts +2 -6
  38. package/src/messaging/outbound/media.ts +6 -20
  39. package/src/messaging/outbound/normalize-outbound-send.ts +327 -0
  40. package/src/messaging/outbound/queue-selectors.ts +1 -5
  41. package/src/messaging/outbound/reply-enqueue-media.ts +4 -0
  42. package/src/messaging/outbound/reply-enqueue.ts +130 -57
  43. package/src/messaging/outbound/session-route.ts +6 -11
  44. package/src/messaging/outbound/target-resolver.ts +1 -6
  45. package/src/openclaw/routing-runtime.ts +0 -11
  46. package/src/openclaw/sdk-helpers.ts +0 -2
  47. package/src/plugin/bridge-file-transfer-push-facade.ts +8 -6
  48. package/src/plugin/bridge-media-facade.ts +1 -1
  49. package/src/plugin/channel-components.ts +0 -13
  50. package/src/plugin/channel-plugin-surface-group.ts +57 -40
  51. package/src/plugin/channel-runtime-builders-delivery.ts +3 -0
  52. package/src/plugin/channel-runtime-types.ts +10 -24
  53. package/src/plugin/channel-send.ts +241 -154
  54. package/src/plugin/channel-utils.ts +8 -29
  55. package/src/plugin/connection-handlers-helpers.ts +0 -15
  56. package/src/plugin/connection-handlers.ts +6 -5
  57. package/src/plugin/file-inbound-abort.ts +5 -0
  58. package/src/plugin/file-inbound-chunk.ts +5 -0
  59. package/src/plugin/file-inbound-complete.ts +5 -0
  60. package/src/plugin/file-transfer-orchestrator.ts +105 -3
  61. package/src/plugin/file-transfer-setup.ts +1 -1
  62. package/src/plugin/media-dedupe-runtime.ts +1 -6
  63. package/src/plugin/media-orchestrators-runtime-group.ts +2 -0
  64. package/src/plugin/message-send.ts +7 -8
  65. package/src/plugin/outbound.ts +6 -22
  66. package/src/plugin/outbox-drain-loop.ts +2 -2
  67. package/src/plugin/outbox-file-push-flow.ts +7 -7
  68. package/src/plugin/outbox-text-push-flow.ts +1 -1
  69. package/src/plugin/target-runtime.ts +1 -6
  70. package/src/runtime/outbound-ack-timeout.ts +1 -8
  71. package/src/runtime/outbox-transitions.ts +1 -4
  72. package/src/runtime/status-worker.ts +1 -9
  73. package/src/messaging/outbound/build-send-action.ts +0 -130
  74. package/src/messaging/outbound/durable-message-adapter.ts +0 -121
  75. package/src/messaging/outbound/send-params.ts +0 -81
  76. package/src/messaging/outbound/send.ts +0 -121
  77. package/src/openclaw/ingress-runtime.ts +0 -25
  78. 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.5",
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';
@@ -288,6 +288,9 @@ class BncrBridgeRuntime {
288
288
  { accountId: string; route: BncrRoute; updatedAt: number }
289
289
  >();
290
290
  private sceneRegistry = new Map<string, BncrSceneRecord>();
291
+ getSceneRegistry(): Map<string, BncrSceneRecord> {
292
+ return this.sceneRegistry;
293
+ }
291
294
  private groupHistories: BncrGroupHistoryMap = new Map();
292
295
  private routeAliases = new Map<
293
296
  string,
@@ -1130,7 +1133,7 @@ class BncrBridgeRuntime {
1130
1133
 
1131
1134
  private async pushFileTransferSuccessPath(args: {
1132
1135
  entry: OutboxEntry;
1133
- meta: Record<string, unknown>;
1136
+ msg: Record<string, unknown>;
1134
1137
  owner: ReturnType<BncrBridgeRuntime['resolveOutboxPushOwner']>;
1135
1138
  connIds: Iterable<string>;
1136
1139
  recentInboundReachable: boolean;
@@ -1154,11 +1157,11 @@ class BncrBridgeRuntime {
1154
1157
 
1155
1158
  private async tryPushFileTransferEntry(
1156
1159
  entry: OutboxEntry,
1157
- meta: Record<string, unknown>,
1160
+ msg: Record<string, unknown>,
1158
1161
  ): Promise<boolean> {
1159
1162
  return await runBncrFileTransferOutboxPush({
1160
1163
  entry,
1161
- meta,
1164
+ msg,
1162
1165
  gatewayContext: this.gatewayContext,
1163
1166
  owner: this.resolveOutboxPushOwner(entry.accountId),
1164
1167
  resolvePushConnIds: (accountId) => this.resolvePushConnIds(accountId),
@@ -1186,12 +1189,12 @@ class BncrBridgeRuntime {
1186
1189
  kind?: 'tool' | 'block' | 'final';
1187
1190
  replyToId?: string;
1188
1191
  replyTargetPolicy?: OutboundReplyTargetPolicy;
1192
+ downloadMedia?: boolean;
1189
1193
  }): OutboxEntry {
1190
1194
  return buildFileTransferOutboxEntryFromRuntime({
1191
1195
  createMessageId: () => randomUUID(),
1192
1196
  now,
1193
1197
  normalizeAccountId,
1194
- pushEvent: BNCR_PUSH_EVENT,
1195
1198
  accountId: params.accountId,
1196
1199
  sessionKey: params.sessionKey,
1197
1200
  route: params.route,
@@ -1205,6 +1208,7 @@ class BncrBridgeRuntime {
1205
1208
  kind: params.kind,
1206
1209
  replyToId: asString(params.replyToId || '').trim() || undefined,
1207
1210
  replyTargetPolicy: params.replyTargetPolicy,
1211
+ downloadMedia: params.downloadMedia,
1208
1212
  });
1209
1213
  }
1210
1214
 
@@ -1222,6 +1226,8 @@ class BncrBridgeRuntime {
1222
1226
  this.mediaDedupeRuntime.rememberRecentMediaSend(params);
1223
1227
  }
1224
1228
 
1229
+ private HTTP_URL_RE = /^https?:\/\//i;
1230
+
1225
1231
  private tryBuildMediaDedupeFallback(params: {
1226
1232
  sessionKey: string;
1227
1233
  mediaUrl: string;
@@ -1229,23 +1235,62 @@ class BncrBridgeRuntime {
1229
1235
  replyToId: string;
1230
1236
  currentTime?: number;
1231
1237
  }): { text: string; reason: 'same-text-sent-checkmark' | 'text-changed-downgrade' } | null {
1238
+ // Only deduplicate local file paths - HTTP URLs bypass dedup
1239
+ if (this.HTTP_URL_RE.test(params.mediaUrl)) return null;
1232
1240
  return this.mediaDedupeRuntime.tryBuildMediaDedupeFallback(params);
1233
1241
  }
1234
1242
 
1235
1243
  private buildFileTransferOutboundFrame(params: {
1236
1244
  entry: OutboxEntry;
1237
- meta: Record<string, unknown>;
1245
+ msg: Record<string, unknown>;
1238
1246
  media: { fileName?: string; mimeType?: string; path?: string; base64?: string; type?: string };
1239
1247
  mediaUrl: string;
1240
1248
  }) {
1241
- const wantsVoice = params.meta.asVoice === true || params.meta.audioAsVoice === true;
1242
- const messageKind =
1243
- params.meta.messageKind === 'tool' ||
1244
- params.meta.messageKind === 'block' ||
1245
- params.meta.messageKind === 'final'
1246
- ? 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')
1247
1254
  : undefined;
1248
1255
 
1256
+ const hasPayload = !!(params.media.base64 || params.media.path);
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
+ }
1292
+ }
1293
+
1249
1294
  return buildBncrMediaOutboundFrame({
1250
1295
  messageId: params.entry.messageId,
1251
1296
  sessionKey: params.entry.sessionKey,
@@ -1254,20 +1299,20 @@ class BncrBridgeRuntime {
1254
1299
  mode: params.media.path ? 'chunk' : 'base64',
1255
1300
  mimeType: params.media.mimeType,
1256
1301
  fileName: params.media.fileName,
1257
- mediaBase64: params.media.base64,
1302
+ base64: params.media.base64,
1258
1303
  path: params.media.path,
1259
1304
  },
1260
1305
  mediaUrl: params.mediaUrl,
1261
- mediaMsg: asString(params.meta.text || ''),
1306
+ mediaMsg: asString(params.msg.msg ?? ''),
1262
1307
  fileName: resolveOutboundFileName({
1263
1308
  mediaUrl: params.mediaUrl,
1264
1309
  fileName: params.media.fileName,
1265
1310
  mimeType: params.media.mimeType,
1266
1311
  }),
1267
- hintedType: wantsVoice ? 'voice' : asString(params.meta.type || '') || undefined,
1268
- 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,
1269
1314
  kind: messageKind,
1270
- replyToId: normalizeReplyToId(params.meta.replyToId) || undefined,
1315
+ replyToId: normalizeReplyToId(asString(params.msg.replyToId || '')) || undefined,
1271
1316
  now: now(),
1272
1317
  });
1273
1318
  }
@@ -1281,6 +1326,7 @@ class BncrBridgeRuntime {
1281
1326
  sessionKey: string;
1282
1327
  route: BncrRoute;
1283
1328
  text: string;
1329
+ extra?: Record<string, unknown>;
1284
1330
  kind?: 'tool' | 'block' | 'final';
1285
1331
  replyToId?: string;
1286
1332
  replyTargetPolicy?: OutboundReplyTargetPolicy;
@@ -1289,11 +1335,11 @@ class BncrBridgeRuntime {
1289
1335
  createMessageId: () => randomUUID(),
1290
1336
  now,
1291
1337
  normalizeAccountId,
1292
- normalizeReplyToId,
1293
1338
  accountId: params.accountId,
1294
1339
  sessionKey: params.sessionKey,
1295
1340
  route: params.route,
1296
1341
  text: params.text,
1342
+ extra: params.extra && Object.keys(params.extra).length > 0 ? { ...params.extra } : undefined,
1297
1343
  kind: params.kind,
1298
1344
  replyToId: params.replyToId,
1299
1345
  replyTargetPolicy: params.replyTargetPolicy,
@@ -1301,11 +1347,14 @@ class BncrBridgeRuntime {
1301
1347
  }
1302
1348
 
1303
1349
  private async tryPushEntry(entry: OutboxEntry): Promise<boolean> {
1304
- const meta = isPlainObject(entry.payload?._meta) ? entry.payload._meta : null;
1305
- if (meta?.kind === 'file-transfer') {
1306
- 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);
1307
1357
  }
1308
-
1309
1358
  return this.tryPushTextEntry(entry);
1310
1359
  }
1311
1360
 
@@ -1729,8 +1778,12 @@ class BncrBridgeRuntime {
1729
1778
  clampFiniteNumber(value, fallback, min ?? fallback, max ?? fallback),
1730
1779
  normalizeAccountId,
1731
1780
  formatDisplayScope,
1732
- isFileTransferEntry: (entry) =>
1733
- 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
+ },
1734
1787
  recommendedAckTimeoutMaxMs: RECOMMENDED_ACK_TIMEOUT_MAX_MS,
1735
1788
  adaptiveAckTimeoutEnabled: ADAPTIVE_ACK_TIMEOUT_DEFAULT_ENABLED,
1736
1789
  defaultAckTimeoutMs: PUSH_ACK_TIMEOUT_MS,
@@ -2722,7 +2775,7 @@ class BncrBridgeRuntime {
2722
2775
  mode: 'base64' | 'chunk';
2723
2776
  mimeType?: string;
2724
2777
  fileName?: string;
2725
- mediaBase64?: string;
2778
+ base64?: string;
2726
2779
  path?: string;
2727
2780
  }> {
2728
2781
  return this.bridgeMediaFacade.transferMediaToBncrClient(params);
@@ -2913,6 +2966,18 @@ class BncrBridgeRuntime {
2913
2966
  logInfo: (scope, message, options) => this.logInfo(scope, message, options),
2914
2967
  ...targetRuntime,
2915
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
+ },
2916
2981
  });
2917
2982
  }
2918
2983
 
@@ -2985,30 +3050,41 @@ class BncrBridgeRuntime {
2985
3050
  await stopBncrStatusWorker(this.buildStatusWorkerRuntime(), ctx);
2986
3051
  };
2987
3052
 
2988
- // Channel send surface assembly ------------------------------------------
2989
- // This group backs the public channel send APIs and should stay adjacent to
2990
- // the final exposed channel* methods for quick top-down scanning.
3053
+ /**
3054
+ * Merge host-level outbound fields (forceDocument, gifPlayback, silent)
3055
+ * into extra before dispatching to channel-send runtime.
3056
+ */
3057
+ private mergeHostFields(ctx: BncrChannelSendContext): BncrChannelSendContext {
3058
+ if (!ctx.forceDocument && !ctx.gifPlayback && !ctx.silent) {
3059
+ return ctx;
3060
+ }
3061
+ const extra = { ...(typeof ctx.extra === 'object' && ctx.extra !== null ? ctx.extra : {}) };
3062
+ if (ctx.forceDocument === true) extra.forceDocument = true;
3063
+ if (ctx.gifPlayback === true) extra.gifPlayback = true;
3064
+ if (ctx.silent === true) extra.silent = true;
3065
+ // Promote host flags into extra so marker params can override them via mergeMarkerAndHostFields
3066
+ return { ...ctx, extra, forceDocument: undefined, gifPlayback: undefined, silent: undefined };
3067
+ }
2991
3068
 
2992
- private readonly channelSendRuntimeGroup = createBncrChannelSendRuntimeGroup(
3069
+ // Channel send surface assembly ------------------------------------------
3070
+ private readonly channelSendRuntime = createBncrChannelSendRuntime(
2993
3071
  this.buildChannelSendRuntime(),
2994
3072
  );
2995
3073
 
2996
- private readonly channelSendRuntime = this.channelSendRuntimeGroup.channelSendRuntime;
2997
-
2998
3074
  channelSendText = async (ctx: BncrChannelSendContext) =>
2999
- this.channelSendRuntime.channelSendText(ctx);
3075
+ this.channelSendRuntime.channelSendText(this.mergeHostFields(ctx));
3000
3076
 
3001
3077
  channelSendMedia = async (ctx: BncrChannelSendContext) =>
3002
- this.channelSendRuntime.channelSendMedia(ctx);
3078
+ this.channelSendRuntime.channelSendMedia(this.mergeHostFields(ctx));
3003
3079
 
3004
3080
  channelMessageSendText = async (ctx: BncrChannelSendContext) =>
3005
- this.channelSendRuntime.channelMessageSendText(ctx);
3081
+ this.channelSendRuntime.channelMessageSendText(this.mergeHostFields(ctx));
3006
3082
 
3007
3083
  channelMessageSendMedia = async (ctx: BncrChannelSendContext) =>
3008
- this.channelSendRuntime.channelMessageSendMedia(ctx);
3084
+ this.channelSendRuntime.channelMessageSendMedia(this.mergeHostFields(ctx));
3009
3085
 
3010
3086
  channelMessageSendPayload = async (ctx: BncrChannelSendContext) =>
3011
- this.channelSendRuntime.channelMessageSendPayload(ctx);
3087
+ this.channelSendRuntime.channelMessageSendPayload(this.mergeHostFields(ctx));
3012
3088
  }
3013
3089
 
3014
3090
  // Plugin surface export -----------------------------------------------------
@@ -3029,6 +3105,7 @@ export function createBncrChannelPlugin(getBridge: () => BncrBridgeRuntime) {
3029
3105
 
3030
3106
  return createBncrChannelPluginSurfaceGroup({
3031
3107
  channelId: CHANNEL_ID,
3108
+ sceneRegistry: getBridge().getSceneRegistry(),
3032
3109
  getMessageSendBridge: bridgeGroup.getMessageSendBridge,
3033
3110
  getOutboundBridge: bridgeGroup.getOutboundBridge,
3034
3111
  getMessagingBridge: bridgeGroup.getMessagingBridge,
@@ -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;
@@ -20,6 +29,7 @@ export function buildFileTransferOutboxEntry(args: {
20
29
  kind?: 'tool' | 'block' | 'final';
21
30
  replyToId?: string;
22
31
  replyTargetPolicy?: OutboundReplyTargetPolicy;
32
+ downloadMedia?: boolean;
23
33
  }): OutboxEntry {
24
34
  const messageId = args.createMessageId();
25
35
  const createdAt = args.now();
@@ -30,24 +40,34 @@ export function buildFileTransferOutboxEntry(args: {
30
40
  route: args.route,
31
41
  payload: {
32
42
  type: 'message.outbound',
43
+ messageId,
44
+ idempotencyKey: messageId,
33
45
  sessionKey: args.sessionKey,
34
- _meta: {
35
- 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,
36
61
  mediaUrl: args.mediaUrl,
37
62
  mediaLocalRoots: args.mediaLocalRoots ? Array.from(args.mediaLocalRoots) : undefined,
38
- text: args.text,
39
63
  asVoice: args.asVoice === true,
40
64
  audioAsVoice: args.audioAsVoice === true,
41
- type: args.type,
42
- ...(args.extra ? { extra: { ...args.extra } } : {}),
43
- finalEvent: args.pushEvent,
44
- replyToId:
45
- normalizeOutboundReplyToId({
46
- kind: args.kind,
47
- replyToId: args.replyToId,
48
- replyTargetPolicy: args.replyTargetPolicy,
49
- }) || undefined,
50
- messageKind: args.kind,
65
+ downloadMedia: args.downloadMedia,
66
+ transferMode: 'media',
67
+ path: '',
68
+ base64: '',
69
+ fileName: '',
70
+ ...(args.extra ? sanitizeExtraSpread(args.extra) : {}),
51
71
  },
52
72
  },
53
73
  createdAt,
@@ -60,8 +80,8 @@ export function buildTextOutboxEntry(args: {
60
80
  createMessageId: () => string;
61
81
  now: () => number;
62
82
  normalizeAccountId: (accountId?: string | null) => string;
63
- normalizeReplyToId: (value?: string | null) => string;
64
83
  accountId: string;
84
+ extra?: Record<string, unknown>;
65
85
  sessionKey: string;
66
86
  route: BncrRoute;
67
87
  text: string;
@@ -92,6 +112,9 @@ export function buildTextOutboxEntry(args: {
92
112
  path: '',
93
113
  base64: '',
94
114
  fileName: '',
115
+ // Extra fields from marker/pipeline spread for adapter-specific data
116
+ // (e.g. type="appmsg", msg="<appmsg>..." from [BncrParam:...])
117
+ ...(args.extra ? sanitizeExtraSpread(args.extra) : {}),
95
118
  },
96
119
  ts: createdAt,
97
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 =