@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.
- package/package.json +1 -1
- package/src/channel.ts +41 -2
- package/src/core/targets.ts +106 -17
- package/src/messaging/inbound/commands.ts +263 -51
- package/src/messaging/inbound/context-facts.ts +126 -14
- package/src/messaging/inbound/contracts.ts +24 -0
- package/src/messaging/inbound/dispatch-prep.ts +214 -39
- package/src/messaging/inbound/dispatch.ts +71 -5
- package/src/messaging/inbound/gate.ts +56 -86
- package/src/messaging/inbound/group-history.ts +189 -0
- package/src/messaging/inbound/native-command-runtime.ts +77 -61
- package/src/messaging/inbound/native-command.ts +92 -8
- package/src/messaging/inbound/parse.ts +113 -8
- package/src/messaging/inbound/reply-dispatch-serial.ts +62 -0
- package/src/messaging/inbound/reply-dispatch.ts +252 -77
- package/src/messaging/inbound/scene-admin.ts +269 -0
- package/src/messaging/inbound/session-label.ts +122 -13
- package/src/messaging/inbound/session-meta-task.ts +17 -0
- package/src/messaging/inbound/turn-context.ts +184 -71
- package/src/openclaw/channel-runtime-contracts.ts +1 -0
- package/src/plugin/channel-components.ts +34 -1
- package/src/plugin/channel-inbound-helpers.ts +9 -2
- package/src/plugin/channel-runtime-builders-delivery.ts +24 -1
- package/src/plugin/channel-runtime-types.ts +42 -0
- package/src/plugin/file-inbound-init.ts +27 -12
- package/src/plugin/file-inbound-runtime.ts +2 -0
- package/src/plugin/inbound-acceptance.ts +82 -1
- package/src/plugin/inbound-handlers.ts +55 -2
- package/src/plugin/inbound-surface-handlers-group.ts +16 -0
- package/src/plugin/messaging.ts +22 -5
- package/src/plugin/scene-registry.ts +155 -0
- package/src/plugin/state-store.ts +133 -0
- package/src/plugin/state-transient-runtime-group.ts +5 -0
- package/src/plugin/target-runtime.ts +2 -2
|
@@ -1,13 +1,66 @@
|
|
|
1
|
+
import { hasControlCommand } from 'openclaw/plugin-sdk/command-auth';
|
|
1
2
|
import { resolveAccount } from '../core/accounts.ts';
|
|
2
3
|
import { checkBncrMessageGate } from '../messaging/inbound/gate.ts';
|
|
4
|
+
import { parseBncrNativeCommand } from '../messaging/inbound/native-command.ts';
|
|
3
5
|
import type { parseBncrInboundParams } from '../messaging/inbound/parse.ts';
|
|
4
6
|
import type { OpenClawResolvedAgentRoute } from '../openclaw/routing-runtime.ts';
|
|
5
7
|
import type { buildInboundResponsePayload } from './channel-inbound-helpers.ts';
|
|
6
8
|
import { resolveInboundSessionContext } from './channel-inbound-helpers.ts';
|
|
7
|
-
import type { BncrChannelConfigRoot } from './channel-runtime-types.ts';
|
|
9
|
+
import type { BncrChannelConfigRoot, BncrSceneRecord } from './channel-runtime-types.ts';
|
|
10
|
+
import { decideSceneAdmission } from './scene-registry.ts';
|
|
8
11
|
|
|
9
12
|
type InboundAcceptanceResponsePayload = ReturnType<typeof buildInboundResponsePayload>;
|
|
10
13
|
|
|
14
|
+
function shouldDispatchForScene(args: {
|
|
15
|
+
parsed: ReturnType<typeof parseBncrInboundParams>;
|
|
16
|
+
admission: ReturnType<typeof decideSceneAdmission>;
|
|
17
|
+
cfg: BncrChannelConfigRoot;
|
|
18
|
+
}) {
|
|
19
|
+
const { parsed, admission, cfg } = args;
|
|
20
|
+
if (parsed.peer.kind === 'direct') return true;
|
|
21
|
+
if (!admission.allowed) return false;
|
|
22
|
+
|
|
23
|
+
const isBncrNativeCommand =
|
|
24
|
+
parseBncrNativeCommand(parsed.extracted.text, {
|
|
25
|
+
allowBareWhoami: parsed.isAdmin !== true,
|
|
26
|
+
}) !== null;
|
|
27
|
+
const isAdminOpenClawNativeCommand =
|
|
28
|
+
parsed.isAdmin === true &&
|
|
29
|
+
!isBncrNativeCommand &&
|
|
30
|
+
hasControlCommand(parsed.extracted.text, cfg as Parameters<typeof hasControlCommand>[1]);
|
|
31
|
+
if (isAdminOpenClawNativeCommand) return true;
|
|
32
|
+
|
|
33
|
+
const mode = admission.scene.groupReplyMode || 'admin';
|
|
34
|
+
if (mode === 'all') return true;
|
|
35
|
+
if (mode === 'admin') return parsed.isAdmin === true;
|
|
36
|
+
if (mode === 'mention') return parsed.shouldRespond === true;
|
|
37
|
+
return parsed.isAdmin === true || parsed.shouldRespond === true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function shouldAccumulateForScene(args: {
|
|
41
|
+
parsed: ReturnType<typeof parseBncrInboundParams>;
|
|
42
|
+
admission: ReturnType<typeof decideSceneAdmission>;
|
|
43
|
+
cfg: BncrChannelConfigRoot;
|
|
44
|
+
}) {
|
|
45
|
+
const { parsed, admission, cfg } = args;
|
|
46
|
+
if (parsed.peer.kind === 'direct') return true;
|
|
47
|
+
if (!admission.allowed) return false;
|
|
48
|
+
|
|
49
|
+
const isBncrNativeCommand =
|
|
50
|
+
parseBncrNativeCommand(parsed.extracted.text, {
|
|
51
|
+
allowBareWhoami: parsed.isAdmin !== true,
|
|
52
|
+
}) !== null;
|
|
53
|
+
const isAdminOpenClawNativeCommand =
|
|
54
|
+
parsed.isAdmin === true &&
|
|
55
|
+
!isBncrNativeCommand &&
|
|
56
|
+
hasControlCommand(parsed.extracted.text, cfg as Parameters<typeof hasControlCommand>[1]);
|
|
57
|
+
if (isAdminOpenClawNativeCommand) return true;
|
|
58
|
+
|
|
59
|
+
const mode = admission.scene.groupReplyMode || 'admin';
|
|
60
|
+
if (mode === 'admin') return parsed.isAdmin === true;
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
11
64
|
export async function prepareBncrInboundAcceptance(args: {
|
|
12
65
|
api: unknown;
|
|
13
66
|
parsed: ReturnType<typeof parseBncrInboundParams>;
|
|
@@ -27,6 +80,10 @@ export async function prepareBncrInboundAcceptance(args: {
|
|
|
27
80
|
| { kind: 'gate-denied'; accountId: string; msgId?: string | null; reason: string },
|
|
28
81
|
) => InboundAcceptanceResponsePayload;
|
|
29
82
|
markInboundDedupSeen: (key: string) => boolean;
|
|
83
|
+
sceneRegistry: Map<string, BncrSceneRecord>;
|
|
84
|
+
now: () => number;
|
|
85
|
+
defaultAdminAgentId: string;
|
|
86
|
+
defaultPublicAgentId: string;
|
|
30
87
|
}) {
|
|
31
88
|
const { parsed, canonicalAgentId } = args;
|
|
32
89
|
const {
|
|
@@ -83,6 +140,26 @@ export async function prepareBncrInboundAcceptance(args: {
|
|
|
83
140
|
};
|
|
84
141
|
}
|
|
85
142
|
|
|
143
|
+
const admission = decideSceneAdmission({
|
|
144
|
+
parsed,
|
|
145
|
+
now: args.now(),
|
|
146
|
+
sceneRegistry: args.sceneRegistry,
|
|
147
|
+
defaultAdminAgentId: args.defaultAdminAgentId,
|
|
148
|
+
defaultPublicAgentId: args.defaultPublicAgentId,
|
|
149
|
+
});
|
|
150
|
+
if (!admission.allowed) {
|
|
151
|
+
return {
|
|
152
|
+
ok: false as const,
|
|
153
|
+
status: admission.replyPolicy === 'pending',
|
|
154
|
+
payload: args.buildInboundResponsePayload({
|
|
155
|
+
kind: 'gate-denied',
|
|
156
|
+
accountId,
|
|
157
|
+
msgId: msgId ?? null,
|
|
158
|
+
reason: admission.reason,
|
|
159
|
+
}),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
86
163
|
const { sessionKey, inboundText } = resolveInboundSessionContext({
|
|
87
164
|
cfg,
|
|
88
165
|
accountId,
|
|
@@ -90,6 +167,7 @@ export async function prepareBncrInboundAcceptance(args: {
|
|
|
90
167
|
route,
|
|
91
168
|
sessionKeyFromRoute: sessionKeyfromroute,
|
|
92
169
|
canonicalAgentId,
|
|
170
|
+
resolvedAgentId: admission.agentId,
|
|
93
171
|
taskKey: extracted.taskKey ?? undefined,
|
|
94
172
|
text,
|
|
95
173
|
extractedText: extracted.text,
|
|
@@ -103,5 +181,8 @@ export async function prepareBncrInboundAcceptance(args: {
|
|
|
103
181
|
sessionKey,
|
|
104
182
|
inboundText,
|
|
105
183
|
hasMedia: Boolean(mediaBase64 || mediaPathFromTransfer),
|
|
184
|
+
resolvedAgentId: admission.agentId,
|
|
185
|
+
shouldDispatch: shouldDispatchForScene({ parsed, admission, cfg }),
|
|
186
|
+
shouldAccumulate: shouldAccumulateForScene({ parsed, admission, cfg }),
|
|
106
187
|
};
|
|
107
188
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { GatewayRequestHandlerOptions } from 'openclaw/plugin-sdk/core';
|
|
2
2
|
import type { BncrConnection, BncrRoute } from '../core/types.ts';
|
|
3
|
+
import type { BncrGroupHistoryMap } from '../messaging/inbound/group-history.ts';
|
|
3
4
|
import type { parseBncrInboundParams } from '../messaging/inbound/parse.ts';
|
|
4
5
|
import type {
|
|
5
6
|
buildInboundResponsePayload,
|
|
6
7
|
resolveInboundSessionContext,
|
|
7
8
|
} from './channel-inbound-helpers.ts';
|
|
8
|
-
import type { BncrChannelConfigRoot } from './channel-runtime-types.ts';
|
|
9
|
+
import type { BncrChannelConfigRoot, BncrSceneRecord } from './channel-runtime-types.ts';
|
|
9
10
|
import { buildBncrGatewayEventContext } from './gateway-event-context.ts';
|
|
10
11
|
|
|
11
12
|
type InboundLifecycleStage = 'accepted';
|
|
@@ -39,6 +40,9 @@ type InboundAcceptanceResult =
|
|
|
39
40
|
sessionKey: string;
|
|
40
41
|
inboundText: string;
|
|
41
42
|
hasMedia: boolean;
|
|
43
|
+
resolvedAgentId: string;
|
|
44
|
+
shouldDispatch: boolean;
|
|
45
|
+
shouldAccumulate: boolean;
|
|
42
46
|
}
|
|
43
47
|
| {
|
|
44
48
|
ok: false;
|
|
@@ -98,6 +102,15 @@ export type BncrInboundHandlersRuntime = {
|
|
|
98
102
|
peer: EnsureCanonicalAgentIdPeer;
|
|
99
103
|
channelId: string;
|
|
100
104
|
}) => string;
|
|
105
|
+
defaultAdminAgentId: (args: {
|
|
106
|
+
cfg: BncrChannelConfigRoot;
|
|
107
|
+
accountId: string;
|
|
108
|
+
peer: EnsureCanonicalAgentIdPeer;
|
|
109
|
+
channelId: string;
|
|
110
|
+
}) => string;
|
|
111
|
+
defaultPublicAgentId: () => string;
|
|
112
|
+
sceneRegistry: Map<string, BncrSceneRecord>;
|
|
113
|
+
groupHistories: BncrGroupHistoryMap;
|
|
101
114
|
prepareInboundAcceptance: (args: {
|
|
102
115
|
parsed: ParsedInboundParams;
|
|
103
116
|
canonicalAgentId: string;
|
|
@@ -116,6 +129,14 @@ export type BncrInboundHandlersRuntime = {
|
|
|
116
129
|
cfg: BncrChannelConfigRoot;
|
|
117
130
|
parsed: ParsedInboundParams;
|
|
118
131
|
canonicalAgentId: string;
|
|
132
|
+
resolvedAgentId: string;
|
|
133
|
+
shouldDispatch: boolean;
|
|
134
|
+
shouldAccumulate: boolean;
|
|
135
|
+
sceneRegistry: Map<string, BncrSceneRecord>;
|
|
136
|
+
groupHistories: BncrGroupHistoryMap;
|
|
137
|
+
defaultAdminAgentId: string;
|
|
138
|
+
defaultPublicAgentId: string;
|
|
139
|
+
now: () => number;
|
|
119
140
|
}) => Promise<unknown>;
|
|
120
141
|
};
|
|
121
142
|
|
|
@@ -191,13 +212,37 @@ export function createBncrInboundHandlers(runtime: Omit<BncrInboundHandlersRunti
|
|
|
191
212
|
peer,
|
|
192
213
|
channelId: runtime.channelId,
|
|
193
214
|
});
|
|
215
|
+
const defaultAdminAgentId = runtime.defaultAdminAgentId({
|
|
216
|
+
cfg,
|
|
217
|
+
accountId,
|
|
218
|
+
peer,
|
|
219
|
+
channelId: runtime.channelId,
|
|
220
|
+
});
|
|
194
221
|
const acceptance = await runtime.prepareInboundAcceptance({ parsed, canonicalAgentId });
|
|
195
222
|
if (!acceptance.ok) {
|
|
223
|
+
const reason = runtime
|
|
224
|
+
.asString((acceptance.payload as { reason?: unknown })?.reason || '')
|
|
225
|
+
.trim();
|
|
226
|
+
runtime.logInfo(
|
|
227
|
+
'inbound',
|
|
228
|
+
`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
|
+
);
|
|
196
230
|
respond(acceptance.status, acceptance.payload);
|
|
197
231
|
return;
|
|
198
232
|
}
|
|
199
233
|
|
|
200
|
-
const {
|
|
234
|
+
const {
|
|
235
|
+
sessionKey,
|
|
236
|
+
inboundText,
|
|
237
|
+
hasMedia,
|
|
238
|
+
resolvedAgentId,
|
|
239
|
+
shouldDispatch,
|
|
240
|
+
shouldAccumulate,
|
|
241
|
+
} = acceptance;
|
|
242
|
+
runtime.logInfo(
|
|
243
|
+
'inbound',
|
|
244
|
+
`accept ok|accountId=${accountId}|msgId=${msgId ?? '-'}|scope=${runtime.formatDisplayScope(route)}|chatType=${peer.kind}|msgType=${msgType}|sessionKey=${sessionKey}|agent=${resolvedAgentId}|dispatch=${shouldDispatch}|accumulate=${shouldAccumulate}|isAdmin=${parsed.isAdmin === true}|shouldRespond=${parsed.shouldRespond === true}`,
|
|
245
|
+
);
|
|
201
246
|
runtime.logInfo(
|
|
202
247
|
'inbound',
|
|
203
248
|
JSON.stringify({
|
|
@@ -239,6 +284,14 @@ export function createBncrInboundHandlers(runtime: Omit<BncrInboundHandlersRunti
|
|
|
239
284
|
cfg,
|
|
240
285
|
parsed,
|
|
241
286
|
canonicalAgentId,
|
|
287
|
+
resolvedAgentId,
|
|
288
|
+
shouldDispatch,
|
|
289
|
+
shouldAccumulate,
|
|
290
|
+
sceneRegistry: runtime.sceneRegistry,
|
|
291
|
+
groupHistories: runtime.groupHistories,
|
|
292
|
+
defaultAdminAgentId,
|
|
293
|
+
defaultPublicAgentId: runtime.defaultPublicAgentId(),
|
|
294
|
+
now: runtime.now,
|
|
242
295
|
})
|
|
243
296
|
.catch((err) => {
|
|
244
297
|
runtime.logError('inbound', `process failed: ${String(err)}`, { debugOnly: true });
|
|
@@ -53,6 +53,9 @@ export function createBncrInboundSurfaceHandlersGroup(runtime: {
|
|
|
53
53
|
clientId?: string;
|
|
54
54
|
context: GatewayRequestHandlerOptions['context'];
|
|
55
55
|
}) => void;
|
|
56
|
+
buildCanonicalSessionKey: Parameters<
|
|
57
|
+
typeof createBncrFileInboundHandlersComponent
|
|
58
|
+
>[0]['buildCanonicalSessionKey'];
|
|
56
59
|
refreshLiveConnectionState: (args: {
|
|
57
60
|
accountId: string;
|
|
58
61
|
connId: string;
|
|
@@ -75,6 +78,14 @@ export function createBncrInboundSurfaceHandlersGroup(runtime: {
|
|
|
75
78
|
ensureCanonicalAgentId: Parameters<
|
|
76
79
|
typeof createBncrInboundHandlersComponent
|
|
77
80
|
>[0]['ensureCanonicalAgentId'];
|
|
81
|
+
defaultAdminAgentId: Parameters<
|
|
82
|
+
typeof createBncrInboundHandlersComponent
|
|
83
|
+
>[0]['defaultAdminAgentId'];
|
|
84
|
+
defaultPublicAgentId: Parameters<
|
|
85
|
+
typeof createBncrInboundHandlersComponent
|
|
86
|
+
>[0]['defaultPublicAgentId'];
|
|
87
|
+
sceneRegistry: Parameters<typeof createBncrInboundHandlersComponent>[0]['sceneRegistry'];
|
|
88
|
+
groupHistories: Parameters<typeof createBncrInboundHandlersComponent>[0]['groupHistories'];
|
|
78
89
|
prepareInboundAcceptance: Parameters<
|
|
79
90
|
typeof createBncrInboundHandlersComponent
|
|
80
91
|
>[0]['prepareInboundAcceptance'];
|
|
@@ -111,6 +122,7 @@ export function createBncrInboundSurfaceHandlersGroup(runtime: {
|
|
|
111
122
|
matchesTransferOwner: runtime.matchesTransferOwner,
|
|
112
123
|
refreshAcceptedFileTransferLiveState: runtime.refreshAcceptedFileTransferLiveState,
|
|
113
124
|
logWarn: runtime.logWarn,
|
|
125
|
+
buildCanonicalSessionKey: runtime.buildCanonicalSessionKey,
|
|
114
126
|
fileRecvTransfers: runtime.fileRecvTransfers,
|
|
115
127
|
inboundFileTransferMaxBytes: runtime.inboundFileTransferMaxBytes,
|
|
116
128
|
inboundFileTransferMaxChunks: runtime.inboundFileTransferMaxChunks,
|
|
@@ -136,6 +148,10 @@ export function createBncrInboundSurfaceHandlersGroup(runtime: {
|
|
|
136
148
|
buildActiveConnectionDebugList: runtime.buildActiveConnectionDebugList,
|
|
137
149
|
markLastInboundAt: runtime.markLastInboundAt,
|
|
138
150
|
ensureCanonicalAgentId: runtime.ensureCanonicalAgentId,
|
|
151
|
+
defaultAdminAgentId: runtime.defaultAdminAgentId,
|
|
152
|
+
defaultPublicAgentId: runtime.defaultPublicAgentId,
|
|
153
|
+
sceneRegistry: runtime.sceneRegistry,
|
|
154
|
+
groupHistories: runtime.groupHistories,
|
|
139
155
|
prepareInboundAcceptance: runtime.prepareInboundAcceptance,
|
|
140
156
|
logInboundSummary: runtime.logInboundSummary,
|
|
141
157
|
flushPushQueueBestEffort: runtime.flushPushQueueBestEffort,
|
package/src/plugin/messaging.ts
CHANGED
|
@@ -10,6 +10,15 @@ import {
|
|
|
10
10
|
} from '../messaging/outbound/target-resolver.ts';
|
|
11
11
|
import type { BncrChannelConfigRoot } from './channel-runtime-types.ts';
|
|
12
12
|
|
|
13
|
+
function formatBncrHumanDisplay(route: BncrRoute): string {
|
|
14
|
+
const platform = asSanitizedString(route?.platform).trim();
|
|
15
|
+
const groupId = asSanitizedString(route?.groupId).trim();
|
|
16
|
+
const userId = asSanitizedString(route?.userId).trim();
|
|
17
|
+
if (platform && groupId && groupId !== '0') return `Bncr:${platform}:Group:${groupId}`;
|
|
18
|
+
if (platform && userId && userId !== '0') return `Bncr:${platform}:User:${userId}`;
|
|
19
|
+
return formatDisplayScope(route);
|
|
20
|
+
}
|
|
21
|
+
|
|
13
22
|
type BncrMessagingRuntimeBridge = {
|
|
14
23
|
canonicalAgentId?: string;
|
|
15
24
|
ensureCanonicalAgentId: (params: { cfg: BncrChannelConfigRoot; accountId: string }) => string;
|
|
@@ -69,16 +78,24 @@ export function normalizeBncrMessagingTarget(raw: string) {
|
|
|
69
78
|
}
|
|
70
79
|
|
|
71
80
|
export function formatBncrMessagingTargetDisplay({ target }: BncrMessagingTargetDisplayInput) {
|
|
72
|
-
if (typeof target === 'string')
|
|
81
|
+
if (typeof target === 'string') {
|
|
82
|
+
const parsed = parseExplicitTarget(asSanitizedString(target).trim());
|
|
83
|
+
return parsed?.route ? formatBncrHumanDisplay(parsed.route) : asSanitizedString(target).trim();
|
|
84
|
+
}
|
|
73
85
|
if (!isDisplayTarget(target)) return '';
|
|
74
86
|
const displayScope = asSanitizedString(target?.displayScope || target?.to).trim();
|
|
75
|
-
if (displayScope)
|
|
87
|
+
if (displayScope) {
|
|
88
|
+
const parsed = parseExplicitTarget(displayScope);
|
|
89
|
+
if (parsed?.route) return formatBncrHumanDisplay(parsed.route);
|
|
90
|
+
return displayScope;
|
|
91
|
+
}
|
|
76
92
|
if (target.platform || target.groupId || target.userId) {
|
|
77
|
-
|
|
93
|
+
const route = {
|
|
78
94
|
platform: asSanitizedString(target.platform).trim(),
|
|
79
95
|
groupId: asSanitizedString(target.groupId).trim(),
|
|
80
96
|
userId: asSanitizedString(target.userId).trim(),
|
|
81
|
-
}
|
|
97
|
+
};
|
|
98
|
+
return formatBncrHumanDisplay(route);
|
|
82
99
|
}
|
|
83
100
|
return '';
|
|
84
101
|
}
|
|
@@ -188,7 +205,7 @@ export function createBncrMessagingTargetResolver(getBridge: () => BncrMessaging
|
|
|
188
205
|
return {
|
|
189
206
|
to: resolved.displayScope,
|
|
190
207
|
kind: resolved.kind,
|
|
191
|
-
display: resolved.
|
|
208
|
+
display: formatBncrHumanDisplay(resolved.route),
|
|
192
209
|
source: 'normalized' as const,
|
|
193
210
|
};
|
|
194
211
|
},
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BncrGroupReplyMode,
|
|
3
|
+
BncrSceneKind,
|
|
4
|
+
BncrSceneRecord,
|
|
5
|
+
BncrSceneStatus,
|
|
6
|
+
} from './channel-runtime-types.ts';
|
|
7
|
+
|
|
8
|
+
type ParsedInbound = ReturnType<
|
|
9
|
+
typeof import('../messaging/inbound/parse.ts')['parseBncrInboundParams']
|
|
10
|
+
>;
|
|
11
|
+
|
|
12
|
+
export type BncrSceneRegistryDecision =
|
|
13
|
+
| {
|
|
14
|
+
allowed: true;
|
|
15
|
+
scene: BncrSceneRecord;
|
|
16
|
+
agentId: string;
|
|
17
|
+
}
|
|
18
|
+
| {
|
|
19
|
+
allowed: false;
|
|
20
|
+
scene: BncrSceneRecord;
|
|
21
|
+
reason: string;
|
|
22
|
+
replyPolicy: 'silent' | 'pending';
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function asSceneKind(parsed: ParsedInbound): BncrSceneKind {
|
|
26
|
+
return parsed.peer.kind === 'group' ? 'group' : 'direct';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function defaultGroupReplyMode(kind: BncrSceneKind): BncrGroupReplyMode | undefined {
|
|
30
|
+
return kind === 'group' ? 'admin' : undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function buildSceneKey(parsed: ParsedInbound): string {
|
|
34
|
+
return parsed.peer.kind === 'group'
|
|
35
|
+
? `${parsed.platform}:${parsed.groupId}`
|
|
36
|
+
: `${parsed.platform}:${parsed.userId}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function buildSceneRecord(args: {
|
|
40
|
+
parsed: ParsedInbound;
|
|
41
|
+
sceneKey: string;
|
|
42
|
+
kind: BncrSceneKind;
|
|
43
|
+
status: BncrSceneStatus;
|
|
44
|
+
agentId?: string;
|
|
45
|
+
lastSeenAt: number;
|
|
46
|
+
}): BncrSceneRecord {
|
|
47
|
+
const { parsed, sceneKey, kind, status, agentId, lastSeenAt } = args;
|
|
48
|
+
return {
|
|
49
|
+
sceneKey,
|
|
50
|
+
kind,
|
|
51
|
+
status,
|
|
52
|
+
platform: parsed.platform,
|
|
53
|
+
...(parsed.userId ? { userId: parsed.userId } : {}),
|
|
54
|
+
...(parsed.userName ? { userName: parsed.userName } : {}),
|
|
55
|
+
...(kind === 'group' && parsed.groupId ? { groupId: parsed.groupId } : {}),
|
|
56
|
+
...(kind === 'group' && parsed.groupName ? { groupName: parsed.groupName } : {}),
|
|
57
|
+
...(agentId ? { agentId } : {}),
|
|
58
|
+
...(kind === 'group' ? { groupReplyMode: defaultGroupReplyMode(kind) } : {}),
|
|
59
|
+
lastSeenAt,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function decideSceneAdmission(args: {
|
|
64
|
+
parsed: ParsedInbound;
|
|
65
|
+
now: number;
|
|
66
|
+
sceneRegistry: Map<string, BncrSceneRecord>;
|
|
67
|
+
defaultAdminAgentId: string;
|
|
68
|
+
defaultPublicAgentId: string;
|
|
69
|
+
}): BncrSceneRegistryDecision {
|
|
70
|
+
const { parsed, now, sceneRegistry, defaultAdminAgentId, defaultPublicAgentId } = args;
|
|
71
|
+
const sceneKey = buildSceneKey(parsed);
|
|
72
|
+
const kind = asSceneKind(parsed);
|
|
73
|
+
const existing = sceneRegistry.get(sceneKey);
|
|
74
|
+
|
|
75
|
+
if (existing) {
|
|
76
|
+
const nextScene = {
|
|
77
|
+
...existing,
|
|
78
|
+
...(parsed.userId ? { userId: parsed.userId } : {}),
|
|
79
|
+
...(parsed.userName ? { userName: parsed.userName } : {}),
|
|
80
|
+
...(kind === 'group' && parsed.groupId ? { groupId: parsed.groupId } : {}),
|
|
81
|
+
...(kind === 'group' && parsed.groupName ? { groupName: parsed.groupName } : {}),
|
|
82
|
+
...(kind === 'group' && !existing.groupReplyMode
|
|
83
|
+
? { groupReplyMode: defaultGroupReplyMode(kind) }
|
|
84
|
+
: {}),
|
|
85
|
+
lastSeenAt: now,
|
|
86
|
+
} satisfies BncrSceneRecord;
|
|
87
|
+
|
|
88
|
+
if (kind === 'group' && nextScene.status === 'denied' && parsed.isAdmin) {
|
|
89
|
+
const allowedScene = {
|
|
90
|
+
...nextScene,
|
|
91
|
+
status: 'allowed',
|
|
92
|
+
} satisfies BncrSceneRecord;
|
|
93
|
+
sceneRegistry.set(sceneKey, allowedScene);
|
|
94
|
+
return {
|
|
95
|
+
allowed: true,
|
|
96
|
+
scene: allowedScene,
|
|
97
|
+
agentId: allowedScene.agentId || defaultPublicAgentId,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
sceneRegistry.set(sceneKey, nextScene);
|
|
102
|
+
|
|
103
|
+
if (nextScene.status === 'allowed') {
|
|
104
|
+
const fallbackAgentId =
|
|
105
|
+
kind === 'group'
|
|
106
|
+
? defaultPublicAgentId
|
|
107
|
+
: parsed.isAdmin
|
|
108
|
+
? defaultAdminAgentId
|
|
109
|
+
: defaultPublicAgentId;
|
|
110
|
+
return {
|
|
111
|
+
allowed: true,
|
|
112
|
+
scene: nextScene,
|
|
113
|
+
agentId: nextScene.agentId || fallbackAgentId,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
allowed: false,
|
|
119
|
+
scene: nextScene,
|
|
120
|
+
reason: nextScene.status === 'pending' ? 'scene pending approval' : 'scene denied',
|
|
121
|
+
replyPolicy:
|
|
122
|
+
kind === 'group' ? 'silent' : nextScene.status === 'pending' ? 'pending' : 'silent',
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (parsed.isAdmin) {
|
|
127
|
+
const agentId = kind === 'group' ? defaultPublicAgentId : defaultAdminAgentId;
|
|
128
|
+
const scene = buildSceneRecord({
|
|
129
|
+
parsed,
|
|
130
|
+
sceneKey,
|
|
131
|
+
kind,
|
|
132
|
+
status: 'allowed',
|
|
133
|
+
agentId,
|
|
134
|
+
lastSeenAt: now,
|
|
135
|
+
});
|
|
136
|
+
sceneRegistry.set(sceneKey, scene);
|
|
137
|
+
return { allowed: true, scene, agentId };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const scene = buildSceneRecord({
|
|
141
|
+
parsed,
|
|
142
|
+
sceneKey,
|
|
143
|
+
kind,
|
|
144
|
+
status: kind === 'direct' ? 'pending' : 'denied',
|
|
145
|
+
lastSeenAt: now,
|
|
146
|
+
});
|
|
147
|
+
sceneRegistry.set(sceneKey, scene);
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
allowed: false,
|
|
151
|
+
scene,
|
|
152
|
+
reason: kind === 'direct' ? 'scene pending approval' : 'scene denied',
|
|
153
|
+
replyPolicy: kind === 'direct' ? 'pending' : 'silent',
|
|
154
|
+
};
|
|
155
|
+
}
|
|
@@ -8,16 +8,25 @@ import {
|
|
|
8
8
|
writeOpenClawJsonFileAtomically,
|
|
9
9
|
} from '../openclaw/sdk-helpers.ts';
|
|
10
10
|
import type {
|
|
11
|
+
BncrGroupReplyMode,
|
|
11
12
|
BncrPersistedAccountTimestamp,
|
|
13
|
+
BncrPersistedGroupHistoryBucket,
|
|
14
|
+
BncrPersistedGroupHistoryEntry,
|
|
15
|
+
BncrPersistedGroupHistoryMediaEntry,
|
|
12
16
|
BncrPersistedLastSession,
|
|
13
17
|
BncrPersistedSessionRoute,
|
|
14
18
|
PersistedState as BncrPersistedState,
|
|
19
|
+
BncrSceneRecord,
|
|
15
20
|
} from './channel-runtime-types.ts';
|
|
16
21
|
|
|
22
|
+
const GROUP_REPLY_MODES = new Set<BncrGroupReplyMode>(['admin', 'mention', 'hybrid', 'all']);
|
|
23
|
+
|
|
17
24
|
type BncrPersistedStateStoreInput = {
|
|
18
25
|
outbox?: unknown;
|
|
19
26
|
deadLetter?: unknown;
|
|
20
27
|
sessionRoutes?: unknown;
|
|
28
|
+
sceneRegistry?: unknown;
|
|
29
|
+
groupHistories?: unknown;
|
|
21
30
|
lastSessionByAccount?: unknown;
|
|
22
31
|
lastActivityByAccount?: unknown;
|
|
23
32
|
lastInboundByAccount?: unknown;
|
|
@@ -28,6 +37,9 @@ type BncrPersistedStateStoreInput = {
|
|
|
28
37
|
type PersistedAccountTimestampInput = Partial<BncrPersistedAccountTimestamp>;
|
|
29
38
|
type PersistedLastSessionInput = Partial<BncrPersistedLastSession>;
|
|
30
39
|
type PersistedSessionRouteInput = Partial<BncrPersistedSessionRoute>;
|
|
40
|
+
type PersistedSceneRecordInput = Partial<BncrSceneRecord>;
|
|
41
|
+
type PersistedGroupHistoryBucketInput = Partial<BncrPersistedGroupHistoryBucket>;
|
|
42
|
+
type PersistedGroupHistoryEntryInput = Partial<BncrPersistedGroupHistoryEntry>;
|
|
31
43
|
|
|
32
44
|
export function createBncrStateStore(runtime: {
|
|
33
45
|
getStatePath: () => string | null;
|
|
@@ -50,6 +62,8 @@ export function createBncrStateStore(runtime: {
|
|
|
50
62
|
maxDeadLetterEntries: number;
|
|
51
63
|
maxSessionRouteEntries: number;
|
|
52
64
|
maxAccountActivityEntries: number;
|
|
65
|
+
sceneRegistry: Map<string, BncrSceneRecord>;
|
|
66
|
+
groupHistories: Map<string, BncrPersistedGroupHistoryEntry[]>;
|
|
53
67
|
outbox: Map<string, OutboxEntry>;
|
|
54
68
|
getDeadLetter: () => OutboxEntry[];
|
|
55
69
|
setDeadLetter: (entries: OutboxEntry[]) => void;
|
|
@@ -62,6 +76,117 @@ export function createBncrStateStore(runtime: {
|
|
|
62
76
|
getLastDriftSnapshot: () => BncrPersistedState['lastDriftSnapshot'];
|
|
63
77
|
setLastDriftSnapshot: (value: BncrPersistedState['lastDriftSnapshot']) => void;
|
|
64
78
|
}) {
|
|
79
|
+
function loadPersistedSceneRegistry(persisted: unknown): void {
|
|
80
|
+
runtime.sceneRegistry.clear();
|
|
81
|
+
const items = Array.isArray(persisted) ? (persisted as PersistedSceneRecordInput[]) : [];
|
|
82
|
+
for (const item of items) {
|
|
83
|
+
if (!item || typeof item !== 'object') continue;
|
|
84
|
+
const sceneKey = runtime.asString(item.sceneKey || '').trim();
|
|
85
|
+
const kind = runtime.asString(item.kind || '').trim();
|
|
86
|
+
const status = runtime.asString(item.status || '').trim();
|
|
87
|
+
const platform = runtime.asString(item.platform || '').trim();
|
|
88
|
+
const lastSeenAt = runtime.finiteNumberOr(item.lastSeenAt, 0);
|
|
89
|
+
if (!sceneKey || !platform || lastSeenAt <= 0) continue;
|
|
90
|
+
if (kind !== 'direct' && kind !== 'group') continue;
|
|
91
|
+
if (status !== 'pending' && status !== 'allowed' && status !== 'denied') continue;
|
|
92
|
+
|
|
93
|
+
runtime.sceneRegistry.set(sceneKey, {
|
|
94
|
+
sceneKey,
|
|
95
|
+
kind,
|
|
96
|
+
status,
|
|
97
|
+
platform,
|
|
98
|
+
...(runtime.asString(item.userId || '').trim()
|
|
99
|
+
? { userId: runtime.asString(item.userId || '').trim() }
|
|
100
|
+
: {}),
|
|
101
|
+
...(runtime.asString(item.userName || '').trim()
|
|
102
|
+
? { userName: runtime.asString(item.userName || '').trim() }
|
|
103
|
+
: {}),
|
|
104
|
+
...(runtime.asString(item.groupId || '').trim()
|
|
105
|
+
? { groupId: runtime.asString(item.groupId || '').trim() }
|
|
106
|
+
: {}),
|
|
107
|
+
...(runtime.asString(item.groupName || '').trim()
|
|
108
|
+
? { groupName: runtime.asString(item.groupName || '').trim() }
|
|
109
|
+
: {}),
|
|
110
|
+
...(runtime.asString(item.agentId || '').trim()
|
|
111
|
+
? { agentId: runtime.asString(item.agentId || '').trim() }
|
|
112
|
+
: {}),
|
|
113
|
+
...(kind === 'group' &&
|
|
114
|
+
GROUP_REPLY_MODES.has(
|
|
115
|
+
runtime.asString(item.groupReplyMode || '').trim() as BncrGroupReplyMode,
|
|
116
|
+
)
|
|
117
|
+
? {
|
|
118
|
+
groupReplyMode: runtime
|
|
119
|
+
.asString(item.groupReplyMode || '')
|
|
120
|
+
.trim() as BncrGroupReplyMode,
|
|
121
|
+
}
|
|
122
|
+
: {}),
|
|
123
|
+
lastSeenAt,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function dumpPersistedSceneRegistry() {
|
|
129
|
+
return Array.from(runtime.sceneRegistry.values()).sort((a, b) => a.lastSeenAt - b.lastSeenAt);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function loadPersistedGroupHistories(persisted: unknown): void {
|
|
133
|
+
runtime.groupHistories.clear();
|
|
134
|
+
const buckets = Array.isArray(persisted)
|
|
135
|
+
? (persisted as PersistedGroupHistoryBucketInput[])
|
|
136
|
+
: [];
|
|
137
|
+
for (const bucket of buckets) {
|
|
138
|
+
const key = runtime.asString(bucket?.key || '').trim();
|
|
139
|
+
if (!key) continue;
|
|
140
|
+
const entries: BncrPersistedGroupHistoryEntry[] = [];
|
|
141
|
+
const rawEntries = Array.isArray(bucket?.entries)
|
|
142
|
+
? (bucket.entries as PersistedGroupHistoryEntryInput[])
|
|
143
|
+
: [];
|
|
144
|
+
for (const entry of rawEntries) {
|
|
145
|
+
const sender = runtime.asString(entry?.sender || '').trim();
|
|
146
|
+
const body = runtime.asString(entry?.body || '').trim();
|
|
147
|
+
if (!sender || !body) continue;
|
|
148
|
+
const timestamp = runtime.finiteNumberOr(entry?.timestamp, 0);
|
|
149
|
+
const messageId = runtime.asString(entry?.messageId || '').trim();
|
|
150
|
+
|
|
151
|
+
const media: BncrPersistedGroupHistoryMediaEntry[] = [];
|
|
152
|
+
const rawMedia = Array.isArray(entry?.media)
|
|
153
|
+
? (entry.media as Partial<BncrPersistedGroupHistoryMediaEntry>[])
|
|
154
|
+
: [];
|
|
155
|
+
for (const item of rawMedia) {
|
|
156
|
+
const path = runtime.asString(item?.path || '').trim();
|
|
157
|
+
if (!path) continue;
|
|
158
|
+
const contentType = runtime.asString(item?.contentType || '').trim();
|
|
159
|
+
const kind = runtime.asString(item?.kind || '').trim();
|
|
160
|
+
const mediaMessageId = runtime.asString(item?.messageId || '').trim();
|
|
161
|
+
const normalizedMedia: BncrPersistedGroupHistoryMediaEntry = {
|
|
162
|
+
path,
|
|
163
|
+
...(contentType ? { contentType } : {}),
|
|
164
|
+
...(kind ? { kind: kind as BncrPersistedGroupHistoryMediaEntry['kind'] } : {}),
|
|
165
|
+
...(mediaMessageId ? { messageId: mediaMessageId } : {}),
|
|
166
|
+
};
|
|
167
|
+
media.push(normalizedMedia);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const normalizedEntry: BncrPersistedGroupHistoryEntry = {
|
|
171
|
+
sender,
|
|
172
|
+
body,
|
|
173
|
+
...(timestamp > 0 ? { timestamp } : {}),
|
|
174
|
+
...(messageId ? { messageId } : {}),
|
|
175
|
+
...(media.length > 0 ? { media } : {}),
|
|
176
|
+
};
|
|
177
|
+
entries.push(normalizedEntry);
|
|
178
|
+
}
|
|
179
|
+
if (entries.length > 0) runtime.groupHistories.set(key, entries.slice(-50));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function dumpPersistedGroupHistories() {
|
|
184
|
+
return Array.from(runtime.groupHistories.entries()).map(([key, entries]) => ({
|
|
185
|
+
key,
|
|
186
|
+
entries: entries.slice(-50),
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
|
|
65
190
|
function loadPersistedAccountTimestampMap(target: Map<string, number>, persisted: unknown): void {
|
|
66
191
|
target.clear();
|
|
67
192
|
const items = Array.isArray(persisted)
|
|
@@ -208,6 +333,8 @@ export function createBncrStateStore(runtime: {
|
|
|
208
333
|
runtime.setDeadLetter(deadLetter);
|
|
209
334
|
|
|
210
335
|
loadPersistedSessionRoutes(data.sessionRoutes);
|
|
336
|
+
loadPersistedSceneRegistry(data.sceneRegistry);
|
|
337
|
+
loadPersistedGroupHistories(data.groupHistories);
|
|
211
338
|
loadPersistedLastSessionMap(data.lastSessionByAccount);
|
|
212
339
|
loadPersistedAccountTimestampMap(runtime.lastActivityByAccount, data.lastActivityByAccount);
|
|
213
340
|
loadPersistedAccountTimestampMap(runtime.lastInboundByAccount, data.lastInboundByAccount);
|
|
@@ -225,6 +352,8 @@ export function createBncrStateStore(runtime: {
|
|
|
225
352
|
outbox: Array.from(runtime.outbox.values()),
|
|
226
353
|
deadLetter: runtime.getDeadLetter().slice(-runtime.maxDeadLetterEntries),
|
|
227
354
|
sessionRoutes: dumpPersistedSessionRoutes(),
|
|
355
|
+
sceneRegistry: dumpPersistedSceneRegistry(),
|
|
356
|
+
groupHistories: dumpPersistedGroupHistories(),
|
|
228
357
|
lastSessionByAccount: dumpPersistedLastSessionMap(),
|
|
229
358
|
lastActivityByAccount: dumpPersistedAccountTimestampMap(runtime.lastActivityByAccount),
|
|
230
359
|
lastInboundByAccount: dumpPersistedAccountTimestampMap(runtime.lastInboundByAccount),
|
|
@@ -242,6 +371,10 @@ export function createBncrStateStore(runtime: {
|
|
|
242
371
|
dumpPersistedLastSessionMap,
|
|
243
372
|
loadPersistedSessionRoutes,
|
|
244
373
|
dumpPersistedSessionRoutes,
|
|
374
|
+
loadPersistedSceneRegistry,
|
|
375
|
+
dumpPersistedSceneRegistry,
|
|
376
|
+
loadPersistedGroupHistories,
|
|
377
|
+
dumpPersistedGroupHistories,
|
|
245
378
|
backfillAccountActivityFromSessionRoutes,
|
|
246
379
|
loadState,
|
|
247
380
|
flushState,
|