@jingyi0605/codingns 0.5.0 → 0.5.5
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/public/assets/AdaptiveButlerPage-CUyNL98E.js +3 -0
- package/dist/public/assets/App-BFP7LCSC.js +30 -0
- package/dist/public/assets/{BootstrapPage-Vu5oEJ8z.js → BootstrapPage-G74dX2Us.js} +1 -1
- package/dist/public/assets/ConversationPage-Bz0_tvvM.js +2 -0
- package/dist/public/assets/{DesktopDetachPreviewPage-BgeEqbc5.js → DesktopDetachPreviewPage-IV7oEdOX.js} +1 -1
- package/dist/public/assets/DesktopWindowPage-BBmHyRg5.js +2 -0
- package/dist/public/assets/FileContextPanel--FVTxDrq.js +1 -0
- package/dist/public/assets/GitSidebar-DAiSi9oc.js +6 -0
- package/dist/public/assets/{MobileCreateSessionSheet-DLq5qPkx.js → MobileCreateSessionSheet-DqVwz_Hp.js} +1 -1
- package/dist/public/assets/MobileSheet-D1lMrcvD.js +1 -0
- package/dist/public/assets/{MobileTopHeaderFrame-DArgZI7L.js → MobileTopHeaderFrame-COTc7cRr.js} +1 -1
- package/dist/public/assets/{MobileWorkspaceSwitcherHeader-0ywJKfBQ.js → MobileWorkspaceSwitcherHeader-DJPV9ym2.js} +1 -1
- package/dist/public/assets/RelayConnectEntryPage-dSwU8VzK.js +1 -0
- package/dist/public/assets/{ServerSettingsModal-izoYMx9U.js → ServerSettingsModal-B34ms3ze.js} +1 -1
- package/dist/public/assets/{SessionIndexPage-C5aG8FIv.js → SessionIndexPage-D3tG1gmM.js} +1 -1
- package/dist/public/assets/SettingsPage-B3-6-5GL.js +1 -0
- package/dist/public/assets/TerminalManagerPanel-DhuTEdzV.js +1 -0
- package/dist/public/assets/{TerminalPage-CtKXIU0h.js → TerminalPage-DpsvQQVR.js} +19 -19
- package/dist/public/assets/{TerminalRuntimeFallbackModal-CRhOQOsT.js → TerminalRuntimeFallbackModal-CNzOt5v5.js} +1 -1
- package/dist/public/assets/{ToolFilesPage-DcYPsS-e.js → ToolFilesPage-BX9QDi9Y.js} +1 -1
- package/dist/public/assets/{ToolGitPage-CsPl89ty.js → ToolGitPage-4VtFox3p.js} +1 -1
- package/dist/public/assets/{ToolProcessesPage-D0dvR8xK.js → ToolProcessesPage-DZJC6Qnt.js} +1 -1
- package/dist/public/assets/{ToolsHomePage-4fP-KRiv.js → ToolsHomePage-D7JbrAWv.js} +1 -1
- package/dist/public/assets/{WorkbenchLandingPage-kvlfyxRo.js → WorkbenchLandingPage-C0yqnzqh.js} +1 -1
- package/dist/public/assets/WorkbenchLayout-Brlj8K3i.js +3 -0
- package/dist/public/assets/{WorkbenchModal-Ctob14VR.js → WorkbenchModal-CbDxaCOR.js} +1 -1
- package/dist/public/assets/WorkbenchShellRoute-BMcnFadA.css +1 -0
- package/dist/public/assets/WorkbenchShellRoute-puGpdDFY.js +1 -0
- package/dist/public/assets/{WorkspaceDebugDetailPage-Com5kEXJ.js → WorkspaceDebugDetailPage-fTGweC9N.js} +1 -1
- package/dist/public/assets/WorkspaceDetailPage-BtaIzSDB.js +1 -0
- package/dist/public/assets/WorkspaceHomePage-CUmmYDrM.js +1 -0
- package/dist/public/assets/client-runtime-manager-RHFa_iWo.js +1 -0
- package/dist/public/assets/{default-session-permission-mode-CcGwR4Kk.js → default-session-permission-mode-Cu5SreTG.js} +1 -1
- package/dist/public/assets/file-tree-icon-BMKuc5pw.js +31 -0
- package/dist/public/assets/index-Cq3ue0za.css +1 -0
- package/dist/public/assets/index-DEbFT-Aq.js +42 -0
- package/dist/public/assets/{preferences-service-KIYeE2gk.js → preferences-service-gv_9vGKz.js} +1 -1
- package/dist/public/assets/session-runtime-machine-Bfnxkk9B.js +17 -0
- package/dist/public/assets/{terminal-runtime-meta-AWXJpN4r.js → terminal-runtime-meta-B9xJGY__.js} +1 -1
- package/dist/public/assets/{useRegisteredDebugTemplates-DBDRdptr.js → useRegisteredDebugTemplates-CDfl54Wt.js} +1 -1
- package/dist/public/index.html +2 -2
- package/dist/server/config/env.d.ts +1 -0
- package/dist/server/config/env.js +36 -0
- package/dist/server/config/env.js.map +1 -1
- package/dist/server/modules/client/client-controller.js +1 -1
- package/dist/server/modules/client/client-controller.js.map +1 -1
- package/dist/server/modules/client/client-service.d.ts +16 -2
- package/dist/server/modules/client/client-service.js +21 -3
- package/dist/server/modules/client/client-service.js.map +1 -1
- package/dist/server/modules/relay-tunnel/crypto/relay-tunnel-packets.d.ts +16 -1
- package/dist/server/modules/relay-tunnel/crypto/relay-tunnel-packets.js.map +1 -1
- package/dist/server/modules/relay-tunnel/crypto/relay-tunnel-protocol.d.ts +2 -1
- package/dist/server/modules/relay-tunnel/crypto/relay-tunnel-protocol.js +18 -0
- package/dist/server/modules/relay-tunnel/crypto/relay-tunnel-protocol.js.map +1 -1
- package/dist/server/modules/relay-tunnel/relay-tunnel-client-context.d.ts +13 -0
- package/dist/server/modules/relay-tunnel/relay-tunnel-client-context.js +2 -0
- package/dist/server/modules/relay-tunnel/relay-tunnel-client-context.js.map +1 -0
- package/dist/server/modules/relay-tunnel/relay-tunnel-gateway-service.d.ts +6 -0
- package/dist/server/modules/relay-tunnel/relay-tunnel-gateway-service.js +110 -10
- package/dist/server/modules/relay-tunnel/relay-tunnel-gateway-service.js.map +1 -1
- package/dist/server/modules/relay-tunnel/relay-tunnel-runtime-adapter.d.ts +16 -4
- package/dist/server/modules/relay-tunnel/relay-tunnel-runtime-adapter.js +216 -101
- package/dist/server/modules/relay-tunnel/relay-tunnel-runtime-adapter.js.map +1 -1
- package/dist/server/modules/relay-tunnel/relay-tunnel-service.d.ts +6 -2
- package/dist/server/modules/relay-tunnel/relay-tunnel-service.js +457 -40
- package/dist/server/modules/relay-tunnel/relay-tunnel-service.js.map +1 -1
- package/dist/server/modules/sessions/session-activity-inspector.js +6 -8
- package/dist/server/modules/sessions/session-activity-inspector.js.map +1 -1
- package/dist/server/modules/sessions/session-history-service.d.ts +1 -0
- package/dist/server/modules/sessions/session-history-service.js +62 -7
- package/dist/server/modules/sessions/session-history-service.js.map +1 -1
- package/dist/server/modules/workbench/snapshot-revision.d.ts +4 -0
- package/dist/server/modules/workbench/snapshot-revision.js +13 -0
- package/dist/server/modules/workbench/snapshot-revision.js.map +1 -0
- package/dist/server/modules/workbench/workbench-service.d.ts +33 -2
- package/dist/server/modules/workbench/workbench-service.js +39 -4
- package/dist/server/modules/workbench/workbench-service.js.map +1 -1
- package/dist/server/modules/workbench/workspace-panel-snapshot-service.d.ts +6 -1
- package/dist/server/modules/workbench/workspace-panel-snapshot-service.js +10 -8
- package/dist/server/modules/workbench/workspace-panel-snapshot-service.js.map +1 -1
- package/dist/server/server/create-server.js +11 -6
- package/dist/server/server/create-server.js.map +1 -1
- package/dist/server/types/domain.d.ts +9 -0
- package/dist/server/ws/workbench-ws-hub.js +295 -43
- package/dist/server/ws/workbench-ws-hub.js.map +1 -1
- package/dist/server/ws/ws-server.js +141 -8
- package/dist/server/ws/ws-server.js.map +1 -1
- package/node_modules/@codingns/session-sync-core/dist/providers/codex.js +33 -6
- package/node_modules/@codingns/session-sync-core/dist/providers/codex.js.map +1 -1
- package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.js +78 -36
- package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.js.map +1 -1
- package/package.json +1 -1
- package/dist/public/assets/AdaptiveButlerPage-R-XZw7pd.js +0 -3
- package/dist/public/assets/App-DkvE5EyM.js +0 -30
- package/dist/public/assets/ConversationPage-Cjpg6g0J.js +0 -2
- package/dist/public/assets/DesktopWindowPage-1WelvxdH.js +0 -2
- package/dist/public/assets/FileContextPanel-D_ghXJuW.js +0 -1
- package/dist/public/assets/GitSidebar-D9f9Jxwr.js +0 -6
- package/dist/public/assets/MobileSheet-DLg-gX1t.js +0 -1
- package/dist/public/assets/SettingsPage-HJIC-P-4.js +0 -1
- package/dist/public/assets/TerminalManagerPanel-DpyUTo9k.js +0 -1
- package/dist/public/assets/WorkbenchLayout-ByFw4eeu.js +0 -3
- package/dist/public/assets/WorkbenchShellRoute-BUITtdAg.css +0 -1
- package/dist/public/assets/WorkbenchShellRoute-Kw7JEZI3.js +0 -1
- package/dist/public/assets/WorkspaceDetailPage-D0Lrx4Uz.js +0 -1
- package/dist/public/assets/WorkspaceHomePage-wR8d3aP9.js +0 -1
- package/dist/public/assets/file-tree-icon-UFVoVzhM.js +0 -31
- package/dist/public/assets/index-Byp9hJ0c.js +0 -42
- package/dist/public/assets/index-_52jxu4a.css +0 -1
- package/dist/public/assets/session-runtime-machine-0KNSSPp5.js +0 -17
|
@@ -1,9 +1,23 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import https from "node:https";
|
|
3
|
+
import os from "node:os";
|
|
1
4
|
import { AppError } from "../../shared/errors/app-error.js";
|
|
2
5
|
import { decryptSecret, encryptSecret } from "../../shared/utils/secret-box.js";
|
|
3
6
|
import { nowIso } from "../../shared/utils/time.js";
|
|
4
7
|
import { RelayTunnelIdentityService } from "./crypto/relay-tunnel-identity-service.js";
|
|
8
|
+
import { RelayTunnelRuntimeHttpError } from "./relay-tunnel-runtime-adapter.js";
|
|
5
9
|
import { createTaskManager } from "../tasks/task-manager.js";
|
|
6
10
|
import { HOST_TASK_TYPES } from "../tasks/task-types.js";
|
|
11
|
+
const DEFAULT_RELAY_TUNNEL_CONTROL_BASE_URL = normalizeHttpBaseUrl("https://channel.codingns.com:1443", "defaultRelayTunnelControlBaseUrl");
|
|
12
|
+
const LEGACY_RELAY_TUNNEL_CONTROL_BASE_URL = normalizeHttpBaseUrl("https://channel.codingns.com:10247", "legacyRelayTunnelControlBaseUrl");
|
|
13
|
+
const DEFAULT_RELAY_TUNNEL_CONTROL_REQUEST_TIMEOUT_MS = 10_000;
|
|
14
|
+
const DEFAULT_RELAY_TUNNEL_CONTROL_TLS_ECDH_CURVE = "X25519";
|
|
15
|
+
// 线上 channel:1443 的 TLS 握手对 Node 默认参数比较挑,控制站请求统一收敛到单个 X25519,
|
|
16
|
+
// 避免首连阶段出现随机握手失败。
|
|
17
|
+
const RELAY_TUNNEL_CONTROL_HTTPS_AGENT = new https.Agent({
|
|
18
|
+
keepAlive: true,
|
|
19
|
+
ecdhCurve: DEFAULT_RELAY_TUNNEL_CONTROL_TLS_ECDH_CURVE
|
|
20
|
+
});
|
|
7
21
|
export class RelayTunnelService {
|
|
8
22
|
db;
|
|
9
23
|
bootstrapStateRepository;
|
|
@@ -11,6 +25,7 @@ export class RelayTunnelService {
|
|
|
11
25
|
defaultLocalTargetBaseUrl;
|
|
12
26
|
legacyLocalTargetBaseUrl;
|
|
13
27
|
controlSessionSecret;
|
|
28
|
+
controlRequestTimeoutMs;
|
|
14
29
|
fetchFn;
|
|
15
30
|
taskManager;
|
|
16
31
|
runtimeAdapter;
|
|
@@ -23,7 +38,8 @@ export class RelayTunnelService {
|
|
|
23
38
|
this.runtimeAdapter = runtimeAdapter;
|
|
24
39
|
this.identityService = new RelayTunnelIdentityService(identityRepository);
|
|
25
40
|
this.controlSessionSecret = normalizeRequiredText(options.controlSessionSecret, "controlSessionSecret");
|
|
26
|
-
this.
|
|
41
|
+
this.controlRequestTimeoutMs = normalizePositiveInt(options.controlRequestTimeoutMs, DEFAULT_RELAY_TUNNEL_CONTROL_REQUEST_TIMEOUT_MS);
|
|
42
|
+
this.fetchFn = resolveRelayTunnelControlFetch(options.fetchFn);
|
|
27
43
|
this.defaultLocalTargetBaseUrl = normalizeHttpBaseUrl(options.defaultLocalTargetBaseUrl, "defaultLocalTargetBaseUrl");
|
|
28
44
|
this.legacyLocalTargetBaseUrl = options.legacyLocalTargetBaseUrl
|
|
29
45
|
? normalizeHttpBaseUrl(options.legacyLocalTargetBaseUrl, "legacyLocalTargetBaseUrl")
|
|
@@ -62,17 +78,17 @@ export class RelayTunnelService {
|
|
|
62
78
|
}
|
|
63
79
|
async updateConfig(input) {
|
|
64
80
|
const snapshot = this.readStateSnapshot();
|
|
81
|
+
const controlBaseUrl = input.controlBaseUrl !== undefined
|
|
82
|
+
? normalizeHttpBaseUrl(input.controlBaseUrl, "controlBaseUrl")
|
|
83
|
+
: snapshot.config.controlBaseUrl;
|
|
65
84
|
const nextConfig = {
|
|
66
85
|
...snapshot.config,
|
|
67
86
|
activated: input.activated !== undefined
|
|
68
87
|
? input.activated
|
|
69
88
|
: snapshot.config.activated,
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
controlBaseUrl: input.controlBaseUrl !== undefined
|
|
74
|
-
? normalizeHttpBaseUrl(input.controlBaseUrl, "controlBaseUrl")
|
|
75
|
-
: snapshot.config.controlBaseUrl,
|
|
89
|
+
// relay 入口统一跟随控制站点,同一条地址避免再出现 control 能通、relay 走死路的分裂配置。
|
|
90
|
+
relayBaseUrl: deriveRelayBaseUrlFromControlBaseUrl(controlBaseUrl),
|
|
91
|
+
controlBaseUrl,
|
|
76
92
|
localTargetBaseUrl: input.localTargetBaseUrl !== undefined
|
|
77
93
|
? normalizeHttpBaseUrl(input.localTargetBaseUrl, "localTargetBaseUrl")
|
|
78
94
|
: snapshot.config.localTargetBaseUrl,
|
|
@@ -168,10 +184,6 @@ export class RelayTunnelService {
|
|
|
168
184
|
}
|
|
169
185
|
async bindControlHost(hostLabel) {
|
|
170
186
|
const snapshot = this.readStateSnapshot();
|
|
171
|
-
if (snapshot.config.bindingId && snapshot.config.tunnelDomain) {
|
|
172
|
-
const effectiveConfig = this.resolveConfigWithIdentity(snapshot.config);
|
|
173
|
-
return this.buildStatusDto(snapshot, effectiveConfig, this.resolveEffectiveStatus(effectiveConfig));
|
|
174
|
-
}
|
|
175
187
|
const normalizedHostLabel = normalizeRequiredText(hostLabel, "hostLabel");
|
|
176
188
|
const { controlBaseUrl, accessToken, accountId } = this.requireControlSession(snapshot.config);
|
|
177
189
|
const identity = this.identityService.ensureIdentity();
|
|
@@ -218,17 +230,15 @@ export class RelayTunnelService {
|
|
|
218
230
|
const bindingId = normalizeRequiredText(input.bindingId, "bindingId");
|
|
219
231
|
const tunnelDomain = normalizeTunnelDomain(input.tunnelDomain, "tunnelDomain");
|
|
220
232
|
const identity = this.identityService.ensureIdentity();
|
|
221
|
-
const relayBaseUrl = input.relayBaseUrl !== undefined
|
|
222
|
-
? normalizeWebsocketBaseUrl(input.relayBaseUrl, "relayBaseUrl")
|
|
223
|
-
: snapshot.config.relayBaseUrl;
|
|
224
233
|
const controlBaseUrl = input.controlBaseUrl !== undefined
|
|
225
234
|
? normalizeHttpBaseUrl(input.controlBaseUrl, "controlBaseUrl")
|
|
226
235
|
: snapshot.config.controlBaseUrl;
|
|
236
|
+
const relayBaseUrl = deriveRelayBaseUrlFromControlBaseUrl(controlBaseUrl);
|
|
227
237
|
if (!relayBaseUrl || !controlBaseUrl) {
|
|
228
238
|
throw new AppError({
|
|
229
239
|
statusCode: 400,
|
|
230
240
|
errorCode: "INVALID_INPUT",
|
|
231
|
-
detail: "绑定前必须提供
|
|
241
|
+
detail: "绑定前必须提供 controlBaseUrl"
|
|
232
242
|
});
|
|
233
243
|
}
|
|
234
244
|
const timestamp = nowIso();
|
|
@@ -293,7 +303,7 @@ export class RelayTunnelService {
|
|
|
293
303
|
throw new AppError({
|
|
294
304
|
statusCode: 409,
|
|
295
305
|
errorCode: "RELAY_TUNNEL_NOT_BOUND",
|
|
296
|
-
detail: "
|
|
306
|
+
detail: "当前实例还没有绑定 CodingNS Connect"
|
|
297
307
|
});
|
|
298
308
|
}
|
|
299
309
|
const timestamp = nowIso();
|
|
@@ -351,7 +361,7 @@ export class RelayTunnelService {
|
|
|
351
361
|
});
|
|
352
362
|
}
|
|
353
363
|
readStateSnapshot() {
|
|
354
|
-
const persistedConfig = this.
|
|
364
|
+
const persistedConfig = this.reconcilePersistedConfig(this.repository.findConfig());
|
|
355
365
|
return {
|
|
356
366
|
config: persistedConfig
|
|
357
367
|
?? {
|
|
@@ -359,7 +369,7 @@ export class RelayTunnelService {
|
|
|
359
369
|
enabled: false,
|
|
360
370
|
provider: "codingns_relay",
|
|
361
371
|
relayBaseUrl: null,
|
|
362
|
-
controlBaseUrl:
|
|
372
|
+
controlBaseUrl: DEFAULT_RELAY_TUNNEL_CONTROL_BASE_URL,
|
|
363
373
|
controlAccessTokenCiphertext: null,
|
|
364
374
|
controlAccountEmail: null,
|
|
365
375
|
controlSessionExpiresAt: null,
|
|
@@ -375,26 +385,58 @@ export class RelayTunnelService {
|
|
|
375
385
|
hasPersistedConfig: persistedConfig !== null
|
|
376
386
|
};
|
|
377
387
|
}
|
|
378
|
-
|
|
388
|
+
reconcilePersistedConfig(config) {
|
|
379
389
|
if (!config) {
|
|
380
390
|
return config;
|
|
381
391
|
}
|
|
382
|
-
|
|
383
|
-
|
|
392
|
+
let nextConfig = config;
|
|
393
|
+
let changed = false;
|
|
394
|
+
if ((nextConfig.localTargetBaseUrlSource ?? "default") === "default"
|
|
395
|
+
&& nextConfig.localTargetBaseUrl !== this.defaultLocalTargetBaseUrl) {
|
|
396
|
+
// `default` 源的目标地址由当前运行模式决定,不应该把历史默认值永久粘在库里。
|
|
397
|
+
// 只要默认入口变化了,就在启动时自动收敛到新的默认值;用户显式写入的 custom 配置不动。
|
|
398
|
+
nextConfig = {
|
|
399
|
+
...nextConfig,
|
|
400
|
+
localTargetBaseUrl: this.defaultLocalTargetBaseUrl,
|
|
401
|
+
localTargetBaseUrlSource: "default",
|
|
402
|
+
updatedAt: nowIso()
|
|
403
|
+
};
|
|
404
|
+
changed = true;
|
|
405
|
+
}
|
|
406
|
+
if (!normalizeOptionalText(nextConfig.controlBaseUrl)) {
|
|
407
|
+
// 正式包已经把官方控制站当成固定入口,Host 侧不能继续允许空值漂着,
|
|
408
|
+
// 否则前端显示的是固定地址,真正发请求时却因为配置为空直接失败。
|
|
409
|
+
nextConfig = {
|
|
410
|
+
...nextConfig,
|
|
411
|
+
controlBaseUrl: DEFAULT_RELAY_TUNNEL_CONTROL_BASE_URL,
|
|
412
|
+
updatedAt: nowIso()
|
|
413
|
+
};
|
|
414
|
+
changed = true;
|
|
415
|
+
}
|
|
416
|
+
if (nextConfig.controlBaseUrl === LEGACY_RELAY_TUNNEL_CONTROL_BASE_URL) {
|
|
417
|
+
// 历史正式版把官方控制站写成了旧端口。正式包又不允许用户手改控制站地址,
|
|
418
|
+
// 所以这里必须在读取配置时自动迁移,否则老用户会永远卡在登录阶段。
|
|
419
|
+
nextConfig = {
|
|
420
|
+
...nextConfig,
|
|
421
|
+
controlBaseUrl: DEFAULT_RELAY_TUNNEL_CONTROL_BASE_URL,
|
|
422
|
+
updatedAt: nowIso()
|
|
423
|
+
};
|
|
424
|
+
changed = true;
|
|
425
|
+
}
|
|
426
|
+
const managedRelayBaseUrl = deriveRelayBaseUrlFromControlBaseUrl(nextConfig.controlBaseUrl);
|
|
427
|
+
if (nextConfig.relayBaseUrl !== managedRelayBaseUrl) {
|
|
428
|
+
nextConfig = {
|
|
429
|
+
...nextConfig,
|
|
430
|
+
relayBaseUrl: managedRelayBaseUrl,
|
|
431
|
+
updatedAt: nowIso()
|
|
432
|
+
};
|
|
433
|
+
changed = true;
|
|
384
434
|
}
|
|
385
|
-
if (
|
|
435
|
+
if (!changed) {
|
|
386
436
|
return config;
|
|
387
437
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
const migratedConfig = {
|
|
391
|
-
...config,
|
|
392
|
-
localTargetBaseUrl: this.defaultLocalTargetBaseUrl,
|
|
393
|
-
localTargetBaseUrlSource: "default",
|
|
394
|
-
updatedAt: nowIso()
|
|
395
|
-
};
|
|
396
|
-
this.repository.upsertConfig(migratedConfig);
|
|
397
|
-
return migratedConfig;
|
|
438
|
+
this.repository.upsertConfig(nextConfig);
|
|
439
|
+
return nextConfig;
|
|
398
440
|
}
|
|
399
441
|
resolveEffectiveStatus(config) {
|
|
400
442
|
const persisted = this.repository.findStatus();
|
|
@@ -469,6 +511,25 @@ export class RelayTunnelService {
|
|
|
469
511
|
const effectiveConfig = this.resolveConfigWithIdentity(latestSnapshot.config);
|
|
470
512
|
return this.buildStatusDto(latestSnapshot, effectiveConfig, this.resolveEffectiveStatus(effectiveConfig));
|
|
471
513
|
}
|
|
514
|
+
if (shouldResetStaleRelayBinding(error)) {
|
|
515
|
+
const nextConfig = this.clearBoundState(snapshot.config, {
|
|
516
|
+
updatedAt: nowIso()
|
|
517
|
+
});
|
|
518
|
+
const nextStatus = {
|
|
519
|
+
...buildSkeletonStatus("unbound", nextConfig, {
|
|
520
|
+
observedAt: nowIso()
|
|
521
|
+
}),
|
|
522
|
+
lastError: error instanceof Error ? error.message : String(error)
|
|
523
|
+
};
|
|
524
|
+
this.db.transaction(() => {
|
|
525
|
+
this.repository.upsertConfig(nextConfig);
|
|
526
|
+
this.repository.upsertStatus(nextStatus);
|
|
527
|
+
})();
|
|
528
|
+
return this.buildStatusDto({
|
|
529
|
+
config: nextConfig,
|
|
530
|
+
hasPersistedConfig: true
|
|
531
|
+
}, nextConfig, nextStatus);
|
|
532
|
+
}
|
|
472
533
|
const failedStatus = {
|
|
473
534
|
...buildSkeletonStatus("error", snapshot.config, {
|
|
474
535
|
observedAt: nowIso()
|
|
@@ -494,6 +555,7 @@ export class RelayTunnelService {
|
|
|
494
555
|
hostPublicKey: effectiveConfig.hostPublicKey,
|
|
495
556
|
hostKeyFingerprint: effectiveConfig.hostKeyFingerprint,
|
|
496
557
|
localTargetBaseUrl: effectiveConfig.localTargetBaseUrl,
|
|
558
|
+
candidateEndpoints: buildHostCandidateEndpoints(effectiveConfig),
|
|
497
559
|
phase: effectiveStatus.phase,
|
|
498
560
|
connected: effectiveStatus.connected,
|
|
499
561
|
hostFingerprint: effectiveStatus.hostFingerprint,
|
|
@@ -530,6 +592,14 @@ export class RelayTunnelService {
|
|
|
530
592
|
this.repository.upsertConfig(nextConfig);
|
|
531
593
|
return nextConfig;
|
|
532
594
|
}
|
|
595
|
+
clearBoundState(config, options) {
|
|
596
|
+
return {
|
|
597
|
+
...config,
|
|
598
|
+
bindingId: null,
|
|
599
|
+
tunnelDomain: null,
|
|
600
|
+
updatedAt: options.updatedAt
|
|
601
|
+
};
|
|
602
|
+
}
|
|
533
603
|
isBootstrapInitialized() {
|
|
534
604
|
return this.bootstrapStateRepository.getState().initialized;
|
|
535
605
|
}
|
|
@@ -565,13 +635,26 @@ export class RelayTunnelService {
|
|
|
565
635
|
}
|
|
566
636
|
}
|
|
567
637
|
async requestControlApi(input) {
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
638
|
+
let response;
|
|
639
|
+
const controller = new AbortController();
|
|
640
|
+
const timeoutId = setTimeout(() => controller.abort(), this.controlRequestTimeoutMs);
|
|
641
|
+
try {
|
|
642
|
+
const requestUrl = new URL(input.path, ensureTrailingSlash(input.controlBaseUrl)).toString();
|
|
643
|
+
response = await this.fetchFn(requestUrl, {
|
|
644
|
+
method: input.method,
|
|
645
|
+
headers: input.headers,
|
|
646
|
+
body: input.body,
|
|
647
|
+
signal: controller.signal
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
catch (error) {
|
|
651
|
+
throw buildControlFetchError(error, input.controlBaseUrl, input.failurePrefix);
|
|
652
|
+
}
|
|
653
|
+
finally {
|
|
654
|
+
clearTimeout(timeoutId);
|
|
655
|
+
}
|
|
573
656
|
if (!response.ok) {
|
|
574
|
-
throw await buildControlApiError(response, input.failurePrefix);
|
|
657
|
+
throw await buildControlApiError(response, input.controlBaseUrl, input.failurePrefix);
|
|
575
658
|
}
|
|
576
659
|
return await response.json();
|
|
577
660
|
}
|
|
@@ -597,6 +680,133 @@ function buildSkeletonStatus(phase, config, overrides) {
|
|
|
597
680
|
observedAt: overrides?.observedAt ?? null
|
|
598
681
|
};
|
|
599
682
|
}
|
|
683
|
+
function buildHostCandidateEndpoints(config) {
|
|
684
|
+
const endpoints = new Map();
|
|
685
|
+
const relayEndpoint = buildRelayPublicUrl(config);
|
|
686
|
+
if (relayEndpoint) {
|
|
687
|
+
endpoints.set(relayEndpoint, {
|
|
688
|
+
endpointId: `relay:${relayEndpoint}`,
|
|
689
|
+
kind: "relay",
|
|
690
|
+
url: relayEndpoint,
|
|
691
|
+
priority: 400,
|
|
692
|
+
expiresAt: null,
|
|
693
|
+
source: "host_reported"
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
for (const localCandidateUrl of buildLocalCandidateUrls(config.localTargetBaseUrl)) {
|
|
697
|
+
endpoints.set(localCandidateUrl, {
|
|
698
|
+
endpointId: `host_reported:${localCandidateUrl}`,
|
|
699
|
+
kind: classifyCandidateEndpointKind(localCandidateUrl),
|
|
700
|
+
url: localCandidateUrl,
|
|
701
|
+
priority: resolveCandidateEndpointPriority(localCandidateUrl),
|
|
702
|
+
expiresAt: null,
|
|
703
|
+
source: "host_reported"
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
return Array.from(endpoints.values()).sort((left, right) => {
|
|
707
|
+
if (left.priority !== right.priority) {
|
|
708
|
+
return left.priority - right.priority;
|
|
709
|
+
}
|
|
710
|
+
return left.url.localeCompare(right.url);
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
function buildRelayPublicUrl(config) {
|
|
714
|
+
if (!config.tunnelDomain || !config.controlBaseUrl) {
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
try {
|
|
718
|
+
const controlUrl = new URL(config.controlBaseUrl);
|
|
719
|
+
controlUrl.hostname = config.tunnelDomain.trim().toLowerCase();
|
|
720
|
+
controlUrl.pathname = "/";
|
|
721
|
+
controlUrl.search = "";
|
|
722
|
+
controlUrl.hash = "";
|
|
723
|
+
return controlUrl.toString().replace(/\/$/, "");
|
|
724
|
+
}
|
|
725
|
+
catch {
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
function buildLocalCandidateUrls(localTargetBaseUrl) {
|
|
730
|
+
let parsed;
|
|
731
|
+
try {
|
|
732
|
+
parsed = new URL(localTargetBaseUrl);
|
|
733
|
+
}
|
|
734
|
+
catch {
|
|
735
|
+
return [];
|
|
736
|
+
}
|
|
737
|
+
const candidates = new Set();
|
|
738
|
+
const hostname = parsed.hostname.trim().toLowerCase();
|
|
739
|
+
candidates.add(normalizeUrlWithoutTrailingSlash(parsed.toString()));
|
|
740
|
+
if (hostname === "0.0.0.0" || hostname === "::" || hostname === "::0") {
|
|
741
|
+
for (const networkAddress of listPrivateIpv4Addresses()) {
|
|
742
|
+
const candidateUrl = new URL(parsed.toString());
|
|
743
|
+
candidateUrl.hostname = networkAddress;
|
|
744
|
+
candidates.add(normalizeUrlWithoutTrailingSlash(candidateUrl.toString()));
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
if (hostname === "127.0.0.1" || hostname === "localhost" || hostname === "::1") {
|
|
748
|
+
for (const networkAddress of listPrivateIpv4Addresses()) {
|
|
749
|
+
const candidateUrl = new URL(parsed.toString());
|
|
750
|
+
candidateUrl.hostname = networkAddress;
|
|
751
|
+
candidates.add(normalizeUrlWithoutTrailingSlash(candidateUrl.toString()));
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return Array.from(candidates);
|
|
755
|
+
}
|
|
756
|
+
function listPrivateIpv4Addresses() {
|
|
757
|
+
const interfaces = os.networkInterfaces();
|
|
758
|
+
const candidates = new Set();
|
|
759
|
+
for (const entries of Object.values(interfaces)) {
|
|
760
|
+
for (const entry of entries ?? []) {
|
|
761
|
+
if (!entry || entry.family !== "IPv4" || entry.internal) {
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
if (!isPrivateIpv4Address(entry.address)) {
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
candidates.add(entry.address);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
return Array.from(candidates).sort();
|
|
771
|
+
}
|
|
772
|
+
function isPrivateIpv4Address(address) {
|
|
773
|
+
return (/^10\./.test(address)
|
|
774
|
+
|| /^192\.168\./.test(address)
|
|
775
|
+
|| /^172\.(1[6-9]|2\d|3[0-1])\./.test(address));
|
|
776
|
+
}
|
|
777
|
+
function classifyCandidateEndpointKind(candidateUrl) {
|
|
778
|
+
try {
|
|
779
|
+
const hostname = new URL(candidateUrl).hostname.toLowerCase();
|
|
780
|
+
if (hostname === "127.0.0.1" || hostname === "localhost" || hostname === "::1") {
|
|
781
|
+
return "loopback";
|
|
782
|
+
}
|
|
783
|
+
if (isPrivateIpv4Address(hostname)) {
|
|
784
|
+
return "lan";
|
|
785
|
+
}
|
|
786
|
+
return "custom";
|
|
787
|
+
}
|
|
788
|
+
catch {
|
|
789
|
+
return "custom";
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
function resolveCandidateEndpointPriority(candidateUrl) {
|
|
793
|
+
const kind = classifyCandidateEndpointKind(candidateUrl);
|
|
794
|
+
switch (kind) {
|
|
795
|
+
case "loopback":
|
|
796
|
+
return 100;
|
|
797
|
+
case "lan":
|
|
798
|
+
return 200;
|
|
799
|
+
case "tailscale":
|
|
800
|
+
return 300;
|
|
801
|
+
case "relay":
|
|
802
|
+
return 400;
|
|
803
|
+
default:
|
|
804
|
+
return 500;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
function normalizeUrlWithoutTrailingSlash(value) {
|
|
808
|
+
return value.endsWith("/") ? value.slice(0, -1) : value;
|
|
809
|
+
}
|
|
600
810
|
function isBound(config) {
|
|
601
811
|
return Boolean(config.bindingId && config.tunnelDomain);
|
|
602
812
|
}
|
|
@@ -714,6 +924,14 @@ function normalizeWebsocketBaseUrl(value, field) {
|
|
|
714
924
|
: parsed.protocol;
|
|
715
925
|
return `${normalizedProtocol}//${parsed.host}${pathname}`;
|
|
716
926
|
}
|
|
927
|
+
function deriveRelayBaseUrlFromControlBaseUrl(controlBaseUrl) {
|
|
928
|
+
const normalizedControlBaseUrl = normalizeHttpBaseUrl(controlBaseUrl, "controlBaseUrl");
|
|
929
|
+
if (!normalizedControlBaseUrl) {
|
|
930
|
+
return null;
|
|
931
|
+
}
|
|
932
|
+
const relayUrl = new URL("relay", ensureTrailingSlash(normalizedControlBaseUrl));
|
|
933
|
+
return normalizeWebsocketBaseUrl(relayUrl.toString(), "relayBaseUrl");
|
|
934
|
+
}
|
|
717
935
|
function requireConfiguredControlBaseUrl(config) {
|
|
718
936
|
const controlBaseUrl = normalizeOptionalText(config.controlBaseUrl);
|
|
719
937
|
if (!controlBaseUrl) {
|
|
@@ -738,12 +956,49 @@ function clearRelayTunnelControlSession(config, options) {
|
|
|
738
956
|
updatedAt: options.updatedAt
|
|
739
957
|
};
|
|
740
958
|
}
|
|
741
|
-
|
|
959
|
+
function shouldResetStaleRelayBinding(error) {
|
|
960
|
+
if (!(error instanceof RelayTunnelRuntimeHttpError)) {
|
|
961
|
+
return false;
|
|
962
|
+
}
|
|
963
|
+
return (error.errorCode === "TUNNEL_NOT_FOUND"
|
|
964
|
+
|| error.errorCode === "BINDING_NOT_FOUND"
|
|
965
|
+
|| error.errorCode === "HOST_AUTH_INVALID"
|
|
966
|
+
|| error.errorCode === "HOST_BINDING_MISMATCH");
|
|
967
|
+
}
|
|
968
|
+
async function buildControlApiError(response, controlBaseUrl, failurePrefix) {
|
|
742
969
|
const detail = await readControlApiErrorDetail(response);
|
|
970
|
+
if (response.status === 401 || response.status === 403) {
|
|
971
|
+
return new AppError({
|
|
972
|
+
statusCode: response.status,
|
|
973
|
+
errorCode: "RELAY_TUNNEL_CONTROL_ACCESS_DENIED",
|
|
974
|
+
detail: `${failurePrefix}:控制站 ${controlBaseUrl} 拒绝了这次请求(HTTP ${response.status})。`
|
|
975
|
+
+ ` 请确认这是正确的控制站地址,并检查账号、密码或访问权限。`
|
|
976
|
+
+ appendControlApiDetail(detail)
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
if (response.status === 404) {
|
|
980
|
+
return new AppError({
|
|
981
|
+
statusCode: 404,
|
|
982
|
+
errorCode: "RELAY_TUNNEL_CONTROL_ENDPOINT_NOT_FOUND",
|
|
983
|
+
detail: `${failurePrefix}:控制站 ${controlBaseUrl} 上没有这个接口(HTTP 404)。`
|
|
984
|
+
+ " 这通常说明地址写错了,或者目标服务不是 CodingNS 控制站。"
|
|
985
|
+
+ appendControlApiDetail(detail)
|
|
986
|
+
});
|
|
987
|
+
}
|
|
743
988
|
return new AppError({
|
|
744
989
|
statusCode: response.status,
|
|
745
990
|
errorCode: "RELAY_TUNNEL_CONTROL_API_ERROR",
|
|
746
|
-
detail: `${failurePrefix}
|
|
991
|
+
detail: `${failurePrefix}:控制站 ${controlBaseUrl} 返回了异常响应(HTTP ${response.status})。`
|
|
992
|
+
+ appendControlApiDetail(detail)
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
function buildControlFetchError(error, controlBaseUrl, failurePrefix) {
|
|
996
|
+
return new AppError({
|
|
997
|
+
statusCode: 502,
|
|
998
|
+
errorCode: "RELAY_TUNNEL_CONTROL_UNREACHABLE",
|
|
999
|
+
detail: `${failurePrefix}:无法连接到控制站 ${controlBaseUrl}。`
|
|
1000
|
+
+ " 请确认服务地址、端口和网络连接是否正确。"
|
|
1001
|
+
+ appendControlApiDetail(resolveFetchErrorDetail(error))
|
|
747
1002
|
});
|
|
748
1003
|
}
|
|
749
1004
|
async function readControlApiErrorDetail(response) {
|
|
@@ -768,4 +1023,166 @@ async function readControlApiErrorDetail(response) {
|
|
|
768
1023
|
function readJsonErrorText(value) {
|
|
769
1024
|
return typeof value === "string" ? normalizeOptionalText(value) : null;
|
|
770
1025
|
}
|
|
1026
|
+
function resolveFetchErrorDetail(error) {
|
|
1027
|
+
if (error instanceof Error) {
|
|
1028
|
+
const code = readFetchErrorCode(error);
|
|
1029
|
+
if (error.name === "AbortError") {
|
|
1030
|
+
return "请求超时。";
|
|
1031
|
+
}
|
|
1032
|
+
if (code === "ECONNREFUSED") {
|
|
1033
|
+
return "连接被目标服务器拒绝。";
|
|
1034
|
+
}
|
|
1035
|
+
if (code === "ENOTFOUND" || code === "EAI_AGAIN") {
|
|
1036
|
+
return "域名无法解析。";
|
|
1037
|
+
}
|
|
1038
|
+
if (code === "ETIMEDOUT" || code === "UND_ERR_CONNECT_TIMEOUT") {
|
|
1039
|
+
return "连接超时。";
|
|
1040
|
+
}
|
|
1041
|
+
if (code === "CERT_HAS_EXPIRED" || code === "DEPTH_ZERO_SELF_SIGNED_CERT") {
|
|
1042
|
+
return "TLS 证书无效。";
|
|
1043
|
+
}
|
|
1044
|
+
return normalizeOptionalText(error.message);
|
|
1045
|
+
}
|
|
1046
|
+
return null;
|
|
1047
|
+
}
|
|
1048
|
+
function readFetchErrorCode(error) {
|
|
1049
|
+
const cause = "cause" in error
|
|
1050
|
+
? error.cause
|
|
1051
|
+
: undefined;
|
|
1052
|
+
return typeof cause?.code === "string" ? cause.code : null;
|
|
1053
|
+
}
|
|
1054
|
+
function normalizePositiveInt(value, fallback) {
|
|
1055
|
+
if (value === null || value === undefined || !Number.isInteger(value) || value <= 0) {
|
|
1056
|
+
return fallback;
|
|
1057
|
+
}
|
|
1058
|
+
return value;
|
|
1059
|
+
}
|
|
1060
|
+
function resolveRelayTunnelControlFetch(fetchFn) {
|
|
1061
|
+
if (fetchFn) {
|
|
1062
|
+
return fetchFn;
|
|
1063
|
+
}
|
|
1064
|
+
if (isBuiltinNodeFetch(globalThis.fetch)) {
|
|
1065
|
+
return createRelayTunnelControlFetch();
|
|
1066
|
+
}
|
|
1067
|
+
return globalThis.fetch;
|
|
1068
|
+
}
|
|
1069
|
+
function isBuiltinNodeFetch(fetchFn) {
|
|
1070
|
+
const source = Function.prototype.toString.call(fetchFn);
|
|
1071
|
+
return source.includes("internal/deps/undici/undici");
|
|
1072
|
+
}
|
|
1073
|
+
function createRelayTunnelControlFetch() {
|
|
1074
|
+
return async (input, init) => {
|
|
1075
|
+
const requestUrl = resolveFetchInputUrl(input);
|
|
1076
|
+
const parsedUrl = new URL(requestUrl);
|
|
1077
|
+
if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
|
|
1078
|
+
throw new TypeError(`Unsupported protocol for relay tunnel control fetch: ${parsedUrl.protocol}`);
|
|
1079
|
+
}
|
|
1080
|
+
return await new Promise((resolve, reject) => {
|
|
1081
|
+
const transport = parsedUrl.protocol === "https:" ? https : http;
|
|
1082
|
+
const headers = normalizeFetchHeaders(init?.headers);
|
|
1083
|
+
const body = normalizeFetchBody(init?.body);
|
|
1084
|
+
const abortHandler = () => request.destroy(createAbortRequestError());
|
|
1085
|
+
const request = transport.request(parsedUrl, {
|
|
1086
|
+
method: init?.method ?? "GET",
|
|
1087
|
+
headers,
|
|
1088
|
+
agent: parsedUrl.protocol === "https:" ? RELAY_TUNNEL_CONTROL_HTTPS_AGENT : undefined
|
|
1089
|
+
}, (response) => {
|
|
1090
|
+
const chunks = [];
|
|
1091
|
+
response.on("data", (chunk) => {
|
|
1092
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
1093
|
+
});
|
|
1094
|
+
response.on("end", () => {
|
|
1095
|
+
cleanup();
|
|
1096
|
+
resolve(new Response(Buffer.concat(chunks), {
|
|
1097
|
+
status: response.statusCode ?? 500,
|
|
1098
|
+
headers: buildFetchResponseHeaders(response.headers)
|
|
1099
|
+
}));
|
|
1100
|
+
});
|
|
1101
|
+
response.on("error", (error) => {
|
|
1102
|
+
cleanup();
|
|
1103
|
+
reject(error);
|
|
1104
|
+
});
|
|
1105
|
+
});
|
|
1106
|
+
function cleanup() {
|
|
1107
|
+
init?.signal?.removeEventListener("abort", abortHandler);
|
|
1108
|
+
}
|
|
1109
|
+
request.on("error", (error) => {
|
|
1110
|
+
cleanup();
|
|
1111
|
+
reject(error);
|
|
1112
|
+
});
|
|
1113
|
+
if (init?.signal) {
|
|
1114
|
+
if (init.signal.aborted) {
|
|
1115
|
+
cleanup();
|
|
1116
|
+
request.destroy(createAbortRequestError());
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
init.signal.addEventListener("abort", abortHandler, { once: true });
|
|
1120
|
+
}
|
|
1121
|
+
if (body !== null) {
|
|
1122
|
+
request.write(body);
|
|
1123
|
+
}
|
|
1124
|
+
request.end();
|
|
1125
|
+
});
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
function resolveFetchInputUrl(input) {
|
|
1129
|
+
if (typeof input === "string") {
|
|
1130
|
+
return input;
|
|
1131
|
+
}
|
|
1132
|
+
if (input instanceof URL) {
|
|
1133
|
+
return input.toString();
|
|
1134
|
+
}
|
|
1135
|
+
return input.url;
|
|
1136
|
+
}
|
|
1137
|
+
function normalizeFetchHeaders(headers) {
|
|
1138
|
+
if (!headers) {
|
|
1139
|
+
return {};
|
|
1140
|
+
}
|
|
1141
|
+
return Object.fromEntries(new Headers(headers).entries());
|
|
1142
|
+
}
|
|
1143
|
+
function normalizeFetchBody(body) {
|
|
1144
|
+
if (body === null || body === undefined) {
|
|
1145
|
+
return null;
|
|
1146
|
+
}
|
|
1147
|
+
if (typeof body === "string" || Buffer.isBuffer(body)) {
|
|
1148
|
+
return body;
|
|
1149
|
+
}
|
|
1150
|
+
if (body instanceof URLSearchParams) {
|
|
1151
|
+
return body.toString();
|
|
1152
|
+
}
|
|
1153
|
+
if (body instanceof ArrayBuffer) {
|
|
1154
|
+
return Buffer.from(body);
|
|
1155
|
+
}
|
|
1156
|
+
if (ArrayBuffer.isView(body)) {
|
|
1157
|
+
return Buffer.from(body.buffer, body.byteOffset, body.byteLength);
|
|
1158
|
+
}
|
|
1159
|
+
throw new TypeError("Unsupported relay tunnel control request body type");
|
|
1160
|
+
}
|
|
1161
|
+
function buildFetchResponseHeaders(headers) {
|
|
1162
|
+
const responseHeaders = new Headers();
|
|
1163
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
1164
|
+
if (Array.isArray(value)) {
|
|
1165
|
+
for (const item of value) {
|
|
1166
|
+
responseHeaders.append(name, item);
|
|
1167
|
+
}
|
|
1168
|
+
continue;
|
|
1169
|
+
}
|
|
1170
|
+
if (typeof value === "string") {
|
|
1171
|
+
responseHeaders.set(name, value);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
return responseHeaders;
|
|
1175
|
+
}
|
|
1176
|
+
function createAbortRequestError() {
|
|
1177
|
+
const error = new Error("This operation was aborted");
|
|
1178
|
+
error.name = "AbortError";
|
|
1179
|
+
return error;
|
|
1180
|
+
}
|
|
1181
|
+
function appendControlApiDetail(detail) {
|
|
1182
|
+
const normalized = normalizeOptionalText(detail);
|
|
1183
|
+
if (!normalized) {
|
|
1184
|
+
return "";
|
|
1185
|
+
}
|
|
1186
|
+
return ` 详情:${normalized}`;
|
|
1187
|
+
}
|
|
771
1188
|
//# sourceMappingURL=relay-tunnel-service.js.map
|