@xmoxmo/bncr 0.2.6 → 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/index.ts +30 -15
- package/package.json +4 -3
- package/scripts/check-pack.mjs +77 -0
- package/scripts/selfcheck.mjs +10 -0
- package/src/channel.ts +398 -642
- package/src/core/extended-diagnostics.ts +10 -0
- package/src/core/file-ack.ts +9 -0
- package/src/core/file-transfer-payloads.ts +72 -0
- package/src/core/register-trace.ts +79 -0
- package/src/core/targets.ts +10 -1
- package/src/messaging/inbound/commands.ts +20 -10
- package/src/messaging/inbound/context-facts.ts +200 -0
- package/src/messaging/inbound/dispatch.ts +66 -14
- package/src/messaging/inbound/gate.ts +66 -26
- package/src/messaging/inbound/runtime-compat.ts +41 -0
- package/src/messaging/inbound/session-label.ts +7 -7
- package/src/messaging/outbound/durable-message-adapter.ts +107 -0
- package/src/messaging/outbound/durable-queue-adapter.ts +157 -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
- package/src/plugin/capabilities.ts +8 -0
- package/src/plugin/config.ts +35 -0
- package/src/plugin/gateway-methods.ts +12 -0
- package/src/plugin/gateway-runtime.ts +11 -0
- package/src/plugin/message-policy.ts +4 -0
- package/src/plugin/message-send.ts +13 -0
- package/src/plugin/messaging.ts +142 -0
- package/src/plugin/meta.ts +10 -0
- package/src/plugin/outbound.ts +51 -0
- package/src/plugin/setup.ts +24 -0
- package/src/plugin/status.ts +38 -0
- package/src/runtime/log-dedupe.ts +56 -0
- package/src/runtime/outbound-ack-timeout.ts +96 -0
- package/src/runtime/outbound-flags.ts +81 -0
- package/src/runtime/outbox-transitions.ts +119 -0
- package/src/runtime/status-snapshots.ts +108 -0
- package/src/runtime/status-worker.ts +172 -0
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import { resolvePinnedMainDmOwnerFromAllowlist } from 'openclaw/plugin-sdk/conversation-runtime';
|
|
3
|
-
import { resolveInboundLastRouteSessionKey } from 'openclaw/plugin-sdk/routing';
|
|
4
2
|
import { emitBncrLogLine } from '../../core/logging.ts';
|
|
5
3
|
import { resolveBncrChannelPolicy } from '../../core/policy.ts';
|
|
6
4
|
import {
|
|
@@ -9,8 +7,29 @@ import {
|
|
|
9
7
|
withTaskSessionKey,
|
|
10
8
|
} from '../../core/targets.ts';
|
|
11
9
|
import { handleBncrNativeCommand } from './commands.ts';
|
|
10
|
+
import {
|
|
11
|
+
buildBncrPromptVisibleContextFacts,
|
|
12
|
+
buildBncrStructuredContextFactsFromInboundParts,
|
|
13
|
+
} from './context-facts.ts';
|
|
12
14
|
import { buildBncrReplyConfig } from './reply-config.ts';
|
|
15
|
+
import { resolveBncrChannelInboundRuntime } from './runtime-compat.ts';
|
|
13
16
|
import { wrapBncrInboundRecordSessionLabelCorrection } from './session-label.ts';
|
|
17
|
+
import { saveOpenClawChannelMediaBuffer } from '../../openclaw/media-runtime.ts';
|
|
18
|
+
import {
|
|
19
|
+
dispatchOpenClawReplyWithBufferedBlockDispatcher,
|
|
20
|
+
formatOpenClawAgentEnvelope,
|
|
21
|
+
resolveOpenClawEnvelopeFormatOptions,
|
|
22
|
+
} from '../../openclaw/reply-runtime.ts';
|
|
23
|
+
import {
|
|
24
|
+
resolveOpenClawAgentRoute,
|
|
25
|
+
resolveOpenClawInboundLastRouteSessionKey,
|
|
26
|
+
} from '../../openclaw/routing-runtime.ts';
|
|
27
|
+
import {
|
|
28
|
+
readBncrSessionUpdatedAt,
|
|
29
|
+
recordBncrInboundSession,
|
|
30
|
+
resolveBncrInboundSessionStorePath,
|
|
31
|
+
resolveBncrPinnedMainDmOwnerFromAllowlist,
|
|
32
|
+
} from '../../openclaw/inbound-session-runtime.ts';
|
|
14
33
|
|
|
15
34
|
type ParsedInbound = ReturnType<typeof import('./parse.ts')['parseBncrInboundParams']>;
|
|
16
35
|
|
|
@@ -93,7 +112,7 @@ export function resolveBncrInboundConversation(args: {
|
|
|
93
112
|
const { api, cfg, channelId, parsed, canonicalAgentId } = args;
|
|
94
113
|
const { accountId, route, peer, sessionKeyfromroute, providedOriginatingTo, extracted } = parsed;
|
|
95
114
|
|
|
96
|
-
const resolvedRoute = api
|
|
115
|
+
const resolvedRoute = resolveOpenClawAgentRoute(api, {
|
|
97
116
|
cfg,
|
|
98
117
|
channel: channelId,
|
|
99
118
|
accountId,
|
|
@@ -149,14 +168,16 @@ async function prepareBncrInboundSessionContext(args: {
|
|
|
149
168
|
rememberSessionRoute(taskSessionKey, accountId, route);
|
|
150
169
|
}
|
|
151
170
|
|
|
152
|
-
const storePath =
|
|
171
|
+
const storePath = resolveBncrInboundSessionStorePath({
|
|
172
|
+
storeConfig: cfg?.session?.store,
|
|
153
173
|
agentId: resolvedRoute.agentId,
|
|
154
174
|
});
|
|
155
175
|
|
|
156
176
|
let mediaPath: string | undefined;
|
|
157
177
|
if (mediaBase64) {
|
|
158
178
|
const mediaBuf = decodeInboundMediaBase64(mediaBase64);
|
|
159
|
-
const saved = await
|
|
179
|
+
const saved = await saveOpenClawChannelMediaBuffer(
|
|
180
|
+
api,
|
|
160
181
|
mediaBuf,
|
|
161
182
|
mimeType,
|
|
162
183
|
'inbound',
|
|
@@ -169,15 +190,15 @@ async function prepareBncrInboundSessionContext(args: {
|
|
|
169
190
|
}
|
|
170
191
|
|
|
171
192
|
const rawBody = extracted.text || (msgType === 'text' ? '' : `[${msgType}]`);
|
|
172
|
-
const body = api
|
|
193
|
+
const body = formatOpenClawAgentEnvelope(api, {
|
|
173
194
|
channel: 'Bncr',
|
|
174
195
|
from: `${platform}:${groupId}:${userId}`,
|
|
175
196
|
timestamp: Date.now(),
|
|
176
|
-
previousTimestamp: api
|
|
197
|
+
previousTimestamp: readBncrSessionUpdatedAt(api, {
|
|
177
198
|
storePath,
|
|
178
199
|
sessionKey: dispatchSessionKey,
|
|
179
200
|
}),
|
|
180
|
-
envelope: api
|
|
201
|
+
envelope: resolveOpenClawEnvelopeFormatOptions(api, cfg),
|
|
181
202
|
body: rawBody,
|
|
182
203
|
});
|
|
183
204
|
|
|
@@ -192,6 +213,7 @@ async function prepareBncrInboundSessionContext(args: {
|
|
|
192
213
|
function buildBncrInboundTurnContext(args: {
|
|
193
214
|
api: any;
|
|
194
215
|
channelId: string;
|
|
216
|
+
parsed: ParsedInbound;
|
|
195
217
|
msgId?: string | null;
|
|
196
218
|
mimeType?: string;
|
|
197
219
|
mediaPath?: string;
|
|
@@ -207,6 +229,7 @@ function buildBncrInboundTurnContext(args: {
|
|
|
207
229
|
const {
|
|
208
230
|
api,
|
|
209
231
|
channelId,
|
|
232
|
+
parsed,
|
|
210
233
|
msgId,
|
|
211
234
|
mimeType,
|
|
212
235
|
mediaPath,
|
|
@@ -216,8 +239,31 @@ function buildBncrInboundTurnContext(args: {
|
|
|
216
239
|
resolution,
|
|
217
240
|
prepared,
|
|
218
241
|
} = args;
|
|
242
|
+
const structuredContextFacts = buildBncrStructuredContextFactsFromInboundParts({
|
|
243
|
+
channelId,
|
|
244
|
+
parsed,
|
|
245
|
+
resolution,
|
|
246
|
+
prepared: {
|
|
247
|
+
rawBody: prepared.rawBody,
|
|
248
|
+
body: prepared.body,
|
|
249
|
+
mediaPath,
|
|
250
|
+
},
|
|
251
|
+
senderIdForContext,
|
|
252
|
+
senderDisplayName,
|
|
253
|
+
});
|
|
254
|
+
const promptVisibleContextFacts = buildBncrPromptVisibleContextFacts(structuredContextFacts);
|
|
255
|
+
const supplementalUntrustedContext = Object.keys(promptVisibleContextFacts).length
|
|
256
|
+
? [
|
|
257
|
+
{
|
|
258
|
+
label: 'Bncr inbound context',
|
|
259
|
+
source: channelId,
|
|
260
|
+
type: 'bncr.inbound_context',
|
|
261
|
+
payload: promptVisibleContextFacts,
|
|
262
|
+
},
|
|
263
|
+
]
|
|
264
|
+
: [];
|
|
219
265
|
|
|
220
|
-
return api.
|
|
266
|
+
return resolveBncrChannelInboundRuntime(api).buildContext({
|
|
221
267
|
channel: channelId,
|
|
222
268
|
provider: channelId,
|
|
223
269
|
surface: channelId,
|
|
@@ -275,8 +321,13 @@ function buildBncrInboundTurnContext(args: {
|
|
|
275
321
|
},
|
|
276
322
|
]
|
|
277
323
|
: [],
|
|
324
|
+
supplemental: {
|
|
325
|
+
untrustedContext: supplementalUntrustedContext,
|
|
326
|
+
},
|
|
278
327
|
extra: {
|
|
279
328
|
OriginatingChannel: channelId,
|
|
329
|
+
BncrStructuredContextFacts: structuredContextFacts,
|
|
330
|
+
StructuredContextFacts: structuredContextFacts,
|
|
280
331
|
},
|
|
281
332
|
});
|
|
282
333
|
}
|
|
@@ -291,7 +342,7 @@ function buildBncrInboundRecordUpdateLastRoute(args: {
|
|
|
291
342
|
const { channelId, peer, senderIdForContext, resolution, pinnedMainDmOwner } = args;
|
|
292
343
|
if (peer.kind !== 'direct') return undefined;
|
|
293
344
|
|
|
294
|
-
const sessionKey =
|
|
345
|
+
const sessionKey = resolveOpenClawInboundLastRouteSessionKey({
|
|
295
346
|
route: resolution.resolvedRoute,
|
|
296
347
|
sessionKey: resolution.dispatchSessionKey,
|
|
297
348
|
});
|
|
@@ -406,6 +457,7 @@ export async function dispatchBncrInbound(params: {
|
|
|
406
457
|
const ctxPayload = buildBncrInboundTurnContext({
|
|
407
458
|
api,
|
|
408
459
|
channelId,
|
|
460
|
+
parsed,
|
|
409
461
|
msgId,
|
|
410
462
|
mimeType,
|
|
411
463
|
mediaPath,
|
|
@@ -420,7 +472,7 @@ export async function dispatchBncrInbound(params: {
|
|
|
420
472
|
const channelPolicy = resolveBncrChannelPolicy(cfg?.channels?.bncr || {});
|
|
421
473
|
const pinnedMainDmOwner =
|
|
422
474
|
peer.kind === 'direct'
|
|
423
|
-
?
|
|
475
|
+
? resolveBncrPinnedMainDmOwnerFromAllowlist({
|
|
424
476
|
dmScope: cfg?.session?.dmScope,
|
|
425
477
|
allowFrom: channelPolicy.allowFrom,
|
|
426
478
|
normalizeEntry: (entry: string) => String(entry || '').trim(),
|
|
@@ -434,7 +486,7 @@ export async function dispatchBncrInbound(params: {
|
|
|
434
486
|
pinnedMainDmOwner,
|
|
435
487
|
});
|
|
436
488
|
|
|
437
|
-
await api.
|
|
489
|
+
await resolveBncrChannelInboundRuntime(api).run({
|
|
438
490
|
channel: channelId,
|
|
439
491
|
accountId,
|
|
440
492
|
raw: parsed,
|
|
@@ -454,7 +506,7 @@ export async function dispatchBncrInbound(params: {
|
|
|
454
506
|
storePath,
|
|
455
507
|
ctxPayload,
|
|
456
508
|
recordInboundSession: wrapBncrInboundRecordSessionLabelCorrection({
|
|
457
|
-
recordInboundSession:
|
|
509
|
+
recordInboundSession: recordBncrInboundSession,
|
|
458
510
|
expectedLabel: canonicalTo,
|
|
459
511
|
}),
|
|
460
512
|
record: {
|
|
@@ -464,7 +516,7 @@ export async function dispatchBncrInbound(params: {
|
|
|
464
516
|
},
|
|
465
517
|
},
|
|
466
518
|
runDispatch: () =>
|
|
467
|
-
api
|
|
519
|
+
dispatchOpenClawReplyWithBufferedBlockDispatcher(api, {
|
|
468
520
|
ctx: ctxPayload,
|
|
469
521
|
cfg: effectiveReply.replyCfg,
|
|
470
522
|
dispatcherOptions: {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { normalizeAccountId } from '../../core/accounts.ts';
|
|
2
2
|
import { resolveBncrChannelPolicy } from '../../core/policy.ts';
|
|
3
3
|
import { buildDisplayScopeCandidates } from '../../core/targets.ts';
|
|
4
|
+
import {
|
|
5
|
+
defineOpenClawStableChannelIngressIdentity,
|
|
6
|
+
resolveOpenClawChannelMessageIngress,
|
|
7
|
+
} from '../../openclaw/ingress-runtime.ts';
|
|
4
8
|
|
|
5
9
|
export type BncrGateResult = { allowed: true } | { allowed: false; reason: string };
|
|
6
10
|
|
|
@@ -10,13 +14,40 @@ function asString(v: unknown, fallback = ''): string {
|
|
|
10
14
|
return String(v);
|
|
11
15
|
}
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
const bncrIngressIdentity = defineOpenClawStableChannelIngressIdentity({
|
|
18
|
+
key: 'displayScope',
|
|
19
|
+
kind: 'plugin:bncr-display-scope',
|
|
20
|
+
normalize: (value: string) => asString(value).trim() || null,
|
|
21
|
+
sensitivity: 'pii',
|
|
22
|
+
entryIdPrefix: 'bncr-allow',
|
|
23
|
+
aliases: [
|
|
24
|
+
{
|
|
25
|
+
key: 'routeKey',
|
|
26
|
+
kind: 'plugin:bncr-route-key',
|
|
27
|
+
normalize: (value: string) => asString(value).trim() || null,
|
|
28
|
+
sensitivity: 'pii',
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
function gateReasonFromIngress(reasonCode?: string): string {
|
|
34
|
+
switch (reasonCode) {
|
|
35
|
+
case 'dm_policy_disabled':
|
|
36
|
+
return 'dm disabled';
|
|
37
|
+
case 'dm_policy_not_allowlisted':
|
|
38
|
+
case 'dm_policy_pairing_required':
|
|
39
|
+
return 'dm allowlist blocked';
|
|
40
|
+
case 'group_policy_disabled':
|
|
41
|
+
return 'group disabled';
|
|
42
|
+
case 'group_policy_not_allowlisted':
|
|
43
|
+
case 'group_policy_empty_allowlist':
|
|
44
|
+
return 'group allowlist blocked';
|
|
45
|
+
default:
|
|
46
|
+
return reasonCode || 'ingress blocked';
|
|
47
|
+
}
|
|
17
48
|
}
|
|
18
49
|
|
|
19
|
-
export function checkBncrMessageGate(params: {
|
|
50
|
+
export async function checkBncrMessageGate(params: {
|
|
20
51
|
parsed: any;
|
|
21
52
|
cfg: any;
|
|
22
53
|
account: { accountId: string; enabled?: boolean };
|
|
@@ -34,31 +65,40 @@ export function checkBncrMessageGate(params: {
|
|
|
34
65
|
const route = parsed?.route;
|
|
35
66
|
const isGroup = asString(route?.groupId || '0') !== '0';
|
|
36
67
|
|
|
37
|
-
if (!isGroup && policy.dmPolicy === 'disabled') {
|
|
38
|
-
return { allowed: false, reason: 'dm disabled' };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (isGroup && policy.groupPolicy === 'disabled') {
|
|
42
|
-
return { allowed: false, reason: 'group disabled' };
|
|
43
|
-
}
|
|
44
|
-
|
|
45
68
|
const candidates = buildDisplayScopeCandidates(route);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (!matchesAllowList(policy.allowFrom, candidates)) {
|
|
49
|
-
return { allowed: false, reason: 'dm allowlist blocked' };
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (isGroup && policy.groupPolicy === 'allowlist') {
|
|
54
|
-
if (!matchesAllowList(policy.groupAllowFrom, candidates)) {
|
|
55
|
-
return { allowed: false, reason: 'group allowlist blocked' };
|
|
56
|
-
}
|
|
57
|
-
}
|
|
69
|
+
const displayScope = candidates[0] || '';
|
|
70
|
+
const routeKey = candidates.find((candidate) => candidate !== displayScope) || displayScope;
|
|
58
71
|
|
|
59
72
|
// requireMention 默认值为 false。
|
|
60
73
|
// 设计目标:当它未来真正生效时,含义是“群消息只有在明确提到机器人时才允许进入处理链”。
|
|
61
74
|
// 但当前 parse 层尚未稳定提取 mentions,上游客户端也未统一透传 mention 信号,
|
|
62
75
|
// 因此现阶段即使配置为 true,也仍不做实际拦截,避免出现半实现状态。
|
|
63
|
-
|
|
76
|
+
const resolved = await resolveOpenClawChannelMessageIngress({
|
|
77
|
+
channelId: 'bncr',
|
|
78
|
+
accountId,
|
|
79
|
+
identity: bncrIngressIdentity,
|
|
80
|
+
subject: {
|
|
81
|
+
stableId: displayScope,
|
|
82
|
+
aliases: { routeKey },
|
|
83
|
+
},
|
|
84
|
+
conversation: {
|
|
85
|
+
kind: isGroup ? 'group' : 'direct',
|
|
86
|
+
id: isGroup ? asString(route?.groupId) : asString(route?.userId || displayScope),
|
|
87
|
+
},
|
|
88
|
+
event: { kind: 'message', authMode: 'inbound', mayPair: !isGroup },
|
|
89
|
+
policy: {
|
|
90
|
+
dmPolicy: policy.dmPolicy,
|
|
91
|
+
groupPolicy: policy.groupPolicy,
|
|
92
|
+
groupAllowFromFallbackToAllowFrom: false,
|
|
93
|
+
},
|
|
94
|
+
allowFrom: policy.dmPolicy === 'open' ? ['*', ...policy.allowFrom] : policy.allowFrom,
|
|
95
|
+
groupAllowFrom: policy.groupAllowFrom,
|
|
96
|
+
accessGroups: cfg?.accessGroups,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (resolved.ingress.admission === 'dispatch' || resolved.ingress.admission === 'observe') {
|
|
100
|
+
return { allowed: true };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { allowed: false, reason: gateReasonFromIngress(resolved.ingress.reasonCode) };
|
|
64
104
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { emitBncrLogLine } from '../../core/logging.ts';
|
|
2
|
+
|
|
3
|
+
type ChannelRuntimeCompat = {
|
|
4
|
+
buildContext: (...args: any[]) => any;
|
|
5
|
+
run: (...args: any[]) => Promise<any> | any;
|
|
6
|
+
runPreparedReply?: (...args: any[]) => Promise<any> | any;
|
|
7
|
+
dispatchReply?: (...args: any[]) => Promise<any> | any;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
let warnedLegacyTurnRuntime = false;
|
|
11
|
+
|
|
12
|
+
export function resolveBncrChannelInboundRuntime(api: any): ChannelRuntimeCompat {
|
|
13
|
+
const channelRuntime = api?.runtime?.channel;
|
|
14
|
+
const inboundRuntime = channelRuntime?.inbound;
|
|
15
|
+
if (inboundRuntime?.buildContext && inboundRuntime?.run) {
|
|
16
|
+
return inboundRuntime;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const legacyTurnRuntime = channelRuntime?.turn;
|
|
20
|
+
if (legacyTurnRuntime?.buildContext && legacyTurnRuntime?.run) {
|
|
21
|
+
if (!warnedLegacyTurnRuntime) {
|
|
22
|
+
warnedLegacyTurnRuntime = true;
|
|
23
|
+
const channelRuntimeKeys = Object.keys(channelRuntime ?? {}).sort().join(',') || 'none';
|
|
24
|
+
const inboundRuntimeKeys = Object.keys(inboundRuntime ?? {}).sort().join(',') || 'none';
|
|
25
|
+
emitBncrLogLine(
|
|
26
|
+
'warn',
|
|
27
|
+
`[bncr] inbound runtime fallback=turn|preferred=inbound|channelKeys=${channelRuntimeKeys}|inboundKeys=${inboundRuntimeKeys}`,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
buildContext: legacyTurnRuntime.buildContext,
|
|
32
|
+
run: legacyTurnRuntime.run,
|
|
33
|
+
runPreparedReply: legacyTurnRuntime.runPrepared,
|
|
34
|
+
dispatchReply: legacyTurnRuntime.dispatchAssembled ?? legacyTurnRuntime.runAssembled,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
throw new Error(
|
|
39
|
+
'OpenClaw channel inbound runtime is unavailable: expected runtime.channel.inbound.* or legacy runtime.channel.turn.*',
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
recordSessionMetaFromInbound,
|
|
3
|
-
updateSessionStoreEntry,
|
|
4
|
-
} from 'openclaw/plugin-sdk/session-store-runtime';
|
|
5
1
|
import { emitBncrLogLine } from '../../core/logging.ts';
|
|
2
|
+
import {
|
|
3
|
+
recordBncrSessionMetaFromInbound,
|
|
4
|
+
updateBncrSessionStoreEntry,
|
|
5
|
+
} from '../../openclaw/inbound-session-runtime.ts';
|
|
6
6
|
|
|
7
7
|
type RecordInboundSessionFn = (args: any) => Promise<unknown> | unknown;
|
|
8
8
|
|
|
@@ -57,7 +57,7 @@ export async function correctBncrInboundSessionLabel(args: {
|
|
|
57
57
|
if (!storePath || !sessionKey || !expectedLabel) return;
|
|
58
58
|
|
|
59
59
|
try {
|
|
60
|
-
await
|
|
60
|
+
await updateBncrSessionStoreEntry({
|
|
61
61
|
storePath,
|
|
62
62
|
sessionKey,
|
|
63
63
|
update: (entry: any) => {
|
|
@@ -82,14 +82,14 @@ export async function recordAndPatchBncrInboundSessionEntry(args: {
|
|
|
82
82
|
|
|
83
83
|
try {
|
|
84
84
|
if (args.ctx) {
|
|
85
|
-
await
|
|
85
|
+
await recordBncrSessionMetaFromInbound({
|
|
86
86
|
storePath,
|
|
87
87
|
sessionKey,
|
|
88
88
|
ctx: args.ctx as any,
|
|
89
89
|
createIfMissing: true,
|
|
90
90
|
});
|
|
91
91
|
}
|
|
92
|
-
await
|
|
92
|
+
await updateBncrSessionStoreEntry({
|
|
93
93
|
storePath,
|
|
94
94
|
sessionKey,
|
|
95
95
|
update: () => args.patch,
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { defineChannelMessageAdapter } from 'openclaw/plugin-sdk/channel-outbound';
|
|
2
|
+
import type {
|
|
3
|
+
ChannelMessageAdapterShape,
|
|
4
|
+
ChannelMessageSendMediaContext,
|
|
5
|
+
ChannelMessageSendPayloadContext,
|
|
6
|
+
ChannelMessageSendResult,
|
|
7
|
+
ChannelMessageSendTextContext,
|
|
8
|
+
} from 'openclaw/plugin-sdk/channel-outbound';
|
|
9
|
+
|
|
10
|
+
import { buildFileTransferOutboxEntry, buildTextOutboxEntry } from '../../core/outbox-entry-builders.ts';
|
|
11
|
+
import type { BncrRoute, OutboxEntry } from '../../core/types.ts';
|
|
12
|
+
import { buildBncrDurableQueuedResult } from './durable-queue-adapter.ts';
|
|
13
|
+
|
|
14
|
+
export type BncrDurableMessageQueuedAdapterDeps<TConfig = unknown> = {
|
|
15
|
+
enqueueText: (ctx: ChannelMessageSendTextContext<TConfig>) => Promise<OutboxEntry> | OutboxEntry;
|
|
16
|
+
enqueueMedia?: (ctx: ChannelMessageSendMediaContext<TConfig>) => Promise<OutboxEntry> | OutboxEntry;
|
|
17
|
+
enqueuePayload?: (ctx: ChannelMessageSendPayloadContext<TConfig>) => Promise<OutboxEntry> | OutboxEntry;
|
|
18
|
+
now?: () => number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type BncrDurableMessageQueuedAdapterBuilderDeps<TConfig = unknown> = {
|
|
22
|
+
createMessageId: () => string;
|
|
23
|
+
now: () => number;
|
|
24
|
+
normalizeAccountId: (accountId?: string | null) => string;
|
|
25
|
+
normalizeReplyToId: (value?: string | null) => string;
|
|
26
|
+
resolveTarget: (ctx: ChannelMessageSendTextContext<TConfig>) => {
|
|
27
|
+
route: BncrRoute;
|
|
28
|
+
sessionKey: string;
|
|
29
|
+
accountId?: string | null;
|
|
30
|
+
};
|
|
31
|
+
filePushEvent?: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// This adapter intentionally models only the OpenClaw -> bncr-plugin handoff.
|
|
35
|
+
// Once a message is accepted into bncr's own outbox, OpenClaw should stop managing it;
|
|
36
|
+
// client/platform ACK, retry, and deadLetter remain owned by the bncr service framework.
|
|
37
|
+
export function createBncrDurableMessageQueuedAdapter<TConfig = unknown>(
|
|
38
|
+
deps: BncrDurableMessageQueuedAdapterDeps<TConfig>,
|
|
39
|
+
): ChannelMessageAdapterShape<TConfig, ChannelMessageSendResult> {
|
|
40
|
+
return defineChannelMessageAdapter({
|
|
41
|
+
id: 'bncr-queued-outbox',
|
|
42
|
+
receive: {
|
|
43
|
+
defaultAckPolicy: 'manual',
|
|
44
|
+
supportedAckPolicies: ['manual'],
|
|
45
|
+
},
|
|
46
|
+
send: {
|
|
47
|
+
text: async (ctx) => toChannelMessageSendResult(await deps.enqueueText(ctx), deps.now),
|
|
48
|
+
media: deps.enqueueMedia
|
|
49
|
+
? async (ctx) => toChannelMessageSendResult(await deps.enqueueMedia?.(ctx), deps.now)
|
|
50
|
+
: undefined,
|
|
51
|
+
payload: deps.enqueuePayload
|
|
52
|
+
? async (ctx) => toChannelMessageSendResult(await deps.enqueuePayload?.(ctx), deps.now)
|
|
53
|
+
: undefined,
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function createBncrDurableMessageQueuedAdapterFromBuilders<TConfig = unknown>(
|
|
59
|
+
deps: BncrDurableMessageQueuedAdapterBuilderDeps<TConfig>,
|
|
60
|
+
): ChannelMessageAdapterShape<TConfig, ChannelMessageSendResult> {
|
|
61
|
+
return createBncrDurableMessageQueuedAdapter<TConfig>({
|
|
62
|
+
now: deps.now,
|
|
63
|
+
enqueueText: (ctx) => {
|
|
64
|
+
const resolved = deps.resolveTarget(ctx);
|
|
65
|
+
return buildTextOutboxEntry({
|
|
66
|
+
createMessageId: deps.createMessageId,
|
|
67
|
+
now: deps.now,
|
|
68
|
+
normalizeAccountId: deps.normalizeAccountId,
|
|
69
|
+
normalizeReplyToId: deps.normalizeReplyToId,
|
|
70
|
+
accountId: resolved.accountId ?? ctx.accountId ?? undefined,
|
|
71
|
+
sessionKey: resolved.sessionKey,
|
|
72
|
+
route: resolved.route,
|
|
73
|
+
text: ctx.text,
|
|
74
|
+
kind: 'final',
|
|
75
|
+
replyToId: ctx.replyToId ?? undefined,
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
enqueueMedia: (ctx) => {
|
|
79
|
+
const resolved = deps.resolveTarget(ctx);
|
|
80
|
+
return buildFileTransferOutboxEntry({
|
|
81
|
+
createMessageId: deps.createMessageId,
|
|
82
|
+
now: deps.now,
|
|
83
|
+
normalizeAccountId: deps.normalizeAccountId,
|
|
84
|
+
pushEvent: deps.filePushEvent ?? 'bncr.file.push',
|
|
85
|
+
accountId: resolved.accountId ?? ctx.accountId ?? undefined,
|
|
86
|
+
sessionKey: resolved.sessionKey,
|
|
87
|
+
route: resolved.route,
|
|
88
|
+
mediaUrl: ctx.mediaUrl,
|
|
89
|
+
mediaLocalRoots: ctx.mediaLocalRoots,
|
|
90
|
+
text: ctx.text,
|
|
91
|
+
asVoice: ctx.audioAsVoice,
|
|
92
|
+
audioAsVoice: ctx.audioAsVoice,
|
|
93
|
+
kind: 'final',
|
|
94
|
+
replyToId: ctx.replyToId ?? undefined,
|
|
95
|
+
});
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function toChannelMessageSendResult(entry: OutboxEntry | undefined, now?: () => number): ChannelMessageSendResult {
|
|
101
|
+
if (!entry) throw new Error('bncr durable message adapter did not receive an outbox entry');
|
|
102
|
+
const queued = buildBncrDurableQueuedResult({ entry, sentAt: now?.() });
|
|
103
|
+
return {
|
|
104
|
+
receipt: queued.receipt as any,
|
|
105
|
+
messageId: queued.receipt.primaryPlatformMessageId,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type { BncrRoute, OutboxEntry } from '../../core/types.ts';
|
|
2
|
+
import { normalizeOutboundReplyToId } from './reply-target-policy.ts';
|
|
3
|
+
|
|
4
|
+
export type BncrDurableQueuedReceipt = {
|
|
5
|
+
primaryPlatformMessageId: string;
|
|
6
|
+
platformMessageIds: string[];
|
|
7
|
+
parts: Array<{
|
|
8
|
+
platformMessageId: string;
|
|
9
|
+
kind: 'text' | 'media' | 'voice' | 'unknown';
|
|
10
|
+
index: number;
|
|
11
|
+
threadId?: string;
|
|
12
|
+
replyToId?: string;
|
|
13
|
+
raw: {
|
|
14
|
+
channel: 'bncr';
|
|
15
|
+
channelId: 'bncr';
|
|
16
|
+
messageId: string;
|
|
17
|
+
chatId: string;
|
|
18
|
+
conversationId: string;
|
|
19
|
+
timestamp: number;
|
|
20
|
+
meta: BncrDurableQueuedReceiptMeta;
|
|
21
|
+
};
|
|
22
|
+
}>;
|
|
23
|
+
threadId?: string;
|
|
24
|
+
replyToId?: string;
|
|
25
|
+
sentAt: number;
|
|
26
|
+
raw: Array<{
|
|
27
|
+
channel: 'bncr';
|
|
28
|
+
channelId: 'bncr';
|
|
29
|
+
messageId: string;
|
|
30
|
+
chatId: string;
|
|
31
|
+
conversationId: string;
|
|
32
|
+
timestamp: number;
|
|
33
|
+
meta: BncrDurableQueuedReceiptMeta;
|
|
34
|
+
}>;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type BncrDurableQueuedReceiptMeta = {
|
|
38
|
+
status: 'accepted';
|
|
39
|
+
deliveryStage: 'queued';
|
|
40
|
+
queue: 'bncr.outbox';
|
|
41
|
+
finalAckManagedBy: 'bncr-outbox';
|
|
42
|
+
ackSemantics: 'plugin-accepted-not-client-acked';
|
|
43
|
+
accountId: string;
|
|
44
|
+
sessionKey: string;
|
|
45
|
+
route: BncrRoute;
|
|
46
|
+
outboxPayloadType?: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type BncrDurableQueuedResult = {
|
|
50
|
+
status: 'sent';
|
|
51
|
+
results: Array<{
|
|
52
|
+
channel: 'bncr';
|
|
53
|
+
channelId: 'bncr';
|
|
54
|
+
messageId: string;
|
|
55
|
+
chatId: string;
|
|
56
|
+
conversationId: string;
|
|
57
|
+
timestamp: number;
|
|
58
|
+
meta: BncrDurableQueuedReceiptMeta;
|
|
59
|
+
}>;
|
|
60
|
+
receipt: BncrDurableQueuedReceipt;
|
|
61
|
+
payloadOutcomes: Array<{
|
|
62
|
+
index: number;
|
|
63
|
+
status: 'sent';
|
|
64
|
+
results: BncrDurableQueuedResult['results'];
|
|
65
|
+
}>;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export function buildBncrDurableQueuedResult(args: {
|
|
69
|
+
entry: OutboxEntry;
|
|
70
|
+
index?: number;
|
|
71
|
+
threadId?: string;
|
|
72
|
+
replyToId?: string;
|
|
73
|
+
sentAt?: number;
|
|
74
|
+
}): BncrDurableQueuedResult {
|
|
75
|
+
const sentAt = Number.isFinite(args.sentAt) ? Number(args.sentAt) : args.entry.createdAt;
|
|
76
|
+
const platformMessageId = args.entry.messageId;
|
|
77
|
+
const replyToId = normalizeOutboundReplyToId({ replyToId: args.replyToId ?? extractReplyToId(args.entry) }) || undefined;
|
|
78
|
+
const chatId = formatQueuedReceiptChatId(args.entry.route);
|
|
79
|
+
const meta: BncrDurableQueuedReceiptMeta = {
|
|
80
|
+
status: 'accepted',
|
|
81
|
+
deliveryStage: 'queued',
|
|
82
|
+
queue: 'bncr.outbox',
|
|
83
|
+
finalAckManagedBy: 'bncr-outbox',
|
|
84
|
+
ackSemantics: 'plugin-accepted-not-client-acked',
|
|
85
|
+
accountId: args.entry.accountId,
|
|
86
|
+
sessionKey: args.entry.sessionKey,
|
|
87
|
+
route: args.entry.route,
|
|
88
|
+
outboxPayloadType: extractPayloadType(args.entry),
|
|
89
|
+
};
|
|
90
|
+
const result = {
|
|
91
|
+
channel: 'bncr' as const,
|
|
92
|
+
channelId: 'bncr' as const,
|
|
93
|
+
messageId: platformMessageId,
|
|
94
|
+
chatId,
|
|
95
|
+
conversationId: args.entry.sessionKey,
|
|
96
|
+
timestamp: sentAt,
|
|
97
|
+
meta,
|
|
98
|
+
};
|
|
99
|
+
const receipt: BncrDurableQueuedReceipt = {
|
|
100
|
+
primaryPlatformMessageId: platformMessageId,
|
|
101
|
+
platformMessageIds: [platformMessageId],
|
|
102
|
+
parts: [
|
|
103
|
+
{
|
|
104
|
+
platformMessageId,
|
|
105
|
+
kind: inferReceiptKind(args.entry),
|
|
106
|
+
index: args.index ?? 0,
|
|
107
|
+
threadId: args.threadId,
|
|
108
|
+
replyToId,
|
|
109
|
+
raw: result,
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
threadId: args.threadId,
|
|
113
|
+
replyToId,
|
|
114
|
+
sentAt,
|
|
115
|
+
raw: [result],
|
|
116
|
+
};
|
|
117
|
+
return {
|
|
118
|
+
status: 'sent',
|
|
119
|
+
results: [result],
|
|
120
|
+
receipt,
|
|
121
|
+
payloadOutcomes: [
|
|
122
|
+
{
|
|
123
|
+
index: args.index ?? 0,
|
|
124
|
+
status: 'sent',
|
|
125
|
+
results: [result],
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function extractPayloadType(entry: OutboxEntry): string | undefined {
|
|
132
|
+
const payload = entry.payload as any;
|
|
133
|
+
return typeof payload?.type === 'string' ? payload.type : undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function extractReplyToId(entry: OutboxEntry): string | undefined {
|
|
137
|
+
const payload = entry.payload as any;
|
|
138
|
+
const metaReply = payload?._meta?.replyToId;
|
|
139
|
+
const replyToId = payload?.replyToId ?? metaReply;
|
|
140
|
+
return typeof replyToId === 'string' ? replyToId : undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function inferReceiptKind(entry: OutboxEntry): 'text' | 'media' | 'voice' | 'unknown' {
|
|
144
|
+
const payload = entry.payload as any;
|
|
145
|
+
if (payload?._meta?.kind === 'file-transfer') {
|
|
146
|
+
if (payload?._meta?.asVoice === true || payload?._meta?.audioAsVoice === true) return 'voice';
|
|
147
|
+
return 'media';
|
|
148
|
+
}
|
|
149
|
+
if (payload?.message?.type === 'text') return 'text';
|
|
150
|
+
return 'unknown';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function formatQueuedReceiptChatId(route: BncrRoute): string {
|
|
154
|
+
const platform = route.platform || 'unknown';
|
|
155
|
+
if (route.groupId) return `Bncr:${platform}:${route.groupId}:${route.userId}`;
|
|
156
|
+
return `Bncr:${platform}:${route.userId}`;
|
|
157
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { buildOpenClawChannelOutboundSessionRoute } from '../../openclaw/session-route-runtime.ts';
|
|
2
2
|
import {
|
|
3
3
|
buildCanonicalBncrSessionKey,
|
|
4
4
|
formatDisplayScope,
|
|
@@ -72,7 +72,7 @@ export function resolveBncrOutboundSessionRoute(params: ResolveBncrOutboundSessi
|
|
|
72
72
|
const sessionKey = buildCanonicalBncrSessionKey(route, canonicalAgentId);
|
|
73
73
|
const displayTo = formatDisplayScope(route);
|
|
74
74
|
|
|
75
|
-
const built =
|
|
75
|
+
const built = buildOpenClawChannelOutboundSessionRoute({
|
|
76
76
|
cfg: params.cfg,
|
|
77
77
|
agentId: canonicalAgentId,
|
|
78
78
|
channel: params.channel,
|