@xmoxmo/bncr 0.2.5 → 0.2.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/README.md +9 -3
- package/index.ts +30 -15
- package/package.json +4 -3
- package/scripts/check-pack.mjs +61 -0
- package/scripts/selfcheck.mjs +10 -0
- package/src/channel.ts +892 -255
- package/src/core/connection-reachability.ts +41 -14
- package/src/core/diagnostics.ts +7 -2
- package/src/core/downlink-health.ts +7 -2
- package/src/core/outbox-entry-builders.ts +3 -2
- package/src/core/policy.ts +9 -0
- package/src/core/register-trace.ts +6 -1
- package/src/core/status.ts +7 -2
- package/src/core/targets.ts +10 -1
- package/src/core/types.ts +1 -0
- package/src/messaging/inbound/commands.ts +330 -77
- package/src/messaging/inbound/context-facts.ts +200 -0
- package/src/messaging/inbound/dispatch.ts +429 -119
- package/src/messaging/inbound/gate.ts +66 -26
- package/src/messaging/inbound/parse.ts +8 -0
- package/src/messaging/inbound/runtime-compat.ts +39 -0
- package/src/messaging/inbound/session-label.ts +115 -0
- package/src/messaging/outbound/diagnostics.ts +16 -0
- package/src/messaging/outbound/durable-message-adapter.ts +107 -0
- package/src/messaging/outbound/durable-queue-adapter.ts +157 -0
- package/src/messaging/outbound/media.ts +3 -1
- package/src/messaging/outbound/queue-selectors.ts +7 -2
- package/src/messaging/outbound/reasons.ts +4 -0
- package/src/messaging/outbound/reply-enqueue.ts +2 -2
- package/src/messaging/outbound/reply-target-policy.ts +13 -0
- package/src/messaging/outbound/retry-policy.ts +12 -3
- package/src/messaging/outbound/send.ts +6 -0
- package/src/messaging/outbound/session-route.ts +2 -2
- package/src/openclaw/config-runtime.ts +52 -0
- package/src/openclaw/inbound-session-runtime.ts +94 -0
- package/src/openclaw/ingress-runtime.ts +35 -0
- package/src/openclaw/media-runtime.ts +73 -0
- package/src/openclaw/reply-runtime.ts +104 -0
- package/src/openclaw/routing-runtime.ts +48 -0
- package/src/openclaw/sdk-helpers.ts +20 -0
- package/src/openclaw/session-route-runtime.ts +15 -0
|
@@ -1,10 +1,27 @@
|
|
|
1
1
|
import { emitBncrLogLine } from '../../core/logging.ts';
|
|
2
|
+
import { resolveBncrChannelPolicy } from '../../core/policy.ts';
|
|
2
3
|
import {
|
|
3
4
|
formatDisplayScope,
|
|
4
5
|
normalizeInboundSessionKey,
|
|
5
6
|
withTaskSessionKey,
|
|
6
7
|
} from '../../core/targets.ts';
|
|
7
8
|
import { buildBncrReplyConfig } from './reply-config.ts';
|
|
9
|
+
import { resolveBncrChannelInboundRuntime } from './runtime-compat.ts';
|
|
10
|
+
import {
|
|
11
|
+
buildBncrInboundSessionIdentityPatch,
|
|
12
|
+
recordAndPatchBncrInboundSessionEntry,
|
|
13
|
+
wrapBncrInboundRecordSessionLabelCorrection,
|
|
14
|
+
} from './session-label.ts';
|
|
15
|
+
import { dispatchOpenClawReplyWithBufferedBlockDispatcher } from '../../openclaw/reply-runtime.ts';
|
|
16
|
+
import {
|
|
17
|
+
resolveOpenClawAgentRoute,
|
|
18
|
+
resolveOpenClawInboundLastRouteSessionKey,
|
|
19
|
+
} from '../../openclaw/routing-runtime.ts';
|
|
20
|
+
import {
|
|
21
|
+
recordBncrInboundSession,
|
|
22
|
+
resolveBncrInboundSessionStorePath,
|
|
23
|
+
resolveBncrPinnedMainDmOwnerFromAllowlist,
|
|
24
|
+
} from '../../openclaw/inbound-session-runtime.ts';
|
|
8
25
|
|
|
9
26
|
type ParsedInbound = ReturnType<typeof import('./parse.ts')['parseBncrInboundParams']>;
|
|
10
27
|
|
|
@@ -14,6 +31,33 @@ type NativeCommand = {
|
|
|
14
31
|
body: string;
|
|
15
32
|
};
|
|
16
33
|
|
|
34
|
+
type NativeVerboseCommand = {
|
|
35
|
+
handled: true;
|
|
36
|
+
verboseLevel?: 'on' | 'off' | 'full';
|
|
37
|
+
text: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function resolveBncrNativeVerboseCommand(command: NativeCommand): NativeVerboseCommand | null {
|
|
41
|
+
if (command.command !== 'verbose') return null;
|
|
42
|
+
const rawLevel = String(command.raw.slice('/verbose'.length) || '').trim().toLowerCase();
|
|
43
|
+
if (!rawLevel || rawLevel === 'status') {
|
|
44
|
+
return { handled: true, text: 'Current verbose level is unchanged.' };
|
|
45
|
+
}
|
|
46
|
+
if (rawLevel === 'on') return { handled: true, verboseLevel: 'on', text: 'Verbose logging enabled.' };
|
|
47
|
+
if (rawLevel === 'off') return { handled: true, verboseLevel: 'off', text: 'Verbose logging disabled.' };
|
|
48
|
+
if (rawLevel === 'full') return { handled: true, verboseLevel: 'full', text: 'Verbose logging set to full.' };
|
|
49
|
+
return { handled: true, text: `Unrecognized verbose level "${rawLevel}". Valid levels: off, on, full.` };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function logBncrNativeCommandEvent(
|
|
53
|
+
event: string,
|
|
54
|
+
fields: Record<string, unknown>,
|
|
55
|
+
options?: { debugOnly?: boolean; debugEnabled?: boolean },
|
|
56
|
+
) {
|
|
57
|
+
if (options?.debugOnly && !options?.debugEnabled) return;
|
|
58
|
+
emitBncrLogLine('info', `[bncr] native-command ${JSON.stringify({ event, ...fields })}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
17
61
|
export function parseBncrNativeCommand(text: string): NativeCommand | null {
|
|
18
62
|
const raw = String(text || '').trim();
|
|
19
63
|
if (!raw.startsWith('/')) return null;
|
|
@@ -45,7 +89,10 @@ export async function handleBncrNativeCommand(params: {
|
|
|
45
89
|
mediaLocalRoots?: readonly string[];
|
|
46
90
|
}) => Promise<void>;
|
|
47
91
|
logger?: { warn?: (msg: string) => void; error?: (msg: string) => void };
|
|
48
|
-
}): Promise<
|
|
92
|
+
}): Promise<
|
|
93
|
+
| { handled: false }
|
|
94
|
+
| { handled: true; command: string; sessionKey: string; fallbackToAgent?: boolean }
|
|
95
|
+
> {
|
|
49
96
|
const {
|
|
50
97
|
api,
|
|
51
98
|
channelId,
|
|
@@ -56,11 +103,23 @@ export async function handleBncrNativeCommand(params: {
|
|
|
56
103
|
enqueueFromReply,
|
|
57
104
|
logger,
|
|
58
105
|
} = params;
|
|
59
|
-
const { accountId, route, peer, sessionKeyfromroute, clientId, extracted, msgId } = parsed;
|
|
106
|
+
const { accountId, route, peer, sessionKeyfromroute, providedOriginatingTo, clientId, extracted, msgId } = parsed;
|
|
60
107
|
const command = parseBncrNativeCommand(extracted.text);
|
|
61
108
|
if (!command) return { handled: false };
|
|
109
|
+
const nativeCommandDebugEnabled = cfg?.channels?.[channelId]?.debug?.verbose === true;
|
|
110
|
+
|
|
111
|
+
logBncrNativeCommandEvent(
|
|
112
|
+
'detected',
|
|
113
|
+
{
|
|
114
|
+
command: command.command,
|
|
115
|
+
accountId,
|
|
116
|
+
to: formatDisplayScope(route),
|
|
117
|
+
msgId: msgId || null,
|
|
118
|
+
},
|
|
119
|
+
{ debugOnly: true, debugEnabled: nativeCommandDebugEnabled },
|
|
120
|
+
);
|
|
62
121
|
|
|
63
|
-
const resolvedRoute = api
|
|
122
|
+
const resolvedRoute = resolveOpenClawAgentRoute(api, {
|
|
64
123
|
cfg,
|
|
65
124
|
channel: channelId,
|
|
66
125
|
accountId,
|
|
@@ -77,102 +136,296 @@ export async function handleBncrNativeCommand(params: {
|
|
|
77
136
|
rememberSessionRoute(taskSessionKey, accountId, route);
|
|
78
137
|
|
|
79
138
|
const displayTo = formatDisplayScope(route);
|
|
139
|
+
const originatingTo = providedOriginatingTo || displayTo;
|
|
80
140
|
const body = command.body;
|
|
81
141
|
if (!clientId) {
|
|
82
|
-
emitBncrLogLine(
|
|
83
|
-
|
|
142
|
+
emitBncrLogLine(
|
|
143
|
+
'warn',
|
|
144
|
+
'[bncr] inbound missing clientId for native command identity; using route identity fallback',
|
|
145
|
+
);
|
|
84
146
|
}
|
|
85
|
-
const senderIdForContext = clientId;
|
|
86
|
-
const senderDisplayName = 'bncr-client';
|
|
87
|
-
const storePath =
|
|
147
|
+
const senderIdForContext = clientId || displayTo;
|
|
148
|
+
const senderDisplayName = clientId ? 'bncr-client' : displayTo;
|
|
149
|
+
const storePath = resolveBncrInboundSessionStorePath({
|
|
150
|
+
storeConfig: cfg?.session?.store,
|
|
88
151
|
agentId: resolvedRoute.agentId,
|
|
89
152
|
});
|
|
90
153
|
|
|
91
|
-
const ctxPayload = api.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
154
|
+
const ctxPayload = resolveBncrChannelInboundRuntime(api).buildContext({
|
|
155
|
+
channel: channelId,
|
|
156
|
+
provider: channelId,
|
|
157
|
+
surface: channelId,
|
|
158
|
+
accountId,
|
|
159
|
+
messageId: msgId,
|
|
160
|
+
timestamp: Date.now(),
|
|
161
|
+
from: senderIdForContext,
|
|
162
|
+
sender: {
|
|
163
|
+
id: senderIdForContext,
|
|
164
|
+
name: senderDisplayName,
|
|
165
|
+
username: senderDisplayName,
|
|
166
|
+
},
|
|
167
|
+
conversation: {
|
|
168
|
+
kind: peer.kind,
|
|
169
|
+
id: peer.id,
|
|
170
|
+
label: displayTo,
|
|
171
|
+
routePeer: {
|
|
172
|
+
kind: peer.kind,
|
|
173
|
+
id: peer.id,
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
route: {
|
|
177
|
+
agentId: resolvedRoute.agentId,
|
|
178
|
+
accountId,
|
|
179
|
+
routeSessionKey: resolvedRoute.sessionKey,
|
|
180
|
+
dispatchSessionKey: sessionKey,
|
|
181
|
+
mainSessionKey: resolvedRoute.mainSessionKey,
|
|
182
|
+
},
|
|
183
|
+
reply: {
|
|
184
|
+
to: displayTo,
|
|
185
|
+
originatingTo,
|
|
186
|
+
replyToId: msgId,
|
|
187
|
+
},
|
|
188
|
+
message: {
|
|
189
|
+
inboundEventKind: 'user_request',
|
|
190
|
+
body,
|
|
191
|
+
rawBody: body,
|
|
192
|
+
bodyForAgent: body,
|
|
193
|
+
commandBody: body,
|
|
194
|
+
envelopeFrom: originatingTo,
|
|
195
|
+
senderLabel: senderDisplayName,
|
|
196
|
+
},
|
|
197
|
+
commandTurn: {
|
|
198
|
+
kind: 'native',
|
|
199
|
+
source: 'native',
|
|
200
|
+
authorized: true,
|
|
201
|
+
body,
|
|
202
|
+
},
|
|
203
|
+
access: {
|
|
204
|
+
mentions: {
|
|
205
|
+
canDetectMention: true,
|
|
206
|
+
wasMentioned: true,
|
|
207
|
+
effectiveWasMentioned: true,
|
|
208
|
+
},
|
|
209
|
+
commands: {
|
|
210
|
+
authorized: true,
|
|
211
|
+
allowTextCommands: true,
|
|
212
|
+
useAccessGroups: false,
|
|
213
|
+
authorizers: [],
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
extra: {
|
|
217
|
+
OriginatingChannel: channelId,
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const sessionIdentityPatch = buildBncrInboundSessionIdentityPatch({
|
|
222
|
+
channelId,
|
|
223
|
+
accountId,
|
|
224
|
+
chatType: peer.kind,
|
|
225
|
+
displayTo,
|
|
226
|
+
senderId: senderIdForContext,
|
|
116
227
|
});
|
|
117
228
|
|
|
118
|
-
|
|
229
|
+
const nativeVerbose = resolveBncrNativeVerboseCommand(command);
|
|
230
|
+
if (nativeVerbose) {
|
|
231
|
+
logBncrNativeCommandEvent('handled-verbose', {
|
|
232
|
+
command: command.command,
|
|
233
|
+
accountId,
|
|
234
|
+
sessionKey,
|
|
235
|
+
to: displayTo,
|
|
236
|
+
msgId: msgId || null,
|
|
237
|
+
fallbackToAgent: false,
|
|
238
|
+
});
|
|
239
|
+
await recordAndPatchBncrInboundSessionEntry({
|
|
240
|
+
storePath,
|
|
241
|
+
sessionKey,
|
|
242
|
+
ctx: ctxPayload,
|
|
243
|
+
patch: {
|
|
244
|
+
...sessionIdentityPatch,
|
|
245
|
+
...(nativeVerbose.verboseLevel ? { verboseLevel: nativeVerbose.verboseLevel } : {}),
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
rememberSessionRoute(baseSessionKey, accountId, route);
|
|
249
|
+
await enqueueFromReply({
|
|
250
|
+
accountId,
|
|
251
|
+
sessionKey,
|
|
252
|
+
route,
|
|
253
|
+
payload: {
|
|
254
|
+
text: nativeVerbose.text,
|
|
255
|
+
replyToId: msgId || undefined,
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
return { handled: true, command: command.command, sessionKey };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
await recordAndPatchBncrInboundSessionEntry({
|
|
119
262
|
storePath,
|
|
120
263
|
sessionKey,
|
|
121
264
|
ctx: ctxPayload,
|
|
122
|
-
|
|
123
|
-
emitBncrLogLine(
|
|
124
|
-
'warn',
|
|
125
|
-
`[bncr] inbound record native command session failed: ${String(err)}`,
|
|
126
|
-
);
|
|
127
|
-
},
|
|
265
|
+
patch: sessionIdentityPatch,
|
|
128
266
|
});
|
|
129
267
|
|
|
130
268
|
const effectiveReply = buildBncrReplyConfig(cfg);
|
|
269
|
+
const channelPolicy = resolveBncrChannelPolicy(cfg?.channels?.bncr || {});
|
|
270
|
+
const pinnedMainDmOwner =
|
|
271
|
+
peer.kind === 'direct'
|
|
272
|
+
? resolveBncrPinnedMainDmOwnerFromAllowlist({
|
|
273
|
+
dmScope: cfg?.session?.dmScope,
|
|
274
|
+
allowFrom: channelPolicy.allowFrom,
|
|
275
|
+
normalizeEntry: (entry: string) => String(entry || '').trim(),
|
|
276
|
+
})
|
|
277
|
+
: null;
|
|
278
|
+
const inboundLastRouteSessionKey = resolveOpenClawInboundLastRouteSessionKey({
|
|
279
|
+
route: resolvedRoute,
|
|
280
|
+
sessionKey,
|
|
281
|
+
});
|
|
131
282
|
|
|
132
283
|
let responded = false;
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const kind = info?.kind;
|
|
142
|
-
const shouldForwardTool = effectiveReply.blockStreaming && effectiveReply.allowTool;
|
|
143
|
-
|
|
144
|
-
if (kind === 'tool' && !shouldForwardTool) {
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const hasPayload = Boolean(
|
|
149
|
-
payload?.text ||
|
|
150
|
-
payload?.mediaUrl ||
|
|
151
|
-
(Array.isArray(payload?.mediaUrls) && payload.mediaUrls.length > 0),
|
|
152
|
-
);
|
|
153
|
-
if (!hasPayload) return;
|
|
154
|
-
responded = true;
|
|
155
|
-
await enqueueFromReply({
|
|
156
|
-
accountId,
|
|
157
|
-
sessionKey,
|
|
158
|
-
route,
|
|
159
|
-
payload: {
|
|
160
|
-
...payload,
|
|
161
|
-
kind: kind as 'tool' | 'block' | 'final' | undefined,
|
|
162
|
-
replyToId: msgId || undefined,
|
|
163
|
-
},
|
|
164
|
-
});
|
|
165
|
-
},
|
|
284
|
+
logBncrNativeCommandEvent(
|
|
285
|
+
'dispatch-native-turn',
|
|
286
|
+
{
|
|
287
|
+
command: command.command,
|
|
288
|
+
accountId,
|
|
289
|
+
sessionKey,
|
|
290
|
+
to: displayTo,
|
|
291
|
+
msgId: msgId || null,
|
|
166
292
|
},
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
293
|
+
{ debugOnly: true, debugEnabled: nativeCommandDebugEnabled },
|
|
294
|
+
);
|
|
295
|
+
await resolveBncrChannelInboundRuntime(api).run({
|
|
296
|
+
channel: channelId,
|
|
297
|
+
accountId,
|
|
298
|
+
raw: parsed,
|
|
299
|
+
adapter: {
|
|
300
|
+
ingest: () => ({
|
|
301
|
+
id: msgId ?? `${displayTo}:${Date.now()}`,
|
|
302
|
+
timestamp: Date.now(),
|
|
303
|
+
rawText: body,
|
|
304
|
+
textForAgent: ctxPayload.BodyForAgent,
|
|
305
|
+
textForCommands: ctxPayload.CommandBody,
|
|
306
|
+
raw: parsed,
|
|
307
|
+
}),
|
|
308
|
+
resolveTurn: () => ({
|
|
309
|
+
channel: channelId,
|
|
310
|
+
accountId,
|
|
311
|
+
routeSessionKey: resolvedRoute.sessionKey,
|
|
312
|
+
storePath,
|
|
313
|
+
ctxPayload,
|
|
314
|
+
recordInboundSession: wrapBncrInboundRecordSessionLabelCorrection({
|
|
315
|
+
recordInboundSession: recordBncrInboundSession,
|
|
316
|
+
expectedLabel: displayTo,
|
|
317
|
+
}),
|
|
318
|
+
record: {
|
|
319
|
+
updateLastRoute:
|
|
320
|
+
peer.kind === 'direct'
|
|
321
|
+
? {
|
|
322
|
+
sessionKey: inboundLastRouteSessionKey,
|
|
323
|
+
channel: channelId,
|
|
324
|
+
to: displayTo,
|
|
325
|
+
accountId,
|
|
326
|
+
mainDmOwnerPin:
|
|
327
|
+
inboundLastRouteSessionKey === resolvedRoute.mainSessionKey && pinnedMainDmOwner
|
|
328
|
+
? {
|
|
329
|
+
ownerRecipient: pinnedMainDmOwner,
|
|
330
|
+
senderRecipient: senderIdForContext,
|
|
331
|
+
}
|
|
332
|
+
: undefined,
|
|
333
|
+
}
|
|
334
|
+
: undefined,
|
|
335
|
+
onRecordError: (err: unknown) => {
|
|
336
|
+
emitBncrLogLine(
|
|
337
|
+
'warn',
|
|
338
|
+
`[bncr] inbound record native command session failed: ${String(err)}`,
|
|
339
|
+
);
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
runDispatch: () =>
|
|
343
|
+
dispatchOpenClawReplyWithBufferedBlockDispatcher(api, {
|
|
344
|
+
ctx: ctxPayload,
|
|
345
|
+
cfg: effectiveReply.replyCfg,
|
|
346
|
+
dispatcherOptions: {
|
|
347
|
+
deliver: async (
|
|
348
|
+
payload: {
|
|
349
|
+
text?: string;
|
|
350
|
+
mediaUrl?: string;
|
|
351
|
+
mediaUrls?: string[];
|
|
352
|
+
audioAsVoice?: boolean;
|
|
353
|
+
},
|
|
354
|
+
info?: { kind?: 'tool' | 'block' | 'final' },
|
|
355
|
+
) => {
|
|
356
|
+
const kind = info?.kind;
|
|
357
|
+
const shouldForwardTool = effectiveReply.blockStreaming && effectiveReply.allowTool;
|
|
358
|
+
|
|
359
|
+
if (kind === 'tool' && !shouldForwardTool) {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const hasPayload = Boolean(
|
|
364
|
+
payload?.text ||
|
|
365
|
+
payload?.mediaUrl ||
|
|
366
|
+
(Array.isArray(payload?.mediaUrls) && payload.mediaUrls.length > 0),
|
|
367
|
+
);
|
|
368
|
+
if (!hasPayload) return;
|
|
369
|
+
if (!responded) {
|
|
370
|
+
logBncrNativeCommandEvent(
|
|
371
|
+
'payload-produced',
|
|
372
|
+
{
|
|
373
|
+
command: command.command,
|
|
374
|
+
accountId,
|
|
375
|
+
sessionKey,
|
|
376
|
+
to: displayTo,
|
|
377
|
+
msgId: msgId || null,
|
|
378
|
+
kind: kind || null,
|
|
379
|
+
fallbackToAgent: false,
|
|
380
|
+
},
|
|
381
|
+
{ debugOnly: true, debugEnabled: nativeCommandDebugEnabled },
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
responded = true;
|
|
385
|
+
await enqueueFromReply({
|
|
386
|
+
accountId,
|
|
387
|
+
sessionKey,
|
|
388
|
+
route,
|
|
389
|
+
payload: {
|
|
390
|
+
...payload,
|
|
391
|
+
kind: kind as 'tool' | 'block' | 'final' | undefined,
|
|
392
|
+
replyToId: msgId || undefined,
|
|
393
|
+
},
|
|
394
|
+
});
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
replyOptions: {
|
|
398
|
+
disableBlockStreaming: !effectiveReply.blockStreaming,
|
|
399
|
+
shouldEmitToolResult: effectiveReply.allowTool ? () => true : undefined,
|
|
400
|
+
},
|
|
401
|
+
}),
|
|
402
|
+
}),
|
|
170
403
|
},
|
|
171
404
|
});
|
|
172
405
|
|
|
173
406
|
if (!responded) {
|
|
174
|
-
|
|
407
|
+
logBncrNativeCommandEvent('no-payload-fallback-to-agent', {
|
|
408
|
+
command: command.command,
|
|
409
|
+
accountId,
|
|
410
|
+
sessionKey,
|
|
411
|
+
to: displayTo,
|
|
412
|
+
msgId: msgId || null,
|
|
413
|
+
fallbackToAgent: true,
|
|
414
|
+
});
|
|
415
|
+
return { handled: true, command: command.command, sessionKey, fallbackToAgent: true };
|
|
175
416
|
}
|
|
176
417
|
|
|
418
|
+
logBncrNativeCommandEvent(
|
|
419
|
+
'handled-with-payload',
|
|
420
|
+
{
|
|
421
|
+
command: command.command,
|
|
422
|
+
accountId,
|
|
423
|
+
sessionKey,
|
|
424
|
+
to: displayTo,
|
|
425
|
+
msgId: msgId || null,
|
|
426
|
+
fallbackToAgent: false,
|
|
427
|
+
},
|
|
428
|
+
{ debugOnly: true, debugEnabled: nativeCommandDebugEnabled },
|
|
429
|
+
);
|
|
177
430
|
return { handled: true, command: command.command, sessionKey };
|
|
178
431
|
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
export type BncrStructuredContextFactsInput = {
|
|
2
|
+
channelId: string;
|
|
3
|
+
accountId: string;
|
|
4
|
+
route: {
|
|
5
|
+
agentId?: string;
|
|
6
|
+
routeSessionKey?: string;
|
|
7
|
+
dispatchSessionKey?: string;
|
|
8
|
+
mainSessionKey?: string;
|
|
9
|
+
};
|
|
10
|
+
conversation: {
|
|
11
|
+
kind: string;
|
|
12
|
+
id: string;
|
|
13
|
+
label: string;
|
|
14
|
+
};
|
|
15
|
+
reply: {
|
|
16
|
+
to: string;
|
|
17
|
+
originatingTo: string;
|
|
18
|
+
};
|
|
19
|
+
sender: {
|
|
20
|
+
id: string;
|
|
21
|
+
displayName?: string;
|
|
22
|
+
};
|
|
23
|
+
message: {
|
|
24
|
+
id?: string | null;
|
|
25
|
+
rawBody: string;
|
|
26
|
+
bodyForAgent?: string;
|
|
27
|
+
commandBody?: string;
|
|
28
|
+
envelopeBody?: string;
|
|
29
|
+
};
|
|
30
|
+
media?: Array<{
|
|
31
|
+
path: string;
|
|
32
|
+
contentType?: string;
|
|
33
|
+
kind?: string;
|
|
34
|
+
messageId?: string;
|
|
35
|
+
}>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export function buildBncrStructuredContextFacts(input: BncrStructuredContextFactsInput) {
|
|
39
|
+
const rawBody = input.message.rawBody;
|
|
40
|
+
return {
|
|
41
|
+
channel: {
|
|
42
|
+
id: input.channelId,
|
|
43
|
+
accountId: input.accountId,
|
|
44
|
+
},
|
|
45
|
+
route: {
|
|
46
|
+
agentId: input.route.agentId,
|
|
47
|
+
routeSessionKey: input.route.routeSessionKey,
|
|
48
|
+
dispatchSessionKey: input.route.dispatchSessionKey,
|
|
49
|
+
mainSessionKey: input.route.mainSessionKey,
|
|
50
|
+
},
|
|
51
|
+
conversation: {
|
|
52
|
+
kind: input.conversation.kind,
|
|
53
|
+
id: input.conversation.id,
|
|
54
|
+
label: input.conversation.label,
|
|
55
|
+
},
|
|
56
|
+
reply: {
|
|
57
|
+
to: input.reply.to,
|
|
58
|
+
originatingTo: input.reply.originatingTo,
|
|
59
|
+
},
|
|
60
|
+
sender: {
|
|
61
|
+
id: input.sender.id,
|
|
62
|
+
displayName: input.sender.displayName || input.sender.id,
|
|
63
|
+
},
|
|
64
|
+
message: {
|
|
65
|
+
id: input.message.id || undefined,
|
|
66
|
+
rawBody,
|
|
67
|
+
bodyForAgent: input.message.bodyForAgent ?? rawBody,
|
|
68
|
+
commandBody: input.message.commandBody ?? rawBody,
|
|
69
|
+
envelopeBody: input.message.envelopeBody,
|
|
70
|
+
},
|
|
71
|
+
media: (input.media || []).map((item) => ({
|
|
72
|
+
path: item.path,
|
|
73
|
+
contentType: item.contentType,
|
|
74
|
+
kind: item.kind,
|
|
75
|
+
messageId: item.messageId,
|
|
76
|
+
})),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Keep this payload intentionally small: OpenClaw already renders standard
|
|
81
|
+
// conversation/sender/message metadata as untrusted context. Only include
|
|
82
|
+
// bncr-specific facts that are not otherwise visible to the model, so normal
|
|
83
|
+
// text turns do not get a duplicate "Bncr inbound context" JSON block.
|
|
84
|
+
export function buildBncrPromptVisibleContextFacts(
|
|
85
|
+
facts: ReturnType<typeof buildBncrStructuredContextFacts>,
|
|
86
|
+
) {
|
|
87
|
+
const result: {
|
|
88
|
+
reply?: {
|
|
89
|
+
to: string;
|
|
90
|
+
originatingTo: string;
|
|
91
|
+
};
|
|
92
|
+
media?: Array<{
|
|
93
|
+
contentType?: string;
|
|
94
|
+
kind?: string;
|
|
95
|
+
messageId?: string;
|
|
96
|
+
}>;
|
|
97
|
+
} = {};
|
|
98
|
+
|
|
99
|
+
if (facts.reply.originatingTo !== facts.reply.to) {
|
|
100
|
+
result.reply = {
|
|
101
|
+
to: facts.reply.to,
|
|
102
|
+
originatingTo: facts.reply.originatingTo,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (facts.media.length > 0) {
|
|
107
|
+
result.media = facts.media.map((item) => ({
|
|
108
|
+
contentType: item.contentType,
|
|
109
|
+
kind: item.kind,
|
|
110
|
+
messageId: item.messageId,
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function inferBncrStructuredMediaKind(contentType: string | undefined) {
|
|
118
|
+
if (contentType?.startsWith('image/')) return 'image';
|
|
119
|
+
if (contentType?.startsWith('video/')) return 'video';
|
|
120
|
+
if (contentType?.startsWith('audio/')) return 'audio';
|
|
121
|
+
return 'document';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export type BncrStructuredContextFactsFromInboundPartsInput = {
|
|
125
|
+
channelId: string;
|
|
126
|
+
parsed: {
|
|
127
|
+
accountId: string;
|
|
128
|
+
peer: {
|
|
129
|
+
kind: string;
|
|
130
|
+
id: string;
|
|
131
|
+
};
|
|
132
|
+
clientId?: string;
|
|
133
|
+
msgId?: string;
|
|
134
|
+
mimeType?: string;
|
|
135
|
+
};
|
|
136
|
+
resolution: {
|
|
137
|
+
chatType: string;
|
|
138
|
+
canonicalTo: string;
|
|
139
|
+
originatingTo: string;
|
|
140
|
+
resolvedRoute: {
|
|
141
|
+
agentId?: string;
|
|
142
|
+
sessionKey?: string;
|
|
143
|
+
mainSessionKey?: string;
|
|
144
|
+
};
|
|
145
|
+
dispatchSessionKey?: string;
|
|
146
|
+
};
|
|
147
|
+
prepared: {
|
|
148
|
+
rawBody: string;
|
|
149
|
+
body?: string;
|
|
150
|
+
mediaPath?: string | null;
|
|
151
|
+
};
|
|
152
|
+
senderIdForContext: string;
|
|
153
|
+
senderDisplayName?: string;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export function buildBncrStructuredContextFactsFromInboundParts(
|
|
157
|
+
input: BncrStructuredContextFactsFromInboundPartsInput,
|
|
158
|
+
) {
|
|
159
|
+
const mediaPath = input.prepared.mediaPath || undefined;
|
|
160
|
+
return buildBncrStructuredContextFacts({
|
|
161
|
+
channelId: input.channelId,
|
|
162
|
+
accountId: input.parsed.accountId,
|
|
163
|
+
route: {
|
|
164
|
+
agentId: input.resolution.resolvedRoute.agentId,
|
|
165
|
+
routeSessionKey: input.resolution.resolvedRoute.sessionKey,
|
|
166
|
+
dispatchSessionKey: input.resolution.dispatchSessionKey,
|
|
167
|
+
mainSessionKey: input.resolution.resolvedRoute.mainSessionKey,
|
|
168
|
+
},
|
|
169
|
+
conversation: {
|
|
170
|
+
kind: input.resolution.chatType,
|
|
171
|
+
id: input.parsed.peer.id,
|
|
172
|
+
label: input.resolution.canonicalTo,
|
|
173
|
+
},
|
|
174
|
+
reply: {
|
|
175
|
+
to: input.resolution.canonicalTo,
|
|
176
|
+
originatingTo: input.resolution.originatingTo,
|
|
177
|
+
},
|
|
178
|
+
sender: {
|
|
179
|
+
id: input.senderIdForContext,
|
|
180
|
+
displayName: input.senderDisplayName,
|
|
181
|
+
},
|
|
182
|
+
message: {
|
|
183
|
+
id: input.parsed.msgId,
|
|
184
|
+
rawBody: input.prepared.rawBody,
|
|
185
|
+
bodyForAgent: input.prepared.rawBody,
|
|
186
|
+
commandBody: input.prepared.rawBody,
|
|
187
|
+
envelopeBody: input.prepared.body,
|
|
188
|
+
},
|
|
189
|
+
media: mediaPath
|
|
190
|
+
? [
|
|
191
|
+
{
|
|
192
|
+
path: mediaPath,
|
|
193
|
+
contentType: input.parsed.mimeType,
|
|
194
|
+
kind: inferBncrStructuredMediaKind(input.parsed.mimeType),
|
|
195
|
+
messageId: input.parsed.msgId,
|
|
196
|
+
},
|
|
197
|
+
]
|
|
198
|
+
: [],
|
|
199
|
+
});
|
|
200
|
+
}
|