@coclaw/openclaw-coclaw 0.21.0 → 0.21.3
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 +8 -0
- package/openclaw.plugin.json +1 -4
- package/package.json +2 -2
- package/src/realtime-bridge.js +90 -61
- package/src/rpc-routing/run-event-routes.js +2 -1
package/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { notBound, bindOk, unbindOk, claimCodeCreated } from './src/common/messa
|
|
|
5
5
|
import { coclawChannelPlugin } from './src/channel-plugin.js';
|
|
6
6
|
import { ensureAgentSession, gatewayAgentRpc, restartRealtimeBridge, stopRealtimeBridge, waitForSessionsReady, broadcastPluginEvent } from './src/realtime-bridge.js';
|
|
7
7
|
import { getHostName, readSettings, writeName, MAX_NAME_LENGTH } from './src/settings.js';
|
|
8
|
+
import { readConfig } from './src/config.js';
|
|
8
9
|
import { setRuntime } from './src/runtime.js';
|
|
9
10
|
import { createSessionManager } from './src/session-manager/manager.js';
|
|
10
11
|
import { TopicManager } from './src/topic-manager/manager.js';
|
|
@@ -157,6 +158,13 @@ const plugin = {
|
|
|
157
158
|
setRuntime(api.runtime);
|
|
158
159
|
const logger = api?.logger ?? console;
|
|
159
160
|
installAbortRegistryDiag(logger);
|
|
161
|
+
|
|
162
|
+
// 未 bind 时打条提示,便于 hub 装机用户看到下一步动作
|
|
163
|
+
readConfig().then((cfg) => {
|
|
164
|
+
if (!cfg?.token) {
|
|
165
|
+
logger.info?.('[coclaw] not bound — run `openclaw coclaw enroll` to connect to CoClaw');
|
|
166
|
+
}
|
|
167
|
+
}).catch(() => {});
|
|
160
168
|
const manager = createSessionManager({ logger });
|
|
161
169
|
const topicManager = new TopicManager({ logger });
|
|
162
170
|
const chatHistoryManager = new ChatHistoryManager({ logger });
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "openclaw-coclaw",
|
|
3
3
|
"name": "CoClaw",
|
|
4
|
-
"description": "OpenClaw CoClaw channel plugin for remote chat",
|
|
4
|
+
"description": "OpenClaw CoClaw channel plugin for remote chat. Run `openclaw coclaw enroll` after install to connect to CoClaw.",
|
|
5
5
|
"activation": {
|
|
6
6
|
"onStartup": true
|
|
7
7
|
},
|
|
@@ -11,9 +11,6 @@
|
|
|
11
11
|
"properties": {
|
|
12
12
|
"serverUrl": {
|
|
13
13
|
"type": "string"
|
|
14
|
-
},
|
|
15
|
-
"gatewayWsUrl": {
|
|
16
|
-
"type": "string"
|
|
17
14
|
}
|
|
18
15
|
}
|
|
19
16
|
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coclaw/openclaw-coclaw",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
|
-
"description": "OpenClaw CoClaw channel plugin for remote chat",
|
|
6
|
+
"description": "OpenClaw CoClaw channel plugin for remote chat. Run `openclaw coclaw enroll` after install to connect to CoClaw.",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "https://github.com/coclaw/coclaw.git",
|
package/src/realtime-bridge.js
CHANGED
|
@@ -23,10 +23,19 @@ 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
|
-
// 最多
|
|
29
|
-
|
|
28
|
+
// 最多 N 次重试(N=length;加上首次尝试共 N+1 次),全部失败后进入 gave-up 终态,
|
|
29
|
+
// 终态后必须 stop+start 才能恢复(无运行期自动复活)——所以预算要够覆盖慢启动场景。
|
|
30
|
+
// 表前置 4 档(1s + 3×1.5s)专门压低"server 启动期 sidecars 还没就绪"窗口(推荐
|
|
31
|
+
// retryAfterMs=500);尾段 9 × 20s 把总预算拉到 ~200s(>3 分钟),覆盖 profile 初始化、
|
|
32
|
+
// 笔记本刚开机磁盘忙、pion 子进程首次 spawn 等慢启动场景,避免误判"放弃"。
|
|
33
|
+
// export 是为了让测试 helper 通过同一份常量识别 retry timer,避免硬编码副本与生产代码漂移。
|
|
34
|
+
export const GATEWAY_RETRY_DELAYS_MS = [
|
|
35
|
+
1_000, 1_500, 1_500, 1_500,
|
|
36
|
+
5_000, 10_000,
|
|
37
|
+
20_000, 20_000, 20_000, 20_000, 20_000, 20_000, 20_000, 20_000, 20_000,
|
|
38
|
+
];
|
|
30
39
|
// v3 握手失败时,只有错误消息匹配此正则才回退到不带 device 的 legacy 握手。
|
|
31
40
|
// 严格限定在"签名/设备/scope/协议"相关错误,避免对网络/内部错误做无意义的降级尝试。
|
|
32
41
|
const GATEWAY_HANDSHAKE_FALLBACK_PATTERN = /signature|device|scope|protocol/i;
|
|
@@ -90,20 +99,26 @@ function maskUrlToken(url) {
|
|
|
90
99
|
}
|
|
91
100
|
|
|
92
101
|
// 仅在未注入 resolveGatewayAuthToken 时使用,依赖 runtime / 环境变量
|
|
102
|
+
// 优先级:config.gateway.auth.token > env OPENCLAW_GATEWAY_TOKEN
|
|
103
|
+
// 与上游 server 的 auth-surface-resolution 同方向:config 主、env 兜底。
|
|
104
|
+
// env 路径保留是为 ensureGatewayStartupAuth 的 env-only 边角(token 由 env 提供时
|
|
105
|
+
// 上游不会写回 cfg),cfg 读不到时仍能拿到正确 token。
|
|
93
106
|
export function defaultResolveGatewayAuthToken() {
|
|
94
|
-
const envToken = process.env.OPENCLAW_GATEWAY_TOKEN?.trim();
|
|
95
|
-
if (envToken) {
|
|
96
|
-
return envToken;
|
|
97
|
-
}
|
|
98
107
|
try {
|
|
99
108
|
const cfg = getClawConfig();
|
|
100
109
|
const token = cfg?.gateway?.auth?.token;
|
|
101
|
-
|
|
110
|
+
if (typeof token === 'string' && token.trim()) {
|
|
111
|
+
return token.trim();
|
|
112
|
+
}
|
|
102
113
|
}
|
|
103
114
|
catch (err) {
|
|
104
115
|
console.warn?.(`[coclaw] resolve gateway auth token failed: ${String(err?.message ?? err)}`);
|
|
105
|
-
return '';
|
|
106
116
|
}
|
|
117
|
+
const envToken = process.env.OPENCLAW_GATEWAY_TOKEN?.trim();
|
|
118
|
+
if (envToken) {
|
|
119
|
+
return envToken;
|
|
120
|
+
}
|
|
121
|
+
return '';
|
|
107
122
|
}
|
|
108
123
|
|
|
109
124
|
/**
|
|
@@ -247,9 +262,7 @@ export class RealtimeBridge {
|
|
|
247
262
|
}
|
|
248
263
|
|
|
249
264
|
__resolveGatewayWsUrl() {
|
|
250
|
-
return
|
|
251
|
-
?? process.env.COCLAW_GATEWAY_WS_URL
|
|
252
|
-
?? DEFAULT_GATEWAY_WS_URL;
|
|
265
|
+
return process.env.COCLAW_GATEWAY_WS_URL ?? DEFAULT_GATEWAY_WS_URL;
|
|
253
266
|
}
|
|
254
267
|
|
|
255
268
|
async __clearTokenLocal(unboundClawId) {
|
|
@@ -265,16 +278,11 @@ export class RealtimeBridge {
|
|
|
265
278
|
}
|
|
266
279
|
|
|
267
280
|
__closeGatewayWs() {
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
this.__gatewayRetryTimer = null;
|
|
274
|
-
}
|
|
275
|
-
this.__gatewayAttempts = 0;
|
|
276
|
-
// 主动关闭时立即清 lag probe,不依赖 close 事件回调时序,避免 close 事件延迟期间 probe 误报
|
|
277
|
-
this.__clearAllLagProbes();
|
|
281
|
+
// 仅由显式销毁路径(stop / refresh)调用:职责单一——关闭当前 gateway WS 实例 +
|
|
282
|
+
// 结算未完成的 gateway RPC。retry timer / attempts 由 stop() 复位;lag probes 由 stop()
|
|
283
|
+
// 显式 __clearAllLagProbes 兜底(ws close 事件异步触发,期间 gatewayWs 已被这里置 null,
|
|
284
|
+
// 走 stale guard 早返不再清理);P2P 路由表(__dcPendingRequests / __runEventRoutes)
|
|
285
|
+
// 已不再随内线翻转清理,stop() 会显式 clear / destroy。
|
|
278
286
|
if (!this.gatewayWs) {
|
|
279
287
|
return;
|
|
280
288
|
}
|
|
@@ -292,10 +300,6 @@ export class RealtimeBridge {
|
|
|
292
300
|
settle({ ok: false, error: 'gateway_closed' });
|
|
293
301
|
}
|
|
294
302
|
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
303
|
}
|
|
300
304
|
|
|
301
305
|
/** 懒加载 WebRtcPeer(promise 锁防并发重复创建) */
|
|
@@ -338,16 +342,21 @@ export class RealtimeBridge {
|
|
|
338
342
|
}
|
|
339
343
|
/* c8 ignore stop */
|
|
340
344
|
|
|
341
|
-
/* c8 ignore next 7 -- 防御性检查,serverWs 通常在调用时可用 */
|
|
342
345
|
__forwardToServer(payload) {
|
|
343
346
|
if (!this.serverWs || this.serverWs.readyState !== 1) {
|
|
347
|
+
// WS 断窗口期 onSend 调用:webrtcPeer 异步回调(trickle ICE candidate、SDP 应答等)
|
|
348
|
+
// 本身没有 queue/rollback 机制,丢失对 plugin 端 PC 状态机不致命——UI 端 sig.state
|
|
349
|
+
// 恢复后会主动触发新一轮 ICE restart,重发完整 candidate set 与 SDP。仅记本地 log
|
|
350
|
+
// 便于事后排查;TODO 跟进完整方案(queue / rollback connId)。
|
|
351
|
+
this.logger.warn?.(`[coclaw] forward dropped: ws not ready (type=${payload?.type ?? 'unknown'})`);
|
|
344
352
|
return;
|
|
345
353
|
}
|
|
346
354
|
try {
|
|
347
355
|
this.serverWs.send(JSON.stringify(payload));
|
|
348
356
|
}
|
|
349
|
-
|
|
350
|
-
|
|
357
|
+
catch (e) {
|
|
358
|
+
this.logger.warn?.(`[coclaw] forward send failed (type=${payload?.type ?? 'unknown'}): ${e?.message ?? e}`);
|
|
359
|
+
}
|
|
351
360
|
}
|
|
352
361
|
|
|
353
362
|
__nextGatewayReqId(prefix = 'coclaw-rpc') {
|
|
@@ -655,7 +664,7 @@ export class RealtimeBridge {
|
|
|
655
664
|
// 用 rpcSeq 保证 ID 唯一,避免 v3→legacy 同毫秒内两次调用产生相同 id
|
|
656
665
|
this.gatewayRpcSeq += 1;
|
|
657
666
|
this.gatewayConnectReqId = `coclaw-connect-${Date.now()}-${this.gatewayRpcSeq}`;
|
|
658
|
-
this.
|
|
667
|
+
this.logger.info?.(`[coclaw] gateway connect request -> id=${this.gatewayConnectReqId} legacy=${legacy}`);
|
|
659
668
|
try {
|
|
660
669
|
const authToken = this.__resolveGatewayAuthToken();
|
|
661
670
|
const params = {
|
|
@@ -726,11 +735,13 @@ export class RealtimeBridge {
|
|
|
726
735
|
if (this.__gatewayGaveUp || this.__gatewayRetryTimer) {
|
|
727
736
|
return;
|
|
728
737
|
}
|
|
729
|
-
|
|
738
|
+
// 外/内/P2P 三条线各自独立:内线(plugin↔本机 gateway)的建连节奏不再看外线脸色。
|
|
739
|
+
// 外线断不影响这里启动,外线即便没起来也允许内线先跑(DC RPC 仍能通过 P2P 直达)。
|
|
740
|
+
if (this.gatewayWs) {
|
|
730
741
|
return;
|
|
731
742
|
}
|
|
732
743
|
const WebSocketCtor = this.__resolveWebSocket();
|
|
733
|
-
/* c8 ignore next 3 --
|
|
744
|
+
/* c8 ignore next 3 -- WebSocketCtor=null 仅测试注入下出现,生产路径 ws 库总能解析 */
|
|
734
745
|
if (!WebSocketCtor) {
|
|
735
746
|
return;
|
|
736
747
|
}
|
|
@@ -770,7 +781,7 @@ export class RealtimeBridge {
|
|
|
770
781
|
if (payload.type === 'event' && payload.event === 'connect.challenge') {
|
|
771
782
|
const nonce = payload?.payload?.nonce ?? '';
|
|
772
783
|
lastChallengeNonce = nonce;
|
|
773
|
-
this.
|
|
784
|
+
this.logger.info?.(`[coclaw] gateway event <- connect.challenge legacyMode=${this.__gatewayLegacyMode}`);
|
|
774
785
|
// 已经学到此 gateway 是 legacy(上一条 WS 回退过)→ 直接发 legacy 握手
|
|
775
786
|
if (this.__gatewayLegacyMode) {
|
|
776
787
|
pendingLegacyAttempted = true;
|
|
@@ -787,7 +798,7 @@ export class RealtimeBridge {
|
|
|
787
798
|
wasReady = true;
|
|
788
799
|
this.__gatewayAttempts = 0; // 成功握手 → 重置失败计数,让后续瞬态断开有完整重试预算
|
|
789
800
|
remoteLog('ws.connected peer=gateway');
|
|
790
|
-
this.
|
|
801
|
+
this.logger.info?.(`[coclaw] gateway connect ok <- id=${payload.id}`);
|
|
791
802
|
this.gatewayConnectReqId = null;
|
|
792
803
|
this.__ensureSessionsPromise = this.__ensureAllAgentSessions();
|
|
793
804
|
this.__pushInstanceInfo();
|
|
@@ -918,7 +929,7 @@ export class RealtimeBridge {
|
|
|
918
929
|
});
|
|
919
930
|
|
|
920
931
|
ws.addEventListener('open', () => {
|
|
921
|
-
this.
|
|
932
|
+
this.logger.info?.('[coclaw] gateway ws open, waiting for connect.challenge');
|
|
922
933
|
});
|
|
923
934
|
ws.addEventListener('close', (ev) => {
|
|
924
935
|
// 区分本端 plugin 主动关闭与对端 OpenClaw gateway 关闭:日志/远程上报用不同事件名,
|
|
@@ -958,14 +969,11 @@ export class RealtimeBridge {
|
|
|
958
969
|
settle({ ok: false, error: 'gateway_closed' });
|
|
959
970
|
}
|
|
960
971
|
this.gatewayPendingRequests.clear();
|
|
961
|
-
//
|
|
962
|
-
|
|
963
|
-
//
|
|
964
|
-
|
|
965
|
-
// 调度下一次尝试:仅在 bridge 仍活着、未 gave-up、server WS 健康时;
|
|
966
|
-
// 其他场景(如 bridge stop、server WS 已断)由上游流程兜底,不参与 gateway 重试。
|
|
972
|
+
// P2P 路由表(__dcPendingRequests / __runEventRoutes)不在这里清:
|
|
973
|
+
// 内线翻转不应级联影响 P2P。已发出去的 RPC 在 UI 侧 30/60s 超时兜底;
|
|
974
|
+
// 路由表条目由 start() 启动的周期扫描器(24h TTL)回收,stop() 时显式 clear。
|
|
975
|
+
// 调度下一次尝试:仅在 bridge 仍活着、未 gave-up 时;不再看外线脸色(外/内独立重试)。
|
|
967
976
|
if (this.started && !this.__gatewayGaveUp
|
|
968
|
-
&& this.serverWs && this.serverWs.readyState === 1
|
|
969
977
|
&& (wasReady || connectFailReported)) {
|
|
970
978
|
if (wasReady) {
|
|
971
979
|
// 之前握成功过,视为瞬态掉线 → 重置计数,让新一轮拿到完整重试预算
|
|
@@ -1241,7 +1249,7 @@ export class RealtimeBridge {
|
|
|
1241
1249
|
this.logger.warn?.(`[coclaw] realtime bridge connect timeout, will retry: ${maskedTarget}`);
|
|
1242
1250
|
remoteLog('ws.connect-timeout peer=server');
|
|
1243
1251
|
this.serverWs = null;
|
|
1244
|
-
|
|
1252
|
+
// 外线超时不级联关内线/清 P2P 路由:三条线各自独立。
|
|
1245
1253
|
this.__scheduleReconnect();
|
|
1246
1254
|
try { sock.close(4000, 'connect_timeout'); }
|
|
1247
1255
|
/* c8 ignore next */
|
|
@@ -1267,7 +1275,12 @@ export class RealtimeBridge {
|
|
|
1267
1275
|
// __buildEnvLine 内部所有读取均为缓存值,无 native syscall。
|
|
1268
1276
|
remoteLog(this.__buildEnvLine());
|
|
1269
1277
|
this.__startServerHeartbeat(sock);
|
|
1270
|
-
|
|
1278
|
+
// 三线独立后内线可能先于外线就绪:那次 push 因外线未 open 在 server 路径被 drop,
|
|
1279
|
+
// 故外线 open 时若内线已 ready 补推一次。门控 gatewayReady 是因为 agentModels 依赖
|
|
1280
|
+
// 内线 agents.list RPC,内线没就绪发出去字段不全会污染 admin 仪表盘。
|
|
1281
|
+
if (this.gatewayReady) {
|
|
1282
|
+
this.__pushInstanceInfo();
|
|
1283
|
+
}
|
|
1271
1284
|
});
|
|
1272
1285
|
|
|
1273
1286
|
sock.addEventListener('message', async (event) => {
|
|
@@ -1321,20 +1334,26 @@ export class RealtimeBridge {
|
|
|
1321
1334
|
const wasIntentional = this.intentionallyClosed;
|
|
1322
1335
|
this.serverWs = null;
|
|
1323
1336
|
this.intentionallyClosed = false;
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
if (
|
|
1333
|
-
this.
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1337
|
+
// 外线 close 不再级联关内线/清 P2P 路由:内线(plugin↔本机 gateway)与 P2P
|
|
1338
|
+
// 与外线(plugin↔远端 server)解耦,各自独立维护生命周期。auth-close 分支
|
|
1339
|
+
// (下方)是唯一允许级联 PC/fileHandler/token 的路径,因为 plugin 已失资格。
|
|
1340
|
+
|
|
1341
|
+
// auth-close (4001/4003) 语义是 plugin 失去服务资格,必须连同 PC/fileHandler 一起清;
|
|
1342
|
+
// 其它 close code(含 4000 心跳超时、1006 abnormal、1011 等)视为信令通道瞬态不通,
|
|
1343
|
+
// 保留 webrtcPeer / fileHandler 实例供重连后复用,避免雪崩式 PC 重建。
|
|
1344
|
+
const isAuthClose = event?.code === 4001 || event?.code === 4003;
|
|
1345
|
+
if (isAuthClose) {
|
|
1346
|
+
if (this.webrtcPeer) {
|
|
1347
|
+
try { await this.webrtcPeer.closeAll(); }
|
|
1348
|
+
/* c8 ignore next 3 -- 防御性兜底,werift close 异常时不可崩溃 gateway */
|
|
1349
|
+
catch (e) { this.logger.warn?.(`[coclaw/rtc] closeAll failed: ${e?.message}`); }
|
|
1350
|
+
this.webrtcPeer = null;
|
|
1351
|
+
this.__webrtcPeerReady = null;
|
|
1352
|
+
}
|
|
1353
|
+
if (this.__fileHandler) {
|
|
1354
|
+
this.__fileHandler.cancelCleanup();
|
|
1355
|
+
this.__fileHandler = null;
|
|
1356
|
+
}
|
|
1338
1357
|
remoteLog(`ws.auth-close peer=server code=${event.code}`);
|
|
1339
1358
|
this.logger.warn?.(`[coclaw] server ws auth-close (code=${event.code}), clearing local token`);
|
|
1340
1359
|
try {
|
|
@@ -1348,8 +1367,9 @@ export class RealtimeBridge {
|
|
|
1348
1367
|
}
|
|
1349
1368
|
|
|
1350
1369
|
if (!wasIntentional) {
|
|
1351
|
-
|
|
1352
|
-
|
|
1370
|
+
const sessionCount = this.webrtcPeer?.__sessions?.size ?? 0;
|
|
1371
|
+
remoteLog(`ws.disconnected peer=server code=${event?.code ?? 'unknown'} reason=${event?.reason ?? 'n/a'} keep-pc=true sessions=${sessionCount}`);
|
|
1372
|
+
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
1373
|
this.__scheduleReconnect();
|
|
1354
1374
|
}
|
|
1355
1375
|
});
|
|
@@ -1365,7 +1385,7 @@ export class RealtimeBridge {
|
|
|
1365
1385
|
/* c8 ignore next -- ?./?? fallback */
|
|
1366
1386
|
this.logger.warn?.(`[coclaw] realtime bridge error, will retry in ${RECONNECT_MS}ms: ${String(err?.message ?? err)}`);
|
|
1367
1387
|
this.serverWs = null;
|
|
1368
|
-
|
|
1388
|
+
// 外线 error 不级联关内线/清 P2P 路由:三条线各自独立。
|
|
1369
1389
|
this.__scheduleReconnect();
|
|
1370
1390
|
try { sock.close(4000, 'connect_error'); }
|
|
1371
1391
|
/* c8 ignore next */
|
|
@@ -1502,7 +1522,11 @@ export class RealtimeBridge {
|
|
|
1502
1522
|
scanMs: this.__runEventRoutesScanMs,
|
|
1503
1523
|
});
|
|
1504
1524
|
this.__runEventRoutes.init();
|
|
1525
|
+
// 外线(plugin↔远端 server)先发起 connectIfNeeded:仅创建 WebSocket 即返回,不阻塞内线。
|
|
1505
1526
|
await this.__connectIfNeeded();
|
|
1527
|
+
// 三条线各自独立启动:内线(plugin↔本机 gateway)由 start() 主动触发,
|
|
1528
|
+
// 不再依附于外线 open。即便外线建连失败/未配置 token,内线仍能起来支撑 DC RPC。
|
|
1529
|
+
this.__ensureGatewayConnection();
|
|
1506
1530
|
}
|
|
1507
1531
|
|
|
1508
1532
|
/**
|
|
@@ -1533,6 +1557,8 @@ export class RealtimeBridge {
|
|
|
1533
1557
|
this.__clearServerHeartbeat();
|
|
1534
1558
|
this.__clearConnectTimer();
|
|
1535
1559
|
// stop() / refresh() 兜底回收 lag 探针的 timer,防 unref 仍残留。
|
|
1560
|
+
// 顺序依赖:必须早于下方 __closeGatewayWs;后者会立刻把 gatewayWs 置 null,
|
|
1561
|
+
// 异步触发的 close 事件会被 stale guard 早返而无法走到 __clearAllLagProbes。
|
|
1536
1562
|
this.__clearAllLagProbes();
|
|
1537
1563
|
if (this.reconnectTimer) {
|
|
1538
1564
|
clearTimeout(this.reconnectTimer);
|
|
@@ -1557,6 +1583,9 @@ export class RealtimeBridge {
|
|
|
1557
1583
|
this.__runEventRoutes.destroy();
|
|
1558
1584
|
this.__runEventRoutes = null;
|
|
1559
1585
|
}
|
|
1586
|
+
// stop / refresh 是显式销毁路径:P2P 路由表不再随内线翻转清理(动作 5),
|
|
1587
|
+
// 但显式销毁时必须清干净,避免 refresh 后留下指向旧 connId 的孤儿条目。
|
|
1588
|
+
this.__dcPendingRequests.clear();
|
|
1560
1589
|
this.__closeGatewayWs();
|
|
1561
1590
|
if (this.webrtcPeer) {
|
|
1562
1591
|
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 表对齐 */
|