@xmoxmo/bncr 0.1.3 → 0.1.5
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/index.ts
CHANGED
|
@@ -59,6 +59,18 @@ const readOpenClawPackageName = (pkgPath: string) => {
|
|
|
59
59
|
}
|
|
60
60
|
};
|
|
61
61
|
|
|
62
|
+
const readPluginVersion = () => {
|
|
63
|
+
try {
|
|
64
|
+
const raw = fs.readFileSync(path.join(pluginDir, 'package.json'), 'utf8');
|
|
65
|
+
const parsed = JSON.parse(raw);
|
|
66
|
+
return typeof parsed?.version === 'string' ? parsed.version : 'unknown';
|
|
67
|
+
} catch {
|
|
68
|
+
return 'unknown';
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const pluginVersion = readPluginVersion();
|
|
73
|
+
|
|
62
74
|
const findOpenClawPackageRoot = (startPath: string) => {
|
|
63
75
|
let current = startPath;
|
|
64
76
|
try {
|
|
@@ -273,7 +285,7 @@ const plugin = {
|
|
|
273
285
|
const { bridge, runtime, created } = getBridgeSingleton(api);
|
|
274
286
|
bridge.noteRegister?.({
|
|
275
287
|
source: '~/.openclaw/workspace/plugins/bncr/index.ts',
|
|
276
|
-
pluginVersion
|
|
288
|
+
pluginVersion,
|
|
277
289
|
apiRebound: !created,
|
|
278
290
|
apiInstanceId: meta.apiInstanceId,
|
|
279
291
|
registryFingerprint: meta.registryFingerprint,
|
package/package.json
CHANGED
package/src/channel.ts
CHANGED
|
@@ -70,6 +70,7 @@ const BNCR_PUSH_EVENT = 'bncr.push';
|
|
|
70
70
|
const CONNECT_TTL_MS = 120_000;
|
|
71
71
|
const MAX_RETRY = 10;
|
|
72
72
|
const PUSH_DRAIN_INTERVAL_MS = 500;
|
|
73
|
+
const PUSH_ACK_TIMEOUT_MS = 30_000;
|
|
73
74
|
const FILE_FORCE_CHUNK = true; // 统一走 WS 分块,保留 base64 仅作兜底
|
|
74
75
|
const FILE_INLINE_THRESHOLD = 5 * 1024 * 1024; // fallback 阈值(仅 FILE_FORCE_CHUNK=false 时生效)
|
|
75
76
|
const FILE_CHUNK_SIZE = 256 * 1024; // 256KB
|
|
@@ -1179,7 +1180,6 @@ class BncrBridgeRuntime {
|
|
|
1179
1180
|
})}`,
|
|
1180
1181
|
);
|
|
1181
1182
|
}
|
|
1182
|
-
this.outbox.delete(entry.messageId);
|
|
1183
1183
|
this.lastOutboundByAccount.set(entry.accountId, now());
|
|
1184
1184
|
this.markActivity(entry.accountId);
|
|
1185
1185
|
this.scheduleSave();
|
|
@@ -1249,92 +1249,6 @@ class BncrBridgeRuntime {
|
|
|
1249
1249
|
})}`,
|
|
1250
1250
|
);
|
|
1251
1251
|
}
|
|
1252
|
-
if (!online) {
|
|
1253
|
-
const ctx = this.gatewayContext;
|
|
1254
|
-
const directConnIds = Array.from(this.connections.values())
|
|
1255
|
-
.filter((c) => normalizeAccountId(c.accountId) === acc && c.connId)
|
|
1256
|
-
.map((c) => c.connId as string);
|
|
1257
|
-
|
|
1258
|
-
if (BNCR_DEBUG_VERBOSE) {
|
|
1259
|
-
this.api.logger.info?.(
|
|
1260
|
-
`[bncr-outbox-direct-push] ${JSON.stringify({
|
|
1261
|
-
bridge: this.bridgeId,
|
|
1262
|
-
accountId: acc,
|
|
1263
|
-
outboxSize: this.outbox.size,
|
|
1264
|
-
hasGatewayContext: Boolean(ctx),
|
|
1265
|
-
connCount: directConnIds.length,
|
|
1266
|
-
})}`,
|
|
1267
|
-
);
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
if (!ctx) {
|
|
1271
|
-
if (BNCR_DEBUG_VERBOSE) {
|
|
1272
|
-
this.api.logger.info?.(
|
|
1273
|
-
`[bncr-outbox-direct-push-skip] ${JSON.stringify({
|
|
1274
|
-
bridge: this.bridgeId,
|
|
1275
|
-
accountId: acc,
|
|
1276
|
-
reason: 'no-gateway-context',
|
|
1277
|
-
})}`,
|
|
1278
|
-
);
|
|
1279
|
-
}
|
|
1280
|
-
continue;
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
if (!directConnIds.length) {
|
|
1284
|
-
if (BNCR_DEBUG_VERBOSE) {
|
|
1285
|
-
this.api.logger.info?.(
|
|
1286
|
-
`[bncr-outbox-direct-push-skip] ${JSON.stringify({
|
|
1287
|
-
accountId: acc,
|
|
1288
|
-
reason: 'no-connection',
|
|
1289
|
-
})}`,
|
|
1290
|
-
);
|
|
1291
|
-
}
|
|
1292
|
-
continue;
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
const directPayloads = this.collectDue(acc, 50);
|
|
1296
|
-
if (!directPayloads.length) continue;
|
|
1297
|
-
|
|
1298
|
-
try {
|
|
1299
|
-
ctx.broadcastToConnIds(
|
|
1300
|
-
BNCR_PUSH_EVENT,
|
|
1301
|
-
{
|
|
1302
|
-
forcePush: true,
|
|
1303
|
-
items: directPayloads,
|
|
1304
|
-
},
|
|
1305
|
-
new Set(directConnIds),
|
|
1306
|
-
);
|
|
1307
|
-
|
|
1308
|
-
const pushedIds = directPayloads
|
|
1309
|
-
.map((item: any) => asString(item?.messageId || item?.idempotencyKey || '').trim())
|
|
1310
|
-
.filter(Boolean);
|
|
1311
|
-
for (const id of pushedIds) this.outbox.delete(id);
|
|
1312
|
-
if (pushedIds.length) this.scheduleSave();
|
|
1313
|
-
|
|
1314
|
-
if (BNCR_DEBUG_VERBOSE) {
|
|
1315
|
-
this.api.logger.info?.(
|
|
1316
|
-
`[bncr-outbox-direct-push-ok] ${JSON.stringify({
|
|
1317
|
-
bridge: this.bridgeId,
|
|
1318
|
-
accountId: acc,
|
|
1319
|
-
count: directPayloads.length,
|
|
1320
|
-
connCount: directConnIds.length,
|
|
1321
|
-
dropped: pushedIds.length,
|
|
1322
|
-
})}`,
|
|
1323
|
-
);
|
|
1324
|
-
}
|
|
1325
|
-
} catch (error) {
|
|
1326
|
-
if (BNCR_DEBUG_VERBOSE) {
|
|
1327
|
-
this.api.logger.info?.(
|
|
1328
|
-
`[bncr-outbox-direct-push-fail] ${JSON.stringify({
|
|
1329
|
-
accountId: acc,
|
|
1330
|
-
error: asString((error as any)?.message || error || 'direct-push-error'),
|
|
1331
|
-
})}`,
|
|
1332
|
-
);
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
continue;
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
1252
|
this.pushDrainRunningAccounts.add(acc);
|
|
1339
1253
|
try {
|
|
1340
1254
|
let localNextDelay: number | null = null;
|
|
@@ -1346,7 +1260,6 @@ class BncrBridgeRuntime {
|
|
|
1346
1260
|
.sort((a, b) => a.createdAt - b.createdAt);
|
|
1347
1261
|
|
|
1348
1262
|
if (!entries.length) break;
|
|
1349
|
-
if (!this.isOnline(acc)) break;
|
|
1350
1263
|
|
|
1351
1264
|
const entry = entries.find((item) => item.nextAttemptAt <= t);
|
|
1352
1265
|
if (!entry) {
|
|
@@ -1355,11 +1268,33 @@ class BncrBridgeRuntime {
|
|
|
1355
1268
|
break;
|
|
1356
1269
|
}
|
|
1357
1270
|
|
|
1271
|
+
const onlineNow = this.isOnline(acc);
|
|
1358
1272
|
const pushed = this.tryPushEntry(entry);
|
|
1359
1273
|
if (pushed) {
|
|
1274
|
+
if (onlineNow) {
|
|
1275
|
+
await this.waitForOutbound(acc, PUSH_ACK_TIMEOUT_MS);
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
if (!this.outbox.has(entry.messageId)) {
|
|
1279
|
+
await this.sleepMs(PUSH_DRAIN_INTERVAL_MS);
|
|
1280
|
+
continue;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
entry.retryCount += 1;
|
|
1284
|
+
entry.lastAttemptAt = now();
|
|
1285
|
+
if (entry.retryCount > MAX_RETRY) {
|
|
1286
|
+
this.moveToDeadLetter(entry, entry.lastError || 'push-ack-timeout');
|
|
1287
|
+
continue;
|
|
1288
|
+
}
|
|
1289
|
+
entry.nextAttemptAt = now() + backoffMs(entry.retryCount);
|
|
1290
|
+
entry.lastError = entry.lastError || 'push-ack-timeout';
|
|
1291
|
+
this.outbox.set(entry.messageId, entry);
|
|
1360
1292
|
this.scheduleSave();
|
|
1293
|
+
|
|
1294
|
+
const wait = Math.max(0, entry.nextAttemptAt - now());
|
|
1295
|
+
localNextDelay = localNextDelay == null ? wait : Math.min(localNextDelay, wait);
|
|
1361
1296
|
await this.sleepMs(PUSH_DRAIN_INTERVAL_MS);
|
|
1362
|
-
|
|
1297
|
+
break;
|
|
1363
1298
|
}
|
|
1364
1299
|
|
|
1365
1300
|
const nextAttempt = entry.retryCount + 1;
|
|
@@ -2230,7 +2165,7 @@ class BncrBridgeRuntime {
|
|
|
2230
2165
|
mediaUrls?: string[];
|
|
2231
2166
|
asVoice?: boolean;
|
|
2232
2167
|
audioAsVoice?: boolean;
|
|
2233
|
-
kind?: 'block' | 'final';
|
|
2168
|
+
kind?: 'tool' | 'block' | 'final';
|
|
2234
2169
|
};
|
|
2235
2170
|
mediaLocalRoots?: readonly string[];
|
|
2236
2171
|
}) {
|
|
@@ -2413,6 +2348,7 @@ class BncrBridgeRuntime {
|
|
|
2413
2348
|
if (ok) {
|
|
2414
2349
|
this.outbox.delete(messageId);
|
|
2415
2350
|
this.scheduleSave();
|
|
2351
|
+
this.wakeAccountWaiters(accountId);
|
|
2416
2352
|
respond(true, { ok: true });
|
|
2417
2353
|
return;
|
|
2418
2354
|
}
|
|
@@ -3189,7 +3125,6 @@ export function createBncrChannelPlugin(bridge: BncrBridgeRuntime) {
|
|
|
3189
3125
|
},
|
|
3190
3126
|
outbound: {
|
|
3191
3127
|
deliveryMode: 'gateway' as const,
|
|
3192
|
-
textChunkLimit: 4000,
|
|
3193
3128
|
sendText: bridge.channelSendText,
|
|
3194
3129
|
sendMedia: bridge.channelSendMedia,
|
|
3195
3130
|
replyAction: async (ctx: any) =>
|
|
@@ -31,6 +31,12 @@ export const BncrConfigSchema = {
|
|
|
31
31
|
},
|
|
32
32
|
},
|
|
33
33
|
},
|
|
34
|
+
allowTool: {
|
|
35
|
+
type: 'boolean',
|
|
36
|
+
default: false,
|
|
37
|
+
description:
|
|
38
|
+
'Allow tool messages to be forwarded when streaming is enabled. Defaults to false; only explicit true enables forwarding. When enabled, bncr also requests upstream tool summaries/results.',
|
|
39
|
+
},
|
|
34
40
|
requireMention: {
|
|
35
41
|
type: 'boolean',
|
|
36
42
|
default: false,
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
normalizeInboundSessionKey,
|
|
4
4
|
withTaskSessionKey,
|
|
5
5
|
} from '../../core/targets.ts';
|
|
6
|
+
import { buildBncrReplyConfig } from './reply-config.ts';
|
|
6
7
|
|
|
7
8
|
type ParsedInbound = ReturnType<typeof import('./parse.ts')['parseBncrInboundParams']>;
|
|
8
9
|
|
|
@@ -122,20 +123,24 @@ export async function handleBncrNativeCommand(params: {
|
|
|
122
123
|
},
|
|
123
124
|
});
|
|
124
125
|
|
|
126
|
+
const effectiveReply = buildBncrReplyConfig(cfg);
|
|
127
|
+
|
|
125
128
|
let responded = false;
|
|
126
129
|
await api.runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
127
130
|
ctx: ctxPayload,
|
|
128
|
-
cfg,
|
|
129
|
-
replyOptions: {
|
|
130
|
-
disableBlockStreaming: true,
|
|
131
|
-
},
|
|
131
|
+
cfg: effectiveReply.replyCfg,
|
|
132
132
|
dispatcherOptions: {
|
|
133
133
|
deliver: async (
|
|
134
134
|
payload: { text?: string; mediaUrl?: string; mediaUrls?: string[]; audioAsVoice?: boolean },
|
|
135
135
|
info?: { kind?: 'tool' | 'block' | 'final' },
|
|
136
136
|
) => {
|
|
137
137
|
const kind = info?.kind;
|
|
138
|
-
|
|
138
|
+
const shouldForwardTool = effectiveReply.blockStreaming && effectiveReply.allowTool;
|
|
139
|
+
|
|
140
|
+
if (kind === 'tool' && !shouldForwardTool) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
139
144
|
const hasPayload = Boolean(
|
|
140
145
|
payload?.text ||
|
|
141
146
|
payload?.mediaUrl ||
|
|
@@ -149,11 +154,15 @@ export async function handleBncrNativeCommand(params: {
|
|
|
149
154
|
route,
|
|
150
155
|
payload: {
|
|
151
156
|
...payload,
|
|
152
|
-
kind: kind as 'block' | 'final' | undefined,
|
|
157
|
+
kind: kind as 'tool' | 'block' | 'final' | undefined,
|
|
153
158
|
},
|
|
154
159
|
});
|
|
155
160
|
},
|
|
156
161
|
},
|
|
162
|
+
replyOptions: {
|
|
163
|
+
disableBlockStreaming: !effectiveReply.blockStreaming,
|
|
164
|
+
shouldEmitToolResult: effectiveReply.allowTool ? () => true : undefined,
|
|
165
|
+
},
|
|
157
166
|
});
|
|
158
167
|
|
|
159
168
|
if (!responded) {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
1
|
import {
|
|
3
2
|
formatDisplayScope,
|
|
4
3
|
normalizeInboundSessionKey,
|
|
5
4
|
withTaskSessionKey,
|
|
6
5
|
} from '../../core/targets.ts';
|
|
7
6
|
import { handleBncrNativeCommand } from './commands.ts';
|
|
7
|
+
import { buildBncrReplyConfig } from './reply-config.ts';
|
|
8
8
|
|
|
9
9
|
type ParsedInbound = ReturnType<typeof import('./parse.ts')['parseBncrInboundParams']>;
|
|
10
10
|
|
|
@@ -179,19 +179,22 @@ export async function dispatchBncrInbound(params: {
|
|
|
179
179
|
setInboundActivity(accountId, inboundAt);
|
|
180
180
|
scheduleSave();
|
|
181
181
|
|
|
182
|
+
const effectiveReply = buildBncrReplyConfig(cfg);
|
|
183
|
+
|
|
182
184
|
await api.runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
183
185
|
ctx: ctxPayload,
|
|
184
|
-
cfg,
|
|
185
|
-
replyOptions: {
|
|
186
|
-
disableBlockStreaming: true,
|
|
187
|
-
},
|
|
186
|
+
cfg: effectiveReply.replyCfg,
|
|
188
187
|
dispatcherOptions: {
|
|
189
188
|
deliver: async (
|
|
190
189
|
payload: { text?: string; mediaUrl?: string; mediaUrls?: string[]; audioAsVoice?: boolean },
|
|
191
190
|
info?: { kind?: 'tool' | 'block' | 'final' },
|
|
192
191
|
) => {
|
|
193
192
|
const kind = info?.kind;
|
|
194
|
-
|
|
193
|
+
const shouldForwardTool = effectiveReply.blockStreaming && effectiveReply.allowTool;
|
|
194
|
+
|
|
195
|
+
if (kind === 'tool' && !shouldForwardTool) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
195
198
|
|
|
196
199
|
await enqueueFromReply({
|
|
197
200
|
accountId,
|
|
@@ -199,7 +202,7 @@ export async function dispatchBncrInbound(params: {
|
|
|
199
202
|
route,
|
|
200
203
|
payload: {
|
|
201
204
|
...payload,
|
|
202
|
-
kind: kind as 'block' | 'final' | undefined,
|
|
205
|
+
kind: kind as 'tool' | 'block' | 'final' | undefined,
|
|
203
206
|
},
|
|
204
207
|
});
|
|
205
208
|
},
|
|
@@ -207,6 +210,10 @@ export async function dispatchBncrInbound(params: {
|
|
|
207
210
|
logger?.error?.(`bncr reply failed: ${String(err)}`);
|
|
208
211
|
},
|
|
209
212
|
},
|
|
213
|
+
replyOptions: {
|
|
214
|
+
disableBlockStreaming: !effectiveReply.blockStreaming,
|
|
215
|
+
shouldEmitToolResult: effectiveReply.allowTool ? () => true : undefined,
|
|
216
|
+
},
|
|
210
217
|
});
|
|
211
218
|
|
|
212
219
|
return {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
type BncrReplyConfigResult = {
|
|
2
|
+
blockStreaming: boolean;
|
|
3
|
+
allowTool: boolean;
|
|
4
|
+
replyCfg: any;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
function parseBooleanLike(value: unknown): boolean | undefined {
|
|
8
|
+
if (typeof value === 'boolean') return value;
|
|
9
|
+
if (typeof value === 'number') {
|
|
10
|
+
if (value === 1) return true;
|
|
11
|
+
if (value === 0) return false;
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
if (typeof value !== 'string') return undefined;
|
|
15
|
+
|
|
16
|
+
const normalized = value.trim().toLowerCase();
|
|
17
|
+
if (!normalized) return undefined;
|
|
18
|
+
if (['true', 'on', '1', 'yes'].includes(normalized)) return true;
|
|
19
|
+
if (['false', 'off', '0', 'no'].includes(normalized)) return false;
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function resolveBncrBlockStreaming(cfg: any): boolean {
|
|
24
|
+
const channelValue = parseBooleanLike(cfg?.channels?.bncr?.blockStreaming);
|
|
25
|
+
if (channelValue !== undefined) return channelValue;
|
|
26
|
+
|
|
27
|
+
const globalValue = parseBooleanLike(cfg?.agents?.defaults?.blockStreamingDefault);
|
|
28
|
+
if (globalValue !== undefined) return globalValue;
|
|
29
|
+
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function resolveBncrAllowTool(cfg: any): boolean {
|
|
34
|
+
return cfg?.channels?.bncr?.allowTool === true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function buildBncrReplyConfig(cfg: any): BncrReplyConfigResult {
|
|
38
|
+
const blockStreaming = resolveBncrBlockStreaming(cfg);
|
|
39
|
+
const allowTool = resolveBncrAllowTool(cfg);
|
|
40
|
+
|
|
41
|
+
const replyCfg = {
|
|
42
|
+
...cfg,
|
|
43
|
+
agents: {
|
|
44
|
+
...(cfg?.agents ?? {}),
|
|
45
|
+
defaults: {
|
|
46
|
+
...(cfg?.agents?.defaults ?? {}),
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
if (replyCfg.agents.defaults.blockStreamingBreak == null) {
|
|
52
|
+
replyCfg.agents.defaults.blockStreamingBreak = 'message_end';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (replyCfg.agents.defaults.blockStreamingChunk == null) {
|
|
56
|
+
replyCfg.agents.defaults.blockStreamingChunk = {
|
|
57
|
+
minChars: 500,
|
|
58
|
+
maxChars: 4096,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { blockStreaming, allowTool, replyCfg };
|
|
63
|
+
}
|