@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
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
1
3
|
import type { RegisterDriftSnapshot } from '../core/register-trace.ts';
|
|
2
4
|
import type {
|
|
3
5
|
BncrConnection,
|
|
@@ -7,7 +9,7 @@ import type {
|
|
|
7
9
|
OutboxEntry,
|
|
8
10
|
} from '../core/types.ts';
|
|
9
11
|
import type {
|
|
10
|
-
|
|
12
|
+
BncrPersistedConversationHistoryEntry,
|
|
11
13
|
BncrPersistedOutboundReplayEntry,
|
|
12
14
|
BncrSceneRecord,
|
|
13
15
|
} from './channel-runtime-types.ts';
|
|
@@ -17,6 +19,13 @@ import { createBncrTransientStateRuntime } from './transient-state-runtime.ts';
|
|
|
17
19
|
type StoredRouteRecord = { accountId: string; route: BncrRoute; updatedAt: number };
|
|
18
20
|
type StoredLastSessionRecord = { sessionKey: string; scope: string; updatedAt: number };
|
|
19
21
|
|
|
22
|
+
export function shouldEnableBncrSqliteState(statePath: string | null): boolean {
|
|
23
|
+
if (!statePath) return false;
|
|
24
|
+
if (process.env.BNCR_SQLITE_STORE === '0') return false;
|
|
25
|
+
if (process.env.BNCR_SQLITE_STORE === '1') return true;
|
|
26
|
+
return existsSync(path.join(path.dirname(statePath), 'bncr.sqlite3'));
|
|
27
|
+
}
|
|
28
|
+
|
|
20
29
|
export function createBncrStateTransientRuntimeGroup(runtime: {
|
|
21
30
|
bridgeId: string;
|
|
22
31
|
getStatePath: () => string | null;
|
|
@@ -37,7 +46,7 @@ export function createBncrStateTransientRuntimeGroup(runtime: {
|
|
|
37
46
|
maxSessionRouteEntries: number;
|
|
38
47
|
maxAccountActivityEntries: number;
|
|
39
48
|
sceneRegistry: Map<string, BncrSceneRecord>;
|
|
40
|
-
|
|
49
|
+
conversationHistories: Map<string, BncrPersistedConversationHistoryEntry[]>;
|
|
41
50
|
outboundReplayCache?: Map<string, BncrPersistedOutboundReplayEntry[]>;
|
|
42
51
|
outbox: Map<string, OutboxEntry>;
|
|
43
52
|
getDeadLetter: () => OutboxEntry[];
|
|
@@ -78,7 +87,7 @@ export function createBncrStateTransientRuntimeGroup(runtime: {
|
|
|
78
87
|
maxSessionRouteEntries: runtime.maxSessionRouteEntries,
|
|
79
88
|
maxAccountActivityEntries: runtime.maxAccountActivityEntries,
|
|
80
89
|
sceneRegistry: runtime.sceneRegistry,
|
|
81
|
-
|
|
90
|
+
conversationHistories: runtime.conversationHistories,
|
|
82
91
|
outboundReplayCache:
|
|
83
92
|
runtime.outboundReplayCache ?? new Map<string, BncrPersistedOutboundReplayEntry[]>(),
|
|
84
93
|
outbox: runtime.outbox,
|
|
@@ -92,6 +101,11 @@ export function createBncrStateTransientRuntimeGroup(runtime: {
|
|
|
92
101
|
lastOutboundByAccount: runtime.lastOutboundByAccount,
|
|
93
102
|
getLastDriftSnapshot: runtime.getLastDriftSnapshot,
|
|
94
103
|
setLastDriftSnapshot: runtime.setLastDriftSnapshot,
|
|
104
|
+
createSqliteState: async (statePath) => {
|
|
105
|
+
if (!statePath || !shouldEnableBncrSqliteState(statePath)) return null;
|
|
106
|
+
const { createBncrSqliteStateDatabase } = await import('./sqlite-state.ts');
|
|
107
|
+
return createBncrSqliteStateDatabase(path.join(path.dirname(statePath), 'bncr.sqlite3'));
|
|
108
|
+
},
|
|
95
109
|
});
|
|
96
110
|
|
|
97
111
|
const transientStateRuntime = createBncrTransientStateRuntime({
|
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createChannelHistoryWindow,
|
|
3
|
-
DEFAULT_GROUP_HISTORY_LIMIT,
|
|
4
|
-
type HistoryEntry,
|
|
5
|
-
type HistoryMediaEntry,
|
|
6
|
-
} from 'openclaw/plugin-sdk/reply-history';
|
|
7
|
-
import { formatOpenClawAgentEnvelope } from '../../openclaw/reply-runtime.ts';
|
|
8
|
-
import type { BncrInboundApi } from './contracts.ts';
|
|
9
|
-
import type { ParsedInbound } from './dispatch-prep.ts';
|
|
10
|
-
|
|
11
|
-
export type BncrHistoryEntry = HistoryEntry & {
|
|
12
|
-
senderId?: string;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export type BncrGroupHistoryMap = Map<string, BncrHistoryEntry[]>;
|
|
16
|
-
|
|
17
|
-
type BncrHistoryMediaKind = NonNullable<HistoryMediaEntry['kind']>;
|
|
18
|
-
|
|
19
|
-
function normalizeTextBody(value: string): string {
|
|
20
|
-
return String(value || '')
|
|
21
|
-
.replace(/\s+/g, ' ')
|
|
22
|
-
.trim();
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function buildBncrGroupHistoryKey(parsed: ParsedInbound): string | null {
|
|
26
|
-
if (parsed.peer.kind !== 'group') return null;
|
|
27
|
-
const platform = String(parsed.platform || '').trim();
|
|
28
|
-
const groupId = String(parsed.groupId || '').trim();
|
|
29
|
-
if (!platform || !groupId || groupId === '0') return null;
|
|
30
|
-
return `${platform}:${groupId}`;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function recordBncrPendingGroupText(args: {
|
|
34
|
-
historyMap: BncrGroupHistoryMap;
|
|
35
|
-
parsed: ParsedInbound;
|
|
36
|
-
senderDisplayName: string;
|
|
37
|
-
senderId: string;
|
|
38
|
-
bodyText: string;
|
|
39
|
-
historyLimit?: number;
|
|
40
|
-
}) {
|
|
41
|
-
const limit =
|
|
42
|
-
typeof args.historyLimit === 'number' &&
|
|
43
|
-
Number.isFinite(args.historyLimit) &&
|
|
44
|
-
args.historyLimit >= 0
|
|
45
|
-
? Math.floor(args.historyLimit)
|
|
46
|
-
: DEFAULT_GROUP_HISTORY_LIMIT;
|
|
47
|
-
const historyKey = buildBncrGroupHistoryKey(args.parsed);
|
|
48
|
-
const body = normalizeTextBody(args.bodyText);
|
|
49
|
-
if (!historyKey || !body || args.parsed.msgType !== 'text') return;
|
|
50
|
-
const msgId = String(args.parsed.msgId || '').trim();
|
|
51
|
-
if (msgId) {
|
|
52
|
-
const existing = args.historyMap.get(historyKey) || [];
|
|
53
|
-
if (existing.some((e) => e.messageId === msgId)) return;
|
|
54
|
-
}
|
|
55
|
-
createChannelHistoryWindow({ historyMap: args.historyMap }).record({
|
|
56
|
-
historyKey,
|
|
57
|
-
limit,
|
|
58
|
-
entry: {
|
|
59
|
-
sender: args.senderDisplayName,
|
|
60
|
-
senderId: args.senderId,
|
|
61
|
-
body,
|
|
62
|
-
timestamp: Date.now(),
|
|
63
|
-
messageId: args.parsed.msgId,
|
|
64
|
-
},
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function inferBncrHistoryMediaKind(args: {
|
|
69
|
-
msgType?: string;
|
|
70
|
-
mediaContentType?: string;
|
|
71
|
-
}): BncrHistoryMediaKind {
|
|
72
|
-
const msgType = String(args.msgType || '')
|
|
73
|
-
.trim()
|
|
74
|
-
.toLowerCase();
|
|
75
|
-
const contentType = String(args.mediaContentType || '')
|
|
76
|
-
.trim()
|
|
77
|
-
.toLowerCase();
|
|
78
|
-
if (msgType === 'image' || contentType.startsWith('image/')) return 'image';
|
|
79
|
-
if (msgType === 'video' || contentType.startsWith('video/')) return 'video';
|
|
80
|
-
if (msgType === 'audio' || msgType === 'voice' || contentType.startsWith('audio/'))
|
|
81
|
-
return 'audio';
|
|
82
|
-
if (msgType === 'file' || msgType === 'document') return 'document';
|
|
83
|
-
if (contentType) return 'document';
|
|
84
|
-
return 'unknown';
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function buildBncrHistoryMediaBody(kind: BncrHistoryMediaKind): string {
|
|
88
|
-
return `<media:${kind}>`;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export async function recordBncrPendingGroupMedia(args: {
|
|
92
|
-
historyMap: BncrGroupHistoryMap;
|
|
93
|
-
parsed: ParsedInbound;
|
|
94
|
-
senderDisplayName: string;
|
|
95
|
-
senderId: string;
|
|
96
|
-
bodyText: string;
|
|
97
|
-
mediaItems?: Array<{
|
|
98
|
-
path: string;
|
|
99
|
-
contentType?: string;
|
|
100
|
-
kind?: BncrHistoryMediaKind;
|
|
101
|
-
}>;
|
|
102
|
-
mediaContentType?: string;
|
|
103
|
-
historyLimit?: number;
|
|
104
|
-
}) {
|
|
105
|
-
const limit =
|
|
106
|
-
typeof args.historyLimit === 'number' &&
|
|
107
|
-
Number.isFinite(args.historyLimit) &&
|
|
108
|
-
args.historyLimit >= 0
|
|
109
|
-
? Math.floor(args.historyLimit)
|
|
110
|
-
: DEFAULT_GROUP_HISTORY_LIMIT;
|
|
111
|
-
const historyKey = buildBncrGroupHistoryKey(args.parsed);
|
|
112
|
-
if (!historyKey || args.parsed.msgType === 'text') return;
|
|
113
|
-
const normalizedMediaItems = Array.isArray(args.mediaItems) ? args.mediaItems : [];
|
|
114
|
-
const itemKinds = normalizedMediaItems
|
|
115
|
-
.map((item) =>
|
|
116
|
-
String(item?.kind || '')
|
|
117
|
-
.trim()
|
|
118
|
-
.toLowerCase(),
|
|
119
|
-
)
|
|
120
|
-
.filter(Boolean);
|
|
121
|
-
const kind = itemKinds[0]
|
|
122
|
-
? itemKinds.every((candidate) => candidate === itemKinds[0])
|
|
123
|
-
? (itemKinds[0] as BncrHistoryMediaKind)
|
|
124
|
-
: 'document'
|
|
125
|
-
: inferBncrHistoryMediaKind({
|
|
126
|
-
msgType: args.parsed.msgType,
|
|
127
|
-
mediaContentType: args.mediaContentType,
|
|
128
|
-
});
|
|
129
|
-
const body = normalizeTextBody(args.bodyText) || buildBncrHistoryMediaBody(kind);
|
|
130
|
-
if (normalizedMediaItems.length === 0) {
|
|
131
|
-
const msgId = String(args.parsed.msgId || '').trim();
|
|
132
|
-
if (msgId) {
|
|
133
|
-
const existing = args.historyMap.get(historyKey) || [];
|
|
134
|
-
if (existing.some((e) => e.messageId === msgId)) return;
|
|
135
|
-
}
|
|
136
|
-
createChannelHistoryWindow({ historyMap: args.historyMap }).record({
|
|
137
|
-
historyKey,
|
|
138
|
-
limit,
|
|
139
|
-
entry: {
|
|
140
|
-
sender: args.senderDisplayName,
|
|
141
|
-
senderId: args.senderId,
|
|
142
|
-
body,
|
|
143
|
-
timestamp: Date.now(),
|
|
144
|
-
messageId: args.parsed.msgId,
|
|
145
|
-
},
|
|
146
|
-
});
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
const msgId = String(args.parsed.msgId || '').trim();
|
|
150
|
-
if (msgId) {
|
|
151
|
-
const existing = args.historyMap.get(historyKey) || [];
|
|
152
|
-
if (existing.some((e) => e.messageId === msgId)) return;
|
|
153
|
-
}
|
|
154
|
-
createChannelHistoryWindow({ historyMap: args.historyMap }).record({
|
|
155
|
-
historyKey,
|
|
156
|
-
limit,
|
|
157
|
-
entry: {
|
|
158
|
-
sender: args.senderDisplayName,
|
|
159
|
-
senderId: args.senderId,
|
|
160
|
-
body,
|
|
161
|
-
timestamp: Date.now(),
|
|
162
|
-
messageId: args.parsed.msgId,
|
|
163
|
-
media: normalizedMediaItems.map(
|
|
164
|
-
(item) =>
|
|
165
|
-
({
|
|
166
|
-
path: item.path,
|
|
167
|
-
contentType: item.contentType || args.mediaContentType,
|
|
168
|
-
kind: item.kind || kind,
|
|
169
|
-
messageId: args.parsed.msgId,
|
|
170
|
-
}) satisfies HistoryMediaEntry,
|
|
171
|
-
),
|
|
172
|
-
},
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function cloneBncrHistoryEntry(entry: BncrHistoryEntry): BncrHistoryEntry {
|
|
177
|
-
return {
|
|
178
|
-
sender: entry.sender,
|
|
179
|
-
...(entry.messageId ? { messageId: entry.messageId } : {}),
|
|
180
|
-
...(typeof entry.timestamp === 'number' ? { timestamp: entry.timestamp } : {}),
|
|
181
|
-
...(entry.senderId ? { senderId: entry.senderId } : {}),
|
|
182
|
-
body: entry.body,
|
|
183
|
-
...(Array.isArray(entry.media)
|
|
184
|
-
? {
|
|
185
|
-
media: entry.media.map((item) => ({
|
|
186
|
-
...(item?.path ? { path: item.path } : {}),
|
|
187
|
-
...(item?.contentType ? { contentType: item.contentType } : {}),
|
|
188
|
-
...(item?.kind ? { kind: item.kind } : {}),
|
|
189
|
-
...(item?.messageId ? { messageId: item.messageId } : {}),
|
|
190
|
-
})),
|
|
191
|
-
}
|
|
192
|
-
: {}),
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
export function readBncrPendingGroupHistorySnapshot(args: {
|
|
197
|
-
historyMap: BncrGroupHistoryMap;
|
|
198
|
-
parsed: ParsedInbound;
|
|
199
|
-
historyLimit?: number;
|
|
200
|
-
}): BncrHistoryEntry[] {
|
|
201
|
-
const limit =
|
|
202
|
-
typeof args.historyLimit === 'number' &&
|
|
203
|
-
Number.isFinite(args.historyLimit) &&
|
|
204
|
-
args.historyLimit >= 0
|
|
205
|
-
? Math.floor(args.historyLimit)
|
|
206
|
-
: DEFAULT_GROUP_HISTORY_LIMIT;
|
|
207
|
-
const historyKey = buildBncrGroupHistoryKey(args.parsed);
|
|
208
|
-
if (!historyKey) return [];
|
|
209
|
-
const entries = args.historyMap.get(historyKey) || [];
|
|
210
|
-
if (entries.length === 0) return [];
|
|
211
|
-
return entries.slice(-limit).map(cloneBncrHistoryEntry);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
export function buildBncrGroupContextFromEntries(args: {
|
|
215
|
-
api: BncrInboundApi;
|
|
216
|
-
entries: readonly BncrHistoryEntry[];
|
|
217
|
-
channelLabel: string;
|
|
218
|
-
currentTimestamp: number;
|
|
219
|
-
previousTimestamp?: unknown;
|
|
220
|
-
envelope?: unknown;
|
|
221
|
-
currentMessage: string;
|
|
222
|
-
}) {
|
|
223
|
-
if (args.entries.length === 0) return args.currentMessage;
|
|
224
|
-
const historyText = args.entries
|
|
225
|
-
.map((entry) =>
|
|
226
|
-
formatOpenClawAgentEnvelope(args.api, {
|
|
227
|
-
channel: 'Bncr',
|
|
228
|
-
from:
|
|
229
|
-
(entry.senderId
|
|
230
|
-
? `${entry.sender} (${entry.senderId})`
|
|
231
|
-
: entry.sender || args.channelLabel) + (entry.messageId ? ` #${entry.messageId}` : ''),
|
|
232
|
-
timestamp: entry.timestamp || args.currentTimestamp,
|
|
233
|
-
previousTimestamp: args.previousTimestamp,
|
|
234
|
-
envelope: args.envelope,
|
|
235
|
-
body: entry.body,
|
|
236
|
-
}),
|
|
237
|
-
)
|
|
238
|
-
.join('\n');
|
|
239
|
-
return `[Chat messages since your last reply - for context]\n${historyText}\n\n[Current message - respond to this]${args.currentMessage}`;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
export function collectBncrHistoryMediaFromEntries(args: {
|
|
243
|
-
entries: readonly BncrHistoryEntry[];
|
|
244
|
-
}): HistoryMediaEntry[] {
|
|
245
|
-
return args.entries.flatMap((entry) => (Array.isArray(entry.media) ? entry.media : []));
|
|
246
|
-
}
|
|
247
|
-
export function clearBncrPendingGroupHistory(args: {
|
|
248
|
-
historyMap: BncrGroupHistoryMap;
|
|
249
|
-
parsed: ParsedInbound;
|
|
250
|
-
historyLimit?: number;
|
|
251
|
-
}) {
|
|
252
|
-
const limit =
|
|
253
|
-
typeof args.historyLimit === 'number' &&
|
|
254
|
-
Number.isFinite(args.historyLimit) &&
|
|
255
|
-
args.historyLimit >= 0
|
|
256
|
-
? Math.floor(args.historyLimit)
|
|
257
|
-
: DEFAULT_GROUP_HISTORY_LIMIT;
|
|
258
|
-
const historyKey = buildBncrGroupHistoryKey(args.parsed);
|
|
259
|
-
if (!historyKey) return;
|
|
260
|
-
createChannelHistoryWindow({ historyMap: args.historyMap }).clear({
|
|
261
|
-
historyKey,
|
|
262
|
-
limit: limit,
|
|
263
|
-
});
|
|
264
|
-
}
|