@xmoxmo/bncr 0.5.6 → 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 +79 -43
- 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 +34 -18
- 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/parse.ts +1 -31
- package/src/messaging/inbound/scene-admin.ts +8 -8
- 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 +62 -28
- package/src/messaging/outbound/media-dedupe.ts +2 -6
- package/src/messaging/outbound/media.ts +1 -6
- 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.ts +121 -70
- 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 +6 -6
- package/src/plugin/bridge-media-facade.ts +0 -1
- package/src/plugin/channel-components.ts +0 -13
- package/src/plugin/channel-plugin-surface-group.ts +52 -65
- package/src/plugin/channel-runtime-builders-delivery.ts +1 -0
- package/src/plugin/channel-runtime-types.ts +2 -24
- package/src/plugin/channel-send.ts +205 -221
- 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/media-dedupe-runtime.ts +1 -6
- package/src/plugin/message-send.ts +7 -8
- package/src/plugin/outbound.ts +6 -26
- 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 -114
- package/src/messaging/outbound/send.ts +0 -127
- package/src/openclaw/ingress-runtime.ts +0 -25
- package/src/plugin/channel-send-runtime-group.ts +0 -37
|
@@ -3,13 +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
7
|
import type { ReplyPayloadInput } from '../messaging/outbound/reply-enqueue.ts';
|
|
8
|
-
import { sendBncrMedia, sendBncrText } from '../messaging/outbound/send.ts';
|
|
9
8
|
import type { BncrChannelSendContext, BncrVerifiedTarget } from './channel-runtime-types.ts';
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
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);
|
|
13
38
|
}
|
|
14
39
|
|
|
15
40
|
export type BncrChannelSendRuntime = {
|
|
@@ -27,106 +52,15 @@ export type BncrChannelSendRuntime = {
|
|
|
27
52
|
mediaLocalRoots?: readonly string[];
|
|
28
53
|
}) => Promise<void>;
|
|
29
54
|
listOutboxEntries: () => OutboxEntry[];
|
|
55
|
+
/** Resolve downloadMedia from scene config: marker > scene > global > default(false). */
|
|
56
|
+
resolveSceneDownloadMedia?: (to: string) => boolean | undefined;
|
|
30
57
|
};
|
|
31
58
|
|
|
32
|
-
/**
|
|
33
|
-
* Merge all extra sources (ctx.extra, marker params, host-level fields)
|
|
34
|
-
* into a single extra bag. Strips consumption fields and returns them
|
|
35
|
-
* as independent params, remaining extra passes through the pipeline.
|
|
36
|
-
*/
|
|
37
|
-
function resolveUnifiedOutboundExtra(ctx: BncrChannelSendContext): {
|
|
38
|
-
cleanText: string;
|
|
39
|
-
extra: Record<string, unknown>;
|
|
40
|
-
consumed: {
|
|
41
|
-
asVoice?: boolean;
|
|
42
|
-
audioAsVoice?: boolean;
|
|
43
|
-
downloadMedia?: boolean;
|
|
44
|
-
type?: string;
|
|
45
|
-
kind?: string;
|
|
46
|
-
replyToId?: string;
|
|
47
|
-
};
|
|
48
|
-
} {
|
|
49
|
-
// 1. Parse marker from text
|
|
50
|
-
const { cleanText, params: markerParams } = parseBncrMarker(
|
|
51
|
-
typeof ctx.text === 'string' ? ctx.text : '',
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
// 2. Merge all sources: extra from ctx + marker params + host-level fields
|
|
55
|
-
const merged: Record<string, unknown> = {
|
|
56
|
-
...(typeof ctx.extra === 'object' && ctx.extra !== null ? ctx.extra : {}),
|
|
57
|
-
...markerParams,
|
|
58
|
-
};
|
|
59
|
-
if (ctx.forceDocument === true) merged.forceDocument = true;
|
|
60
|
-
if (ctx.gifPlayback === true) merged.gifPlayback = true;
|
|
61
|
-
if (ctx.silent === true) merged.silent = true;
|
|
62
|
-
|
|
63
|
-
// 3. Strip consumption fields
|
|
64
|
-
const { consumed, remaining } = extractConsumptionFields(merged);
|
|
65
|
-
|
|
66
|
-
return {
|
|
67
|
-
cleanText,
|
|
68
|
-
extra: remaining,
|
|
69
|
-
consumed,
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function resolveChannelSendReplyToId(
|
|
74
|
-
asString: BncrChannelSendRuntime['asString'],
|
|
75
|
-
ctx: BncrChannelSendContext,
|
|
76
|
-
) {
|
|
77
|
-
return asString(ctx?.replyToId || ctx?.replyToMessageId || '').trim() || undefined;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function logChannelSendEntry(
|
|
81
|
-
runtime: BncrChannelSendRuntime,
|
|
82
|
-
args: {
|
|
83
|
-
kind: 'text' | 'media';
|
|
84
|
-
accountId: string;
|
|
85
|
-
to: string;
|
|
86
|
-
ctx: BncrChannelSendContext;
|
|
87
|
-
payload: {
|
|
88
|
-
text: string;
|
|
89
|
-
mediaUrl: string;
|
|
90
|
-
mediaUrls?: string[];
|
|
91
|
-
asVoice?: boolean;
|
|
92
|
-
audioAsVoice?: boolean;
|
|
93
|
-
downloadMedia?: boolean;
|
|
94
|
-
};
|
|
95
|
-
},
|
|
96
|
-
) {
|
|
97
|
-
runtime.logInfo(
|
|
98
|
-
'outbound',
|
|
99
|
-
buildBncrDebugJsonMessage(`send-entry:${args.kind}`, {
|
|
100
|
-
accountId: args.accountId,
|
|
101
|
-
to: args.to,
|
|
102
|
-
text: args.payload.text,
|
|
103
|
-
mediaUrl: args.payload.mediaUrl,
|
|
104
|
-
mediaUrls: args.payload.mediaUrls,
|
|
105
|
-
asVoice: args.payload.asVoice,
|
|
106
|
-
audioAsVoice: args.payload.audioAsVoice,
|
|
107
|
-
downloadMedia: args.payload.downloadMedia,
|
|
108
|
-
sessionKey: runtime.asString(args.ctx?.sessionKey || ''),
|
|
109
|
-
mirrorSessionKey: runtime.asString(args.ctx?.mirror?.sessionKey || ''),
|
|
110
|
-
rawCtx: {
|
|
111
|
-
to: args.ctx?.to,
|
|
112
|
-
accountId: args.ctx?.accountId,
|
|
113
|
-
threadId: args.ctx?.threadId,
|
|
114
|
-
replyToId: args.ctx?.replyToId,
|
|
115
|
-
},
|
|
116
|
-
}),
|
|
117
|
-
{ debugOnly: true },
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
59
|
async function enqueueChannelMessageHandoff(
|
|
122
60
|
runtime: BncrChannelSendRuntime,
|
|
123
61
|
ctx: BncrChannelSendContext,
|
|
124
62
|
payload: ReplyPayloadInput,
|
|
125
|
-
extra?: Record<string, unknown>,
|
|
126
63
|
) {
|
|
127
|
-
if (extra && Object.keys(extra).length > 0) {
|
|
128
|
-
payload = { ...payload, extra: { ...extra } };
|
|
129
|
-
}
|
|
130
64
|
const accountId = normalizeAccountId(ctx.accountId);
|
|
131
65
|
const to = runtime.asString(ctx.to || '').trim();
|
|
132
66
|
const verified = runtime.resolveVerifiedTarget(to, accountId);
|
|
@@ -146,180 +80,230 @@ async function enqueueChannelMessageHandoff(
|
|
|
146
80
|
return entries[entries.length - 1];
|
|
147
81
|
}
|
|
148
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
|
+
|
|
149
88
|
export function createBncrChannelSendRuntime(runtime: BncrChannelSendRuntime) {
|
|
150
|
-
/**
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
+
}
|
|
99
|
+
|
|
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
|
+
} {
|
|
157
117
|
const to = runtime.asString(ctx.to || '').trim();
|
|
158
|
-
const { cleanText, extra, consumed } = resolveUnifiedOutboundExtra(ctx);
|
|
159
|
-
const replyToId = resolveChannelSendReplyToId(runtime.asString, ctx);
|
|
160
118
|
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
hasMedia +
|
|
187
|
-
'|mediaUrl=' +
|
|
188
|
-
JSON.stringify(effectiveMediaUrl),
|
|
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,
|
|
189
144
|
);
|
|
190
145
|
|
|
191
|
-
|
|
192
|
-
|
|
146
|
+
return {
|
|
147
|
+
accountId: normalizeAccountId(ctx.accountId),
|
|
148
|
+
to,
|
|
149
|
+
text: text,
|
|
150
|
+
mediaUrl: mediaUrl,
|
|
151
|
+
mediaUrls,
|
|
152
|
+
asVoice,
|
|
153
|
+
audioAsVoice,
|
|
154
|
+
type,
|
|
193
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;
|
|
180
|
+
},
|
|
181
|
+
) {
|
|
182
|
+
await runtime.syncDebugFlag();
|
|
183
|
+
const {
|
|
194
184
|
accountId,
|
|
195
185
|
to,
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
186
|
+
text,
|
|
187
|
+
mediaUrl,
|
|
188
|
+
mediaUrls,
|
|
189
|
+
asVoice,
|
|
190
|
+
audioAsVoice,
|
|
191
|
+
type,
|
|
192
|
+
kind,
|
|
193
|
+
replyToId,
|
|
194
|
+
extra,
|
|
195
|
+
downloadMedia,
|
|
196
|
+
} = extractSendFields(ctx, overrides);
|
|
205
197
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
198
|
+
runtime.logInfo(
|
|
199
|
+
'outbound',
|
|
200
|
+
buildBncrDebugJsonMessage('send-raw', {
|
|
209
201
|
accountId,
|
|
210
202
|
to,
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
mediaUrls: effectiveMediaUrls,
|
|
214
|
-
asVoice,
|
|
215
|
-
audioAsVoice,
|
|
203
|
+
textLen: text.length,
|
|
204
|
+
hasMedia: Boolean(mediaUrl || (Array.isArray(mediaUrls) && mediaUrls.length)),
|
|
216
205
|
downloadMedia,
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
rememberSessionRoute: (...a) => runtime.rememberSessionRoute(...a),
|
|
224
|
-
enqueueFromReply: (a) => runtime.enqueueFromReply(a),
|
|
225
|
-
createMessageId: () => randomUUID(),
|
|
226
|
-
});
|
|
227
|
-
}
|
|
206
|
+
}),
|
|
207
|
+
{ debugOnly: true },
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
const verified = runtime.resolveVerifiedTarget(to, accountId);
|
|
211
|
+
runtime.rememberSessionRoute(verified.sessionKey, accountId, verified.route);
|
|
228
212
|
|
|
229
|
-
|
|
230
|
-
channelId: runtime.channelId,
|
|
213
|
+
await runtime.enqueueFromReply({
|
|
231
214
|
accountId,
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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,
|
|
224
|
+
type,
|
|
225
|
+
extra,
|
|
226
|
+
kind: kind as 'tool' | 'block' | 'final' | undefined,
|
|
227
|
+
replyToId,
|
|
228
|
+
},
|
|
237
229
|
mediaLocalRoots: ctx.mediaLocalRoots,
|
|
238
|
-
resolveVerifiedTarget: (...a) => runtime.resolveVerifiedTarget(...a),
|
|
239
|
-
rememberSessionRoute: (...a) => runtime.rememberSessionRoute(...a),
|
|
240
|
-
enqueueFromReply: (a) => runtime.enqueueFromReply(a),
|
|
241
|
-
createMessageId: () => randomUUID(),
|
|
242
230
|
});
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
channel: runtime.channelId,
|
|
234
|
+
messageId: randomUUID(),
|
|
235
|
+
chatId: verified.sessionKey,
|
|
236
|
+
};
|
|
243
237
|
}
|
|
244
238
|
|
|
245
239
|
/**
|
|
246
|
-
* Durable
|
|
240
|
+
* Durable channel.message path: same normalize, then enqueue handoff.
|
|
247
241
|
*/
|
|
248
242
|
async function messageSendDispatch(
|
|
249
243
|
ctx: BncrChannelSendContext,
|
|
250
|
-
|
|
244
|
+
overrides?: {
|
|
251
245
|
text?: string;
|
|
252
246
|
mediaUrl?: string;
|
|
253
|
-
mediaUrls?:
|
|
247
|
+
mediaUrls?: unknown;
|
|
254
248
|
asVoice?: boolean;
|
|
255
249
|
audioAsVoice?: boolean;
|
|
256
250
|
downloadMedia?: boolean;
|
|
257
251
|
type?: string;
|
|
258
|
-
kind?:
|
|
252
|
+
kind?: string;
|
|
259
253
|
replyToId?: string;
|
|
260
254
|
},
|
|
261
255
|
) {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
JSON.stringify(inputOverrides?.mediaUrl),
|
|
275
|
-
);
|
|
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);
|
|
276
268
|
|
|
277
|
-
const
|
|
278
|
-
text:
|
|
279
|
-
mediaUrl:
|
|
280
|
-
mediaUrls:
|
|
281
|
-
asVoice
|
|
282
|
-
audioAsVoice
|
|
283
|
-
downloadMedia
|
|
284
|
-
type
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
};
|
|
289
|
-
const entry = await enqueueChannelMessageHandoff(runtime, ctx, payload, extra);
|
|
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
|
+
});
|
|
290
281
|
return buildBncrDurableQueuedResult({ entry });
|
|
291
282
|
}
|
|
292
283
|
|
|
293
284
|
return {
|
|
294
|
-
channelSendText: sendDispatch,
|
|
295
|
-
channelSendMedia: sendDispatch,
|
|
285
|
+
channelSendText: (ctx: BncrChannelSendContext) => sendDispatch(ctx),
|
|
286
|
+
channelSendMedia: (ctx: BncrChannelSendContext) => sendDispatch(ctx, mediaOverrides(ctx)),
|
|
296
287
|
|
|
297
|
-
channelMessageSendText: async (ctx: BncrChannelSendContext) =>
|
|
298
|
-
messageSendDispatch(ctx, { kind: normalizeReplyKind(ctx?.kind) }),
|
|
288
|
+
channelMessageSendText: async (ctx: BncrChannelSendContext) => messageSendDispatch(ctx),
|
|
299
289
|
|
|
300
290
|
channelMessageSendMedia: async (ctx: BncrChannelSendContext) =>
|
|
301
|
-
messageSendDispatch(ctx,
|
|
302
|
-
mediaUrl: runtime.asString(ctx.mediaUrl || ''),
|
|
303
|
-
mediaUrls: Array.isArray(ctx?.mediaUrls) ? ctx.mediaUrls : undefined,
|
|
304
|
-
asVoice: ctx?.asVoice === true,
|
|
305
|
-
audioAsVoice: ctx?.audioAsVoice === true,
|
|
306
|
-
type: runtime.asString(ctx.type || '').trim() || undefined,
|
|
307
|
-
kind: normalizeReplyKind(ctx?.kind),
|
|
308
|
-
}),
|
|
291
|
+
messageSendDispatch(ctx, mediaOverrides(ctx)),
|
|
309
292
|
|
|
310
293
|
channelMessageSendPayload: async (ctx: BncrChannelSendContext) => {
|
|
311
294
|
const p = ctx?.payload || {};
|
|
312
|
-
if (!p || typeof p !== 'object')
|
|
295
|
+
if (!p || typeof p !== 'object') {
|
|
313
296
|
throw new Error('bncr channel.message payload must be an object');
|
|
297
|
+
}
|
|
314
298
|
return messageSendDispatch(ctx, {
|
|
315
299
|
text: runtime.asString(p.text || p.message || p.caption || ''),
|
|
316
300
|
mediaUrl: runtime.asString(p.mediaUrl || ''),
|
|
317
301
|
mediaUrls: Array.isArray(p.mediaUrls) ? p.mediaUrls : undefined,
|
|
318
302
|
asVoice: p.asVoice === true,
|
|
319
303
|
audioAsVoice: p.audioAsVoice === true,
|
|
320
|
-
downloadMedia: p.downloadMedia
|
|
304
|
+
downloadMedia: p.downloadMedia as boolean | undefined,
|
|
321
305
|
type: runtime.asString(p.type || '').trim() || undefined,
|
|
322
|
-
kind:
|
|
306
|
+
kind: typeof p.kind === 'string' ? p.kind : ctx.kind,
|
|
323
307
|
replyToId:
|
|
324
308
|
runtime.asString(p.replyToId || ctx?.replyToId || ctx?.replyToMessageId || '').trim() ||
|
|
325
309
|
undefined,
|
|
@@ -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({
|