@xmoxmo/bncr 0.4.5 → 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.
Files changed (36) hide show
  1. package/dist/index.js +6 -0
  2. package/index.ts +6 -0
  3. package/package.json +1 -1
  4. package/src/channel.ts +41 -2
  5. package/src/core/targets.ts +106 -17
  6. package/src/messaging/inbound/commands.ts +263 -51
  7. package/src/messaging/inbound/context-facts.ts +126 -14
  8. package/src/messaging/inbound/contracts.ts +24 -0
  9. package/src/messaging/inbound/dispatch-prep.ts +214 -39
  10. package/src/messaging/inbound/dispatch.ts +71 -5
  11. package/src/messaging/inbound/gate.ts +56 -86
  12. package/src/messaging/inbound/group-history.ts +189 -0
  13. package/src/messaging/inbound/native-command-runtime.ts +77 -61
  14. package/src/messaging/inbound/native-command.ts +92 -8
  15. package/src/messaging/inbound/parse.ts +113 -8
  16. package/src/messaging/inbound/reply-dispatch-serial.ts +62 -0
  17. package/src/messaging/inbound/reply-dispatch.ts +252 -77
  18. package/src/messaging/inbound/scene-admin.ts +269 -0
  19. package/src/messaging/inbound/session-label.ts +122 -13
  20. package/src/messaging/inbound/session-meta-task.ts +17 -0
  21. package/src/messaging/inbound/turn-context.ts +184 -71
  22. package/src/openclaw/channel-runtime-contracts.ts +1 -0
  23. package/src/plugin/channel-components.ts +34 -1
  24. package/src/plugin/channel-inbound-helpers.ts +9 -2
  25. package/src/plugin/channel-runtime-builders-delivery.ts +24 -1
  26. package/src/plugin/channel-runtime-types.ts +42 -0
  27. package/src/plugin/file-inbound-init.ts +27 -12
  28. package/src/plugin/file-inbound-runtime.ts +2 -0
  29. package/src/plugin/inbound-acceptance.ts +82 -1
  30. package/src/plugin/inbound-handlers.ts +55 -2
  31. package/src/plugin/inbound-surface-handlers-group.ts +16 -0
  32. package/src/plugin/messaging.ts +22 -5
  33. package/src/plugin/scene-registry.ts +155 -0
  34. package/src/plugin/state-store.ts +133 -0
  35. package/src/plugin/state-transient-runtime-group.ts +5 -0
  36. package/src/plugin/target-runtime.ts +2 -2
@@ -1,41 +1,93 @@
1
+ import { readBncrSessionUpdatedAt } from '../../openclaw/inbound-session-runtime.ts';
2
+ import { resolveOpenClawEnvelopeFormatOptions } from '../../openclaw/reply-runtime.ts';
1
3
  import {
2
4
  buildBncrPromptVisibleContextFacts,
3
5
  buildBncrStructuredContextFactsFromInboundParts,
4
6
  } from './context-facts.ts';
5
- import type { BncrInboundApi, BncrInboundContextPayload } from './contracts.ts';
7
+ import type { BncrInboundApi, BncrInboundConfig, BncrInboundContextPayload } from './contracts.ts';
6
8
  import type {
7
9
  BncrInboundConversationResolution,
8
10
  BncrPreparedInboundSessionContext,
9
11
  ParsedInbound,
10
12
  } from './dispatch-prep.ts';
13
+ import {
14
+ type BncrGroupHistoryMap,
15
+ buildBncrInboundHistory,
16
+ buildBncrPendingGroupContext,
17
+ } from './group-history.ts';
11
18
  import { resolveBncrChannelInboundRuntime } from './runtime-compat.ts';
12
19
 
