@xmoxmo/bncr 0.6.3 → 0.6.5
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 +36 -5
- package/package.json +1 -1
- package/scripts/check-pack.mjs +2 -0
- package/scripts/selfcheck.mjs +2 -0
- package/src/messaging/inbound/command-owner.ts +33 -0
- package/src/messaging/inbound/commands.ts +386 -252
- package/src/messaging/inbound/conversation-history.ts +0 -5
- package/src/messaging/inbound/dispatch-prep.ts +8 -4
- package/src/messaging/inbound/dispatch.ts +3 -1
- package/src/messaging/inbound/native-command-runtime.ts +4 -0
- package/src/messaging/inbound/native-command.ts +89 -15
- package/src/messaging/inbound/reply-dispatch.ts +17 -36
- package/src/messaging/inbound/scene-admin.ts +31 -3
- package/src/messaging/inbound/turn-context.ts +2 -9
- package/src/openclaw/inbound-session-runtime.ts +17 -0
- package/src/openclaw/session-reset-runtime.ts +77 -0
- package/src/plugin/inbound-acceptance.ts +18 -7
|
@@ -405,11 +405,6 @@ export function buildBncrConversationContextFromEntries(args: {
|
|
|
405
405
|
return `[Chat messages since your last reply - for context]\n${historyText}\n\n[Current message - respond to this]${args.currentMessage}`;
|
|
406
406
|
}
|
|
407
407
|
|
|
408
|
-
export function collectBncrHistoryMediaFromEntries(args: {
|
|
409
|
-
entries: readonly BncrHistoryEntry[];
|
|
410
|
-
}): HistoryMediaEntry[] {
|
|
411
|
-
return args.entries.flatMap((entry) => (Array.isArray(entry.media) ? entry.media : []));
|
|
412
|
-
}
|
|
413
408
|
export function clearBncrPendingConversationHistory(args: {
|
|
414
409
|
historyMap: BncrConversationHistoryMap;
|
|
415
410
|
parsed: ParsedInbound;
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
} from '../../openclaw/routing-runtime.ts';
|
|
21
21
|
import type { BncrInboundApi, BncrInboundConfig, BncrRememberSessionRoute } from './contracts.ts';
|
|
22
22
|
import { INBOUND_MEDIA_URL_MAX_BYTES, isHttpMediaUrl } from './media-url-download.ts';
|
|
23
|
-
import { parseBncrNativeCommand } from './native-command.ts';
|
|
23
|
+
import { parseBncrNativeCommand, resolveBncrNativeCommandParseOptions } from './native-command.ts';
|
|
24
24
|
import { loadInboundRemoteMedia } from './remote-media.ts';
|
|
25
25
|
|
|
26
26
|
export type ParsedInbound = ReturnType<typeof import('./parse.ts')['parseBncrInboundParams']>;
|
|
@@ -460,9 +460,13 @@ export async function prepareBncrInboundDispatch(args: {
|
|
|
460
460
|
const senderIdForContext = parsed.userId || parsed.clientId || resolution.canonicalTo;
|
|
461
461
|
const senderDisplayName = parsed.userName || resolution.canonicalTo;
|
|
462
462
|
const isBncrNativeCommand =
|
|
463
|
-
parseBncrNativeCommand(
|
|
464
|
-
|
|
465
|
-
|
|
463
|
+
parseBncrNativeCommand(
|
|
464
|
+
parsed.extracted.text,
|
|
465
|
+
resolveBncrNativeCommandParseOptions({
|
|
466
|
+
isAdmin: parsed.isAdmin,
|
|
467
|
+
peerKind: parsed.peer.kind as 'direct' | 'group',
|
|
468
|
+
}),
|
|
469
|
+
) !== null;
|
|
466
470
|
const isOpenClawNativeCommand =
|
|
467
471
|
!isBncrNativeCommand &&
|
|
468
472
|
hasControlCommand(parsed.extracted.text, cfg as Parameters<typeof hasControlCommand>[1]);
|
|
@@ -176,6 +176,8 @@ export async function dispatchBncrInbound(params: {
|
|
|
176
176
|
// A stop command must not wait behind the history serial chain or mutate
|
|
177
177
|
// the accumulated conversation window. Deliver it immediately so the host
|
|
178
178
|
// can abort the active run instead of queuing behind it.
|
|
179
|
+
const stopOwnerAllowFrom =
|
|
180
|
+
parsed.peer.kind === 'direct' && senderIdForContext ? [senderIdForContext] : ownerAllowFrom;
|
|
179
181
|
const ctxPayload = await buildBncrInboundTurnContext({
|
|
180
182
|
api,
|
|
181
183
|
cfg,
|
|
@@ -185,7 +187,7 @@ export async function dispatchBncrInbound(params: {
|
|
|
185
187
|
peer,
|
|
186
188
|
senderIdForContext,
|
|
187
189
|
senderDisplayName,
|
|
188
|
-
ownerAllowFrom,
|
|
190
|
+
ownerAllowFrom: stopOwnerAllowFrom,
|
|
189
191
|
bridgeSenderId,
|
|
190
192
|
bridgeSenderName,
|
|
191
193
|
historyLimit: resolvedHistoryLimit,
|
|
@@ -81,6 +81,7 @@ export function createNativeCommandTurnContext(args: {
|
|
|
81
81
|
peer: ParsedInbound['peer'];
|
|
82
82
|
resolvedRoute: { sessionKey: string; agentId: string; mainSessionKey?: string };
|
|
83
83
|
sessionKey: string;
|
|
84
|
+
ownerAllowFrom?: string[];
|
|
84
85
|
displayTo: string;
|
|
85
86
|
originatingTo: string;
|
|
86
87
|
senderIdForContext: string;
|
|
@@ -167,6 +168,9 @@ export function createNativeCommandTurnContext(args: {
|
|
|
167
168
|
authorized: true,
|
|
168
169
|
body: args.body,
|
|
169
170
|
};
|
|
171
|
+
if (args.ownerAllowFrom?.length) {
|
|
172
|
+
ctx.OwnerAllowFrom = args.ownerAllowFrom;
|
|
173
|
+
}
|
|
170
174
|
return ctx;
|
|
171
175
|
});
|
|
172
176
|
}
|
|
@@ -50,7 +50,74 @@ export type NativeSessionResetCommand = {
|
|
|
50
50
|
text: string;
|
|
51
51
|
};
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Commands that non-admin private-chat users may self-elevate for.
|
|
55
|
+
* When a non-admin direct-chat caller issues one of these commands,
|
|
56
|
+
* the plugin temporarily treats them as admin so the command follows
|
|
57
|
+
* the same code path as an admin caller. This whitelist is shared
|
|
58
|
+
* across permission checks so admin and non-admin logic stay in sync.
|
|
59
|
+
*
|
|
60
|
+
* `stop` stays in this whitelist for semantic consistency, but it is
|
|
61
|
+
* routed through the unified stop fast path in dispatch.ts rather than
|
|
62
|
+
* the native command handler, so it is never processed here.
|
|
63
|
+
*/
|
|
64
|
+
export const BNCR_SELF_SERVICE_COMMANDS: ReadonlySet<string> = new Set([
|
|
65
|
+
'whoami',
|
|
66
|
+
'status',
|
|
67
|
+
'verbose',
|
|
68
|
+
'model',
|
|
69
|
+
'new',
|
|
70
|
+
'reset',
|
|
71
|
+
'stop',
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Synthetic command name used when an admin bare command is routed to the
|
|
76
|
+
* OpenClaw native parser. It must never be exposed to the user.
|
|
77
|
+
*/
|
|
78
|
+
export const BNCR_OPENCLAW_NATIVE_COMMAND = '__openclaw_native__';
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Detect if the raw text is a bare whitelist command that should be
|
|
82
|
+
* elevated to admin for OpenClaw native parsing (private non-admin)
|
|
83
|
+
* or mapped to /bncr (group non-admin).
|
|
84
|
+
*/
|
|
85
|
+
export function isBncrWhitelistBareCommandText(rawBody: string): string | null {
|
|
86
|
+
const raw = String(rawBody || '')
|
|
87
|
+
.trim()
|
|
88
|
+
.toLowerCase();
|
|
89
|
+
if (!raw.startsWith('/')) return null;
|
|
90
|
+
const bare = raw.slice(1);
|
|
91
|
+
if (!bare) return null;
|
|
92
|
+
// Whitelist commands match the exact command name only. @bot suffixes are
|
|
93
|
+
// intentionally unsupported so bare commands stay consistent with /stop.
|
|
94
|
+
const commandName = bare.split(/\s+/, 1)[0];
|
|
95
|
+
if (!commandName) return null;
|
|
96
|
+
return BNCR_SELF_SERVICE_COMMANDS.has(commandName) ? commandName : null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Resolve which bare slash commands bncr should treat as native commands.
|
|
101
|
+
*
|
|
102
|
+
* Admin callers never have whitelist restrictions: their bare commands go to
|
|
103
|
+
* the OpenClaw parser. Non-admin group callers map the whitelist onto /bncr
|
|
104
|
+
* native commands; non-admin private callers are elevated and routed to the
|
|
105
|
+
* OpenClaw parser instead (handled before this helper runs).
|
|
106
|
+
*/
|
|
107
|
+
export function resolveBncrNativeCommandParseOptions(args: {
|
|
108
|
+
isAdmin: boolean;
|
|
109
|
+
peerKind: 'direct' | 'group';
|
|
110
|
+
}): ParseBncrNativeCommandOptions {
|
|
111
|
+
const isGroup = args.peerKind === 'group';
|
|
112
|
+
const isNonAdmin = args.isAdmin !== true;
|
|
113
|
+
return {
|
|
114
|
+
allowBareWhoami: isNonAdmin && isGroup,
|
|
115
|
+
allowBareStatus: isNonAdmin && isGroup,
|
|
116
|
+
allowBareSessionReset: isNonAdmin && isGroup,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export const BNCR_NATIVE_COMMANDS = new Set([
|
|
54
121
|
'help',
|
|
55
122
|
'new',
|
|
56
123
|
'reset',
|
|
@@ -128,7 +195,9 @@ export function parseBncrUnsupportedDirectCommand(text: string): {
|
|
|
128
195
|
const raw = String(text || '').trim();
|
|
129
196
|
if (!raw.startsWith('/')) return null;
|
|
130
197
|
|
|
131
|
-
|
|
198
|
+
// Hyphens are allowed in command names (e.g. /history-limit) so bare slash
|
|
199
|
+
// parsing matches the same command vocabulary as the /bncr subcommands.
|
|
200
|
+
const bareMatch = raw.match(/^\/([A-Za-z0-9_-]+)(?:\s+.*)?$/);
|
|
132
201
|
if (!bareMatch) return null;
|
|
133
202
|
const bareCommand = String(bareMatch[1] || '')
|
|
134
203
|
.trim()
|
|
@@ -158,7 +227,7 @@ const BNCR_HELP_SECTIONS = [
|
|
|
158
227
|
{ label: '/bncr status', scopes: ['admin', 'direct', 'group'] },
|
|
159
228
|
{ label: '/bncr new', scopes: ['admin', 'direct'] },
|
|
160
229
|
{ label: '/bncr reset', scopes: ['admin', 'direct'] },
|
|
161
|
-
{ label: '/bncr verbose on|off|full', scopes: ['admin'] },
|
|
230
|
+
{ label: '/bncr verbose on|off|full', scopes: ['admin', 'direct'] },
|
|
162
231
|
],
|
|
163
232
|
},
|
|
164
233
|
{
|
|
@@ -180,20 +249,20 @@ const BNCR_HELP_SECTIONS = [
|
|
|
180
249
|
},
|
|
181
250
|
{
|
|
182
251
|
title: '📋 Conversation history',
|
|
183
|
-
audience: '
|
|
252
|
+
audience: 'all',
|
|
184
253
|
commands: [
|
|
185
|
-
{ label: '/bncr history-help', scopes: ['admin'] },
|
|
186
|
-
{ label: '/bncr history-limit [<number>|clear] [<SceneId>]', scopes: ['admin'] },
|
|
187
|
-
{ label: '/bncr history-force on|off|clear [<SceneId>]', scopes: ['admin'] },
|
|
254
|
+
{ label: '/bncr history-help', scopes: ['admin', 'direct'] },
|
|
255
|
+
{ label: '/bncr history-limit [<number>|clear] [<SceneId>]', scopes: ['admin', 'direct'] },
|
|
256
|
+
{ label: '/bncr history-force on|off|clear [<SceneId>]', scopes: ['admin', 'direct'] },
|
|
188
257
|
],
|
|
189
258
|
},
|
|
190
259
|
{
|
|
191
260
|
title: '🌐 Remote media',
|
|
192
|
-
audience: '
|
|
261
|
+
audience: 'all',
|
|
193
262
|
commands: [
|
|
194
263
|
{
|
|
195
264
|
label: '/bncr download-media on|off|clear|default on|off [<SceneId>]',
|
|
196
|
-
scopes: ['admin'],
|
|
265
|
+
scopes: ['admin', 'direct'],
|
|
197
266
|
},
|
|
198
267
|
],
|
|
199
268
|
},
|
|
@@ -272,26 +341,31 @@ export function resolveBncrNativeSessionResetCommand(args: {
|
|
|
272
341
|
}): NativeSessionResetCommand | null {
|
|
273
342
|
if (args.command.command !== 'new' && args.command.command !== 'reset') return null;
|
|
274
343
|
const reason = args.command.command === 'new' ? 'new' : 'reset';
|
|
275
|
-
const scope = args.peerKind === 'group' ? 'this group chat' : 'this private chat';
|
|
276
344
|
return {
|
|
277
345
|
handled: true,
|
|
278
346
|
reason,
|
|
279
|
-
text:
|
|
280
|
-
reason === 'new'
|
|
281
|
-
? `Started a new session for ${scope}.`
|
|
282
|
-
: `Reset the current session for ${scope}.`,
|
|
347
|
+
text: reason === 'new' ? '✅ New session started.' : '✅ Session reset.',
|
|
283
348
|
};
|
|
284
349
|
}
|
|
285
350
|
|
|
286
351
|
export function resolveBncrNativeVerboseCommand(
|
|
287
352
|
command: NativeCommand,
|
|
353
|
+
currentLevel?: 'on' | 'off' | 'full',
|
|
288
354
|
): NativeVerboseCommand | null {
|
|
289
355
|
if (command.command !== 'verbose') return null;
|
|
290
356
|
const rawLevel = String(command.argsText || '')
|
|
291
357
|
.trim()
|
|
292
358
|
.toLowerCase();
|
|
293
359
|
if (!rawLevel || rawLevel === 'status') {
|
|
294
|
-
|
|
360
|
+
const label =
|
|
361
|
+
currentLevel === 'full'
|
|
362
|
+
? 'full'
|
|
363
|
+
: currentLevel === 'off'
|
|
364
|
+
? 'off'
|
|
365
|
+
: currentLevel === 'on'
|
|
366
|
+
? 'on'
|
|
367
|
+
: 'default';
|
|
368
|
+
return { handled: true, text: `Current verbose level: ${label}` };
|
|
295
369
|
}
|
|
296
370
|
if (rawLevel === 'on')
|
|
297
371
|
return { handled: true, verboseLevel: 'on', text: 'Verbose logging enabled.' };
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
resolveBncrPinnedMainDmOwnerFromAllowlist,
|
|
7
7
|
} from '../../openclaw/inbound-session-runtime.ts';
|
|
8
8
|
import { dispatchOpenClawReplyWithBufferedBlockDispatcher } from '../../openclaw/reply-runtime.ts';
|
|
9
|
+
import { mergeBncrOwnerAllowFromIntoConfig } from './command-owner.ts';
|
|
9
10
|
import type {
|
|
10
11
|
BncrEnqueueFromReply,
|
|
11
12
|
BncrInboundApi,
|
|
@@ -18,7 +19,11 @@ import type {
|
|
|
18
19
|
ParsedInbound,
|
|
19
20
|
} from './dispatch-prep.ts';
|
|
20
21
|
import { buildBncrInboundRecordUpdateLastRoute } from './last-route.ts';
|
|
21
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
isBncrStopCommandText,
|
|
24
|
+
parseBncrNativeCommand,
|
|
25
|
+
resolveBncrNativeCommandParseOptions,
|
|
26
|
+
} from './native-command.ts';
|
|
22
27
|
import { buildBncrReplyConfig } from './reply-config.ts';
|
|
23
28
|
import { runBncrReplyDispatchSerial } from './reply-dispatch-serial.ts';
|
|
24
29
|
import { resolveBncrChannelInboundRuntime } from './runtime-compat.ts';
|
|
@@ -29,32 +34,6 @@ import {
|
|
|
29
34
|
} from './session-label.ts';
|
|
30
35
|
import { createBncrSessionMetaTaskBarrier } from './session-meta-task.ts';
|
|
31
36
|
|
|
32
|
-
function mergeBncrCommandOwnerAllowFrom(args: {
|
|
33
|
-
cfg: BncrInboundConfig;
|
|
34
|
-
parsed: ParsedInbound;
|
|
35
|
-
isBncrNativeCommand: boolean;
|
|
36
|
-
senderIdForContext: string;
|
|
37
|
-
}) {
|
|
38
|
-
const { cfg, parsed, isBncrNativeCommand, senderIdForContext } = args;
|
|
39
|
-
if (parsed.isAdmin !== true || isBncrNativeCommand) return cfg;
|
|
40
|
-
const senderId = String(senderIdForContext || '').trim();
|
|
41
|
-
if (!senderId) return cfg;
|
|
42
|
-
|
|
43
|
-
const currentCommands = (cfg.commands || {}) as { ownerAllowFrom?: string[] };
|
|
44
|
-
const currentOwnerAllowFrom = Array.isArray(currentCommands.ownerAllowFrom)
|
|
45
|
-
? currentCommands.ownerAllowFrom
|
|
46
|
-
: [];
|
|
47
|
-
if (currentOwnerAllowFrom.includes(senderId)) return cfg;
|
|
48
|
-
|
|
49
|
-
return {
|
|
50
|
-
...cfg,
|
|
51
|
-
commands: {
|
|
52
|
-
...currentCommands,
|
|
53
|
-
ownerAllowFrom: [...currentOwnerAllowFrom, senderId],
|
|
54
|
-
},
|
|
55
|
-
} satisfies BncrInboundConfig;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
37
|
function sleep(ms: number) {
|
|
59
38
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
60
39
|
}
|
|
@@ -180,15 +159,17 @@ export async function runBncrInboundReplyDispatch(args: {
|
|
|
180
159
|
pinnedMainDmOwner,
|
|
181
160
|
});
|
|
182
161
|
const isBncrNativeCommand =
|
|
183
|
-
parseBncrNativeCommand(
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
162
|
+
parseBncrNativeCommand(
|
|
163
|
+
rawBody,
|
|
164
|
+
resolveBncrNativeCommandParseOptions({
|
|
165
|
+
isAdmin: parsed.isAdmin,
|
|
166
|
+
peerKind: parsed.peer.kind as 'direct' | 'group',
|
|
167
|
+
}),
|
|
168
|
+
) !== null;
|
|
169
|
+
const commandDispatchCfg =
|
|
170
|
+
parsed.isAdmin === true && !isBncrNativeCommand
|
|
171
|
+
? mergeBncrOwnerAllowFromIntoConfig({ cfg, senderIdForContext })
|
|
172
|
+
: cfg;
|
|
192
173
|
|
|
193
174
|
if (!shouldDispatch) {
|
|
194
175
|
await wrapBncrInboundRecordSessionLabelCorrection({
|
|
@@ -435,13 +435,41 @@ export function executeSceneAdminCommand(args: {
|
|
|
435
435
|
defaultAdminAgentId: string;
|
|
436
436
|
defaultPublicAgentId: string;
|
|
437
437
|
now: () => number;
|
|
438
|
+
/**
|
|
439
|
+
* When true, non-admin callers in direct chat are allowed to execute
|
|
440
|
+
* self-admin commands (history-*, download-media) scoped to
|
|
441
|
+
* their own session only. The flag is set by the caller after an
|
|
442
|
+
* explicit allow-list check – it must NOT be granted unconditionally
|
|
443
|
+
* for direct chat.
|
|
444
|
+
*/
|
|
445
|
+
allowNonAdminSelfAdmin?: boolean;
|
|
438
446
|
}): { ok: true; text: string } | { ok: false; text: string } {
|
|
439
|
-
const {
|
|
440
|
-
|
|
441
|
-
|
|
447
|
+
const {
|
|
448
|
+
parsed,
|
|
449
|
+
command,
|
|
450
|
+
sceneRegistry,
|
|
451
|
+
defaultAdminAgentId,
|
|
452
|
+
defaultPublicAgentId,
|
|
453
|
+
now,
|
|
454
|
+
allowNonAdminSelfAdmin,
|
|
455
|
+
} = args;
|
|
456
|
+
|
|
457
|
+
// Admin always passes; non-admin only passes when the caller explicitly
|
|
458
|
+
// opted in via allowNonAdminSelfAdmin (self-admin commands in direct chat).
|
|
459
|
+
if (!parsed.isAdmin && !allowNonAdminSelfAdmin) {
|
|
442
460
|
return { ok: false, text: 'Admin permission required.' };
|
|
443
461
|
}
|
|
444
462
|
|
|
463
|
+
// Non-admin self-admin: enforce that sceneId targets only the caller's own session.
|
|
464
|
+
if (allowNonAdminSelfAdmin && !parsed.isAdmin) {
|
|
465
|
+
const currentSceneKey = resolveCurrentSceneKey(parsed);
|
|
466
|
+
const cmdSceneKey =
|
|
467
|
+
'sceneKey' in command ? (command as { sceneKey?: string }).sceneKey : undefined;
|
|
468
|
+
if (cmdSceneKey && currentSceneKey && cmdSceneKey !== currentSceneKey) {
|
|
469
|
+
return { ok: false, text: 'You can only manage your own private chat session.' };
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
445
473
|
if (command.kind === 'history-help') {
|
|
446
474
|
return { ok: true, text: HISTORY_HELP_TEXT };
|
|
447
475
|
}
|
|
@@ -9,11 +9,7 @@ import {
|
|
|
9
9
|
buildBncrStructuredContextFactsFromInboundParts,
|
|
10
10
|
} from './context-facts.ts';
|
|
11
11
|
import type { BncrInboundApi, BncrInboundConfig, BncrInboundContextPayload } from './contracts.ts';
|
|
12
|
-
import {
|
|
13
|
-
type BncrConversationHistoryMap,
|
|
14
|
-
type BncrHistoryEntry,
|
|
15
|
-
collectBncrHistoryMediaFromEntries,
|
|
16
|
-
} from './conversation-history.ts';
|
|
12
|
+
import type { BncrConversationHistoryMap, BncrHistoryEntry } from './conversation-history.ts';
|
|
17
13
|
import type {
|
|
18
14
|
BncrInboundConversationResolution,
|
|
19
15
|
BncrPreparedInboundSessionContext,
|
|
@@ -193,9 +189,6 @@ export function buildBncrInboundTurnContext(args: {
|
|
|
193
189
|
const senderIsOwner = parsed.isAdmin === true || Boolean(ownerAllowFrom?.length);
|
|
194
190
|
const senderIsAuthorized = senderIsOwner;
|
|
195
191
|
const inboundHistory = undefined;
|
|
196
|
-
const contextMedia = shouldDispatch
|
|
197
|
-
? collectBncrHistoryMediaFromEntries({ entries: contextEntries })
|
|
198
|
-
: [];
|
|
199
192
|
const structuredContextEntries = shouldDispatch
|
|
200
193
|
? contextEntries.map((entry) => ({
|
|
201
194
|
messageId: entry.messageId,
|
|
@@ -271,7 +264,7 @@ export function buildBncrInboundTurnContext(args: {
|
|
|
271
264
|
kind: item.kind,
|
|
272
265
|
messageId: msgId ?? undefined,
|
|
273
266
|
}));
|
|
274
|
-
const media = dedupeBncrTurnMedia(
|
|
267
|
+
const media = dedupeBncrTurnMedia(currentTurnMedia);
|
|
275
268
|
|
|
276
269
|
return Promise.resolve(
|
|
277
270
|
resolveBncrChannelInboundRuntime(api).buildContext({
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { recordInboundSession as sdkRecordInboundSession } from 'openclaw/plugin-sdk/conversation-runtime';
|
|
2
2
|
import { resolvePinnedMainDmOwnerFromAllowlist as sdkResolvePinnedMainDmOwnerFromAllowlist } from 'openclaw/plugin-sdk/security-runtime';
|
|
3
3
|
import {
|
|
4
|
+
getSessionEntry as sdkGetSessionEntry,
|
|
4
5
|
recordSessionMetaFromInbound as sdkRecordSessionMetaFromInbound,
|
|
5
6
|
resolveStorePath as sdkResolveStorePath,
|
|
6
7
|
updateSessionStoreEntry as sdkUpdateSessionStoreEntry,
|
|
@@ -14,6 +15,7 @@ type ResolveStorePathFn = (
|
|
|
14
15
|
type RecordInboundSessionFn = typeof sdkRecordInboundSession;
|
|
15
16
|
type RecordSessionMetaFromInboundFn = typeof sdkRecordSessionMetaFromInbound;
|
|
16
17
|
type UpdateSessionStoreEntryFn = typeof sdkUpdateSessionStoreEntry;
|
|
18
|
+
type GetSessionEntryFn = typeof sdkGetSessionEntry;
|
|
17
19
|
type ReadSessionUpdatedAtFn = (params: { storePath: string; sessionKey: string }) => unknown;
|
|
18
20
|
type ResolvePinnedMainDmOwnerFromAllowlistFn = typeof sdkResolvePinnedMainDmOwnerFromAllowlist;
|
|
19
21
|
|
|
@@ -22,6 +24,7 @@ type BncrInboundSessionRuntime = {
|
|
|
22
24
|
recordInboundSession: RecordInboundSessionFn;
|
|
23
25
|
recordSessionMetaFromInbound: RecordSessionMetaFromInboundFn;
|
|
24
26
|
updateSessionStoreEntry: UpdateSessionStoreEntryFn;
|
|
27
|
+
getSessionEntry: GetSessionEntryFn;
|
|
25
28
|
readSessionUpdatedAt?: ReadSessionUpdatedAtFn;
|
|
26
29
|
resolvePinnedMainDmOwnerFromAllowlist: ResolvePinnedMainDmOwnerFromAllowlistFn;
|
|
27
30
|
};
|
|
@@ -36,6 +39,7 @@ function resolveRuntime(): BncrInboundSessionRuntime {
|
|
|
36
39
|
testRuntimeOverride?.recordSessionMetaFromInbound ?? sdkRecordSessionMetaFromInbound,
|
|
37
40
|
updateSessionStoreEntry:
|
|
38
41
|
testRuntimeOverride?.updateSessionStoreEntry ?? sdkUpdateSessionStoreEntry,
|
|
42
|
+
getSessionEntry: testRuntimeOverride?.getSessionEntry ?? sdkGetSessionEntry,
|
|
39
43
|
readSessionUpdatedAt: testRuntimeOverride?.readSessionUpdatedAt,
|
|
40
44
|
resolvePinnedMainDmOwnerFromAllowlist:
|
|
41
45
|
testRuntimeOverride?.resolvePinnedMainDmOwnerFromAllowlist ??
|
|
@@ -81,6 +85,19 @@ export function readBncrSessionUpdatedAt(
|
|
|
81
85
|
return readSessionUpdatedAt(params);
|
|
82
86
|
}
|
|
83
87
|
|
|
88
|
+
export async function readBncrSessionEntry(params: {
|
|
89
|
+
storePath: string;
|
|
90
|
+
sessionKey: string;
|
|
91
|
+
}): Promise<Record<string, unknown> | undefined> {
|
|
92
|
+
const runtime = resolveRuntime();
|
|
93
|
+
try {
|
|
94
|
+
const entry = await runtime.getSessionEntry(params);
|
|
95
|
+
return entry && typeof entry === 'object' ? (entry as Record<string, unknown>) : undefined;
|
|
96
|
+
} catch {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
84
101
|
export function resolveBncrPinnedMainDmOwnerFromAllowlist(
|
|
85
102
|
params: Parameters<ResolvePinnedMainDmOwnerFromAllowlistFn>[0],
|
|
86
103
|
): ReturnType<ResolvePinnedMainDmOwnerFromAllowlistFn> {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
|
+
|
|
5
|
+
type SessionResetResult = { ok?: boolean } | undefined;
|
|
6
|
+
type SessionResetParams = {
|
|
7
|
+
key: string;
|
|
8
|
+
reason: 'new' | 'reset';
|
|
9
|
+
agentId?: string;
|
|
10
|
+
commandSource?: string;
|
|
11
|
+
};
|
|
12
|
+
type SessionResetFn = (params: SessionResetParams) => Promise<SessionResetResult>;
|
|
13
|
+
|
|
14
|
+
let testOverride: SessionResetFn | null = null;
|
|
15
|
+
let resolvedSessionRuntimeUrlCache: string | null = null;
|
|
16
|
+
|
|
17
|
+
export function setBncrSessionResetRuntimeForTest(fn: SessionResetFn | null): () => void {
|
|
18
|
+
const previous = testOverride;
|
|
19
|
+
testOverride = fn;
|
|
20
|
+
return () => {
|
|
21
|
+
testOverride = previous;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function resolveOpenClawSessionsRuntimeUrl(): string {
|
|
26
|
+
if (resolvedSessionRuntimeUrlCache) return resolvedSessionRuntimeUrlCache;
|
|
27
|
+
let current = dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
let pluginRoot: string | null = null;
|
|
29
|
+
while (true) {
|
|
30
|
+
const pkgPath = join(current, 'package.json');
|
|
31
|
+
try {
|
|
32
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as { name?: string };
|
|
33
|
+
if (pkg.name === '@xmoxmo/bncr') {
|
|
34
|
+
pluginRoot = current;
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
// Keep walking up toward the plugin root.
|
|
39
|
+
}
|
|
40
|
+
const parent = dirname(current);
|
|
41
|
+
if (parent === current) break;
|
|
42
|
+
current = parent;
|
|
43
|
+
}
|
|
44
|
+
if (!pluginRoot) {
|
|
45
|
+
throw new Error('bncr plugin root not found for session reset runtime');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Try candidate paths for the sessions runtime re-export file.
|
|
49
|
+
// The hash-stamped bundle name may change across OpenClaw releases, so we
|
|
50
|
+
// first try the stable re-export entry and fall back to the known hash.
|
|
51
|
+
const candidates = [
|
|
52
|
+
join(pluginRoot, 'node_modules', 'openclaw', 'dist', 'sessions.runtime.js'),
|
|
53
|
+
join(pluginRoot, 'node_modules', 'openclaw', 'dist', 'sessions.runtime-BQwwkuFq.js'),
|
|
54
|
+
];
|
|
55
|
+
for (const target of candidates) {
|
|
56
|
+
if (existsSync(target)) {
|
|
57
|
+
resolvedSessionRuntimeUrlCache = pathToFileURL(target).href;
|
|
58
|
+
return resolvedSessionRuntimeUrlCache;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
throw new Error(
|
|
62
|
+
`OpenClaw sessions runtime not found under ${join(pluginRoot, 'node_modules', 'openclaw', 'dist')}`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function performBncrGatewaySessionReset(
|
|
67
|
+
params: SessionResetParams,
|
|
68
|
+
): Promise<SessionResetResult> {
|
|
69
|
+
if (testOverride) return testOverride(params);
|
|
70
|
+
const mod = (await import(resolveOpenClawSessionsRuntimeUrl())) as {
|
|
71
|
+
performGatewaySessionReset?: SessionResetFn;
|
|
72
|
+
};
|
|
73
|
+
if (typeof mod.performGatewaySessionReset !== 'function') {
|
|
74
|
+
throw new Error('OpenClaw performGatewaySessionReset is unavailable');
|
|
75
|
+
}
|
|
76
|
+
return mod.performGatewaySessionReset(params);
|
|
77
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { hasControlCommand } from 'openclaw/plugin-sdk/command-auth';
|
|
2
2
|
import { resolveAccount } from '../core/accounts.ts';
|
|
3
3
|
import { checkBncrMessageGate } from '../messaging/inbound/gate.ts';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
parseBncrNativeCommand,
|
|
6
|
+
resolveBncrNativeCommandParseOptions,
|
|
7
|
+
} from '../messaging/inbound/native-command.ts';
|
|
5
8
|
import type { parseBncrInboundParams } from '../messaging/inbound/parse.ts';
|
|
6
9
|
import type { OpenClawResolvedAgentRoute } from '../openclaw/routing-runtime.ts';
|
|
7
10
|
import type { buildInboundResponsePayload } from './channel-inbound-helpers.ts';
|
|
@@ -21,9 +24,13 @@ function resolveDispatchBy(args: {
|
|
|
21
24
|
if (!admission.allowed) return 'denied';
|
|
22
25
|
|
|
23
26
|
const isBncrNativeCommand =
|
|
24
|
-
parseBncrNativeCommand(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
parseBncrNativeCommand(
|
|
28
|
+
parsed.extracted.text,
|
|
29
|
+
resolveBncrNativeCommandParseOptions({
|
|
30
|
+
isAdmin: parsed.isAdmin,
|
|
31
|
+
peerKind: parsed.peer.kind as 'direct' | 'group',
|
|
32
|
+
}),
|
|
33
|
+
) !== null;
|
|
27
34
|
const isAdminOpenClawNativeCommand =
|
|
28
35
|
parsed.isAdmin === true &&
|
|
29
36
|
!isBncrNativeCommand &&
|
|
@@ -49,9 +56,13 @@ function shouldDispatchForScene(args: {
|
|
|
49
56
|
if (!admission.allowed) return false;
|
|
50
57
|
|
|
51
58
|
const isBncrNativeCommand =
|
|
52
|
-
parseBncrNativeCommand(
|
|
53
|
-
|
|
54
|
-
|
|
59
|
+
parseBncrNativeCommand(
|
|
60
|
+
parsed.extracted.text,
|
|
61
|
+
resolveBncrNativeCommandParseOptions({
|
|
62
|
+
isAdmin: parsed.isAdmin,
|
|
63
|
+
peerKind: parsed.peer.kind as 'direct' | 'group',
|
|
64
|
+
}),
|
|
65
|
+
) !== null;
|
|
55
66
|
const isAdminOpenClawNativeCommand =
|
|
56
67
|
parsed.isAdmin === true &&
|
|
57
68
|
!isBncrNativeCommand &&
|