@xmoxmo/bncr 0.2.5 → 0.2.6
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 +2 -2
- package/package.json +1 -1
- package/src/channel.ts +762 -209
- 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/types.ts +1 -0
- package/src/messaging/inbound/commands.ts +318 -75
- package/src/messaging/inbound/dispatch.ts +372 -114
- package/src/messaging/inbound/parse.ts +8 -0
- package/src/messaging/inbound/session-label.ts +115 -0
- package/src/messaging/outbound/diagnostics.ts +16 -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
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { resolvePinnedMainDmOwnerFromAllowlist } from 'openclaw/plugin-sdk/conversation-runtime';
|
|
3
|
+
import { resolveInboundLastRouteSessionKey } from 'openclaw/plugin-sdk/routing';
|
|
1
4
|
import { emitBncrLogLine } from '../../core/logging.ts';
|
|
5
|
+
import { resolveBncrChannelPolicy } from '../../core/policy.ts';
|
|
2
6
|
import {
|
|
3
7
|
formatDisplayScope,
|
|
4
8
|
normalizeInboundSessionKey,
|
|
@@ -6,34 +10,88 @@ import {
|
|
|
6
10
|
} from '../../core/targets.ts';
|
|
7
11
|
import { handleBncrNativeCommand } from './commands.ts';
|
|
8
12
|
import { buildBncrReplyConfig } from './reply-config.ts';
|
|
13
|
+
import { wrapBncrInboundRecordSessionLabelCorrection } from './session-label.ts';
|
|
9
14
|
|
|
10
15
|
type ParsedInbound = ReturnType<typeof import('./parse.ts')['parseBncrInboundParams']>;
|
|
11
16
|
|
|
12
|
-
|
|
17
|
+
type BncrInboundConversationResolution = {
|
|
18
|
+
accountId: string;
|
|
19
|
+
chatType: 'direct' | 'group';
|
|
20
|
+
route: ParsedInbound['route'];
|
|
21
|
+
resolvedRoute: {
|
|
22
|
+
sessionKey: string;
|
|
23
|
+
agentId: string;
|
|
24
|
+
mainSessionKey?: string;
|
|
25
|
+
};
|
|
26
|
+
canonicalTo: string;
|
|
27
|
+
rawTo: string;
|
|
28
|
+
originatingTo: string;
|
|
29
|
+
baseSessionKey: string;
|
|
30
|
+
taskSessionKey?: string;
|
|
31
|
+
dispatchSessionKey: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type BncrInboundReplyRouteFact = {
|
|
35
|
+
accountId: string;
|
|
36
|
+
sessionKey: string;
|
|
37
|
+
route: ParsedInbound['route'];
|
|
38
|
+
canonicalTo: string;
|
|
39
|
+
originatingTo: string;
|
|
40
|
+
chatType: 'direct' | 'group';
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const INBOUND_MEDIA_MAX_BYTES = 30 * 1024 * 1024;
|
|
44
|
+
|
|
45
|
+
export function estimateBase64DecodedBytes(value: string): number {
|
|
46
|
+
const normalized = String(value || '').replace(/\s+/g, '');
|
|
47
|
+
if (!normalized) return 0;
|
|
48
|
+
const padding = normalized.endsWith('==') ? 2 : normalized.endsWith('=') ? 1 : 0;
|
|
49
|
+
return Math.max(0, Math.floor((normalized.length * 3) / 4) - padding);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function assertInboundMediaBase64Size(
|
|
53
|
+
value: string,
|
|
54
|
+
maxBytes = INBOUND_MEDIA_MAX_BYTES,
|
|
55
|
+
) {
|
|
56
|
+
const estimatedBytes = estimateBase64DecodedBytes(value);
|
|
57
|
+
if (estimatedBytes > maxBytes) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`inbound media too large: estimated ${estimatedBytes} bytes exceeds ${maxBytes} bytes`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function decodeInboundMediaBase64(
|
|
65
|
+
value: string,
|
|
66
|
+
maxBytes = INBOUND_MEDIA_MAX_BYTES,
|
|
67
|
+
): Buffer {
|
|
68
|
+
assertInboundMediaBase64Size(value, maxBytes);
|
|
69
|
+
const normalized = String(value || '').replace(/\s+/g, '');
|
|
70
|
+
const mediaBuf = Buffer.from(normalized, 'base64');
|
|
71
|
+
if (!mediaBuf.length) {
|
|
72
|
+
throw new Error('inbound media base64 decoded to empty buffer');
|
|
73
|
+
}
|
|
74
|
+
if (mediaBuf.length > maxBytes) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`inbound media too large: decoded ${mediaBuf.length} bytes exceeds ${maxBytes} bytes`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return mediaBuf;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function formatRawBncrInboundTarget(route: ParsedInbound['route']): string {
|
|
83
|
+
return `Bncr:${String(route.platform || '').trim()}:${String(route.groupId || '').trim()}:${String(route.userId || '').trim()}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function resolveBncrInboundConversation(args: {
|
|
13
87
|
api: any;
|
|
14
88
|
cfg: any;
|
|
15
89
|
channelId: string;
|
|
16
90
|
parsed: ParsedInbound;
|
|
17
91
|
canonicalAgentId: string;
|
|
18
|
-
rememberSessionRoute: (sessionKey: string, accountId: string, route: any) => void;
|
|
19
92
|
}) {
|
|
20
|
-
const { api, cfg, channelId, parsed, canonicalAgentId
|
|
21
|
-
const {
|
|
22
|
-
accountId,
|
|
23
|
-
route,
|
|
24
|
-
peer,
|
|
25
|
-
sessionKeyfromroute,
|
|
26
|
-
text,
|
|
27
|
-
msgType,
|
|
28
|
-
mediaBase64,
|
|
29
|
-
mediaPathFromTransfer,
|
|
30
|
-
mimeType,
|
|
31
|
-
fileName,
|
|
32
|
-
extracted,
|
|
33
|
-
platform,
|
|
34
|
-
groupId,
|
|
35
|
-
userId,
|
|
36
|
-
} = parsed;
|
|
93
|
+
const { api, cfg, channelId, parsed, canonicalAgentId } = args;
|
|
94
|
+
const { accountId, route, peer, sessionKeyfromroute, providedOriginatingTo, extracted } = parsed;
|
|
37
95
|
|
|
38
96
|
const resolvedRoute = api.runtime.channel.routing.resolveAgentRoute({
|
|
39
97
|
cfg,
|
|
@@ -45,9 +103,46 @@ async function prepareBncrInboundSessionContext(args: {
|
|
|
45
103
|
const baseSessionKey =
|
|
46
104
|
normalizeInboundSessionKey(sessionKeyfromroute, route, canonicalAgentId) ||
|
|
47
105
|
resolvedRoute.sessionKey;
|
|
48
|
-
const agentText = extracted.text;
|
|
49
106
|
const taskSessionKey = withTaskSessionKey(baseSessionKey, extracted.taskKey);
|
|
50
|
-
const
|
|
107
|
+
const dispatchSessionKey = taskSessionKey || baseSessionKey;
|
|
108
|
+
const rawTo = formatRawBncrInboundTarget(route);
|
|
109
|
+
const canonicalTo = formatDisplayScope(route);
|
|
110
|
+
const originatingTo = providedOriginatingTo || rawTo;
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
accountId,
|
|
114
|
+
chatType: peer.kind,
|
|
115
|
+
route,
|
|
116
|
+
resolvedRoute,
|
|
117
|
+
canonicalTo,
|
|
118
|
+
rawTo,
|
|
119
|
+
originatingTo,
|
|
120
|
+
baseSessionKey,
|
|
121
|
+
...(taskSessionKey ? { taskSessionKey } : {}),
|
|
122
|
+
dispatchSessionKey,
|
|
123
|
+
} satisfies BncrInboundConversationResolution;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function prepareBncrInboundSessionContext(args: {
|
|
127
|
+
api: any;
|
|
128
|
+
cfg: any;
|
|
129
|
+
parsed: ParsedInbound;
|
|
130
|
+
resolution: BncrInboundConversationResolution;
|
|
131
|
+
rememberSessionRoute: (sessionKey: string, accountId: string, route: any) => void;
|
|
132
|
+
}) {
|
|
133
|
+
const { api, cfg, parsed, resolution, rememberSessionRoute } = args;
|
|
134
|
+
const {
|
|
135
|
+
msgType,
|
|
136
|
+
mediaBase64,
|
|
137
|
+
mediaPathFromTransfer,
|
|
138
|
+
mimeType,
|
|
139
|
+
fileName,
|
|
140
|
+
extracted,
|
|
141
|
+
platform,
|
|
142
|
+
groupId,
|
|
143
|
+
userId,
|
|
144
|
+
} = parsed;
|
|
145
|
+
const { accountId, route, resolvedRoute, baseSessionKey, taskSessionKey, dispatchSessionKey } = resolution;
|
|
51
146
|
|
|
52
147
|
rememberSessionRoute(baseSessionKey, accountId, route);
|
|
53
148
|
if (taskSessionKey && taskSessionKey !== baseSessionKey) {
|
|
@@ -60,7 +155,7 @@ async function prepareBncrInboundSessionContext(args: {
|
|
|
60
155
|
|
|
61
156
|
let mediaPath: string | undefined;
|
|
62
157
|
if (mediaBase64) {
|
|
63
|
-
const mediaBuf =
|
|
158
|
+
const mediaBuf = decodeInboundMediaBase64(mediaBase64);
|
|
64
159
|
const saved = await api.runtime.channel.media.saveMediaBuffer(
|
|
65
160
|
mediaBuf,
|
|
66
161
|
mimeType,
|
|
@@ -73,30 +168,159 @@ async function prepareBncrInboundSessionContext(args: {
|
|
|
73
168
|
mediaPath = mediaPathFromTransfer;
|
|
74
169
|
}
|
|
75
170
|
|
|
76
|
-
const rawBody =
|
|
171
|
+
const rawBody = extracted.text || (msgType === 'text' ? '' : `[${msgType}]`);
|
|
77
172
|
const body = api.runtime.channel.reply.formatAgentEnvelope({
|
|
78
173
|
channel: 'Bncr',
|
|
79
174
|
from: `${platform}:${groupId}:${userId}`,
|
|
80
175
|
timestamp: Date.now(),
|
|
81
176
|
previousTimestamp: api.runtime.channel.session.readSessionUpdatedAt({
|
|
82
177
|
storePath,
|
|
83
|
-
sessionKey,
|
|
178
|
+
sessionKey: dispatchSessionKey,
|
|
84
179
|
}),
|
|
85
180
|
envelope: api.runtime.channel.reply.resolveEnvelopeFormatOptions(cfg),
|
|
86
181
|
body: rawBody,
|
|
87
182
|
});
|
|
88
183
|
|
|
89
|
-
const displayTo = formatDisplayScope(route);
|
|
90
184
|
return {
|
|
91
|
-
resolvedRoute,
|
|
92
|
-
baseSessionKey,
|
|
93
|
-
taskSessionKey,
|
|
94
|
-
sessionKey,
|
|
95
185
|
storePath,
|
|
96
186
|
mediaPath,
|
|
97
187
|
rawBody,
|
|
98
188
|
body,
|
|
99
|
-
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function buildBncrInboundTurnContext(args: {
|
|
193
|
+
api: any;
|
|
194
|
+
channelId: string;
|
|
195
|
+
msgId?: string | null;
|
|
196
|
+
mimeType?: string;
|
|
197
|
+
mediaPath?: string;
|
|
198
|
+
peer: ParsedInbound['peer'];
|
|
199
|
+
senderIdForContext: string;
|
|
200
|
+
senderDisplayName: string;
|
|
201
|
+
resolution: BncrInboundConversationResolution;
|
|
202
|
+
prepared: {
|
|
203
|
+
rawBody: string;
|
|
204
|
+
body: string;
|
|
205
|
+
};
|
|
206
|
+
}) {
|
|
207
|
+
const {
|
|
208
|
+
api,
|
|
209
|
+
channelId,
|
|
210
|
+
msgId,
|
|
211
|
+
mimeType,
|
|
212
|
+
mediaPath,
|
|
213
|
+
peer,
|
|
214
|
+
senderIdForContext,
|
|
215
|
+
senderDisplayName,
|
|
216
|
+
resolution,
|
|
217
|
+
prepared,
|
|
218
|
+
} = args;
|
|
219
|
+
|
|
220
|
+
return api.runtime.channel.turn.buildContext({
|
|
221
|
+
channel: channelId,
|
|
222
|
+
provider: channelId,
|
|
223
|
+
surface: channelId,
|
|
224
|
+
accountId: resolution.accountId,
|
|
225
|
+
messageId: msgId,
|
|
226
|
+
timestamp: Date.now(),
|
|
227
|
+
from: senderIdForContext,
|
|
228
|
+
sender: {
|
|
229
|
+
id: senderIdForContext,
|
|
230
|
+
name: senderDisplayName,
|
|
231
|
+
username: senderDisplayName,
|
|
232
|
+
},
|
|
233
|
+
conversation: {
|
|
234
|
+
kind: resolution.chatType,
|
|
235
|
+
id: peer.id,
|
|
236
|
+
label: resolution.canonicalTo,
|
|
237
|
+
routePeer: {
|
|
238
|
+
kind: peer.kind,
|
|
239
|
+
id: peer.id,
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
route: {
|
|
243
|
+
agentId: resolution.resolvedRoute.agentId,
|
|
244
|
+
accountId: resolution.accountId,
|
|
245
|
+
routeSessionKey: resolution.resolvedRoute.sessionKey,
|
|
246
|
+
dispatchSessionKey: resolution.dispatchSessionKey,
|
|
247
|
+
mainSessionKey: resolution.resolvedRoute.mainSessionKey,
|
|
248
|
+
},
|
|
249
|
+
reply: {
|
|
250
|
+
to: resolution.canonicalTo,
|
|
251
|
+
originatingTo: resolution.originatingTo,
|
|
252
|
+
},
|
|
253
|
+
message: {
|
|
254
|
+
inboundEventKind: 'user_request',
|
|
255
|
+
body: prepared.body,
|
|
256
|
+
rawBody: prepared.rawBody,
|
|
257
|
+
bodyForAgent: prepared.rawBody,
|
|
258
|
+
commandBody: prepared.rawBody,
|
|
259
|
+
envelopeFrom: resolution.originatingTo,
|
|
260
|
+
senderLabel: senderDisplayName,
|
|
261
|
+
},
|
|
262
|
+
media: mediaPath
|
|
263
|
+
? [
|
|
264
|
+
{
|
|
265
|
+
path: mediaPath,
|
|
266
|
+
contentType: mimeType,
|
|
267
|
+
kind: mimeType?.startsWith('image/')
|
|
268
|
+
? 'image'
|
|
269
|
+
: mimeType?.startsWith('video/')
|
|
270
|
+
? 'video'
|
|
271
|
+
: mimeType?.startsWith('audio/')
|
|
272
|
+
? 'audio'
|
|
273
|
+
: 'document',
|
|
274
|
+
messageId: msgId ?? undefined,
|
|
275
|
+
},
|
|
276
|
+
]
|
|
277
|
+
: [],
|
|
278
|
+
extra: {
|
|
279
|
+
OriginatingChannel: channelId,
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function buildBncrInboundRecordUpdateLastRoute(args: {
|
|
285
|
+
channelId: string;
|
|
286
|
+
peer: ParsedInbound['peer'];
|
|
287
|
+
senderIdForContext: string;
|
|
288
|
+
resolution: BncrInboundConversationResolution;
|
|
289
|
+
pinnedMainDmOwner: string | null;
|
|
290
|
+
}) {
|
|
291
|
+
const { channelId, peer, senderIdForContext, resolution, pinnedMainDmOwner } = args;
|
|
292
|
+
if (peer.kind !== 'direct') return undefined;
|
|
293
|
+
|
|
294
|
+
const sessionKey = resolveInboundLastRouteSessionKey({
|
|
295
|
+
route: resolution.resolvedRoute,
|
|
296
|
+
sessionKey: resolution.dispatchSessionKey,
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
sessionKey,
|
|
301
|
+
channel: channelId,
|
|
302
|
+
to: resolution.canonicalTo,
|
|
303
|
+
accountId: resolution.accountId,
|
|
304
|
+
mainDmOwnerPin:
|
|
305
|
+
sessionKey === resolution.resolvedRoute.mainSessionKey && pinnedMainDmOwner
|
|
306
|
+
? {
|
|
307
|
+
ownerRecipient: pinnedMainDmOwner,
|
|
308
|
+
senderRecipient: senderIdForContext,
|
|
309
|
+
}
|
|
310
|
+
: undefined,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function buildBncrInboundReplyRouteFact(
|
|
315
|
+
resolution: BncrInboundConversationResolution,
|
|
316
|
+
): BncrInboundReplyRouteFact {
|
|
317
|
+
return {
|
|
318
|
+
accountId: resolution.accountId,
|
|
319
|
+
sessionKey: resolution.dispatchSessionKey,
|
|
320
|
+
route: resolution.route,
|
|
321
|
+
canonicalTo: resolution.canonicalTo,
|
|
322
|
+
originatingTo: resolution.originatingTo,
|
|
323
|
+
chatType: resolution.chatType,
|
|
100
324
|
};
|
|
101
325
|
}
|
|
102
326
|
|
|
@@ -130,7 +354,7 @@ export async function dispatchBncrInbound(params: {
|
|
|
130
354
|
scheduleSave,
|
|
131
355
|
logger,
|
|
132
356
|
} = params;
|
|
133
|
-
const { accountId,
|
|
357
|
+
const { accountId, clientId, msgId, extracted, mimeType, peer } = parsed;
|
|
134
358
|
|
|
135
359
|
const nativeCommand = await handleBncrNativeCommand({
|
|
136
360
|
api,
|
|
@@ -142,7 +366,7 @@ export async function dispatchBncrInbound(params: {
|
|
|
142
366
|
enqueueFromReply,
|
|
143
367
|
logger,
|
|
144
368
|
});
|
|
145
|
-
if (nativeCommand.handled) {
|
|
369
|
+
if (nativeCommand.handled && !nativeCommand.fallbackToAgent) {
|
|
146
370
|
const inboundAt = Date.now();
|
|
147
371
|
setInboundActivity(accountId, inboundAt);
|
|
148
372
|
scheduleSave();
|
|
@@ -154,104 +378,138 @@ export async function dispatchBncrInbound(params: {
|
|
|
154
378
|
};
|
|
155
379
|
}
|
|
156
380
|
|
|
157
|
-
const {
|
|
158
|
-
resolvedRoute,
|
|
159
|
-
sessionKey,
|
|
160
|
-
storePath,
|
|
161
|
-
mediaPath,
|
|
162
|
-
rawBody,
|
|
163
|
-
body,
|
|
164
|
-
displayTo,
|
|
165
|
-
} = await prepareBncrInboundSessionContext({
|
|
381
|
+
const resolution = resolveBncrInboundConversation({
|
|
166
382
|
api,
|
|
167
383
|
cfg,
|
|
168
384
|
channelId,
|
|
169
385
|
parsed,
|
|
170
386
|
canonicalAgentId,
|
|
387
|
+
});
|
|
388
|
+
const { resolvedRoute, canonicalTo, dispatchSessionKey: sessionKey } = resolution;
|
|
389
|
+
const prepared = await prepareBncrInboundSessionContext({
|
|
390
|
+
api,
|
|
391
|
+
cfg,
|
|
392
|
+
parsed,
|
|
393
|
+
resolution,
|
|
171
394
|
rememberSessionRoute,
|
|
172
395
|
});
|
|
396
|
+
const { storePath, mediaPath, rawBody, body } = prepared;
|
|
397
|
+
const replyRouteFact = buildBncrInboundReplyRouteFact(resolution);
|
|
173
398
|
if (!clientId) {
|
|
174
|
-
emitBncrLogLine(
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
taskKey: extracted.taskKey ?? null,
|
|
179
|
-
msgId: msgId ?? null,
|
|
180
|
-
};
|
|
399
|
+
emitBncrLogLine(
|
|
400
|
+
'warn',
|
|
401
|
+
'[bncr] inbound missing clientId for chat identity; using route identity fallback',
|
|
402
|
+
);
|
|
181
403
|
}
|
|
182
|
-
const senderIdForContext = clientId;
|
|
183
|
-
const senderDisplayName = 'bncr-client';
|
|
184
|
-
const ctxPayload =
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
ChatType: peer.kind,
|
|
196
|
-
ConversationLabel: displayTo,
|
|
197
|
-
SenderId: senderIdForContext,
|
|
198
|
-
SenderName: senderDisplayName,
|
|
199
|
-
SenderUsername: senderDisplayName,
|
|
200
|
-
Provider: channelId,
|
|
201
|
-
Surface: channelId,
|
|
202
|
-
MessageSid: msgId,
|
|
203
|
-
Timestamp: Date.now(),
|
|
204
|
-
OriginatingChannel: channelId,
|
|
205
|
-
OriginatingTo: displayTo,
|
|
404
|
+
const senderIdForContext = clientId || canonicalTo;
|
|
405
|
+
const senderDisplayName = clientId ? 'bncr-client' : canonicalTo;
|
|
406
|
+
const ctxPayload = buildBncrInboundTurnContext({
|
|
407
|
+
api,
|
|
408
|
+
channelId,
|
|
409
|
+
msgId,
|
|
410
|
+
mimeType,
|
|
411
|
+
mediaPath,
|
|
412
|
+
peer,
|
|
413
|
+
senderIdForContext,
|
|
414
|
+
senderDisplayName,
|
|
415
|
+
resolution,
|
|
416
|
+
prepared,
|
|
206
417
|
});
|
|
207
418
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
419
|
+
const effectiveReply = buildBncrReplyConfig(cfg);
|
|
420
|
+
const channelPolicy = resolveBncrChannelPolicy(cfg?.channels?.bncr || {});
|
|
421
|
+
const pinnedMainDmOwner =
|
|
422
|
+
peer.kind === 'direct'
|
|
423
|
+
? resolvePinnedMainDmOwnerFromAllowlist({
|
|
424
|
+
dmScope: cfg?.session?.dmScope,
|
|
425
|
+
allowFrom: channelPolicy.allowFrom,
|
|
426
|
+
normalizeEntry: (entry: string) => String(entry || '').trim(),
|
|
427
|
+
})
|
|
428
|
+
: null;
|
|
429
|
+
const updateLastRoute = buildBncrInboundRecordUpdateLastRoute({
|
|
430
|
+
channelId,
|
|
431
|
+
peer,
|
|
432
|
+
senderIdForContext,
|
|
433
|
+
resolution,
|
|
434
|
+
pinnedMainDmOwner,
|
|
215
435
|
});
|
|
216
436
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
437
|
+
await api.runtime.channel.turn.run({
|
|
438
|
+
channel: channelId,
|
|
439
|
+
accountId,
|
|
440
|
+
raw: parsed,
|
|
441
|
+
adapter: {
|
|
442
|
+
ingest: () => ({
|
|
443
|
+
id: msgId ?? `${canonicalTo}:${Date.now()}`,
|
|
444
|
+
timestamp: Date.now(),
|
|
445
|
+
rawText: rawBody,
|
|
446
|
+
textForAgent: ctxPayload.BodyForAgent,
|
|
447
|
+
textForCommands: ctxPayload.CommandBody,
|
|
448
|
+
raw: parsed,
|
|
449
|
+
}),
|
|
450
|
+
resolveTurn: () => ({
|
|
451
|
+
channel: channelId,
|
|
452
|
+
accountId,
|
|
453
|
+
routeSessionKey: resolvedRoute.sessionKey,
|
|
454
|
+
storePath,
|
|
455
|
+
ctxPayload,
|
|
456
|
+
recordInboundSession: wrapBncrInboundRecordSessionLabelCorrection({
|
|
457
|
+
recordInboundSession: api.runtime.channel.session.recordInboundSession,
|
|
458
|
+
expectedLabel: canonicalTo,
|
|
459
|
+
}),
|
|
460
|
+
record: {
|
|
461
|
+
updateLastRoute,
|
|
462
|
+
onRecordError: (err: unknown) => {
|
|
463
|
+
emitBncrLogLine('warn', `[bncr] inbound record session failed: ${String(err)}`);
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
runDispatch: () =>
|
|
467
|
+
api.runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
468
|
+
ctx: ctxPayload,
|
|
469
|
+
cfg: effectiveReply.replyCfg,
|
|
470
|
+
dispatcherOptions: {
|
|
471
|
+
deliver: async (
|
|
472
|
+
payload: {
|
|
473
|
+
text?: string;
|
|
474
|
+
mediaUrl?: string;
|
|
475
|
+
mediaUrls?: string[];
|
|
476
|
+
audioAsVoice?: boolean;
|
|
477
|
+
},
|
|
478
|
+
info?: { kind?: 'tool' | 'block' | 'final' },
|
|
479
|
+
) => {
|
|
480
|
+
const kind = info?.kind;
|
|
481
|
+
const shouldForwardTool = effectiveReply.blockStreaming && effectiveReply.allowTool;
|
|
220
482
|
|
|
221
|
-
|
|
483
|
+
if (kind === 'tool' && !shouldForwardTool) {
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
222
486
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
487
|
+
await enqueueFromReply({
|
|
488
|
+
accountId: replyRouteFact.accountId,
|
|
489
|
+
sessionKey: replyRouteFact.sessionKey,
|
|
490
|
+
route: replyRouteFact.route,
|
|
491
|
+
payload: {
|
|
492
|
+
...payload,
|
|
493
|
+
kind: kind as 'tool' | 'block' | 'final' | undefined,
|
|
494
|
+
replyToId: msgId || undefined,
|
|
495
|
+
},
|
|
496
|
+
});
|
|
497
|
+
},
|
|
498
|
+
onError: (err: unknown) => {
|
|
499
|
+
emitBncrLogLine('error', `[bncr] outbound reply failed: ${String(err)}`);
|
|
500
|
+
},
|
|
501
|
+
},
|
|
502
|
+
replyOptions: {
|
|
503
|
+
disableBlockStreaming: !effectiveReply.blockStreaming,
|
|
504
|
+
shouldEmitToolResult: effectiveReply.allowTool ? () => true : undefined,
|
|
505
|
+
},
|
|
506
|
+
}),
|
|
507
|
+
}),
|
|
508
|
+
onFinalize: () => {
|
|
509
|
+
const inboundAt = Date.now();
|
|
510
|
+
setInboundActivity(accountId, inboundAt);
|
|
511
|
+
scheduleSave();
|
|
247
512
|
},
|
|
248
|
-
onError: (err: unknown) => {
|
|
249
|
-
emitBncrLogLine('error', `[bncr] outbound reply failed: ${String(err)}`);
|
|
250
|
-
},
|
|
251
|
-
},
|
|
252
|
-
replyOptions: {
|
|
253
|
-
disableBlockStreaming: !effectiveReply.blockStreaming,
|
|
254
|
-
shouldEmitToolResult: effectiveReply.allowTool ? () => true : undefined,
|
|
255
513
|
},
|
|
256
514
|
});
|
|
257
515
|
|
|
@@ -36,6 +36,10 @@ export function inboundDedupKey(params: {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
export function resolveChatType(_route: BncrRoute): 'direct' | 'group' {
|
|
39
|
+
// Compatibility boundary: bncr currently records and dispatches all conversations as direct
|
|
40
|
+
// sessions even when the display scope contains a group id. Do not change this to true
|
|
41
|
+
// group semantics without updating session routing, reply target policy, and requireMention
|
|
42
|
+
// behavior together.
|
|
39
43
|
return 'direct';
|
|
40
44
|
}
|
|
41
45
|
|
|
@@ -45,6 +49,9 @@ export function parseBncrInboundParams(params: any) {
|
|
|
45
49
|
const groupId = asString(params?.groupId || '0').trim() || '0';
|
|
46
50
|
const userId = asString(params?.userId || '').trim();
|
|
47
51
|
const sessionKeyfromroute = asString(params?.sessionKey || '').trim();
|
|
52
|
+
const providedOriginatingTo = asString(
|
|
53
|
+
params?.originatingTo || params?.providedOriginatingTo || params?.to || '',
|
|
54
|
+
).trim() || undefined;
|
|
48
55
|
const clientId = asString(params?.clientId || '').trim() || undefined;
|
|
49
56
|
|
|
50
57
|
const route: BncrRoute = {
|
|
@@ -84,6 +91,7 @@ export function parseBncrInboundParams(params: any) {
|
|
|
84
91
|
groupId,
|
|
85
92
|
userId,
|
|
86
93
|
sessionKeyfromroute,
|
|
94
|
+
providedOriginatingTo,
|
|
87
95
|
clientId,
|
|
88
96
|
route,
|
|
89
97
|
text,
|