@artenx/dshb 0.1.18 → 0.1.19

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 (101) hide show
  1. package/lib/client.js +2157 -567
  2. package/lib/client.js.map +1 -1
  3. package/lib/core/index.js +72 -4
  4. package/lib/core/index.js.map +1 -1
  5. package/lib/core/node-registry.d.ts +2 -108
  6. package/lib/core/node-registry.js +43 -2
  7. package/lib/core/node-registry.js.map +1 -1
  8. package/lib/core/routes.d.ts +4 -3
  9. package/lib/core/routes.js +67 -23
  10. package/lib/core/routes.js.map +1 -1
  11. package/lib/core/sync/coordinator.d.ts +48 -0
  12. package/lib/core/sync/coordinator.js +247 -0
  13. package/lib/core/sync/coordinator.js.map +1 -0
  14. package/lib/core/sync/host-key.d.ts +2 -0
  15. package/lib/core/sync/host-key.js +71 -0
  16. package/lib/core/sync/host-key.js.map +1 -0
  17. package/lib/core/sync/internal-routes.d.ts +9 -0
  18. package/lib/core/sync/internal-routes.js +236 -0
  19. package/lib/core/sync/internal-routes.js.map +1 -0
  20. package/lib/core/sync/model.d.ts +2 -0
  21. package/lib/core/sync/model.js +106 -0
  22. package/lib/core/sync/model.js.map +1 -0
  23. package/lib/core/sync/node-sync.d.ts +2 -0
  24. package/lib/core/sync/node-sync.js +660 -0
  25. package/lib/core/sync/node-sync.js.map +1 -0
  26. package/lib/core/sync/offline-archive.d.ts +55 -0
  27. package/lib/core/sync/offline-archive.js +673 -0
  28. package/lib/core/sync/offline-archive.js.map +1 -0
  29. package/lib/core/sync/offline-journal.d.ts +22 -0
  30. package/lib/core/sync/offline-journal.js +74 -0
  31. package/lib/core/sync/offline-journal.js.map +1 -0
  32. package/lib/core/sync/offline-routes.d.ts +7 -0
  33. package/lib/core/sync/offline-routes.js +192 -0
  34. package/lib/core/sync/offline-routes.js.map +1 -0
  35. package/lib/core/sync/peer-manager.d.ts +56 -0
  36. package/lib/core/sync/peer-manager.js +258 -0
  37. package/lib/core/sync/peer-manager.js.map +1 -0
  38. package/lib/core/sync/protocol.d.ts +2 -0
  39. package/lib/core/sync/protocol.js +124 -0
  40. package/lib/core/sync/protocol.js.map +1 -0
  41. package/lib/core/sync/remote-helper.d.ts +5 -0
  42. package/lib/core/sync/remote-helper.js +160 -0
  43. package/lib/core/sync/remote-helper.js.map +1 -0
  44. package/lib/core/sync/revision.d.ts +2 -0
  45. package/lib/core/sync/revision.js +77 -0
  46. package/lib/core/sync/revision.js.map +1 -0
  47. package/lib/core/sync/routes.d.ts +9 -0
  48. package/lib/core/sync/routes.js +339 -0
  49. package/lib/core/sync/routes.js.map +1 -0
  50. package/lib/core/sync/scheduler.d.ts +14 -0
  51. package/lib/core/sync/scheduler.js +81 -0
  52. package/lib/core/sync/scheduler.js.map +1 -0
  53. package/lib/core/sync/session-journal.d.ts +2 -0
  54. package/lib/core/sync/session-journal.js +191 -0
  55. package/lib/core/sync/session-journal.js.map +1 -0
  56. package/lib/core/sync/session-routes.d.ts +7 -0
  57. package/lib/core/sync/session-routes.js +67 -0
  58. package/lib/core/sync/session-routes.js.map +1 -0
  59. package/lib/core/sync/session-sync.d.ts +2 -0
  60. package/lib/core/sync/session-sync.js +2 -0
  61. package/lib/core/sync/ssh-transport.d.ts +2 -0
  62. package/lib/core/sync/ssh-transport.js +408 -0
  63. package/lib/core/sync/ssh-transport.js.map +1 -0
  64. package/lib/core/sync/stores.d.ts +3 -0
  65. package/lib/core/sync/stores.js +385 -0
  66. package/lib/core/sync/stores.js.map +1 -0
  67. package/lib/core/test.d.ts +1 -1
  68. package/lib/core/workspace-bindings.d.ts +2 -28
  69. package/lib/core/workspace-bindings.js +14 -14
  70. package/lib/core/workspace-bindings.js.map +1 -1
  71. package/lib/core/workspace-instruction-context.d.ts +3 -2260
  72. package/lib/core/workspace-instruction-context.js +104 -2
  73. package/lib/core/workspace-instruction-context.js.map +1 -0
  74. package/lib/core/workspace-instruction-routes.d.ts +1 -1
  75. package/lib/core/workspace-instructions.d.ts +2 -68
  76. package/lib/core/workspace-instructions.js +13 -0
  77. package/lib/core/workspace-instructions.js.map +1 -1
  78. package/lib/core/workspaces.d.ts +4 -3
  79. package/lib/core/workspaces.js +17 -1
  80. package/lib/core/workspaces.js.map +1 -1
  81. package/lib/host-key-C5afhAUd.d.ts +30 -0
  82. package/lib/index-VF8bcBeX.d.ts +2293 -0
  83. package/lib/{workspace-instruction-context-DytAmaPY.js → lib-gdtU_WKT.js} +159 -102
  84. package/lib/lib-gdtU_WKT.js.map +1 -0
  85. package/lib/model-BP7Gw1yk.d.ts +104 -0
  86. package/lib/node-registry-BTnnM5-k.d.ts +112 -0
  87. package/lib/node-sync-3Z9QLz2n.d.ts +89 -0
  88. package/lib/protocol-CV7LGE44.d.ts +35 -0
  89. package/lib/revision-C9sOlDZ6.d.ts +25 -0
  90. package/lib/router/router-fs.d.ts +12 -3
  91. package/lib/router/router-fs.js.map +1 -1
  92. package/lib/session-journal-pgPqcWn9.d.ts +40 -0
  93. package/lib/session-sync-EdfwUON5.js +2177 -0
  94. package/lib/session-sync-EdfwUON5.js.map +1 -0
  95. package/lib/session-sync-g6qKZzjV.d.ts +1252 -0
  96. package/lib/ssh-transport-BcIiLxsX.d.ts +55 -0
  97. package/lib/stores-CKyEt0yk.d.ts +31 -0
  98. package/lib/workspace-bindings-CsB0TIJB.d.ts +28 -0
  99. package/lib/workspace-instructions-DirYYiku.d.ts +69 -0
  100. package/package.json +20 -1
  101. package/lib/workspace-instruction-context-DytAmaPY.js.map +0 -1
