@xmoxmo/bncr 0.3.6 → 0.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/README.md +5 -0
  2. package/dist/index.js +28 -5
  3. package/index.ts +55 -721
  4. package/openclaw.plugin.json +1 -0
  5. package/package.json +8 -4
  6. package/scripts/check-pack.mjs +93 -18
  7. package/scripts/check-register-drift.mjs +35 -13
  8. package/scripts/selfcheck.mjs +80 -11
  9. package/src/bootstrap/channel-plugin-runtime.ts +81 -0
  10. package/src/bootstrap/cli.ts +97 -0
  11. package/src/bootstrap/register-runtime-gateway.ts +129 -0
  12. package/src/bootstrap/register-runtime-helpers.ts +140 -0
  13. package/src/bootstrap/register-runtime-singleton.ts +137 -0
  14. package/src/bootstrap/register-runtime.ts +201 -0
  15. package/src/bootstrap/runtime-discovery.ts +187 -0
  16. package/src/bootstrap/runtime-loader.ts +54 -0
  17. package/src/channel.ts +1590 -4967
  18. package/src/core/accounts.ts +23 -4
  19. package/src/core/dead-letter-diagnostics.ts +37 -5
  20. package/src/core/diagnostics.ts +31 -15
  21. package/src/core/downlink-health.ts +3 -11
  22. package/src/core/extended-diagnostics.ts +78 -36
  23. package/src/core/file-transfer-payloads.ts +1 -1
  24. package/src/core/logging.ts +1 -0
  25. package/src/core/outbox-enqueue.ts +13 -2
  26. package/src/core/outbox-entry-builders.ts +2 -0
  27. package/src/core/outbox-summary.ts +75 -3
  28. package/src/core/permissions.ts +15 -2
  29. package/src/core/persisted-outbox-entry.ts +21 -6
  30. package/src/core/policy.ts +45 -4
  31. package/src/core/probe.ts +3 -15
  32. package/src/core/register-trace.ts +3 -3
  33. package/src/core/status.ts +43 -4
  34. package/src/core/targets.ts +216 -205
  35. package/src/core/types.ts +221 -0
  36. package/src/core/value-sanitize.ts +29 -0
  37. package/src/messaging/inbound/commands.ts +147 -172
  38. package/src/messaging/inbound/context-facts.ts +4 -2
  39. package/src/messaging/inbound/contracts.ts +70 -0
  40. package/src/messaging/inbound/dispatch-prep.ts +303 -0
  41. package/src/messaging/inbound/dispatch.ts +49 -462
  42. package/src/messaging/inbound/gate.ts +18 -5
  43. package/src/messaging/inbound/last-route.ts +10 -4
  44. package/src/messaging/inbound/media-url-download.ts +109 -0
  45. package/src/messaging/inbound/native-command-runtime.ts +225 -0
  46. package/src/messaging/inbound/parse.ts +2 -1
  47. package/src/messaging/inbound/remote-media.ts +49 -0
  48. package/src/messaging/inbound/reply-config.ts +16 -4
  49. package/src/messaging/inbound/reply-dispatch.ts +162 -0
  50. package/src/messaging/inbound/runtime-compat.ts +31 -10
  51. package/src/messaging/inbound/session-label.ts +15 -7
  52. package/src/messaging/inbound/turn-context.ts +131 -0
  53. package/src/messaging/outbound/actions.ts +24 -10
  54. package/src/messaging/outbound/diagnostics-debug-builders.ts +365 -0
  55. package/src/messaging/outbound/diagnostics.ts +31 -355
  56. package/src/messaging/outbound/durable-message-adapter.ts +20 -16
  57. package/src/messaging/outbound/durable-queue-adapter.ts +20 -7
  58. package/src/messaging/outbound/media.ts +24 -13
  59. package/src/messaging/outbound/reply-enqueue-media.ts +181 -0
  60. package/src/messaging/outbound/reply-enqueue.ts +46 -155
  61. package/src/messaging/outbound/send-params.ts +3 -0
  62. package/src/messaging/outbound/send.ts +19 -10
  63. package/src/messaging/outbound/session-route.ts +18 -3
  64. package/src/openclaw/channel-runtime-contracts.ts +76 -0
  65. package/src/openclaw/config-runtime.ts +13 -7
  66. package/src/openclaw/inbound-session-runtime.ts +7 -3
  67. package/src/openclaw/ingress-runtime.ts +17 -27
  68. package/src/openclaw/reply-runtime.ts +54 -59
  69. package/src/openclaw/routing-runtime.ts +35 -18
  70. package/src/openclaw/runtime-surface.ts +156 -12
  71. package/src/openclaw/sdk-helpers.ts +8 -1
  72. package/src/openclaw/session-route-runtime.ts +12 -12
  73. package/src/plugin/ack-outbox-runtime-group.ts +264 -0
  74. package/src/plugin/bridge-ack-facade.ts +137 -0
  75. package/src/plugin/bridge-connection-facade.ts +111 -0
  76. package/src/plugin/bridge-diagnostics-facade.ts +23 -0
  77. package/src/plugin/bridge-drain-facade.ts +98 -0
  78. package/src/plugin/bridge-extended-diagnostics-facade.ts +149 -0
  79. package/src/plugin/bridge-file-transfer-push-facade.ts +140 -0
  80. package/src/plugin/bridge-lifecycle.ts +156 -0
  81. package/src/plugin/bridge-media-facade.ts +241 -0
  82. package/src/plugin/bridge-outbox-facade.ts +182 -0
  83. package/src/plugin/bridge-runtime-helpers.ts +266 -0
  84. package/src/plugin/bridge-runtime-snapshots.ts +104 -0
  85. package/src/plugin/bridge-runtime-surface-facade.ts +8 -0
  86. package/src/plugin/bridge-status-facade.ts +76 -0
  87. package/src/plugin/bridge-status-worker-facade.ts +72 -0
  88. package/src/plugin/bridge-support-runtime.ts +137 -0
  89. package/src/plugin/bridge-surface-handlers-group.ts +242 -0
  90. package/src/plugin/bridge-surface-helpers.ts +28 -0
  91. package/src/plugin/capabilities.ts +1 -3
  92. package/src/plugin/channel-components.ts +289 -0
  93. package/src/plugin/channel-inbound-helpers.ts +149 -0
  94. package/src/plugin/channel-plugin-bridge-group.ts +129 -0
  95. package/src/plugin/channel-plugin-surface-group.ts +202 -0
  96. package/src/plugin/channel-runtime-builders-delivery.ts +513 -0
  97. package/src/plugin/channel-runtime-builders-status.ts +331 -0
  98. package/src/plugin/channel-runtime-builders.ts +25 -0
  99. package/src/plugin/channel-runtime-constants.ts +40 -0
  100. package/src/plugin/channel-runtime-types.ts +146 -0
  101. package/src/plugin/channel-send-runtime-group.ts +37 -0
  102. package/src/plugin/channel-send.ts +226 -0
  103. package/src/plugin/channel-utils.ts +102 -0
  104. package/src/plugin/config.ts +24 -3
  105. package/src/plugin/connection-handlers-helpers.ts +254 -0
  106. package/src/plugin/connection-handlers.ts +440 -0
  107. package/src/plugin/connection-state-helpers.ts +159 -0
  108. package/src/plugin/connection-state-runtime-group.ts +51 -0
  109. package/src/plugin/connection-state.ts +527 -0
  110. package/src/plugin/diagnostics-handlers.ts +211 -0
  111. package/src/plugin/error-message.ts +15 -0
  112. package/src/plugin/file-ack-runtime.ts +284 -0
  113. package/src/plugin/file-inbound-abort.ts +112 -0
  114. package/src/plugin/file-inbound-chunk.ts +146 -0
  115. package/src/plugin/file-inbound-complete.ts +153 -0
  116. package/src/plugin/file-inbound-handlers.ts +19 -0
  117. package/src/plugin/file-inbound-init.ts +122 -0
  118. package/src/plugin/file-inbound-runtime.ts +51 -0
  119. package/src/plugin/file-inbound-state.ts +62 -0
  120. package/src/plugin/file-transfer-logs.ts +227 -0
  121. package/src/plugin/file-transfer-orchestrator-chunk.ts +135 -0
  122. package/src/plugin/file-transfer-orchestrator.ts +304 -0
  123. package/src/plugin/file-transfer-runtime-group.ts +102 -0
  124. package/src/plugin/file-transfer-send.ts +89 -0
  125. package/src/plugin/file-transfer-setup.ts +206 -0
  126. package/src/plugin/gateway-event-context.ts +41 -0
  127. package/src/plugin/gateway-runtime.ts +17 -4
  128. package/src/plugin/inbound-acceptance.ts +107 -0
  129. package/src/plugin/inbound-handlers.ts +248 -0
  130. package/src/plugin/inbound-surface-handlers-group.ts +152 -0
  131. package/src/plugin/media-dedupe-runtime.ts +90 -0
  132. package/src/plugin/media-orchestrators-runtime-group.ts +316 -0
  133. package/src/plugin/message-ack-runtime.ts +284 -0
  134. package/src/plugin/message-send.ts +16 -6
  135. package/src/plugin/messaging.ts +98 -36
  136. package/src/plugin/outbound.ts +50 -8
  137. package/src/plugin/outbox-ack-logs.ts +136 -0
  138. package/src/plugin/outbox-ack-outcome.ts +128 -0
  139. package/src/plugin/outbox-drain-ack.ts +145 -0
  140. package/src/plugin/outbox-drain-failure.ts +84 -0
  141. package/src/plugin/outbox-drain-loop.ts +554 -0
  142. package/src/plugin/outbox-drain-post-push.ts +159 -0
  143. package/src/plugin/outbox-drain-runtime.ts +141 -0
  144. package/src/plugin/outbox-drain-schedule.ts +116 -0
  145. package/src/plugin/outbox-file-push-flow.ts +69 -0
  146. package/src/plugin/outbox-push-route-runtime-group.ts +81 -0
  147. package/src/plugin/outbox-push.ts +267 -0
  148. package/src/plugin/outbox-route.ts +181 -0
  149. package/src/plugin/outbox-text-push-flow.ts +90 -0
  150. package/src/plugin/runtime-diagnostics-assembler.ts +183 -0
  151. package/src/plugin/runtime-diagnostics-helpers.ts +302 -0
  152. package/src/plugin/runtime-diagnostics-payload-builders.ts +171 -0
  153. package/src/plugin/runtime-diagnostics-snapshot.ts +31 -0
  154. package/src/plugin/setup.ts +33 -6
  155. package/src/plugin/state-store.ts +249 -0
  156. package/src/plugin/state-transient-runtime-group.ts +105 -0
  157. package/src/plugin/status-runtime.ts +251 -0
  158. package/src/plugin/status.ts +33 -7
  159. package/src/plugin/target-runtime.ts +141 -0
  160. package/src/plugin/target-status-runtime-group.ts +130 -0
  161. package/src/plugin/transient-state-runtime.ts +82 -0
  162. package/src/runtime/outbound-ack-timeout.ts +5 -3
  163. package/src/runtime/outbound-flags.ts +24 -8
  164. package/src/runtime/status-snapshots.ts +36 -7
  165. package/src/runtime/status-worker.ts +34 -4
