@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
@@ -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,19 +90,25 @@ export type ReplyMediaFallbackTextEntryParams = {
79
90
  fallback: { text: string; reason: string };
80
91
  };
81
92
 
82
- const MEDIA_TEXT_SPLIT_THRESHOLD = 1020;
83
-
84
- export function hasReplyMediaEntries(payload: NormalizedReplyPayload) {
85
- return payload.mediaList.length > 0;
86
- }
87
-
88
93
  export function shouldSplitReplyMediaText(payload: NormalizedReplyPayload) {
89
94
  if (!payload.text) return false;
90
95
  if (payload.mediaList.length > 1) return true;
91
96
  return payload.text.length > MEDIA_TEXT_SPLIT_THRESHOLD;
92
97
  }
93
98
 
94
- function withoutReplyMediaText(payload: NormalizedReplyPayload): NormalizedReplyPayload {
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 {
95
112
  return {
96
113
  ...payload,
97
114
  text: '',
@@ -169,135 +186,6 @@ export function enqueueReplyTextEntry(
169
186
  );
170
187
  }
171
188
 
172
- export function enqueueReplyMediaFallbackTextEntry(
173
- params: ReplyMediaFallbackTextEntryParams,
174
- helpers: {
175
- logInfo: (
176
- scope: string | undefined,
177
- message: string,
178
- options?: { debugOnly?: boolean },
179
- ) => void;
180
- enqueueOutbound: (entry: OutboxEntry) => void;
181
- buildTextOutboxEntry: (args: {
182
- accountId: string;
183
- sessionKey: string;
184
- route: BncrRoute;
185
- text: string;
186
- kind?: 'tool' | 'block' | 'final';
187
- replyToId?: string;
188
- replyTargetPolicy?: OutboundReplyTargetPolicy;
189
- }) => OutboxEntry;
190
- },
191
- ): void {
192
- helpers.logInfo(
193
- 'outbound',
194
- `media-dedupe-hit ${JSON.stringify(buildReplyMediaFallbackDebugInfo(params))}`,
195
- { debugOnly: true },
196
- );
197
- helpers.enqueueOutbound(
198
- buildReplyTextOutboxEntry(
199
- {
200
- accountId: params.accountId,
201
- sessionKey: params.sessionKey,
202
- route: params.route,
203
- text: params.fallback.text,
204
- kind: params.kind,
205
- replyToId: params.replyToId,
206
- replyTargetPolicy: params.replyTargetPolicy,
207
- },
208
- { buildTextOutboxEntry: helpers.buildTextOutboxEntry },
209
- ),
210
- );
211
- }
212
-
213
- export function enqueueReplyMediaFileTransferEntry(
214
- params: ReplyMediaFileTransferParams,
215
- helpers: {
216
- enqueueOutbound: (entry: OutboxEntry) => void;
217
- buildFileTransferOutboxEntry: (args: {
218
- accountId: string;
219
- sessionKey: string;
220
- route: BncrRoute;
221
- mediaUrl: string;
222
- mediaLocalRoots?: readonly string[];
223
- text: string;
224
- asVoice: boolean;
225
- audioAsVoice: boolean;
226
- kind?: 'tool' | 'block' | 'final';
227
- replyToId?: string;
228
- replyTargetPolicy?: OutboundReplyTargetPolicy;
229
- }) => OutboxEntry;
230
- rememberRecentMediaSend: (args: {
231
- sessionKey: string;
232
- mediaUrl: string;
233
- text: string;
234
- replyToId: string;
235
- createdAt: number;
236
- }) => void;
237
- },
238
- ): void {
239
- helpers.enqueueOutbound(
240
- helpers.buildFileTransferOutboxEntry({
241
- accountId: params.accountId,
242
- sessionKey: params.sessionKey,
243
- route: params.route,
244
- mediaUrl: params.mediaUrl,
245
- mediaLocalRoots: params.mediaLocalRoots,
246
- text: params.text,
247
- asVoice: params.asVoice,
248
- audioAsVoice: params.audioAsVoice,
249
- kind: params.kind,
250
- replyToId: params.replyToId || undefined,
251
- replyTargetPolicy: params.replyTargetPolicy,
252
- }),
253
- );
254
- helpers.rememberRecentMediaSend({
255
- sessionKey: params.sessionKey,
256
- mediaUrl: params.mediaUrl,
257
- text: params.normalizedText,
258
- replyToId: params.replyToId,
259
- createdAt: params.createdAt,
260
- });
261
- }
262
-
263
- export function enqueueSingleReplyMediaEntry(
264
- params: EnqueueSingleReplyMediaEntryParams,
265
- helpers: {
266
- enqueueReplyMediaFallbackTextEntry: (params: ReplyMediaFallbackTextEntryParams) => void;
267
- enqueueReplyMediaFileTransferEntry: (params: ReplyMediaFileTransferParams) => void;
268
- },
269
- ): void {
270
- if (params.fallback !== null) {
271
- helpers.enqueueReplyMediaFallbackTextEntry({
272
- accountId: params.params.accountId,
273
- sessionKey: params.params.sessionKey,
274
- route: params.params.route,
275
- mediaUrl: params.mediaUrl,
276
- kind: params.params.payload.kind,
277
- replyToId: params.params.payload.replyToId,
278
- replyTargetPolicy: params.params.payload.replyTargetPolicy,
279
- fallback: params.fallback,
280
- });
281
- return;
282
- }
283
-
284
- helpers.enqueueReplyMediaFileTransferEntry({
285
- accountId: params.params.accountId,
286
- sessionKey: params.params.sessionKey,
287
- route: params.params.route,
288
- mediaUrl: params.mediaUrl,
289
- mediaLocalRoots: params.params.mediaLocalRoots,
290
- text: params.text,
291
- normalizedText: params.normalizedText,
292
- asVoice: params.params.payload.asVoice,
293
- audioAsVoice: params.params.payload.audioAsVoice,
294
- kind: params.params.payload.kind,
295
- replyToId: params.params.payload.replyToId,
296
- replyTargetPolicy: params.params.payload.replyTargetPolicy,
297
- createdAt: params.currentTime,
298
- });
299
- }
300
-
301
189
  export function enqueueNormalizedReplyPayload(
302
190
  params: EnqueueNormalizedReplyPayloadParams,
303
191
  helpers: {
@@ -307,7 +195,6 @@ export function enqueueNormalizedReplyPayload(
307
195
  route: BncrRoute;
308
196
  payload: NormalizedReplyPayload;
309
197
  }) => void;
310
- hasReplyMediaEntries: (payload: NormalizedReplyPayload) => boolean;
311
198
  enqueueReplyMediaEntries: (params: ReplyMediaEntriesParams) => void;
312
199
  enqueueReplyTextEntry: (params: {
313
200
  accountId: string;
@@ -324,24 +211,26 @@ export function enqueueNormalizedReplyPayload(
324
211
  payload: params.payload,
325
212
  });
326
213
 
327
- if (helpers.hasReplyMediaEntries(params.payload)) {
328
- if (shouldSplitReplyMediaText(params.payload)) {
329
- helpers.enqueueReplyTextEntry({
330
- accountId: params.accountId,
331
- sessionKey: params.sessionKey,
332
- route: params.route,
333
- payload: params.payload,
334
- });
335
- helpers.enqueueReplyMediaEntries({
336
- accountId: params.accountId,
337
- sessionKey: params.sessionKey,
338
- route: params.route,
339
- payload: withoutReplyMediaText(params.payload),
340
- mediaLocalRoots: params.mediaLocalRoots,
341
- });
342
- return;
343
- }
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
+ }
344
232
 
233
+ if (plan.kind !== 'text-only') {
345
234
  helpers.enqueueReplyMediaEntries({
346
235
  accountId: params.accountId,
347
236
  sessionKey: params.sessionKey,
@@ -370,6 +259,7 @@ export function normalizeReplyPayload(
370
259
  const mediaUrls = Array.isArray(payload?.mediaUrls)
371
260
  ? payload.mediaUrls.map((v) => helpers.asString(v || '').trim()).filter(Boolean)
372
261
  : undefined;
262
+ const type = helpers.asString(payload?.type || '').trim();
373
263
  return {
374
264
  text,
375
265
  mediaUrl,
@@ -377,6 +267,7 @@ export function normalizeReplyPayload(
377
267
  mediaList: mediaUrls?.length ? mediaUrls : mediaUrl ? [mediaUrl] : [],
378
268
  asVoice: payload?.asVoice === true,
379
269
  audioAsVoice: payload?.audioAsVoice === true,
270
+ ...(type ? { type } : {}),
380
271
  kind: payload?.kind,
381
272
  replyTargetPolicy: options?.replyTargetPolicy ?? 'agent-default',
382
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;