@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,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,13 @@ 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 type { BncrConversationHistoryMap } from '../messaging/inbound/conversation-history.ts';
|
|
11
|
+
import {
|
|
12
|
+
type BncrOutboundReplayCache,
|
|
13
|
+
type BncrOutboundReplayEntry,
|
|
14
|
+
recordBncrOutboundReplay,
|
|
15
|
+
} from '../messaging/inbound/outbound-replay-cache.ts';
|
|
10
16
|
import {
|
|
11
17
|
buildBncrOutboxFailureEntryPatch,
|
|
12
18
|
buildBncrOutboxPushSuccessEntryPatch,
|
|
@@ -36,6 +42,11 @@ export function createBncrBridgeOutboxFacade(runtime: {
|
|
|
36
42
|
lastPrePushGuardSkipReasonByAccount: Map<string, string>;
|
|
37
43
|
deadLetterSinceStartByAccount: Map<string, number>;
|
|
38
44
|
lastOutboundByAccount: Map<string, number>;
|
|
45
|
+
outboundReplayCache?: BncrOutboundReplayCache;
|
|
46
|
+
conversationHistories?: BncrConversationHistoryMap;
|
|
47
|
+
resolveOutboundHistoryLimit?: (entry: OutboxEntry) => number;
|
|
48
|
+
resolveOutboundSender?: (entry: OutboxEntry) => { sender: string; senderId?: string };
|
|
49
|
+
isOutboundAckRequired?: (accountId: string) => boolean;
|
|
39
50
|
scheduleSave: () => void;
|
|
40
51
|
flushPushQueueBestEffort: (args?: {
|
|
41
52
|
accountId?: string;
|
|
@@ -48,6 +59,31 @@ export function createBncrBridgeOutboxFacade(runtime: {
|
|
|
48
59
|
resolveMessageAck: (messageId: string, result?: 'acked' | 'timeout') => boolean;
|
|
49
60
|
markActivity: (accountId: string, at?: number) => void;
|
|
50
61
|
}) {
|
|
62
|
+
const recordOutboundMessage = (entry: OutboxEntry, status: 'pushed' | 'acked') => {
|
|
63
|
+
if (!runtime.outboundReplayCache) return;
|
|
64
|
+
const senderInfo = runtime.resolveOutboundSender?.(entry) ?? {
|
|
65
|
+
sender: entry.accountId,
|
|
66
|
+
senderId: entry.accountId,
|
|
67
|
+
};
|
|
68
|
+
recordBncrOutboundReplay({
|
|
69
|
+
cache: runtime.outboundReplayCache,
|
|
70
|
+
...(runtime.conversationHistories
|
|
71
|
+
? { conversationHistories: runtime.conversationHistories }
|
|
72
|
+
: {}),
|
|
73
|
+
...(runtime.resolveOutboundHistoryLimit
|
|
74
|
+
? { historyLimit: runtime.resolveOutboundHistoryLimit(entry) }
|
|
75
|
+
: {}),
|
|
76
|
+
entry,
|
|
77
|
+
sender: senderInfo.sender,
|
|
78
|
+
senderId: senderInfo.senderId,
|
|
79
|
+
status,
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const markRecentOutboundAcked = (entry: OutboxEntry) => {
|
|
84
|
+
recordOutboundMessage(entry, 'acked');
|
|
85
|
+
};
|
|
86
|
+
|
|
51
87
|
const recordOutboxPrePushFailure = (args: {
|
|
52
88
|
entry: OutboxEntry;
|
|
53
89
|
lastError: string;
|
|
@@ -120,6 +156,9 @@ export function createBncrBridgeOutboxFacade(runtime: {
|
|
|
120
156
|
});
|
|
121
157
|
Object.assign(args.entry, nextEntry);
|
|
122
158
|
runtime.outbox.set(nextEntry.messageId, args.entry);
|
|
159
|
+
if (runtime.isOutboundAckRequired?.(args.entry.accountId) === false) {
|
|
160
|
+
recordOutboundMessage(args.entry, 'pushed');
|
|
161
|
+
}
|
|
123
162
|
runtime.lastOutboundByAccount.set(nextEntry.accountId, pushedAt);
|
|
124
163
|
runtime.markActivity(nextEntry.accountId, pushedAt);
|
|
125
164
|
runtime.scheduleSave();
|
|
@@ -181,6 +220,41 @@ export function createBncrBridgeOutboxFacade(runtime: {
|
|
|
181
220
|
return result.duePayloads;
|
|
182
221
|
};
|
|
183
222
|
|
|
223
|
+
const buildRecentOutboundEntry = (
|
|
224
|
+
cacheKey: string,
|
|
225
|
+
replay: BncrOutboundReplayEntry,
|
|
226
|
+
): BncrRecentOutboundEntry => {
|
|
227
|
+
const keyParts = cacheKey.split(':');
|
|
228
|
+
const accountId = replay.accountId || keyParts[0] || '';
|
|
229
|
+
const route = replay.route
|
|
230
|
+
? replay.route
|
|
231
|
+
: keyParts[1] && keyParts[2]
|
|
232
|
+
? keyParts[2].startsWith('-')
|
|
233
|
+
? { platform: keyParts[1], groupId: keyParts[2], userId: '0' }
|
|
234
|
+
: { platform: keyParts[1], groupId: '0', userId: keyParts[2] }
|
|
235
|
+
: { platform: '', groupId: '0', userId: '0' };
|
|
236
|
+
return {
|
|
237
|
+
messageId: replay.messageId || '',
|
|
238
|
+
accountId,
|
|
239
|
+
sessionKey: replay.sessionKey || '',
|
|
240
|
+
route,
|
|
241
|
+
...(replay.type ? { type: replay.type } : {}),
|
|
242
|
+
...(replay.type ? { kind: replay.type } : {}),
|
|
243
|
+
text: replay.body,
|
|
244
|
+
...(replay.mediaUrl ? { mediaUrl: replay.mediaUrl } : {}),
|
|
245
|
+
createdAt: replay.createdAt ?? replay.timestamp ?? runtime.now(),
|
|
246
|
+
...(replay.timestamp ? { lastPushAt: replay.timestamp } : {}),
|
|
247
|
+
status: replay.status === 'acked' ? 'acked' : 'pushed',
|
|
248
|
+
};
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const listReplayEntries = () => {
|
|
252
|
+
if (!runtime.outboundReplayCache) return [];
|
|
253
|
+
return Array.from(runtime.outboundReplayCache.entries()).flatMap(([cacheKey, entries]) =>
|
|
254
|
+
entries.map((entry) => buildRecentOutboundEntry(cacheKey, entry)),
|
|
255
|
+
);
|
|
256
|
+
};
|
|
257
|
+
|
|
184
258
|
return {
|
|
185
259
|
recordOutboxPrePushFailure,
|
|
186
260
|
recordPrePushGuardSkip,
|
|
@@ -190,5 +264,18 @@ export function createBncrBridgeOutboxFacade(runtime: {
|
|
|
190
264
|
enqueueOutbound,
|
|
191
265
|
moveToDeadLetter,
|
|
192
266
|
collectDue,
|
|
267
|
+
listRecentOutbound: (sessionKey: string) => {
|
|
268
|
+
const normalized = runtime.asString(sessionKey || '').trim();
|
|
269
|
+
return listReplayEntries()
|
|
270
|
+
.filter((entry) => entry.sessionKey === normalized)
|
|
271
|
+
.sort((a, b) => b.createdAt - a.createdAt);
|
|
272
|
+
},
|
|
273
|
+
listRecentOutboundByAccount: (accountId: string) => {
|
|
274
|
+
const normalized = runtime.normalizeAccountId(accountId);
|
|
275
|
+
return listReplayEntries()
|
|
276
|
+
.filter((entry) => entry.accountId === normalized)
|
|
277
|
+
.sort((a, b) => b.createdAt - a.createdAt);
|
|
278
|
+
},
|
|
279
|
+
markRecentOutboundAcked,
|
|
193
280
|
};
|
|
194
281
|
}
|
|
@@ -4,8 +4,9 @@ import { emitBncrLogLine } from '../core/logging.ts';
|
|
|
4
4
|
import { formatDisplayScope, normalizeStoredSessionKey, parseRouteLike } from '../core/targets.ts';
|
|
5
5
|
import type { BncrRoute } from '../core/types.ts';
|
|
6
6
|
import type { BncrInboundParamsInput } from '../messaging/inbound/contracts.ts';
|
|
7
|
+
import type { BncrConversationHistoryMap } from '../messaging/inbound/conversation-history.ts';
|
|
7
8
|
import { dispatchBncrInbound } from '../messaging/inbound/dispatch.ts';
|
|
8
|
-
import type {
|
|
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';
|
|
@@ -210,7 +211,8 @@ export function createBncrInboundHandlersComponent(runtime: {
|
|
|
210
211
|
defaultAdminAgentId: Parameters<typeof createBncrInboundHandlers>[0]['defaultAdminAgentId'];
|
|
211
212
|
defaultPublicAgentId: Parameters<typeof createBncrInboundHandlers>[0]['defaultPublicAgentId'];
|
|
212
213
|
sceneRegistry: Parameters<typeof createBncrInboundHandlers>[0]['sceneRegistry'];
|
|
213
|
-
|
|
214
|
+
conversationHistories: BncrConversationHistoryMap;
|
|
215
|
+
outboundReplayCache: BncrOutboundReplayCache;
|
|
214
216
|
prepareInboundAcceptance: Parameters<
|
|
215
217
|
typeof createBncrInboundHandlers
|
|
216
218
|
>[0]['prepareInboundAcceptance'];
|
|
@@ -255,7 +257,8 @@ export function createBncrInboundHandlersComponent(runtime: {
|
|
|
255
257
|
defaultAdminAgentId: runtime.defaultAdminAgentId,
|
|
256
258
|
defaultPublicAgentId: runtime.defaultPublicAgentId,
|
|
257
259
|
sceneRegistry: runtime.sceneRegistry,
|
|
258
|
-
|
|
260
|
+
conversationHistories: runtime.conversationHistories,
|
|
261
|
+
outboundReplayCache: runtime.outboundReplayCache,
|
|
259
262
|
prepareInboundAcceptance: runtime.prepareInboundAcceptance,
|
|
260
263
|
formatDisplayScope,
|
|
261
264
|
logInboundSummary: runtime.logInboundSummary,
|
|
@@ -274,7 +277,8 @@ export function createBncrInboundHandlersComponent(runtime: {
|
|
|
274
277
|
shouldDispatch,
|
|
275
278
|
shouldAccumulate,
|
|
276
279
|
sceneRegistry,
|
|
277
|
-
|
|
280
|
+
conversationHistories,
|
|
281
|
+
outboundReplayCache,
|
|
278
282
|
defaultAdminAgentId,
|
|
279
283
|
defaultPublicAgentId,
|
|
280
284
|
now,
|
|
@@ -289,7 +293,8 @@ export function createBncrInboundHandlersComponent(runtime: {
|
|
|
289
293
|
shouldDispatch,
|
|
290
294
|
shouldAccumulate,
|
|
291
295
|
sceneRegistry,
|
|
292
|
-
|
|
296
|
+
conversationHistories,
|
|
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
|
-
import type {
|
|
3
|
+
import type { BncrConversationHistoryMap } from '../messaging/inbound/conversation-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';
|
|
@@ -106,7 +107,8 @@ export function buildBncrInboundSurfaceRuntime(deps: {
|
|
|
106
107
|
}) => string;
|
|
107
108
|
defaultPublicAgentId: () => string;
|
|
108
109
|
sceneRegistry: Map<string, BncrSceneRecord>;
|
|
109
|
-
|
|
110
|
+
conversationHistories: BncrConversationHistoryMap;
|
|
111
|
+
outboundReplayCache: BncrOutboundReplayCache;
|
|
110
112
|
prepareInboundAcceptance: (args: {
|
|
111
113
|
parsed: ReturnType<typeof parseBncrInboundParams>;
|
|
112
114
|
canonicalAgentId: string;
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
+
import type { TSchema } from 'typebox';
|
|
1
2
|
import type { RegisterDriftSnapshot } from '../core/register-trace.ts';
|
|
2
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
BncrDiagnosticsSummary,
|
|
5
|
+
BncrOutboundReplayEntry,
|
|
6
|
+
BncrRoute,
|
|
7
|
+
OutboxEntry,
|
|
8
|
+
} from '../core/types.ts';
|
|
3
9
|
import type { OpenClawChannelToolSend, openClawJsonResult } from '../openclaw/sdk-helpers.ts';
|
|
4
10
|
|
|
5
11
|
type OpenClawJsonResultPayload = ReturnType<typeof openClawJsonResult>;
|
|
@@ -67,25 +73,33 @@ export type BncrSceneRecord = {
|
|
|
67
73
|
lastSeenAt: number;
|
|
68
74
|
};
|
|
69
75
|
|
|
70
|
-
export type
|
|
76
|
+
export type BncrPersistedConversationHistoryEntry = {
|
|
71
77
|
sender: string;
|
|
72
78
|
senderId?: string;
|
|
79
|
+
role?: 'user' | 'assistant' | 'system';
|
|
73
80
|
body: string;
|
|
74
81
|
timestamp?: number;
|
|
75
82
|
messageId?: string;
|
|
76
|
-
media?:
|
|
83
|
+
media?: BncrPersistedConversationHistoryMediaEntry[];
|
|
77
84
|
};
|
|
78
85
|
|
|
79
|
-
export type
|
|
86
|
+
export type BncrPersistedConversationHistoryMediaEntry = {
|
|
80
87
|
path?: string;
|
|
81
88
|
contentType?: string;
|
|
82
89
|
kind?: 'image' | 'video' | 'audio' | 'document' | 'unknown';
|
|
83
90
|
messageId?: string;
|
|
84
91
|
};
|
|
85
92
|
|
|
86
|
-
export type
|
|
93
|
+
export type BncrPersistedConversationHistoryBucket = {
|
|
87
94
|
key: string;
|
|
88
|
-
entries:
|
|
95
|
+
entries: BncrPersistedConversationHistoryEntry[];
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export type BncrPersistedOutboundReplayEntry = BncrOutboundReplayEntry;
|
|
99
|
+
export type BncrPersistedOutboundReplayMediaEntry = BncrPersistedConversationHistoryMediaEntry;
|
|
100
|
+
export type BncrPersistedOutboundReplayBucket = {
|
|
101
|
+
key: string;
|
|
102
|
+
entries: BncrPersistedOutboundReplayEntry[];
|
|
89
103
|
};
|
|
90
104
|
|
|
91
105
|
export type BncrStatusRuntimeSnapshot = {
|
|
@@ -143,10 +157,20 @@ export type BncrChannelSendContext = {
|
|
|
143
157
|
extra?: Record<string, unknown>;
|
|
144
158
|
};
|
|
145
159
|
|
|
160
|
+
/**
|
|
161
|
+
* TypeBox schema contribution for the shared `message` tool.
|
|
162
|
+
* The host wraps each property with Type.Optional before validation.
|
|
163
|
+
*/
|
|
164
|
+
export type BncrMessageToolSchemaContribution = {
|
|
165
|
+
visibility?: 'current-channel' | 'all-configured';
|
|
166
|
+
properties: Record<string, TSchema>;
|
|
167
|
+
};
|
|
168
|
+
|
|
146
169
|
export type ChannelMessageActionAdapter = {
|
|
147
170
|
describeMessageTool: (ctx: { cfg: BncrChannelConfigRoot }) => {
|
|
148
|
-
actions: readonly
|
|
171
|
+
actions: readonly ('send' | 'delete' | 'unsend')[];
|
|
149
172
|
capabilities: readonly [];
|
|
173
|
+
schema?: BncrMessageToolSchemaContribution;
|
|
150
174
|
} | null;
|
|
151
175
|
supportsAction: (ctx: { action: string }) => boolean;
|
|
152
176
|
extractToolSend: (ctx: { args: unknown }) => OpenClawChannelToolSend | null;
|
|
@@ -163,7 +187,9 @@ export type PersistedState = {
|
|
|
163
187
|
deadLetter: OutboxEntry[];
|
|
164
188
|
sessionRoutes: BncrPersistedSessionRoute[];
|
|
165
189
|
sceneRegistry?: BncrSceneRecord[];
|
|
166
|
-
|
|
190
|
+
conversationHistories?: BncrPersistedConversationHistoryBucket[];
|
|
191
|
+
groupHistories?: BncrPersistedConversationHistoryBucket[];
|
|
192
|
+
outboundReplayCache?: BncrPersistedOutboundReplayBucket[];
|
|
167
193
|
lastSessionByAccount?: BncrPersistedLastSession[];
|
|
168
194
|
lastActivityByAccount?: BncrPersistedAccountTimestamp[];
|
|
169
195
|
lastInboundByAccount?: BncrPersistedAccountTimestamp[];
|