package/lib/client.js CHANGED
@@ -458,7 +458,7 @@ window.__ModuleLoader__.load({
458
458
  }
459
459
  //#endregion
460
460
  //#region src/client/directory-flow.tsx
461
- const inputStyle$1 = {
461
+ const inputStyle$2 = {
462
462
  width: "100%",
463
463
  boxSizing: "border-box",
464
464
  padding: "0 10px",
@@ -471,7 +471,7 @@ window.__ModuleLoader__.load({
471
471
  color: "var(--dsw-alias-label-primary)",
472
472
  outline: "none"
473
473
  };
474
- const buttonStyle$1 = {
474
+ const buttonStyle$2 = {
475
475
  boxSizing: "border-box",
476
476
  height: 36,
477
477
  padding: "0 14px",
@@ -487,12 +487,12 @@ window.__ModuleLoader__.load({
487
487
  gap: 4
488
488
  };
489
489
  const primaryButtonStyle$1 = {
490
- ...buttonStyle$1,
490
+ ...buttonStyle$2,
491
491
  background: "var(--dsw-alias-button-primary-fill)",
492
492
  color: "var(--dsw-alias-label-primary-foreground)"
493
493
  };
494
494
  const secondaryButtonStyle$1 = {
495
- ...buttonStyle$1,
495
+ ...buttonStyle$2,
496
496
  border: "1px solid var(--dsw-alias-border-l2)",
497
497
  color: "var(--dsw-alias-label-primary)",
498
498
  background: "transparent"
@@ -637,6 +637,7 @@ window.__ModuleLoader__.load({
637
637
  ]);
638
638
  if (!props.open) return null;
639
639
  const node = nodes.find((n) => n.id === nodeId);
640
+ const selectableNodes = nodes.filter((item) => !item.sync?.pendingRemoval);
640
641
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
641
642
  style: overlayStyle,
642
643
  role: "dialog",
@@ -676,7 +677,7 @@ window.__ModuleLoader__.load({
676
677
  }),
677
678
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
678
679
  style: {
679
- ...inputStyle$1,
680
+ ...inputStyle$2,
680
681
  marginBottom: 14
681
682
  },
682
683
  value: nodeId,
@@ -685,7 +686,7 @@ window.__ModuleLoader__.load({
685
686
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
686
687
  value: "",
687
688
  children: "选择节点…"
688
- }), nodes.map((n) => {
689
+ }), selectableNodes.map((n) => {
689
690
  const typeLabel = n.type === "local-host" ? "本地" : n.type === "remote-ssh" ? "远程 SSH" : n.type === "local-docker" ? "本地 Docker" : n.type === "remote-docker" ? "远程 Docker" : n.type;
690
691
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
691
692
  value: n.id,
@@ -693,6 +694,7 @@ window.__ModuleLoader__.load({
693
694
  n.name,
694
695
  "(",
695
696
  typeLabel,
697
+ n.sync?.importedVia === "peer" && n.sync.sourceName ? ` · 同步自 ${n.sync.sourceName}` : "",
696
698
  ")"
697
699
  ]
698
700
  }, n.id);
@@ -726,7 +728,7 @@ window.__ModuleLoader__.load({
726
728
  }),
727
729
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
728
730
  style: {
729
- ...inputStyle$1,
731
+ ...inputStyle$2,
730
732
  marginBottom: 8,
731
733
  height: "auto",
732
734
  minHeight: 32,
@@ -825,7 +827,7 @@ window.__ModuleLoader__.load({
825
827
  },
826
828
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
827
829
  style: {
828
- ...inputStyle$1,
830
+ ...inputStyle$2,
829
831
  flex: 1,
830
832
  minWidth: 0
831
833
  },
@@ -2116,11 +2118,9 @@ window.__ModuleLoader__.load({
2116
2118
  scan();
2117
2119
  }
2118
2120
  //#endregion
2119
- //#region src/client/ui.tsx
2120
- const SECTION_ID = "dshb-nodes";
2121
- const EMPTY_FORM = {
2121
+ //#region src/client/sync-peer-form.ts
2122
+ const EMPTY_SYNC_PEER_FORM = {
2122
2123
  name: "",
2123
- type: "remote-ssh",
2124
2124
  host: "",
2125
2125
  port: "22",
2126
2126
  username: "",
@@ -2128,266 +2128,1724 @@ window.__ModuleLoader__.load({
2128
2128
  password: "",
2129
2129
  privateKey: "",
2130
2130
  passphrase: "",
2131
- keyPath: "",
2132
- dockerImage: "",
2133
- dockerCpus: "",
2134
- dockerMemory: ""
2131
+ remoteDshHome: "",
2132
+ remoteProfile: "",
2133
+ remoteWebPort: "",
2134
+ direction: "pull",
2135
+ schedule: "manual",
2136
+ syncSessions: false,
2137
+ enabled: true
2135
2138
  };
2136
- const inputStyle = {
2139
+ function validPort(value) {
2140
+ const port = Number(value);
2141
+ return Number.isInteger(port) && port >= 1 && port <= 65535;
2142
+ }
2143
+ function syncPeerFormError(form, savedSecrets) {
2144
+ if (!form.name.trim() || !form.host.trim() || !form.username.trim()) return "请填写名称、SSH 主机和系统账号";
2145
+ if (!validPort(form.port)) return "SSH 端口必须在 1 到 65535 之间";
2146
+ if (form.remoteWebPort.trim() && !validPort(form.remoteWebPort)) return "远端 Web 端口必须在 1 到 65535 之间";
2147
+ if (form.authKind === "password" && !form.password && !savedSecrets?.hasPassword) return "请填写 SSH 密码";
2148
+ if (form.authKind === "key" && !form.privateKey && !savedSecrets?.hasKey) return "请填写 SSH 私钥内容";
2149
+ }
2150
+ function toSyncPeerForm(peer) {
2151
+ return {
2152
+ name: peer.name,
2153
+ host: peer.host,
2154
+ port: String(peer.port),
2155
+ username: peer.username,
2156
+ authKind: peer.authKind,
2157
+ password: "",
2158
+ privateKey: "",
2159
+ passphrase: "",
2160
+ remoteDshHome: peer.remoteDshHome ?? "",
2161
+ remoteProfile: peer.remoteProfile ?? "",
2162
+ remoteWebPort: peer.remoteWebPort == null ? "" : String(peer.remoteWebPort),
2163
+ direction: peer.direction,
2164
+ schedule: peer.schedule,
2165
+ syncSessions: peer.syncSessions,
2166
+ enabled: peer.enabled
2167
+ };
2168
+ }
2169
+ function buildSyncPeerFormRequest(form) {
2170
+ const fields = {
2171
+ name: form.name.trim(),
2172
+ host: form.host.trim(),
2173
+ port: Number(form.port),
2174
+ username: form.username.trim(),
2175
+ authKind: form.authKind,
2176
+ direction: form.direction,
2177
+ schedule: form.schedule,
2178
+ syncSessions: form.syncSessions,
2179
+ enabled: form.enabled,
2180
+ ...form.remoteDshHome.trim() ? { remoteDshHome: form.remoteDshHome.trim() } : {},
2181
+ ...form.remoteProfile.trim() ? { remoteProfile: form.remoteProfile.trim() } : {},
2182
+ ...form.remoteWebPort.trim() ? { remoteWebPort: Number(form.remoteWebPort) } : {}
2183
+ };
2184
+ const secrets = {};
2185
+ if (form.authKind === "password") {
2186
+ if (form.password) secrets.password = form.password;
2187
+ } else {
2188
+ if (form.privateKey) secrets.privateKey = form.privateKey;
2189
+ if (form.passphrase) secrets.passphrase = form.passphrase;
2190
+ }
2191
+ return {
2192
+ fields,
2193
+ secrets
2194
+ };
2195
+ }
2196
+ //#endregion
2197
+ //#region src/client/sync-peers.tsx
2198
+ function shortenSessionId(id) {
2199
+ const suffix = id.replace(/^session-sync-/, "");
2200
+ return `${suffix.slice(0, 8)}…${suffix.slice(-4)}`;
2201
+ }
2202
+ function copyText(value) {
2203
+ navigator.clipboard?.writeText(value);
2204
+ }
2205
+ function resultSummary(peer) {
2206
+ const result = peer.progress?.lastResult;
2207
+ if (!result) return void 0;
2208
+ return `节点导入 ${result.imported}、导出 ${result.exported};会话导入 ${result.importedSessions}、导出 ${result.exportedSessions};跳过 ${result.skipped}`;
2209
+ }
2210
+ async function readJsonResponse(response, fallback) {
2211
+ try {
2212
+ return await response.json();
2213
+ } catch {
2214
+ const message = response.status === 404 ? "同步 API 尚未加载,请重启 DSH 服务后重试" : `${fallback}:服务器返回了无效响应(HTTP ${response.status})`;
2215
+ throw new Error(message);
2216
+ }
2217
+ }
2218
+ const inputStyle$1 = {
2137
2219
  width: "100%",
2138
2220
  boxSizing: "border-box",
2139
2221
  padding: "0 10px",
2140
- height: 32,
2222
+ minHeight: 34,
2141
2223
  border: "1px solid var(--dsw-alias-border-l2)",
2142
2224
  borderRadius: 8,
2143
2225
  fontSize: 14,
2144
2226
  fontFamily: "inherit",
2145
2227
  background: "var(--dsw-alias-bg-layer-1)",
2146
- color: "var(--dsw-alias-label-primary)",
2147
- outline: "none"
2228
+ color: "var(--dsw-alias-label-primary)"
2148
2229
  };
2149
- const buttonStyle = {
2230
+ const btn = {
2150
2231
  boxSizing: "border-box",
2151
- height: 36,
2152
- padding: "0 14px",
2153
- borderRadius: 18,
2154
- fontSize: 14,
2155
- lineHeight: "22px",
2232
+ height: 32,
2233
+ padding: "0 12px",
2234
+ borderRadius: 8,
2235
+ fontSize: 13,
2156
2236
  fontFamily: "inherit",
2157
2237
  cursor: "pointer",
2158
- border: "none",
2159
2238
  display: "inline-flex",
2160
- justifyContent: "center",
2161
2239
  alignItems: "center",
2162
- gap: 4
2240
+ justifyContent: "center",
2241
+ whiteSpace: "nowrap",
2242
+ flexShrink: 0
2163
2243
  };
2164
- const primaryButtonStyle = {
2165
- ...buttonStyle,
2244
+ const primaryBtn = {
2245
+ ...btn,
2246
+ border: "none",
2166
2247
  background: "var(--dsw-alias-button-primary-fill)",
2167
2248
  color: "var(--dsw-alias-label-primary-foreground)"
2168
2249
  };
2169
- const secondaryButtonStyle = {
2170
- ...buttonStyle,
2250
+ const secondaryBtn = {
2251
+ ...btn,
2171
2252
  border: "1px solid var(--dsw-alias-border-l2)",
2253
+ background: "transparent",
2254
+ color: "var(--dsw-alias-label-primary)"
2255
+ };
2256
+ const dangerBtn = {
2257
+ ...btn,
2258
+ border: "1px solid var(--dsw-alias-state-error-primary, #f87171)",
2259
+ background: "transparent",
2260
+ color: "var(--dsw-alias-state-error-primary, #f87171)"
2261
+ };
2262
+ const label = {
2263
+ fontSize: 12,
2264
+ fontWeight: 500,
2265
+ opacity: .75,
2172
2266
  color: "var(--dsw-alias-label-primary)",
2173
- background: "transparent"
2267
+ marginBottom: 4,
2268
+ display: "block"
2174
2269
  };
2175
- const dangerOutlineButtonStyle = {
2176
- ...buttonStyle,
2177
- border: "1px solid var(--dsw-alias-state-error-primary)",
2178
- color: "var(--dsw-alias-state-error-primary)",
2179
- background: "transparent"
2270
+ const field = { marginBottom: 14 };
2271
+ const hint = {
2272
+ fontSize: 11,
2273
+ opacity: .5,
2274
+ color: "var(--dsw-alias-label-primary)",
2275
+ marginTop: 3
2180
2276
  };
2181
- function useNodes(reloadKey) {
2182
- const [nodes, setNodes] = (0, react.useState)([]);
2277
+ const groupTitle = {
2278
+ fontSize: 11,
2279
+ fontWeight: 600,
2280
+ letterSpacing: "0.06em",
2281
+ textTransform: "uppercase",
2282
+ opacity: .45,
2283
+ marginBottom: 12,
2284
+ paddingBottom: 6,
2285
+ borderBottom: "1px solid var(--dsw-alias-border-l2)"
2286
+ };
2287
+ const required = { color: "var(--dsw-alias-state-error-primary, #f87171)" };
2288
+ function SyncPeersSection() {
2289
+ const [reloadKey, setReloadKey] = (0, react.useState)(0);
2290
+ const [peers, setPeers] = (0, react.useState)([]);
2291
+ const [archives, setArchives] = (0, react.useState)([]);
2292
+ const [selectedId, setSelectedId] = (0, react.useState)("new");
2293
+ const [form, setForm] = (0, react.useState)(EMPTY_SYNC_PEER_FORM);
2294
+ const [busy, setBusy] = (0, react.useState)(false);
2295
+ const [probing, setProbing] = (0, react.useState)(false);
2296
+ const [syncing, setSyncing] = (0, react.useState)(false);
2297
+ const [notice, setNotice] = (0, react.useState)(void 0);
2298
+ const [hostKeyChallenge, setHostKeyChallenge] = (0, react.useState)(void 0);
2183
2299
  (0, react.useEffect)(() => {
2184
2300
  let cancelled = false;
2185
2301
  (async () => {
2186
2302
  try {
2187
- const res = await fetch("/api/dshb/nodes");
2188
- if (!res.ok) return;
2189
- const data = await res.json();
2190
- if (!cancelled) setNodes(data.nodes ?? []);
2191
- } catch {}
2303
+ const response = await fetch("/api/dshb/sync/peers");
2304
+ const data = await readJsonResponse(response, "读取同步节点失败");
2305
+ if (!response.ok) throw new Error(data.error ?? "读取同步节点失败");
2306
+ if (!cancelled) setPeers(data.peers ?? []);
2307
+ const archiveResponse = await fetch("/api/dshb/sync/archives");
2308
+ const archiveData = await readJsonResponse(archiveResponse, "读取会话归档失败");
2309
+ if (!archiveResponse.ok) throw new Error(archiveData.error ?? "读取会话归档失败");
2310
+ if (!cancelled) setArchives(archiveData.archives ?? []);
2311
+ } catch (error) {
2312
+ if (!cancelled) setNotice({
2313
+ kind: "error",
2314
+ text: error instanceof Error ? error.message : "读取同步节点失败"
2315
+ });
2316
+ }
2192
2317
  })();
2193
2318
  return () => {
2194
2319
  cancelled = true;
2195
2320
  };
2196
2321
  }, [reloadKey]);
2197
- return nodes;
2198
- }
2199
- function NodeSection(_props) {
2200
- const [reloadKey, setReloadKey] = (0, react.useState)(0);
2201
- const nodes = useNodes(reloadKey);
2202
- const [selectedId, setSelectedId] = (0, react.useState)("new");
2203
- const [form, setForm] = (0, react.useState)(EMPTY_FORM);
2204
- const [notice, setNotice] = (0, react.useState)(void 0);
2205
- const [busy, setBusy] = (0, react.useState)(false);
2206
- const [testing, setTesting] = (0, react.useState)(false);
2207
- const [testResult, setTestResult] = (0, react.useState)(void 0);
2208
- const [sshConfigEntries, setSshConfigEntries] = (0, react.useState)([]);
2209
- const [dockerBusy, setDockerBusy] = (0, react.useState)(false);
2210
- const flash = (0, react.useCallback)((n) => {
2211
- setNotice(n);
2212
- if (n) setTimeout(() => setNotice(void 0), 6e3);
2213
- }, []);
2214
- const reload = (0, react.useCallback)(() => setReloadKey((k) => k + 1), []);
2215
2322
  (0, react.useEffect)(() => {
2216
- (async () => {
2217
- try {
2218
- const res = await fetch("/api/dshb/ssh-config");
2219
- if (!res.ok) return;
2220
- const data = await res.json();
2221
- setSshConfigEntries(data.entries ?? []);
2222
- } catch {}
2223
- })();
2224
- }, []);
2225
- const selectNode = (0, react.useCallback)((id) => {
2323
+ const interval = syncing || peers.some((peer) => peer.progress?.lastRunStatus === "running") ? 3e3 : 15e3;
2324
+ const timer = setInterval(() => setReloadKey((key) => key + 1), interval);
2325
+ return () => clearInterval(timer);
2326
+ }, [peers, syncing]);
2327
+ const reload = () => setReloadKey((key) => key + 1);
2328
+ const selectedPeer = selectedId === "new" ? void 0 : peers.find((peer) => peer.id === selectedId);
2329
+ const runningCount = peers.filter((peer) => peer.progress?.lastRunStatus === "running").length;
2330
+ const statusLabel = (peer) => {
2331
+ const status = peer.progress?.lastRunStatus;
2332
+ if (!peer.enabled) return "已停用";
2333
+ if (status === "running") return "同步中";
2334
+ if (status === "failed") return "上次失败";
2335
+ if (status === "succeeded") return "上次成功";
2336
+ return "尚未同步";
2337
+ };
2338
+ const select = (id) => {
2226
2339
  setSelectedId(id);
2227
- setTestResult(void 0);
2228
- setTesting(false);
2229
- setDockerBusy(false);
2230
- if (id === "new") {
2231
- setForm(EMPTY_FORM);
2232
- return;
2233
- }
2234
- const node = nodes.find((n) => n.id === id);
2235
- if (!node) return;
2236
- setForm({
2237
- name: node.name,
2238
- type: node.type,
2239
- host: node.ssh?.host ?? "",
2240
- port: String(node.ssh?.port ?? 22),
2241
- username: node.ssh?.username ?? "",
2242
- authKind: node.ssh?.auth.kind ?? "password",
2243
- password: "",
2244
- privateKey: "",
2245
- passphrase: "",
2246
- keyPath: node.ssh?.auth.keyPath ?? "",
2247
- dockerImage: node.docker?.image ?? "",
2248
- dockerCpus: node.docker?.resources?.cpus != null ? String(node.docker?.resources?.cpus) : "",
2249
- dockerMemory: node.docker?.resources?.memoryMB != null ? String(node.docker?.resources?.memoryMB) : ""
2250
- });
2251
- }, [nodes]);
2252
- const importSshConfig = (0, react.useCallback)((entry) => {
2253
- setForm((f) => ({
2254
- ...f,
2255
- type: "remote-ssh",
2256
- host: entry.host,
2257
- port: String(entry.port),
2258
- username: entry.username,
2259
- authKind: entry.hasIdentityFile ? "key" : f.authKind,
2260
- keyPath: entry.identityFile ?? f.keyPath,
2261
- name: f.name || entry.host
2262
- }));
2263
- setTestResult(void 0);
2264
- flash({
2265
- kind: "ok",
2266
- text: `已从 ~/.ssh/config 导入 ${entry.host}`
2267
- });
2268
- }, [flash]);
2269
- const save = (0, react.useCallback)(async () => {
2270
- if (!form.name.trim()) {
2271
- flash({
2340
+ setNotice(void 0);
2341
+ setHostKeyChallenge(void 0);
2342
+ const peer = id === "new" ? void 0 : peers.find((item) => item.id === id);
2343
+ setForm(peer ? toSyncPeerForm(peer) : EMPTY_SYNC_PEER_FORM);
2344
+ };
2345
+ const save = async () => {
2346
+ const formError = syncPeerFormError(form, selectedPeer?.hasSecret);
2347
+ if (formError) {
2348
+ setNotice({
2272
2349
  kind: "error",
2273
- text: "请填写节点名称"
2350
+ text: formError
2274
2351
  });
2275
2352
  return;
2276
2353
  }
2354
+ const request = buildSyncPeerFormRequest(form);
2355
+ const payload = {
2356
+ ...request.fields,
2357
+ secrets: request.secrets
2358
+ };
2277
2359
  setBusy(true);
2278
2360
  try {
2279
- const isRemote = form.type === "remote-ssh" || form.type === "remote-docker";
2280
- const isDocker = form.type === "local-docker" || form.type === "remote-docker";
2281
- const payload = {
2282
- name: form.name.trim(),
2283
- type: form.type,
2284
- ssh: isRemote ? {
2285
- host: form.host.trim(),
2286
- port: Number(form.port) || 22,
2287
- username: form.username.trim(),
2288
- auth: {
2289
- kind: form.authKind,
2290
- ...form.keyPath.trim() ? { keyPath: form.keyPath.trim() } : {}
2291
- }
2292
- } : void 0,
2293
- secrets: isRemote ? {
2294
- ...form.password ? { password: form.password } : {},
2295
- ...form.privateKey ? { privateKey: form.privateKey } : {},
2296
- ...form.passphrase ? { passphrase: form.passphrase } : {}
2297
- } : void 0,
2298
- docker: isDocker ? {
2299
- mode: "managed",
2300
- ...form.dockerImage.trim() ? { image: form.dockerImage.trim() } : {},
2301
- ...form.dockerCpus ? { resources: { cpus: Number(form.dockerCpus) || void 0 } } : {},
2302
- ...form.dockerMemory ? { resources: { memoryMB: Number(form.dockerMemory) || void 0 } } : {},
2303
- ...form.dockerCpus && form.dockerMemory ? { resources: {
2304
- cpus: Number(form.dockerCpus) || void 0,
2305
- memoryMB: Number(form.dockerMemory) || void 0
2306
- } } : {}
2307
- } : void 0
2308
- };
2309
- const url = selectedId === "new" ? "/api/dshb/nodes" : `/api/dshb/nodes/${selectedId}`;
2310
- const res = await fetch(url, {
2361
+ const response = await fetch(selectedId === "new" ? "/api/dshb/sync/peers" : `/api/dshb/sync/peers/${selectedId}`, {
2311
2362
  method: selectedId === "new" ? "POST" : "PATCH",
2312
2363
  headers: { "content-type": "application/json" },
2313
2364
  body: JSON.stringify(payload)
2314
2365
  });
2315
- const data = await res.json();
2316
- if (res.ok && data.ok) {
2317
- flash({
2318
- kind: "ok",
2319
- text: selectedId === "new" ? "节点已创建" : "节点已保存"
2320
- });
2321
- reload();
2322
- if (data.node) setSelectedId(data.node.id);
2323
- } else flash({
2324
- kind: "error",
2325
- text: data.error ?? "保存失败"
2366
+ const data = await readJsonResponse(response, "保存同步节点失败");
2367
+ if (!response.ok || !data.ok || !data.peer) throw new Error(data.error ?? "保存同步节点失败");
2368
+ setNotice({
2369
+ kind: "ok",
2370
+ text: selectedId === "new" ? "同步节点已创建" : "同步节点已保存"
2326
2371
  });
2327
- } catch {
2328
- flash({
2372
+ setHostKeyChallenge(void 0);
2373
+ setSelectedId(data.peer.id);
2374
+ setForm(toSyncPeerForm(data.peer));
2375
+ reload();
2376
+ } catch (error) {
2377
+ setNotice({
2329
2378
  kind: "error",
2330
- text: "网络错误"
2379
+ text: error instanceof Error ? error.message : "保存同步节点失败"
2331
2380
  });
2332
2381
  } finally {
2333
2382
  setBusy(false);
2334
2383
  }
2335
- }, [
2336
- form,
2337
- selectedId,
2338
- flash,
2339
- reload
2340
- ]);
2341
- const remove = (0, react.useCallback)(async () => {
2384
+ };
2385
+ const probe = async () => {
2342
2386
  if (selectedId === "new") return;
2343
- setBusy(true);
2387
+ setProbing(true);
2388
+ setHostKeyChallenge(void 0);
2344
2389
  try {
2345
- const res = await fetch(`/api/dshb/nodes/${selectedId}`, { method: "DELETE" });
2346
- const data = await res.json();
2347
- if (res.ok && data.ok) {
2348
- flash({
2349
- kind: "ok",
2350
- text: "节点已删除"
2390
+ const response = await fetch(`/api/dshb/sync/peers/${selectedId}/probe`, { method: "POST" });
2391
+ const data = await readJsonResponse(response, "SSH 测试失败");
2392
+ if (data.code === "host-key-untrusted" || data.code === "host-key-changed") {
2393
+ setHostKeyChallenge({
2394
+ code: data.code,
2395
+ fingerprint: data.fingerprint ?? "",
2396
+ ...data.expected ? { expected: data.expected } : {}
2351
2397
  });
2352
- setSelectedId("new");
2353
- setForm(EMPTY_FORM);
2354
- reload();
2355
- } else flash({
2356
- kind: "error",
2357
- text: data.error ?? "删除失败"
2398
+ return;
2399
+ }
2400
+ if (!response.ok || !data.ok) throw new Error(data.error ?? "SSH 测试失败");
2401
+ setNotice({
2402
+ kind: "ok",
2403
+ text: `SSH 和 DSHB 握手成功(${data.handshake?.instanceId ?? "远端实例"})`
2358
2404
  });
2359
- } catch {
2360
- flash({
2405
+ } catch (error) {
2406
+ setNotice({
2361
2407
  kind: "error",
2362
- text: "删除失败"
2408
+ text: error instanceof Error ? error.message : "SSH 测试失败"
2363
2409
  });
2364
2410
  } finally {
2365
- setBusy(false);
2411
+ setProbing(false);
2366
2412
  }
2367
- }, [
2368
- selectedId,
2369
- flash,
2370
- reload
2371
- ]);
2372
- const test = (0, react.useCallback)(async () => {
2373
- if (form.type === "local-host") {
2374
- flash({
2413
+ };
2414
+ const trustObservedHostKey = async () => {
2415
+ if (selectedId === "new" || !hostKeyChallenge) return;
2416
+ const confirmation = hostKeyChallenge.code === "host-key-changed" ? `远端 SSH 主机指纹已变化。\n当前保存:${hostKeyChallenge.expected}\n本次观察:${hostKeyChallenge.fingerprint}\n确认信任新指纹?` : `首次连接观察到 SSH 主机指纹:\n${hostKeyChallenge.fingerprint}\n请核对后确认信任。`;
2417
+ if (!window.confirm(confirmation)) return;
2418
+ setProbing(true);
2419
+ try {
2420
+ const response = await fetch(`/api/dshb/sync/peers/${selectedId}/trust-host-key`, { method: "POST" });
2421
+ const data = await readJsonResponse(response, "保存 SSH 主机信任失败");
2422
+ if (!response.ok || !data.ok || !data.peer) throw new Error(data.error ?? "保存 SSH 主机信任失败");
2423
+ setHostKeyChallenge(void 0);
2424
+ setForm(toSyncPeerForm(data.peer));
2425
+ reload();
2426
+ setNotice({
2427
+ kind: "ok",
2428
+ text: "主机指纹已保存,正在重新验证连接"
2429
+ });
2430
+ await probe();
2431
+ } catch (error) {
2432
+ setNotice({
2375
2433
  kind: "error",
2376
- text: "本地节点无需测试连接"
2434
+ text: error instanceof Error ? error.message : "保存 SSH 主机信任失败"
2377
2435
  });
2378
- return;
2436
+ } finally {
2437
+ setProbing(false);
2379
2438
  }
2380
- const isRemote = form.type === "remote-ssh" || form.type === "remote-docker";
2381
- if (isRemote && !form.host.trim()) {
2382
- flash({
2439
+ };
2440
+ const runSync = async () => {
2441
+ if (selectedId === "new" || !selectedPeer?.enabled) return;
2442
+ setSyncing(true);
2443
+ try {
2444
+ const response = await fetch(`/api/dshb/sync/peers/${selectedId}/run`, { method: "POST" });
2445
+ const data = await readJsonResponse(response, "同步失败");
2446
+ if (!response.ok || !data.ok || !data.accepted) throw new Error(data.error ?? "同步失败");
2447
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
2448
+ setPeers((current) => current.map((peer) => peer.id === selectedId ? {
2449
+ ...peer,
2450
+ progress: {
2451
+ ...peer.progress,
2452
+ lastRunAt: startedAt,
2453
+ lastRunStatus: "running"
2454
+ }
2455
+ } : peer));
2456
+ setNotice({
2457
+ kind: "ok",
2458
+ text: "同步作业已启动,状态会自动更新"
2459
+ });
2460
+ } catch (error) {
2461
+ setNotice({
2383
2462
  kind: "error",
2384
- text: "请填写主机地址"
2463
+ text: error instanceof Error ? error.message : "同步失败"
2385
2464
  });
2386
- return;
2465
+ } finally {
2466
+ setSyncing(false);
2387
2467
  }
2388
- setTesting(true);
2389
- setTestResult(void 0);
2390
- try {
2468
+ };
2469
+ const restoreArchive = async (branchId) => {
2470
+ const archive = archives.find((entry) => entry.branchId === branchId);
2471
+ if (!archive || !window.confirm(`恢复会话 ${archive.originSessionId} 的归档版本?当前版本将保留在归档中。`)) return;
2472
+ setBusy(true);
2473
+ try {
2474
+ const response = await fetch(`/api/dshb/sync/archives/${branchId}/restore`, { method: "POST" });
2475
+ const data = await readJsonResponse(response, "恢复会话归档失败");
2476
+ if (!response.ok || !data.ok) throw new Error(data.error ?? "恢复会话归档失败");
2477
+ setNotice({
2478
+ kind: "ok",
2479
+ text: "会话归档已恢复为当前版本"
2480
+ });
2481
+ reload();
2482
+ } catch (error) {
2483
+ setNotice({
2484
+ kind: "error",
2485
+ text: error instanceof Error ? error.message : "恢复会话归档失败"
2486
+ });
2487
+ } finally {
2488
+ setBusy(false);
2489
+ }
2490
+ };
2491
+ const progressLabel = (peer) => {
2492
+ if (peer.progress?.lastRunStatus === "running") return "同步中";
2493
+ if (peer.progress?.lastRunStatus === "failed") return `上次失败:${peer.progress.lastErrorMessage ?? "同步失败"}`;
2494
+ if (peer.progress?.lastSuccessAt) return `上次成功:${new Date(peer.progress.lastSuccessAt).toLocaleString()}`;
2495
+ return "尚未同步";
2496
+ };
2497
+ const remove = async () => {
2498
+ if (selectedId === "new" || !window.confirm("删除此同步节点及其连接凭据?")) return;
2499
+ setBusy(true);
2500
+ try {
2501
+ const response = await fetch(`/api/dshb/sync/peers/${selectedId}`, { method: "DELETE" });
2502
+ const data = await readJsonResponse(response, "删除同步节点失败");
2503
+ if (!response.ok || !data.ok) throw new Error(data.error ?? "删除同步节点失败");
2504
+ setNotice({
2505
+ kind: "ok",
2506
+ text: "同步节点已删除"
2507
+ });
2508
+ setSelectedId("new");
2509
+ setForm(EMPTY_SYNC_PEER_FORM);
2510
+ reload();
2511
+ } catch (error) {
2512
+ setNotice({
2513
+ kind: "error",
2514
+ text: error instanceof Error ? error.message : "删除同步节点失败"
2515
+ });
2516
+ } finally {
2517
+ setBusy(false);
2518
+ }
2519
+ };
2520
+ const isNew = selectedId === "new";
2521
+ const syncInProgress = syncing || selectedPeer?.progress?.lastRunStatus === "running";
2522
+ const selectedSummary = selectedPeer ? resultSummary(selectedPeer) : void 0;
2523
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2524
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: `
2525
+ .dshb-sync-peer-tabs{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:20px;padding-bottom:14px;border-bottom:1px solid var(--dsw-alias-border-l2)}
2526
+ .dshb-sync-peer-tab{padding:5px 12px;border-radius:16px;font-size:12px;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-primary);white-space:nowrap;flex-shrink:0}
2527
+ .dshb-sync-peer-tab.active{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);border-color:transparent}
2528
+ .dshb-sync-peer-tab:hover:not(.active){background:var(--dsw-alias-interactive-bg-hover)}
2529
+ .dshb-sync-peer-add{width:28px;height:28px;padding:0;border-radius:50%;font-size:16px;line-height:1;opacity:.65}
2530
+ .dshb-sync-peer-add:hover{opacity:1}
2531
+ .dshb-sync-peer-status{font-size:11px;opacity:.6;margin-top:2px;display:block}
2532
+ .dshb-sync-peer-tab{max-width:220px;overflow:hidden;text-overflow:ellipsis}
2533
+ .dshb-sync-peer-tab.running{border-color:var(--dsw-alias-button-primary-fill)}
2534
+ .dshb-sync-peer-tab.failed{border-color:var(--dsw-alias-state-error-primary, #f87171)}
2535
+ .dshb-sync-peer-add{width:28px;height:28px;padding:0;border-radius:50%;font-size:16px;line-height:1;opacity:.65}
2536
+ .dshb-sync-peer-add:hover{opacity:1}
2537
+ .dshb-sync-peer-status{font-size:11px;opacity:.6;margin-top:2px;display:block}
2538
+ .dshb-sync-two-col{display:grid;grid-template-columns:1fr 1fr;gap:0 16px}
2539
+ @media(max-width:600px){.dshb-sync-two-col{grid-template-columns:1fr}}
2540
+
2541
+ ` }),
2542
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2543
+ className: "dshb-sync-peer-tabs",
2544
+ children: [
2545
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2546
+ type: "button",
2547
+ className: "dshb-sync-peer-tab dshb-sync-peer-add",
2548
+ "aria-label": "新增同步节点",
2549
+ title: "新增同步节点",
2550
+ onClick: () => select("new"),
2551
+ children: "+"
2552
+ }),
2553
+ peers.map((peer) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2554
+ type: "button",
2555
+ className: `dshb-sync-peer-tab${selectedId === peer.id ? " active" : ""}${peer.progress?.lastRunStatus === "running" ? " running" : ""}${peer.progress?.lastRunStatus === "failed" ? " failed" : ""}`,
2556
+ onClick: () => select(peer.id),
2557
+ title: `${peer.name} · ${statusLabel(peer)}`,
2558
+ "aria-label": `同步节点 ${peer.name},${statusLabel(peer)}`,
2559
+ children: [peer.name, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2560
+ className: "dshb-sync-peer-status",
2561
+ children: statusLabel(peer)
2562
+ })]
2563
+ }, peer.id)),
2564
+ runningCount > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2565
+ style: {
2566
+ fontSize: 11,
2567
+ alignSelf: "center",
2568
+ opacity: .65
2569
+ },
2570
+ children: [runningCount, " 个同步作业进行中"]
2571
+ }),
2572
+ peers.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2573
+ style: {
2574
+ fontSize: 12,
2575
+ opacity: .5,
2576
+ alignSelf: "center"
2577
+ },
2578
+ children: "点击“+”添加第一个同步节点"
2579
+ })
2580
+ ]
2581
+ }),
2582
+ selectedPeer && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2583
+ role: "status",
2584
+ style: {
2585
+ maxWidth: 560,
2586
+ marginBottom: 16,
2587
+ padding: "12px 14px",
2588
+ border: "1px solid var(--dsw-alias-border-l2)",
2589
+ borderRadius: 10,
2590
+ background: "var(--dsw-alias-bg-layer-1)",
2591
+ fontSize: 12
2592
+ },
2593
+ children: [
2594
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2595
+ style: {
2596
+ display: "flex",
2597
+ justifyContent: "space-between",
2598
+ gap: 12,
2599
+ alignItems: "baseline"
2600
+ },
2601
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: progressLabel(selectedPeer) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2602
+ type: "button",
2603
+ onClick: reload,
2604
+ style: {
2605
+ ...secondaryBtn,
2606
+ height: 26,
2607
+ fontSize: 11
2608
+ },
2609
+ children: "刷新"
2610
+ })]
2611
+ }),
2612
+ selectedSummary && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2613
+ style: {
2614
+ marginTop: 6,
2615
+ opacity: .72,
2616
+ lineHeight: 1.55
2617
+ },
2618
+ children: selectedSummary
2619
+ }),
2620
+ selectedPeer.progress?.lastErrorMessage && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2621
+ style: {
2622
+ marginTop: 6,
2623
+ color: "var(--dsw-alias-state-error-primary, #f87171)",
2624
+ lineHeight: 1.55
2625
+ },
2626
+ children: selectedPeer.progress.lastErrorMessage
2627
+ }),
2628
+ syncInProgress && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2629
+ style: {
2630
+ marginTop: 6,
2631
+ opacity: .6
2632
+ },
2633
+ children: "大工作区或大量会话可能需要数分钟,页面每 3 秒更新进度。"
2634
+ })
2635
+ ]
2636
+ }),
2637
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2638
+ style: { maxWidth: 560 },
2639
+ children: [
2640
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2641
+ style: groupTitle,
2642
+ children: "连接信息"
2643
+ }),
2644
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2645
+ style: field,
2646
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2647
+ style: label,
2648
+ children: ["节点名称 ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2649
+ style: required,
2650
+ children: "*"
2651
+ })]
2652
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2653
+ style: inputStyle$1,
2654
+ value: form.name,
2655
+ placeholder: "例如:生产服务器",
2656
+ onChange: (e) => setForm((c) => ({
2657
+ ...c,
2658
+ name: e.target.value
2659
+ }))
2660
+ })]
2661
+ }),
2662
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2663
+ className: "dshb-sync-two-col",
2664
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2665
+ style: field,
2666
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2667
+ style: label,
2668
+ children: ["SSH 主机 ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2669
+ style: required,
2670
+ children: "*"
2671
+ })]
2672
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2673
+ style: inputStyle$1,
2674
+ value: form.host,
2675
+ placeholder: "sync.example.com",
2676
+ onChange: (e) => setForm((c) => ({
2677
+ ...c,
2678
+ host: e.target.value
2679
+ }))
2680
+ })]
2681
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2682
+ style: field,
2683
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2684
+ style: label,
2685
+ children: ["SSH 端口 ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2686
+ style: required,
2687
+ children: "*"
2688
+ })]
2689
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2690
+ style: inputStyle$1,
2691
+ inputMode: "numeric",
2692
+ value: form.port,
2693
+ placeholder: "22",
2694
+ onChange: (e) => setForm((c) => ({
2695
+ ...c,
2696
+ port: e.target.value
2697
+ }))
2698
+ })]
2699
+ })]
2700
+ }),
2701
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2702
+ style: field,
2703
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2704
+ style: label,
2705
+ children: ["SSH 系统账号 ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2706
+ style: required,
2707
+ children: "*"
2708
+ })]
2709
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2710
+ style: inputStyle$1,
2711
+ value: form.username,
2712
+ placeholder: "root",
2713
+ onChange: (e) => setForm((c) => ({
2714
+ ...c,
2715
+ username: e.target.value
2716
+ }))
2717
+ })]
2718
+ }),
2719
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2720
+ style: {
2721
+ ...groupTitle,
2722
+ marginTop: 20
2723
+ },
2724
+ children: "认证方式"
2725
+ }),
2726
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2727
+ style: field,
2728
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2729
+ style: label,
2730
+ children: "认证类型"
2731
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2732
+ style: inputStyle$1,
2733
+ value: form.authKind,
2734
+ onChange: (e) => setForm((c) => ({
2735
+ ...c,
2736
+ authKind: e.target.value
2737
+ })),
2738
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2739
+ value: "password",
2740
+ children: "密码"
2741
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2742
+ value: "key",
2743
+ children: "私钥"
2744
+ })]
2745
+ })]
2746
+ }),
2747
+ form.authKind === "password" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2748
+ style: field,
2749
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2750
+ style: label,
2751
+ children: ["SSH 密码", selectedPeer?.hasSecret.hasPassword && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2752
+ style: {
2753
+ fontWeight: 400,
2754
+ opacity: .55
2755
+ },
2756
+ children: "(已保存,留空保持不变)"
2757
+ })]
2758
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2759
+ style: inputStyle$1,
2760
+ type: "password",
2761
+ autoComplete: "new-password",
2762
+ value: form.password,
2763
+ placeholder: selectedPeer?.hasSecret.hasPassword ? "••••••••" : "",
2764
+ onChange: (e) => setForm((c) => ({
2765
+ ...c,
2766
+ password: e.target.value
2767
+ }))
2768
+ })]
2769
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2770
+ style: field,
2771
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2772
+ style: label,
2773
+ children: ["私钥内容", selectedPeer?.hasSecret.hasKey && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2774
+ style: {
2775
+ fontWeight: 400,
2776
+ opacity: .55
2777
+ },
2778
+ children: "(已保存,留空保持不变)"
2779
+ })]
2780
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
2781
+ style: {
2782
+ ...inputStyle$1,
2783
+ minHeight: 100,
2784
+ padding: "10px",
2785
+ resize: "vertical",
2786
+ fontFamily: "monospace",
2787
+ fontSize: 12
2788
+ },
2789
+ value: form.privateKey,
2790
+ placeholder: "-----BEGIN OPENSSH PRIVATE KEY-----",
2791
+ onChange: (e) => setForm((c) => ({
2792
+ ...c,
2793
+ privateKey: e.target.value
2794
+ }))
2795
+ })]
2796
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2797
+ style: field,
2798
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2799
+ style: label,
2800
+ children: ["私钥 passphrase", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2801
+ style: {
2802
+ fontWeight: 400,
2803
+ opacity: .55
2804
+ },
2805
+ children: selectedPeer?.hasSecret.hasPassphrase ? "(已保存,留空保持不变)" : "(可选)"
2806
+ })]
2807
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2808
+ style: inputStyle$1,
2809
+ type: "password",
2810
+ autoComplete: "new-password",
2811
+ value: form.passphrase,
2812
+ onChange: (e) => setForm((c) => ({
2813
+ ...c,
2814
+ passphrase: e.target.value
2815
+ }))
2816
+ })]
2817
+ })] }),
2818
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2819
+ style: {
2820
+ ...groupTitle,
2821
+ marginTop: 20
2822
+ },
2823
+ children: "远端配置"
2824
+ }),
2825
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2826
+ style: field,
2827
+ children: [
2828
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2829
+ style: label,
2830
+ children: "远端 DSH_HOME"
2831
+ }),
2832
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2833
+ style: inputStyle$1,
2834
+ value: form.remoteDshHome,
2835
+ placeholder: "默认使用 SSH 账号的 ~/.dsh",
2836
+ onChange: (e) => setForm((c) => ({
2837
+ ...c,
2838
+ remoteDshHome: e.target.value
2839
+ }))
2840
+ }),
2841
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2842
+ style: hint,
2843
+ children: "远端 DSH 数据目录,留空使用默认路径"
2844
+ })
2845
+ ]
2846
+ }),
2847
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2848
+ className: "dshb-sync-two-col",
2849
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2850
+ style: field,
2851
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2852
+ style: label,
2853
+ children: "远端 profile"
2854
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2855
+ style: inputStyle$1,
2856
+ value: form.remoteProfile,
2857
+ placeholder: "web",
2858
+ onChange: (e) => setForm((c) => ({
2859
+ ...c,
2860
+ remoteProfile: e.target.value
2861
+ }))
2862
+ })]
2863
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2864
+ style: field,
2865
+ children: [
2866
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2867
+ style: label,
2868
+ children: "远端 Web 端口"
2869
+ }),
2870
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2871
+ style: inputStyle$1,
2872
+ inputMode: "numeric",
2873
+ value: form.remoteWebPort,
2874
+ placeholder: "3080",
2875
+ onChange: (e) => setForm((c) => ({
2876
+ ...c,
2877
+ remoteWebPort: e.target.value
2878
+ }))
2879
+ }),
2880
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2881
+ style: hint,
2882
+ children: "留空使用默认 3080"
2883
+ })
2884
+ ]
2885
+ })]
2886
+ }),
2887
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2888
+ style: {
2889
+ ...groupTitle,
2890
+ marginTop: 20
2891
+ },
2892
+ children: "同步策略"
2893
+ }),
2894
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2895
+ className: "dshb-sync-two-col",
2896
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2897
+ style: field,
2898
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2899
+ style: label,
2900
+ children: "同步方向"
2901
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2902
+ style: inputStyle$1,
2903
+ value: form.direction,
2904
+ onChange: (e) => setForm((c) => ({
2905
+ ...c,
2906
+ direction: e.target.value
2907
+ })),
2908
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2909
+ value: "pull",
2910
+ children: "单向拉取到本机"
2911
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2912
+ value: "bidirectional",
2913
+ children: "双向同步"
2914
+ })]
2915
+ })]
2916
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2917
+ style: field,
2918
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2919
+ style: label,
2920
+ children: "同步时机"
2921
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2922
+ style: inputStyle$1,
2923
+ value: form.schedule,
2924
+ onChange: (e) => setForm((c) => ({
2925
+ ...c,
2926
+ schedule: e.target.value
2927
+ })),
2928
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2929
+ value: "manual",
2930
+ children: "手动"
2931
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2932
+ value: "every-5m",
2933
+ children: "每 5 分钟"
2934
+ })]
2935
+ })]
2936
+ })]
2937
+ }),
2938
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2939
+ style: {
2940
+ ...field,
2941
+ display: "flex",
2942
+ flexDirection: "column",
2943
+ gap: 10
2944
+ },
2945
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2946
+ style: {
2947
+ display: "flex",
2948
+ alignItems: "flex-start",
2949
+ gap: 8,
2950
+ cursor: "pointer"
2951
+ },
2952
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2953
+ type: "checkbox",
2954
+ checked: form.syncSessions,
2955
+ style: { marginTop: 2 },
2956
+ onChange: (e) => setForm((c) => ({
2957
+ ...c,
2958
+ syncSessions: e.target.checked
2959
+ }))
2960
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2961
+ style: { fontSize: 13 },
2962
+ children: "同步工作区会话"
2963
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2964
+ style: {
2965
+ display: "block",
2966
+ fontSize: 11,
2967
+ opacity: .5,
2968
+ marginTop: 2
2969
+ },
2970
+ children: "覆盖已绑定 SSH 或 Docker 远程节点的工作区,保留冲突分支供恢复"
2971
+ })] })]
2972
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2973
+ style: {
2974
+ display: "flex",
2975
+ alignItems: "center",
2976
+ gap: 8,
2977
+ cursor: "pointer"
2978
+ },
2979
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2980
+ type: "checkbox",
2981
+ checked: form.enabled,
2982
+ onChange: (e) => setForm((c) => ({
2983
+ ...c,
2984
+ enabled: e.target.checked
2985
+ }))
2986
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2987
+ style: { fontSize: 13 },
2988
+ children: "启用此同步节点"
2989
+ })]
2990
+ })]
2991
+ }),
2992
+ selectedPeer?.hostKeyFingerprint && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2993
+ style: field,
2994
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2995
+ style: label,
2996
+ children: "已信任 SSH 主机指纹"
2997
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
2998
+ style: {
2999
+ fontSize: 11,
3000
+ opacity: .6,
3001
+ overflowWrap: "anywhere",
3002
+ fontFamily: "monospace"
3003
+ },
3004
+ children: selectedPeer.hostKeyFingerprint
3005
+ })]
3006
+ }),
3007
+ hostKeyChallenge && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3008
+ style: {
3009
+ ...field,
3010
+ padding: "12px 14px",
3011
+ border: "1px solid var(--dsw-alias-state-warn-label, #f59e0b)",
3012
+ borderRadius: 10,
3013
+ display: "flex",
3014
+ flexDirection: "column",
3015
+ gap: 6,
3016
+ fontSize: 12
3017
+ },
3018
+ children: [
3019
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: hostKeyChallenge.code === "host-key-changed" ? "主机指纹发生变化" : "首次连接需要确认主机身份" }),
3020
+ hostKeyChallenge.expected && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3021
+ style: { overflowWrap: "anywhere" },
3022
+ children: ["已保存:", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: hostKeyChallenge.expected })]
3023
+ }),
3024
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3025
+ style: { overflowWrap: "anywhere" },
3026
+ children: ["本次观察:", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: hostKeyChallenge.fingerprint })]
3027
+ }),
3028
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3029
+ type: "button",
3030
+ onClick: () => void trustObservedHostKey(),
3031
+ disabled: busy || probing,
3032
+ style: {
3033
+ ...secondaryBtn,
3034
+ alignSelf: "flex-start",
3035
+ marginTop: 4
3036
+ },
3037
+ children: "信任本次观察的指纹"
3038
+ })
3039
+ ]
3040
+ }),
3041
+ notice && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3042
+ style: {
3043
+ ...field,
3044
+ padding: "10px 14px",
3045
+ borderRadius: 8,
3046
+ fontSize: 13,
3047
+ background: notice.kind === "ok" ? "var(--dsw-alias-state-success-bg, rgba(34,197,94,.1))" : "var(--dsw-alias-state-error-bg, rgba(248,113,113,.1))",
3048
+ color: notice.kind === "ok" ? "var(--dsw-alias-state-success-primary, #22c55e)" : "var(--dsw-alias-state-error-primary, #f87171)"
3049
+ },
3050
+ children: notice.text
3051
+ }),
3052
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3053
+ style: {
3054
+ display: "flex",
3055
+ gap: 8,
3056
+ flexWrap: "wrap",
3057
+ marginTop: 4,
3058
+ marginBottom: 16
3059
+ },
3060
+ children: [
3061
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3062
+ type: "button",
3063
+ onClick: () => void save(),
3064
+ disabled: busy,
3065
+ style: primaryBtn,
3066
+ children: busy ? "保存中…" : isNew ? "创建同步节点" : "保存同步节点"
3067
+ }),
3068
+ !isNew && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3069
+ type: "button",
3070
+ onClick: () => void probe(),
3071
+ disabled: busy || probing,
3072
+ style: secondaryBtn,
3073
+ children: probing ? "测试中…" : "测试 SSH 握手"
3074
+ }),
3075
+ !isNew && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3076
+ type: "button",
3077
+ onClick: () => void runSync(),
3078
+ disabled: busy || probing || syncInProgress || !selectedPeer?.enabled,
3079
+ style: secondaryBtn,
3080
+ children: syncInProgress ? "同步中…" : "立即同步"
3081
+ }),
3082
+ !isNew && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3083
+ type: "button",
3084
+ onClick: () => void remove(),
3085
+ disabled: busy,
3086
+ style: dangerBtn,
3087
+ children: "删除"
3088
+ })
3089
+ ]
3090
+ }),
3091
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3092
+ style: {
3093
+ fontSize: 12,
3094
+ opacity: .45,
3095
+ marginBottom: 24
3096
+ },
3097
+ children: "节点与会话数据通过 SSH 执行通道传输;会话附件与 spill 对象随 session bundle 一并校验。"
3098
+ })
3099
+ ]
3100
+ }),
3101
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3102
+ style: {
3103
+ borderTop: "1px solid var(--dsw-alias-border-l2)",
3104
+ paddingTop: 16,
3105
+ maxWidth: 560
3106
+ },
3107
+ children: [
3108
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
3109
+ style: {
3110
+ margin: "0 0 6px",
3111
+ fontSize: 13,
3112
+ fontWeight: 600,
3113
+ opacity: .7
3114
+ },
3115
+ children: "会话冲突归档"
3116
+ }),
3117
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3118
+ style: {
3119
+ fontSize: 11,
3120
+ opacity: .55,
3121
+ lineHeight: 1.55,
3122
+ marginBottom: 12
3123
+ },
3124
+ children: "冲突的旧版本会保留在这里;恢复版本后,原当前版本也会进入归档。"
3125
+ }),
3126
+ archives.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3127
+ style: {
3128
+ fontSize: 12,
3129
+ opacity: .5
3130
+ },
3131
+ children: "暂无待恢复的会话分支"
3132
+ }) : archives.map((archive) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3133
+ style: {
3134
+ display: "flex",
3135
+ alignItems: "center",
3136
+ justifyContent: "space-between",
3137
+ gap: 12,
3138
+ padding: "10px 12px",
3139
+ border: "1px solid var(--dsw-alias-border-l2)",
3140
+ borderRadius: 8,
3141
+ marginBottom: 8
3142
+ },
3143
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3144
+ style: { minWidth: 0 },
3145
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3146
+ style: {
3147
+ fontSize: 13,
3148
+ display: "flex",
3149
+ gap: 8,
3150
+ alignItems: "baseline",
3151
+ flexWrap: "wrap"
3152
+ },
3153
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
3154
+ title: archive.originSessionId,
3155
+ children: shortenSessionId(archive.originSessionId)
3156
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3157
+ type: "button",
3158
+ title: "复制完整会话 ID",
3159
+ "aria-label": "复制完整会话 ID",
3160
+ onClick: () => copyText(archive.originSessionId),
3161
+ style: {
3162
+ ...secondaryBtn,
3163
+ height: 22,
3164
+ padding: "0 6px",
3165
+ fontSize: 10
3166
+ },
3167
+ children: "复制 ID"
3168
+ })]
3169
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3170
+ style: {
3171
+ fontSize: 11,
3172
+ opacity: .5,
3173
+ marginTop: 3
3174
+ },
3175
+ children: ["归档于 ", new Date(archive.archivedAt).toLocaleString()]
3176
+ })]
3177
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3178
+ type: "button",
3179
+ onClick: () => void restoreArchive(archive.branchId),
3180
+ disabled: busy,
3181
+ style: secondaryBtn,
3182
+ children: "恢复版本"
3183
+ })]
3184
+ }, archive.branchId))
3185
+ ]
3186
+ })
3187
+ ] });
3188
+ }
3189
+ //#endregion
3190
+ //#region src/client/sync-offline.tsx
3191
+ const fieldStyle = {
3192
+ width: "100%",
3193
+ boxSizing: "border-box",
3194
+ minHeight: 36,
3195
+ padding: "8px 10px",
3196
+ border: "1px solid var(--dsw-alias-border-l2)",
3197
+ borderRadius: 8,
3198
+ background: "var(--dsw-alias-bg-layer-1)",
3199
+ color: "var(--dsw-alias-label-primary)",
3200
+ font: "inherit"
3201
+ };
3202
+ const buttonStyle$1 = {
3203
+ minHeight: 36,
3204
+ padding: "0 14px",
3205
+ border: "1px solid var(--dsw-alias-border-l2)",
3206
+ borderRadius: 18,
3207
+ background: "transparent",
3208
+ color: "var(--dsw-alias-label-primary)",
3209
+ font: "inherit",
3210
+ cursor: "pointer"
3211
+ };
3212
+ function passwordError(value) {
3213
+ const bytes = new TextEncoder().encode(value).byteLength;
3214
+ if (bytes < 8 || bytes > 1024) return "包密码需为 8-1024 个 UTF-8 字节";
3215
+ if (value.includes("\0")) return "包密码包含无效字符";
3216
+ }
3217
+ async function responseError(response, fallback) {
3218
+ try {
3219
+ const data = await response.json();
3220
+ return new Error(data.error ?? fallback);
3221
+ } catch {
3222
+ return new Error(fallback);
3223
+ }
3224
+ }
3225
+ function SyncOfflineSection(props) {
3226
+ const [exportPassword, setExportPassword] = (0, react.useState)("");
3227
+ const [confirmPassword, setConfirmPassword] = (0, react.useState)("");
3228
+ const [includeSessions, setIncludeSessions] = (0, react.useState)(false);
3229
+ const [importPassword, setImportPassword] = (0, react.useState)("");
3230
+ const [file, setFile] = (0, react.useState)(void 0);
3231
+ const [fileInputKey, setFileInputKey] = (0, react.useState)(0);
3232
+ const [busy, setBusy] = (0, react.useState)(void 0);
3233
+ const [status, setStatus] = (0, react.useState)("");
3234
+ const [notice, setNotice] = (0, react.useState)(void 0);
3235
+ const exportArchive = async () => {
3236
+ const error = passwordError(exportPassword);
3237
+ if (error) {
3238
+ setNotice({
3239
+ kind: "error",
3240
+ text: error
3241
+ });
3242
+ return;
3243
+ }
3244
+ if (exportPassword !== confirmPassword) {
3245
+ setNotice({
3246
+ kind: "error",
3247
+ text: "两次输入的包密码不一致"
3248
+ });
3249
+ return;
3250
+ }
3251
+ setBusy("export");
3252
+ setStatus("正在生成并加密归档…");
3253
+ setNotice(void 0);
3254
+ try {
3255
+ const response = await fetch("/api/dshb/sync/offline/export", {
3256
+ method: "POST",
3257
+ headers: { "content-type": "application/json" },
3258
+ body: JSON.stringify({
3259
+ password: exportPassword,
3260
+ includeSessions
3261
+ })
3262
+ });
3263
+ if (!response.ok) throw await responseError(response, "导出离线配置失败");
3264
+ const blob = await response.blob();
3265
+ const url = URL.createObjectURL(blob);
3266
+ const anchor = document.createElement("a");
3267
+ anchor.href = url;
3268
+ anchor.download = `dshb-sync-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.dshb-sync`;
3269
+ document.body.append(anchor);
3270
+ anchor.click();
3271
+ anchor.remove();
3272
+ window.setTimeout(() => URL.revokeObjectURL(url), 1e3);
3273
+ setExportPassword("");
3274
+ setConfirmPassword("");
3275
+ setNotice({
3276
+ kind: "ok",
3277
+ text: "加密配置包已生成并下载"
3278
+ });
3279
+ } catch (caught) {
3280
+ setNotice({
3281
+ kind: "error",
3282
+ text: caught instanceof Error ? caught.message : "导出离线配置失败"
3283
+ });
3284
+ } finally {
3285
+ setBusy(void 0);
3286
+ setStatus("");
3287
+ setExportPassword("");
3288
+ setConfirmPassword("");
3289
+ }
3290
+ };
3291
+ const importArchive = async () => {
3292
+ const error = passwordError(importPassword);
3293
+ if (error) {
3294
+ setNotice({
3295
+ kind: "error",
3296
+ text: error
3297
+ });
3298
+ return;
3299
+ }
3300
+ if (!file) {
3301
+ setNotice({
3302
+ kind: "error",
3303
+ text: "请选择 .dshb-sync 文件"
3304
+ });
3305
+ return;
3306
+ }
3307
+ if (file.size > 134217728) {
3308
+ setNotice({
3309
+ kind: "error",
3310
+ text: "归档文件超过 128 MiB 上限"
3311
+ });
3312
+ return;
3313
+ }
3314
+ setBusy("import");
3315
+ setStatus("正在准备安全上传…");
3316
+ setNotice(void 0);
3317
+ try {
3318
+ const tokenResponse = await fetch("/api/dshb/sync/offline/import", {
3319
+ method: "POST",
3320
+ headers: { "content-type": "application/json" },
3321
+ body: JSON.stringify({ password: importPassword })
3322
+ });
3323
+ const tokenData = await tokenResponse.json();
3324
+ if (!tokenResponse.ok || !tokenData.ok || !tokenData.uploadToken) throw new Error(tokenData.error ?? "准备离线导入失败");
3325
+ setStatus("正在上传、解密并校验归档…");
3326
+ const response = await fetch(`/api/dshb/sync/offline/import/${tokenData.uploadToken}`, {
3327
+ method: "PUT",
3328
+ headers: { "content-type": "application/octet-stream" },
3329
+ body: file
3330
+ });
3331
+ if (!response.ok) throw await responseError(response, "导入离线配置失败");
3332
+ const data = await response.json();
3333
+ if (!data.ok || !data.result) throw new Error(data.error ?? "导入离线配置失败");
3334
+ const nodes = data.result.nodes;
3335
+ const sessions = data.result.sessions;
3336
+ setNotice({
3337
+ kind: "ok",
3338
+ text: `导入完成:节点新增 ${nodes?.created ?? 0}、更新 ${nodes?.updated ?? 0}、删除 ${nodes?.deleted ?? 0}${sessions ? `;会话新增 ${sessions.created}、更新 ${sessions.updated}、归档 ${sessions.archived}` : ""}`
3339
+ });
3340
+ setImportPassword("");
3341
+ setFile(void 0);
3342
+ setFileInputKey((key) => key + 1);
3343
+ props.onImported?.();
3344
+ } catch (caught) {
3345
+ setNotice({
3346
+ kind: "error",
3347
+ text: caught instanceof Error ? caught.message : "导入离线配置失败"
3348
+ });
3349
+ } finally {
3350
+ setBusy(void 0);
3351
+ setStatus("");
3352
+ setImportPassword("");
3353
+ }
3354
+ };
3355
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3356
+ style: {
3357
+ flex: 1,
3358
+ minWidth: 0,
3359
+ width: "100%",
3360
+ maxWidth: 900
3361
+ },
3362
+ children: [
3363
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: `.dshb-sync-offline-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,300px),1fr));gap:16px;align-items:start}.dshb-sync-offline-card{border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:16px;background:var(--dsw-alias-bg-layer-1);min-width:0}.dshb-sync-offline-hint{font-size:12px;line-height:1.5;opacity:.68;margin:0 0 12px}.dshb-sync-offline-actions{margin-top:2px}.dshb-sync-offline-actions button{max-width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dshb-sync-offline-file{max-width:100%;width:100%;box-sizing:border-box;font-size:12px;line-height:1.5}.dshb-sync-offline-file::file-selector-button{max-width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}@media(max-width:600px){.dshb-sync-offline-grid{grid-template-columns:1fr;gap:12px}.dshb-sync-offline-actions{display:flex}.dshb-sync-offline-actions button{width:100%}}` }),
3364
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3365
+ style: {
3366
+ marginBottom: 14,
3367
+ fontSize: 13,
3368
+ lineHeight: 1.55,
3369
+ opacity: .75,
3370
+ maxWidth: "72ch"
3371
+ },
3372
+ children: "通过加密文件在实例间迁移远程节点和可选会话,适用于网络隔离或备份恢复。"
3373
+ }),
3374
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3375
+ className: "dshb-sync-offline-grid",
3376
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
3377
+ className: "dshb-sync-offline-card",
3378
+ style: { borderTop: "3px solid var(--dsw-alias-button-primary-fill)" },
3379
+ children: [
3380
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
3381
+ style: {
3382
+ margin: "0 0 6px",
3383
+ fontSize: 16
3384
+ },
3385
+ children: "导出加密配置包"
3386
+ }),
3387
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3388
+ className: "dshb-sync-offline-hint",
3389
+ children: "默认导出远程节点配置与凭据;勾选后可包含可映射的工作区会话。同步 peer 凭据和本地节点不会进入导出包。"
3390
+ }),
3391
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3392
+ style: {
3393
+ display: "grid",
3394
+ gap: 10
3395
+ },
3396
+ children: [
3397
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3398
+ style: {
3399
+ display: "grid",
3400
+ gap: 4,
3401
+ fontSize: 13
3402
+ },
3403
+ children: ["包密码", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3404
+ style: fieldStyle,
3405
+ type: "password",
3406
+ autoComplete: "new-password",
3407
+ value: exportPassword,
3408
+ disabled: busy !== void 0,
3409
+ onChange: (event) => setExportPassword(event.target.value)
3410
+ })]
3411
+ }),
3412
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3413
+ style: {
3414
+ display: "grid",
3415
+ gap: 4,
3416
+ fontSize: 13
3417
+ },
3418
+ children: ["确认包密码", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3419
+ style: fieldStyle,
3420
+ type: "password",
3421
+ autoComplete: "new-password",
3422
+ value: confirmPassword,
3423
+ disabled: busy !== void 0,
3424
+ onChange: (event) => setConfirmPassword(event.target.value)
3425
+ })]
3426
+ }),
3427
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3428
+ style: {
3429
+ display: "flex",
3430
+ alignItems: "flex-start",
3431
+ gap: 8,
3432
+ fontSize: 13
3433
+ },
3434
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3435
+ type: "checkbox",
3436
+ checked: includeSessions,
3437
+ disabled: busy !== void 0,
3438
+ onChange: (event) => setIncludeSessions(event.target.checked)
3439
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: ["包含可映射的远程工作区会话", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3440
+ style: {
3441
+ display: "block",
3442
+ fontSize: 11,
3443
+ opacity: .65,
3444
+ marginTop: 3
3445
+ },
3446
+ children: "包含会话日志与附件,导出文件可能较大。"
3447
+ })] })]
3448
+ }),
3449
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3450
+ className: "dshb-sync-offline-actions",
3451
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3452
+ type: "button",
3453
+ style: buttonStyle$1,
3454
+ disabled: busy !== void 0,
3455
+ onClick: () => void exportArchive(),
3456
+ children: busy === "export" ? "正在导出…" : "导出 .dshb-sync"
3457
+ })
3458
+ })
3459
+ ]
3460
+ })
3461
+ ]
3462
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
3463
+ className: "dshb-sync-offline-card",
3464
+ style: { borderTop: "3px solid var(--dsw-alias-border-l2)" },
3465
+ children: [
3466
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
3467
+ style: {
3468
+ margin: "0 0 6px",
3469
+ fontSize: 16
3470
+ },
3471
+ children: "导入加密配置包"
3472
+ }),
3473
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3474
+ className: "dshb-sync-offline-hint",
3475
+ children: "先验证密码、格式、摘要、路径和空间,再应用配置。同一来源的重复导入会按身份去重。"
3476
+ }),
3477
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3478
+ style: {
3479
+ display: "grid",
3480
+ gap: 10
3481
+ },
3482
+ children: [
3483
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3484
+ style: {
3485
+ display: "grid",
3486
+ gap: 4,
3487
+ fontSize: 13
3488
+ },
3489
+ children: ["归档文件", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3490
+ className: "dshb-sync-offline-file",
3491
+ type: "file",
3492
+ accept: ".dshb-sync,application/octet-stream",
3493
+ disabled: busy !== void 0,
3494
+ onChange: (event) => setFile(event.target.files?.[0])
3495
+ }, fileInputKey)]
3496
+ }),
3497
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3498
+ style: {
3499
+ fontSize: 12,
3500
+ opacity: .7,
3501
+ minHeight: 18,
3502
+ wordBreak: "break-all"
3503
+ },
3504
+ children: file ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: file.name }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3505
+ style: { marginLeft: 6 },
3506
+ children: [(file.size / 1024 / 1024).toFixed(2), " MiB / 上限 128 MiB"]
3507
+ })] }) : "尚未选择文件,支持 .dshb-sync,最大 128 MiB"
3508
+ }),
3509
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3510
+ style: {
3511
+ display: "grid",
3512
+ gap: 4,
3513
+ fontSize: 13
3514
+ },
3515
+ children: ["包密码", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3516
+ style: fieldStyle,
3517
+ type: "password",
3518
+ autoComplete: "current-password",
3519
+ value: importPassword,
3520
+ disabled: busy !== void 0,
3521
+ onChange: (event) => setImportPassword(event.target.value)
3522
+ })]
3523
+ }),
3524
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3525
+ className: "dshb-sync-offline-actions",
3526
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3527
+ type: "button",
3528
+ style: buttonStyle$1,
3529
+ disabled: busy !== void 0 || !file,
3530
+ onClick: () => void importArchive(),
3531
+ children: busy === "import" ? "正在导入…" : "验证并导入"
3532
+ })
3533
+ })
3534
+ ]
3535
+ })
3536
+ ]
3537
+ })]
3538
+ }),
3539
+ status && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3540
+ role: "status",
3541
+ style: {
3542
+ marginTop: 14,
3543
+ padding: "10px 12px",
3544
+ border: "1px solid var(--dsw-alias-border-l2)",
3545
+ borderRadius: 8,
3546
+ fontSize: 13,
3547
+ opacity: .8
3548
+ },
3549
+ children: status
3550
+ }),
3551
+ notice && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3552
+ role: "status",
3553
+ style: {
3554
+ marginTop: 14,
3555
+ padding: "10px 12px",
3556
+ borderRadius: 8,
3557
+ fontSize: 13,
3558
+ background: notice.kind === "ok" ? "var(--dsw-alias-state-success-bg, rgba(34,197,94,.1))" : "var(--dsw-alias-state-error-bg, rgba(248,113,113,.1))",
3559
+ color: notice.kind === "ok" ? "var(--dsw-alias-state-success-primary)" : "var(--dsw-alias-state-error-primary)"
3560
+ },
3561
+ children: notice.text
3562
+ }),
3563
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3564
+ style: {
3565
+ marginTop: 14,
3566
+ fontSize: 11,
3567
+ lineHeight: 1.6,
3568
+ opacity: .55
3569
+ },
3570
+ children: "加密配置包包含敏感凭据,请妥善保存并仅在可信实例间传输。"
3571
+ })
3572
+ ]
3573
+ });
3574
+ }
3575
+ //#endregion
3576
+ //#region src/client/ui.tsx
3577
+ const SECTION_ID = "dshb-nodes";
3578
+ const EMPTY_FORM = {
3579
+ name: "",
3580
+ type: "remote-ssh",
3581
+ host: "",
3582
+ port: "22",
3583
+ username: "",
3584
+ authKind: "password",
3585
+ password: "",
3586
+ privateKey: "",
3587
+ passphrase: "",
3588
+ keyPath: "",
3589
+ dockerImage: "",
3590
+ dockerCpus: "",
3591
+ dockerMemory: ""
3592
+ };
3593
+ const inputStyle = {
3594
+ width: "100%",
3595
+ boxSizing: "border-box",
3596
+ padding: "0 10px",
3597
+ height: 32,
3598
+ border: "1px solid var(--dsw-alias-border-l2)",
3599
+ borderRadius: 8,
3600
+ fontSize: 14,
3601
+ fontFamily: "inherit",
3602
+ background: "var(--dsw-alias-bg-layer-1)",
3603
+ color: "var(--dsw-alias-label-primary)",
3604
+ outline: "none"
3605
+ };
3606
+ const buttonStyle = {
3607
+ boxSizing: "border-box",
3608
+ height: 36,
3609
+ padding: "0 14px",
3610
+ borderRadius: 18,
3611
+ fontSize: 14,
3612
+ lineHeight: "22px",
3613
+ fontFamily: "inherit",
3614
+ cursor: "pointer",
3615
+ border: "none",
3616
+ display: "inline-flex",
3617
+ justifyContent: "center",
3618
+ alignItems: "center",
3619
+ gap: 4
3620
+ };
3621
+ const primaryButtonStyle = {
3622
+ ...buttonStyle,
3623
+ background: "var(--dsw-alias-button-primary-fill)",
3624
+ color: "var(--dsw-alias-label-primary-foreground)"
3625
+ };
3626
+ const secondaryButtonStyle = {
3627
+ ...buttonStyle,
3628
+ border: "1px solid var(--dsw-alias-border-l2)",
3629
+ color: "var(--dsw-alias-label-primary)",
3630
+ background: "transparent"
3631
+ };
3632
+ const dangerOutlineButtonStyle = {
3633
+ ...buttonStyle,
3634
+ border: "1px solid var(--dsw-alias-state-error-primary)",
3635
+ color: "var(--dsw-alias-state-error-primary)",
3636
+ background: "transparent"
3637
+ };
3638
+ function useNodes(reloadKey) {
3639
+ const [nodes, setNodes] = (0, react.useState)([]);
3640
+ (0, react.useEffect)(() => {
3641
+ let cancelled = false;
3642
+ (async () => {
3643
+ try {
3644
+ const res = await fetch("/api/dshb/nodes");
3645
+ if (!res.ok) return;
3646
+ const data = await res.json();
3647
+ if (!cancelled) setNodes(data.nodes ?? []);
3648
+ } catch {}
3649
+ })();
3650
+ return () => {
3651
+ cancelled = true;
3652
+ };
3653
+ }, [reloadKey]);
3654
+ return nodes;
3655
+ }
3656
+ function NodeSection(_props) {
3657
+ const [reloadKey, setReloadKey] = (0, react.useState)(0);
3658
+ const [panel, setPanel] = (0, react.useState)("nodes");
3659
+ const nodes = useNodes(reloadKey);
3660
+ const [selectedId, setSelectedId] = (0, react.useState)("new");
3661
+ const [form, setForm] = (0, react.useState)(EMPTY_FORM);
3662
+ const [notice, setNotice] = (0, react.useState)(void 0);
3663
+ const [busy, setBusy] = (0, react.useState)(false);
3664
+ const [testing, setTesting] = (0, react.useState)(false);
3665
+ const [testResult, setTestResult] = (0, react.useState)(void 0);
3666
+ const [sshConfigEntries, setSshConfigEntries] = (0, react.useState)([]);
3667
+ const [dockerBusy, setDockerBusy] = (0, react.useState)(false);
3668
+ const flash = (0, react.useCallback)((n) => {
3669
+ setNotice(n);
3670
+ if (n) setTimeout(() => setNotice(void 0), 6e3);
3671
+ }, []);
3672
+ const reload = (0, react.useCallback)(() => setReloadKey((k) => k + 1), []);
3673
+ (0, react.useEffect)(() => {
3674
+ (async () => {
3675
+ try {
3676
+ const res = await fetch("/api/dshb/ssh-config");
3677
+ if (!res.ok) return;
3678
+ const data = await res.json();
3679
+ setSshConfigEntries(data.entries ?? []);
3680
+ } catch {}
3681
+ })();
3682
+ }, []);
3683
+ const selectNode = (0, react.useCallback)((id) => {
3684
+ setSelectedId(id);
3685
+ setTestResult(void 0);
3686
+ setTesting(false);
3687
+ setDockerBusy(false);
3688
+ if (id === "new") {
3689
+ setForm(EMPTY_FORM);
3690
+ return;
3691
+ }
3692
+ const node = nodes.find((n) => n.id === id);
3693
+ if (!node) return;
3694
+ setForm({
3695
+ name: node.name,
3696
+ type: node.type,
3697
+ host: node.ssh?.host ?? "",
3698
+ port: String(node.ssh?.port ?? 22),
3699
+ username: node.ssh?.username ?? "",
3700
+ authKind: node.ssh?.auth.kind ?? "password",
3701
+ password: "",
3702
+ privateKey: "",
3703
+ passphrase: "",
3704
+ keyPath: node.ssh?.auth.keyPath ?? "",
3705
+ dockerImage: node.docker?.image ?? "",
3706
+ dockerCpus: node.docker?.resources?.cpus != null ? String(node.docker?.resources?.cpus) : "",
3707
+ dockerMemory: node.docker?.resources?.memoryMB != null ? String(node.docker?.resources?.memoryMB) : ""
3708
+ });
3709
+ }, [nodes]);
3710
+ const importSshConfig = (0, react.useCallback)((entry) => {
3711
+ setForm((f) => ({
3712
+ ...f,
3713
+ type: "remote-ssh",
3714
+ host: entry.host,
3715
+ port: String(entry.port),
3716
+ username: entry.username,
3717
+ authKind: entry.hasIdentityFile ? "key" : f.authKind,
3718
+ keyPath: entry.identityFile ?? f.keyPath,
3719
+ name: f.name || entry.host
3720
+ }));
3721
+ setTestResult(void 0);
3722
+ flash({
3723
+ kind: "ok",
3724
+ text: `已从 ~/.ssh/config 导入 ${entry.host}`
3725
+ });
3726
+ }, [flash]);
3727
+ const save = (0, react.useCallback)(async () => {
3728
+ if (!form.name.trim()) {
3729
+ flash({
3730
+ kind: "error",
3731
+ text: "请填写节点名称"
3732
+ });
3733
+ return;
3734
+ }
3735
+ setBusy(true);
3736
+ try {
3737
+ const isRemote = form.type === "remote-ssh" || form.type === "remote-docker";
3738
+ const isDocker = form.type === "local-docker" || form.type === "remote-docker";
3739
+ const payload = {
3740
+ name: form.name.trim(),
3741
+ type: form.type,
3742
+ ssh: isRemote ? {
3743
+ host: form.host.trim(),
3744
+ port: Number(form.port) || 22,
3745
+ username: form.username.trim(),
3746
+ auth: {
3747
+ kind: form.authKind,
3748
+ ...form.keyPath.trim() ? { keyPath: form.keyPath.trim() } : {}
3749
+ }
3750
+ } : void 0,
3751
+ secrets: isRemote ? {
3752
+ ...form.password ? { password: form.password } : {},
3753
+ ...form.privateKey ? { privateKey: form.privateKey } : {},
3754
+ ...form.passphrase ? { passphrase: form.passphrase } : {}
3755
+ } : void 0,
3756
+ docker: isDocker ? {
3757
+ mode: "managed",
3758
+ ...form.dockerImage.trim() ? { image: form.dockerImage.trim() } : {},
3759
+ ...form.dockerCpus ? { resources: { cpus: Number(form.dockerCpus) || void 0 } } : {},
3760
+ ...form.dockerMemory ? { resources: { memoryMB: Number(form.dockerMemory) || void 0 } } : {},
3761
+ ...form.dockerCpus && form.dockerMemory ? { resources: {
3762
+ cpus: Number(form.dockerCpus) || void 0,
3763
+ memoryMB: Number(form.dockerMemory) || void 0
3764
+ } } : {}
3765
+ } : void 0
3766
+ };
3767
+ const url = selectedId === "new" ? "/api/dshb/nodes" : `/api/dshb/nodes/${selectedId}`;
3768
+ const res = await fetch(url, {
3769
+ method: selectedId === "new" ? "POST" : "PATCH",
3770
+ headers: { "content-type": "application/json" },
3771
+ body: JSON.stringify(payload)
3772
+ });
3773
+ const data = await res.json();
3774
+ if (res.ok && data.ok) {
3775
+ flash({
3776
+ kind: "ok",
3777
+ text: selectedId === "new" ? "节点已创建" : "节点已保存"
3778
+ });
3779
+ reload();
3780
+ if (data.node) setSelectedId(data.node.id);
3781
+ } else flash({
3782
+ kind: "error",
3783
+ text: data.error ?? "保存失败"
3784
+ });
3785
+ } catch {
3786
+ flash({
3787
+ kind: "error",
3788
+ text: "网络错误"
3789
+ });
3790
+ } finally {
3791
+ setBusy(false);
3792
+ }
3793
+ }, [
3794
+ form,
3795
+ selectedId,
3796
+ flash,
3797
+ reload
3798
+ ]);
3799
+ const remove = (0, react.useCallback)(async () => {
3800
+ if (selectedId === "new") return;
3801
+ setBusy(true);
3802
+ try {
3803
+ const res = await fetch(`/api/dshb/nodes/${selectedId}`, { method: "DELETE" });
3804
+ const data = await res.json();
3805
+ if (res.ok && data.ok) {
3806
+ flash({
3807
+ kind: "ok",
3808
+ text: "节点已删除"
3809
+ });
3810
+ setSelectedId("new");
3811
+ setForm(EMPTY_FORM);
3812
+ reload();
3813
+ } else flash({
3814
+ kind: "error",
3815
+ text: data.error ?? "删除失败"
3816
+ });
3817
+ } catch {
3818
+ flash({
3819
+ kind: "error",
3820
+ text: "删除失败"
3821
+ });
3822
+ } finally {
3823
+ setBusy(false);
3824
+ }
3825
+ }, [
3826
+ selectedId,
3827
+ flash,
3828
+ reload
3829
+ ]);
3830
+ const test = (0, react.useCallback)(async () => {
3831
+ if (form.type === "local-host") {
3832
+ flash({
3833
+ kind: "error",
3834
+ text: "本地节点无需测试连接"
3835
+ });
3836
+ return;
3837
+ }
3838
+ const isRemote = form.type === "remote-ssh" || form.type === "remote-docker";
3839
+ if (isRemote && !form.host.trim()) {
3840
+ flash({
3841
+ kind: "error",
3842
+ text: "请填写主机地址"
3843
+ });
3844
+ return;
3845
+ }
3846
+ setTesting(true);
3847
+ setTestResult(void 0);
3848
+ try {
2391
3849
  const payload = isRemote ? {
2392
3850
  ssh: {
2393
3851
  host: form.host.trim(),
@@ -2503,21 +3961,37 @@ window.__ModuleLoader__.load({
2503
3961
  width: 200,
2504
3962
  flexShrink: 0,
2505
3963
  borderRight: "1px solid var(--dsw-alias-border-l2)",
2506
- paddingRight: 16
3964
+ paddingRight: 16,
3965
+ display: "flex",
3966
+ flexDirection: "column",
3967
+ minHeight: 0
2507
3968
  },
2508
- children: [
2509
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2510
- type: "button",
2511
- onClick: () => selectNode("new"),
2512
- style: {
2513
- ...primaryButtonStyle,
2514
- width: "100%",
2515
- marginBottom: 10
3969
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3970
+ type: "button",
3971
+ onClick: () => {
3972
+ setPanel("nodes");
3973
+ selectNode("new");
3974
+ },
3975
+ style: {
3976
+ ...secondaryButtonStyle,
3977
+ width: "100%",
3978
+ marginBottom: 10,
3979
+ borderRadius: 8,
3980
+ background: panel !== "offline" ? "var(--dsw-alias-interactive-bg-active, var(--dsw-alias-interactive-bg-hover))" : "transparent"
3981
+ },
3982
+ children: "节点列表"
3983
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3984
+ style: {
3985
+ flex: "0 1 auto",
3986
+ minHeight: 0,
3987
+ maxHeight: "min(60vh, 480px)",
3988
+ overflowY: "auto"
3989
+ },
3990
+ children: [nodes.map((n) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3991
+ onClick: () => {
3992
+ setPanel("nodes");
3993
+ selectNode(n.id);
2516
3994
  },
2517
- children: "新建节点"
2518
- }),
2519
- nodes.map((n) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2520
- onClick: () => selectNode(n.id),
2521
3995
  style: {
2522
3996
  padding: "8px 10px",
2523
3997
  borderRadius: 6,
@@ -2527,9 +4001,22 @@ window.__ModuleLoader__.load({
2527
4001
  fontSize: 13
2528
4002
  },
2529
4003
  children: [
2530
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2531
- style: { fontWeight: 500 },
2532
- children: n.name
4004
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4005
+ style: {
4006
+ fontWeight: 500,
4007
+ display: "flex",
4008
+ alignItems: "center",
4009
+ gap: 4
4010
+ },
4011
+ children: [n.name, n.sync?.importedVia === "peer" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4012
+ "aria-label": `同步自 ${n.sync.sourceName ?? "peer"}`,
4013
+ title: `同步自 ${n.sync.sourceName ?? "peer"}`,
4014
+ style: {
4015
+ fontSize: 12,
4016
+ opacity: .7
4017
+ },
4018
+ children: "↗"
4019
+ })]
2533
4020
  }),
2534
4021
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2535
4022
  style: {
@@ -2563,419 +4050,522 @@ window.__ModuleLoader__.load({
2563
4050
  color: n.provision.state === "ready" ? "var(--dsw-alias-state-success-primary)" : n.provision.state === "failed" ? "var(--dsw-alias-state-error-primary)" : "var(--dsw-alias-state-warn-label)"
2564
4051
  },
2565
4052
  children: n.provision.state === "provisioning" ? "容器拉起中…" : n.provision.state === "ready" ? "容器已就绪" : `容器拉起失败:${n.provision.error ?? "未知错误"}`
4053
+ }),
4054
+ n.sync?.pendingRemoval && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4055
+ style: {
4056
+ fontSize: 11,
4057
+ marginTop: 2,
4058
+ color: "var(--dsw-alias-state-warn-label)"
4059
+ },
4060
+ children: "等待工作区解绑后删除"
4061
+ }),
4062
+ n.sync?.credentialsRequired && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4063
+ style: {
4064
+ fontSize: 11,
4065
+ marginTop: 2,
4066
+ color: "var(--dsw-alias-state-warn-label)"
4067
+ },
4068
+ children: "同步源未包含可迁移凭据"
2566
4069
  })
2567
4070
  ]
2568
- }, n.id)),
2569
- nodes.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4071
+ }, n.id)), nodes.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2570
4072
  style: {
2571
4073
  fontSize: 12,
2572
4074
  opacity: .6
2573
4075
  },
