@jingyi0605/codingns 0.5.1 → 0.6.0

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 (196) hide show
  1. package/dist/public/assets/{AdaptiveButlerPage-SffCV4Vb.js → AdaptiveButlerPage-uFwDdN-F.js} +3 -3
  2. package/dist/public/assets/App-BZvapsi8.js +30 -0
  3. package/dist/public/assets/App-CcDXqFl1.css +1 -0
  4. package/dist/public/assets/{BootstrapPage--zExdgfM.js → BootstrapPage-gHSoa4JN.js} +1 -1
  5. package/dist/public/assets/ConversationPage-z3sXtKZ7.js +4 -0
  6. package/dist/public/assets/{DesktopDetachPreviewPage-DvI9CIKi.js → DesktopDetachPreviewPage-4eMRxiBW.js} +1 -1
  7. package/dist/public/assets/DesktopWindowPage-CZcoGApB.js +2 -0
  8. package/dist/public/assets/FileContextPanel-C3qex8bb.js +1 -0
  9. package/dist/public/assets/GitSidebar-BK6H16XU.js +6 -0
  10. package/dist/public/assets/{MobileCreateSessionSheet-CXSKMnYn.js → MobileCreateSessionSheet-BYfbvK8o.js} +1 -1
  11. package/dist/public/assets/MobileSheet-Ckug8hTb.js +1 -0
  12. package/dist/public/assets/{MobileTopHeaderFrame-BWorAJ1C.js → MobileTopHeaderFrame-Bwv8Ovm_.js} +1 -1
  13. package/dist/public/assets/MobileWorkspaceSwitcherHeader-RqWrBdn2.js +1 -0
  14. package/dist/public/assets/RelayConnectEntryPage-D_4YL-YH.js +1 -0
  15. package/dist/public/assets/ServerSettingsModal-CMSm3BZU.js +1 -0
  16. package/dist/public/assets/SessionIndexPage-DuK10DL5.js +1 -0
  17. package/dist/public/assets/SettingsPage-fyD-xaHL.js +1 -0
  18. package/dist/public/assets/TerminalManagerPanel-CCLr1Ypk.js +1 -0
  19. package/dist/public/assets/{TerminalPage-CvnHXBhw.js → TerminalPage-DaooFaJ4.js} +19 -19
  20. package/dist/public/assets/{TerminalRuntimeFallbackModal-D7Aq186N.js → TerminalRuntimeFallbackModal-aUzjEBwP.js} +1 -1
  21. package/dist/public/assets/ToolFilesPage-CGxBvYG0.js +1 -0
  22. package/dist/public/assets/ToolGitPage-C264yjS9.js +1 -0
  23. package/dist/public/assets/ToolProcessesPage-BOP4A1cb.js +1 -0
  24. package/dist/public/assets/ToolsHomePage-CQxGiKQA.js +1 -0
  25. package/dist/public/assets/WorkbenchLandingPage-CvAY68ca.js +1 -0
  26. package/dist/public/assets/WorkbenchLayout-DGm8Tc5M.js +3 -0
  27. package/dist/public/assets/{WorkbenchModal-B09hC9b5.js → WorkbenchModal-0tPIIhca.js} +1 -1
  28. package/dist/public/assets/{WorkbenchShellRoute-DsW4mBTX.css → WorkbenchShellRoute-BF0nHWOk.css} +1 -1
  29. package/dist/public/assets/WorkbenchShellRoute-DBBOsJo9.js +1 -0
  30. package/dist/public/assets/WorkspaceDebugDetailPage-CDerFYd2.js +1 -0
  31. package/dist/public/assets/WorkspaceDetailPage-BlJc1CHE.js +1 -0
  32. package/dist/public/assets/WorkspaceHomePage-BUsKJ3lv.js +1 -0
  33. package/dist/public/assets/client-runtime-manager-BZpL17fc.js +1 -0
  34. package/dist/public/assets/{default-session-permission-mode-D0wZ9Jek.js → default-session-permission-mode-DT4SGiwp.js} +1 -1
  35. package/dist/public/assets/file-tree-icon-Db5LXC8h.js +31 -0
  36. package/dist/public/assets/index-BZLcEHW3.js +42 -0
  37. package/dist/public/assets/index-BbspQPC2.css +1 -0
  38. package/dist/public/assets/login-direct-candidate-resolver-1mxe_Oh8.js +1 -0
  39. package/dist/public/assets/{preferences-service-gOt2ZjKZ.js → preferences-service-DWnzl5a0.js} +1 -1
  40. package/dist/public/assets/relay-entry-C5_Iay0I.js +1 -0
  41. package/dist/public/assets/session-runtime-machine-DdLeDqQr.js +17 -0
  42. package/dist/public/assets/{styles-BWPBZvze.css → styles-CsEMfdaS.css} +1 -1
  43. package/dist/public/assets/{terminal-runtime-meta-BMT-rSEe.js → terminal-runtime-meta-cdtWVfCm.js} +1 -1
  44. package/dist/public/assets/{useRegisteredDebugTemplates-zMcEOGca.js → useRegisteredDebugTemplates-oFAQNIqh.js} +1 -1
  45. package/dist/public/assets/window-BVUB8gMK.js +1 -0
  46. package/dist/public/index.html +2 -2
  47. package/dist/server/config/env.d.ts +2 -0
  48. package/dist/server/config/env.js +39 -0
  49. package/dist/server/config/env.js.map +1 -1
  50. package/dist/server/modules/client/npm-global-package-service.d.ts +7 -1
  51. package/dist/server/modules/client/npm-global-package-service.js +149 -43
  52. package/dist/server/modules/client/npm-global-package-service.js.map +1 -1
  53. package/dist/server/modules/client/service-update-task-service.js +6 -2
  54. package/dist/server/modules/client/service-update-task-service.js.map +1 -1
  55. package/dist/server/modules/client/service-update-types.d.ts +2 -0
  56. package/dist/server/modules/git/git-controller.d.ts +3 -0
  57. package/dist/server/modules/git/git-controller.js +3 -0
  58. package/dist/server/modules/git/git-controller.js.map +1 -1
  59. package/dist/server/modules/git/git-read-service.js +47 -1
  60. package/dist/server/modules/git/git-read-service.js.map +1 -1
  61. package/dist/server/modules/git/git-write-service.d.ts +4 -0
  62. package/dist/server/modules/git/git-write-service.js +24 -0
  63. package/dist/server/modules/git/git-write-service.js.map +1 -1
  64. package/dist/server/modules/git/types.d.ts +1 -0
  65. package/dist/server/modules/git/workspace-repo-guard.d.ts +2 -0
  66. package/dist/server/modules/git/workspace-repo-guard.js +24 -10
  67. package/dist/server/modules/git/workspace-repo-guard.js.map +1 -1
  68. package/dist/server/modules/parallel-sessions/parallel-session-controller.d.ts +53 -0
  69. package/dist/server/modules/parallel-sessions/parallel-session-controller.js +70 -0
  70. package/dist/server/modules/parallel-sessions/parallel-session-controller.js.map +1 -0
  71. package/dist/server/modules/parallel-sessions/parallel-session-group-service.d.ts +83 -0
  72. package/dist/server/modules/parallel-sessions/parallel-session-group-service.js +591 -0
  73. package/dist/server/modules/parallel-sessions/parallel-session-group-service.js.map +1 -0
  74. package/dist/server/modules/parallel-sessions/session-isolated-workspace-service.d.ts +56 -0
  75. package/dist/server/modules/parallel-sessions/session-isolated-workspace-service.js +483 -0
  76. package/dist/server/modules/parallel-sessions/session-isolated-workspace-service.js.map +1 -0
  77. package/dist/server/modules/relay-tunnel/crypto/relay-tunnel-packets.d.ts +16 -1
  78. package/dist/server/modules/relay-tunnel/crypto/relay-tunnel-packets.js.map +1 -1
  79. package/dist/server/modules/relay-tunnel/crypto/relay-tunnel-protocol.d.ts +2 -1
  80. package/dist/server/modules/relay-tunnel/crypto/relay-tunnel-protocol.js +18 -0
  81. package/dist/server/modules/relay-tunnel/crypto/relay-tunnel-protocol.js.map +1 -1
  82. package/dist/server/modules/relay-tunnel/relay-tunnel-candidate-endpoints.d.ts +2 -0
  83. package/dist/server/modules/relay-tunnel/relay-tunnel-candidate-endpoints.js +129 -0
  84. package/dist/server/modules/relay-tunnel/relay-tunnel-candidate-endpoints.js.map +1 -0
  85. package/dist/server/modules/relay-tunnel/relay-tunnel-client-context.d.ts +13 -0
  86. package/dist/server/modules/relay-tunnel/relay-tunnel-client-context.js +2 -0
  87. package/dist/server/modules/relay-tunnel/relay-tunnel-client-context.js.map +1 -0
  88. package/dist/server/modules/relay-tunnel/relay-tunnel-gateway-service.d.ts +6 -0
  89. package/dist/server/modules/relay-tunnel/relay-tunnel-gateway-service.js +110 -10
  90. package/dist/server/modules/relay-tunnel/relay-tunnel-gateway-service.js.map +1 -1
  91. package/dist/server/modules/relay-tunnel/relay-tunnel-runtime-adapter.d.ts +16 -4
  92. package/dist/server/modules/relay-tunnel/relay-tunnel-runtime-adapter.js +220 -102
  93. package/dist/server/modules/relay-tunnel/relay-tunnel-runtime-adapter.js.map +1 -1
  94. package/dist/server/modules/relay-tunnel/relay-tunnel-service.d.ts +4 -1
  95. package/dist/server/modules/relay-tunnel/relay-tunnel-service.js +257 -162
  96. package/dist/server/modules/relay-tunnel/relay-tunnel-service.js.map +1 -1
  97. package/dist/server/modules/sessions/codex-app-server-helper-client.d.ts +3 -0
  98. package/dist/server/modules/sessions/codex-app-server-helper-client.js +56 -45
  99. package/dist/server/modules/sessions/codex-app-server-helper-client.js.map +1 -1
  100. package/dist/server/modules/sessions/codex-app-server-helper-process.js +21 -2
  101. package/dist/server/modules/sessions/codex-app-server-helper-process.js.map +1 -1
  102. package/dist/server/modules/sessions/session-activity-inspector.js +6 -8
  103. package/dist/server/modules/sessions/session-activity-inspector.js.map +1 -1
  104. package/dist/server/modules/sessions/session-history-service.d.ts +11 -1
  105. package/dist/server/modules/sessions/session-history-service.js +204 -21
  106. package/dist/server/modules/sessions/session-history-service.js.map +1 -1
  107. package/dist/server/modules/sessions/session-live-runtime-service.d.ts +8 -0
  108. package/dist/server/modules/sessions/session-live-runtime-service.js +208 -25
  109. package/dist/server/modules/sessions/session-live-runtime-service.js.map +1 -1
  110. package/dist/server/modules/workbench/codex-archive-watcher.d.ts +16 -0
  111. package/dist/server/modules/workbench/codex-archive-watcher.js +50 -0
  112. package/dist/server/modules/workbench/codex-archive-watcher.js.map +1 -0
  113. package/dist/server/modules/workbench/workbench-service.d.ts +43 -4
  114. package/dist/server/modules/workbench/workbench-service.js +72 -9
  115. package/dist/server/modules/workbench/workbench-service.js.map +1 -1
  116. package/dist/server/modules/workbench/workspace-panel-snapshot-service.d.ts +1 -1
  117. package/dist/server/modules/workbench/workspace-panel-snapshot-service.js +26 -3
  118. package/dist/server/modules/workbench/workspace-panel-snapshot-service.js.map +1 -1
  119. package/dist/server/modules/workspace/workspace-service.d.ts +3 -1
  120. package/dist/server/modules/workspace/workspace-service.js +10 -2
  121. package/dist/server/modules/workspace/workspace-service.js.map +1 -1
  122. package/dist/server/modules/worktree/worktree-base-ref-resolver.d.ts +20 -0
  123. package/dist/server/modules/worktree/worktree-base-ref-resolver.js +111 -0
  124. package/dist/server/modules/worktree/worktree-base-ref-resolver.js.map +1 -0
  125. package/dist/server/modules/worktree/worktree-cleanup-service.js +9 -3
  126. package/dist/server/modules/worktree/worktree-cleanup-service.js.map +1 -1
  127. package/dist/server/modules/worktree/worktree-manager.d.ts +0 -1
  128. package/dist/server/modules/worktree/worktree-manager.js +14 -20
  129. package/dist/server/modules/worktree/worktree-manager.js.map +1 -1
  130. package/dist/server/routes/git.js +1 -0
  131. package/dist/server/routes/git.js.map +1 -1
  132. package/dist/server/routes/parallel-groups.d.ts +3 -0
  133. package/dist/server/routes/parallel-groups.js +9 -0
  134. package/dist/server/routes/parallel-groups.js.map +1 -0
  135. package/dist/server/server/create-server.d.ts +8 -0
  136. package/dist/server/server/create-server.js +48 -9
  137. package/dist/server/server/create-server.js.map +1 -1
  138. package/dist/server/server/workbench-runtime-terminal-sync.d.ts +14 -0
  139. package/dist/server/server/workbench-runtime-terminal-sync.js +17 -0
  140. package/dist/server/server/workbench-runtime-terminal-sync.js.map +1 -0
  141. package/dist/server/storage/repositories/parallel-session-group-repository.d.ts +11 -0
  142. package/dist/server/storage/repositories/parallel-session-group-repository.js +131 -0
  143. package/dist/server/storage/repositories/parallel-session-group-repository.js.map +1 -0
  144. package/dist/server/storage/repositories/parallel-session-member-repository.d.ts +12 -0
  145. package/dist/server/storage/repositories/parallel-session-member-repository.js +150 -0
  146. package/dist/server/storage/repositories/parallel-session-member-repository.js.map +1 -0
  147. package/dist/server/storage/repositories/session-isolated-workspace-repository.d.ts +15 -0
  148. package/dist/server/storage/repositories/session-isolated-workspace-repository.js +230 -0
  149. package/dist/server/storage/repositories/session-isolated-workspace-repository.js.map +1 -0
  150. package/dist/server/storage/sqlite/schema.sql +73 -0
  151. package/dist/server/types/domain.d.ts +72 -0
  152. package/dist/server/ws/workbench-ws-hub.d.ts +3 -1
  153. package/dist/server/ws/workbench-ws-hub.js +189 -20
  154. package/dist/server/ws/workbench-ws-hub.js.map +1 -1
  155. package/dist/server/ws/ws-server.js +141 -8
  156. package/dist/server/ws/ws-server.js.map +1 -1
  157. package/node_modules/@codingns/session-sync-core/dist/providers/codex.d.ts +1 -0
  158. package/node_modules/@codingns/session-sync-core/dist/providers/codex.js +67 -6
  159. package/node_modules/@codingns/session-sync-core/dist/providers/codex.js.map +1 -1
  160. package/node_modules/@codingns/session-sync-core/dist/runtime/active-run-registry.d.ts +3 -0
  161. package/node_modules/@codingns/session-sync-core/dist/runtime/active-run-registry.js +46 -22
  162. package/node_modules/@codingns/session-sync-core/dist/runtime/active-run-registry.js.map +1 -1
  163. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.js +558 -309
  164. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.js.map +1 -1
  165. package/node_modules/@codingns/session-sync-core/dist/runtime/opencode-runtime.js +29 -5
  166. package/node_modules/@codingns/session-sync-core/dist/runtime/opencode-runtime.js.map +1 -1
  167. package/package.json +1 -1
  168. package/dist/public/assets/App-DUAg5urj.css +0 -1
  169. package/dist/public/assets/App-WOLwMld_.js +0 -30
  170. package/dist/public/assets/ConversationPage-D9pzRmOg.js +0 -2
  171. package/dist/public/assets/DesktopWindowPage-D8FpOSLE.js +0 -2
  172. package/dist/public/assets/FileContextPanel-C8T7oqRN.js +0 -1
  173. package/dist/public/assets/GitSidebar-Bze7DNnc.js +0 -6
  174. package/dist/public/assets/MobileSheet-Gzc14EpR.js +0 -1
  175. package/dist/public/assets/MobileWorkspaceSwitcherHeader-DOr4pTUq.js +0 -1
  176. package/dist/public/assets/ServerSettingsModal-BYB0GvTl.js +0 -1
  177. package/dist/public/assets/SessionIndexPage-CR3IARXX.js +0 -1
  178. package/dist/public/assets/SettingsPage-B_BQtnwE.js +0 -1
  179. package/dist/public/assets/TerminalManagerPanel-PQ-EM64j.js +0 -1
  180. package/dist/public/assets/ToolFilesPage-Qzkc6K2I.js +0 -1
  181. package/dist/public/assets/ToolGitPage-BdNDN-cV.js +0 -1
  182. package/dist/public/assets/ToolProcessesPage-EXJ9DHWI.js +0 -1
  183. package/dist/public/assets/ToolsHomePage-CjF3CWzR.js +0 -1
  184. package/dist/public/assets/WorkbenchLandingPage-DZPk4SmX.js +0 -1
  185. package/dist/public/assets/WorkbenchLayout-rwQib5In.js +0 -3
  186. package/dist/public/assets/WorkbenchShellRoute-Cerk5uK7.js +0 -1
  187. package/dist/public/assets/WorkspaceDebugDetailPage-Bcq8s-Ma.js +0 -1
  188. package/dist/public/assets/WorkspaceDetailPage-DNAa8pKr.js +0 -1
  189. package/dist/public/assets/WorkspaceHomePage-BoiLuACV.js +0 -1
  190. package/dist/public/assets/client-runtime-manager-CRQ-F5d2.js +0 -1
  191. package/dist/public/assets/file-tree-icon-Dp_xhVfD.js +0 -31
  192. package/dist/public/assets/index-C2G8Gmf1.js +0 -42
  193. package/dist/public/assets/index-CpPTUeA3.css +0 -1
  194. package/dist/public/assets/session-runtime-machine-Dq3pW-UF.js +0 -17
  195. package/dist/public/assets/window-BWqRixxq.js +0 -1
  196. /package/dist/public/assets/{styles-CSUx5LGe.js → styles-DRVvx_kv.js} +0 -0