@@ -1,365 +1,41 @@
1
- import fs from 'node:fs';
2
1
  import { emitBncrLogLine } from '../../core/logging.ts';
3
- import { resolveBncrChannelPolicy } from '../../core/policy.ts';
4
- import {
5
- formatDisplayScope,
6
- normalizeInboundSessionKey,
7
- withTaskSessionKey,
8
- } from '../../core/targets.ts';
9
- import {
10
- readBncrSessionUpdatedAt,
11
- recordBncrInboundSession,
12
- resolveBncrInboundSessionStorePath,
13
- resolveBncrPinnedMainDmOwnerFromAllowlist,
14
- } from '../../openclaw/inbound-session-runtime.ts';
15
- import { saveOpenClawChannelMediaBuffer } from '../../openclaw/media-runtime.ts';
16
- import {
17
- dispatchOpenClawReplyWithBufferedBlockDispatcher,
18
- formatOpenClawAgentEnvelope,
19
- resolveOpenClawEnvelopeFormatOptions,
20
- } from '../../openclaw/reply-runtime.ts';
21
- import { resolveOpenClawAgentRoute } from '../../openclaw/routing-runtime.ts';
22
- import type { OutboundReplyTargetPolicy } from '../outbound/reply-target-policy.ts';
23
2
  import { handleBncrNativeCommand } from './commands.ts';
