@xmoxmo/bncr 0.0.3 → 0.0.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/README.md CHANGED
@@ -1,394 +1,150 @@
1
- # bncr-channel
1
+ # bncr
2
2
 
3
- OpenClaw 的 Bncr WebSocket Bridge 频道插件(`channelId=bncr`)。
3
+ OpenClaw 的 Bncr 频道插件(`channelId=bncr`)。
4
4
 
5
- 这份文档按“拿到插件就能对接”的目标编写:你只看本 README,也能完成接入。
5
+ 作用很简单:把 **Bncr / 无界客户端** 接到 **OpenClaw 网关**,用于消息双向通信与媒体/文件传输。
6
6
 
7
- ---
8
-
9
- ## 1. 概览
10
-
11
- - **OpenClaw Channel ID**:`bncr`
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`
7
+ > 当前定位说明:bncr **不是 agent**。它保留既有 **WS 接入链路** 作为 transport / 通信承载,
8
+ > 在 OpenClaw 内部则按 **正式频道插件(channel plugin)** 建模。
23
9
 
24
10
  ---
25
11
 
26
- ## 2. 工作模式(重点)
27
-
28
- 当前是 **push-only**:
29
-
30
- - Bncr 在线:OpenClaw 通过 WS `event=bncr.push` 直接下发回复。
31
- - Bncr 离线:消息进入 outbox;重连后自动冲队列。
32
- - `bncr.activity` 仅用于在线保活,不承载拉取。
33
- - `bncr.ack` 保留兼容,当前主链路不强依赖。
12
+ ## 1. 这是什么
34
13
 
35
- > 结论:客户端最小实现只需两件事:
36
- > 1) `bncr.inbound`;2) 监听 `bncr.push`。
14
+ - 一个 OpenClaw 的正式频道插件
15
+ - 负责把 Bncr / 无界客户端接入 OpenClaw
16
+ - 负责消息、媒体、文件与基础状态链路
37
17
 
38
18
  ---
39
19
 
40
- ## 3. SessionKey 规则(严格)
20
+ ## 2. 安装方式
41
21
 
42
- 标准格式(canonical):
22
+ ### OpenClaw 侧
43
23
 
44
- ```text
45
- agent:main:bncr:direct:<hexScope>
24
+ ```bash
25
+ openclaw plugins install @xmoxmo/bncr
26
+ openclaw plugins enable bncr
27
+ openclaw gateway restart
46
28
  ```
47
29
 
48
- 其中:
30
+ ### Bncr / 无界侧
49
31
 
50
- - `<hexScope>` = `platform:groupId:userId` 的 UTF-8 十六进制。
51
- - 推荐使用小写 hex(插件兼容大小写输入)。
52
- - 兼容输入会在内部归一到 `agent:main:bncr:direct:<hexScope>`。
32
+ 安装:
53
33
 
54
- 示例:
34
+ - `openclawclient.js`
55
35
 
56
- ```text
57
- scope = qq:0:888888
58
- hexScope = 71713a303a383838383838
59
- sessionKey = agent:main:bncr:direct:71713a303a383838383838
60
- ```
36
+ 然后完成客户端配置并连上 OpenClaw 网关即可。
61
37
 
62
38
  ---
63
39
 
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`
40
+ ## 3. 当前能力
41
+
42
+ - 文本
43
+ - 图片
44
+ - 视频
45
+ - 语音
46
+ - 音频
47
+ - 文件
48
+ - 下行推送
49
+ - ACK
50
+ - 离线消息排队
51
+ - 重连后继续发送
52
+ - 状态诊断
53
+ - 文件互传
78
54
 
79
55
  ---
80
56
 
81
- ## 5. 接入流程(最小可用)
82
-
83
- ### Step A:建立 WS 并发送 `bncr.connect`
57
+ ## 4. 架构定位
84
58
 
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
- ```
59
+ bncr 当前采用两层模型:
98
60
 
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
- ```
61
+ 1. **WS 承载层**
62
+ - Bncr 客户端通过 WebSocket 接入 OpenClaw 网关
63
+ - 负责连接、推送、ACK、文件分块等 transport 能力
155
64
 
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
- ```
65
+ 2. **OpenClaw 频道插件层**
66
+ - 在 OpenClaw 内部按正式 `channel plugin` 建模
67
+ - 负责入站解析、消息分发、出站适配、状态与治理
178
68
 
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
- ```
69
+ 当前代码结构:
195
70
 
196
- > `bncr.inbound` 先快速 ACK,再异步处理,最终回复经 `bncr.push` 回推。
197
-
198
- ### Step C:消费 `bncr.push`
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
- }
71
+ ```text
72
+ plugins/bncr/src/
73
+ channel.ts
74
+ core/
75
+ messaging/
224
76
  ```
225
77
 
226
78
  ---
227
79
 
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`
80
+ ## 5. 配置项总览
249
81
 
