@xmoxmo/bncr 0.5.5 → 0.5.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.
- package/dist/index.js +1 -9
- package/index.ts +1 -9
- package/package.json +1 -1
- package/scripts/check-pack.mjs +0 -6
- package/scripts/selfcheck.mjs +0 -7
- package/src/channel.ts +114 -37
- package/src/core/accounts.ts +1 -6
- package/src/core/connection-reachability.ts +1 -5
- package/src/core/dead-letter-diagnostics.ts +4 -10
- package/src/core/outbox-entry-builders.ts +38 -15
- package/src/core/outbox-file-transfer-failure.ts +5 -0
- package/src/core/outbox-file-transfer-guards.ts +6 -0
- package/src/core/outbox-file-transfer-prep.ts +8 -5
- package/src/core/outbox-file-transfer-success.ts +5 -0
- package/src/core/outbox-queue.ts +1 -7
- package/src/core/outbox-summary.ts +15 -27
- package/src/core/outbox-text-push-prep.ts +1 -36
- package/src/core/permissions.ts +1 -9
- package/src/core/persisted-outbox-entry.ts +54 -11
- package/src/core/policy.ts +1 -11
- package/src/core/register-trace.ts +2 -5
- package/src/core/status-meta.ts +1 -9
- package/src/core/status.ts +1 -21
- package/src/core/targets.ts +52 -70
- package/src/core/types.ts +1 -17
- package/src/core/value-sanitize.ts +38 -1
- package/src/messaging/inbound/commands.ts +2 -24
- package/src/messaging/inbound/dispatch-prep.ts +1 -1
- package/src/messaging/inbound/gate.ts +2 -31
- package/src/messaging/inbound/group-history.ts +0 -75
- package/src/messaging/inbound/native-command.ts +7 -3
- package/src/messaging/inbound/parse.ts +1 -31
- package/src/messaging/inbound/scene-admin.ts +181 -5
- package/src/messaging/outbound/diagnostics.ts +1 -6
- package/src/messaging/outbound/durable-queue-adapter.ts +17 -11
- package/src/messaging/outbound/marker-parser.ts +188 -0
- package/src/messaging/outbound/media-dedupe.ts +2 -6
- package/src/messaging/outbound/media.ts +6 -20
- package/src/messaging/outbound/normalize-outbound-send.ts +327 -0
- package/src/messaging/outbound/queue-selectors.ts +1 -5
- package/src/messaging/outbound/reply-enqueue-media.ts +4 -0
- package/src/messaging/outbound/reply-enqueue.ts +130 -57
- package/src/messaging/outbound/session-route.ts +6 -11
- package/src/messaging/outbound/target-resolver.ts +1 -6
- package/src/openclaw/routing-runtime.ts +0 -11
- package/src/openclaw/sdk-helpers.ts +0 -2
- package/src/plugin/bridge-file-transfer-push-facade.ts +8 -6
- package/src/plugin/bridge-media-facade.ts +1 -1
- package/src/plugin/channel-components.ts +0 -13
- package/src/plugin/channel-plugin-surface-group.ts +57 -40
- package/src/plugin/channel-runtime-builders-delivery.ts +3 -0
- package/src/plugin/channel-runtime-types.ts +10 -24
- package/src/plugin/channel-send.ts +241 -154
- package/src/plugin/channel-utils.ts +8 -29
- package/src/plugin/connection-handlers-helpers.ts +0 -15
- package/src/plugin/connection-handlers.ts +6 -5
- package/src/plugin/file-inbound-abort.ts +5 -0
- package/src/plugin/file-inbound-chunk.ts +5 -0
- package/src/plugin/file-inbound-complete.ts +5 -0
- package/src/plugin/file-transfer-orchestrator.ts +105 -3
- package/src/plugin/file-transfer-setup.ts +1 -1
- package/src/plugin/media-dedupe-runtime.ts +1 -6
- package/src/plugin/media-orchestrators-runtime-group.ts +2 -0
- package/src/plugin/message-send.ts +7 -8
- package/src/plugin/outbound.ts +6 -22
- package/src/plugin/outbox-drain-loop.ts +2 -2
- package/src/plugin/outbox-file-push-flow.ts +7 -7
- package/src/plugin/outbox-text-push-flow.ts +1 -1
- package/src/plugin/target-runtime.ts +1 -6
- package/src/runtime/outbound-ack-timeout.ts +1 -8
- package/src/runtime/outbox-transitions.ts +1 -4
- package/src/runtime/status-worker.ts +1 -9
- package/src/messaging/outbound/build-send-action.ts +0 -130
- package/src/messaging/outbound/durable-message-adapter.ts +0 -121
- package/src/messaging/outbound/send-params.ts +0 -81
- package/src/messaging/outbound/send.ts +0 -121
- package/src/openclaw/ingress-runtime.ts +0 -25
- package/src/plugin/channel-send-runtime-group.ts +0 -37
|
@@ -3,12 +3,38 @@ import { normalizeAccountId } from '../core/accounts.ts';
|
|
|
3
3
|
import { buildBncrDebugJsonMessage } from '../core/logging.ts';
|
|
4
4
|
import type { BncrRoute, OutboxEntry } from '../core/types.ts';
|
|
5
5
|
import { buildBncrDurableQueuedResult } from '../messaging/outbound/durable-queue-adapter.ts';
|
|
6
|
+
|
|
6
7
|
import type { ReplyPayloadInput } from '../messaging/outbound/reply-enqueue.ts';
|
|
7
|
-
import { sendBncrMedia, sendBncrText } from '../messaging/outbound/send.ts';
|
|
8
8
|
import type { BncrChannelSendContext, BncrVerifiedTarget } from './channel-runtime-types.ts';
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Resolve downloadMedia with cascade: override (host/user) > ctx > scene.
|
|
12
|
+
* Marker-level downloadMedia is extracted downstream by the orchestrator's
|
|
13
|
+
* normalisation and has highest priority (overrides whatever we pass).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Override fields shared by sendDispatch and messageSendDispatch. */
|
|
17
|
+
type ChannelSendOverrideFields = {
|
|
18
|
+
text?: string;
|
|
19
|
+
mediaUrl?: string;
|
|
20
|
+
mediaUrls?: unknown;
|
|
21
|
+
asVoice?: boolean;
|
|
22
|
+
audioAsVoice?: boolean;
|
|
23
|
+
downloadMedia?: boolean;
|
|
24
|
+
type?: string;
|
|
25
|
+
kind?: string;
|
|
26
|
+
replyToId?: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function resolveDownloadMedia(
|
|
30
|
+
to: string,
|
|
31
|
+
overrideValue: boolean | undefined,
|
|
32
|
+
ctxValue: boolean | undefined,
|
|
33
|
+
runtime: BncrChannelSendRuntime,
|
|
34
|
+
): boolean | undefined {
|
|
35
|
+
if (overrideValue !== undefined) return overrideValue;
|
|
36
|
+
if (ctxValue !== undefined) return ctxValue;
|
|
37
|
+
return runtime.resolveSceneDownloadMedia?.(to);
|
|
12
38
|
}
|
|
13
39
|
|
|
14
40
|
export type BncrChannelSendRuntime = {
|
|
@@ -26,54 +52,10 @@ export type BncrChannelSendRuntime = {
|
|
|
26
52
|
mediaLocalRoots?: readonly string[];
|
|
27
53
|
}) => Promise<void>;
|
|
28
54
|
listOutboxEntries: () => OutboxEntry[];
|
|
55
|
+
/** Resolve downloadMedia from scene config: marker > scene > global > default(false). */
|
|
56
|
+
resolveSceneDownloadMedia?: (to: string) => boolean | undefined;
|
|
29
57
|
};
|
|
30
58
|
|
|
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
59
|
async function enqueueChannelMessageHandoff(
|
|
78
60
|
runtime: BncrChannelSendRuntime,
|
|
79
61
|
ctx: BncrChannelSendContext,
|
|
@@ -98,129 +80,234 @@ async function enqueueChannelMessageHandoff(
|
|
|
98
80
|
return entries[entries.length - 1];
|
|
99
81
|
}
|
|
100
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Resolve downloadMedia from scene config when not already set by marker/host.
|
|
85
|
+
* Scene cascade: marker > scene config > global > default(false).
|
|
86
|
+
*/
|
|
87
|
+
|
|
101
88
|
export function createBncrChannelSendRuntime(runtime: BncrChannelSendRuntime) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
to,
|
|
113
|
-
ctx,
|
|
114
|
-
payload: {
|
|
115
|
-
text: runtime.asString(ctx?.text || ''),
|
|
116
|
-
mediaUrl: runtime.asString(ctx?.mediaUrl || ''),
|
|
117
|
-
},
|
|
118
|
-
});
|
|
89
|
+
/** Extract media-related override fields from a channel send context. */
|
|
90
|
+
function mediaOverrides(ctx: BncrChannelSendContext) {
|
|
91
|
+
return {
|
|
92
|
+
mediaUrl: runtime.asString(ctx.mediaUrl || ''),
|
|
93
|
+
mediaUrls: ctx.mediaUrls,
|
|
94
|
+
asVoice: ctx.asVoice === true,
|
|
95
|
+
audioAsVoice: ctx.audioAsVoice === true,
|
|
96
|
+
type: runtime.asString(ctx.type || '').trim() || undefined,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
119
99
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
100
|
+
function extractSendFields(
|
|
101
|
+
ctx: BncrChannelSendContext,
|
|
102
|
+
overrides?: ChannelSendOverrideFields,
|
|
103
|
+
): {
|
|
104
|
+
accountId: string;
|
|
105
|
+
to: string;
|
|
106
|
+
text: string;
|
|
107
|
+
mediaUrl: string;
|
|
108
|
+
mediaUrls: unknown;
|
|
109
|
+
asVoice: boolean;
|
|
110
|
+
audioAsVoice: boolean;
|
|
111
|
+
type: string | undefined;
|
|
112
|
+
kind: unknown;
|
|
113
|
+
replyToId: string | undefined;
|
|
114
|
+
extra: Record<string, unknown> | undefined;
|
|
115
|
+
downloadMedia: boolean | undefined;
|
|
116
|
+
} {
|
|
117
|
+
const to = runtime.asString(ctx.to || '').trim();
|
|
118
|
+
|
|
119
|
+
// Extract fields from context / overrides WITHOUT normalising markers.
|
|
120
|
+
// Raw text (with markers) is passed downstream; the orchestrator handles
|
|
121
|
+
// all normalisation including marker parsing, pre-send, and media routing.
|
|
122
|
+
const text = overrides?.text !== undefined ? overrides.text : runtime.asString(ctx.text || '');
|
|
123
|
+
const mediaUrl =
|
|
124
|
+
overrides?.mediaUrl !== undefined ? overrides.mediaUrl : runtime.asString(ctx.mediaUrl || '');
|
|
125
|
+
const mediaUrls = overrides?.mediaUrls !== undefined ? overrides.mediaUrls : ctx.mediaUrls;
|
|
126
|
+
const asVoice = overrides?.asVoice ?? ctx.asVoice === true;
|
|
127
|
+
const audioAsVoice = overrides?.audioAsVoice ?? ctx.audioAsVoice === true;
|
|
128
|
+
const type =
|
|
129
|
+
overrides?.type !== undefined
|
|
130
|
+
? overrides.type
|
|
131
|
+
: runtime.asString(ctx.type || '').trim() || undefined;
|
|
132
|
+
const kind = overrides?.kind ?? ctx.kind;
|
|
133
|
+
const replyToId =
|
|
134
|
+
(overrides?.replyToId ??
|
|
135
|
+
runtime.asString(ctx.replyToId || ctx.replyToMessageId || '').trim()) ||
|
|
136
|
+
undefined;
|
|
137
|
+
const extra =
|
|
138
|
+
typeof ctx.extra === 'object' && ctx.extra !== null ? { ...ctx.extra } : undefined;
|
|
139
|
+
const downloadMedia = resolveDownloadMedia(
|
|
140
|
+
to,
|
|
141
|
+
overrides?.downloadMedia,
|
|
142
|
+
ctx.downloadMedia,
|
|
143
|
+
runtime,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
accountId: normalizeAccountId(ctx.accountId),
|
|
148
|
+
to,
|
|
149
|
+
text: text,
|
|
150
|
+
mediaUrl: mediaUrl,
|
|
151
|
+
mediaUrls,
|
|
152
|
+
asVoice,
|
|
153
|
+
audioAsVoice,
|
|
154
|
+
type,
|
|
155
|
+
kind,
|
|
156
|
+
replyToId,
|
|
157
|
+
extra,
|
|
158
|
+
downloadMedia,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Single dispatch path for channelSendText / channelSendMedia.
|
|
164
|
+
* Both normalize first then branch media vs text via hasMedia.
|
|
165
|
+
* channelSendMedia passes mediaOverrides(ctx) to ensure media routing,
|
|
166
|
+
* channelSendText passes no overrides and relies on ctx content alone.
|
|
167
|
+
*/
|
|
168
|
+
async function sendDispatch(
|
|
169
|
+
ctx: BncrChannelSendContext,
|
|
170
|
+
overrides?: {
|
|
171
|
+
text?: string;
|
|
172
|
+
mediaUrl?: string;
|
|
173
|
+
mediaUrls?: unknown;
|
|
174
|
+
asVoice?: boolean;
|
|
175
|
+
audioAsVoice?: boolean;
|
|
176
|
+
downloadMedia?: boolean;
|
|
177
|
+
type?: string;
|
|
178
|
+
kind?: string;
|
|
179
|
+
replyToId?: string;
|
|
135
180
|
},
|
|
181
|
+
) {
|
|
182
|
+
await runtime.syncDebugFlag();
|
|
183
|
+
const {
|
|
184
|
+
accountId,
|
|
185
|
+
to,
|
|
186
|
+
text,
|
|
187
|
+
mediaUrl,
|
|
188
|
+
mediaUrls,
|
|
189
|
+
asVoice,
|
|
190
|
+
audioAsVoice,
|
|
191
|
+
type,
|
|
192
|
+
kind,
|
|
193
|
+
replyToId,
|
|
194
|
+
extra,
|
|
195
|
+
downloadMedia,
|
|
196
|
+
} = extractSendFields(ctx, overrides);
|
|
136
197
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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',
|
|
198
|
+
runtime.logInfo(
|
|
199
|
+
'outbound',
|
|
200
|
+
buildBncrDebugJsonMessage('send-raw', {
|
|
148
201
|
accountId,
|
|
149
202
|
to,
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
audioAsVoice,
|
|
157
|
-
},
|
|
158
|
-
});
|
|
203
|
+
textLen: text.length,
|
|
204
|
+
hasMedia: Boolean(mediaUrl || (Array.isArray(mediaUrls) && mediaUrls.length)),
|
|
205
|
+
downloadMedia,
|
|
206
|
+
}),
|
|
207
|
+
{ debugOnly: true },
|
|
208
|
+
);
|
|
159
209
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
210
|
+
const verified = runtime.resolveVerifiedTarget(to, accountId);
|
|
211
|
+
runtime.rememberSessionRoute(verified.sessionKey, accountId, verified.route);
|
|
212
|
+
|
|
213
|
+
await runtime.enqueueFromReply({
|
|
214
|
+
accountId,
|
|
215
|
+
sessionKey: verified.sessionKey,
|
|
216
|
+
route: verified.route,
|
|
217
|
+
payload: {
|
|
218
|
+
text: text,
|
|
219
|
+
mediaUrl: mediaUrl || undefined,
|
|
220
|
+
mediaUrls: Array.isArray(mediaUrls) && mediaUrls.length ? mediaUrls : undefined,
|
|
221
|
+
asVoice: asVoice || undefined,
|
|
222
|
+
audioAsVoice: audioAsVoice || undefined,
|
|
223
|
+
downloadMedia,
|
|
169
224
|
type,
|
|
170
|
-
|
|
225
|
+
extra,
|
|
226
|
+
kind: kind as 'tool' | 'block' | 'final' | undefined,
|
|
171
227
|
replyToId,
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
rememberSessionRoute: (sessionKey, routeAccountId, route) =>
|
|
176
|
-
runtime.rememberSessionRoute(sessionKey, routeAccountId, route),
|
|
177
|
-
enqueueFromReply: (args) => runtime.enqueueFromReply(args),
|
|
178
|
-
createMessageId: () => randomUUID(),
|
|
179
|
-
});
|
|
180
|
-
},
|
|
228
|
+
},
|
|
229
|
+
mediaLocalRoots: ctx.mediaLocalRoots,
|
|
230
|
+
});
|
|
181
231
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
return buildBncrDurableQueuedResult({ entry });
|
|
189
|
-
},
|
|
232
|
+
return {
|
|
233
|
+
channel: runtime.channelId,
|
|
234
|
+
messageId: randomUUID(),
|
|
235
|
+
chatId: verified.sessionKey,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
190
238
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
239
|
+
/**
|
|
240
|
+
* Durable channel.message path: same normalize, then enqueue handoff.
|
|
241
|
+
*/
|
|
242
|
+
async function messageSendDispatch(
|
|
243
|
+
ctx: BncrChannelSendContext,
|
|
244
|
+
overrides?: {
|
|
245
|
+
text?: string;
|
|
246
|
+
mediaUrl?: string;
|
|
247
|
+
mediaUrls?: unknown;
|
|
248
|
+
asVoice?: boolean;
|
|
249
|
+
audioAsVoice?: boolean;
|
|
250
|
+
downloadMedia?: boolean;
|
|
251
|
+
type?: string;
|
|
252
|
+
kind?: string;
|
|
253
|
+
replyToId?: string;
|
|
203
254
|
},
|
|
255
|
+
) {
|
|
256
|
+
const {
|
|
257
|
+
text,
|
|
258
|
+
mediaUrl,
|
|
259
|
+
mediaUrls,
|
|
260
|
+
asVoice,
|
|
261
|
+
audioAsVoice,
|
|
262
|
+
type,
|
|
263
|
+
kind,
|
|
264
|
+
replyToId,
|
|
265
|
+
extra,
|
|
266
|
+
downloadMedia,
|
|
267
|
+
} = extractSendFields(ctx, overrides);
|
|
268
|
+
|
|
269
|
+
const entry = await enqueueChannelMessageHandoff(runtime, ctx, {
|
|
270
|
+
text: text,
|
|
271
|
+
mediaUrl: mediaUrl || '',
|
|
272
|
+
mediaUrls: Array.isArray(mediaUrls) && mediaUrls.length ? mediaUrls : undefined,
|
|
273
|
+
asVoice,
|
|
274
|
+
audioAsVoice,
|
|
275
|
+
downloadMedia,
|
|
276
|
+
type,
|
|
277
|
+
extra,
|
|
278
|
+
kind: kind as 'tool' | 'block' | 'final' | undefined,
|
|
279
|
+
replyToId,
|
|
280
|
+
});
|
|
281
|
+
return buildBncrDurableQueuedResult({ entry });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
channelSendText: (ctx: BncrChannelSendContext) => sendDispatch(ctx),
|
|
286
|
+
channelSendMedia: (ctx: BncrChannelSendContext) => sendDispatch(ctx, mediaOverrides(ctx)),
|
|
287
|
+
|
|
288
|
+
channelMessageSendText: async (ctx: BncrChannelSendContext) => messageSendDispatch(ctx),
|
|
289
|
+
|
|
290
|
+
channelMessageSendMedia: async (ctx: BncrChannelSendContext) =>
|
|
291
|
+
messageSendDispatch(ctx, mediaOverrides(ctx)),
|
|
204
292
|
|
|
205
293
|
channelMessageSendPayload: async (ctx: BncrChannelSendContext) => {
|
|
206
|
-
const
|
|
207
|
-
if (!
|
|
294
|
+
const p = ctx?.payload || {};
|
|
295
|
+
if (!p || typeof p !== 'object') {
|
|
208
296
|
throw new Error('bncr channel.message payload must be an object');
|
|
209
297
|
}
|
|
210
|
-
|
|
211
|
-
text: runtime.asString(
|
|
212
|
-
mediaUrl: runtime.asString(
|
|
213
|
-
mediaUrls: Array.isArray(
|
|
214
|
-
asVoice:
|
|
215
|
-
audioAsVoice:
|
|
216
|
-
|
|
217
|
-
|
|
298
|
+
return messageSendDispatch(ctx, {
|
|
299
|
+
text: runtime.asString(p.text || p.message || p.caption || ''),
|
|
300
|
+
mediaUrl: runtime.asString(p.mediaUrl || ''),
|
|
301
|
+
mediaUrls: Array.isArray(p.mediaUrls) ? p.mediaUrls : undefined,
|
|
302
|
+
asVoice: p.asVoice === true,
|
|
303
|
+
audioAsVoice: p.audioAsVoice === true,
|
|
304
|
+
downloadMedia: p.downloadMedia as boolean | undefined,
|
|
305
|
+
type: runtime.asString(p.type || '').trim() || undefined,
|
|
306
|
+
kind: typeof p.kind === 'string' ? p.kind : ctx.kind,
|
|
218
307
|
replyToId:
|
|
219
|
-
runtime
|
|
220
|
-
|
|
221
|
-
.trim() || undefined,
|
|
308
|
+
runtime.asString(p.replyToId || ctx?.replyToId || ctx?.replyToMessageId || '').trim() ||
|
|
309
|
+
undefined,
|
|
222
310
|
});
|
|
223
|
-
return buildBncrDurableQueuedResult({ entry });
|
|
224
311
|
},
|
|
225
312
|
};
|
|
226
313
|
}
|
|
@@ -1,40 +1,19 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import {
|
|
3
|
+
asString,
|
|
4
|
+
clampFiniteNumber,
|
|
5
|
+
finiteNumberOr,
|
|
6
|
+
isPlainObject,
|
|
7
|
+
now,
|
|
8
|
+
} from '../core/value-sanitize.ts';
|
|
2
9
|
|
|
3
|
-
export
|
|
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
|
-
}
|
|
10
|
+
export { asString, clampFiniteNumber, finiteNumberOr, isPlainObject, now };
|
|
17
11
|
|
|
18
12
|
export function finiteNonNegativeNumberOrNull(value: unknown): number | null {
|
|
19
13
|
const n = Number(value);
|
|
20
14
|
return Number.isFinite(n) && n >= 0 ? n : null;
|
|
21
15
|
}
|
|
22
16
|
|
|
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
17
|
export function backoffMs(retryCount: number): number {
|
|
39
18
|
return Math.max(1_000, 1_000 * 2 ** Math.max(0, retryCount - 1));
|
|
40
19
|
}
|
|
@@ -203,21 +203,6 @@ export function hasTerminalFileAckState(state: FileAckState | undefined): state
|
|
|
203
203
|
return state?.status === 'completed' || state?.status === 'aborted';
|
|
204
204
|
}
|
|
205
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
206
|
export function applyFileAckState(args: {
|
|
222
207
|
transfer: FileSendTransferState;
|
|
223
208
|
stage: BncrFileAckStage;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { GatewayRequestHandlerOptions } from 'openclaw/plugin-sdk/core';
|
|
2
2
|
import { CHANNEL_ID } from '../core/accounts.ts';
|
|
3
|
+
import { matchesTransferOwner } from '../core/lease-state.ts';
|
|
3
4
|
import type { FileSendTransferState } from '../core/types.ts';
|
|
4
5
|
import {
|
|
5
6
|
applyFileAckState,
|
|
@@ -16,7 +17,6 @@ import {
|
|
|
16
17
|
hasTerminalFileAckState,
|
|
17
18
|
isBncrFileAckStage,
|
|
18
19
|
type LeaseEventPayload,
|
|
19
|
-
matchesTransferOwner,
|
|
20
20
|
type PreparedAckHandling,
|
|
21
21
|
resolveFileAckLeaseEventKind,
|
|
22
22
|
} from './connection-handlers-helpers.ts';
|
|
@@ -363,20 +363,21 @@ export function createBncrConnectionHandlers(runtime: BncrConnectionHandlersRunt
|
|
|
363
363
|
let resolvedState = activeTransfer?.status ?? 'late';
|
|
364
364
|
|
|
365
365
|
if (staleObserved.stale) {
|
|
366
|
-
const
|
|
367
|
-
|
|
366
|
+
const sameOwner = matchesTransferOwner({
|
|
367
|
+
ownerConnId: transferOwnerConnId,
|
|
368
|
+
ownerClientId: transferOwnerClientId,
|
|
368
369
|
connId,
|
|
369
370
|
clientId,
|
|
370
371
|
});
|
|
371
372
|
const adopted =
|
|
372
|
-
!
|
|
373
|
+
!sameOwner &&
|
|
373
374
|
runtime.tryAdoptTransferOwner({
|
|
374
375
|
accountId,
|
|
375
376
|
transfer: activeTransfer,
|
|
376
377
|
connId,
|
|
377
378
|
clientId,
|
|
378
379
|
});
|
|
379
|
-
if (!
|
|
380
|
+
if (!sameOwner && !adopted) {
|
|
380
381
|
runtime.logWarn(
|
|
381
382
|
'stale',
|
|
382
383
|
`ignore kind=file.ack accountId=${accountId} connId=${connId} clientId=${clientId || '-'} transferId=${transferId} stage=${stage} reason=owner-mismatch ownerConnId=${transferOwnerConnId || '-'} ownerClientId=${transferOwnerClientId || '-'}`,
|
|
@@ -59,6 +59,11 @@ export function createBncrFileInboundAbortHandler(runtime: BncrFileInboundRuntim
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
const staleObserved = runtime.observeLease('file.abort', params ?? {});
|
|
62
|
+
// #not-merged Intentionally duplicated across complete/chunk/abort.
|
|
63
|
+
// Each handler emits a distinct logWarn message with its own event-kind
|
|
64
|
+
// in the format string, and the surrounding logic diverges in more than
|
|
65
|
+
// just the event name. Merging would add parameter-gluing boilerplate
|
|
66
|
+
// with no reduction in total LOC. See also the sibling files.
|
|
62
67
|
if (staleObserved.stale) {
|
|
63
68
|
if (
|
|
64
69
|
!runtime.matchesTransferOwner({
|
|
@@ -78,6 +78,11 @@ export function createBncrFileInboundChunkHandler(runtime: BncrFileInboundRuntim
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
const staleObserved = runtime.observeLease('file.chunk', params ?? {});
|
|
81
|
+
// #not-merged Intentionally duplicated across complete/chunk/abort.
|
|
82
|
+
// Each handler emits a distinct logWarn message with its own event-kind
|
|
83
|
+
// in the format string, and the surrounding logic diverges in more than
|
|
84
|
+
// just the event name. Merging would add parameter-gluing boilerplate
|
|
85
|
+
// with no reduction in total LOC. See also the sibling files.
|
|
81
86
|
if (staleObserved.stale) {
|
|
82
87
|
if (
|
|
83
88
|
!runtime.matchesTransferOwner({
|
|
@@ -63,6 +63,11 @@ export function createBncrFileInboundCompleteHandler(runtime: BncrFileInboundRun
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
const staleObserved = runtime.observeLease('file.complete', params ?? {});
|
|
66
|
+
// #not-merged Intentionally duplicated across complete/chunk/abort.
|
|
67
|
+
// Each handler emits a distinct logWarn message with its own event-kind
|
|
68
|
+
// in the format string, and the surrounding logic diverges in more than
|
|
69
|
+
// just the event name. Merging would add parameter-gluing boilerplate
|
|
70
|
+
// with no reduction in total LOC. See also the sibling files.
|
|
66
71
|
if (staleObserved.stale) {
|
|
67
72
|
if (
|
|
68
73
|
!runtime.matchesTransferOwner({
|