@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,226 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { normalizeAccountId } from '../core/accounts.ts';
3
+ import { buildBncrDebugJsonMessage } from '../core/logging.ts';
4
+ import type { BncrRoute, OutboxEntry } from '../core/types.ts';
5
+ import { buildBncrDurableQueuedResult } from '../messaging/outbound/durable-queue-adapter.ts';
6
+ import type { ReplyPayloadInput } from '../messaging/outbound/reply-enqueue.ts';
7
+ import { sendBncrMedia, sendBncrText } from '../messaging/outbound/send.ts';
8
+ import type { BncrChannelSendContext, BncrVerifiedTarget } from './channel-runtime-types.ts';
9
+
10
+ function normalizeReplyKind(value: unknown): ReplyPayloadInput['kind'] {
11
+ return value === 'tool' || value === 'block' || value === 'final' ? value : undefined;
12
+ }
13
+
14
+ export type BncrChannelSendRuntime = {
15
+ channelId: string;
16
+ asString: (value: unknown, fallback?: string) => string;
17
+ syncDebugFlag: () => Promise<void>;
18
+ logInfo: (scope: string, message: string, options?: Record<string, unknown>) => void;
19
+ resolveVerifiedTarget: (to: string, accountId: string) => BncrVerifiedTarget;
20
+ rememberSessionRoute: (sessionKey: string, accountId: string, route: BncrRoute) => void;
21
+ enqueueFromReply: (args: {
22
+ accountId: string;
23
+ sessionKey: string;
24
+ route: BncrRoute;
25
+ payload: ReplyPayloadInput;
26
+ mediaLocalRoots?: readonly string[];
27
+ }) => Promise<void>;
28
+ listOutboxEntries: () => OutboxEntry[];
29
+ };
30
+
31
+ function resolveChannelSendReplyToId(
32
+ asString: BncrChannelSendRuntime['asString'],
33
+ ctx: BncrChannelSendContext,
34
+ ) {
35
+ return asString(ctx?.replyToId || ctx?.replyToMessageId || '').trim() || undefined;
36
+ }
37
+
38
+ function logChannelSendEntry(
39
+ runtime: BncrChannelSendRuntime,
40
+ args: {
41
+ kind: 'text' | 'media';
42
+ accountId: string;
43
+ to: string;
44
+ ctx: BncrChannelSendContext;
45
+ payload: {
46
+ text: string;
47
+ mediaUrl: string;
48
+ mediaUrls?: string[];
49
+ asVoice?: boolean;
50
+ audioAsVoice?: boolean;
51
+ };
52
+ },
53
+ ) {
54
+ runtime.logInfo(
55
+ 'outbound',
56
+ buildBncrDebugJsonMessage(`send-entry:${args.kind}`, {
57
+ accountId: args.accountId,
58
+ to: args.to,
59
+ text: args.payload.text,
60
+ mediaUrl: args.payload.mediaUrl,
61
+ mediaUrls: args.payload.mediaUrls,
62
+ asVoice: args.payload.asVoice,
63
+ audioAsVoice: args.payload.audioAsVoice,
64
+ sessionKey: runtime.asString(args.ctx?.sessionKey || ''),
65
+ mirrorSessionKey: runtime.asString(args.ctx?.mirror?.sessionKey || ''),
66
+ rawCtx: {
67
+ to: args.ctx?.to,
68
+ accountId: args.ctx?.accountId,
69
+ threadId: args.ctx?.threadId,
70
+ replyToId: args.ctx?.replyToId,
71
+ },
72
+ }),
73
+ { debugOnly: true },
74
+ );
75
+ }
76
+
77
+ async function enqueueChannelMessageHandoff(
78
+ runtime: BncrChannelSendRuntime,
79
+ ctx: BncrChannelSendContext,
80
+ payload: ReplyPayloadInput,
81
+ ) {
82
+ const accountId = normalizeAccountId(ctx.accountId);
83
+ const to = runtime.asString(ctx.to || '').trim();
84
+ const verified = runtime.resolveVerifiedTarget(to, accountId);
85
+ runtime.rememberSessionRoute(verified.sessionKey, accountId, verified.route);
86
+ const before = new Set(runtime.listOutboxEntries().map((entry) => entry.messageId));
87
+ await runtime.enqueueFromReply({
88
+ accountId,
89
+ sessionKey: verified.sessionKey,
90
+ route: verified.route,
91
+ payload,
92
+ mediaLocalRoots: ctx.mediaLocalRoots,
93
+ });
94
+ const entries = runtime.listOutboxEntries().filter((entry) => !before.has(entry.messageId));
95
+ if (!entries.length) {
96
+ throw new Error('bncr channel.message handoff did not enqueue an outbox entry');
97
+ }
98
+ return entries[entries.length - 1];
99
+ }
100
+
101
+ export function createBncrChannelSendRuntime(runtime: BncrChannelSendRuntime) {
102
+ return {
103
+ channelSendText: async (ctx: BncrChannelSendContext) => {
104
+ await runtime.syncDebugFlag();
105
+ const accountId = normalizeAccountId(ctx.accountId);
106
+ const to = runtime.asString(ctx.to || '').trim();
107
+ const replyToId = resolveChannelSendReplyToId(runtime.asString, ctx);
108
+
109
+ logChannelSendEntry(runtime, {
110
+ kind: 'text',
111
+ accountId,
112
+ to,
113
+ ctx,
114
+ payload: {
115
+ text: runtime.asString(ctx?.text || ''),
116
+ mediaUrl: runtime.asString(ctx?.mediaUrl || ''),
117
+ },
118
+ });
119
+
120
+ return sendBncrText({
121
+ channelId: runtime.channelId,
122
+ accountId,
123
+ to,
124
+ text: runtime.asString(ctx.text || ''),
125
+ kind: normalizeReplyKind(ctx?.kind),
126
+ replyToId,
127
+ mediaLocalRoots: ctx.mediaLocalRoots,
128
+ resolveVerifiedTarget: (targetTo, targetAccountId) =>
129
+ runtime.resolveVerifiedTarget(targetTo, targetAccountId),
130
+ rememberSessionRoute: (sessionKey, routeAccountId, route) =>
131
+ runtime.rememberSessionRoute(sessionKey, routeAccountId, route),
132
+ enqueueFromReply: (args) => runtime.enqueueFromReply(args),
133
+ createMessageId: () => randomUUID(),
134
+ });
135
+ },
136
+
137
+ channelSendMedia: async (ctx: BncrChannelSendContext) => {
138
+ await runtime.syncDebugFlag();
139
+ const accountId = normalizeAccountId(ctx.accountId);
140
+ const to = runtime.asString(ctx.to || '').trim();
141
+ const asVoice = ctx?.asVoice === true;
142
+ const audioAsVoice = ctx?.audioAsVoice === true;
143
+ const type = runtime.asString(ctx?.type || '').trim() || undefined;
144
+ const replyToId = resolveChannelSendReplyToId(runtime.asString, ctx);
145
+
146
+ logChannelSendEntry(runtime, {
147
+ kind: 'media',
148
+ accountId,
149
+ to,
150
+ ctx,
151
+ payload: {
152
+ text: runtime.asString(ctx?.text || ''),
153
+ mediaUrl: runtime.asString(ctx?.mediaUrl || ''),
154
+ mediaUrls: Array.isArray(ctx?.mediaUrls) ? ctx.mediaUrls : undefined,
155
+ asVoice,
156
+ audioAsVoice,
157
+ },
158
+ });
159
+
160
+ return sendBncrMedia({
161
+ channelId: runtime.channelId,
162
+ accountId,
163
+ to,
164
+ text: runtime.asString(ctx.text || ''),
165
+ mediaUrl: runtime.asString(ctx.mediaUrl || ''),
166
+ mediaUrls: Array.isArray(ctx?.mediaUrls) ? ctx.mediaUrls : undefined,
167
+ asVoice,
168
+ audioAsVoice,
169
+ type,
170
+ kind: normalizeReplyKind(ctx?.kind),
171
+ replyToId,
172
+ mediaLocalRoots: ctx.mediaLocalRoots,
173
+ resolveVerifiedTarget: (targetTo, targetAccountId) =>
174
+ runtime.resolveVerifiedTarget(targetTo, targetAccountId),
175
+ rememberSessionRoute: (sessionKey, routeAccountId, route) =>
176
+ runtime.rememberSessionRoute(sessionKey, routeAccountId, route),
177
+ enqueueFromReply: (args) => runtime.enqueueFromReply(args),
178
+ createMessageId: () => randomUUID(),
179
+ });
180
+ },
181
+
182
+ channelMessageSendText: async (ctx: BncrChannelSendContext) => {
183
+ const entry = await enqueueChannelMessageHandoff(runtime, ctx, {
184
+ text: runtime.asString(ctx.text || ''),
185
+ kind: normalizeReplyKind(ctx?.kind),
186
+ replyToId: resolveChannelSendReplyToId(runtime.asString, ctx),
187
+ });
188
+ return buildBncrDurableQueuedResult({ entry });
189
+ },
190
+
191
+ channelMessageSendMedia: async (ctx: BncrChannelSendContext) => {
192
+ const entry = await enqueueChannelMessageHandoff(runtime, ctx, {
193
+ text: runtime.asString(ctx.text || ''),
194
+ mediaUrl: runtime.asString(ctx.mediaUrl || ''),
195
+ mediaUrls: Array.isArray(ctx?.mediaUrls) ? ctx.mediaUrls : undefined,
196
+ asVoice: ctx?.asVoice === true,
197
+ audioAsVoice: ctx?.audioAsVoice === true,
198
+ type: runtime.asString(ctx?.type || '').trim() || undefined,
199
+ kind: normalizeReplyKind(ctx?.kind),
200
+ replyToId: resolveChannelSendReplyToId(runtime.asString, ctx),
201
+ });
202
+ return buildBncrDurableQueuedResult({ entry });
203
+ },
204
+
205
+ channelMessageSendPayload: async (ctx: BncrChannelSendContext) => {
206
+ const payload = ctx?.payload || {};
207
+ if (!payload || typeof payload !== 'object') {
208
+ throw new Error('bncr channel.message payload must be an object');
209
+ }
210
+ const entry = await enqueueChannelMessageHandoff(runtime, ctx, {
211
+ text: runtime.asString(payload.text || payload.message || payload.caption || ''),
212
+ mediaUrl: runtime.asString(payload.mediaUrl || ''),
213
+ mediaUrls: Array.isArray(payload.mediaUrls) ? payload.mediaUrls : undefined,
214
+ asVoice: payload.asVoice === true,
215
+ audioAsVoice: payload.audioAsVoice === true,
216
+ type: runtime.asString(payload.type || '').trim() || undefined,
217
+ kind: normalizeReplyKind(payload.kind),
218
+ replyToId:
219
+ runtime
220
+ .asString(payload.replyToId || ctx?.replyToId || ctx?.replyToMessageId || '')
221
+ .trim() || undefined,
222
+ });
223
+ return buildBncrDurableQueuedResult({ entry });
224
+ },
225
+ };
226
+ }
@@ -0,0 +1,102 @@
1
+ import path from 'node:path';
2
+
3
+ export function now() {
4
+ return Date.now();
5
+ }
6
+
7
+ export function asString(v: unknown, fallback = ''): string {
8
+ if (typeof v === 'string') return v;
9
+ if (v == null) return fallback;
10
+ return String(v);
11
+ }
12
+
13
+ export function finiteNumberOr(value: unknown, fallback: number): number {
14
+ const n = Number(value);
15
+ return Number.isFinite(n) ? n : fallback;
16
+ }
17
+
18
+ export function finiteNonNegativeNumberOrNull(value: unknown): number | null {
19
+ const n = Number(value);
20
+ return Number.isFinite(n) && n >= 0 ? n : null;
21
+ }
22
+
23
+ export function clampFiniteNumber(
24
+ value: unknown,
25
+ fallback: number,
26
+ min: number,
27
+ max: number,
28
+ ): number {
29
+ const n = Number(value);
30
+ const finite = Number.isFinite(n) ? n : fallback;
31
+ return Math.max(min, Math.min(finite, max));
32
+ }
33
+
34
+ export function isPlainObject(value: unknown): value is Record<string, unknown> {
35
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
36
+ }
37
+
38
+ export function backoffMs(retryCount: number): number {
39
+ return Math.max(1_000, 1_000 * 2 ** Math.max(0, retryCount - 1));
40
+ }
41
+
42
+ function fileExtFromMime(mimeType?: string): string {
43
+ const mt = asString(mimeType || '').toLowerCase();
44
+ const map: Record<string, string> = {
45
+ 'image/jpeg': '.jpg',
46
+ 'image/jpg': '.jpg',
47
+ 'image/png': '.png',
48
+ 'image/webp': '.webp',
49
+ 'image/gif': '.gif',
50
+ 'video/mp4': '.mp4',
51
+ 'video/webm': '.webm',
52
+ 'video/quicktime': '.mov',
53
+ 'audio/mpeg': '.mp3',
54
+ 'audio/mp4': '.m4a',
55
+ 'application/pdf': '.pdf',
56
+ 'text/plain': '.txt',
57
+ };
58
+ return map[mt] || '';
59
+ }
60
+
61
+ function sanitizeFileName(rawName?: string, fallback = 'file.bin'): string {
62
+ const name = asString(rawName || '').trim();
63
+ const base = name || fallback;
64
+ const cleaned = Array.from(base, (ch) => {
65
+ const code = ch.charCodeAt(0);
66
+ if (code <= 0x1f) return '_';
67
+ if ('\\/:*?"<>|'.includes(ch)) return '_';
68
+ return ch;
69
+ })
70
+ .join('')
71
+ .replace(/\s+/g, ' ')
72
+ .trim();
73
+ return cleaned || fallback;
74
+ }
75
+
76
+ function buildTimestampFileName(mimeType?: string): string {
77
+ const d = new Date();
78
+ const pad = (n: number) => String(n).padStart(2, '0');
79
+ const ts = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}_${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
80
+ const ext = fileExtFromMime(mimeType) || '.bin';
81
+ return `bncr_${ts}_${Math.random().toString(16).slice(2, 8)}${ext}`;
82
+ }
83
+
84
+ export function resolveOutboundFileName(params: {
85
+ mediaUrl?: string;
86
+ fileName?: string;
87
+ mimeType?: string;
88
+ }): string {
89
+ const mediaUrl = asString(params.mediaUrl || '').trim();
90
+ const mimeType = asString(params.mimeType || '').trim();
91
+
92
+ if (/^https?:\/\//i.test(mediaUrl)) {
93
+ return buildTimestampFileName(mimeType);
94
+ }
95
+
96
+ const candidate = sanitizeFileName(params.fileName, 'file.bin');
97
+ if (candidate.length <= 80) return candidate;
98
+
99
+ const ext = path.extname(candidate);
100
+ const stem = candidate.slice(0, Math.max(1, 80 - ext.length));
101
+ return `${stem}${ext}`;
102
+ }
@@ -7,10 +7,31 @@ import {
7
7
  import { resolveBncrChannelPolicy } from '../core/policy.ts';
8
8
  import { setOpenClawAccountEnabledInConfigSection } from '../openclaw/sdk-helpers.ts';
9
9
 
10
+ type BncrConfigSurfaceRoot = {
11
+ channels?: {
12
+ [CHANNEL_ID]?: {
13
+ enabled?: boolean;
14
+ accounts?: Record<string, unknown>;
15
+ } & Record<string, unknown>;
16
+ } & Record<string, unknown>;
17
+ };
18
+
19
+ type BncrConfigSurfaceAccount = {
20
+ accountId: string;
21
+ name?: unknown;
22
+ enabled?: boolean;
23
+ };
24
+
25
+ type BncrSetAccountEnabledArgs = {
26
+ cfg: BncrConfigSurfaceRoot;
27
+ accountId: string;
28
+ enabled: boolean;
29
+ };
30
+
10
31
  export const BNCR_CONFIG_SURFACE = {
11
32
  listAccountIds,
12
33
  resolveAccount,
13
- setAccountEnabled: ({ cfg, accountId, enabled }: any) =>
34
+ setAccountEnabled: ({ cfg, accountId, enabled }: BncrSetAccountEnabledArgs) =>
14
35
  setOpenClawAccountEnabledInConfigSection({
15
36
  cfg,
16
37
  sectionKey: CHANNEL_ID,
@@ -18,12 +39,12 @@ export const BNCR_CONFIG_SURFACE = {
18
39
  enabled,
19
40
  allowTopLevel: true,
20
41
  }),
21
- isEnabled: (account: any, cfg: any) => {
42
+ isEnabled: (account: BncrConfigSurfaceAccount, cfg: BncrConfigSurfaceRoot) => {
22
43
  const policy = resolveBncrChannelPolicy(cfg?.channels?.[CHANNEL_ID] || {});
23
44
  return policy.enabled !== false && account?.enabled !== false;
24
45
  },
25
46
  isConfigured: () => true,
26
- describeAccount: (account: any) => {
47
+ describeAccount: (account: BncrConfigSurfaceAccount) => {
27
48
  const displayName = resolveDefaultDisplayName(account?.name, account?.accountId);
28
49
  return {
29
50
  accountId: account.accountId,
@@ -0,0 +1,254 @@
1
+ import type { FileSendTransferState, OutboxEntry } from '../core/types.ts';
2
+
3
+ export type LeaseEventPayload = { leaseId?: string; connectionEpoch?: number };
4
+
5
+ export type ConnectionRuntimeFlags = Record<string, unknown>;
6
+ export type ConnectionQueueCounters = Record<string, unknown>;
7
+ export type ConnectionDiagnostics = Record<string, unknown>;
8
+
9
+ export type PreparedAckHandling = {
10
+ accountId: string;
11
+ connId: string;
12
+ clientId?: string;
13
+ messageId: string;
14
+ entry: OutboxEntry;
15
+ staleObserved: { stale: boolean };
16
+ };
17
+
18
+ export type FileAckPayload = {
19
+ ok: boolean;
20
+ transferId: string;
21
+ stage: string;
22
+ path: string;
23
+ errorCode: string;
24
+ errorMessage: string;
25
+ };
26
+
27
+ type FileAckState = Pick<
28
+ FileSendTransferState,
29
+ | 'status'
30
+ | 'ownerConnId'
31
+ | 'ownerClientId'
32
+ | 'error'
33
+ | 'terminalAt'
34
+ | 'completedPath'
35
+ | 'ackedChunks'
36
+ | 'failedChunks'
37
+ >;
38
+
39
+ const BNCR_FILE_ACK_STAGES = ['init', 'chunk', 'complete', 'abort'] as const;
40
+
41
+ export type BncrFileAckStage = (typeof BNCR_FILE_ACK_STAGES)[number];
42
+
43
+ export function buildConnectionHandlerConnectResponse(args: {
44
+ channelId: string;
45
+ accountId: string;
46
+ bridgeVersion: number;
47
+ pushEvent: string;
48
+ online: boolean;
49
+ isPrimary: boolean;
50
+ queueCounters: ConnectionQueueCounters;
51
+ diagnostics: ConnectionDiagnostics;
52
+ runtimeFlags: ConnectionRuntimeFlags;
53
+ messageAckWaiters: number;
54
+ fileAckWaiters: number;
55
+ leaseId: string;
56
+ connectionEpoch: number;
57
+ acceptedAt: number;
58
+ serverPid: number;
59
+ bridgeId: string;
60
+ now: number;
61
+ }) {
62
+ return {
63
+ channel: args.channelId,
64
+ accountId: args.accountId,
65
+ bridgeVersion: args.bridgeVersion,
66
+ pushEvent: args.pushEvent,
67
+ online: args.online,
68
+ isPrimary: args.isPrimary,
69
+ ...args.queueCounters,
70
+ diagnostics: args.diagnostics,
71
+ runtimeFlags: args.runtimeFlags,
72
+ waiters: {
73
+ messageAck: args.messageAckWaiters,
74
+ fileAck: args.fileAckWaiters,
75
+ },
76
+ leaseId: args.leaseId,
77
+ connectionEpoch: args.connectionEpoch,
78
+ protocolVersion: 2,
79
+ acceptedAt: args.acceptedAt,
80
+ serverPid: args.serverPid,
81
+ bridgeId: args.bridgeId,
82
+ now: args.now,
83
+ };
84
+ }
85
+
86
+ export function buildConnectionHandlerActivityResponse(args: {
87
+ accountId: string;
88
+ queueCounters: ConnectionQueueCounters;
89
+ now: number;
90
+ }) {
91
+ return {
92
+ accountId: args.accountId,
93
+ ok: true,
94
+ event: 'activity',
95
+ ...args.queueCounters,
96
+ now: args.now,
97
+ };
98
+ }
99
+
100
+ export function buildGatewayDebugFields(args: {
101
+ bridgeId: string;
102
+ accountId: string;
103
+ connId: string;
104
+ clientId?: string;
105
+ hasContext?: boolean;
106
+ outboundReady?: boolean;
107
+ preferredForOutbound?: boolean;
108
+ inboundOnly?: boolean;
109
+ }) {
110
+ return {
111
+ bridge: args.bridgeId,
112
+ accountId: args.accountId,
113
+ connId: args.connId,
114
+ clientId: args.clientId,
115
+ ...(typeof args.outboundReady === 'boolean' ? { outboundReady: args.outboundReady } : {}),
116
+ ...(typeof args.preferredForOutbound === 'boolean'
117
+ ? { preferredForOutbound: args.preferredForOutbound }
118
+ : {}),
119
+ ...(typeof args.inboundOnly === 'boolean' ? { inboundOnly: args.inboundOnly } : {}),
120
+ ...(typeof args.hasContext === 'boolean' ? { hasContext: args.hasContext } : {}),
121
+ };
122
+ }
123
+
124
+ export function isBncrFileAckStage(stage: string): stage is BncrFileAckStage {
125
+ return BNCR_FILE_ACK_STAGES.includes(stage as BncrFileAckStage);
126
+ }
127
+
128
+ export function resolveFileAckLeaseEventKind(stage: BncrFileAckStage) {
129
+ if (stage === 'init') return 'file.init';
130
+ if (stage === 'chunk') return 'file.chunk';
131
+ if (stage === 'abort') return 'file.abort';
132
+ return 'file.complete';
133
+ }
134
+
135
+ export function buildTerminalFileAckResponse(args: {
136
+ transferId: string;
137
+ stage: string;
138
+ state: string;
139
+ stale: boolean;
140
+ }) {
141
+ return args.stale
142
+ ? {
143
+ ok: true,
144
+ transferId: args.transferId,
145
+ stage: args.stage,
146
+ state: args.state,
147
+ stale: true,
148
+ ignored: true,
149
+ terminal: true,
150
+ }
151
+ : {
152
+ ok: true,
153
+ transferId: args.transferId,
154
+ stage: args.stage,
155
+ state: args.state,
156
+ ignored: true,
157
+ terminal: true,
158
+ };
159
+ }
160
+
161
+ export function buildHandledFileAckResponse(args: {
162
+ transferId: string;
163
+ stage: string;
164
+ state: string;
165
+ stale: boolean;
166
+ }) {
167
+ return args.stale
168
+ ? {
169
+ ok: true,
170
+ transferId: args.transferId,
171
+ stage: args.stage,
172
+ state: args.state,
173
+ stale: true,
174
+ staleAccepted: true,
175
+ }
176
+ : {
177
+ ok: true,
178
+ transferId: args.transferId,
179
+ stage: args.stage,
180
+ state: args.state,
181
+ };
182
+ }
183
+
184
+ export function buildFileAckPayload(args: {
185
+ ok: boolean;
186
+ transferId: string;
187
+ stage: string;
188
+ path: string;
189
+ errorCode: string;
190
+ errorMessage: string;
191
+ }): FileAckPayload {
192
+ return {
193
+ ok: args.ok,
194
+ transferId: args.transferId,
195
+ stage: args.stage,
196
+ path: args.path,
197
+ errorCode: args.errorCode,
198
+ errorMessage: args.errorMessage,
199
+ };
200
+ }
201
+
202
+ export function hasTerminalFileAckState(state: FileAckState | undefined): state is FileAckState {
203
+ return state?.status === 'completed' || state?.status === 'aborted';
204
+ }
205
+
206
+ export function matchesTransferOwner(args: {
207
+ transfer: FileAckState | undefined;
208
+ connId: string;
209
+ clientId?: string;
210
+ }) {
211
+ const { transfer, connId, clientId } = args;
212
+ const sameConn = !!transfer?.ownerConnId && transfer.ownerConnId === connId;
213
+ const sameClient =
214
+ !transfer?.ownerConnId &&
215
+ !!transfer?.ownerClientId &&
216
+ !!clientId &&
217
+ transfer.ownerClientId === clientId;
218
+ return { sameConn, sameClient };
219
+ }
220
+
221
+ export function applyFileAckState(args: {
222
+ transfer: FileSendTransferState;
223
+ stage: BncrFileAckStage;
224
+ ok: boolean;
225
+ chunkIndex: number | null;
226
+ now: number;
227
+ path: string;
228
+ errorCode: string;
229
+ errorMessage: string;
230
+ }) {
231
+ const { transfer, stage, ok, chunkIndex, now, path, errorCode, errorMessage } = args;
232
+ if (!ok) {
233
+ transfer.error = `${errorCode || 'ACK_FAILED'}:${errorMessage || 'ack failed'}`;
234
+ if (stage === 'chunk' && chunkIndex != null) {
235
+ transfer.failedChunks.set(chunkIndex, transfer.error);
236
+ }
237
+ if (stage === 'complete') {
238
+ transfer.status = 'aborted';
239
+ transfer.terminalAt = now;
240
+ }
241
+ return;
242
+ }
243
+
244
+ if (stage === 'chunk' && chunkIndex != null) {
245
+ transfer.ackedChunks.add(chunkIndex);
246
+ transfer.status = 'transferring';
247
+ }
248
+
249
+ if (stage === 'complete') {
250
+ transfer.status = 'completed';
251
+ transfer.terminalAt = now;
252
+ transfer.completedPath = path || transfer.completedPath;
253
+ }
254
+ }