@@ -1,10 +1,23 @@
1
- import os from "node:os";
1
+ import http from "node:http";
2
+ import https from "node:https";
2
3
  import { AppError } from "../../shared/errors/app-error.js";
3
4
  import { decryptSecret, encryptSecret } from "../../shared/utils/secret-box.js";
4
5
  import { nowIso } from "../../shared/utils/time.js";
5
6
  import { RelayTunnelIdentityService } from "./crypto/relay-tunnel-identity-service.js";
7
+ import { RelayTunnelRuntimeHttpError } from "./relay-tunnel-runtime-adapter.js";
6
8
  import { createTaskManager } from "../tasks/task-manager.js";
7
9
  import { HOST_TASK_TYPES } from "../tasks/task-types.js";
10
+ import { buildHostCandidateEndpoints } from "./relay-tunnel-candidate-endpoints.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
+ });
8
21
  export class RelayTunnelService {
9
22
  db;
10
23
  bootstrapStateRepository;
@@ -12,6 +25,7 @@ export class RelayTunnelService {
12
25
  defaultLocalTargetBaseUrl;
13
26
  legacyLocalTargetBaseUrl;
14
27
  controlSessionSecret;
28
+ controlRequestTimeoutMs;
15
29
  fetchFn;
16
30
  taskManager;
17
31
  runtimeAdapter;
@@ -24,7 +38,8 @@ export class RelayTunnelService {
24
38
  this.runtimeAdapter = runtimeAdapter;
25
39
  this.identityService = new RelayTunnelIdentityService(identityRepository);
26
40
  this.controlSessionSecret = normalizeRequiredText(options.controlSessionSecret, "controlSessionSecret");
27
- this.fetchFn = options.fetchFn ?? fetch;
41
+ this.controlRequestTimeoutMs = normalizePositiveInt(options.controlRequestTimeoutMs, DEFAULT_RELAY_TUNNEL_CONTROL_REQUEST_TIMEOUT_MS);
42
+ this.fetchFn = resolveRelayTunnelControlFetch(options.fetchFn);
28
43
  this.defaultLocalTargetBaseUrl = normalizeHttpBaseUrl(options.defaultLocalTargetBaseUrl, "defaultLocalTargetBaseUrl");
29
44
  this.legacyLocalTargetBaseUrl = options.legacyLocalTargetBaseUrl
30
45
  ? normalizeHttpBaseUrl(options.legacyLocalTargetBaseUrl, "legacyLocalTargetBaseUrl")
@@ -63,17 +78,17 @@ export class RelayTunnelService {
63
78
  }
64
79
  async updateConfig(input) {
65
80
  const snapshot = this.readStateSnapshot();
81
+ const controlBaseUrl = input.controlBaseUrl !== undefined
82
+ ? normalizeHttpBaseUrl(input.controlBaseUrl, "controlBaseUrl")
83
+ : snapshot.config.controlBaseUrl;
66
84
  const nextConfig = {
67
85
  ...snapshot.config,
68
86
  activated: input.activated !== undefined
69
87
  ? input.activated
70
88
  : snapshot.config.activated,
71
- relayBaseUrl: input.relayBaseUrl !== undefined
72
- ? normalizeWebsocketBaseUrl(input.relayBaseUrl, "relayBaseUrl")
73
- : snapshot.config.relayBaseUrl,
74
- controlBaseUrl: input.controlBaseUrl !== undefined
75
- ? normalizeHttpBaseUrl(input.controlBaseUrl, "controlBaseUrl")
76
- : snapshot.config.controlBaseUrl,
89
+ // relay 入口统一跟随控制站点,同一条地址避免再出现 control 能通、relay 走死路的分裂配置。
90
+ relayBaseUrl: deriveRelayBaseUrlFromControlBaseUrl(controlBaseUrl),
91
+ controlBaseUrl,
77
92
  localTargetBaseUrl: input.localTargetBaseUrl !== undefined
78
93
  ? normalizeHttpBaseUrl(input.localTargetBaseUrl, "localTargetBaseUrl")
79
94
  : snapshot.config.localTargetBaseUrl,
@@ -169,10 +184,6 @@ export class RelayTunnelService {
169
184
  }
170
185
  async bindControlHost(hostLabel) {
171
186
  const snapshot = this.readStateSnapshot();
172
- if (snapshot.config.bindingId && snapshot.config.tunnelDomain) {
173
- const effectiveConfig = this.resolveConfigWithIdentity(snapshot.config);
174
- return this.buildStatusDto(snapshot, effectiveConfig, this.resolveEffectiveStatus(effectiveConfig));
175
- }
176
187
  const normalizedHostLabel = normalizeRequiredText(hostLabel, "hostLabel");
177
188
  const { controlBaseUrl, accessToken, accountId } = this.requireControlSession(snapshot.config);
178
189
  const identity = this.identityService.ensureIdentity();
@@ -219,17 +230,15 @@ export class RelayTunnelService {
219
230
  const bindingId = normalizeRequiredText(input.bindingId, "bindingId");
220
231
  const tunnelDomain = normalizeTunnelDomain(input.tunnelDomain, "tunnelDomain");
221
232
  const identity = this.identityService.ensureIdentity();
222
- const relayBaseUrl = input.relayBaseUrl !== undefined
223
- ? normalizeWebsocketBaseUrl(input.relayBaseUrl, "relayBaseUrl")
224
- : snapshot.config.relayBaseUrl;
225
233
  const controlBaseUrl = input.controlBaseUrl !== undefined
226
234
  ? normalizeHttpBaseUrl(input.controlBaseUrl, "controlBaseUrl")
227
235
  : snapshot.config.controlBaseUrl;
236
+ const relayBaseUrl = deriveRelayBaseUrlFromControlBaseUrl(controlBaseUrl);
228
237
  if (!relayBaseUrl || !controlBaseUrl) {
229
238
  throw new AppError({
230
239
  statusCode: 400,
231
240
  errorCode: "INVALID_INPUT",
232
- detail: "绑定前必须提供 relayBaseUrl 和 controlBaseUrl"
241
+ detail: "绑定前必须提供 controlBaseUrl"
233
242
  });
234
243
  }
235
244
  const timestamp = nowIso();
@@ -294,7 +303,7 @@ export class RelayTunnelService {
294
303
  throw new AppError({
295
304
  statusCode: 409,
296
305
  errorCode: "RELAY_TUNNEL_NOT_BOUND",
297
- detail: "当前实例还没有绑定公共隧道"
306
+ detail: "当前实例还没有绑定 CodingNS Connect"
298
307
  });
299
308
  }
300
309
  const timestamp = nowIso();
@@ -352,7 +361,7 @@ export class RelayTunnelService {
352
361
  });
353
362
  }
354
363
  readStateSnapshot() {
355
- const persistedConfig = this.reconcileLegacyLocalTargetBaseUrl(this.repository.findConfig());
364
+ const persistedConfig = this.reconcilePersistedConfig(this.repository.findConfig());
356
365
  return {
357
366
  config: persistedConfig
358
367
  ?? {
@@ -360,7 +369,7 @@ export class RelayTunnelService {
360
369
  enabled: false,
361
370
  provider: "codingns_relay",
362
371
  relayBaseUrl: null,
363
- controlBaseUrl: null,
372
+ controlBaseUrl: DEFAULT_RELAY_TUNNEL_CONTROL_BASE_URL,
364
373
  controlAccessTokenCiphertext: null,
365
374
  controlAccountEmail: null,
366
375
  controlSessionExpiresAt: null,
@@ -376,26 +385,58 @@ export class RelayTunnelService {
376
385
  hasPersistedConfig: persistedConfig !== null
377
386
  };
378
387
  }
379
- reconcileLegacyLocalTargetBaseUrl(config) {
388
+ reconcilePersistedConfig(config) {
380
389
  if (!config) {
381
390
  return config;
382
391
  }
383
- if ((config.localTargetBaseUrlSource ?? "default") !== "default") {
384
- 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;
385
405
  }
386
- if (config.localTargetBaseUrl === this.defaultLocalTargetBaseUrl) {
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;
434
+ }
435
+ if (!changed) {
387
436
  return config;
388
437
  }
389
- // `default` 源的目标地址由当前运行模式决定,不应该把历史默认值永久粘在库里。
390
- // 只要默认入口变化了,就在启动时自动收敛到新的默认值;用户显式写入的 custom 配置不动。
391
- const migratedConfig = {
392
- ...config,
393
- localTargetBaseUrl: this.defaultLocalTargetBaseUrl,
394
- localTargetBaseUrlSource: "default",
395
- updatedAt: nowIso()
396
- };
397
- this.repository.upsertConfig(migratedConfig);
398
- return migratedConfig;
438
+ this.repository.upsertConfig(nextConfig);
439
+ return nextConfig;
399
440
  }
400
441
  resolveEffectiveStatus(config) {
401
442
  const persisted = this.repository.findStatus();
@@ -470,6 +511,25 @@ export class RelayTunnelService {
470
511
  const effectiveConfig = this.resolveConfigWithIdentity(latestSnapshot.config);
471
512
  return this.buildStatusDto(latestSnapshot, effectiveConfig, this.resolveEffectiveStatus(effectiveConfig));
472
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
+ }
473
533
  const failedStatus = {
474
534
  ...buildSkeletonStatus("error", snapshot.config, {
475
535
  observedAt: nowIso()
@@ -532,6 +592,14 @@ export class RelayTunnelService {
532
592
  this.repository.upsertConfig(nextConfig);
533
593
  return nextConfig;
534
594
  }
595
+ clearBoundState(config, options) {
596
+ return {
597
+ ...config,
598
+ bindingId: null,
599
+ tunnelDomain: null,
600
+ updatedAt: options.updatedAt
601
+ };
602
+ }
535
603
  isBootstrapInitialized() {
536
604
  return this.bootstrapStateRepository.getState().initialized;
537
605
  }
@@ -568,16 +636,23 @@ export class RelayTunnelService {
568
636
  }
569
637
  async requestControlApi(input) {
570
638
  let response;
639
+ const controller = new AbortController();
640
+ const timeoutId = setTimeout(() => controller.abort(), this.controlRequestTimeoutMs);
571
641
  try {
572
- response = await this.fetchFn(new URL(input.path, ensureTrailingSlash(input.controlBaseUrl)), {
642
+ const requestUrl = new URL(input.path, ensureTrailingSlash(input.controlBaseUrl)).toString();
643
+ response = await this.fetchFn(requestUrl, {
573
644
  method: input.method,
574
645
  headers: input.headers,
575
- body: input.body
646
+ body: input.body,
647
+ signal: controller.signal
576
648
  });
577
649
  }
578
650
  catch (error) {
579
651
  throw buildControlFetchError(error, input.controlBaseUrl, input.failurePrefix);
580
652
  }
653
+ finally {
654
+ clearTimeout(timeoutId);
655
+ }
581
656
  if (!response.ok) {
582
657
  throw await buildControlApiError(response, input.controlBaseUrl, input.failurePrefix);
583
658
  }
@@ -605,133 +680,6 @@ function buildSkeletonStatus(phase, config, overrides) {
605
680
  observedAt: overrides?.observedAt ?? null
606
681
  };
607
682
  }
608
- function buildHostCandidateEndpoints(config) {
609
- const endpoints = new Map();
610
- const relayEndpoint = buildRelayPublicUrl(config);
611
- if (relayEndpoint) {
612
- endpoints.set(relayEndpoint, {
613
- endpointId: `relay:${relayEndpoint}`,
614
- kind: "relay",
615
- url: relayEndpoint,
616
- priority: 400,
617
- expiresAt: null,
618
- source: "host_reported"
619
- });
620
- }
621
- for (const localCandidateUrl of buildLocalCandidateUrls(config.localTargetBaseUrl)) {
622
- endpoints.set(localCandidateUrl, {
623
- endpointId: `host_reported:${localCandidateUrl}`,
624
- kind: classifyCandidateEndpointKind(localCandidateUrl),
625
- url: localCandidateUrl,
626
- priority: resolveCandidateEndpointPriority(localCandidateUrl),
627
- expiresAt: null,
628
- source: "host_reported"
629
- });
630
- }
631
- return Array.from(endpoints.values()).sort((left, right) => {
632
- if (left.priority !== right.priority) {
633
- return left.priority - right.priority;
634
- }
635
- return left.url.localeCompare(right.url);
636
- });
637
- }
638
- function buildRelayPublicUrl(config) {
639
- if (!config.tunnelDomain || !config.controlBaseUrl) {
640
- return null;
641
- }
642
- try {
643
- const controlUrl = new URL(config.controlBaseUrl);
644
- controlUrl.hostname = config.tunnelDomain.trim().toLowerCase();
645
- controlUrl.pathname = "/";
646
- controlUrl.search = "";
647
- controlUrl.hash = "";
648
- return controlUrl.toString().replace(/\/$/, "");
649
- }
650
- catch {
651
- return null;
652
- }
653
- }
654
- function buildLocalCandidateUrls(localTargetBaseUrl) {
655
- let parsed;
656
- try {
657
- parsed = new URL(localTargetBaseUrl);
658
- }
659
- catch {
660
- return [];
661
- }
662
- const candidates = new Set();
663
- const hostname = parsed.hostname.trim().toLowerCase();
664
- candidates.add(normalizeUrlWithoutTrailingSlash(parsed.toString()));
665
- if (hostname === "0.0.0.0" || hostname === "::" || hostname === "::0") {
666
- for (const networkAddress of listPrivateIpv4Addresses()) {
667
- const candidateUrl = new URL(parsed.toString());
668
- candidateUrl.hostname = networkAddress;
669
- candidates.add(normalizeUrlWithoutTrailingSlash(candidateUrl.toString()));
670
- }
671
- }
672
- if (hostname === "127.0.0.1" || hostname === "localhost" || hostname === "::1") {
673
- for (const networkAddress of listPrivateIpv4Addresses()) {
674
- const candidateUrl = new URL(parsed.toString());
675
- candidateUrl.hostname = networkAddress;
676
- candidates.add(normalizeUrlWithoutTrailingSlash(candidateUrl.toString()));
677
- }
678
- }
679
- return Array.from(candidates);
680
- }
681
- function listPrivateIpv4Addresses() {
682
- const interfaces = os.networkInterfaces();
683
- const candidates = new Set();
684
- for (const entries of Object.values(interfaces)) {
685
- for (const entry of entries ?? []) {
686
- if (!entry || entry.family !== "IPv4" || entry.internal) {
687
- continue;
688
- }
689
- if (!isPrivateIpv4Address(entry.address)) {
690
- continue;
691
- }
692
- candidates.add(entry.address);
693
- }
694
- }
695
- return Array.from(candidates).sort();
696
- }
697
- function isPrivateIpv4Address(address) {
698
- return (/^10\./.test(address)
699
- || /^192\.168\./.test(address)
700
- || /^172\.(1[6-9]|2\d|3[0-1])\./.test(address));
701
- }
702
- function classifyCandidateEndpointKind(candidateUrl) {
703
- try {
704
- const hostname = new URL(candidateUrl).hostname.toLowerCase();
705
- if (hostname === "127.0.0.1" || hostname === "localhost" || hostname === "::1") {
706
- return "loopback";
707
- }
708
- if (isPrivateIpv4Address(hostname)) {
709
- return "lan";
710
- }
711
- return "custom";
712
- }
713
- catch {
714
- return "custom";
715
- }
716
- }
717
- function resolveCandidateEndpointPriority(candidateUrl) {
718
- const kind = classifyCandidateEndpointKind(candidateUrl);
719
- switch (kind) {
720
- case "loopback":
721
- return 100;
722
- case "lan":
723
- return 200;
724
- case "tailscale":
725
- return 300;
726
- case "relay":
727
- return 400;
728
- default:
729
- return 500;
730
- }
731
- }
732
- function normalizeUrlWithoutTrailingSlash(value) {
733
- return value.endsWith("/") ? value.slice(0, -1) : value;
734
- }
735
683
  function isBound(config) {
736
684
  return Boolean(config.bindingId && config.tunnelDomain);
737
685
  }
@@ -849,6 +797,14 @@ function normalizeWebsocketBaseUrl(value, field) {
849
797
  : parsed.protocol;
850
798
  return `${normalizedProtocol}//${parsed.host}${pathname}`;
851
799
  }
800
+ function deriveRelayBaseUrlFromControlBaseUrl(controlBaseUrl) {
801
+ const normalizedControlBaseUrl = normalizeHttpBaseUrl(controlBaseUrl, "controlBaseUrl");
802
+ if (!normalizedControlBaseUrl) {
803
+ return null;
804
+ }
805
+ const relayUrl = new URL("relay", ensureTrailingSlash(normalizedControlBaseUrl));
806
+ return normalizeWebsocketBaseUrl(relayUrl.toString(), "relayBaseUrl");
807
+ }
852
808
  function requireConfiguredControlBaseUrl(config) {
853
809
  const controlBaseUrl = normalizeOptionalText(config.controlBaseUrl);
854
810
  if (!controlBaseUrl) {
@@ -873,6 +829,15 @@ function clearRelayTunnelControlSession(config, options) {
873
829
  updatedAt: options.updatedAt
874
830
  };
875
831
  }
832
+ function shouldResetStaleRelayBinding(error) {
833
+ if (!(error instanceof RelayTunnelRuntimeHttpError)) {
834
+ return false;
835
+ }
836
+ return (error.errorCode === "TUNNEL_NOT_FOUND"
837
+ || error.errorCode === "BINDING_NOT_FOUND"
838
+ || error.errorCode === "HOST_AUTH_INVALID"
839
+ || error.errorCode === "HOST_BINDING_MISMATCH");
840
+ }
876
841
  async function buildControlApiError(response, controlBaseUrl, failurePrefix) {
877
842
  const detail = await readControlApiErrorDetail(response);
878
843
  if (response.status === 401 || response.status === 403) {
@@ -934,6 +899,9 @@ function readJsonErrorText(value) {
934
899
  function resolveFetchErrorDetail(error) {
935
900
  if (error instanceof Error) {
936
901
  const code = readFetchErrorCode(error);
902
+ if (error.name === "AbortError") {
903
+ return "请求超时。";
904
+ }
937
905
  if (code === "ECONNREFUSED") {
938
906
  return "连接被目标服务器拒绝。";
939
907
  }
@@ -956,6 +924,133 @@ function readFetchErrorCode(error) {
956
924
  : undefined;
957
925
  return typeof cause?.code === "string" ? cause.code : null;
958
926
  }
927
+ function normalizePositiveInt(value, fallback) {
928
+ if (value === null || value === undefined || !Number.isInteger(value) || value <= 0) {
929
+ return fallback;
930
+ }
931
+ return value;
932
+ }
933
+ function resolveRelayTunnelControlFetch(fetchFn) {
934
+ if (fetchFn) {
935
+ return fetchFn;
936
+ }
937
+ if (isBuiltinNodeFetch(globalThis.fetch)) {
938
+ return createRelayTunnelControlFetch();
939
+ }
940
+ return globalThis.fetch;
941
+ }
942
+ function isBuiltinNodeFetch(fetchFn) {
943
+ const source = Function.prototype.toString.call(fetchFn);
944
+ return source.includes("internal/deps/undici/undici");
945
+ }
946
+ function createRelayTunnelControlFetch() {
947
+ return async (input, init) => {
948
+ const requestUrl = resolveFetchInputUrl(input);
949
+ const parsedUrl = new URL(requestUrl);
950
+ if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
951
+ throw new TypeError(`Unsupported protocol for relay tunnel control fetch: ${parsedUrl.protocol}`);
952
+ }
953
+ return await new Promise((resolve, reject) => {
954
+ const transport = parsedUrl.protocol === "https:" ? https : http;
955
+ const headers = normalizeFetchHeaders(init?.headers);
956
+ const body = normalizeFetchBody(init?.body);
957
+ const abortHandler = () => request.destroy(createAbortRequestError());
958
+ const request = transport.request(parsedUrl, {
959
+ method: init?.method ?? "GET",
960
+ headers,
961
+ agent: parsedUrl.protocol === "https:" ? RELAY_TUNNEL_CONTROL_HTTPS_AGENT : undefined
962
+ }, (response) => {
963
+ const chunks = [];
964
+ response.on("data", (chunk) => {
965
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
966
+ });
967
+ response.on("end", () => {
968
+ cleanup();
969
+ resolve(new Response(Buffer.concat(chunks), {
970
+ status: response.statusCode ?? 500,
971
+ headers: buildFetchResponseHeaders(response.headers)
972
+ }));
973
+ });
974
+ response.on("error", (error) => {
975
+ cleanup();
976
+ reject(error);
977
+ });
978
+ });
979
+ function cleanup() {
980
+ init?.signal?.removeEventListener("abort", abortHandler);
981
+ }
982
+ request.on("error", (error) => {
983
+ cleanup();
984
+ reject(error);
985
+ });
986
+ if (init?.signal) {
987
+ if (init.signal.aborted) {
988
+ cleanup();
989
+ request.destroy(createAbortRequestError());
990
+ return;
991
+ }
992
+ init.signal.addEventListener("abort", abortHandler, { once: true });
993
+ }
994
+ if (body !== null) {
995
+ request.write(body);
996
+ }
997
+ request.end();
998
+ });
999
+ };
1000
+ }
1001
+ function resolveFetchInputUrl(input) {
1002
+ if (typeof input === "string") {
1003
+ return input;
1004
+ }
1005
+ if (input instanceof URL) {
1006
+ return input.toString();
1007
+ }
1008
+ return input.url;
1009
+ }
1010
+ function normalizeFetchHeaders(headers) {
1011
+ if (!headers) {
1012
+ return {};
1013
+ }
1014
+ return Object.fromEntries(new Headers(headers).entries());
1015
+ }
1016
+ function normalizeFetchBody(body) {
1017
+ if (body === null || body === undefined) {
1018
+ return null;
1019
+ }
1020
+ if (typeof body === "string" || Buffer.isBuffer(body)) {
1021
+ return body;
1022
+ }
1023
+ if (body instanceof URLSearchParams) {
1024
+ return body.toString();
1025
+ }
1026
+ if (body instanceof ArrayBuffer) {
1027
+ return Buffer.from(body);
1028
+ }
1029
+ if (ArrayBuffer.isView(body)) {
1030
+ return Buffer.from(body.buffer, body.byteOffset, body.byteLength);
1031
+ }
1032
+ throw new TypeError("Unsupported relay tunnel control request body type");
1033
+ }
1034
+ function buildFetchResponseHeaders(headers) {
1035
+ const responseHeaders = new Headers();
1036
+ for (const [name, value] of Object.entries(headers)) {
1037
+ if (Array.isArray(value)) {
1038
+ for (const item of value) {
1039
+ responseHeaders.append(name, item);
1040
+ }
1041
+ continue;
1042
+ }
1043
+ if (typeof value === "string") {
1044
+ responseHeaders.set(name, value);
1045
+ }
1046
+ }
1047
+ return responseHeaders;
1048
+ }
1049
+ function createAbortRequestError() {
1050
+ const error = new Error("This operation was aborted");
1051
+ error.name = "AbortError";
1052
+ return error;
1053
+ }
959
1054
  function appendControlApiDetail(detail) {
960
1055
  const normalized = normalizeOptionalText(detail);
961
1056
  if (!normalized) {