@xmoxmo/bncr 0.6.0 → 0.6.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/README.md +8 -0
- package/dist/index.js +2 -0
- package/index.ts +1 -0
- package/package.json +4 -1
- package/scripts/check-pack.mjs +41 -1
- package/scripts/selfcheck.mjs +4 -1
- package/src/bootstrap/register-runtime-gateway.ts +1 -0
- package/src/bootstrap/register-runtime.ts +1 -0
- package/src/channel.ts +66 -5
- package/src/core/types.ts +41 -0
- package/src/messaging/inbound/context-facts.ts +88 -58
- package/src/messaging/inbound/{group-history.ts → conversation-history.ts} +92 -25
- package/src/messaging/inbound/dispatch.ts +90 -47
- package/src/messaging/inbound/native-command.ts +1 -1
- package/src/messaging/inbound/outbound-replay-cache.ts +312 -0
- package/src/messaging/inbound/scene-admin.ts +24 -9
- package/src/messaging/inbound/turn-context.ts +105 -33
- package/src/plugin/ack-outbox-runtime-group.ts +2 -0
- package/src/plugin/bridge-call.ts +168 -0
- package/src/plugin/bridge-outbox-facade.ts +88 -1
- package/src/plugin/channel-components.ts +10 -5
- package/src/plugin/channel-plugin-bridge-group.ts +43 -1
- package/src/plugin/channel-plugin-surface-group.ts +82 -7
- package/src/plugin/channel-runtime-builders-delivery.ts +4 -2
- package/src/plugin/channel-runtime-types.ts +34 -8
- package/src/plugin/channel-send.ts +12 -19
- package/src/plugin/client-rpc-runtime.ts +129 -0
- package/src/plugin/gateway-methods.ts +1 -0
- package/src/plugin/inbound-acceptance.ts +9 -24
- package/src/plugin/inbound-handlers.ts +8 -4
- package/src/plugin/inbound-surface-handlers-group.ts +7 -2
- package/src/plugin/outbox-ack-outcome.ts +2 -0
- package/src/plugin/scene-registry.ts +4 -4
- package/src/plugin/state-store.ts +153 -29
- package/src/plugin/state-transient-runtime-group.ts +10 -3
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BncrOutboundReplayEntry,
|
|
3
|
+
BncrOutboundReplayMediaEntry,
|
|
4
|
+
BncrOutboundReplayStatus,
|
|
5
|
+
OutboxEntry,
|
|
6
|
+
} from '../../core/types.ts';
|
|
7
|
+
import {
|
|
8
|
+
type BncrConversationHistoryMap,
|
|
9
|
+
buildBncrConversationHistoryKey,
|
|
10
|
+
buildBncrConversationHistoryKeyFromRoute,
|
|
11
|
+
recordBncrBotReply,
|
|
12
|
+
} from './conversation-history.ts';
|
|
13
|
+
import type { ParsedInbound } from './dispatch-prep.ts';
|
|
14
|
+
|
|
15
|
+
export type {
|
|
16
|
+
BncrOutboundReplayEntry,
|
|
17
|
+
BncrOutboundReplayMediaEntry,
|
|
18
|
+
BncrOutboundReplayStatus,
|
|
19
|
+
} from '../../core/types.ts';
|
|
20
|
+
|
|
21
|
+
export type BncrOutboundReplayCache = Map<string, BncrOutboundReplayEntry[]>;
|
|
22
|
+
|
|
23
|
+
function normalizeTextBody(value: string): string {
|
|
24
|
+
return String(value || '')
|
|
25
|
+
.replace(/\s+/g, ' ')
|
|
26
|
+
.trim();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function cloneMediaEntry(entry: BncrOutboundReplayMediaEntry): BncrOutboundReplayMediaEntry {
|
|
30
|
+
return {
|
|
31
|
+
...(entry?.path ? { path: entry.path } : {}),
|
|
32
|
+
...(entry?.contentType ? { contentType: entry.contentType } : {}),
|
|
33
|
+
...(entry?.kind ? { kind: entry.kind } : {}),
|
|
34
|
+
...(entry?.messageId ? { messageId: entry.messageId } : {}),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function cloneBncrOutboundReplayEntry(
|
|
39
|
+
entry: BncrOutboundReplayEntry,
|
|
40
|
+
): BncrOutboundReplayEntry {
|
|
41
|
+
return {
|
|
42
|
+
sender: entry.sender,
|
|
43
|
+
...(entry.senderId ? { senderId: entry.senderId } : {}),
|
|
44
|
+
body: entry.body,
|
|
45
|
+
...(typeof entry.timestamp === 'number' ? { timestamp: entry.timestamp } : {}),
|
|
46
|
+
...(entry.messageId ? { messageId: entry.messageId } : {}),
|
|
47
|
+
...(Array.isArray(entry.media)
|
|
48
|
+
? { media: entry.media.map((item) => cloneMediaEntry(item)) }
|
|
49
|
+
: {}),
|
|
50
|
+
...(entry.accountId ? { accountId: entry.accountId } : {}),
|
|
51
|
+
...(entry.sessionKey ? { sessionKey: entry.sessionKey } : {}),
|
|
52
|
+
...(entry.route ? { route: { ...entry.route } } : {}),
|
|
53
|
+
...(entry.type ? { type: entry.type } : {}),
|
|
54
|
+
...(entry.mediaUrl ? { mediaUrl: entry.mediaUrl } : {}),
|
|
55
|
+
...(typeof entry.createdAt === 'number' ? { createdAt: entry.createdAt } : {}),
|
|
56
|
+
...(entry.status ? { status: entry.status } : {}),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function buildBncrOutboundReplayKeyFromRoute(args: {
|
|
61
|
+
accountId?: string;
|
|
62
|
+
platform?: string;
|
|
63
|
+
groupId?: string;
|
|
64
|
+
userId?: string;
|
|
65
|
+
}): string | null {
|
|
66
|
+
const accountId = String(args.accountId || '').trim() || 'default';
|
|
67
|
+
const platform = String(args.platform || '').trim();
|
|
68
|
+
const groupId = String(args.groupId || '0').trim() || '0';
|
|
69
|
+
const userId = String(args.userId || '0').trim() || '0';
|
|
70
|
+
if (!platform) return null;
|
|
71
|
+
if (groupId !== '0') return `${accountId}:${platform}:${groupId}`;
|
|
72
|
+
if (userId !== '0') return `${accountId}:${platform}:${userId}`;
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function buildBncrOutboundReplayKey(
|
|
77
|
+
parsed: ParsedInbound,
|
|
78
|
+
accountId: string,
|
|
79
|
+
): string | null {
|
|
80
|
+
return buildBncrOutboundReplayKeyFromRoute({
|
|
81
|
+
accountId,
|
|
82
|
+
platform: parsed.platform,
|
|
83
|
+
groupId: parsed.groupId,
|
|
84
|
+
userId: parsed.userId,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function recordOutboundReplayEntry(args: {
|
|
89
|
+
cache: BncrOutboundReplayCache;
|
|
90
|
+
cacheKey: string;
|
|
91
|
+
entry: BncrOutboundReplayEntry;
|
|
92
|
+
}) {
|
|
93
|
+
const msgId = String(args.entry.messageId || '').trim();
|
|
94
|
+
const existing = args.cache.get(args.cacheKey) || [];
|
|
95
|
+
if (msgId && existing.some((entry) => entry.messageId === msgId)) return;
|
|
96
|
+
|
|
97
|
+
const next = [...existing, cloneBncrOutboundReplayEntry(args.entry)].sort(
|
|
98
|
+
(a, b) => (a.timestamp ?? 0) - (b.timestamp ?? 0),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
if (args.cache.has(args.cacheKey)) {
|
|
102
|
+
args.cache.delete(args.cacheKey);
|
|
103
|
+
}
|
|
104
|
+
args.cache.set(args.cacheKey, next);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function inferMediaKind(args: {
|
|
108
|
+
msgType?: string;
|
|
109
|
+
contentType?: string;
|
|
110
|
+
mediaUrl?: string;
|
|
111
|
+
type?: string;
|
|
112
|
+
}): NonNullable<BncrOutboundReplayMediaEntry['kind']> {
|
|
113
|
+
const msgType = String(args.msgType || '')
|
|
114
|
+
.trim()
|
|
115
|
+
.toLowerCase();
|
|
116
|
+
const type = String(args.type || '')
|
|
117
|
+
.trim()
|
|
118
|
+
.toLowerCase();
|
|
119
|
+
const contentType = String(args.contentType || '')
|
|
120
|
+
.trim()
|
|
121
|
+
.toLowerCase();
|
|
122
|
+
const mediaUrl = String(args.mediaUrl || '')
|
|
123
|
+
.trim()
|
|
124
|
+
.toLowerCase();
|
|
125
|
+
|
|
126
|
+
if (msgType === 'image' || type === 'image' || contentType.startsWith('image/')) return 'image';
|
|
127
|
+
if (msgType === 'video' || type === 'video' || contentType.startsWith('video/')) return 'video';
|
|
128
|
+
if (
|
|
129
|
+
msgType === 'audio' ||
|
|
130
|
+
msgType === 'voice' ||
|
|
131
|
+
type === 'audio' ||
|
|
132
|
+
type === 'voice' ||
|
|
133
|
+
contentType.startsWith('audio/')
|
|
134
|
+
) {
|
|
135
|
+
return 'audio';
|
|
136
|
+
}
|
|
137
|
+
if (type === 'file' || type === 'document' || msgType === 'file' || msgType === 'document') {
|
|
138
|
+
return 'document';
|
|
139
|
+
}
|
|
140
|
+
if (/\.(png|jpe?g|gif|webp|bmp|svg)$/.test(mediaUrl)) return 'image';
|
|
141
|
+
if (/\.(mp4|mov|webm|mkv|avi)$/.test(mediaUrl)) return 'video';
|
|
142
|
+
if (/\.(mp3|ogg|oga|silk|amr|wav|m4a|aac|flac)$/.test(mediaUrl)) return 'audio';
|
|
143
|
+
if (contentType) return 'document';
|
|
144
|
+
return 'unknown';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function buildMediaBody(kind: NonNullable<BncrOutboundReplayMediaEntry['kind']>): string {
|
|
148
|
+
return `<media:${kind}>`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function isAppMsg(type: string | undefined, msg: string): boolean {
|
|
152
|
+
return (
|
|
153
|
+
String(type || '')
|
|
154
|
+
.trim()
|
|
155
|
+
.toLowerCase() === 'appmsg' || /^<appmsg/i.test(msg)
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function recordBncrOutboundReplay(args: {
|
|
160
|
+
cache: BncrOutboundReplayCache;
|
|
161
|
+
conversationHistories?: BncrConversationHistoryMap;
|
|
162
|
+
historyLimit?: number;
|
|
163
|
+
entry: OutboxEntry;
|
|
164
|
+
sender: string;
|
|
165
|
+
senderId?: string;
|
|
166
|
+
status?: BncrOutboundReplayStatus;
|
|
167
|
+
}) {
|
|
168
|
+
const cacheKey = buildBncrOutboundReplayKeyFromRoute({
|
|
169
|
+
accountId: args.entry.accountId,
|
|
170
|
+
platform: args.entry.route?.platform,
|
|
171
|
+
groupId: args.entry.route?.groupId,
|
|
172
|
+
userId: args.entry.route?.userId,
|
|
173
|
+
});
|
|
174
|
+
if (!cacheKey) return;
|
|
175
|
+
|
|
176
|
+
const message =
|
|
177
|
+
args.entry.payload?.message && typeof args.entry.payload.message === 'object'
|
|
178
|
+
? (args.entry.payload.message as Record<string, unknown>)
|
|
179
|
+
: {};
|
|
180
|
+
const type = typeof message.type === 'string' ? message.type : undefined;
|
|
181
|
+
const rawBody = typeof message.msg === 'string' ? message.msg : '';
|
|
182
|
+
const mediaUrl = typeof message.mediaUrl === 'string' ? message.mediaUrl.trim() : '';
|
|
183
|
+
const kind = inferMediaKind({ type, mediaUrl });
|
|
184
|
+
const isMedia =
|
|
185
|
+
message.transferMode === 'media' || Boolean(mediaUrl) || (!!type && type !== 'text');
|
|
186
|
+
const body = isAppMsg(type, rawBody)
|
|
187
|
+
? buildMediaBody('document')
|
|
188
|
+
: normalizeTextBody(rawBody) || (isMedia ? buildMediaBody(kind) : '');
|
|
189
|
+
if (!body) return;
|
|
190
|
+
const recordTimestamp = args.entry.lastPushAt ?? Date.now();
|
|
191
|
+
|
|
192
|
+
if (args.conversationHistories) {
|
|
193
|
+
const historyKey = buildBncrConversationHistoryKeyFromRoute({
|
|
194
|
+
platform: args.entry.route?.platform,
|
|
195
|
+
groupId: args.entry.route?.groupId,
|
|
196
|
+
userId: args.entry.route?.userId,
|
|
197
|
+
});
|
|
198
|
+
if (historyKey) {
|
|
199
|
+
recordBncrBotReply({
|
|
200
|
+
historyMap: args.conversationHistories,
|
|
201
|
+
historyKey,
|
|
202
|
+
sender: args.sender,
|
|
203
|
+
senderId: args.senderId,
|
|
204
|
+
body,
|
|
205
|
+
timestamp: recordTimestamp,
|
|
206
|
+
messageId: args.entry.messageId,
|
|
207
|
+
historyLimit: args.historyLimit,
|
|
208
|
+
...(mediaUrl
|
|
209
|
+
? {
|
|
210
|
+
media: [
|
|
211
|
+
{
|
|
212
|
+
path: mediaUrl,
|
|
213
|
+
kind,
|
|
214
|
+
messageId: args.entry.messageId,
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
}
|
|
218
|
+
: {}),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
recordOutboundReplayEntry({
|
|
224
|
+
cache: args.cache,
|
|
225
|
+
cacheKey,
|
|
226
|
+
entry: {
|
|
227
|
+
sender: args.sender,
|
|
228
|
+
...(args.senderId ? { senderId: args.senderId } : {}),
|
|
229
|
+
body,
|
|
230
|
+
timestamp: recordTimestamp,
|
|
231
|
+
messageId: args.entry.messageId,
|
|
232
|
+
accountId: args.entry.accountId,
|
|
233
|
+
sessionKey: args.entry.sessionKey,
|
|
234
|
+
route: { ...args.entry.route },
|
|
235
|
+
...(type ? { type } : {}),
|
|
236
|
+
...(mediaUrl ? { mediaUrl } : {}),
|
|
237
|
+
createdAt: args.entry.createdAt,
|
|
238
|
+
...(args.status ? { status: args.status } : {}),
|
|
239
|
+
...(mediaUrl
|
|
240
|
+
? {
|
|
241
|
+
media: [
|
|
242
|
+
{
|
|
243
|
+
path: mediaUrl,
|
|
244
|
+
kind,
|
|
245
|
+
messageId: args.entry.messageId,
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
}
|
|
249
|
+
: {}),
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function readBncrOutboundReplaySnapshot(args: {
|
|
255
|
+
cache: BncrOutboundReplayCache;
|
|
256
|
+
conversationHistories?: BncrConversationHistoryMap;
|
|
257
|
+
parsed: ParsedInbound;
|
|
258
|
+
accountId: string;
|
|
259
|
+
excludeMessageId?: string | null;
|
|
260
|
+
}): BncrOutboundReplayEntry[] {
|
|
261
|
+
const excludedMessageId = String(args.excludeMessageId || '').trim();
|
|
262
|
+
const cacheKey = buildBncrOutboundReplayKey(args.parsed, args.accountId);
|
|
263
|
+
const legacyEntries = cacheKey
|
|
264
|
+
? (args.cache.get(cacheKey) || [])
|
|
265
|
+
.filter((entry) => !excludedMessageId || entry.messageId !== excludedMessageId)
|
|
266
|
+
.map(cloneBncrOutboundReplayEntry)
|
|
267
|
+
: [];
|
|
268
|
+
if (!args.conversationHistories) return legacyEntries;
|
|
269
|
+
|
|
270
|
+
const historyKey = buildBncrConversationHistoryKey(args.parsed);
|
|
271
|
+
const historyEntries = historyKey
|
|
272
|
+
? (args.conversationHistories.get(historyKey) || [])
|
|
273
|
+
.filter((entry) => entry.role === 'assistant')
|
|
274
|
+
.filter((entry) => !excludedMessageId || entry.messageId !== excludedMessageId)
|
|
275
|
+
.map((entry) => ({
|
|
276
|
+
sender: entry.sender,
|
|
277
|
+
...(entry.senderId ? { senderId: entry.senderId } : {}),
|
|
278
|
+
body: entry.body,
|
|
279
|
+
...(typeof entry.timestamp === 'number' ? { timestamp: entry.timestamp } : {}),
|
|
280
|
+
...(entry.messageId ? { messageId: entry.messageId } : {}),
|
|
281
|
+
...(Array.isArray(entry.media)
|
|
282
|
+
? {
|
|
283
|
+
media: entry.media.map((item) => ({
|
|
284
|
+
path: item.path,
|
|
285
|
+
contentType: item.contentType,
|
|
286
|
+
kind: item.kind,
|
|
287
|
+
messageId: item.messageId,
|
|
288
|
+
})),
|
|
289
|
+
}
|
|
290
|
+
: {}),
|
|
291
|
+
}))
|
|
292
|
+
: [];
|
|
293
|
+
const knownMessageIds = new Set(
|
|
294
|
+
historyEntries
|
|
295
|
+
.map((entry) => entry.messageId)
|
|
296
|
+
.filter((messageId): messageId is string => Boolean(messageId)),
|
|
297
|
+
);
|
|
298
|
+
return [
|
|
299
|
+
...historyEntries,
|
|
300
|
+
...legacyEntries.filter((entry) => !entry.messageId || !knownMessageIds.has(entry.messageId)),
|
|
301
|
+
].sort((a, b) => (a.timestamp ?? 0) - (b.timestamp ?? 0));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function clearBncrOutboundReplay(args: {
|
|
305
|
+
cache: BncrOutboundReplayCache;
|
|
306
|
+
parsed: ParsedInbound;
|
|
307
|
+
accountId: string;
|
|
308
|
+
}) {
|
|
309
|
+
const cacheKey = buildBncrOutboundReplayKey(args.parsed, args.accountId);
|
|
310
|
+
if (!cacheKey) return;
|
|
311
|
+
args.cache.delete(cacheKey);
|
|
312
|
+
}
|
|
@@ -49,7 +49,7 @@ const MODE_HELP_TEXT = [
|
|
|
49
49
|
].join('\n');
|
|
50
50
|
|
|
51
51
|
export const HISTORY_HELP_TEXT = [
|
|
52
|
-
'📋 Bncr
|
|
52
|
+
'📋 Bncr Conversation History Configuration',
|
|
53
53
|
'',
|
|
54
54
|
'Commands:',
|
|
55
55
|
' • /bncr history-limit <number> [<SceneId>]',
|
|
@@ -88,6 +88,22 @@ function resolveCurrentGroupSceneKey(parsed: ParsedInbound): string | null {
|
|
|
88
88
|
return `${platform}:${groupId}`;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
function resolveCurrentSceneKey(parsed: ParsedInbound): string | null {
|
|
92
|
+
const platform = normalizeToken(parsed.platform);
|
|
93
|
+
if (!platform) return null;
|
|
94
|
+
if (parsed.peer.kind === 'group') {
|
|
95
|
+
const groupId = normalizeToken(parsed.groupId);
|
|
96
|
+
if (!groupId || groupId === '0') return null;
|
|
97
|
+
return `${platform}:${groupId}`;
|
|
98
|
+
}
|
|
99
|
+
if (parsed.peer.kind === 'direct') {
|
|
100
|
+
const userId = normalizeToken(parsed.userId);
|
|
101
|
+
if (!userId || userId === '0') return null;
|
|
102
|
+
return `${platform}:${userId}`;
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
91
107
|
export function parseSceneAdminCommand(command: NativeCommand): ParsedSceneAdminCommand {
|
|
92
108
|
const args = splitArgs(command.argsText);
|
|
93
109
|
switch (command.command) {
|
|
@@ -340,8 +356,7 @@ function formatSceneDetailsLine(scene: BncrSceneRecord): string {
|
|
|
340
356
|
const historyLimitVal = scene.historyLimit;
|
|
341
357
|
const historyForceVal = scene.historyForce;
|
|
342
358
|
const hasNonDefaultHistory =
|
|
343
|
-
|
|
344
|
-
((typeof historyLimitVal === 'number' && historyLimitVal !== 50) || historyForceVal === false);
|
|
359
|
+
(typeof historyLimitVal === 'number' && historyLimitVal !== 50) || historyForceVal === false;
|
|
345
360
|
const historyPart = hasNonDefaultHistory
|
|
346
361
|
? ` historyLimit=${historyLimitVal ?? 50} autoFlush=${historyForceVal !== false ? 'on' : 'off'}`
|
|
347
362
|
: '';
|
|
@@ -475,7 +490,7 @@ export function executeSceneAdminCommand(args: {
|
|
|
475
490
|
}
|
|
476
491
|
|
|
477
492
|
if (command.kind === 'history-limit-get') {
|
|
478
|
-
const qSceneKey = command.sceneKey ||
|
|
493
|
+
const qSceneKey = command.sceneKey || resolveCurrentSceneKey(parsed);
|
|
479
494
|
if (!qSceneKey) {
|
|
480
495
|
return { ok: true, text: 'Default history limit is 50.' };
|
|
481
496
|
}
|
|
@@ -485,9 +500,9 @@ export function executeSceneAdminCommand(args: {
|
|
|
485
500
|
}
|
|
486
501
|
|
|
487
502
|
if (command.kind === 'history-limit-set') {
|
|
488
|
-
const sSceneKey = command.sceneKey ||
|
|
503
|
+
const sSceneKey = command.sceneKey || resolveCurrentSceneKey(parsed);
|
|
489
504
|
if (!sSceneKey) {
|
|
490
|
-
return { ok: false, text: 'Current
|
|
505
|
+
return { ok: false, text: 'Current conversation shortcut requires a valid chat.' };
|
|
491
506
|
}
|
|
492
507
|
const sExisting = sceneRegistry.get(sSceneKey);
|
|
493
508
|
if (!sExisting) return { ok: false, text: `Scene not found: ${sSceneKey}` };
|
|
@@ -520,7 +535,7 @@ export function executeSceneAdminCommand(args: {
|
|
|
520
535
|
}
|
|
521
536
|
|
|
522
537
|
if (command.kind === 'history-force-get') {
|
|
523
|
-
const qSceneKey = command.sceneKey ||
|
|
538
|
+
const qSceneKey = command.sceneKey || resolveCurrentSceneKey(parsed);
|
|
524
539
|
if (!qSceneKey) {
|
|
525
540
|
return { ok: true, text: 'Default history auto flush is on.' };
|
|
526
541
|
}
|
|
@@ -534,9 +549,9 @@ export function executeSceneAdminCommand(args: {
|
|
|
534
549
|
}
|
|
535
550
|
|
|
536
551
|
if (command.kind === 'history-force-set') {
|
|
537
|
-
const sSceneKey = command.sceneKey ||
|
|
552
|
+
const sSceneKey = command.sceneKey || resolveCurrentSceneKey(parsed);
|
|
538
553
|
if (!sSceneKey) {
|
|
539
|
-
return { ok: false, text: 'Current
|
|
554
|
+
return { ok: false, text: 'Current conversation shortcut requires a valid chat.' };
|
|
540
555
|
}
|
|
541
556
|
const sExisting = sceneRegistry.get(sSceneKey);
|
|
542
557
|
if (!sExisting) return { ok: false, text: `Scene not found: ${sSceneKey}` };
|
|
@@ -9,19 +9,88 @@ import {
|
|
|
9
9
|
buildBncrStructuredContextFactsFromInboundParts,
|
|
10
10
|
} from './context-facts.ts';
|
|
11
11
|
import type { BncrInboundApi, BncrInboundConfig, BncrInboundContextPayload } from './contracts.ts';
|
|
12
|
+
import {
|
|
13
|
+
type BncrConversationHistoryMap,
|
|
14
|
+
type BncrHistoryEntry,
|
|
15
|
+
collectBncrHistoryMediaFromEntries,
|
|
16
|
+
} from './conversation-history.ts';
|
|
12
17
|
import type {
|
|
13
18
|
BncrInboundConversationResolution,
|
|
14
19
|
BncrPreparedInboundSessionContext,
|
|
15
20
|
ParsedInbound,
|
|
16
21
|
} from './dispatch-prep.ts';
|
|
17
|
-
import {
|
|
18
|
-
type BncrGroupHistoryMap,
|
|
19
|
-
type BncrHistoryEntry,
|
|
20
|
-
buildBncrGroupContextFromEntries,
|
|
21
|
-
collectBncrHistoryMediaFromEntries,
|
|
22
|
-
} from './group-history.ts';
|
|
23
22
|
import { resolveBncrChannelInboundRuntime } from './runtime-compat.ts';
|
|
24
23
|
|
|
24
|
+
function dedupeBncrTurnMedia(entries: readonly HistoryMediaEntry[]): HistoryMediaEntry[] {
|
|
25
|
+
const seen = new Set<string>();
|
|
26
|
+
const result: HistoryMediaEntry[] = [];
|
|
27
|
+
for (const item of entries) {
|
|
28
|
+
const key = `${item.path ?? ''}:${item.messageId ?? ''}`;
|
|
29
|
+
if (key && seen.has(key)) continue;
|
|
30
|
+
seen.add(key);
|
|
31
|
+
result.push(item);
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildBncrParticipantsMap(args: {
|
|
37
|
+
entries: readonly BncrHistoryEntry[];
|
|
38
|
+
parsed: ParsedInbound;
|
|
39
|
+
currentSenderId: string;
|
|
40
|
+
currentSenderName: string;
|
|
41
|
+
currentSenderUsername?: string;
|
|
42
|
+
currentSenderIsAdmin: boolean;
|
|
43
|
+
currentSenderIsOwner: boolean;
|
|
44
|
+
currentSenderIsAuthorized: boolean;
|
|
45
|
+
}): Record<
|
|
46
|
+
string,
|
|
47
|
+
{
|
|
48
|
+
name: string;
|
|
49
|
+
username?: string;
|
|
50
|
+
isBot?: boolean;
|
|
51
|
+
isAdmin?: boolean;
|
|
52
|
+
isOwner?: boolean;
|
|
53
|
+
isAuthorizedSender?: boolean;
|
|
54
|
+
role?: 'owner' | 'admin' | 'user';
|
|
55
|
+
displayName?: string;
|
|
56
|
+
}
|
|
57
|
+
> {
|
|
58
|
+
const map: Record<
|
|
59
|
+
string,
|
|
60
|
+
{
|
|
61
|
+
name: string;
|
|
62
|
+
username?: string;
|
|
63
|
+
isBot?: boolean;
|
|
64
|
+
isAdmin?: boolean;
|
|
65
|
+
isOwner?: boolean;
|
|
66
|
+
isAuthorizedSender?: boolean;
|
|
67
|
+
role?: 'owner' | 'admin' | 'user';
|
|
68
|
+
displayName?: string;
|
|
69
|
+
}
|
|
70
|
+
> = {};
|
|
71
|
+
map[args.currentSenderId] = {
|
|
72
|
+
name: args.currentSenderName,
|
|
73
|
+
...(args.currentSenderUsername ? { username: args.currentSenderUsername } : {}),
|
|
74
|
+
isBot: false,
|
|
75
|
+
...(args.currentSenderIsAdmin ? { isAdmin: true } : {}),
|
|
76
|
+
...(args.currentSenderIsOwner ? { isOwner: true } : {}),
|
|
77
|
+
...(args.currentSenderIsAuthorized ? { isAuthorizedSender: true } : {}),
|
|
78
|
+
role: args.currentSenderIsOwner ? 'owner' : args.currentSenderIsAdmin ? 'admin' : 'user',
|
|
79
|
+
displayName: args.currentSenderName,
|
|
80
|
+
};
|
|
81
|
+
for (const entry of args.entries) {
|
|
82
|
+
if (!entry.senderId || map[entry.senderId]) continue;
|
|
83
|
+
const isBot = entry.role === 'assistant';
|
|
84
|
+
map[entry.senderId] = {
|
|
85
|
+
name: entry.sender || entry.senderId,
|
|
86
|
+
...(isBot ? { isBot: true } : {}),
|
|
87
|
+
role: isBot ? 'admin' : 'user',
|
|
88
|
+
displayName: entry.sender || entry.senderId,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return map;
|
|
92
|
+
}
|
|
93
|
+
|
|
25
94
|
function parseSlashCommandName(body: string): string | undefined {
|
|
26
95
|
const raw = String(body || '').trim();
|
|
27
96
|
if (!raw.startsWith('/')) return undefined;
|
|
@@ -73,7 +142,7 @@ export function buildBncrInboundTurnContext(args: {
|
|
|
73
142
|
bridgeSenderName?: string;
|
|
74
143
|
resolution: BncrInboundConversationResolution;
|
|
75
144
|
prepared: BncrPreparedInboundSessionContext;
|
|
76
|
-
|
|
145
|
+
conversationHistories: BncrConversationHistoryMap;
|
|
77
146
|
shouldDispatch: boolean;
|
|
78
147
|
historyLimit?: number;
|
|
79
148
|
silentHistoryFlush?: boolean;
|
|
@@ -99,10 +168,11 @@ export function buildBncrInboundTurnContext(args: {
|
|
|
99
168
|
? args.pendingHistoryEntries
|
|
100
169
|
: [];
|
|
101
170
|
const silentHistoryFlush = args.silentHistoryFlush === true;
|
|
171
|
+
const contextEntries = pendingHistoryEntries;
|
|
102
172
|
const flushSenderId = 'bncr-history-system';
|
|
103
173
|
const flushSenderName = 'Bncr History System';
|
|
104
174
|
const flushRawBody =
|
|
105
|
-
'This message was automatically generated by the system for
|
|
175
|
+
'This message was automatically generated by the system for conversation context accumulation. Do not reply to the user. Output exactly NO_REPLY.';
|
|
106
176
|
const effectiveSenderId = silentHistoryFlush ? flushSenderId : senderIdForContext;
|
|
107
177
|
const effectiveSenderDisplayName = silentHistoryFlush ? flushSenderName : senderDisplayName;
|
|
108
178
|
const effectiveRawBody = silentHistoryFlush ? flushRawBody : prepared.rawBody;
|
|
@@ -123,23 +193,38 @@ export function buildBncrInboundTurnContext(args: {
|
|
|
123
193
|
const senderIsOwner = parsed.isAdmin === true || Boolean(ownerAllowFrom?.length);
|
|
124
194
|
const senderIsAuthorized = senderIsOwner;
|
|
125
195
|
const inboundHistory = undefined;
|
|
126
|
-
const
|
|
127
|
-
? collectBncrHistoryMediaFromEntries({ entries:
|
|
196
|
+
const contextMedia = shouldDispatch
|
|
197
|
+
? collectBncrHistoryMediaFromEntries({ entries: contextEntries })
|
|
128
198
|
: [];
|
|
129
|
-
const
|
|
130
|
-
?
|
|
199
|
+
const structuredContextEntries = shouldDispatch
|
|
200
|
+
? contextEntries.map((entry) => ({
|
|
131
201
|
messageId: entry.messageId,
|
|
202
|
+
timestamp: entry.timestamp,
|
|
203
|
+
role: entry.role || 'user',
|
|
132
204
|
sender: entry.sender,
|
|
133
205
|
senderId: entry.senderId,
|
|
134
|
-
|
|
206
|
+
content: entry.body,
|
|
135
207
|
media: (entry.media || []).map((item: HistoryMediaEntry) => ({
|
|
208
|
+
type: item.kind || 'document',
|
|
136
209
|
path: item.path,
|
|
137
210
|
contentType: item.contentType,
|
|
138
|
-
kind: item.kind,
|
|
139
211
|
messageId: item.messageId,
|
|
140
212
|
})),
|
|
141
213
|
}))
|
|
142
214
|
: [];
|
|
215
|
+
const isGroupChat = parsed.isGroup === true;
|
|
216
|
+
const participants = isGroupChat
|
|
217
|
+
? buildBncrParticipantsMap({
|
|
218
|
+
entries: contextEntries,
|
|
219
|
+
parsed,
|
|
220
|
+
currentSenderId: effectiveSenderId,
|
|
221
|
+
currentSenderName: effectiveSenderDisplayName,
|
|
222
|
+
currentSenderUsername: parsed.userName,
|
|
223
|
+
currentSenderIsAdmin: parsed.isAdmin === true,
|
|
224
|
+
currentSenderIsOwner: senderIsOwner,
|
|
225
|
+
currentSenderIsAuthorized: senderIsAuthorized,
|
|
226
|
+
})
|
|
227
|
+
: null;
|
|
143
228
|
const structuredContextFacts = buildBncrStructuredContextFactsFromInboundParts({
|
|
144
229
|
channelId,
|
|
145
230
|
parsed,
|
|
@@ -155,7 +240,10 @@ export function buildBncrInboundTurnContext(args: {
|
|
|
155
240
|
bridgeSenderName,
|
|
156
241
|
senderIsOwner,
|
|
157
242
|
senderIsAuthorized,
|
|
158
|
-
|
|
243
|
+
conversationHistoryContext: structuredContextEntries,
|
|
244
|
+
participants,
|
|
245
|
+
isGroupChat,
|
|
246
|
+
groupSubject: parsed.groupName,
|
|
159
247
|
});
|
|
160
248
|
const promptVisibleContextFacts = buildBncrPromptVisibleContextFacts(structuredContextFacts);
|
|
161
249
|
const supplementalUntrustedContext = [
|
|
@@ -176,30 +264,14 @@ export function buildBncrInboundTurnContext(args: {
|
|
|
176
264
|
const platformWantsReply = parsed.shouldRespond === true;
|
|
177
265
|
const wasMentioned = parsed.isBotMentioned === true || platformWantsReply;
|
|
178
266
|
const isAuthorizedTextCommand = Boolean(ownerAllowFrom?.length);
|
|
179
|
-
const bodyForAgent =
|
|
180
|
-
shouldDispatch && pendingHistoryEntries.length > 0
|
|
181
|
-
? buildBncrGroupContextFromEntries({
|
|
182
|
-
api,
|
|
183
|
-
entries: pendingHistoryEntries,
|
|
184
|
-
channelLabel: resolution.canonicalTo,
|
|
185
|
-
currentTimestamp: Date.now(),
|
|
186
|
-
previousTimestamp: readBncrSessionUpdatedAt(api, {
|
|
187
|
-
storePath: prepared.storePath,
|
|
188
|
-
sessionKey: resolution.dispatchSessionKey,
|
|
189
|
-
}),
|
|
190
|
-
envelope: envelopeOptions,
|
|
191
|
-
currentMessage: typeof effectiveBody === 'string' ? effectiveBody : prepared.body,
|
|
192
|
-
})
|
|
193
|
-
: typeof effectiveBody === 'string'
|
|
194
|
-
? effectiveBody
|
|
195
|
-
: prepared.body;
|
|
267
|
+
const bodyForAgent = typeof effectiveBody === 'string' ? effectiveBody : prepared.body;
|
|
196
268
|
const currentTurnMedia = (silentHistoryFlush ? [] : prepared.mediaItems).map((item) => ({
|
|
197
269
|
path: item.path,
|
|
198
270
|
contentType: item.contentType,
|
|
199
271
|
kind: item.kind,
|
|
200
272
|
messageId: msgId ?? undefined,
|
|
201
273
|
}));
|
|
202
|
-
const media = [...
|
|
274
|
+
const media = dedupeBncrTurnMedia([...contextMedia, ...currentTurnMedia]);
|
|
203
275
|
|
|
204
276
|
return Promise.resolve(
|
|
205
277
|
resolveBncrChannelInboundRuntime(api).buildContext({
|
|
@@ -105,6 +105,7 @@ export function createBncrAckOutboxRuntimeGroup(runtime: {
|
|
|
105
105
|
setOutboxEntry: (messageId: string, entry: OutboxEntry) => void;
|
|
106
106
|
resolveMessageAck: (messageId: string, result: 'acked' | 'timeout') => boolean;
|
|
107
107
|
moveToDeadLetter: (entry: OutboxEntry, reason: string) => void;
|
|
108
|
+
markRecentOutboundAcked?: (entry: OutboxEntry) => void;
|
|
108
109
|
recordAckTimeoutTelemetry: (accountId: string) => void;
|
|
109
110
|
degradeOutboundCapability: (args: {
|
|
110
111
|
accountId: string;
|
|
@@ -154,6 +155,7 @@ export function createBncrAckOutboxRuntimeGroup(runtime: {
|
|
|
154
155
|
runtime.resolveMessageAck(messageId, result);
|
|
155
156
|
},
|
|
156
157
|
moveToDeadLetter: runtime.moveToDeadLetter,
|
|
158
|
+
markRecentOutboundAcked: runtime.markRecentOutboundAcked,
|
|
157
159
|
logOutboxAckSummary: outboxAckLogs.logOutboxAckSummary,
|
|
158
160
|
});
|
|
159
161
|
|