250
- #### 6.1.1 任务分流前缀(可选)
82
+ 当前主要配置字段:
251
83
 
252
- `msg` 支持前缀:
84
+ - `enabled`
85
+ - `dmPolicy`
86
+ - `groupPolicy`
87
+ - `allowFrom`
88
+ - `groupAllowFrom`
89
+ - `outboundRequireAck`
90
+ - `requireMention`
91
+ - `accounts`
253
92
 
254
- - `#task:foo`
255
- - `/task:foo`
256
- - `/task foo 正文...`
93
+ 补充:
257
94
 
258
- 命中后会把会话键附加为 `:task:<taskKey>` 用于子任务分流,ACK 中会返回 `taskKey`。
259
-
260
- ### 6.2 OpenClaw -> Bncr(`bncr.push`)
261
-
262
- 关键字段:
263
-
264
- - `type`(固定 `message.outbound`)
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` 主链路。
95
+ - `dmPolicy` / `groupPolicy` 支持:`open | allowlist | disabled`
96
+ - `outboundRequireAck` 控制文本外发是否等待 `bncr.ack` 再出队
97
+ - `requireMention` 当前仍是保留字段
277
98
 
278
99
  ---
279
100
 
280
- ## 7. `message.send(channel=bncr)` 目标解析规则(重要)
281
-
282
- 发送前支持并兼容以下 6 种目标输入:
283
-
284
- 1. `agent:main:bncr:direct:<hex>`
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>`
101
+ ## 6. 状态与诊断
290
102
 
291
- 推荐写法:
103
+ 常用检查:
292
104
 
293
- - `to=bncr:<platform>:<groupId>:<userId>`
294
-
295
- 内部会做**反查校验**:
296
-
297
- - 必须在已知会话路由中反查到真实 session 才发送。
298
- - 查不到会报:`target not found in known sessions`。
299
-
300
- ---
301
-
302
- ## 8. 文件互传(V1)
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 客户端可通过:
322
-
323
- - `bncr.file.init`
324
- - `bncr.file.chunk`
325
- - `bncr.file.complete`
326
- - `bncr.file.abort`
327
-
328
- 完成上传后,OpenClaw 会落盘并在后续 `bncr.inbound` 中通过 `path` 传递。
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`
105
+ ```bash
106
+ openclaw gateway status
107
+ openclaw health --json
108
+ ```
346
109
 
347
- 常用状态字段:
110
+ 重点看:
348
111
 
112
+ - `linked`
349
113
  - `pending`
350
114
  - `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 残留、账号残留等
115
+ - diagnostics / probe / status 摘要
363
116
 
364
117
  ---
365
118
 
366
- ## 11. FAQ
367
-
368
- ### Q1:为什么看不到回复?
119
+ ## 7. 自检与测试
369
120
 
370
- 1. 先确认 `bncr.connect` 成功。
371
- 2. 客户端确认监听的是 `bncr.push`。
372
- 3. `sessionKey` 是否符合规范。
373
- 4. 若用 `message.send`,目标是否能反查到已知会话。
121
+ ```bash
122
+ cd plugins/bncr
123
+ npm test
124
+ npm run selfcheck
125
+ npm pack
126
+ ```
374
127
 
375
- ### Q2:为什么不需要 `bncr.pull`?
128
+ 用途:
376
129
 
377
- 因为当前是 push-only,统一走 `bncr.push`。
130
+ - `npm test`:跑回归测试
131
+ - `npm run selfcheck`:检查插件骨架是否完整
132
+ - `npm pack`:确认当前版本可正常打包
378
133
 
379
- ### Q3:如何避免重复消息?
134
+ ---
380
135
 
381
- - 入站带稳定 `msgId`。
382
- - 出站按 `idempotencyKey` 幂等处理。
383
- - 客户端侧建议仅消费 `message.outbound`,并按需过滤 `NO_REPLY/HEARTBEAT_OK`。
136
+ ## 8. 上线前检查
384
137
 
385
- ### Q4:如何看桥接健康状态?
138
+ 上线前建议至少确认:
386
139
 
387
- - 可直接调用 `bncr.diagnostics`。
388
- - 或看 `bncr.connect`/状态卡片中的 `diagnostics` 字段。
140
+ - README 与当前实现一致
141
+ - 配置 schema 与实际字段一致
142
+ - 测试通过
143
+ - 自检通过
144
+ - 可以正常打包
145
+ - 本地版本号与 npm / 发布目标一致
146
+ - 运行态 `linked / pending / deadLetter` 正常
389
147
 
