@yoooclaw/phone-notifications 1.11.13 → 1.11.15
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/dist/index.cjs +416 -78
- package/dist/index.cjs.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -5574,7 +5574,7 @@ function readBuildInjectedVersion() {
|
|
|
5574
5574
|
if (false) {
|
|
5575
5575
|
return void 0;
|
|
5576
5576
|
}
|
|
5577
|
-
const version = "1.11.
|
|
5577
|
+
const version = "1.11.15".trim();
|
|
5578
5578
|
return version || void 0;
|
|
5579
5579
|
}
|
|
5580
5580
|
function readPluginVersionFromPackageJson() {
|
|
@@ -12379,6 +12379,7 @@ var RelayClient = class {
|
|
|
12379
12379
|
reconnectTimer = null;
|
|
12380
12380
|
handlers = [];
|
|
12381
12381
|
connectedHandlers = [];
|
|
12382
|
+
disconnectedHandlers = [];
|
|
12382
12383
|
aborted = false;
|
|
12383
12384
|
lastInboundAt = 0;
|
|
12384
12385
|
stopPromise = null;
|
|
@@ -12407,6 +12408,10 @@ var RelayClient = class {
|
|
|
12407
12408
|
onConnected(handler) {
|
|
12408
12409
|
this.connectedHandlers.push(handler);
|
|
12409
12410
|
}
|
|
12411
|
+
/** 注册 Relay 连接断开后的回调 */
|
|
12412
|
+
onDisconnected(handler) {
|
|
12413
|
+
this.disconnectedHandlers.push(handler);
|
|
12414
|
+
}
|
|
12410
12415
|
/** 当前是否已连上 Relay */
|
|
12411
12416
|
isConnected() {
|
|
12412
12417
|
return this.ws?.readyState === wrapper_default.OPEN;
|
|
@@ -12480,23 +12485,41 @@ var RelayClient = class {
|
|
|
12480
12485
|
}
|
|
12481
12486
|
/** 发送出站帧 */
|
|
12482
12487
|
send(frame) {
|
|
12483
|
-
|
|
12488
|
+
const ws = this.ws;
|
|
12489
|
+
if (ws?.readyState === wrapper_default.OPEN) {
|
|
12484
12490
|
const payload = JSON.stringify(frame);
|
|
12485
12491
|
this.opts.logger.info(
|
|
12486
12492
|
`Relay tunnel: \u25B6 send frame type=${frame.type}, id=${"id" in frame ? frame.id : "N/A"} (${payload.length} chars)`
|
|
12487
12493
|
);
|
|
12488
|
-
|
|
12494
|
+
try {
|
|
12495
|
+
ws.send(payload);
|
|
12496
|
+
} catch (err2) {
|
|
12497
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
12498
|
+
this.opts.logger.warn(
|
|
12499
|
+
`Relay tunnel: send failed, forcing reconnect: ${message}`
|
|
12500
|
+
);
|
|
12501
|
+
this.forceReconnectFromSocket(ws, `send-failed: ${message}`);
|
|
12502
|
+
}
|
|
12489
12503
|
} else {
|
|
12490
12504
|
this.logSendSkipped("send", `frame type=${frame.type}`);
|
|
12491
12505
|
}
|
|
12492
12506
|
}
|
|
12493
12507
|
/** 原样透传文本到 Relay(用于 Gateway WS 响应直接回传) */
|
|
12494
12508
|
sendRaw(text) {
|
|
12495
|
-
|
|
12509
|
+
const ws = this.ws;
|
|
12510
|
+
if (ws?.readyState === wrapper_default.OPEN) {
|
|
12496
12511
|
this.opts.logger.info(
|
|
12497
12512
|
`Relay tunnel: \u25B6 sendRaw (${text.length} chars): ${text.length <= 500 ? text : text.substring(0, 500) + "\u2026"}`
|
|
12498
12513
|
);
|
|
12499
|
-
|
|
12514
|
+
try {
|
|
12515
|
+
ws.send(text);
|
|
12516
|
+
} catch (err2) {
|
|
12517
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
12518
|
+
this.opts.logger.warn(
|
|
12519
|
+
`Relay tunnel: sendRaw failed, forcing reconnect: ${message}`
|
|
12520
|
+
);
|
|
12521
|
+
this.forceReconnectFromSocket(ws, `sendRaw-failed: ${message}`);
|
|
12522
|
+
}
|
|
12500
12523
|
} else {
|
|
12501
12524
|
this.logSendSkipped("sendRaw");
|
|
12502
12525
|
}
|
|
@@ -12547,33 +12570,20 @@ var RelayClient = class {
|
|
|
12547
12570
|
handshakeTimeout: HANDSHAKE_TIMEOUT_MS
|
|
12548
12571
|
});
|
|
12549
12572
|
this.ws = ws;
|
|
12550
|
-
let connectWatchdog = setTimeout(
|
|
12551
|
-
|
|
12552
|
-
|
|
12553
|
-
|
|
12554
|
-
|
|
12555
|
-
|
|
12556
|
-
|
|
12557
|
-
|
|
12558
|
-
|
|
12559
|
-
|
|
12560
|
-
|
|
12561
|
-
}
|
|
12562
|
-
|
|
12563
|
-
|
|
12564
|
-
if (this.ws === ws) {
|
|
12565
|
-
this.opts.logger.warn(
|
|
12566
|
-
"Relay tunnel: terminate did not emit close, forcing reconnect"
|
|
12567
|
-
);
|
|
12568
|
-
this.stopHeartbeat();
|
|
12569
|
-
this.ws = null;
|
|
12570
|
-
this.writeStatus("disconnected", "watchdog-force");
|
|
12571
|
-
this.scheduleReconnect();
|
|
12572
|
-
settle();
|
|
12573
|
-
}
|
|
12574
|
-
}, 1e3);
|
|
12575
|
-
}
|
|
12576
|
-
}, CONNECT_WATCHDOG_MS);
|
|
12573
|
+
let connectWatchdog = setTimeout(
|
|
12574
|
+
() => {
|
|
12575
|
+
connectWatchdog = null;
|
|
12576
|
+
if (this.ws !== ws || ws.readyState === wrapper_default.OPEN) {
|
|
12577
|
+
return;
|
|
12578
|
+
}
|
|
12579
|
+
this.opts.logger.warn(
|
|
12580
|
+
`Relay tunnel: connect watchdog fired (readyState=${ws.readyState}, attempt=${this.reconnectAttempt}), forcing reconnect`
|
|
12581
|
+
);
|
|
12582
|
+
this.forceReconnectFromSocket(ws, "connect-watchdog-timeout");
|
|
12583
|
+
settle();
|
|
12584
|
+
},
|
|
12585
|
+
CONNECT_WATCHDOG_MS
|
|
12586
|
+
);
|
|
12577
12587
|
const clearConnectWatchdog = () => {
|
|
12578
12588
|
if (connectWatchdog) {
|
|
12579
12589
|
clearTimeout(connectWatchdog);
|
|
@@ -12627,7 +12637,9 @@ var RelayClient = class {
|
|
|
12627
12637
|
if (isCurrentSocket) {
|
|
12628
12638
|
this.stopHeartbeat();
|
|
12629
12639
|
this.ws = null;
|
|
12630
|
-
|
|
12640
|
+
const disconnectReason = `code=${code}, reason=${reasonStr}`;
|
|
12641
|
+
this.writeStatus("disconnected", disconnectReason);
|
|
12642
|
+
this.emitDisconnected(disconnectReason);
|
|
12631
12643
|
this.scheduleReconnect();
|
|
12632
12644
|
}
|
|
12633
12645
|
settle();
|
|
@@ -12653,6 +12665,15 @@ var RelayClient = class {
|
|
|
12653
12665
|
});
|
|
12654
12666
|
}
|
|
12655
12667
|
}
|
|
12668
|
+
emitDisconnected(reason) {
|
|
12669
|
+
for (const handler of this.disconnectedHandlers) {
|
|
12670
|
+
Promise.resolve(handler(reason)).catch((err2) => {
|
|
12671
|
+
this.opts.logger.warn(
|
|
12672
|
+
`Relay tunnel: onDisconnected handler failed: ${err2 instanceof Error ? err2.message : String(err2)}`
|
|
12673
|
+
);
|
|
12674
|
+
});
|
|
12675
|
+
}
|
|
12676
|
+
}
|
|
12656
12677
|
handleMessage(data) {
|
|
12657
12678
|
const text = typeof data === "string" ? data : Buffer.isBuffer(data) ? data.toString() : String(data);
|
|
12658
12679
|
this.markInboundActivity();
|
|
@@ -12719,11 +12740,22 @@ var RelayClient = class {
|
|
|
12719
12740
|
this.opts.logger.info('Relay tunnel: \u2192 heartbeat "ping"');
|
|
12720
12741
|
try {
|
|
12721
12742
|
ws.send("ping");
|
|
12722
|
-
} catch {
|
|
12743
|
+
} catch (err2) {
|
|
12744
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
12745
|
+
this.opts.logger.warn(
|
|
12746
|
+
`Relay tunnel: heartbeat text ping failed, forcing reconnect: ${message}`
|
|
12747
|
+
);
|
|
12748
|
+
this.forceReconnectFromSocket(ws, `heartbeat-send-failed: ${message}`);
|
|
12749
|
+
return;
|
|
12723
12750
|
}
|
|
12724
12751
|
try {
|
|
12725
12752
|
ws.ping();
|
|
12726
|
-
} catch {
|
|
12753
|
+
} catch (err2) {
|
|
12754
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
12755
|
+
this.opts.logger.warn(
|
|
12756
|
+
`Relay tunnel: heartbeat control ping failed, forcing reconnect: ${message}`
|
|
12757
|
+
);
|
|
12758
|
+
this.forceReconnectFromSocket(ws, `heartbeat-ping-failed: ${message}`);
|
|
12727
12759
|
}
|
|
12728
12760
|
}
|
|
12729
12761
|
markInboundActivity() {
|
|
@@ -12755,6 +12787,7 @@ var RelayClient = class {
|
|
|
12755
12787
|
this.stopHeartbeat();
|
|
12756
12788
|
this.ws = null;
|
|
12757
12789
|
this.writeStatus("disconnected", reason);
|
|
12790
|
+
this.emitDisconnected(reason);
|
|
12758
12791
|
this.scheduleReconnect();
|
|
12759
12792
|
try {
|
|
12760
12793
|
if (ws.readyState !== wrapper_default.CLOSED) {
|
|
@@ -13054,6 +13087,7 @@ async function handleHttpRequest(opts, frame) {
|
|
|
13054
13087
|
const res = await fetch(url.toString(), {
|
|
13055
13088
|
method: frame.method,
|
|
13056
13089
|
headers: attempt.headers,
|
|
13090
|
+
signal: opts.abortSignal,
|
|
13057
13091
|
body: frame.method !== "GET" && frame.method !== "HEAD" ? frame.body : void 0
|
|
13058
13092
|
});
|
|
13059
13093
|
const hasFallback = attemptIndex < authAttempts.length - 1;
|
|
@@ -13122,6 +13156,9 @@ async function streamResponse(opts, requestId, res, startedAtMs) {
|
|
|
13122
13156
|
opts.logger.info(`TunnelProxy: stream start id=${requestId}`);
|
|
13123
13157
|
try {
|
|
13124
13158
|
while (true) {
|
|
13159
|
+
if (opts.abortSignal?.aborted) {
|
|
13160
|
+
throw new DOMException("relay tunnel disconnected", "AbortError");
|
|
13161
|
+
}
|
|
13125
13162
|
const { done, value } = await reader.read();
|
|
13126
13163
|
if (done) break;
|
|
13127
13164
|
chunkCount++;
|
|
@@ -13159,6 +13196,7 @@ async function streamResponse(opts, requestId, res, startedAtMs) {
|
|
|
13159
13196
|
}
|
|
13160
13197
|
|
|
13161
13198
|
// src/tunnel/ws-proxy.ts
|
|
13199
|
+
var GATEWAY_WS_HANDSHAKE_TIMEOUT_MS = 1e4;
|
|
13162
13200
|
var WsProxy = class {
|
|
13163
13201
|
constructor(opts) {
|
|
13164
13202
|
this.opts = opts;
|
|
@@ -13169,14 +13207,15 @@ var WsProxy = class {
|
|
|
13169
13207
|
return this.connections.size;
|
|
13170
13208
|
}
|
|
13171
13209
|
cleanup() {
|
|
13172
|
-
|
|
13210
|
+
const connections = [...this.connections.entries()];
|
|
13211
|
+
this.connections.clear();
|
|
13212
|
+
for (const [id, ws] of connections) {
|
|
13173
13213
|
this.opts.logger.info(`WsProxy: closing WS id=${id}`);
|
|
13174
13214
|
try {
|
|
13175
13215
|
ws.close();
|
|
13176
13216
|
} catch {
|
|
13177
13217
|
}
|
|
13178
13218
|
}
|
|
13179
|
-
this.connections.clear();
|
|
13180
13219
|
}
|
|
13181
13220
|
handleWsOpen(frame) {
|
|
13182
13221
|
const wsUrl = this.opts.gatewayBaseUrl.replace(/^http/, "ws") + mapPath(frame.path);
|
|
@@ -13184,11 +13223,72 @@ var WsProxy = class {
|
|
|
13184
13223
|
`TunnelProxy: WS open id=${frame.id}, path=${frame.path} \u2192 ${wsUrl}`
|
|
13185
13224
|
);
|
|
13186
13225
|
try {
|
|
13226
|
+
const existing = this.connections.get(frame.id);
|
|
13227
|
+
if (existing) {
|
|
13228
|
+
this.opts.logger.warn(
|
|
13229
|
+
`TunnelProxy: WS id=${frame.id} already exists, closing previous gateway connection`
|
|
13230
|
+
);
|
|
13231
|
+
try {
|
|
13232
|
+
existing.close(1e3, "replaced by new relay ws_open");
|
|
13233
|
+
} catch {
|
|
13234
|
+
}
|
|
13235
|
+
}
|
|
13187
13236
|
const ws = new wrapper_default(wsUrl, {
|
|
13188
|
-
headers: frame.headers
|
|
13237
|
+
headers: frame.headers,
|
|
13238
|
+
handshakeTimeout: GATEWAY_WS_HANDSHAKE_TIMEOUT_MS
|
|
13189
13239
|
});
|
|
13240
|
+
this.connections.set(frame.id, ws);
|
|
13241
|
+
let notifiedClosed = false;
|
|
13242
|
+
let connectTimer = setTimeout(
|
|
13243
|
+
() => {
|
|
13244
|
+
connectTimer = null;
|
|
13245
|
+
if (this.connections.get(frame.id) !== ws || ws.readyState === wrapper_default.OPEN) {
|
|
13246
|
+
return;
|
|
13247
|
+
}
|
|
13248
|
+
this.opts.logger.warn(
|
|
13249
|
+
`TunnelProxy: WS id=${frame.id} connect timed out after ${GATEWAY_WS_HANDSHAKE_TIMEOUT_MS}ms`
|
|
13250
|
+
);
|
|
13251
|
+
notifyClosed(1011, "gateway websocket connect timeout");
|
|
13252
|
+
try {
|
|
13253
|
+
if (typeof ws.terminate === "function") {
|
|
13254
|
+
ws.terminate();
|
|
13255
|
+
} else {
|
|
13256
|
+
ws.close();
|
|
13257
|
+
}
|
|
13258
|
+
} catch {
|
|
13259
|
+
}
|
|
13260
|
+
},
|
|
13261
|
+
GATEWAY_WS_HANDSHAKE_TIMEOUT_MS
|
|
13262
|
+
);
|
|
13263
|
+
const clearConnectTimer = () => {
|
|
13264
|
+
if (connectTimer) {
|
|
13265
|
+
clearTimeout(connectTimer);
|
|
13266
|
+
connectTimer = null;
|
|
13267
|
+
}
|
|
13268
|
+
};
|
|
13269
|
+
const notifyClosed = (code, reason) => {
|
|
13270
|
+
if (notifiedClosed) return;
|
|
13271
|
+
notifiedClosed = true;
|
|
13272
|
+
clearConnectTimer();
|
|
13273
|
+
if (this.connections.get(frame.id) === ws) {
|
|
13274
|
+
this.connections.delete(frame.id);
|
|
13275
|
+
}
|
|
13276
|
+
this.opts.client.send({
|
|
13277
|
+
type: "ws_close",
|
|
13278
|
+
id: frame.id,
|
|
13279
|
+
code,
|
|
13280
|
+
reason
|
|
13281
|
+
});
|
|
13282
|
+
};
|
|
13190
13283
|
ws.on("open", () => {
|
|
13191
|
-
|
|
13284
|
+
clearConnectTimer();
|
|
13285
|
+
if (this.connections.get(frame.id) !== ws) {
|
|
13286
|
+
try {
|
|
13287
|
+
ws.close(1e3, "stale relay ws_open");
|
|
13288
|
+
} catch {
|
|
13289
|
+
}
|
|
13290
|
+
return;
|
|
13291
|
+
}
|
|
13192
13292
|
this.opts.logger.info(
|
|
13193
13293
|
`TunnelProxy: WS id=${frame.id} connected to gateway, active=${this.connections.size}`
|
|
13194
13294
|
);
|
|
@@ -13198,6 +13298,9 @@ var WsProxy = class {
|
|
|
13198
13298
|
});
|
|
13199
13299
|
});
|
|
13200
13300
|
ws.on("message", (data) => {
|
|
13301
|
+
if (this.connections.get(frame.id) !== ws) {
|
|
13302
|
+
return;
|
|
13303
|
+
}
|
|
13201
13304
|
const text = typeof data === "string" ? data : Buffer.isBuffer(data) ? data.toString() : String(data);
|
|
13202
13305
|
this.opts.logger.info(
|
|
13203
13306
|
`TunnelProxy: WS id=${frame.id} \u2190 gateway data (${text.length} chars): ${text.substring(0, 200)}`
|
|
@@ -13209,28 +13312,33 @@ var WsProxy = class {
|
|
|
13209
13312
|
});
|
|
13210
13313
|
});
|
|
13211
13314
|
ws.on("close", (code, reason) => {
|
|
13212
|
-
|
|
13315
|
+
clearConnectTimer();
|
|
13316
|
+
const reasonText = reason.toString();
|
|
13317
|
+
const isCurrent = this.connections.get(frame.id) === ws;
|
|
13318
|
+
if (isCurrent) {
|
|
13319
|
+
this.connections.delete(frame.id);
|
|
13320
|
+
}
|
|
13213
13321
|
this.opts.logger.info(
|
|
13214
|
-
`TunnelProxy: WS id=${frame.id} closed by gateway (code=${code}, reason=${
|
|
13322
|
+
`TunnelProxy: WS id=${frame.id} closed by gateway (code=${code}, reason=${reasonText}), active=${this.connections.size}`
|
|
13215
13323
|
);
|
|
13216
|
-
|
|
13217
|
-
|
|
13218
|
-
|
|
13219
|
-
code,
|
|
13220
|
-
reason: reason.toString()
|
|
13221
|
-
});
|
|
13324
|
+
if (isCurrent) {
|
|
13325
|
+
notifyClosed(code, reasonText);
|
|
13326
|
+
}
|
|
13222
13327
|
});
|
|
13223
13328
|
ws.on("error", (err2) => {
|
|
13329
|
+
clearConnectTimer();
|
|
13224
13330
|
this.opts.logger.warn(
|
|
13225
13331
|
`TunnelProxy: WS id=${frame.id} error: ${err2.message}, active=${this.connections.size}`
|
|
13226
13332
|
);
|
|
13227
|
-
this.connections.
|
|
13228
|
-
|
|
13229
|
-
|
|
13230
|
-
|
|
13231
|
-
|
|
13232
|
-
|
|
13233
|
-
|
|
13333
|
+
if (this.connections.get(frame.id) === ws) {
|
|
13334
|
+
notifyClosed(1011, err2.message);
|
|
13335
|
+
}
|
|
13336
|
+
try {
|
|
13337
|
+
if (typeof ws.terminate === "function") {
|
|
13338
|
+
ws.terminate();
|
|
13339
|
+
}
|
|
13340
|
+
} catch {
|
|
13341
|
+
}
|
|
13234
13342
|
});
|
|
13235
13343
|
} catch (err2) {
|
|
13236
13344
|
this.opts.logger.error(
|
|
@@ -13278,8 +13386,17 @@ var WsProxy = class {
|
|
|
13278
13386
|
|
|
13279
13387
|
// src/tunnel/proxy.ts
|
|
13280
13388
|
var RELAY_TUNNEL_GATEWAY_CLIENT_INSTANCE_ID = "phone-notifications-relay-tunnel";
|
|
13389
|
+
var GATEWAY_OPERATOR_SCOPES = [
|
|
13390
|
+
"operator.admin",
|
|
13391
|
+
"operator.approvals",
|
|
13392
|
+
"operator.pairing",
|
|
13393
|
+
"operator.read",
|
|
13394
|
+
"operator.write"
|
|
13395
|
+
];
|
|
13281
13396
|
var MAX_AUTO_PAIRING_APPROVALS = 3;
|
|
13282
13397
|
var RECENT_ABORTED_CHAT_RUN_TTL_MS = 6e4;
|
|
13398
|
+
var GATEWAY_RPC_HANDSHAKE_TIMEOUT_MS = 1e4;
|
|
13399
|
+
var MAX_GATEWAY_WS_PENDING = 200;
|
|
13283
13400
|
var approveDevicePairingPromise = null;
|
|
13284
13401
|
var approveDevicePairingWarned = false;
|
|
13285
13402
|
function formatErrorMessage(err2) {
|
|
@@ -13331,6 +13448,8 @@ var TunnelProxy = class {
|
|
|
13331
13448
|
gatewayWsReady = false;
|
|
13332
13449
|
/** 收到本地自动配对成功后,在 close 回调里触发重连 */
|
|
13333
13450
|
gatewayWsReconnectRequested = false;
|
|
13451
|
+
/** 本地自动配对审批进行中,期间 Gateway 关闭连接时先保留 pending 请求。 */
|
|
13452
|
+
gatewayWsPairingApprovalPending = false;
|
|
13334
13453
|
/** 防止配对失败时无限重连 */
|
|
13335
13454
|
gatewayWsAutoPairingApprovals = 0;
|
|
13336
13455
|
/** 等待 Gateway WS 握手完成后发送的帧队列 */
|
|
@@ -13339,6 +13458,8 @@ var TunnelProxy = class {
|
|
|
13339
13458
|
gatewayReqMetaById = /* @__PURE__ */ new Map();
|
|
13340
13459
|
/** 最近由用户手动中断的 chat.run,用于过滤 runtime 误补发的 synthetic failure。 */
|
|
13341
13460
|
recentAbortedChatRuns = /* @__PURE__ */ new Map();
|
|
13461
|
+
/** Relay 断开或服务停止时要中断的 HTTP 代理请求。 */
|
|
13462
|
+
httpAbortControllers = /* @__PURE__ */ new Map();
|
|
13342
13463
|
/** 设备身份,用于 Gateway connect 握手 */
|
|
13343
13464
|
deviceIdentity;
|
|
13344
13465
|
stateDir;
|
|
@@ -13352,7 +13473,7 @@ var TunnelProxy = class {
|
|
|
13352
13473
|
);
|
|
13353
13474
|
switch (frame.type) {
|
|
13354
13475
|
case "request":
|
|
13355
|
-
await
|
|
13476
|
+
await this.handleRequestFrame(frame);
|
|
13356
13477
|
break;
|
|
13357
13478
|
case "req":
|
|
13358
13479
|
this.handleReqFrame(frame);
|
|
@@ -13380,35 +13501,73 @@ var TunnelProxy = class {
|
|
|
13380
13501
|
`TunnelProxy: cleanup, closing ${this.wsProxy.activeCount} active WS connections, gatewayWs=${!!this.gatewayWs}`
|
|
13381
13502
|
);
|
|
13382
13503
|
this.wsProxy.cleanup();
|
|
13383
|
-
|
|
13384
|
-
|
|
13385
|
-
|
|
13386
|
-
} catch {
|
|
13387
|
-
}
|
|
13388
|
-
this.gatewayWs = null;
|
|
13504
|
+
for (const [id, controller] of this.httpAbortControllers) {
|
|
13505
|
+
this.opts.logger.info(`TunnelProxy: aborting HTTP proxy id=${id}`);
|
|
13506
|
+
controller.abort();
|
|
13389
13507
|
}
|
|
13508
|
+
this.httpAbortControllers.clear();
|
|
13509
|
+
const gatewayWs = this.gatewayWs;
|
|
13510
|
+
this.gatewayWs = null;
|
|
13390
13511
|
this.gatewayWsReady = false;
|
|
13391
13512
|
this.gatewayWsConnecting = false;
|
|
13392
13513
|
this.gatewayWsReconnectRequested = false;
|
|
13514
|
+
this.gatewayWsPairingApprovalPending = false;
|
|
13393
13515
|
this.gatewayWsAutoPairingApprovals = 0;
|
|
13394
13516
|
this.gatewayWsPending = [];
|
|
13395
13517
|
this.gatewayReqMetaById.clear();
|
|
13396
13518
|
this.recentAbortedChatRuns.clear();
|
|
13519
|
+
if (gatewayWs) {
|
|
13520
|
+
try {
|
|
13521
|
+
gatewayWs.close();
|
|
13522
|
+
} catch {
|
|
13523
|
+
}
|
|
13524
|
+
}
|
|
13397
13525
|
}
|
|
13398
13526
|
// ─── Gateway RPC WebSocket ───
|
|
13399
|
-
|
|
13400
|
-
|
|
13527
|
+
async handleRequestFrame(frame) {
|
|
13528
|
+
const controller = new AbortController();
|
|
13529
|
+
this.httpAbortControllers.set(frame.id, controller);
|
|
13530
|
+
try {
|
|
13531
|
+
await handleHttpRequest(
|
|
13532
|
+
{
|
|
13533
|
+
...this.opts,
|
|
13534
|
+
abortSignal: controller.signal
|
|
13535
|
+
},
|
|
13536
|
+
frame
|
|
13537
|
+
);
|
|
13538
|
+
} finally {
|
|
13539
|
+
this.httpAbortControllers.delete(frame.id);
|
|
13540
|
+
}
|
|
13541
|
+
}
|
|
13542
|
+
pushGatewayPending(entry, reason) {
|
|
13543
|
+
if (this.gatewayWsPending.length >= MAX_GATEWAY_WS_PENDING) {
|
|
13544
|
+
this.opts.logger.error(
|
|
13545
|
+
`TunnelProxy: gateway WS pending queue overflow (${this.gatewayWsPending.length}/${MAX_GATEWAY_WS_PENDING}); failing ${entry.reqId ? `req id=${entry.reqId}` : "raw frame"}`
|
|
13546
|
+
);
|
|
13547
|
+
this.failGatewayPendingEntry(
|
|
13548
|
+
entry,
|
|
13549
|
+
"GATEWAY_RPC_QUEUE_FULL",
|
|
13550
|
+
"local gateway RPC queue is full"
|
|
13551
|
+
);
|
|
13552
|
+
return false;
|
|
13553
|
+
}
|
|
13554
|
+
this.gatewayWsPending.push(entry);
|
|
13401
13555
|
this.opts.logger.info(
|
|
13402
13556
|
`TunnelProxy: gateway WS pending queue size=${this.gatewayWsPending.length} (${reason})`
|
|
13403
13557
|
);
|
|
13558
|
+
return true;
|
|
13404
13559
|
}
|
|
13405
13560
|
/** 将未知帧原样转发到 Gateway WS */
|
|
13406
13561
|
forwardRawToGateway(payload) {
|
|
13407
|
-
this.ensureGatewayWs();
|
|
13408
13562
|
if (this.gatewayWsReady && this.gatewayWs?.readyState === wrapper_default.OPEN) {
|
|
13409
|
-
this.gatewayWs
|
|
13563
|
+
this.sendGatewayPayload(this.gatewayWs, { payload }, "raw frame");
|
|
13410
13564
|
} else {
|
|
13411
|
-
this.pushGatewayPending(
|
|
13565
|
+
if (this.pushGatewayPending(
|
|
13566
|
+
{ payload },
|
|
13567
|
+
"raw frame queued before gateway WS ready"
|
|
13568
|
+
)) {
|
|
13569
|
+
this.ensureGatewayWs();
|
|
13570
|
+
}
|
|
13412
13571
|
}
|
|
13413
13572
|
}
|
|
13414
13573
|
/** 处理 Relay 转发的 Gateway RPC 请求帧,原样通过 WebSocket 发给本地 Gateway */
|
|
@@ -13421,17 +13580,99 @@ var TunnelProxy = class {
|
|
|
13421
13580
|
this.opts.logger.info(
|
|
13422
13581
|
`TunnelProxy: req id=${frame.id} method=${frame.method} \u2192 gateway WS (${payload.length} chars)`
|
|
13423
13582
|
);
|
|
13424
|
-
this.ensureGatewayWs();
|
|
13425
13583
|
if (this.gatewayWsReady && this.gatewayWs?.readyState === wrapper_default.OPEN) {
|
|
13426
|
-
this.
|
|
13584
|
+
this.sendGatewayPayload(
|
|
13585
|
+
this.gatewayWs,
|
|
13586
|
+
{ payload, reqId: frame.id },
|
|
13587
|
+
`req id=${frame.id}`
|
|
13588
|
+
);
|
|
13427
13589
|
} else {
|
|
13428
13590
|
this.opts.logger.info(
|
|
13429
13591
|
`TunnelProxy: req id=${frame.id} queued, gateway WS not ready yet`
|
|
13430
13592
|
);
|
|
13431
|
-
this.pushGatewayPending(
|
|
13432
|
-
payload,
|
|
13593
|
+
if (this.pushGatewayPending(
|
|
13594
|
+
{ payload, reqId: frame.id },
|
|
13433
13595
|
`req id=${frame.id} queued before gateway WS handshake`
|
|
13596
|
+
)) {
|
|
13597
|
+
this.ensureGatewayWs();
|
|
13598
|
+
}
|
|
13599
|
+
}
|
|
13600
|
+
}
|
|
13601
|
+
sendGatewayRpcError(id, code, message) {
|
|
13602
|
+
this.gatewayReqMetaById.delete(id);
|
|
13603
|
+
this.opts.client.sendRaw(
|
|
13604
|
+
JSON.stringify({
|
|
13605
|
+
type: "res",
|
|
13606
|
+
id,
|
|
13607
|
+
ok: false,
|
|
13608
|
+
error: {
|
|
13609
|
+
code,
|
|
13610
|
+
message
|
|
13611
|
+
}
|
|
13612
|
+
})
|
|
13613
|
+
);
|
|
13614
|
+
}
|
|
13615
|
+
failGatewayPendingEntry(entry, code, message) {
|
|
13616
|
+
if (!entry.reqId) return;
|
|
13617
|
+
this.sendGatewayRpcError(entry.reqId, code, message);
|
|
13618
|
+
}
|
|
13619
|
+
failGatewayPending(code, message) {
|
|
13620
|
+
if (this.gatewayWsPending.length === 0) return;
|
|
13621
|
+
const pending = this.gatewayWsPending;
|
|
13622
|
+
this.gatewayWsPending = [];
|
|
13623
|
+
this.opts.logger.warn(
|
|
13624
|
+
`TunnelProxy: failing ${pending.length} pending gateway RPC frame(s): ${message}`
|
|
13625
|
+
);
|
|
13626
|
+
for (const entry of pending) {
|
|
13627
|
+
this.failGatewayPendingEntry(entry, code, message);
|
|
13628
|
+
}
|
|
13629
|
+
}
|
|
13630
|
+
failGatewayInflight(code, message) {
|
|
13631
|
+
if (this.gatewayReqMetaById.size === 0) return;
|
|
13632
|
+
const ids = [...this.gatewayReqMetaById.keys()];
|
|
13633
|
+
this.opts.logger.warn(
|
|
13634
|
+
`TunnelProxy: failing ${ids.length} in-flight gateway RPC request(s): ${message}`
|
|
13635
|
+
);
|
|
13636
|
+
for (const id of ids) {
|
|
13637
|
+
this.sendGatewayRpcError(id, code, message);
|
|
13638
|
+
}
|
|
13639
|
+
}
|
|
13640
|
+
failAllGatewayRequests(code, message) {
|
|
13641
|
+
this.failGatewayPending(code, message);
|
|
13642
|
+
this.failGatewayInflight(code, message);
|
|
13643
|
+
}
|
|
13644
|
+
sendGatewayPayload(ws, entry, context) {
|
|
13645
|
+
try {
|
|
13646
|
+
ws.send(entry.payload);
|
|
13647
|
+
return true;
|
|
13648
|
+
} catch (err2) {
|
|
13649
|
+
const message = formatErrorMessage(err2);
|
|
13650
|
+
this.opts.logger.warn(
|
|
13651
|
+
`TunnelProxy: RPC WS send failed for ${context}: ${message}`
|
|
13652
|
+
);
|
|
13653
|
+
this.failGatewayPendingEntry(
|
|
13654
|
+
entry,
|
|
13655
|
+
"GATEWAY_RPC_SEND_FAILED",
|
|
13656
|
+
`local gateway RPC send failed: ${message}`
|
|
13657
|
+
);
|
|
13658
|
+
this.failGatewayInflight(
|
|
13659
|
+
"GATEWAY_RPC_SEND_FAILED",
|
|
13660
|
+
"local gateway RPC connection closed after send failure"
|
|
13434
13661
|
);
|
|
13662
|
+
if (this.gatewayWs === ws) {
|
|
13663
|
+
this.gatewayWs = null;
|
|
13664
|
+
this.gatewayWsReady = false;
|
|
13665
|
+
this.gatewayWsConnecting = false;
|
|
13666
|
+
}
|
|
13667
|
+
try {
|
|
13668
|
+
if (typeof ws.terminate === "function") {
|
|
13669
|
+
ws.terminate();
|
|
13670
|
+
} else {
|
|
13671
|
+
ws.close();
|
|
13672
|
+
}
|
|
13673
|
+
} catch {
|
|
13674
|
+
}
|
|
13675
|
+
return false;
|
|
13435
13676
|
}
|
|
13436
13677
|
}
|
|
13437
13678
|
// ─── Gateway connect auth helpers ───
|
|
@@ -13511,13 +13752,16 @@ var TunnelProxy = class {
|
|
|
13511
13752
|
);
|
|
13512
13753
|
return false;
|
|
13513
13754
|
}
|
|
13755
|
+
this.gatewayWsPairingApprovalPending = true;
|
|
13514
13756
|
try {
|
|
13515
13757
|
const approveDevicePairing = await loadApproveDevicePairing(this.opts.logger);
|
|
13516
13758
|
if (!approveDevicePairing) {
|
|
13759
|
+
this.gatewayWsPairingApprovalPending = false;
|
|
13517
13760
|
return false;
|
|
13518
13761
|
}
|
|
13519
13762
|
const approved = await approveDevicePairing(requestId, this.hostStateDir);
|
|
13520
13763
|
if (!approved) {
|
|
13764
|
+
this.gatewayWsPairingApprovalPending = false;
|
|
13521
13765
|
this.opts.logger.warn(
|
|
13522
13766
|
`TunnelProxy: gateway pairing request ${requestId} not found in host state ${this.hostStateDir}`
|
|
13523
13767
|
);
|
|
@@ -13525,11 +13769,13 @@ var TunnelProxy = class {
|
|
|
13525
13769
|
}
|
|
13526
13770
|
this.gatewayWsAutoPairingApprovals += 1;
|
|
13527
13771
|
this.gatewayWsReconnectRequested = true;
|
|
13772
|
+
this.gatewayWsPairingApprovalPending = false;
|
|
13528
13773
|
this.opts.logger.info(
|
|
13529
13774
|
`TunnelProxy: auto-approved local gateway pairing request ${requestId} (reason=${reason || "not-paired"}, hostStateDir=${this.hostStateDir}, approval=${this.gatewayWsAutoPairingApprovals}/${MAX_AUTO_PAIRING_APPROVALS})`
|
|
13530
13775
|
);
|
|
13531
13776
|
return true;
|
|
13532
13777
|
} catch (err2) {
|
|
13778
|
+
this.gatewayWsPairingApprovalPending = false;
|
|
13533
13779
|
this.opts.logger.warn(
|
|
13534
13780
|
`TunnelProxy: failed to auto-approve gateway pairing request ${requestId}: ${err2?.message ?? String(err2)}`
|
|
13535
13781
|
);
|
|
@@ -13658,9 +13904,56 @@ var TunnelProxy = class {
|
|
|
13658
13904
|
this.opts.logger.info(
|
|
13659
13905
|
`TunnelProxy: RPC WS connecting to gateway ${wsUrl} (pending=${this.gatewayWsPending.length})`
|
|
13660
13906
|
);
|
|
13661
|
-
|
|
13907
|
+
let ws;
|
|
13908
|
+
try {
|
|
13909
|
+
ws = new wrapper_default(wsUrl, {
|
|
13910
|
+
handshakeTimeout: GATEWAY_RPC_HANDSHAKE_TIMEOUT_MS
|
|
13911
|
+
});
|
|
13912
|
+
} catch (err2) {
|
|
13913
|
+
const message = formatErrorMessage(err2);
|
|
13914
|
+
this.gatewayWsConnecting = false;
|
|
13915
|
+
this.opts.logger.error(
|
|
13916
|
+
`TunnelProxy: RPC WS failed to create gateway connection: ${message}`
|
|
13917
|
+
);
|
|
13918
|
+
this.failAllGatewayRequests(
|
|
13919
|
+
"GATEWAY_RPC_CONNECT_FAILED",
|
|
13920
|
+
`local gateway RPC connect failed: ${message}`
|
|
13921
|
+
);
|
|
13922
|
+
return;
|
|
13923
|
+
}
|
|
13924
|
+
this.gatewayWs = ws;
|
|
13925
|
+
let handshakeTimer = setTimeout(
|
|
13926
|
+
() => {
|
|
13927
|
+
handshakeTimer = null;
|
|
13928
|
+
if (this.gatewayWs !== ws || this.gatewayWsReady) return;
|
|
13929
|
+
this.opts.logger.warn(
|
|
13930
|
+
`TunnelProxy: RPC WS handshake timed out after ${GATEWAY_RPC_HANDSHAKE_TIMEOUT_MS}ms (pending=${this.gatewayWsPending.length})`
|
|
13931
|
+
);
|
|
13932
|
+
this.gatewayWs = null;
|
|
13933
|
+
this.gatewayWsReady = false;
|
|
13934
|
+
this.gatewayWsConnecting = false;
|
|
13935
|
+
this.failAllGatewayRequests(
|
|
13936
|
+
"GATEWAY_RPC_HANDSHAKE_TIMEOUT",
|
|
13937
|
+
"local gateway RPC handshake timed out"
|
|
13938
|
+
);
|
|
13939
|
+
try {
|
|
13940
|
+
if (typeof ws.terminate === "function") {
|
|
13941
|
+
ws.terminate();
|
|
13942
|
+
} else {
|
|
13943
|
+
ws.close();
|
|
13944
|
+
}
|
|
13945
|
+
} catch {
|
|
13946
|
+
}
|
|
13947
|
+
},
|
|
13948
|
+
GATEWAY_RPC_HANDSHAKE_TIMEOUT_MS
|
|
13949
|
+
);
|
|
13950
|
+
const clearHandshakeTimer = () => {
|
|
13951
|
+
if (handshakeTimer) {
|
|
13952
|
+
clearTimeout(handshakeTimer);
|
|
13953
|
+
handshakeTimer = null;
|
|
13954
|
+
}
|
|
13955
|
+
};
|
|
13662
13956
|
ws.on("open", () => {
|
|
13663
|
-
this.gatewayWs = ws;
|
|
13664
13957
|
this.opts.logger.info(
|
|
13665
13958
|
`TunnelProxy: RPC WS tcp connected, waiting for connect.challenge (pending=${this.gatewayWsPending.length})`
|
|
13666
13959
|
);
|
|
@@ -13692,9 +13985,10 @@ var TunnelProxy = class {
|
|
|
13692
13985
|
return;
|
|
13693
13986
|
}
|
|
13694
13987
|
if (frame.type === "res" && frame.ok === true && frame.payload?.type === "hello-ok") {
|
|
13988
|
+
clearHandshakeTimer();
|
|
13695
13989
|
this.storeIssuedDeviceToken({
|
|
13696
13990
|
fallbackRole: "operator",
|
|
13697
|
-
fallbackScopes: [
|
|
13991
|
+
fallbackScopes: [...GATEWAY_OPERATOR_SCOPES],
|
|
13698
13992
|
authInfo: frame.payload?.auth
|
|
13699
13993
|
});
|
|
13700
13994
|
this.gatewayWsAutoPairingApprovals = 0;
|
|
@@ -13704,10 +13998,14 @@ var TunnelProxy = class {
|
|
|
13704
13998
|
this.opts.logger.info(
|
|
13705
13999
|
`TunnelProxy: RPC WS handshake done (hello-ok), flushing ${this.gatewayWsPending.length} pending frames`
|
|
13706
14000
|
);
|
|
13707
|
-
|
|
13708
|
-
ws.send(pending);
|
|
13709
|
-
}
|
|
14001
|
+
const pending = this.gatewayWsPending;
|
|
13710
14002
|
this.gatewayWsPending = [];
|
|
14003
|
+
for (let i = 0; i < pending.length; i++) {
|
|
14004
|
+
const entry = pending[i];
|
|
14005
|
+
if (!this.sendGatewayPayload(ws, entry, entry.reqId ?? "pending frame")) {
|
|
14006
|
+
return;
|
|
14007
|
+
}
|
|
14008
|
+
}
|
|
13711
14009
|
return;
|
|
13712
14010
|
}
|
|
13713
14011
|
if (frame.type === "res" && frame.ok === false && !this.gatewayWsReady) {
|
|
@@ -13728,6 +14026,11 @@ var TunnelProxy = class {
|
|
|
13728
14026
|
this.opts.logger.error(
|
|
13729
14027
|
`TunnelProxy: RPC WS handshake failed (pending=${this.gatewayWsPending.length}): ${previewText(JSON.stringify(frame.error), 500)}`
|
|
13730
14028
|
);
|
|
14029
|
+
clearHandshakeTimer();
|
|
14030
|
+
this.failAllGatewayRequests(
|
|
14031
|
+
"GATEWAY_RPC_HANDSHAKE_FAILED",
|
|
14032
|
+
`local gateway RPC handshake failed: ${previewText(JSON.stringify(frame.error), 500)}`
|
|
14033
|
+
);
|
|
13731
14034
|
ws.close();
|
|
13732
14035
|
return;
|
|
13733
14036
|
}
|
|
@@ -13737,10 +14040,12 @@ var TunnelProxy = class {
|
|
|
13737
14040
|
}
|
|
13738
14041
|
});
|
|
13739
14042
|
ws.on("close", (code, reason) => {
|
|
14043
|
+
clearHandshakeTimer();
|
|
13740
14044
|
const wasReady = this.gatewayWsReady;
|
|
13741
14045
|
const pendingCount = this.gatewayWsPending.length;
|
|
13742
14046
|
const reasonText = reason.toString();
|
|
13743
14047
|
const shouldReconnect = this.gatewayWsReconnectRequested && pendingCount > 0;
|
|
14048
|
+
const shouldHoldForPairing = this.gatewayWsPairingApprovalPending && pendingCount > 0;
|
|
13744
14049
|
this.opts.logger.info(
|
|
13745
14050
|
`TunnelProxy: RPC WS closed by gateway (code=${code}, reason=${reasonText}, ready=${wasReady}, pending=${pendingCount}, activeWs=${this.wsProxy.activeCount})`
|
|
13746
14051
|
);
|
|
@@ -13756,24 +14061,43 @@ var TunnelProxy = class {
|
|
|
13756
14061
|
`TunnelProxy: retrying RPC WS after local pairing approval (pending=${this.gatewayWsPending.length})`
|
|
13757
14062
|
);
|
|
13758
14063
|
queueMicrotask(() => this.ensureGatewayWs());
|
|
14064
|
+
} else if (!shouldHoldForPairing && (pendingCount > 0 || this.gatewayReqMetaById.size > 0)) {
|
|
14065
|
+
this.failAllGatewayRequests(
|
|
14066
|
+
"GATEWAY_RPC_DISCONNECTED",
|
|
14067
|
+
`local gateway RPC disconnected: code=${code}, reason=${reasonText}`
|
|
14068
|
+
);
|
|
13759
14069
|
}
|
|
13760
14070
|
});
|
|
13761
14071
|
ws.on("error", (err2) => {
|
|
14072
|
+
clearHandshakeTimer();
|
|
13762
14073
|
this.opts.logger.warn(
|
|
13763
14074
|
`TunnelProxy: RPC WS error: ${err2.message} (ready=${this.gatewayWsReady}, pending=${this.gatewayWsPending.length}, activeWs=${this.wsProxy.activeCount})`
|
|
13764
14075
|
);
|
|
14076
|
+
const shouldFailRequests = this.gatewayWs === ws && (this.gatewayWsPending.length > 0 || this.gatewayReqMetaById.size > 0);
|
|
13765
14077
|
this.gatewayWsConnecting = false;
|
|
13766
14078
|
if (this.gatewayWs === ws) {
|
|
13767
14079
|
this.gatewayWs = null;
|
|
13768
14080
|
this.gatewayWsReady = false;
|
|
13769
14081
|
}
|
|
14082
|
+
if (shouldFailRequests) {
|
|
14083
|
+
this.failAllGatewayRequests(
|
|
14084
|
+
"GATEWAY_RPC_ERROR",
|
|
14085
|
+
`local gateway RPC error: ${err2.message}`
|
|
14086
|
+
);
|
|
14087
|
+
}
|
|
14088
|
+
try {
|
|
14089
|
+
if (typeof ws.terminate === "function") {
|
|
14090
|
+
ws.terminate();
|
|
14091
|
+
}
|
|
14092
|
+
} catch {
|
|
14093
|
+
}
|
|
13770
14094
|
});
|
|
13771
14095
|
}
|
|
13772
14096
|
handleConnectChallenge(ws, frame) {
|
|
13773
14097
|
const challengeNonce = frame.payload?.nonce ?? "";
|
|
13774
14098
|
const connectRequestId = `tunnel-connect-${(0, import_node_crypto5.randomUUID)()}`;
|
|
13775
14099
|
const role = "operator";
|
|
13776
|
-
const scopes = [
|
|
14100
|
+
const scopes = [...GATEWAY_OPERATOR_SCOPES];
|
|
13777
14101
|
const gatewayConnectAuth = this.buildGatewayConnectAuth(role);
|
|
13778
14102
|
this.opts.logger.info(
|
|
13779
14103
|
`TunnelProxy: received connect.challenge (nonce=${challengeNonce}, connectReqId=${connectRequestId}, hasToken=${!!gatewayConnectAuth.authToken}, hasPassword=${!!gatewayConnectAuth.authPassword}, hasDeviceToken=${!!gatewayConnectAuth.deviceToken}), sending connect request with device identity`
|
|
@@ -13998,6 +14322,12 @@ function createTunnelService(opts) {
|
|
|
13998
14322
|
client.onConnected(() => {
|
|
13999
14323
|
emitPendingPluginUpdate("relay connected");
|
|
14000
14324
|
});
|
|
14325
|
+
client.onDisconnected((reason) => {
|
|
14326
|
+
logger.warn(
|
|
14327
|
+
`Relay tunnel: relay disconnected, cleaning local proxy state (${reason})`
|
|
14328
|
+
);
|
|
14329
|
+
proxy?.cleanup();
|
|
14330
|
+
});
|
|
14001
14331
|
abortController = new AbortController();
|
|
14002
14332
|
client.connectWithAutoReconnect(abortController.signal).catch((err2) => {
|
|
14003
14333
|
releaseLock();
|
|
@@ -14120,10 +14450,18 @@ function resolveLocalGatewayAuth(params) {
|
|
|
14120
14450
|
gatewayPassword: envGatewayPassword ?? configGatewayPassword
|
|
14121
14451
|
};
|
|
14122
14452
|
}
|
|
14453
|
+
var DISABLED_TAILSCALE_MODES = /* @__PURE__ */ new Set([
|
|
14454
|
+
"off",
|
|
14455
|
+
"disabled",
|
|
14456
|
+
"disable",
|
|
14457
|
+
"none",
|
|
14458
|
+
"false",
|
|
14459
|
+
"no"
|
|
14460
|
+
]);
|
|
14123
14461
|
function resolveExclusiveTunnelHint(params) {
|
|
14124
14462
|
const configData = readHostGatewayConfig(params);
|
|
14125
14463
|
const tailscaleMode = trimToUndefined2(configData?.gateway?.tailscale?.mode);
|
|
14126
|
-
if (tailscaleMode) {
|
|
14464
|
+
if (tailscaleMode && !DISABLED_TAILSCALE_MODES.has(tailscaleMode.toLowerCase())) {
|
|
14127
14465
|
return `gateway.tailscale.mode=${tailscaleMode}`;
|
|
14128
14466
|
}
|
|
14129
14467
|
const remoteUrl = trimToUndefined2(configData?.gateway?.remote?.url);
|