2574
4076
  children: "暂无节点"
2575
- })
2576
- ]
4077
+ })]
4078
+ })]
2577
4079
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2578
4080
  style: {
2579
4081
  flex: 1,
2580
- maxWidth: 480,
2581
4082
  minWidth: 0,
4083
+ display: "flex",
4084
+ flexDirection: "column",
2582
4085
  overflow: "hidden"
2583
4086
  },
2584
- children: [sshConfigEntries.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2585
- style: { marginBottom: 14 },
2586
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
2587
- style: {
2588
- fontSize: 12,
2589
- opacity: .75,
2590
- display: "block",
2591
- marginBottom: 4
2592
- },
2593
- children: "从 ~/.ssh/config 导入"
2594
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2595
- style: inputStyle,
2596
- value: "",
2597
- onChange: (e) => {
2598
- const entry = sshConfigEntries[Number(e.target.value)];
2599
- if (entry) importSshConfig(entry);
2600
- e.target.value = "";
2601
- },
2602
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2603
- value: "",
2604
- children: "选择主机…"
2605
- }), sshConfigEntries.map((entry, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
2606
- value: i,
2607
- children: [
2608
- entry.host,
2609
- ":",
2610
- entry.port,
2611
- entry.username ? ` (${entry.username})` : "",
2612
- entry.hasProxyJump ? " 跳板" : ""
2613
- ]
2614
- }, entry.host))]
2615
- })]
2616
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4087
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2617
4088
  style: {
2618
4089
  display: "flex",
2619
- flexDirection: "column",
2620
- gap: 12
4090
+ gap: 6,
4091
+ flexWrap: "wrap",
4092
+ paddingBottom: 8,
4093
+ marginBottom: 10,
4094
+ borderBottom: "1px solid var(--dsw-alias-border-l2)"
2621
4095
  },
