@xmoxmo/bncr 0.3.5 → 0.3.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 (164) hide show
  1. package/README.md +5 -0
  2. package/dist/index.js +28 -5
  3. package/index.ts +55 -721
  4. package/package.json +8 -4
  5. package/scripts/check-pack.mjs +93 -18
  6. package/scripts/check-register-drift.mjs +35 -13
  7. package/scripts/selfcheck.mjs +80 -11
  8. package/src/bootstrap/channel-plugin-runtime.ts +81 -0
  9. package/src/bootstrap/cli.ts +97 -0
  10. package/src/bootstrap/register-runtime-gateway.ts +129 -0
  11. package/src/bootstrap/register-runtime-helpers.ts +140 -0
  12. package/src/bootstrap/register-runtime-singleton.ts +137 -0
  13. package/src/bootstrap/register-runtime.ts +201 -0
  14. package/src/bootstrap/runtime-discovery.ts +187 -0
  15. package/src/bootstrap/runtime-loader.ts +54 -0
  16. package/src/channel.ts +1590 -4967
  17. package/src/core/accounts.ts +23 -4
  18. package/src/core/dead-letter-diagnostics.ts +37 -5
  19. package/src/core/diagnostics.ts +31 -15
  20. package/src/core/downlink-health.ts +3 -11
  21. package/src/core/extended-diagnostics.ts +78 -36
  22. package/src/core/file-transfer-payloads.ts +1 -1
  23. package/src/core/logging.ts +1 -0
  24. package/src/core/outbox-enqueue.ts +13 -2
  25. package/src/core/outbox-entry-builders.ts +2 -0
  26. package/src/core/outbox-summary.ts +75 -3
  27. package/src/core/permissions.ts +15 -2
  28. package/src/core/persisted-outbox-entry.ts +21 -6
  29. package/src/core/policy.ts +45 -4
  30. package/src/core/probe.ts +3 -15
  31. package/src/core/register-trace.ts +3 -3
  32. package/src/core/status.ts +43 -4
  33. package/src/core/targets.ts +216 -205
  34. package/src/core/types.ts +221 -0
  35. package/src/core/value-sanitize.ts +29 -0
  36. package/src/messaging/inbound/commands.ts +147 -172
  37. package/src/messaging/inbound/context-facts.ts +4 -2
  38. package/src/messaging/inbound/contracts.ts +70 -0
  39. package/src/messaging/inbound/dispatch-prep.ts +303 -0
  40. package/src/messaging/inbound/dispatch.ts +49 -462
  41. package/src/messaging/inbound/gate.ts +18 -5
  42. package/src/messaging/inbound/last-route.ts +10 -4
  43. package/src/messaging/inbound/media-url-download.ts +109 -0
  44. package/src/messaging/inbound/native-command-runtime.ts +225 -0
  45. package/src/messaging/inbound/parse.ts +2 -1
  46. package/src/messaging/inbound/remote-media.ts +49 -0
  47. package/src/messaging/inbound/reply-config.ts +16 -4
  48. package/src/messaging/inbound/reply-dispatch.ts +162 -0
  49. package/src/messaging/inbound/runtime-compat.ts +31 -10
  50. package/src/messaging/inbound/session-label.ts +15 -7
  51. package/src/messaging/inbound/turn-context.ts +131 -0
  52. package/src/messaging/outbound/actions.ts +24 -10
  53. package/src/messaging/outbound/diagnostics-debug-builders.ts +365 -0
  54. package/src/messaging/outbound/diagnostics.ts +31 -355
  55. package/src/messaging/outbound/durable-message-adapter.ts +20 -16
  56. package/src/messaging/outbound/durable-queue-adapter.ts +20 -7
  57. package/src/messaging/outbound/media.ts +24 -13
  58. package/src/messaging/outbound/reply-enqueue-media.ts +181 -0
  59. package/src/messaging/outbound/reply-enqueue.ts +57 -134
  60. package/src/messaging/outbound/send-params.ts +3 -0
  61. package/src/messaging/outbound/send.ts +19 -10
  62. package/src/messaging/outbound/session-route.ts +18 -3
  63. package/src/openclaw/channel-runtime-contracts.ts +76 -0
  64. package/src/openclaw/config-runtime.ts +13 -7
  65. package/src/openclaw/inbound-session-runtime.ts +7 -3
  66. package/src/openclaw/ingress-runtime.ts +17 -27
  67. package/src/openclaw/reply-runtime.ts +54 -59
  68. package/src/openclaw/routing-runtime.ts +35 -18
  69. package/src/openclaw/runtime-surface.ts +156 -12
  70. package/src/openclaw/sdk-helpers.ts +8 -1
  71. package/src/openclaw/session-route-runtime.ts +12 -12
  72. package/src/plugin/ack-outbox-runtime-group.ts +264 -0
  73. package/src/plugin/bridge-ack-facade.ts +137 -0
  74. package/src/plugin/bridge-connection-facade.ts +111 -0
  75. package/src/plugin/bridge-diagnostics-facade.ts +23 -0
  76. package/src/plugin/bridge-drain-facade.ts +98 -0
  77. package/src/plugin/bridge-extended-diagnostics-facade.ts +149 -0
  78. package/src/plugin/bridge-file-transfer-push-facade.ts +140 -0
  79. package/src/plugin/bridge-lifecycle.ts +156 -0
  80. package/src/plugin/bridge-media-facade.ts +241 -0
  81. package/src/plugin/bridge-outbox-facade.ts +182 -0
  82. package/src/plugin/bridge-runtime-helpers.ts +266 -0
  83. package/src/plugin/bridge-runtime-snapshots.ts +104 -0
  84. package/src/plugin/bridge-runtime-surface-facade.ts +8 -0
  85. package/src/plugin/bridge-status-facade.ts +76 -0
  86. package/src/plugin/bridge-status-worker-facade.ts +72 -0
  87. package/src/plugin/bridge-support-runtime.ts +137 -0
  88. package/src/plugin/bridge-surface-handlers-group.ts +242 -0
  89. package/src/plugin/bridge-surface-helpers.ts +28 -0
  90. package/src/plugin/capabilities.ts +1 -3
  91. package/src/plugin/channel-components.ts +289 -0
  92. package/src/plugin/channel-inbound-helpers.ts +149 -0
  93. package/src/plugin/channel-plugin-bridge-group.ts +129 -0
  94. package/src/plugin/channel-plugin-surface-group.ts +202 -0
  95. package/src/plugin/channel-runtime-builders-delivery.ts +513 -0
  96. package/src/plugin/channel-runtime-builders-status.ts +331 -0
  97. package/src/plugin/channel-runtime-builders.ts +25 -0
  98. package/src/plugin/channel-runtime-constants.ts +40 -0
  99. package/src/plugin/channel-runtime-types.ts +146 -0
  100. package/src/plugin/channel-send-runtime-group.ts +37 -0
  101. package/src/plugin/channel-send.ts +226 -0
  102. package/src/plugin/channel-utils.ts +102 -0
  103. package/src/plugin/config.ts +24 -3
  104. package/src/plugin/connection-handlers-helpers.ts +254 -0
  105. package/src/plugin/connection-handlers.ts +440 -0
  106. package/src/plugin/connection-state-helpers.ts +159 -0
  107. package/src/plugin/connection-state-runtime-group.ts +51 -0
  108. package/src/plugin/connection-state.ts +527 -0
  109. package/src/plugin/diagnostics-handlers.ts +211 -0
  110. package/src/plugin/error-message.ts +15 -0
  111. package/src/plugin/file-ack-runtime.ts +284 -0
  112. package/src/plugin/file-inbound-abort.ts +112 -0
  113. package/src/plugin/file-inbound-chunk.ts +146 -0
  114. package/src/plugin/file-inbound-complete.ts +153 -0
  115. package/src/plugin/file-inbound-handlers.ts +19 -0
  116. package/src/plugin/file-inbound-init.ts +122 -0
  117. package/src/plugin/file-inbound-runtime.ts +51 -0
  118. package/src/plugin/file-inbound-state.ts +62 -0
  119. package/src/plugin/file-transfer-logs.ts +227 -0
  120. package/src/plugin/file-transfer-orchestrator-chunk.ts +135 -0
  121. package/src/plugin/file-transfer-orchestrator.ts +304 -0
  122. package/src/plugin/file-transfer-runtime-group.ts +102 -0
  123. package/src/plugin/file-transfer-send.ts +89 -0
  124. package/src/plugin/file-transfer-setup.ts +206 -0
  125. package/src/plugin/gateway-event-context.ts +41 -0
  126. package/src/plugin/gateway-runtime.ts +14 -4
  127. package/src/plugin/inbound-acceptance.ts +107 -0
  128. package/src/plugin/inbound-handlers.ts +248 -0
  129. package/src/plugin/inbound-surface-handlers-group.ts +152 -0
  130. package/src/plugin/media-dedupe-runtime.ts +90 -0
  131. package/src/plugin/media-orchestrators-runtime-group.ts +316 -0
  132. package/src/plugin/message-ack-runtime.ts +284 -0
  133. package/src/plugin/message-send.ts +16 -6
  134. package/src/plugin/messaging.ts +98 -36
  135. package/src/plugin/outbound.ts +50 -8
  136. package/src/plugin/outbox-ack-logs.ts +136 -0
  137. package/src/plugin/outbox-ack-outcome.ts +128 -0
  138. package/src/plugin/outbox-drain-ack.ts +145 -0
  139. package/src/plugin/outbox-drain-failure.ts +84 -0
  140. package/src/plugin/outbox-drain-loop.ts +554 -0
  141. package/src/plugin/outbox-drain-post-push.ts +159 -0
  142. package/src/plugin/outbox-drain-runtime.ts +141 -0
  143. package/src/plugin/outbox-drain-schedule.ts +116 -0
  144. package/src/plugin/outbox-file-push-flow.ts +69 -0
  145. package/src/plugin/outbox-push-route-runtime-group.ts +81 -0
  146. package/src/plugin/outbox-push.ts +267 -0
  147. package/src/plugin/outbox-route.ts +181 -0
  148. package/src/plugin/outbox-text-push-flow.ts +90 -0
  149. package/src/plugin/runtime-diagnostics-assembler.ts +183 -0
  150. package/src/plugin/runtime-diagnostics-helpers.ts +302 -0
  151. package/src/plugin/runtime-diagnostics-payload-builders.ts +171 -0
  152. package/src/plugin/runtime-diagnostics-snapshot.ts +31 -0
  153. package/src/plugin/setup.ts +33 -6
  154. package/src/plugin/state-store.ts +249 -0
  155. package/src/plugin/state-transient-runtime-group.ts +105 -0
  156. package/src/plugin/status-runtime.ts +251 -0
  157. package/src/plugin/status.ts +33 -7
  158. package/src/plugin/target-runtime.ts +141 -0
  159. package/src/plugin/target-status-runtime-group.ts +130 -0
  160. package/src/plugin/transient-state-runtime.ts +82 -0
  161. package/src/runtime/outbound-ack-timeout.ts +5 -3
  162. package/src/runtime/outbound-flags.ts +24 -8
  163. package/src/runtime/status-snapshots.ts +36 -7
  164. package/src/runtime/status-worker.ts +34 -4
