@xmoxmo/bncr 0.6.0 → 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.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
|
@@ -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,
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import type { ChannelMessageSendResult } from 'openclaw/plugin-sdk/channel-message';
|
|
2
|
-
import type {
|
|
2
|
+
import type { GatewayRequestHandlerOptions } from 'openclaw/plugin-sdk/core';
|
|
3
|
+
import type { BncrRecentOutboundEntry, BncrRoute } from '../core/types.ts';
|
|
3
4
|
import type { ReplyPayloadInput } from '../messaging/outbound/reply-enqueue.ts';
|
|
4
5
|
import type { OutboundReplyTargetPolicy } from '../messaging/outbound/reply-target-policy.ts';
|
|
6
|
+
import {
|
|
7
|
+
type BncrBridgeCallDiagnosticsBridge,
|
|
8
|
+
createBncrBridgeCallBridge,
|
|
9
|
+
invokeBncrBridgeGatewayHandler,
|
|
10
|
+
} from './bridge-call.ts';
|
|
5
11
|
import type {
|
|
6
12
|
BncrChannelConfigRoot,
|
|
7
13
|
BncrChannelSendContext,
|
|
@@ -34,6 +40,8 @@ export type BncrChannelPluginBridge = {
|
|
|
34
40
|
getStatusHeadline: (accountId: string) => string;
|
|
35
41
|
resolveVerifiedTarget: (to: string, accountId: string) => BncrVerifiedTarget;
|
|
36
42
|
rememberSessionRoute: (sessionKey: string, accountId: string, route: BncrRoute) => void;
|
|
43
|
+
listRecentOutbound: (sessionKey: string) => BncrRecentOutboundEntry[];
|
|
44
|
+
listRecentOutboundByAccount: (accountId: string) => BncrRecentOutboundEntry[];
|
|
37
45
|
enqueueFromReply: (args: {
|
|
38
46
|
accountId: string;
|
|
39
47
|
sessionKey: string;
|
|
@@ -44,6 +52,14 @@ export type BncrChannelPluginBridge = {
|
|
|
44
52
|
}) => Promise<void>;
|
|
45
53
|
channelStartAccount: (ctx: unknown) => void | Promise<void>;
|
|
46
54
|
channelStopAccount: (ctx: unknown) => void | Promise<void>;
|
|
55
|
+
handleDiagnostics: (ctx: GatewayRequestHandlerOptions) => Promise<void>;
|
|
56
|
+
handleDeadLetterInspect: (ctx: GatewayRequestHandlerOptions) => Promise<void>;
|
|
57
|
+
handleDeadLetterPrune: (ctx: GatewayRequestHandlerOptions) => Promise<void>;
|
|
58
|
+
callClientRpc: (
|
|
59
|
+
method: string,
|
|
60
|
+
args: Record<string, unknown>,
|
|
61
|
+
accountId: string,
|
|
62
|
+
) => Promise<Record<string, unknown>>;
|
|
47
63
|
};
|
|
48
64
|
|
|
49
65
|
export function createBncrChannelPluginBridgeGroup(runtime: {
|
|
@@ -118,6 +134,31 @@ export function createBncrChannelPluginBridgeGroup(runtime: {
|
|
|
118
134
|
};
|
|
119
135
|
};
|
|
120
136
|
|
|
137
|
+
const getDiagnosticsBridge = (): BncrBridgeCallDiagnosticsBridge => {
|
|
138
|
+
const bridge = runtime.getBridge();
|
|
139
|
+
return {
|
|
140
|
+
diagnostics: async (args) => invokeBncrBridgeGatewayHandler(bridge.handleDiagnostics, args),
|
|
141
|
+
deadLetterInspect: async (args) =>
|
|
142
|
+
invokeBncrBridgeGatewayHandler(bridge.handleDeadLetterInspect, args),
|
|
143
|
+
deadLetterPrune: async (args) =>
|
|
144
|
+
invokeBncrBridgeGatewayHandler(bridge.handleDeadLetterPrune, args),
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const getBridgeCallBridge = () => {
|
|
149
|
+
const bridge = runtime.getBridge();
|
|
150
|
+
return createBncrBridgeCallBridge({
|
|
151
|
+
defaultAccountId: runtime.defaultAccountId,
|
|
152
|
+
getStatusBridge,
|
|
153
|
+
getDiagnosticsBridge,
|
|
154
|
+
getRecentOutboundBridge: () => ({
|
|
155
|
+
listRecentOutbound: (sessionKey) => bridge.listRecentOutbound(sessionKey),
|
|
156
|
+
listRecentOutboundByAccount: (accountId) => bridge.listRecentOutboundByAccount(accountId),
|
|
157
|
+
}),
|
|
158
|
+
callClientRpc: (method, args, accountId) => bridge.callClientRpc(method, args, accountId),
|
|
159
|
+
});
|
|
160
|
+
};
|
|
161
|
+
|
|
121
162
|
return {
|
|
122
163
|
getMessageSendBridge,
|
|
123
164
|
getOutboundBridge,
|
|
@@ -125,5 +166,6 @@ export function createBncrChannelPluginBridgeGroup(runtime: {
|
|
|
125
166
|
getStatusBridge,
|
|
126
167
|
getToolActionBridge,
|
|
127
168
|
getGatewayBridge,
|
|
169
|
+
getBridgeCallBridge,
|
|
128
170
|
};
|
|
129
171
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ChannelMessageSendResult } from 'openclaw/plugin-sdk/channel-message';
|
|
2
|
+
import { Type } from 'typebox';
|
|
2
3
|
import { listAccountIds, normalizeAccountId, resolveAccount } from '../core/accounts.ts';
|
|
3
4
|
import { BncrConfigSchema } from '../core/config-schema.ts';
|
|
4
5
|
import { resolveBncrChannelPolicy } from '../core/policy.ts';
|
|
@@ -6,9 +7,11 @@ import type { ReplyPayloadInput } from '../messaging/outbound/reply-enqueue.ts';
|
|
|
6
7
|
import type { OutboundReplyTargetPolicy } from '../messaging/outbound/reply-target-policy.ts';
|
|
7
8
|
import type { OpenClawChannelToolSend, openClawJsonResult } from '../openclaw/sdk-helpers.ts';
|
|
8
9
|
import { readOpenClawStringParam } from '../openclaw/sdk-helpers.ts';
|
|
10
|
+
import type { BncrBridgeCallBridge } from './bridge-call.ts';
|
|
9
11
|
import type {
|
|
10
12
|
BncrChannelConfigRoot,
|
|
11
13
|
BncrChannelSendContext,
|
|
14
|
+
BncrMessageToolSchemaContribution,
|
|
12
15
|
BncrSceneRecord,
|
|
13
16
|
BncrStatusRuntimeSnapshot,
|
|
14
17
|
BncrVerifiedTarget,
|
|
@@ -35,6 +38,33 @@ type BncrChannelMeta = {
|
|
|
35
38
|
|
|
36
39
|
type PluginSurfaceResult = ReturnType<typeof openClawJsonResult>;
|
|
37
40
|
|
|
41
|
+
const BNCR_MESSAGE_TOOL_SCHEMA: BncrMessageToolSchemaContribution = {
|
|
42
|
+
visibility: 'current-channel',
|
|
43
|
+
properties: {
|
|
44
|
+
bridgeMethod: Type.Optional(
|
|
45
|
+
Type.String({
|
|
46
|
+
description:
|
|
47
|
+
'Bncr-only bridge method forwarded to the OpenClawClient RPC bridge; use bncr.methods to list calls, or bncr.client.adapters.call to invoke a Bncr adapter method generically.',
|
|
48
|
+
}),
|
|
49
|
+
),
|
|
50
|
+
extra: Type.Optional(
|
|
51
|
+
Type.Record(Type.String(), Type.Unknown(), {
|
|
52
|
+
description: 'Bncr-specific structured params merged into the unified outbound normalizer.',
|
|
53
|
+
}),
|
|
54
|
+
),
|
|
55
|
+
type: Type.Optional(
|
|
56
|
+
Type.String({
|
|
57
|
+
description: 'Explicit outbound type such as text, audio, video, voice, file, or appmsg.',
|
|
58
|
+
}),
|
|
59
|
+
),
|
|
60
|
+
downloadMedia: Type.Optional(
|
|
61
|
+
Type.Boolean({
|
|
62
|
+
description: 'Force URL media to be downloaded before sending.',
|
|
63
|
+
}),
|
|
64
|
+
),
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
38
68
|
export function createBncrChannelPluginSurfaceGroup(runtime: {
|
|
39
69
|
channelId: string;
|
|
40
70
|
sceneRegistry: Map<string, BncrSceneRecord>;
|
|
@@ -80,6 +110,7 @@ export function createBncrChannelPluginSurfaceGroup(runtime: {
|
|
|
80
110
|
channelStartAccount: BncrGatewayAccountBridge['channelStartAccount'];
|
|
81
111
|
channelStopAccount: BncrGatewayAccountBridge['channelStopAccount'];
|
|
82
112
|
};
|
|
113
|
+
getBridgeCallBridge: () => BncrBridgeCallBridge;
|
|
83
114
|
channelMeta: BncrChannelMeta;
|
|
84
115
|
channelCapabilities: {
|
|
85
116
|
chatTypes: Array<'direct' | 'group' | 'thread'>;
|
|
@@ -121,11 +152,12 @@ export function createBncrChannelPluginSurfaceGroup(runtime: {
|
|
|
121
152
|
}
|
|
122
153
|
|
|
123
154
|
return {
|
|
124
|
-
actions: ['send'] as const,
|
|
155
|
+
actions: ['send', 'delete', 'unsend'] as const,
|
|
125
156
|
capabilities: [] as const,
|
|
157
|
+
schema: BNCR_MESSAGE_TOOL_SCHEMA,
|
|
126
158
|
};
|
|
127
159
|
},
|
|
128
|
-
supportsAction: ({ action }) => action === 'send',
|
|
160
|
+
supportsAction: ({ action }) => action === 'send' || action === 'delete' || action === 'unsend',
|
|
129
161
|
extractToolSend: ({ args }) =>
|
|
130
162
|
runtime.extractToolSend(
|
|
131
163
|
(args && typeof args === 'object' ? (args as Record<string, unknown>) : {}) as Record<
|
|
@@ -135,19 +167,62 @@ export function createBncrChannelPluginSurfaceGroup(runtime: {
|
|
|
135
167
|
'sendMessage',
|
|
136
168
|
) || null,
|
|
137
169
|
handleAction: async ({ action, params, accountId, mediaLocalRoots }) => {
|
|
138
|
-
if (action !== 'send') {
|
|
139
|
-
throw new Error(`Action ${action} is not supported for provider ${runtime.channelId}.`);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
170
|
/* -- extract raw params (no normalization) ----------------------- */
|
|
143
171
|
const paramsObj = (params && typeof params === 'object' ? params : {}) as Record<
|
|
144
172
|
string,
|
|
145
173
|
unknown
|
|
146
174
|
>;
|
|
147
|
-
const to = readOpenClawStringParam(paramsObj, 'to', { required: true });
|
|
148
175
|
const resolvedAccountId = normalizeAccountId(
|
|
149
176
|
(readOpenClawStringParam(paramsObj, 'accountId') ?? accountId) || '',
|
|
150
177
|
);
|
|
178
|
+
if (action === 'delete' || action === 'unsend') {
|
|
179
|
+
const to = readOpenClawStringParam(paramsObj, 'to', { required: true });
|
|
180
|
+
const messageId =
|
|
181
|
+
(typeof paramsObj.messageId === 'number'
|
|
182
|
+
? String(paramsObj.messageId)
|
|
183
|
+
: readOpenClawStringParam(paramsObj, 'messageId')) ||
|
|
184
|
+
readOpenClawStringParam(paramsObj, 'message_id');
|
|
185
|
+
if (!messageId) {
|
|
186
|
+
throw new Error(`${action} requires messageId`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const verified = runtime.getToolActionBridge().resolveVerifiedTarget(to, resolvedAccountId);
|
|
190
|
+
const bridgeResult = await runtime.getBridgeCallBridge().call(
|
|
191
|
+
'bncr.client.adapters.call',
|
|
192
|
+
{
|
|
193
|
+
msgInfo: {
|
|
194
|
+
groupId: verified.route.groupId || '0',
|
|
195
|
+
userId: verified.route.userId || '0',
|
|
196
|
+
},
|
|
197
|
+
from: verified.route.platform,
|
|
198
|
+
method: 'delMsg',
|
|
199
|
+
messageId,
|
|
200
|
+
},
|
|
201
|
+
resolvedAccountId,
|
|
202
|
+
);
|
|
203
|
+
return runtime.openClawJsonResult({ ok: true, ...bridgeResult });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (action !== 'send') {
|
|
207
|
+
throw new Error(`Action ${action} is not supported for provider ${runtime.channelId}.`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const bridgeMethod =
|
|
211
|
+
typeof paramsObj.bridgeMethod === 'string' ? paramsObj.bridgeMethod.trim() : '';
|
|
212
|
+
if (bridgeMethod) {
|
|
213
|
+
const rawExtra = paramsObj.extra;
|
|
214
|
+
const bridgeArgs =
|
|
215
|
+
rawExtra && typeof rawExtra === 'object' && !Array.isArray(rawExtra)
|
|
216
|
+
? { ...(rawExtra as Record<string, unknown>) }
|
|
217
|
+
: {};
|
|
218
|
+
if (!('accountId' in bridgeArgs)) {
|
|
219
|
+
bridgeArgs.accountId = resolvedAccountId || undefined;
|
|
220
|
+
}
|
|
221
|
+
const bridgeResult = await runtime.getBridgeCallBridge().call(bridgeMethod, bridgeArgs);
|
|
222
|
+
return runtime.openClawJsonResult(bridgeResult);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const to = readOpenClawStringParam(paramsObj, 'to', { required: true });
|
|
151
226
|
const toolMediaUrl =
|
|
152
227
|
(typeof paramsObj.mediaUrl === 'string' ? paramsObj.mediaUrl : '') ||
|
|
153
228
|
(typeof paramsObj.media === 'string' ? paramsObj.media : '') ||
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { GatewayRequestHandlerOptions } from 'openclaw/plugin-sdk/core';
|
|
2
2
|
import type { BncrRoute, OutboxEntry } from '../core/types.ts';
|
|
3
3
|
import type { BncrGroupHistoryMap } from '../messaging/inbound/group-history.ts';
|
|
4
|
+
import type { BncrOutboundReplayCache } from '../messaging/inbound/outbound-replay-cache.ts';
|
|
4
5
|
import type { parseBncrInboundParams } from '../messaging/inbound/parse.ts';
|
|
5
6
|
import type { ReplyPayloadInput } from '../messaging/outbound/reply-enqueue.ts';
|
|
6
7
|
import type { OutboundReplyTargetPolicy } from '../messaging/outbound/reply-target-policy.ts';
|
|
@@ -107,6 +108,7 @@ export function buildBncrInboundSurfaceRuntime(deps: {
|
|
|
107
108
|
defaultPublicAgentId: () => string;
|
|
108
109
|
sceneRegistry: Map<string, BncrSceneRecord>;
|
|
109
110
|
groupHistories: BncrGroupHistoryMap;
|
|
111
|
+
outboundReplayCache: BncrOutboundReplayCache;
|
|
110
112
|
prepareInboundAcceptance: (args: {
|
|
111
113
|
parsed: ReturnType<typeof parseBncrInboundParams>;
|
|
112
114
|
canonicalAgentId: string;
|