@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.
Files changed (34) hide show
  1. package/package.json +1 -1
  2. package/src/channel.ts +41 -2
  3. package/src/core/targets.ts +106 -17
  4. package/src/messaging/inbound/commands.ts +263 -51
  5. package/src/messaging/inbound/context-facts.ts +126 -14
  6. package/src/messaging/inbound/contracts.ts +24 -0
  7. package/src/messaging/inbound/dispatch-prep.ts +214 -39
  8. package/src/messaging/inbound/dispatch.ts +71 -5
  9. package/src/messaging/inbound/gate.ts +56 -86
  10. package/src/messaging/inbound/group-history.ts +189 -0
  11. package/src/messaging/inbound/native-command-runtime.ts +77 -61
  12. package/src/messaging/inbound/native-command.ts +92 -8
  13. package/src/messaging/inbound/parse.ts +113 -8
  14. package/src/messaging/inbound/reply-dispatch-serial.ts +62 -0
  15. package/src/messaging/inbound/reply-dispatch.ts +252 -77
  16. package/src/messaging/inbound/scene-admin.ts +269 -0
  17. package/src/messaging/inbound/session-label.ts +122 -13
  18. package/src/messaging/inbound/session-meta-task.ts +17 -0
  19. package/src/messaging/inbound/turn-context.ts +184 -71
  20. package/src/openclaw/channel-runtime-contracts.ts +1 -0
  21. package/src/plugin/channel-components.ts +34 -1
  22. package/src/plugin/channel-inbound-helpers.ts +9 -2
  23. package/src/plugin/channel-runtime-builders-delivery.ts +24 -1
  24. package/src/plugin/channel-runtime-types.ts +42 -0
  25. package/src/plugin/file-inbound-init.ts +27 -12
  26. package/src/plugin/file-inbound-runtime.ts +2 -0
  27. package/src/plugin/inbound-acceptance.ts +82 -1
  28. package/src/plugin/inbound-handlers.ts +55 -2
  29. package/src/plugin/inbound-surface-handlers-group.ts +16 -0
  30. package/src/plugin/messaging.ts +22 -5
  31. package/src/plugin/scene-registry.ts +155 -0
  32. package/src/plugin/state-store.ts +133 -0
  33. package/src/plugin/state-transient-runtime-group.ts +5 -0
  34. package/src/plugin/target-runtime.ts +2 -2
