@coclaw/openclaw-coclaw 0.21.0 → 0.21.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
CHANGED
package/src/realtime-bridge.js
CHANGED
|
@@ -23,7 +23,7 @@ const RECONNECT_MS = 10_000;
|
|
|
23
23
|
const CONNECT_TIMEOUT_MS = 10_000;
|
|
24
24
|
const SERVER_HB_PING_MS = 25_000;
|
|
25
25
|
const SERVER_HB_TIMEOUT_MS = 45_000;
|
|
26
|
-
const SERVER_HB_MAX_MISS =
|
|
26
|
+
const SERVER_HB_MAX_MISS = 3; // 连续 3 次无响应才断连(~135s)。上游主线程 spike 实测最坏 ~89.5s(issue #75069),余量 ~1.5x
|
|
27
27
|
// gateway 握手失败的指数退避表:每个元素是"上一次失败"之后、"下一次尝试"之前的等待时间。
|
|
28
28
|
// 最多 5 次重试(加上首次尝试共 6 次),全部失败后进入 gave-up 终态,不再自动尝试。
|
|
29
29
|
const GATEWAY_RETRY_DELAYS_MS = [5_000, 10_000, 20_000, 20_000, 20_000];
|
|
@@ -265,16 +265,11 @@ export class RealtimeBridge {
|
|
|
265
265
|
}
|
|
266
266
|
|
|
267
267
|
__closeGatewayWs() {
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
this.__gatewayRetryTimer = null;
|
|
274
|
-
}
|
|
275
|
-
this.__gatewayAttempts = 0;
|
|
276
|
-
// 主动关闭时立即清 lag probe,不依赖 close 事件回调时序,避免 close 事件延迟期间 probe 误报
|
|
277
|
-
this.__clearAllLagProbes();
|
|
268
|
+
// 仅由显式销毁路径(stop / refresh)调用:职责单一——关闭当前 gateway WS 实例 +
|
|
269
|
+
// 结算未完成的 gateway RPC。retry timer / attempts 由 stop() 复位;lag probes 由 stop()
|
|
270
|
+
// 显式 __clearAllLagProbes 兜底(ws close 事件异步触发,期间 gatewayWs 已被这里置 null,
|
|
271
|
+
// 走 stale guard 早返不再清理);P2P 路由表(__dcPendingRequests / __runEventRoutes)
|
|
272
|
+
// 已不再随内线翻转清理,stop() 会显式 clear / destroy。
|
|
278
273
|
if (!this.gatewayWs) {
|
|
279
274
|
return;
|
|
280
275
|
}
|
|
@@ -292,10 +287,6 @@ export class RealtimeBridge {
|
|
|
292
287
|
settle({ ok: false, error: 'gateway_closed' });
|
|
293
288
|
}
|
|
294
289
|
this.gatewayPendingRequests.clear();
|
|
295
|
-
// 清空 UI 转发 RPC 路由表:gateway 已断,不会再有响应回来;不主动通知 UI,由 UI 30/60s 超时兜底
|
|
296
|
-
this.__dcPendingRequests.clear();
|
|
297
|
-
// 同步清空 runId 路由表(gateway 已断,不会再有 event:agent 推过来)
|
|
298
|
-
this.__runEventRoutes?.clear();
|
|
299
290
|
}
|
|
300
291
|
|
|
301
292
|
/** 懒加载 WebRtcPeer(promise 锁防并发重复创建) */
|
|
@@ -338,16 +329,21 @@ export class RealtimeBridge {
|
|
|
338
329
|
}
|
|
339
330
|
/* c8 ignore stop */
|
|
340
331
|
|
|
341
|
-
/* c8 ignore next 7 -- 防御性检查,serverWs 通常在调用时可用 */
|
|
342
332
|
__forwardToServer(payload) {
|
|
343
333
|
if (!this.serverWs || this.serverWs.readyState !== 1) {
|
|
334
|
+
// WS 断窗口期 onSend 调用:webrtcPeer 异步回调(trickle ICE candidate、SDP 应答等)
|
|
335
|
+
// 本身没有 queue/rollback 机制,丢失对 plugin 端 PC 状态机不致命——UI 端 sig.state
|
|
336
|
+
// 恢复后会主动触发新一轮 ICE restart,重发完整 candidate set 与 SDP。仅记本地 log
|
|
337
|
+
// 便于事后排查;TODO 跟进完整方案(queue / rollback connId)。
|
|
338
|
+
this.logger.warn?.(`[coclaw] forward dropped: ws not ready (type=${payload?.type ?? 'unknown'})`);
|
|
344
339
|
return;
|
|
345
340
|
}
|
|
346
341
|
try {
|
|
347
342
|
this.serverWs.send(JSON.stringify(payload));
|
|
348
343
|
}
|
|
349
|
-
|
|
350
|
-
|
|
344
|
+
catch (e) {
|
|
345
|
+
this.logger.warn?.(`[coclaw] forward send failed (type=${payload?.type ?? 'unknown'}): ${e?.message ?? e}`);
|
|
346
|
+
}
|
|
351
347
|
}
|
|
352
348
|
|
|
353
349
|
__nextGatewayReqId(prefix = 'coclaw-rpc') {
|
|
@@ -655,7 +651,7 @@ export class RealtimeBridge {
|
|
|
655
651
|
// 用 rpcSeq 保证 ID 唯一,避免 v3→legacy 同毫秒内两次调用产生相同 id
|
|
656
652
|
this.gatewayRpcSeq += 1;
|
|
657
653
|
this.gatewayConnectReqId = `coclaw-connect-${Date.now()}-${this.gatewayRpcSeq}`;
|
|
658
|
-
this.
|
|
654
|
+
this.logger.info?.(`[coclaw] gateway connect request -> id=${this.gatewayConnectReqId} legacy=${legacy}`);
|
|
659
655
|
try {
|
|
660
656
|
const authToken = this.__resolveGatewayAuthToken();
|
|
661
657
|
const params = {
|
|
@@ -726,11 +722,13 @@ export class RealtimeBridge {
|
|
|
726
722
|
if (this.__gatewayGaveUp || this.__gatewayRetryTimer) {
|
|
727
723
|
return;
|
|
728
724
|
}
|
|
729
|
-
|
|
725
|
+
// 外/内/P2P 三条线各自独立:内线(plugin↔本机 gateway)的建连节奏不再看外线脸色。
|
|
726
|
+
// 外线断不影响这里启动,外线即便没起来也允许内线先跑(DC RPC 仍能通过 P2P 直达)。
|
|
727
|
+
if (this.gatewayWs) {
|
|
730
728
|
return;
|
|
731
729
|
}
|
|
732
730
|
const WebSocketCtor = this.__resolveWebSocket();
|
|
733
|
-
/* c8 ignore next 3 --
|
|
731
|
+
/* c8 ignore next 3 -- WebSocketCtor=null 仅测试注入下出现,生产路径 ws 库总能解析 */
|
|
734
732
|
if (!WebSocketCtor) {
|
|
735
733
|
return;
|
|
736
734
|
}
|
|
@@ -770,7 +768,7 @@ export class RealtimeBridge {
|
|
|
770
768
|
if (payload.type === 'event' && payload.event === 'connect.challenge') {
|
|
771
769
|
const nonce = payload?.payload?.nonce ?? '';
|
|
772
770
|
lastChallengeNonce = nonce;
|
|
773
|
-
this.
|
|
771
|
+
this.logger.info?.(`[coclaw] gateway event <- connect.challenge legacyMode=${this.__gatewayLegacyMode}`);
|
|
774
772
|
// 已经学到此 gateway 是 legacy(上一条 WS 回退过)→ 直接发 legacy 握手
|
|
775
773
|
if (this.__gatewayLegacyMode) {
|
|
776
774
|
pendingLegacyAttempted = true;
|
|
@@ -787,7 +785,7 @@ export class RealtimeBridge {
|
|
|
787
785
|
wasReady = true;
|
|
788
786
|
this.__gatewayAttempts = 0; // 成功握手 → 重置失败计数,让后续瞬态断开有完整重试预算
|
|
789
787
|
remoteLog('ws.connected peer=gateway');
|
|
790
|
-
this.
|
|
788
|
+
this.logger.info?.(`[coclaw] gateway connect ok <- id=${payload.id}`);
|
|
791
789
|
this.gatewayConnectReqId = null;
|
|
792
790
|
this.__ensureSessionsPromise = this.__ensureAllAgentSessions();
|
|
793
791
|
this.__pushInstanceInfo();
|
|
@@ -918,7 +916,7 @@ export class RealtimeBridge {
|
|
|
918
916
|
});
|
|
919
917
|
|
|
920
918
|
ws.addEventListener('open', () => {
|
|
921
|
-
this.
|
|
919
|
+
this.logger.info?.('[coclaw] gateway ws open, waiting for connect.challenge');
|
|
922
920
|
});
|
|
923
921
|
ws.addEventListener('close', (ev) => {
|
|
924
922
|
// 区分本端 plugin 主动关闭与对端 OpenClaw gateway 关闭:日志/远程上报用不同事件名,
|
|
@@ -958,14 +956,11 @@ export class RealtimeBridge {
|
|
|
958
956
|
settle({ ok: false, error: 'gateway_closed' });
|
|
959
957
|
}
|
|
960
958
|
this.gatewayPendingRequests.clear();
|
|
961
|
-
//
|
|
962
|
-
|
|
963
|
-
//
|
|
964
|
-
|
|
965
|
-
// 调度下一次尝试:仅在 bridge 仍活着、未 gave-up、server WS 健康时;
|
|
966
|
-
// 其他场景(如 bridge stop、server WS 已断)由上游流程兜底,不参与 gateway 重试。
|
|
959
|
+
// P2P 路由表(__dcPendingRequests / __runEventRoutes)不在这里清:
|
|
960
|
+
// 内线翻转不应级联影响 P2P。已发出去的 RPC 在 UI 侧 30/60s 超时兜底;
|
|
961
|
+
// 路由表条目由 start() 启动的周期扫描器(24h TTL)回收,stop() 时显式 clear。
|
|
962
|
+
// 调度下一次尝试:仅在 bridge 仍活着、未 gave-up 时;不再看外线脸色(外/内独立重试)。
|
|
967
963
|
if (this.started && !this.__gatewayGaveUp
|
|
968
|
-
&& this.serverWs && this.serverWs.readyState === 1
|
|
969
964
|
&& (wasReady || connectFailReported)) {
|
|
970
965
|
if (wasReady) {
|
|
971
966
|
// 之前握成功过,视为瞬态掉线 → 重置计数,让新一轮拿到完整重试预算
|
|
@@ -1241,7 +1236,7 @@ export class RealtimeBridge {
|
|
|
1241
1236
|
this.logger.warn?.(`[coclaw] realtime bridge connect timeout, will retry: ${maskedTarget}`);
|
|
1242
1237
|
remoteLog('ws.connect-timeout peer=server');
|
|
1243
1238
|
this.serverWs = null;
|
|
1244
|
-
|
|
1239
|
+
// 外线超时不级联关内线/清 P2P 路由:三条线各自独立。
|
|
1245
1240
|
this.__scheduleReconnect();
|
|
1246
1241
|
try { sock.close(4000, 'connect_timeout'); }
|
|
1247
1242
|
/* c8 ignore next */
|
|
@@ -1267,7 +1262,12 @@ export class RealtimeBridge {
|
|
|
1267
1262
|
// __buildEnvLine 内部所有读取均为缓存值,无 native syscall。
|
|
1268
1263
|
remoteLog(this.__buildEnvLine());
|
|
1269
1264
|
this.__startServerHeartbeat(sock);
|
|
1270
|
-
|
|
1265
|
+
// 三线独立后内线可能先于外线就绪:那次 push 因外线未 open 在 server 路径被 drop,
|
|
1266
|
+
// 故外线 open 时若内线已 ready 补推一次。门控 gatewayReady 是因为 agentModels 依赖
|
|
1267
|
+
// 内线 agents.list RPC,内线没就绪发出去字段不全会污染 admin 仪表盘。
|
|
1268
|
+
if (this.gatewayReady) {
|
|
1269
|
+
this.__pushInstanceInfo();
|
|
1270
|
+
}
|
|
1271
1271
|
});
|
|
1272
1272
|
|
|
1273
1273
|
sock.addEventListener('message', async (event) => {
|
|
@@ -1321,20 +1321,26 @@ export class RealtimeBridge {
|
|
|
1321
1321
|
const wasIntentional = this.intentionallyClosed;
|
|
1322
1322
|
this.serverWs = null;
|
|
1323
1323
|
this.intentionallyClosed = false;
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
if (
|
|
1333
|
-
this.
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1324
|
+
// 外线 close 不再级联关内线/清 P2P 路由:内线(plugin↔本机 gateway)与 P2P
|
|
1325
|
+
// 与外线(plugin↔远端 server)解耦,各自独立维护生命周期。auth-close 分支
|
|
1326
|
+
// (下方)是唯一允许级联 PC/fileHandler/token 的路径,因为 plugin 已失资格。
|
|
1327
|
+
|
|
1328
|
+
// auth-close (4001/4003) 语义是 plugin 失去服务资格,必须连同 PC/fileHandler 一起清;
|
|
1329
|
+
// 其它 close code(含 4000 心跳超时、1006 abnormal、1011 等)视为信令通道瞬态不通,
|
|
1330
|
+
// 保留 webrtcPeer / fileHandler 实例供重连后复用,避免雪崩式 PC 重建。
|
|
1331
|
+
const isAuthClose = event?.code === 4001 || event?.code === 4003;
|
|
1332
|
+
if (isAuthClose) {
|
|
1333
|
+
if (this.webrtcPeer) {
|
|
1334
|
+
try { await this.webrtcPeer.closeAll(); }
|
|
1335
|
+
/* c8 ignore next 3 -- 防御性兜底,werift close 异常时不可崩溃 gateway */
|
|
1336
|
+
catch (e) { this.logger.warn?.(`[coclaw/rtc] closeAll failed: ${e?.message}`); }
|
|
1337
|
+
this.webrtcPeer = null;
|
|
1338
|
+
this.__webrtcPeerReady = null;
|
|
1339
|
+
}
|
|
1340
|
+
if (this.__fileHandler) {
|
|
1341
|
+
this.__fileHandler.cancelCleanup();
|
|
1342
|
+
this.__fileHandler = null;
|
|
1343
|
+
}
|
|
1338
1344
|
remoteLog(`ws.auth-close peer=server code=${event.code}`);
|
|
1339
1345
|
this.logger.warn?.(`[coclaw] server ws auth-close (code=${event.code}), clearing local token`);
|
|
1340
1346
|
try {
|
|
@@ -1348,8 +1354,9 @@ export class RealtimeBridge {
|
|
|
1348
1354
|
}
|
|
1349
1355
|
|
|
1350
1356
|
if (!wasIntentional) {
|
|
1351
|
-
|
|
1352
|
-
|
|
1357
|
+
const sessionCount = this.webrtcPeer?.__sessions?.size ?? 0;
|
|
1358
|
+
remoteLog(`ws.disconnected peer=server code=${event?.code ?? 'unknown'} reason=${event?.reason ?? 'n/a'} keep-pc=true sessions=${sessionCount}`);
|
|
1359
|
+
this.logger.warn?.(`[coclaw] realtime bridge closed (${event?.code ?? 'unknown'}: ${event?.reason ?? 'n/a'}), will retry in ${RECONNECT_MS}ms (keep-pc, sessions=${sessionCount})`);
|
|
1353
1360
|
this.__scheduleReconnect();
|
|
1354
1361
|
}
|
|
1355
1362
|
});
|
|
@@ -1365,7 +1372,7 @@ export class RealtimeBridge {
|
|
|
1365
1372
|
/* c8 ignore next -- ?./?? fallback */
|
|
1366
1373
|
this.logger.warn?.(`[coclaw] realtime bridge error, will retry in ${RECONNECT_MS}ms: ${String(err?.message ?? err)}`);
|
|
1367
1374
|
this.serverWs = null;
|
|
1368
|
-
|
|
1375
|
+
// 外线 error 不级联关内线/清 P2P 路由:三条线各自独立。
|
|
1369
1376
|
this.__scheduleReconnect();
|
|
1370
1377
|
try { sock.close(4000, 'connect_error'); }
|
|
1371
1378
|
/* c8 ignore next */
|
|
@@ -1502,7 +1509,11 @@ export class RealtimeBridge {
|
|
|
1502
1509
|
scanMs: this.__runEventRoutesScanMs,
|
|
1503
1510
|
});
|
|
1504
1511
|
this.__runEventRoutes.init();
|
|
1512
|
+
// 外线(plugin↔远端 server)先发起 connectIfNeeded:仅创建 WebSocket 即返回,不阻塞内线。
|
|
1505
1513
|
await this.__connectIfNeeded();
|
|
1514
|
+
// 三条线各自独立启动:内线(plugin↔本机 gateway)由 start() 主动触发,
|
|
1515
|
+
// 不再依附于外线 open。即便外线建连失败/未配置 token,内线仍能起来支撑 DC RPC。
|
|
1516
|
+
this.__ensureGatewayConnection();
|
|
1506
1517
|
}
|
|
1507
1518
|
|
|
1508
1519
|
/**
|
|
@@ -1533,6 +1544,8 @@ export class RealtimeBridge {
|
|
|
1533
1544
|
this.__clearServerHeartbeat();
|
|
1534
1545
|
this.__clearConnectTimer();
|
|
1535
1546
|
// stop() / refresh() 兜底回收 lag 探针的 timer,防 unref 仍残留。
|
|
1547
|
+
// 顺序依赖:必须早于下方 __closeGatewayWs;后者会立刻把 gatewayWs 置 null,
|
|
1548
|
+
// 异步触发的 close 事件会被 stale guard 早返而无法走到 __clearAllLagProbes。
|
|
1536
1549
|
this.__clearAllLagProbes();
|
|
1537
1550
|
if (this.reconnectTimer) {
|
|
1538
1551
|
clearTimeout(this.reconnectTimer);
|
|
@@ -1557,6 +1570,9 @@ export class RealtimeBridge {
|
|
|
1557
1570
|
this.__runEventRoutes.destroy();
|
|
1558
1571
|
this.__runEventRoutes = null;
|
|
1559
1572
|
}
|
|
1573
|
+
// stop / refresh 是显式销毁路径:P2P 路由表不再随内线翻转清理(动作 5),
|
|
1574
|
+
// 但显式销毁时必须清干净,避免 refresh 后留下指向旧 connId 的孤儿条目。
|
|
1575
|
+
this.__dcPendingRequests.clear();
|
|
1560
1576
|
this.__closeGatewayWs();
|
|
1561
1577
|
if (this.webrtcPeer) {
|
|
1562
1578
|
await this.webrtcPeer.closeAll().catch(() => {});
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
* - timer 必须 unref()——避免 hold 进程退出
|
|
15
15
|
*
|
|
16
16
|
* 与 reqId 路由表(realtime-bridge.js __dcPendingRequests)保持行为对齐:
|
|
17
|
-
* 对 PC
|
|
17
|
+
* 对 PC 关闭和网关 WS 翻转都不做联动清理(外/内/P2P 三线独立),TTL 兜底;
|
|
18
|
+
* 仅显式销毁路径(bridge.stop / refresh)会通过 destroy() 清表。
|
|
18
19
|
*/
|
|
19
20
|
|
|
20
21
|
/** 路由条目最大存活时间(24h),与 reqId 表对齐 */
|