@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
|
@@ -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;
|
|
@@ -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>;
|
|
@@ -88,6 +94,13 @@ export type BncrPersistedGroupHistoryBucket = {
|
|
|
88
94
|
entries: BncrPersistedGroupHistoryEntry[];
|
|
89
95
|
};
|
|
90
96
|
|
|
97
|
+
export type BncrPersistedOutboundReplayEntry = BncrOutboundReplayEntry;
|
|
98
|
+
export type BncrPersistedOutboundReplayMediaEntry = BncrPersistedGroupHistoryMediaEntry;
|
|
99
|
+
export type BncrPersistedOutboundReplayBucket = {
|
|
100
|
+
key: string;
|
|
101
|
+
entries: BncrPersistedOutboundReplayEntry[];
|
|
102
|
+
};
|
|
103
|
+
|
|
91
104
|
export type BncrStatusRuntimeSnapshot = {
|
|
92
105
|
connected?: boolean;
|
|
93
106
|
running?: boolean;
|
|
@@ -143,10 +156,20 @@ export type BncrChannelSendContext = {
|
|
|
143
156
|
extra?: Record<string, unknown>;
|
|
144
157
|
};
|
|
145
158
|
|
|
159
|
+
/**
|
|
160
|
+
* TypeBox schema contribution for the shared `message` tool.
|
|
161
|
+
* The host wraps each property with Type.Optional before validation.
|
|
162
|
+
*/
|
|
163
|
+
export type BncrMessageToolSchemaContribution = {
|
|
164
|
+
visibility?: 'current-channel' | 'all-configured';
|
|
165
|
+
properties: Record<string, TSchema>;
|
|
166
|
+
};
|
|
167
|
+
|
|
146
168
|
export type ChannelMessageActionAdapter = {
|
|
147
169
|
describeMessageTool: (ctx: { cfg: BncrChannelConfigRoot }) => {
|
|
148
|
-
actions: readonly
|
|
170
|
+
actions: readonly ('send' | 'delete' | 'unsend')[];
|
|
149
171
|
capabilities: readonly [];
|
|
172
|
+
schema?: BncrMessageToolSchemaContribution;
|
|
150
173
|
} | null;
|
|
151
174
|
supportsAction: (ctx: { action: string }) => boolean;
|
|
152
175
|
extractToolSend: (ctx: { args: unknown }) => OpenClawChannelToolSend | null;
|
|
@@ -164,6 +187,7 @@ export type PersistedState = {
|
|
|
164
187
|
sessionRoutes: BncrPersistedSessionRoute[];
|
|
165
188
|
sceneRegistry?: BncrSceneRecord[];
|
|
166
189
|
groupHistories?: BncrPersistedGroupHistoryBucket[];
|
|
190
|
+
outboundReplayCache?: BncrPersistedOutboundReplayBucket[];
|
|
167
191
|
lastSessionByAccount?: BncrPersistedLastSession[];
|
|
168
192
|
lastActivityByAccount?: BncrPersistedAccountTimestamp[];
|
|
169
193
|
lastInboundByAccount?: BncrPersistedAccountTimestamp[];
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
1
|
import { normalizeAccountId } from '../core/accounts.ts';
|
|
3
2
|
import { buildBncrDebugJsonMessage } from '../core/logging.ts';
|
|
4
3
|
import type { BncrRoute, OutboxEntry } from '../core/types.ts';
|
|
@@ -211,28 +210,22 @@ export function createBncrChannelSendRuntime(runtime: BncrChannelSendRuntime) {
|
|
|
211
210
|
const verified = runtime.resolveVerifiedTarget(to, accountId);
|
|
212
211
|
runtime.rememberSessionRoute(verified.sessionKey, accountId, verified.route);
|
|
213
212
|
|
|
214
|
-
await runtime
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
type,
|
|
226
|
-
extra,
|
|
227
|
-
kind: kind as 'tool' | 'block' | 'final' | undefined,
|
|
228
|
-
replyToId,
|
|
229
|
-
},
|
|
230
|
-
mediaLocalRoots: ctx.mediaLocalRoots,
|
|
213
|
+
const entry = await enqueueChannelMessageHandoff(runtime, ctx, {
|
|
214
|
+
text: text,
|
|
215
|
+
mediaUrl: mediaUrl || undefined,
|
|
216
|
+
mediaUrls: Array.isArray(mediaUrls) && mediaUrls.length ? mediaUrls : undefined,
|
|
217
|
+
asVoice: asVoice || undefined,
|
|
218
|
+
audioAsVoice: audioAsVoice || undefined,
|
|
219
|
+
downloadMedia,
|
|
220
|
+
type,
|
|
221
|
+
extra,
|
|
222
|
+
kind: kind as 'tool' | 'block' | 'final' | undefined,
|
|
223
|
+
replyToId,
|
|
231
224
|
});
|
|
232
225
|
|
|
233
226
|
return {
|
|
234
227
|
channel: runtime.channelId,
|
|
235
|
-
messageId:
|
|
228
|
+
messageId: entry.messageId,
|
|
236
229
|
chatId: verified.sessionKey,
|
|
237
230
|
};
|
|
238
231
|
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { GatewayRequestHandlerOptions } from 'openclaw/plugin-sdk/core';
|
|
2
|
+
|
|
3
|
+
export const BNCR_CLIENT_RPC_REQUEST_EVENT = 'plugin.bncr.rpc.request';
|
|
4
|
+
export const BNCR_CLIENT_RPC_RESPONSE_METHOD = 'bncr.rpc.response';
|
|
5
|
+
export const BNCR_CLIENT_RPC_TIMEOUT_MS = 15000;
|
|
6
|
+
|
|
7
|
+
type BncrClientRpcPending = {
|
|
8
|
+
method: string;
|
|
9
|
+
resolve: (result: Record<string, unknown>) => void;
|
|
10
|
+
reject: (error: Error) => void;
|
|
11
|
+
timer: NodeJS.Timeout;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type BncrClientRpcRuntime = {
|
|
15
|
+
call: (
|
|
16
|
+
method: string,
|
|
17
|
+
args: Record<string, unknown>,
|
|
18
|
+
accountId: string,
|
|
19
|
+
) => Promise<Record<string, unknown>>;
|
|
20
|
+
handleResponse: (ctx: GatewayRequestHandlerOptions) => void;
|
|
21
|
+
pendingCount: () => number;
|
|
22
|
+
shutdown: () => void;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function createBncrClientRpcRuntime(runtime: {
|
|
26
|
+
resolvePushConnIds: (accountId: string) => ReadonlySet<string>;
|
|
27
|
+
broadcastToConnIds: (event: string, payload: unknown, connIds: ReadonlySet<string>) => void;
|
|
28
|
+
now: () => number;
|
|
29
|
+
logInfo: (scope: string, message: string, options?: { debugOnly?: boolean }) => void;
|
|
30
|
+
timeoutMs?: number;
|
|
31
|
+
}): BncrClientRpcRuntime {
|
|
32
|
+
const pending = new Map<string, BncrClientRpcPending>();
|
|
33
|
+
let requestSeq = 0;
|
|
34
|
+
const timeoutMs = Math.max(Number(runtime.timeoutMs ?? BNCR_CLIENT_RPC_TIMEOUT_MS), 1);
|
|
35
|
+
|
|
36
|
+
const cleanup = (requestId: string) => {
|
|
37
|
+
const entry = pending.get(requestId);
|
|
38
|
+
if (!entry) return;
|
|
39
|
+
clearTimeout(entry.timer);
|
|
40
|
+
pending.delete(requestId);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const call = (method: string, args: Record<string, unknown>, accountId: string) => {
|
|
44
|
+
const normalizedMethod = String(method || '').trim();
|
|
45
|
+
if (!normalizedMethod) {
|
|
46
|
+
return Promise.reject(new Error('bncr client RPC requires a method name'));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const connIds = runtime.resolvePushConnIds(accountId);
|
|
50
|
+
if (!connIds.size) {
|
|
51
|
+
return Promise.reject(new Error(`no active bncr client connection for account ${accountId}`));
|
|
52
|
+
}
|
|
53
|
+
const targetConnIds = new Set([Array.from(connIds)[0]]);
|
|
54
|
+
|
|
55
|
+
const requestId = `rpc_${runtime.now().toString(36)}_${String(process.pid)}_${++requestSeq}`;
|
|
56
|
+
return new Promise<Record<string, unknown>>((resolve, reject) => {
|
|
57
|
+
const timer = setTimeout(() => {
|
|
58
|
+
cleanup(requestId);
|
|
59
|
+
reject(
|
|
60
|
+
new Error(
|
|
61
|
+
`bncr client RPC timeout method=${normalizedMethod}|accountId=${accountId}|timeoutMs=${timeoutMs}`,
|
|
62
|
+
),
|
|
63
|
+
);
|
|
64
|
+
}, timeoutMs);
|
|
65
|
+
pending.set(requestId, {
|
|
66
|
+
method: normalizedMethod,
|
|
67
|
+
resolve,
|
|
68
|
+
reject,
|
|
69
|
+
timer,
|
|
70
|
+
});
|
|
71
|
+
runtime.broadcastToConnIds(
|
|
72
|
+
BNCR_CLIENT_RPC_REQUEST_EVENT,
|
|
73
|
+
{
|
|
74
|
+
requestId,
|
|
75
|
+
method: normalizedMethod,
|
|
76
|
+
args: args ?? {},
|
|
77
|
+
},
|
|
78
|
+
targetConnIds,
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const handleResponse = (ctx: GatewayRequestHandlerOptions) => {
|
|
84
|
+
const params =
|
|
85
|
+
ctx.params && typeof ctx.params === 'object' ? (ctx.params as Record<string, unknown>) : {};
|
|
86
|
+
const requestId = typeof params.requestId === 'string' ? params.requestId.trim() : '';
|
|
87
|
+
const entry = requestId ? pending.get(requestId) : undefined;
|
|
88
|
+
if (!entry) {
|
|
89
|
+
runtime.logInfo('client-rpc', `rpc response ignored requestId=${requestId || '(missing)'}`, {
|
|
90
|
+
debugOnly: true,
|
|
91
|
+
});
|
|
92
|
+
ctx.respond(true, { ok: true, consumed: false, requestId });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
cleanup(requestId);
|
|
97
|
+
if (params.ok === true) {
|
|
98
|
+
const result =
|
|
99
|
+
params.result && typeof params.result === 'object'
|
|
100
|
+
? (params.result as Record<string, unknown>)
|
|
101
|
+
: { value: params.result };
|
|
102
|
+
entry.resolve({
|
|
103
|
+
ok: true,
|
|
104
|
+
method: entry.method,
|
|
105
|
+
result,
|
|
106
|
+
});
|
|
107
|
+
} else {
|
|
108
|
+
const message =
|
|
109
|
+
typeof params.error === 'string'
|
|
110
|
+
? params.error
|
|
111
|
+
: `bncr client RPC failed method=${entry.method}`;
|
|
112
|
+
entry.reject(new Error(message));
|
|
113
|
+
}
|
|
114
|
+
ctx.respond(true, { ok: true, consumed: true, requestId });
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
call,
|
|
119
|
+
handleResponse,
|
|
120
|
+
pendingCount: () => pending.size,
|
|
121
|
+
shutdown: () => {
|
|
122
|
+
for (const [requestId, entry] of pending.entries()) {
|
|
123
|
+
clearTimeout(entry.timer);
|
|
124
|
+
entry.reject(new Error(`bncr client RPC cancelled method=${entry.method}`));
|
|
125
|
+
pending.delete(requestId);
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -2,6 +2,7 @@ import type { GatewayRequestHandlerOptions } from 'openclaw/plugin-sdk/core';
|
|
|
2
2
|
import { buildCanonicalBncrSessionKey } from '../core/targets.ts';
|
|
3
3
|
import type { BncrConnection, BncrRoute } from '../core/types.ts';
|
|
4
4
|
import type { BncrGroupHistoryMap } from '../messaging/inbound/group-history.ts';
|
|
5
|
+
import type { BncrOutboundReplayCache } from '../messaging/inbound/outbound-replay-cache.ts';
|
|
5
6
|
import type { parseBncrInboundParams } from '../messaging/inbound/parse.ts';
|
|
6
7
|
import type { ReplyPayloadInput } from '../messaging/outbound/reply-enqueue.ts';
|
|
7
8
|
import type {
|
|
@@ -134,6 +135,7 @@ export type BncrInboundHandlersRuntime = {
|
|
|
134
135
|
defaultPublicAgentId: () => string;
|
|
135
136
|
sceneRegistry: Map<string, BncrSceneRecord>;
|
|
136
137
|
groupHistories: BncrGroupHistoryMap;
|
|
138
|
+
outboundReplayCache: BncrOutboundReplayCache;
|
|
137
139
|
prepareInboundAcceptance: (args: {
|
|
138
140
|
parsed: ParsedInboundParams;
|
|
139
141
|
canonicalAgentId: string;
|
|
@@ -164,6 +166,7 @@ export type BncrInboundHandlersRuntime = {
|
|
|
164
166
|
shouldAccumulate: boolean;
|
|
165
167
|
sceneRegistry: Map<string, BncrSceneRecord>;
|
|
166
168
|
groupHistories: BncrGroupHistoryMap;
|
|
169
|
+
outboundReplayCache: BncrOutboundReplayCache;
|
|
167
170
|
defaultAdminAgentId: string;
|
|
168
171
|
defaultPublicAgentId: string;
|
|
169
172
|
now: () => number;
|
|
@@ -333,6 +336,7 @@ export function createBncrInboundHandlers(runtime: Omit<BncrInboundHandlersRunti
|
|
|
333
336
|
shouldAccumulate,
|
|
334
337
|
sceneRegistry: runtime.sceneRegistry,
|
|
335
338
|
groupHistories: runtime.groupHistories,
|
|
339
|
+
outboundReplayCache: runtime.outboundReplayCache,
|
|
336
340
|
defaultAdminAgentId,
|
|
337
341
|
defaultPublicAgentId: runtime.defaultPublicAgentId(),
|
|
338
342
|
now: runtime.now,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { GatewayRequestHandlerOptions } from 'openclaw/plugin-sdk/core';
|
|
2
2
|
import type { BncrConnection, BncrRoute } from '../core/types.ts';
|
|
3
|
+
import type { BncrOutboundReplayCache } from '../messaging/inbound/outbound-replay-cache.ts';
|
|
3
4
|
import {
|
|
4
5
|
createBncrFileInboundHandlersComponent,
|
|
5
6
|
createBncrInboundHandlersComponent,
|
|
@@ -86,6 +87,7 @@ export function createBncrInboundSurfaceHandlersGroup(runtime: {
|
|
|
86
87
|
>[0]['defaultPublicAgentId'];
|
|
87
88
|
sceneRegistry: Parameters<typeof createBncrInboundHandlersComponent>[0]['sceneRegistry'];
|
|
88
89
|
groupHistories: Parameters<typeof createBncrInboundHandlersComponent>[0]['groupHistories'];
|
|
90
|
+
outboundReplayCache: BncrOutboundReplayCache;
|
|
89
91
|
prepareInboundAcceptance: Parameters<
|
|
90
92
|
typeof createBncrInboundHandlersComponent
|
|
91
93
|
>[0]['prepareInboundAcceptance'];
|
|
@@ -152,6 +154,7 @@ export function createBncrInboundSurfaceHandlersGroup(runtime: {
|
|
|
152
154
|
defaultPublicAgentId: runtime.defaultPublicAgentId,
|
|
153
155
|
sceneRegistry: runtime.sceneRegistry,
|
|
154
156
|
groupHistories: runtime.groupHistories,
|
|
157
|
+
outboundReplayCache: runtime.outboundReplayCache,
|
|
155
158
|
prepareInboundAcceptance: runtime.prepareInboundAcceptance,
|
|
156
159
|
logInboundSummary: runtime.logInboundSummary,
|
|
157
160
|
flushPushQueueBestEffort: runtime.flushPushQueueBestEffort,
|
|
@@ -26,6 +26,7 @@ export type BncrOutboxAckOutcomeRuntime = {
|
|
|
26
26
|
scheduleSave: () => void;
|
|
27
27
|
resolveMessageAck: (messageId: string, result: 'acked' | 'timeout') => void;
|
|
28
28
|
moveToDeadLetter: (entry: OutboxEntry, reason: string) => void;
|
|
29
|
+
markRecentOutboundAcked?: (entry: OutboxEntry) => void;
|
|
29
30
|
logOutboxAckSummary: (
|
|
30
31
|
scope: 'outbox ack ok' | 'outbox ack ok late' | 'outbox ack retry' | 'outbox ack fatal',
|
|
31
32
|
args: {
|
|
@@ -69,6 +70,7 @@ export function createBncrOutboxAckOutcome(runtime: BncrOutboxAckOutcomeRuntime)
|
|
|
69
70
|
runtime.deleteOutboxEntry(args.messageId);
|
|
70
71
|
runtime.scheduleSave();
|
|
71
72
|
runtime.resolveMessageAck(args.messageId, 'acked');
|
|
73
|
+
runtime.markRecentOutboundAcked?.(args.entry);
|
|
72
74
|
runtime.logOutboxAckSummary(
|
|
73
75
|
telemetryPatch.lateAccepted ? 'outbox ack ok late' : 'outbox ack ok',
|
|
74
76
|
{
|