@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: '0.1.1',
288
+ pluginVersion,
277
289
  apiRebound: !created,
278
290
  apiInstanceId: meta.apiInstanceId,
279
291
  registryFingerprint: meta.registryFingerprint,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmoxmo/bncr",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
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
- continue;
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
+ }