390
148
  ---
391
149
 
392
- ## 12. 版本提示
393
-
394
- 历史版本接入过的话,请以当前文档(push-only + 6 格式目标兼容 + 文件互传 V1 + 诊断字段)为准。
150
+ 如果你接触过旧版本,请以当前 README 和当前代码为准。
package/index.ts CHANGED
@@ -1,15 +1,15 @@
1
1
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
2
  import {
3
- emptyPluginConfigSchema,
4
3
  type GatewayRequestHandlerOptions,
5
4
  } from "openclaw/plugin-sdk";
5
+ import { BncrConfigSchema } from "./src/core/config-schema.js";
6
6
  import { createBncrBridge, createBncrChannelPlugin } from "./src/channel.js";
7
7
 
8
8
  const plugin = {
9
9
  id: "bncr",
10
10
  name: "Bncr",
11
11
  description: "Bncr channel plugin",
12
- configSchema: emptyPluginConfigSchema(),
12
+ configSchema: BncrConfigSchema,
13
13
  register(api: OpenClawPluginApi) {
14
14
  const bridge = createBncrBridge(api);
15
15
 
@@ -3,7 +3,38 @@
3
3
  "channels": ["bncr"],
4
4
  "configSchema": {
5
5
  "type": "object",
6
- "additionalProperties": false,
7
- "properties": {}
6
+ "additionalProperties": true,
7
+ "properties": {
8
+ "enabled": { "type": "boolean" },
9
+ "dmPolicy": {
10
+ "type": "string",
11
+ "enum": ["open", "allowlist", "disabled"]
12
+ },
13
+ "groupPolicy": {
14
+ "type": "string",
15
+ "enum": ["open", "allowlist", "disabled"]
16
+ },
17
+ "allowFrom": {
18
+ "type": "array",
19
+ "items": { "type": "string" }
20
+ },
21
+ "groupAllowFrom": {
22
+ "type": "array",
23
+ "items": { "type": "string" }
24
+ },
25
+ "requireMention": { "type": "boolean" },
26
+ "outboundRequireAck": { "type": "boolean" },
27
+ "accounts": {
28
+ "type": "object",
29
+ "additionalProperties": {
30
+ "type": "object",
31
+ "additionalProperties": true,
32
+ "properties": {
33
+ "enabled": { "type": "boolean" },
34
+ "name": { "type": "string" }
35
+ }
36
+ }
37
+ }
38
+ }
8
39
  }
9
40
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmoxmo/bncr",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -20,8 +20,13 @@
20
20
  "openclaw.plugin.json",
21
21
  "README.md",
22
22
  "LICENSE",
23
- "src"
23
+ "src",
24
+ "scripts"
24
25
  ],
26
+ "scripts": {
27
+ "selfcheck": "node ./scripts/selfcheck.mjs",
28
+ "test": "node --import ./tests/register-ts-hooks.mjs --test ./tests/*.test.mjs"
29
+ },
25
30
  "openclaw": {
26
31
  "extensions": [
27
32
  "./index.ts"
@@ -0,0 +1,38 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+ const root = path.resolve(__dirname, '..');
8
+
9
+ const requiredFiles = [
10
+ 'index.ts',
11
+ 'openclaw.plugin.json',
12
+ 'src/channel.ts',
13
+ 'src/core/types.ts',
14
+ 'src/core/accounts.ts',
15
+ 'src/core/targets.ts',
16
+ 'src/core/status.ts',
17
+ 'src/core/probe.ts',
18
+ 'src/core/config-schema.ts',
19
+ 'src/core/policy.ts',
20
+ 'src/core/permissions.ts',
21
+ 'src/messaging/inbound/parse.ts',
22
+ 'src/messaging/inbound/gate.ts',
23
+ 'src/messaging/inbound/dispatch.ts',
24
+ 'src/messaging/outbound/send.ts',
25
+ 'src/messaging/outbound/media.ts',
26
+ 'src/messaging/outbound/actions.ts',
27
+ ];
28
+
29
+ const missing = requiredFiles.filter((rel) => !fs.existsSync(path.join(root, rel)));
30
+ const result = {
31
+ ok: missing.length === 0,
32
+ checkedRoot: root,
33
+ requiredCount: requiredFiles.length,
34
+ missing,
35
+ };
36
+
37
+ console.log(JSON.stringify(result, null, 2));
38
+ if (missing.length > 0) process.exit(1);