3
+ import type {
4
+ BncrEnqueueFromReply,
5
+ BncrInboundApi,
6
+ BncrInboundConfig,
7
+ BncrInboundLogger,
8
+ BncrRememberSessionRoute,
9
+ } from './contracts.ts';
24
10
  import {
25
- buildBncrPromptVisibleContextFacts,
26
- buildBncrStructuredContextFactsFromInboundParts,
27
- } from './context-facts.ts';
28
- import { buildBncrInboundRecordUpdateLastRoute } from './last-route.ts';
29
- import { buildBncrReplyConfig } from './reply-config.ts';
30
- import { resolveBncrChannelInboundRuntime } from './runtime-compat.ts';
31
- import { wrapBncrInboundRecordSessionLabelCorrection } from './session-label.ts';
32
-
33
- type ParsedInbound = ReturnType<typeof import('./parse.ts')['parseBncrInboundParams']>;
34
-
35
- type BncrInboundConversationResolution = {
36
- accountId: string;
37
- chatType: 'direct' | 'group';
38
- route: ParsedInbound['route'];
39
- resolvedRoute: {
40
- sessionKey: string;
41
- agentId: string;
42
- mainSessionKey?: string;
43
- };
44
- canonicalTo: string;
45
- rawTo: string;
46
- originatingTo: string;
47
- baseSessionKey: string;
48
- taskSessionKey?: string;
49
- dispatchSessionKey: string;
50
- };
51
-
52
- type BncrInboundReplyRouteFact = {
53
- accountId: string;
54
- sessionKey: string;
55
- route: ParsedInbound['route'];
56
- canonicalTo: string;
57
- originatingTo: string;
58
- chatType: 'direct' | 'group';
11
+ assertInboundMediaBase64Size,
12
+ decodeInboundMediaBase64,
13
+ estimateBase64DecodedBytes,
14
+ type ParsedInbound,
15
+ prepareBncrInboundDispatch,
16
+ resolveBncrInboundConversation,
17
+ } from './dispatch-prep.ts';
18
+ import { runBncrInboundReplyDispatch } from './reply-dispatch.ts';
19
+ import { buildBncrInboundTurnContext } from './turn-context.ts';
20
+
21
+ export {
22
+ assertInboundMediaBase64Size,
23
+ decodeInboundMediaBase64,
24
+ estimateBase64DecodedBytes,
25
+ resolveBncrInboundConversation,
59
26
  };
