@coclaw/openclaw-coclaw 0.17.0 → 0.17.1
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/package.json
CHANGED
package/src/realtime-bridge.js
CHANGED
|
@@ -659,6 +659,14 @@ export class RealtimeBridge {
|
|
|
659
659
|
return;
|
|
660
660
|
}
|
|
661
661
|
if (payload.type === 'res' || payload.type === 'event') {
|
|
662
|
+
// 过滤 gateway 的管理层广播事件,这些对 WebChat / plugin 客户端无意义:
|
|
663
|
+
// - health: 全量状态快照(~3KB, ~60s 一次 + RPC 触发),给 Admin UI 的监控仪表盘用
|
|
664
|
+
// - tick: gateway WS 保活心跳(30s 一次),UI 隔着 DC 不需要,DC 自己有 probe 机制
|
|
665
|
+
// 不转发可避免后台时 rpc DC 队列被灌满。上游支持按需订阅前先在插件侧拦截。
|
|
666
|
+
if (payload.type === 'event'
|
|
667
|
+
&& (payload.event === 'health' || payload.event === 'tick')) {
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
662
670
|
this.webrtcPeer?.broadcast(payload);
|
|
663
671
|
}
|
|
664
672
|
});
|
|
@@ -62,6 +62,10 @@ export class RpcSendQueue {
|
|
|
62
62
|
send(jsonStr) {
|
|
63
63
|
if (this.closed || this.dc.readyState !== 'open') return false;
|
|
64
64
|
|
|
65
|
+
// 诊断日志:打印每次入队的事件,跟踪 gateway 还会推哪些事件
|
|
66
|
+
// 需要时临时打开,平时保持注释避免日志噪音
|
|
67
|
+
// this.logger.info?.(`[rpc-queue${this.__tagSuffix()}] send-payload ${jsonStr}`);
|
|
68
|
+
|
|
65
69
|
const chunks = buildChunks(jsonStr, this.maxMessageSize, this.getNextMsgId);
|
|
66
70
|
const totalBytes = chunks
|
|
67
71
|
? chunks.reduce((n, c) => n + c.length, 0)
|
|
@@ -80,6 +84,8 @@ export class RpcSendQueue {
|
|
|
80
84
|
this.droppedCount += 1;
|
|
81
85
|
this.droppedBytes += totalBytes;
|
|
82
86
|
this.logger.warn?.(`[rpc-queue${this.__tagSuffix()}] drop reason=queue-full size=${totalBytes} queueBytes=${this.queueBytes}`);
|
|
87
|
+
// 诊断日志:定位后台长时间占队的事件来源。需要时临时打开
|
|
88
|
+
// this.logger.info?.(`[rpc-queue${this.__tagSuffix()}] dropped-payload ${jsonStr}`);
|
|
83
89
|
if (!this.queueOverflowActive) {
|
|
84
90
|
this.queueOverflowActive = true;
|
|
85
91
|
remoteLog(`rpc-queue.overflow-start${this.__tagSuffix()} queueBytes=${this.queueBytes}`);
|
|
@@ -69,6 +69,17 @@ export class WebRtcPeer {
|
|
|
69
69
|
clearTimeout(session.__failedTimer);
|
|
70
70
|
session.__failedTimer = null;
|
|
71
71
|
}
|
|
72
|
+
// 清理 plugin-probe 定时器(避免 session 已关闭仍触发 timeout 日志,
|
|
73
|
+
// 或 500ms 调度窗口内 session 被替换时对着新 session 误发探针)
|
|
74
|
+
if (session.__pluginProbeSchedTimer) {
|
|
75
|
+
clearTimeout(session.__pluginProbeSchedTimer);
|
|
76
|
+
session.__pluginProbeSchedTimer = null;
|
|
77
|
+
}
|
|
78
|
+
if (session.__pluginProbeTimer) {
|
|
79
|
+
clearTimeout(session.__pluginProbeTimer);
|
|
80
|
+
session.__pluginProbeTimer = null;
|
|
81
|
+
session.__pluginProbeInFlight = null;
|
|
82
|
+
}
|
|
72
83
|
this.__sessions.delete(connId);
|
|
73
84
|
// 显式关闭 rpc 发送队列:dc.onclose 路径中 `sessions.get(connId)` 已返回 undefined 而短路,
|
|
74
85
|
// 此处不主动 close 会丢失 drop 汇总 remoteLog 诊断
|
|
@@ -83,6 +94,9 @@ export class WebRtcPeer {
|
|
|
83
94
|
if ('onselectedcandidatepairchange' in session.pc) {
|
|
84
95
|
session.pc.onselectedcandidatepairchange = null;
|
|
85
96
|
}
|
|
97
|
+
if ('oniceconnectionstatechange' in session.pc) {
|
|
98
|
+
session.pc.oniceconnectionstatechange = null;
|
|
99
|
+
}
|
|
86
100
|
await session.pc.close();
|
|
87
101
|
this.__remoteLog(`rtc.closed conn=${connId}`);
|
|
88
102
|
this.logger.info?.(`${this.__rtcTag} [${connId}] closed`);
|
|
@@ -175,6 +189,7 @@ export class WebRtcPeer {
|
|
|
175
189
|
toConnId: connId,
|
|
176
190
|
payload: { sdp: answer.sdp },
|
|
177
191
|
});
|
|
192
|
+
this.__remoteLog(`rtc.restart-answer-sent conn=${connId}`);
|
|
178
193
|
this.logger.info?.(`${this.__rtcTag} ICE restart answer sent to ${connId}`);
|
|
179
194
|
return;
|
|
180
195
|
} catch (err) {
|
|
@@ -245,11 +260,24 @@ export class WebRtcPeer {
|
|
|
245
260
|
this.__sessions.set(connId, session);
|
|
246
261
|
|
|
247
262
|
// ICE candidate → 发给 UI,并统计各类型 candidate 数量
|
|
263
|
+
// gather complete 时一并输出 host 候选的 IP:port 列表(诊断 docker/vbridge 误 gather)
|
|
248
264
|
const candidateCounts = { host: 0, srflx: 0, relay: 0 };
|
|
265
|
+
const hostAddrs = [];
|
|
266
|
+
let gatheringEmitted = false;
|
|
267
|
+
const flushGatherDiag = () => {
|
|
268
|
+
if (gatheringEmitted) return;
|
|
269
|
+
gatheringEmitted = true;
|
|
270
|
+
const hostInfo = hostAddrs.length ? ` hosts=${hostAddrs.join(',')}` : '';
|
|
271
|
+
this.__remoteLog(`rtc.ice-gathered conn=${connId} host=${candidateCounts.host} srflx=${candidateCounts.srflx} relay=${candidateCounts.relay}${hostInfo}`);
|
|
272
|
+
candidateCounts.host = 0;
|
|
273
|
+
candidateCounts.srflx = 0;
|
|
274
|
+
candidateCounts.relay = 0;
|
|
275
|
+
hostAddrs.length = 0;
|
|
276
|
+
};
|
|
249
277
|
pc.onicecandidate = ({ candidate }) => {
|
|
250
278
|
if (!candidate) {
|
|
251
|
-
// gathering
|
|
252
|
-
|
|
279
|
+
// 浏览器路径:gathering 完成通过 null candidate 通知
|
|
280
|
+
flushGatherDiag();
|
|
253
281
|
return;
|
|
254
282
|
}
|
|
255
283
|
// 从 candidate 字符串中提取类型(typ host / typ srflx / typ relay)
|
|
@@ -257,6 +285,14 @@ export class WebRtcPeer {
|
|
|
257
285
|
if (typMatch && candidateCounts[typMatch[1]] !== undefined) {
|
|
258
286
|
candidateCounts[typMatch[1]]++;
|
|
259
287
|
}
|
|
288
|
+
// host 候选记录 addr:port,用于观察 pion 是否把 docker0 / br-* / loopback 等接口当成 host
|
|
289
|
+
// candidate 格式: "candidate:<foundation> <comp> <proto> <prio> <ADDR> <PORT> typ host ..."
|
|
290
|
+
if (typMatch?.[1] === 'host') {
|
|
291
|
+
const parts = candidate.candidate.split(' ');
|
|
292
|
+
if (parts.length >= 6) {
|
|
293
|
+
hostAddrs.push(`${parts[4]}:${parts[5]}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
260
296
|
this.__onSend({
|
|
261
297
|
type: 'rtc:ice',
|
|
262
298
|
toConnId: connId,
|
|
@@ -267,6 +303,29 @@ export class WebRtcPeer {
|
|
|
267
303
|
},
|
|
268
304
|
});
|
|
269
305
|
};
|
|
306
|
+
// pion-node 不会在 gather complete 时 fire onicecandidate(null),用 icegatheringstatechange 兜底。
|
|
307
|
+
// gathering→ 重置 flag 支持 ICE restart;complete→ flush 汇总
|
|
308
|
+
if ('onicegatheringstatechange' in pc) {
|
|
309
|
+
pc.onicegatheringstatechange = () => {
|
|
310
|
+
const state = pc.iceGatheringState;
|
|
311
|
+
if (state === 'gathering') {
|
|
312
|
+
gatheringEmitted = false;
|
|
313
|
+
} else if (state === 'complete') {
|
|
314
|
+
flushGatherDiag();
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ICE agent 状态(pion 暴露的独立事件):能看到 checking / connected / failed 等纯 ICE 侧跳转,
|
|
320
|
+
// 与复合 connectionState 互补。对诊断"pion 说 connected 但 UI 看不到数据"非常关键。
|
|
321
|
+
// 仅在 pion-node 实现中可用;其他实现赋值是 no-op。
|
|
322
|
+
if ('oniceconnectionstatechange' in pc) {
|
|
323
|
+
pc.oniceconnectionstatechange = () => {
|
|
324
|
+
const cur = this.__sessions.get(connId);
|
|
325
|
+
if (!cur || cur.pc !== pc) return;
|
|
326
|
+
this.__remoteLog(`rtc.iceState conn=${connId} ${pc.iceConnectionState ?? '?'}`);
|
|
327
|
+
};
|
|
328
|
+
}
|
|
270
329
|
|
|
271
330
|
// 连接状态变更(校验 pc 归属,防止旧 PC 异步回调删除新 session)
|
|
272
331
|
pc.onconnectionstatechange = () => {
|
|
@@ -285,6 +344,7 @@ export class WebRtcPeer {
|
|
|
285
344
|
}
|
|
286
345
|
|
|
287
346
|
if (state === 'connected') {
|
|
347
|
+
const prevDumpState = cur.__lastDumpState;
|
|
288
348
|
// 重置 dump 去重水位(disconnected → connected → disconnected 仍能再 dump)
|
|
289
349
|
cur.__lastDumpState = null;
|
|
290
350
|
// werift: iceTransports[0].connection.nominated
|
|
@@ -298,6 +358,22 @@ export class WebRtcPeer {
|
|
|
298
358
|
this.logger.info?.(`${this.__rtcTag} [${connId}] ICE nominated: local=${localInfo} remote=${remoteInfo}`);
|
|
299
359
|
}
|
|
300
360
|
// pion: pair 通过独立的 selectedcandidatepairchange 事件上报
|
|
361
|
+
// ICE restart 恢复(disconnected/failed → connected)时做诊断动作:
|
|
362
|
+
// - dump 当前 session DC 状态,对照"UI 看不到 connected 时 plugin 侧看到什么"
|
|
363
|
+
// - 发一次 plugin-probe,实测 DC 是否双向可用
|
|
364
|
+
// 只对 pion 生效:werift/ndc 为兼容路径,不涉及本次调查的病态场景。
|
|
365
|
+
if (this.__impl === 'pion' && (prevDumpState === 'disconnected' || prevDumpState === 'failed')) {
|
|
366
|
+
this.__dumpSessionState(connId, cur, 'connected');
|
|
367
|
+
// 挂到 session 上,使 closeByConnId 能在 500ms 窗口内取消;
|
|
368
|
+
// 否则 session 被替换(同 connId 新 offer)时会对着新 session 误发探针。
|
|
369
|
+
if (cur.__pluginProbeSchedTimer) clearTimeout(cur.__pluginProbeSchedTimer);
|
|
370
|
+
cur.__pluginProbeSchedTimer = setTimeout(() => {
|
|
371
|
+
cur.__pluginProbeSchedTimer = null;
|
|
372
|
+
this.__sendPluginProbe(connId);
|
|
373
|
+
}, 500);
|
|
374
|
+
// unref() 避免定时器阻塞 gateway 进程退出(gateway 由其他连接保活)。
|
|
375
|
+
cur.__pluginProbeSchedTimer.unref?.();
|
|
376
|
+
}
|
|
301
377
|
} else if (state === 'disconnected' || state === 'failed' || state === 'closed') {
|
|
302
378
|
// 诊断 dump:失败/断连/关闭时输出当前 PC 上 DC 状态,定位"PC 假活/DC 死"现象
|
|
303
379
|
// - closed 由 closeByConnId 接管清理,dump 收敛诊断噪声
|
|
@@ -427,6 +503,11 @@ export class WebRtcPeer {
|
|
|
427
503
|
catch { /* DC 已关闭,忽略 */ }
|
|
428
504
|
return;
|
|
429
505
|
}
|
|
506
|
+
// 来自 UI 的 plugin-probe 回复:验证 plugin → UI 方向确实传达并被回传
|
|
507
|
+
if (payload.type === 'plugin-probe-ack') {
|
|
508
|
+
this.__handlePluginProbeAck(connId, payload.id);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
430
511
|
if (payload.type === 'req') {
|
|
431
512
|
// coclaw.files.* 方法本地处理,不转发 gateway
|
|
432
513
|
if (payload.method?.startsWith('coclaw.files.') && this.__onFileRpc) {
|
|
@@ -558,6 +639,59 @@ export class WebRtcPeer {
|
|
|
558
639
|
this.__remoteLog(`rtc.peer-transport conn=${connId} type=${payload.candidateType} proto=${payload.protocol} relay=${payload.relayProtocol ?? '-'}`);
|
|
559
640
|
}
|
|
560
641
|
|
|
642
|
+
/**
|
|
643
|
+
* 主动探针:在 rpc DC 上发一个 plugin-probe,期待 UI 回 plugin-probe-ack。
|
|
644
|
+
* 用于区分"pion 报告 connected 但 UI 其实没收到数据"与"UI 真的收到了但没记录事件"。
|
|
645
|
+
* 绕过 RpcSendQueue(与 probe-ack 对称),仅测量传输层,不受应用层积压影响。
|
|
646
|
+
* 同一 session 同时只保留一条 in-flight 探针;超时仅打日志,不影响业务恢复。
|
|
647
|
+
*/
|
|
648
|
+
__sendPluginProbe(connId) {
|
|
649
|
+
const session = this.__sessions.get(connId);
|
|
650
|
+
if (!session) return;
|
|
651
|
+
const dc = session.rpcChannel;
|
|
652
|
+
if (!dc || dc.readyState !== 'open') return;
|
|
653
|
+
// 已有 in-flight 则跳过(避免重复)
|
|
654
|
+
if (session.__pluginProbeInFlight) return;
|
|
655
|
+
|
|
656
|
+
const id = (session.__pluginProbeIdSeq = (session.__pluginProbeIdSeq ?? 0) + 1);
|
|
657
|
+
const startMs = Date.now();
|
|
658
|
+
const timer = setTimeout(() => {
|
|
659
|
+
if (session.__pluginProbeInFlight?.id === id) {
|
|
660
|
+
session.__pluginProbeInFlight = null;
|
|
661
|
+
session.__pluginProbeTimer = null;
|
|
662
|
+
this.__remoteLog(`rtc.plugin-probe conn=${connId} id=${id} timeout`);
|
|
663
|
+
}
|
|
664
|
+
}, 5000);
|
|
665
|
+
timer.unref?.();
|
|
666
|
+
session.__pluginProbeInFlight = { id, startMs };
|
|
667
|
+
session.__pluginProbeTimer = timer;
|
|
668
|
+
|
|
669
|
+
try {
|
|
670
|
+
dc.send(JSON.stringify({ type: 'plugin-probe', id }));
|
|
671
|
+
this.__remoteLog(`rtc.plugin-probe conn=${connId} id=${id} sent`);
|
|
672
|
+
} catch (err) {
|
|
673
|
+
clearTimeout(timer);
|
|
674
|
+
session.__pluginProbeInFlight = null;
|
|
675
|
+
session.__pluginProbeTimer = null;
|
|
676
|
+
this.__remoteLog(`rtc.plugin-probe conn=${connId} id=${id} send-failed msg=${err?.message ?? err}`);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/** 收到 UI 的 plugin-probe-ack:计算 RTT 并释放 in-flight 槽位 */
|
|
681
|
+
__handlePluginProbeAck(connId, id) {
|
|
682
|
+
const session = this.__sessions.get(connId);
|
|
683
|
+
if (!session) return;
|
|
684
|
+
const inFlight = session.__pluginProbeInFlight;
|
|
685
|
+
if (!inFlight || inFlight.id !== id) return; // 过期 ack,忽略
|
|
686
|
+
const rtt = Date.now() - inFlight.startMs;
|
|
687
|
+
if (session.__pluginProbeTimer) {
|
|
688
|
+
clearTimeout(session.__pluginProbeTimer);
|
|
689
|
+
session.__pluginProbeTimer = null;
|
|
690
|
+
}
|
|
691
|
+
session.__pluginProbeInFlight = null;
|
|
692
|
+
this.__remoteLog(`rtc.plugin-probe conn=${connId} id=${id} acked rtt=${rtt}`);
|
|
693
|
+
}
|
|
694
|
+
|
|
561
695
|
__remoteLog(msg) {
|
|
562
696
|
remoteLog(this.__impl ? `${msg} rtc=${this.__impl}` : msg);
|
|
563
697
|
}
|