@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,181 @@
1
+ import { buildBncrDebugJsonMessage } from '../../core/logging.ts';
2
+ import type { BncrRoute, OutboxEntry } from '../../core/types.ts';
3
+ import { buildReplyMediaFallbackDebugInfo } from './diagnostics.ts';
4
+ import type {
5
+ EnqueueSingleReplyMediaEntryParams,
6
+ NormalizedReplyPayload,
7
+ ReplyMediaEntriesParams,
8
+ ReplyMediaFallbackTextEntryParams,
9
+ ReplyMediaFileTransferParams,
10
+ } from './reply-enqueue.ts';
11
+ import type { OutboundReplyTargetPolicy } from './reply-target-policy.ts';
12
+
13
+ export function enqueueReplyMediaFallbackTextEntry(
14
+ params: ReplyMediaFallbackTextEntryParams,
15
+ helpers: {
16
+ logInfo: (
17
+ scope: string | undefined,
18
+ message: string,
19
+ options?: { debugOnly?: boolean },
20
+ ) => void;
21
+ enqueueOutbound: (entry: OutboxEntry) => void;
22
+ buildTextOutboxEntry: (args: {
23
+ accountId: string;
24
+ sessionKey: string;
25
+ route: BncrRoute;
26
+ text: string;
27
+ kind?: 'tool' | 'block' | 'final';
28
+ replyToId?: string;
29
+ replyTargetPolicy?: OutboundReplyTargetPolicy;
30
+ }) => OutboxEntry;
31
+ },
32
+ ): void {
33
+ helpers.logInfo(
34
+ 'outbound',
35
+ buildBncrDebugJsonMessage('media-dedupe-hit', buildReplyMediaFallbackDebugInfo(params)),
36
+ { debugOnly: true },
37
+ );
38
+ helpers.enqueueOutbound(
39
+ helpers.buildTextOutboxEntry({
40
+ accountId: params.accountId,
41
+ sessionKey: params.sessionKey,
42
+ route: params.route,
43
+ text: params.fallback.text,
44
+ kind: params.kind,
45
+ replyToId: params.replyToId || undefined,
46
+ replyTargetPolicy: params.replyTargetPolicy,
47
+ }),
48
+ );
49
+ }
50
+
51
+ export function enqueueReplyMediaFileTransferEntry(
52
+ params: ReplyMediaFileTransferParams,
53
+ helpers: {
54
+ enqueueOutbound: (entry: OutboxEntry) => void;
55
+ buildFileTransferOutboxEntry: (args: {
56
+ accountId: string;
57
+ sessionKey: string;
58
+ route: BncrRoute;
59
+ mediaUrl: string;
60
+ mediaLocalRoots?: readonly string[];
61
+ text: string;
62
+ asVoice: boolean;
63
+ audioAsVoice: boolean;
64
+ type?: string;
65
+ kind?: 'tool' | 'block' | 'final';
66
+ replyToId?: string;
67
+ replyTargetPolicy?: OutboundReplyTargetPolicy;
68
+ }) => OutboxEntry;
69
+ rememberRecentMediaSend: (args: {
70
+ sessionKey: string;
71
+ mediaUrl: string;
72
+ text: string;
73
+ replyToId: string;
74
+ createdAt: number;
75
+ }) => void;
76
+ },
77
+ ): void {
78
+ helpers.enqueueOutbound(
79
+ helpers.buildFileTransferOutboxEntry({
80
+ accountId: params.accountId,
81
+ sessionKey: params.sessionKey,
82
+ route: params.route,
83
+ mediaUrl: params.mediaUrl,
84
+ mediaLocalRoots: params.mediaLocalRoots,
85
+ text: params.text,
86
+ asVoice: params.asVoice,
87
+ audioAsVoice: params.audioAsVoice,
88
+ type: params.type,
89
+ kind: params.kind,
90
+ replyToId: params.replyToId || undefined,
91
+ replyTargetPolicy: params.replyTargetPolicy,
92
+ }),
93
+ );
94
+ helpers.rememberRecentMediaSend({
95
+ sessionKey: params.sessionKey,
96
+ mediaUrl: params.mediaUrl,
97
+ text: params.normalizedText,
98
+ replyToId: params.replyToId,
99
+ createdAt: params.createdAt,
100
+ });
101
+ }
102
+
103
+ export function enqueueSingleReplyMediaEntry(
104
+ params: EnqueueSingleReplyMediaEntryParams,
105
+ helpers: {
106
+ enqueueReplyMediaFallbackTextEntry: (params: ReplyMediaFallbackTextEntryParams) => void;
107
+ enqueueReplyMediaFileTransferEntry: (params: ReplyMediaFileTransferParams) => void;
108
+ },
109
+ ): void {
110
+ if (params.fallback !== null) {
111
+ helpers.enqueueReplyMediaFallbackTextEntry({
112
+ accountId: params.params.accountId,
113
+ sessionKey: params.params.sessionKey,
114
+ route: params.params.route,
115
+ mediaUrl: params.mediaUrl,
116
+ kind: params.params.payload.kind,
117
+ replyToId: params.params.payload.replyToId,
118
+ replyTargetPolicy: params.params.payload.replyTargetPolicy,
119
+ fallback: params.fallback,
120
+ });
121
+ return;
122
+ }
123
+
124
+ helpers.enqueueReplyMediaFileTransferEntry({
125
+ accountId: params.params.accountId,
126
+ sessionKey: params.params.sessionKey,
127
+ route: params.params.route,
128
+ mediaUrl: params.mediaUrl,
129
+ mediaLocalRoots: params.params.mediaLocalRoots,
130
+ text: params.text,
131
+ normalizedText: params.normalizedText,
132
+ asVoice: params.params.payload.asVoice,
133
+ audioAsVoice: params.params.payload.audioAsVoice,
134
+ type: params.params.payload.type,
135
+ kind: params.params.payload.kind,
136
+ replyToId: params.params.payload.replyToId,
137
+ replyTargetPolicy: params.params.payload.replyTargetPolicy,
138
+ createdAt: params.currentTime,
139
+ });
140
+ }
141
+
142
+ export function enqueueReplyMediaEntries(
143
+ params: ReplyMediaEntriesParams,
144
+ helpers: {
145
+ now: () => number;
146
+ normalizeMessageText: (text: string) => string;
147
+ tryBuildMediaDedupeFallback: (args: {
148
+ sessionKey: string;
149
+ mediaUrl: string;
150
+ text: string;
151
+ replyToId: string;
152
+ currentTime: number;
153
+ }) => { text: string; reason: string } | null;
154
+ enqueueSingleReplyMediaEntry: (params: EnqueueSingleReplyMediaEntryParams) => void;
155
+ },
156
+ ): void {
157
+ const currentTime = helpers.now();
158
+ const normalizedText = helpers.normalizeMessageText(params.payload.text);
159
+
160
+ for (const mediaUrl of params.payload.mediaList) {
161
+ const fallback = helpers.tryBuildMediaDedupeFallback({
162
+ sessionKey: params.sessionKey,
163
+ mediaUrl,
164
+ text: normalizedText,
165
+ replyToId: params.payload.replyToId,
166
+ currentTime,
167
+ });
168
+ helpers.enqueueSingleReplyMediaEntry({
169
+ params,
170
+ mediaUrl,
171
+ normalizedText,
172
+ text: params.payload.text,
173
+ fallback,
174
+ currentTime,
175
+ });
176
+ }
177
+ }
178
+
179
+ export function hasReplyMediaEntries(payload: NormalizedReplyPayload) {
180
+ return payload.mediaList.length > 0;
181
+ }
@@ -1,8 +1,15 @@
1
1
  import type { BncrRoute, OutboxEntry } from '../../core/types.ts';
