lightclawbot 1.2.5 → 1.2.6-beta.0

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.
@@ -11,6 +11,7 @@
11
11
  * - 消息标准化
12
12
  */
13
13
  import { stripTransportMetadata, extractFileAttachments, deduplicateFiles, } from "./text-processing.js";
14
+ import { normalizeUsage } from "../usage/index.js";
14
15
  // ============================================================
15
16
  // 系统注入消息检测
16
17
  // ============================================================
@@ -227,6 +228,24 @@ export function normalizeMessage(msg) {
227
228
  || undefined;
228
229
  toolResult = { toolCallId, name, output };
229
230
  }
231
+ // assistant 角色:归一 usage 字段(缺失时 normalizeUsage 返回 null)。
232
+ // OpenClaw jsonl 在 message 节点上挂 provider/model/api 三个上下文字段,
233
+ // 传给 normalizeUsage 用于回填到 UnifiedUsage 顶层。
234
+ const usage = normalizedRole === "assistant"
235
+ ? normalizeUsage(msg.usage, {
236
+ provider: typeof msg.provider === "string" ? msg.provider : undefined,
237
+ model: typeof msg.model === "string" ? msg.model : undefined,
238
+ api: typeof msg.api === "string" ? msg.api : undefined,
239
+ })
240
+ : null;
241
+ // 过滤全零 usage(来自 LLM 调用失败的场景,如 401 / 404 / 网络异常等):
242
+ // OpenClaw 对错误的 assistant 消息也会落盘 usage 字段,但所有 token 字段全是 0。
243
+ // 这种数据没有任何信息量,透出反而会误导用户在历史里看到「0 token」当作"无消耗"展示。
244
+ // 过滤后 HistoryMessage.usage 缺失 → 与"未知/未上报"语义一致,前端按 undefined 渲染即可。
245
+ const usageIsEmpty = usage
246
+ && !usage.inputTokens && !usage.outputTokens && !usage.totalTokens
247
+ && !usage.cachedInputTokens && !usage.cacheWriteTokens && !usage.reasoningTokens;
248
+ const effectiveUsage = usageIsEmpty ? null : usage;
230
249
  // 跳过完全空的消息
231
250
  if (!text && !toolCalls && !toolResult && !thinking && !files?.length)
232
251
  return null;
@@ -238,5 +257,6 @@ export function normalizeMessage(msg) {
238
257
  ...(toolResult && { toolResult }),
239
258
  ...(thinking && { thinking }),
240
259
  ...(files && files.length > 0 && { files }),
260
+ ...(effectiveUsage && { usage: effectiveUsage }),
241
261
  };
242
262
  }
@@ -11,6 +11,7 @@
11
11
  import fs from "node:fs";
12
12
  import { resolveTranscriptPath, loadSessionStore, resolveTranscriptPathBySessionId } from "./session-store.js";
13
13
  import { isSystemInjectedUserMessage, normalizeMessage } from "./message-parser.js";
14
+ import { aggregateUsageByTurn } from "./usage-aggregator.js";
14
15
  import { listCronSessions, classifySessionKey, extractCronJobId, extractCronJobIdsFromTranscript, findCronSessionsByJobIds, } from "./cron-utils.js";
15
16
  // ============================================================
16
17
  // 核心读取:单个 Session