2622
4096
  children: [
2623
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4097
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4098
+ type: "button",
4099
+ onClick: () => {
4100
+ setPanel("nodes");
4101
+ selectNode("new");
4102
+ },
2624
4103
  style: {
2625
- display: "flex",
2626
- flexDirection: "column",
2627
- gap: 4,
2628
- fontSize: 13
4104
+ ...panel === "nodes" ? primaryButtonStyle : secondaryButtonStyle,
4105
+ height: 32,
4106
+ padding: "0 16px",
4107
+ borderRadius: 8
2629
4108
  },
2630
- children: ["节点名称", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2631
- style: inputStyle,
2632
- value: form.name,
2633
- onChange: (e) => setForm((f) => ({
2634
- ...f,
2635
- name: e.target.value
2636
- })),
2637
- placeholder: "例如:构建机"
2638
- })]
4109
+ children: "新建"
2639
4110
  }),
2640
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4111
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4112
+ type: "button",
4113
+ onClick: () => setPanel("sync"),
2641
4114
  style: {
2642
- display: "flex",
2643
- flexDirection: "column",
2644
- gap: 4,
2645
- fontSize: 13
4115
+ ...panel === "sync" ? primaryButtonStyle : secondaryButtonStyle,
4116
+ height: 32,
4117
+ padding: "0 16px",
4118
+ borderRadius: 8
2646
4119
  },
