@coclaw/openclaw-coclaw 0.13.0 → 0.13.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/package.json +1 -1
- package/src/webrtc/webrtc-peer.js +74 -5
package/package.json
CHANGED
|
@@ -5,6 +5,14 @@ import { remoteLog } from '../remote-log.js';
|
|
|
5
5
|
// 用于诊断 dump:过大会撑爆 remoteLog 单帧,20 足以覆盖典型多文件传输会话。
|
|
6
6
|
const FILE_CHANNEL_HISTORY_LIMIT = 20;
|
|
7
7
|
|
|
8
|
+
// Failed session 保留 24 小时,支持 Capacitor 长时间后台恢复后 ICE restart。
|
|
9
|
+
// 超时后 session 被回收释放 IPC listeners 和 Go 侧资源。
|
|
10
|
+
const FAILED_SESSION_TTL_MS = 24 * 60 * 60 * 1000;
|
|
11
|
+
|
|
12
|
+
// Session 总数上限(活跃 + failed)。溢出时淘汰最旧的 failed session。
|
|
13
|
+
// 20 足以覆盖多 UI 实例(浏览器多标签 + 移动端)的典型场景。
|
|
14
|
+
const MAX_SESSIONS = 20;
|
|
15
|
+
|
|
8
16
|
/**
|
|
9
17
|
* 管理多个 WebRTC PeerConnection(以 connId 为粒度)。
|
|
10
18
|
* Plugin 作为被叫方:收到 UI 的 offer → 回复 answer。
|
|
@@ -55,6 +63,11 @@ export class WebRtcPeer {
|
|
|
55
63
|
async closeByConnId(connId) {
|
|
56
64
|
const session = this.__sessions.get(connId);
|
|
57
65
|
if (!session) return;
|
|
66
|
+
// 清理 failed TTL 定时器
|
|
67
|
+
if (session.__failedTimer) {
|
|
68
|
+
clearTimeout(session.__failedTimer);
|
|
69
|
+
session.__failedTimer = null;
|
|
70
|
+
}
|
|
58
71
|
this.__sessions.delete(connId);
|
|
59
72
|
// 先 detach 事件,防止 pc.close() 异步触发 onconnectionstatechange 删除新 session
|
|
60
73
|
session.pc.onconnectionstatechange = null;
|
|
@@ -96,6 +109,22 @@ export class WebRtcPeer {
|
|
|
96
109
|
if (isIceRestart) {
|
|
97
110
|
const existing = this.__sessions.get(connId);
|
|
98
111
|
if (existing) {
|
|
112
|
+
// 仅已验证支持 ICE restart 的 impl 放行,其余立即 reject 让 UI 走 rebuild
|
|
113
|
+
if (this.__impl !== 'pion') {
|
|
114
|
+
this.__remoteLog(`rtc.ice-restart-unsupported conn=${connId} impl=${this.__impl}`);
|
|
115
|
+
this.logger.info?.(`${this.__rtcTag} ICE restart rejected: impl=${this.__impl} not verified`);
|
|
116
|
+
this.__onSend({
|
|
117
|
+
type: 'rtc:restart-rejected',
|
|
118
|
+
toConnId: connId,
|
|
119
|
+
payload: { reason: 'impl_unsupported' },
|
|
120
|
+
});
|
|
121
|
+
return; // TTL timer 保持不变(reject 是同步的,不影响 timer 正常工作)
|
|
122
|
+
}
|
|
123
|
+
// 暂停 failed TTL timer:pion restart 涉及异步协商,期间不应被回收
|
|
124
|
+
if (existing.__failedTimer) {
|
|
125
|
+
clearTimeout(existing.__failedTimer);
|
|
126
|
+
existing.__failedTimer = null;
|
|
127
|
+
}
|
|
99
128
|
this.__remoteLog(`rtc.ice-restart conn=${connId}`);
|
|
100
129
|
this.logger.info?.(`${this.__rtcTag} ICE restart offer from ${connId}, renegotiating`);
|
|
101
130
|
try {
|
|
@@ -144,6 +173,11 @@ export class WebRtcPeer {
|
|
|
144
173
|
await this.closeByConnId(connId);
|
|
145
174
|
}
|
|
146
175
|
|
|
176
|
+
// session 总数限制:溢出时淘汰最旧的 failed session
|
|
177
|
+
if (this.__sessions.size >= MAX_SESSIONS) {
|
|
178
|
+
this.__evictOldestFailed();
|
|
179
|
+
}
|
|
180
|
+
|
|
147
181
|
// 从 Server 注入的 turnCreds 构建 iceServers
|
|
148
182
|
// werift 的 urls 必须是单个 string,每个 URL 独立一个对象
|
|
149
183
|
const iceServers = [];
|
|
@@ -211,6 +245,12 @@ export class WebRtcPeer {
|
|
|
211
245
|
const cur = this.__sessions.get(connId);
|
|
212
246
|
if (!cur || cur.pc !== pc) return;
|
|
213
247
|
|
|
248
|
+
// 离开 failed 状态时清理 TTL timer(ICE restart 恢复、自然关闭等)
|
|
249
|
+
if (state !== 'failed' && cur.__failedTimer) {
|
|
250
|
+
clearTimeout(cur.__failedTimer);
|
|
251
|
+
cur.__failedTimer = null;
|
|
252
|
+
}
|
|
253
|
+
|
|
214
254
|
if (state === 'connected') {
|
|
215
255
|
// 重置 dump 去重水位(disconnected → connected → disconnected 仍能再 dump)
|
|
216
256
|
cur.__lastDumpState = null;
|
|
@@ -227,16 +267,25 @@ export class WebRtcPeer {
|
|
|
227
267
|
// pion: pair 通过独立的 selectedcandidatepairchange 事件上报
|
|
228
268
|
} else if (state === 'disconnected' || state === 'failed' || state === 'closed') {
|
|
229
269
|
// 诊断 dump:失败/断连/关闭时输出当前 PC 上 DC 状态,定位"PC 假活/DC 死"现象
|
|
230
|
-
// - closed
|
|
270
|
+
// - closed 由 closeByConnId 接管清理,dump 收敛诊断噪声
|
|
231
271
|
// - disconnected 可能反复触发,去重避免噪声
|
|
232
272
|
if (state !== 'closed' && cur.__lastDumpState !== state) {
|
|
233
273
|
cur.__lastDumpState = state;
|
|
234
274
|
this.__dumpSessionState(connId, cur, state);
|
|
235
275
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
276
|
+
if (state === 'failed') {
|
|
277
|
+
// 启动 TTL 定时器:超时后回收 session 释放 IPC listeners 和 Go 侧资源。
|
|
278
|
+
// unref() 确保定时器不阻止进程退出(gateway 由其他连接保活)。
|
|
279
|
+
if (cur.__failedTimer) clearTimeout(cur.__failedTimer);
|
|
280
|
+
cur.__failedTimer = setTimeout(() => {
|
|
281
|
+
this.__remoteLog(`rtc.session-expired conn=${connId} ttl=${FAILED_SESSION_TTL_MS / 1000}s`);
|
|
282
|
+
this.logger.info?.(`${this.__rtcTag} [${connId}] session TTL expired, closing`);
|
|
283
|
+
this.closeByConnId(connId).catch(() => {});
|
|
284
|
+
}, FAILED_SESSION_TTL_MS);
|
|
285
|
+
cur.__failedTimer.unref?.();
|
|
286
|
+
} else if (state === 'closed') {
|
|
287
|
+
// 自然进入 closed 时也需通过 closeByConnId 释放 IPC listeners 和 Go 资源
|
|
288
|
+
this.closeByConnId(connId).catch(() => {});
|
|
240
289
|
}
|
|
241
290
|
}
|
|
242
291
|
};
|
|
@@ -287,6 +336,10 @@ export class WebRtcPeer {
|
|
|
287
336
|
// SDP 协商失败 → 清理已入 Map 的 session,避免泄漏
|
|
288
337
|
const cur = this.__sessions.get(connId);
|
|
289
338
|
if (cur && cur.pc === pc) {
|
|
339
|
+
if (cur.__failedTimer) {
|
|
340
|
+
clearTimeout(cur.__failedTimer);
|
|
341
|
+
cur.__failedTimer = null;
|
|
342
|
+
}
|
|
290
343
|
this.__sessions.delete(connId);
|
|
291
344
|
}
|
|
292
345
|
await pc.close().catch(() => {});
|
|
@@ -393,9 +446,25 @@ export class WebRtcPeer {
|
|
|
393
446
|
remoteLog(this.__impl ? `${msg} rtc=${this.__impl}` : msg);
|
|
394
447
|
}
|
|
395
448
|
|
|
449
|
+
/** 淘汰最旧的 failed session(Map 迭代序 ≈ 创建时间序),用于 queue length 限制 */
|
|
450
|
+
__evictOldestFailed() {
|
|
451
|
+
for (const [connId, session] of this.__sessions) {
|
|
452
|
+
if (session.pc.connectionState === 'failed') {
|
|
453
|
+
this.__remoteLog(`rtc.session-evicted conn=${connId} sessions=${this.__sessions.size}`);
|
|
454
|
+
this.logger.info?.(`${this.__rtcTag} [${connId}] session evicted (limit ${MAX_SESSIONS}), closing`);
|
|
455
|
+
this.closeByConnId(connId).catch(() => {});
|
|
456
|
+
return true;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
this.logger.warn?.(`${this.__rtcTag} session limit (${MAX_SESSIONS}) reached, no failed sessions to evict`);
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
|
|
396
463
|
__logDebug(message) {
|
|
397
464
|
if (typeof this.logger?.debug === 'function') {
|
|
398
465
|
this.logger.debug(`${this.__rtcTag} ${message}`);
|
|
399
466
|
}
|
|
400
467
|
}
|
|
401
468
|
}
|
|
469
|
+
|
|
470
|
+
export { FAILED_SESSION_TTL_MS, MAX_SESSIONS };
|