@xmoxmo/bncr 0.5.6 → 0.5.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.
- package/dist/index.js +1 -9
- package/index.ts +1 -9
- package/package.json +1 -1
- package/scripts/check-pack.mjs +0 -8
- package/scripts/selfcheck.mjs +0 -10
- package/src/bootstrap/runtime-loader.ts +1 -1
- package/src/channel.ts +118 -171
- package/src/core/accounts.ts +1 -6
- package/src/core/connection-reachability.ts +2 -6
- package/src/core/dead-letter-diagnostics.ts +4 -10
- package/src/core/logging.ts +1 -1
- package/src/core/outbox-entry-builders.ts +78 -84
- 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 +10 -56
- package/src/core/permissions.ts +2 -10
- package/src/core/persisted-outbox-entry.ts +54 -11
- package/src/core/policy.ts +1 -11
- package/src/core/register-trace.ts +4 -7
- 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 +2 -32
- package/src/messaging/inbound/scene-admin.ts +8 -8
- package/src/messaging/outbound/diagnostics-debug-builders.ts +0 -15
- package/src/messaging/outbound/diagnostics.ts +1 -7
- package/src/messaging/outbound/durable-queue-adapter.ts +17 -11
- package/src/messaging/outbound/marker-parser.ts +277 -28
- package/src/messaging/outbound/media-dedupe.ts +3 -10
- package/src/messaging/outbound/media.ts +1 -6
- package/src/messaging/outbound/normalize-outbound-send.ts +329 -0
- package/src/messaging/outbound/queue-selectors.ts +1 -5
- package/src/messaging/outbound/reply-enqueue-media.ts +1 -184
- package/src/messaging/outbound/reply-enqueue.ts +313 -175
- package/src/messaging/outbound/session-route.ts +6 -11
- package/src/messaging/outbound/target-resolver.ts +1 -6
- package/src/openclaw/media-runtime.ts +1 -1
- package/src/openclaw/routing-runtime.ts +0 -11
- package/src/openclaw/sdk-helpers.ts +0 -2
- package/src/plugin/bridge-media-facade.ts +0 -4
- package/src/plugin/bridge-outbox-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 +3 -362
- package/src/plugin/channel-runtime-builders-status.ts +0 -160
- package/src/plugin/channel-runtime-builders.ts +3 -17
- package/src/plugin/channel-runtime-constants.ts +1 -1
- package/src/plugin/channel-runtime-types.ts +2 -24
- package/src/plugin/channel-send.ts +206 -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/file-transfer-orchestrator.ts +39 -2
- package/src/plugin/media-dedupe-runtime.ts +1 -6
- package/src/plugin/media-orchestrators-runtime-group.ts +20 -149
- package/src/plugin/message-send.ts +7 -8
- package/src/plugin/messaging.ts +3 -3
- package/src/plugin/outbound.ts +6 -26
- package/src/plugin/outbox-drain-loop.ts +2 -2
- package/src/plugin/outbox-push.ts +1 -52
- package/src/plugin/outbox-unified-push-flow.ts +307 -0
- package/src/plugin/scene-registry.ts +1 -1
- package/src/plugin/target-runtime.ts +1 -6
- package/src/runtime/log-dedupe.ts +3 -3
- package/src/runtime/outbound-ack-timeout.ts +3 -12
- package/src/runtime/outbox-transitions.ts +1 -4
- package/src/runtime/status-snapshots.ts +2 -2
- package/src/runtime/status-worker.ts +3 -15
- package/src/core/outbox-text-push-prep.ts +0 -36
- 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/bridge-file-transfer-push-facade.ts +0 -142
- package/src/plugin/channel-send-runtime-group.ts +0 -37
- package/src/plugin/outbox-file-push-flow.ts +0 -69
- package/src/plugin/outbox-text-push-flow.ts +0 -90
|
@@ -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: host override > ctx > scene > default(false).
|
|
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 (host/ctx already resolved above). */
|
|
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,231 @@ 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.
|
|
85
|
+
* Scene cascade: host override > ctx > scene config > default(false).
|
|
86
|
+
* (Marker-level downloadMedia overrides everything and is handled downstream.)
|
|
87
|
+
*/
|
|
88
|
+
|
|
149
89
|
export function createBncrChannelSendRuntime(runtime: BncrChannelSendRuntime) {
|
|
150
|
-
/**
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
90
|
+
/** Extract media-related override fields from a channel send context. */
|
|
91
|
+
function mediaOverrides(ctx: BncrChannelSendContext) {
|
|
92
|
+
return {
|
|
93
|
+
mediaUrl: runtime.asString(ctx.mediaUrl || ''),
|
|
94
|
+
mediaUrls: ctx.mediaUrls,
|
|
95
|
+
asVoice: ctx.asVoice === true,
|
|
96
|
+
audioAsVoice: ctx.audioAsVoice === true,
|
|
97
|
+
type: runtime.asString(ctx.type || '').trim() || undefined,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function extractSendFields(
|
|
102
|
+
ctx: BncrChannelSendContext,
|
|
103
|
+
overrides?: ChannelSendOverrideFields,
|
|
104
|
+
): {
|
|
105
|
+
accountId: string;
|
|
106
|
+
to: string;
|
|
107
|
+
text: string;
|
|
108
|
+
mediaUrl: string;
|
|
109
|
+
mediaUrls: unknown;
|
|
110
|
+
asVoice: boolean;
|
|
111
|
+
audioAsVoice: boolean;
|
|
112
|
+
type: string | undefined;
|
|
113
|
+
kind: unknown;
|
|
114
|
+
replyToId: string | undefined;
|
|
115
|
+
extra: Record<string, unknown> | undefined;
|
|
116
|
+
downloadMedia: boolean | undefined;
|
|
117
|
+
} {
|
|
157
118
|
const to = runtime.asString(ctx.to || '').trim();
|
|
158
|
-
const { cleanText, extra, consumed } = resolveUnifiedOutboundExtra(ctx);
|
|
159
|
-
const replyToId = resolveChannelSendReplyToId(runtime.asString, ctx);
|
|
160
119
|
|
|
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),
|
|
120
|
+
// Extract fields from context / overrides WITHOUT normalising markers.
|
|
121
|
+
// Raw text (with markers) is passed downstream; the orchestrator handles
|
|
122
|
+
// all normalisation including marker parsing, pre-send, and media routing.
|
|
123
|
+
const text = overrides?.text !== undefined ? overrides.text : runtime.asString(ctx.text || '');
|
|
124
|
+
const mediaUrl =
|
|
125
|
+
overrides?.mediaUrl !== undefined ? overrides.mediaUrl : runtime.asString(ctx.mediaUrl || '');
|
|
126
|
+
const mediaUrls = overrides?.mediaUrls !== undefined ? overrides.mediaUrls : ctx.mediaUrls;
|
|
127
|
+
const asVoice = overrides?.asVoice ?? ctx.asVoice === true;
|
|
128
|
+
const audioAsVoice = overrides?.audioAsVoice ?? ctx.audioAsVoice === true;
|
|
129
|
+
const type =
|
|
130
|
+
overrides?.type !== undefined
|
|
131
|
+
? overrides.type
|
|
132
|
+
: runtime.asString(ctx.type || '').trim() || undefined;
|
|
133
|
+
const kind = overrides?.kind ?? ctx.kind;
|
|
134
|
+
const replyToId =
|
|
135
|
+
(overrides?.replyToId ??
|
|
136
|
+
runtime.asString(ctx.replyToId || ctx.replyToMessageId || '').trim()) ||
|
|
137
|
+
undefined;
|
|
138
|
+
const extra =
|
|
139
|
+
typeof ctx.extra === 'object' && ctx.extra !== null ? { ...ctx.extra } : undefined;
|
|
140
|
+
const downloadMedia = resolveDownloadMedia(
|
|
141
|
+
to,
|
|
142
|
+
overrides?.downloadMedia,
|
|
143
|
+
ctx.downloadMedia,
|
|
144
|
+
runtime,
|
|
189
145
|
);
|
|
190
146
|
|
|
191
|
-
|
|
192
|
-
|
|
147
|
+
return {
|
|
148
|
+
accountId: normalizeAccountId(ctx.accountId),
|
|
149
|
+
to,
|
|
150
|
+
text: text,
|
|
151
|
+
mediaUrl: mediaUrl,
|
|
152
|
+
mediaUrls,
|
|
153
|
+
asVoice,
|
|
154
|
+
audioAsVoice,
|
|
155
|
+
type,
|
|
193
156
|
kind,
|
|
157
|
+
replyToId,
|
|
158
|
+
extra,
|
|
159
|
+
downloadMedia,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Single dispatch path for channelSendText / channelSendMedia.
|
|
165
|
+
* Both normalize first then branch media vs text via hasMedia.
|
|
166
|
+
* channelSendMedia passes mediaOverrides(ctx) to ensure media routing,
|
|
167
|
+
* channelSendText passes no overrides and relies on ctx content alone.
|
|
168
|
+
*/
|
|
169
|
+
async function sendDispatch(
|
|
170
|
+
ctx: BncrChannelSendContext,
|
|
171
|
+
overrides?: {
|
|
172
|
+
text?: string;
|
|
173
|
+
mediaUrl?: string;
|
|
174
|
+
mediaUrls?: unknown;
|
|
175
|
+
asVoice?: boolean;
|
|
176
|
+
audioAsVoice?: boolean;
|
|
177
|
+
downloadMedia?: boolean;
|
|
178
|
+
type?: string;
|
|
179
|
+
kind?: string;
|
|
180
|
+
replyToId?: string;
|
|
181
|
+
},
|
|
182
|
+
) {
|
|
183
|
+
await runtime.syncDebugFlag();
|
|
184
|
+
const {
|
|
194
185
|
accountId,
|
|
195
186
|
to,
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
187
|
+
text,
|
|
188
|
+
mediaUrl,
|
|
189
|
+
mediaUrls,
|
|
190
|
+
asVoice,
|
|
191
|
+
audioAsVoice,
|
|
192
|
+
type,
|
|
193
|
+
kind,
|
|
194
|
+
replyToId,
|
|
195
|
+
extra,
|
|
196
|
+
downloadMedia,
|
|
197
|
+
} = extractSendFields(ctx, overrides);
|
|
205
198
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
199
|
+
runtime.logInfo(
|
|
200
|
+
'outbound',
|
|
201
|
+
buildBncrDebugJsonMessage('send-raw', {
|
|
209
202
|
accountId,
|
|
210
203
|
to,
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
mediaUrls: effectiveMediaUrls,
|
|
214
|
-
asVoice,
|
|
215
|
-
audioAsVoice,
|
|
204
|
+
textLen: text.length,
|
|
205
|
+
hasMedia: Boolean(mediaUrl || (Array.isArray(mediaUrls) && mediaUrls.length)),
|
|
216
206
|
downloadMedia,
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
rememberSessionRoute: (...a) => runtime.rememberSessionRoute(...a),
|
|
224
|
-
enqueueFromReply: (a) => runtime.enqueueFromReply(a),
|
|
225
|
-
createMessageId: () => randomUUID(),
|
|
226
|
-
});
|
|
227
|
-
}
|
|
207
|
+
}),
|
|
208
|
+
{ debugOnly: true },
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const verified = runtime.resolveVerifiedTarget(to, accountId);
|
|
212
|
+
runtime.rememberSessionRoute(verified.sessionKey, accountId, verified.route);
|
|
228
213
|
|
|
229
|
-
|
|
230
|
-
channelId: runtime.channelId,
|
|
214
|
+
await runtime.enqueueFromReply({
|
|
231
215
|
accountId,
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
216
|
+
sessionKey: verified.sessionKey,
|
|
217
|
+
route: verified.route,
|
|
218
|
+
payload: {
|
|
219
|
+
text: text,
|
|
220
|
+
mediaUrl: mediaUrl || undefined,
|
|
221
|
+
mediaUrls: Array.isArray(mediaUrls) && mediaUrls.length ? mediaUrls : undefined,
|
|
222
|
+
asVoice: asVoice || undefined,
|
|
223
|
+
audioAsVoice: audioAsVoice || undefined,
|
|
224
|
+
downloadMedia,
|
|
225
|
+
type,
|
|
226
|
+
extra,
|
|
227
|
+
kind: kind as 'tool' | 'block' | 'final' | undefined,
|
|
228
|
+
replyToId,
|
|
229
|
+
},
|
|
237
230
|
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
231
|
});
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
channel: runtime.channelId,
|
|
235
|
+
messageId: randomUUID(),
|
|
236
|
+
chatId: verified.sessionKey,
|
|
237
|
+
};
|
|
243
238
|
}
|
|
244
239
|
|
|
245
240
|
/**
|
|
246
|
-
* Durable
|
|
241
|
+
* Durable channel.message path: same normalize, then enqueue handoff.
|
|
247
242
|
*/
|
|
248
243
|
async function messageSendDispatch(
|
|
249
244
|
ctx: BncrChannelSendContext,
|
|
250
|
-
|
|
245
|
+
overrides?: {
|
|
251
246
|
text?: string;
|
|
252
247
|
mediaUrl?: string;
|
|
253
|
-
mediaUrls?:
|
|
248
|
+
mediaUrls?: unknown;
|
|
254
249
|
asVoice?: boolean;
|
|
255
250
|
audioAsVoice?: boolean;
|
|
256
251
|
downloadMedia?: boolean;
|
|
257
252
|
type?: string;
|
|
258
|
-
kind?:
|
|
253
|
+
kind?: string;
|
|
259
254
|
replyToId?: string;
|
|
260
255
|
},
|
|
261
256
|
) {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
JSON.stringify(inputOverrides?.mediaUrl),
|
|
275
|
-
);
|
|
257
|
+
const {
|
|
258
|
+
text,
|
|
259
|
+
mediaUrl,
|
|
260
|
+
mediaUrls,
|
|
261
|
+
asVoice,
|
|
262
|
+
audioAsVoice,
|
|
263
|
+
type,
|
|
264
|
+
kind,
|
|
265
|
+
replyToId,
|
|
266
|
+
extra,
|
|
267
|
+
downloadMedia,
|
|
268
|
+
} = extractSendFields(ctx, overrides);
|
|
276
269
|
|
|
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);
|
|
270
|
+
const entry = await enqueueChannelMessageHandoff(runtime, ctx, {
|
|
271
|
+
text: text,
|
|
272
|
+
mediaUrl: mediaUrl || '',
|
|
273
|
+
mediaUrls: Array.isArray(mediaUrls) && mediaUrls.length ? mediaUrls : undefined,
|
|
274
|
+
asVoice,
|
|
275
|
+
audioAsVoice,
|
|
276
|
+
downloadMedia,
|
|
277
|
+
type,
|
|
278
|
+
extra,
|
|
279
|
+
kind: kind as 'tool' | 'block' | 'final' | undefined,
|
|
280
|
+
replyToId,
|
|
281
|
+
});
|
|
290
282
|
return buildBncrDurableQueuedResult({ entry });
|
|
291
283
|
}
|
|
292
284
|
|
|
293
285
|
return {
|
|
294
|
-
channelSendText: sendDispatch,
|
|
295
|
-
channelSendMedia: sendDispatch,
|
|
286
|
+
channelSendText: (ctx: BncrChannelSendContext) => sendDispatch(ctx),
|
|
287
|
+
channelSendMedia: (ctx: BncrChannelSendContext) => sendDispatch(ctx, mediaOverrides(ctx)),
|
|
296
288
|
|
|
297
|
-
channelMessageSendText: async (ctx: BncrChannelSendContext) =>
|
|
298
|
-
messageSendDispatch(ctx, { kind: normalizeReplyKind(ctx?.kind) }),
|
|
289
|
+
channelMessageSendText: async (ctx: BncrChannelSendContext) => messageSendDispatch(ctx),
|
|
299
290
|
|
|
300
291
|
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
|
-
}),
|
|
292
|
+
messageSendDispatch(ctx, mediaOverrides(ctx)),
|
|
309
293
|
|
|
310
294
|
channelMessageSendPayload: async (ctx: BncrChannelSendContext) => {
|
|
311
295
|
const p = ctx?.payload || {};
|
|
312
|
-
if (!p || typeof p !== 'object')
|
|
296
|
+
if (!p || typeof p !== 'object') {
|
|
313
297
|
throw new Error('bncr channel.message payload must be an object');
|
|
298
|
+
}
|
|
314
299
|
return messageSendDispatch(ctx, {
|
|
315
300
|
text: runtime.asString(p.text || p.message || p.caption || ''),
|
|
316
301
|
mediaUrl: runtime.asString(p.mediaUrl || ''),
|
|
317
302
|
mediaUrls: Array.isArray(p.mediaUrls) ? p.mediaUrls : undefined,
|
|
318
303
|
asVoice: p.asVoice === true,
|
|
319
304
|
audioAsVoice: p.audioAsVoice === true,
|
|
320
|
-
downloadMedia: p.downloadMedia
|
|
305
|
+
downloadMedia: p.downloadMedia as boolean | undefined,
|
|
321
306
|
type: runtime.asString(p.type || '').trim() || undefined,
|
|
322
|
-
kind:
|
|
307
|
+
kind: typeof p.kind === 'string' ? p.kind : ctx.kind,
|
|
323
308
|
replyToId:
|
|
324
309
|
runtime.asString(p.replyToId || ctx?.replyToId || ctx?.replyToMessageId || '').trim() ||
|
|
325
310
|
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({
|