2647
- children: ["节点类型", /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2648
- style: inputStyle,
2649
- value: form.type,
2650
- onChange: (e) => setForm((f) => ({
2651
- ...f,
2652
- type: e.target.value
2653
- })),
2654
- children: [
2655
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2656
- value: "remote-ssh",
2657
- children: "远程 SSH 宿主机"
2658
- }),
2659
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2660
- value: "remote-docker",
2661
- children: "远程 Docker(SSH 通道)"
2662
- }),
2663
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2664
- value: "local-host",
2665
- children: "默认环境(本地)"
2666
- }),
2667
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2668
- value: "local-docker",
2669
- children: "本地 Docker 容器"
2670
- })
2671
- ]
2672
- })]
4120
+ children: "同步"
2673
4121
  }),
2674
- (form.type === "remote-ssh" || form.type === "remote-docker") && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2675
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2676
- className: "dshb-form-row",
4122
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4123
+ type: "button",
4124
+ onClick: () => setPanel("offline"),
4125
+ style: {
4126
+ ...panel === "offline" ? primaryButtonStyle : secondaryButtonStyle,
4127
+ height: 32,
4128
+ padding: "0 12px",
4129
+ borderRadius: 8
4130
+ },
4131
+ children: "导入/导出"
4132
+ })
4133
+ ]
4134
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4135
+ style: {
4136
+ flex: 1,
4137
+ minWidth: 0,
4138
+ overflowY: "auto",
4139
+ overflowX: "hidden"
4140
+ },
4141
+ children: panel === "sync" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4142
+ role: "note",
4143
+ style: {
4144
+ padding: "10px 12px",
4145
+ marginBottom: 12,
4146
+ border: "1px solid var(--dsw-alias-border-l2)",
4147
+ borderRadius: 8,
4148
+ background: "var(--dsw-alias-bg-layer-1)"
4149
+ },
4150
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4151
+ style: {
4152
+ fontSize: 12,
4153
+ fontWeight: 600,
4154
+ marginBottom: 4
4155
+ },
4156
+ children: "节点与会话同步"
4157
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4158
+ style: {
4159
+ fontSize: 12,
4160
+ lineHeight: 1.45,
4161
+ opacity: .72
4162
+ },
4163
+ children: "通过 SSH 在 DSHB 实例间同步远端节点配置,支持单向拉取和双向同步;也可选择同步已映射工作区的会话与附件。首次连接需确认 SSH 主机指纹。"
4164
+ })]
4165
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SyncPeersSection, {})] }) : panel === "offline" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SyncOfflineSection, { onImported: reload }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4166
+ style: {
4167
+ flex: 1,
4168
+ maxWidth: 480,
4169
+ minWidth: 0,
4170
+ overflow: "hidden"
4171
+ },
4172
+ children: [sshConfigEntries.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4173
+ style: { marginBottom: 14 },
4174
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
2677
4175
  style: {
2678
- display: "flex",
2679
- gap: 10
4176
+ fontSize: 12,
4177
+ opacity: .75,
4178
+ display: "block",
4179
+ marginBottom: 4
4180
+ },
4181
+ children: "从 ~/.ssh/config 导入"
4182
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
4183
+ style: inputStyle,
4184
+ value: "",
4185
+ onChange: (e) => {
4186
+ const entry = sshConfigEntries[Number(e.target.value)];
4187
+ if (entry) importSshConfig(entry);
4188
+ e.target.value = "";
2680
4189
  },
2681
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4190
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4191
+ value: "",
4192
+ children: "选择主机…"
4193
+ }), sshConfigEntries.map((entry, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
4194
+ value: i,
4195
+ children: [
4196
+ entry.host,
4197
+ ":",
4198
+ entry.port,
4199
+ entry.username ? ` (${entry.username})` : "",
4200
+ entry.hasProxyJump ? " 跳板" : ""
4201
+ ]
4202
+ }, entry.host))]
4203
+ })]
4204
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4205
+ style: {
4206
+ display: "flex",
4207
+ flexDirection: "column",
4208
+ gap: 12
4209
+ },
4210
+ children: [
4211
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2682
4212
  style: {
2683
4213
  display: "flex",
2684
4214
  flexDirection: "column",
2685
4215
  gap: 4,
2686
- fontSize: 13,
2687
- flex: 1,
2688
- minWidth: 0
4216
+ fontSize: 13
2689
4217
  },
2690
- children: ["主机地址", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4218
+ children: ["节点名称", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2691
4219
  style: inputStyle,
2692
- value: form.host,
4220
+ value: form.name,
2693
4221
  onChange: (e) => setForm((f) => ({
2694
4222
  ...f,
2695
- host: e.target.value
4223
+ name: e.target.value
2696
4224
  })),
2697
- placeholder: "192.168.1.10"
4225
+ placeholder: "例如:构建机"
2698
4226
  })]
