@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.
Files changed (110) hide show
  1. package/dist/public/assets/AdaptiveButlerPage-CUyNL98E.js +3 -0
  2. package/dist/public/assets/App-BFP7LCSC.js +30 -0
  3. package/dist/public/assets/{BootstrapPage-Vu5oEJ8z.js → BootstrapPage-G74dX2Us.js} +1 -1
  4. package/dist/public/assets/ConversationPage-Bz0_tvvM.js +2 -0
  5. package/dist/public/assets/{DesktopDetachPreviewPage-BgeEqbc5.js → DesktopDetachPreviewPage-IV7oEdOX.js} +1 -1
  6. package/dist/public/assets/DesktopWindowPage-BBmHyRg5.js +2 -0
  7. package/dist/public/assets/FileContextPanel--FVTxDrq.js +1 -0
  8. package/dist/public/assets/GitSidebar-DAiSi9oc.js +6 -0
  9. package/dist/public/assets/{MobileCreateSessionSheet-DLq5qPkx.js → MobileCreateSessionSheet-DqVwz_Hp.js} +1 -1
  10. package/dist/public/assets/MobileSheet-D1lMrcvD.js +1 -0
  11. package/dist/public/assets/{MobileTopHeaderFrame-DArgZI7L.js → MobileTopHeaderFrame-COTc7cRr.js} +1 -1
  12. package/dist/public/assets/{MobileWorkspaceSwitcherHeader-0ywJKfBQ.js → MobileWorkspaceSwitcherHeader-DJPV9ym2.js} +1 -1
  13. package/dist/public/assets/RelayConnectEntryPage-dSwU8VzK.js +1 -0
  14. package/dist/public/assets/{ServerSettingsModal-izoYMx9U.js → ServerSettingsModal-B34ms3ze.js} +1 -1
  15. package/dist/public/assets/{SessionIndexPage-C5aG8FIv.js → SessionIndexPage-D3tG1gmM.js} +1 -1
  16. package/dist/public/assets/SettingsPage-B3-6-5GL.js +1 -0
  17. package/dist/public/assets/TerminalManagerPanel-DhuTEdzV.js +1 -0
  18. package/dist/public/assets/{TerminalPage-CtKXIU0h.js → TerminalPage-DpsvQQVR.js} +19 -19
  19. package/dist/public/assets/{TerminalRuntimeFallbackModal-CRhOQOsT.js → TerminalRuntimeFallbackModal-CNzOt5v5.js} +1 -1
  20. package/dist/public/assets/{ToolFilesPage-DcYPsS-e.js → ToolFilesPage-BX9QDi9Y.js} +1 -1
  21. package/dist/public/assets/{ToolGitPage-CsPl89ty.js → ToolGitPage-4VtFox3p.js} +1 -1
  22. package/dist/public/assets/{ToolProcessesPage-D0dvR8xK.js → ToolProcessesPage-DZJC6Qnt.js} +1 -1
  23. package/dist/public/assets/{ToolsHomePage-4fP-KRiv.js → ToolsHomePage-D7JbrAWv.js} +1 -1
  24. package/dist/public/assets/{WorkbenchLandingPage-kvlfyxRo.js → WorkbenchLandingPage-C0yqnzqh.js} +1 -1
  25. package/dist/public/assets/WorkbenchLayout-Brlj8K3i.js +3 -0
  26. package/dist/public/assets/{WorkbenchModal-Ctob14VR.js → WorkbenchModal-CbDxaCOR.js} +1 -1
  27. package/dist/public/assets/WorkbenchShellRoute-BMcnFadA.css +1 -0
  28. package/dist/public/assets/WorkbenchShellRoute-puGpdDFY.js +1 -0
  29. package/dist/public/assets/{WorkspaceDebugDetailPage-Com5kEXJ.js → WorkspaceDebugDetailPage-fTGweC9N.js} +1 -1
  30. package/dist/public/assets/WorkspaceDetailPage-BtaIzSDB.js +1 -0
  31. package/dist/public/assets/WorkspaceHomePage-CUmmYDrM.js +1 -0
  32. package/dist/public/assets/client-runtime-manager-RHFa_iWo.js +1 -0
  33. package/dist/public/assets/{default-session-permission-mode-CcGwR4Kk.js → default-session-permission-mode-Cu5SreTG.js} +1 -1
  34. package/dist/public/assets/file-tree-icon-BMKuc5pw.js +31 -0
  35. package/dist/public/assets/index-Cq3ue0za.css +1 -0
  36. package/dist/public/assets/index-DEbFT-Aq.js +42 -0
  37. package/dist/public/assets/{preferences-service-KIYeE2gk.js → preferences-service-gv_9vGKz.js} +1 -1
  38. package/dist/public/assets/session-runtime-machine-Bfnxkk9B.js +17 -0
  39. package/dist/public/assets/{terminal-runtime-meta-AWXJpN4r.js → terminal-runtime-meta-B9xJGY__.js} +1 -1
  40. package/dist/public/assets/{useRegisteredDebugTemplates-DBDRdptr.js → useRegisteredDebugTemplates-CDfl54Wt.js} +1 -1
  41. package/dist/public/index.html +2 -2
  42. package/dist/server/config/env.d.ts +1 -0
  43. package/dist/server/config/env.js +36 -0
  44. package/dist/server/config/env.js.map +1 -1
  45. package/dist/server/modules/client/client-controller.js +1 -1
  46. package/dist/server/modules/client/client-controller.js.map +1 -1
  47. package/dist/server/modules/client/client-service.d.ts +16 -2
  48. package/dist/server/modules/client/client-service.js +21 -3
  49. package/dist/server/modules/client/client-service.js.map +1 -1
  50. package/dist/server/modules/relay-tunnel/crypto/relay-tunnel-packets.d.ts +16 -1
  51. package/dist/server/modules/relay-tunnel/crypto/relay-tunnel-packets.js.map +1 -1
  52. package/dist/server/modules/relay-tunnel/crypto/relay-tunnel-protocol.d.ts +2 -1
  53. package/dist/server/modules/relay-tunnel/crypto/relay-tunnel-protocol.js +18 -0
  54. package/dist/server/modules/relay-tunnel/crypto/relay-tunnel-protocol.js.map +1 -1
  55. package/dist/server/modules/relay-tunnel/relay-tunnel-client-context.d.ts +13 -0
  56. package/dist/server/modules/relay-tunnel/relay-tunnel-client-context.js +2 -0
  57. package/dist/server/modules/relay-tunnel/relay-tunnel-client-context.js.map +1 -0
  58. package/dist/server/modules/relay-tunnel/relay-tunnel-gateway-service.d.ts +6 -0
  59. package/dist/server/modules/relay-tunnel/relay-tunnel-gateway-service.js +110 -10
  60. package/dist/server/modules/relay-tunnel/relay-tunnel-gateway-service.js.map +1 -1
  61. package/dist/server/modules/relay-tunnel/relay-tunnel-runtime-adapter.d.ts +16 -4
  62. package/dist/server/modules/relay-tunnel/relay-tunnel-runtime-adapter.js +216 -101
  63. package/dist/server/modules/relay-tunnel/relay-tunnel-runtime-adapter.js.map +1 -1
  64. package/dist/server/modules/relay-tunnel/relay-tunnel-service.d.ts +6 -2
  65. package/dist/server/modules/relay-tunnel/relay-tunnel-service.js +457 -40
  66. package/dist/server/modules/relay-tunnel/relay-tunnel-service.js.map +1 -1
  67. package/dist/server/modules/sessions/session-activity-inspector.js +6 -8
  68. package/dist/server/modules/sessions/session-activity-inspector.js.map +1 -1
  69. package/dist/server/modules/sessions/session-history-service.d.ts +1 -0
  70. package/dist/server/modules/sessions/session-history-service.js +62 -7
  71. package/dist/server/modules/sessions/session-history-service.js.map +1 -1
  72. package/dist/server/modules/workbench/snapshot-revision.d.ts +4 -0
  73. package/dist/server/modules/workbench/snapshot-revision.js +13 -0
  74. package/dist/server/modules/workbench/snapshot-revision.js.map +1 -0
  75. package/dist/server/modules/workbench/workbench-service.d.ts +33 -2
  76. package/dist/server/modules/workbench/workbench-service.js +39 -4
  77. package/dist/server/modules/workbench/workbench-service.js.map +1 -1
  78. package/dist/server/modules/workbench/workspace-panel-snapshot-service.d.ts +6 -1
  79. package/dist/server/modules/workbench/workspace-panel-snapshot-service.js +10 -8
  80. package/dist/server/modules/workbench/workspace-panel-snapshot-service.js.map +1 -1
  81. package/dist/server/server/create-server.js +11 -6
  82. package/dist/server/server/create-server.js.map +1 -1
  83. package/dist/server/types/domain.d.ts +9 -0
  84. package/dist/server/ws/workbench-ws-hub.js +295 -43
  85. package/dist/server/ws/workbench-ws-hub.js.map +1 -1
  86. package/dist/server/ws/ws-server.js +141 -8
  87. package/dist/server/ws/ws-server.js.map +1 -1
  88. package/node_modules/@codingns/session-sync-core/dist/providers/codex.js +33 -6
  89. package/node_modules/@codingns/session-sync-core/dist/providers/codex.js.map +1 -1
  90. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.js +78 -36
  91. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.js.map +1 -1
  92. package/package.json +1 -1
  93. package/dist/public/assets/AdaptiveButlerPage-R-XZw7pd.js +0 -3
  94. package/dist/public/assets/App-DkvE5EyM.js +0 -30
  95. package/dist/public/assets/ConversationPage-Cjpg6g0J.js +0 -2
  96. package/dist/public/assets/DesktopWindowPage-1WelvxdH.js +0 -2
  97. package/dist/public/assets/FileContextPanel-D_ghXJuW.js +0 -1
  98. package/dist/public/assets/GitSidebar-D9f9Jxwr.js +0 -6
  99. package/dist/public/assets/MobileSheet-DLg-gX1t.js +0 -1
  100. package/dist/public/assets/SettingsPage-HJIC-P-4.js +0 -1
  101. package/dist/public/assets/TerminalManagerPanel-DpyUTo9k.js +0 -1
  102. package/dist/public/assets/WorkbenchLayout-ByFw4eeu.js +0 -3
  103. package/dist/public/assets/WorkbenchShellRoute-BUITtdAg.css +0 -1
  104. package/dist/public/assets/WorkbenchShellRoute-Kw7JEZI3.js +0 -1
  105. package/dist/public/assets/WorkspaceDetailPage-D0Lrx4Uz.js +0 -1
  106. package/dist/public/assets/WorkspaceHomePage-wR8d3aP9.js +0 -1
  107. package/dist/public/assets/file-tree-icon-UFVoVzhM.js +0 -31
  108. package/dist/public/assets/index-Byp9hJ0c.js +0 -42
  109. package/dist/public/assets/index-_52jxu4a.css +0 -1
  110. 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.fetchFn = options.fetchFn ?? fetch;
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
- relayBaseUrl: input.relayBaseUrl !== undefined
71
- ? normalizeWebsocketBaseUrl(input.relayBaseUrl, "relayBaseUrl")
72
- : snapshot.config.relayBaseUrl,
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: "绑定前必须提供 relayBaseUrl 和 controlBaseUrl"
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.reconcileLegacyLocalTargetBaseUrl(this.repository.findConfig());
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: null,
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
- reconcileLegacyLocalTargetBaseUrl(config) {
388
+ reconcilePersistedConfig(config) {
379
389
  if (!config) {
380
390
  return config;
381
391
  }
382
- if ((config.localTargetBaseUrlSource ?? "default") !== "default") {
383
- return config;
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 (config.localTargetBaseUrl === this.defaultLocalTargetBaseUrl) {
435
+ if (!changed) {
386
436
  return config;
387
437
  }
388
- // `default` 源的目标地址由当前运行模式决定,不应该把历史默认值永久粘在库里。
389
- // 只要默认入口变化了,就在启动时自动收敛到新的默认值;用户显式写入的 custom 配置不动。
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
- const response = await this.fetchFn(new URL(input.path, ensureTrailingSlash(input.controlBaseUrl)), {
569
- method: input.method,
570
- headers: input.headers,
571
- body: input.body
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
- async function buildControlApiError(response, failurePrefix) {
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}:${detail}`
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