20
+ function parseSlashCommandName(body: string): string | undefined {
21
+ const raw = String(body || '').trim();
22
+ if (!raw.startsWith('/')) return undefined;
23
+ return raw.slice(1).split(/\s+/, 1)[0]?.split('@', 1)[0] || undefined;
24
+ }
25
+
26
+ function applyBncrLegacyCommandFields(args: {
27
+ ctx: BncrInboundContextPayload;
28
+ channelId: string;
29
+ senderIdForContext: string;
30
+ resolution: BncrInboundConversationResolution;
31
+ rawBody: string;
32
+ ownerAllowFrom?: string[];
33
+ isAuthorizedTextCommand: boolean;
34
+ }) {
35
+ const { ctx, channelId, senderIdForContext, resolution, rawBody, ownerAllowFrom } = args;
36
+ ctx.From = senderIdForContext;
37
+ ctx.To = resolution.canonicalTo;
38
+ ctx.SenderId = senderIdForContext;
39
+ ctx.OriginatingChannel = channelId;
40
+ if (ownerAllowFrom?.length) ctx.OwnerAllowFrom = ownerAllowFrom;
41
+ if (args.isAuthorizedTextCommand) {
42
+ ctx.CommandAuthorized = true;
43
+ ctx.CommandSource = 'text';
44
+ ctx.CommandTurn = {
45
+ kind: 'text-slash',
46
+ source: 'text',
47
+ authorized: true,
48
+ commandName: parseSlashCommandName(rawBody),
49
+ body: rawBody,
50
+ };
51
+ }
52
+ return ctx;
53
+ }
54
+
13
55
  export function buildBncrInboundTurnContext(args: {
14
56
  api: BncrInboundApi;
57
+ cfg: BncrInboundConfig;
15
58
  channelId: string;
16
59
  parsed: ParsedInbound;
17
60
  msgId?: string | null;
18
- mimeType?: string;
19
- mediaPath?: string;
20
61
  peer: ParsedInbound['peer'];
21
62
  senderIdForContext: string;
22
63
  senderDisplayName: string;
64
+ ownerAllowFrom?: string[];
65
+ bridgeSenderId?: string;
66
+ bridgeSenderName?: string;
23
67
  resolution: BncrInboundConversationResolution;
24
68
  prepared: BncrPreparedInboundSessionContext;
69
+ groupHistories: BncrGroupHistoryMap;
70
+ shouldDispatch: boolean;
25
71
  }): BncrInboundContextPayload | Promise<BncrInboundContextPayload> {
26
72
  const {
27
73
  api,
74
+ cfg,
28
75
  channelId,
29
76
  parsed,
30
77
  msgId,
31
- mimeType,
32
- mediaPath,
33
78
  peer,
34
79
  senderIdForContext,
35
80
  senderDisplayName,
81
+ ownerAllowFrom,
82
+ bridgeSenderId,
83
+ bridgeSenderName,
36
84
  resolution,
37
85
  prepared,
86
+ groupHistories,
87
+ shouldDispatch,
38
88
  } = args;
89
+ const senderIsOwner = parsed.isAdmin === true || Boolean(ownerAllowFrom?.length);
90
+ const senderIsAuthorized = senderIsOwner;
39
91
  const structuredContextFacts = buildBncrStructuredContextFactsFromInboundParts({
40
92
  channelId,
41
93
  parsed,
@@ -43,11 +95,14 @@ export function buildBncrInboundTurnContext(args: {
43
95
  prepared: {
44
96
  rawBody: prepared.rawBody,
45
97
  body: prepared.body,
46
- mediaPath,
47
- mediaContentType: mimeType,
98
+ mediaItems: prepared.mediaItems,
48
99
  },
49
100
  senderIdForContext,
50
101
  senderDisplayName,
102
+ bridgeSenderId,
103
+ bridgeSenderName,
104
+ senderIsOwner,
105
+ senderIsAuthorized,
51
106
  });
52
107
  const promptVisibleContextFacts = buildBncrPromptVisibleContextFacts(structuredContextFacts);
53
108
  const supplementalUntrustedContext = Object.keys(promptVisibleContextFacts).length
@@ -60,72 +115,130 @@ export function buildBncrInboundTurnContext(args: {
60
115
  },
61
116
  ]
62
117
  : [];
118
+ const platformWantsReply = parsed.shouldRespond === true;
119
+ const wasMentioned = parsed.isBotMentioned === true || platformWantsReply;
120
+ const isAuthorizedTextCommand = Boolean(ownerAllowFrom?.length);
121
+ const bodyForAgent =
122
+ shouldDispatch && parsed.peer.kind === 'group'
123
+ ? buildBncrPendingGroupContext({
124
+ api,
125
+ historyMap: groupHistories,
126
+ parsed,
127
+ channelLabel: resolution.canonicalTo,
128
+ currentTimestamp: Date.now(),
129
+ previousTimestamp: readBncrSessionUpdatedAt(api, {
130
+ storePath: prepared.storePath,
131
+ sessionKey: resolution.dispatchSessionKey,
132
+ }),
133
+ envelope: resolveOpenClawEnvelopeFormatOptions(api, cfg),
134
+ currentMessage: prepared.body,
135
+ })
136
+ : prepared.body;
137
+ const inboundHistory =
138
+ shouldDispatch && parsed.peer.kind === 'group'
139
+ ? buildBncrInboundHistory({ historyMap: groupHistories, parsed })
140
+ : undefined;
63
141
 
64
- return resolveBncrChannelInboundRuntime(api).buildContext({
65
- channel: channelId,
66
- provider: channelId,
67
- surface: channelId,
68
- accountId: resolution.accountId,
69
- messageId: msgId,
70
- timestamp: Date.now(),
71
- from: senderIdForContext,
72
- sender: {
73
- id: senderIdForContext,
74
- name: senderDisplayName,
75
- username: senderDisplayName,
76
- },
77
- conversation: {
78
- kind: resolution.chatType,
79
- id: peer.id,
80
- label: resolution.canonicalTo,
81
- routePeer: {
82
- kind: peer.kind,
142
+ return Promise.resolve(
143
+ resolveBncrChannelInboundRuntime(api).buildContext({
144
+ channel: channelId,
145
+ provider: channelId,
146
+ surface: channelId,
147
+ accountId: resolution.accountId,
148
+ messageId: msgId,
149
+ timestamp: Date.now(),
150
+ from: senderIdForContext,
151
+ sender: {
152
+ id: senderIdForContext,
153
+ name: senderDisplayName,
154
+ username: parsed.userName || senderDisplayName,
155
+ },
156
+ conversation: {
157
+ kind: resolution.chatType,
83
158
  id: peer.id,
159
+ label: resolution.canonicalTo,
160
+ routePeer: {
161
+ kind: peer.kind,
162
+ id: peer.id,
163
+ },
84
164
  },
85
- },
86
- route: {
87
- agentId: resolution.resolvedRoute.agentId,
88
- accountId: resolution.accountId,
89
- routeSessionKey: resolution.resolvedRoute.sessionKey,
90
- dispatchSessionKey: resolution.dispatchSessionKey,
91
- mainSessionKey: resolution.resolvedRoute.mainSessionKey,
92
- },
93
- reply: {
94
- to: resolution.canonicalTo,
95
- originatingTo: resolution.originatingTo,
96
- },
97
- message: {
98
- inboundEventKind: 'user_request',
99
- body: prepared.body,
165
+ route: {
166
+ agentId: resolution.resolvedRoute.agentId,
167
+ accountId: resolution.accountId,
168
+ routeSessionKey: resolution.resolvedRoute.sessionKey,
169
+ dispatchSessionKey: resolution.dispatchSessionKey,
170
+ mainSessionKey: resolution.resolvedRoute.mainSessionKey,
171
+ },
172
+ reply: {
173
+ to: resolution.canonicalTo,
174
+ originatingTo: resolution.originatingTo,
175
+ },
176
+ message: {
177
+ inboundEventKind: 'user_request',
178
+ body: prepared.body,
179
+ rawBody: prepared.rawBody,
180
+ bodyForAgent,
181
+ inboundHistory,
182
+ commandBody: prepared.rawBody,
183
+ envelopeFrom: resolution.originatingTo,
184
+ senderLabel: senderDisplayName,
185
+ },
186
+ ...(isAuthorizedTextCommand
187
+ ? {
188
+ commandTurn: {
189
+ kind: 'text-slash' as const,
190
+ source: 'text' as const,
191
+ authorized: true,
192
+ commandName: parseSlashCommandName(prepared.rawBody),
193
+ body: prepared.rawBody,
194
+ },
195
+ }
196
+ : {}),
197
+ media:
198
+ prepared.mediaItems.length > 0
199
+ ? prepared.mediaItems.map((item) => ({
200
+ path: item.path,
201
+ contentType: item.contentType,
202
+ kind: item.kind,
203
+ messageId: msgId ?? undefined,
204
+ }))
205
+ : [],
206
+ access: {
207
+ mentions: {
208
+ canDetectMention: true,
209
+ wasMentioned,
210
+ effectiveWasMentioned: wasMentioned,
211
+ },
212
+ ...(isAuthorizedTextCommand
213
+ ? {
214
+ commands: {
215
+ authorized: true,
216
+ allowTextCommands: true,
217
+ useAccessGroups: false,
218
+ authorizers: [],
219
+ },
220
+ }
221
+ : {}),
222
+ },
223
+ supplemental: {
224
+ untrustedContext: supplementalUntrustedContext,
225
+ },
226
+ extra: {
227
+ OriginatingChannel: channelId,
228
+ ...(ownerAllowFrom?.length ? { OwnerAllowFrom: ownerAllowFrom } : {}),
229
+ BncrStructuredContextFacts: structuredContextFacts,
230
+ StructuredContextFacts: structuredContextFacts,
231
+ },
232
+ }),
233
+ ).then((ctx) =>
234
+ applyBncrLegacyCommandFields({
235
+ ctx,
236
+ channelId,
237
+ senderIdForContext,
238
+ resolution,
100
239
  rawBody: prepared.rawBody,
101
- bodyForAgent: prepared.rawBody,
102
- commandBody: prepared.rawBody,
103
- envelopeFrom: resolution.originatingTo,
104
- senderLabel: senderDisplayName,
105
- },
106
- media: mediaPath
107
- ? [
108
- {
109
- path: mediaPath,
110
- contentType: mimeType,
111
- kind: mimeType?.startsWith('image/')
112
- ? 'image'
113
- : mimeType?.startsWith('video/')
114
- ? 'video'
115
- : mimeType?.startsWith('audio/')
116
- ? 'audio'
117
- : 'document',
118
- messageId: msgId ?? undefined,
119
- },
120
- ]
121
- : [],
122
- supplemental: {
123
- untrustedContext: supplementalUntrustedContext,
124
- },
125
- extra: {
126
- OriginatingChannel: channelId,
127
- BncrStructuredContextFacts: structuredContextFacts,
128
- StructuredContextFacts: structuredContextFacts,
129
- },
130
- });
240
+ ownerAllowFrom,
241
+ isAuthorizedTextCommand,
242
+ }),
243
+ );
131
244
  }
@@ -47,6 +47,7 @@ export type OpenClawInboundRuntimeResolvedTurn = {
47
47
  record: {
48
48
  updateLastRoute: unknown;
49
49
  onRecordError: (err: unknown) => void;
50
+ trackSessionMetaTask?: (task: Promise<unknown>) => void;
50
51
  };
51
52
  runDispatch: () => Promise<unknown> | unknown;
52
53
  };
@@ -5,6 +5,7 @@ import { formatDisplayScope, normalizeStoredSessionKey, parseRouteLike } from '.
5
5
  import type { BncrRoute } from '../core/types.ts';
6
6
  import type { BncrInboundParamsInput } from '../messaging/inbound/contracts.ts';
7
7
  import { dispatchBncrInbound } from '../messaging/inbound/dispatch.ts';
8
+ import type { BncrGroupHistoryMap } from '../messaging/inbound/group-history.ts';
8
9
  import { parseBncrInboundParams } from '../messaging/inbound/parse.ts';
9
10
  import { OUTBOUND_FLUSH_REASON, OUTBOUND_FLUSH_TRIGGER } from '../messaging/outbound/reasons.ts';
10
11
  import type { ReplyPayloadInput } from '../messaging/outbound/reply-enqueue.ts';
@@ -150,6 +151,9 @@ export function createBncrFileInboundHandlersComponent(runtime: {
150
151
  typeof createBncrFileInboundHandlers
151
152
  >[0]['refreshAcceptedFileTransferLiveState'];
152
153
  logWarn: Parameters<typeof createBncrFileInboundHandlers>[0]['logWarn'];
154
+ buildCanonicalSessionKey: Parameters<
155
+ typeof createBncrFileInboundHandlers
156
+ >[0]['buildCanonicalSessionKey'];
153
157
  fileRecvTransfers: Parameters<typeof createBncrFileInboundHandlers>[0]['fileRecvTransfers'];
154
158
  inboundFileTransferMaxBytes: number;
155
159
  inboundFileTransferMaxChunks: number;
@@ -166,6 +170,7 @@ export function createBncrFileInboundHandlersComponent(runtime: {
166
170
  logWarn: runtime.logWarn,
167
171
  parseRouteLike,
168
172
  normalizeStoredSessionKey,
173
+ buildCanonicalSessionKey: runtime.buildCanonicalSessionKey,
169
174
  saveInboundMediaBuffer: async ({ buffer, mimeType, fileName }) =>
170
175
  await saveOpenClawChannelMediaBuffer(
171
176
  runtime.getApi(),
@@ -209,6 +214,10 @@ export function createBncrInboundHandlersComponent(runtime: {
209
214
  >[0]['buildActiveConnectionDebugList'];
210
215
  markLastInboundAt: (accountId: string) => void;
211
216
  ensureCanonicalAgentId: Parameters<typeof createBncrInboundHandlers>[0]['ensureCanonicalAgentId'];
217
+ defaultAdminAgentId: Parameters<typeof createBncrInboundHandlers>[0]['defaultAdminAgentId'];
218
+ defaultPublicAgentId: Parameters<typeof createBncrInboundHandlers>[0]['defaultPublicAgentId'];
219
+ sceneRegistry: Parameters<typeof createBncrInboundHandlers>[0]['sceneRegistry'];
220
+ groupHistories: BncrGroupHistoryMap;
212
221
  prepareInboundAcceptance: Parameters<
213
222
  typeof createBncrInboundHandlers
214
223
  >[0]['prepareInboundAcceptance'];
@@ -250,6 +259,10 @@ export function createBncrInboundHandlersComponent(runtime: {
250
259
  markLastInboundAt: runtime.markLastInboundAt,
251
260
  getConfig: () => getOpenClawRuntimeConfig(runtime.getApi()),
252
261
  ensureCanonicalAgentId: runtime.ensureCanonicalAgentId,
262
+ defaultAdminAgentId: runtime.defaultAdminAgentId,
263
+ defaultPublicAgentId: runtime.defaultPublicAgentId,
264
+ sceneRegistry: runtime.sceneRegistry,
265
+ groupHistories: runtime.groupHistories,
253
266
  prepareInboundAcceptance: runtime.prepareInboundAcceptance,
254
267
  formatDisplayScope,
255
268
  logInboundSummary: runtime.logInboundSummary,
@@ -259,13 +272,33 @@ export function createBncrInboundHandlersComponent(runtime: {
259
272
  trigger: OUTBOUND_FLUSH_TRIGGER.INBOUND,
260
273
  reason: OUTBOUND_FLUSH_REASON.INBOUND_ACCEPTED,
261
274
  }),
262
- dispatchInbound: ({ cfg, parsed, canonicalAgentId }) =>
275
+ dispatchInbound: ({
276
+ cfg,
277
+ parsed,
278
+ canonicalAgentId,
279
+ resolvedAgentId,
280
+ shouldDispatch,
281
+ shouldAccumulate,
282
+ sceneRegistry,
283
+ groupHistories,
284
+ defaultAdminAgentId,
285
+ defaultPublicAgentId,
286
+ now,
287
+ }) =>
263
288
  dispatchBncrInbound({
264
289
  api: runtime.getApi(),
265
290
  channelId: runtime.channelId,
266
291
  cfg,
267
292
  parsed,
268
293
  canonicalAgentId,
294
+ resolvedAgentId,
295
+ shouldDispatch,
296
+ shouldAccumulate,
297
+ sceneRegistry,
298
+ groupHistories,
299
+ defaultAdminAgentId,
300
+ defaultPublicAgentId,
301
+ now,
269
302
  rememberSessionRoute: runtime.rememberSessionRoute,
270
303
  enqueueFromReply: runtime.enqueueFromReply,
271
304
  setInboundActivity: runtime.setInboundActivity,
@@ -55,6 +55,7 @@ export function resolveInboundSessionContext(args: {
55
55
  route: BncrRoute;
56
56
  sessionKeyFromRoute?: string;
57
57
  canonicalAgentId: string;
58
+ resolvedAgentId?: string;
58
59
  taskKey?: string;
59
60
  text: string;
60
61
  extractedText?: string;
@@ -74,15 +75,21 @@ export function resolveInboundSessionContext(args: {
74
75
  peer: args.peer,
75
76
  }),
76
77
  );
78
+ const normalizedAgentId =
79
+ (args.resolvedAgentId || '').trim() || resolvedRoute.agentId || args.canonicalAgentId;
77
80
  const baseSessionKey =
78
81
  normalizeInboundSessionKey(
79
82
  args.sessionKeyFromRoute || '',
80
83
  args.route,
81
- args.canonicalAgentId || '',
84
+ normalizedAgentId || '',
82
85
  ) || resolvedRoute.sessionKey;
83
86
  const taskSessionKey = withTaskSessionKey(baseSessionKey, args.taskKey);
84
87
  return {
85
- resolvedRoute,
88
+ resolvedRoute: {
89
+ ...resolvedRoute,
90
+ agentId: normalizedAgentId || resolvedRoute.agentId,
91
+ sessionKey: baseSessionKey || resolvedRoute.sessionKey,
92
+ },
86
93
  baseSessionKey,
87
94
  taskSessionKey,
88
95
  sessionKey: taskSessionKey || baseSessionKey,
@@ -7,6 +7,7 @@ import type {
7
7
  FileSendTransferState,
8
8
  OutboxEntry,
9
9
  } from '../core/types.ts';
10
+ import type { BncrGroupHistoryMap } from '../messaging/inbound/group-history.ts';
10
11
  import type { parseBncrInboundParams } from '../messaging/inbound/parse.ts';
11
12
  import type {
12
13
  NormalizedReplyPayload,
@@ -18,7 +19,11 @@ import type {
18
19
  buildInboundAcceptedLifecycleDebugInfo as buildInboundAcceptedLifecycleDebugInfoFromRuntime,
19
20
  buildInboundResponsePayload as buildInboundResponsePayloadFromRuntime,
20
21
  } from './channel-inbound-helpers.ts';
21
- import type { BncrChannelConfigRoot, BncrVerifiedTarget } from './channel-runtime-types.ts';
22
+ import type {
23
+ BncrChannelConfigRoot,
24
+ BncrSceneRecord,
25
+ BncrVerifiedTarget,
26
+ } from './channel-runtime-types.ts';
22
27
  import type { LeaseEventPayload } from './connection-handlers.ts';
23
28
  import type { BncrActiveConnectionDebugEntry } from './connection-state.ts';
24
29
  import type { FileAckPayloadState, FileAckWaiter } from './file-ack-runtime.ts';
@@ -121,6 +126,11 @@ export function buildBncrStateTransientRuntime(deps: {
121
126
  maxDeadLetterEntries: number;
122
127
  maxSessionRouteEntries: number;
123
128
  maxAccountActivityEntries: number;
129
+ sceneRegistry: Map<string, BncrSceneRecord>;
130
+ groupHistories: Map<
131
+ string,
132
+ import('./channel-runtime-types.ts').BncrPersistedGroupHistoryEntry[]
133
+ >;
124
134
  outbox: Map<string, OutboxEntry>;
125
135
  getDeadLetter: () => OutboxEntry[];
126
136
  setDeadLetter: (entries: OutboxEntry[]) => void;
@@ -446,6 +456,15 @@ export function buildBncrInboundSurfaceRuntime(deps: {
446
456
  peer?: unknown;
447
457
  channelId?: string;
448
458
  }) => string;
459
+ defaultAdminAgentId: (args: {
460
+ cfg: BncrChannelConfigRoot;
461
+ accountId: string;
462
+ peer?: unknown;
463
+ channelId?: string;
464
+ }) => string;
465
+ defaultPublicAgentId: () => string;
466
+ sceneRegistry: Map<string, BncrSceneRecord>;
467
+ groupHistories: BncrGroupHistoryMap;
449
468
  prepareInboundAcceptance: (args: {
450
469
  parsed: ReturnType<typeof parseBncrInboundParams>;
451
470
  canonicalAgentId: string;
@@ -456,6 +475,9 @@ export function buildBncrInboundSurfaceRuntime(deps: {
456
475
  sessionKey: string;
457
476
  inboundText: string;
458
477
  hasMedia: boolean;
478
+ resolvedAgentId: string;
479
+ shouldDispatch: boolean;
480
+ shouldAccumulate: boolean;
459
481
  }
460
482
  | {
461
483
  ok: false;
@@ -486,6 +508,7 @@ export function buildBncrInboundSurfaceRuntime(deps: {
486
508
  }) => Promise<void>;
487
509
  setInboundActivity: (accountId: string, at: number) => void;
488
510
  scheduleSave: () => void;
511
+ buildCanonicalSessionKey: (route: BncrRoute) => string;
489
512
  fileRecvTransfers: Parameters<typeof createBncrFileInboundHandlers>[0]['fileRecvTransfers'];
490
513
  inboundFileTransferMaxBytes: number;
491
514
  inboundFileTransferMaxChunks: number;
@@ -44,6 +44,46 @@ export type BncrPersistedLastSession = {
44
44
  updatedAt: number;
45
45
  };
46
46
 
47
+ export type BncrSceneKind = 'direct' | 'group';
48
+
49
+ export type BncrSceneStatus = 'pending' | 'allowed' | 'denied';
50
+
51
+ export type BncrGroupReplyMode = 'admin' | 'mention' | 'hybrid' | 'all';
52
+
53
+ export type BncrSceneRecord = {
54
+ sceneKey: string;
55
+ kind: BncrSceneKind;
56
+ status: BncrSceneStatus;
57
+ platform: string;
58
+ userId?: string;
59
+ userName?: string;
60
+ groupId?: string;
61
+ groupName?: string;
62
+ agentId?: string;
63
+ groupReplyMode?: BncrGroupReplyMode;
64
+ lastSeenAt: number;
65
+ };
66
+
67
+ export type BncrPersistedGroupHistoryEntry = {
68
+ sender: string;
69
+ body: string;
70
+ timestamp?: number;
71
+ messageId?: string;
72
+ media?: BncrPersistedGroupHistoryMediaEntry[];
73
+ };
74
+
75
+ export type BncrPersistedGroupHistoryMediaEntry = {
76
+ path?: string;
77
+ contentType?: string;
78
+ kind?: 'image' | 'video' | 'audio' | 'document' | 'unknown';
79
+ messageId?: string;
80
+ };
81
+
82
+ export type BncrPersistedGroupHistoryBucket = {
83
+ key: string;
84
+ entries: BncrPersistedGroupHistoryEntry[];
85
+ };
86
+
47
87
  export type BncrStatusRuntimeSnapshot = {
48
88
  connected?: boolean;
49
89
  running?: boolean;
@@ -133,6 +173,8 @@ export type PersistedState = {
133
173
  outbox: OutboxEntry[];
134
174
  deadLetter: OutboxEntry[];
135
175
  sessionRoutes: BncrPersistedSessionRoute[];
176
+ sceneRegistry?: BncrSceneRecord[];
177
+ groupHistories?: BncrPersistedGroupHistoryBucket[];
136
178
  lastSessionByAccount?: BncrPersistedLastSession[];
137
179
  lastActivityByAccount?: BncrPersistedAccountTimestamp[];
138
180
  lastInboundByAccount?: BncrPersistedAccountTimestamp[];
@@ -45,34 +45,49 @@ export function createBncrFileInboundInitHandler(runtime: BncrFileInboundRuntime
45
45
  const chunkSize = runtime.finiteNonNegativeNumberOrNull(params?.chunkSize ?? 256 * 1024);
46
46
  const totalChunks = runtime.finiteNonNegativeNumberOrNull(params?.totalChunks);
47
47
  const fileSha256 = runtime.asString(params?.fileSha256 || '').trim();
48
+ const routeFromParams = runtime.parseRouteLike({
49
+ platform: runtime.asString(params?.platform || '').trim(),
50
+ groupId: runtime.asString(params?.groupId || '0').trim() || '0',
51
+ userId: runtime.asString(params?.userId || '0').trim() || '0',
52
+ });
53
+
54
+ const reject = (error: string) => {
55
+ runtime.logWarn(
56
+ 'file',
57
+ `file.init reject accountId=${accountId} connId=${connId} clientId=${clientId || '-'} transferId=${transferId || '-'} error=${error} sessionKey=${sessionKey || '-'} fileSize=${fileSize ?? 'null'} chunkSize=${chunkSize ?? 'null'} totalChunks=${totalChunks ?? 'null'}`,
58
+ );
59
+ respond(false, { error });
60
+ };
48
61
 
49
62
  if (!transferId || !sessionKey || !fileSize || !chunkSize || !totalChunks) {
50
- respond(false, { error: 'transferId/sessionKey/fileSize/chunkSize/totalChunks required' });
63
+ reject('transferId/sessionKey/fileSize/chunkSize/totalChunks required');
51
64
  return;
52
65
  }
53
66
  if (fileSize > runtime.inboundFileTransferMaxBytes) {
54
- respond(false, {
55
- error: `fileSize too large size=${fileSize} max=${runtime.inboundFileTransferMaxBytes}`,
56
- });
67
+ reject(`fileSize too large size=${fileSize} max=${runtime.inboundFileTransferMaxBytes}`);
57
68
  return;
58
69
  }
59
70
  if (totalChunks > runtime.inboundFileTransferMaxChunks) {
60
- respond(false, {
61
- error: `totalChunks too large total=${totalChunks} max=${runtime.inboundFileTransferMaxChunks}`,
62
- });
71
+ reject(
72
+ `totalChunks too large total=${totalChunks} max=${runtime.inboundFileTransferMaxChunks}`,
73
+ );
63
74
  return;
64
75
  }
65
76
  const expectedTotalChunks = Math.ceil(fileSize / chunkSize);
66
77
  if (totalChunks !== expectedTotalChunks) {
67
- respond(false, {
68
- error: `totalChunks mismatch total=${totalChunks} expected=${expectedTotalChunks}`,
69
- });
78
+ reject(`totalChunks mismatch total=${totalChunks} expected=${expectedTotalChunks}`);
70
79
  return;
71
80
  }
72
81
 
73
- const normalized = runtime.normalizeStoredSessionKey(sessionKey);
82
+ let normalized = runtime.normalizeStoredSessionKey(sessionKey);
83
+ if (!normalized && routeFromParams) {
84
+ normalized = {
85
+ sessionKey: runtime.buildCanonicalSessionKey(routeFromParams),
86
+ route: routeFromParams,
87
+ };
88
+ }
74
89
  if (!normalized) {
75
- respond(false, { error: 'invalid sessionKey' });
90
+ reject('invalid sessionKey');
76
91
  return;
77
92
  }
78
93
 
@@ -1,4 +1,5 @@
1
1
  import type { GatewayRequestHandlerOptions } from 'openclaw/plugin-sdk/core';
2
+ import type { BncrSessionKind } from '../core/targets.ts';
2
3
  import type { BncrRoute, FileRecvTransferState } from '../core/types.ts';
3
4
 
4
5
  export type BncrFileInboundLeaseEventKind =
@@ -40,6 +41,7 @@ export type BncrFileInboundRuntime = {
40
41
  normalizeStoredSessionKey: (
41
42
  sessionKey: string,
42
43
  ) => { sessionKey: string; route: BncrRoute } | null;
44
+ buildCanonicalSessionKey: (route: BncrRoute, kind?: BncrSessionKind) => string;
43
45
  saveInboundMediaBuffer: (args: {
44
46
  buffer: Buffer;
45
47
  mimeType: string;