@xmoxmo/bncr 0.0.3 → 0.0.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/README.md +46 -360
- package/package.json +1 -1
- package/src/channel.ts +72 -49
- package/LICENSE +0 -21
package/README.md
CHANGED
|
@@ -1,394 +1,80 @@
|
|
|
1
|
-
# bncr
|
|
1
|
+
# bncr
|
|
2
2
|
|
|
3
|
-
OpenClaw 的 Bncr
|
|
3
|
+
OpenClaw 的 Bncr 频道插件(`channelId=bncr`)。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
作用很简单:把 **Bncr / 无界客户端** 接到 **OpenClaw 网关**,用于消息双向通信与媒体/文件传输。
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## 安装
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
- **Bridge Version**:`2`
|
|
13
|
-
- **出站事件名**:`bncr.push`
|
|
14
|
-
- **出站模式**:`push-only`(不依赖 pull 轮询)
|
|
15
|
-
- **活动心跳方法**:`bncr.activity`
|
|
16
|
-
- **诊断方法**:`bncr.diagnostics`
|
|
17
|
-
- **文件互传方法(V1)**:
|
|
18
|
-
- `bncr.file.init`
|
|
19
|
-
- `bncr.file.chunk`
|
|
20
|
-
- `bncr.file.complete`
|
|
21
|
-
- `bncr.file.abort`
|
|
22
|
-
- `bncr.file.ack`
|
|
11
|
+
### OpenClaw 侧
|
|
23
12
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
## 2. 工作模式(重点)
|
|
27
|
-
|
|
28
|
-
当前是 **push-only**:
|
|
29
|
-
|
|
30
|
-
- Bncr 在线:OpenClaw 通过 WS `event=bncr.push` 直接下发回复。
|
|
31
|
-
- Bncr 离线:消息进入 outbox;重连后自动冲队列。
|
|
32
|
-
- `bncr.activity` 仅用于在线保活,不承载拉取。
|
|
33
|
-
- `bncr.ack` 保留兼容,当前主链路不强依赖。
|
|
34
|
-
|
|
35
|
-
> 结论:客户端最小实现只需两件事:
|
|
36
|
-
> 1) 发 `bncr.inbound`;2) 监听 `bncr.push`。
|
|
37
|
-
|
|
38
|
-
---
|
|
39
|
-
|
|
40
|
-
## 3. SessionKey 规则(严格)
|
|
41
|
-
|
|
42
|
-
标准格式(canonical):
|
|
43
|
-
|
|
44
|
-
```text
|
|
45
|
-
agent:main:bncr:direct:<hexScope>
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
其中:
|
|
49
|
-
|
|
50
|
-
- `<hexScope>` = `platform:groupId:userId` 的 UTF-8 十六进制。
|
|
51
|
-
- 推荐使用小写 hex(插件兼容大小写输入)。
|
|
52
|
-
- 兼容输入会在内部归一到 `agent:main:bncr:direct:<hexScope>`。
|
|
53
|
-
|
|
54
|
-
示例:
|
|
55
|
-
|
|
56
|
-
```text
|
|
57
|
-
scope = qq:0:888888
|
|
58
|
-
hexScope = 71713a303a383838383838
|
|
59
|
-
sessionKey = agent:main:bncr:direct:71713a303a383838383838
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
---
|
|
63
|
-
|
|
64
|
-
## 4. Gateway Methods
|
|
65
|
-
|
|
66
|
-
插件注册方法:
|
|
67
|
-
|
|
68
|
-
- `bncr.connect`
|
|
69
|
-
- `bncr.inbound`
|
|
70
|
-
- `bncr.activity`
|
|
71
|
-
- `bncr.ack`
|
|
72
|
-
- `bncr.diagnostics`
|
|
73
|
-
- `bncr.file.init`
|
|
74
|
-
- `bncr.file.chunk`
|
|
75
|
-
- `bncr.file.complete`
|
|
76
|
-
- `bncr.file.abort`
|
|
77
|
-
- `bncr.file.ack`
|
|
78
|
-
|
|
79
|
-
---
|
|
80
|
-
|
|
81
|
-
## 5. 接入流程(最小可用)
|
|
82
|
-
|
|
83
|
-
### Step A:建立 WS 并发送 `bncr.connect`
|
|
84
|
-
|
|
85
|
-
请求示例:
|
|
86
|
-
|
|
87
|
-
```json
|
|
88
|
-
{
|
|
89
|
-
"type": "req",
|
|
90
|
-
"id": "c1",
|
|
91
|
-
"method": "bncr.connect",
|
|
92
|
-
"params": {
|
|
93
|
-
"accountId": "Primary",
|
|
94
|
-
"clientId": "bncr-client-1"
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
响应示例:
|
|
100
|
-
|
|
101
|
-
```json
|
|
102
|
-
{
|
|
103
|
-
"type": "res",
|
|
104
|
-
"id": "c1",
|
|
105
|
-
"ok": true,
|
|
106
|
-
"result": {
|
|
107
|
-
"channel": "bncr",
|
|
108
|
-
"accountId": "Primary",
|
|
109
|
-
"bridgeVersion": 2,
|
|
110
|
-
"pushEvent": "bncr.push",
|
|
111
|
-
"online": true,
|
|
112
|
-
"isPrimary": true,
|
|
113
|
-
"activeConnections": 1,
|
|
114
|
-
"pending": 0,
|
|
115
|
-
"deadLetter": 0,
|
|
116
|
-
"diagnostics": {
|
|
117
|
-
"health": {
|
|
118
|
-
"connected": true,
|
|
119
|
-
"pending": 0,
|
|
120
|
-
"deadLetter": 0,
|
|
121
|
-
"activeConnections": 1
|
|
122
|
-
},
|
|
123
|
-
"regression": {
|
|
124
|
-
"ok": true
|
|
125
|
-
}
|
|
126
|
-
},
|
|
127
|
-
"now": 1772476800000
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
> `accountId` 示例是 `Primary`,请按你实际配置替换;不传默认使用 `Primary`。
|
|
133
|
-
|
|
134
|
-
### Step B:上行消息用 `bncr.inbound`
|
|
135
|
-
|
|
136
|
-
文本请求示例(`msg` 形态):
|
|
137
|
-
|
|
138
|
-
```json
|
|
139
|
-
{
|
|
140
|
-
"type": "req",
|
|
141
|
-
"id": "i1",
|
|
142
|
-
"method": "bncr.inbound",
|
|
143
|
-
"params": {
|
|
144
|
-
"accountId": "Primary",
|
|
145
|
-
"platform": "qq",
|
|
146
|
-
"groupId": "0",
|
|
147
|
-
"userId": "888888",
|
|
148
|
-
"sessionKey": "agent:main:bncr:direct:71713a303a383838383838",
|
|
149
|
-
"msgId": "msg-1001",
|
|
150
|
-
"type": "text",
|
|
151
|
-
"msg": "你好"
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
媒体请求示例(字段是 `base64`):
|
|
157
|
-
|
|
158
|
-
```json
|
|
159
|
-
{
|
|
160
|
-
"type": "req",
|
|
161
|
-
"id": "i2",
|
|
162
|
-
"method": "bncr.inbound",
|
|
163
|
-
"params": {
|
|
164
|
-
"accountId": "Primary",
|
|
165
|
-
"platform": "qq",
|
|
166
|
-
"groupId": "0",
|
|
167
|
-
"userId": "888888",
|
|
168
|
-
"sessionKey": "agent:main:bncr:direct:71713a303a383838383838",
|
|
169
|
-
"msgId": "msg-1002",
|
|
170
|
-
"type": "image/png",
|
|
171
|
-
"msg": "",
|
|
172
|
-
"base64": "<BASE64_PAYLOAD>",
|
|
173
|
-
"mimeType": "image/png",
|
|
174
|
-
"fileName": "demo.png"
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
响应示例:
|
|
180
|
-
|
|
181
|
-
```json
|
|
182
|
-
{
|
|
183
|
-
"type": "res",
|
|
184
|
-
"id": "i1",
|
|
185
|
-
"ok": true,
|
|
186
|
-
"result": {
|
|
187
|
-
"accepted": true,
|
|
188
|
-
"accountId": "Primary",
|
|
189
|
-
"sessionKey": "agent:main:bncr:direct:71713a303a383838383838",
|
|
190
|
-
"msgId": "msg-1001",
|
|
191
|
-
"taskKey": null
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
> `bncr.inbound` 先快速 ACK,再异步处理,最终回复经 `bncr.push` 回推。
|
|
13
|
+
在 OpenClaw 上执行:
|
|
197
14
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
```json
|
|
203
|
-
{
|
|
204
|
-
"type": "event",
|
|
205
|
-
"event": "bncr.push",
|
|
206
|
-
"payload": {
|
|
207
|
-
"type": "message.outbound",
|
|
208
|
-
"messageId": "3f8b1f9b-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
|
209
|
-
"idempotencyKey": "3f8b1f9b-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
|
210
|
-
"sessionKey": "agent:main:bncr:direct:71713a303a383838383838",
|
|
211
|
-
"message": {
|
|
212
|
-
"platform": "qq",
|
|
213
|
-
"groupId": "0",
|
|
214
|
-
"userId": "888888",
|
|
215
|
-
"type": "text",
|
|
216
|
-
"msg": "收到,已处理。",
|
|
217
|
-
"path": "",
|
|
218
|
-
"base64": "",
|
|
219
|
-
"fileName": ""
|
|
220
|
-
},
|
|
221
|
-
"ts": 1772476801234
|
|
222
|
-
}
|
|
223
|
-
}
|
|
15
|
+
```bash
|
|
16
|
+
openclaw plugins install @xmoxmo/bncr
|
|
17
|
+
openclaw plugins enable bncr
|
|
18
|
+
openclaw gateway restart
|
|
224
19
|
```
|
|
225
20
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
## 6. 字段协议
|
|
229
|
-
|
|
230
|
-
### 6.1 Bncr -> OpenClaw(`bncr.inbound`)
|
|
231
|
-
|
|
232
|
-
常用字段:
|
|
233
|
-
|
|
234
|
-
- `accountId`:可选(默认 `Primary`)
|
|
235
|
-
- `platform`:必填
|
|
236
|
-
- `groupId`:可选,默认 `"0"`(私聊)
|
|
237
|
-
- `userId`:建议填写(私聊/群聊都建议带上)
|
|
238
|
-
- `sessionKey`:可选,建议传严格 sessionKey
|
|
239
|
-
- `msgId`:建议传(便于短窗口去重)
|
|
240
|
-
- `type`:`text/image/video/file/...`
|
|
241
|
-
- `msg`:文本
|
|
242
|
-
- `base64`:媒体 base64
|
|
243
|
-
- `path`:可选,文件直传完成后的落盘路径(与 `base64` 二选一)
|
|
244
|
-
- `mimeType` / `fileName`:媒体元数据(可选)
|
|
245
|
-
|
|
246
|
-
校验失败常见错误:
|
|
247
|
-
|
|
248
|
-
- `platform/groupId/userId required`
|
|
249
|
-
|
|
250
|
-
#### 6.1.1 任务分流前缀(可选)
|
|
251
|
-
|
|
252
|
-
`msg` 支持前缀:
|
|
21
|
+
### Bncr / 无界侧
|
|
253
22
|
|
|
254
|
-
|
|
255
|
-
- `/task:foo`
|
|
256
|
-
- `/task foo 正文...`
|
|
23
|
+
安装:
|
|
257
24
|
|
|
258
|
-
|
|
25
|
+
- `openclawclient.js`
|
|
259
26
|
|
|
260
|
-
|
|
27
|
+
然后完成客户端配置,至少包括:
|
|
261
28
|
|
|
262
|
-
|
|
29
|
+
- OpenClaw 地址
|
|
30
|
+
- 端口
|
|
31
|
+
- Token
|
|
32
|
+
- 连接相关参数
|
|
263
33
|
|
|
264
|
-
|
|
265
|
-
- `messageId`
|
|
266
|
-
- `idempotencyKey`(当前等于 `messageId`)
|
|
267
|
-
- `sessionKey`
|
|
268
|
-
- `message.platform/groupId/userId`
|
|
269
|
-
- `message.type/msg/path/base64/fileName/mimeType`
|
|
270
|
-
- `message.transferMode`(媒体场景可出现:`base64`/`chunk`)
|
|
271
|
-
- `ts`
|
|
272
|
-
|
|
273
|
-
说明:
|
|
274
|
-
|
|
275
|
-
- 主类型固定为 `type="message.outbound"`。
|
|
276
|
-
- 推荐客户端仅消费 `message.outbound` 主链路。
|
|
34
|
+
配置完成后,让客户端成功连到 OpenClaw 网关即可。
|
|
277
35
|
|
|
278
36
|
---
|
|
279
37
|
|
|
280
|
-
##
|
|
281
|
-
|
|
282
|
-
发送前支持并兼容以下 6 种目标输入:
|
|
38
|
+
## 支持能力
|
|
283
39
|
|
|
284
|
-
|
|
285
|
-
2. `agent:main:bncr:group:<hex>`
|
|
286
|
-
3. `bncr:<hex>`
|
|
287
|
-
4. `bncr:g-<hex>`
|
|
288
|
-
5. `bncr:<platform>:<groupId>:<userId>`
|
|
289
|
-
6. `bncr:g-<platform>:<groupId>:<userId>`
|
|
40
|
+
### 支持内容
|
|
290
41
|
|
|
291
|
-
|
|
42
|
+
- 文本
|
|
43
|
+
- 图片
|
|
44
|
+
- 视频
|
|
45
|
+
- 语音
|
|
46
|
+
- 音频
|
|
47
|
+
- 文件
|
|
292
48
|
|
|
293
|
-
|
|
49
|
+
### 其它特性
|
|
294
50
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
-
|
|
298
|
-
-
|
|
51
|
+
- 下行推送
|
|
52
|
+
- 离线消息自动排队
|
|
53
|
+
- 重连后继续发送
|
|
54
|
+
- 支持诊断信息
|
|
55
|
+
- 支持文件互传
|
|
299
56
|
|
|
300
57
|
---
|
|
301
58
|
|
|
302
|
-
##
|
|
303
|
-
|
|
304
|
-
### 8.1 OpenClaw -> Bncr(下行媒体)
|
|
305
|
-
|
|
306
|
-
当前默认 **强制分块**(chunk)传输:
|
|
307
|
-
|
|
308
|
-
- `bncr.file.init`
|
|
309
|
-
- `bncr.file.chunk`
|
|
310
|
-
- `bncr.file.complete`
|
|
311
|
-
- Bncr 客户端通过 `bncr.file.ack` 回 ACK
|
|
312
|
-
|
|
313
|
-
特性:
|
|
314
|
-
|
|
315
|
-
- 分块大小默认 256KB
|
|
316
|
-
- chunk ACK 超时/失败会重试
|
|
317
|
-
- 完成后 `message.outbound.message.path` 回填客户端可用路径
|
|
318
|
-
|
|
319
|
-
### 8.2 Bncr -> OpenClaw(上行文件)
|
|
320
|
-
|
|
321
|
-
Bncr 客户端可通过:
|
|
59
|
+
## 安装后如何确认成功
|
|
322
60
|
|
|
323
|
-
|
|
324
|
-
- `bncr.file.chunk`
|
|
325
|
-
- `bncr.file.complete`
|
|
326
|
-
- `bncr.file.abort`
|
|
61
|
+
可以通过以下方式检查:
|
|
327
62
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
## 9. 可靠性
|
|
333
|
-
|
|
334
|
-
- 离线入队 + 重连自动冲队列。
|
|
335
|
-
- 指数退避:`1s,2s,4s,8s...`
|
|
336
|
-
- 最大重试次数:`10`
|
|
337
|
-
- 超限进入 dead-letter。
|
|
338
|
-
|
|
339
|
-
---
|
|
340
|
-
|
|
341
|
-
## 10. 状态判定与诊断
|
|
342
|
-
|
|
343
|
-
- 实际链路在线:`linked`
|
|
344
|
-
- 已配置但离线:`configured`
|
|
345
|
-
- 账户卡片离线展示口径会显示 `Status`
|
|
346
|
-
|
|
347
|
-
常用状态字段:
|
|
348
|
-
|
|
349
|
-
- `pending`
|
|
350
|
-
- `deadLetter`
|
|
351
|
-
- `lastSessionKey`
|
|
352
|
-
- `lastSessionScope`(`bncr:platform:group:user`)
|
|
353
|
-
- `lastSessionAt`
|
|
354
|
-
- `lastActivityAt`
|
|
355
|
-
- `lastInboundAt`
|
|
356
|
-
- `lastOutboundAt`
|
|
357
|
-
- `diagnostics`
|
|
358
|
-
|
|
359
|
-
`diagnostics` 中包含:
|
|
360
|
-
|
|
361
|
-
- `health`:连接数、pending、dead-letter、事件计数、uptime
|
|
362
|
-
- `regression`:已知路由数、无效 sessionKey 残留、账号残留等
|
|
363
|
-
|
|
364
|
-
---
|
|
365
|
-
|
|
366
|
-
## 11. FAQ
|
|
367
|
-
|
|
368
|
-
### Q1:为什么看不到回复?
|
|
369
|
-
|
|
370
|
-
1. 先确认 `bncr.connect` 成功。
|
|
371
|
-
2. 客户端确认监听的是 `bncr.push`。
|
|
372
|
-
3. `sessionKey` 是否符合规范。
|
|
373
|
-
4. 若用 `message.send`,目标是否能反查到已知会话。
|
|
374
|
-
|
|
375
|
-
### Q2:为什么不需要 `bncr.pull`?
|
|
376
|
-
|
|
377
|
-
因为当前是 push-only,统一走 `bncr.push`。
|
|
378
|
-
|
|
379
|
-
### Q3:如何避免重复消息?
|
|
63
|
+
```bash
|
|
64
|
+
openclaw gateway status
|
|
65
|
+
openclaw health --json
|
|
66
|
+
```
|
|
380
67
|
|
|
381
|
-
|
|
382
|
-
- 出站按 `idempotencyKey` 幂等处理。
|
|
383
|
-
- 客户端侧建议仅消费 `message.outbound`,并按需过滤 `NO_REPLY/HEARTBEAT_OK`。
|
|
68
|
+
重点看:
|
|
384
69
|
|
|
385
|
-
|
|
70
|
+
- 网关是否正常运行
|
|
71
|
+
- bncr 是否已经 `linked`
|
|
72
|
+
- 是否存在异常 pending / deadLetter
|
|
386
73
|
|
|
387
|
-
|
|
388
|
-
- 或看 `bncr.connect`/状态卡片中的 `diagnostics` 字段。
|
|
74
|
+
如果 bncr 已成功连上,一般就说明插件安装和基础链路已经正常。
|
|
389
75
|
|
|
390
76
|
---
|
|
391
77
|
|
|
392
|
-
##
|
|
78
|
+
## 说明
|
|
393
79
|
|
|
394
|
-
|
|
80
|
+
如果你接触过旧版本,请以当前 README 和当前代码为准。
|
package/package.json
CHANGED
package/src/channel.ts
CHANGED
|
@@ -21,6 +21,7 @@ const BRIDGE_VERSION = 2;
|
|
|
21
21
|
const BNCR_PUSH_EVENT = 'bncr.push';
|
|
22
22
|
const CONNECT_TTL_MS = 120_000;
|
|
23
23
|
const MAX_RETRY = 10;
|
|
24
|
+
const PUSH_DRAIN_INTERVAL_MS = 500;
|
|
24
25
|
const FILE_FORCE_CHUNK = true; // 统一走 WS 分块,保留 base64 仅作兜底
|
|
25
26
|
const FILE_INLINE_THRESHOLD = 5 * 1024 * 1024; // fallback 阈值(仅 FILE_FORCE_CHUNK=false 时生效)
|
|
26
27
|
const FILE_CHUNK_SIZE = 256 * 1024; // 256KB
|
|
@@ -529,20 +530,20 @@ function resolveOutboundFileName(params: { mediaUrl?: string; fileName?: string;
|
|
|
529
530
|
return `${stem}${ext}`;
|
|
530
531
|
}
|
|
531
532
|
|
|
532
|
-
function resolveBncrOutboundMessageType(params: { mimeType?: string; fileName?: string; hintedType?: string }): 'text' | 'image' | 'video' | 'file' {
|
|
533
|
+
function resolveBncrOutboundMessageType(params: { mimeType?: string; fileName?: string; hintedType?: string; hasPayload?: boolean }): 'text' | 'image' | 'video' | 'voice' | 'audio' | 'file' {
|
|
533
534
|
const hinted = asString(params.hintedType || '').toLowerCase();
|
|
534
|
-
|
|
535
|
-
if (hinted === 'text' || hinted === 'image' || hinted === 'video') return hinted as any;
|
|
536
|
-
|
|
535
|
+
const hasPayload = !!params.hasPayload;
|
|
537
536
|
const mt = asString(params.mimeType || '').toLowerCase();
|
|
538
537
|
const major = mt.split('/')[0] || '';
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
538
|
+
const isStandard = hinted === 'text' || hinted === 'image' || hinted === 'video' || hinted === 'voice' || hinted === 'audio' || hinted === 'file';
|
|
539
|
+
|
|
540
|
+
// 文本类附件不应落成 text:当上游显式给 text,或上游 type 不在标准列表时,若带文件载荷且 mime 主类型为 text,则归到 file。
|
|
541
|
+
if (hasPayload && major === 'text' && (hinted === 'text' || !isStandard)) return 'file';
|
|
542
|
+
|
|
543
|
+
// 优先使用上游已给出的标准类型;仅当不在支持列表时再尝试纠正
|
|
544
|
+
if (isStandard) return hinted as any;
|
|
542
545
|
|
|
543
|
-
|
|
544
|
-
if (/\.(png|jpe?g|gif|webp|bmp|svg)$/.test(fn)) return 'image';
|
|
545
|
-
if (/\.(mp4|mov|mkv|avi|webm)$/.test(fn)) return 'video';
|
|
546
|
+
if (major === 'text' || major === 'image' || major === 'video' || major === 'audio') return major as any;
|
|
546
547
|
|
|
547
548
|
return 'file';
|
|
548
549
|
}
|
|
@@ -574,6 +575,7 @@ class BncrBridgeRuntime {
|
|
|
574
575
|
|
|
575
576
|
private saveTimer: NodeJS.Timeout | null = null;
|
|
576
577
|
private pushTimer: NodeJS.Timeout | null = null;
|
|
578
|
+
private pushDrainRunningAccounts = new Set<string>();
|
|
577
579
|
private waiters = new Map<string, Array<() => void>>();
|
|
578
580
|
private gatewayContext: GatewayRequestHandlerOptions['context'] | null = null;
|
|
579
581
|
|
|
@@ -965,55 +967,76 @@ class BncrBridgeRuntime {
|
|
|
965
967
|
const delay = Math.max(0, Math.min(Number(delayMs || 0), 30_000));
|
|
966
968
|
this.pushTimer = setTimeout(() => {
|
|
967
969
|
this.pushTimer = null;
|
|
968
|
-
this.flushPushQueue();
|
|
970
|
+
void this.flushPushQueue();
|
|
969
971
|
}, delay);
|
|
970
972
|
}
|
|
971
973
|
|
|
972
|
-
private flushPushQueue(accountId?: string) {
|
|
973
|
-
const t = now();
|
|
974
|
+
private async flushPushQueue(accountId?: string): Promise<void> {
|
|
974
975
|
const filterAcc = accountId ? normalizeAccountId(accountId) : null;
|
|
975
|
-
const
|
|
976
|
-
|
|
977
|
-
.
|
|
976
|
+
const targetAccounts = filterAcc
|
|
977
|
+
? [filterAcc]
|
|
978
|
+
: Array.from(new Set(Array.from(this.outbox.values()).map((entry) => normalizeAccountId(entry.accountId))));
|
|
978
979
|
|
|
979
|
-
let
|
|
980
|
-
let nextDelay: number | null = null;
|
|
980
|
+
let globalNextDelay: number | null = null;
|
|
981
981
|
|
|
982
|
-
for (const
|
|
983
|
-
if (!this.
|
|
982
|
+
for (const acc of targetAccounts) {
|
|
983
|
+
if (!acc || this.pushDrainRunningAccounts.has(acc)) continue;
|
|
984
|
+
if (!this.isOnline(acc)) continue;
|
|
984
985
|
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
986
|
+
this.pushDrainRunningAccounts.add(acc);
|
|
987
|
+
try {
|
|
988
|
+
let localNextDelay: number | null = null;
|
|
989
|
+
|
|
990
|
+
while (true) {
|
|
991
|
+
const t = now();
|
|
992
|
+
const entries = Array.from(this.outbox.values())
|
|
993
|
+
.filter((entry) => normalizeAccountId(entry.accountId) === acc)
|
|
994
|
+
.sort((a, b) => a.createdAt - b.createdAt);
|
|
995
|
+
|
|
996
|
+
if (!entries.length) break;
|
|
997
|
+
if (!this.isOnline(acc)) break;
|
|
998
|
+
|
|
999
|
+
const entry = entries.find((item) => item.nextAttemptAt <= t);
|
|
1000
|
+
if (!entry) {
|
|
1001
|
+
const wait = Math.max(0, entries[0].nextAttemptAt - t);
|
|
1002
|
+
localNextDelay = localNextDelay == null ? wait : Math.min(localNextDelay, wait);
|
|
1003
|
+
break;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
const pushed = this.tryPushEntry(entry);
|
|
1007
|
+
if (pushed) {
|
|
1008
|
+
this.scheduleSave();
|
|
1009
|
+
await this.sleepMs(PUSH_DRAIN_INTERVAL_MS);
|
|
1010
|
+
continue;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
const nextAttempt = entry.retryCount + 1;
|
|
1014
|
+
if (nextAttempt > MAX_RETRY) {
|
|
1015
|
+
this.moveToDeadLetter(entry, entry.lastError || 'push-retry-limit');
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
entry.retryCount = nextAttempt;
|
|
1020
|
+
entry.lastAttemptAt = t;
|
|
1021
|
+
entry.nextAttemptAt = t + backoffMs(nextAttempt);
|
|
1022
|
+
entry.lastError = entry.lastError || 'push-retry';
|
|
1023
|
+
this.outbox.set(entry.messageId, entry);
|
|
1024
|
+
this.scheduleSave();
|
|
1025
|
+
|
|
1026
|
+
const wait = Math.max(0, entry.nextAttemptAt - t);
|
|
1027
|
+
localNextDelay = localNextDelay == null ? wait : Math.min(localNextDelay, wait);
|
|
1028
|
+
break;
|
|
1029
|
+
}
|
|
996
1030
|
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1031
|
+
if (localNextDelay != null) {
|
|
1032
|
+
globalNextDelay = globalNextDelay == null ? localNextDelay : Math.min(globalNextDelay, localNextDelay);
|
|
1033
|
+
}
|
|
1034
|
+
} finally {
|
|
1035
|
+
this.pushDrainRunningAccounts.delete(acc);
|
|
1002
1036
|
}
|
|
1003
|
-
|
|
1004
|
-
entry.retryCount = nextAttempt;
|
|
1005
|
-
entry.lastAttemptAt = t;
|
|
1006
|
-
entry.nextAttemptAt = t + backoffMs(nextAttempt);
|
|
1007
|
-
entry.lastError = entry.lastError || 'push-retry';
|
|
1008
|
-
this.outbox.set(entry.messageId, entry);
|
|
1009
|
-
changed = true;
|
|
1010
|
-
|
|
1011
|
-
const wait = entry.nextAttemptAt - t;
|
|
1012
|
-
nextDelay = nextDelay == null ? wait : Math.min(nextDelay, wait);
|
|
1013
1037
|
}
|
|
1014
1038
|
|
|
1015
|
-
if (
|
|
1016
|
-
if (nextDelay != null) this.schedulePushDrain(nextDelay);
|
|
1039
|
+
if (globalNextDelay != null) this.schedulePushDrain(globalNextDelay);
|
|
1017
1040
|
}
|
|
1018
1041
|
|
|
1019
1042
|
private async waitForOutbound(accountId: string, waitMs: number): Promise<void> {
|
|
@@ -1751,7 +1774,7 @@ class BncrBridgeRuntime {
|
|
|
1751
1774
|
type: resolveBncrOutboundMessageType({
|
|
1752
1775
|
mimeType: media.mimeType,
|
|
1753
1776
|
fileName: media.fileName,
|
|
1754
|
-
|
|
1777
|
+
hasPayload: !!(media.path || media.mediaBase64),
|
|
1755
1778
|
}),
|
|
1756
1779
|
mimeType: media.mimeType || '',
|
|
1757
1780
|
msg: mediaMsg,
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 xmoxmo
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|