2
- import { buildReplyMediaFallbackDebugInfo } from './diagnostics.ts';
2
+ import { hasReplyMediaEntries } from './reply-enqueue-media.ts';
3
3
  import type { OutboundReplyTargetPolicy } from './reply-target-policy.ts';
4
4
  import { normalizeOutboundReplyToId } from './reply-target-policy.ts';
5
5
 
6
+ const MEDIA_TEXT_SPLIT_THRESHOLD = 1020;
7
+
8
+ export type ReplyEnqueuePlan =
9
+ | { kind: 'text-only' }
10
+ | { kind: 'media-only'; clearText: false }
11
+ | { kind: 'text-and-media'; clearText: true };
12
+
6
13
  export type { OutboundReplyTargetPolicy } from './reply-target-policy.ts';
7
14
 
8
15
  export type ReplyPayloadInput = {
@@ -11,6 +18,7 @@ export type ReplyPayloadInput = {
11
18
  mediaUrls?: string[];
12
19
  asVoice?: boolean;
13
20
  audioAsVoice?: boolean;
21
+ type?: string;
14
22
  kind?: 'tool' | 'block' | 'final';
15
23
  replyToId?: string;
16
24
  };
@@ -22,6 +30,7 @@ export type NormalizedReplyPayload = {
22
30
  mediaList: string[];
23
31
  asVoice: boolean;
24
32
  audioAsVoice: boolean;
33
+ type?: string;
25
34
  kind?: 'tool' | 'block' | 'final';
26
35
  replyToId: string;
27
36
  replyTargetPolicy: OutboundReplyTargetPolicy;
@@ -41,6 +50,7 @@ export type EnqueueNormalizedReplyPayloadParams = {
41
50
  route: BncrRoute;
42
51
  payload: NormalizedReplyPayload;
43
52
  mediaLocalRoots?: readonly string[];
53
+ replyToId?: string;
44
54
  };
45
55
 
46
56
  export type ReplyMediaFileTransferParams = {
@@ -53,6 +63,7 @@ export type ReplyMediaFileTransferParams = {
53
63
  normalizedText: string;
54
64
  asVoice: boolean;
55
65
  audioAsVoice: boolean;
66
+ type?: string;
56
67
  kind?: 'tool' | 'block' | 'final';
57
68
  replyToId: string;
58
69
  replyTargetPolicy: OutboundReplyTargetPolicy;
@@ -79,8 +90,29 @@ export type ReplyMediaFallbackTextEntryParams = {
79
90
  fallback: { text: string; reason: string };
80
91
  };
81
92
 
82
- export function hasReplyMediaEntries(payload: NormalizedReplyPayload) {
83
- return payload.mediaList.length > 0;
93
+ export function shouldSplitReplyMediaText(payload: NormalizedReplyPayload) {
94
+ if (!payload.text) return false;
95
+ if (payload.mediaList.length > 1) return true;
96
+ return payload.text.length > MEDIA_TEXT_SPLIT_THRESHOLD;
97
+ }
98
+
99
+ export function buildReplyEnqueuePlan(payload: NormalizedReplyPayload): ReplyEnqueuePlan {
100
+ if (!hasReplyMediaEntries(payload)) {
101
+ return { kind: 'text-only' };
102
+ }
103
+
104
+ if (shouldSplitReplyMediaText(payload)) {
105
+ return { kind: 'text-and-media', clearText: true };
106
+ }
107
+
108
+ return { kind: 'media-only', clearText: false };
109
+ }
110
+
111
+ export function withoutReplyMediaText(payload: NormalizedReplyPayload): NormalizedReplyPayload {
112
+ return {
113
+ ...payload,
114
+ text: '',
115
+ };
84
116
  }
85
117
 
86
118
  export function buildReplyTextOutboxEntry(
@@ -154,135 +186,6 @@ export function enqueueReplyTextEntry(
154
186
  );
155
187
  }
156
188
 
157
- export function enqueueReplyMediaFallbackTextEntry(
158
- params: ReplyMediaFallbackTextEntryParams,
159
- helpers: {
160
- logInfo: (
161
- scope: string | undefined,
162
- message: string,
163
- options?: { debugOnly?: boolean },
164
- ) => void;
165
- enqueueOutbound: (entry: OutboxEntry) => void;
166
- buildTextOutboxEntry: (args: {
167
- accountId: string;
168
- sessionKey: string;
169
- route: BncrRoute;
170
- text: string;
171
- kind?: 'tool' | 'block' | 'final';
172
- replyToId?: string;
173
- replyTargetPolicy?: OutboundReplyTargetPolicy;
174
- }) => OutboxEntry;
175
- },
176
- ): void {
177
- helpers.logInfo(
178
- 'outbound',
179
- `media-dedupe-hit ${JSON.stringify(buildReplyMediaFallbackDebugInfo(params))}`,
180
- { debugOnly: true },
181
- );
182
- helpers.enqueueOutbound(
183
- buildReplyTextOutboxEntry(
184
- {
185
- accountId: params.accountId,
186
- sessionKey: params.sessionKey,
187
- route: params.route,
188
- text: params.fallback.text,
189
- kind: params.kind,
190
- replyToId: params.replyToId,
191
- replyTargetPolicy: params.replyTargetPolicy,
192
- },
193
- { buildTextOutboxEntry: helpers.buildTextOutboxEntry },
194
- ),
195
- );
196
- }
197
-
198
- export function enqueueReplyMediaFileTransferEntry(
199
- params: ReplyMediaFileTransferParams,
200
- helpers: {
201
- enqueueOutbound: (entry: OutboxEntry) => void;
202
- buildFileTransferOutboxEntry: (args: {
203
- accountId: string;
204
- sessionKey: string;
205
- route: BncrRoute;
206
- mediaUrl: string;
207
- mediaLocalRoots?: readonly string[];
208
- text: string;
209
- asVoice: boolean;
210
- audioAsVoice: boolean;
211
- kind?: 'tool' | 'block' | 'final';
212
- replyToId?: string;
213
- replyTargetPolicy?: OutboundReplyTargetPolicy;
214
- }) => OutboxEntry;
215
- rememberRecentMediaSend: (args: {
216
- sessionKey: string;
217
- mediaUrl: string;
218
- text: string;
219
- replyToId: string;
220
- createdAt: number;
221
- }) => void;
222
- },
223
- ): void {
224
- helpers.enqueueOutbound(
225
- helpers.buildFileTransferOutboxEntry({
226
- accountId: params.accountId,
227
- sessionKey: params.sessionKey,
228
- route: params.route,
229
- mediaUrl: params.mediaUrl,
230
- mediaLocalRoots: params.mediaLocalRoots,
231
- text: params.text,
232
- asVoice: params.asVoice,
233
- audioAsVoice: params.audioAsVoice,
234
- kind: params.kind,
235
- replyToId: params.replyToId || undefined,
236
- replyTargetPolicy: params.replyTargetPolicy,
237
- }),
238
- );
239
- helpers.rememberRecentMediaSend({
240
- sessionKey: params.sessionKey,
241
- mediaUrl: params.mediaUrl,
242
- text: params.normalizedText,
243
- replyToId: params.replyToId,
244
- createdAt: params.createdAt,
245
- });
246
- }
247
-
248
- export function enqueueSingleReplyMediaEntry(
249
- params: EnqueueSingleReplyMediaEntryParams,
250
- helpers: {
251
- enqueueReplyMediaFallbackTextEntry: (params: ReplyMediaFallbackTextEntryParams) => void;
252
- enqueueReplyMediaFileTransferEntry: (params: ReplyMediaFileTransferParams) => void;
253
- },
254
- ): void {
255
- if (params.fallback !== null) {
256
- helpers.enqueueReplyMediaFallbackTextEntry({
257
- accountId: params.params.accountId,
258
- sessionKey: params.params.sessionKey,
259
- route: params.params.route,
260
- mediaUrl: params.mediaUrl,
261
- kind: params.params.payload.kind,
262
- replyToId: params.params.payload.replyToId,
263
- replyTargetPolicy: params.params.payload.replyTargetPolicy,
264
- fallback: params.fallback,
265
- });
266
- return;
267
- }
268
-
269
- helpers.enqueueReplyMediaFileTransferEntry({
270
- accountId: params.params.accountId,
271
- sessionKey: params.params.sessionKey,
272
- route: params.params.route,
273
- mediaUrl: params.mediaUrl,
274
- mediaLocalRoots: params.params.mediaLocalRoots,
275
- text: params.text,
276
- normalizedText: params.normalizedText,
277
- asVoice: params.params.payload.asVoice,
278
- audioAsVoice: params.params.payload.audioAsVoice,
279
- kind: params.params.payload.kind,
280
- replyToId: params.params.payload.replyToId,
281
- replyTargetPolicy: params.params.payload.replyTargetPolicy,
282
- createdAt: params.currentTime,
283
- });
284
- }
285
-
286
189
  export function enqueueNormalizedReplyPayload(
287
190
  params: EnqueueNormalizedReplyPayloadParams,
288
191
  helpers: {
@@ -292,7 +195,6 @@ export function enqueueNormalizedReplyPayload(
292
195
  route: BncrRoute;
293
196
  payload: NormalizedReplyPayload;
294
197
  }) => void;
295
- hasReplyMediaEntries: (payload: NormalizedReplyPayload) => boolean;
296
198
  enqueueReplyMediaEntries: (params: ReplyMediaEntriesParams) => void;
297
199
  enqueueReplyTextEntry: (params: {
298
200
  accountId: string;
@@ -309,7 +211,26 @@ export function enqueueNormalizedReplyPayload(
309
211
  payload: params.payload,
310
212
  });
311
213
 
312
- if (helpers.hasReplyMediaEntries(params.payload)) {
214
+ const plan = buildReplyEnqueuePlan(params.payload);
215
+
216
+ if (plan.kind === 'text-and-media') {
217
+ helpers.enqueueReplyTextEntry({
218
+ accountId: params.accountId,
219
+ sessionKey: params.sessionKey,
220
+ route: params.route,
221
+ payload: params.payload,
222
+ });
223
+ helpers.enqueueReplyMediaEntries({
224
+ accountId: params.accountId,
225
+ sessionKey: params.sessionKey,
226
+ route: params.route,
227
+ payload: withoutReplyMediaText(params.payload),
228
+ mediaLocalRoots: params.mediaLocalRoots,
229
+ });
230
+ return;
231
+ }
232
+
233
+ if (plan.kind !== 'text-only') {
313
234
  helpers.enqueueReplyMediaEntries({
314
235
  accountId: params.accountId,
315
236
  sessionKey: params.sessionKey,
@@ -338,6 +259,7 @@ export function normalizeReplyPayload(
338
259
  const mediaUrls = Array.isArray(payload?.mediaUrls)
339
260
  ? payload.mediaUrls.map((v) => helpers.asString(v || '').trim()).filter(Boolean)
340
261
  : undefined;
262
+ const type = helpers.asString(payload?.type || '').trim();
341
263
  return {
342
264
  text,
343
265
  mediaUrl,
@@ -345,6 +267,7 @@ export function normalizeReplyPayload(
345
267
  mediaList: mediaUrls?.length ? mediaUrls : mediaUrl ? [mediaUrl] : [],
346
268
  asVoice: payload?.asVoice === true,
347
269
  audioAsVoice: payload?.audioAsVoice === true,
270
+ ...(type ? { type } : {}),
348
271
  kind: payload?.kind,
349
272
  replyTargetPolicy: options?.replyTargetPolicy ?? 'agent-default',
350
273
  replyToId: normalizeOutboundReplyToId({
@@ -9,6 +9,7 @@ export type NormalizedBncrSendParams = {
9
9
  mediaUrl?: string;
10
10
  asVoice: boolean;
11
11
  audioAsVoice: boolean;
12
+ type?: string;
12
13
  };
13
14
 
14
15
  function isPlainObject(value: unknown): value is Record<string, unknown> {
@@ -34,6 +35,7 @@ export function normalizeBncrSendParams(input: {
34
35
  readOpenClawStringParam(paramsObj, 'mediaUrl', { trim: false });
35
36
  const asVoice = readOpenClawBooleanParam(paramsObj, 'asVoice') ?? false;
36
37
  const audioAsVoice = readOpenClawBooleanParam(paramsObj, 'audioAsVoice') ?? false;
38
+ const type = readOpenClawStringParam(paramsObj, 'type') || undefined;
37
39
 
38
40
  if (asVoice && !mediaUrl) throw new Error('send voice requires media path');
39
41
 
@@ -52,5 +54,6 @@ export function normalizeBncrSendParams(input: {
52
54
  mediaUrl: mediaUrl || undefined,
53
55
  asVoice,
54
56
  audioAsVoice,
57
+ ...(type ? { type } : {}),
55
58
  };
56
59
  }
@@ -1,20 +1,26 @@
1
+ import type { BncrRoute } from '../../core/types.ts';
2
+
3
+ function normalizeReplyKind(value: unknown): 'tool' | 'block' | 'final' | undefined {
4
+ return value === 'tool' || value === 'block' || value === 'final' ? value : undefined;
5
+ }
6
+
1
7
  export async function sendBncrText(params: {
2
8
  channelId: string;
3
9
  accountId: string;
4
10
  to: string;
5
11
  text: string;
6
- kind?: 'tool' | 'block' | 'final';
12
+ kind?: string;
7
13
  replyToId?: string;
8
14
  mediaLocalRoots?: readonly string[];
9
15
  resolveVerifiedTarget: (
10
16
  to: string,
11
17
  accountId: string,
12
- ) => { sessionKey: string; route: any; displayScope: string };
13
- rememberSessionRoute: (sessionKey: string, accountId: string, route: any) => void;
18
+ ) => { sessionKey: string; route: BncrRoute; displayScope: string };
19
+ rememberSessionRoute: (sessionKey: string, accountId: string, route: BncrRoute) => void;
14
20
  enqueueFromReply: (args: {
15
21
  accountId: string;
16
22
  sessionKey: string;
17
- route: any;
23
+ route: BncrRoute;
18
24
  payload: {
19
25
  text?: string;
20
26
  mediaUrl?: string;
@@ -35,7 +41,7 @@ export async function sendBncrText(params: {
35
41
  route: verified.route,
36
42
  payload: {
37
43
  text: params.text,
38
- kind: params.kind,
44
+ kind: normalizeReplyKind(params.kind),
39
45
  replyToId: params.replyToId,
40
46
  },
41
47
  mediaLocalRoots: params.mediaLocalRoots,
@@ -57,24 +63,26 @@ export async function sendBncrMedia(params: {
57
63
  mediaUrls?: string[];
58
64
  asVoice?: boolean;
59
65
  audioAsVoice?: boolean;
60
- kind?: 'tool' | 'block' | 'final';
66
+ type?: string;
67
+ kind?: string;
61
68
  replyToId?: string;
62
69
  mediaLocalRoots?: readonly string[];
63
70
  resolveVerifiedTarget: (
64
71
  to: string,
65
72
  accountId: string,
66
- ) => { sessionKey: string; route: any; displayScope: string };
67
- rememberSessionRoute: (sessionKey: string, accountId: string, route: any) => void;
73
+ ) => { sessionKey: string; route: BncrRoute; displayScope: string };
74
+ rememberSessionRoute: (sessionKey: string, accountId: string, route: BncrRoute) => void;
68
75
  enqueueFromReply: (args: {
69
76
  accountId: string;
70
77
  sessionKey: string;
71
- route: any;
78
+ route: BncrRoute;
72
79
  payload: {
73
80
  text?: string;
74
81
  mediaUrl?: string;
75
82
  mediaUrls?: string[];
76
83
  asVoice?: boolean;
77
84
  audioAsVoice?: boolean;
85
+ type?: string;
78
86
  kind?: 'tool' | 'block' | 'final';
79
87
  replyToId?: string;
80
88
  };
@@ -95,7 +103,8 @@ export async function sendBncrMedia(params: {
95
103
  mediaUrls: params.mediaUrls?.length ? params.mediaUrls : undefined,
96
104
  asVoice: params.asVoice === true ? true : undefined,
97
105
  audioAsVoice: params.audioAsVoice === true ? true : undefined,
98
- kind: params.kind,
106
+ type: params.type,
107
+ kind: normalizeReplyKind(params.kind),
99
108
  replyToId: params.replyToId,
100
109
  },
101
110
  mediaLocalRoots: params.mediaLocalRoots,
@@ -1,3 +1,4 @@
1
+ import type { ChannelOutboundSessionRoute } from 'openclaw/plugin-sdk/core';
1
2
  import {
2
3
  buildCanonicalBncrSessionKey,
3
4
  formatDisplayScope,
@@ -7,9 +8,23 @@ import {
7
8
  } from '../../core/targets.ts';
8
9
  import type { BncrRoute } from '../../core/types.ts';
9
10
  import { buildOpenClawChannelOutboundSessionRoute } from '../../openclaw/session-route-runtime.ts';
11
+ import type { BncrChannelConfigRoot } from '../../plugin/channel-runtime-types.ts';
12
+
13
+ type BncrChannelRouteRefFields = {
14
+ channel: string;
15
+ accountId?: string;
16
+ target: {
17
+ to: string;
18
+ rawTo: string;
19
+ chatType: 'direct' | 'group';
20
+ };
21
+ thread?: { id: string };
22
+ };
23
+
24
+ type BncrOutboundSessionRoute = ChannelOutboundSessionRoute & BncrChannelRouteRefFields;
10
25
 
11
26
  type ResolveBncrOutboundSessionRouteParams = {
12
- cfg: any;
27
+ cfg: BncrChannelConfigRoot;
13
28
  channel: string;
14
29
  agentId: string;
15
30
  accountId?: string;
@@ -27,13 +42,13 @@ function asString(v: unknown, fallback = ''): string {
27
42
  }
28
43
 
29
44
  function attachBncrChannelRouteRefFields(args: {
30
- built: Record<string, unknown>;
45
+ built: ChannelOutboundSessionRoute;
31
46
  channel: string;
32
47
  accountId?: string;
33
48
  to: string;
34
49
  chatType: 'direct' | 'group';
35
50
  threadId?: string;
36
- }) {
51
+ }): BncrOutboundSessionRoute {
37
52
  const { built, channel, accountId, to, chatType, threadId } = args;
38
53
  return {
39
54
  ...built,
@@ -0,0 +1,76 @@
1
+ import type { OpenClawPluginApi } from 'openclaw/plugin-sdk/core';
2
+ import type { BncrRoute } from '../core/types.ts';
3
+
4
+ export type OpenClawChannelRuntimeContext = Record<string, unknown> & {
5
+ BodyForAgent?: string;
6
+ CommandBody?: string;
7
+ };
8
+
9
+ export type OpenClawChannelPeer = {
10
+ kind: 'direct' | 'group';
11
+ id: string;
12
+ };
13
+
14
+ export type OpenClawResolvedAgentRoute = {
15
+ sessionKey?: string;
16
+ mainSessionKey?: string;
17
+ route?: BncrRoute;
18
+ agentId?: string;
19
+ [key: string]: unknown;
20
+ };
21
+
22
+ export type OpenClawReplyDispatcherPayload = {
23
+ text?: string;
24
+ mediaUrl?: string;
25
+ mediaUrls?: string[];
26
+ audioAsVoice?: boolean;
27
+ };
28
+
29
+ export type OpenClawReplyDispatchInfo = { kind?: 'tool' | 'block' | 'final' };
30
+
31
+ export type OpenClawInboundRuntimeIngested = {
32
+ id: string;
33
+ timestamp: number;
34
+ rawText: string;
35
+ textForAgent?: string;
36
+ textForCommands?: string;
37
+ raw: unknown;
38
+ };
39
+
40
+ export type OpenClawInboundRuntimeResolvedTurn = {
41
+ channel: string;
42
+ accountId: string;
43
+ routeSessionKey: string;
44
+ storePath: string;
45
+ ctxPayload: OpenClawChannelRuntimeContext;
46
+ recordInboundSession: (...args: unknown[]) => Promise<unknown> | unknown;
47
+ record: {
48
+ updateLastRoute: unknown;
49
+ onRecordError: (err: unknown) => void;
50
+ };
51
+ runDispatch: () => Promise<unknown> | unknown;
52
+ };
53
+
54
+ export type OpenClawInboundRuntimeAdapter = {
55
+ ingest: () => OpenClawInboundRuntimeIngested;
56
+ resolveTurn: () => OpenClawInboundRuntimeResolvedTurn;
57
+ };
58
+
59
+ export type OpenClawInboundRuntimeRunParams = {
60
+ channel: string;
61
+ accountId: string;
62
+ raw: unknown;
63
+ adapter: OpenClawInboundRuntimeAdapter;
64
+ onFinalize?: () => void;
65
+ };
66
+
67
+ export type OpenClawInboundRuntime = {
68
+ buildContext: (
69
+ params: unknown,
70
+ ) => OpenClawChannelRuntimeContext | Promise<OpenClawChannelRuntimeContext>;
71
+ run: (params: unknown) => Promise<unknown> | unknown;
72
+ runPreparedReply?: (params: unknown) => Promise<unknown> | unknown;
73
+ dispatchReply?: (params: unknown) => Promise<unknown> | unknown;
74
+ };
75
+
76
+ export type OpenClawChannelRuntimeApiHolder = OpenClawPluginApi;