@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
|
@@ -1,11 +1,34 @@
|
|
|
1
|
+
import { buildBncrDebugJsonMessage } from '../../core/logging.ts';
|
|
1
2
|
import type { BncrRoute, OutboxEntry } from '../../core/types.ts';
|
|
2
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
buildNormalizedSends,
|
|
5
|
+
type NormalizedOutboundSend,
|
|
6
|
+
normalizeOutboundSend,
|
|
7
|
+
} from './normalize-outbound-send.ts';
|
|
3
8
|
import { hasReplyMediaEntries } from './reply-enqueue-media.ts';
|
|
4
9
|
import type { OutboundReplyTargetPolicy } from './reply-target-policy.ts';
|
|
5
10
|
import { normalizeOutboundReplyToId } from './reply-target-policy.ts';
|
|
6
11
|
|
|
7
12
|
const MEDIA_TEXT_SPLIT_THRESHOLD = 1020;
|
|
8
13
|
|
|
14
|
+
type BuildOutboxEntryFn = (args: {
|
|
15
|
+
accountId: string;
|
|
16
|
+
sessionKey: string;
|
|
17
|
+
route: BncrRoute;
|
|
18
|
+
type?: string;
|
|
19
|
+
msg: string;
|
|
20
|
+
kind?: 'tool' | 'block' | 'final';
|
|
21
|
+
replyToId?: string;
|
|
22
|
+
replyTargetPolicy?: OutboundReplyTargetPolicy;
|
|
23
|
+
extra?: Record<string, unknown>;
|
|
24
|
+
transferMode?: 'text' | 'media';
|
|
25
|
+
mediaUrl?: string;
|
|
26
|
+
mediaLocalRoots?: readonly string[];
|
|
27
|
+
asVoice?: boolean;
|
|
28
|
+
audioAsVoice?: boolean;
|
|
29
|
+
downloadMedia?: boolean;
|
|
30
|
+
}) => OutboxEntry;
|
|
31
|
+
|
|
9
32
|
export type ReplyEnqueuePlan =
|
|
10
33
|
| { kind: 'text-only' }
|
|
11
34
|
| { kind: 'media-only'; clearText: false }
|
|
@@ -21,6 +44,7 @@ export type ReplyPayloadInput = {
|
|
|
21
44
|
audioAsVoice?: boolean;
|
|
22
45
|
downloadMedia?: boolean;
|
|
23
46
|
type?: string;
|
|
47
|
+
markerHasMsg?: boolean;
|
|
24
48
|
extra?: Record<string, unknown>;
|
|
25
49
|
kind?: 'tool' | 'block' | 'final';
|
|
26
50
|
replyToId?: string;
|
|
@@ -35,20 +59,13 @@ export type NormalizedReplyPayload = {
|
|
|
35
59
|
audioAsVoice: boolean;
|
|
36
60
|
downloadMedia?: boolean;
|
|
37
61
|
type?: string;
|
|
62
|
+
markerHasMsg?: boolean;
|
|
38
63
|
extra?: Record<string, unknown>;
|
|
39
64
|
kind?: 'tool' | 'block' | 'final';
|
|
40
65
|
replyToId: string;
|
|
41
66
|
replyTargetPolicy: OutboundReplyTargetPolicy;
|
|
42
67
|
};
|
|
43
68
|
|
|
44
|
-
export type ReplyMediaEntriesParams = {
|
|
45
|
-
accountId: string;
|
|
46
|
-
sessionKey: string;
|
|
47
|
-
route: BncrRoute;
|
|
48
|
-
payload: NormalizedReplyPayload;
|
|
49
|
-
mediaLocalRoots?: readonly string[];
|
|
50
|
-
};
|
|
51
|
-
|
|
52
69
|
export type EnqueueNormalizedReplyPayloadParams = {
|
|
53
70
|
accountId: string;
|
|
54
71
|
sessionKey: string;
|
|
@@ -58,46 +75,7 @@ export type EnqueueNormalizedReplyPayloadParams = {
|
|
|
58
75
|
replyToId?: string;
|
|
59
76
|
};
|
|
60
77
|
|
|
61
|
-
|
|
62
|
-
accountId: string;
|
|
63
|
-
sessionKey: string;
|
|
64
|
-
route: BncrRoute;
|
|
65
|
-
mediaUrl: string;
|
|
66
|
-
mediaLocalRoots?: readonly string[];
|
|
67
|
-
text: string;
|
|
68
|
-
normalizedText: string;
|
|
69
|
-
asVoice: boolean;
|
|
70
|
-
audioAsVoice: boolean;
|
|
71
|
-
downloadMedia?: boolean;
|
|
72
|
-
type?: string;
|
|
73
|
-
extra?: Record<string, unknown>;
|
|
74
|
-
kind?: 'tool' | 'block' | 'final';
|
|
75
|
-
replyToId: string;
|
|
76
|
-
replyTargetPolicy: OutboundReplyTargetPolicy;
|
|
77
|
-
createdAt: number;
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
export type EnqueueSingleReplyMediaEntryParams = {
|
|
81
|
-
params: ReplyMediaEntriesParams;
|
|
82
|
-
mediaUrl: string;
|
|
83
|
-
normalizedText: string;
|
|
84
|
-
text: string;
|
|
85
|
-
fallback: { text: string; reason: string } | null;
|
|
86
|
-
currentTime: number;
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
export type ReplyMediaFallbackTextEntryParams = {
|
|
90
|
-
accountId: string;
|
|
91
|
-
sessionKey: string;
|
|
92
|
-
route: BncrRoute;
|
|
93
|
-
mediaUrl: string;
|
|
94
|
-
kind?: 'tool' | 'block' | 'final';
|
|
95
|
-
replyToId: string;
|
|
96
|
-
replyTargetPolicy: OutboundReplyTargetPolicy;
|
|
97
|
-
fallback: { text: string; reason: string };
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
export function shouldSplitReplyMediaText(payload: NormalizedReplyPayload) {
|
|
78
|
+
function shouldSplitReplyMediaText(payload: NormalizedReplyPayload) {
|
|
101
79
|
if (!payload.text) return false;
|
|
102
80
|
if (payload.mediaList.length > 1) return true;
|
|
103
81
|
return payload.text.length > MEDIA_TEXT_SPLIT_THRESHOLD;
|
|
@@ -115,87 +93,260 @@ export function buildReplyEnqueuePlan(payload: NormalizedReplyPayload): ReplyEnq
|
|
|
115
93
|
return { kind: 'media-only', clearText: false };
|
|
116
94
|
}
|
|
117
95
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export function buildReplyTextOutboxEntry(
|
|
96
|
+
/**
|
|
97
|
+
* @deprecated Only used by native-reply-delivery.test.mjs. Not called from production code.
|
|
98
|
+
*/
|
|
99
|
+
export function enqueueReplyTextEntry(
|
|
126
100
|
params: {
|
|
127
101
|
accountId: string;
|
|
128
102
|
sessionKey: string;
|
|
129
103
|
route: BncrRoute;
|
|
130
|
-
|
|
131
|
-
kind?: 'tool' | 'block' | 'final';
|
|
132
|
-
replyToId: string;
|
|
133
|
-
replyTargetPolicy: OutboundReplyTargetPolicy;
|
|
134
|
-
extra?: Record<string, unknown>;
|
|
104
|
+
payload: NormalizedReplyPayload;
|
|
135
105
|
},
|
|
136
106
|
helpers: {
|
|
137
|
-
|
|
107
|
+
enqueueOutbound: (entry: OutboxEntry) => void;
|
|
108
|
+
buildOutboxEntry: (args: {
|
|
138
109
|
accountId: string;
|
|
139
110
|
sessionKey: string;
|
|
140
111
|
route: BncrRoute;
|
|
141
|
-
|
|
112
|
+
type?: string;
|
|
113
|
+
msg: string;
|
|
142
114
|
kind?: 'tool' | 'block' | 'final';
|
|
143
115
|
replyToId?: string;
|
|
144
116
|
replyTargetPolicy?: OutboundReplyTargetPolicy;
|
|
145
117
|
extra?: Record<string, unknown>;
|
|
118
|
+
transferMode?: 'text' | 'media';
|
|
146
119
|
}) => OutboxEntry;
|
|
147
120
|
},
|
|
148
|
-
):
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
121
|
+
): void {
|
|
122
|
+
if (!params.payload.text && !params.payload.extra) return;
|
|
123
|
+
|
|
124
|
+
helpers.enqueueOutbound(
|
|
125
|
+
helpers.buildOutboxEntry({
|
|
126
|
+
accountId: params.accountId,
|
|
127
|
+
sessionKey: params.sessionKey,
|
|
128
|
+
route: params.route,
|
|
129
|
+
type: 'text',
|
|
130
|
+
msg: params.payload.text,
|
|
131
|
+
extra: params.payload.extra,
|
|
132
|
+
kind: params.payload.kind,
|
|
133
|
+
replyToId: params.payload.replyToId,
|
|
134
|
+
replyTargetPolicy: params.payload.replyTargetPolicy,
|
|
135
|
+
}),
|
|
136
|
+
);
|
|
159
137
|
}
|
|
160
138
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
139
|
+
/**
|
|
140
|
+
* Dispatch all entries from the normalisation plan through a unified path.
|
|
141
|
+
*
|
|
142
|
+
* `buildNormalizedSends` returns one or more sends (pre-send text-only +
|
|
143
|
+
* main payload). Every entry goes through `buildReplyEnqueuePlan` → the same
|
|
144
|
+
* text/media dispatch. There is no separate pre-send or main-send branch.
|
|
145
|
+
*/
|
|
146
|
+
function buildSendPayload(
|
|
147
|
+
entry: NormalizedOutboundSend,
|
|
148
|
+
parent: NormalizedReplyPayload,
|
|
149
|
+
): NormalizedReplyPayload {
|
|
150
|
+
return {
|
|
151
|
+
text: entry.text,
|
|
152
|
+
mediaUrl: entry.mediaUrl || '',
|
|
153
|
+
mediaUrls: entry.mediaUrls,
|
|
154
|
+
mediaList: entry.mediaUrl ? [entry.mediaUrl] : entry.mediaUrls?.length ? entry.mediaUrls : [],
|
|
155
|
+
asVoice: entry.asVoice,
|
|
156
|
+
audioAsVoice: entry.audioAsVoice,
|
|
157
|
+
downloadMedia: entry.downloadMedia,
|
|
158
|
+
type: entry.type,
|
|
159
|
+
extra: entry.extra,
|
|
160
|
+
kind: entry.kind,
|
|
161
|
+
replyToId: entry.replyToId ?? '',
|
|
162
|
+
replyTargetPolicy: parent.replyTargetPolicy,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Unified outbound dispatch: single exit point for all sends.
|
|
168
|
+
*
|
|
169
|
+
* Iterates `buildNormalizedSends` entries and dispatches each through
|
|
170
|
+
* the existing text or media helpers internally. External callers only
|
|
171
|
+
* see one unified function — they do not choose the branch.
|
|
172
|
+
*/
|
|
173
|
+
function enqueueUnifiedOutbound(
|
|
174
|
+
params: EnqueueNormalizedReplyPayloadParams,
|
|
175
|
+
helpers: {
|
|
176
|
+
enqueueOutbound: (entry: OutboxEntry) => void;
|
|
177
|
+
buildOutboxEntry: BuildOutboxEntryFn;
|
|
178
|
+
tryBuildMediaDedupeFallback: (args: {
|
|
179
|
+
sessionKey: string;
|
|
180
|
+
mediaUrl: string;
|
|
181
|
+
text: string;
|
|
182
|
+
replyToId: string;
|
|
183
|
+
currentTime: number;
|
|
184
|
+
}) => {
|
|
185
|
+
text: string;
|
|
186
|
+
reason: 'same-text-sent-checkmark' | 'text-changed-downgrade';
|
|
187
|
+
} | null;
|
|
188
|
+
rememberRecentMediaSend: (args: {
|
|
189
|
+
sessionKey: string;
|
|
190
|
+
mediaUrl: string;
|
|
191
|
+
text: string;
|
|
192
|
+
replyToId: string;
|
|
193
|
+
createdAt: number;
|
|
194
|
+
}) => void;
|
|
195
|
+
logInfo: (
|
|
196
|
+
scope: string | undefined,
|
|
197
|
+
message: string,
|
|
198
|
+
options?: { debugOnly?: boolean },
|
|
199
|
+
) => void;
|
|
167
200
|
},
|
|
201
|
+
): void {
|
|
202
|
+
for (const entry of buildNormalizedSends(params.payload)) {
|
|
203
|
+
const sendPayload = buildSendPayload(entry, params.payload);
|
|
204
|
+
const plan = buildReplyEnqueuePlan(sendPayload);
|
|
205
|
+
|
|
206
|
+
if (plan.kind === 'text-and-media') {
|
|
207
|
+
// Text first, then media entries with empty text
|
|
208
|
+
helpers.enqueueOutbound(
|
|
209
|
+
helpers.buildOutboxEntry({
|
|
210
|
+
accountId: params.accountId,
|
|
211
|
+
sessionKey: params.sessionKey,
|
|
212
|
+
route: params.route,
|
|
213
|
+
type: 'text',
|
|
214
|
+
msg: sendPayload.text,
|
|
215
|
+
kind: sendPayload.kind,
|
|
216
|
+
replyToId: sendPayload.replyToId || undefined,
|
|
217
|
+
replyTargetPolicy: sendPayload.replyTargetPolicy,
|
|
218
|
+
extra: sendPayload.extra,
|
|
219
|
+
}),
|
|
220
|
+
);
|
|
221
|
+
enqueueUnifiedMediaEntries(params, sendPayload, '', helpers);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (plan.kind !== 'text-only') {
|
|
226
|
+
// Media-only: first entry carries caption text
|
|
227
|
+
enqueueUnifiedMediaEntries(params, sendPayload, sendPayload.text, helpers);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Text-only
|
|
232
|
+
if (!sendPayload.text && !sendPayload.extra) continue;
|
|
233
|
+
helpers.enqueueOutbound(
|
|
234
|
+
helpers.buildOutboxEntry({
|
|
235
|
+
accountId: params.accountId,
|
|
236
|
+
sessionKey: params.sessionKey,
|
|
237
|
+
route: params.route,
|
|
238
|
+
type: 'text',
|
|
239
|
+
msg: sendPayload.text,
|
|
240
|
+
kind: sendPayload.kind,
|
|
241
|
+
replyToId: sendPayload.replyToId || undefined,
|
|
242
|
+
replyTargetPolicy: sendPayload.replyTargetPolicy,
|
|
243
|
+
extra: sendPayload.extra,
|
|
244
|
+
}),
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Internal: iterate mediaList and enqueue file-transfer entries (with dedup). */
|
|
250
|
+
function enqueueUnifiedMediaEntries(
|
|
251
|
+
params: EnqueueNormalizedReplyPayloadParams,
|
|
252
|
+
sendPayload: NormalizedReplyPayload,
|
|
253
|
+
firstCaption: string,
|
|
168
254
|
helpers: {
|
|
169
255
|
enqueueOutbound: (entry: OutboxEntry) => void;
|
|
170
|
-
|
|
171
|
-
|
|
256
|
+
buildOutboxEntry: BuildOutboxEntryFn;
|
|
257
|
+
tryBuildMediaDedupeFallback: (args: {
|
|
172
258
|
sessionKey: string;
|
|
173
|
-
|
|
259
|
+
mediaUrl: string;
|
|
174
260
|
text: string;
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
261
|
+
replyToId: string;
|
|
262
|
+
currentTime: number;
|
|
263
|
+
}) => {
|
|
264
|
+
text: string;
|
|
265
|
+
reason: 'same-text-sent-checkmark' | 'text-changed-downgrade';
|
|
266
|
+
} | null;
|
|
267
|
+
rememberRecentMediaSend: (args: {
|
|
268
|
+
sessionKey: string;
|
|
269
|
+
mediaUrl: string;
|
|
270
|
+
text: string;
|
|
271
|
+
replyToId: string;
|
|
272
|
+
createdAt: number;
|
|
273
|
+
}) => void;
|
|
274
|
+
logInfo: (
|
|
275
|
+
scope: string | undefined,
|
|
276
|
+
message: string,
|
|
277
|
+
options?: { debugOnly?: boolean },
|
|
278
|
+
) => void;
|
|
180
279
|
},
|
|
181
280
|
): void {
|
|
182
|
-
|
|
281
|
+
let first = true;
|
|
282
|
+
for (const mediaUrl of sendPayload.mediaList) {
|
|
283
|
+
const text = first ? firstCaption : '';
|
|
284
|
+
const currentTime = Date.now();
|
|
285
|
+
const normalizedText = text || '';
|
|
286
|
+
const fallback = helpers.tryBuildMediaDedupeFallback({
|
|
287
|
+
sessionKey: params.sessionKey,
|
|
288
|
+
mediaUrl,
|
|
289
|
+
text: normalizedText,
|
|
290
|
+
replyToId: sendPayload.replyToId,
|
|
291
|
+
currentTime,
|
|
292
|
+
});
|
|
183
293
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
294
|
+
if (fallback) {
|
|
295
|
+
helpers.logInfo(
|
|
296
|
+
'outbound',
|
|
297
|
+
buildBncrDebugJsonMessage('media-dedupe-hit', {
|
|
298
|
+
sessionKey: params.sessionKey,
|
|
299
|
+
mediaUrl,
|
|
300
|
+
text: normalizedText,
|
|
301
|
+
replyToId: sendPayload.replyToId,
|
|
302
|
+
reason: fallback.reason,
|
|
303
|
+
}),
|
|
304
|
+
{ debugOnly: true },
|
|
305
|
+
);
|
|
306
|
+
helpers.enqueueOutbound(
|
|
307
|
+
helpers.buildOutboxEntry({
|
|
308
|
+
accountId: params.accountId,
|
|
309
|
+
sessionKey: params.sessionKey,
|
|
310
|
+
route: params.route,
|
|
311
|
+
type: 'text',
|
|
312
|
+
msg: fallback.text,
|
|
313
|
+
kind: sendPayload.kind,
|
|
314
|
+
replyToId: sendPayload.replyToId || undefined,
|
|
315
|
+
replyTargetPolicy: sendPayload.replyTargetPolicy,
|
|
316
|
+
}),
|
|
317
|
+
);
|
|
318
|
+
first = false;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
helpers.enqueueOutbound(
|
|
323
|
+
helpers.buildOutboxEntry({
|
|
187
324
|
accountId: params.accountId,
|
|
188
325
|
sessionKey: params.sessionKey,
|
|
189
326
|
route: params.route,
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
kind:
|
|
193
|
-
replyToId:
|
|
194
|
-
replyTargetPolicy:
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
327
|
+
type: sendPayload.type,
|
|
328
|
+
msg: text,
|
|
329
|
+
kind: sendPayload.kind,
|
|
330
|
+
replyToId: sendPayload.replyToId,
|
|
331
|
+
replyTargetPolicy: sendPayload.replyTargetPolicy,
|
|
332
|
+
extra: sendPayload.extra,
|
|
333
|
+
transferMode: 'media',
|
|
334
|
+
mediaUrl,
|
|
335
|
+
mediaLocalRoots: params.mediaLocalRoots,
|
|
336
|
+
asVoice: sendPayload.asVoice,
|
|
337
|
+
audioAsVoice: sendPayload.audioAsVoice,
|
|
338
|
+
downloadMedia: sendPayload.downloadMedia,
|
|
339
|
+
}),
|
|
340
|
+
);
|
|
341
|
+
helpers.rememberRecentMediaSend({
|
|
342
|
+
sessionKey: params.sessionKey,
|
|
343
|
+
mediaUrl,
|
|
344
|
+
text: normalizedText,
|
|
345
|
+
replyToId: sendPayload.replyToId,
|
|
346
|
+
createdAt: currentTime,
|
|
347
|
+
});
|
|
348
|
+
first = false;
|
|
349
|
+
}
|
|
199
350
|
}
|
|
200
351
|
|
|
201
352
|
export function enqueueNormalizedReplyPayload(
|
|
@@ -207,15 +358,33 @@ export function enqueueNormalizedReplyPayload(
|
|
|
207
358
|
route: BncrRoute;
|
|
208
359
|
payload: NormalizedReplyPayload;
|
|
209
360
|
}) => void;
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
361
|
+
enqueueOutbound: (entry: OutboxEntry) => void;
|
|
362
|
+
buildOutboxEntry: BuildOutboxEntryFn;
|
|
363
|
+
tryBuildMediaDedupeFallback: (args: {
|
|
213
364
|
sessionKey: string;
|
|
214
|
-
|
|
215
|
-
|
|
365
|
+
mediaUrl: string;
|
|
366
|
+
text: string;
|
|
367
|
+
replyToId: string;
|
|
368
|
+
currentTime: number;
|
|
369
|
+
}) => {
|
|
370
|
+
text: string;
|
|
371
|
+
reason: 'same-text-sent-checkmark' | 'text-changed-downgrade';
|
|
372
|
+
} | null;
|
|
373
|
+
rememberRecentMediaSend: (args: {
|
|
374
|
+
sessionKey: string;
|
|
375
|
+
mediaUrl: string;
|
|
376
|
+
text: string;
|
|
377
|
+
replyToId: string;
|
|
378
|
+
createdAt: number;
|
|
216
379
|
}) => void;
|
|
380
|
+
logInfo: (
|
|
381
|
+
scope: string | undefined,
|
|
382
|
+
message: string,
|
|
383
|
+
options?: { debugOnly?: boolean },
|
|
384
|
+
) => void;
|
|
217
385
|
},
|
|
218
386
|
): void {
|
|
387
|
+
// Log only the main (last) send entry for traceability.
|
|
219
388
|
helpers.logEnqueueFromReply({
|
|
220
389
|
accountId: params.accountId,
|
|
221
390
|
sessionKey: params.sessionKey,
|
|
@@ -223,83 +392,52 @@ export function enqueueNormalizedReplyPayload(
|
|
|
223
392
|
payload: params.payload,
|
|
224
393
|
});
|
|
225
394
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (plan.kind === 'text-and-media') {
|
|
229
|
-
helpers.enqueueReplyTextEntry({
|
|
230
|
-
accountId: params.accountId,
|
|
231
|
-
sessionKey: params.sessionKey,
|
|
232
|
-
route: params.route,
|
|
233
|
-
payload: params.payload,
|
|
234
|
-
});
|
|
235
|
-
helpers.enqueueReplyMediaEntries({
|
|
236
|
-
accountId: params.accountId,
|
|
237
|
-
sessionKey: params.sessionKey,
|
|
238
|
-
route: params.route,
|
|
239
|
-
payload: withoutReplyMediaText(params.payload),
|
|
240
|
-
mediaLocalRoots: params.mediaLocalRoots,
|
|
241
|
-
});
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (plan.kind !== 'text-only') {
|
|
246
|
-
helpers.enqueueReplyMediaEntries({
|
|
247
|
-
accountId: params.accountId,
|
|
248
|
-
sessionKey: params.sessionKey,
|
|
249
|
-
route: params.route,
|
|
250
|
-
payload: params.payload,
|
|
251
|
-
mediaLocalRoots: params.mediaLocalRoots,
|
|
252
|
-
});
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
helpers.enqueueReplyTextEntry({
|
|
257
|
-
accountId: params.accountId,
|
|
258
|
-
sessionKey: params.sessionKey,
|
|
259
|
-
route: params.route,
|
|
260
|
-
payload: params.payload,
|
|
261
|
-
});
|
|
395
|
+
// Dispatch ALL entries (pre-send + main) through the same path.
|
|
396
|
+
enqueueUnifiedOutbound(params, helpers);
|
|
262
397
|
}
|
|
263
398
|
|
|
264
|
-
export function normalizeReplyPayload(
|
|
399
|
+
export async function normalizeReplyPayload(
|
|
265
400
|
payload: ReplyPayloadInput,
|
|
266
401
|
helpers: { asString: (value: unknown, fallback?: string) => string },
|
|
267
402
|
options?: { replyTargetPolicy?: OutboundReplyTargetPolicy },
|
|
268
|
-
): NormalizedReplyPayload {
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const mediaUrl = helpers.asString(payload?.mediaUrl || '').trim();
|
|
274
|
-
const mediaUrls = Array.isArray(payload?.mediaUrls)
|
|
275
|
-
? payload.mediaUrls.map((v) => helpers.asString(v || '').trim()).filter(Boolean)
|
|
276
|
-
: undefined;
|
|
277
|
-
const type = helpers.asString(payload?.type || '').trim();
|
|
278
|
-
// Merge marker params with payload extra (marker wins), strip consumption fields
|
|
279
|
-
const mergedExtra: Record<string, unknown> = {
|
|
280
|
-
...(payload?.extra ? { ...payload.extra } : {}),
|
|
281
|
-
...markerParams,
|
|
282
|
-
};
|
|
283
|
-
const { remaining } = extractConsumptionFields(
|
|
284
|
-
Object.keys(mergedExtra).length > 0 ? mergedExtra : undefined,
|
|
285
|
-
);
|
|
286
|
-
const finalExtra = Object.keys(remaining).length > 0 ? remaining : undefined;
|
|
287
|
-
return {
|
|
288
|
-
text,
|
|
289
|
-
mediaUrl,
|
|
290
|
-
mediaUrls,
|
|
291
|
-
mediaList: mediaUrls?.length ? mediaUrls : mediaUrl ? [mediaUrl] : [],
|
|
403
|
+
): Promise<NormalizedReplyPayload> {
|
|
404
|
+
const normalized = await normalizeOutboundSend({
|
|
405
|
+
text: helpers.asString(payload?.text || ''),
|
|
406
|
+
mediaUrl: helpers.asString(payload?.mediaUrl || ''),
|
|
407
|
+
mediaUrls: payload?.mediaUrls,
|
|
292
408
|
asVoice: payload?.asVoice === true,
|
|
293
409
|
audioAsVoice: payload?.audioAsVoice === true,
|
|
294
|
-
downloadMedia: payload?.downloadMedia
|
|
295
|
-
|
|
296
|
-
...(finalExtra ? { extra: finalExtra } : {}),
|
|
410
|
+
downloadMedia: payload?.downloadMedia,
|
|
411
|
+
type: helpers.asString(payload?.type || '').trim() || undefined,
|
|
297
412
|
kind: payload?.kind,
|
|
298
|
-
|
|
413
|
+
replyToId: payload?.replyToId,
|
|
414
|
+
extra: payload?.extra,
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
const markerHasMsg = normalized.markerHasMsg === true;
|
|
418
|
+
const mediaUrl = helpers.asString(normalized.mediaUrl || '');
|
|
419
|
+
const mediaUrls = normalized.mediaUrls;
|
|
420
|
+
const mediaList = mediaUrls?.length ? mediaUrls : mediaUrl ? [mediaUrl] : [];
|
|
421
|
+
const kind = normalized.kind;
|
|
422
|
+
const replyTargetPolicy = options?.replyTargetPolicy ?? 'agent-default';
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
text: normalized.text,
|
|
426
|
+
mediaUrl,
|
|
427
|
+
mediaUrls,
|
|
428
|
+
mediaList,
|
|
429
|
+
asVoice: normalized.asVoice,
|
|
430
|
+
audioAsVoice: normalized.audioAsVoice,
|
|
431
|
+
downloadMedia: normalized.downloadMedia,
|
|
432
|
+
...(normalized.type ? { type: normalized.type } : {}),
|
|
433
|
+
...(normalized.extra ? { extra: normalized.extra } : {}),
|
|
434
|
+
markerHasMsg,
|
|
435
|
+
kind,
|
|
436
|
+
replyTargetPolicy,
|
|
299
437
|
replyToId: normalizeOutboundReplyToId({
|
|
300
|
-
kind
|
|
301
|
-
replyToId: payload?.replyToId,
|
|
302
|
-
replyTargetPolicy
|
|
438
|
+
kind,
|
|
439
|
+
replyToId: normalized.replyToId ?? payload?.replyToId,
|
|
440
|
+
replyTargetPolicy,
|
|
303
441
|
}),
|
|
304
442
|
};
|
|
305
443
|
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
routeScopeToHex,
|
|
8
8
|
} from '../../core/targets.ts';
|
|
9
9
|
import type { BncrRoute } from '../../core/types.ts';
|
|
10
|
+
import { asString } from '../../core/value-sanitize.ts';
|
|
10
11
|
import { buildOpenClawChannelOutboundSessionRoute } from '../../openclaw/session-route-runtime.ts';
|
|
11
12
|
import type { BncrChannelConfigRoot } from '../../plugin/channel-runtime-types.ts';
|
|
12
13
|
|
|
@@ -18,7 +19,7 @@ type BncrChannelRouteRefFields = {
|
|
|
18
19
|
rawTo: string;
|
|
19
20
|
chatType: 'direct' | 'group';
|
|
20
21
|
};
|
|
21
|
-
thread?: { id: string };
|
|
22
|
+
thread?: { id: string | number };
|
|
22
23
|
};
|
|
23
24
|
|
|
24
25
|
type BncrOutboundSessionRoute = ChannelOutboundSessionRoute & BncrChannelRouteRefFields;
|
|
@@ -30,24 +31,18 @@ type ResolveBncrOutboundSessionRouteParams = {
|
|
|
30
31
|
accountId?: string;
|
|
31
32
|
target: string;
|
|
32
33
|
resolvedTarget?: { to?: string } | null;
|
|
33
|
-
threadId?: string;
|
|
34
|
+
threadId?: string | number | null;
|
|
34
35
|
canonicalAgentId: string;
|
|
35
36
|
resolveRouteBySession?: (raw: string, accountId: string) => BncrRoute | null;
|
|
36
37
|
};
|
|
37
38
|
|
|
38
|
-
function asString(v: unknown, fallback = ''): string {
|
|
39
|
-
if (typeof v === 'string') return v;
|
|
40
|
-
if (v == null) return fallback;
|
|
41
|
-
return String(v);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
39
|
function attachBncrChannelRouteRefFields(args: {
|
|
45
40
|
built: ChannelOutboundSessionRoute;
|
|
46
41
|
channel: string;
|
|
47
42
|
accountId?: string;
|
|
48
43
|
to: string;
|
|
49
44
|
chatType: 'direct' | 'group';
|
|
50
|
-
threadId?: string;
|
|
45
|
+
threadId?: string | number | null;
|
|
51
46
|
}): BncrOutboundSessionRoute {
|
|
52
47
|
const { built, channel, accountId, to, chatType, threadId } = args;
|
|
53
48
|
return {
|
|
@@ -59,7 +54,7 @@ function attachBncrChannelRouteRefFields(args: {
|
|
|
59
54
|
rawTo: to,
|
|
60
55
|
chatType,
|
|
61
56
|
},
|
|
62
|
-
...(threadId
|
|
57
|
+
...(threadId != null ? { thread: { id: threadId } } : {}),
|
|
63
58
|
};
|
|
64
59
|
}
|
|
65
60
|
|
|
@@ -99,7 +94,7 @@ export function resolveBncrOutboundSessionRoute(params: ResolveBncrOutboundSessi
|
|
|
99
94
|
chatType: 'direct',
|
|
100
95
|
from: displayTo,
|
|
101
96
|
to: displayTo,
|
|
102
|
-
...(params.threadId
|
|
97
|
+
...(params.threadId != null ? { threadId: params.threadId } : {}),
|
|
103
98
|
});
|
|
104
99
|
|
|
105
100
|
return attachBncrChannelRouteRefFields({
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
parseStrictBncrSessionKey,
|
|
5
5
|
} from '../../core/targets.ts';
|
|
6
6
|
import type { BncrRoute } from '../../core/types.ts';
|
|
7
|
+
import { asString } from '../../core/value-sanitize.ts';
|
|
7
8
|
|
|
8
9
|
type ResolveBncrOutboundTargetParams = {
|
|
9
10
|
target: string;
|
|
@@ -11,12 +12,6 @@ type ResolveBncrOutboundTargetParams = {
|
|
|
11
12
|
resolveRouteBySession?: (raw: string, accountId: string) => BncrRoute | null;
|
|
12
13
|
};
|
|
13
14
|
|
|
14
|
-
function asString(v: unknown, fallback = ''): string {
|
|
15
|
-
if (typeof v === 'string') return v;
|
|
16
|
-
if (v == null) return fallback;
|
|
17
|
-
return String(v);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
15
|
export function looksLikeBncrExplicitTarget(input: string): boolean {
|
|
21
16
|
const raw = asString(input).trim();
|
|
22
17
|
if (!raw) return false;
|
|
@@ -48,7 +48,7 @@ export function isOpenClawRemoteHttpMediaUrl(mediaUrl: string): boolean {
|
|
|
48
48
|
* Returns the first absolute path that exists on disk, or the original
|
|
49
49
|
* relative path if nothing is found (the host will then emit its own error).
|
|
50
50
|
*/
|
|
51
|
-
|
|
51
|
+
function resolveRelativeMediaPath(mediaUrl: string, localRoots?: readonly string[]): string {
|
|
52
52
|
if (!mediaUrl || !localRoots?.length) return mediaUrl;
|
|
53
53
|
if (path.isAbsolute(mediaUrl)) return mediaUrl;
|
|
54
54
|
// HTTP / file:// / data: / ~ paths are handled elsewhere
|