@xmoxmo/bncr 0.5.1 → 0.5.2
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/scripts/check-pack.mjs +4 -1
- package/src/messaging/inbound/commands.ts +0 -6
- package/src/messaging/inbound/dispatch.ts +89 -22
- package/src/messaging/inbound/group-history.ts +92 -29
- package/src/messaging/inbound/native-command.ts +15 -2
- package/src/messaging/inbound/reply-dispatch.ts +10 -3
- package/src/messaging/inbound/scene-admin.ts +165 -6
- package/src/messaging/inbound/turn-context.ts +73 -40
- package/src/plugin/channel-runtime-types.ts +3 -0
- package/src/plugin/inbound-acceptance.ts +4 -1
- package/src/plugin/scene-registry.ts +4 -0
- package/src/plugin/state-store.ts +26 -2
package/package.json
CHANGED
package/scripts/check-pack.mjs
CHANGED
|
@@ -193,7 +193,10 @@ const output = execFileSync('npm', ['pack', '--dry-run', '--json'], {
|
|
|
193
193
|
encoding: 'utf8',
|
|
194
194
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
195
195
|
});
|
|
196
|
-
const
|
|
196
|
+
const raw = JSON.parse(output);
|
|
197
|
+
const pack = Array.isArray(raw)
|
|
198
|
+
? raw[0]
|
|
199
|
+
: raw[Object.keys(raw).find((k) => k.startsWith('@')) || Object.keys(raw)[0]];
|
|
197
200
|
const packedFiles = new Set((pack?.files ?? []).map((file) => file.path));
|
|
198
201
|
const missing = requiredPackFiles.filter((file) => !packedFiles.has(file));
|
|
199
202
|
const channelSource = fs.readFileSync(path.join(root, 'src/channel.ts'), 'utf8');
|
|
@@ -515,12 +515,6 @@ export async function handleBncrNativeCommand(params: {
|
|
|
515
515
|
result: 'rejected',
|
|
516
516
|
}),
|
|
517
517
|
);
|
|
518
|
-
await recordAndPatchBncrInboundSessionEntry({
|
|
519
|
-
storePath,
|
|
520
|
-
sessionKey,
|
|
521
|
-
ctx: ctxPayload,
|
|
522
|
-
patch: sessionIdentityPatch,
|
|
523
|
-
});
|
|
524
518
|
if (!silentNonAdminGroupReject) {
|
|
525
519
|
await enqueueFromReply({
|
|
526
520
|
accountId,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { DEFAULT_GROUP_HISTORY_LIMIT } from 'openclaw/plugin-sdk/reply-history';
|
|
1
2
|
import { emitBncrLogLine } from '../../core/logging.ts';
|
|
2
3
|
import type { BncrSceneRecord } from '../../plugin/channel-runtime-types.ts';
|
|
4
|
+
import { buildSceneKey } from '../../plugin/scene-registry.ts';
|
|
3
5
|
import { handleBncrNativeCommand } from './commands.ts';
|
|
4
6
|
import type {
|
|
5
7
|
BncrEnqueueFromReply,
|
|
@@ -18,7 +20,10 @@ import {
|
|
|
18
20
|
} from './dispatch-prep.ts';
|
|
19
21
|
import {
|
|
20
22
|
type BncrGroupHistoryMap,
|
|
23
|
+
type BncrHistoryEntry,
|
|
24
|
+
buildBncrGroupHistoryKey,
|
|
21
25
|
clearBncrPendingGroupHistory,
|
|
26
|
+
readBncrPendingGroupHistorySnapshot,
|
|
22
27
|
recordBncrPendingGroupMedia,
|
|
23
28
|
recordBncrPendingGroupText,
|
|
24
29
|
} from './group-history.ts';
|
|
@@ -58,10 +63,10 @@ export async function dispatchBncrInbound(params: {
|
|
|
58
63
|
cfg,
|
|
59
64
|
parsed,
|
|
60
65
|
canonicalAgentId,
|
|
61
|
-
shouldDispatch = true,
|
|
62
|
-
shouldAccumulate =
|
|
66
|
+
shouldDispatch: initialShouldDispatch = true,
|
|
67
|
+
shouldAccumulate = initialShouldDispatch,
|
|
63
68
|
resolvedAgentId,
|
|
64
|
-
sceneRegistry,
|
|
69
|
+
sceneRegistry = new Map(),
|
|
65
70
|
groupHistories = new Map(),
|
|
66
71
|
defaultAdminAgentId,
|
|
67
72
|
defaultPublicAgentId,
|
|
@@ -73,6 +78,7 @@ export async function dispatchBncrInbound(params: {
|
|
|
73
78
|
logger,
|
|
74
79
|
} = params;
|
|
75
80
|
const { accountId, clientId, msgId, extracted, mimeType, peer } = parsed;
|
|
81
|
+
let shouldDispatch = initialShouldDispatch;
|
|
76
82
|
|
|
77
83
|
const nativeCommand = await handleBncrNativeCommand({
|
|
78
84
|
api,
|
|
@@ -130,31 +136,29 @@ export async function dispatchBncrInbound(params: {
|
|
|
130
136
|
'[bncr] inbound missing clientId for chat identity; using route identity fallback',
|
|
131
137
|
);
|
|
132
138
|
}
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
bridgeSenderId,
|
|
144
|
-
bridgeSenderName,
|
|
145
|
-
resolution,
|
|
146
|
-
prepared,
|
|
147
|
-
groupHistories,
|
|
148
|
-
shouldDispatch,
|
|
149
|
-
});
|
|
139
|
+
const sceneHistoryLimit =
|
|
140
|
+
parsed.peer.kind === 'group'
|
|
141
|
+
? (sceneRegistry.get(buildSceneKey(parsed))?.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT)
|
|
142
|
+
: DEFAULT_GROUP_HISTORY_LIMIT;
|
|
143
|
+
const resolvedHistoryLimit =
|
|
144
|
+
typeof sceneHistoryLimit === 'number' &&
|
|
145
|
+
Number.isFinite(sceneHistoryLimit) &&
|
|
146
|
+
sceneHistoryLimit >= 0
|
|
147
|
+
? Math.floor(sceneHistoryLimit)
|
|
148
|
+
: DEFAULT_GROUP_HISTORY_LIMIT;
|
|
150
149
|
|
|
150
|
+
let silentHistoryFlush = false;
|
|
151
|
+
let pendingHistoryEntries: BncrHistoryEntry[] = [];
|
|
151
152
|
if (!shouldDispatch && shouldAccumulate) {
|
|
153
|
+
const historyKey = buildBncrGroupHistoryKey(parsed);
|
|
154
|
+
// Record the message first so overflow context includes it
|
|
152
155
|
recordBncrPendingGroupText({
|
|
153
156
|
historyMap: groupHistories,
|
|
154
157
|
parsed,
|
|
155
158
|
senderDisplayName,
|
|
156
159
|
senderId: senderIdForContext,
|
|
157
160
|
bodyText: rawBody,
|
|
161
|
+
historyLimit: resolvedHistoryLimit,
|
|
158
162
|
});
|
|
159
163
|
await recordBncrPendingGroupMedia({
|
|
160
164
|
historyMap: groupHistories,
|
|
@@ -164,9 +168,67 @@ export async function dispatchBncrInbound(params: {
|
|
|
164
168
|
bodyText: rawBody,
|
|
165
169
|
mediaItems,
|
|
166
170
|
mediaContentType: mediaContentType || mimeType,
|
|
171
|
+
historyLimit: resolvedHistoryLimit,
|
|
172
|
+
});
|
|
173
|
+
if (historyKey && parsed.peer.kind === 'group') {
|
|
174
|
+
const entries = groupHistories.get(historyKey);
|
|
175
|
+
if (entries && resolvedHistoryLimit > 0 && entries.length >= resolvedHistoryLimit) {
|
|
176
|
+
const sceneForHistory = sceneRegistry.get(buildSceneKey(parsed));
|
|
177
|
+
const historyForce = sceneForHistory?.historyForce !== false;
|
|
178
|
+
if (historyForce) {
|
|
179
|
+
pendingHistoryEntries = readBncrPendingGroupHistorySnapshot({
|
|
180
|
+
historyMap: groupHistories,
|
|
181
|
+
parsed,
|
|
182
|
+
historyLimit: resolvedHistoryLimit,
|
|
183
|
+
});
|
|
184
|
+
clearBncrPendingGroupHistory({
|
|
185
|
+
historyMap: groupHistories,
|
|
186
|
+
parsed,
|
|
187
|
+
historyLimit: resolvedHistoryLimit,
|
|
188
|
+
});
|
|
189
|
+
shouldDispatch = true;
|
|
190
|
+
silentHistoryFlush = true;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (shouldDispatch && !silentHistoryFlush && parsed.peer.kind === 'group') {
|
|
197
|
+
pendingHistoryEntries = readBncrPendingGroupHistorySnapshot({
|
|
198
|
+
historyMap: groupHistories,
|
|
199
|
+
parsed,
|
|
200
|
+
historyLimit: resolvedHistoryLimit,
|
|
167
201
|
});
|
|
202
|
+
if (pendingHistoryEntries.length > 0) {
|
|
203
|
+
clearBncrPendingGroupHistory({
|
|
204
|
+
historyMap: groupHistories,
|
|
205
|
+
parsed,
|
|
206
|
+
historyLimit: resolvedHistoryLimit,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
168
209
|
}
|
|
169
210
|
|
|
211
|
+
const ctxPayload = await buildBncrInboundTurnContext({
|
|
212
|
+
api,
|
|
213
|
+
cfg,
|
|
214
|
+
channelId,
|
|
215
|
+
parsed,
|
|
216
|
+
msgId,
|
|
217
|
+
peer,
|
|
218
|
+
senderIdForContext,
|
|
219
|
+
senderDisplayName,
|
|
220
|
+
ownerAllowFrom,
|
|
221
|
+
bridgeSenderId,
|
|
222
|
+
bridgeSenderName,
|
|
223
|
+
historyLimit: resolvedHistoryLimit,
|
|
224
|
+
resolution,
|
|
225
|
+
prepared,
|
|
226
|
+
groupHistories,
|
|
227
|
+
shouldDispatch,
|
|
228
|
+
silentHistoryFlush,
|
|
229
|
+
pendingHistoryEntries,
|
|
230
|
+
});
|
|
231
|
+
|
|
170
232
|
await runBncrInboundReplyDispatch({
|
|
171
233
|
api,
|
|
172
234
|
channelId,
|
|
@@ -182,13 +244,18 @@ export async function dispatchBncrInbound(params: {
|
|
|
182
244
|
senderIdForContext,
|
|
183
245
|
senderDisplayName,
|
|
184
246
|
shouldDispatch,
|
|
247
|
+
silentHistoryFlush,
|
|
185
248
|
setInboundActivity,
|
|
186
249
|
scheduleSave,
|
|
187
250
|
enqueueFromReply,
|
|
188
251
|
});
|
|
189
252
|
|
|
190
|
-
if (shouldDispatch) {
|
|
191
|
-
clearBncrPendingGroupHistory({
|
|
253
|
+
if (shouldDispatch && pendingHistoryEntries.length === 0) {
|
|
254
|
+
clearBncrPendingGroupHistory({
|
|
255
|
+
historyMap: groupHistories,
|
|
256
|
+
parsed,
|
|
257
|
+
historyLimit: resolvedHistoryLimit,
|
|
258
|
+
});
|
|
192
259
|
}
|
|
193
260
|
|
|
194
261
|
return {
|
|
@@ -37,13 +37,25 @@ export function recordBncrPendingGroupText(args: {
|
|
|
37
37
|
senderDisplayName: string;
|
|
38
38
|
senderId: string;
|
|
39
39
|
bodyText: string;
|
|
40
|
+
historyLimit?: number;
|
|
40
41
|
}) {
|
|
42
|
+
const limit =
|
|
43
|
+
typeof args.historyLimit === 'number' &&
|
|
44
|
+
Number.isFinite(args.historyLimit) &&
|
|
45
|
+
args.historyLimit >= 0
|
|
46
|
+
? Math.floor(args.historyLimit)
|
|
47
|
+
: DEFAULT_GROUP_HISTORY_LIMIT;
|
|
41
48
|
const historyKey = buildBncrGroupHistoryKey(args.parsed);
|
|
42
49
|
const body = normalizeTextBody(args.bodyText);
|
|
43
50
|
if (!historyKey || !body || args.parsed.msgType !== 'text') return;
|
|
51
|
+
const msgId = String(args.parsed.msgId || '').trim();
|
|
52
|
+
if (msgId) {
|
|
53
|
+
const existing = args.historyMap.get(historyKey) || [];
|
|
54
|
+
if (existing.some((e) => e.messageId === msgId)) return;
|
|
55
|
+
}
|
|
44
56
|
createChannelHistoryWindow({ historyMap: args.historyMap }).record({
|
|
45
57
|
historyKey,
|
|
46
|
-
limit
|
|
58
|
+
limit,
|
|
47
59
|
entry: {
|
|
48
60
|
sender: args.senderDisplayName,
|
|
49
61
|
senderId: args.senderId,
|
|
@@ -89,7 +101,14 @@ export async function recordBncrPendingGroupMedia(args: {
|
|
|
89
101
|
kind?: BncrHistoryMediaKind;
|
|
90
102
|
}>;
|
|
91
103
|
mediaContentType?: string;
|
|
104
|
+
historyLimit?: number;
|
|
92
105
|
}) {
|
|
106
|
+
const limit =
|
|
107
|
+
typeof args.historyLimit === 'number' &&
|
|
108
|
+
Number.isFinite(args.historyLimit) &&
|
|
109
|
+
args.historyLimit >= 0
|
|
110
|
+
? Math.floor(args.historyLimit)
|
|
111
|
+
: DEFAULT_GROUP_HISTORY_LIMIT;
|
|
93
112
|
const historyKey = buildBncrGroupHistoryKey(args.parsed);
|
|
94
113
|
if (!historyKey || args.parsed.msgType === 'text') return;
|
|
95
114
|
const normalizedMediaItems = Array.isArray(args.mediaItems) ? args.mediaItems : [];
|
|
@@ -110,9 +129,14 @@ export async function recordBncrPendingGroupMedia(args: {
|
|
|
110
129
|
});
|
|
111
130
|
const body = normalizeTextBody(args.bodyText) || buildBncrHistoryMediaBody(kind);
|
|
112
131
|
if (normalizedMediaItems.length === 0) {
|
|
132
|
+
const msgId = String(args.parsed.msgId || '').trim();
|
|
133
|
+
if (msgId) {
|
|
134
|
+
const existing = args.historyMap.get(historyKey) || [];
|
|
135
|
+
if (existing.some((e) => e.messageId === msgId)) return;
|
|
136
|
+
}
|
|
113
137
|
createChannelHistoryWindow({ historyMap: args.historyMap }).record({
|
|
114
138
|
historyKey,
|
|
115
|
-
limit
|
|
139
|
+
limit,
|
|
116
140
|
entry: {
|
|
117
141
|
sender: args.senderDisplayName,
|
|
118
142
|
senderId: args.senderId,
|
|
@@ -123,9 +147,14 @@ export async function recordBncrPendingGroupMedia(args: {
|
|
|
123
147
|
});
|
|
124
148
|
return;
|
|
125
149
|
}
|
|
150
|
+
const msgId = String(args.parsed.msgId || '').trim();
|
|
151
|
+
if (msgId) {
|
|
152
|
+
const existing = args.historyMap.get(historyKey) || [];
|
|
153
|
+
if (existing.some((e) => e.messageId === msgId)) return;
|
|
154
|
+
}
|
|
126
155
|
createChannelHistoryWindow({ historyMap: args.historyMap }).record({
|
|
127
156
|
historyKey,
|
|
128
|
-
limit
|
|
157
|
+
limit,
|
|
129
158
|
entry: {
|
|
130
159
|
sender: args.senderDisplayName,
|
|
131
160
|
senderId: args.senderId,
|
|
@@ -145,23 +174,56 @@ export async function recordBncrPendingGroupMedia(args: {
|
|
|
145
174
|
});
|
|
146
175
|
}
|
|
147
176
|
|
|
148
|
-
|
|
149
|
-
|
|
177
|
+
function cloneBncrHistoryEntry(entry: BncrHistoryEntry): BncrHistoryEntry {
|
|
178
|
+
return {
|
|
179
|
+
sender: entry.sender,
|
|
180
|
+
...(entry.messageId ? { messageId: entry.messageId } : {}),
|
|
181
|
+
...(typeof entry.timestamp === 'number' ? { timestamp: entry.timestamp } : {}),
|
|
182
|
+
...(entry.senderId ? { senderId: entry.senderId } : {}),
|
|
183
|
+
body: entry.body,
|
|
184
|
+
...(Array.isArray(entry.media)
|
|
185
|
+
? {
|
|
186
|
+
media: entry.media.map((item) => ({
|
|
187
|
+
...(item?.path ? { path: item.path } : {}),
|
|
188
|
+
...(item?.contentType ? { contentType: item.contentType } : {}),
|
|
189
|
+
...(item?.kind ? { kind: item.kind } : {}),
|
|
190
|
+
...(item?.messageId ? { messageId: item.messageId } : {}),
|
|
191
|
+
})),
|
|
192
|
+
}
|
|
193
|
+
: {}),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function readBncrPendingGroupHistorySnapshot(args: {
|
|
150
198
|
historyMap: BncrGroupHistoryMap;
|
|
151
199
|
parsed: ParsedInbound;
|
|
200
|
+
historyLimit?: number;
|
|
201
|
+
}): BncrHistoryEntry[] {
|
|
202
|
+
const limit =
|
|
203
|
+
typeof args.historyLimit === 'number' &&
|
|
204
|
+
Number.isFinite(args.historyLimit) &&
|
|
205
|
+
args.historyLimit >= 0
|
|
206
|
+
? Math.floor(args.historyLimit)
|
|
207
|
+
: DEFAULT_GROUP_HISTORY_LIMIT;
|
|
208
|
+
const historyKey = buildBncrGroupHistoryKey(args.parsed);
|
|
209
|
+
if (!historyKey) return [];
|
|
210
|
+
const entries = args.historyMap.get(historyKey) || [];
|
|
211
|
+
if (entries.length === 0) return [];
|
|
212
|
+
return entries.slice(-limit).map(cloneBncrHistoryEntry);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function buildBncrGroupContextFromEntries(args: {
|
|
216
|
+
api: BncrInboundApi;
|
|
217
|
+
entries: readonly BncrHistoryEntry[];
|
|
152
218
|
channelLabel: string;
|
|
153
219
|
currentTimestamp: number;
|
|
154
220
|
previousTimestamp?: unknown;
|
|
155
221
|
envelope?: unknown;
|
|
156
222
|
currentMessage: string;
|
|
157
223
|
}) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
historyKey,
|
|
162
|
-
limit: DEFAULT_GROUP_HISTORY_LIMIT,
|
|
163
|
-
currentMessage: args.currentMessage,
|
|
164
|
-
formatEntry: (entry) =>
|
|
224
|
+
if (args.entries.length === 0) return args.currentMessage;
|
|
225
|
+
const historyText = args.entries
|
|
226
|
+
.map((entry) =>
|
|
165
227
|
formatOpenClawAgentEnvelope(args.api, {
|
|
166
228
|
channel: 'Bncr',
|
|
167
229
|
from: args.channelLabel,
|
|
@@ -170,17 +232,15 @@ export function buildBncrPendingGroupContext(args: {
|
|
|
170
232
|
envelope: args.envelope,
|
|
171
233
|
body: entry.body,
|
|
172
234
|
}),
|
|
173
|
-
|
|
235
|
+
)
|
|
236
|
+
.join('\n');
|
|
237
|
+
return `[Chat messages since your last reply - for context]\n${historyText}\n\n[Current message - respond to this]${args.currentMessage}`;
|
|
174
238
|
}
|
|
175
239
|
|
|
176
|
-
export function
|
|
177
|
-
|
|
178
|
-
parsed: ParsedInbound;
|
|
240
|
+
export function collectBncrHistoryMediaFromEntries(args: {
|
|
241
|
+
entries: readonly BncrHistoryEntry[];
|
|
179
242
|
}): HistoryMediaEntry[] {
|
|
180
|
-
|
|
181
|
-
if (!historyKey) return [];
|
|
182
|
-
const entries = args.historyMap.get(historyKey) || [];
|
|
183
|
-
return entries.flatMap((entry) => (Array.isArray(entry.media) ? entry.media : []));
|
|
243
|
+
return args.entries.flatMap((entry) => (Array.isArray(entry.media) ? entry.media : []));
|
|
184
244
|
}
|
|
185
245
|
|
|
186
246
|
function toBncrPromptMediaPath(value: string | undefined): string | undefined {
|
|
@@ -230,16 +290,12 @@ function buildHistoryWindowMedias(media: HistoryMediaEntry[] | undefined) {
|
|
|
230
290
|
});
|
|
231
291
|
}
|
|
232
292
|
|
|
233
|
-
export function
|
|
234
|
-
|
|
235
|
-
parsed: ParsedInbound;
|
|
293
|
+
export function buildBncrHistoryWindowContextFromEntries(args: {
|
|
294
|
+
entries: readonly BncrHistoryEntry[];
|
|
236
295
|
channelId: string;
|
|
237
296
|
}) {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const entries = args.historyMap.get(historyKey) || [];
|
|
241
|
-
if (entries.length === 0) return undefined;
|
|
242
|
-
const messages = entries.slice(-DEFAULT_GROUP_HISTORY_LIMIT).map((entry) => ({
|
|
297
|
+
if (args.entries.length === 0) return undefined;
|
|
298
|
+
const messages = args.entries.map((entry) => ({
|
|
243
299
|
...(entry.messageId ? { messageId: entry.messageId } : {}),
|
|
244
300
|
sender: entry.sender,
|
|
245
301
|
senderId: entry.senderId,
|
|
@@ -263,11 +319,18 @@ export function buildBncrPendingHistoryWindowContext(args: {
|
|
|
263
319
|
export function clearBncrPendingGroupHistory(args: {
|
|
264
320
|
historyMap: BncrGroupHistoryMap;
|
|
265
321
|
parsed: ParsedInbound;
|
|
322
|
+
historyLimit?: number;
|
|
266
323
|
}) {
|
|
324
|
+
const limit =
|
|
325
|
+
typeof args.historyLimit === 'number' &&
|
|
326
|
+
Number.isFinite(args.historyLimit) &&
|
|
327
|
+
args.historyLimit >= 0
|
|
328
|
+
? Math.floor(args.historyLimit)
|
|
329
|
+
: DEFAULT_GROUP_HISTORY_LIMIT;
|
|
267
330
|
const historyKey = buildBncrGroupHistoryKey(args.parsed);
|
|
268
331
|
if (!historyKey) return;
|
|
269
332
|
createChannelHistoryWindow({ historyMap: args.historyMap }).clear({
|
|
270
333
|
historyKey,
|
|
271
|
-
limit:
|
|
334
|
+
limit: limit,
|
|
272
335
|
});
|
|
273
336
|
}
|
|
@@ -49,12 +49,16 @@ const BNCR_HELP_TEXT = [
|
|
|
49
49
|
' • /bncr allow [<SceneId>]',
|
|
50
50
|
' • /bncr deny [<SceneId>]',
|
|
51
51
|
' • /bncr bind <agentId> [<SceneId>]',
|
|
52
|
-
' • /bncr mode',
|
|
53
52
|
' • /bncr mode help',
|
|
54
53
|
' • /bncr mode <admin|mention|hybrid|all> [<SceneId>]',
|
|
55
54
|
' • /bncr revoke [<SceneId>]',
|
|
56
55
|
' • /bncr list pending',
|
|
57
56
|
' • /bncr list scenes',
|
|
57
|
+
'',
|
|
58
|
+
'📋 Group history',
|
|
59
|
+
' • /bncr history-help',
|
|
60
|
+
' • /bncr history-limit [<number>] [<SceneId>]',
|
|
61
|
+
' • /bncr history-force on|off [<SceneId>]',
|
|
58
62
|
].join('\n');
|
|
59
63
|
|
|
60
64
|
const BNCR_NATIVE_COMMANDS = new Set([
|
|
@@ -70,9 +74,18 @@ const BNCR_NATIVE_COMMANDS = new Set([
|
|
|
70
74
|
'mode',
|
|
71
75
|
'revoke',
|
|
72
76
|
'list',
|
|
77
|
+
'history-limit',
|
|
78
|
+
'history-help',
|
|
79
|
+
'history-force',
|
|
73
80
|
]);
|
|
74
81
|
|
|
75
|
-
export const BNCR_DIRECT_ALLOWED_BARE_COMMANDS = new Set([
|
|
82
|
+
export const BNCR_DIRECT_ALLOWED_BARE_COMMANDS = new Set([
|
|
83
|
+
'whoami',
|
|
84
|
+
'status',
|
|
85
|
+
'new',
|
|
86
|
+
'reset',
|
|
87
|
+
'stop',
|
|
88
|
+
]);
|
|
76
89
|
|
|
77
90
|
export const BNCR_DIRECT_ALLOWED_SUBCOMMANDS = new Set([
|
|
78
91
|
'help',
|
|
@@ -125,6 +125,7 @@ export async function runBncrInboundReplyDispatch(args: {
|
|
|
125
125
|
senderIdForContext: string;
|
|
126
126
|
senderDisplayName: string;
|
|
127
127
|
shouldDispatch: boolean;
|
|
128
|
+
silentHistoryFlush?: boolean;
|
|
128
129
|
setInboundActivity: (accountId: string, at: number) => void;
|
|
129
130
|
scheduleSave: () => void;
|
|
130
131
|
enqueueFromReply: BncrEnqueueFromReply;
|
|
@@ -143,6 +144,7 @@ export async function runBncrInboundReplyDispatch(args: {
|
|
|
143
144
|
replyRouteFact,
|
|
144
145
|
senderIdForContext,
|
|
145
146
|
shouldDispatch,
|
|
147
|
+
silentHistoryFlush = false,
|
|
146
148
|
setInboundActivity,
|
|
147
149
|
scheduleSave,
|
|
148
150
|
enqueueFromReply,
|
|
@@ -230,15 +232,16 @@ export async function runBncrInboundReplyDispatch(args: {
|
|
|
230
232
|
textForCommands: ctxPayload.CommandBody,
|
|
231
233
|
raw: parsed,
|
|
232
234
|
}),
|
|
233
|
-
preflight: () =>
|
|
234
|
-
shouldDispatch
|
|
235
|
+
preflight: () => {
|
|
236
|
+
return shouldDispatch
|
|
235
237
|
? undefined
|
|
236
238
|
: {
|
|
237
239
|
admission: {
|
|
238
240
|
kind: 'observeOnly' as const,
|
|
239
241
|
reason: 'bncr-group-mode-no-reply',
|
|
240
242
|
},
|
|
241
|
-
}
|
|
243
|
+
};
|
|
244
|
+
},
|
|
242
245
|
resolveTurn: () => {
|
|
243
246
|
const sessionMetaBarrier = createBncrSessionMetaTaskBarrier();
|
|
244
247
|
return {
|
|
@@ -291,6 +294,10 @@ export async function runBncrInboundReplyDispatch(args: {
|
|
|
291
294
|
return;
|
|
292
295
|
}
|
|
293
296
|
|
|
297
|
+
if (silentHistoryFlush) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
294
301
|
await enqueueFromReply({
|
|
295
302
|
accountId: replyRouteFact.accountId,
|
|
296
303
|
sessionKey: replyRouteFact.sessionKey,
|
|
@@ -14,12 +14,19 @@ export type BncrSceneAdminCommand =
|
|
|
14
14
|
| { kind: 'mode-help' }
|
|
15
15
|
| { kind: 'mode-get'; sceneKey?: string }
|
|
16
16
|
| { kind: 'mode'; sceneKey: string; mode: BncrGroupReplyMode }
|
|
17
|
-
| { kind: 'list'; scope: 'pending' | 'scenes' }
|
|
17
|
+
| { kind: 'list'; scope: 'pending' | 'scenes' }
|
|
18
|
+
| { kind: 'history-limit-get'; sceneKey?: string }
|
|
19
|
+
| { kind: 'history-limit-set'; sceneKey: string; limit: number }
|
|
20
|
+
| { kind: 'history-force-get'; sceneKey?: string }
|
|
21
|
+
| { kind: 'history-force-set'; sceneKey: string; enabled: boolean }
|
|
22
|
+
| { kind: 'history-help' };
|
|
18
23
|
|
|
19
24
|
const GROUP_REPLY_MODES = new Set<BncrGroupReplyMode>(['admin', 'mention', 'hybrid', 'all']);
|
|
20
25
|
|
|
21
26
|
const MODE_HELP_TEXT = [
|
|
22
|
-
'💬 Group
|
|
27
|
+
'💬 Bncr Group Reply Mode Configuration',
|
|
28
|
+
'',
|
|
29
|
+
'Modes:',
|
|
23
30
|
' • default: admin',
|
|
24
31
|
' • admin: 仅管理员|消息上送并逐条回复',
|
|
25
32
|
' • mention: 全员|消息上送 仅指定消息触发回复',
|
|
@@ -32,9 +39,18 @@ const MODE_HELP_TEXT = [
|
|
|
32
39
|
' • platform-marked should-respond messages',
|
|
33
40
|
'',
|
|
34
41
|
'Usage:',
|
|
35
|
-
' • /bncr mode',
|
|
36
|
-
|
|
37
|
-
|
|
42
|
+
' • /bncr mode <admin|mention|hybrid|all> [<SceneId>]',
|
|
43
|
+
].join('\n');
|
|
44
|
+
|
|
45
|
+
export const HISTORY_HELP_TEXT = [
|
|
46
|
+
'📋 Bncr Group History Configuration',
|
|
47
|
+
'',
|
|
48
|
+
'Commands:',
|
|
49
|
+
' • /bncr history-limit <number> [<SceneId>]',
|
|
50
|
+
' Set history limit (default: 50)',
|
|
51
|
+
' • /bncr history-force on|off [<SceneId>]',
|
|
52
|
+
' When on, overflow triggers auto context at limit (default: on)',
|
|
53
|
+
' When off, oldest messages trim silently without context',
|
|
38
54
|
].join('\n');
|
|
39
55
|
|
|
40
56
|
export type ParsedSceneAdminCommand =
|
|
@@ -141,6 +157,65 @@ export function parseSceneAdminCommand(command: NativeCommand): ParsedSceneAdmin
|
|
|
141
157
|
return { matched: true, valid: true, command: { kind: 'list', scope: 'scenes' } };
|
|
142
158
|
}
|
|
143
159
|
return { matched: true, valid: false, text: 'Usage: /bncr list <pending|scenes>' };
|
|
160
|
+
case 'history-limit':
|
|
161
|
+
if (args.length === 0) {
|
|
162
|
+
return { matched: true, valid: true, command: { kind: 'history-limit-get' } };
|
|
163
|
+
}
|
|
164
|
+
if (args.length === 1) {
|
|
165
|
+
const num = parseInt(args[0], 10);
|
|
166
|
+
if (Number.isFinite(num)) {
|
|
167
|
+
return {
|
|
168
|
+
matched: true,
|
|
169
|
+
valid: true,
|
|
170
|
+
command: { kind: 'history-limit-set', sceneKey: '', limit: num },
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
matched: true,
|
|
175
|
+
valid: true,
|
|
176
|
+
command: { kind: 'history-limit-get', sceneKey: args[0] },
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
if (args.length === 2) {
|
|
180
|
+
const num = parseInt(args[0], 10);
|
|
181
|
+
if (Number.isFinite(num)) {
|
|
182
|
+
return {
|
|
183
|
+
matched: true,
|
|
184
|
+
valid: true,
|
|
185
|
+
command: { kind: 'history-limit-set', sceneKey: args[1], limit: num },
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
matched: true,
|
|
191
|
+
valid: false,
|
|
192
|
+
text: 'Usage: /bncr history-limit [<number>] [<sceneKey>]',
|
|
193
|
+
};
|
|
194
|
+
case 'history-force':
|
|
195
|
+
if (args.length === 0) {
|
|
196
|
+
return { matched: true, valid: true, command: { kind: 'history-force-get' } };
|
|
197
|
+
}
|
|
198
|
+
if (args[0] === 'on' || args[0] === 'off') {
|
|
199
|
+
if (args.length === 1) {
|
|
200
|
+
return {
|
|
201
|
+
matched: true,
|
|
202
|
+
valid: true,
|
|
203
|
+
command: { kind: 'history-force-set', sceneKey: '', enabled: args[0] === 'on' },
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
matched: true,
|
|
208
|
+
valid: true,
|
|
209
|
+
command: { kind: 'history-force-set', sceneKey: args[1], enabled: args[0] === 'on' },
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
matched: true,
|
|
214
|
+
valid: true,
|
|
215
|
+
command: { kind: 'history-force-get', sceneKey: args[0] },
|
|
216
|
+
};
|
|
217
|
+
case 'history-help':
|
|
218
|
+
return { matched: true, valid: true, command: { kind: 'history-help' } };
|
|
144
219
|
default:
|
|
145
220
|
return { matched: false };
|
|
146
221
|
}
|
|
@@ -151,7 +226,15 @@ function formatSceneDetailsLine(scene: BncrSceneRecord): string {
|
|
|
151
226
|
scene.kind === 'group' ? scene.groupId || scene.sceneKey : scene.userId || scene.sceneKey;
|
|
152
227
|
const namePart = scene.kind === 'group' ? scene.groupName || '' : scene.userName || '';
|
|
153
228
|
const labelPart = namePart ? ` name=${namePart}` : '';
|
|
154
|
-
|
|
229
|
+
const historyLimitVal = scene.historyLimit;
|
|
230
|
+
const historyForceVal = scene.historyForce;
|
|
231
|
+
const hasNonDefaultHistory =
|
|
232
|
+
scene.kind === 'group' &&
|
|
233
|
+
((typeof historyLimitVal === 'number' && historyLimitVal !== 50) || historyForceVal === false);
|
|
234
|
+
const historyPart = hasNonDefaultHistory
|
|
235
|
+
? ` historyLimit=${historyLimitVal ?? 50} autoFlush=${historyForceVal !== false ? 'on' : 'off'}`
|
|
236
|
+
: '';
|
|
237
|
+
return ` Details: status=${scene.status} id=${idPart}${labelPart}${historyPart}`;
|
|
155
238
|
}
|
|
156
239
|
|
|
157
240
|
function formatSceneEntry(scene: BncrSceneRecord): string {
|
|
@@ -209,6 +292,10 @@ export function executeSceneAdminCommand(args: {
|
|
|
209
292
|
return { ok: false, text: 'Admin permission required.' };
|
|
210
293
|
}
|
|
211
294
|
|
|
295
|
+
if (command.kind === 'history-help') {
|
|
296
|
+
return { ok: true, text: HISTORY_HELP_TEXT };
|
|
297
|
+
}
|
|
298
|
+
|
|
212
299
|
if (command.kind === 'list') {
|
|
213
300
|
const scenes = Array.from(sceneRegistry.values())
|
|
214
301
|
.filter((scene) => (command.scope === 'pending' ? scene.status === 'pending' : true))
|
|
@@ -244,6 +331,78 @@ export function executeSceneAdminCommand(args: {
|
|
|
244
331
|
};
|
|
245
332
|
}
|
|
246
333
|
|
|
334
|
+
if (command.kind === 'history-limit-get') {
|
|
335
|
+
const qSceneKey = command.sceneKey || resolveCurrentGroupSceneKey(parsed);
|
|
336
|
+
if (!qSceneKey) {
|
|
337
|
+
return { ok: true, text: 'Default history limit is 50.' };
|
|
338
|
+
}
|
|
339
|
+
const s = sceneRegistry.get(qSceneKey);
|
|
340
|
+
if (!s) return { ok: true, text: 'Default history limit is 50.' };
|
|
341
|
+
return { ok: true, text: `Current ${qSceneKey} history limit is ${s.historyLimit ?? 50}.` };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (command.kind === 'history-limit-set') {
|
|
345
|
+
const sSceneKey = command.sceneKey || resolveCurrentGroupSceneKey(parsed);
|
|
346
|
+
if (!sSceneKey) {
|
|
347
|
+
return { ok: false, text: 'Current group shortcut only works inside a group chat.' };
|
|
348
|
+
}
|
|
349
|
+
const sExisting = sceneRegistry.get(sSceneKey);
|
|
350
|
+
if (!sExisting) return { ok: false, text: `Scene not found: ${sSceneKey}` };
|
|
351
|
+
const rawLimit = command.limit;
|
|
352
|
+
if (!Number.isFinite(rawLimit) || Number.isNaN(rawLimit)) {
|
|
353
|
+
return { ok: false, text: 'Invalid number.' };
|
|
354
|
+
}
|
|
355
|
+
let resolvedLimit = rawLimit;
|
|
356
|
+
if (resolvedLimit >= 51) {
|
|
357
|
+
resolvedLimit = Math.min(Math.floor(resolvedLimit), 10000);
|
|
358
|
+
} else if (resolvedLimit < 0 && Math.abs(resolvedLimit) >= 3) {
|
|
359
|
+
resolvedLimit = Math.floor(Math.abs(resolvedLimit));
|
|
360
|
+
} else {
|
|
361
|
+
return {
|
|
362
|
+
ok: false,
|
|
363
|
+
text: `Value too small, must be >= 51, or use negative number (abs >= 3) for hidden override.`,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
sceneRegistry.set(sSceneKey, {
|
|
367
|
+
...sExisting,
|
|
368
|
+
historyLimit: resolvedLimit,
|
|
369
|
+
lastSeenAt: now(),
|
|
370
|
+
});
|
|
371
|
+
return { ok: true, text: `Set ${sSceneKey} history limit to ${resolvedLimit}.` };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (command.kind === 'history-force-get') {
|
|
375
|
+
const qSceneKey = command.sceneKey || resolveCurrentGroupSceneKey(parsed);
|
|
376
|
+
if (!qSceneKey) {
|
|
377
|
+
return { ok: true, text: 'Default history auto flush is on.' };
|
|
378
|
+
}
|
|
379
|
+
const s = sceneRegistry.get(qSceneKey);
|
|
380
|
+
if (!s) return { ok: true, text: 'Default history auto flush is on.' };
|
|
381
|
+
const enabled = s.historyForce !== false;
|
|
382
|
+
return {
|
|
383
|
+
ok: true,
|
|
384
|
+
text: `Current ${qSceneKey} history auto flush is ${enabled ? 'on' : 'off'}.`,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (command.kind === 'history-force-set') {
|
|
389
|
+
const sSceneKey = command.sceneKey || resolveCurrentGroupSceneKey(parsed);
|
|
390
|
+
if (!sSceneKey) {
|
|
391
|
+
return { ok: false, text: 'Current group shortcut only works inside a group chat.' };
|
|
392
|
+
}
|
|
393
|
+
const sExisting = sceneRegistry.get(sSceneKey);
|
|
394
|
+
if (!sExisting) return { ok: false, text: `Scene not found: ${sSceneKey}` };
|
|
395
|
+
sceneRegistry.set(sSceneKey, {
|
|
396
|
+
...sExisting,
|
|
397
|
+
historyForce: command.enabled,
|
|
398
|
+
lastSeenAt: now(),
|
|
399
|
+
});
|
|
400
|
+
return {
|
|
401
|
+
ok: true,
|
|
402
|
+
text: `Set ${sSceneKey} history auto flush to ${command.enabled ? 'on' : 'off'}.`,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
247
406
|
const sceneKey = command.sceneKey || resolveCurrentGroupSceneKey(parsed);
|
|
248
407
|
if (!sceneKey) {
|
|
249
408
|
return { ok: false, text: 'Current group shortcut only works inside a group chat.' };
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { readBncrSessionUpdatedAt } from '../../openclaw/inbound-session-runtime.ts';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
formatOpenClawAgentEnvelope,
|
|
4
|
+
resolveOpenClawEnvelopeFormatOptions,
|
|
5
|
+
} from '../../openclaw/reply-runtime.ts';
|
|
3
6
|
import {
|
|
4
7
|
buildBncrPromptVisibleContextFacts,
|
|
5
8
|
buildBncrStructuredContextFactsFromInboundParts,
|
|
@@ -12,9 +15,9 @@ import type {
|
|
|
12
15
|
} from './dispatch-prep.ts';
|
|
13
16
|
import {
|
|
14
17
|
type BncrGroupHistoryMap,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
type BncrHistoryEntry,
|
|
19
|
+
buildBncrGroupContextFromEntries,
|
|
20
|
+
collectBncrHistoryMediaFromEntries,
|
|
18
21
|
} from './group-history.ts';
|
|
19
22
|
import { resolveBncrChannelInboundRuntime } from './runtime-compat.ts';
|
|
20
23
|
|
|
@@ -28,13 +31,15 @@ function applyBncrLegacyCommandFields(args: {
|
|
|
28
31
|
ctx: BncrInboundContextPayload;
|
|
29
32
|
channelId: string;
|
|
30
33
|
senderIdForContext: string;
|
|
34
|
+
fromOverride?: string;
|
|
31
35
|
resolution: BncrInboundConversationResolution;
|
|
32
36
|
rawBody: string;
|
|
33
37
|
ownerAllowFrom?: string[];
|
|
34
38
|
isAuthorizedTextCommand: boolean;
|
|
35
39
|
}) {
|
|
36
|
-
const { ctx, channelId, senderIdForContext, resolution, rawBody, ownerAllowFrom } =
|
|
37
|
-
|
|
40
|
+
const { ctx, channelId, senderIdForContext, fromOverride, resolution, rawBody, ownerAllowFrom } =
|
|
41
|
+
args;
|
|
42
|
+
ctx.From = fromOverride || senderIdForContext;
|
|
38
43
|
ctx.To = resolution.canonicalTo;
|
|
39
44
|
ctx.SenderId = senderIdForContext;
|
|
40
45
|
ctx.OriginatingChannel = channelId;
|
|
@@ -69,6 +74,9 @@ export function buildBncrInboundTurnContext(args: {
|
|
|
69
74
|
prepared: BncrPreparedInboundSessionContext;
|
|
70
75
|
groupHistories: BncrGroupHistoryMap;
|
|
71
76
|
shouldDispatch: boolean;
|
|
77
|
+
historyLimit?: number;
|
|
78
|
+
silentHistoryFlush?: boolean;
|
|
79
|
+
pendingHistoryEntries?: readonly BncrHistoryEntry[];
|
|
72
80
|
}): BncrInboundContextPayload | Promise<BncrInboundContextPayload> {
|
|
73
81
|
const {
|
|
74
82
|
api,
|
|
@@ -84,31 +92,50 @@ export function buildBncrInboundTurnContext(args: {
|
|
|
84
92
|
bridgeSenderName,
|
|
85
93
|
resolution,
|
|
86
94
|
prepared,
|
|
87
|
-
groupHistories,
|
|
88
95
|
shouldDispatch,
|
|
89
96
|
} = args;
|
|
97
|
+
const pendingHistoryEntries = Array.isArray(args.pendingHistoryEntries)
|
|
98
|
+
? args.pendingHistoryEntries
|
|
99
|
+
: [];
|
|
100
|
+
const silentHistoryFlush = args.silentHistoryFlush === true;
|
|
101
|
+
const flushSenderId = 'bncr-history-system';
|
|
102
|
+
const flushSenderName = 'Bncr History System';
|
|
103
|
+
const flushRawBody =
|
|
104
|
+
'This message was automatically generated by the system for group context accumulation. Do not reply to the user. Output exactly NO_REPLY.';
|
|
105
|
+
const effectiveSenderId = silentHistoryFlush ? flushSenderId : senderIdForContext;
|
|
106
|
+
const effectiveSenderDisplayName = silentHistoryFlush ? flushSenderName : senderDisplayName;
|
|
107
|
+
const effectiveRawBody = silentHistoryFlush ? flushRawBody : prepared.rawBody;
|
|
108
|
+
const envelopeOptions = resolveOpenClawEnvelopeFormatOptions(api, cfg);
|
|
109
|
+
const effectiveBody = silentHistoryFlush
|
|
110
|
+
? formatOpenClawAgentEnvelope(api, {
|
|
111
|
+
channel: 'Bncr',
|
|
112
|
+
from: resolution.canonicalTo,
|
|
113
|
+
timestamp: Date.now(),
|
|
114
|
+
previousTimestamp: readBncrSessionUpdatedAt(api, {
|
|
115
|
+
storePath: prepared.storePath,
|
|
116
|
+
sessionKey: resolution.dispatchSessionKey,
|
|
117
|
+
}),
|
|
118
|
+
envelope: envelopeOptions,
|
|
119
|
+
body: effectiveRawBody,
|
|
120
|
+
})
|
|
121
|
+
: prepared.body;
|
|
90
122
|
const senderIsOwner = parsed.isAdmin === true || Boolean(ownerAllowFrom?.length);
|
|
91
123
|
const senderIsAuthorized = senderIsOwner;
|
|
92
124
|
const inboundHistory = undefined;
|
|
93
|
-
const pendingHistoryMedia =
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
: [];
|
|
97
|
-
const pendingHistoryWindowContext =
|
|
98
|
-
shouldDispatch && parsed.peer.kind === 'group'
|
|
99
|
-
? buildBncrPendingHistoryWindowContext({ historyMap: groupHistories, parsed, channelId })
|
|
100
|
-
: undefined;
|
|
125
|
+
const pendingHistoryMedia = shouldDispatch
|
|
126
|
+
? collectBncrHistoryMediaFromEntries({ entries: pendingHistoryEntries })
|
|
127
|
+
: [];
|
|
101
128
|
const structuredContextFacts = buildBncrStructuredContextFactsFromInboundParts({
|
|
102
129
|
channelId,
|
|
103
130
|
parsed,
|
|
104
131
|
resolution,
|
|
105
132
|
prepared: {
|
|
106
|
-
rawBody:
|
|
107
|
-
body: prepared.body,
|
|
108
|
-
mediaItems: prepared.mediaItems,
|
|
133
|
+
rawBody: effectiveRawBody,
|
|
134
|
+
body: typeof effectiveBody === 'string' ? effectiveBody : prepared.body,
|
|
135
|
+
mediaItems: silentHistoryFlush ? [] : prepared.mediaItems,
|
|
109
136
|
},
|
|
110
|
-
senderIdForContext,
|
|
111
|
-
senderDisplayName,
|
|
137
|
+
senderIdForContext: effectiveSenderId,
|
|
138
|
+
senderDisplayName: effectiveSenderDisplayName,
|
|
112
139
|
bridgeSenderId,
|
|
113
140
|
bridgeSenderName,
|
|
114
141
|
senderIsOwner,
|
|
@@ -116,14 +143,16 @@ export function buildBncrInboundTurnContext(args: {
|
|
|
116
143
|
});
|
|
117
144
|
const promptVisibleContextFacts = buildBncrPromptVisibleContextFacts(structuredContextFacts);
|
|
118
145
|
const supplementalUntrustedContext = [
|
|
119
|
-
...(pendingHistoryWindowContext ? [pendingHistoryWindowContext] : []),
|
|
120
146
|
...(Object.keys(promptVisibleContextFacts).length
|
|
121
147
|
? [
|
|
122
148
|
{
|
|
123
149
|
label: 'Bncr inbound context',
|
|
124
150
|
source: channelId,
|
|
125
151
|
type: 'bncr.inbound_context',
|
|
126
|
-
payload:
|
|
152
|
+
payload: {
|
|
153
|
+
...promptVisibleContextFacts,
|
|
154
|
+
...(silentHistoryFlush ? { historyFlush: true } : {}),
|
|
155
|
+
},
|
|
127
156
|
},
|
|
128
157
|
]
|
|
129
158
|
: []),
|
|
@@ -132,22 +161,23 @@ export function buildBncrInboundTurnContext(args: {
|
|
|
132
161
|
const wasMentioned = parsed.isBotMentioned === true || platformWantsReply;
|
|
133
162
|
const isAuthorizedTextCommand = Boolean(ownerAllowFrom?.length);
|
|
134
163
|
const bodyForAgent =
|
|
135
|
-
shouldDispatch &&
|
|
136
|
-
?
|
|
164
|
+
shouldDispatch && pendingHistoryEntries.length > 0
|
|
165
|
+
? buildBncrGroupContextFromEntries({
|
|
137
166
|
api,
|
|
138
|
-
|
|
139
|
-
parsed,
|
|
167
|
+
entries: pendingHistoryEntries,
|
|
140
168
|
channelLabel: resolution.canonicalTo,
|
|
141
169
|
currentTimestamp: Date.now(),
|
|
142
170
|
previousTimestamp: readBncrSessionUpdatedAt(api, {
|
|
143
171
|
storePath: prepared.storePath,
|
|
144
172
|
sessionKey: resolution.dispatchSessionKey,
|
|
145
173
|
}),
|
|
146
|
-
envelope:
|
|
147
|
-
currentMessage: prepared.body,
|
|
174
|
+
envelope: envelopeOptions,
|
|
175
|
+
currentMessage: typeof effectiveBody === 'string' ? effectiveBody : prepared.body,
|
|
148
176
|
})
|
|
149
|
-
:
|
|
150
|
-
|
|
177
|
+
: typeof effectiveBody === 'string'
|
|
178
|
+
? effectiveBody
|
|
179
|
+
: prepared.body;
|
|
180
|
+
const currentTurnMedia = (silentHistoryFlush ? [] : prepared.mediaItems).map((item) => ({
|
|
151
181
|
path: item.path,
|
|
152
182
|
contentType: item.contentType,
|
|
153
183
|
kind: item.kind,
|
|
@@ -163,11 +193,13 @@ export function buildBncrInboundTurnContext(args: {
|
|
|
163
193
|
accountId: resolution.accountId,
|
|
164
194
|
messageId: msgId,
|
|
165
195
|
timestamp: Date.now(),
|
|
166
|
-
from:
|
|
196
|
+
from: effectiveSenderId,
|
|
167
197
|
sender: {
|
|
168
|
-
id:
|
|
169
|
-
name:
|
|
170
|
-
username:
|
|
198
|
+
id: effectiveSenderId,
|
|
199
|
+
name: effectiveSenderDisplayName,
|
|
200
|
+
username: silentHistoryFlush
|
|
201
|
+
? effectiveSenderDisplayName
|
|
202
|
+
: parsed.userName || effectiveSenderDisplayName,
|
|
171
203
|
},
|
|
172
204
|
conversation: {
|
|
173
205
|
kind: resolution.chatType,
|
|
@@ -191,13 +223,13 @@ export function buildBncrInboundTurnContext(args: {
|
|
|
191
223
|
},
|
|
192
224
|
message: {
|
|
193
225
|
inboundEventKind: 'user_request',
|
|
194
|
-
body: prepared.body,
|
|
195
|
-
rawBody:
|
|
226
|
+
body: typeof effectiveBody === 'string' ? effectiveBody : prepared.body,
|
|
227
|
+
rawBody: effectiveRawBody,
|
|
196
228
|
bodyForAgent,
|
|
197
229
|
inboundHistory,
|
|
198
|
-
commandBody:
|
|
230
|
+
commandBody: effectiveRawBody,
|
|
199
231
|
envelopeFrom: resolution.originatingTo,
|
|
200
|
-
senderLabel:
|
|
232
|
+
senderLabel: effectiveSenderDisplayName,
|
|
201
233
|
},
|
|
202
234
|
...(isAuthorizedTextCommand
|
|
203
235
|
? {
|
|
@@ -242,9 +274,10 @@ export function buildBncrInboundTurnContext(args: {
|
|
|
242
274
|
applyBncrLegacyCommandFields({
|
|
243
275
|
ctx,
|
|
244
276
|
channelId,
|
|
245
|
-
senderIdForContext,
|
|
277
|
+
senderIdForContext: effectiveSenderId,
|
|
278
|
+
fromOverride: effectiveSenderId,
|
|
246
279
|
resolution,
|
|
247
|
-
rawBody:
|
|
280
|
+
rawBody: effectiveRawBody,
|
|
248
281
|
ownerAllowFrom,
|
|
249
282
|
isAuthorizedTextCommand,
|
|
250
283
|
}),
|
|
@@ -61,11 +61,14 @@ export type BncrSceneRecord = {
|
|
|
61
61
|
groupName?: string;
|
|
62
62
|
agentId?: string;
|
|
63
63
|
groupReplyMode?: BncrGroupReplyMode;
|
|
64
|
+
historyLimit?: number;
|
|
65
|
+
historyForce?: boolean;
|
|
64
66
|
lastSeenAt: number;
|
|
65
67
|
};
|
|
66
68
|
|
|
67
69
|
export type BncrPersistedGroupHistoryEntry = {
|
|
68
70
|
sender: string;
|
|
71
|
+
senderId?: string;
|
|
69
72
|
body: string;
|
|
70
73
|
timestamp?: number;
|
|
71
74
|
messageId?: string;
|
|
@@ -85,7 +85,10 @@ function shouldAccumulateForScene(args: {
|
|
|
85
85
|
if (isAdminOpenClawNativeCommand) return true;
|
|
86
86
|
|
|
87
87
|
const mode = admission.scene.groupReplyMode || 'admin';
|
|
88
|
-
|
|
88
|
+
// admin and all: no accumulation needed - messages are either dispatched directly or discarded
|
|
89
|
+
if (mode === 'admin') return false;
|
|
90
|
+
if (mode === 'all') return false;
|
|
91
|
+
// mention and hybrid: non-trigger messages accumulate for future context
|
|
89
92
|
return true;
|
|
90
93
|
}
|
|
91
94
|
|
|
@@ -56,6 +56,8 @@ export function buildSceneRecord(args: {
|
|
|
56
56
|
...(kind === 'group' && parsed.groupName ? { groupName: parsed.groupName } : {}),
|
|
57
57
|
...(agentId ? { agentId } : {}),
|
|
58
58
|
...(kind === 'group' ? { groupReplyMode: defaultGroupReplyMode(kind) } : {}),
|
|
59
|
+
...(kind === 'group' ? { historyLimit: 50 } : {}),
|
|
60
|
+
...(kind === 'group' ? { historyForce: true } : {}),
|
|
59
61
|
lastSeenAt,
|
|
60
62
|
};
|
|
61
63
|
}
|
|
@@ -88,6 +90,8 @@ export function decideSceneAdmission(args: {
|
|
|
88
90
|
...(kind === 'group' && !existing.groupReplyMode
|
|
89
91
|
? { groupReplyMode: defaultGroupReplyMode(kind) }
|
|
90
92
|
: {}),
|
|
93
|
+
...(kind === 'group' && existing.historyLimit === undefined ? { historyLimit: 50 } : {}),
|
|
94
|
+
...(kind === 'group' && existing.historyForce === undefined ? { historyForce: true } : {}),
|
|
91
95
|
lastSeenAt: now,
|
|
92
96
|
} satisfies BncrSceneRecord;
|
|
93
97
|
|
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
} from './channel-runtime-types.ts';
|
|
21
21
|
|
|
22
22
|
const GROUP_REPLY_MODES = new Set<BncrGroupReplyMode>(['admin', 'mention', 'hybrid', 'all']);
|
|
23
|
+
const DEFAULT_PERSISTED_GROUP_HISTORY_LIMIT = 50;
|
|
23
24
|
|
|
24
25
|
type BncrPersistedStateStoreInput = {
|
|
25
26
|
outbox?: unknown;
|
|
@@ -76,6 +77,18 @@ export function createBncrStateStore(runtime: {
|
|
|
76
77
|
getLastDriftSnapshot: () => BncrPersistedState['lastDriftSnapshot'];
|
|
77
78
|
setLastDriftSnapshot: (value: BncrPersistedState['lastDriftSnapshot']) => void;
|
|
78
79
|
}) {
|
|
80
|
+
function resolvePersistedGroupHistoryLimit(key: string): number {
|
|
81
|
+
const scene = runtime.sceneRegistry.get(key);
|
|
82
|
+
const sceneLimit = scene?.kind === 'group' ? scene.historyLimit : undefined;
|
|
83
|
+
if (typeof sceneLimit === 'number' && Number.isFinite(sceneLimit) && sceneLimit >= 0) {
|
|
84
|
+
return Math.max(
|
|
85
|
+
DEFAULT_PERSISTED_GROUP_HISTORY_LIMIT,
|
|
86
|
+
Math.ceil(Math.floor(sceneLimit) * 1.2),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
return DEFAULT_PERSISTED_GROUP_HISTORY_LIMIT;
|
|
90
|
+
}
|
|
91
|
+
|
|
79
92
|
function loadPersistedSceneRegistry(persisted: unknown): void {
|
|
80
93
|
runtime.sceneRegistry.clear();
|
|
81
94
|
const items = Array.isArray(persisted) ? (persisted as PersistedSceneRecordInput[]) : [];
|
|
@@ -120,6 +133,12 @@ export function createBncrStateStore(runtime: {
|
|
|
120
133
|
.trim() as BncrGroupReplyMode,
|
|
121
134
|
}
|
|
122
135
|
: {}),
|
|
136
|
+
...(typeof item.historyLimit === 'number' &&
|
|
137
|
+
Number.isFinite(item.historyLimit) &&
|
|
138
|
+
item.historyLimit >= 0
|
|
139
|
+
? { historyLimit: Math.floor(item.historyLimit) }
|
|
140
|
+
: {}),
|
|
141
|
+
...(typeof item.historyForce === 'boolean' ? { historyForce: item.historyForce } : {}),
|
|
123
142
|
lastSeenAt,
|
|
124
143
|
});
|
|
125
144
|
}
|
|
@@ -169,6 +188,9 @@ export function createBncrStateStore(runtime: {
|
|
|
169
188
|
|
|
170
189
|
const normalizedEntry: BncrPersistedGroupHistoryEntry = {
|
|
171
190
|
sender,
|
|
191
|
+
...(runtime.asString(entry?.senderId || '').trim()
|
|
192
|
+
? { senderId: runtime.asString(entry?.senderId || '').trim() }
|
|
193
|
+
: {}),
|
|
172
194
|
body,
|
|
173
195
|
...(timestamp > 0 ? { timestamp } : {}),
|
|
174
196
|
...(messageId ? { messageId } : {}),
|
|
@@ -176,14 +198,16 @@ export function createBncrStateStore(runtime: {
|
|
|
176
198
|
};
|
|
177
199
|
entries.push(normalizedEntry);
|
|
178
200
|
}
|
|
179
|
-
if (entries.length > 0)
|
|
201
|
+
if (entries.length > 0) {
|
|
202
|
+
runtime.groupHistories.set(key, entries.slice(-resolvePersistedGroupHistoryLimit(key)));
|
|
203
|
+
}
|
|
180
204
|
}
|
|
181
205
|
}
|
|
182
206
|
|
|
183
207
|
function dumpPersistedGroupHistories() {
|
|
184
208
|
return Array.from(runtime.groupHistories.entries()).map(([key, entries]) => ({
|
|
185
209
|
key,
|
|
186
|
-
entries: entries.slice(-
|
|
210
|
+
entries: entries.slice(-resolvePersistedGroupHistoryLimit(key)),
|
|
187
211
|
}));
|
|
188
212
|
}
|
|
189
213
|
|