2699
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4227
+ }),
4228
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2700
4229
  style: {
2701
4230
  display: "flex",
2702
4231
  flexDirection: "column",
2703
4232
  gap: 4,
2704
- fontSize: 13,
2705
- width: 90
4233
+ fontSize: 13
2706
4234
  },
2707
- children: ["端口", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2708
- style: inputStyle,
2709
- value: form.port,
2710
- onChange: (e) => setForm((f) => ({
2711
- ...f,
2712
- port: e.target.value
2713
- }))
2714
- })]
2715
- })]
2716
- }),
2717
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2718
- style: {
2719
- display: "flex",
2720
- flexDirection: "column",
2721
- gap: 4,
2722
- fontSize: 13
2723
- },
2724
- children: ["用户名", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2725
- style: inputStyle,
2726
- value: form.username,
2727
- onChange: (e) => setForm((f) => ({
2728
- ...f,
2729
- username: e.target.value
2730
- })),
2731
- placeholder: "root"
2732
- })]
2733
- }),
2734
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2735
- style: {
2736
- display: "flex",
2737
- flexDirection: "column",
2738
- gap: 4,
2739
- fontSize: 13
2740
- },
2741
- children: ["认证方式", /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2742
- style: inputStyle,
2743
- value: form.authKind,
2744
- onChange: (e) => setForm((f) => ({
2745
- ...f,
2746
- authKind: e.target.value
2747
- })),
2748
- children: [
2749
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2750
- value: "password",
2751
- children: "密码"
2752
- }),
2753
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2754
- value: "key",
2755
- children: "私钥"
2756
- }),
2757
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2758
- value: "agent",
2759
- children: "ssh-agent"
2760
- })
2761
- ]
2762
- })]
2763
- }),
2764
- form.authKind === "password" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2765
- style: {
2766
- display: "flex",
2767
- flexDirection: "column",
2768
- gap: 4,
2769
- fontSize: 13
2770
- },
2771
- children: [
2772
- "密码",
2773
- selectedNode?.hasSecret.hasPassword ? "(已保存,留空保持不变)" : "",
2774
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4235
+ children: ["节点类型", /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2775
4236
  style: inputStyle,
2776
- type: "password",
2777
- value: form.password,
4237
+ value: form.type,
2778
4238
  onChange: (e) => setForm((f) => ({
2779
4239
  ...f,
2780
- password: e.target.value
4240
+ type: e.target.value
2781
4241
  })),
2782
- autoComplete: "new-password"
2783
- })
2784
- ]
2785
- }),
2786
- form.authKind === "key" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2787
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2788
- style: {
2789
- display: "flex",
2790
- flexDirection: "column",
2791
- gap: 4,
2792
- fontSize: 13
2793
- },
2794
- children: [
2795
- "私钥内容",
2796
- selectedNode?.hasSecret.hasKey ? "(已保存,留空保持不变)" : "",
2797
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
4242
+ children: [
4243
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4244
+ value: "remote-ssh",
4245
+ children: "远程 SSH 宿主机"
4246
+ }),
4247
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4248
+ value: "remote-docker",
4249
+ children: "远程 Docker(SSH 通道)"
4250
+ }),
4251
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4252
+ value: "local-host",
4253
+ children: "默认环境(本地)"
4254
+ }),
4255
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4256
+ value: "local-docker",
4257
+ children: "本地 Docker 容器"
4258
+ })
4259
+ ]
4260
+ })]
4261
+ }),
4262
+ (form.type === "remote-ssh" || form.type === "remote-docker") && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4263
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4264
+ className: "dshb-form-row",
4265
+ style: {
4266
+ display: "flex",
4267
+ gap: 10
4268
+ },
4269
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4270
+ style: {
4271
+ display: "flex",
4272
+ flexDirection: "column",
4273
+ gap: 4,
4274
+ fontSize: 13,
4275
+ flex: 1,
4276
+ minWidth: 0
4277
+ },
4278
+ children: ["主机地址", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4279
+ style: inputStyle,
4280
+ value: form.host,
4281
+ onChange: (e) => setForm((f) => ({
4282
+ ...f,
4283
+ host: e.target.value
4284
+ })),
4285
+ placeholder: "192.168.1.10"
4286
+ })]
4287
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2798
4288
  style: {
2799
- ...inputStyle,
2800
- minHeight: 90,
2801
- resize: "vertical"
4289
+ display: "flex",
4290
+ flexDirection: "column",
4291
+ gap: 4,
4292
+ fontSize: 13,
4293
+ width: 90
2802
4294
  },