@@ -30,7 +31,10 @@ export function readSessionHistory(sessionKey, opts) {
30
31
  return [];
31
32
  try {
32
33
  const raw = fs.readFileSync(filePath, "utf-8");
33
- return parseTranscriptLines(raw, { limit, chatOnly });
34
+ const messages = parseTranscriptLines(raw, { limit, chatOnly });
35
+ // 按"轮次"聚合 usage(详见 aggregateUsageByTurn 注释)
36
+ aggregateUsageByTurn(messages);
37
+ return messages;
34
38
  }
35
39
  catch {
36
40
  return [];
@@ -72,7 +76,10 @@ export function readSessionHistoryTail(sessionKey, opts) {
72
76
  if (readStart > 0 && lines.length > 0) {
73
77
  lines.shift();
74
78
  }
75
- return parseLines(lines, { limit, chatOnly });
79
+ const messages = parseLines(lines, { limit, chatOnly });
80
+ // 按"轮次"聚合 usage(详见 aggregateUsageByTurn 注释)
81
+ aggregateUsageByTurn(messages);
82
+ return messages;
76
83
  }
77
84
  catch {
78
85
  return [];
@@ -198,6 +205,10 @@ export function readSessionHistoryWithCron(sessionKey, opts) {
198
205
  // 6. 按时间合并
199
206
  const merged = [...mainMessages, ...cronMessages]
200
207
  .sort((a, b) => (a.timestamp ?? 0) - (b.timestamp ?? 0));
208
+ // 主会话与 cron 消息合并后再聚合一次:cron 触发的"system: ⏰ ..."类消息
209
+ // 通常被识别为 user 角色(系统注入),会自然分隔轮次,因此聚合不会把
210
+ // 主会话和 cron 提醒的 usage 错误合并。
211
+ aggregateUsageByTurn(merged);
201
212
  return merged.length > limit ? merged.slice(-limit) : merged;
202
213
  }
203
214
  // ============================================================
@@ -278,6 +289,10 @@ export function readSessionHistoriesByIds(sessionIds, opts) {
278
289
  return [];
279
290
  // 多段合并:按 timestamp 升序;无 timestamp 的消息按相对顺序兜底
280
291
  all.sort((a, b) => (a.timestamp ?? 0) - (b.timestamp ?? 0));
292
+ // 合并后再做 usage 轮次聚合:跨 sessionId 的同一对话回合也能被识别
293
+ // (注意:每个 sessionId 内部的消息已经在 readSessionHistory 中聚合过一次,
294
+ // 幂等,再聚合一次结果不变;此处主要解决跨 reset 的轮次合并场景)
295
+ aggregateUsageByTurn(all);
281
296
  return all.length > limit ? all.slice(-limit) : all;
282
297
  }
283
298
  // ============================================================
@@ -0,0 +1,53 @@
1
+ /**
2
+ * LightClaw — Usage 轮次聚合、一次问答只展示一次消耗。
3
+ */
4
+ /**
5
+ * 把每一轮中除"最后一条" assistant 之外的所有 assistant 消息的 usage 字段删掉。
6
+ * - **就地修改** 传入的 messages 数组中的 assistant 消息(不创建新数组);
7
+ * - 同轮内中间 assistant 消息:删掉 usage 字段;
8
+ * - 同轮内最后一条 assistant 消息:保持原 usage 不变;
9
+ * - 整轮内没有 usage 的消息不做任何修改。
10
+ *
11
+ * 调用时机:在 4 个公共读取入口(readSessionHistory / Tail / ByIds / WithCron)
12
+ * 的最后一步统一调用,确保聚合基于完整对话流。
13
+ *
14
+ * @param messages 已 normalize 完成的历史消息数组(按时间正序)
15
+ */
16
+ export function aggregateUsageByTurn(messages) {
17
+ // 当前轮内最后一条 assistant 的下标(轮次结束时它的 usage 保留)
18
+ let lastAssistantIdx = -1;
19
+ // 当前轮内除"最后一条"之外的 assistant 下标(这些节点的 usage 要被清空)
20
+ let intermediateAssistantIdxs = [];
21
+ /** 把当前轮的中间 assistant 节点 usage 全部清空,重置状态准备进入下一轮。 */
22
+ const flushTurn = () => {
23
+ for (const idx of intermediateAssistantIdxs) {
24
+ if (messages[idx]?.usage) {
25
+ delete messages[idx].usage;
26
+ }
27
+ }
28
+ lastAssistantIdx = -1;
29
+ intermediateAssistantIdxs = [];
30
+ };
31
+ for (let i = 0; i < messages.length; i += 1) {
32
+ const msg = messages[i];
33
+ // cron 提醒类消息(带 cronInfo)属于独立的提醒上下文,不参与主会话轮次聚合,
34
+ // 保留它们各自的 usage 原样展示。
35
+ if (msg.cronInfo)
36
+ continue;
37
+ if (msg.role === "user") {
38
+ // 新一轮开始前,先把上一轮的中间 assistant usage 清空
39
+ flushTurn();
40
+ continue;
41
+ }
42
+ if (msg.role !== "assistant")
43
+ continue; // tool / system 节点不参与
44
+ // 把上一个"最后一条"挪到中间列表(它的 usage 不再代表轮末,要删)
45
+ if (lastAssistantIdx >= 0) {
46
+ intermediateAssistantIdxs.push(lastAssistantIdx);
47
+ }
48
+ // 当前节点暂定为"最后一条"
49
+ lastAssistantIdx = i;
50
+ }
51
+ // 处理最后一轮(可能没有后续 user 消息触发 flushTurn)
52
+ flushTurn();
53
+ }
@@ -246,6 +246,9 @@ export function createInboundHandler(account, emitter, log) {
246
246
  log,
247
247
  effectiveApiKey,
248
248
  typingAlreadyStarted: true,
249
+ // 透传 sessionKey + agentId 给 sink,markComplete 时用来从 transcript 兜底读 usage
250
+ sessionKey: route?.sessionKey,
251
+ agentId: resolvedAgentId,
249
252
  }, { ...prefixOptions, onModelSelected }, signalCtx);
250
253
  // 8. 分发给 AI 引擎(真流式)。abort 由 openclaw fast-abort 处理,sink 层
251
254
  // 把英文回执汉化;此处仅兜底自身抛错。
@@ -5,7 +5,8 @@
5
5
  * - onPartialReply → 计算增量 → emit stream_chunk
6
6
  * - onToolStart → emit tool_start(前端决定如何渲染)
7
7
  * - sendFinalReply → 跨轮次去重后发送剩余增量(或丢弃)
8
- * - markComplete → 发 typing_stop;NO_REPLY 场景兜底一条提示
8
+ * - markComplete → 从 transcript 读 usage 并 emit usage 帧 → 发 typing_stop;
9
+ * NO_REPLY 场景兜底一条提示
9
10
  *
10
11
  * 关键状态:
11
12
  * streamedText 当前 assistant 轮已流式推送的文本
@@ -17,6 +18,7 @@ import { uploadFileToServer } from "../file-storage.js";
17
18
  import { mediaUrlsToFiles } from "../media.js";
18
19
  import { createDeltaTrackerState, toStreamDeltaText } from "./delta-tracker.js";
19
20
  import { emitSignal } from "../utils/common.js";
21
+ import { readSessionHistoryTail } from "../history/index.js";
20
22
  /** 派生/管理 subagent 的工具名;前端可据此特化渲染 tool_start。 */
21
23
  const SUBAGENT_TOOL_NAMES = new Set([
22
24
  "sessions_spawn",
@@ -30,7 +32,7 @@ function localizeAbortReplyText(text) {
30
32
  return OPENCLAW_ABORT_REPLY_RE.test(text.trim()) ? LOCALIZED_ABORT_REPLY : text;
31
33
  }
32
34
  export function createStreamReplyConfig(opts, prefixOptions, signalCtx) {
33
- const { emitter, targetId, originalMsgId, log, effectiveApiKey, typingAlreadyStarted } = opts;
35
+ const { emitter, targetId, originalMsgId, log, effectiveApiKey, typingAlreadyStarted, sessionKey, agentId } = opts;
34
36
  // ── 增量追踪 & 已推送文本 ──
35
37
  let partialReplyState = createDeltaTrackerState();
36
38
  let streamedText = "";
@@ -54,6 +56,10 @@ export function createStreamReplyConfig(opts, prefixOptions, signalCtx) {
54
56
  let completed = false;
55
57
  /** openclaw abort 检测:sendFinalReply 收到 abort 文案时置 true,markComplete 据此跳过 NO_REPLY。 */
56
58
  let abortDetected = false;
59
+ // ── Token 用量 ──
60
+ // 把每一轮的 usage 写入 transcript jsonl(与 history 模块同源),
61
+ // 因此在 markComplete 时从 transcript 末尾读最近一条 assistant 消息的 usage,
62
+ // 然后 emit `kind='usage'` 帧给前端。
57
63
  // ── 串行化回调队列:保证 COS 上传等异步操作按顺序执行 ──
58
64
  let queuePromise = Promise.resolve();
59
65
  const enqueue = (fn) => {
@@ -223,13 +229,18 @@ export function createStreamReplyConfig(opts, prefixOptions, signalCtx) {
223
229
  getQueuedCounts: () => ({ ...counts }),
224
230
  getFailedCounts: () => ({ tool: 0, block: 0, final: 0 }),
225
231
  /**
226
- * 标记本次 dispatch 完成 → 发 typing_stop。
232
+ * 标记本次 dispatch 完成 → 发 usage 帧(如有)→ 发 typing_stop。
227
233
  *
228
234
  * NO_REPLY 仅在"LLM 正常完成一次完整 run 但没产出可见文本"时才触发。
229
235
  * 以下场景均不发:
230
236
  * - isAborted / abortDetected:/stop 或 abort 触发
231
237
  * - hadLLMActivity 但 hadDispatch=false:LLM 启动但被中途打断(abort 没来得及 sendFinalReply)
232
238
  * - !hadLLMActivity:followup 被 collect 合并 / plugin 拦截
239
+ *
240
+ * Token 用量帧的 emit 时机:
241
+ * 在 typing_stop **之前** 从 transcript 读 usage 并 emit 一次。前端依次收到:
242
+ * ...stream_chunk → tool_start/end → ... → usage(可选) → typing_stop
243
+ * 这样保证"一次问答一次消耗",且消耗信息在结束态前到达。
233
244
  */
234
245
  markComplete: (markOpts) => {
235
246
  completed = true;
@@ -263,6 +274,47 @@ export function createStreamReplyConfig(opts, prefixOptions, signalCtx) {
263
274
  `toolStart=${toolStartCount} assistantMsg=${assistantMessageCount} ` +
264
275
  `streamedLen=${streamedText.length} historyRounds=${streamedHistory.length}`);
265
276
  }
277
+ // 在 typing_stop 之前 emit 一帧 usage(如有)。
278
+ // 数据源:从 transcript jsonl 读最近一条 assistant 消息的 usage(与 history 模块同源)。
279
+ // abort / 静默 dispatch / NO_REPLY 等异常路径下 transcript 可能没有可读 usage,
280
+ // 此时不 emit,前端按"无用量数据"渲染。
281
+ let usageToEmit;
282
+ // 非 abort + sessionKey 已知 → 从 transcript 读 usage
283
+ if (!isAborted && sessionKey) {
284
+ try {
285
+ // 找到本轮最后一条 assistant
286
+ const tailMessages = readSessionHistoryTail(sessionKey, {
287
+ limit: 10,
288
+ chatOnly: true,
289
+ agentId,
290
+ });
291
+ // 找到最后一条 assistant 消息(一轮的最后一条 = 本次问答的累计 usage)
292
+ let foundUsage;
293
+ for (let i = tailMessages.length - 1; i >= 0; i -= 1) {
294
+ const m = tailMessages[i];
295
+ if (m.role === "assistant" && m.usage) {
296
+ foundUsage = m.usage;
297
+ break;
298
+ }
299
+ }
300
+ if (foundUsage) {
301
+ usageToEmit = foundUsage;
302
+ }
303
+ else {
304
+ log?.warn(`[${CHANNEL_KEY}] [stream] no assistant.usage found in transcript tail ` +
305
+ `(messages=${tailMessages.length})`);
306
+ }
307
+ }
308
+ catch (err) {
309
+ log?.warn(`[${CHANNEL_KEY}] [stream] read transcript failed: ` +
310
+ `${err instanceof Error ? err.message : String(err)}`);
311
+ }
312
+ }
313
+ if (usageToEmit) {
314
+ log?.info(`[${CHANNEL_KEY}] [stream] emit final usage: ` +
315
+ `input=${usageToEmit.inputTokens} output=${usageToEmit.outputTokens} total=${usageToEmit.totalTokens}`);
316
+ emitSignal(signalCtx, "usage", "", undefined, { usage: usageToEmit });
317
+ }
266
318
  sendTypingStop();
267
319
  },
268
320
  };
@@ -0,0 +1,4 @@
1
+ /**
2
+ * LightClaw — Usage 模块
3
+ */
4
+ export { normalizeUsage } from './normalize.js';
@@ -0,0 +1,47 @@
1
+ /**
2
+ * LightClaw — Usage 归一函数
3
+ *
4
+ * 当前阶段直接读取 OpenClaw 的字段(input/output/...),
5
+ * 未来如果遇到和当前 OpenClaw 不一致的agent或者其他模型需要兼容
6
+ * 可以把本函数升级为「多适配器」,对外 API保持不变,调用方 0 改动。
7
+ */
8
+ export function normalizeUsage(raw, ctx = {}) {
9
+ if (!raw || typeof raw !== 'object')
10
+ return null;
11
+ const r = raw;
12
+ // OpenClaw 落盘格式的字段直接取(v1:input/output;v2:inputTokens/outputTokens 双兼容)
13
+ const inputTokens = pickNumber(r, ['inputTokens', 'input']);
14
+ const outputTokens = pickNumber(r, ['outputTokens', 'output']);
15
+ // 三者全无则视为无效数据
16
+ if (inputTokens === undefined && outputTokens === undefined) {
17
+ const totalOnly = pickNumber(r, ['totalTokens', 'total']);
18
+ if (totalOnly === undefined)
19
+ return null;
20
+ }
21
+ const totalTokensRaw = pickNumber(r, ['totalTokens', 'total']);
22
+ const totalTokens = totalTokensRaw ?? (inputTokens ?? 0) + (outputTokens ?? 0);
23
+ return {
24
+ inputTokens,
25
+ outputTokens,
26
+ totalTokens,
27
+ cachedInputTokens: pickNumber(r, ['cachedInputTokens', 'cacheRead']),
28
+ cacheWriteTokens: pickNumber(r, ['cacheWriteTokens', 'cacheWrite']),
29
+ reasoningTokens: pickNumber(r, ['reasoningTokens']),
30
+ model: ctx.model,
31
+ provider: ctx.provider,
32
+ raw: r,
33
+ };
34
+ }
35
+ /**
36
+ * 在对象中按候选键名顺序查找第一个 finite number 字段。
37
+ * 抽出此函数是为了让字段命名兼容(v1 短名 / v2 长名)逻辑保持一处,
38
+ * 后续如果 OpenClaw 又新增字段命名(比如 inputTokenCount),只需在候选列表里追加。
39
+ */
40
+ function pickNumber(obj, keys) {
41
+ for (const key of keys) {
42
+ const v = obj[key];
43
+ if (typeof v === 'number' && Number.isFinite(v))
44
+ return v;
45
+ }
46
+ return undefined;
47
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -50,12 +50,14 @@ export function buildAuthHeaders(apiKey) {
50
50
  };
51
51
  }
52
52
  /**
53
- * 统一的信号发送出口,收敛 typing / stream / tool 控制帧的构造逻辑。
53
+ * 统一的信号发送出口,收敛 typing / stream / tool / usage 控制帧的构造逻辑。
54
54
  *
55
55
  * - typing_start 不带 replyToMsgId(协议要求);其余帧都携带。
56
- * - extra 用于透传 kind 相关字段(如 tool_start 的 toolName/toolPhase、tool_end 的 toolIsError)。
56
+ * - topLevelExtra 用于透传顶层 kind 相关字段(如 tool_start 的 toolName/toolPhase、tool_end 的 toolIsError)。
57
+ * - innerExtra 用于在 PrivateMessageData.extra 内追加自定义字段(如 usage 帧的 usage 对象)。
58
+ * chatId 始终由本函数管理,调用方无需也不能在 innerExtra 中传 chatId(会被覆盖)。
57
59
  */
58
- export function emitSignal(ctx, kind, content = '', extra) {
60
+ export function emitSignal(ctx, kind, content = '', topLevelExtra, innerExtra) {
59
61
  const { emitter, targetId, replyMsgId, originalMsgId, agentId, chatId } = ctx;
60
62
  return emitter.emit({
61
63
  msgId: replyMsgId,
@@ -65,9 +67,10 @@ export function emitSignal(ctx, kind, content = '', extra) {
65
67
  timestamp: Date.now(),
66
68
  kind,
67
69
  ...(kind !== 'typing_start' ? { replyToMsgId: originalMsgId } : {}),
68
- ...extra,
70
+ ...topLevelExtra,
69
71
  agentId,
70
- extra: { chatId: chatId ?? '' },
72
+ // chatId 始终位于 extra.chatId;innerExtra 在前展开,确保 chatId 不被覆盖
73
+ extra: { ...(innerExtra ?? {}), chatId: chatId ?? '' },
71
74
  });
72
75
  }
73
76
  /**
@@ -40,6 +40,10 @@ class Receiver extends Writable {
40
40
  * extensions
41
41
  * @param {Boolean} [options.isServer=false] Specifies whether to operate in
42
42
  * client or server mode
43
+ * @param {Number} [options.maxBufferedChunks=0] The maximum number of
44
+ * buffered data chunks
45
+ * @param {Number} [options.maxFragments=0] The maximum number of message
46
+ * fragments
43
47
  * @param {Number} [options.maxPayload=0] The maximum allowed message length
44
48
  * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
45
49
  * not to skip UTF-8 validation for text and close messages
@@ -54,6 +58,8 @@ class Receiver extends Writable {
54
58
  this._binaryType = options.binaryType || BINARY_TYPES[0];
55
59
  this._extensions = options.extensions || {};
56
60
  this._isServer = !!options.isServer;
61
+ this._maxBufferedChunks = options.maxBufferedChunks | 0;
62
+ this._maxFragments = options.maxFragments | 0;
57
63
  this._maxPayload = options.maxPayload | 0;
58
64
  this._skipUTF8Validation = !!options.skipUTF8Validation;
59
65
  this[kWebSocket] = undefined;
@@ -89,6 +95,22 @@ class Receiver extends Writable {
89
95
  _write(chunk, encoding, cb) {
90
96
  if (this._opcode === 0x08 && this._state == GET_INFO) return cb();
91
97
 
98
+ if (
99
+ this._maxBufferedChunks > 0 &&
100
+ this._buffers.length >= this._maxBufferedChunks
101
+ ) {
102
+ cb(
103
+ this.createError(
104
+ RangeError,
105
+ 'Too many buffered chunks',
106
+ false,
107
+ 1008,
108
+ 'WS_ERR_TOO_MANY_BUFFERED_PARTS'
109
+ )
110
+ );
111
+ return;
112
+ }
113
+
92
114
  this._bufferedBytes += chunk.length;
93
115
  this._buffers.push(chunk);
94
116
  this.startLoop(cb);
@@ -485,6 +507,22 @@ class Receiver extends Writable {
485
507
  }
486
508
 
487
509
  if (data.length) {
510
+ if (
511
+ this._maxFragments > 0 &&
512
+ this._fragments.length >= this._maxFragments
513
+ ) {
514
+ const error = this.createError(
515
+ RangeError,
516
+ 'Too many message fragments',
517
+ false,
518
+ 1008,
519
+ 'WS_ERR_TOO_MANY_BUFFERED_PARTS'
520
+ );
521
+
522
+ cb(error);
523
+ return;
524
+ }
525
+
488
526
  //
489
527
  // This message is not compressed so its length is the sum of the payload
490
528
  // length of all fragments.
@@ -524,6 +562,22 @@ class Receiver extends Writable {
524
562
  return;
525
563
  }
526
564
 
565
+ if (
566
+ this._maxFragments > 0 &&
567
+ this._fragments.length >= this._maxFragments
568
+ ) {
569
+ const error = this.createError(
570
+ RangeError,
571
+ 'Too many message fragments',
572
+ false,
573
+ 1008,
574
+ 'WS_ERR_TOO_MANY_BUFFERED_PARTS'
575
+ );
576
+
577
+ cb(error);
578
+ return;
579
+ }
580
+
527
581
  this._fragments.push(buf);
528
582
  }
529
583
 
@@ -43,6 +43,10 @@ class WebSocketServer extends EventEmitter {
43
43
  * called
44
44
  * @param {Function} [options.handleProtocols] A hook to handle protocols
45
45
  * @param {String} [options.host] The hostname where to bind the server
46
+ * @param {Number} [options.maxBufferedChunks=1048576] The maximum number of
47
+ * buffered data chunks
48
+ * @param {Number} [options.maxFragments=131072] The maximum number of message
49
+ * fragments
46
50
  * @param {Number} [options.maxPayload=104857600] The maximum allowed message
47
51
  * size
48
52
  * @param {Boolean} [options.noServer=false] Enable no server mode
@@ -65,6 +69,8 @@ class WebSocketServer extends EventEmitter {
65
69
  options = {
66
70
  allowSynchronousEvents: true,
67
71
  autoPong: true,
72
+ maxBufferedChunks: 1024 * 1024,
73
+ maxFragments: 128 * 1024,
68
74
  maxPayload: 100 * 1024 * 1024,
69
75
  skipUTF8Validation: false,
70
76
  perMessageDeflate: false,
@@ -424,6 +430,8 @@ class WebSocketServer extends EventEmitter {
424
430
 
425
431
  ws.setSocket(socket, head, {
426
432
  allowSynchronousEvents: this.options.allowSynchronousEvents,
433
+ maxBufferedChunks: this.options.maxBufferedChunks,
434
+ maxFragments: this.options.maxFragments,
427
435
  maxPayload: this.options.maxPayload,
428
436
  skipUTF8Validation: this.options.skipUTF8Validation
429
437
  });
@@ -201,6 +201,10 @@ class WebSocket extends EventEmitter {
201
201
  * multiple times in the same tick
202
202
  * @param {Function} [options.generateMask] The function used to generate the
203
203
  * masking key
204
+ * @param {Number} [options.maxBufferedChunks=0] The maximum number of
205
+ * buffered data chunks
206
+ * @param {Number} [options.maxFragments=0] The maximum number of message
207
+ * fragments
204
208
  * @param {Number} [options.maxPayload=0] The maximum allowed message size
205
209
  * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
206
210
  * not to skip UTF-8 validation for text and close messages
@@ -212,6 +216,8 @@ class WebSocket extends EventEmitter {
212
216
  binaryType: this.binaryType,
213
217
  extensions: this._extensions,
214
218
  isServer: this._isServer,
219
+ maxBufferedChunks: options.maxBufferedChunks,
220
+ maxFragments: options.maxFragments,
215
221
  maxPayload: options.maxPayload,
216
222
  skipUTF8Validation: options.skipUTF8Validation
217
223
  });
@@ -640,6 +646,10 @@ module.exports = WebSocket;
640
646
  * masking key
641
647
  * @param {Number} [options.handshakeTimeout] Timeout in milliseconds for the
642
648
  * handshake request
649
+ * @param {Number} [options.maxBufferedChunks=1048576] The maximum number of
650
+ * buffered data chunks
651
+ * @param {Number} [options.maxFragments=131072] The maximum number of message
652
+ * fragments
643
653
  * @param {Number} [options.maxPayload=104857600] The maximum allowed message
644
654
  * size
645
655
  * @param {Number} [options.maxRedirects=10] The maximum number of redirects
@@ -660,6 +670,8 @@ function initAsClient(websocket, address, protocols, options) {
660
670
  autoPong: true,
661
671
  closeTimeout: CLOSE_TIMEOUT,
662
672
  protocolVersion: protocolVersions[1],
673
+ maxBufferedChunks: 1024 * 1024,
674
+ maxFragments: 128 * 1024,
663
675
  maxPayload: 100 * 1024 * 1024,
664
676
  skipUTF8Validation: false,
665
677
  perMessageDeflate: true,
@@ -1017,6 +1029,8 @@ function initAsClient(websocket, address, protocols, options) {
1017
1029
  websocket.setSocket(socket, head, {
1018
1030
  allowSynchronousEvents: opts.allowSynchronousEvents,
1019
1031
  generateMask: opts.generateMask,
1032
+ maxBufferedChunks: opts.maxBufferedChunks,
1033
+ maxFragments: opts.maxFragments,
1020
1034
  maxPayload: opts.maxPayload,
1021
1035
  skipUTF8Validation: opts.skipUTF8Validation
1022
1036
  });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ws",
3
- "version": "8.20.1",
3
+ "version": "8.21.0",
4
4
  "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js",
5
5
  "keywords": [
6
6
  "HyBi",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lightclawbot",
3
- "version": "1.2.5",
3
+ "version": "1.2.6-beta.0",
4
4
  "description": "LightClawBot channel plugin with message support, cron jobs, and proactive messaging",
5
5
  "type": "module",
6
6
  "files": [