@coclaw/openclaw-coclaw 0.18.0 → 0.19.2
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.js +62 -17
- package/openclaw.plugin.json +3 -0
- package/package.json +1 -1
- package/src/agent-cancel-heuristic.js +42 -0
- package/src/auto-upgrade/state.js +4 -3
- package/src/auto-upgrade/updater.js +2 -3
- package/src/common/claw-binding.js +53 -14
- package/src/device-identity.js +3 -7
- package/src/file-manager/handler.js +1 -1
- package/src/realtime-bridge.js +158 -105
- package/src/utils/atomic-write.js +37 -1
- package/src/utils/memory-queue.js +310 -0
- package/src/webrtc/agent-run-response.js +20 -0
- package/src/webrtc/dc-chunking.js +20 -1
- package/src/webrtc/rpc-dc-sender.js +178 -0
- package/src/webrtc/webrtc-peer.js +225 -65
- package/src/webrtc/rpc-send-queue.js +0 -271
|
@@ -1,271 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* rpc DataChannel 发送流控队列
|
|
3
|
-
*
|
|
4
|
-
* 针对 plugin 侧 rpc DC 的应用层流控:与 UI 侧 `webrtc-connection.js` 语义对齐,
|
|
5
|
-
* 但因插件运行在 gateway 进程内,必须对队列大小设硬/软上限,避免 OOM。
|
|
6
|
-
*
|
|
7
|
-
* 使用方式:每条 rpc DC 一个实例,绑定到 session.rpcSendQueue。
|
|
8
|
-
* - send(jsonStr):同步入口,fire-and-forget;返回 accepted/dropped
|
|
9
|
-
* - onBufferedAmountLow():由 DC `bufferedamountlow` 事件转调,触发 drain
|
|
10
|
-
* - close():DC 关闭时调用,清空队列并汇总 drop 统计
|
|
11
|
-
*
|
|
12
|
-
* 不做:Promise 送达保证;单条消息硬上限内的背压;自动重试。
|
|
13
|
-
*
|
|
14
|
-
* 契约(重要,修改 send/__drain/onBufferedAmountLow 时务必维持):
|
|
15
|
-
* send/__drain/onBufferedAmountLow 必须是「总函数」——任何分支都不得抛异常给调用方。
|
|
16
|
-
* 这是上游(webrtc-peer 的 broadcast/sendTo/files sendFn)能去掉 try/catch 的前提。
|
|
17
|
-
* 队列内部所有外部调用(buildChunks、dc.send、logger.*、remoteLog 等)都必须就地保护,
|
|
18
|
-
* 失败转化为 dropped 计数 / 静默吞掉,返回 false。日志输出统一走 __safeWarn / __safeInfo /
|
|
19
|
-
* __safeRemoteLog,避免外部传入的 logger 实现自身抛异常时破坏契约。
|
|
20
|
-
* 入参防御:jsonStr 必须是 string;非 string 直接 drop,避免 Buffer.byteLength 抛 TypeError。
|
|
21
|
-
* 仅构造器允许抛(参数校验,初始化期),运行期入口都不允许。
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
import { buildChunks } from './dc-chunking.js';
|
|
25
|
-
import { remoteLog } from '../remote-log.js';
|
|
26
|
-
|
|
27
|
-
/** 高水位:`dc.bufferedAmount >= HIGH` 时暂停 fast-path / drain */
|
|
28
|
-
export const DC_HIGH_WATER_MARK = 1024 * 1024; // 1 MB
|
|
29
|
-
/** 低水位:设置 `dc.bufferedAmountLowThreshold`,触发 `bufferedamountlow` 事件 */
|
|
30
|
-
export const DC_LOW_WATER_MARK = 256 * 1024; // 256 KB
|
|
31
|
-
/** 应用层队列软上限:`queueBytes >= MAX_QUEUE_BYTES` 时新消息被 drop */
|
|
32
|
-
export const MAX_QUEUE_BYTES = 10 * 1024 * 1024; // 10 MB
|
|
33
|
-
/** 单条消息硬上限(对齐 dc-chunking.js MAX_REASSEMBLY_BYTES,接收端重组不了也无意义) */
|
|
34
|
-
export const MAX_SINGLE_MSG_BYTES = 50 * 1024 * 1024; // 50 MB
|
|
35
|
-
|
|
36
|
-
export class RpcSendQueue {
|
|
37
|
-
/**
|
|
38
|
-
* @param {object} opts
|
|
39
|
-
* @param {object} opts.dc - DataChannel 实例(需支持 send / bufferedAmount / readyState)
|
|
40
|
-
* @param {number} opts.maxMessageSize - 对端 SDP 声明的 a=max-message-size
|
|
41
|
-
* @param {() => number} opts.getNextMsgId - 分片 msgId 生成器
|
|
42
|
-
* @param {object} [opts.logger] - pino 风格 logger
|
|
43
|
-
* @param {string} [opts.tag] - 诊断 tag(通常是 connId)
|
|
44
|
-
*/
|
|
45
|
-
constructor({ dc, maxMessageSize, getNextMsgId, logger, tag }) {
|
|
46
|
-
if (!dc) throw new Error('RpcSendQueue: dc is required');
|
|
47
|
-
this.dc = dc;
|
|
48
|
-
this.maxMessageSize = maxMessageSize;
|
|
49
|
-
this.getNextMsgId = getNextMsgId;
|
|
50
|
-
this.logger = logger ?? console;
|
|
51
|
-
this.tag = tag ?? '';
|
|
52
|
-
|
|
53
|
-
// 队列元素显式记录原始类型:drain 出口按 isString=true → string 帧,false → binary 帧。
|
|
54
|
-
// 早期实现统一存 Buffer,导致 string 帧被对端当分片残片静默丢弃
|
|
55
|
-
/** @type {{data: string|Buffer, isString: boolean, bytes: number}[]} */
|
|
56
|
-
this.queue = [];
|
|
57
|
-
this.queueBytes = 0;
|
|
58
|
-
this.closed = false;
|
|
59
|
-
|
|
60
|
-
// drop 统计(累计到 close 时汇总)
|
|
61
|
-
this.droppedCount = 0;
|
|
62
|
-
this.droppedBytes = 0;
|
|
63
|
-
// 队列"满"状态:仅 queue-full drop 触发 true;drain 下降到 < MAX 翻回 false
|
|
64
|
-
// single-msg-oversize drop 不影响此状态(它是应用 bug 性质,不代表队列压力)
|
|
65
|
-
this.queueOverflowActive = false;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* 同步发送一条 JSON 字符串。
|
|
70
|
-
* @param {string} jsonStr
|
|
71
|
-
* @returns {boolean} true=accepted(至少已入队或已直发),false=dropped
|
|
72
|
-
*/
|
|
73
|
-
send(jsonStr) {
|
|
74
|
-
if (this.closed || this.dc.readyState !== 'open') return false;
|
|
75
|
-
|
|
76
|
-
// 入参防御:契约要求 jsonStr 是 string;非 string 直接 drop(避免 Buffer.byteLength 抛 TypeError)
|
|
77
|
-
if (typeof jsonStr !== 'string') {
|
|
78
|
-
this.droppedCount += 1;
|
|
79
|
-
this.__safeWarn(`drop reason=non-string-input type=${typeof jsonStr}`);
|
|
80
|
-
return false;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// 诊断日志:打印每次入队的事件,跟踪 gateway 还会推哪些事件
|
|
84
|
-
// 需要时临时打开,平时保持注释避免日志噪音
|
|
85
|
-
// this.__safeInfo(`send-payload ${jsonStr}`);
|
|
86
|
-
|
|
87
|
-
// payload 字节:UTF-8 实际字节数,与对端 reassembly 上限同口径
|
|
88
|
-
const payloadBytes = Buffer.byteLength(jsonStr, 'utf8');
|
|
89
|
-
|
|
90
|
-
// 分片:异常需在 send 内吃掉,避免抛回 gateway 主循环(plugin 硬约束)
|
|
91
|
-
let chunks;
|
|
92
|
-
try {
|
|
93
|
-
chunks = buildChunks(jsonStr, this.maxMessageSize, this.getNextMsgId);
|
|
94
|
-
} catch (err) {
|
|
95
|
-
this.droppedCount += 1;
|
|
96
|
-
this.droppedBytes += payloadBytes;
|
|
97
|
-
const errMsg = err?.message ?? String(err);
|
|
98
|
-
this.__safeWarn(`drop reason=build-chunks-failed size=${payloadBytes} maxMessageSize=${this.maxMessageSize} err=${errMsg}`);
|
|
99
|
-
this.__safeRemoteLog(`rpc-queue.build-chunks-failed${this.__tagSuffix()} size=${payloadBytes} maxMessageSize=${this.maxMessageSize} err=${errMsg}`);
|
|
100
|
-
return false;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// 帧字节:含 5 字节 header 的实际网络字节,用于队列核算
|
|
104
|
-
const frameBytes = chunks
|
|
105
|
-
? chunks.reduce((n, c) => n + c.length, 0)
|
|
106
|
-
: payloadBytes;
|
|
107
|
-
|
|
108
|
-
// 硬上限:单条超限——按 payload 字节判断,对齐对端 reassembly payload 上限
|
|
109
|
-
// (帧字节因 header 累计可能在 payload 恰好不超时被误判 drop)
|
|
110
|
-
if (payloadBytes > MAX_SINGLE_MSG_BYTES) {
|
|
111
|
-
this.droppedCount += 1;
|
|
112
|
-
this.droppedBytes += frameBytes;
|
|
113
|
-
this.__safeWarn(`drop reason=single-msg-oversize size=${payloadBytes} cap=${MAX_SINGLE_MSG_BYTES}`);
|
|
114
|
-
return false;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// 软上限:队列已积压到 MAX(允许之前单条溢出,但新消息从此开始拒绝)
|
|
118
|
-
// 白名单豁免:agent run 类 RPC 响应(顶层 type=res + payload.runId 顶层存在)
|
|
119
|
-
// 即使队列已满也强行入队,避免 UI 端因 phase-2 res 被 drop 而无法收到 run 终态。
|
|
120
|
-
// 仍受 50MB 单条硬上限约束(接收端重组上限,超过也无意义)。
|
|
121
|
-
if (this.queueBytes >= MAX_QUEUE_BYTES && !isAgentRunResponse(jsonStr)) {
|
|
122
|
-
this.droppedCount += 1;
|
|
123
|
-
this.droppedBytes += frameBytes;
|
|
124
|
-
// 仅状态翻转点打 log(warn + remoteLog 各一次);overflow 持续期间所有 drop 静默累加,
|
|
125
|
-
// 避免 UI 离线 + ICE 失败导致 DC 永远不 drain 时的日志刷屏
|
|
126
|
-
if (!this.queueOverflowActive) {
|
|
127
|
-
this.queueOverflowActive = true;
|
|
128
|
-
this.__safeWarn(`overflow-start queueBytes=${this.queueBytes}`);
|
|
129
|
-
this.__safeRemoteLog(`rpc-queue.overflow-start${this.__tagSuffix()} queueBytes=${this.queueBytes}`);
|
|
130
|
-
}
|
|
131
|
-
return false;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// 不分片:单条 string 或 Buffer 直接处理
|
|
135
|
-
if (!chunks) {
|
|
136
|
-
if (this.queue.length === 0
|
|
137
|
-
&& this.dc.readyState === 'open'
|
|
138
|
-
&& this.dc.bufferedAmount < DC_HIGH_WATER_MARK) {
|
|
139
|
-
try {
|
|
140
|
-
this.dc.send(jsonStr);
|
|
141
|
-
return true;
|
|
142
|
-
} catch (err) {
|
|
143
|
-
this.__safeWarn(`fast-path send failed: ${err?.message}`);
|
|
144
|
-
return false;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
const bytes = Buffer.byteLength(jsonStr, 'utf8');
|
|
148
|
-
this.queue.push({ data: jsonStr, isString: true, bytes });
|
|
149
|
-
this.queueBytes += bytes;
|
|
150
|
-
return true;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// 分片:fast-path 尝试同步直发尽可能多的 chunk
|
|
154
|
-
// 循环条件与 __drain 一致:DC 仍 open 且 bufferedAmount 未顶到 HIGH
|
|
155
|
-
let i = 0;
|
|
156
|
-
if (this.queue.length === 0) {
|
|
157
|
-
while (i < chunks.length
|
|
158
|
-
&& this.dc.readyState === 'open'
|
|
159
|
-
&& this.dc.bufferedAmount < DC_HIGH_WATER_MARK) {
|
|
160
|
-
try {
|
|
161
|
-
this.dc.send(chunks[i]);
|
|
162
|
-
i += 1;
|
|
163
|
-
} catch (err) {
|
|
164
|
-
this.__safeWarn(`fast-path send failed at chunk ${i}/${chunks.length}: ${err?.message}`);
|
|
165
|
-
return false;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
// 剩余 chunk 原子性入队(保证同一消息分片连续,不被其他消息插入)
|
|
170
|
-
for (; i < chunks.length; i += 1) {
|
|
171
|
-
this.queue.push({ data: chunks[i], isString: false, bytes: chunks[i].length });
|
|
172
|
-
this.queueBytes += chunks[i].length;
|
|
173
|
-
}
|
|
174
|
-
return true;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/** 由外部 `dc.onbufferedamountlow` 事件触发 */
|
|
178
|
-
onBufferedAmountLow() {
|
|
179
|
-
this.__drain();
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* 关闭队列:清空待发送 chunks,汇总并 remoteLog drop 统计。幂等。
|
|
184
|
-
*/
|
|
185
|
-
close() {
|
|
186
|
-
if (this.closed) return;
|
|
187
|
-
this.closed = true;
|
|
188
|
-
const residual = this.queue.length;
|
|
189
|
-
const residualBytes = this.queueBytes;
|
|
190
|
-
this.queue.length = 0;
|
|
191
|
-
this.queueBytes = 0;
|
|
192
|
-
this.queueOverflowActive = false;
|
|
193
|
-
if (this.droppedCount > 0 || residual > 0) {
|
|
194
|
-
this.__safeRemoteLog(`rpc-queue.close${this.__tagSuffix()} dropped=${this.droppedCount} droppedBytes=${this.droppedBytes} residualChunks=${residual} residualBytes=${residualBytes}`);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/** @private 排队持续发送直至 HIGH 水位或队列空 */
|
|
199
|
-
__drain() {
|
|
200
|
-
if (this.closed) return;
|
|
201
|
-
const dc = this.dc;
|
|
202
|
-
while (this.queue.length > 0
|
|
203
|
-
&& dc.readyState === 'open'
|
|
204
|
-
&& dc.bufferedAmount < DC_HIGH_WATER_MARK) {
|
|
205
|
-
const item = this.queue[0];
|
|
206
|
-
try {
|
|
207
|
-
dc.send(item.data);
|
|
208
|
-
} catch (err) {
|
|
209
|
-
this.__safeWarn(`drain send failed: ${err?.message}`);
|
|
210
|
-
return; // 保留队列,等 onclose 统一清理
|
|
211
|
-
}
|
|
212
|
-
this.queue.shift();
|
|
213
|
-
this.queueBytes -= item.bytes;
|
|
214
|
-
// 满 → 未满 状态转换:打一条带累计数的 log,与 overflow-start 对称
|
|
215
|
-
if (this.queueOverflowActive && this.queueBytes < MAX_QUEUE_BYTES) {
|
|
216
|
-
this.queueOverflowActive = false;
|
|
217
|
-
this.__safeInfo(`overflow-end dropped=${this.droppedCount} droppedBytes=${this.droppedBytes}`);
|
|
218
|
-
this.__safeRemoteLog(`rpc-queue.overflow-end${this.__tagSuffix()} dropped=${this.droppedCount} droppedBytes=${this.droppedBytes}`);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/** @private */
|
|
224
|
-
__tagSuffix() {
|
|
225
|
-
return this.tag ? ` ${this.tag}` : '';
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* @private logger.warn 安全包装:吃掉 logger 自身抛的异常,保护 send/__drain 的 no-throw 契约
|
|
230
|
-
* @param {string} msg
|
|
231
|
-
*/
|
|
232
|
-
__safeWarn(msg) {
|
|
233
|
-
try { this.logger.warn?.(`[rpc-queue${this.__tagSuffix()}] ${msg}`); } catch { /* logger 自身坏了也不能让 send 抛 */ }
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/** @private 同 __safeWarn,info 级别 */
|
|
237
|
-
__safeInfo(msg) {
|
|
238
|
-
try { this.logger.info?.(`[rpc-queue${this.__tagSuffix()}] ${msg}`); } catch { /* logger 自身坏了也不能让 send 抛 */ }
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* @private remoteLog 安全包装。
|
|
243
|
-
* 当前 `remoteLog` 实现同步路径仅做数组操作 + flush().catch(),不抛异常,本 catch 块覆盖率因此为 0。
|
|
244
|
-
* 保留 try/catch 是为了让"send 不抛"契约不依赖 remoteLog 模块的具体实现——如果未来 remoteLog
|
|
245
|
-
* 内部加入可能抛的同步路径(如序列化、外部 sink 注入),此 wrapper 仍能兜底。
|
|
246
|
-
*/
|
|
247
|
-
__safeRemoteLog(text) {
|
|
248
|
-
try { remoteLog(text); } catch { /* 未来防御:见上方注释 */ }
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* 判断一条 JSON 字符串是否为带 runId 的 RPC 响应(用于队列满时白名单豁免)。
|
|
254
|
-
*
|
|
255
|
-
* 命中条件(仅看顶层):`type === 'res'` 且 `payload.runId` 为 truthy。
|
|
256
|
-
* 设计取舍:硬编码识别、不维护方法白名单表。该条件主要为覆盖 OpenClaw `agent` 二阶段 res
|
|
257
|
-
* 与 `agent.wait` 全部分支(accepted/ok/error/timeout/race/dedupe);同时也会顺带豁免
|
|
258
|
-
* `chat.send` 等其他顶层带 `runId` 的响应——这类 rsp 极小,加白无副作用。
|
|
259
|
-
* 解析失败或不命中按非白名单处理。
|
|
260
|
-
*
|
|
261
|
-
* @param {string} jsonStr - 待发送的 RPC 帧 JSON 字符串
|
|
262
|
-
* @returns {boolean} 命中白名单返回 true;解析失败或不命中返回 false
|
|
263
|
-
*/
|
|
264
|
-
function isAgentRunResponse(jsonStr) {
|
|
265
|
-
try {
|
|
266
|
-
const parsed = JSON.parse(jsonStr);
|
|
267
|
-
return parsed?.type === 'res' && Boolean(parsed?.payload?.runId);
|
|
268
|
-
} catch {
|
|
269
|
-
return false;
|
|
270
|
-
}
|
|
271
|
-
}
|