@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.
Files changed (4) hide show
  1. package/README.md +46 -360
  2. package/package.json +1 -1
  3. package/src/channel.ts +72 -49
  4. package/LICENSE +0 -21
package/README.md CHANGED
@@ -1,394 +1,80 @@
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
7
  ---
8
8
 
9
- ## 1. 概览
9
+ ## 安装
10
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`
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
- ### 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
- }
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
- - `#task:foo`
255
- - `/task:foo`
256
- - `/task foo 正文...`
23
+ 安装:
257
24
 
258
- 命中后会把会话键附加为 `:task:<taskKey>` 用于子任务分流,ACK 中会返回 `taskKey`。
25
+ - `openclawclient.js`
259
26
 
260
- ### 6.2 OpenClaw -> Bncr(`bncr.push`)
27
+ 然后完成客户端配置,至少包括:
261
28
 
262
- 关键字段:
29
+ - OpenClaw 地址
30
+ - 端口
31
+ - Token
32
+ - 连接相关参数
263
33
 
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` 主链路。
34
+ 配置完成后,让客户端成功连到 OpenClaw 网关即可。
277
35
 
278
36
  ---
279
37
 
280
- ## 7. `message.send(channel=bncr)` 目标解析规则(重要)
281
-
282
- 发送前支持并兼容以下 6 种目标输入:
38
+ ## 支持能力
283
39
 
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>`
40
+ ### 支持内容
290
41
 
291
- 推荐写法:
42
+ - 文本
43
+ - 图片
44
+ - 视频
45
+ - 语音
46
+ - 音频
47
+ - 文件
292
48
 
293
- - `to=bncr:<platform>:<groupId>:<userId>`
49
+ ### 其它特性
294
50
 
295
- 内部会做**反查校验**:
296
-
297
- - 必须在已知会话路由中反查到真实 session 才发送。
298
- - 查不到会报:`target not found in known sessions`。
51
+ - 下行推送
52
+ - 离线消息自动排队
53
+ - 重连后继续发送
54
+ - 支持诊断信息
55
+ - 支持文件互传
299
56
 
300
57
  ---
301
58
 
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 客户端可通过:
59
+ ## 安装后如何确认成功
322
60
 
323
- - `bncr.file.init`
324
- - `bncr.file.chunk`
325
- - `bncr.file.complete`
326
- - `bncr.file.abort`
61
+ 可以通过以下方式检查:
327
62
 
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`
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
- - 入站带稳定 `msgId`。
382
- - 出站按 `idempotencyKey` 幂等处理。
383
- - 客户端侧建议仅消费 `message.outbound`,并按需过滤 `NO_REPLY/HEARTBEAT_OK`。
68
+ 重点看:
384
69
 
385
- ### Q4:如何看桥接健康状态?
70
+ - 网关是否正常运行
71
+ - bncr 是否已经 `linked`
72
+ - 是否存在异常 pending / deadLetter
386
73
 
387
- - 可直接调用 `bncr.diagnostics`。
388
- - 或看 `bncr.connect`/状态卡片中的 `diagnostics` 字段。
74
+ 如果 bncr 已成功连上,一般就说明插件安装和基础链路已经正常。
389
75
 
390
76
  ---
391
77
 
392
- ## 12. 版本提示
78
+ ## 说明
393
79
 
394
- 历史版本接入过的话,请以当前文档(push-only + 6 格式目标兼容 + 文件互传 V1 + 诊断字段)为准。
80
+ 如果你接触过旧版本,请以当前 README 和当前代码为准。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmoxmo/bncr",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
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
- // 优先使用可确定类型;“file”属于兜底,不覆盖更明确的 mime/扩展判断
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
- if (major === 'text') return 'text';
540
- if (major === 'image') return 'image';
541
- if (major === 'video') return 'video';
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
- const fn = asString(params.fileName || '').toLowerCase();
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 entries = Array.from(this.outbox.values())
976
- .filter((entry) => (filterAcc ? entry.accountId === filterAcc : true))
977
- .sort((a, b) => a.createdAt - b.createdAt);
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 changed = false;
980
- let nextDelay: number | null = null;
980
+ let globalNextDelay: number | null = null;
981
981
 
982
- for (const entry of entries) {
983
- if (!this.isOnline(entry.accountId)) continue;
982
+ for (const acc of targetAccounts) {
983
+ if (!acc || this.pushDrainRunningAccounts.has(acc)) continue;
984
+ if (!this.isOnline(acc)) continue;
984
985
 
985
- if (entry.nextAttemptAt > t) {
986
- const wait = entry.nextAttemptAt - t;
987
- nextDelay = nextDelay == null ? wait : Math.min(nextDelay, wait);
988
- continue;
989
- }
990
-
991
- const pushed = this.tryPushEntry(entry);
992
- if (pushed) {
993
- changed = true;
994
- continue;
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
- const nextAttempt = entry.retryCount + 1;
998
- if (nextAttempt > MAX_RETRY) {
999
- this.moveToDeadLetter(entry, entry.lastError || 'push-retry-limit');
1000
- changed = true;
1001
- continue;
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 (changed) this.scheduleSave();
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
- hintedType: 'file',
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.