@xmoxmo/bncr 0.2.6 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,3 @@
1
- import { resolvePinnedMainDmOwnerFromAllowlist } from 'openclaw/plugin-sdk/conversation-runtime';
2
- import { resolveInboundLastRouteSessionKey } from 'openclaw/plugin-sdk/routing';
3
1
  import { emitBncrLogLine } from '../../core/logging.ts';
4
2
  import { resolveBncrChannelPolicy } from '../../core/policy.ts';
5
3
  import {
@@ -8,11 +6,22 @@ import {
8
6
  withTaskSessionKey,
9
7
  } from '../../core/targets.ts';
10
8
  import { buildBncrReplyConfig } from './reply-config.ts';
9
+ import { resolveBncrChannelInboundRuntime } from './runtime-compat.ts';
11
10
  import {
12
11
  buildBncrInboundSessionIdentityPatch,
13
12
  recordAndPatchBncrInboundSessionEntry,
14
13
  wrapBncrInboundRecordSessionLabelCorrection,
15
14
  } from './session-label.ts';
15
+ import { dispatchOpenClawReplyWithBufferedBlockDispatcher } from '../../openclaw/reply-runtime.ts';
16
+ import {
17
+ resolveOpenClawAgentRoute,
18
+ resolveOpenClawInboundLastRouteSessionKey,
19
+ } from '../../openclaw/routing-runtime.ts';
20
+ import {
21
+ recordBncrInboundSession,
22
+ resolveBncrInboundSessionStorePath,
23
+ resolveBncrPinnedMainDmOwnerFromAllowlist,
24
+ } from '../../openclaw/inbound-session-runtime.ts';
16
25
 
17
26
  type ParsedInbound = ReturnType<typeof import('./parse.ts')['parseBncrInboundParams']>;
18
27
 
@@ -110,7 +119,7 @@ export async function handleBncrNativeCommand(params: {
110
119
  { debugOnly: true, debugEnabled: nativeCommandDebugEnabled },
111
120
  );
112
121
 
113
- const resolvedRoute = api.runtime.channel.routing.resolveAgentRoute({
122
+ const resolvedRoute = resolveOpenClawAgentRoute(api, {
114
123
  cfg,
115
124
  channel: channelId,
116
125
  accountId,
@@ -137,11 +146,12 @@ export async function handleBncrNativeCommand(params: {
137
146
  }
138
147
  const senderIdForContext = clientId || displayTo;
139
148
  const senderDisplayName = clientId ? 'bncr-client' : displayTo;
140
- const storePath = api.runtime.channel.session.resolveStorePath(cfg?.session?.store, {
149
+ const storePath = resolveBncrInboundSessionStorePath({
150
+ storeConfig: cfg?.session?.store,
141
151
  agentId: resolvedRoute.agentId,
142
152
  });
143
153
 
144
- const ctxPayload = api.runtime.channel.turn.buildContext({
154
+ const ctxPayload = resolveBncrChannelInboundRuntime(api).buildContext({
145
155
  channel: channelId,
146
156
  provider: channelId,
147
157
  surface: channelId,
@@ -259,13 +269,13 @@ export async function handleBncrNativeCommand(params: {
259
269
  const channelPolicy = resolveBncrChannelPolicy(cfg?.channels?.bncr || {});
260
270
  const pinnedMainDmOwner =
261
271
  peer.kind === 'direct'
262
- ? resolvePinnedMainDmOwnerFromAllowlist({
272
+ ? resolveBncrPinnedMainDmOwnerFromAllowlist({
263
273
  dmScope: cfg?.session?.dmScope,
264
274
  allowFrom: channelPolicy.allowFrom,
265
275
  normalizeEntry: (entry: string) => String(entry || '').trim(),
266
276
  })
267
277
  : null;
268
- const inboundLastRouteSessionKey = resolveInboundLastRouteSessionKey({
278
+ const inboundLastRouteSessionKey = resolveOpenClawInboundLastRouteSessionKey({
269
279
  route: resolvedRoute,
270
280
  sessionKey,
271
281
  });
@@ -282,7 +292,7 @@ export async function handleBncrNativeCommand(params: {
282
292
  },
283
293
  { debugOnly: true, debugEnabled: nativeCommandDebugEnabled },
284
294
  );
285
- await api.runtime.channel.turn.run({
295
+ await resolveBncrChannelInboundRuntime(api).run({
286
296
  channel: channelId,
287
297
  accountId,
288
298
  raw: parsed,
@@ -302,7 +312,7 @@ export async function handleBncrNativeCommand(params: {
302
312
  storePath,
303
313
  ctxPayload,
304
314
  recordInboundSession: wrapBncrInboundRecordSessionLabelCorrection({
305
- recordInboundSession: api.runtime.channel.session.recordInboundSession,
315
+ recordInboundSession: recordBncrInboundSession,
306
316
  expectedLabel: displayTo,
307
317
  }),
308
318
  record: {
@@ -330,7 +340,7 @@ export async function handleBncrNativeCommand(params: {
330
340
  },
331
341
  },
332
342
  runDispatch: () =>
333
- api.runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
343
+ dispatchOpenClawReplyWithBufferedBlockDispatcher(api, {
334
344
  ctx: ctxPayload,
335
345
  cfg: effectiveReply.replyCfg,
336
346
  dispatcherOptions: {
@@ -0,0 +1,200 @@
1
+ export type BncrStructuredContextFactsInput = {
2
+ channelId: string;
3
+ accountId: string;
4
+ route: {
5
+ agentId?: string;
6
+ routeSessionKey?: string;
7
+ dispatchSessionKey?: string;
8
+ mainSessionKey?: string;
9
+ };
10
+ conversation: {
11
+ kind: string;
12
+ id: string;
13
+ label: string;
14
+ };
15
+ reply: {
16
+ to: string;
17
+ originatingTo: string;
18
+ };
19
+ sender: {
20
+ id: string;
21
+ displayName?: string;
22
+ };
23
+ message: {
24
+ id?: string | null;
25
+ rawBody: string;
26
+ bodyForAgent?: string;
27
+ commandBody?: string;
28
+ envelopeBody?: string;
29
+ };
30
+ media?: Array<{
31
+ path: string;
32
+ contentType?: string;
33
+ kind?: string;
34
+ messageId?: string;
35
+ }>;
36
+ };
37
+
38
+ export function buildBncrStructuredContextFacts(input: BncrStructuredContextFactsInput) {
39
+ const rawBody = input.message.rawBody;
40
+ return {
41
+ channel: {
42
+ id: input.channelId,
43
+ accountId: input.accountId,
44
+ },
45
+ route: {
46
+ agentId: input.route.agentId,
47
+ routeSessionKey: input.route.routeSessionKey,
48
+ dispatchSessionKey: input.route.dispatchSessionKey,
49
+ mainSessionKey: input.route.mainSessionKey,
50
+ },
51
+ conversation: {
52
+ kind: input.conversation.kind,
53
+ id: input.conversation.id,
54
+ label: input.conversation.label,
55
+ },
56
+ reply: {
57
+ to: input.reply.to,
58
+ originatingTo: input.reply.originatingTo,
59
+ },
60
+ sender: {
61
+ id: input.sender.id,
62
+ displayName: input.sender.displayName || input.sender.id,
63
+ },
64
+ message: {
65
+ id: input.message.id || undefined,
66
+ rawBody,
67
+ bodyForAgent: input.message.bodyForAgent ?? rawBody,
68
+ commandBody: input.message.commandBody ?? rawBody,
69
+ envelopeBody: input.message.envelopeBody,
70
+ },
71
+ media: (input.media || []).map((item) => ({
72
+ path: item.path,
73
+ contentType: item.contentType,
74
+ kind: item.kind,
75
+ messageId: item.messageId,
76
+ })),
77
+ };
78
+ }
79
+
80
+ // Keep this payload intentionally small: OpenClaw already renders standard
81
+ // conversation/sender/message metadata as untrusted context. Only include
82
+ // bncr-specific facts that are not otherwise visible to the model, so normal
83
+ // text turns do not get a duplicate "Bncr inbound context" JSON block.
84
+ export function buildBncrPromptVisibleContextFacts(
85
+ facts: ReturnType<typeof buildBncrStructuredContextFacts>,
86
+ ) {
87
+ const result: {
88
+ reply?: {
89
+ to: string;
90
+ originatingTo: string;
91
+ };
92
+ media?: Array<{
93
+ contentType?: string;
94
+ kind?: string;
95
+ messageId?: string;
96
+ }>;
97
+ } = {};
98
+
99
+ if (facts.reply.originatingTo !== facts.reply.to) {
100
+ result.reply = {
101
+ to: facts.reply.to,
102
+ originatingTo: facts.reply.originatingTo,
103
+ };
104
+ }
105
+
106
+ if (facts.media.length > 0) {
107
+ result.media = facts.media.map((item) => ({
108
+ contentType: item.contentType,
109
+ kind: item.kind,
110
+ messageId: item.messageId,
111
+ }));
112
+ }
113
+
114
+ return result;
115
+ }
116
+
117
+ function inferBncrStructuredMediaKind(contentType: string | undefined) {
118
+ if (contentType?.startsWith('image/')) return 'image';
119
+ if (contentType?.startsWith('video/')) return 'video';
120
+ if (contentType?.startsWith('audio/')) return 'audio';
121
+ return 'document';
122
+ }
123
+
124
+ export type BncrStructuredContextFactsFromInboundPartsInput = {
125
+ channelId: string;
126
+ parsed: {
127
+ accountId: string;
128
+ peer: {
129
+ kind: string;
130
+ id: string;
131
+ };
132
+ clientId?: string;
133
+ msgId?: string;
134
+ mimeType?: string;
135
+ };
136
+ resolution: {
137
+ chatType: string;
138
+ canonicalTo: string;
139
+ originatingTo: string;
140
+ resolvedRoute: {
141
+ agentId?: string;
142
+ sessionKey?: string;
143
+ mainSessionKey?: string;
144
+ };
145
+ dispatchSessionKey?: string;
146
+ };
147
+ prepared: {
148
+ rawBody: string;
149
+ body?: string;
150
+ mediaPath?: string | null;
151
+ };
152
+ senderIdForContext: string;
153
+ senderDisplayName?: string;
154
+ };
155
+
156
+ export function buildBncrStructuredContextFactsFromInboundParts(
157
+ input: BncrStructuredContextFactsFromInboundPartsInput,
158
+ ) {
159
+ const mediaPath = input.prepared.mediaPath || undefined;
160
+ return buildBncrStructuredContextFacts({
161
+ channelId: input.channelId,
162
+ accountId: input.parsed.accountId,
163
+ route: {
164
+ agentId: input.resolution.resolvedRoute.agentId,
165
+ routeSessionKey: input.resolution.resolvedRoute.sessionKey,
166
+ dispatchSessionKey: input.resolution.dispatchSessionKey,
167
+ mainSessionKey: input.resolution.resolvedRoute.mainSessionKey,
168
+ },
169
+ conversation: {
170
+ kind: input.resolution.chatType,
171
+ id: input.parsed.peer.id,
172
+ label: input.resolution.canonicalTo,
173
+ },
174
+ reply: {
175
+ to: input.resolution.canonicalTo,
176
+ originatingTo: input.resolution.originatingTo,
177
+ },
178
+ sender: {
179
+ id: input.senderIdForContext,
180
+ displayName: input.senderDisplayName,
181
+ },
182
+ message: {
183
+ id: input.parsed.msgId,
184
+ rawBody: input.prepared.rawBody,
185
+ bodyForAgent: input.prepared.rawBody,
186
+ commandBody: input.prepared.rawBody,
187
+ envelopeBody: input.prepared.body,
188
+ },
189
+ media: mediaPath
190
+ ? [
191
+ {
192
+ path: mediaPath,
193
+ contentType: input.parsed.mimeType,
194
+ kind: inferBncrStructuredMediaKind(input.parsed.mimeType),
195
+ messageId: input.parsed.msgId,
196
+ },
197
+ ]
198
+ : [],
199
+ });
200
+ }
@@ -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.runtime.channel.routing.resolveAgentRoute({
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 = api.runtime.channel.session.resolveStorePath(cfg?.session?.store, {
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 api.runtime.channel.media.saveMediaBuffer(
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.runtime.channel.reply.formatAgentEnvelope({
193
+ const body = formatOpenClawAgentEnvelope(api, {
173
194
  channel: 'Bncr',
174
195
  from: `${platform}:${groupId}:${userId}`,
175
196
  timestamp: Date.now(),
176
- previousTimestamp: api.runtime.channel.session.readSessionUpdatedAt({
197
+ previousTimestamp: readBncrSessionUpdatedAt(api, {
177
198
  storePath,
178
199
  sessionKey: dispatchSessionKey,
179
200
  }),
180
- envelope: api.runtime.channel.reply.resolveEnvelopeFormatOptions(cfg),
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.runtime.channel.turn.buildContext({
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 = resolveInboundLastRouteSessionKey({
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
- ? resolvePinnedMainDmOwnerFromAllowlist({
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.runtime.channel.turn.run({
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: api.runtime.channel.session.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.runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
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
- function matchesAllowList(list: string[], candidates: string[]): boolean {
14
- if (!list.length) return false;
15
- const normalized = new Set(list.map((x) => asString(x).trim()).filter(Boolean));
16
- return candidates.some((x) => normalized.has(asString(x).trim()));
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
- if (!isGroup && policy.dmPolicy === 'allowlist') {
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
- return { allowed: true };
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,39 @@
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
+ emitBncrLogLine(
24
+ 'warn',
25
+ '[bncr] using legacy runtime.channel.turn compatibility path; upgrade path prefers runtime.channel.inbound',
26
+ );
27
+ }
28
+ return {
29
+ buildContext: legacyTurnRuntime.buildContext,
30
+ run: legacyTurnRuntime.run,
31
+ runPreparedReply: legacyTurnRuntime.runPrepared,
32
+ dispatchReply: legacyTurnRuntime.dispatchAssembled ?? legacyTurnRuntime.runAssembled,
33
+ };
34
+ }
35
+
36
+ throw new Error(
37
+ 'OpenClaw channel inbound runtime is unavailable: expected runtime.channel.inbound.* or legacy runtime.channel.turn.*',
38
+ );
39
+ }
@@ -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 updateSessionStoreEntry({
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 recordSessionMetaFromInbound({
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 updateSessionStoreEntry({
92
+ await updateBncrSessionStoreEntry({
93
93
  storePath,
94
94
  sessionKey,
95
95
  update: () => args.patch,