@xmoxmo/bncr 0.4.6 → 0.4.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/package.json +1 -1
- package/src/channel.ts +41 -2
- package/src/core/targets.ts +106 -17
- package/src/messaging/inbound/commands.ts +263 -51
- package/src/messaging/inbound/context-facts.ts +126 -14
- package/src/messaging/inbound/contracts.ts +24 -0
- package/src/messaging/inbound/dispatch-prep.ts +214 -39
- package/src/messaging/inbound/dispatch.ts +71 -5
- package/src/messaging/inbound/gate.ts +56 -86
- package/src/messaging/inbound/group-history.ts +189 -0
- package/src/messaging/inbound/native-command-runtime.ts +77 -61
- package/src/messaging/inbound/native-command.ts +92 -8
- package/src/messaging/inbound/parse.ts +113 -8
- package/src/messaging/inbound/reply-dispatch-serial.ts +62 -0
- package/src/messaging/inbound/reply-dispatch.ts +252 -77
- package/src/messaging/inbound/scene-admin.ts +269 -0
- package/src/messaging/inbound/session-label.ts +122 -13
- package/src/messaging/inbound/session-meta-task.ts +17 -0
- package/src/messaging/inbound/turn-context.ts +184 -71
- package/src/openclaw/channel-runtime-contracts.ts +1 -0
- package/src/plugin/channel-components.ts +34 -1
- package/src/plugin/channel-inbound-helpers.ts +9 -2
- package/src/plugin/channel-runtime-builders-delivery.ts +24 -1
- package/src/plugin/channel-runtime-types.ts +42 -0
- package/src/plugin/file-inbound-init.ts +27 -12
- package/src/plugin/file-inbound-runtime.ts +2 -0
- package/src/plugin/inbound-acceptance.ts +82 -1
- package/src/plugin/inbound-handlers.ts +55 -2
- package/src/plugin/inbound-surface-handlers-group.ts +16 -0
- package/src/plugin/messaging.ts +22 -5
- package/src/plugin/scene-registry.ts +155 -0
- package/src/plugin/state-store.ts +133 -0
- package/src/plugin/state-transient-runtime-group.ts +5 -0
- package/src/plugin/target-runtime.ts +2 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
+
import { hasControlCommand } from 'openclaw/plugin-sdk/command-auth';
|
|
2
3
|
import {
|
|
3
4
|
formatDisplayScope,
|
|
4
5
|
normalizeInboundSessionKey,
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
} from '../../openclaw/routing-runtime.ts';
|
|
20
21
|
import type { BncrInboundApi, BncrInboundConfig, BncrRememberSessionRoute } from './contracts.ts';
|
|
21
22
|
import { INBOUND_MEDIA_URL_MAX_BYTES, isHttpMediaUrl } from './media-url-download.ts';
|
|
23
|
+
import { parseBncrNativeCommand } from './native-command.ts';
|
|
22
24
|
import { loadInboundRemoteMedia } from './remote-media.ts';
|
|
23
25
|
|
|
24
26
|
export type ParsedInbound = ReturnType<typeof import('./parse.ts')['parseBncrInboundParams']>;
|
|
@@ -27,6 +29,7 @@ export type BncrInboundConversationResolution = {
|
|
|
27
29
|
accountId: string;
|
|
28
30
|
chatType: 'direct' | 'group';
|
|
29
31
|
route: ParsedInbound['route'];
|
|
32
|
+
originalResolvedRouteSessionKey: string;
|
|
30
33
|
resolvedRoute: {
|
|
31
34
|
sessionKey: string;
|
|
32
35
|
agentId: string;
|
|
@@ -51,6 +54,13 @@ export type BncrInboundReplyRouteFact = {
|
|
|
51
54
|
|
|
52
55
|
export type BncrPreparedInboundSessionContext = {
|
|
53
56
|
storePath: string;
|
|
57
|
+
mediaItems: Array<{
|
|
58
|
+
path: string;
|
|
59
|
+
contentType?: string;
|
|
60
|
+
fileName?: string;
|
|
61
|
+
kind: 'image' | 'video' | 'audio' | 'document';
|
|
62
|
+
transferId?: string;
|
|
63
|
+
}>;
|
|
54
64
|
mediaPath?: string;
|
|
55
65
|
mediaContentType?: string;
|
|
56
66
|
rawBody: string;
|
|
@@ -63,10 +73,27 @@ export type BncrInboundPreparation = {
|
|
|
63
73
|
replyRouteFact: BncrInboundReplyRouteFact;
|
|
64
74
|
senderIdForContext: string;
|
|
65
75
|
senderDisplayName: string;
|
|
76
|
+
ownerAllowFrom?: string[];
|
|
77
|
+
bridgeSenderId?: string;
|
|
78
|
+
bridgeSenderName?: string;
|
|
66
79
|
};
|
|
67
80
|
|
|
68
81
|
const INBOUND_MEDIA_MAX_BYTES = 30 * 1024 * 1024;
|
|
69
82
|
|
|
83
|
+
const BNCR_GENERIC_MEDIA_TEXTS = new Set([
|
|
84
|
+
'收到媒体文件',
|
|
85
|
+
'收到图片',
|
|
86
|
+
'收到视频',
|
|
87
|
+
'收到语音',
|
|
88
|
+
'收到音频',
|
|
89
|
+
'收到文件',
|
|
90
|
+
'[图片]',
|
|
91
|
+
'[视频]',
|
|
92
|
+
'[语音]',
|
|
93
|
+
'[音频]',
|
|
94
|
+
'[文件]',
|
|
95
|
+
]);
|
|
96
|
+
|
|
70
97
|
function assertResolvedAgentRoute(resolvedRoute: OpenClawResolvedAgentRoute): {
|
|
71
98
|
sessionKey: string;
|
|
72
99
|
agentId: string;
|
|
@@ -90,14 +117,140 @@ function formatRawBncrInboundTarget(route: ParsedInbound['route']): string {
|
|
|
90
117
|
return `Bncr:${String(route.platform || '').trim()}:${String(route.groupId || '').trim()}:${String(route.userId || '').trim()}`;
|
|
91
118
|
}
|
|
92
119
|
|
|
120
|
+
function inferBncrInboundMediaKind(args: {
|
|
121
|
+
msgType?: string;
|
|
122
|
+
mediaContentType?: string;
|
|
123
|
+
}): 'image' | 'video' | 'audio' | 'document' {
|
|
124
|
+
const msgType = String(args.msgType || '')
|
|
125
|
+
.trim()
|
|
126
|
+
.toLowerCase();
|
|
127
|
+
const contentType = String(args.mediaContentType || '')
|
|
128
|
+
.trim()
|
|
129
|
+
.toLowerCase();
|
|
130
|
+
if (msgType === 'image' || contentType.startsWith('image/')) return 'image';
|
|
131
|
+
if (msgType === 'video' || contentType.startsWith('video/')) return 'video';
|
|
132
|
+
if (msgType === 'audio' || msgType === 'voice' || contentType.startsWith('audio/'))
|
|
133
|
+
return 'audio';
|
|
134
|
+
return 'document';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function isBncrGenericMediaText(value: string): boolean {
|
|
138
|
+
const normalized = String(value || '').trim();
|
|
139
|
+
if (!normalized) return true;
|
|
140
|
+
return BNCR_GENERIC_MEDIA_TEXTS.has(normalized);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function resolveBncrInboundRawBody(args: {
|
|
144
|
+
extractedText?: string;
|
|
145
|
+
msgType?: string;
|
|
146
|
+
mediaItems?: Array<{ contentType?: string; kind?: string }>;
|
|
147
|
+
}) {
|
|
148
|
+
const text = String(args.extractedText || '').trim();
|
|
149
|
+
if (
|
|
150
|
+
String(args.msgType || 'text')
|
|
151
|
+
.trim()
|
|
152
|
+
.toLowerCase() === 'text'
|
|
153
|
+
)
|
|
154
|
+
return text;
|
|
155
|
+
if (text && !isBncrGenericMediaText(text)) return text;
|
|
156
|
+
const items = Array.isArray(args.mediaItems) ? args.mediaItems : [];
|
|
157
|
+
if (items.length === 0) {
|
|
158
|
+
const mediaKind = inferBncrInboundMediaKind({
|
|
159
|
+
msgType: args.msgType,
|
|
160
|
+
mediaContentType: undefined,
|
|
161
|
+
});
|
|
162
|
+
return `<media:${mediaKind}>`;
|
|
163
|
+
}
|
|
164
|
+
const kinds = items
|
|
165
|
+
.map((item) =>
|
|
166
|
+
String(item?.kind || '')
|
|
167
|
+
.trim()
|
|
168
|
+
.toLowerCase(),
|
|
169
|
+
)
|
|
170
|
+
.filter(Boolean);
|
|
171
|
+
const contentTypes = items.map((item) => String(item?.contentType || '').trim()).filter(Boolean);
|
|
172
|
+
const firstKind =
|
|
173
|
+
kinds[0] ||
|
|
174
|
+
inferBncrInboundMediaKind({
|
|
175
|
+
msgType: args.msgType,
|
|
176
|
+
mediaContentType: contentTypes[0],
|
|
177
|
+
});
|
|
178
|
+
const uniformKind =
|
|
179
|
+
kinds.length > 0 && kinds.every((candidate) => candidate === firstKind)
|
|
180
|
+
? firstKind
|
|
181
|
+
: 'document';
|
|
182
|
+
const count = Math.max(items.length, 1);
|
|
183
|
+
if (count <= 1) return `<media:${uniformKind}>`;
|
|
184
|
+
if (uniformKind === 'image') return `<media:image> (${count} images)`;
|
|
185
|
+
if (uniformKind === 'video') return `<media:video> (${count} videos)`;
|
|
186
|
+
if (uniformKind === 'audio') return `<media:audio> (${count} audio attachments)`;
|
|
187
|
+
return `<media:document> (${count} attachments)`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function saveBncrInboundMediaItem(args: {
|
|
191
|
+
api: BncrInboundApi;
|
|
192
|
+
item: ParsedInbound['mediaItems'][number];
|
|
193
|
+
}) {
|
|
194
|
+
const pathFromTransfer = String(args.item?.path || '').trim();
|
|
195
|
+
const base64 = String(args.item?.base64 || '').trim();
|
|
196
|
+
const mimeType = String(args.item?.mimeType || '').trim() || undefined;
|
|
197
|
+
const fileName = String(args.item?.fileName || '').trim() || undefined;
|
|
198
|
+
const transferId = String(args.item?.transferId || '').trim() || undefined;
|
|
199
|
+
let mediaPath: string | undefined;
|
|
200
|
+
let mediaContentType = mimeType;
|
|
201
|
+
if (base64) {
|
|
202
|
+
const mediaBuf = decodeInboundMediaBase64(base64);
|
|
203
|
+
const saved = await saveOpenClawChannelMediaBuffer(
|
|
204
|
+
args.api,
|
|
205
|
+
mediaBuf,
|
|
206
|
+
mimeType,
|
|
207
|
+
'inbound',
|
|
208
|
+
30 * 1024 * 1024,
|
|
209
|
+
fileName,
|
|
210
|
+
);
|
|
211
|
+
mediaPath = saved.path;
|
|
212
|
+
} else if (pathFromTransfer && isHttpMediaUrl(pathFromTransfer)) {
|
|
213
|
+
const downloaded = await loadInboundRemoteMedia(
|
|
214
|
+
args.api,
|
|
215
|
+
pathFromTransfer,
|
|
216
|
+
INBOUND_MEDIA_URL_MAX_BYTES,
|
|
217
|
+
);
|
|
218
|
+
mediaContentType = downloaded.contentType || mimeType;
|
|
219
|
+
const saved = await saveOpenClawChannelMediaBuffer(
|
|
220
|
+
args.api,
|
|
221
|
+
downloaded.buffer,
|
|
222
|
+
mediaContentType,
|
|
223
|
+
'inbound',
|
|
224
|
+
INBOUND_MEDIA_URL_MAX_BYTES,
|
|
225
|
+
fileName,
|
|
226
|
+
);
|
|
227
|
+
mediaPath = saved.path;
|
|
228
|
+
} else if (pathFromTransfer && fs.existsSync(pathFromTransfer)) {
|
|
229
|
+
mediaPath = pathFromTransfer;
|
|
230
|
+
}
|
|
231
|
+
if (!mediaPath) return null;
|
|
232
|
+
const kind = inferBncrInboundMediaKind({
|
|
233
|
+
msgType: String(args.item?.type || '').trim(),
|
|
234
|
+
mediaContentType: mediaContentType || mimeType,
|
|
235
|
+
});
|
|
236
|
+
return {
|
|
237
|
+
path: mediaPath,
|
|
238
|
+
...(mediaContentType ? { contentType: mediaContentType } : {}),
|
|
239
|
+
...(fileName ? { fileName } : {}),
|
|
240
|
+
kind,
|
|
241
|
+
...(transferId ? { transferId } : {}),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
93
245
|
export function resolveBncrInboundConversation(args: {
|
|
94
246
|
api: BncrInboundApi;
|
|
95
247
|
cfg: BncrInboundConfig;
|
|
96
248
|
channelId: string;
|
|
97
249
|
parsed: ParsedInbound;
|
|
98
250
|
canonicalAgentId: string;
|
|
251
|
+
resolvedAgentId?: string;
|
|
99
252
|
}) {
|
|
100
|
-
const { api, cfg, channelId, parsed, canonicalAgentId } = args;
|
|
253
|
+
const { api, cfg, channelId, parsed, canonicalAgentId, resolvedAgentId } = args;
|
|
101
254
|
const { accountId, route, peer, sessionKeyfromroute, providedOriginatingTo, extracted } = parsed;
|
|
102
255
|
|
|
103
256
|
const resolvedRoute = assertResolvedAgentRoute(
|
|
@@ -109,8 +262,9 @@ export function resolveBncrInboundConversation(args: {
|
|
|
109
262
|
}),
|
|
110
263
|
);
|
|
111
264
|
|
|
265
|
+
const targetAgentId = (resolvedAgentId || '').trim() || resolvedRoute.agentId || canonicalAgentId;
|
|
112
266
|
const baseSessionKey =
|
|
113
|
-
normalizeInboundSessionKey(sessionKeyfromroute, route,
|
|
267
|
+
normalizeInboundSessionKey(sessionKeyfromroute, route, targetAgentId) ||
|
|
114
268
|
resolvedRoute.sessionKey;
|
|
115
269
|
const taskSessionKey = withTaskSessionKey(baseSessionKey, extracted.taskKey);
|
|
116
270
|
const dispatchSessionKey = taskSessionKey || baseSessionKey;
|
|
@@ -122,7 +276,12 @@ export function resolveBncrInboundConversation(args: {
|
|
|
122
276
|
accountId,
|
|
123
277
|
chatType: peer.kind,
|
|
124
278
|
route,
|
|
125
|
-
resolvedRoute,
|
|
279
|
+
originalResolvedRouteSessionKey: resolvedRoute.sessionKey,
|
|
280
|
+
resolvedRoute: {
|
|
281
|
+
...resolvedRoute,
|
|
282
|
+
agentId: targetAgentId || resolvedRoute.agentId,
|
|
283
|
+
sessionKey: baseSessionKey || resolvedRoute.sessionKey,
|
|
284
|
+
},
|
|
126
285
|
canonicalTo,
|
|
127
286
|
rawTo,
|
|
128
287
|
originatingTo,
|
|
@@ -198,40 +357,38 @@ export async function prepareBncrInboundSessionContext(args: {
|
|
|
198
357
|
agentId: resolvedRoute.agentId,
|
|
199
358
|
});
|
|
200
359
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
fileName,
|
|
228
|
-
);
|
|
229
|
-
mediaPath = saved.path;
|
|
230
|
-
} else if (mediaPathFromTransfer && fs.existsSync(mediaPathFromTransfer)) {
|
|
231
|
-
mediaPath = mediaPathFromTransfer;
|
|
232
|
-
}
|
|
360
|
+
const parsedMediaItems = Array.isArray(parsed.mediaItems) ? parsed.mediaItems : [];
|
|
361
|
+
const fallbackMediaItems =
|
|
362
|
+
parsedMediaItems.length > 0
|
|
363
|
+
? parsedMediaItems
|
|
364
|
+
: [
|
|
365
|
+
{
|
|
366
|
+
...(mediaPathFromTransfer ? { path: mediaPathFromTransfer } : {}),
|
|
367
|
+
...(mediaBase64 ? { base64: mediaBase64 } : {}),
|
|
368
|
+
...(mimeType ? { mimeType } : {}),
|
|
369
|
+
...(fileName ? { fileName } : {}),
|
|
370
|
+
...(msgType ? { type: msgType } : {}),
|
|
371
|
+
},
|
|
372
|
+
];
|
|
373
|
+
const mediaItems = (
|
|
374
|
+
await Promise.all(
|
|
375
|
+
fallbackMediaItems.map((item) =>
|
|
376
|
+
saveBncrInboundMediaItem({
|
|
377
|
+
api,
|
|
378
|
+
item,
|
|
379
|
+
}),
|
|
380
|
+
),
|
|
381
|
+
)
|
|
382
|
+
).filter((item): item is NonNullable<typeof item> => Boolean(item));
|
|
383
|
+
const primaryMedia = mediaItems[0];
|
|
384
|
+
const mediaPath = primaryMedia?.path;
|
|
385
|
+
const mediaContentType = primaryMedia?.contentType || mimeType;
|
|
233
386
|
|
|
234
|
-
const rawBody =
|
|
387
|
+
const rawBody = resolveBncrInboundRawBody({
|
|
388
|
+
extractedText: extracted.text,
|
|
389
|
+
msgType,
|
|
390
|
+
mediaItems,
|
|
391
|
+
});
|
|
235
392
|
const body = formatOpenClawAgentEnvelope(api, {
|
|
236
393
|
channel: 'Bncr',
|
|
237
394
|
from: `${platform}:${groupId}:${userId}`,
|
|
@@ -246,6 +403,7 @@ export async function prepareBncrInboundSessionContext(args: {
|
|
|
246
403
|
|
|
247
404
|
return {
|
|
248
405
|
storePath,
|
|
406
|
+
mediaItems,
|
|
249
407
|
mediaPath,
|
|
250
408
|
mediaContentType,
|
|
251
409
|
rawBody,
|
|
@@ -272,15 +430,18 @@ export async function prepareBncrInboundDispatch(args: {
|
|
|
272
430
|
cfg: BncrInboundConfig;
|
|
273
431
|
parsed: ParsedInbound;
|
|
274
432
|
canonicalAgentId: string;
|
|
433
|
+
resolvedAgentId?: string;
|
|
275
434
|
rememberSessionRoute: BncrRememberSessionRoute;
|
|
276
435
|
}) {
|
|
277
|
-
const { api, channelId, cfg, parsed, canonicalAgentId, rememberSessionRoute } =
|
|
436
|
+
const { api, channelId, cfg, parsed, canonicalAgentId, resolvedAgentId, rememberSessionRoute } =
|
|
437
|
+
args;
|
|
278
438
|
const resolution = resolveBncrInboundConversation({
|
|
279
439
|
api,
|
|
280
440
|
cfg,
|
|
281
441
|
channelId,
|
|
282
442
|
parsed,
|
|
283
443
|
canonicalAgentId,
|
|
444
|
+
resolvedAgentId,
|
|
284
445
|
});
|
|
285
446
|
const prepared = await prepareBncrInboundSessionContext({
|
|
286
447
|
api,
|
|
@@ -290,8 +451,19 @@ export async function prepareBncrInboundDispatch(args: {
|
|
|
290
451
|
rememberSessionRoute,
|
|
291
452
|
});
|
|
292
453
|
const replyRouteFact = buildBncrInboundReplyRouteFact(resolution);
|
|
293
|
-
const senderIdForContext = parsed.clientId || resolution.canonicalTo;
|
|
294
|
-
const senderDisplayName = parsed.
|
|
454
|
+
const senderIdForContext = parsed.userId || parsed.clientId || resolution.canonicalTo;
|
|
455
|
+
const senderDisplayName = parsed.userName || resolution.canonicalTo;
|
|
456
|
+
const isBncrNativeCommand =
|
|
457
|
+
parseBncrNativeCommand(parsed.extracted.text, {
|
|
458
|
+
allowBareWhoami: parsed.isAdmin !== true,
|
|
459
|
+
}) !== null;
|
|
460
|
+
const isOpenClawNativeCommand =
|
|
461
|
+
!isBncrNativeCommand &&
|
|
462
|
+
hasControlCommand(parsed.extracted.text, cfg as Parameters<typeof hasControlCommand>[1]);
|
|
463
|
+
const ownerAllowFrom =
|
|
464
|
+
parsed.isAdmin === true && isOpenClawNativeCommand && senderIdForContext
|
|
465
|
+
? [senderIdForContext]
|
|
466
|
+
: undefined;
|
|
295
467
|
|
|
296
468
|
return {
|
|
297
469
|
resolution,
|
|
@@ -299,5 +471,8 @@ export async function prepareBncrInboundDispatch(args: {
|
|
|
299
471
|
replyRouteFact,
|
|
300
472
|
senderIdForContext,
|
|
301
473
|
senderDisplayName,
|
|
474
|
+
...(ownerAllowFrom ? { ownerAllowFrom } : {}),
|
|
475
|
+
bridgeSenderId: parsed.bridgeId || parsed.clientId,
|
|
476
|
+
bridgeSenderName: parsed.bridgeName || 'Bncr',
|
|
302
477
|
} satisfies BncrInboundPreparation;
|
|
303
478
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { emitBncrLogLine } from '../../core/logging.ts';
|
|
2
|
+
import type { BncrSceneRecord } from '../../plugin/channel-runtime-types.ts';
|
|
2
3
|
import { handleBncrNativeCommand } from './commands.ts';
|
|
3
4
|
import type {
|
|
4
5
|
BncrEnqueueFromReply,
|
|
@@ -15,6 +16,12 @@ import {
|
|
|
15
16
|
prepareBncrInboundDispatch,
|
|
16
17
|
resolveBncrInboundConversation,
|
|
17
18
|
} from './dispatch-prep.ts';
|
|
19
|
+
import {
|
|
20
|
+
type BncrGroupHistoryMap,
|
|
21
|
+
clearBncrPendingGroupHistory,
|
|
22
|
+
recordBncrPendingGroupMedia,
|
|
23
|
+
recordBncrPendingGroupText,
|
|
24
|
+
} from './group-history.ts';
|
|
18
25
|
import { runBncrInboundReplyDispatch } from './reply-dispatch.ts';
|
|
19
26
|
import { buildBncrInboundTurnContext } from './turn-context.ts';
|
|
20
27
|
|
|
@@ -31,6 +38,14 @@ export async function dispatchBncrInbound(params: {
|
|
|
31
38
|
cfg: BncrInboundConfig;
|
|
32
39
|
parsed: ParsedInbound;
|
|
33
40
|
canonicalAgentId: string;
|
|
41
|
+
shouldDispatch?: boolean;
|
|
42
|
+
shouldAccumulate?: boolean;
|
|
43
|
+
resolvedAgentId?: string;
|
|
44
|
+
sceneRegistry: Map<string, BncrSceneRecord>;
|
|
45
|
+
groupHistories: BncrGroupHistoryMap;
|
|
46
|
+
defaultAdminAgentId: string;
|
|
47
|
+
defaultPublicAgentId: string;
|
|
48
|
+
now: () => number;
|
|
34
49
|
rememberSessionRoute: BncrRememberSessionRoute;
|
|
35
50
|
enqueueFromReply: BncrEnqueueFromReply;
|
|
36
51
|
setInboundActivity: (accountId: string, at: number) => void;
|
|
@@ -43,6 +58,14 @@ export async function dispatchBncrInbound(params: {
|
|
|
43
58
|
cfg,
|
|
44
59
|
parsed,
|
|
45
60
|
canonicalAgentId,
|
|
61
|
+
shouldDispatch = true,
|
|
62
|
+
shouldAccumulate = shouldDispatch,
|
|
63
|
+
resolvedAgentId,
|
|
64
|
+
sceneRegistry,
|
|
65
|
+
groupHistories = new Map(),
|
|
66
|
+
defaultAdminAgentId,
|
|
67
|
+
defaultPublicAgentId,
|
|
68
|
+
now,
|
|
46
69
|
rememberSessionRoute,
|
|
47
70
|
enqueueFromReply,
|
|
48
71
|
setInboundActivity,
|
|
@@ -57,6 +80,11 @@ export async function dispatchBncrInbound(params: {
|
|
|
57
80
|
cfg,
|
|
58
81
|
parsed,
|
|
59
82
|
canonicalAgentId,
|
|
83
|
+
resolvedAgentId,
|
|
84
|
+
sceneRegistry,
|
|
85
|
+
defaultAdminAgentId,
|
|
86
|
+
defaultPublicAgentId,
|
|
87
|
+
now,
|
|
60
88
|
rememberSessionRoute,
|
|
61
89
|
enqueueFromReply,
|
|
62
90
|
logger,
|
|
@@ -79,12 +107,23 @@ export async function dispatchBncrInbound(params: {
|
|
|
79
107
|
cfg,
|
|
80
108
|
parsed,
|
|
81
109
|
canonicalAgentId,
|
|
110
|
+
resolvedAgentId,
|
|
82
111
|
rememberSessionRoute,
|
|
83
112
|
});
|
|
84
|
-
const {
|
|
85
|
-
|
|
113
|
+
const {
|
|
114
|
+
resolution,
|
|
115
|
+
prepared,
|
|
116
|
+
replyRouteFact,
|
|
117
|
+
senderIdForContext,
|
|
118
|
+
senderDisplayName,
|
|
119
|
+
ownerAllowFrom,
|
|
120
|
+
bridgeSenderId,
|
|
121
|
+
bridgeSenderName,
|
|
122
|
+
} = preparedDispatch;
|
|
86
123
|
const { dispatchSessionKey: sessionKey } = resolution;
|
|
87
|
-
const { storePath,
|
|
124
|
+
const { storePath, mediaItems, rawBody } = prepared;
|
|
125
|
+
const primaryMedia = mediaItems[0];
|
|
126
|
+
const mediaContentType = primaryMedia?.contentType;
|
|
88
127
|
if (!clientId) {
|
|
89
128
|
emitBncrLogLine(
|
|
90
129
|
'warn',
|
|
@@ -93,18 +132,39 @@ export async function dispatchBncrInbound(params: {
|
|
|
93
132
|
}
|
|
94
133
|
const ctxPayload = await buildBncrInboundTurnContext({
|
|
95
134
|
api,
|
|
135
|
+
cfg,
|
|
96
136
|
channelId,
|
|
97
137
|
parsed,
|
|
98
138
|
msgId,
|
|
99
|
-
mimeType: mediaContentType || mimeType,
|
|
100
|
-
mediaPath,
|
|
101
139
|
peer,
|
|
102
140
|
senderIdForContext,
|
|
103
141
|
senderDisplayName,
|
|
142
|
+
ownerAllowFrom,
|
|
143
|
+
bridgeSenderId,
|
|
144
|
+
bridgeSenderName,
|
|
104
145
|
resolution,
|
|
105
146
|
prepared,
|
|
147
|
+
groupHistories,
|
|
148
|
+
shouldDispatch,
|
|
106
149
|
});
|
|
107
150
|
|
|
151
|
+
if (!shouldDispatch && shouldAccumulate) {
|
|
152
|
+
recordBncrPendingGroupText({
|
|
153
|
+
historyMap: groupHistories,
|
|
154
|
+
parsed,
|
|
155
|
+
senderDisplayName,
|
|
156
|
+
bodyText: rawBody,
|
|
157
|
+
});
|
|
158
|
+
await recordBncrPendingGroupMedia({
|
|
159
|
+
historyMap: groupHistories,
|
|
160
|
+
parsed,
|
|
161
|
+
senderDisplayName,
|
|
162
|
+
bodyText: rawBody,
|
|
163
|
+
mediaItems,
|
|
164
|
+
mediaContentType: mediaContentType || mimeType,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
108
168
|
await runBncrInboundReplyDispatch({
|
|
109
169
|
api,
|
|
110
170
|
channelId,
|
|
@@ -118,11 +178,17 @@ export async function dispatchBncrInbound(params: {
|
|
|
118
178
|
resolution,
|
|
119
179
|
replyRouteFact,
|
|
120
180
|
senderIdForContext,
|
|
181
|
+
senderDisplayName,
|
|
182
|
+
shouldDispatch,
|
|
121
183
|
setInboundActivity,
|
|
122
184
|
scheduleSave,
|
|
123
185
|
enqueueFromReply,
|
|
124
186
|
});
|
|
125
187
|
|
|
188
|
+
if (shouldDispatch) {
|
|
189
|
+
clearBncrPendingGroupHistory({ historyMap: groupHistories, parsed });
|
|
190
|
+
}
|
|
191
|
+
|
|
126
192
|
return {
|
|
127
193
|
accountId,
|
|
128
194
|
sessionKey,
|
|
@@ -1,59 +1,46 @@
|
|
|
1
1
|
import { normalizeAccountId } from '../../core/accounts.ts';
|
|
2
|
-
import { resolveBncrChannelPolicy } from '../../core/policy.ts';
|
|
3
|
-
import { buildDisplayScopeCandidates } from '../../core/targets.ts';
|
|
4
|
-
import type { BncrRoute } from '../../core/types.ts';
|
|
5
|
-
import {
|
|
6
|
-
defineOpenClawStableChannelIngressIdentity,
|
|
7
|
-
resolveOpenClawChannelMessageIngress,
|
|
8
|
-
} from '../../openclaw/ingress-runtime.ts';
|
|
9
2
|
import type { BncrInboundConfig, BncrInboundParamsInput } from './contracts.ts';
|
|
10
3
|
|
|
11
|
-
type RouteLike = Partial<Pick<BncrRoute, 'groupId' | 'userId' | 'platform'>>;
|
|
12
|
-
type AccessGroupsLike = Parameters<typeof resolveOpenClawChannelMessageIngress>[0]['accessGroups'];
|
|
13
|
-
|
|
14
4
|
export type BncrGateResult = { allowed: true } | { allowed: false; reason: string };
|
|
15
5
|
|
|
6
|
+
const REQUIRED_PROTOCOL_VERSION = 'scene-routing-v1';
|
|
7
|
+
const REQUIRED_CAPABILITY = 'scene-routing-v1';
|
|
8
|
+
|
|
16
9
|
function asString(v: unknown, fallback = ''): string {
|
|
17
10
|
if (typeof v === 'string') return v;
|
|
18
11
|
if (v == null) return fallback;
|
|
19
12
|
return String(v);
|
|
20
13
|
}
|
|
21
14
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
sensitivity: 'pii',
|
|
34
|
-
},
|
|
35
|
-
],
|
|
36
|
-
});
|
|
15
|
+
function asBoolean(v: unknown, fallback = false): boolean {
|
|
16
|
+
if (typeof v === 'boolean') return v;
|
|
17
|
+
if (typeof v === 'number') return v !== 0;
|
|
18
|
+
if (typeof v === 'string') {
|
|
19
|
+
const raw = v.trim().toLowerCase();
|
|
20
|
+
if (!raw) return fallback;
|
|
21
|
+
if (['true', '1', 'yes', 'y', 'on'].includes(raw)) return true;
|
|
22
|
+
if (['false', '0', 'no', 'n', 'off'].includes(raw)) return false;
|
|
23
|
+
}
|
|
24
|
+
return fallback;
|
|
25
|
+
}
|
|
37
26
|
|
|
38
|
-
function
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
case 'group_policy_not_allowlisted':
|
|
48
|
-
case 'group_policy_empty_allowlist':
|
|
49
|
-
return 'group allowlist blocked';
|
|
50
|
-
default:
|
|
51
|
-
return reasonCode || 'ingress blocked';
|
|
27
|
+
function asCapabilities(v: unknown): string[] {
|
|
28
|
+
if (Array.isArray(v)) {
|
|
29
|
+
return v.map((item) => asString(item).trim()).filter(Boolean);
|
|
30
|
+
}
|
|
31
|
+
if (typeof v === 'string') {
|
|
32
|
+
return v
|
|
33
|
+
.split(',')
|
|
34
|
+
.map((item) => item.trim())
|
|
35
|
+
.filter(Boolean);
|
|
52
36
|
}
|
|
37
|
+
return [];
|
|
53
38
|
}
|
|
54
39
|
|
|
55
40
|
export async function checkBncrMessageGate(params: {
|
|
56
|
-
parsed: BncrInboundParamsInput & {
|
|
41
|
+
parsed: BncrInboundParamsInput & {
|
|
42
|
+
route?: Partial<{ groupId: string; userId: string; platform: string }>;
|
|
43
|
+
};
|
|
57
44
|
cfg: BncrInboundConfig;
|
|
58
45
|
account: { accountId: string; enabled?: boolean };
|
|
59
46
|
}): Promise<BncrGateResult> {
|
|
@@ -61,57 +48,40 @@ export async function checkBncrMessageGate(params: {
|
|
|
61
48
|
const accountId = normalizeAccountId(account?.accountId);
|
|
62
49
|
const channelCfg = cfg?.channels?.bncr || {};
|
|
63
50
|
const accountCfg = channelCfg?.accounts?.[accountId] || {};
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
51
|
+
if (
|
|
52
|
+
channelCfg?.enabled === false ||
|
|
53
|
+
account?.enabled === false ||
|
|
54
|
+
accountCfg?.enabled === false
|
|
55
|
+
) {
|
|
67
56
|
return { allowed: false, reason: 'account disabled' };
|
|
68
57
|
}
|
|
69
58
|
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
59
|
+
const protocolVersion = asString(parsed?.protocolVersion || '').trim();
|
|
60
|
+
const capabilities = asCapabilities(parsed?.capabilities);
|
|
61
|
+
if (
|
|
62
|
+
protocolVersion !== REQUIRED_PROTOCOL_VERSION ||
|
|
63
|
+
!capabilities.includes(REQUIRED_CAPABILITY)
|
|
64
|
+
) {
|
|
65
|
+
return { allowed: false, reason: 'client protocol outdated' };
|
|
73
66
|
}
|
|
74
|
-
const resolvedRoute: BncrRoute = {
|
|
75
|
-
platform: route.platform,
|
|
76
|
-
groupId: route.groupId,
|
|
77
|
-
userId: route.userId,
|
|
78
|
-
};
|
|
79
|
-
const isGroup = asString(route?.groupId || '0') !== '0';
|
|
80
|
-
|
|
81
|
-
const candidates = buildDisplayScopeCandidates(resolvedRoute);
|
|
82
|
-
const displayScope = candidates[0] || '';
|
|
83
|
-
const routeKey = candidates.find((candidate) => candidate !== displayScope) || displayScope;
|
|
84
67
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
conversation: {
|
|
98
|
-
kind: isGroup ? 'group' : 'direct',
|
|
99
|
-
id: isGroup ? asString(route?.groupId) : asString(route?.userId || displayScope),
|
|
100
|
-
},
|
|
101
|
-
event: { kind: 'message', authMode: 'inbound', mayPair: !isGroup },
|
|
102
|
-
policy: {
|
|
103
|
-
dmPolicy: policy.dmPolicy,
|
|
104
|
-
groupPolicy: policy.groupPolicy,
|
|
105
|
-
groupAllowFromFallbackToAllowFrom: false,
|
|
106
|
-
},
|
|
107
|
-
allowFrom: policy.dmPolicy === 'open' ? ['*', ...policy.allowFrom] : policy.allowFrom,
|
|
108
|
-
groupAllowFrom: policy.groupAllowFrom,
|
|
109
|
-
accessGroups: cfg?.accessGroups as AccessGroupsLike,
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
if (resolved.ingress.admission === 'dispatch' || resolved.ingress.admission === 'observe') {
|
|
113
|
-
return { allowed: true };
|
|
68
|
+
const platform = asString(parsed?.platform || '').trim();
|
|
69
|
+
const userId = asString(parsed?.userId || '').trim();
|
|
70
|
+
const clientId = asString(parsed?.clientId || '').trim();
|
|
71
|
+
const groupId = asString(parsed?.groupId || '').trim();
|
|
72
|
+
const isGroupProvided = parsed && Object.hasOwn(parsed, 'isGroup');
|
|
73
|
+
const isAdminProvided = parsed && Object.hasOwn(parsed, 'isAdmin');
|
|
74
|
+
const isGroup = asBoolean(parsed?.isGroup, false);
|
|
75
|
+
if (!platform || !userId || !clientId || !isGroupProvided || !isAdminProvided) {
|
|
76
|
+
return { allowed: false, reason: 'inbound schema incomplete' };
|
|
77
|
+
}
|
|
78
|
+
if (isGroup && !groupId) {
|
|
79
|
+
return { allowed: false, reason: 'inbound schema incomplete' };
|
|
114
80
|
}
|
|
115
81
|
|
|
116
|
-
|
|
82
|
+
const route = parsed?.route;
|
|
83
|
+
if (!route?.platform || !route?.groupId || !route?.userId) {
|
|
84
|
+
return { allowed: false, reason: 'invalid route' };
|
|
85
|
+
}
|
|
86
|
+
return { allowed: true };
|
|
117
87
|
}
|