@xmoxmo/bncr 0.3.6 → 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 +46 -155
  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,146 @@
1
+ import { createHash } from 'node:crypto';
2
+ import type { GatewayRequestHandlerOptions } from 'openclaw/plugin-sdk/core';
3
+ import { getErrorMessage } from './error-message.ts';
4
+ import type { BncrFileInboundRuntime } from './file-inbound-runtime.ts';
5
+ import {
6
+ isAbortedInboundTransfer,
7
+ isActiveInboundTransfer,
8
+ isCompletedInboundTransfer,
9
+ markInboundTransferTransferring,
10
+ } from './file-inbound-state.ts';
11
+ import { buildBncrGatewayEventContext } from './gateway-event-context.ts';
12
+
13
+ export function createBncrFileInboundChunkHandler(runtime: BncrFileInboundRuntime) {
14
+ return async function handleFileChunk({
15
+ params,
16
+ respond,
17
+ client,
18
+ context,
19
+ }: GatewayRequestHandlerOptions) {
20
+ const gatewayContext = buildBncrGatewayEventContext({
21
+ params,
22
+ client,
23
+ context,
24
+ asString: runtime.asString,
25
+ normalizeAccountId: runtime.normalizeAccountId,
26
+ now: runtime.now,
27
+ });
28
+ const { accountId, connId, clientId } = gatewayContext;
29
+
30
+ const transferId = runtime.asString(params?.transferId || '').trim();
31
+ const chunkIndex = runtime.finiteNonNegativeNumberOrNull(params?.chunkIndex);
32
+ const offset = runtime.finiteNonNegativeNumberOrNull(params?.offset ?? 0);
33
+ const size = runtime.finiteNonNegativeNumberOrNull(params?.size ?? 0);
34
+ const chunkSha256 = runtime.asString(params?.chunkSha256 || '').trim();
35
+ const base64 = runtime.asString(params?.base64 || '');
36
+
37
+ if (!transferId || chunkIndex == null || !base64) {
38
+ respond(false, { error: 'transferId/chunkIndex/base64 required' });
39
+ return;
40
+ }
41
+
42
+ const st = runtime.fileRecvTransfers.get(transferId);
43
+ if (!st) {
44
+ respond(false, { error: 'transfer not found' });
45
+ return;
46
+ }
47
+ if (isCompletedInboundTransfer(st)) {
48
+ respond(true, {
49
+ ok: true,
50
+ transferId,
51
+ status: 'completed',
52
+ path: st.completedPath,
53
+ ignored: true,
54
+ terminal: true,
55
+ });
56
+ return;
57
+ }
58
+ if (isAbortedInboundTransfer(st)) {
59
+ respond(true, {
60
+ ok: true,
61
+ transferId,
62
+ status: 'aborted',
63
+ error: st.error,
64
+ ignored: true,
65
+ terminal: true,
66
+ });
67
+ return;
68
+ }
69
+ if (!isActiveInboundTransfer(st)) {
70
+ respond(false, { error: 'transfer not active' });
71
+ return;
72
+ }
73
+ if (chunkIndex >= st.totalChunks) {
74
+ respond(false, {
75
+ error: `chunkIndex out of range index=${chunkIndex} total=${st.totalChunks}`,
76
+ });
77
+ return;
78
+ }
79
+
80
+ const staleObserved = runtime.observeLease('file.chunk', params ?? {});
81
+ if (staleObserved.stale) {
82
+ if (
83
+ !runtime.matchesTransferOwner({
84
+ ownerConnId: st.ownerConnId,
85
+ ownerClientId: st.ownerClientId,
86
+ connId,
87
+ clientId,
88
+ })
89
+ ) {
90
+ runtime.logWarn(
91
+ 'stale',
92
+ `ignore kind=file.chunk accountId=${accountId} connId=${connId} clientId=${clientId || '-'} transferId=${transferId} reason=owner-mismatch ownerConnId=${st.ownerConnId || '-'} ownerClientId=${st.ownerClientId || '-'}`,
93
+ { debugOnly: true },
94
+ );
95
+ respond(true, { ok: true, stale: true, ignored: true });
96
+ return;
97
+ }
98
+ } else {
99
+ runtime.refreshAcceptedFileTransferLiveState({
100
+ accountId,
101
+ connId,
102
+ clientId,
103
+ context: gatewayContext.context,
104
+ });
105
+ }
106
+
107
+ try {
108
+ const buf = Buffer.from(base64, 'base64');
109
+ if (size != null && size > 0 && buf.length !== size) {
110
+ throw new Error(`chunk size mismatch expected=${size} got=${buf.length}`);
111
+ }
112
+ if (chunkSha256) {
113
+ const digest = createHash('sha256').update(buf).digest('hex');
114
+ if (digest !== chunkSha256) throw new Error('chunk sha256 mismatch');
115
+ }
116
+ st.bufferByChunk.set(chunkIndex, buf);
117
+ st.receivedChunks.add(chunkIndex);
118
+ runtime.fileRecvTransfers.set(transferId, markInboundTransferTransferring(st));
119
+
120
+ respond(
121
+ true,
122
+ staleObserved.stale
123
+ ? {
124
+ ok: true,
125
+ transferId,
126
+ chunkIndex,
127
+ offset,
128
+ received: st.receivedChunks.size,
129
+ totalChunks: st.totalChunks,
130
+ stale: true,
131
+ staleAccepted: true,
132
+ }
133
+ : {
134
+ ok: true,
135
+ transferId,
136
+ chunkIndex,
137
+ offset,
138
+ received: st.receivedChunks.size,
139
+ totalChunks: st.totalChunks,
140
+ },
141
+ );
142
+ } catch (error) {
143
+ respond(false, { error: getErrorMessage(error, 'chunk invalid') });
144
+ }
145
+ };
146
+ }
@@ -0,0 +1,153 @@
1
+ import { createHash } from 'node:crypto';
2
+ import type { GatewayRequestHandlerOptions } from 'openclaw/plugin-sdk/core';
3
+ import { getErrorMessage } from './error-message.ts';
4
+ import type { BncrFileInboundRuntime } from './file-inbound-runtime.ts';
5
+ import {
6
+ type BncrInboundFileChunkEntry,
7
+ isAbortedInboundTransfer,
8
+ isCompletedInboundTransfer,
9
+ markInboundTransferAborted,
10
+ markInboundTransferCompleted,
11
+ } from './file-inbound-state.ts';
12
+ import { buildBncrGatewayEventContext } from './gateway-event-context.ts';
13
+
14
+ export function createBncrFileInboundCompleteHandler(runtime: BncrFileInboundRuntime) {
15
+ return async function handleFileComplete({
16
+ params,
17
+ respond,
18
+ client,
19
+ context,
20
+ }: GatewayRequestHandlerOptions) {
21
+ const gatewayContext = buildBncrGatewayEventContext({
22
+ params,
23
+ client,
24
+ context,
25
+ asString: runtime.asString,
26
+ normalizeAccountId: runtime.normalizeAccountId,
27
+ now: runtime.now,
28
+ });
29
+ const { accountId, connId, clientId } = gatewayContext;
30
+
31
+ const transferId = runtime.asString(params?.transferId || '').trim();
32
+ if (!transferId) {
33
+ respond(false, { error: 'transferId required' });
34
+ return;
35
+ }
36
+
37
+ const st = runtime.fileRecvTransfers.get(transferId);
38
+ if (!st) {
39
+ respond(false, { error: 'transfer not found' });
40
+ return;
41
+ }
42
+ if (isCompletedInboundTransfer(st)) {
43
+ respond(true, {
44
+ ok: true,
45
+ transferId,
46
+ status: 'completed',
47
+ path: st.completedPath,
48
+ ignored: true,
49
+ terminal: true,
50
+ });
51
+ return;
52
+ }
53
+ if (isAbortedInboundTransfer(st)) {
54
+ respond(true, {
55
+ ok: true,
56
+ transferId,
57
+ status: 'aborted',
58
+ error: st.error,
59
+ ignored: true,
60
+ terminal: true,
61
+ });
62
+ return;
63
+ }
64
+
65
+ const staleObserved = runtime.observeLease('file.complete', params ?? {});
66
+ if (staleObserved.stale) {
67
+ if (
68
+ !runtime.matchesTransferOwner({
69
+ ownerConnId: st.ownerConnId,
70
+ ownerClientId: st.ownerClientId,
71
+ connId,
72
+ clientId,
73
+ })
74
+ ) {
75
+ runtime.logWarn(
76
+ 'stale',
77
+ `ignore kind=file.complete accountId=${accountId} connId=${connId} clientId=${clientId || '-'} transferId=${transferId} reason=owner-mismatch ownerConnId=${st.ownerConnId || '-'} ownerClientId=${st.ownerClientId || '-'}`,
78
+ { debugOnly: true },
79
+ );
80
+ respond(true, { ok: true, stale: true, ignored: true });
81
+ return;
82
+ }
83
+ } else {
84
+ runtime.refreshAcceptedFileTransferLiveState({
85
+ accountId,
86
+ connId,
87
+ clientId,
88
+ context: gatewayContext.context,
89
+ });
90
+ }
91
+
92
+ try {
93
+ if (st.receivedChunks.size < st.totalChunks) {
94
+ throw new Error(
95
+ `chunk not complete received=${st.receivedChunks.size} total=${st.totalChunks}`,
96
+ );
97
+ }
98
+
99
+ const ordered = (Array.from(st.bufferByChunk.entries()) as BncrInboundFileChunkEntry[])
100
+ .sort((a, b) => a[0] - b[0])
101
+ .map((x) => x[1]);
102
+ const merged = Buffer.concat(ordered);
103
+ if (st.fileSize > 0 && merged.length !== st.fileSize) {
104
+ throw new Error(`file size mismatch expected=${st.fileSize} got=${merged.length}`);
105
+ }
106
+ const digest = createHash('sha256').update(merged).digest('hex');
107
+ if (st.fileSha256 && digest !== st.fileSha256) {
108
+ throw new Error('file sha256 mismatch');
109
+ }
110
+
111
+ const saved = await runtime.saveInboundMediaBuffer({
112
+ buffer: merged,
113
+ mimeType: st.mimeType,
114
+ fileName: st.fileName,
115
+ });
116
+ const completedState = markInboundTransferCompleted(st, saved.path, runtime.now());
117
+ runtime.fileRecvTransfers.set(transferId, completedState);
118
+
119
+ respond(
120
+ true,
121
+ staleObserved.stale
122
+ ? {
123
+ ok: true,
124
+ transferId,
125
+ path: saved.path,
126
+ size: merged.length,
127
+ fileName: st.fileName,
128
+ mimeType: st.mimeType,
129
+ fileSha256: digest,
130
+ stale: true,
131
+ staleAccepted: true,
132
+ }
133
+ : {
134
+ ok: true,
135
+ transferId,
136
+ path: saved.path,
137
+ size: merged.length,
138
+ fileName: st.fileName,
139
+ mimeType: st.mimeType,
140
+ fileSha256: digest,
141
+ },
142
+ );
143
+ } catch (error) {
144
+ const abortedState = markInboundTransferAborted(
145
+ st,
146
+ getErrorMessage(error, 'complete failed'),
147
+ runtime.now(),
148
+ );
149
+ runtime.fileRecvTransfers.set(transferId, abortedState);
150
+ respond(false, { error: abortedState.error });
151
+ }
152
+ };
153
+ }
@@ -0,0 +1,19 @@
1
+ import { createBncrFileInboundAbortHandler } from './file-inbound-abort.ts';
2
+ import { createBncrFileInboundChunkHandler } from './file-inbound-chunk.ts';
3
+ import { createBncrFileInboundCompleteHandler } from './file-inbound-complete.ts';
4
+ import { createBncrFileInboundInitHandler } from './file-inbound-init.ts';
5
+ import type { BncrFileInboundRuntime } from './file-inbound-runtime.ts';
6
+
7
+ export function createBncrFileInboundHandlers(runtime: BncrFileInboundRuntime) {
8
+ const handleFileInit = createBncrFileInboundInitHandler(runtime);
9
+ const handleFileChunk = createBncrFileInboundChunkHandler(runtime);
10
+ const handleFileComplete = createBncrFileInboundCompleteHandler(runtime);
11
+ const handleFileAbort = createBncrFileInboundAbortHandler(runtime);
12
+
13
+ return {
14
+ handleFileInit,
15
+ handleFileChunk,
16
+ handleFileComplete,
17
+ handleFileAbort,
18
+ };
19
+ }
@@ -0,0 +1,122 @@
1
+ import type { GatewayRequestHandlerOptions } from 'openclaw/plugin-sdk/core';
2
+ import type { BncrFileInboundRuntime } from './file-inbound-runtime.ts';
3
+ import { buildBncrGatewayEventContext } from './gateway-event-context.ts';
4
+
5
+ export function createBncrFileInboundInitHandler(runtime: BncrFileInboundRuntime) {
6
+ return async function handleFileInit({
7
+ params,
8
+ respond,
9
+ client,
10
+ context,
11
+ }: GatewayRequestHandlerOptions) {
12
+ const gatewayContext = buildBncrGatewayEventContext({
13
+ params,
14
+ client,
15
+ context,
16
+ asString: runtime.asString,
17
+ normalizeAccountId: runtime.normalizeAccountId,
18
+ now: runtime.now,
19
+ });
20
+ const { accountId, connId, clientId } = gatewayContext;
21
+ if (
22
+ runtime.shouldIgnoreStaleEvent({
23
+ kind: 'file.init',
24
+ payload: params ?? {},
25
+ accountId,
26
+ connId,
27
+ clientId,
28
+ })
29
+ ) {
30
+ respond(true, { ok: true, stale: true, ignored: true });
31
+ return;
32
+ }
33
+ runtime.refreshAcceptedFileTransferLiveState({
34
+ accountId,
35
+ connId,
36
+ clientId,
37
+ context: gatewayContext.context,
38
+ });
39
+
40
+ const transferId = runtime.asString(params?.transferId || '').trim();
41
+ const sessionKey = runtime.asString(params?.sessionKey || '').trim();
42
+ const fileName = runtime.asString(params?.fileName || '').trim() || 'file.bin';
43
+ const mimeType = runtime.asString(params?.mimeType || '').trim() || 'application/octet-stream';
44
+ const fileSize = runtime.finiteNonNegativeNumberOrNull(params?.fileSize);
45
+ const chunkSize = runtime.finiteNonNegativeNumberOrNull(params?.chunkSize ?? 256 * 1024);
46
+ const totalChunks = runtime.finiteNonNegativeNumberOrNull(params?.totalChunks);
47
+ const fileSha256 = runtime.asString(params?.fileSha256 || '').trim();
48
+
49
+ if (!transferId || !sessionKey || !fileSize || !chunkSize || !totalChunks) {
50
+ respond(false, { error: 'transferId/sessionKey/fileSize/chunkSize/totalChunks required' });
51
+ return;
52
+ }
53
+ if (fileSize > runtime.inboundFileTransferMaxBytes) {
54
+ respond(false, {
55
+ error: `fileSize too large size=${fileSize} max=${runtime.inboundFileTransferMaxBytes}`,
56
+ });
57
+ return;
58
+ }
59
+ if (totalChunks > runtime.inboundFileTransferMaxChunks) {
60
+ respond(false, {
61
+ error: `totalChunks too large total=${totalChunks} max=${runtime.inboundFileTransferMaxChunks}`,
62
+ });
63
+ return;
64
+ }
65
+ const expectedTotalChunks = Math.ceil(fileSize / chunkSize);
66
+ if (totalChunks !== expectedTotalChunks) {
67
+ respond(false, {
68
+ error: `totalChunks mismatch total=${totalChunks} expected=${expectedTotalChunks}`,
69
+ });
70
+ return;
71
+ }
72
+
73
+ const normalized = runtime.normalizeStoredSessionKey(sessionKey);
74
+ if (!normalized) {
75
+ respond(false, { error: 'invalid sessionKey' });
76
+ return;
77
+ }
78
+
79
+ const existing = runtime.fileRecvTransfers.get(transferId);
80
+ if (existing) {
81
+ respond(true, {
82
+ ok: true,
83
+ transferId,
84
+ status: existing.status,
85
+ duplicated: true,
86
+ });
87
+ return;
88
+ }
89
+
90
+ const route =
91
+ runtime.parseRouteLike({
92
+ platform: runtime.asString(params?.platform || normalized.route.platform),
93
+ groupId: runtime.asString(params?.groupId || normalized.route.groupId),
94
+ userId: runtime.asString(params?.userId || normalized.route.userId),
95
+ }) || normalized.route;
96
+
97
+ runtime.fileRecvTransfers.set(transferId, {
98
+ transferId,
99
+ accountId,
100
+ sessionKey: normalized.sessionKey,
101
+ route,
102
+ fileName,
103
+ mimeType,
104
+ fileSize,
105
+ chunkSize,
106
+ totalChunks,
107
+ fileSha256,
108
+ startedAt: runtime.now(),
109
+ status: 'init',
110
+ bufferByChunk: new Map(),
111
+ receivedChunks: new Set(),
112
+ ownerConnId: connId,
113
+ ownerClientId: clientId,
114
+ });
115
+
116
+ respond(true, {
117
+ ok: true,
118
+ transferId,
119
+ status: 'init',
120
+ });
121
+ };
122
+ }
@@ -0,0 +1,51 @@
1
+ import type { GatewayRequestHandlerOptions } from 'openclaw/plugin-sdk/core';
2
+ import type { BncrRoute, FileRecvTransferState } from '../core/types.ts';
3
+
4
+ export type BncrFileInboundLeaseEventKind =
5
+ | 'file.init'
6
+ | 'file.chunk'
7
+ | 'file.complete'
8
+ | 'file.abort';
9
+
10
+ export type BncrFileInboundRuntime = {
11
+ asString: (value: unknown, fallback?: string) => string;
12
+ now: () => number;
13
+ normalizeAccountId: (accountId: string) => string;
14
+ finiteNonNegativeNumberOrNull: (value: unknown) => number | null;
15
+ shouldIgnoreStaleEvent: (args: {
16
+ kind: BncrFileInboundLeaseEventKind;
17
+ payload: Record<string, unknown>;
18
+ accountId: string;
19
+ connId: string;
20
+ clientId?: string;
21
+ }) => boolean;
22
+ observeLease: (
23
+ kind: BncrFileInboundLeaseEventKind,
24
+ payload: Record<string, unknown>,
25
+ ) => { stale: boolean };
26
+ matchesTransferOwner: (args: {
27
+ ownerConnId?: string;
28
+ ownerClientId?: string;
29
+ connId: string;
30
+ clientId?: string;
31
+ }) => boolean;
32
+ refreshAcceptedFileTransferLiveState: (args: {
33
+ accountId: string;
34
+ connId: string;
35
+ clientId?: string;
36
+ context: GatewayRequestHandlerOptions['context'];
37
+ }) => void;
38
+ logWarn: (scope: string | undefined, message: string, options?: { debugOnly?: boolean }) => void;
39
+ parseRouteLike: (args: { platform: string; groupId: string; userId: string }) => BncrRoute | null;
40
+ normalizeStoredSessionKey: (
41
+ sessionKey: string,
42
+ ) => { sessionKey: string; route: BncrRoute } | null;
43
+ saveInboundMediaBuffer: (args: {
44
+ buffer: Buffer;
45
+ mimeType: string;
46
+ fileName: string;
47
+ }) => Promise<{ path: string }>;
48
+ fileRecvTransfers: Map<string, FileRecvTransferState>;
49
+ inboundFileTransferMaxBytes: number;
50
+ inboundFileTransferMaxChunks: number;
51
+ };
@@ -0,0 +1,62 @@
1
+ import type {
2
+ FileRecvTransferAbortedState,
3
+ FileRecvTransferCompletedState,
4
+ FileRecvTransferInitState,
5
+ FileRecvTransferState,
6
+ FileRecvTransferTransferringState,
7
+ } from '../core/types.ts';
8
+
9
+ export type BncrInboundFileChunkEntry = [number, Buffer];
10
+
11
+ export function isCompletedInboundTransfer(
12
+ state: FileRecvTransferState,
13
+ ): state is FileRecvTransferCompletedState {
14
+ return state.status === 'completed';
15
+ }
16
+
17
+ export function isAbortedInboundTransfer(
18
+ state: FileRecvTransferState,
19
+ ): state is FileRecvTransferAbortedState {
20
+ return state.status === 'aborted';
21
+ }
22
+
23
+ export function isActiveInboundTransfer(
24
+ state: FileRecvTransferState,
25
+ ): state is FileRecvTransferInitState | FileRecvTransferTransferringState {
26
+ return state.status === 'init' || state.status === 'transferring';
27
+ }
28
+
29
+ export function markInboundTransferCompleted(
30
+ state: FileRecvTransferState,
31
+ completedPath: string,
32
+ terminalAt: number,
33
+ ): FileRecvTransferCompletedState {
34
+ return {
35
+ ...state,
36
+ status: 'completed',
37
+ completedPath,
38
+ terminalAt,
39
+ };
40
+ }
41
+
42
+ export function markInboundTransferTransferring(
43
+ state: FileRecvTransferInitState | FileRecvTransferTransferringState,
44
+ ): FileRecvTransferTransferringState {
45
+ return {
46
+ ...state,
47
+ status: 'transferring',
48
+ };
49
+ }
50
+
51
+ export function markInboundTransferAborted(
52
+ state: FileRecvTransferState,
53
+ error: string,
54
+ terminalAt: number,
55
+ ): FileRecvTransferAbortedState {
56
+ return {
57
+ ...state,
58
+ status: 'aborted',
59
+ terminalAt,
60
+ error,
61
+ };
62
+ }