@@ -0,0 +1,189 @@
1
+ import {
2
+ createChannelHistoryWindow,
3
+ DEFAULT_GROUP_HISTORY_LIMIT,
4
+ type HistoryEntry,
5
+ type HistoryMediaEntry,
6
+ } from 'openclaw/plugin-sdk/reply-history';
7
+ import { formatOpenClawAgentEnvelope } from '../../openclaw/reply-runtime.ts';
8
+ import type { BncrInboundApi } from './contracts.ts';
9
+ import type { ParsedInbound } from './dispatch-prep.ts';
10
+
11
+ export type BncrGroupHistoryMap = Map<string, HistoryEntry[]>;
12
+
13
+ type BncrHistoryMediaKind = NonNullable<HistoryMediaEntry['kind']>;
14
+
15
+ function normalizeTextBody(value: string): string {
16
+ return String(value || '')
17
+ .replace(/\s+/g, ' ')
18
+ .trim();
19
+ }
20
+
21
+ export function buildBncrGroupHistoryKey(parsed: ParsedInbound): string | null {
22
+ if (parsed.peer.kind !== 'group') return null;
23
+ const platform = String(parsed.platform || '').trim();
24
+ const groupId = String(parsed.groupId || '').trim();
25
+ if (!platform || !groupId || groupId === '0') return null;
26
+ return `${platform}:${groupId}`;
27
+ }
28
+
29
+ export function recordBncrPendingGroupText(args: {
30
+ historyMap: BncrGroupHistoryMap;
31
+ parsed: ParsedInbound;
32
+ senderDisplayName: string;
33
+ bodyText: string;
34
+ }) {
35
+ const historyKey = buildBncrGroupHistoryKey(args.parsed);
36
+ const body = normalizeTextBody(args.bodyText);
37
+ if (!historyKey || !body || args.parsed.msgType !== 'text') return;
38
+ createChannelHistoryWindow({ historyMap: args.historyMap }).record({
39
+ historyKey,
40
+ limit: DEFAULT_GROUP_HISTORY_LIMIT,
41
+ entry: {
42
+ sender: args.senderDisplayName,
43
+ body,
44
+ timestamp: Date.now(),
45
+ messageId: args.parsed.msgId,
46
+ },
47
+ });
48
+ }
49
+
50
+ function inferBncrHistoryMediaKind(args: {
51
+ msgType?: string;
52
+ mediaContentType?: string;
53
+ }): BncrHistoryMediaKind {
54
+ const msgType = String(args.msgType || '')
55
+ .trim()
56
+ .toLowerCase();
57
+ const contentType = String(args.mediaContentType || '')
58
+ .trim()
59
+ .toLowerCase();
60
+ if (msgType === 'image' || contentType.startsWith('image/')) return 'image';
61
+ if (msgType === 'video' || contentType.startsWith('video/')) return 'video';
62
+ if (msgType === 'audio' || msgType === 'voice' || contentType.startsWith('audio/'))
63
+ return 'audio';
64
+ if (msgType === 'file' || msgType === 'document') return 'document';
65
+ if (contentType) return 'document';
66
+ return 'unknown';
67
+ }
68
+
69
+ function buildBncrHistoryMediaBody(kind: BncrHistoryMediaKind): string {
70
+ return `<media:${kind}>`;
71
+ }
72
+
73
+ export async function recordBncrPendingGroupMedia(args: {
74
+ historyMap: BncrGroupHistoryMap;
75
+ parsed: ParsedInbound;
76
+ senderDisplayName: string;
77
+ bodyText: string;
78
+ mediaItems?: Array<{
79
+ path: string;
80
+ contentType?: string;
81
+ kind?: BncrHistoryMediaKind;
82
+ }>;
83
+ mediaContentType?: string;
84
+ }) {
85
+ const historyKey = buildBncrGroupHistoryKey(args.parsed);
86
+ if (!historyKey || args.parsed.msgType === 'text') return;
87
+ const normalizedMediaItems = Array.isArray(args.mediaItems) ? args.mediaItems : [];
88
+ const itemKinds = normalizedMediaItems
89
+ .map((item) =>
90
+ String(item?.kind || '')
91
+ .trim()
92
+ .toLowerCase(),
93
+ )
94
+ .filter(Boolean);
95
+ const kind = itemKinds[0]
96
+ ? itemKinds.every((candidate) => candidate === itemKinds[0])
97
+ ? (itemKinds[0] as BncrHistoryMediaKind)
98
+ : 'document'
99
+ : inferBncrHistoryMediaKind({
100
+ msgType: args.parsed.msgType,
101
+ mediaContentType: args.mediaContentType,
102
+ });
103
+ const body = normalizeTextBody(args.bodyText) || buildBncrHistoryMediaBody(kind);
104
+ if (normalizedMediaItems.length === 0 || kind !== 'image') {
105
+ createChannelHistoryWindow({ historyMap: args.historyMap }).record({
106
+ historyKey,
107
+ limit: DEFAULT_GROUP_HISTORY_LIMIT,
108
+ entry: {
109
+ sender: args.senderDisplayName,
110
+ body,
111
+ timestamp: Date.now(),
112
+ messageId: args.parsed.msgId,
113
+ },
114
+ });
115
+ return;
116
+ }
117
+ await createChannelHistoryWindow({ historyMap: args.historyMap }).recordWithMedia({
118
+ historyKey,
119
+ limit: DEFAULT_GROUP_HISTORY_LIMIT,
120
+ entry: {
121
+ sender: args.senderDisplayName,
122
+ body,
123
+ timestamp: Date.now(),
124
+ messageId: args.parsed.msgId,
125
+ },
126
+ messageId: args.parsed.msgId,
127
+ media: normalizedMediaItems.map(
128
+ (item) =>
129
+ ({
130
+ path: item.path,
131
+ contentType: item.contentType || args.mediaContentType || 'image/*',
132
+ kind: 'image',
133
+ messageId: args.parsed.msgId,
134
+ }) satisfies HistoryMediaEntry,
135
+ ),
136
+ });
137
+ }
138
+
139
+ export function buildBncrPendingGroupContext(args: {
140
+ api: BncrInboundApi;
141
+ historyMap: BncrGroupHistoryMap;
142
+ parsed: ParsedInbound;
143
+ channelLabel: string;
144
+ currentTimestamp: number;
145
+ previousTimestamp?: unknown;
146
+ envelope?: unknown;
147
+ currentMessage: string;
148
+ }) {
149
+ const historyKey = buildBncrGroupHistoryKey(args.parsed);
150
+ if (!historyKey) return args.currentMessage;
151
+ return createChannelHistoryWindow({ historyMap: args.historyMap }).buildPendingContext({
152
+ historyKey,
153
+ limit: DEFAULT_GROUP_HISTORY_LIMIT,
154
+ currentMessage: args.currentMessage,
155
+ formatEntry: (entry) =>
156
+ formatOpenClawAgentEnvelope(args.api, {
157
+ channel: 'Bncr',
158
+ from: args.channelLabel,
159
+ timestamp: entry.timestamp || args.currentTimestamp,
160
+ previousTimestamp: args.previousTimestamp,
161
+ envelope: args.envelope,
162
+ body: entry.body,
163
+ }),
164
+ });
165
+ }
166
+
167
+ export function buildBncrInboundHistory(args: {
168
+ historyMap: BncrGroupHistoryMap;
169
+ parsed: ParsedInbound;
170
+ }) {
171
+ const historyKey = buildBncrGroupHistoryKey(args.parsed);
172
+ if (!historyKey) return undefined;
173
+ return createChannelHistoryWindow({ historyMap: args.historyMap }).buildInboundHistory({
174
+ historyKey,
175
+ limit: DEFAULT_GROUP_HISTORY_LIMIT,
176
+ });
177
+ }
178
+
179
+ export function clearBncrPendingGroupHistory(args: {
180
+ historyMap: BncrGroupHistoryMap;
181
+ parsed: ParsedInbound;
182
+ }) {
183
+ const historyKey = buildBncrGroupHistoryKey(args.parsed);
184
+ if (!historyKey) return;
185
+ createChannelHistoryWindow({ historyMap: args.historyMap }).clear({
186
+ historyKey,
187
+ limit: DEFAULT_GROUP_HISTORY_LIMIT,
188
+ });
189
+ }
@@ -53,12 +53,12 @@ export function buildNativeCommandHandledResult(args: {
53
53
 
54
54
  export function buildBncrNativeCommandSessionState(args: {
55
55
  parsed: ParsedInbound;
56
- canonicalAgentId: string;
56
+ sessionAgentId: string;
57
57
  resolvedRoute: { sessionKey: string };
58
58
  }) {
59
- const { parsed, canonicalAgentId, resolvedRoute } = args;
59
+ const { parsed, sessionAgentId, resolvedRoute } = args;
60
60
  const baseSessionKey =
61
- normalizeInboundSessionKey(parsed.sessionKeyfromroute, parsed.route, canonicalAgentId) ||
61
+ normalizeInboundSessionKey(parsed.sessionKeyfromroute, parsed.route, sessionAgentId) ||
62
62
  resolvedRoute.sessionKey;
63
63
  const taskSessionKey = withTaskSessionKey(baseSessionKey, parsed.extracted.taskKey);
64
64
  const sessionKey = taskSessionKey || baseSessionKey;
@@ -87,71 +87,87 @@ export function createNativeCommandTurnContext(args: {
87
87
  senderDisplayName: string;
88
88
  body: string;
89
89
  }): BncrInboundContextPayload | Promise<BncrInboundContextPayload> {
90
- return resolveBncrChannelInboundRuntime(args.api).buildContext({
91
- channel: args.channelId,
92
- provider: args.channelId,
93
- surface: args.channelId,
94
- accountId: args.accountId,
95
- messageId: args.msgId,
96
- timestamp: Date.now(),
97
- from: args.senderIdForContext,
98
- sender: {
99
- id: args.senderIdForContext,
100
- name: args.senderDisplayName,
101
- username: args.senderDisplayName,
102
- },
103
- conversation: {
104
- kind: args.peer.kind,
105
- id: args.peer.id,
106
- label: args.displayTo,
107
- routePeer: {
90
+ return Promise.resolve(
91
+ resolveBncrChannelInboundRuntime(args.api).buildContext({
92
+ channel: args.channelId,
93
+ provider: args.channelId,
94
+ surface: args.channelId,
95
+ accountId: args.accountId,
96
+ messageId: args.msgId,
97
+ timestamp: Date.now(),
98
+ from: args.senderIdForContext,
99
+ sender: {
100
+ id: args.senderIdForContext,
101
+ name: args.senderDisplayName,
102
+ username: args.senderDisplayName,
103
+ },
104
+ conversation: {
108
105
  kind: args.peer.kind,
109
106
  id: args.peer.id,
107
+ label: args.displayTo,
108
+ routePeer: {
109
+ kind: args.peer.kind,
110
+ id: args.peer.id,
111
+ },
110
112
  },
111
- },
112
- route: {
113
- agentId: args.resolvedRoute.agentId,
114
- accountId: args.accountId,
115
- routeSessionKey: args.resolvedRoute.sessionKey,
116
- dispatchSessionKey: args.sessionKey,
117
- mainSessionKey: args.resolvedRoute.mainSessionKey,
118
- },
119
- reply: {
120
- to: args.displayTo,
121
- originatingTo: args.originatingTo,
122
- replyToId: args.msgId,
123
- },
124
- message: {
125
- inboundEventKind: 'user_request',
126
- body: args.body,
127
- rawBody: args.body,
128
- bodyForAgent: args.body,
129
- commandBody: args.body,
130
- envelopeFrom: args.originatingTo,
131
- senderLabel: args.senderDisplayName,
132
- },
133
- commandTurn: {
113
+ route: {
114
+ agentId: args.resolvedRoute.agentId,
115
+ accountId: args.accountId,
116
+ routeSessionKey: args.resolvedRoute.sessionKey,
117
+ dispatchSessionKey: args.sessionKey,
118
+ mainSessionKey: args.resolvedRoute.mainSessionKey,
119
+ },
120
+ reply: {
121
+ to: args.displayTo,
122
+ originatingTo: args.originatingTo,
123
+ replyToId: args.msgId,
124
+ },
125
+ message: {
126
+ inboundEventKind: 'user_request',
127
+ body: args.body,
128
+ rawBody: args.body,
129
+ bodyForAgent: args.body,
130
+ commandBody: args.body,
131
+ envelopeFrom: args.originatingTo,
132
+ senderLabel: args.senderDisplayName,
133
+ },
134
+ commandTurn: {
135
+ kind: 'native',
136
+ source: 'native',
137
+ authorized: true,
138
+ body: args.body,
139
+ },
140
+ access: {
141
+ mentions: {
142
+ canDetectMention: true,
143
+ wasMentioned: true,
144
+ effectiveWasMentioned: true,
145
+ },
146
+ commands: {
147
+ authorized: true,
148
+ allowTextCommands: true,
149
+ useAccessGroups: false,
150
+ authorizers: [],
151
+ },
152
+ },
153
+ extra: {
154
+ OriginatingChannel: args.channelId,
155
+ },
156
+ }),
157
+ ).then((ctx) => {
158
+ ctx.From = args.senderIdForContext;
159
+ ctx.To = args.displayTo;
160
+ ctx.SenderId = args.senderIdForContext;
161
+ ctx.OriginatingChannel = args.channelId;
162
+ ctx.CommandAuthorized = true;
163
+ ctx.CommandSource = 'native';
164
+ ctx.CommandTurn = {
134
165
  kind: 'native',
135
166
  source: 'native',
136
167
  authorized: true,
137
168
  body: args.body,
138
- },
139
- access: {
140
- mentions: {
141
- canDetectMention: true,
142
- wasMentioned: true,
143
- effectiveWasMentioned: true,
144
- },
145
- commands: {
146
- authorized: true,
147
- allowTextCommands: true,
148
- useAccessGroups: false,
149
- authorizers: [],
150
- },
151
- },
152
- extra: {
153
- OriginatingChannel: args.channelId,
154
- },
169
+ };
170
+ return ctx;
155
171
  });
156
172
  }
157
173
 
@@ -2,6 +2,11 @@ export type NativeCommand = {
2
2
  command: string;
3
3
  raw: string;
4
4
  body: string;
5
+ argsText: string;
6
+ };
7
+
8
+ export type ParseBncrNativeCommandOptions = {
9
+ allowBareWhoami?: boolean;
5
10
  };
6
11
 
7
12
  export type NativeVerboseCommand = {
@@ -10,27 +15,106 @@ export type NativeVerboseCommand = {
10
15
  text: string;
11
16
  };
12
17
 
13
- export function parseBncrNativeCommand(text: string): NativeCommand | null {
18
+ export type NativeHelpCommand = {
19
+ handled: true;
20
+ text: string;
21
+ };
22
+
23
+ export type NativeWhoamiCommand = {
24
+ handled: true;
25
+ text: string;
26
+ };
27
+
28
+ const BNCR_HELP_TEXT = [
29
+ '🦞 Bncr command usage',
30
+ '',
31
+ '📌 Bncr builtins',
32
+ ' • /bncr whoami',
33
+ ' • /bncr verbose on|off|full',
34
+ '',
35
+ '🛡 Scene approval',
36
+ ' • /bncr allow [<platform>:<groupId>]',
37
+ ' • /bncr deny [<platform>:<groupId>]',
38
+ ' • /bncr bind <agentId> [<platform>:<groupId>]',
39
+ ' • /bncr mode',
40
+ ' • /bncr mode help',
41
+ ' • /bncr mode <admin|mention|hybrid|all> [<platform>:<groupId>]',
42
+ ' • /bncr revoke [<platform>:<groupId>]',
43
+ ' • /bncr list pending',
44
+ ' • /bncr list scenes',
45
+ ].join('\n');
46
+
47
+ const BNCR_NATIVE_COMMANDS = new Set([
48
+ 'help',
49
+ 'whoami',
50
+ 'verbose',
51
+ 'allow',
52
+ 'deny',
53
+ 'bind',
54
+ 'mode',
55
+ 'revoke',
56
+ 'list',
57
+ ]);
58
+
59
+ export function parseBncrNativeCommand(
60
+ text: string,
61
+ options?: ParseBncrNativeCommandOptions,
62
+ ): NativeCommand | null {
14
63
  const raw = String(text || '').trim();
15
- if (!raw.startsWith('/')) return null;
16
- const match = raw.match(/^\/([^\s]+)(?:\s+([\s\S]*))?$/i);
64
+ const allowBareWhoami = options?.allowBareWhoami !== false;
65
+ if (allowBareWhoami && raw.toLowerCase() === '/whoami') {
66
+ return { command: 'whoami', raw, body: '/whoami', argsText: '' };
67
+ }
68
+ if (!raw.startsWith('/bncr')) return null;
69
+ const match = raw.match(/^\/bncr(?:@[A-Za-z0-9_]+)?(?:\s+([^\s]+)(?:\s+([\s\S]*))?)?$/i);
17
70
  if (!match) return null;
18
71
 
19
- const command = String(match[1] || '')
72
+ const command = String(match[1] || 'help')
20
73
  .trim()
21
74
  .toLowerCase();
22
75
  if (!command) return null;
76
+ if (!BNCR_NATIVE_COMMANDS.has(command)) return null;
23
77
 
24
- const rest = String(match[2] || '').trim();
25
- const body = command === 'help' ? ['/commands', rest].filter(Boolean).join(' ') : raw;
26
- return { command, raw, body };
78
+ const argsText = String(match[2] || '').trim();
79
+ const body =
80
+ command === 'help' ? '/commands' : [`/${command}`, argsText].filter(Boolean).join(' ');
81
+ return { command, raw, body, argsText };
82
+ }
83
+
84
+ export function resolveBncrNativeHelpCommand(command: NativeCommand): NativeHelpCommand | null {
85
+ if (command.command !== 'help') return null;
86
+ return { handled: true, text: BNCR_HELP_TEXT };
87
+ }
88
+
89
+ export function resolveBncrNativeWhoamiCommand(args: {
90
+ command: NativeCommand;
91
+ platform: string;
92
+ groupId: string;
93
+ groupName?: string;
94
+ userId: string;
95
+ userName?: string;
96
+ isGroup: boolean;
97
+ isAdmin: boolean;
98
+ }): NativeWhoamiCommand | null {
99
+ if (args.command.command !== 'whoami') return null;
100
+ const lines = ['🧭 Bncr Identity', ''];
101
+ lines.push(`Platform: ${args.platform || '(unknown)'}`);
102
+ lines.push(`User: ${args.userName || '(unknown)'} (${args.userId || '0'})`);
103
+ if (args.isGroup) {
104
+ lines.push(`Group: ${args.groupName || '(unknown)'} (${args.groupId || '0'})`);
105
+ lines.push(`Scene: ${args.platform || '(unknown)'}:${args.groupId || '0'}`);
106
+ } else {
107
+ lines.push(`Scene: ${args.platform || '(unknown)'}:${args.userId || '0'}`);
108
+ }
109
+ lines.push(`Admin: ${args.isAdmin ? 'true' : 'false'}`);
110
+ return { handled: true, text: lines.join('\n') };
27
111
  }
28
112
 
29
113
  export function resolveBncrNativeVerboseCommand(
30
114
  command: NativeCommand,
31
115
  ): NativeVerboseCommand | null {
32
116
  if (command.command !== 'verbose') return null;
33
- const rawLevel = String(command.raw.slice('/verbose'.length) || '')
117
+ const rawLevel = String(command.argsText || '')
34
118
  .trim()
35
119
  .toLowerCase();
36
120
  if (!rawLevel || rawLevel === 'status') {
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto';
2
2
  import { normalizeAccountId } from '../../core/accounts.ts';
3
3
  import { extractInlineTaskKey } from '../../core/targets.ts';
4
4
  import type { BncrRoute } from '../../core/types.ts';
5
- import type { BncrInboundParamsInput } from './contracts.ts';
5
+ import type { BncrInboundMediaItem, BncrInboundParamsInput } from './contracts.ts';
6
6
 
7
7
  function asString(v: unknown, fallback = ''): string {
8
8
  if (typeof v === 'string') return v;
@@ -10,6 +10,84 @@ function asString(v: unknown, fallback = ''): string {
10
10
  return String(v);
11
11
  }
12
12
 
13
+ function asBoolean(v: unknown, fallback = false): boolean {
14
+ if (typeof v === 'boolean') return v;
15
+ if (typeof v === 'number') return v !== 0;
16
+ if (typeof v === 'string') {
17
+ const raw = v.trim().toLowerCase();
18
+ if (!raw) return fallback;
19
+ if (['true', '1', 'yes', 'y', 'on'].includes(raw)) return true;
20
+ if (['false', '0', 'no', 'n', 'off'].includes(raw)) return false;
21
+ }
22
+ return fallback;
23
+ }
24
+
25
+ function asStringArray(v: unknown): string[] {
26
+ if (Array.isArray(v)) {
27
+ return v.map((item) => asString(item).trim()).filter(Boolean);
28
+ }
29
+ if (typeof v === 'string') {
30
+ return v
31
+ .split(',')
32
+ .map((item) => item.trim())
33
+ .filter(Boolean);
34
+ }
35
+ return [];
36
+ }
37
+
38
+ function asInboundMediaItems(params: BncrInboundParamsInput): BncrInboundMediaItem[] {
39
+ const normalized: BncrInboundMediaItem[] = [];
40
+ const rawList = Array.isArray(params?.mediaList) ? params.mediaList : [];
41
+ for (const item of rawList) {
42
+ if (!item || typeof item !== 'object') continue;
43
+ const path = asString((item as { path?: unknown }).path || '').trim();
44
+ const base64 = asString((item as { base64?: unknown }).base64 || '').trim();
45
+ const mimeType = asString((item as { mimeType?: unknown }).mimeType || '').trim();
46
+ const fileName = asString((item as { fileName?: unknown }).fileName || '').trim();
47
+ const type = asString((item as { type?: unknown }).type || '').trim();
48
+ const transferId = asString((item as { transferId?: unknown }).transferId || '').trim();
49
+ if (!path && !base64) continue;
50
+ normalized.push({
51
+ ...(path ? { path } : {}),
52
+ ...(base64 ? { base64 } : {}),
53
+ ...(mimeType ? { mimeType } : {}),
54
+ ...(fileName ? { fileName } : {}),
55
+ ...(type ? { type } : {}),
56
+ ...(transferId ? { transferId } : {}),
57
+ });
58
+ }
59
+
60
+ if (normalized.length > 0) return normalized;
61
+
62
+ const legacyPath = asString(params?.path || '').trim();
63
+ const legacyBase64 = asString(params?.base64 || '').trim();
64
+ const legacyMimeType = asString(params?.mimeType || '').trim();
65
+ const legacyFileName = asString(params?.fileName || '').trim();
66
+ const legacyType = asString(params?.type || '').trim();
67
+ const legacyTransferId = asString((params as { transferId?: unknown })?.transferId || '').trim();
68
+ if (legacyPath || legacyBase64) {
69
+ return [
70
+ {
71
+ ...(legacyPath ? { path: legacyPath } : {}),
72
+ ...(legacyBase64 ? { base64: legacyBase64 } : {}),
73
+ ...(legacyMimeType ? { mimeType: legacyMimeType } : {}),
74
+ ...(legacyFileName ? { fileName: legacyFileName } : {}),
75
+ ...(legacyType ? { type: legacyType } : {}),
76
+ ...(legacyTransferId ? { transferId: legacyTransferId } : {}),
77
+ },
78
+ ];
79
+ }
80
+
81
+ const legacyPaths = asStringArray(params?.paths);
82
+ return legacyPaths.map((item) => ({
83
+ path: item,
84
+ ...(legacyMimeType ? { mimeType: legacyMimeType } : {}),
85
+ ...(legacyFileName ? { fileName: legacyFileName } : {}),
86
+ ...(legacyType ? { type: legacyType } : {}),
87
+ ...(legacyTransferId ? { transferId: legacyTransferId } : {}),
88
+ }));
89
+ }
90
+
13
91
  export function inboundDedupKey(params: {
14
92
  accountId: string;
15
93
  platform: string;
@@ -36,24 +114,30 @@ export function inboundDedupKey(params: {
36
114
  return `${accountId}|${platform}|${groupId}|${userId}|hash:${digest}`;
37
115
  }
38
116
 
39
- export function resolveChatType(_route: BncrRoute): 'direct' | 'group' {
40
- // Compatibility boundary: bncr currently records and dispatches all conversations as direct
41
- // sessions even when the display scope contains a group id. Do not change this to true
42
- // group semantics without updating session routing, reply target policy, and requireMention
43
- // behavior together.
117
+ export function resolveChatType(route: BncrRoute, isGroup: boolean): 'direct' | 'group' {
118
+ if (isGroup) return 'group';
119
+ if (route.groupId !== '0') return 'group';
44
120
  return 'direct';
45
121
  }
46
122
 
47
123
  export function parseBncrInboundParams(params: BncrInboundParamsInput) {
48
124
  const accountId = normalizeAccountId(asString(params?.accountId || ''));
125
+ const protocolVersion = asString(params?.protocolVersion || '').trim() || undefined;
126
+ const capabilities = asStringArray(params?.capabilities);
49
127
  const platform = asString(params?.platform || '').trim();
50
128
  const groupId = asString(params?.groupId || '0').trim() || '0';
129
+ const groupName = asString(params?.groupName || '').trim();
51
130
  const userId = asString(params?.userId || '').trim();
131
+ const userName = asString(params?.userName || '').trim();
52
132
  const sessionKeyfromroute = asString(params?.sessionKey || '').trim();
53
133
  const providedOriginatingTo =
54
134
  asString(params?.originatingTo || params?.providedOriginatingTo || params?.to || '').trim() ||
55
135
  undefined;
56
136
  const clientId = asString(params?.clientId || '').trim() || undefined;
137
+ const bridgeId = asString(params?.bridgeId || params?.clientId || '').trim() || undefined;
138
+ const bridgeName = asString(params?.bridgeName || 'Bncr').trim() || 'Bncr';
139
+ const isGroup = asBoolean(params?.isGroup, groupId !== '0');
140
+ const isAdmin = asBoolean(params?.isAdmin, false);
57
141
 
58
142
  const route: BncrRoute = {
59
143
  platform,
@@ -63,11 +147,17 @@ export function parseBncrInboundParams(params: BncrInboundParamsInput) {
63
147
 
64
148
  const text = asString(params?.msg || '');
65
149
  const msgType = asString(params?.type || 'text') || 'text';
150
+ const mediaItems = asInboundMediaItems(params);
66
151
  const mediaBase64 = asString(params?.base64 || '');
67
152
  const mediaPathFromTransfer = asString(params?.path || '').trim();
68
153
  const mimeType = asString(params?.mimeType || '').trim() || undefined;
69
154
  const fileName = asString(params?.fileName || '').trim() || undefined;
70
155
  const msgId = asString(params?.msgId || '').trim() || undefined;
156
+ const shouldRespond = asBoolean(params?.shouldRespond, false);
157
+ const triggerKind = asString(params?.triggerKind || 'none').trim() || 'none';
158
+ const botName = asString(params?.botName || '').trim();
159
+ const isBotMentioned = asBoolean(params?.isBotMentioned, false);
160
+ const isReplyToBot = asBoolean(params?.isReplyToBot, false);
71
161
 
72
162
  const dedupKey = inboundDedupKey({
73
163
  accountId,
@@ -79,29 +169,44 @@ export function parseBncrInboundParams(params: BncrInboundParamsInput) {
79
169
  mediaBase64,
80
170
  });
81
171
 
172
+ const peerKind = resolveChatType(route, isGroup);
82
173
  const peer = {
83
- kind: resolveChatType(route),
84
- id: route.groupId === '0' ? route.userId : route.groupId,
174
+ kind: peerKind,
175
+ id: peerKind === 'group' ? route.groupId : route.userId,
85
176
  } as const;
86
177
 
87
178
  const extracted = extractInlineTaskKey(text);
88
179
 
89
180
  return {
90
181
  accountId,
182
+ protocolVersion,
183
+ capabilities,
91
184
  platform,
92
185
  groupId,
186
+ groupName,
93
187
  userId,
188
+ userName,
94
189
  sessionKeyfromroute,
95
190
  providedOriginatingTo,
96
191
  clientId,
192
+ bridgeId,
193
+ bridgeName,
194
+ isGroup,
195
+ isAdmin,
97
196
  route,
98
197
  text,
99
198
  msgType,
199
+ mediaItems,
100
200
  mediaBase64,
101
201
  mediaPathFromTransfer,
102
202
  mimeType,
103
203
  fileName,
104
204
  msgId,
205
+ shouldRespond,
206
+ triggerKind,
207
+ botName,
208
+ isBotMentioned,
209
+ isReplyToBot,
105
210
  dedupKey,
106
211
  peer,
107
212
  extracted,