@@ -0,0 +1,109 @@
1
+ export const INBOUND_MEDIA_URL_MAX_BYTES = 200 * 1024 * 1024;
2
+ const INBOUND_MEDIA_URL_TIMEOUT_MS = 30_000;
3
+
4
+ export function isHttpMediaUrl(value: string): boolean {
5
+ try {
6
+ const parsed = new URL(value);
7
+ return parsed.protocol === 'http:' || parsed.protocol === 'https:';
8
+ } catch {
9
+ return false;
10
+ }
11
+ }
12
+
13
+ function readResponseHeader(headers: unknown, name: string): string | undefined {
14
+ if (!headers || typeof (headers as { get?: unknown }).get !== 'function') return undefined;
15
+ const value = (headers as { get(name: string): string | null }).get(name);
16
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
17
+ }
18
+
19
+ function normalizeInboundMediaUrlTimeoutMs(timeoutMs: number): number {
20
+ return Number.isFinite(timeoutMs) && timeoutMs > 0
21
+ ? Math.floor(timeoutMs)
22
+ : INBOUND_MEDIA_URL_TIMEOUT_MS;
23
+ }
24
+
25
+ async function readInboundMediaResponseBody(args: {
26
+ response: Response;
27
+ maxBytes: number;
28
+ abort: () => void;
29
+ }): Promise<Buffer> {
30
+ const body = args.response.body;
31
+ if (!body || typeof body.getReader !== 'function') {
32
+ const buffer = Buffer.from(await args.response.arrayBuffer());
33
+ if (buffer.length > args.maxBytes) {
34
+ throw new Error(
35
+ `inbound media url too large: ${buffer.length} bytes exceeds ${args.maxBytes} bytes`,
36
+ );
37
+ }
38
+ return buffer;
39
+ }
40
+
41
+ const reader = body.getReader();
42
+ const chunks: Buffer[] = [];
43
+ let total = 0;
44
+ try {
45
+ while (true) {
46
+ const { done, value } = await reader.read();
47
+ if (done) break;
48
+ if (!value) continue;
49
+ const chunk = Buffer.from(value);
50
+ total += chunk.byteLength;
51
+ if (total > args.maxBytes) {
52
+ args.abort();
53
+ throw new Error(
54
+ `inbound media url too large: ${total} bytes exceeds ${args.maxBytes} bytes`,
55
+ );
56
+ }
57
+ chunks.push(chunk);
58
+ }
59
+ } finally {
60
+ reader.releaseLock();
61
+ }
62
+ return Buffer.concat(chunks, total);
63
+ }
64
+
65
+ export async function downloadInboundMediaUrl(
66
+ url: string,
67
+ maxBytes = INBOUND_MEDIA_URL_MAX_BYTES,
68
+ timeoutMs = INBOUND_MEDIA_URL_TIMEOUT_MS,
69
+ ): Promise<{ buffer: Buffer; contentType?: string }> {
70
+ if (typeof fetch !== 'function') {
71
+ throw new Error('inbound media url download unavailable: fetch is not available');
72
+ }
73
+
74
+ const abortController = new AbortController();
75
+ let timedOut = false;
76
+ const timer = setTimeout(() => {
77
+ timedOut = true;
78
+ abortController.abort();
79
+ }, normalizeInboundMediaUrlTimeoutMs(timeoutMs));
80
+
81
+ try {
82
+ const response = await fetch(url, { signal: abortController.signal });
83
+ if (!response.ok) {
84
+ throw new Error(`inbound media url download failed: HTTP ${response.status}`);
85
+ }
86
+
87
+ const contentLength = Number(readResponseHeader(response.headers, 'content-length') || 0);
88
+ if (Number.isFinite(contentLength) && contentLength > maxBytes) {
89
+ abortController.abort();
90
+ throw new Error(
91
+ `inbound media url too large: content-length ${contentLength} bytes exceeds ${maxBytes} bytes`,
92
+ );
93
+ }
94
+
95
+ const buffer = await readInboundMediaResponseBody({
96
+ response,
97
+ maxBytes,
98
+ abort: () => abortController.abort(),
99
+ });
100
+ if (!buffer.length) throw new Error('inbound media url downloaded empty buffer');
101
+
102
+ return { buffer, contentType: readResponseHeader(response.headers, 'content-type') };
103
+ } catch (err) {
104
+ if (timedOut) throw new Error(`inbound media url download timed out after ${timeoutMs}ms`);
105
+ throw err;
106
+ } finally {
107
+ clearTimeout(timer);
108
+ }
109
+ }
@@ -0,0 +1,225 @@
1
+ import { emitBncrLogLine } from '../../core/logging.ts';
2
+ import {
3
+ formatDisplayScope,
4
+ normalizeInboundSessionKey,
5
+ withTaskSessionKey,
6
+ } from '../../core/targets.ts';
7
+ import type {
8
+ BncrEnqueueFromReply,
9
+ BncrInboundApi,
10
+ BncrInboundConfig,
11
+ BncrInboundContextPayload,
12
+ } from './contracts.ts';
13
+ import type { ParsedInbound } from './dispatch-prep.ts';
14
+ import { buildBncrNativeReplyDeliveryPayload } from './native-reply-delivery.ts';
15
+ import { resolveBncrChannelInboundRuntime } from './runtime-compat.ts';
16
+
17
+ export function logBncrNativeCommandEvent(
18
+ event: string,
19
+ fields: Record<string, unknown>,
20
+ options?: { debugOnly?: boolean; debugEnabled?: boolean },
21
+ ) {
22
+ if (options?.debugOnly && !options?.debugEnabled) return;
23
+ emitBncrLogLine('info', `[bncr] native-command ${JSON.stringify({ event, ...fields })}`);
24
+ }
25
+
26
+ export function logBncrNativeCommandSummary(message: string) {
27
+ emitBncrLogLine('info', `[bncr] native-command ${String(message || '').trim()}`);
28
+ }
29
+
30
+ export function buildBncrNativeCommandSummary(args: {
31
+ kind: string;
32
+ command: string;
33
+ accountId: string;
34
+ to: string;
35
+ msgId?: string | null;
36
+ result: string;
37
+ }) {
38
+ return `${args.kind} command=${args.command}|accountId=${args.accountId}|to=${args.to}|msgId=${args.msgId || '-'}|result=${args.result}`;
39
+ }
40
+
41
+ export function buildNativeCommandHandledResult(args: {
42
+ command: string;
43
+ sessionKey: string;
44
+ fallbackToAgent?: boolean;
45
+ }) {
46
+ return {
47
+ handled: true as const,
48
+ command: args.command,
49
+ sessionKey: args.sessionKey,
50
+ ...(args.fallbackToAgent ? { fallbackToAgent: true } : {}),
51
+ };
52
+ }
53
+
54
+ export function buildBncrNativeCommandSessionState(args: {
55
+ parsed: ParsedInbound;
56
+ canonicalAgentId: string;
57
+ resolvedRoute: { sessionKey: string };
58
+ }) {
59
+ const { parsed, canonicalAgentId, resolvedRoute } = args;
60
+ const baseSessionKey =
61
+ normalizeInboundSessionKey(parsed.sessionKeyfromroute, parsed.route, canonicalAgentId) ||
62
+ resolvedRoute.sessionKey;
63
+ const taskSessionKey = withTaskSessionKey(baseSessionKey, parsed.extracted.taskKey);
64
+ const sessionKey = taskSessionKey || baseSessionKey;
65
+ const displayTo = formatDisplayScope(parsed.route);
66
+ const originatingTo = parsed.providedOriginatingTo || displayTo;
67
+ return {
68
+ baseSessionKey,
69
+ taskSessionKey,
70
+ sessionKey,
71
+ displayTo,
72
+ originatingTo,
73
+ };
74
+ }
75
+
76
+ export function createNativeCommandTurnContext(args: {
77
+ api: BncrInboundApi;
78
+ channelId: string;
79
+ accountId: string;
80
+ msgId?: string;
81
+ peer: ParsedInbound['peer'];
82
+ resolvedRoute: { sessionKey: string; agentId: string; mainSessionKey?: string };
83
+ sessionKey: string;
84
+ displayTo: string;
85
+ originatingTo: string;
86
+ senderIdForContext: string;
87
+ senderDisplayName: string;
88
+ body: string;
89
+ }): BncrInboundContextPayload | Promise<BncrInboundContextPayload> {
90
+ return resolveBncrChannelInboundRuntime(args.api).buildContext({
91
+ channel: args.channelId,
92
+ provider: args.channelId,
93
+ surface: args.channelId,
94
+ accountId: args.accountId,
95
+ messageId: args.msgId,
96
+ timestamp: Date.now(),
97
+ from: args.senderIdForContext,
98
+ sender: {
99
+ id: args.senderIdForContext,
100
+ name: args.senderDisplayName,
101
+ username: args.senderDisplayName,
102
+ },
103
+ conversation: {
104
+ kind: args.peer.kind,
105
+ id: args.peer.id,
106
+ label: args.displayTo,
107
+ routePeer: {
108
+ kind: args.peer.kind,
109
+ id: args.peer.id,
110
+ },
111
+ },
112
+ route: {
113
+ agentId: args.resolvedRoute.agentId,
114
+ accountId: args.accountId,
115
+ routeSessionKey: args.resolvedRoute.sessionKey,
116
+ dispatchSessionKey: args.sessionKey,
117
+ mainSessionKey: args.resolvedRoute.mainSessionKey,
118
+ },
119
+ reply: {
120
+ to: args.displayTo,
121
+ originatingTo: args.originatingTo,
122
+ replyToId: args.msgId,
123
+ },
124
+ message: {
125
+ inboundEventKind: 'user_request',
126
+ body: args.body,
127
+ rawBody: args.body,
128
+ bodyForAgent: args.body,
129
+ commandBody: args.body,
130
+ envelopeFrom: args.originatingTo,
131
+ senderLabel: args.senderDisplayName,
132
+ },
133
+ commandTurn: {
134
+ kind: 'native',
135
+ source: 'native',
136
+ authorized: true,
137
+ body: args.body,
138
+ },
139
+ access: {
140
+ mentions: {
141
+ canDetectMention: true,
142
+ wasMentioned: true,
143
+ effectiveWasMentioned: true,
144
+ },
145
+ commands: {
146
+ authorized: true,
147
+ allowTextCommands: true,
148
+ useAccessGroups: false,
149
+ authorizers: [],
150
+ },
151
+ },
152
+ extra: {
153
+ OriginatingChannel: args.channelId,
154
+ },
155
+ });
156
+ }
157
+
158
+ export function createNativeCommandReplyDeliverer(args: {
159
+ command: string;
160
+ accountId: string;
161
+ sessionKey: string;
162
+ to: string;
163
+ msgId?: string;
164
+ effectiveReply: { blockStreaming: boolean; allowTool: boolean };
165
+ route: ParsedInbound['route'];
166
+ enqueueFromReply: BncrEnqueueFromReply;
167
+ nativeCommandDebugEnabled: boolean;
168
+ onResponded: () => boolean;
169
+ markResponded: () => void;
170
+ }) {
171
+ return async (
172
+ payload: {
173
+ text?: string;
174
+ mediaUrl?: string;
175
+ mediaUrls?: string[];
176
+ audioAsVoice?: boolean;
177
+ },
178
+ info?: { kind?: 'tool' | 'block' | 'final' },
179
+ ) => {
180
+ const kind = info?.kind;
181
+ const deliveryPayload = buildBncrNativeReplyDeliveryPayload({
182
+ payload,
183
+ kind,
184
+ effectiveReply: args.effectiveReply,
185
+ msgId: args.msgId,
186
+ });
187
+ if (!deliveryPayload) return;
188
+
189
+ if (!args.onResponded()) {
190
+ logBncrNativeCommandEvent(
191
+ 'payload-produced',
192
+ {
193
+ command: args.command,
194
+ accountId: args.accountId,
195
+ sessionKey: args.sessionKey,
196
+ to: args.to,
197
+ msgId: args.msgId || null,
198
+ kind: kind || null,
199
+ fallbackToAgent: false,
200
+ },
201
+ { debugOnly: true, debugEnabled: args.nativeCommandDebugEnabled },
202
+ );
203
+ }
204
+
205
+ args.markResponded();
206
+ await args.enqueueFromReply({
207
+ accountId: args.accountId,
208
+ sessionKey: args.sessionKey,
209
+ route: args.route,
210
+ payload: deliveryPayload,
211
+ replyTargetPolicy: 'preserve',
212
+ });
213
+ };
214
+ }
215
+
216
+ export function buildNativeCommandRecordErrorLogger(err: unknown) {
217
+ emitBncrLogLine('warn', `[bncr] inbound record native command session failed: ${String(err)}`);
218
+ }
219
+
220
+ export function resolveNativeCommandDebugEnabled(args: {
221
+ cfg: BncrInboundConfig;
222
+ channelId: string;
223
+ }) {
224
+ return args.cfg?.channels?.[args.channelId]?.debug?.verbose === true;
225
+ }
@@ -2,6 +2,7 @@ import { createHash } from 'node:crypto';
2
2
  import { normalizeAccountId } from '../../core/accounts.ts';
