@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
package/README.md
CHANGED
|
@@ -81,6 +81,13 @@ openclaw devices approve --latest
|
|
|
81
81
|
- 状态诊断
|
|
82
82
|
- 文件互传
|
|
83
83
|
|
|
84
|
+
### 0.6.1 新增说明
|
|
85
|
+
|
|
86
|
+
- `bncr.outbound.recent`:本地 bridge 方法,可直接查询最近的出站记录;该记录来自出站重放缓存,不额外维护一份独立最近出站存储。
|
|
87
|
+
- 出站重放缓存:最近出站消息会在下轮入站上下文回灌时带给 agent,用于让 agent 知道自己刚发过什么;查询和回灌共享同一份 `outboundReplayCache`。
|
|
88
|
+
- bridge 方法调用:`message` 工具支持传入 `bridgeMethod`,可调用插件本地方法(如 `bncr.status.*`、`bncr.outbound.recent`)或转发到 OpenClawClient RPC。
|
|
89
|
+
- unsend / delete:`message` 工具支持撤回/删除动作,按发送缓存映射回真实平台消息,不需要业务侧单独拼平台 message id。
|
|
90
|
+
|
|
84
91
|
补充口径:
|
|
85
92
|
|
|
86
93
|
- 入站会话按正式 scene / route 语义接入 OpenClaw,区分私聊与群聊会话。
|
|
@@ -285,6 +292,7 @@ npm pack
|
|
|
285
292
|
- `npm run typecheck`:执行 `tsconfig.typecheck.json`,确保类型边界与 OpenClaw surface 对齐
|
|
286
293
|
- `npm run selfcheck`:检查插件骨架是否完整,并验证关键 OpenClaw SDK subpath 可解析
|
|
287
294
|
- `npm run check-pack`:执行 `npm pack --dry-run --json`,确认发布包包含关键入口与 OpenClaw adapter 文件
|
|
295
|
+
- `tests/integrity/source-manifest.test.mjs`:确保 `selfcheck` / `check-pack` 的源文件清单与当前 `src/**/*.ts` 一致,新增源文件不会漏进发布清单
|
|
288
296
|
- `npm run format:check`:执行 Biome `format` 只读检查(不带 `--write`),防止格式漂移混进发布门禁
|
|
289
297
|
- `npm pack`:确认当前版本可正常打包
|
|
290
298
|
- `npm run check-register-drift -- --duration-sec 300 --interval-sec 15`:静置采样 `bncr.diagnostics`,观察 `registerCount / apiGeneration / postWarmupRegisterCount` 是否在 warmup 后继续增长
|
package/dist/index.js
CHANGED
|
@@ -626,6 +626,7 @@ function createBncrRegisterRuntime() {
|
|
|
626
626
|
"bncr.diagnostics": (bridge, opts) => bridge.handleDiagnostics(opts),
|
|
627
627
|
"bncr.deadLetter.inspect": (bridge, opts) => bridge.handleDeadLetterInspect(opts),
|
|
628
628
|
"bncr.deadLetter.prune": (bridge, opts) => bridge.handleDeadLetterPrune(opts),
|
|
629
|
+
"bncr.rpc.response": (bridge, opts) => bridge.handleRpcResponse(opts),
|
|
629
630
|
"bncr.file.init": (bridge, opts) => bridge.handleFileInit(opts),
|
|
630
631
|
"bncr.file.chunk": (bridge, opts) => bridge.handleFileChunk(opts),
|
|
631
632
|
"bncr.file.complete": (bridge, opts) => bridge.handleFileComplete(opts),
|
|
@@ -936,6 +937,7 @@ var plugin = {
|
|
|
936
937
|
ensureGatewayMethodRegistered(api, "bncr.diagnostics", debugLog);
|
|
937
938
|
ensureGatewayMethodRegistered(api, "bncr.deadLetter.inspect", debugLog);
|
|
938
939
|
ensureGatewayMethodRegistered(api, "bncr.deadLetter.prune", debugLog);
|
|
940
|
+
ensureGatewayMethodRegistered(api, "bncr.rpc.response", debugLog);
|
|
939
941
|
ensureGatewayMethodRegistered(api, "bncr.file.init", debugLog);
|
|
940
942
|
ensureGatewayMethodRegistered(api, "bncr.file.chunk", debugLog);
|
|
941
943
|
ensureGatewayMethodRegistered(api, "bncr.file.complete", debugLog);
|
package/index.ts
CHANGED
|
@@ -208,6 +208,7 @@ const plugin = {
|
|
|
208
208
|
ensureGatewayMethodRegistered(api, 'bncr.diagnostics', debugLog);
|
|
209
209
|
ensureGatewayMethodRegistered(api, 'bncr.deadLetter.inspect', debugLog);
|
|
210
210
|
ensureGatewayMethodRegistered(api, 'bncr.deadLetter.prune', debugLog);
|
|
211
|
+
ensureGatewayMethodRegistered(api, 'bncr.rpc.response', debugLog);
|
|
211
212
|
ensureGatewayMethodRegistered(api, 'bncr.file.init', debugLog);
|
|
212
213
|
ensureGatewayMethodRegistered(api, 'bncr.file.chunk', debugLog);
|
|
213
214
|
ensureGatewayMethodRegistered(api, 'bncr.file.complete', debugLog);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xmoxmo/bncr",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -62,5 +62,8 @@
|
|
|
62
62
|
"runtimeExtensions": [
|
|
63
63
|
"./dist/index.js"
|
|
64
64
|
]
|
|
65
|
+
},
|
|
66
|
+
"dependencies": {
|
|
67
|
+
"typebox": "^1.3.10"
|
|
65
68
|
}
|
|
66
69
|
}
|
package/scripts/check-pack.mjs
CHANGED
|
@@ -48,7 +48,6 @@ const requiredSourceFiles = [
|
|
|
48
48
|
'src/core/outbox-summary.ts',
|
|
49
49
|
'src/core/outbox-text-push-failure.ts',
|
|
50
50
|
'src/core/outbox-text-push-guards.ts',
|
|
51
|
-
'src/core/outbox-text-push-prep.ts',
|
|
52
51
|
'src/core/outbox-text-push-success.ts',
|
|
53
52
|
'src/core/permissions.ts',
|
|
54
53
|
'src/core/persisted-outbox-entry.ts',
|
|
@@ -68,6 +67,7 @@ const requiredSourceFiles = [
|
|
|
68
67
|
'src/messaging/inbound/media-url-download.ts',
|
|
69
68
|
'src/messaging/inbound/native-command.ts',
|
|
70
69
|
'src/messaging/inbound/native-reply-delivery.ts',
|
|
70
|
+
'src/messaging/inbound/outbound-replay-cache.ts',
|
|
71
71
|
'src/messaging/inbound/parse.ts',
|
|
72
72
|
'src/messaging/inbound/remote-media.ts',
|
|
73
73
|
'src/messaging/inbound/reply-config.ts',
|
|
@@ -109,6 +109,8 @@ const requiredSourceFiles = [
|
|
|
109
109
|
'src/plugin/channel-runtime-types.ts',
|
|
110
110
|
'src/plugin/channel-send.ts',
|
|
111
111
|
'src/plugin/channel-utils.ts',
|
|
112
|
+
'src/plugin/bridge-call.ts',
|
|
113
|
+
'src/plugin/client-rpc-runtime.ts',
|
|
112
114
|
'src/plugin/config.ts',
|
|
113
115
|
'src/plugin/connection-handlers.ts',
|
|
114
116
|
'src/plugin/connection-state.ts',
|
|
@@ -168,6 +170,44 @@ const requiredSourceFiles = [
|
|
|
168
170
|
'src/runtime/register-trace-runtime.ts',
|
|
169
171
|
'src/runtime/status-snapshots.ts',
|
|
170
172
|
'src/runtime/status-worker.ts',
|
|
173
|
+
'src/bootstrap/register-runtime-gateway.ts',
|
|
174
|
+
'src/bootstrap/register-runtime-singleton.ts',
|
|
175
|
+
'src/core/value-sanitize.ts',
|
|
176
|
+
'src/messaging/inbound/contracts.ts',
|
|
177
|
+
'src/messaging/inbound/group-history.ts',
|
|
178
|
+
'src/messaging/inbound/native-command-runtime.ts',
|
|
179
|
+
'src/messaging/inbound/reply-dispatch-serial.ts',
|
|
180
|
+
'src/messaging/inbound/scene-admin.ts',
|
|
181
|
+
'src/messaging/inbound/session-meta-task.ts',
|
|
182
|
+
'src/messaging/outbound/marker-parser.ts',
|
|
183
|
+
'src/messaging/outbound/normalize-outbound-send.ts',
|
|
184
|
+
'src/messaging/outbound/reply-enqueue-media.ts',
|
|
185
|
+
'src/openclaw/channel-runtime-contracts.ts',
|
|
186
|
+
'src/plugin/bridge-ack-facade.ts',
|
|
187
|
+
'src/plugin/bridge-connection-facade.ts',
|
|
188
|
+
'src/plugin/bridge-diagnostics-facade.ts',
|
|
189
|
+
'src/plugin/bridge-drain-facade.ts',
|
|
190
|
+
'src/plugin/bridge-extended-diagnostics-facade.ts',
|
|
191
|
+
'src/plugin/bridge-lifecycle.ts',
|
|
192
|
+
'src/plugin/bridge-media-facade.ts',
|
|
193
|
+
'src/plugin/bridge-outbox-facade.ts',
|
|
194
|
+
'src/plugin/bridge-runtime-helpers.ts',
|
|
195
|
+
'src/plugin/bridge-runtime-snapshots.ts',
|
|
196
|
+
'src/plugin/bridge-runtime-surface-facade.ts',
|
|
197
|
+
'src/plugin/bridge-status-facade.ts',
|
|
198
|
+
'src/plugin/bridge-status-worker-facade.ts',
|
|
199
|
+
'src/plugin/bridge-support-runtime.ts',
|
|
200
|
+
'src/plugin/channel-runtime-builders-delivery.ts',
|
|
201
|
+
'src/plugin/channel-runtime-builders-status.ts',
|
|
202
|
+
'src/plugin/connection-handlers-helpers.ts',
|
|
203
|
+
'src/plugin/connection-state-helpers.ts',
|
|
204
|
+
'src/plugin/error-message.ts',
|
|
205
|
+
'src/plugin/file-transfer-orchestrator-chunk.ts',
|
|
206
|
+
'src/plugin/outbox-unified-push-flow.ts',
|
|
207
|
+
'src/plugin/runtime-diagnostics-assembler.ts',
|
|
208
|
+
'src/plugin/runtime-diagnostics-helpers.ts',
|
|
209
|
+
'src/plugin/runtime-diagnostics-payload-builders.ts',
|
|
210
|
+
'src/plugin/scene-registry.ts',
|
|
171
211
|
];
|
|
172
212
|
|
|
173
213
|
const requiredPackFiles = [
|
package/scripts/selfcheck.mjs
CHANGED
|
@@ -69,6 +69,7 @@ const requiredSourceFiles = [
|
|
|
69
69
|
'src/messaging/inbound/native-command-runtime.ts',
|
|
70
70
|
'src/messaging/inbound/native-command.ts',
|
|
71
71
|
'src/messaging/inbound/native-reply-delivery.ts',
|
|
72
|
+
'src/messaging/inbound/outbound-replay-cache.ts',
|
|
72
73
|
'src/messaging/inbound/parse.ts',
|
|
73
74
|
'src/messaging/inbound/remote-media.ts',
|
|
74
75
|
'src/messaging/inbound/reply-config.ts',
|
|
@@ -106,6 +107,7 @@ const requiredSourceFiles = [
|
|
|
106
107
|
'src/openclaw/session-route-runtime.ts',
|
|
107
108
|
'src/plugin/ack-outbox-runtime-group.ts',
|
|
108
109
|
'src/plugin/bridge-ack-facade.ts',
|
|
110
|
+
'src/plugin/bridge-call.ts',
|
|
109
111
|
'src/plugin/bridge-connection-facade.ts',
|
|
110
112
|
'src/plugin/bridge-diagnostics-facade.ts',
|
|
111
113
|
'src/plugin/bridge-drain-facade.ts',
|
|
@@ -133,6 +135,7 @@ const requiredSourceFiles = [
|
|
|
133
135
|
'src/plugin/channel-runtime-types.ts',
|
|
134
136
|
'src/plugin/channel-send.ts',
|
|
135
137
|
'src/plugin/channel-utils.ts',
|
|
138
|
+
'src/plugin/client-rpc-runtime.ts',
|
|
136
139
|
'src/plugin/config.ts',
|
|
137
140
|
'src/plugin/connection-handlers-helpers.ts',
|
|
138
141
|
'src/plugin/connection-handlers.ts',
|
|
@@ -97,6 +97,7 @@ export function createBncrRegisterRuntime() {
|
|
|
97
97
|
'bncr.diagnostics': (bridge, opts) => bridge.handleDiagnostics(opts),
|
|
98
98
|
'bncr.deadLetter.inspect': (bridge, opts) => bridge.handleDeadLetterInspect(opts),
|
|
99
99
|
'bncr.deadLetter.prune': (bridge, opts) => bridge.handleDeadLetterPrune(opts),
|
|
100
|
+
'bncr.rpc.response': (bridge, opts) => bridge.handleRpcResponse(opts),
|
|
100
101
|
'bncr.file.init': (bridge, opts) => bridge.handleFileInit(opts),
|
|
101
102
|
'bncr.file.chunk': (bridge, opts) => bridge.handleFileChunk(opts),
|
|
102
103
|
'bncr.file.complete': (bridge, opts) => bridge.handleFileComplete(opts),
|
package/src/channel.ts
CHANGED
|
@@ -4,7 +4,12 @@ import type {
|
|
|
4
4
|
OpenClawPluginApi,
|
|
5
5
|
OpenClawPluginServiceContext,
|
|
6
6
|
} from 'openclaw/plugin-sdk/core';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
BNCR_DEFAULT_ACCOUNT_ID,
|
|
9
|
+
CHANNEL_ID,
|
|
10
|
+
normalizeAccountId,
|
|
11
|
+
resolveAccount,
|
|
12
|
+
} from './core/accounts.ts';
|
|
8
13
|
import {
|
|
9
14
|
countInvalidOutboxSessionKeys as countInvalidOutboxSessionKeysFromRuntime,
|
|
10
15
|
countLegacyAccountResidue as countLegacyAccountResidueFromRuntime,
|
|
@@ -39,6 +44,7 @@ import type {
|
|
|
39
44
|
OutboxEntry,
|
|
40
45
|
} from './core/types.ts';
|
|
41
46
|
import type { BncrGroupHistoryMap } from './messaging/inbound/group-history.ts';
|
|
47
|
+
import type { BncrOutboundReplayCache } from './messaging/inbound/outbound-replay-cache.ts';
|
|
42
48
|
import type { parseBncrInboundParams } from './messaging/inbound/parse.ts';
|
|
43
49
|
import { buildEnqueueFromReplyDebugInfo } from './messaging/outbound/diagnostics.ts';
|
|
44
50
|
import { buildBncrMediaOutboundFrame } from './messaging/outbound/media.ts';
|
|
@@ -160,6 +166,7 @@ import {
|
|
|
160
166
|
now,
|
|
161
167
|
resolveOutboundFileName,
|
|
162
168
|
} from './plugin/channel-utils.ts';
|
|
169
|
+
import { createBncrClientRpcRuntime } from './plugin/client-rpc-runtime.ts';
|
|
163
170
|
import { BNCR_CONFIG_SURFACE } from './plugin/config.ts';
|
|
164
171
|
import { createBncrConnectionStateRuntimeGroup } from './plugin/connection-state-runtime-group.ts';
|
|
165
172
|
import type { FileAckPayloadState } from './plugin/file-ack-runtime.ts';
|
|
@@ -287,6 +294,7 @@ class BncrBridgeRuntime {
|
|
|
287
294
|
return this.sceneRegistry;
|
|
288
295
|
}
|
|
289
296
|
private groupHistories: BncrGroupHistoryMap = new Map();
|
|
297
|
+
private outboundReplayCache: BncrOutboundReplayCache = new Map();
|
|
290
298
|
private routeAliases = new Map<
|
|
291
299
|
string,
|
|
292
300
|
{ accountId: string; route: BncrRoute; updatedAt: number }
|
|
@@ -373,6 +381,14 @@ class BncrBridgeRuntime {
|
|
|
373
381
|
>();
|
|
374
382
|
private earlyFileAcks = new Map<string, FileAckPayloadState>();
|
|
375
383
|
|
|
384
|
+
private readonly clientRpcRuntime = createBncrClientRpcRuntime({
|
|
385
|
+
resolvePushConnIds: (accountId) => this.resolvePushConnIds(accountId),
|
|
386
|
+
broadcastToConnIds: (event, payload, connIds) =>
|
|
387
|
+
this.gatewayContext?.broadcastToConnIds(event, payload, connIds),
|
|
388
|
+
now,
|
|
389
|
+
logInfo: (scope, message, options) => this.logInfo(scope, message, options),
|
|
390
|
+
});
|
|
391
|
+
|
|
376
392
|
private readonly bridgeSupportRuntime = createBridgeSupportRuntime({
|
|
377
393
|
isStopped: () => this.stopped,
|
|
378
394
|
hasSaveTimer: () => Boolean(this.saveTimer),
|
|
@@ -851,6 +867,7 @@ class BncrBridgeRuntime {
|
|
|
851
867
|
}
|
|
852
868
|
|
|
853
869
|
private cleanupRuntimeWaitersAndTimers(reason: string) {
|
|
870
|
+
this.clientRpcRuntime.shutdown();
|
|
854
871
|
cleanupBncrBridgeRuntime(
|
|
855
872
|
{
|
|
856
873
|
bridgeId: this.bridgeId,
|
|
@@ -1759,6 +1776,7 @@ class BncrBridgeRuntime {
|
|
|
1759
1776
|
setOutboxEntry: (messageId, entry) => this.outbox.set(messageId, entry),
|
|
1760
1777
|
resolveMessageAck: (messageId, result) => this.resolveMessageAck(messageId, result),
|
|
1761
1778
|
moveToDeadLetter: (entry, reason) => this.moveToDeadLetter(entry, reason),
|
|
1779
|
+
markRecentOutboundAcked: (entry) => this.markRecentOutboundAcked(entry),
|
|
1762
1780
|
recordAckTimeoutTelemetry: (accountId) => this.recordAckTimeoutTelemetry(accountId),
|
|
1763
1781
|
degradeOutboundCapability: (args) => this.degradeOutboundCapability(args),
|
|
1764
1782
|
flushPushQueueBestEffort: (args) => this.flushPushQueueBestEffort(args),
|
|
@@ -1884,6 +1902,7 @@ class BncrBridgeRuntime {
|
|
|
1884
1902
|
maxAccountActivityEntries: MAX_ACCOUNT_ACTIVITY_ENTRIES,
|
|
1885
1903
|
sceneRegistry: this.sceneRegistry,
|
|
1886
1904
|
groupHistories: this.groupHistories,
|
|
1905
|
+
outboundReplayCache: this.outboundReplayCache,
|
|
1887
1906
|
outbox: this.outbox,
|
|
1888
1907
|
getDeadLetter: () => this.deadLetter,
|
|
1889
1908
|
setDeadLetter: (entries) => {
|
|
@@ -2148,6 +2167,16 @@ class BncrBridgeRuntime {
|
|
|
2148
2167
|
lastPrePushGuardSkipReasonByAccount: this.lastPrePushGuardSkipReasonByAccount,
|
|
2149
2168
|
deadLetterSinceStartByAccount: this.deadLetterSinceStartByAccount,
|
|
2150
2169
|
lastOutboundByAccount: this.lastOutboundByAccount,
|
|
2170
|
+
outboundReplayCache: this.outboundReplayCache,
|
|
2171
|
+
resolveOutboundSender: (entry) => {
|
|
2172
|
+
try {
|
|
2173
|
+
const account = resolveAccount(getOpenClawRuntimeConfig(this.api), entry.accountId);
|
|
2174
|
+
return { sender: account.name, senderId: entry.accountId };
|
|
2175
|
+
} catch {
|
|
2176
|
+
return { sender: 'OpenClaw', senderId: entry.accountId };
|
|
2177
|
+
}
|
|
2178
|
+
},
|
|
2179
|
+
isOutboundAckRequired: (accountId) => this.isOutboundAckRequired(accountId),
|
|
2151
2180
|
scheduleSave: () => this.scheduleSave(),
|
|
2152
2181
|
flushPushQueueBestEffort: (args) => this.flushPushQueueBestEffort(args),
|
|
2153
2182
|
logInfo: (scope, message, options) => this.logInfo(scope, message, options),
|
|
@@ -2722,6 +2751,18 @@ class BncrBridgeRuntime {
|
|
|
2722
2751
|
return this.bridgeMediaFacade.enqueueFromReply(params);
|
|
2723
2752
|
}
|
|
2724
2753
|
|
|
2754
|
+
listRecentOutbound(sessionKey: string) {
|
|
2755
|
+
return this.bridgeOutboxFacade.listRecentOutbound(sessionKey);
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
listRecentOutboundByAccount(accountId: string) {
|
|
2759
|
+
return this.bridgeOutboxFacade.listRecentOutboundByAccount(accountId);
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
private markRecentOutboundAcked(entry: OutboxEntry) {
|
|
2763
|
+
this.bridgeOutboxFacade.markRecentOutboundAcked(entry);
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2725
2766
|
private logEnqueueFromReply(args: {
|
|
2726
2767
|
accountId: string;
|
|
2727
2768
|
sessionKey: string;
|
|
@@ -2853,6 +2894,7 @@ class BncrBridgeRuntime {
|
|
|
2853
2894
|
defaultPublicAgentId: () => this.defaultPublicAgentId(),
|
|
2854
2895
|
sceneRegistry: this.sceneRegistry,
|
|
2855
2896
|
groupHistories: this.groupHistories,
|
|
2897
|
+
outboundReplayCache: this.outboundReplayCache,
|
|
2856
2898
|
prepareInboundAcceptance: (args) => this.prepareInboundAcceptance(args),
|
|
2857
2899
|
logInboundSummary: (args) => this.logInboundSummary(args),
|
|
2858
2900
|
flushPushQueueBestEffort: (args) => this.flushPushQueueBestEffort(args),
|
|
@@ -2922,6 +2964,12 @@ class BncrBridgeRuntime {
|
|
|
2922
2964
|
handleDeadLetterPrune = async (ctx: GatewayRequestHandlerOptions) =>
|
|
2923
2965
|
this.diagnosticsHandlers.handleDeadLetterPrune(ctx);
|
|
2924
2966
|
|
|
2967
|
+
handleRpcResponse = async (ctx: GatewayRequestHandlerOptions) =>
|
|
2968
|
+
this.clientRpcRuntime.handleResponse(ctx);
|
|
2969
|
+
|
|
2970
|
+
callClientRpc = async (method: string, args: Record<string, unknown>, accountId: string) =>
|
|
2971
|
+
this.clientRpcRuntime.call(method, args, normalizeAccountId(accountId));
|
|
2972
|
+
|
|
2925
2973
|
handleFileInit = async (ctx: GatewayRequestHandlerOptions) =>
|
|
2926
2974
|
this.fileInboundHandlers.handleFileInit(ctx);
|
|
2927
2975
|
|
|
@@ -3024,6 +3072,7 @@ export function createBncrChannelPlugin(getBridge: () => BncrBridgeRuntime) {
|
|
|
3024
3072
|
getStatusBridge: bridgeGroup.getStatusBridge,
|
|
3025
3073
|
getToolActionBridge: bridgeGroup.getToolActionBridge,
|
|
3026
3074
|
getGatewayBridge: bridgeGroup.getGatewayBridge,
|
|
3075
|
+
getBridgeCallBridge: bridgeGroup.getBridgeCallBridge,
|
|
3027
3076
|
channelMeta: BNCR_CHANNEL_META,
|
|
3028
3077
|
channelCapabilities: BNCR_CHANNEL_CAPABILITIES,
|
|
3029
3078
|
gatewayMethods: BNCR_GATEWAY_METHODS,
|
package/src/core/types.ts
CHANGED
|
@@ -118,6 +118,47 @@ export type OutboxEntry = {
|
|
|
118
118
|
awaitingRetryPush?: boolean;
|
|
119
119
|
};
|
|
120
120
|
|
|
121
|
+
export type BncrRecentOutboundEntry = {
|
|
122
|
+
messageId: string;
|
|
123
|
+
accountId: string;
|
|
124
|
+
sessionKey: string;
|
|
125
|
+
route: BncrRoute;
|
|
126
|
+
type?: string;
|
|
127
|
+
kind?: string;
|
|
128
|
+
text: string;
|
|
129
|
+
mediaUrl?: string;
|
|
130
|
+
createdAt: number;
|
|
131
|
+
lastPushAt?: number;
|
|
132
|
+
lastError?: string;
|
|
133
|
+
ackedAt?: number;
|
|
134
|
+
status: 'queued' | 'pushed' | 'failed' | 'acked';
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export type BncrOutboundReplayMediaEntry = {
|
|
138
|
+
path?: string;
|
|
139
|
+
contentType?: string;
|
|
140
|
+
kind?: 'image' | 'video' | 'audio' | 'document' | 'unknown';
|
|
141
|
+
messageId?: string;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export type BncrOutboundReplayStatus = 'pushed' | 'acked';
|
|
145
|
+
|
|
146
|
+
export type BncrOutboundReplayEntry = {
|
|
147
|
+
sender: string;
|
|
148
|
+
senderId?: string;
|
|
149
|
+
body: string;
|
|
150
|
+
timestamp?: number;
|
|
151
|
+
messageId?: string;
|
|
152
|
+
media?: BncrOutboundReplayMediaEntry[];
|
|
153
|
+
accountId?: string;
|
|
154
|
+
sessionKey?: string;
|
|
155
|
+
route?: BncrRoute;
|
|
156
|
+
type?: string;
|
|
157
|
+
mediaUrl?: string;
|
|
158
|
+
createdAt?: number;
|
|
159
|
+
status?: BncrOutboundReplayStatus;
|
|
160
|
+
};
|
|
161
|
+
|
|
121
162
|
export type BncrDiagnosticsSummary = {
|
|
122
163
|
health: {
|
|
123
164
|
connected: boolean;
|
|
@@ -27,6 +27,12 @@ import {
|
|
|
27
27
|
recordBncrPendingGroupMedia,
|
|
28
28
|
recordBncrPendingGroupText,
|
|
29
29
|
} from './group-history.ts';
|
|
30
|
+
import {
|
|
31
|
+
type BncrOutboundReplayCache,
|
|
32
|
+
type BncrOutboundReplayEntry,
|
|
33
|
+
clearBncrOutboundReplay,
|
|
34
|
+
readBncrOutboundReplaySnapshot,
|
|
35
|
+
} from './outbound-replay-cache.ts';
|
|
30
36
|
import { runBncrInboundReplyDispatch } from './reply-dispatch.ts';
|
|
31
37
|
import { buildBncrInboundTurnContext } from './turn-context.ts';
|
|
32
38
|
|
|
@@ -48,6 +54,7 @@ export async function dispatchBncrInbound(params: {
|
|
|
48
54
|
resolvedAgentId?: string;
|
|
49
55
|
sceneRegistry: Map<string, BncrSceneRecord>;
|
|
50
56
|
groupHistories: BncrGroupHistoryMap;
|
|
57
|
+
outboundReplayCache?: BncrOutboundReplayCache;
|
|
51
58
|
defaultAdminAgentId: string;
|
|
52
59
|
defaultPublicAgentId: string;
|
|
53
60
|
now: () => number;
|
|
@@ -68,6 +75,7 @@ export async function dispatchBncrInbound(params: {
|
|
|
68
75
|
resolvedAgentId,
|
|
69
76
|
sceneRegistry = new Map(),
|
|
70
77
|
groupHistories = new Map(),
|
|
78
|
+
outboundReplayCache = new Map(),
|
|
71
79
|
defaultAdminAgentId,
|
|
72
80
|
defaultPublicAgentId,
|
|
73
81
|
now,
|
|
@@ -193,7 +201,7 @@ export async function dispatchBncrInbound(params: {
|
|
|
193
201
|
}
|
|
194
202
|
}
|
|
195
203
|
|
|
196
|
-
if (shouldDispatch && !silentHistoryFlush
|
|
204
|
+
if (shouldDispatch && !silentHistoryFlush) {
|
|
197
205
|
pendingHistoryEntries = readBncrPendingGroupHistorySnapshot({
|
|
198
206
|
historyMap: groupHistories,
|
|
199
207
|
parsed,
|
|
@@ -208,6 +216,21 @@ export async function dispatchBncrInbound(params: {
|
|
|
208
216
|
}
|
|
209
217
|
}
|
|
210
218
|
|
|
219
|
+
let outboundReplayEntries: BncrOutboundReplayEntry[] = [];
|
|
220
|
+
if (shouldDispatch && !silentHistoryFlush) {
|
|
221
|
+
outboundReplayEntries = readBncrOutboundReplaySnapshot({
|
|
222
|
+
cache: outboundReplayCache,
|
|
223
|
+
parsed,
|
|
224
|
+
accountId,
|
|
225
|
+
excludeMessageId: msgId,
|
|
226
|
+
});
|
|
227
|
+
clearBncrOutboundReplay({
|
|
228
|
+
cache: outboundReplayCache,
|
|
229
|
+
parsed,
|
|
230
|
+
accountId,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
211
234
|
const ctxPayload = await buildBncrInboundTurnContext({
|
|
212
235
|
api,
|
|
213
236
|
cfg,
|
|
@@ -227,6 +250,7 @@ export async function dispatchBncrInbound(params: {
|
|
|
227
250
|
shouldDispatch,
|
|
228
251
|
silentHistoryFlush,
|
|
229
252
|
pendingHistoryEntries,
|
|
253
|
+
outboundReplayEntries,
|
|
230
254
|
});
|
|
231
255
|
|
|
232
256
|
await runBncrInboundReplyDispatch({
|
|
@@ -250,13 +274,7 @@ export async function dispatchBncrInbound(params: {
|
|
|
250
274
|
enqueueFromReply,
|
|
251
275
|
});
|
|
252
276
|
|
|
253
|
-
|
|
254
|
-
clearBncrPendingGroupHistory({
|
|
255
|
-
historyMap: groupHistories,
|
|
256
|
-
parsed,
|
|
257
|
-
historyLimit: resolvedHistoryLimit,
|
|
258
|
-
});
|
|
259
|
-
}
|
|
277
|
+
scheduleSave();
|
|
260
278
|
|
|
261
279
|
return {
|
|
262
280
|
accountId,
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BncrOutboundReplayEntry,
|
|
3
|
+
BncrOutboundReplayMediaEntry,
|
|
4
|
+
BncrOutboundReplayStatus,
|
|
5
|
+
OutboxEntry,
|
|
6
|
+
} from '../../core/types.ts';
|
|
7
|
+
import type { ParsedInbound } from './dispatch-prep.ts';
|
|
8
|
+
|
|
9
|
+
export type {
|
|
10
|
+
BncrOutboundReplayEntry,
|
|
11
|
+
BncrOutboundReplayMediaEntry,
|
|
12
|
+
BncrOutboundReplayStatus,
|
|
13
|
+
} from '../../core/types.ts';
|
|
14
|
+
|
|
15
|
+
export type BncrOutboundReplayCache = Map<string, BncrOutboundReplayEntry[]>;
|
|
16
|
+
|
|
17
|
+
function normalizeTextBody(value: string): string {
|
|
18
|
+
return String(value || '')
|
|
19
|
+
.replace(/\s+/g, ' ')
|
|
20
|
+
.trim();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function cloneMediaEntry(entry: BncrOutboundReplayMediaEntry): BncrOutboundReplayMediaEntry {
|
|
24
|
+
return {
|
|
25
|
+
...(entry?.path ? { path: entry.path } : {}),
|
|
26
|
+
...(entry?.contentType ? { contentType: entry.contentType } : {}),
|
|
27
|
+
...(entry?.kind ? { kind: entry.kind } : {}),
|
|
28
|
+
...(entry?.messageId ? { messageId: entry.messageId } : {}),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function cloneBncrOutboundReplayEntry(
|
|
33
|
+
entry: BncrOutboundReplayEntry,
|
|
34
|
+
): BncrOutboundReplayEntry {
|
|
35
|
+
return {
|
|
36
|
+
sender: entry.sender,
|
|
37
|
+
...(entry.senderId ? { senderId: entry.senderId } : {}),
|
|
38
|
+
body: entry.body,
|
|
39
|
+
...(typeof entry.timestamp === 'number' ? { timestamp: entry.timestamp } : {}),
|
|
40
|
+
...(entry.messageId ? { messageId: entry.messageId } : {}),
|
|
41
|
+
...(Array.isArray(entry.media)
|
|
42
|
+
? { media: entry.media.map((item) => cloneMediaEntry(item)) }
|
|
43
|
+
: {}),
|
|
44
|
+
...(entry.accountId ? { accountId: entry.accountId } : {}),
|
|
45
|
+
...(entry.sessionKey ? { sessionKey: entry.sessionKey } : {}),
|
|
46
|
+
...(entry.route ? { route: { ...entry.route } } : {}),
|
|
47
|
+
...(entry.type ? { type: entry.type } : {}),
|
|
48
|
+
...(entry.mediaUrl ? { mediaUrl: entry.mediaUrl } : {}),
|
|
49
|
+
...(typeof entry.createdAt === 'number' ? { createdAt: entry.createdAt } : {}),
|
|
50
|
+
...(entry.status ? { status: entry.status } : {}),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function buildBncrOutboundReplayKeyFromRoute(args: {
|
|
55
|
+
accountId?: string;
|
|
56
|
+
platform?: string;
|
|
57
|
+
groupId?: string;
|
|
58
|
+
userId?: string;
|
|
59
|
+
}): string | null {
|
|
60
|
+
const accountId = String(args.accountId || '').trim() || 'default';
|
|
61
|
+
const platform = String(args.platform || '').trim();
|
|
62
|
+
const groupId = String(args.groupId || '0').trim() || '0';
|
|
63
|
+
const userId = String(args.userId || '0').trim() || '0';
|
|
64
|
+
if (!platform) return null;
|
|
65
|
+
if (groupId !== '0') return `${accountId}:${platform}:${groupId}`;
|
|
66
|
+
if (userId !== '0') return `${accountId}:${platform}:${userId}`;
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function buildBncrOutboundReplayKey(
|
|
71
|
+
parsed: ParsedInbound,
|
|
72
|
+
accountId: string,
|
|
73
|
+
): string | null {
|
|
74
|
+
return buildBncrOutboundReplayKeyFromRoute({
|
|
75
|
+
accountId,
|
|
76
|
+
platform: parsed.platform,
|
|
77
|
+
groupId: parsed.groupId,
|
|
78
|
+
userId: parsed.userId,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function recordOutboundReplayEntry(args: {
|
|
83
|
+
cache: BncrOutboundReplayCache;
|
|
84
|
+
cacheKey: string;
|
|
85
|
+
entry: BncrOutboundReplayEntry;
|
|
86
|
+
}) {
|
|
87
|
+
const msgId = String(args.entry.messageId || '').trim();
|
|
88
|
+
const existing = args.cache.get(args.cacheKey) || [];
|
|
89
|
+
if (msgId && existing.some((entry) => entry.messageId === msgId)) return;
|
|
90
|
+
|
|
91
|
+
const next = [...existing, cloneBncrOutboundReplayEntry(args.entry)].sort(
|
|
92
|
+
(a, b) => (a.timestamp ?? 0) - (b.timestamp ?? 0),
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
if (args.cache.has(args.cacheKey)) {
|
|
96
|
+
args.cache.delete(args.cacheKey);
|
|
97
|
+
}
|
|
98
|
+
args.cache.set(args.cacheKey, next);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function inferMediaKind(args: {
|
|
102
|
+
msgType?: string;
|
|
103
|
+
contentType?: string;
|
|
104
|
+
mediaUrl?: string;
|
|
105
|
+
type?: string;
|
|
106
|
+
}): NonNullable<BncrOutboundReplayMediaEntry['kind']> {
|
|
107
|
+
const msgType = String(args.msgType || '')
|
|
108
|
+
.trim()
|
|
109
|
+
.toLowerCase();
|
|
110
|
+
const type = String(args.type || '')
|
|
111
|
+
.trim()
|
|
112
|
+
.toLowerCase();
|
|
113
|
+
const contentType = String(args.contentType || '')
|
|
114
|
+
.trim()
|
|
115
|
+
.toLowerCase();
|
|
116
|
+
const mediaUrl = String(args.mediaUrl || '')
|
|
117
|
+
.trim()
|
|
118
|
+
.toLowerCase();
|
|
119
|
+
|
|
120
|
+
if (msgType === 'image' || type === 'image' || contentType.startsWith('image/')) return 'image';
|
|
121
|
+
if (msgType === 'video' || type === 'video' || contentType.startsWith('video/')) return 'video';
|
|
122
|
+
if (
|
|
123
|
+
msgType === 'audio' ||
|
|
124
|
+
msgType === 'voice' ||
|
|
125
|
+
type === 'audio' ||
|
|
126
|
+
type === 'voice' ||
|
|
127
|
+
contentType.startsWith('audio/')
|
|
128
|
+
) {
|
|
129
|
+
return 'audio';
|
|
130
|
+
}
|
|
131
|
+
if (type === 'file' || type === 'document' || msgType === 'file' || msgType === 'document') {
|
|
132
|
+
return 'document';
|
|
133
|
+
}
|
|
134
|
+
if (/\.(png|jpe?g|gif|webp|bmp|svg)$/.test(mediaUrl)) return 'image';
|
|
135
|
+
if (/\.(mp4|mov|webm|mkv|avi)$/.test(mediaUrl)) return 'video';
|
|
136
|
+
if (/\.(mp3|ogg|oga|silk|amr|wav|m4a|aac|flac)$/.test(mediaUrl)) return 'audio';
|
|
137
|
+
if (contentType) return 'document';
|
|
138
|
+
return 'unknown';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function buildMediaBody(kind: NonNullable<BncrOutboundReplayMediaEntry['kind']>): string {
|
|
142
|
+
return `<media:${kind}>`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function isAppMsg(type: string | undefined, msg: string): boolean {
|
|
146
|
+
return (
|
|
147
|
+
String(type || '')
|
|
148
|
+
.trim()
|
|
149
|
+
.toLowerCase() === 'appmsg' || /^<appmsg/i.test(msg)
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function recordBncrOutboundReplay(args: {
|
|
154
|
+
cache: BncrOutboundReplayCache;
|
|
155
|
+
entry: OutboxEntry;
|
|
156
|
+
sender: string;
|
|
157
|
+
senderId?: string;
|
|
158
|
+
status?: BncrOutboundReplayStatus;
|
|
159
|
+
}) {
|
|
160
|
+
const cacheKey = buildBncrOutboundReplayKeyFromRoute({
|
|
161
|
+
accountId: args.entry.accountId,
|
|
162
|
+
platform: args.entry.route?.platform,
|
|
163
|
+
groupId: args.entry.route?.groupId,
|
|
164
|
+
userId: args.entry.route?.userId,
|
|
165
|
+
});
|
|
166
|
+
if (!cacheKey) return;
|
|
167
|
+
|
|
168
|
+
const message =
|
|
169
|
+
args.entry.payload?.message && typeof args.entry.payload.message === 'object'
|
|
170
|
+
? (args.entry.payload.message as Record<string, unknown>)
|
|
171
|
+
: {};
|
|
172
|
+
const type = typeof message.type === 'string' ? message.type : undefined;
|
|
173
|
+
const rawBody = typeof message.msg === 'string' ? message.msg : '';
|
|
174
|
+
const mediaUrl = typeof message.mediaUrl === 'string' ? message.mediaUrl.trim() : '';
|
|
175
|
+
const kind = inferMediaKind({ type, mediaUrl });
|
|
176
|
+
const isMedia =
|
|
177
|
+
message.transferMode === 'media' || Boolean(mediaUrl) || (!!type && type !== 'text');
|
|
178
|
+
const body = isAppMsg(type, rawBody)
|
|
179
|
+
? buildMediaBody('document')
|
|
180
|
+
: normalizeTextBody(rawBody) || (isMedia ? buildMediaBody(kind) : '');
|
|
181
|
+
if (!body) return;
|
|
182
|
+
|
|
183
|
+
recordOutboundReplayEntry({
|
|
184
|
+
cache: args.cache,
|
|
185
|
+
cacheKey,
|
|
186
|
+
entry: {
|
|
187
|
+
sender: args.sender,
|
|
188
|
+
...(args.senderId ? { senderId: args.senderId } : {}),
|
|
189
|
+
body,
|
|
190
|
+
timestamp: Date.now(),
|
|
191
|
+
messageId: args.entry.messageId,
|
|
192
|
+
accountId: args.entry.accountId,
|
|
193
|
+
sessionKey: args.entry.sessionKey,
|
|
194
|
+
route: { ...args.entry.route },
|
|
195
|
+
...(type ? { type } : {}),
|
|
196
|
+
...(mediaUrl ? { mediaUrl } : {}),
|
|
197
|
+
createdAt: args.entry.createdAt,
|
|
198
|
+
...(args.status ? { status: args.status } : {}),
|
|
199
|
+
...(mediaUrl
|
|
200
|
+
? {
|
|
201
|
+
media: [
|
|
202
|
+
{
|
|
203
|
+
path: mediaUrl,
|
|
204
|
+
kind,
|
|
205
|
+
messageId: args.entry.messageId,
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
}
|
|
209
|
+
: {}),
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function readBncrOutboundReplaySnapshot(args: {
|
|
215
|
+
cache: BncrOutboundReplayCache;
|
|
216
|
+
parsed: ParsedInbound;
|
|
217
|
+
accountId: string;
|
|
218
|
+
excludeMessageId?: string | null;
|
|
219
|
+
}): BncrOutboundReplayEntry[] {
|
|
220
|
+
const cacheKey = buildBncrOutboundReplayKey(args.parsed, args.accountId);
|
|
221
|
+
if (!cacheKey) return [];
|
|
222
|
+
const excludedMessageId = String(args.excludeMessageId || '').trim();
|
|
223
|
+
return (args.cache.get(cacheKey) || [])
|
|
224
|
+
.filter((entry) => !excludedMessageId || entry.messageId !== excludedMessageId)
|
|
225
|
+
.map(cloneBncrOutboundReplayEntry);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function clearBncrOutboundReplay(args: {
|
|
229
|
+
cache: BncrOutboundReplayCache;
|
|
230
|
+
parsed: ParsedInbound;
|
|
231
|
+
accountId: string;
|
|
232
|
+
}) {
|
|
233
|
+
const cacheKey = buildBncrOutboundReplayKey(args.parsed, args.accountId);
|
|
234
|
+
if (!cacheKey) return;
|
|
235
|
+
args.cache.delete(cacheKey);
|
|
236
|
+
}
|