2803
- value: form.privateKey,
4295
+ children: ["端口", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4296
+ style: inputStyle,
4297
+ value: form.port,
4298
+ onChange: (e) => setForm((f) => ({
4299
+ ...f,
4300
+ port: e.target.value
4301
+ }))
4302
+ })]
4303
+ })]
4304
+ }),
4305
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4306
+ style: {
4307
+ display: "flex",
4308
+ flexDirection: "column",
4309
+ gap: 4,
4310
+ fontSize: 13
4311
+ },
4312
+ children: ["用户名", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4313
+ style: inputStyle,
4314
+ value: form.username,
4315
+ onChange: (e) => setForm((f) => ({
4316
+ ...f,
4317
+ username: e.target.value
4318
+ })),
4319
+ placeholder: "root"
4320
+ })]
4321
+ }),
4322
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4323
+ style: {
4324
+ display: "flex",
4325
+ flexDirection: "column",
4326
+ gap: 4,
4327
+ fontSize: 13
4328
+ },
4329
+ children: ["认证方式", /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
4330
+ style: inputStyle,
4331
+ value: form.authKind,
2804
4332
  onChange: (e) => setForm((f) => ({
2805
4333
  ...f,
2806
- privateKey: e.target.value
4334
+ authKind: e.target.value
2807
4335
  })),
2808
- placeholder: "-----BEGIN OPENSSH PRIVATE KEY-----"
4336
+ children: [
4337
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4338
+ value: "password",
4339
+ children: "密码"
4340
+ }),
4341
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4342
+ value: "key",
4343
+ children: "私钥"
4344
+ }),
4345
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
4346
+ value: "agent",
4347
+ children: "ssh-agent"
4348
+ })
4349
+ ]
4350
+ })]
4351
+ }),
4352
+ form.authKind === "password" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4353
+ style: {
4354
+ display: "flex",
4355
+ flexDirection: "column",
4356
+ gap: 4,
4357
+ fontSize: 13
4358
+ },
4359
+ children: [
4360
+ "密码",
4361
+ selectedNode?.hasSecret.hasPassword ? "(已保存,留空保持不变)" : "",
4362
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4363
+ style: inputStyle,
4364
+ type: "password",
4365
+ value: form.password,
4366
+ onChange: (e) => setForm((f) => ({
4367
+ ...f,
4368
+ password: e.target.value
4369
+ })),
4370
+ autoComplete: "new-password"
4371
+ })
4372
+ ]
4373
+ }),
4374
+ form.authKind === "key" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4375
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4376
+ style: {
4377
+ display: "flex",
4378
+ flexDirection: "column",
4379
+ gap: 4,
4380
+ fontSize: 13
4381
+ },
4382
+ children: [
4383
+ "私钥内容",
4384
+ selectedNode?.hasSecret.hasKey ? "(已保存,留空保持不变)" : "",
4385
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
4386
+ style: {
4387
+ ...inputStyle,
4388
+ minHeight: 90,
4389
+ resize: "vertical"
4390
+ },
4391
+ value: form.privateKey,
4392
+ onChange: (e) => setForm((f) => ({
4393
+ ...f,
4394
+ privateKey: e.target.value
4395
+ })),
4396
+ placeholder: "-----BEGIN OPENSSH PRIVATE KEY-----"
4397
+ })
4398
+ ]
4399
+ }),
4400
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4401
+ style: {
4402
+ display: "flex",
4403
+ flexDirection: "column",
4404
+ gap: 4,
4405
+ fontSize: 13
4406
+ },
4407
+ children: ["私钥 passphrase(可选)", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4408
+ style: inputStyle,
4409
+ type: "password",
4410
+ value: form.passphrase,
4411
+ onChange: (e) => setForm((f) => ({
4412
+ ...f,
4413
+ passphrase: e.target.value
4414
+ })),
4415
+ autoComplete: "new-password"
4416
+ })]
4417
+ }),
4418
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4419
+ style: {
4420
+ display: "flex",
4421
+ flexDirection: "column",
4422
+ gap: 4,
4423
+ fontSize: 13
4424
+ },
4425
+ children: ["或私钥文件路径(可选,优先使用内容)", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4426
+ style: inputStyle,
4427
+ value: form.keyPath,
4428
+ onChange: (e) => setForm((f) => ({
4429
+ ...f,
4430
+ keyPath: e.target.value
4431
+ })),
4432
+ placeholder: "~/.ssh/id_ed25519"
4433
+ })]
2809
4434
  })
