@xmoxmo/bncr 0.4.7 → 0.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/package.json +1 -1
- package/src/messaging/inbound/commands.ts +185 -27
- package/src/messaging/inbound/native-command.ts +105 -5
- package/src/messaging/inbound/scene-admin.ts +37 -6
- package/src/plugin/channel-components.ts +1 -0
- package/src/plugin/inbound-handlers.ts +42 -0
- package/src/plugin/scene-registry.ts +19 -3
package/README.md
CHANGED
|
@@ -81,6 +81,14 @@ openclaw devices approve --latest
|
|
|
81
81
|
- 状态诊断
|
|
82
82
|
- 文件互传
|
|
83
83
|
|
|
84
|
+
补充口径:
|
|
85
|
+
|
|
86
|
+
- 入站会话按正式 scene / route 语义接入 OpenClaw,区分私聊与群聊会话。
|
|
87
|
+
- 非管理员私聊支持受控的本地命令能力:`/whoami`、`/status`、`/new`、`/reset`,以及对应的 `/bncr ...` 入口。
|
|
88
|
+
- 非管理员私聊中,不在支持范围内的 slash 命令会由 bncr 直接拒绝,不再 fallback 到 agent。
|
|
89
|
+
- 群聊里的管理型 / 控制型命令对非管理员按静默拒绝处理,不给用户可见回复。
|
|
90
|
+
- `/bncr list pending` 与 `/bncr list scenes` 采用按私聊 / 群聊分组的摘要展示格式,便于现场排查 scene 状态。
|
|
91
|
+
|
|
84
92
|
---
|
|
85
93
|
|
|
86
94
|
## 5. 架构定位
|
package/package.json
CHANGED
|
@@ -22,7 +22,10 @@ import type { ParsedInbound } from './dispatch-prep.ts';
|
|
|
22
22
|
import { buildBncrInboundRecordUpdateLastRoute } from './last-route.ts';
|
|
23
23
|
import {
|
|
24
24
|
parseBncrNativeCommand,
|
|
25
|
+
parseBncrUnsupportedDirectCommand,
|
|
25
26
|
resolveBncrNativeHelpCommand,
|
|
27
|
+
resolveBncrNativeSessionResetCommand,
|
|
28
|
+
resolveBncrNativeStatusCommand,
|
|
26
29
|
resolveBncrNativeVerboseCommand,
|
|
27
30
|
resolveBncrNativeWhoamiCommand,
|
|
28
31
|
} from './native-command.ts';
|
|
@@ -129,7 +132,48 @@ export async function handleBncrNativeCommand(params: {
|
|
|
129
132
|
const { accountId, route, peer, clientId, extracted, msgId } = parsed;
|
|
130
133
|
const command = parseBncrNativeCommand(extracted.text, {
|
|
131
134
|
allowBareWhoami: parsed.isAdmin !== true,
|
|
135
|
+
allowBareStatus: parsed.isAdmin !== true && parsed.peer.kind === 'direct',
|
|
136
|
+
allowBareSessionReset: parsed.isAdmin !== true && parsed.peer.kind === 'direct',
|
|
132
137
|
});
|
|
138
|
+
if (!command && parsed.peer.kind === 'direct') {
|
|
139
|
+
const unsupportedDirectCommand = parseBncrUnsupportedDirectCommand(extracted.text);
|
|
140
|
+
if (unsupportedDirectCommand) {
|
|
141
|
+
const rejectedRoute = buildBncrNativeCommandResolvedRoute({
|
|
142
|
+
api,
|
|
143
|
+
cfg,
|
|
144
|
+
channelId,
|
|
145
|
+
accountId,
|
|
146
|
+
peer,
|
|
147
|
+
resolvedAgentId,
|
|
148
|
+
});
|
|
149
|
+
const { baseSessionKey, sessionKey } = buildBncrNativeCommandSessionState({
|
|
150
|
+
parsed,
|
|
151
|
+
sessionAgentId: rejectedRoute.agentId || canonicalAgentId,
|
|
152
|
+
resolvedRoute: rejectedRoute,
|
|
153
|
+
});
|
|
154
|
+
const displayTo = formatDisplayScope(route);
|
|
155
|
+
logBncrNativeCommandSummary(
|
|
156
|
+
buildBncrNativeCommandSummary({
|
|
157
|
+
kind: 'unsupported',
|
|
158
|
+
command: unsupportedDirectCommand.command,
|
|
159
|
+
accountId,
|
|
160
|
+
to: displayTo,
|
|
161
|
+
msgId: msgId || null,
|
|
162
|
+
result: 'rejected',
|
|
163
|
+
}),
|
|
164
|
+
);
|
|
165
|
+
await enqueueFromReply({
|
|
166
|
+
accountId,
|
|
167
|
+
sessionKey: baseSessionKey,
|
|
168
|
+
route,
|
|
169
|
+
payload: {
|
|
170
|
+
text: `Unsupported private-chat command: /${unsupportedDirectCommand.command}`,
|
|
171
|
+
replyToId: msgId || undefined,
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
return { handled: true, command: unsupportedDirectCommand.command, sessionKey };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
133
177
|
if (!command) return { handled: false };
|
|
134
178
|
const nativeCommandDebugEnabled = resolveNativeCommandDebugEnabled({ cfg, channelId });
|
|
135
179
|
|
|
@@ -213,6 +257,16 @@ export async function handleBncrNativeCommand(params: {
|
|
|
213
257
|
isGroup: parsed.isGroup,
|
|
214
258
|
isAdmin: parsed.isAdmin,
|
|
215
259
|
});
|
|
260
|
+
const nativeStatus = resolveBncrNativeStatusCommand({
|
|
261
|
+
command,
|
|
262
|
+
accountId,
|
|
263
|
+
platform: parsed.platform,
|
|
264
|
+
userId: parsed.userId,
|
|
265
|
+
userName: parsed.userName,
|
|
266
|
+
resolvedAgentId: resolvedRoute.agentId || canonicalAgentId,
|
|
267
|
+
sessionKey,
|
|
268
|
+
});
|
|
269
|
+
const nativeSessionReset = resolveBncrNativeSessionResetCommand({ command });
|
|
216
270
|
if (nativeHelp) {
|
|
217
271
|
logBncrNativeCommandSummary(
|
|
218
272
|
buildBncrNativeCommandSummary({
|
|
@@ -273,11 +327,42 @@ export async function handleBncrNativeCommand(params: {
|
|
|
273
327
|
return { handled: true, command: command.command, sessionKey };
|
|
274
328
|
}
|
|
275
329
|
|
|
276
|
-
if (
|
|
277
|
-
|
|
330
|
+
if (nativeStatus) {
|
|
331
|
+
logBncrNativeCommandSummary(
|
|
332
|
+
buildBncrNativeCommandSummary({
|
|
333
|
+
kind: 'status',
|
|
334
|
+
command: command.command,
|
|
335
|
+
accountId,
|
|
336
|
+
to: displayTo,
|
|
337
|
+
msgId: msgId || null,
|
|
338
|
+
result: 'handled',
|
|
339
|
+
}),
|
|
340
|
+
);
|
|
341
|
+
await recordAndPatchBncrInboundSessionEntry({
|
|
342
|
+
storePath,
|
|
343
|
+
sessionKey,
|
|
344
|
+
ctx: ctxPayload,
|
|
345
|
+
patch: sessionIdentityPatch,
|
|
346
|
+
});
|
|
347
|
+
rememberSessionRoute(baseSessionKey, accountId, route);
|
|
348
|
+
await enqueueFromReply({
|
|
349
|
+
accountId,
|
|
350
|
+
sessionKey,
|
|
351
|
+
route,
|
|
352
|
+
payload: {
|
|
353
|
+
text: nativeStatus.text,
|
|
354
|
+
replyToId: msgId || undefined,
|
|
355
|
+
},
|
|
356
|
+
});
|
|
357
|
+
return { handled: true, command: command.command, sessionKey };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (nativeSessionReset) {
|
|
361
|
+
const allowLocalSessionReset = parsed.peer.kind === 'direct';
|
|
362
|
+
if (!allowLocalSessionReset) {
|
|
278
363
|
logBncrNativeCommandSummary(
|
|
279
364
|
buildBncrNativeCommandSummary({
|
|
280
|
-
kind:
|
|
365
|
+
kind: nativeSessionReset.reason,
|
|
281
366
|
command: command.command,
|
|
282
367
|
accountId,
|
|
283
368
|
to: displayTo,
|
|
@@ -292,15 +377,83 @@ export async function handleBncrNativeCommand(params: {
|
|
|
292
377
|
patch: sessionIdentityPatch,
|
|
293
378
|
});
|
|
294
379
|
rememberSessionRoute(baseSessionKey, accountId, route);
|
|
295
|
-
|
|
380
|
+
return { handled: true, command: command.command, sessionKey };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const requestedAgentId = resolvedRoute.agentId || canonicalAgentId;
|
|
384
|
+
const requestApi = api as BncrInboundApi & {
|
|
385
|
+
request?: (method: string, params: Record<string, unknown>) => Promise<unknown>;
|
|
386
|
+
};
|
|
387
|
+
if (typeof requestApi.request !== 'function') {
|
|
388
|
+
throw new Error('OpenClaw plugin api.request is unavailable for sessions.reset');
|
|
389
|
+
}
|
|
390
|
+
await recordAndPatchBncrInboundSessionEntry({
|
|
391
|
+
storePath,
|
|
392
|
+
sessionKey,
|
|
393
|
+
ctx: ctxPayload,
|
|
394
|
+
patch: sessionIdentityPatch,
|
|
395
|
+
});
|
|
396
|
+
const resetResult = (await requestApi.request('sessions.reset', {
|
|
397
|
+
key: baseSessionKey,
|
|
398
|
+
reason: nativeSessionReset.reason,
|
|
399
|
+
...(requestedAgentId ? { agentId: requestedAgentId } : {}),
|
|
400
|
+
})) as { ok?: boolean } | undefined;
|
|
401
|
+
if (resetResult?.ok === false) {
|
|
402
|
+
throw new Error(`sessions.reset failed for ${baseSessionKey}`);
|
|
403
|
+
}
|
|
404
|
+
logBncrNativeCommandSummary(
|
|
405
|
+
buildBncrNativeCommandSummary({
|
|
406
|
+
kind: nativeSessionReset.reason,
|
|
407
|
+
command: command.command,
|
|
296
408
|
accountId,
|
|
409
|
+
to: displayTo,
|
|
410
|
+
msgId: msgId || null,
|
|
411
|
+
result: 'handled',
|
|
412
|
+
}),
|
|
413
|
+
);
|
|
414
|
+
rememberSessionRoute(baseSessionKey, accountId, route);
|
|
415
|
+
await enqueueFromReply({
|
|
416
|
+
accountId,
|
|
417
|
+
sessionKey: baseSessionKey,
|
|
418
|
+
route,
|
|
419
|
+
payload: {
|
|
420
|
+
text: nativeSessionReset.text,
|
|
421
|
+
replyToId: msgId || undefined,
|
|
422
|
+
},
|
|
423
|
+
});
|
|
424
|
+
return { handled: true, command: command.command, sessionKey: baseSessionKey };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (nativeVerbose) {
|
|
428
|
+
if (!parsed.isAdmin) {
|
|
429
|
+
logBncrNativeCommandSummary(
|
|
430
|
+
buildBncrNativeCommandSummary({
|
|
431
|
+
kind: 'verbose',
|
|
432
|
+
command: command.command,
|
|
433
|
+
accountId,
|
|
434
|
+
to: displayTo,
|
|
435
|
+
msgId: msgId || null,
|
|
436
|
+
result: 'rejected',
|
|
437
|
+
}),
|
|
438
|
+
);
|
|
439
|
+
await recordAndPatchBncrInboundSessionEntry({
|
|
440
|
+
storePath,
|
|
297
441
|
sessionKey,
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
text: 'Admin permission required.',
|
|
301
|
-
replyToId: msgId || undefined,
|
|
302
|
-
},
|
|
442
|
+
ctx: ctxPayload,
|
|
443
|
+
patch: sessionIdentityPatch,
|
|
303
444
|
});
|
|
445
|
+
rememberSessionRoute(baseSessionKey, accountId, route);
|
|
446
|
+
if (parsed.peer.kind !== 'group') {
|
|
447
|
+
await enqueueFromReply({
|
|
448
|
+
accountId,
|
|
449
|
+
sessionKey,
|
|
450
|
+
route,
|
|
451
|
+
payload: {
|
|
452
|
+
text: 'Admin permission required.',
|
|
453
|
+
replyToId: msgId || undefined,
|
|
454
|
+
},
|
|
455
|
+
});
|
|
456
|
+
}
|
|
304
457
|
return { handled: true, command: command.command, sessionKey };
|
|
305
458
|
}
|
|
306
459
|
|
|
@@ -350,6 +503,7 @@ export async function handleBncrNativeCommand(params: {
|
|
|
350
503
|
|
|
351
504
|
const sceneAdmin = parseSceneAdminCommand(command);
|
|
352
505
|
if (sceneAdmin.matched) {
|
|
506
|
+
const silentNonAdminGroupReject = !parsed.isAdmin && parsed.peer.kind === 'group';
|
|
353
507
|
if (!sceneAdmin.valid) {
|
|
354
508
|
logBncrNativeCommandSummary(
|
|
355
509
|
buildBncrNativeCommandSummary({
|
|
@@ -367,15 +521,17 @@ export async function handleBncrNativeCommand(params: {
|
|
|
367
521
|
ctx: ctxPayload,
|
|
368
522
|
patch: sessionIdentityPatch,
|
|
369
523
|
});
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
524
|
+
if (!silentNonAdminGroupReject) {
|
|
525
|
+
await enqueueFromReply({
|
|
526
|
+
accountId,
|
|
527
|
+
sessionKey,
|
|
528
|
+
route,
|
|
529
|
+
payload: {
|
|
530
|
+
text: sceneAdmin.text,
|
|
531
|
+
replyToId: msgId || undefined,
|
|
532
|
+
},
|
|
533
|
+
});
|
|
534
|
+
}
|
|
379
535
|
return { handled: true, command: command.command, sessionKey };
|
|
380
536
|
}
|
|
381
537
|
|
|
@@ -403,15 +559,17 @@ export async function handleBncrNativeCommand(params: {
|
|
|
403
559
|
ctx: ctxPayload,
|
|
404
560
|
patch: sessionIdentityPatch,
|
|
405
561
|
});
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
562
|
+
if (!(silentNonAdminGroupReject && !outcome.ok)) {
|
|
563
|
+
await enqueueFromReply({
|
|
564
|
+
accountId,
|
|
565
|
+
sessionKey,
|
|
566
|
+
route,
|
|
567
|
+
payload: {
|
|
568
|
+
text: outcome.text,
|
|
569
|
+
replyToId: msgId || undefined,
|
|
570
|
+
},
|
|
571
|
+
});
|
|
572
|
+
}
|
|
415
573
|
return { handled: true, command: command.command, sessionKey };
|
|
416
574
|
}
|
|
417
575
|
|
|
@@ -7,6 +7,8 @@ export type NativeCommand = {
|
|
|
7
7
|
|
|
8
8
|
export type ParseBncrNativeCommandOptions = {
|
|
9
9
|
allowBareWhoami?: boolean;
|
|
10
|
+
allowBareStatus?: boolean;
|
|
11
|
+
allowBareSessionReset?: boolean;
|
|
10
12
|
};
|
|
11
13
|
|
|
12
14
|
export type NativeVerboseCommand = {
|
|
@@ -25,6 +27,17 @@ export type NativeWhoamiCommand = {
|
|
|
25
27
|
text: string;
|
|
26
28
|
};
|
|
27
29
|
|
|
30
|
+
export type NativeStatusCommand = {
|
|
31
|
+
handled: true;
|
|
32
|
+
text: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type NativeSessionResetCommand = {
|
|
36
|
+
handled: true;
|
|
37
|
+
reason: 'new' | 'reset';
|
|
38
|
+
text: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
28
41
|
const BNCR_HELP_TEXT = [
|
|
29
42
|
'🦞 Bncr command usage',
|
|
30
43
|
'',
|
|
@@ -33,19 +46,22 @@ const BNCR_HELP_TEXT = [
|
|
|
33
46
|
' • /bncr verbose on|off|full',
|
|
34
47
|
'',
|
|
35
48
|
'🛡 Scene approval',
|
|
36
|
-
' • /bncr allow [<
|
|
37
|
-
' • /bncr deny [<
|
|
38
|
-
' • /bncr bind <agentId> [<
|
|
49
|
+
' • /bncr allow [<SceneId>]',
|
|
50
|
+
' • /bncr deny [<SceneId>]',
|
|
51
|
+
' • /bncr bind <agentId> [<SceneId>]',
|
|
39
52
|
' • /bncr mode',
|
|
40
53
|
' • /bncr mode help',
|
|
41
|
-
' • /bncr mode <admin|mention|hybrid|all> [<
|
|
42
|
-
' • /bncr revoke [<
|
|
54
|
+
' • /bncr mode <admin|mention|hybrid|all> [<SceneId>]',
|
|
55
|
+
' • /bncr revoke [<SceneId>]',
|
|
43
56
|
' • /bncr list pending',
|
|
44
57
|
' • /bncr list scenes',
|
|
45
58
|
].join('\n');
|
|
46
59
|
|
|
47
60
|
const BNCR_NATIVE_COMMANDS = new Set([
|
|
48
61
|
'help',
|
|
62
|
+
'new',
|
|
63
|
+
'reset',
|
|
64
|
+
'status',
|
|
49
65
|
'whoami',
|
|
50
66
|
'verbose',
|
|
51
67
|
'allow',
|
|
@@ -56,12 +72,33 @@ const BNCR_NATIVE_COMMANDS = new Set([
|
|
|
56
72
|
'list',
|
|
57
73
|
]);
|
|
58
74
|
|
|
75
|
+
export const BNCR_DIRECT_ALLOWED_BARE_COMMANDS = new Set(['whoami', 'status', 'new', 'reset']);
|
|
76
|
+
|
|
77
|
+
export const BNCR_DIRECT_ALLOWED_SUBCOMMANDS = new Set([
|
|
78
|
+
'help',
|
|
79
|
+
'whoami',
|
|
80
|
+
'status',
|
|
81
|
+
'new',
|
|
82
|
+
'reset',
|
|
83
|
+
]);
|
|
84
|
+
|
|
59
85
|
export function parseBncrNativeCommand(
|
|
60
86
|
text: string,
|
|
61
87
|
options?: ParseBncrNativeCommandOptions,
|
|
62
88
|
): NativeCommand | null {
|
|
63
89
|
const raw = String(text || '').trim();
|
|
64
90
|
const allowBareWhoami = options?.allowBareWhoami !== false;
|
|
91
|
+
const allowBareStatus = options?.allowBareStatus === true;
|
|
92
|
+
const allowBareSessionReset = options?.allowBareSessionReset === true;
|
|
93
|
+
if (allowBareSessionReset && raw.toLowerCase() === '/new') {
|
|
94
|
+
return { command: 'new', raw, body: '/new', argsText: '' };
|
|
95
|
+
}
|
|
96
|
+
if (allowBareSessionReset && raw.toLowerCase() === '/reset') {
|
|
97
|
+
return { command: 'reset', raw, body: '/reset', argsText: '' };
|
|
98
|
+
}
|
|
99
|
+
if (allowBareStatus && raw.toLowerCase() === '/status') {
|
|
100
|
+
return { command: 'status', raw, body: '/status', argsText: '' };
|
|
101
|
+
}
|
|
65
102
|
if (allowBareWhoami && raw.toLowerCase() === '/whoami') {
|
|
66
103
|
return { command: 'whoami', raw, body: '/whoami', argsText: '' };
|
|
67
104
|
}
|
|
@@ -81,6 +118,34 @@ export function parseBncrNativeCommand(
|
|
|
81
118
|
return { command, raw, body, argsText };
|
|
82
119
|
}
|
|
83
120
|
|
|
121
|
+
export function parseBncrUnsupportedDirectCommand(text: string): {
|
|
122
|
+
command: string;
|
|
123
|
+
raw: string;
|
|
124
|
+
} | null {
|
|
125
|
+
const raw = String(text || '').trim();
|
|
126
|
+
if (!raw.startsWith('/')) return null;
|
|
127
|
+
|
|
128
|
+
const bareMatch = raw.match(/^\/([A-Za-z0-9_]+)(?:@[A-Za-z0-9_]+)?(?:\s+.*)?$/);
|
|
129
|
+
if (!bareMatch) return null;
|
|
130
|
+
const bareCommand = String(bareMatch[1] || '')
|
|
131
|
+
.trim()
|
|
132
|
+
.toLowerCase();
|
|
133
|
+
if (!bareCommand) return null;
|
|
134
|
+
|
|
135
|
+
if (bareCommand === 'bncr') {
|
|
136
|
+
const subMatch = raw.match(/^\/bncr(?:@[A-Za-z0-9_]+)?(?:\s+([A-Za-z0-9_-]+))?/i);
|
|
137
|
+
const subCommand =
|
|
138
|
+
String(subMatch?.[1] || '')
|
|
139
|
+
.trim()
|
|
140
|
+
.toLowerCase() || 'help';
|
|
141
|
+
if (BNCR_DIRECT_ALLOWED_SUBCOMMANDS.has(subCommand)) return null;
|
|
142
|
+
return { command: `bncr ${subCommand}`, raw };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (BNCR_DIRECT_ALLOWED_BARE_COMMANDS.has(bareCommand)) return null;
|
|
146
|
+
return { command: bareCommand, raw };
|
|
147
|
+
}
|
|
148
|
+
|
|
84
149
|
export function resolveBncrNativeHelpCommand(command: NativeCommand): NativeHelpCommand | null {
|
|
85
150
|
if (command.command !== 'help') return null;
|
|
86
151
|
return { handled: true, text: BNCR_HELP_TEXT };
|
|
@@ -110,6 +175,41 @@ export function resolveBncrNativeWhoamiCommand(args: {
|
|
|
110
175
|
return { handled: true, text: lines.join('\n') };
|
|
111
176
|
}
|
|
112
177
|
|
|
178
|
+
export function resolveBncrNativeStatusCommand(args: {
|
|
179
|
+
command: NativeCommand;
|
|
180
|
+
accountId: string;
|
|
181
|
+
platform: string;
|
|
182
|
+
userId: string;
|
|
183
|
+
userName?: string;
|
|
184
|
+
resolvedAgentId: string;
|
|
185
|
+
sessionKey: string;
|
|
186
|
+
}): NativeStatusCommand | null {
|
|
187
|
+
if (args.command.command !== 'status') return null;
|
|
188
|
+
const lines = ['🦞 Bncr Status', ''];
|
|
189
|
+
lines.push(`Channel: bncr`);
|
|
190
|
+
lines.push(`Account: ${args.accountId || 'Primary'}`);
|
|
191
|
+
lines.push(`User: ${args.userName || '(unknown)'} (${args.userId || '0'})`);
|
|
192
|
+
lines.push(`Scene: ${args.platform || '(unknown)'}:${args.userId || '0'}`);
|
|
193
|
+
lines.push(`Agent: ${args.resolvedAgentId || '(unknown)'}`);
|
|
194
|
+
lines.push(`SessionKey: ${args.sessionKey || '(unknown)'}`);
|
|
195
|
+
return { handled: true, text: lines.join('\n') };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function resolveBncrNativeSessionResetCommand(args: {
|
|
199
|
+
command: NativeCommand;
|
|
200
|
+
}): NativeSessionResetCommand | null {
|
|
201
|
+
if (args.command.command !== 'new' && args.command.command !== 'reset') return null;
|
|
202
|
+
const reason = args.command.command === 'new' ? 'new' : 'reset';
|
|
203
|
+
return {
|
|
204
|
+
handled: true,
|
|
205
|
+
reason,
|
|
206
|
+
text:
|
|
207
|
+
reason === 'new'
|
|
208
|
+
? 'Started a new session for this private chat.'
|
|
209
|
+
: 'Reset the current session for this private chat.',
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
113
213
|
export function resolveBncrNativeVerboseCommand(
|
|
114
214
|
command: NativeCommand,
|
|
115
215
|
): NativeVerboseCommand | null {
|
|
@@ -146,15 +146,46 @@ export function parseSceneAdminCommand(command: NativeCommand): ParsedSceneAdmin
|
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
function
|
|
149
|
+
function formatSceneDetailsLine(scene: BncrSceneRecord): string {
|
|
150
150
|
const idPart =
|
|
151
151
|
scene.kind === 'group' ? scene.groupId || scene.sceneKey : scene.userId || scene.sceneKey;
|
|
152
152
|
const namePart = scene.kind === 'group' ? scene.groupName || '' : scene.userName || '';
|
|
153
|
-
const agentPart = scene.agentId ? ` agent=${scene.agentId}` : '';
|
|
154
|
-
const modePart =
|
|
155
|
-
scene.kind === 'group' && scene.groupReplyMode ? ` mode=${scene.groupReplyMode}` : '';
|
|
156
153
|
const labelPart = namePart ? ` name=${namePart}` : '';
|
|
157
|
-
return
|
|
154
|
+
return ` Details: status=${scene.status} id=${idPart}${labelPart}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function formatSceneEntry(scene: BncrSceneRecord): string {
|
|
158
|
+
return [` SceneId: ${scene.sceneKey}`, formatSceneDetailsLine(scene)].join('\n');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function buildSceneGroupTitle(scene: BncrSceneRecord): string {
|
|
162
|
+
const agentId = normalizeToken(scene.agentId || '') || 'public';
|
|
163
|
+
if (scene.kind === 'group') {
|
|
164
|
+
const mode = normalizeToken(scene.groupReplyMode || '') || 'admin';
|
|
165
|
+
return `👥 Group Chat ${agentId} ${mode}`;
|
|
166
|
+
}
|
|
167
|
+
return `📱 Private Chat ${agentId}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function formatSceneGroups(scenes: BncrSceneRecord[]): string {
|
|
171
|
+
const directGrouped = new Map<string, string[]>();
|
|
172
|
+
const groupGrouped = new Map<string, string[]>();
|
|
173
|
+
for (const scene of scenes) {
|
|
174
|
+
const title = buildSceneGroupTitle(scene);
|
|
175
|
+
const target = scene.kind === 'group' ? groupGrouped : directGrouped;
|
|
176
|
+
const current = target.get(title) || [];
|
|
177
|
+
current.push(formatSceneEntry(scene));
|
|
178
|
+
target.set(title, current);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const orderedGroups = [
|
|
182
|
+
...Array.from(directGrouped.entries()),
|
|
183
|
+
...Array.from(groupGrouped.entries()).sort(([left], [right]) => left.localeCompare(right)),
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
return orderedGroups
|
|
187
|
+
.map(([title, entries]) => [title, '', entries.join('\n\n')].join('\n'))
|
|
188
|
+
.join('\n\n');
|
|
158
189
|
}
|
|
159
190
|
|
|
160
191
|
function applySceneStatus(scene: BncrSceneRecord, status: BncrSceneStatus): BncrSceneRecord {
|
|
@@ -188,7 +219,7 @@ export function executeSceneAdminCommand(args: {
|
|
|
188
219
|
text: command.scope === 'pending' ? 'No pending scenes.' : 'No scenes recorded.',
|
|
189
220
|
};
|
|
190
221
|
}
|
|
191
|
-
return { ok: true, text: scenes
|
|
222
|
+
return { ok: true, text: formatSceneGroups(scenes) };
|
|
192
223
|
}
|
|
193
224
|
|
|
194
225
|
if (command.kind === 'mode-help') {
|
|
@@ -266,6 +266,7 @@ export function createBncrInboundHandlersComponent(runtime: {
|
|
|
266
266
|
prepareInboundAcceptance: runtime.prepareInboundAcceptance,
|
|
267
267
|
formatDisplayScope,
|
|
268
268
|
logInboundSummary: runtime.logInboundSummary,
|
|
269
|
+
enqueueFromReply: runtime.enqueueFromReply,
|
|
269
270
|
flushOnInboundAccepted: (accountId) =>
|
|
270
271
|
runtime.flushPushQueueBestEffort({
|
|
271
272
|
accountId,
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { GatewayRequestHandlerOptions } from 'openclaw/plugin-sdk/core';
|
|
2
|
+
import { buildCanonicalBncrSessionKey } from '../core/targets.ts';
|
|
2
3
|
import type { BncrConnection, BncrRoute } from '../core/types.ts';
|
|
3
4
|
import type { BncrGroupHistoryMap } from '../messaging/inbound/group-history.ts';
|
|
4
5
|
import type { parseBncrInboundParams } from '../messaging/inbound/parse.ts';
|
|
6
|
+
import type { ReplyPayloadInput } from '../messaging/outbound/reply-enqueue.ts';
|
|
5
7
|
import type {
|
|
6
8
|
buildInboundResponsePayload,
|
|
7
9
|
resolveInboundSessionContext,
|
|
@@ -50,6 +52,26 @@ type InboundAcceptanceResult =
|
|
|
50
52
|
payload: Record<string, unknown>;
|
|
51
53
|
};
|
|
52
54
|
|
|
55
|
+
function buildBncrPendingDirectApprovalNotice(args: {
|
|
56
|
+
parsed: ParsedInboundParams;
|
|
57
|
+
reason: string;
|
|
58
|
+
}): string | null {
|
|
59
|
+
if (args.parsed.peer.kind !== 'direct') return null;
|
|
60
|
+
if (args.reason !== 'scene pending approval') return null;
|
|
61
|
+
return `This private chat is pending approval. Ask the administrator to allow your SceneId: ${args.parsed.platform}:${args.parsed.userId}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function buildBncrPendingDirectApprovalPayload(args: {
|
|
65
|
+
msgId?: string;
|
|
66
|
+
text: string;
|
|
67
|
+
}): ReplyPayloadInput {
|
|
68
|
+
return {
|
|
69
|
+
text: args.text,
|
|
70
|
+
kind: 'final',
|
|
71
|
+
...(args.msgId ? { replyToId: args.msgId } : {}),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
53
75
|
export type BncrInboundHandlersRuntime = {
|
|
54
76
|
channelId: string;
|
|
55
77
|
bridgeId: string;
|
|
@@ -123,6 +145,13 @@ export type BncrInboundHandlersRuntime = {
|
|
|
123
145
|
text: string;
|
|
124
146
|
hasMedia: boolean;
|
|
125
147
|
}) => void;
|
|
148
|
+
enqueueFromReply: (args: {
|
|
149
|
+
accountId: string;
|
|
150
|
+
sessionKey: string;
|
|
151
|
+
route: BncrRoute;
|
|
152
|
+
payload: ReplyPayloadInput;
|
|
153
|
+
mediaLocalRoots?: readonly string[];
|
|
154
|
+
}) => Promise<void>;
|
|
126
155
|
respond: GatewayRequestHandlerOptions['respond'];
|
|
127
156
|
flushOnInboundAccepted: (accountId: string) => void;
|
|
128
157
|
dispatchInbound: (args: {
|
|
@@ -227,6 +256,19 @@ export function createBncrInboundHandlers(runtime: Omit<BncrInboundHandlersRunti
|
|
|
227
256
|
'inbound',
|
|
228
257
|
`accept reject|accountId=${accountId}|msgId=${msgId ?? '-'}|scope=${runtime.formatDisplayScope(route)}|chatType=${peer.kind}|msgType=${msgType}|status=${acceptance.status}|reason=${reason || '-'}|isAdmin=${parsed.isAdmin === true}|shouldRespond=${parsed.shouldRespond === true}`,
|
|
229
258
|
);
|
|
259
|
+
const pendingNotice = buildBncrPendingDirectApprovalNotice({ parsed, reason });
|
|
260
|
+
if (pendingNotice) {
|
|
261
|
+
const sessionKey = buildCanonicalBncrSessionKey(route, canonicalAgentId);
|
|
262
|
+
await runtime.enqueueFromReply({
|
|
263
|
+
accountId,
|
|
264
|
+
sessionKey,
|
|
265
|
+
route,
|
|
266
|
+
payload: buildBncrPendingDirectApprovalPayload({
|
|
267
|
+
msgId: msgId ?? undefined,
|
|
268
|
+
text: pendingNotice,
|
|
269
|
+
}),
|
|
270
|
+
});
|
|
271
|
+
}
|
|
230
272
|
respond(acceptance.status, acceptance.payload);
|
|
231
273
|
return;
|
|
232
274
|
}
|
|
@@ -71,14 +71,20 @@ export function decideSceneAdmission(args: {
|
|
|
71
71
|
const sceneKey = buildSceneKey(parsed);
|
|
72
72
|
const kind = asSceneKind(parsed);
|
|
73
73
|
const existing = sceneRegistry.get(sceneKey);
|
|
74
|
+
const shouldForceDirectNonAdminPublic = (agentId?: string) =>
|
|
75
|
+
kind === 'direct' && parsed.isAdmin !== true && (!agentId || agentId === defaultAdminAgentId);
|
|
74
76
|
|
|
75
77
|
if (existing) {
|
|
78
|
+
const forcedAgentId = shouldForceDirectNonAdminPublic(existing.agentId)
|
|
79
|
+
? defaultPublicAgentId
|
|
80
|
+
: existing.agentId;
|
|
76
81
|
const nextScene = {
|
|
77
82
|
...existing,
|
|
78
83
|
...(parsed.userId ? { userId: parsed.userId } : {}),
|
|
79
84
|
...(parsed.userName ? { userName: parsed.userName } : {}),
|
|
80
85
|
...(kind === 'group' && parsed.groupId ? { groupId: parsed.groupId } : {}),
|
|
81
86
|
...(kind === 'group' && parsed.groupName ? { groupName: parsed.groupName } : {}),
|
|
87
|
+
...(forcedAgentId ? { agentId: forcedAgentId } : {}),
|
|
82
88
|
...(kind === 'group' && !existing.groupReplyMode
|
|
83
89
|
? { groupReplyMode: defaultGroupReplyMode(kind) }
|
|
84
90
|
: {}),
|
|
@@ -137,19 +143,29 @@ export function decideSceneAdmission(args: {
|
|
|
137
143
|
return { allowed: true, scene, agentId };
|
|
138
144
|
}
|
|
139
145
|
|
|
146
|
+
const directNonAdminAllowed = kind === 'direct';
|
|
140
147
|
const scene = buildSceneRecord({
|
|
141
148
|
parsed,
|
|
142
149
|
sceneKey,
|
|
143
150
|
kind,
|
|
144
|
-
status:
|
|
151
|
+
status: directNonAdminAllowed ? 'allowed' : 'denied',
|
|
152
|
+
agentId: directNonAdminAllowed ? defaultPublicAgentId : undefined,
|
|
145
153
|
lastSeenAt: now,
|
|
146
154
|
});
|
|
147
155
|
sceneRegistry.set(sceneKey, scene);
|
|
148
156
|
|
|
157
|
+
if (directNonAdminAllowed) {
|
|
158
|
+
return {
|
|
159
|
+
allowed: true,
|
|
160
|
+
scene,
|
|
161
|
+
agentId: scene.agentId || defaultPublicAgentId,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
149
165
|
return {
|
|
150
166
|
allowed: false,
|
|
151
167
|
scene,
|
|
152
|
-
reason:
|
|
153
|
-
replyPolicy:
|
|
168
|
+
reason: 'scene denied',
|
|
169
|
+
replyPolicy: 'silent',
|
|
154
170
|
};
|
|
155
171
|
}
|