@xmoxmo/bncr 0.6.1 → 0.6.3
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 +65 -1
- package/package.json +1 -1
- package/scripts/check-pack.mjs +4 -1
- package/scripts/selfcheck.mjs +4 -1
- package/src/channel.ts +144 -5
- package/src/messaging/inbound/commands.ts +62 -26
- package/src/messaging/inbound/context-facts.ts +88 -58
- package/src/messaging/inbound/conversation-history-serial.ts +258 -0
- package/src/messaging/inbound/conversation-history.ts +450 -0
- package/src/messaging/inbound/dispatch.ts +568 -123
- package/src/messaging/inbound/history-shard-worker.ts +180 -0
- package/src/messaging/inbound/native-command.ts +89 -33
- package/src/messaging/inbound/outbound-replay-cache.ts +202 -17
- package/src/messaging/inbound/reply-dispatch.ts +7 -6
- package/src/messaging/inbound/scene-admin.ts +40 -20
- package/src/messaging/inbound/turn-context.ts +101 -37
- package/src/plugin/bridge-lifecycle.ts +8 -0
- package/src/plugin/bridge-outbox-facade.ts +15 -1
- package/src/plugin/channel-components.ts +5 -5
- package/src/plugin/channel-runtime-builders-delivery.ts +2 -2
- package/src/plugin/channel-runtime-constants.ts +2 -0
- package/src/plugin/channel-runtime-types.ts +9 -7
- package/src/plugin/inbound-acceptance.ts +9 -24
- package/src/plugin/inbound-handlers.ts +4 -4
- package/src/plugin/inbound-surface-handlers-group.ts +4 -2
- package/src/plugin/outbox-drain-post-push.ts +3 -0
- package/src/plugin/scene-registry.ts +4 -4
- package/src/plugin/sqlite-state.ts +2109 -0
- package/src/plugin/state-store.ts +731 -69
- package/src/plugin/state-transient-runtime-group.ts +17 -3
- package/src/messaging/inbound/group-history.ts +0 -264
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import {
|
|
3
|
+
createChannelHistoryWindow,
|
|
4
|
+
DEFAULT_GROUP_HISTORY_LIMIT as DEFAULT_HISTORY_LIMIT,
|
|
5
|
+
type HistoryEntry,
|
|
6
|
+
type HistoryMediaEntry,
|
|
7
|
+
} from 'openclaw/plugin-sdk/reply-history';
|
|
8
|
+
import { formatOpenClawAgentEnvelope } from '../../openclaw/reply-runtime.ts';
|
|
9
|
+
import type { BncrInboundApi } from './contracts.ts';
|
|
10
|
+
import type { ParsedInbound } from './dispatch-prep.ts';
|
|
11
|
+
|
|
12
|
+
export type BncrHistoryEntry = HistoryEntry & {
|
|
13
|
+
senderId?: string;
|
|
14
|
+
role?: 'user' | 'assistant' | 'system';
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type BncrConversationHistoryMap = Map<string, BncrHistoryEntry[]>;
|
|
18
|
+
|
|
19
|
+
type BncrHistoryMediaKind = NonNullable<HistoryMediaEntry['kind']>;
|
|
20
|
+
|
|
21
|
+
const conversationHistoryVersions = new WeakMap<BncrConversationHistoryMap, Map<string, number>>();
|
|
22
|
+
|
|
23
|
+
function buildSyntheticHistoryMessageId(seed: string): string {
|
|
24
|
+
const digest = createHash('sha1').update(seed).digest('hex').slice(0, 24);
|
|
25
|
+
return `bncr-synthetic:${digest}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function buildBncrBotReplyMessageId(args: {
|
|
29
|
+
historyKey: string;
|
|
30
|
+
sender: string;
|
|
31
|
+
senderId?: string;
|
|
32
|
+
body: string;
|
|
33
|
+
timestamp?: number;
|
|
34
|
+
messageId?: string;
|
|
35
|
+
media?: HistoryMediaEntry[];
|
|
36
|
+
}): string {
|
|
37
|
+
const provided = String(args.messageId || '').trim();
|
|
38
|
+
if (provided) return provided;
|
|
39
|
+
return buildSyntheticHistoryMessageId(
|
|
40
|
+
JSON.stringify({
|
|
41
|
+
historyKey: args.historyKey,
|
|
42
|
+
sender: args.sender,
|
|
43
|
+
senderId: args.senderId,
|
|
44
|
+
body: args.body,
|
|
45
|
+
timestamp: args.timestamp,
|
|
46
|
+
media: args.media?.map((item) => [item.path, item.contentType, item.kind, item.messageId]),
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function bumpConversationHistoryVersion(
|
|
52
|
+
historyMap: BncrConversationHistoryMap,
|
|
53
|
+
historyKey: string,
|
|
54
|
+
): number {
|
|
55
|
+
let versions = conversationHistoryVersions.get(historyMap);
|
|
56
|
+
if (!versions) {
|
|
57
|
+
versions = new Map();
|
|
58
|
+
conversationHistoryVersions.set(historyMap, versions);
|
|
59
|
+
}
|
|
60
|
+
const next = (versions.get(historyKey) || 0) + 1;
|
|
61
|
+
versions.set(historyKey, next);
|
|
62
|
+
return next;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function readConversationHistoryVersion(
|
|
66
|
+
historyMap: BncrConversationHistoryMap,
|
|
67
|
+
historyKey: string,
|
|
68
|
+
): number {
|
|
69
|
+
return conversationHistoryVersions.get(historyMap)?.get(historyKey) ?? 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function resetConversationHistoryVersions(historyMap: BncrConversationHistoryMap): void {
|
|
73
|
+
conversationHistoryVersions.delete(historyMap);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function normalizeTextBody(value: string): string {
|
|
77
|
+
return String(value || '')
|
|
78
|
+
.replace(/\s+/g, ' ')
|
|
79
|
+
.trim();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function resolveBncrHistoryLimit(historyLimit?: number): number {
|
|
83
|
+
if (typeof historyLimit !== 'number' || !Number.isFinite(historyLimit)) {
|
|
84
|
+
return DEFAULT_HISTORY_LIMIT;
|
|
85
|
+
}
|
|
86
|
+
const resolved = Math.floor(historyLimit);
|
|
87
|
+
return resolved >= 2 ? resolved : DEFAULT_HISTORY_LIMIT;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function buildBncrConversationHistoryKey(parsed: ParsedInbound): string | null {
|
|
91
|
+
const platform = String(parsed.platform || '').trim();
|
|
92
|
+
if (!platform) return null;
|
|
93
|
+
if (parsed.peer.kind === 'group') {
|
|
94
|
+
const groupId = String(parsed.groupId || '').trim();
|
|
95
|
+
if (!groupId || groupId === '0') return null;
|
|
96
|
+
return `${platform}:${groupId}`;
|
|
97
|
+
}
|
|
98
|
+
const userId = String(parsed.userId || '').trim();
|
|
99
|
+
if (!userId || userId === '0') return null;
|
|
100
|
+
return `${platform}:${userId}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function buildBncrConversationHistoryKeyFromRoute(args: {
|
|
104
|
+
platform?: string;
|
|
105
|
+
groupId?: string;
|
|
106
|
+
userId?: string;
|
|
107
|
+
}): string | null {
|
|
108
|
+
const platform = String(args.platform || '').trim();
|
|
109
|
+
if (!platform) return null;
|
|
110
|
+
const groupId = String(args.groupId || '0').trim() || '0';
|
|
111
|
+
const userId = String(args.userId || '0').trim() || '0';
|
|
112
|
+
if (groupId !== '0') return `${platform}:${groupId}`;
|
|
113
|
+
if (userId !== '0') return `${platform}:${userId}`;
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function resolveBncrConversationHistoryMessageId(parsed: ParsedInbound): string | undefined {
|
|
118
|
+
const msgId = String(parsed.msgId || '').trim();
|
|
119
|
+
return msgId || undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function recordBncrPendingConversationText(args: {
|
|
123
|
+
historyMap: BncrConversationHistoryMap;
|
|
124
|
+
parsed: ParsedInbound;
|
|
125
|
+
senderDisplayName: string;
|
|
126
|
+
senderId: string;
|
|
127
|
+
bodyText: string;
|
|
128
|
+
historyLimit?: number;
|
|
129
|
+
}) {
|
|
130
|
+
const limit = resolveBncrHistoryLimit(args.historyLimit);
|
|
131
|
+
const historyKey = buildBncrConversationHistoryKey(args.parsed);
|
|
132
|
+
const body = normalizeTextBody(args.bodyText);
|
|
133
|
+
if (!historyKey || !body || args.parsed.msgType !== 'text') return;
|
|
134
|
+
const messageId = resolveBncrConversationHistoryMessageId(args.parsed);
|
|
135
|
+
if (messageId) {
|
|
136
|
+
const existing = args.historyMap.get(historyKey) || [];
|
|
137
|
+
if (existing.some((e) => e.messageId === messageId)) return;
|
|
138
|
+
}
|
|
139
|
+
createChannelHistoryWindow({ historyMap: args.historyMap }).record({
|
|
140
|
+
historyKey,
|
|
141
|
+
limit,
|
|
142
|
+
entry: {
|
|
143
|
+
sender: args.senderDisplayName,
|
|
144
|
+
senderId: args.senderId,
|
|
145
|
+
body,
|
|
146
|
+
timestamp: Date.now(),
|
|
147
|
+
...(messageId ? { messageId } : {}),
|
|
148
|
+
role: 'user',
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
bumpConversationHistoryVersion(args.historyMap, historyKey);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function inferBncrHistoryMediaKind(args: {
|
|
155
|
+
msgType?: string;
|
|
156
|
+
mediaContentType?: string;
|
|
157
|
+
}): BncrHistoryMediaKind {
|
|
158
|
+
const msgType = String(args.msgType || '')
|
|
159
|
+
.trim()
|
|
160
|
+
.toLowerCase();
|
|
161
|
+
const contentType = String(args.mediaContentType || '')
|
|
162
|
+
.trim()
|
|
163
|
+
.toLowerCase();
|
|
164
|
+
if (msgType === 'image' || contentType.startsWith('image/')) return 'image';
|
|
165
|
+
if (msgType === 'video' || contentType.startsWith('video/')) return 'video';
|
|
166
|
+
if (msgType === 'audio' || msgType === 'voice' || contentType.startsWith('audio/'))
|
|
167
|
+
return 'audio';
|
|
168
|
+
if (msgType === 'file' || msgType === 'document') return 'document';
|
|
169
|
+
if (contentType) return 'document';
|
|
170
|
+
return 'unknown';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function buildBncrHistoryMediaBody(kind: BncrHistoryMediaKind): string {
|
|
174
|
+
return `<media:${kind}>`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function recordBncrPendingConversationMedia(args: {
|
|
178
|
+
historyMap: BncrConversationHistoryMap;
|
|
179
|
+
parsed: ParsedInbound;
|
|
180
|
+
senderDisplayName: string;
|
|
181
|
+
senderId: string;
|
|
182
|
+
bodyText: string;
|
|
183
|
+
mediaItems?: Array<{
|
|
184
|
+
path: string;
|
|
185
|
+
contentType?: string;
|
|
186
|
+
kind?: BncrHistoryMediaKind;
|
|
187
|
+
}>;
|
|
188
|
+
mediaContentType?: string;
|
|
189
|
+
historyLimit?: number;
|
|
190
|
+
}) {
|
|
191
|
+
const limit = resolveBncrHistoryLimit(args.historyLimit);
|
|
192
|
+
const historyKey = buildBncrConversationHistoryKey(args.parsed);
|
|
193
|
+
if (!historyKey || args.parsed.msgType === 'text') return;
|
|
194
|
+
const normalizedMediaItems = Array.isArray(args.mediaItems) ? args.mediaItems : [];
|
|
195
|
+
const itemKinds = normalizedMediaItems
|
|
196
|
+
.map((item) =>
|
|
197
|
+
String(item?.kind || '')
|
|
198
|
+
.trim()
|
|
199
|
+
.toLowerCase(),
|
|
200
|
+
)
|
|
201
|
+
.filter(Boolean);
|
|
202
|
+
const kind = itemKinds[0]
|
|
203
|
+
? itemKinds.every((candidate) => candidate === itemKinds[0])
|
|
204
|
+
? (itemKinds[0] as BncrHistoryMediaKind)
|
|
205
|
+
: 'document'
|
|
206
|
+
: inferBncrHistoryMediaKind({
|
|
207
|
+
msgType: args.parsed.msgType,
|
|
208
|
+
mediaContentType: args.mediaContentType,
|
|
209
|
+
});
|
|
210
|
+
const body = normalizeTextBody(args.bodyText) || buildBncrHistoryMediaBody(kind);
|
|
211
|
+
if (normalizedMediaItems.length === 0) {
|
|
212
|
+
const messageId = resolveBncrConversationHistoryMessageId(args.parsed);
|
|
213
|
+
if (messageId) {
|
|
214
|
+
const existing = args.historyMap.get(historyKey) || [];
|
|
215
|
+
if (existing.some((e) => e.messageId === messageId)) return;
|
|
216
|
+
}
|
|
217
|
+
createChannelHistoryWindow({ historyMap: args.historyMap }).record({
|
|
218
|
+
historyKey,
|
|
219
|
+
limit,
|
|
220
|
+
entry: {
|
|
221
|
+
sender: args.senderDisplayName,
|
|
222
|
+
senderId: args.senderId,
|
|
223
|
+
body,
|
|
224
|
+
timestamp: Date.now(),
|
|
225
|
+
...(messageId ? { messageId } : {}),
|
|
226
|
+
role: 'user',
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
bumpConversationHistoryVersion(args.historyMap, historyKey);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const messageId = resolveBncrConversationHistoryMessageId(args.parsed);
|
|
233
|
+
if (messageId) {
|
|
234
|
+
const existing = args.historyMap.get(historyKey) || [];
|
|
235
|
+
if (existing.some((e) => e.messageId === messageId)) return;
|
|
236
|
+
}
|
|
237
|
+
createChannelHistoryWindow({ historyMap: args.historyMap }).record({
|
|
238
|
+
historyKey,
|
|
239
|
+
limit,
|
|
240
|
+
entry: {
|
|
241
|
+
sender: args.senderDisplayName,
|
|
242
|
+
senderId: args.senderId,
|
|
243
|
+
body,
|
|
244
|
+
timestamp: Date.now(),
|
|
245
|
+
...(messageId ? { messageId } : {}),
|
|
246
|
+
role: 'user',
|
|
247
|
+
media: normalizedMediaItems.map(
|
|
248
|
+
(item) =>
|
|
249
|
+
({
|
|
250
|
+
path: item.path,
|
|
251
|
+
contentType: item.contentType || args.mediaContentType,
|
|
252
|
+
kind: item.kind || kind,
|
|
253
|
+
messageId,
|
|
254
|
+
}) satisfies HistoryMediaEntry,
|
|
255
|
+
),
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
bumpConversationHistoryVersion(args.historyMap, historyKey);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function recordBncrBotReply(args: {
|
|
262
|
+
historyMap: BncrConversationHistoryMap;
|
|
263
|
+
historyKey: string;
|
|
264
|
+
sender: string;
|
|
265
|
+
senderId?: string;
|
|
266
|
+
body: string;
|
|
267
|
+
timestamp?: number;
|
|
268
|
+
messageId?: string;
|
|
269
|
+
media?: HistoryMediaEntry[];
|
|
270
|
+
historyLimit?: number;
|
|
271
|
+
}): boolean {
|
|
272
|
+
const limit = resolveBncrHistoryLimit(args.historyLimit);
|
|
273
|
+
const body = normalizeTextBody(args.body);
|
|
274
|
+
if (!body) return false;
|
|
275
|
+
const msgId = buildBncrBotReplyMessageId({
|
|
276
|
+
historyKey: args.historyKey,
|
|
277
|
+
sender: args.sender,
|
|
278
|
+
senderId: args.senderId,
|
|
279
|
+
body,
|
|
280
|
+
timestamp: args.timestamp,
|
|
281
|
+
messageId: args.messageId,
|
|
282
|
+
media: args.media,
|
|
283
|
+
});
|
|
284
|
+
const existing = args.historyMap.get(args.historyKey) || [];
|
|
285
|
+
if (existing.some((e) => e.messageId === msgId)) return false;
|
|
286
|
+
createChannelHistoryWindow({ historyMap: args.historyMap }).record({
|
|
287
|
+
historyKey: args.historyKey,
|
|
288
|
+
limit,
|
|
289
|
+
entry: {
|
|
290
|
+
sender: args.sender,
|
|
291
|
+
...(args.senderId ? { senderId: args.senderId } : {}),
|
|
292
|
+
body,
|
|
293
|
+
timestamp: args.timestamp ?? Date.now(),
|
|
294
|
+
messageId: msgId,
|
|
295
|
+
role: 'assistant',
|
|
296
|
+
...(Array.isArray(args.media) && args.media.length > 0
|
|
297
|
+
? {
|
|
298
|
+
media: args.media.map((item) => ({
|
|
299
|
+
...(item?.path ? { path: item.path } : {}),
|
|
300
|
+
...(item?.contentType ? { contentType: item.contentType } : {}),
|
|
301
|
+
...(item?.kind ? { kind: item.kind } : {}),
|
|
302
|
+
messageId: item?.messageId || msgId,
|
|
303
|
+
})),
|
|
304
|
+
}
|
|
305
|
+
: {}),
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
bumpConversationHistoryVersion(args.historyMap, args.historyKey);
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function cloneBncrHistoryEntry(entry: BncrHistoryEntry): BncrHistoryEntry {
|
|
313
|
+
return {
|
|
314
|
+
sender: entry.sender,
|
|
315
|
+
...(entry.messageId ? { messageId: entry.messageId } : {}),
|
|
316
|
+
...(typeof entry.timestamp === 'number' ? { timestamp: entry.timestamp } : {}),
|
|
317
|
+
...(entry.senderId ? { senderId: entry.senderId } : {}),
|
|
318
|
+
...(entry.role ? { role: entry.role } : {}),
|
|
319
|
+
body: entry.body,
|
|
320
|
+
...(Array.isArray(entry.media)
|
|
321
|
+
? {
|
|
322
|
+
media: entry.media.map((item) => ({
|
|
323
|
+
...(item?.path ? { path: item.path } : {}),
|
|
324
|
+
...(item?.contentType ? { contentType: item.contentType } : {}),
|
|
325
|
+
...(item?.kind ? { kind: item.kind } : {}),
|
|
326
|
+
...(item?.messageId ? { messageId: item.messageId } : {}),
|
|
327
|
+
})),
|
|
328
|
+
}
|
|
329
|
+
: {}),
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function readBncrPendingConversationHistorySnapshot(args: {
|
|
334
|
+
historyMap: BncrConversationHistoryMap;
|
|
335
|
+
parsed: ParsedInbound;
|
|
336
|
+
historyLimit?: number;
|
|
337
|
+
}): BncrHistoryEntry[] {
|
|
338
|
+
const limit = resolveBncrHistoryLimit(args.historyLimit);
|
|
339
|
+
const historyKey = buildBncrConversationHistoryKey(args.parsed);
|
|
340
|
+
if (!historyKey) return [];
|
|
341
|
+
const rawEntries = args.historyMap.get(historyKey) || [];
|
|
342
|
+
if (rawEntries.length === 0) return [];
|
|
343
|
+
let normalized = false;
|
|
344
|
+
const entries = rawEntries.map((entry, entryIndex) => {
|
|
345
|
+
const messageId =
|
|
346
|
+
String(entry.messageId || '').trim() ||
|
|
347
|
+
buildSyntheticHistoryMessageId(
|
|
348
|
+
JSON.stringify({
|
|
349
|
+
historyKey,
|
|
350
|
+
entryIndex,
|
|
351
|
+
sender: entry.sender,
|
|
352
|
+
senderId: entry.senderId,
|
|
353
|
+
body: entry.body,
|
|
354
|
+
timestamp: entry.timestamp,
|
|
355
|
+
}),
|
|
356
|
+
);
|
|
357
|
+
if (messageId === entry.messageId) return entry;
|
|
358
|
+
normalized = true;
|
|
359
|
+
return {
|
|
360
|
+
...entry,
|
|
361
|
+
messageId,
|
|
362
|
+
...(Array.isArray(entry.media)
|
|
363
|
+
? {
|
|
364
|
+
media: entry.media.map((item) => ({
|
|
365
|
+
...item,
|
|
366
|
+
messageId: item.messageId || messageId,
|
|
367
|
+
})),
|
|
368
|
+
}
|
|
369
|
+
: {}),
|
|
370
|
+
};
|
|
371
|
+
});
|
|
372
|
+
if (normalized) args.historyMap.set(historyKey, entries);
|
|
373
|
+
return entries
|
|
374
|
+
.slice()
|
|
375
|
+
.sort((a, b) => (a.timestamp ?? 0) - (b.timestamp ?? 0))
|
|
376
|
+
.slice(-limit)
|
|
377
|
+
.map(cloneBncrHistoryEntry);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function buildBncrConversationContextFromEntries(args: {
|
|
381
|
+
api: BncrInboundApi;
|
|
382
|
+
entries: readonly BncrHistoryEntry[];
|
|
383
|
+
channelLabel: string;
|
|
384
|
+
currentTimestamp: number;
|
|
385
|
+
previousTimestamp?: unknown;
|
|
386
|
+
envelope?: unknown;
|
|
387
|
+
currentMessage: string;
|
|
388
|
+
}) {
|
|
389
|
+
if (args.entries.length === 0) return args.currentMessage;
|
|
390
|
+
const historyText = args.entries
|
|
391
|
+
.map((entry) =>
|
|
392
|
+
formatOpenClawAgentEnvelope(args.api, {
|
|
393
|
+
channel: 'Bncr',
|
|
394
|
+
from:
|
|
395
|
+
(entry.senderId
|
|
396
|
+
? `${entry.sender} (${entry.senderId})`
|
|
397
|
+
: entry.sender || args.channelLabel) + (entry.messageId ? ` #${entry.messageId}` : ''),
|
|
398
|
+
timestamp: entry.timestamp || args.currentTimestamp,
|
|
399
|
+
previousTimestamp: args.previousTimestamp,
|
|
400
|
+
envelope: args.envelope,
|
|
401
|
+
body: entry.body,
|
|
402
|
+
}),
|
|
403
|
+
)
|
|
404
|
+
.join('\n');
|
|
405
|
+
return `[Chat messages since your last reply - for context]\n${historyText}\n\n[Current message - respond to this]${args.currentMessage}`;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export function collectBncrHistoryMediaFromEntries(args: {
|
|
409
|
+
entries: readonly BncrHistoryEntry[];
|
|
410
|
+
}): HistoryMediaEntry[] {
|
|
411
|
+
return args.entries.flatMap((entry) => (Array.isArray(entry.media) ? entry.media : []));
|
|
412
|
+
}
|
|
413
|
+
export function clearBncrPendingConversationHistory(args: {
|
|
414
|
+
historyMap: BncrConversationHistoryMap;
|
|
415
|
+
parsed: ParsedInbound;
|
|
416
|
+
historyLimit?: number;
|
|
417
|
+
}) {
|
|
418
|
+
const limit = resolveBncrHistoryLimit(args.historyLimit);
|
|
419
|
+
const historyKey = buildBncrConversationHistoryKey(args.parsed);
|
|
420
|
+
if (!historyKey) return;
|
|
421
|
+
createChannelHistoryWindow({ historyMap: args.historyMap }).clear({
|
|
422
|
+
historyKey,
|
|
423
|
+
limit: limit,
|
|
424
|
+
});
|
|
425
|
+
bumpConversationHistoryVersion(args.historyMap, historyKey);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export function removeBncrConversationHistoryMessageIds(args: {
|
|
429
|
+
historyMap: BncrConversationHistoryMap;
|
|
430
|
+
historyKey: string;
|
|
431
|
+
messageIds: ReadonlyArray<string | undefined | null>;
|
|
432
|
+
}): number {
|
|
433
|
+
const messageIds = new Set(
|
|
434
|
+
args.messageIds
|
|
435
|
+
.map((messageId) => String(messageId || '').trim())
|
|
436
|
+
.filter((messageId) => Boolean(messageId)),
|
|
437
|
+
);
|
|
438
|
+
if (messageIds.size === 0) return 0;
|
|
439
|
+
const current = args.historyMap.get(args.historyKey) || [];
|
|
440
|
+
if (current.length === 0) return 0;
|
|
441
|
+
const next = current.filter(
|
|
442
|
+
(entry) =>
|
|
443
|
+
!String(entry.messageId || '').trim() || !messageIds.has(String(entry.messageId).trim()),
|
|
444
|
+
);
|
|
445
|
+
if (next.length === current.length) return 0;
|
|
446
|
+
args.historyMap.set(args.historyKey, next);
|
|
447
|
+
// Snapshot cleanup must not invalidate flushes queued from replies written
|
|
448
|
+
// while the earlier snapshot was uploading.
|
|
449
|
+
return current.length - next.length;
|
|
450
|
+
}
|