3
3
  import { extractInlineTaskKey } from '../../core/targets.ts';
4
4
  import type { BncrRoute } from '../../core/types.ts';
5
+ import type { BncrInboundParamsInput } from './contracts.ts';
5
6
 
6
7
  function asString(v: unknown, fallback = ''): string {
7
8
  if (typeof v === 'string') return v;
@@ -43,7 +44,7 @@ export function resolveChatType(_route: BncrRoute): 'direct' | 'group' {
43
44
  return 'direct';
44
45
  }
45
46
 
46
- export function parseBncrInboundParams(params: any) {
47
+ export function parseBncrInboundParams(params: BncrInboundParamsInput) {
47
48
  const accountId = normalizeAccountId(asString(params?.accountId || ''));
48
49
  const platform = asString(params?.platform || '').trim();
49
50
  const groupId = asString(params?.groupId || '0').trim() || '0';
@@ -0,0 +1,49 @@
1
+ import type { OpenClawLoadedMedia } from '../../openclaw/media-runtime.ts';
2
+ import { downloadInboundMediaUrl, INBOUND_MEDIA_URL_MAX_BYTES } from './media-url-download.ts';
3
+
4
+ type RuntimeChannelMediaReader = {
5
+ runtime?: {
6
+ channel?: {
7
+ media?: {
8
+ readRemoteMediaBuffer?: (options: {
9
+ url: string;
10
+ maxBytes?: number;
11
+ }) => Promise<OpenClawLoadedMedia>;
12
+ };
13
+ };
14
+ };
15
+ };
16
+
17
+ function assertLoadedInboundRemoteMedia(
18
+ loaded: OpenClawLoadedMedia,
19
+ source: 'runtime' | 'fallback',
20
+ ): OpenClawLoadedMedia {
21
+ if (!loaded?.buffer?.length) {
22
+ throw new Error(`inbound remote media ${source} download returned empty buffer`);
23
+ }
24
+ return loaded;
25
+ }
26
+
27
+ async function readRemoteMediaBufferFromRuntime(
28
+ api: RuntimeChannelMediaReader,
29
+ url: string,
30
+ maxBytes: number,
31
+ ): Promise<OpenClawLoadedMedia> {
32
+ const readRemoteMediaBuffer = api?.runtime?.channel?.media?.readRemoteMediaBuffer;
33
+ if (typeof readRemoteMediaBuffer !== 'function') {
34
+ throw new Error('OpenClaw channel media readRemoteMediaBuffer API is unavailable');
35
+ }
36
+ return assertLoadedInboundRemoteMedia(await readRemoteMediaBuffer({ url, maxBytes }), 'runtime');
37
+ }
38
+
39
+ export async function loadInboundRemoteMedia(
40
+ api: RuntimeChannelMediaReader,
41
+ url: string,
42
+ maxBytes = INBOUND_MEDIA_URL_MAX_BYTES,
43
+ ): Promise<OpenClawLoadedMedia> {
44
+ try {
45
+ return await readRemoteMediaBufferFromRuntime(api, url, maxBytes);
46
+ } catch {
47
+ return assertLoadedInboundRemoteMedia(await downloadInboundMediaUrl(url, maxBytes), 'fallback');
48
+ }
49
+ }
@@ -1,7 +1,9 @@
1
+ import type { BncrInboundConfig } from './contracts.ts';
2
+
1
3
  type BncrReplyConfigResult = {
2
4
  blockStreaming: boolean;
3
5
  allowTool: boolean;
4
- replyCfg: any;
6
+ replyCfg: BncrInboundConfig;
5
7
  };
6
8
 
7
9
  function parseBooleanLike(value: unknown): boolean | undefined {
@@ -20,7 +22,7 @@ function parseBooleanLike(value: unknown): boolean | undefined {
20
22
  return undefined;
21
23
  }
22
24
 
23
- export function resolveBncrBlockStreaming(cfg: any): boolean {
25
+ export function resolveBncrBlockStreaming(cfg: BncrInboundConfig): boolean {
24
26
  const channelValue = parseBooleanLike(cfg?.channels?.bncr?.blockStreaming);
25
27
  if (channelValue !== undefined) return channelValue;
26
28
 
@@ -30,11 +32,11 @@ export function resolveBncrBlockStreaming(cfg: any): boolean {
30
32
  return true;
31
33
  }
32
34
 
33
- export function resolveBncrAllowTool(cfg: any): boolean {
35
+ export function resolveBncrAllowTool(cfg: BncrInboundConfig): boolean {
34
36
  return cfg?.channels?.bncr?.allowTool === true;
35
37
  }
36
38
 
37
- export function buildBncrReplyConfig(cfg: any): BncrReplyConfigResult {
39
+ export function buildBncrReplyConfig(cfg: BncrInboundConfig): BncrReplyConfigResult {
38
40
  const blockStreaming = resolveBncrBlockStreaming(cfg);
39
41
  const allowTool = resolveBncrAllowTool(cfg);
40
42
 
@@ -46,6 +48,16 @@ export function buildBncrReplyConfig(cfg: any): BncrReplyConfigResult {
46
48
  ...(cfg?.agents?.defaults ?? {}),
47
49
  },
48
50
  },
51
+ } as BncrInboundConfig & {
52
+ agents: {
53
+ defaults: {
54
+ blockStreamingBreak?: string;
55
+ blockStreamingChunk?: { minChars: number; maxChars: number };
56
+ blockStreamingDefault?: unknown;
57
+ [key: string]: unknown;
58
+ };
59
+ [key: string]: unknown;
60
+ };
49
61
  };
50
62
 
51
63
  if (replyCfg.agents.defaults.blockStreamingBreak == null) {
@@ -0,0 +1,162 @@
1
+ import { emitBncrLogLine } from '../../core/logging.ts';
2
+ import { resolveBncrChannelPolicy } from '../../core/policy.ts';
3
+ import {
4
+ recordBncrInboundSession,
5
+ resolveBncrPinnedMainDmOwnerFromAllowlist,
6
+ } from '../../openclaw/inbound-session-runtime.ts';
7
+ import { dispatchOpenClawReplyWithBufferedBlockDispatcher } from '../../openclaw/reply-runtime.ts';
8
+ import type {
9
+ BncrEnqueueFromReply,
10
+ BncrInboundApi,
11
+ BncrInboundConfig,
12
+ BncrInboundContextPayload,
13
+ } from './contracts.ts';
14
+ import type {
15
+ BncrInboundConversationResolution,
16
+ BncrInboundReplyRouteFact,
17
+ ParsedInbound,
18
+ } from './dispatch-prep.ts';
19
+ import { buildBncrInboundRecordUpdateLastRoute } from './last-route.ts';
20
+ import { buildBncrReplyConfig } from './reply-config.ts';
21
+ import { resolveBncrChannelInboundRuntime } from './runtime-compat.ts';
22
+ import { wrapBncrInboundRecordSessionLabelCorrection } from './session-label.ts';
23
+
24
+ export async function runBncrInboundReplyDispatch(args: {
25
+ api: BncrInboundApi;
26
+ channelId: string;
27
+ cfg: BncrInboundConfig;
28
+ parsed: ParsedInbound;
29
+ msgId?: string | null;
30
+ peer: ParsedInbound['peer'];
31
+ rawBody: string;
32
+ storePath: string;
33
+ ctxPayload: BncrInboundContextPayload;
34
+ resolution: BncrInboundConversationResolution;
35
+ replyRouteFact: BncrInboundReplyRouteFact;
36
+ senderIdForContext: string;
37
+ setInboundActivity: (accountId: string, at: number) => void;
38
+ scheduleSave: () => void;
39
+ enqueueFromReply: BncrEnqueueFromReply;
40
+ }) {
41
+ const {
42
+ api,
43
+ channelId,
44
+ cfg,
45
+ parsed,
46
+ msgId,
47
+ peer,
48
+ rawBody,
49
+ storePath,
50
+ ctxPayload,
51
+ resolution,
52
+ replyRouteFact,
53
+ senderIdForContext,
54
+ setInboundActivity,
55
+ scheduleSave,
56
+ enqueueFromReply,
57
+ } = args;
58
+
59
+ const effectiveReply = buildBncrReplyConfig(cfg);
60
+ const channelPolicy = resolveBncrChannelPolicy(cfg?.channels?.bncr || {});
61
+ const pinnedMainDmOwner =
62
+ peer.kind === 'direct'
63
+ ? resolveBncrPinnedMainDmOwnerFromAllowlist({
64
+ dmScope: cfg?.session?.dmScope as string | undefined,
65
+ allowFrom: channelPolicy.allowFrom,
66
+ normalizeEntry: (entry: string) => String(entry || '').trim(),
67
+ })
68
+ : null;
69
+ const updateLastRoute = buildBncrInboundRecordUpdateLastRoute({
70
+ channelId,
71
+ peerKind: peer.kind,
72
+ senderIdForContext,
73
+ accountId: resolution.accountId,
74
+ to: resolution.canonicalTo,
75
+ resolvedRoute: resolution.resolvedRoute,
76
+ sessionKey: resolution.dispatchSessionKey,
77
+ pinnedMainDmOwner,
78
+ });
79
+
80
+ await resolveBncrChannelInboundRuntime(api).run({
81
+ channel: channelId,
82
+ accountId: resolution.accountId,
83
+ raw: parsed,
84
+ adapter: {
85
+ ingest: () => ({
86
+ id: msgId ?? `${resolution.canonicalTo}:${Date.now()}`,
87
+ timestamp: Date.now(),
88
+ rawText: rawBody,
89
+ textForAgent: ctxPayload.BodyForAgent,
90
+ textForCommands: ctxPayload.CommandBody,
91
+ raw: parsed,
92
+ }),
93
+ resolveTurn: () => ({
94
+ channel: channelId,
95
+ accountId: resolution.accountId,
96
+ routeSessionKey: resolution.resolvedRoute.sessionKey,
97
+ storePath,
98
+ ctxPayload,
99
+ recordInboundSession: wrapBncrInboundRecordSessionLabelCorrection({
100
+ recordInboundSession: recordBncrInboundSession as (
101
+ ...args: unknown[]
102
+ ) => Promise<unknown> | unknown,
103
+ expectedLabel: resolution.canonicalTo,
104
+ }),
105
+ record: {
106
+ updateLastRoute,
107
+ onRecordError: (err: unknown) => {
108
+ emitBncrLogLine('warn', `[bncr] inbound record session failed: ${String(err)}`);
109
+ },
110
+ },
111
+ runDispatch: () =>
112
+ dispatchOpenClawReplyWithBufferedBlockDispatcher(api, {
113
+ ctx: ctxPayload,
114
+ cfg: effectiveReply.replyCfg,
115
+ dispatcherOptions: {
116
+ deliver: async (
117
+ payload: {
118
+ text?: string;
119
+ mediaUrl?: string;
120
+ mediaUrls?: string[];
121
+ audioAsVoice?: boolean;
122
+ },
123
+ info?: { kind?: 'tool' | 'block' | 'final' },
124
+ ) => {
125
+ const kind = info?.kind;
126
+ const shouldForwardTool = effectiveReply.blockStreaming && effectiveReply.allowTool;
127
+
128
+ if (kind === 'tool' && !shouldForwardTool) {
129
+ return;
130
+ }
131
+
132
+ await enqueueFromReply({
133
+ accountId: replyRouteFact.accountId,
134
+ sessionKey: replyRouteFact.sessionKey,
135
+ route: replyRouteFact.route,
136
+ payload: {
137
+ text: payload.text,
138
+ mediaUrl: payload.mediaUrl,
139
+ mediaUrls: payload.mediaUrls,
140
+ kind: kind || 'final',
141
+ replyToId: msgId || undefined,
142
+ },
143
+ });
144
+ },
145
+ onError: (err: unknown) => {
146
+ emitBncrLogLine('error', `[bncr] outbound reply failed: ${String(err)}`);
147
+ },
148
+ },
149
+ replyOptions: {
150
+ disableBlockStreaming: !effectiveReply.blockStreaming,
151
+ shouldEmitToolResult: effectiveReply.allowTool ? () => true : undefined,
152
+ },
153
+ }),
154
+ }),
155
+ onFinalize: () => {
156
+ const inboundAt = Date.now();
157
+ setInboundActivity(resolution.accountId, inboundAt);
158
+ scheduleSave();
159
+ },
160
+ },
161
+ });
162
+ }
@@ -1,19 +1,40 @@
1
1
  import { emitBncrLogLine } from '../../core/logging.ts';
2
+ import type { OpenClawInboundRuntime } from '../../openclaw/channel-runtime-contracts.ts';
3
+ import type { BncrInboundApi } from './contracts.ts';
2
4
 
3
- type ChannelRuntimeCompat = {
4
- buildContext: (...args: any[]) => any;
5
- run: (...args: any[]) => Promise<any> | any;
6
- runPreparedReply?: (...args: any[]) => Promise<any> | any;
7
- dispatchReply?: (...args: any[]) => Promise<any> | any;
5
+ type InboundRuntimeShape = {
6
+ buildContext?: (params: unknown) => unknown;
7
+ run?: (params: unknown) => unknown;
8
+ runPreparedReply?: (params: unknown) => unknown;
9
+ dispatchReply?: (params: unknown) => unknown;
10
+ };
11
+
12
+ type LegacyTurnRuntimeShape = {
13
+ buildContext?: (params: unknown) => unknown;
14
+ run?: (params: unknown) => unknown;
15
+ runPrepared?: (params: unknown) => unknown;
16
+ dispatchAssembled?: (params: unknown) => unknown;
17
+ runAssembled?: (params: unknown) => unknown;
18
+ };
19
+
20
+ type ChannelRuntimeShape = {
21
+ inbound?: InboundRuntimeShape;
22
+ turn?: LegacyTurnRuntimeShape;
23
+ [key: string]: unknown;
8
24
  };
9
25
 
10
26
  let warnedLegacyTurnRuntime = false;
11
27
 
12
- export function resolveBncrChannelInboundRuntime(api: any): ChannelRuntimeCompat {
13
- const channelRuntime = api?.runtime?.channel;
28
+ export function resolveBncrChannelInboundRuntime(api: BncrInboundApi): OpenClawInboundRuntime {
29
+ const channelRuntime = api?.runtime?.channel as ChannelRuntimeShape | undefined;
14
30
  const inboundRuntime = channelRuntime?.inbound;
15
31
  if (inboundRuntime?.buildContext && inboundRuntime?.run) {
16
- return inboundRuntime;
32
+ return {
33
+ buildContext: inboundRuntime.buildContext as OpenClawInboundRuntime['buildContext'],
34
+ run: inboundRuntime.run as OpenClawInboundRuntime['run'],
35
+ runPreparedReply: inboundRuntime.runPreparedReply,
36
+ dispatchReply: inboundRuntime.dispatchReply,
37
+ };
17
38
  }
18
39
 
19
40
  const legacyTurnRuntime = channelRuntime?.turn;
@@ -34,8 +55,8 @@ export function resolveBncrChannelInboundRuntime(api: any): ChannelRuntimeCompat
34
55
  );
35
56
  }
36
57
  return {
37
- buildContext: legacyTurnRuntime.buildContext,
38
- run: legacyTurnRuntime.run,
58
+ buildContext: legacyTurnRuntime.buildContext as OpenClawInboundRuntime['buildContext'],
59
+ run: legacyTurnRuntime.run as OpenClawInboundRuntime['run'],
39
60
  runPreparedReply: legacyTurnRuntime.runPrepared,
40
61
  dispatchReply: legacyTurnRuntime.dispatchAssembled ?? legacyTurnRuntime.runAssembled,
41
62
  };