@xmoxmo/bncr 0.5.9 → 0.6.1
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 +3 -0
- package/src/bootstrap/register-runtime-gateway.ts +1 -0
- package/src/bootstrap/register-runtime.ts +1 -0
- package/src/channel.ts +50 -1
- package/src/core/types.ts +41 -0
- package/src/messaging/inbound/dispatch-prep.ts +5 -1
- package/src/messaging/inbound/dispatch.ts +26 -8
- package/src/messaging/inbound/outbound-replay-cache.ts +236 -0
- package/src/messaging/inbound/turn-context.ts +16 -8
- 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 +79 -1
- package/src/plugin/channel-components.ts +5 -0
- 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 +2 -0
- package/src/plugin/channel-runtime-types.ts +26 -2
- 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-handlers.ts +4 -0
- package/src/plugin/inbound-surface-handlers-group.ts +3 -0
- package/src/plugin/outbox-ack-outcome.ts +2 -0
- package/src/plugin/state-store.ts +125 -5
- package/src/plugin/state-transient-runtime-group.ts +8 -1
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BncrOutboundReplayEntry,
|
|
3
|
+
BncrOutboundReplayMediaEntry,
|
|
4
|
+
BncrOutboundReplayStatus,
|
|
5
|
+
OutboxEntry,
|
|
6
|
+
} from '../../core/types.ts';
|
|
7
|
+
import type { ParsedInbound } from './dispatch-prep.ts';
|
|
8
|
+
|
|
9
|
+
export type {
|
|
10
|
+
BncrOutboundReplayEntry,
|
|
11
|
+
BncrOutboundReplayMediaEntry,
|
|
12
|
+
BncrOutboundReplayStatus,
|
|
13
|
+
} from '../../core/types.ts';
|
|
14
|
+
|
|
15
|
+
export type BncrOutboundReplayCache = Map<string, BncrOutboundReplayEntry[]>;
|
|
16
|
+
|
|
17
|
+
function normalizeTextBody(value: string): string {
|
|
18
|
+
return String(value || '')
|
|
19
|
+
.replace(/\s+/g, ' ')
|
|
20
|
+
.trim();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function cloneMediaEntry(entry: BncrOutboundReplayMediaEntry): BncrOutboundReplayMediaEntry {
|
|
24
|
+
return {
|
|
25
|
+
...(entry?.path ? { path: entry.path } : {}),
|
|
26
|
+
...(entry?.contentType ? { contentType: entry.contentType } : {}),
|
|
27
|
+
...(entry?.kind ? { kind: entry.kind } : {}),
|
|
28
|
+
...(entry?.messageId ? { messageId: entry.messageId } : {}),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function cloneBncrOutboundReplayEntry(
|
|
33
|
+
entry: BncrOutboundReplayEntry,
|
|
34
|
+
): BncrOutboundReplayEntry {
|
|
35
|
+
return {
|
|
36
|
+
sender: entry.sender,
|
|
37
|
+
...(entry.senderId ? { senderId: entry.senderId } : {}),
|
|
38
|
+
body: entry.body,
|
|
39
|
+
...(typeof entry.timestamp === 'number' ? { timestamp: entry.timestamp } : {}),
|
|
40
|
+
...(entry.messageId ? { messageId: entry.messageId } : {}),
|
|
41
|
+
...(Array.isArray(entry.media)
|
|
42
|
+
? { media: entry.media.map((item) => cloneMediaEntry(item)) }
|
|
43
|
+
: {}),
|
|
44
|
+
...(entry.accountId ? { accountId: entry.accountId } : {}),
|
|
45
|
+
...(entry.sessionKey ? { sessionKey: entry.sessionKey } : {}),
|
|
46
|
+
...(entry.route ? { route: { ...entry.route } } : {}),
|
|
47
|
+
...(entry.type ? { type: entry.type } : {}),
|
|
48
|
+
...(entry.mediaUrl ? { mediaUrl: entry.mediaUrl } : {}),
|
|
49
|
+
...(typeof entry.createdAt === 'number' ? { createdAt: entry.createdAt } : {}),
|
|
50
|
+
...(entry.status ? { status: entry.status } : {}),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function buildBncrOutboundReplayKeyFromRoute(args: {
|
|
55
|
+
accountId?: string;
|
|
56
|
+
platform?: string;
|
|
57
|
+
groupId?: string;
|
|
58
|
+
userId?: string;
|
|
59
|
+
}): string | null {
|
|
60
|
+
const accountId = String(args.accountId || '').trim() || 'default';
|
|
61
|
+
const platform = String(args.platform || '').trim();
|
|
62
|
+
const groupId = String(args.groupId || '0').trim() || '0';
|
|
63
|
+
const userId = String(args.userId || '0').trim() || '0';
|
|
64
|
+
if (!platform) return null;
|
|
65
|
+
if (groupId !== '0') return `${accountId}:${platform}:${groupId}`;
|
|
66
|
+
if (userId !== '0') return `${accountId}:${platform}:${userId}`;
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function buildBncrOutboundReplayKey(
|
|
71
|
+
parsed: ParsedInbound,
|
|
72
|
+
accountId: string,
|
|
73
|
+
): string | null {
|
|
74
|
+
return buildBncrOutboundReplayKeyFromRoute({
|
|
75
|
+
accountId,
|
|
76
|
+
platform: parsed.platform,
|
|
77
|
+
groupId: parsed.groupId,
|
|
78
|
+
userId: parsed.userId,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function recordOutboundReplayEntry(args: {
|
|
83
|
+
cache: BncrOutboundReplayCache;
|
|
84
|
+
cacheKey: string;
|
|
85
|
+
entry: BncrOutboundReplayEntry;
|
|
86
|
+
}) {
|
|
87
|
+
const msgId = String(args.entry.messageId || '').trim();
|
|
88
|
+
const existing = args.cache.get(args.cacheKey) || [];
|
|
89
|
+
if (msgId && existing.some((entry) => entry.messageId === msgId)) return;
|
|
90
|
+
|
|
91
|
+
const next = [...existing, cloneBncrOutboundReplayEntry(args.entry)].sort(
|
|
92
|
+
(a, b) => (a.timestamp ?? 0) - (b.timestamp ?? 0),
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
if (args.cache.has(args.cacheKey)) {
|
|
96
|
+
args.cache.delete(args.cacheKey);
|
|
97
|
+
}
|
|
98
|
+
args.cache.set(args.cacheKey, next);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function inferMediaKind(args: {
|
|
102
|
+
msgType?: string;
|
|
103
|
+
contentType?: string;
|
|
104
|
+
mediaUrl?: string;
|
|
105
|
+
type?: string;
|
|
106
|
+
}): NonNullable<BncrOutboundReplayMediaEntry['kind']> {
|
|
107
|
+
const msgType = String(args.msgType || '')
|
|
108
|
+
.trim()
|
|
109
|
+
.toLowerCase();
|
|
110
|
+
const type = String(args.type || '')
|
|
111
|
+
.trim()
|
|
112
|
+
.toLowerCase();
|
|
113
|
+
const contentType = String(args.contentType || '')
|
|
114
|
+
.trim()
|
|
115
|
+
.toLowerCase();
|
|
116
|
+
const mediaUrl = String(args.mediaUrl || '')
|
|
117
|
+
.trim()
|
|
118
|
+
.toLowerCase();
|
|
119
|
+
|
|
120
|
+
if (msgType === 'image' || type === 'image' || contentType.startsWith('image/')) return 'image';
|
|
121
|
+
if (msgType === 'video' || type === 'video' || contentType.startsWith('video/')) return 'video';
|
|
122
|
+
if (
|
|
123
|
+
msgType === 'audio' ||
|
|
124
|
+
msgType === 'voice' ||
|
|
125
|
+
type === 'audio' ||
|
|
126
|
+
type === 'voice' ||
|
|
127
|
+
contentType.startsWith('audio/')
|
|
128
|
+
) {
|
|
129
|
+
return 'audio';
|
|
130
|
+
}
|
|
131
|
+
if (type === 'file' || type === 'document' || msgType === 'file' || msgType === 'document') {
|
|
132
|
+
return 'document';
|
|
133
|
+
}
|
|
134
|
+
if (/\.(png|jpe?g|gif|webp|bmp|svg)$/.test(mediaUrl)) return 'image';
|
|
135
|
+
if (/\.(mp4|mov|webm|mkv|avi)$/.test(mediaUrl)) return 'video';
|
|
136
|
+
if (/\.(mp3|ogg|oga|silk|amr|wav|m4a|aac|flac)$/.test(mediaUrl)) return 'audio';
|
|
137
|
+
if (contentType) return 'document';
|
|
138
|
+
return 'unknown';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function buildMediaBody(kind: NonNullable<BncrOutboundReplayMediaEntry['kind']>): string {
|
|
142
|
+
return `<media:${kind}>`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function isAppMsg(type: string | undefined, msg: string): boolean {
|
|
146
|
+
return (
|
|
147
|
+
String(type || '')
|
|
148
|
+
.trim()
|
|
149
|
+
.toLowerCase() === 'appmsg' || /^<appmsg/i.test(msg)
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function recordBncrOutboundReplay(args: {
|
|
154
|
+
cache: BncrOutboundReplayCache;
|
|
155
|
+
entry: OutboxEntry;
|
|
156
|
+
sender: string;
|
|
157
|
+
senderId?: string;
|
|
158
|
+
status?: BncrOutboundReplayStatus;
|
|
159
|
+
}) {
|
|
160
|
+
const cacheKey = buildBncrOutboundReplayKeyFromRoute({
|
|
161
|
+
accountId: args.entry.accountId,
|
|
162
|
+
platform: args.entry.route?.platform,
|
|
163
|
+
groupId: args.entry.route?.groupId,
|
|
164
|
+
userId: args.entry.route?.userId,
|
|
165
|
+
});
|
|
166
|
+
if (!cacheKey) return;
|
|
167
|
+
|
|
168
|
+
const message =
|
|
169
|
+
args.entry.payload?.message && typeof args.entry.payload.message === 'object'
|
|
170
|
+
? (args.entry.payload.message as Record<string, unknown>)
|
|
171
|
+
: {};
|
|
172
|
+
const type = typeof message.type === 'string' ? message.type : undefined;
|
|
173
|
+
const rawBody = typeof message.msg === 'string' ? message.msg : '';
|
|
174
|
+
const mediaUrl = typeof message.mediaUrl === 'string' ? message.mediaUrl.trim() : '';
|
|
175
|
+
const kind = inferMediaKind({ type, mediaUrl });
|
|
176
|
+
const isMedia =
|
|
177
|
+
message.transferMode === 'media' || Boolean(mediaUrl) || (!!type && type !== 'text');
|
|
178
|
+
const body = isAppMsg(type, rawBody)
|
|
179
|
+
? buildMediaBody('document')
|
|
180
|
+
: normalizeTextBody(rawBody) || (isMedia ? buildMediaBody(kind) : '');
|
|
181
|
+
if (!body) return;
|
|
182
|
+
|
|
183
|
+
recordOutboundReplayEntry({
|
|
184
|
+
cache: args.cache,
|
|
185
|
+
cacheKey,
|
|
186
|
+
entry: {
|
|
187
|
+
sender: args.sender,
|
|
188
|
+
...(args.senderId ? { senderId: args.senderId } : {}),
|
|
189
|
+
body,
|
|
190
|
+
timestamp: Date.now(),
|
|
191
|
+
messageId: args.entry.messageId,
|
|
192
|
+
accountId: args.entry.accountId,
|
|
193
|
+
sessionKey: args.entry.sessionKey,
|
|
194
|
+
route: { ...args.entry.route },
|
|
195
|
+
...(type ? { type } : {}),
|
|
196
|
+
...(mediaUrl ? { mediaUrl } : {}),
|
|
197
|
+
createdAt: args.entry.createdAt,
|
|
198
|
+
...(args.status ? { status: args.status } : {}),
|
|
199
|
+
...(mediaUrl
|
|
200
|
+
? {
|
|
201
|
+
media: [
|
|
202
|
+
{
|
|
203
|
+
path: mediaUrl,
|
|
204
|
+
kind,
|
|
205
|
+
messageId: args.entry.messageId,
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
}
|
|
209
|
+
: {}),
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function readBncrOutboundReplaySnapshot(args: {
|
|
215
|
+
cache: BncrOutboundReplayCache;
|
|
216
|
+
parsed: ParsedInbound;
|
|
217
|
+
accountId: string;
|
|
218
|
+
excludeMessageId?: string | null;
|
|
219
|
+
}): BncrOutboundReplayEntry[] {
|
|
220
|
+
const cacheKey = buildBncrOutboundReplayKey(args.parsed, args.accountId);
|
|
221
|
+
if (!cacheKey) return [];
|
|
222
|
+
const excludedMessageId = String(args.excludeMessageId || '').trim();
|
|
223
|
+
return (args.cache.get(cacheKey) || [])
|
|
224
|
+
.filter((entry) => !excludedMessageId || entry.messageId !== excludedMessageId)
|
|
225
|
+
.map(cloneBncrOutboundReplayEntry);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function clearBncrOutboundReplay(args: {
|
|
229
|
+
cache: BncrOutboundReplayCache;
|
|
230
|
+
parsed: ParsedInbound;
|
|
231
|
+
accountId: string;
|
|
232
|
+
}) {
|
|
233
|
+
const cacheKey = buildBncrOutboundReplayKey(args.parsed, args.accountId);
|
|
234
|
+
if (!cacheKey) return;
|
|
235
|
+
args.cache.delete(cacheKey);
|
|
236
|
+
}
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
buildBncrGroupContextFromEntries,
|
|
21
21
|
collectBncrHistoryMediaFromEntries,
|
|
22
22
|
} from './group-history.ts';
|
|
23
|
+
import type { BncrOutboundReplayEntry } from './outbound-replay-cache.ts';
|
|
23
24
|
import { resolveBncrChannelInboundRuntime } from './runtime-compat.ts';
|
|
24
25
|
|
|
25
26
|
function parseSlashCommandName(body: string): string | undefined {
|
|
@@ -78,6 +79,7 @@ export function buildBncrInboundTurnContext(args: {
|
|
|
78
79
|
historyLimit?: number;
|
|
79
80
|
silentHistoryFlush?: boolean;
|
|
80
81
|
pendingHistoryEntries?: readonly BncrHistoryEntry[];
|
|
82
|
+
outboundReplayEntries?: readonly BncrOutboundReplayEntry[];
|
|
81
83
|
}): BncrInboundContextPayload | Promise<BncrInboundContextPayload> {
|
|
82
84
|
const {
|
|
83
85
|
api,
|
|
@@ -98,7 +100,13 @@ export function buildBncrInboundTurnContext(args: {
|
|
|
98
100
|
const pendingHistoryEntries = Array.isArray(args.pendingHistoryEntries)
|
|
99
101
|
? args.pendingHistoryEntries
|
|
100
102
|
: [];
|
|
103
|
+
const outboundReplayEntries = Array.isArray(args.outboundReplayEntries)
|
|
104
|
+
? args.outboundReplayEntries
|
|
105
|
+
: [];
|
|
101
106
|
const silentHistoryFlush = args.silentHistoryFlush === true;
|
|
107
|
+
const contextEntries = silentHistoryFlush
|
|
108
|
+
? pendingHistoryEntries
|
|
109
|
+
: [...pendingHistoryEntries, ...outboundReplayEntries];
|
|
102
110
|
const flushSenderId = 'bncr-history-system';
|
|
103
111
|
const flushSenderName = 'Bncr History System';
|
|
104
112
|
const flushRawBody =
|
|
@@ -123,11 +131,11 @@ export function buildBncrInboundTurnContext(args: {
|
|
|
123
131
|
const senderIsOwner = parsed.isAdmin === true || Boolean(ownerAllowFrom?.length);
|
|
124
132
|
const senderIsAuthorized = senderIsOwner;
|
|
125
133
|
const inboundHistory = undefined;
|
|
126
|
-
const
|
|
127
|
-
? collectBncrHistoryMediaFromEntries({ entries:
|
|
134
|
+
const contextMedia = shouldDispatch
|
|
135
|
+
? collectBncrHistoryMediaFromEntries({ entries: contextEntries })
|
|
128
136
|
: [];
|
|
129
|
-
const
|
|
130
|
-
?
|
|
137
|
+
const structuredContextEntries = shouldDispatch
|
|
138
|
+
? contextEntries.map((entry) => ({
|
|
131
139
|
messageId: entry.messageId,
|
|
132
140
|
sender: entry.sender,
|
|
133
141
|
senderId: entry.senderId,
|
|
@@ -155,7 +163,7 @@ export function buildBncrInboundTurnContext(args: {
|
|
|
155
163
|
bridgeSenderName,
|
|
156
164
|
senderIsOwner,
|
|
157
165
|
senderIsAuthorized,
|
|
158
|
-
pendingHistoryContext,
|
|
166
|
+
pendingHistoryContext: structuredContextEntries,
|
|
159
167
|
});
|
|
160
168
|
const promptVisibleContextFacts = buildBncrPromptVisibleContextFacts(structuredContextFacts);
|
|
161
169
|
const supplementalUntrustedContext = [
|
|
@@ -177,10 +185,10 @@ export function buildBncrInboundTurnContext(args: {
|
|
|
177
185
|
const wasMentioned = parsed.isBotMentioned === true || platformWantsReply;
|
|
178
186
|
const isAuthorizedTextCommand = Boolean(ownerAllowFrom?.length);
|
|
179
187
|
const bodyForAgent =
|
|
180
|
-
shouldDispatch &&
|
|
188
|
+
shouldDispatch && contextEntries.length > 0
|
|
181
189
|
? buildBncrGroupContextFromEntries({
|
|
182
190
|
api,
|
|
183
|
-
entries:
|
|
191
|
+
entries: contextEntries,
|
|
184
192
|
channelLabel: resolution.canonicalTo,
|
|
185
193
|
currentTimestamp: Date.now(),
|
|
186
194
|
previousTimestamp: readBncrSessionUpdatedAt(api, {
|
|
@@ -199,7 +207,7 @@ export function buildBncrInboundTurnContext(args: {
|
|
|
199
207
|
kind: item.kind,
|
|
200
208
|
messageId: msgId ?? undefined,
|
|
201
209
|
}));
|
|
202
|
-
const media = [...
|
|
210
|
+
const media = [...contextMedia, ...currentTurnMedia];
|
|
203
211
|
|
|
204
212
|
return Promise.resolve(
|
|
205
213
|
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
|
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import type { GatewayRequestHandlerOptions } from 'openclaw/plugin-sdk/core';
|
|
2
|
+
import type { BncrRecentOutboundEntry } from '../core/types.ts';
|
|
3
|
+
import type { BncrStatusRuntimeSnapshot } from './channel-runtime-types.ts';
|
|
4
|
+
|
|
5
|
+
export const BNCR_LOCAL_BRIDGE_METHODS = [
|
|
6
|
+
'bncr.methods',
|
|
7
|
+
'bncr.status.channel',
|
|
8
|
+
'bncr.status.account',
|
|
9
|
+
'bncr.status.headline',
|
|
10
|
+
'bncr.outbound.recent',
|
|
11
|
+
'bncr.diagnostics',
|
|
12
|
+
'bncr.deadLetter.inspect',
|
|
13
|
+
'bncr.deadLetter.prune',
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
16
|
+
export type BncrBridgeCallDiagnosticsBridge = {
|
|
17
|
+
diagnostics: (args: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
|
18
|
+
deadLetterInspect: (args: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
|
19
|
+
deadLetterPrune: (args: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type BncrBridgeCallBridge = {
|
|
23
|
+
call: (
|
|
24
|
+
method: string,
|
|
25
|
+
args: Record<string, unknown>,
|
|
26
|
+
accountId?: string,
|
|
27
|
+
) => Promise<Record<string, unknown>>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type BncrBridgeCallRecentOutboundBridge = {
|
|
31
|
+
listRecentOutbound: (sessionKey: string) => BncrRecentOutboundEntry[];
|
|
32
|
+
listRecentOutboundByAccount: (accountId: string) => BncrRecentOutboundEntry[];
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Runs a gateway-style handler with a captured respond callback so local
|
|
37
|
+
* diagnostics/dead-letter handlers can be exposed through the message tool.
|
|
38
|
+
*/
|
|
39
|
+
export async function invokeBncrBridgeGatewayHandler(
|
|
40
|
+
handler: (ctx: GatewayRequestHandlerOptions) => Promise<void>,
|
|
41
|
+
args: Record<string, unknown>,
|
|
42
|
+
): Promise<Record<string, unknown>> {
|
|
43
|
+
let ok = false;
|
|
44
|
+
let payload: unknown;
|
|
45
|
+
const ctx = {
|
|
46
|
+
params: args,
|
|
47
|
+
respond: (accepted: boolean, value: unknown) => {
|
|
48
|
+
ok = accepted;
|
|
49
|
+
payload = value;
|
|
50
|
+
},
|
|
51
|
+
} as unknown as GatewayRequestHandlerOptions;
|
|
52
|
+
|
|
53
|
+
await handler(ctx);
|
|
54
|
+
|
|
55
|
+
if (!ok) {
|
|
56
|
+
const message =
|
|
57
|
+
payload &&
|
|
58
|
+
typeof payload === 'object' &&
|
|
59
|
+
typeof (payload as { message?: unknown }).message === 'string'
|
|
60
|
+
? (payload as { message: string }).message
|
|
61
|
+
: 'bridge handler rejected request';
|
|
62
|
+
throw new Error(message);
|
|
63
|
+
}
|
|
64
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
65
|
+
throw new Error('bridge handler returned an invalid payload');
|
|
66
|
+
}
|
|
67
|
+
return payload as Record<string, unknown>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Local methods run inside the plugin process; any other method is forwarded
|
|
72
|
+
* dynamically to the OpenClawClient RPC bridge.
|
|
73
|
+
*/
|
|
74
|
+
export function createBncrBridgeCallBridge(runtime: {
|
|
75
|
+
defaultAccountId: string;
|
|
76
|
+
getStatusBridge: () => {
|
|
77
|
+
getChannelSummary: (
|
|
78
|
+
defaultAccountId: string,
|
|
79
|
+
) => Record<string, unknown> | Promise<Record<string, unknown>>;
|
|
80
|
+
getAccountRuntimeSnapshot: (accountId?: string) => BncrStatusRuntimeSnapshot;
|
|
81
|
+
getStatusHeadline: (accountId?: string) => string;
|
|
82
|
+
};
|
|
83
|
+
getDiagnosticsBridge: () => BncrBridgeCallDiagnosticsBridge;
|
|
84
|
+
getRecentOutboundBridge: () => BncrBridgeCallRecentOutboundBridge;
|
|
85
|
+
callClientRpc: (
|
|
86
|
+
method: string,
|
|
87
|
+
args: Record<string, unknown>,
|
|
88
|
+
accountId: string,
|
|
89
|
+
) => Promise<Record<string, unknown>>;
|
|
90
|
+
}): BncrBridgeCallBridge {
|
|
91
|
+
const resolveAccountId = (args: Record<string, unknown>, accountId?: string) => {
|
|
92
|
+
const argsAccountId = typeof args.accountId === 'string' ? args.accountId.trim() : '';
|
|
93
|
+
return accountId || argsAccountId || runtime.defaultAccountId;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const localHandlers: Record<
|
|
97
|
+
string,
|
|
98
|
+
(args: Record<string, unknown>, accountId: string) => Promise<Record<string, unknown>>
|
|
99
|
+
> = {
|
|
100
|
+
'bncr.methods': async (_args, accountId) => {
|
|
101
|
+
const localMethods = [...BNCR_LOCAL_BRIDGE_METHODS];
|
|
102
|
+
let clientMethods: string[] = [];
|
|
103
|
+
let clientError: string | null = null;
|
|
104
|
+
try {
|
|
105
|
+
const clientResult = await runtime.callClientRpc('bncr.methods', { accountId }, accountId);
|
|
106
|
+
const raw = (clientResult as { result?: { methods?: unknown } })?.result?.methods;
|
|
107
|
+
clientMethods = Array.isArray(raw)
|
|
108
|
+
? raw.filter((item): item is string => typeof item === 'string')
|
|
109
|
+
: [];
|
|
110
|
+
} catch (error) {
|
|
111
|
+
clientError = error instanceof Error ? error.message : String(error);
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
methods: [...new Set([...localMethods, ...clientMethods])],
|
|
115
|
+
...(clientError ? { clientError } : {}),
|
|
116
|
+
};
|
|
117
|
+
},
|
|
118
|
+
'bncr.status.channel': async (_args, accountId) => ({
|
|
119
|
+
value: await runtime.getStatusBridge().getChannelSummary(accountId),
|
|
120
|
+
}),
|
|
121
|
+
'bncr.status.account': async (_args, accountId) => ({
|
|
122
|
+
value: runtime.getStatusBridge().getAccountRuntimeSnapshot(accountId),
|
|
123
|
+
}),
|
|
124
|
+
'bncr.status.headline': async (_args, accountId) => ({
|
|
125
|
+
value: runtime.getStatusBridge().getStatusHeadline(accountId),
|
|
126
|
+
}),
|
|
127
|
+
'bncr.outbound.recent': async (args, accountId) => {
|
|
128
|
+
const sessionKey = typeof args.sessionKey === 'string' ? args.sessionKey.trim() : '';
|
|
129
|
+
const rawLimit = typeof args.limit === 'number' ? args.limit : Number(args.limit);
|
|
130
|
+
const limit = Number.isFinite(rawLimit) ? Math.max(1, Math.floor(rawLimit)) : undefined;
|
|
131
|
+
const recentBridge = runtime.getRecentOutboundBridge();
|
|
132
|
+
const entries = sessionKey
|
|
133
|
+
? recentBridge.listRecentOutbound(sessionKey)
|
|
134
|
+
: recentBridge.listRecentOutboundByAccount(accountId);
|
|
135
|
+
return {
|
|
136
|
+
accountId,
|
|
137
|
+
sessionKey: sessionKey || undefined,
|
|
138
|
+
limit,
|
|
139
|
+
total: entries.length,
|
|
140
|
+
entries: typeof limit === 'number' ? entries.slice(0, limit) : entries,
|
|
141
|
+
now: Date.now(),
|
|
142
|
+
};
|
|
143
|
+
},
|
|
144
|
+
'bncr.diagnostics': (args) => runtime.getDiagnosticsBridge().diagnostics(args),
|
|
145
|
+
'bncr.deadLetter.inspect': (args) => runtime.getDiagnosticsBridge().deadLetterInspect(args),
|
|
146
|
+
'bncr.deadLetter.prune': (args) => runtime.getDiagnosticsBridge().deadLetterPrune(args),
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
call: async (method, args, accountId) => {
|
|
151
|
+
const normalizedMethod = String(method || '').trim();
|
|
152
|
+
if (!normalizedMethod) {
|
|
153
|
+
throw new Error('bridge method is required');
|
|
154
|
+
}
|
|
155
|
+
const normalizedArgs = args ?? {};
|
|
156
|
+
const resolvedAccountId = resolveAccountId(normalizedArgs, accountId);
|
|
157
|
+
const localHandler = localHandlers[normalizedMethod];
|
|
158
|
+
if (localHandler) {
|
|
159
|
+
return {
|
|
160
|
+
ok: true,
|
|
161
|
+
method: normalizedMethod,
|
|
162
|
+
result: await localHandler(normalizedArgs, resolvedAccountId),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return runtime.callClientRpc(normalizedMethod, normalizedArgs, resolvedAccountId);
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
@@ -6,7 +6,12 @@ import {
|
|
|
6
6
|
collectDueOutboxEntries,
|
|
7
7
|
} from '../core/outbox-queue.ts';
|
|
8
8
|
import { formatDisplayScope } from '../core/targets.ts';
|
|
9
|
-
import type { OutboxEntry } from '../core/types.ts';
|
|
9
|
+
import type { BncrRecentOutboundEntry, OutboxEntry } from '../core/types.ts';
|
|
10
|
+
import {
|
|
11
|
+
type BncrOutboundReplayCache,
|
|
12
|
+
type BncrOutboundReplayEntry,
|
|
13
|
+
recordBncrOutboundReplay,
|
|
14
|
+
} from '../messaging/inbound/outbound-replay-cache.ts';
|
|
10
15
|
import {
|
|
11
16
|
buildBncrOutboxFailureEntryPatch,
|
|
12
17
|
buildBncrOutboxPushSuccessEntryPatch,
|
|
@@ -36,6 +41,9 @@ export function createBncrBridgeOutboxFacade(runtime: {
|
|
|
36
41
|
lastPrePushGuardSkipReasonByAccount: Map<string, string>;
|
|
37
42
|
deadLetterSinceStartByAccount: Map<string, number>;
|
|
38
43
|
lastOutboundByAccount: Map<string, number>;
|
|
44
|
+
outboundReplayCache?: BncrOutboundReplayCache;
|
|
45
|
+
resolveOutboundSender?: (entry: OutboxEntry) => { sender: string; senderId?: string };
|
|
46
|
+
isOutboundAckRequired?: (accountId: string) => boolean;
|
|
39
47
|
scheduleSave: () => void;
|
|
40
48
|
flushPushQueueBestEffort: (args?: {
|
|
41
49
|
accountId?: string;
|
|
@@ -48,6 +56,25 @@ export function createBncrBridgeOutboxFacade(runtime: {
|
|
|
48
56
|
resolveMessageAck: (messageId: string, result?: 'acked' | 'timeout') => boolean;
|
|
49
57
|
markActivity: (accountId: string, at?: number) => void;
|
|
50
58
|
}) {
|
|
59
|
+
const recordOutboundMessage = (entry: OutboxEntry, status: 'pushed' | 'acked') => {
|
|
60
|
+
if (!runtime.outboundReplayCache) return;
|
|
61
|
+
const senderInfo = runtime.resolveOutboundSender?.(entry) ?? {
|
|
62
|
+
sender: entry.accountId,
|
|
63
|
+
senderId: entry.accountId,
|
|
64
|
+
};
|
|
65
|
+
recordBncrOutboundReplay({
|
|
66
|
+
cache: runtime.outboundReplayCache,
|
|
67
|
+
entry,
|
|
68
|
+
sender: senderInfo.sender,
|
|
69
|
+
senderId: senderInfo.senderId,
|
|
70
|
+
status,
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const markRecentOutboundAcked = (entry: OutboxEntry) => {
|
|
75
|
+
recordOutboundMessage(entry, 'acked');
|
|
76
|
+
};
|
|
77
|
+
|
|
51
78
|
const recordOutboxPrePushFailure = (args: {
|
|
52
79
|
entry: OutboxEntry;
|
|
53
80
|
lastError: string;
|
|
@@ -120,6 +147,9 @@ export function createBncrBridgeOutboxFacade(runtime: {
|
|
|
120
147
|
});
|
|
121
148
|
Object.assign(args.entry, nextEntry);
|
|
122
149
|
runtime.outbox.set(nextEntry.messageId, args.entry);
|
|
150
|
+
if (runtime.isOutboundAckRequired?.(args.entry.accountId) === false) {
|
|
151
|
+
recordOutboundMessage(args.entry, 'pushed');
|
|
152
|
+
}
|
|
123
153
|
runtime.lastOutboundByAccount.set(nextEntry.accountId, pushedAt);
|
|
124
154
|
runtime.markActivity(nextEntry.accountId, pushedAt);
|
|
125
155
|
runtime.scheduleSave();
|
|
@@ -181,6 +211,41 @@ export function createBncrBridgeOutboxFacade(runtime: {
|
|
|
181
211
|
return result.duePayloads;
|
|
182
212
|
};
|
|
183
213
|
|
|
214
|
+
const buildRecentOutboundEntry = (
|
|
215
|
+
cacheKey: string,
|
|
216
|
+
replay: BncrOutboundReplayEntry,
|
|
217
|
+
): BncrRecentOutboundEntry => {
|
|
218
|
+
const keyParts = cacheKey.split(':');
|
|
219
|
+
const accountId = replay.accountId || keyParts[0] || '';
|
|
220
|
+
const route = replay.route
|
|
221
|
+
? replay.route
|
|
222
|
+
: keyParts[1] && keyParts[2]
|
|
223
|
+
? keyParts[2].startsWith('-')
|
|
224
|
+
? { platform: keyParts[1], groupId: keyParts[2], userId: '0' }
|
|
225
|
+
: { platform: keyParts[1], groupId: '0', userId: keyParts[2] }
|
|
226
|
+
: { platform: '', groupId: '0', userId: '0' };
|
|
227
|
+
return {
|
|
228
|
+
messageId: replay.messageId || '',
|
|
229
|
+
accountId,
|
|
230
|
+
sessionKey: replay.sessionKey || '',
|
|
231
|
+
route,
|
|
232
|
+
...(replay.type ? { type: replay.type } : {}),
|
|
233
|
+
...(replay.type ? { kind: replay.type } : {}),
|
|
234
|
+
text: replay.body,
|
|
235
|
+
...(replay.mediaUrl ? { mediaUrl: replay.mediaUrl } : {}),
|
|
236
|
+
createdAt: replay.createdAt ?? replay.timestamp ?? runtime.now(),
|
|
237
|
+
...(replay.timestamp ? { lastPushAt: replay.timestamp } : {}),
|
|
238
|
+
status: replay.status === 'acked' ? 'acked' : 'pushed',
|
|
239
|
+
};
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const listReplayEntries = () => {
|
|
243
|
+
if (!runtime.outboundReplayCache) return [];
|
|
244
|
+
return Array.from(runtime.outboundReplayCache.entries()).flatMap(([cacheKey, entries]) =>
|
|
245
|
+
entries.map((entry) => buildRecentOutboundEntry(cacheKey, entry)),
|
|
246
|
+
);
|
|
247
|
+
};
|
|
248
|
+
|
|
184
249
|
return {
|
|
185
250
|
recordOutboxPrePushFailure,
|
|
186
251
|
recordPrePushGuardSkip,
|
|
@@ -190,5 +255,18 @@ export function createBncrBridgeOutboxFacade(runtime: {
|
|
|
190
255
|
enqueueOutbound,
|
|
191
256
|
moveToDeadLetter,
|
|
192
257
|
collectDue,
|
|
258
|
+
listRecentOutbound: (sessionKey: string) => {
|
|
259
|
+
const normalized = runtime.asString(sessionKey || '').trim();
|
|
260
|
+
return listReplayEntries()
|
|
261
|
+
.filter((entry) => entry.sessionKey === normalized)
|
|
262
|
+
.sort((a, b) => b.createdAt - a.createdAt);
|
|
263
|
+
},
|
|
264
|
+
listRecentOutboundByAccount: (accountId: string) => {
|
|
265
|
+
const normalized = runtime.normalizeAccountId(accountId);
|
|
266
|
+
return listReplayEntries()
|
|
267
|
+
.filter((entry) => entry.accountId === normalized)
|
|
268
|
+
.sort((a, b) => b.createdAt - a.createdAt);
|
|
269
|
+
},
|
|
270
|
+
markRecentOutboundAcked,
|
|
193
271
|
};
|
|
194
272
|
}
|
|
@@ -6,6 +6,7 @@ import type { BncrRoute } from '../core/types.ts';
|
|
|
6
6
|
import type { BncrInboundParamsInput } from '../messaging/inbound/contracts.ts';
|
|
7
7
|
import { dispatchBncrInbound } from '../messaging/inbound/dispatch.ts';
|
|
8
8
|
import type { BncrGroupHistoryMap } from '../messaging/inbound/group-history.ts';
|
|
9
|
+
import type { BncrOutboundReplayCache } from '../messaging/inbound/outbound-replay-cache.ts';
|
|
9
10
|
import { parseBncrInboundParams } from '../messaging/inbound/parse.ts';
|
|
10
11
|
import { OUTBOUND_FLUSH_REASON, OUTBOUND_FLUSH_TRIGGER } from '../messaging/outbound/reasons.ts';
|
|
11
12
|
import type { ReplyPayloadInput } from '../messaging/outbound/reply-enqueue.ts';
|
|
@@ -211,6 +212,7 @@ export function createBncrInboundHandlersComponent(runtime: {
|
|
|
211
212
|
defaultPublicAgentId: Parameters<typeof createBncrInboundHandlers>[0]['defaultPublicAgentId'];
|
|
212
213
|
sceneRegistry: Parameters<typeof createBncrInboundHandlers>[0]['sceneRegistry'];
|
|
213
214
|
groupHistories: BncrGroupHistoryMap;
|
|
215
|
+
outboundReplayCache: BncrOutboundReplayCache;
|
|
214
216
|
prepareInboundAcceptance: Parameters<
|
|
215
217
|
typeof createBncrInboundHandlers
|
|
216
218
|
>[0]['prepareInboundAcceptance'];
|
|
@@ -256,6 +258,7 @@ export function createBncrInboundHandlersComponent(runtime: {
|
|
|
256
258
|
defaultPublicAgentId: runtime.defaultPublicAgentId,
|
|
257
259
|
sceneRegistry: runtime.sceneRegistry,
|
|
258
260
|
groupHistories: runtime.groupHistories,
|
|
261
|
+
outboundReplayCache: runtime.outboundReplayCache,
|
|
259
262
|
prepareInboundAcceptance: runtime.prepareInboundAcceptance,
|
|
260
263
|
formatDisplayScope,
|
|
261
264
|
logInboundSummary: runtime.logInboundSummary,
|
|
@@ -275,6 +278,7 @@ export function createBncrInboundHandlersComponent(runtime: {
|
|
|
275
278
|
shouldAccumulate,
|
|
276
279
|
sceneRegistry,
|
|
277
280
|
groupHistories,
|
|
281
|
+
outboundReplayCache,
|
|
278
282
|
defaultAdminAgentId,
|
|
279
283
|
defaultPublicAgentId,
|
|
280
284
|
now,
|
|
@@ -290,6 +294,7 @@ export function createBncrInboundHandlersComponent(runtime: {
|
|
|
290
294
|
shouldAccumulate,
|
|
291
295
|
sceneRegistry,
|
|
292
296
|
groupHistories,
|
|
297
|
+
outboundReplayCache,
|
|
293
298
|
defaultAdminAgentId,
|
|
294
299
|
defaultPublicAgentId,
|
|
295
300
|
now,
|