@xmoxmo/bncr 0.2.4 → 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 +68 -5
- package/package.json +1 -1
- package/src/channel.ts +2985 -1706
- package/src/core/connection-capability.ts +70 -0
- package/src/core/connection-reachability.ts +168 -0
- package/src/core/diagnostics.ts +54 -0
- package/src/core/downlink-health.ts +61 -0
- package/src/core/extended-diagnostics.ts +65 -0
- package/src/core/lease-state.ts +94 -0
- package/src/core/outbox-enqueue.ts +22 -0
- package/src/core/outbox-entry-builders.ts +92 -0
- package/src/core/outbox-file-transfer-bookkeeping.ts +31 -0
- package/src/core/outbox-file-transfer-failure.ts +25 -0
- package/src/core/outbox-file-transfer-guards.ts +66 -0
- package/src/core/outbox-file-transfer-prep.ts +31 -0
- package/src/core/outbox-file-transfer-success.ts +34 -0
- package/src/core/outbox-push-args.ts +67 -0
- package/src/core/outbox-queue.ts +69 -0
- package/src/core/outbox-summary.ts +14 -0
- package/src/core/outbox-text-push-failure.ts +10 -0
- package/src/core/outbox-text-push-guards.ts +51 -0
- package/src/core/outbox-text-push-prep.ts +36 -0
- package/src/core/outbox-text-push-success.ts +62 -0
- package/src/core/policy.ts +9 -0
- package/src/core/register-trace.ts +115 -0
- package/src/core/status.ts +57 -0
- package/src/core/types.ts +1 -0
- package/src/messaging/inbound/commands.ts +318 -75
- package/src/messaging/inbound/dispatch.ts +435 -139
- package/src/messaging/inbound/parse.ts +8 -0
- package/src/messaging/inbound/session-label.ts +115 -0
- package/src/messaging/outbound/diagnostics.ts +262 -0
- package/src/messaging/outbound/media-dedupe.ts +51 -0
- package/src/messaging/outbound/media.ts +3 -1
- package/src/messaging/outbound/queue-selectors.ts +191 -0
- package/src/messaging/outbound/reasons.ts +52 -0
- package/src/messaging/outbound/reply-enqueue.ts +329 -0
- package/src/messaging/outbound/reply-target-policy.ts +13 -0
- package/src/messaging/outbound/retry-policy.ts +142 -0
- package/src/messaging/outbound/send.ts +6 -0
- package/src/messaging/outbound/session-route.ts +34 -5
|
@@ -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,93 +10,139 @@ 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
|
-
channelId: string;
|
|
15
88
|
cfg: any;
|
|
89
|
+
channelId: string;
|
|
16
90
|
parsed: ParsedInbound;
|
|
17
91
|
canonicalAgentId: string;
|
|
18
|
-
rememberSessionRoute: (sessionKey: string, accountId: string, route: any) => void;
|
|
19
|
-
enqueueFromReply: (args: {
|
|
20
|
-
accountId: string;
|
|
21
|
-
sessionKey: string;
|
|
22
|
-
route: any;
|
|
23
|
-
payload: { text?: string; mediaUrl?: string; mediaUrls?: string[] };
|
|
24
|
-
mediaLocalRoots?: readonly string[];
|
|
25
|
-
}) => Promise<void>;
|
|
26
|
-
setInboundActivity: (accountId: string, at: number) => void;
|
|
27
|
-
scheduleSave: () => void;
|
|
28
|
-
logger?: { warn?: (msg: string) => void; error?: (msg: string) => void };
|
|
29
92
|
}) {
|
|
30
|
-
const {
|
|
31
|
-
|
|
32
|
-
|
|
93
|
+
const { api, cfg, channelId, parsed, canonicalAgentId } = args;
|
|
94
|
+
const { accountId, route, peer, sessionKeyfromroute, providedOriginatingTo, extracted } = parsed;
|
|
95
|
+
|
|
96
|
+
const resolvedRoute = api.runtime.channel.routing.resolveAgentRoute({
|
|
33
97
|
cfg,
|
|
34
|
-
|
|
35
|
-
canonicalAgentId,
|
|
36
|
-
rememberSessionRoute,
|
|
37
|
-
enqueueFromReply,
|
|
38
|
-
setInboundActivity,
|
|
39
|
-
scheduleSave,
|
|
40
|
-
logger,
|
|
41
|
-
} = params;
|
|
42
|
-
const {
|
|
98
|
+
channel: channelId,
|
|
43
99
|
accountId,
|
|
44
|
-
route,
|
|
45
100
|
peer,
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const baseSessionKey =
|
|
104
|
+
normalizeInboundSessionKey(sessionKeyfromroute, route, canonicalAgentId) ||
|
|
105
|
+
resolvedRoute.sessionKey;
|
|
106
|
+
const taskSessionKey = withTaskSessionKey(baseSessionKey, extracted.taskKey);
|
|
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 {
|
|
49
135
|
msgType,
|
|
50
136
|
mediaBase64,
|
|
51
137
|
mediaPathFromTransfer,
|
|
52
138
|
mimeType,
|
|
53
139
|
fileName,
|
|
54
|
-
msgId,
|
|
55
140
|
extracted,
|
|
56
141
|
platform,
|
|
57
142
|
groupId,
|
|
58
143
|
userId,
|
|
59
144
|
} = parsed;
|
|
60
|
-
|
|
61
|
-
const nativeCommand = await handleBncrNativeCommand({
|
|
62
|
-
api,
|
|
63
|
-
channelId,
|
|
64
|
-
cfg,
|
|
65
|
-
parsed,
|
|
66
|
-
canonicalAgentId,
|
|
67
|
-
rememberSessionRoute,
|
|
68
|
-
enqueueFromReply,
|
|
69
|
-
logger,
|
|
70
|
-
});
|
|
71
|
-
if (nativeCommand.handled) {
|
|
72
|
-
const inboundAt = Date.now();
|
|
73
|
-
setInboundActivity(accountId, inboundAt);
|
|
74
|
-
scheduleSave();
|
|
75
|
-
return {
|
|
76
|
-
accountId,
|
|
77
|
-
sessionKey: nativeCommand.sessionKey,
|
|
78
|
-
taskKey: extracted.taskKey ?? null,
|
|
79
|
-
msgId: msgId ?? null,
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const resolvedRoute = api.runtime.channel.routing.resolveAgentRoute({
|
|
84
|
-
cfg,
|
|
85
|
-
channel: channelId,
|
|
86
|
-
accountId,
|
|
87
|
-
peer,
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
const baseSessionKey =
|
|
91
|
-
normalizeInboundSessionKey(sessionKeyfromroute, route, canonicalAgentId) ||
|
|
92
|
-
resolvedRoute.sessionKey;
|
|
93
|
-
const agentText = extracted.text;
|
|
94
|
-
const taskSessionKey = withTaskSessionKey(baseSessionKey, extracted.taskKey);
|
|
95
|
-
const sessionKey = taskSessionKey || baseSessionKey;
|
|
145
|
+
const { accountId, route, resolvedRoute, baseSessionKey, taskSessionKey, dispatchSessionKey } = resolution;
|
|
96
146
|
|
|
97
147
|
rememberSessionRoute(baseSessionKey, accountId, route);
|
|
98
148
|
if (taskSessionKey && taskSessionKey !== baseSessionKey) {
|
|
@@ -105,7 +155,7 @@ export async function dispatchBncrInbound(params: {
|
|
|
105
155
|
|
|
106
156
|
let mediaPath: string | undefined;
|
|
107
157
|
if (mediaBase64) {
|
|
108
|
-
const mediaBuf =
|
|
158
|
+
const mediaBuf = decodeInboundMediaBase64(mediaBase64);
|
|
109
159
|
const saved = await api.runtime.channel.media.saveMediaBuffer(
|
|
110
160
|
mediaBuf,
|
|
111
161
|
mimeType,
|
|
@@ -118,103 +168,349 @@ export async function dispatchBncrInbound(params: {
|
|
|
118
168
|
mediaPath = mediaPathFromTransfer;
|
|
119
169
|
}
|
|
120
170
|
|
|
121
|
-
const rawBody =
|
|
171
|
+
const rawBody = extracted.text || (msgType === 'text' ? '' : `[${msgType}]`);
|
|
122
172
|
const body = api.runtime.channel.reply.formatAgentEnvelope({
|
|
123
173
|
channel: 'Bncr',
|
|
124
174
|
from: `${platform}:${groupId}:${userId}`,
|
|
125
175
|
timestamp: Date.now(),
|
|
126
176
|
previousTimestamp: api.runtime.channel.session.readSessionUpdatedAt({
|
|
127
177
|
storePath,
|
|
128
|
-
sessionKey,
|
|
178
|
+
sessionKey: dispatchSessionKey,
|
|
129
179
|
}),
|
|
130
180
|
envelope: api.runtime.channel.reply.resolveEnvelopeFormatOptions(cfg),
|
|
131
181
|
body: rawBody,
|
|
132
182
|
});
|
|
133
183
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
184
|
+
return {
|
|
185
|
+
storePath,
|
|
186
|
+
mediaPath,
|
|
187
|
+
rawBody,
|
|
188
|
+
body,
|
|
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,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export async function dispatchBncrInbound(params: {
|
|
328
|
+
api: any;
|
|
329
|
+
channelId: string;
|
|
330
|
+
cfg: any;
|
|
331
|
+
parsed: ParsedInbound;
|
|
332
|
+
canonicalAgentId: string;
|
|
333
|
+
rememberSessionRoute: (sessionKey: string, accountId: string, route: any) => void;
|
|
334
|
+
enqueueFromReply: (args: {
|
|
335
|
+
accountId: string;
|
|
336
|
+
sessionKey: string;
|
|
337
|
+
route: any;
|
|
338
|
+
payload: { text?: string; mediaUrl?: string; mediaUrls?: string[] };
|
|
339
|
+
mediaLocalRoots?: readonly string[];
|
|
340
|
+
}) => Promise<void>;
|
|
341
|
+
setInboundActivity: (accountId: string, at: number) => void;
|
|
342
|
+
scheduleSave: () => void;
|
|
343
|
+
logger?: { warn?: (msg: string) => void; error?: (msg: string) => void };
|
|
344
|
+
}) {
|
|
345
|
+
const {
|
|
346
|
+
api,
|
|
347
|
+
channelId,
|
|
348
|
+
cfg,
|
|
349
|
+
parsed,
|
|
350
|
+
canonicalAgentId,
|
|
351
|
+
rememberSessionRoute,
|
|
352
|
+
enqueueFromReply,
|
|
353
|
+
setInboundActivity,
|
|
354
|
+
scheduleSave,
|
|
355
|
+
logger,
|
|
356
|
+
} = params;
|
|
357
|
+
const { accountId, clientId, msgId, extracted, mimeType, peer } = parsed;
|
|
358
|
+
|
|
359
|
+
const nativeCommand = await handleBncrNativeCommand({
|
|
360
|
+
api,
|
|
361
|
+
channelId,
|
|
362
|
+
cfg,
|
|
363
|
+
parsed,
|
|
364
|
+
canonicalAgentId,
|
|
365
|
+
rememberSessionRoute,
|
|
366
|
+
enqueueFromReply,
|
|
367
|
+
logger,
|
|
368
|
+
});
|
|
369
|
+
if (nativeCommand.handled && !nativeCommand.fallbackToAgent) {
|
|
370
|
+
const inboundAt = Date.now();
|
|
371
|
+
setInboundActivity(accountId, inboundAt);
|
|
372
|
+
scheduleSave();
|
|
137
373
|
return {
|
|
138
374
|
accountId,
|
|
139
|
-
sessionKey,
|
|
375
|
+
sessionKey: nativeCommand.sessionKey,
|
|
140
376
|
taskKey: extracted.taskKey ?? null,
|
|
141
377
|
msgId: msgId ?? null,
|
|
142
378
|
};
|
|
143
379
|
}
|
|
144
|
-
const senderIdForContext = clientId;
|
|
145
|
-
const senderDisplayName = 'bncr-client';
|
|
146
|
-
const ctxPayload = api.runtime.channel.reply.finalizeInboundContext({
|
|
147
|
-
Body: body,
|
|
148
|
-
BodyForAgent: rawBody,
|
|
149
|
-
RawBody: rawBody,
|
|
150
|
-
CommandBody: rawBody,
|
|
151
|
-
MediaPath: mediaPath,
|
|
152
|
-
MediaType: mimeType,
|
|
153
|
-
From: senderIdForContext,
|
|
154
|
-
To: displayTo,
|
|
155
|
-
SessionKey: sessionKey,
|
|
156
|
-
AccountId: accountId,
|
|
157
|
-
ChatType: peer.kind,
|
|
158
|
-
ConversationLabel: displayTo,
|
|
159
|
-
SenderId: senderIdForContext,
|
|
160
|
-
SenderName: senderDisplayName,
|
|
161
|
-
SenderUsername: senderDisplayName,
|
|
162
|
-
Provider: channelId,
|
|
163
|
-
Surface: channelId,
|
|
164
|
-
MessageSid: msgId,
|
|
165
|
-
Timestamp: Date.now(),
|
|
166
|
-
OriginatingChannel: channelId,
|
|
167
|
-
OriginatingTo: displayTo,
|
|
168
|
-
});
|
|
169
380
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
381
|
+
const resolution = resolveBncrInboundConversation({
|
|
382
|
+
api,
|
|
383
|
+
cfg,
|
|
384
|
+
channelId,
|
|
385
|
+
parsed,
|
|
386
|
+
canonicalAgentId,
|
|
387
|
+
});
|
|
388
|
+
const { resolvedRoute, canonicalTo, dispatchSessionKey: sessionKey } = resolution;
|
|
389
|
+
const prepared = await prepareBncrInboundSessionContext({
|
|
390
|
+
api,
|
|
391
|
+
cfg,
|
|
392
|
+
parsed,
|
|
393
|
+
resolution,
|
|
394
|
+
rememberSessionRoute,
|
|
395
|
+
});
|
|
396
|
+
const { storePath, mediaPath, rawBody, body } = prepared;
|
|
397
|
+
const replyRouteFact = buildBncrInboundReplyRouteFact(resolution);
|
|
398
|
+
if (!clientId) {
|
|
399
|
+
emitBncrLogLine(
|
|
400
|
+
'warn',
|
|
401
|
+
'[bncr] inbound missing clientId for chat identity; using route identity fallback',
|
|
402
|
+
);
|
|
403
|
+
}
|
|
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,
|
|
177
417
|
});
|
|
178
|
-
|
|
179
|
-
const inboundAt = Date.now();
|
|
180
|
-
setInboundActivity(accountId, inboundAt);
|
|
181
|
-
scheduleSave();
|
|
182
418
|
|
|
183
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,
|
|
435
|
+
});
|
|
184
436
|
|
|
185
|
-
await api.runtime.channel.
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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)}`);
|
|
207
464
|
},
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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;
|
|
482
|
+
|
|
483
|
+
if (kind === 'tool' && !shouldForwardTool) {
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
|
|
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();
|
|
212
512
|
},
|
|
213
513
|
},
|
|
214
|
-
replyOptions: {
|
|
215
|
-
disableBlockStreaming: !effectiveReply.blockStreaming,
|
|
216
|
-
shouldEmitToolResult: effectiveReply.allowTool ? () => true : undefined,
|
|
217
|
-
},
|
|
218
514
|
});
|
|
219
515
|
|
|
220
516
|
return {
|
|
@@ -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,
|