60
27
 
61
- const INBOUND_MEDIA_MAX_BYTES = 30 * 1024 * 1024;
62
-
63
- export function estimateBase64DecodedBytes(value: string): number {
64
- const normalized = String(value || '').replace(/\s+/g, '');
65
- if (!normalized) return 0;
66
- const padding = normalized.endsWith('==') ? 2 : normalized.endsWith('=') ? 1 : 0;
67
- return Math.max(0, Math.floor((normalized.length * 3) / 4) - padding);
68
- }
69
-
70
- export function assertInboundMediaBase64Size(value: string, maxBytes = INBOUND_MEDIA_MAX_BYTES) {
71
- const estimatedBytes = estimateBase64DecodedBytes(value);
72
- if (estimatedBytes > maxBytes) {
73
- throw new Error(
74
- `inbound media too large: estimated ${estimatedBytes} bytes exceeds ${maxBytes} bytes`,
75
- );
76
- }
77
- }
78
-
79
- export function decodeInboundMediaBase64(
80
- value: string,
81
- maxBytes = INBOUND_MEDIA_MAX_BYTES,
82
- ): Buffer {
83
- assertInboundMediaBase64Size(value, maxBytes);
84
- const normalized = String(value || '').replace(/\s+/g, '');
85
- const mediaBuf = Buffer.from(normalized, 'base64');
86
- if (!mediaBuf.length) {
87
- throw new Error('inbound media base64 decoded to empty buffer');
88
- }
89
- if (mediaBuf.length > maxBytes) {
90
- throw new Error(
91
- `inbound media too large: decoded ${mediaBuf.length} bytes exceeds ${maxBytes} bytes`,
92
- );
93
- }
94
- return mediaBuf;
95
- }
96
-
97
- function formatRawBncrInboundTarget(route: ParsedInbound['route']): string {
98
- return `Bncr:${String(route.platform || '').trim()}:${String(route.groupId || '').trim()}:${String(route.userId || '').trim()}`;
99
- }
100
-
101
- export function resolveBncrInboundConversation(args: {
102
- api: any;
103
- cfg: any;
104
- channelId: string;
105
- parsed: ParsedInbound;
106
- canonicalAgentId: string;
107
- }) {
108
- const { api, cfg, channelId, parsed, canonicalAgentId } = args;
109
- const { accountId, route, peer, sessionKeyfromroute, providedOriginatingTo, extracted } = parsed;
110
-
111
- const resolvedRoute = resolveOpenClawAgentRoute(api, {
112
- cfg,
113
- channel: channelId,
114
- accountId,
115
- peer,
116
- });
117
-
118
- const baseSessionKey =
119
- normalizeInboundSessionKey(sessionKeyfromroute, route, canonicalAgentId) ||
120
- resolvedRoute.sessionKey;
121
- const taskSessionKey = withTaskSessionKey(baseSessionKey, extracted.taskKey);
122
- const dispatchSessionKey = taskSessionKey || baseSessionKey;
123
- const rawTo = formatRawBncrInboundTarget(route);
124
- const canonicalTo = formatDisplayScope(route);
125
- const originatingTo = providedOriginatingTo || rawTo;
126
-
127
- return {
128
- accountId,
129
- chatType: peer.kind,
130
- route,
131
- resolvedRoute,
132
- canonicalTo,
133
- rawTo,
134
- originatingTo,
135
- baseSessionKey,
136
- ...(taskSessionKey ? { taskSessionKey } : {}),
137
- dispatchSessionKey,
138
- } satisfies BncrInboundConversationResolution;
139
- }
140
-
141
- async function prepareBncrInboundSessionContext(args: {
142
- api: any;
143
- cfg: any;
144
- parsed: ParsedInbound;
145
- resolution: BncrInboundConversationResolution;
146
- rememberSessionRoute: (sessionKey: string, accountId: string, route: any) => void;
147
- }) {
148
- const { api, cfg, parsed, resolution, rememberSessionRoute } = args;
149
- const {
150
- msgType,
151
- mediaBase64,
152
- mediaPathFromTransfer,
153
- mimeType,
154
- fileName,
155
- extracted,
156
- platform,
157
- groupId,
158
- userId,
159
- } = parsed;
160
- const { accountId, route, resolvedRoute, baseSessionKey, taskSessionKey, dispatchSessionKey } =
161
- resolution;
162
-
163
- rememberSessionRoute(baseSessionKey, accountId, route);
164
- if (taskSessionKey && taskSessionKey !== baseSessionKey) {
165
- rememberSessionRoute(taskSessionKey, accountId, route);
166
- }
167
-
168
- const storePath = resolveBncrInboundSessionStorePath({
169
- storeConfig: cfg?.session?.store,
170
- agentId: resolvedRoute.agentId,
171
- });
172
-
173
- let mediaPath: string | undefined;
174
- if (mediaBase64) {
175
- const mediaBuf = decodeInboundMediaBase64(mediaBase64);
176
- const saved = await saveOpenClawChannelMediaBuffer(
177
- api,
178
- mediaBuf,
179
- mimeType,
180
- 'inbound',
181
- 30 * 1024 * 1024,
182
- fileName,
183
- );
184
- mediaPath = saved.path;
185
- } else if (mediaPathFromTransfer && fs.existsSync(mediaPathFromTransfer)) {
186
- mediaPath = mediaPathFromTransfer;
187
- }
188
-
189
- const rawBody = extracted.text || (msgType === 'text' ? '' : `[${msgType}]`);
190
- const body = formatOpenClawAgentEnvelope(api, {
191
- channel: 'Bncr',
192
- from: `${platform}:${groupId}:${userId}`,
193
- timestamp: Date.now(),
194
- previousTimestamp: readBncrSessionUpdatedAt(api, {
195
- storePath,
196
- sessionKey: dispatchSessionKey,
197
- }),
198
- envelope: resolveOpenClawEnvelopeFormatOptions(api, cfg),
199
- body: rawBody,
200
- });
201
-
202
- return {
203
- storePath,
204
- mediaPath,
205
- rawBody,
206
- body,
207
- };
208
- }
209
-
210
- function buildBncrInboundTurnContext(args: {
211
- api: any;
212
- channelId: string;
213
- parsed: ParsedInbound;
214
- msgId?: string | null;
215
- mimeType?: string;
216
- mediaPath?: string;
217
- peer: ParsedInbound['peer'];
218
- senderIdForContext: string;
219
- senderDisplayName: string;
220
- resolution: BncrInboundConversationResolution;
221
- prepared: {
222
- rawBody: string;
223
- body: string;
224
- };
225
- }) {
226
- const {
227
- api,
228
- channelId,
229
- parsed,
230
- msgId,
231
- mimeType,
232
- mediaPath,
233
- peer,
234
- senderIdForContext,
235
- senderDisplayName,
236
- resolution,
237
- prepared,
238
- } = args;
239
- const structuredContextFacts = buildBncrStructuredContextFactsFromInboundParts({
240
- channelId,
241
- parsed,
242
- resolution,
243
- prepared: {
244
- rawBody: prepared.rawBody,
245
- body: prepared.body,
246
- mediaPath,
247
- },
248
- senderIdForContext,
249
- senderDisplayName,
250
- });
251
- const promptVisibleContextFacts = buildBncrPromptVisibleContextFacts(structuredContextFacts);
252
- const supplementalUntrustedContext = Object.keys(promptVisibleContextFacts).length
253
- ? [
254
- {
255
- label: 'Bncr inbound context',
256
- source: channelId,
257
- type: 'bncr.inbound_context',
258
- payload: promptVisibleContextFacts,
259
- },
260
- ]
261
- : [];
262
-
263
- return resolveBncrChannelInboundRuntime(api).buildContext({
264
- channel: channelId,
265
- provider: channelId,
266
- surface: channelId,
267
- accountId: resolution.accountId,
268
- messageId: msgId,
269
- timestamp: Date.now(),
270
- from: senderIdForContext,
271
- sender: {
272
- id: senderIdForContext,
273
- name: senderDisplayName,
274
- username: senderDisplayName,
275
- },
276
- conversation: {
277
- kind: resolution.chatType,
278
- id: peer.id,
279
- label: resolution.canonicalTo,
280
- routePeer: {
281
- kind: peer.kind,
282
- id: peer.id,
283
- },
284
- },
285
- route: {
286
- agentId: resolution.resolvedRoute.agentId,
287
- accountId: resolution.accountId,
288
- routeSessionKey: resolution.resolvedRoute.sessionKey,
289
- dispatchSessionKey: resolution.dispatchSessionKey,
290
- mainSessionKey: resolution.resolvedRoute.mainSessionKey,
291
- },
292
- reply: {
293
- to: resolution.canonicalTo,
294
- originatingTo: resolution.originatingTo,
295
- },
296
- message: {
297
- inboundEventKind: 'user_request',
298
- body: prepared.body,
299
- rawBody: prepared.rawBody,
300
- bodyForAgent: prepared.rawBody,
301
- commandBody: prepared.rawBody,
302
- envelopeFrom: resolution.originatingTo,
303
- senderLabel: senderDisplayName,
304
- },
305
- media: mediaPath
306
- ? [
307
- {
308
- path: mediaPath,
309
- contentType: mimeType,
310
- kind: mimeType?.startsWith('image/')
311
- ? 'image'
312
- : mimeType?.startsWith('video/')
313
- ? 'video'
314
- : mimeType?.startsWith('audio/')
315
- ? 'audio'
316
- : 'document',
317
- messageId: msgId ?? undefined,
318
- },
319
- ]
320
- : [],
321
- supplemental: {
322
- untrustedContext: supplementalUntrustedContext,
323
- },
324
- extra: {
325
- OriginatingChannel: channelId,
326
- BncrStructuredContextFacts: structuredContextFacts,
327
- StructuredContextFacts: structuredContextFacts,
328
- },
329
- });
330
- }
331
-
332
- function buildBncrInboundReplyRouteFact(
333
- resolution: BncrInboundConversationResolution,
334
- ): BncrInboundReplyRouteFact {
335
- return {
336
- accountId: resolution.accountId,
337
- sessionKey: resolution.dispatchSessionKey,
338
- route: resolution.route,
339
- canonicalTo: resolution.canonicalTo,
340
- originatingTo: resolution.originatingTo,
341
- chatType: resolution.chatType,
342
- };
343
- }
344
-
345
28
  export async function dispatchBncrInbound(params: {
346
- api: any;
29
+ api: BncrInboundApi;
347
30
  channelId: string;
348
- cfg: any;
31
+ cfg: BncrInboundConfig;
349
32
  parsed: ParsedInbound;
350
33
  canonicalAgentId: string;
351
- rememberSessionRoute: (sessionKey: string, accountId: string, route: any) => void;
352
- enqueueFromReply: (args: {
353
- accountId: string;
354
- sessionKey: string;
355
- route: any;
356
- payload: { text?: string; mediaUrl?: string; mediaUrls?: string[] };
357
- mediaLocalRoots?: readonly string[];
358
- replyTargetPolicy?: OutboundReplyTargetPolicy;
359
- }) => Promise<void>;
34
+ rememberSessionRoute: BncrRememberSessionRoute;
35
+ enqueueFromReply: BncrEnqueueFromReply;
360
36
  setInboundActivity: (accountId: string, at: number) => void;
361
37
  scheduleSave: () => void;
362
- logger?: { warn?: (msg: string) => void; error?: (msg: string) => void };
38
+ logger?: BncrInboundLogger;
363
39
  }) {
364
40
  const {
365
41
  api,
@@ -397,37 +73,30 @@ export async function dispatchBncrInbound(params: {
397
73
  };
398
74
  }
399
75
 
400
- const resolution = resolveBncrInboundConversation({
76
+ const preparedDispatch = await prepareBncrInboundDispatch({
401
77
  api,
402
- cfg,
403
78
  channelId,
404
- parsed,
405
- canonicalAgentId,
406
- });
407
- const { resolvedRoute, canonicalTo, dispatchSessionKey: sessionKey } = resolution;
408
- const prepared = await prepareBncrInboundSessionContext({
409
- api,
410
79
  cfg,
411
80
  parsed,
412
- resolution,
81
+ canonicalAgentId,
413
82
  rememberSessionRoute,
414
83
  });
415
- const { storePath, mediaPath, rawBody } = prepared;
416
- const replyRouteFact = buildBncrInboundReplyRouteFact(resolution);
84
+ const { resolution, prepared, replyRouteFact, senderIdForContext, senderDisplayName } =
85
+ preparedDispatch;
86
+ const { dispatchSessionKey: sessionKey } = resolution;
87
+ const { storePath, mediaPath, mediaContentType, rawBody } = prepared;
417
88
  if (!clientId) {
418
89
  emitBncrLogLine(
419
90
  'warn',
420
91
  '[bncr] inbound missing clientId for chat identity; using route identity fallback',
421
92
  );
422
93
  }
423
- const senderIdForContext = clientId || canonicalTo;
424
- const senderDisplayName = clientId ? 'bncr-client' : canonicalTo;
425
- const ctxPayload = buildBncrInboundTurnContext({
94
+ const ctxPayload = await buildBncrInboundTurnContext({
426
95
  api,
427
96
  channelId,
428
97
  parsed,
429
98
  msgId,
430
- mimeType,
99
+ mimeType: mediaContentType || mimeType,
431
100
  mediaPath,
432
101
  peer,
433
102
  senderIdForContext,
@@ -436,104 +105,22 @@ export async function dispatchBncrInbound(params: {
436
105
  prepared,
437
106
  });
438
107
 
439
- const effectiveReply = buildBncrReplyConfig(cfg);
440
- const channelPolicy = resolveBncrChannelPolicy(cfg?.channels?.bncr || {});
441
- const pinnedMainDmOwner =
442
- peer.kind === 'direct'
443
- ? resolveBncrPinnedMainDmOwnerFromAllowlist({
444
- dmScope: cfg?.session?.dmScope,
445
- allowFrom: channelPolicy.allowFrom,
446
- normalizeEntry: (entry: string) => String(entry || '').trim(),
447
- })
448
- : null;
449
- const updateLastRoute = buildBncrInboundRecordUpdateLastRoute({
108
+ await runBncrInboundReplyDispatch({
109
+ api,
450
110
  channelId,
451
- peerKind: peer.kind,
111
+ cfg,
112
+ parsed,
113
+ msgId,
114
+ peer,
115
+ rawBody,
116
+ storePath,
117
+ ctxPayload,
118
+ resolution,
119
+ replyRouteFact,
452
120
  senderIdForContext,
453
- accountId: resolution.accountId,
454
- to: resolution.canonicalTo,
455
- resolvedRoute: resolution.resolvedRoute,
456
- sessionKey: resolution.dispatchSessionKey,
457
- pinnedMainDmOwner,
458
- });
459
-
460
- await resolveBncrChannelInboundRuntime(api).run({
461
- channel: channelId,
462
- accountId,
463
- raw: parsed,
464
- adapter: {
465
- ingest: () => ({
466
- id: msgId ?? `${canonicalTo}:${Date.now()}`,
467
- timestamp: Date.now(),
468
- rawText: rawBody,
469
- textForAgent: ctxPayload.BodyForAgent,
470
- textForCommands: ctxPayload.CommandBody,
471
- raw: parsed,
472
- }),
473
- resolveTurn: () => ({
474
- channel: channelId,
475
- accountId,
476
- routeSessionKey: resolvedRoute.sessionKey,
477
- storePath,
478
- ctxPayload,
479
- recordInboundSession: wrapBncrInboundRecordSessionLabelCorrection({
480
- recordInboundSession: recordBncrInboundSession,
481
- expectedLabel: canonicalTo,
482
- }),
483
- record: {
484
- updateLastRoute,
485
- onRecordError: (err: unknown) => {
486
- emitBncrLogLine('warn', `[bncr] inbound record session failed: ${String(err)}`);
487
- },
488
- },
489
- runDispatch: () =>
490
- dispatchOpenClawReplyWithBufferedBlockDispatcher(api, {
491
- ctx: ctxPayload,
492
- cfg: effectiveReply.replyCfg,
493
- dispatcherOptions: {
494
- deliver: async (
495
- payload: {
496
- text?: string;
497
- mediaUrl?: string;
498
- mediaUrls?: string[];
499
- audioAsVoice?: boolean;
500
- },
501
- info?: { kind?: 'tool' | 'block' | 'final' },
502
- ) => {
503
- const kind = info?.kind;
504
- const shouldForwardTool = effectiveReply.blockStreaming && effectiveReply.allowTool;
505
-
506
- if (kind === 'tool' && !shouldForwardTool) {
507
- return;
508
- }
509
-
510
- await enqueueFromReply({
511
- accountId: replyRouteFact.accountId,
512
- sessionKey: replyRouteFact.sessionKey,
513
- route: replyRouteFact.route,
514
- payload: {
515
- ...payload,
516
- kind: kind as 'tool' | 'block' | 'final' | undefined,
517
- replyToId: msgId || undefined,
518
- },
519
- });
520
- },
521
- onError: (err: unknown) => {
522
- emitBncrLogLine('error', `[bncr] outbound reply failed: ${String(err)}`);
523
- },
524
- },
525
- replyOptions: {
526
- disableBlockStreaming: !effectiveReply.blockStreaming,
527
- shouldEmitToolResult: effectiveReply.allowTool ? () => true : undefined,
528
- },
529
- }),
530
- }),
531
- onFinalize: () => {
532
- const inboundAt = Date.now();
533
- setInboundActivity(accountId, inboundAt);
534
- scheduleSave();
535
- },
536
- },
121
+ setInboundActivity,
122
+ scheduleSave,
123
+ enqueueFromReply,
537
124
  });
538
125
 
539
126
  return {
@@ -1,10 +1,15 @@
1
1
  import { normalizeAccountId } from '../../core/accounts.ts';
2
2
  import { resolveBncrChannelPolicy } from '../../core/policy.ts';
3
3
  import { buildDisplayScopeCandidates } from '../../core/targets.ts';
4
+ import type { BncrRoute } from '../../core/types.ts';
4
5
  import {
5
6
  defineOpenClawStableChannelIngressIdentity,
6
7
  resolveOpenClawChannelMessageIngress,
7
8
  } from '../../openclaw/ingress-runtime.ts';
9
+ import type { BncrInboundConfig, BncrInboundParamsInput } from './contracts.ts';
10
+
11
+ type RouteLike = Partial<Pick<BncrRoute, 'groupId' | 'userId' | 'platform'>>;
12
+ type AccessGroupsLike = Parameters<typeof resolveOpenClawChannelMessageIngress>[0]['accessGroups'];
8
13
 
9
14
  export type BncrGateResult = { allowed: true } | { allowed: false; reason: string };
10
15
 
@@ -48,10 +53,10 @@ function gateReasonFromIngress(reasonCode?: string): string {
48
53
  }
49
54
 
50
55
  export async function checkBncrMessageGate(params: {
51
- parsed: any;
52
- cfg: any;
56
+ parsed: BncrInboundParamsInput & { route?: RouteLike };
57
+ cfg: BncrInboundConfig;
53
58
  account: { accountId: string; enabled?: boolean };
54
- }): BncrGateResult {
59
+ }): Promise<BncrGateResult> {
55
60
  const { parsed, cfg, account } = params;
56
61
  const accountId = normalizeAccountId(account?.accountId);
57
62
  const channelCfg = cfg?.channels?.bncr || {};
@@ -63,9 +68,17 @@ export async function checkBncrMessageGate(params: {
63
68
  }
64
69
 
65
70
  const route = parsed?.route;
71
+ if (!route?.platform || !route?.groupId || !route?.userId) {
72
+ return { allowed: false, reason: 'invalid route' };
73
+ }
74
+ const resolvedRoute: BncrRoute = {
75
+ platform: route.platform,
76
+ groupId: route.groupId,
77
+ userId: route.userId,
78
+ };
66
79
  const isGroup = asString(route?.groupId || '0') !== '0';
67
80
 
68
- const candidates = buildDisplayScopeCandidates(route);
81
+ const candidates = buildDisplayScopeCandidates(resolvedRoute);
69
82
  const displayScope = candidates[0] || '';
70
83
  const routeKey = candidates.find((candidate) => candidate !== displayScope) || displayScope;
71
84
 
@@ -93,7 +106,7 @@ export async function checkBncrMessageGate(params: {
93
106
  },
94
107
  allowFrom: policy.dmPolicy === 'open' ? ['*', ...policy.allowFrom] : policy.allowFrom,
95
108
  groupAllowFrom: policy.groupAllowFrom,
96
- accessGroups: cfg?.accessGroups,
109
+ accessGroups: cfg?.accessGroups as AccessGroupsLike,
97
110
  });
98
111
 
99
112
  if (resolved.ingress.admission === 'dispatch' || resolved.ingress.admission === 'observe') {
@@ -9,6 +9,7 @@ export function buildBncrInboundRecordUpdateLastRoute(args: {
9
9
  resolvedRoute: {
10
10
  sessionKey: string;
11
11
  mainSessionKey?: string;
12
+ lastRoutePolicy?: 'main' | 'session';
12
13
  };
13
14
  sessionKey: string;
14
15
  pinnedMainDmOwner: string | null;
@@ -25,10 +26,15 @@ export function buildBncrInboundRecordUpdateLastRoute(args: {
25
26
  } = args;
26
27
  if (peerKind !== 'direct') return undefined;
27
28
 
28
- const inboundLastRouteSessionKey = resolveOpenClawInboundLastRouteSessionKey({
29
- route: resolvedRoute,
30
- sessionKey,
31
- });
29
+ const inboundLastRouteSessionKey = resolvedRoute.mainSessionKey
30
+ ? resolveOpenClawInboundLastRouteSessionKey({
31
+ route: {
32
+ mainSessionKey: resolvedRoute.mainSessionKey,
33
+ lastRoutePolicy: resolvedRoute.lastRoutePolicy || 'session',
34
+ },
35
+ sessionKey,
36
+ })
37
+ : sessionKey;
32
38
 
33
39
  return {
34
40
  sessionKey: inboundLastRouteSessionKey,