@xmoxmo/bncr 0.1.3 → 0.1.4
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) =>
|
|
@@ -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,18 @@ 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
|
-
if (kind && kind !== 'final') return;
|
|
139
138
|
const hasPayload = Boolean(
|
|
140
139
|
payload?.text ||
|
|
141
140
|
payload?.mediaUrl ||
|
|
@@ -149,11 +148,14 @@ export async function handleBncrNativeCommand(params: {
|
|
|
149
148
|
route,
|
|
150
149
|
payload: {
|
|
151
150
|
...payload,
|
|
152
|
-
kind: kind as 'block' | 'final' | undefined,
|
|
151
|
+
kind: kind as 'tool' | 'block' | 'final' | undefined,
|
|
153
152
|
},
|
|
154
153
|
});
|
|
155
154
|
},
|
|
156
155
|
},
|
|
156
|
+
replyOptions: {
|
|
157
|
+
disableBlockStreaming: !effectiveReply.blockStreaming,
|
|
158
|
+
},
|
|
157
159
|
});
|
|
158
160
|
|
|
159
161
|
if (!responded) {
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
withTaskSessionKey,
|
|
6
6
|
} from '../../core/targets.ts';
|
|
7
7
|
import { handleBncrNativeCommand } from './commands.ts';
|
|
8
|
+
import { buildBncrReplyConfig } from './reply-config.ts';
|
|
8
9
|
|
|
9
10
|
type ParsedInbound = ReturnType<typeof import('./parse.ts')['parseBncrInboundParams']>;
|
|
10
11
|
|
|
@@ -179,19 +180,17 @@ export async function dispatchBncrInbound(params: {
|
|
|
179
180
|
setInboundActivity(accountId, inboundAt);
|
|
180
181
|
scheduleSave();
|
|
181
182
|
|
|
183
|
+
const effectiveReply = buildBncrReplyConfig(cfg);
|
|
184
|
+
|
|
182
185
|
await api.runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
183
186
|
ctx: ctxPayload,
|
|
184
|
-
cfg,
|
|
185
|
-
replyOptions: {
|
|
186
|
-
disableBlockStreaming: true,
|
|
187
|
-
},
|
|
187
|
+
cfg: effectiveReply.replyCfg,
|
|
188
188
|
dispatcherOptions: {
|
|
189
189
|
deliver: async (
|
|
190
190
|
payload: { text?: string; mediaUrl?: string; mediaUrls?: string[]; audioAsVoice?: boolean },
|
|
191
191
|
info?: { kind?: 'tool' | 'block' | 'final' },
|
|
192
192
|
) => {
|
|
193
193
|
const kind = info?.kind;
|
|
194
|
-
if (kind && kind !== 'final') return;
|
|
195
194
|
|
|
196
195
|
await enqueueFromReply({
|
|
197
196
|
accountId,
|
|
@@ -199,7 +198,7 @@ export async function dispatchBncrInbound(params: {
|
|
|
199
198
|
route,
|
|
200
199
|
payload: {
|
|
201
200
|
...payload,
|
|
202
|
-
kind: kind as 'block' | 'final' | undefined,
|
|
201
|
+
kind: kind as 'tool' | 'block' | 'final' | undefined,
|
|
203
202
|
},
|
|
204
203
|
});
|
|
205
204
|
},
|
|
@@ -207,6 +206,9 @@ export async function dispatchBncrInbound(params: {
|
|
|
207
206
|
logger?.error?.(`bncr reply failed: ${String(err)}`);
|
|
208
207
|
},
|
|
209
208
|
},
|
|
209
|
+
replyOptions: {
|
|
210
|
+
disableBlockStreaming: !effectiveReply.blockStreaming,
|
|
211
|
+
},
|
|
210
212
|
});
|
|
211
213
|
|
|
212
214
|
return {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
type BncrReplyConfigResult = {
|
|
2
|
+
blockStreaming: boolean;
|
|
3
|
+
replyCfg: any;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
function parseBooleanLike(value: unknown): boolean | undefined {
|
|
7
|
+
if (typeof value === 'boolean') return value;
|
|
8
|
+
if (typeof value === 'number') {
|
|
9
|
+
if (value === 1) return true;
|
|
10
|
+
if (value === 0) return false;
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
if (typeof value !== 'string') return undefined;
|
|
14
|
+
|
|
15
|
+
const normalized = value.trim().toLowerCase();
|
|
16
|
+
if (!normalized) return undefined;
|
|
17
|
+
if (['true', 'on', '1', 'yes'].includes(normalized)) return true;
|
|
18
|
+
if (['false', 'off', '0', 'no'].includes(normalized)) return false;
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function resolveBncrBlockStreaming(cfg: any): boolean {
|
|
23
|
+
const channelValue = parseBooleanLike(cfg?.channels?.bncr?.blockStreaming);
|
|
24
|
+
if (channelValue !== undefined) return channelValue;
|
|
25
|
+
|
|
26
|
+
const globalValue = parseBooleanLike(cfg?.agents?.defaults?.blockStreamingDefault);
|
|
27
|
+
if (globalValue !== undefined) return globalValue;
|
|
28
|
+
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function buildBncrReplyConfig(cfg: any): BncrReplyConfigResult {
|
|
33
|
+
const blockStreaming = resolveBncrBlockStreaming(cfg);
|
|
34
|
+
|
|
35
|
+
const replyCfg = {
|
|
36
|
+
...cfg,
|
|
37
|
+
agents: {
|
|
38
|
+
...(cfg?.agents ?? {}),
|
|
39
|
+
defaults: {
|
|
40
|
+
...(cfg?.agents?.defaults ?? {}),
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
if (replyCfg.agents.defaults.blockStreamingBreak == null) {
|
|
46
|
+
replyCfg.agents.defaults.blockStreamingBreak = 'message_end';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { blockStreaming, replyCfg };
|
|
50
|
+
}
|