2810
- ]
2811
- }),
2812
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4435
+ ] })
4436
+ ] }),
4437
+ (form.type === "local-docker" || form.type === "remote-docker") && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2813
4438
  style: {
2814
4439
  display: "flex",
2815
4440
  flexDirection: "column",
2816
4441
  gap: 4,
2817
4442
  fontSize: 13
2818
4443
  },
2819
- children: ["私钥 passphrase(可选)", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4444
+ children: ["镜像地址", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2820
4445
  style: inputStyle,
2821
- type: "password",
2822
- value: form.passphrase,
4446
+ value: form.dockerImage,
2823
4447
  onChange: (e) => setForm((f) => ({
2824
4448
  ...f,
2825
- passphrase: e.target.value
4449
+ dockerImage: e.target.value
2826
4450
  })),
2827
- autoComplete: "new-password"
4451
+ placeholder: "alpine:latest"
2828
4452
  })]
2829
- }),
2830
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4453
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4454
+ className: "dshb-form-row",
2831
4455
  style: {
2832
4456
  display: "flex",
2833
- flexDirection: "column",
2834
- gap: 4,
2835
- fontSize: 13
4457
+ gap: 10
2836
4458
  },
2837
- children: ["或私钥文件路径(可选,优先使用内容)", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2838
- style: inputStyle,
2839
- value: form.keyPath,
2840
- onChange: (e) => setForm((f) => ({
2841
- ...f,
2842
- keyPath: e.target.value
2843
- })),
2844
- placeholder: "~/.ssh/id_ed25519"
4459
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4460
+ style: {
4461
+ display: "flex",
4462
+ flexDirection: "column",
4463
+ gap: 4,
4464
+ fontSize: 13,
4465
+ flex: 1,
4466
+ minWidth: 0
4467
+ },
4468
+ children: ["CPU 核数(可选)", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4469
+ style: inputStyle,
4470
+ value: form.dockerCpus,
4471
+ onChange: (e) => setForm((f) => ({
4472
+ ...f,
4473
+ dockerCpus: e.target.value
4474
+ })),
4475
+ placeholder: "2"
4476
+ })]
4477
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4478
+ style: {
4479
+ display: "flex",
4480
+ flexDirection: "column",
4481
+ gap: 4,
4482
+ fontSize: 13,
4483
+ flex: 1,
4484
+ minWidth: 0
4485
+ },
4486
+ children: ["内存 MB(可选)", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4487
+ style: inputStyle,
4488
+ value: form.dockerMemory,
4489
+ onChange: (e) => setForm((f) => ({
4490
+ ...f,
4491
+ dockerMemory: e.target.value
4492
+ })),
4493
+ placeholder: "512"
4494
+ })]
2845
4495
  })]
2846
- })
2847
- ] })
2848
- ] }),
2849
- (form.type === "local-docker" || form.type === "remote-docker") && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2850
- style: {
2851
- display: "flex",
2852
- flexDirection: "column",
2853
- gap: 4,
2854
- fontSize: 13
2855
- },
2856
- children: ["镜像地址", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2857
- style: inputStyle,
2858
- value: form.dockerImage,
2859
- onChange: (e) => setForm((f) => ({
2860
- ...f,
2861
- dockerImage: e.target.value
2862
- })),
2863
- placeholder: "alpine:latest"
2864
- })]
2865
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2866
- className: "dshb-form-row",
2867
- style: {
2868
- display: "flex",
2869
- gap: 10
2870
- },
2871
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2872
- style: {
2873
- display: "flex",
2874
- flexDirection: "column",
2875
- gap: 4,
2876
- fontSize: 13,
2877
- flex: 1,
2878
- minWidth: 0
2879
- },
2880
- children: ["CPU 核数(可选)", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2881
- style: inputStyle,
2882
- value: form.dockerCpus,
2883
- onChange: (e) => setForm((f) => ({
2884
- ...f,
2885
- dockerCpus: e.target.value
2886
- })),
2887
- placeholder: "2"
2888
- })]
2889
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2890
- style: {
2891
- display: "flex",
2892
- flexDirection: "column",
2893
- gap: 4,
2894
- fontSize: 13,
2895
- flex: 1,
2896
- minWidth: 0
2897
- },
2898
- children: ["内存 MB(可选)", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2899
- style: inputStyle,
2900
- value: form.dockerMemory,
2901
- onChange: (e) => setForm((f) => ({
2902
- ...f,
2903
- dockerMemory: e.target.value
2904
- })),
2905
- placeholder: "512"
2906
- })]
2907
- })]
2908
- })] }),
2909
- testResult !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2910
- style: {
2911
- fontSize: 13,
2912
- color: testResult === "连接成功" ? "var(--dsw-alias-state-success-primary)" : "var(--dsw-alias-state-error-primary)"
2913
- },
2914
- children: testResult
2915
- }),
2916
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2917
- style: {
2918
- display: "flex",
2919
- gap: 10,
2920
- marginTop: 6,
2921
- flexWrap: "wrap"
2922
- },
2923
- children: [
2924
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2925
- type: "button",
2926
- onClick: () => void save(),
2927
- disabled: busy,
2928
- style: primaryButtonStyle,
2929
- children: selectedId === "new" ? "创建节点" : "保存"
2930
- }),
2931
- (form.type === "remote-ssh" || form.type === "remote-docker" || selectedId !== "new" && form.type === "local-docker") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2932
- type: "button",
2933
- onClick: () => void test(),
2934
- disabled: busy || testing,
2935
- style: secondaryButtonStyle,
2936
- children: testing ? "测试中…" : "测试连接"
2937
- }),
2938
- selectedId !== "new" && (form.type === "local-docker" || form.type === "remote-docker") && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2939
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2940
- type: "button",
2941
- onClick: () => void dockerAction("provision"),
2942
- disabled: busy || dockerBusy,
2943
- style: primaryButtonStyle,
2944
- children: dockerBusy ? "操作中…" : "拉起容器"
2945
- }),
2946
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2947
- type: "button",
2948
- onClick: () => void dockerAction("restart"),
2949
- disabled: busy || dockerBusy,
2950
- style: secondaryButtonStyle,
2951
- children: dockerBusy ? "操作中…" : "重启容器"
2952
- }),
2953
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2954
- type: "button",
2955
- onClick: () => void dockerAction("stop"),
2956
- disabled: busy || dockerBusy,
2957
- style: secondaryButtonStyle,
2958
- children: dockerBusy ? "操作中…" : "停止容器"
2959
- })
2960
- ] }),
2961
- selectedId !== "new" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2962
- type: "button",
2963
- onClick: () => void remove(),
2964
- disabled: busy,
2965
- style: dangerOutlineButtonStyle,
2966
- children: "删除"
2967
- }),
2968
- notice && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4496
+ })] }),
4497
+ testResult !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2969
4498
  style: {
2970
4499
  fontSize: 13,
2971
- alignSelf: "center",
2972
- color: notice.kind === "ok" ? "var(--dsw-alias-state-success-primary)" : "var(--dsw-alias-state-error-primary)"
4500
+ color: testResult === "连接成功" ? "var(--dsw-alias-state-success-primary)" : "var(--dsw-alias-state-error-primary)"
2973
4501
  },
2974
- children: notice.text
4502
+ children: testResult
4503
+ }),
4504
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4505
+ style: {
4506
+ display: "flex",
4507
+ gap: 10,
4508
+ marginTop: 6,
4509
+ flexWrap: "wrap"
4510
+ },
4511
+ children: [
4512
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4513
+ type: "button",
4514
+ onClick: () => void save(),
4515
+ disabled: busy,
4516
+ style: primaryButtonStyle,
4517
+ children: selectedId === "new" ? "创建节点" : "保存"
4518
+ }),
4519
+ (form.type === "remote-ssh" || form.type === "remote-docker" || selectedId !== "new" && form.type === "local-docker") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4520
+ type: "button",
4521
+ onClick: () => void test(),
4522
+ disabled: busy || testing,
4523
+ style: secondaryButtonStyle,
4524
+ children: testing ? "测试中…" : "测试连接"
4525
+ }),
4526
+ selectedId !== "new" && (form.type === "local-docker" || form.type === "remote-docker") && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4527
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4528
+ type: "button",
4529
+ onClick: () => void dockerAction("provision"),
4530
+ disabled: busy || dockerBusy,
4531
+ style: primaryButtonStyle,
4532
+ children: dockerBusy ? "操作中…" : "拉起容器"
4533
+ }),
4534
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4535
+ type: "button",
4536
+ onClick: () => void dockerAction("restart"),
4537
+ disabled: busy || dockerBusy,
4538
+ style: secondaryButtonStyle,
4539
+ children: dockerBusy ? "操作中…" : "重启容器"
4540
+ }),
4541
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4542
+ type: "button",
4543
+ onClick: () => void dockerAction("stop"),
4544
+ disabled: busy || dockerBusy,
4545
+ style: secondaryButtonStyle,
4546
+ children: dockerBusy ? "操作中…" : "停止容器"
4547
+ })
4548
+ ] }),
4549
+ selectedId !== "new" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4550
+ type: "button",
4551
+ onClick: () => void remove(),
4552
+ disabled: busy,
4553
+ style: dangerOutlineButtonStyle,
4554
+ children: "删除"
4555
+ }),
4556
+ notice && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4557
+ style: {
4558
+ fontSize: 13,
4559
+ alignSelf: "center",
4560
+ color: notice.kind === "ok" ? "var(--dsw-alias-state-success-primary)" : "var(--dsw-alias-state-error-primary)"
4561
+ },
4562
+ children: notice.text
4563
+ })
4564
+ ]
2975
4565
  })
2976
4566
  ]
2977
- })
2978
- ]
4567
+ })]
4568
+ })
2979
4569
  })]
2980
4570
  })]
2981
4571
  })] });