@artenx/dshb 0.0.3 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/cordis.patch.yml +8 -49
  2. package/lib/auth/credentials.d.ts +30 -0
  3. package/lib/auth/credentials.js +121 -0
  4. package/lib/auth/credentials.js.map +1 -0
  5. package/lib/auth/decorate-webserver.d.ts +7 -0
  6. package/lib/auth/decorate-webserver.js +75 -0
  7. package/lib/auth/decorate-webserver.js.map +1 -0
  8. package/lib/auth/gate.d.ts +14 -0
  9. package/lib/auth/gate.js +45 -0
  10. package/lib/auth/gate.js.map +1 -0
  11. package/lib/auth/index.d.ts +8 -0
  12. package/lib/auth/index.js +51 -0
  13. package/lib/auth/index.js.map +1 -0
  14. package/lib/auth/login-page.d.ts +5 -0
  15. package/lib/auth/login-page.js +87 -0
  16. package/lib/auth/login-page.js.map +1 -0
  17. package/lib/auth/loopback-compat.d.ts +6 -0
  18. package/lib/auth/loopback-compat.js +94 -0
  19. package/lib/auth/loopback-compat.js.map +1 -0
  20. package/lib/auth/loopback.d.ts +7 -0
  21. package/lib/auth/loopback.js +24 -0
  22. package/lib/auth/loopback.js.map +1 -0
  23. package/lib/auth/ratelimit.d.ts +11 -0
  24. package/lib/auth/ratelimit.js +37 -0
  25. package/lib/auth/ratelimit.js.map +1 -0
  26. package/lib/auth/routes.d.ts +8 -0
  27. package/lib/auth/routes.js +224 -0
  28. package/lib/auth/routes.js.map +1 -0
  29. package/lib/auth/session.d.ts +11 -0
  30. package/lib/auth/session.js +52 -0
  31. package/lib/auth/session.js.map +1 -0
  32. package/lib/auth/startup.d.ts +15 -0
  33. package/lib/auth/startup.js +68 -0
  34. package/lib/auth/startup.js.map +1 -0
  35. package/lib/auth/webserver.d.ts +18 -0
  36. package/lib/auth/webserver.js +60 -0
  37. package/lib/auth/webserver.js.map +1 -0
  38. package/lib/client.js +2770 -0
  39. package/lib/client.js.map +1 -0
  40. package/lib/connection-sIX25pkS.d.ts +46 -0
  41. package/lib/core/audit.d.ts +16 -0
  42. package/lib/core/audit.js +38 -0
  43. package/lib/core/audit.js.map +1 -0
  44. package/lib/core/index.d.ts +8 -0
  45. package/lib/core/index.js +77 -0
  46. package/lib/core/index.js.map +1 -0
  47. package/lib/core/known-hosts.d.ts +23 -0
  48. package/lib/core/known-hosts.js +76 -0
  49. package/lib/core/known-hosts.js.map +1 -0
  50. package/lib/core/node-registry.d.ts +108 -0
  51. package/lib/core/node-registry.js +191 -0
  52. package/lib/core/node-registry.js.map +1 -0
  53. package/lib/core/routes.d.ts +9 -0
  54. package/lib/core/routes.js +404 -0
  55. package/lib/core/routes.js.map +1 -0
  56. package/lib/core/ssh-config.d.ts +23 -0
  57. package/lib/core/ssh-config.js +70 -0
  58. package/lib/core/ssh-config.js.map +1 -0
  59. package/lib/core/test.d.ts +43 -0
  60. package/lib/core/test.js +116 -0
  61. package/lib/core/test.js.map +1 -0
  62. package/lib/core/workspace-bindings.d.ts +28 -0
  63. package/lib/core/workspace-bindings.js +86 -0
  64. package/lib/core/workspace-bindings.js.map +1 -0
  65. package/lib/core/workspaces.d.ts +9 -0
  66. package/lib/core/workspaces.js +283 -0
  67. package/lib/core/workspaces.js.map +1 -0
  68. package/lib/exec-docker/daemon-runner.d.ts +21 -0
  69. package/lib/exec-docker/daemon-runner.js +326 -0
  70. package/lib/exec-docker/daemon-runner.js.map +1 -0
  71. package/lib/exec-docker/docker-backend.d.ts +48 -0
  72. package/lib/exec-docker/docker-backend.js +1 -0
  73. package/lib/exec-docker/docker-client.d.ts +46 -0
  74. package/lib/exec-docker/docker-client.js +302 -0
  75. package/lib/exec-docker/docker-client.js.map +1 -0
  76. package/lib/exec-docker/docker-routes.d.ts +6 -0
  77. package/lib/exec-docker/docker-routes.js +215 -0
  78. package/lib/exec-docker/docker-routes.js.map +1 -0
  79. package/lib/exec-docker/fs-provider.d.ts +50 -0
  80. package/lib/exec-docker/fs-provider.js +92 -0
  81. package/lib/exec-docker/fs-provider.js.map +1 -0
  82. package/lib/exec-docker/host-runner.d.ts +29 -0
  83. package/lib/exec-docker/host-runner.js +17 -0
  84. package/lib/exec-docker/host-runner.js.map +1 -0
  85. package/lib/exec-docker/index.d.ts +21 -0
  86. package/lib/exec-docker/index.js +137 -0
  87. package/lib/exec-docker/index.js.map +1 -0
  88. package/lib/exec-docker/process-provider.d.ts +20 -0
  89. package/lib/exec-docker/process-provider.js +75 -0
  90. package/lib/exec-docker/process-provider.js.map +1 -0
  91. package/lib/exec-docker/provision.d.ts +24 -0
  92. package/lib/exec-docker/provision.js +103 -0
  93. package/lib/exec-docker/provision.js.map +1 -0
  94. package/lib/exec-docker/remote-docker-cli.d.ts +52 -0
  95. package/lib/exec-docker/remote-docker-cli.js +155 -0
  96. package/lib/exec-docker/remote-docker-cli.js.map +1 -0
  97. package/lib/exec-docker/world.d.ts +37 -0
  98. package/lib/exec-docker/world.js +62 -0
  99. package/lib/exec-docker/world.js.map +1 -0
  100. package/lib/exec-ssh/connection-pool.d.ts +25 -0
  101. package/lib/exec-ssh/connection-pool.js +54 -0
  102. package/lib/exec-ssh/connection-pool.js.map +1 -0
  103. package/lib/exec-ssh/connection.d.ts +2 -0
  104. package/lib/exec-ssh/connection.js +125 -0
  105. package/lib/exec-ssh/connection.js.map +1 -0
  106. package/lib/exec-ssh/env-scrub.d.ts +7 -0
  107. package/lib/exec-ssh/env-scrub.js +40 -0
  108. package/lib/exec-ssh/env-scrub.js.map +1 -0
  109. package/lib/exec-ssh/executor.d.ts +46 -0
  110. package/lib/exec-ssh/executor.js +212 -0
  111. package/lib/exec-ssh/executor.js.map +1 -0
  112. package/lib/exec-ssh/fs-provider.d.ts +50 -0
  113. package/lib/exec-ssh/fs-provider.js +92 -0
  114. package/lib/exec-ssh/fs-provider.js.map +1 -0
  115. package/lib/exec-ssh/index.d.ts +16 -0
  116. package/lib/exec-ssh/index.js +30 -0
  117. package/lib/exec-ssh/index.js.map +1 -0
  118. package/lib/exec-ssh/process-provider.d.ts +20 -0
  119. package/lib/exec-ssh/process-provider.js +75 -0
  120. package/lib/exec-ssh/process-provider.js.map +1 -0
  121. package/lib/exec-ssh/runner.d.ts +13 -0
  122. package/lib/exec-ssh/runner.js +62 -0
  123. package/lib/exec-ssh/runner.js.map +1 -0
  124. package/lib/exec-ssh/world.d.ts +79 -0
  125. package/lib/exec-ssh/world.js +95 -0
  126. package/lib/exec-ssh/world.js.map +1 -0
  127. package/lib/index.d.ts +2 -0
  128. package/lib/index.js +2 -0
  129. package/lib/router/decorate-fs.d.ts +6 -0
  130. package/lib/router/decorate-fs.js +78 -0
  131. package/lib/router/decorate-fs.js.map +1 -0
  132. package/lib/router/decorate-shell.d.ts +6 -0
  133. package/lib/router/decorate-shell.js +35 -0
  134. package/lib/router/decorate-shell.js.map +1 -0
  135. package/lib/router/decorate-subprocess.d.ts +6 -0
  136. package/lib/router/decorate-subprocess.js +35 -0
  137. package/lib/router/decorate-subprocess.js.map +1 -0
  138. package/lib/router/index.d.ts +10 -0
  139. package/lib/router/index.js +21 -0
  140. package/lib/router/index.js.map +1 -0
  141. package/lib/router/resolve.d.ts +22 -0
  142. package/lib/router/resolve.js +45 -0
  143. package/lib/router/resolve.js.map +1 -0
  144. package/lib/router/router-fs.d.ts +25 -0
  145. package/lib/router/router-fs.js +64 -0
  146. package/lib/router/router-fs.js.map +1 -0
  147. package/lib/router/router-shell.d.ts +14 -0
  148. package/lib/router/router-shell.js +36 -0
  149. package/lib/router/router-shell.js.map +1 -0
  150. package/lib/router/router-subprocess.d.ts +13 -0
  151. package/lib/router/router-subprocess.js +33 -0
  152. package/lib/router/router-subprocess.js.map +1 -0
  153. package/lib/router/types.d.ts +2 -0
  154. package/lib/router/types.js +1 -0
  155. package/lib/types-C0TtEO7z.d.ts +67 -0
  156. package/lib/ui/decorate-picker.d.ts +6 -0
  157. package/lib/ui/decorate-picker.js +58 -0
  158. package/lib/ui/decorate-picker.js.map +1 -0
  159. package/lib/ui/index.d.ts +8 -0
  160. package/lib/ui/index.js +11 -0
  161. package/lib/ui/index.js.map +1 -0
  162. package/lib/ui/picker.d.ts +10 -0
  163. package/lib/ui/picker.js +58 -0
  164. package/lib/ui/picker.js.map +1 -0
  165. package/package.json +158 -9
  166. package/LICENSE +0 -21
package/lib/client.js ADDED
@@ -0,0 +1,2770 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@artenx/dshb",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react = require("react");
8
+ let react_jsx_runtime = require("react/jsx-runtime");
9
+ //#region src/client/auth.tsx
10
+ const SECTION_ID$1 = "auth";
11
+ const MESSAGE_MS = 5e3;
12
+ function useUsername() {
13
+ const [username, setUsername] = (0, react.useState)(void 0);
14
+ (0, react.useEffect)(() => {
15
+ let cancelled = false;
16
+ (async () => {
17
+ try {
18
+ const res = await fetch("/api/auth/status");
19
+ if (!res.ok) return;
20
+ const data = await res.json();
21
+ if (!cancelled && typeof data.username === "string") setUsername(data.username);
22
+ } catch {}
23
+ })();
24
+ return () => {
25
+ cancelled = true;
26
+ };
27
+ }, []);
28
+ return [username, setUsername];
29
+ }
30
+ function AuthSection(_props) {
31
+ const [username, setUsername] = useUsername();
32
+ const [newUsername, setNewUsername] = (0, react.useState)("");
33
+ const [usernamePassword, setUsernamePassword] = (0, react.useState)("");
34
+ const [oldPassword, setOldPassword] = (0, react.useState)("");
35
+ const [newPassword, setNewPassword] = (0, react.useState)("");
36
+ const [confirm, setConfirm] = (0, react.useState)("");
37
+ const [busy, setBusy] = (0, react.useState)(false);
38
+ const [notice, setNotice] = (0, react.useState)(void 0);
39
+ const [confirmingSignOut, setConfirmingSignOut] = (0, react.useState)(false);
40
+ const flash = (0, react.useCallback)((n) => {
41
+ setNotice(n);
42
+ if (n !== void 0) setTimeout(() => setNotice(void 0), MESSAGE_MS);
43
+ }, []);
44
+ const signOut = (0, react.useCallback)(async () => {
45
+ setBusy(true);
46
+ try {
47
+ await fetch("/api/auth/logout", { method: "POST" });
48
+ window.location.href = "/login";
49
+ } catch {
50
+ setBusy(false);
51
+ setConfirmingSignOut(false);
52
+ flash({
53
+ kind: "error",
54
+ text: "退出失败,请重试",
55
+ owner: "account"
56
+ });
57
+ }
58
+ }, [flash]);
59
+ const changePassword = (0, react.useCallback)(async () => {
60
+ if (newPassword !== confirm) {
61
+ flash({
62
+ kind: "error",
63
+ text: "两次输入的新密码不一致",
64
+ owner: "password"
65
+ });
66
+ return;
67
+ }
68
+ setBusy(true);
69
+ try {
70
+ const res = await fetch("/api/auth/change-password", {
71
+ method: "POST",
72
+ headers: { "content-type": "application/json" },
73
+ body: JSON.stringify({
74
+ currentPassword: oldPassword,
75
+ password: newPassword
76
+ })
77
+ });
78
+ const data = await res.json();
79
+ if (res.ok) {
80
+ setOldPassword("");
81
+ setNewPassword("");
82
+ setConfirm("");
83
+ flash({
84
+ kind: "ok",
85
+ text: "密码已修改",
86
+ owner: "password"
87
+ });
88
+ } else flash({
89
+ kind: "error",
90
+ text: data.error ?? "修改失败,请重试",
91
+ owner: "password"
92
+ });
93
+ } catch {
94
+ flash({
95
+ kind: "error",
96
+ text: "修改失败,请重试",
97
+ owner: "password"
98
+ });
99
+ } finally {
100
+ setBusy(false);
101
+ }
102
+ }, [
103
+ oldPassword,
104
+ newPassword,
105
+ confirm,
106
+ flash
107
+ ]);
108
+ const changeUsername = (0, react.useCallback)(async () => {
109
+ setBusy(true);
110
+ try {
111
+ const res = await fetch("/api/auth/change-username", {
112
+ method: "POST",
113
+ headers: { "content-type": "application/json" },
114
+ body: JSON.stringify({
115
+ username: newUsername,
116
+ currentPassword: usernamePassword
117
+ })
118
+ });
119
+ const data = await res.json();
120
+ if (res.ok) {
121
+ setNewUsername("");
122
+ setUsernamePassword("");
123
+ if (newUsername) setUsername(newUsername);
124
+ flash({
125
+ kind: "ok",
126
+ text: "用户名已更新",
127
+ owner: "username"
128
+ });
129
+ } else flash({
130
+ kind: "error",
131
+ text: data.error ?? "修改失败,请重试",
132
+ owner: "username"
133
+ });
134
+ } catch {
135
+ flash({
136
+ kind: "error",
137
+ text: "修改失败,请重试",
138
+ owner: "username"
139
+ });
140
+ } finally {
141
+ setBusy(false);
142
+ }
143
+ }, [
144
+ newUsername,
145
+ usernamePassword,
146
+ flash,
147
+ setUsername
148
+ ]);
149
+ const inputStyle = {
150
+ width: "100%",
151
+ boxSizing: "border-box",
152
+ padding: "0 10px",
153
+ height: 32,
154
+ border: "1px solid var(--dsw-alias-border-l2)",
155
+ borderRadius: 8,
156
+ fontSize: 14,
157
+ fontFamily: "inherit",
158
+ background: "var(--dsw-alias-bg-layer-1)",
159
+ color: "var(--dsw-alias-label-primary)",
160
+ outline: "none"
161
+ };
162
+ const buttonStyle = {
163
+ boxSizing: "border-box",
164
+ height: 36,
165
+ padding: "0 14px",
166
+ borderRadius: 18,
167
+ fontSize: 14,
168
+ lineHeight: "22px",
169
+ fontFamily: "inherit",
170
+ cursor: "pointer",
171
+ border: "none",
172
+ display: "inline-flex",
173
+ justifyContent: "center",
174
+ alignItems: "center",
175
+ gap: 4
176
+ };
177
+ const primaryButtonStyle = {
178
+ ...buttonStyle,
179
+ background: "var(--dsw-alias-button-primary-fill)",
180
+ color: "var(--dsw-alias-label-primary-foreground)"
181
+ };
182
+ const secondaryButtonStyle = {
183
+ ...buttonStyle,
184
+ border: "1px solid var(--dsw-alias-border-l2)",
185
+ color: "var(--dsw-alias-label-primary)",
186
+ background: "transparent"
187
+ };
188
+ ({ ...buttonStyle });
189
+ const dangerOutlineButtonStyle = {
190
+ ...buttonStyle,
191
+ border: "1px solid var(--dsw-alias-state-error-primary)",
192
+ color: "var(--dsw-alias-state-error-primary)",
193
+ background: "transparent"
194
+ };
195
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
196
+ style: {
197
+ display: "flex",
198
+ flexDirection: "column",
199
+ gap: 24,
200
+ maxWidth: 420
201
+ },
202
+ children: [
203
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", { children: [
204
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
205
+ style: {
206
+ fontSize: 16,
207
+ fontWeight: 600,
208
+ margin: "0 0 4px"
209
+ },
210
+ children: "账号"
211
+ }),
212
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
213
+ style: {
214
+ fontSize: 13,
215
+ opacity: .7,
216
+ margin: "0 0 12px"
217
+ },
218
+ children: username !== void 0 ? `当前登录:${username}` : "当前登录:管理员"
219
+ }),
220
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
221
+ type: "button",
222
+ onClick: () => setConfirmingSignOut(true),
223
+ disabled: busy,
224
+ style: dangerOutlineButtonStyle,
225
+ children: "退出登录"
226
+ }),
227
+ confirmingSignOut && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
228
+ role: "alertdialog",
229
+ "aria-label": "确认退出登录",
230
+ style: {
231
+ marginTop: 10,
232
+ padding: "12px 14px",
233
+ border: "1px solid var(--dsw-alias-border-l2)",
234
+ borderRadius: 8,
235
+ maxWidth: 320
236
+ },
237
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
238
+ style: {
239
+ fontSize: 13,
240
+ marginBottom: 10,
241
+ color: "var(--dsw-alias-label-primary)"
242
+ },
243
+ children: "退出登录将回到登录页"
244
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
245
+ style: {
246
+ display: "flex",
247
+ justifyContent: "flex-end",
248
+ gap: 8
249
+ },
250
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
251
+ type: "button",
252
+ onClick: () => setConfirmingSignOut(false),
253
+ disabled: busy,
254
+ style: {
255
+ ...secondaryButtonStyle,
256
+ height: 32,
257
+ padding: "0 12px",
258
+ borderRadius: 16
259
+ },
260
+ children: "取消"
261
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
262
+ type: "button",
263
+ onClick: () => void signOut(),
264
+ disabled: busy,
265
+ style: {
266
+ ...dangerOutlineButtonStyle,
267
+ height: 32,
268
+ padding: "0 12px",
269
+ borderRadius: 16,
270
+ background: "var(--dsw-alias-interactive-bg-hover-danger)"
271
+ },
272
+ children: "确认退出"
273
+ })]
274
+ })]
275
+ }),
276
+ notice?.owner === "account" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
277
+ style: {
278
+ fontSize: 13,
279
+ color: notice.kind === "ok" ? "var(--dsw-alias-state-success-primary)" : "var(--dsw-alias-state-error-primary)",
280
+ margin: "8px 0 0"
281
+ },
282
+ children: notice.text
283
+ })
284
+ ] }),
285
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
286
+ style: {
287
+ fontSize: 16,
288
+ fontWeight: 600,
289
+ margin: "0 0 12px"
290
+ },
291
+ children: "修改用户名"
292
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
293
+ onSubmit: (e) => {
294
+ e.preventDefault();
295
+ changeUsername();
296
+ },
297
+ style: {
298
+ display: "flex",
299
+ flexDirection: "column",
300
+ gap: 12
301
+ },
302
+ children: [
303
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
304
+ style: {
305
+ display: "flex",
306
+ flexDirection: "column",
307
+ gap: 4,
308
+ fontSize: 13
309
+ },
310
+ children: ["新用户名", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
311
+ type: "text",
312
+ value: newUsername,
313
+ onChange: (e) => setNewUsername(e.target.value),
314
+ autoComplete: "username",
315
+ style: inputStyle
316
+ })]
317
+ }),
318
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
319
+ style: {
320
+ display: "flex",
321
+ flexDirection: "column",
322
+ gap: 4,
323
+ fontSize: 13
324
+ },
325
+ children: ["当前密码", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
326
+ type: "password",
327
+ value: usernamePassword,
328
+ onChange: (e) => setUsernamePassword(e.target.value),
329
+ autoComplete: "current-password",
330
+ style: inputStyle
331
+ })]
332
+ }),
333
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
334
+ style: {
335
+ display: "flex",
336
+ alignItems: "center",
337
+ gap: 12
338
+ },
339
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
340
+ type: "submit",
341
+ disabled: busy,
342
+ style: primaryButtonStyle,
343
+ children: "修改用户名"
344
+ }), notice?.owner === "username" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
345
+ style: {
346
+ fontSize: 13,
347
+ color: notice.kind === "ok" ? "var(--dsw-alias-state-success-primary)" : "var(--dsw-alias-state-error-primary)"
348
+ },
349
+ children: notice.text
350
+ })]
351
+ })
352
+ ]
353
+ })] }),
354
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
355
+ style: {
356
+ fontSize: 16,
357
+ fontWeight: 600,
358
+ margin: "0 0 12px"
359
+ },
360
+ children: "修改密码"
361
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
362
+ onSubmit: (e) => {
363
+ e.preventDefault();
364
+ changePassword();
365
+ },
366
+ style: {
367
+ display: "flex",
368
+ flexDirection: "column",
369
+ gap: 12
370
+ },
371
+ children: [
372
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
373
+ style: {
374
+ display: "flex",
375
+ flexDirection: "column",
376
+ gap: 4,
377
+ fontSize: 13
378
+ },
379
+ children: ["当前密码", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
380
+ type: "password",
381
+ value: oldPassword,
382
+ onChange: (e) => setOldPassword(e.target.value),
383
+ autoComplete: "current-password",
384
+ style: inputStyle
385
+ })]
386
+ }),
387
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
388
+ style: {
389
+ display: "flex",
390
+ flexDirection: "column",
391
+ gap: 4,
392
+ fontSize: 13
393
+ },
394
+ children: ["新密码", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
395
+ type: "password",
396
+ value: newPassword,
397
+ onChange: (e) => setNewPassword(e.target.value),
398
+ autoComplete: "new-password",
399
+ style: inputStyle
400
+ })]
401
+ }),
402
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
403
+ style: {
404
+ display: "flex",
405
+ flexDirection: "column",
406
+ gap: 4,
407
+ fontSize: 13
408
+ },
409
+ children: ["确认新密码", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
410
+ type: "password",
411
+ value: confirm,
412
+ onChange: (e) => setConfirm(e.target.value),
413
+ autoComplete: "new-password",
414
+ style: inputStyle
415
+ })]
416
+ }),
417
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
418
+ style: {
419
+ display: "flex",
420
+ alignItems: "center",
421
+ gap: 12
422
+ },
423
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
424
+ type: "submit",
425
+ disabled: busy,
426
+ style: primaryButtonStyle,
427
+ children: "修改密码"
428
+ }), notice?.owner === "password" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
429
+ style: {
430
+ fontSize: 13,
431
+ color: notice.kind === "ok" ? "var(--dsw-alias-state-success-primary)" : "var(--dsw-alias-state-error-primary)"
432
+ },
433
+ children: notice.text
434
+ })]
435
+ })
436
+ ]
437
+ })] })
438
+ ]
439
+ });
440
+ }
441
+ function apply$2(ctx) {
442
+ const connection = ctx.get("connection");
443
+ if (connection !== void 0) Object.defineProperty(connection, "isLoopback", {
444
+ configurable: true,
445
+ get: () => true
446
+ });
447
+ ctx.plugin({
448
+ inject: ["slots", "settingsScope"],
449
+ apply: (sub) => {
450
+ sub.slots.inject("settings.section", () => sub.slots.register({
451
+ name: "settings.section",
452
+ id: SECTION_ID$1,
453
+ order: 100,
454
+ label: () => "认证"
455
+ }, AuthSection));
456
+ }
457
+ });
458
+ }
459
+ //#endregion
460
+ //#region src/client/directory-flow.tsx
461
+ const inputStyle$1 = {
462
+ width: "100%",
463
+ boxSizing: "border-box",
464
+ padding: "0 10px",
465
+ height: 32,
466
+ border: "1px solid var(--dsw-alias-border-l2)",
467
+ borderRadius: 8,
468
+ fontSize: 14,
469
+ fontFamily: "inherit",
470
+ background: "var(--dsw-alias-bg-layer-1)",
471
+ color: "var(--dsw-alias-label-primary)",
472
+ outline: "none"
473
+ };
474
+ const buttonStyle$1 = {
475
+ boxSizing: "border-box",
476
+ height: 36,
477
+ padding: "0 14px",
478
+ borderRadius: 18,
479
+ fontSize: 14,
480
+ lineHeight: "22px",
481
+ fontFamily: "inherit",
482
+ cursor: "pointer",
483
+ border: "none",
484
+ display: "inline-flex",
485
+ justifyContent: "center",
486
+ alignItems: "center",
487
+ gap: 4
488
+ };
489
+ const primaryButtonStyle$1 = {
490
+ ...buttonStyle$1,
491
+ background: "var(--dsw-alias-button-primary-fill)",
492
+ color: "var(--dsw-alias-label-primary-foreground)"
493
+ };
494
+ const secondaryButtonStyle$1 = {
495
+ ...buttonStyle$1,
496
+ border: "1px solid var(--dsw-alias-border-l2)",
497
+ color: "var(--dsw-alias-label-primary)",
498
+ background: "transparent"
499
+ };
500
+ const overlayStyle = {
501
+ position: "fixed",
502
+ inset: 0,
503
+ background: "var(--dsw-alias-overlay-mask, rgba(0,0,0,0.45))",
504
+ display: "flex",
505
+ alignItems: "center",
506
+ justifyContent: "center",
507
+ zIndex: 1e3
508
+ };
509
+ const dialogStyle = {
510
+ width: "min(92vw, 560px)",
511
+ maxWidth: "100%",
512
+ maxHeight: "80vh",
513
+ overflow: "auto",
514
+ overflowX: "hidden",
515
+ background: "var(--dsw-alias-bg-layer-1)",
516
+ border: "1px solid var(--dsw-alias-border-l2)",
517
+ borderRadius: 12,
518
+ padding: 20,
519
+ boxSizing: "border-box",
520
+ color: "var(--dsw-alias-label-primary)"
521
+ };
522
+ function joinPath(base, name) {
523
+ return `${base.replace(/\/+$/, "")}/${name}`;
524
+ }
525
+ function DirectoryFlowOccupant(props) {
526
+ const [nodes, setNodes] = (0, react.useState)([]);
527
+ const [nodeId, setNodeId] = (0, react.useState)("");
528
+ const [path, setPath] = (0, react.useState)("");
529
+ const [entries, setEntries] = (0, react.useState)([]);
530
+ const [error, setError] = (0, react.useState)(void 0);
531
+ const [busy, setBusy] = (0, react.useState)(false);
532
+ const [newDirName, setNewDirName] = (0, react.useState)("");
533
+ (0, react.useEffect)(() => {
534
+ if (!props.open) return;
535
+ (async () => {
536
+ try {
537
+ const res = await fetch("/api/dshb/nodes");
538
+ if (!res.ok) return;
539
+ const data = await res.json();
540
+ setNodes((data.nodes ?? []).filter((n) => n.type === "local-host" || n.status?.reachable === true));
541
+ } catch {}
542
+ })();
543
+ }, [props.open]);
544
+ const browse = (0, react.useCallback)(async (id, dir) => {
545
+ setError(void 0);
546
+ try {
547
+ const res = await fetch(`/api/dshb/nodes/${id}/browse?path=${encodeURIComponent(dir)}`);
548
+ const data = await res.json();
549
+ if (!res.ok || !data.ok) {
550
+ setError(data.error ?? "目录读取失败");
551
+ return;
552
+ }
553
+ setPath(data.path ?? dir);
554
+ setEntries((data.entries ?? []).filter((e) => e.isDirectory));
555
+ } catch {
556
+ setError("网络错误");
557
+ }
558
+ }, []);
559
+ const selectNode = (0, react.useCallback)((id) => {
560
+ setNodeId(id);
561
+ setPath("");
562
+ setEntries([]);
563
+ setError(void 0);
564
+ browse(id, "/");
565
+ }, [browse]);
566
+ const mkdir = (0, react.useCallback)(async () => {
567
+ if (!newDirName.trim()) return;
568
+ setBusy(true);
569
+ setError(void 0);
570
+ try {
571
+ const target = joinPath(path, newDirName.trim());
572
+ const res = await fetch(`/api/dshb/nodes/${nodeId}/mkdir`, {
573
+ method: "POST",
574
+ headers: { "content-type": "application/json" },
575
+ body: JSON.stringify({ path: target })
576
+ });
577
+ const data = await res.json();
578
+ if (!res.ok || !data.ok) {
579
+ setError(data.error ?? "创建目录失败");
580
+ return;
581
+ }
582
+ setNewDirName("");
583
+ browse(nodeId, target);
584
+ } catch {
585
+ setError("网络错误");
586
+ } finally {
587
+ setBusy(false);
588
+ }
589
+ }, [
590
+ newDirName,
591
+ nodeId,
592
+ path,
593
+ browse
594
+ ]);
595
+ const confirm = (0, react.useCallback)(async () => {
596
+ if (!nodeId || !path) {
597
+ setError("请选择节点与目录");
598
+ return;
599
+ }
600
+ setBusy(true);
601
+ setError(void 0);
602
+ try {
603
+ const node = nodes.find((n) => n.id === nodeId);
604
+ if (!node) return;
605
+ if (node.type === "local-host") {
606
+ props.onPicked(path);
607
+ return;
608
+ }
609
+ const res = await fetch("/api/dshb/workspaces/bind", {
610
+ method: "POST",
611
+ headers: { "content-type": "application/json" },
612
+ body: JSON.stringify({
613
+ nodeId,
614
+ remotePath: path
615
+ })
616
+ });
617
+ const data = await res.json();
618
+ if (!res.ok || !data.ok || !data.mirrorPath) {
619
+ setError(data.error ?? "工作区注册失败");
620
+ return;
621
+ }
622
+ props.onPicked(data.mirrorPath);
623
+ } catch {
624
+ setError("网络错误");
625
+ } finally {
626
+ setBusy(false);
627
+ }
628
+ }, [
629
+ nodeId,
630
+ path,
631
+ nodes,
632
+ props
633
+ ]);
634
+ if (!props.open) return null;
635
+ const node = nodes.find((n) => n.id === nodeId);
636
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
637
+ style: overlayStyle,
638
+ role: "dialog",
639
+ "aria-label": "添加工作区",
640
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
641
+ style: dialogStyle,
642
+ children: [
643
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
644
+ style: {
645
+ display: "flex",
646
+ justifyContent: "space-between",
647
+ alignItems: "center",
648
+ marginBottom: 14
649
+ },
650
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
651
+ style: {
652
+ margin: 0,
653
+ fontSize: 16,
654
+ color: "var(--dsw-alias-label-primary)"
655
+ },
656
+ children: "添加工作区"
657
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
658
+ type: "button",
659
+ onClick: props.onCancel,
660
+ style: secondaryButtonStyle$1,
661
+ children: "关闭"
662
+ })]
663
+ }),
664
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
665
+ style: {
666
+ display: "block",
667
+ fontSize: 12,
668
+ color: "var(--dsw-alias-label-secondary)",
669
+ marginBottom: 4
670
+ },
671
+ children: "工作节点"
672
+ }),
673
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
674
+ style: {
675
+ ...inputStyle$1,
676
+ marginBottom: 14
677
+ },
678
+ value: nodeId,
679
+ onChange: (e) => selectNode(e.target.value),
680
+ disabled: props.busy || busy,
681
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
682
+ value: "",
683
+ children: "选择节点…"
684
+ }), nodes.map((n) => {
685
+ const typeLabel = n.type === "local-host" ? "本地" : n.type === "remote-ssh" ? "远程 SSH" : n.type === "local-docker" ? "本地 Docker" : n.type === "remote-docker" ? "远程 Docker" : n.type;
686
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
687
+ value: n.id,
688
+ children: [
689
+ n.name,
690
+ "(",
691
+ typeLabel,
692
+ ")"
693
+ ]
694
+ }, n.id);
695
+ })]
696
+ }),
697
+ nodeId !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
698
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
699
+ style: {
700
+ display: "flex",
701
+ justifyContent: "space-between",
702
+ alignItems: "center",
703
+ marginBottom: 6
704
+ },
705
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
706
+ style: {
707
+ fontSize: 12,
708
+ color: "var(--dsw-alias-label-secondary)"
709
+ },
710
+ children: [
711
+ "当前目录(",
712
+ node?.type === "local-host" ? "本机" : "远端",
713
+ ")"
714
+ ]
715
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
716
+ type: "button",
717
+ onClick: () => void browse(nodeId, dirnameOf(path)),
718
+ disabled: busy,
719
+ style: secondaryButtonStyle$1,
720
+ children: "上一级"
721
+ })]
722
+ }),
723
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
724
+ style: {
725
+ ...inputStyle$1,
726
+ marginBottom: 8,
727
+ height: "auto",
728
+ minHeight: 32,
729
+ display: "flex",
730
+ alignItems: "center",
731
+ flexWrap: "wrap",
732
+ gap: 2,
733
+ cursor: "pointer",
734
+ lineHeight: "32px",
735
+ overflow: "hidden"
736
+ },
737
+ onClick: () => void browse(nodeId, path),
738
+ children: path.split("/").filter(Boolean).length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
739
+ style: { opacity: .5 },
740
+ children: "根目录"
741
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
742
+ onClick: (ev) => {
743
+ ev.stopPropagation();
744
+ browse(nodeId, "/");
745
+ },
746
+ style: {
747
+ cursor: "pointer",
748
+ color: "var(--dsw-alias-label-secondary)"
749
+ },
750
+ children: "/"
751
+ }), path.split("/").filter(Boolean).map((seg, i, arr) => {
752
+ const dir = "/" + arr.slice(0, i + 1).join("/");
753
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
754
+ style: {
755
+ display: "inline-flex",
756
+ alignItems: "center",
757
+ gap: 2
758
+ },
759
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
760
+ onClick: (ev) => {
761
+ ev.stopPropagation();
762
+ browse(nodeId, dir);
763
+ },
764
+ style: { cursor: "pointer" },
765
+ children: seg
766
+ }), i < arr.length - 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
767
+ style: { opacity: .4 },
768
+ children: "/"
769
+ })]
770
+ }, i);
771
+ })] })
772
+ }),
773
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
774
+ style: {
775
+ maxHeight: 200,
776
+ overflow: "auto",
777
+ border: "1px solid var(--dsw-alias-border-l2)",
778
+ borderRadius: 6,
779
+ marginBottom: 10
780
+ },
781
+ children: [entries.map((e) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
782
+ onClick: () => void browse(nodeId, e.path),
783
+ style: {
784
+ padding: "7px 10px",
785
+ cursor: "pointer",
786
+ fontSize: 13,
787
+ borderRadius: 6,
788
+ color: "var(--dsw-alias-label-primary)",
789
+ display: "flex",
790
+ alignItems: "center",
791
+ gap: 6,
792
+ overflow: "hidden"
793
+ },
794
+ onMouseEnter: (ev) => ev.currentTarget.style.background = "var(--dsw-alias-interactive-bg-hover)",
795
+ onMouseLeave: (ev) => ev.currentTarget.style.background = "transparent",
796
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
797
+ style: { flexShrink: 0 },
798
+ children: "📁"
799
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
800
+ style: {
801
+ overflow: "hidden",
802
+ textOverflow: "ellipsis",
803
+ whiteSpace: "nowrap"
804
+ },
805
+ children: e.name
806
+ })]
807
+ }, e.path)), entries.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
808
+ style: {
809
+ padding: 12,
810
+ fontSize: 12,
811
+ color: "var(--dsw-alias-label-tertiary)"
812
+ },
813
+ children: "无子目录"
814
+ })]
815
+ }),
816
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
817
+ style: {
818
+ display: "flex",
819
+ gap: 8,
820
+ marginBottom: 12
821
+ },
822
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
823
+ style: {
824
+ ...inputStyle$1,
825
+ flex: 1,
826
+ minWidth: 0
827
+ },
828
+ value: newDirName,
829
+ onChange: (e) => setNewDirName(e.target.value),
830
+ placeholder: "在当前目录下新建子目录…",
831
+ onKeyDown: (e) => {
832
+ if (e.key === "Enter") mkdir();
833
+ }
834
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
835
+ type: "button",
836
+ onClick: () => void mkdir(),
837
+ disabled: busy || !newDirName.trim(),
838
+ style: {
839
+ ...secondaryButtonStyle$1,
840
+ flexShrink: 0,
841
+ whiteSpace: "nowrap"
842
+ },
843
+ children: "新建"
844
+ })]
845
+ })
846
+ ] }),
847
+ error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
848
+ style: {
849
+ fontSize: 13,
850
+ color: "var(--dsw-alias-state-error-primary)",
851
+ marginBottom: 10
852
+ },
853
+ children: error
854
+ }),
855
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
856
+ style: {
857
+ display: "flex",
858
+ justifyContent: "flex-end",
859
+ gap: 10
860
+ },
861
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
862
+ type: "button",
863
+ onClick: props.onCancel,
864
+ style: secondaryButtonStyle$1,
865
+ children: "取消"
866
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
867
+ type: "button",
868
+ onClick: () => void confirm(),
869
+ disabled: busy || props.busy || !nodeId || !path,
870
+ style: primaryButtonStyle$1,
871
+ children: "设为工作区"
872
+ })]
873
+ })
874
+ ]
875
+ })
876
+ });
877
+ }
878
+ function dirnameOf(path) {
879
+ const parts = path.split("/").filter(Boolean);
880
+ parts.pop();
881
+ return parts.length === 0 ? "/" : `/${parts.join("/")}`;
882
+ }
883
+ //#endregion
884
+ //#region src/client/mobile.tsx
885
+ /**
886
+ * dshb 移动端屏幕适配(参考 dsh-web-mobile 实现,仅保留页面适配与目录抽屉
887
+ * 切换,不含文件浏览 / 导出会话日志等功能按钮)。
888
+ *
889
+ * 机制与上游一致:dsh 客户端在窄屏会自动给 AppFrame 加 data-sidebar-collapsed
890
+ * 折叠侧边栏;这里把侧边栏列(frame 的第一个 grid 子元素)用 CSS 抽屉化
891
+ * (绝对定位 + transform 移出/滑入),并在会话头部注入一个切换按钮调用
892
+ * ctx.layout.toggleSidebar()。窄屏判定与上游一致(<1024 且粗指针,避免桌面
893
+ * 小窗误触发)。
894
+ */
895
+ const MOBILE_QUERY = "(max-width: 1023px) and (pointer: coarse)";
896
+ /** 纯视口宽度判定:与放宽后的 MOBILE_CSS 媒体查询一致(不依赖 pointer 类型)。 */
897
+ const NARROW_QUERY = "(max-width: 1023px)";
898
+ const FRAME_ATTR = "data-dshb-mobile";
899
+ const COLLAPSED_ATTR = "data-sidebar-collapsed";
900
+ const MOBILE_CSS = `
901
+ /* 仅按视口宽度判定,不绑定 pointer:coarse —— 触屏笔记本 / DevTools 响应式模式
902
+ 常报 pointer:fine,绑定 coarse 会让全部窄屏修复失效。抽屉相关选择器都挂在
903
+ [data-dshb-mobile] 标记上,标记由 JS 的 coarse 判定添加,故放宽 CSS 不会误开抽屉。 */
904
+ @media (max-width: 1023px) {
905
+ html, body { touch-action: pan-y pinch-zoom !important; overscroll-behavior-x: none !important; -webkit-text-size-adjust: 100% !important; text-size-adjust: 100% !important; }
906
+
907
+ [${FRAME_ATTR}="frame"] {
908
+ box-sizing: border-box !important;
909
+ position: relative !important;
910
+ grid-template-columns: minmax(0, 1fr) 0 0 !important;
911
+ padding-top: env(safe-area-inset-top, 0px) !important;
912
+ }
913
+
914
+ /* 侧边栏列 -> 抽屉:默认移出屏幕,展开(无 collapsed)时滑入 */
915
+ [${FRAME_ATTR}="frame"] > :first-child {
916
+ position: absolute !important;
917
+ inset: 0 auto 0 0 !important;
918
+ width: max-content;
919
+ max-width: 92vw;
920
+ z-index: 40 !important;
921
+ transform: translateX(-110%);
922
+ transition: transform .28s ease-in-out;
923
+ background: var(--dsw-alias-bg-base, #ffffff);
924
+ padding-top: env(safe-area-inset-top, 0px) !important;
925
+ border-right: none !important;
926
+ touch-action: pan-y pinch-zoom !important;
927
+ }
928
+ [${FRAME_ATTR}="frame"]:not([${COLLAPSED_ATTR}]) > :first-child { transform: none !important; }
929
+ @media (prefers-reduced-motion: reduce) {
930
+ [${FRAME_ATTR}="frame"] > :first-child { transition: none !important; }
931
+ }
932
+
933
+ /* 拖拽手柄在触屏上无用 */
934
+ [data-side="sidebar"], [data-side="details"] { display: none !important; }
935
+
936
+ /* 对话区:去桌面滚动条占位,收窄 gutter,字号降一档 */
937
+ [data-phase] [class*="_scrollBody"] { scrollbar-gutter: auto !important; scrollbar-width: none; }
938
+ [data-phase] [class*="_scrollBody"]::-webkit-scrollbar { display: none !important; width: 0; height: 0; }
939
+ [data-phase] [class*="_scroll"]:not([class*="_scrollBody"]):has(p) { padding-left: 20px; padding-right: 20px; font-size: 15px !important; }
940
+ [data-phase] [class*="_scroll"]:not([class*="_scrollBody"]):has(p) p,
941
+ [data-phase] [class*="_scroll"]:not([class*="_scrollBody"]):has(p) li,
942
+ [data-phase] [class*="_scroll"]:not([class*="_scrollBody"]):has(p) [class*="_text_"] { font-size: 15px !important; }
943
+ [data-phase] table { width: 100%; max-width: 100%; }
944
+ [data-phase] th, [data-phase] td { max-width: none; min-width: 0; }
945
+ [data-phase] [class*="_scroll"]:not([class*="_scrollBody"]) img { width: auto !important; max-width: 100% !important; height: auto !important; max-height: 220px !important; }
946
+ [data-phase] [class*="_userStack"], [data-phase] [class*="_userStack"] [class*="_bubble"] { box-sizing: border-box; width: fit-content; max-width: 100%; }
947
+ [data-phase] [class*="_actions"] { overflow: hidden; }
948
+
949
+ /* 统计行(turns/steps/LLM/TPS):窄屏横向滚动,指标可滑动看全 */
950
+ [${FRAME_ATTR}="stats"] {
951
+ display: flex !important;
952
+ flex-wrap: nowrap !important;
953
+ align-items: center;
954
+ gap: 6px;
955
+ overflow-x: auto !important;
956
+ max-width: 100% !important;
957
+ scrollbar-width: none;
958
+ -webkit-overflow-scrolling: touch;
959
+ }
960
+ [${FRAME_ATTR}="stats"]::-webkit-scrollbar { display: none; }
961
+ [${FRAME_ATTR}="stats"] > * { flex-shrink: 0 !important; white-space: nowrap !important; }
962
+ [${FRAME_ATTR}="stats"] { gap: 6px !important; }
963
+ [class*="composerStack"] { gap: 4px !important; }
964
+ /* stats line 竖线分隔符(|)两边 margin 10px 太松,适度收紧 */
965
+ [data-phase] [class*="_sep"] { margin: 0 3px !important; }
966
+
967
+ /* 每轮回复下方的统计文字(footer):收紧 actions 内部 gap 与气泡到 footer 间距,减少松散感 */
968
+ [data-phase] [class*="_actions"] { gap: 4px !important; }
969
+ [data-phase] [class*="_actions"] [class*="_timeEnd"] { gap: 4px !important; padding-left: 6px !important; }
970
+ [data-phase] [class*="userRow"], [data-phase] [class*="assistantRow"] { gap: 4px !important; }
971
+ /* 点号(·)左右 margin 10px 太松,收紧并抵消前后空格 */
972
+ [data-phase] [class*="_runTimeDot"] { margin: 0 -2px !important; }
973
+ /* footer 容器各项间距 16px 偏松 */
974
+ [data-phase] [class*="_root"][class*="osXY9a"] { gap: 8px !important; }
975
+ /* footer 统计行窄屏可横向滑动看全,不溢出页面 */
976
+ [data-phase] [class*="_actions"] { max-width: 100% !important; overflow-x: auto !important; scrollbar-width: none; touch-action: pan-x pan-y !important; }
977
+ [data-phase] [class*="_actions"]::-webkit-scrollbar { display: none; }
978
+
979
+ /* 模型切换菜单:名称完整展示,长名称在菜单项内换行。 */
980
+ [class*="_submenu_"] [class*="_itemLabel_"] {
981
+ overflow: visible !important;
982
+ text-overflow: clip !important;
983
+ white-space: normal !important;
984
+ overflow-wrap: anywhere;
985
+ }
986
+
987
+ /* 模型选择器(dsh-client-ui-model-selection,CSS 模块前缀 7KE1Ra_):
988
+ 列表内模型名/描述默认 ellipsis 截断,改换行完整显示。除当前 hash 外,
989
+ 再加 modelName 语义匹配,防止上游升级换 hash 后规则失效。
990
+ 注意:必须用 break-word 而非 anywhere —— anywhere 让文字 min-content
991
+ 宽度≈1 字符,配合全局 * {min-width:0} 会把 flex 行内名字列压到 1 字符宽,
992
+ 出现"一字符就换行"。break-word 的 min-content 取最长词宽,只在真正
993
+ 放不下时才断词。 */
994
+ [class*="7KE1Ra_modelName"],
995
+ [class*="7KE1Ra_description"],
996
+ [class*="modelName"] {
997
+ white-space: normal !important;
998
+ overflow-wrap: break-word;
999
+ text-overflow: clip !important;
1000
+ overflow: visible !important;
1001
+ }
1002
+
1003
+ /* 模型选择器弹窗:JS(installModelMenuPosition)在窄屏下改为 position:fixed
1004
+ 视口级浮层(fixed 不受祖先 overflow 裁剪),这里仅做静态兜底(JS 未生效时
1005
+ 仍从触发按钮左缘向右展开、宽度收进视口)。 */
1006
+ [class*="7KE1Ra_menu"],
1007
+ [class*="_menu"]:has([class*="modelName"]) {
1008
+ left: 0 !important;
1009
+ right: auto !important;
1010
+ width: calc(100vw - 32px) !important;
1011
+ min-width: 0 !important;
1012
+ max-width: calc(100vw - 32px) !important;
1013
+ overflow-y: auto !important;
1014
+ max-height: calc(100vh - 120px) !important;
1015
+ }
1016
+
1017
+ /* 工具调用执行结果/代码/JSON:窄屏可横向滑动。details 抽屉宽 min(92%,420px)
1018
+ 较窄,命令行输出/JSON 长行溢出被外层 overflow:hidden 裁剪看不到。命令行输出
1019
+ 改 white-space:pre(保持原格式不折行,像终端)+ overflow-x:auto 横向滑动;
1020
+ min-width:0+max-width:100% 约束宽度触发滚动。 */
1021
+ [data-phase] [class*="resultBlockText"],
1022
+ [data-phase] [class*="payload"]:not([class*="errorPayload"]),
1023
+ [data-phase] [class*="sourceBlockContent"] {
1024
+ white-space: pre !important;
1025
+ min-width: 0 !important;
1026
+ max-width: 100% !important;
1027
+ overflow-x: auto !important;
1028
+ -webkit-overflow-scrolling: touch;
1029
+ scrollbar-width: none;
1030
+ touch-action: pan-x pan-y !important;
1031
+ }
1032
+ [data-phase] [class*="resultBlockText"]::-webkit-scrollbar,
1033
+ [data-phase] [class*="payload"]::-webkit-scrollbar,
1034
+ [data-phase] [class*="sourceBlockContent"]::-webkit-scrollbar { display: none !important; }
1035
+
1036
+ /* JSON 预览/代码块/promptDiff:横向滑动(不折行,保持格式) */
1037
+ [data-phase] [class*="jsonPayload"],
1038
+ [data-phase] [class*="jsonPreview"],
1039
+ [data-phase] [class*="promptDiff"],
1040
+ [data-phase] [class*="overviewPreview"],
1041
+ [data-phase] [class*="resultBlocks"],
1042
+ [data-phase] pre {
1043
+ min-width: 0 !important;
1044
+ max-width: 100% !important;
1045
+ overflow-x: auto !important;
1046
+ -webkit-overflow-scrolling: touch;
1047
+ scrollbar-width: none;
1048
+ touch-action: pan-x pan-y !important;
1049
+ }
1050
+ [data-phase] [class*="jsonPayload"]::-webkit-scrollbar,
1051
+ [data-phase] [class*="jsonPreview"]::-webkit-scrollbar,
1052
+ [data-phase] [class*="promptDiff"]::-webkit-scrollbar,
1053
+ [data-phase] [class*="overviewPreview"]::-webkit-scrollbar,
1054
+ [data-phase] [class*="resultBlocks"]::-webkit-scrollbar,
1055
+ [data-phase] pre::-webkit-scrollbar { display: none !important; }
1056
+
1057
+ /* dsh-client-ui-tool 工具卡片(Bash 等命令输出,class 前缀 o3BgMG_):
1058
+ bodyScroll/ioSection 只有 overflow-y:auto,横向溢出传到 row{overflow:hidden}
1059
+ 被裁剪且无法滑动。给输出容器链设 overflow-x:auto(不覆盖上游 overflow-y),
1060
+ 让终端格式长行在卡片内横向滑动。 */
1061
+ [data-phase] [class*="bodyScroll"],
1062
+ [data-phase] [class*="ioCard"],
1063
+ [data-phase] [class*="ioSection"],
1064
+ [data-phase] [class*="terminalBody"],
1065
+ [data-phase] [class*="codeBody"] {
1066
+ min-width: 0 !important;
1067
+ max-width: 100% !important;
1068
+ overflow-x: auto !important;
1069
+ -webkit-overflow-scrolling: touch;
1070
+ scrollbar-width: none;
1071
+ touch-action: pan-x pan-y !important;
1072
+ }
1073
+ [data-phase] [class*="bodyScroll"]::-webkit-scrollbar,
1074
+ [data-phase] [class*="ioCard"]::-webkit-scrollbar,
1075
+ [data-phase] [class*="ioSection"]::-webkit-scrollbar,
1076
+ [data-phase] [class*="terminalBody"]::-webkit-scrollbar,
1077
+ [data-phase] [class*="codeBody"]::-webkit-scrollbar { display: none !important; }
1078
+
1079
+ /* 终端组件(dsh-web-frontend,CSS 模块前缀 1gdtu_):多行命令上游按行渲染成
1080
+ 多个 _promptLine_>_command_ 元素,行级不能各自设 overflow(会变成每行独立
1081
+ 滚动)。唯一横向滚动容器是公共祖先 _header_1gdtu_32(本就 overflow-y:auto):
1082
+ 行链 _prompt_/_promptLine_/_command_ 全部撑到 max-content 且 overflow 可见,
1083
+ 长行内容溢出传入 _header_ 的 scrollWidth,所有行随 _header_ 同步滚动。
1084
+ _output_1gdtu_156(输出区)同理以其为唯一容器,_line_ 撑宽不裁剪。 */
1085
+ [data-phase] [class*="_header_1gdtu_"] {
1086
+ min-width: 0 !important;
1087
+ max-width: 100% !important;
1088
+ overflow-x: auto !important;
1089
+ overscroll-behavior-x: contain;
1090
+ touch-action: pan-x pan-y !important;
1091
+ }
1092
+ [data-phase] [class*="_prompt_1gdtu_"],
1093
+ [data-phase] [class*="_promptLine_1gdtu_"],
1094
+ [data-phase] [class*="_command_1gdtu_"] {
1095
+ min-width: max-content !important;
1096
+ max-width: none !important;
1097
+ overflow: visible !important;
1098
+ /* Android Chrome TextAutosizer 按块宽计算 font boosting,max-content 行宽不一
1099
+ 导致各行字号参差;max-height 是其豁免条件,禁用放大并锁定继承字号。 */
1100
+ max-height: 999999px !important;
1101
+ }
1102
+ [data-phase] [class*="_command_1gdtu_"] {
1103
+ display: inline-block;
1104
+ text-overflow: clip !important;
1105
+ white-space: pre !important;
1106
+ }
1107
+ [data-phase] [class*="_copyButton_1gdtu_"] { position: sticky !important; right: 0; }
1108
+ [data-phase] [class*="_output_1gdtu_"] {
1109
+ overflow-x: auto !important;
1110
+ -webkit-overflow-scrolling: touch;
1111
+ scrollbar-width: none;
1112
+ touch-action: pan-x pan-y !important;
1113
+ }
1114
+ [data-phase] [class*="_output_1gdtu_"]::-webkit-scrollbar { display: none !important; }
1115
+ [data-phase] [class*="_line_1gdtu_"] {
1116
+ min-width: max-content !important;
1117
+ overflow-x: visible !important;
1118
+ max-height: 999999px !important;
1119
+ }
1120
+
1121
+ /* 复制按钮周边也允许纵向页面手势,避免代码/总结区域形成触控死区。 */
1122
+ [data-phase] [class*="_copyButton_"] { touch-action: manipulation !important; }
1123
+ [data-phase] [class*="_copyAnchor_"],
1124
+ [data-phase] [class*="_body_1ye18_"],
1125
+ [data-phase] [class*="_root_4qrvp_"] { touch-action: pan-x pan-y !important; }
1126
+
1127
+ /* 上下文用量弹窗(ContextMeter .JObwrW_panel):上游 dsh-client-ui-trajectory 的
1128
+ [class*="panel"] 全局规则给它加了 max-width:100%!important,命中本弹窗,使宽度
1129
+ 从 264px 塌缩到包含块(.root 28px)宽,内容竖排成竖条。窄屏解除该误伤。 */
1130
+ .JObwrW_panel { max-width: none !important; overflow-x: visible !important; }
1131
+
1132
+ /* 头部:标题省略,tab 条横向滚动 */
1133
+ [${FRAME_ATTR}="frame"] [data-phase] header { padding-left: 44px; padding-right: 8px; }
1134
+ [${FRAME_ATTR}="frame"] [data-phase] header [class*="_crumbs"] { flex: 1 1 0; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap !important; }
1135
+ [${FRAME_ATTR}="frame"] [data-phase] header [role="tablist"] { flex-wrap: nowrap; gap: 0 16px; overflow-x: auto; overscroll-behavior-x: contain; scrollbar-width: none; }
1136
+ [${FRAME_ATTR}="frame"] [data-phase] header [role="tablist"]::-webkit-scrollbar { display: none; }
1137
+ [${FRAME_ATTR}="frame"] [data-phase] header [role="tablist"] > button { flex-shrink: 0; white-space: nowrap; }
1138
+
1139
+ /* 设置对话框:窄屏近全宽 sheet(排除导出等普通对话框与目录选择器) */
1140
+ [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"])):not(:has([class*="ZuhsRW"])) {
1141
+ position: absolute !important;
1142
+ left: 8px !important;
1143
+ top: calc(env(safe-area-inset-top, 0px) + 12px) !important;
1144
+ width: calc(100vw - 16px) !important;
1145
+ max-width: calc(100vw - 16px) !important;
1146
+ height: auto !important;
1147
+ max-height: calc(100dvh - 24px - env(safe-area-inset-top, 0px)) !important;
1148
+ flex-direction: column !important;
1149
+ border-radius: 14px !important;
1150
+ }
1151
+ [aria-modal="true"]:not(:has(> :first-child > :last-child > button)) { max-width: calc(100vw - 32px) !important; }
1152
+ /* nav:横排换行(避免 CJK 标签竖排楼梯状),隐藏冗余 caption */
1153
+ [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"])):not(:has([class*="ZuhsRW"])) > :first-child {
1154
+ width: 100%;
1155
+ flex-direction: row !important;
1156
+ align-items: center;
1157
+ gap: 6px;
1158
+ padding: 10px 12px 8px;
1159
+ }
1160
+ [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"])):not(:has([class*="ZuhsRW"])) > :first-child > :first-child { display: none !important; }
1161
+ [aria-modal="true"] [class*="_navList"] { flex: 1 1 auto; min-width: 0; flex-direction: row !important; flex-wrap: wrap; gap: 6px; overflow: visible; }
1162
+ [aria-modal="true"] [class*="_navList"] > button { flex-shrink: 0; white-space: nowrap; }
1163
+ /* 外观模式卡片横排 */
1164
+ [aria-modal="true"] [class*="_cubeRow"] { gap: 6px; }
1165
+ [aria-modal="true"] [class*="_cubeRow"] > * { flex: 1 1 0; flex-direction: row !important; align-items: center; justify-content: center; gap: 6px; padding: 10px 8px; min-height: 0; }
1166
+ /* section 填充 sheet 宽度 */
1167
+ [aria-modal="true"] [class*="_section"] { width: 100% !important; max-width: none !important; }
1168
+ [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"])):not(:has([class*="ZuhsRW"])) > :last-child { flex: 1 1 auto; min-height: 0; }
1169
+ [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"])):not(:has([class*="ZuhsRW"])) > :last-child > :last-child { padding: 0 12px 24px; }
1170
+
1171
+ /* 切换按钮:固定头部左侧 */
1172
+ [${FRAME_ATTR}="toggle"] {
1173
+ position: absolute !important;
1174
+ left: 8px !important;
1175
+ top: 12px !important;
1176
+ z-index: 2 !important;
1177
+ display: inline-flex !important;
1178
+ align-items: center; justify-content: center;
1179
+ width: 28px; height: 28px;
1180
+ padding: 0; border: none; background: none; cursor: pointer; color: inherit;
1181
+ -webkit-tap-highlight-color: transparent;
1182
+ }
1183
+
1184
+ /* 首页/hero(无活跃会话,session header 不存在)的抽屉切换 FAB:
1185
+ 浮在顶部相机带下方,避免与 hero 头部内容重叠 */
1186
+ [${FRAME_ATTR}="fab"] {
1187
+ position: absolute !important;
1188
+ top: calc(env(safe-area-inset-top, 0px) + 72px) !important;
1189
+ left: 10px !important;
1190
+ z-index: 21 !important;
1191
+ display: inline-flex !important;
1192
+ align-items: center; justify-content: center;
1193
+ width: 38px; height: 38px;
1194
+ padding: 0;
1195
+ border: 1px solid var(--dsw-alias-border-l1, rgba(0, 0, 0, .12)) !important;
1196
+ border-radius: 50% !important;
1197
+ background: var(--dsw-alias-button-floating-fill, #ffffff) !important;
1198
+ color: var(--dsw-alias-label-primary, inherit) !important;
1199
+ cursor: pointer !important;
1200
+ box-shadow: 0 2px 12px rgba(0, 0, 0, .18) !important;
1201
+ -webkit-tap-highlight-color: transparent;
1202
+ }
1203
+ }
1204
+
1205
+ /* 宽屏 / 鼠标指针:隐藏切换按钮 */
1206
+ @media (min-width: 1024px), (pointer: fine) {
1207
+ [${FRAME_ATTR}="toggle"], [${FRAME_ATTR}="fab"] { display: none !important; }
1208
+ }
1209
+ `;
1210
+ function findFrame() {
1211
+ return document.querySelector("[data-shell-overlay]")?.parentElement ?? null;
1212
+ }
1213
+ function getFrame() {
1214
+ return document.querySelector(`[${FRAME_ATTR}="frame"]`) ?? findFrame();
1215
+ }
1216
+ /** viewport-fit=cover:让 safe-area-inset 生效,内容避开刘海/状态栏。 */
1217
+ function installViewport(ctx) {
1218
+ ctx.effect(() => {
1219
+ const narrow = window.matchMedia(MOBILE_QUERY);
1220
+ const viewport = document.querySelector("meta[name=\"viewport\"]");
1221
+ if (viewport === null) return () => {};
1222
+ const original = viewport.content;
1223
+ const sync = () => {
1224
+ viewport.content = narrow.matches ? "width=device-width, initial-scale=1, viewport-fit=cover" : original;
1225
+ };
1226
+ sync();
1227
+ narrow.addEventListener("change", sync);
1228
+ return () => {
1229
+ narrow.removeEventListener("change", sync);
1230
+ viewport.content = original;
1231
+ };
1232
+ }, "dshb-mobile: viewport");
1233
+ }
1234
+ function installStyles(ctx) {
1235
+ ctx.effect(() => {
1236
+ const tag = document.createElement("style");
1237
+ tag.dataset.plugin = "dshb-mobile";
1238
+ tag.textContent = MOBILE_CSS;
1239
+ document.head.appendChild(tag);
1240
+ setTimeout(() => {
1241
+ if (tag.isConnected) document.head.appendChild(tag);
1242
+ }, 0);
1243
+ return () => {
1244
+ tag.remove();
1245
+ };
1246
+ }, "dshb-mobile: styles");
1247
+ }
1248
+ /** 给 AppFrame 打标记(drawer CSS 选择器挂载点),窄屏才打。 */
1249
+ function installFrameMarker(ctx) {
1250
+ ctx.effect(() => {
1251
+ const narrow = window.matchMedia(MOBILE_QUERY);
1252
+ let frame = null;
1253
+ const clear = () => {
1254
+ if (frame !== null) frame.removeAttribute(FRAME_ATTR);
1255
+ frame = null;
1256
+ };
1257
+ const ensure = () => {
1258
+ if (!narrow.matches) return;
1259
+ frame = findFrame();
1260
+ if (frame !== null && !frame.hasAttribute(FRAME_ATTR)) frame.setAttribute(FRAME_ATTR, "frame");
1261
+ };
1262
+ ensure();
1263
+ const mo = new MutationObserver(ensure);
1264
+ mo.observe(document.documentElement, {
1265
+ childList: true,
1266
+ subtree: true
1267
+ });
1268
+ const onChange = () => {
1269
+ if (narrow.matches) ensure();
1270
+ else clear();
1271
+ };
1272
+ narrow.addEventListener("change", onChange);
1273
+ return () => {
1274
+ mo.disconnect();
1275
+ narrow.removeEventListener("change", onChange);
1276
+ clear();
1277
+ };
1278
+ }, "dshb-mobile: frame marker");
1279
+ }
1280
+ /**
1281
+ * 抽屉打开时覆盖内容区的半透明遮罩,点击关闭;Escape 同样关闭;
1282
+ * 点击抽屉内的会话/导航项后关闭抽屉。
1283
+ */
1284
+ function installDrawerInteractions(ctx, toggleSidebar) {
1285
+ ctx.effect(() => {
1286
+ const narrow = window.matchMedia(MOBILE_QUERY);
1287
+ let backdrop = null;
1288
+ const drawerOpen = () => {
1289
+ const frame = getFrame();
1290
+ return narrow.matches && frame !== null && !frame.hasAttribute(COLLAPSED_ATTR);
1291
+ };
1292
+ const sync = () => {
1293
+ if (drawerOpen() && backdrop === null) {
1294
+ backdrop = document.createElement("div");
1295
+ backdrop.setAttribute(`${FRAME_ATTR}-backdrop`, "");
1296
+ backdrop.style.cssText = "position:fixed;inset:0;z-index:39;background:rgba(0,0,0,.4);-webkit-tap-highlight-color:transparent;";
1297
+ backdrop.addEventListener("click", () => toggleSidebar());
1298
+ document.body.appendChild(backdrop);
1299
+ } else if (!drawerOpen() && backdrop !== null) {
1300
+ backdrop.remove();
1301
+ backdrop = null;
1302
+ }
1303
+ };
1304
+ const onKeyDown = (event) => {
1305
+ if (event.key !== "Escape") return;
1306
+ if (document.querySelector("[aria-modal=\"true\"]") !== null) return;
1307
+ if (drawerOpen()) toggleSidebar();
1308
+ };
1309
+ const onClick = (event) => {
1310
+ if (!drawerOpen()) return;
1311
+ const target = event.target;
1312
+ if (!(target instanceof Element)) return;
1313
+ const drawer = document.querySelector(`[${FRAME_ATTR}="frame"] > :first-child`);
1314
+ if (drawer === null || !drawer.contains(target)) return;
1315
+ if (target.closest("button, a, [role=\"button\"]") !== null) return;
1316
+ if (target.closest("[class*=\"newSession\"], [class*=\"sessionRow\"], [role=\"treeitem\"], [class*=\"searchResult\"]") !== null) toggleSidebar();
1317
+ };
1318
+ sync();
1319
+ const mo = new MutationObserver(sync);
1320
+ mo.observe(document.documentElement, {
1321
+ childList: true,
1322
+ subtree: true,
1323
+ attributes: true,
1324
+ attributeFilter: [COLLAPSED_ATTR]
1325
+ });
1326
+ document.addEventListener("keydown", onKeyDown, true);
1327
+ document.addEventListener("click", onClick, true);
1328
+ narrow.addEventListener("change", sync);
1329
+ return () => {
1330
+ mo.disconnect();
1331
+ document.removeEventListener("keydown", onKeyDown, true);
1332
+ document.removeEventListener("click", onClick, true);
1333
+ narrow.removeEventListener("change", sync);
1334
+ backdrop?.remove();
1335
+ };
1336
+ }, "dshb-mobile: drawer interactions");
1337
+ }
1338
+ /** 对话/输入框下方的统计行(turns/steps/LLM/TPS):hashed class 无法直接选,按文本特征标记后由 CSS 横向滚动,避免溢出。 */
1339
+ function installStatsLine(ctx) {
1340
+ ctx.effect(() => {
1341
+ const narrow = window.matchMedia(MOBILE_QUERY);
1342
+ const STATS_LINE_RE = /(\d+\s*轮|\d+\s*步|\bturns\b|\bsteps\b)/;
1343
+ const isStatsText = (el) => {
1344
+ const text = (el.textContent ?? "").trim();
1345
+ if (text.length === 0 || text.length > 160) return false;
1346
+ if (!STATS_LINE_RE.test(text)) return false;
1347
+ if (el.querySelector("textarea, input, button, select, a") !== null) return false;
1348
+ return true;
1349
+ };
1350
+ const mark = () => {
1351
+ if (!narrow.matches) return;
1352
+ document.querySelectorAll(`[${FRAME_ATTR}="stats"]`).forEach((el) => {
1353
+ if (!el.isConnected || !STATS_LINE_RE.test((el.textContent ?? "").trim())) el.removeAttribute(FRAME_ATTR);
1354
+ });
1355
+ const cands = Array.from(document.querySelectorAll("span, div")).filter((el) => {
1356
+ if (!isStatsText(el)) return false;
1357
+ if (getComputedStyle(el).display === "contents") return false;
1358
+ return true;
1359
+ });
1360
+ for (const el of cands) {
1361
+ if (el.hasAttribute(FRAME_ATTR)) continue;
1362
+ let anc = el.parentElement;
1363
+ let hasOuterCand = false;
1364
+ while (anc) {
1365
+ if (cands.includes(anc)) {
1366
+ hasOuterCand = true;
1367
+ break;
1368
+ }
1369
+ anc = anc.parentElement;
1370
+ }
1371
+ if (!hasOuterCand) {
1372
+ if (!el.hasAttribute(FRAME_ATTR)) el.setAttribute(FRAME_ATTR, "stats");
1373
+ }
1374
+ }
1375
+ };
1376
+ mark();
1377
+ const mo = new MutationObserver(() => {
1378
+ if (narrow.matches) mark();
1379
+ });
1380
+ mo.observe(document.documentElement, {
1381
+ childList: true,
1382
+ subtree: true
1383
+ });
1384
+ narrow.addEventListener("change", mark);
1385
+ return () => {
1386
+ mo.disconnect();
1387
+ narrow.removeEventListener("change", mark);
1388
+ };
1389
+ }, "dshb-mobile: stats line");
1390
+ }
1391
+ /** 浮层 fixed 定位需管理的内联样式键,清理时逐一移除以交还上游 CSS。 */
1392
+ const SHEET_PROPS = [
1393
+ "position",
1394
+ "width",
1395
+ "left",
1396
+ "right",
1397
+ "top",
1398
+ "bottom",
1399
+ "min-width",
1400
+ "max-width",
1401
+ "max-height",
1402
+ "overflow-y",
1403
+ "box-sizing"
1404
+ ];
1405
+ /**
1406
+ * 把挂在 composer 内部的绝对定位浮层统一改造成 position:fixed 视口级浮层:
1407
+ * 水平居中(左右各留 8px,绝不贴边/溢出),高度按视口比例封顶并内容滚动,
1408
+ * 触发元素上方空间足够则向上展开、否则向下展开,四边都约束在视口内。
1409
+ * fixed 不受祖先 overflow 裁剪(祖先带 transform 时失效)。
1410
+ */
1411
+ function positionFixedSheet(el, rootRect, maxWidth, maxHeightRatio) {
1412
+ const margin = 8;
1413
+ const vw = window.innerWidth;
1414
+ const vh = window.innerHeight;
1415
+ const width = Math.max(0, Math.min(maxWidth, vw - 16));
1416
+ const left = Math.round((vw - width) / 2);
1417
+ const maxH = Math.round(vh * maxHeightRatio);
1418
+ const spaceAbove = rootRect.top - margin;
1419
+ const spaceBelow = vh - rootRect.bottom - margin;
1420
+ const expandUp = spaceAbove >= 140 && spaceAbove >= spaceBelow;
1421
+ let top;
1422
+ if (expandUp) {
1423
+ const h = Math.min(maxH, Math.max(spaceAbove, 140));
1424
+ top = rootRect.top - margin - h;
1425
+ } else top = rootRect.bottom + margin;
1426
+ top = Math.max(margin, top);
1427
+ el.style.setProperty("position", "fixed", "important");
1428
+ el.style.setProperty("box-sizing", "border-box", "important");
1429
+ el.style.setProperty("width", `${width}px`, "important");
1430
+ el.style.setProperty("left", `${left}px`, "important");
1431
+ el.style.setProperty("right", "auto", "important");
1432
+ el.style.setProperty("top", `${top}px`, "important");
1433
+ el.style.setProperty("bottom", "auto", "important");
1434
+ el.style.setProperty("min-width", "0", "important");
1435
+ el.style.setProperty("max-width", "none", "important");
1436
+ el.style.setProperty("max-height", `${maxH}px`, "important");
1437
+ el.style.setProperty("overflow-y", "auto", "important");
1438
+ }
1439
+ /**
1440
+ * 一级 pane(Model/Effort 单元格小菜单)紧凑浮层:右缘对齐触发按钮右缘
1441
+ * (面板右缘 x = 按钮右缘 x,钳制在视口内 8px 起,避免宽面板超出屏幕左缘时
1442
+ * 被甩到左边贴边),向上展开、空间不足向下展开;宽度跟随内容
1443
+ * (max-content,封顶 min(420, vw-16)),避免小菜单被全宽 sheet 拉远到
1444
+ * 屏幕中间。测量与定位在同一任务内同步完成,无闪烁。
1445
+ */
1446
+ function positionCompactPanel(el, root, rootRect) {
1447
+ const margin = 8;
1448
+ const vw = window.innerWidth;
1449
+ const trigger = root.querySelector("[class*=\"7KE1Ra_trigger\"], [class*=\"_trigger\"]") ?? root;
1450
+ const anchorRect = trigger === root ? rootRect : trigger.getBoundingClientRect();
1451
+ el.style.setProperty("position", "fixed", "important");
1452
+ el.style.setProperty("box-sizing", "border-box", "important");
1453
+ el.style.setProperty("left", "0", "important");
1454
+ el.style.setProperty("top", "0", "important");
1455
+ el.style.setProperty("right", "auto", "important");
1456
+ el.style.setProperty("bottom", "auto", "important");
1457
+ el.style.setProperty("width", "max-content", "important");
1458
+ el.style.setProperty("min-width", "0", "important");
1459
+ el.style.setProperty("max-width", `${Math.min(420, vw - 16)}px`, "important");
1460
+ el.style.setProperty("max-height", "none", "important");
1461
+ el.style.setProperty("overflow-y", "visible", "important");
1462
+ const w = el.offsetWidth;
1463
+ const h = el.offsetHeight;
1464
+ const panelRightX = Math.min(anchorRect.right, vw - margin);
1465
+ const expandUp = anchorRect.top - margin - h >= margin;
1466
+ const top = Math.max(margin, expandUp ? anchorRect.top - margin - h : anchorRect.bottom + margin);
1467
+ const left = Math.max(margin, panelRightX - w);
1468
+ el.style.setProperty("left", `${left}px`, "important");
1469
+ el.style.setProperty("top", `${top}px`, "important");
1470
+ el.style.setProperty("width", `${w}px`, "important");
1471
+ }
1472
+ /**
1473
+ * 模型名多为纯连字符词(DeepSeek-V4-Flash-Vision-Exp);white-space:normal 下
1474
+ * 每个 "-" 都是软断点,名字一旦超列宽就在第一个连字符处断行("换行太早")。
1475
+ * 把 "-" 就地换成不换行连字符 U+2011:放得下就整行显示,真超宽时仍由
1476
+ * overflow-wrap:break-word 兜底任意断。只改文本节点 data、不换节点,
1477
+ * 避免与 React 文本协调冲突;React 重渲染重置文本后由 MutationObserver
1478
+ * 重新应用(幂等:换过之后不再有 "-")。
1479
+ */
1480
+ function applyNonBreakingHyphens(menu) {
1481
+ for (const el of Array.from(menu.querySelectorAll("[class*=\"modelName\"]"))) {
1482
+ const tn = el.firstChild;
1483
+ if (tn && tn.nodeType === Node.TEXT_NODE && tn.nodeValue !== null && tn.nodeValue.includes("-")) tn.nodeValue = tn.nodeValue.replace(/-/g, "‑");
1484
+ }
1485
+ }
1486
+ /**
1487
+ * 模型名"整行显示、不换行、不截断":窄屏下把名字列里的所有 .modelName 设为
1488
+ * nowrap,并按"最宽名字 vs 可用列宽"统一缩小字号(下限 11px,上限 14px),
1489
+ * 让最长的名字也恰好放得下——从根上消除"换行太早"。只测一次最宽名字
1490
+ * (scrollWidth 一次布局读取),缩放结果应用到全部名字,保持同一字号。
1491
+ * 调用方按 vw + 名字集合签名缓存,避免每次 DOM 变更都强制作废布局。
1492
+ */
1493
+ function fitModelNameFont(menu) {
1494
+ const names = Array.from(menu.querySelectorAll("[class*=\"modelName\"]"));
1495
+ if (names.length === 0) return;
1496
+ const col = names[0].parentElement;
1497
+ if (col === null) return;
1498
+ const colW = col.clientWidth - 4;
1499
+ if (colW <= 0) return;
1500
+ const base = 14;
1501
+ let longest = names[0];
1502
+ for (const n of names) if ((n.textContent?.length ?? 0) > (longest.textContent?.length ?? 0)) longest = n;
1503
+ const prevWs = longest.style.whiteSpace;
1504
+ longest.style.whiteSpace = "nowrap";
1505
+ longest.style.fontSize = `${base}px`;
1506
+ const natural = longest.scrollWidth;
1507
+ longest.style.whiteSpace = prevWs;
1508
+ longest.style.fontSize = "";
1509
+ const fs = natural > colW ? Math.max(11, Math.min(base, Math.round(base * (colW / natural)))) : base;
1510
+ for (const n of names) {
1511
+ n.style.setProperty("white-space", "nowrap", "important");
1512
+ n.style.setProperty("font-size", `${fs}px`, "important");
1513
+ }
1514
+ }
1515
+ /** 宽屏(窄屏判定不成立)时复位名字列内联样式,交还上游 CSS(nowrap+ellipsis)。 */
1516
+ function clearModelNameFit(menu) {
1517
+ if (menu === null || menu === void 0) return;
1518
+ for (const n of Array.from(menu.querySelectorAll("[class*=\"modelName\"]"))) {
1519
+ n.style.removeProperty("white-space");
1520
+ n.style.removeProperty("font-size");
1521
+ }
1522
+ }
1523
+ /** 清除 positionFixedSheet 写入的内联样式,交还上游/移动端 CSS 兜底。 */
1524
+ function clearFixedSheet(el) {
1525
+ if (el === null || el === void 0) return;
1526
+ for (const key of SHEET_PROPS) el.style.removeProperty(key);
1527
+ }
1528
+ /**
1529
+ * 模型选择弹窗(dsh-client-ui-model-selection)是两级 pane 的同一个 `_7KE1Ra_menu`
1530
+ * 元素:一级 Model/Effort 单元格列表(cellLabel),点击后切到模型列表(modelName)
1531
+ * 或档位列表。上游默认 `position:absolute;right:0` 贴触发按钮右缘,触发按钮在
1532
+ * 可横向滚动的 stats 行内时菜单会整体出屏。两级 pane 都必须接管(不能只按
1533
+ * modelName 匹配,否则一级 pane 漏掉)。窄屏下:一级小菜单走紧凑浮层
1534
+ * (右缘贴按钮、向上展开,避免全宽 sheet 把小菜单拉到屏幕中间离按钮太远);
1535
+ * 二级列表走视口级居中 fixed sheet(左右各 8px、高度 45% 视口封顶、
1536
+ * 内容滚动、上方空间不足时向下展开)。
1537
+ */
1538
+ function installModelMenuPosition(ctx) {
1539
+ ctx.effect(() => {
1540
+ const narrow = window.matchMedia(NARROW_QUERY);
1541
+ const findMenus = () => {
1542
+ const byHash = Array.from(document.querySelectorAll("[class*=\"7KE1Ra_menu\"]"));
1543
+ if (byHash.length > 0) return byHash;
1544
+ return Array.from(document.querySelectorAll("[class*=\"_menu\"]")).filter((el) => el.querySelector("[class*=\"cellLabel\"], [class*=\"modelName\"]") !== null);
1545
+ };
1546
+ const findTrigger = (menu) => {
1547
+ if (menu.id) {
1548
+ const controlled = Array.from(document.querySelectorAll("[aria-controls]")).find((el) => el.getAttribute("aria-controls") === menu.id);
1549
+ if (controlled) return controlled;
1550
+ }
1551
+ return document.querySelector("[class*=\"7KE1Ra_trigger\"][aria-expanded=\"true\"]");
1552
+ };
1553
+ let fittedVw = 0;
1554
+ let fittedSig = -1;
1555
+ let fittedMenus = [];
1556
+ const sync = () => {
1557
+ const menus = findMenus();
1558
+ if (!narrow.matches) {
1559
+ if (fittedMenus.length > 0) {
1560
+ for (const menu of fittedMenus) {
1561
+ clearFixedSheet(menu);
1562
+ clearModelNameFit(menu);
1563
+ }
1564
+ fittedVw = 0;
1565
+ fittedSig = -1;
1566
+ fittedMenus = [];
1567
+ window.dispatchEvent(new Event("resize"));
1568
+ }
1569
+ return;
1570
+ }
1571
+ let vw = 0;
1572
+ let sig = 0;
1573
+ const modelMenus = [];
1574
+ for (const menu of menus) {
1575
+ const trigger = findTrigger(menu);
1576
+ if (trigger === null) continue;
1577
+ const rect = menu.getBoundingClientRect();
1578
+ if (rect.width === 0 && rect.height === 0) continue;
1579
+ const triggerRect = trigger.getBoundingClientRect();
1580
+ if (menu.querySelector("[class*=\"modelName\"]")) {
1581
+ positionFixedSheet(menu, triggerRect, Number.POSITIVE_INFINITY, .45);
1582
+ applyNonBreakingHyphens(menu);
1583
+ modelMenus.push(menu);
1584
+ vw = window.innerWidth;
1585
+ for (const n of menu.querySelectorAll("[class*=\"modelName\"]")) sig += n.textContent?.length ?? 0;
1586
+ } else positionCompactPanel(menu, trigger, triggerRect);
1587
+ }
1588
+ const needFit = modelMenus.some((m) => !fittedMenus.includes(m));
1589
+ if (vw > 0 && (needFit || vw !== fittedVw || sig !== fittedSig)) {
1590
+ for (const menu of modelMenus) fitModelNameFont(menu);
1591
+ fittedVw = vw;
1592
+ fittedSig = sig;
1593
+ fittedMenus = modelMenus;
1594
+ }
1595
+ };
1596
+ sync();
1597
+ const mo = new MutationObserver(() => requestAnimationFrame(sync));
1598
+ mo.observe(document.documentElement, {
1599
+ childList: true,
1600
+ subtree: true
1601
+ });
1602
+ narrow.addEventListener("change", sync);
1603
+ window.addEventListener("resize", sync);
1604
+ window.addEventListener("scroll", sync, true);
1605
+ return () => {
1606
+ mo.disconnect();
1607
+ narrow.removeEventListener("change", sync);
1608
+ window.removeEventListener("resize", sync);
1609
+ window.removeEventListener("scroll", sync, true);
1610
+ };
1611
+ }, "dshb-mobile: model menu position");
1612
+ }
1613
+ /**
1614
+ * 上下文用量弹窗(ContextMeter .JObwrW_panel,dsh-client-ui-conversation)上游是
1615
+ * `position:absolute; bottom:calc(100%+8px); right:0` 挂 .JObwrW_root 内。窄屏下
1616
+ * 改为 fixed 视口浮层,右缘锚定触发按钮右缘、向上展开(用户已验证该形态正常,勿改)。
1617
+ */
1618
+ function installContextPanelPosition(ctx) {
1619
+ ctx.effect(() => {
1620
+ const narrow = window.matchMedia(NARROW_QUERY);
1621
+ const sync = () => {
1622
+ if (!narrow.matches) return;
1623
+ const panels = document.querySelectorAll("[class*=\"JObwrW_panel\"]");
1624
+ for (const panel of Array.from(panels)) {
1625
+ const root = panel.parentElement;
1626
+ if (root === null || !root.className.includes("JObwrW_root")) continue;
1627
+ const rootRect = root.getBoundingClientRect();
1628
+ if (rootRect.width === 0 && rootRect.height === 0) continue;
1629
+ const gap = 8;
1630
+ panel.style.setProperty("position", "fixed", "important");
1631
+ panel.style.setProperty("top", "auto", "important");
1632
+ panel.style.setProperty("bottom", `${window.innerHeight - rootRect.top + gap}px`, "important");
1633
+ panel.style.setProperty("left", "auto", "important");
1634
+ panel.style.setProperty("right", `${Math.max(gap, window.innerWidth - rootRect.right)}px`, "important");
1635
+ panel.style.setProperty("max-height", `${Math.max(160, rootRect.top - 16)}px`, "important");
1636
+ panel.style.setProperty("overflow-y", "auto", "important");
1637
+ }
1638
+ };
1639
+ sync();
1640
+ const mo = new MutationObserver(() => requestAnimationFrame(sync));
1641
+ mo.observe(document.documentElement, {
1642
+ childList: true,
1643
+ subtree: true
1644
+ });
1645
+ narrow.addEventListener("change", sync);
1646
+ window.addEventListener("resize", sync);
1647
+ window.addEventListener("scroll", sync, true);
1648
+ return () => {
1649
+ mo.disconnect();
1650
+ narrow.removeEventListener("change", sync);
1651
+ window.removeEventListener("resize", sync);
1652
+ window.removeEventListener("scroll", sync, true);
1653
+ };
1654
+ }, "dshb-mobile: context panel position");
1655
+ }
1656
+ /** 目录抽屉切换按钮(仅切换,不含文件浏览/日志导出等功能按钮)。 */
1657
+ function DrawerToggle({ toggleSidebar }) {
1658
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1659
+ type: "button",
1660
+ "data-dshb-mobile": "toggle",
1661
+ "aria-label": "打开目录",
1662
+ title: "打开目录",
1663
+ onClick: () => toggleSidebar(),
1664
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
1665
+ width: "16",
1666
+ height: "16",
1667
+ viewBox: "0 0 16 16",
1668
+ fill: "none",
1669
+ xmlns: "http://www.w3.org/2000/svg",
1670
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
1671
+ x: "1.5",
1672
+ y: "2.5",
1673
+ width: "13",
1674
+ height: "11",
1675
+ rx: "2",
1676
+ stroke: "currentColor",
1677
+ strokeWidth: "1.5"
1678
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1679
+ d: "M6 2.5v11",
1680
+ stroke: "currentColor",
1681
+ strokeWidth: "1.5"
1682
+ })]
1683
+ })
1684
+ });
1685
+ }
1686
+ const PANEL_ICON_SVG = "<svg width=\"18\" height=\"18\" viewBox=\"0 0 16 16\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\"><rect x=\"1.5\" y=\"2.5\" width=\"13\" height=\"11\" rx=\"2\" stroke=\"currentColor\" strokeWidth=\"1.5\"/><path d=\"M6 2.5v11\" stroke=\"currentColor\" strokeWidth=\"1.5\"/></svg>";
1687
+ /**
1688
+ * 首页 / hero(无活跃会话)没有 session header,header 上的切换按钮不渲染;
1689
+ * 这里在 frame 上注入一个浮动切换按钮(FAB),仅 hero 且抽屉收起时显示。
1690
+ */
1691
+ function installFab(ctx, toggleSidebar) {
1692
+ ctx.effect(() => {
1693
+ const narrow = window.matchMedia(MOBILE_QUERY);
1694
+ const FAB_ATTR = "fab";
1695
+ const heroPhase = () => document.querySelector("[data-phase=\"active\"]") === null;
1696
+ const drawerOpen = () => {
1697
+ const frame = getFrame();
1698
+ return frame !== null && !frame.hasAttribute(COLLAPSED_ATTR);
1699
+ };
1700
+ let fab = null;
1701
+ const sync = () => {
1702
+ if (!narrow.matches || !heroPhase() || drawerOpen()) {
1703
+ if (fab !== null) {
1704
+ fab.remove();
1705
+ fab = null;
1706
+ }
1707
+ return;
1708
+ }
1709
+ const frame = getFrame();
1710
+ if (frame === null) return;
1711
+ if (fab !== null && fab.parentElement === frame) return;
1712
+ fab?.remove();
1713
+ fab = document.createElement("button");
1714
+ fab.type = "button";
1715
+ fab.setAttribute(FRAME_ATTR, FAB_ATTR);
1716
+ fab.setAttribute("aria-label", "打开目录");
1717
+ fab.setAttribute("title", "打开目录");
1718
+ fab.innerHTML = PANEL_ICON_SVG;
1719
+ fab.addEventListener("click", toggleSidebar);
1720
+ frame.appendChild(fab);
1721
+ };
1722
+ sync();
1723
+ const mo = new MutationObserver(sync);
1724
+ mo.observe(document.documentElement, {
1725
+ childList: true,
1726
+ subtree: true,
1727
+ attributes: true,
1728
+ attributeFilter: [
1729
+ "data-phase",
1730
+ "data-sidebar-collapsed",
1731
+ "class"
1732
+ ]
1733
+ });
1734
+ narrow.addEventListener("change", sync);
1735
+ return () => {
1736
+ mo.disconnect();
1737
+ narrow.removeEventListener("change", sync);
1738
+ fab?.remove();
1739
+ };
1740
+ }, "dshb-mobile: hero fab");
1741
+ }
1742
+ /** 安装移动端适配:样式 + viewport + 抽屉标记 + 抽屉交互 + 切换按钮。 */
1743
+ function installMobile(ctx) {
1744
+ installStyles(ctx);
1745
+ installViewport(ctx);
1746
+ installFrameMarker(ctx);
1747
+ installStatsLine(ctx);
1748
+ installModelMenuPosition(ctx);
1749
+ installContextPanelPosition(ctx);
1750
+ const layout = ctx.layout;
1751
+ const slots = ctx.slots;
1752
+ if (layout && typeof layout.toggleSidebar === "function") {
1753
+ const toggleSidebar = () => layout.toggleSidebar();
1754
+ installDrawerInteractions(ctx, toggleSidebar);
1755
+ installFab(ctx, toggleSidebar);
1756
+ }
1757
+ if (slots && layout) slots.inject("conversation.session.header.actions", () => slots.register({
1758
+ name: "conversation.session.header.actions",
1759
+ id: "dshb-mobile-toggle",
1760
+ order: 10,
1761
+ inject: () => ({ toggleSidebar: () => layout.toggleSidebar() })
1762
+ }, DrawerToggle));
1763
+ }
1764
+ //#endregion
1765
+ //#region src/client/ui.tsx
1766
+ const SECTION_ID = "dshb-nodes";
1767
+ const EMPTY_FORM = {
1768
+ name: "",
1769
+ type: "remote-ssh",
1770
+ host: "",
1771
+ port: "22",
1772
+ username: "",
1773
+ authKind: "password",
1774
+ password: "",
1775
+ privateKey: "",
1776
+ passphrase: "",
1777
+ keyPath: "",
1778
+ dockerImage: "",
1779
+ dockerCpus: "",
1780
+ dockerMemory: ""
1781
+ };
1782
+ const inputStyle = {
1783
+ width: "100%",
1784
+ boxSizing: "border-box",
1785
+ padding: "0 10px",
1786
+ height: 32,
1787
+ border: "1px solid var(--dsw-alias-border-l2)",
1788
+ borderRadius: 8,
1789
+ fontSize: 14,
1790
+ fontFamily: "inherit",
1791
+ background: "var(--dsw-alias-bg-layer-1)",
1792
+ color: "var(--dsw-alias-label-primary)",
1793
+ outline: "none"
1794
+ };
1795
+ const buttonStyle = {
1796
+ boxSizing: "border-box",
1797
+ height: 36,
1798
+ padding: "0 14px",
1799
+ borderRadius: 18,
1800
+ fontSize: 14,
1801
+ lineHeight: "22px",
1802
+ fontFamily: "inherit",
1803
+ cursor: "pointer",
1804
+ border: "none",
1805
+ display: "inline-flex",
1806
+ justifyContent: "center",
1807
+ alignItems: "center",
1808
+ gap: 4
1809
+ };
1810
+ const primaryButtonStyle = {
1811
+ ...buttonStyle,
1812
+ background: "var(--dsw-alias-button-primary-fill)",
1813
+ color: "var(--dsw-alias-label-primary-foreground)"
1814
+ };
1815
+ const secondaryButtonStyle = {
1816
+ ...buttonStyle,
1817
+ border: "1px solid var(--dsw-alias-border-l2)",
1818
+ color: "var(--dsw-alias-label-primary)",
1819
+ background: "transparent"
1820
+ };
1821
+ const dangerOutlineButtonStyle = {
1822
+ ...buttonStyle,
1823
+ border: "1px solid var(--dsw-alias-state-error-primary)",
1824
+ color: "var(--dsw-alias-state-error-primary)",
1825
+ background: "transparent"
1826
+ };
1827
+ function useNodes(reloadKey) {
1828
+ const [nodes, setNodes] = (0, react.useState)([]);
1829
+ (0, react.useEffect)(() => {
1830
+ let cancelled = false;
1831
+ (async () => {
1832
+ try {
1833
+ const res = await fetch("/api/dshb/nodes");
1834
+ if (!res.ok) return;
1835
+ const data = await res.json();
1836
+ if (!cancelled) setNodes(data.nodes ?? []);
1837
+ } catch {}
1838
+ })();
1839
+ return () => {
1840
+ cancelled = true;
1841
+ };
1842
+ }, [reloadKey]);
1843
+ return nodes;
1844
+ }
1845
+ function NodeSection(_props) {
1846
+ const [reloadKey, setReloadKey] = (0, react.useState)(0);
1847
+ const nodes = useNodes(reloadKey);
1848
+ const [selectedId, setSelectedId] = (0, react.useState)("new");
1849
+ const [form, setForm] = (0, react.useState)(EMPTY_FORM);
1850
+ const [notice, setNotice] = (0, react.useState)(void 0);
1851
+ const [busy, setBusy] = (0, react.useState)(false);
1852
+ const [testing, setTesting] = (0, react.useState)(false);
1853
+ const [testResult, setTestResult] = (0, react.useState)(void 0);
1854
+ const [sshConfigEntries, setSshConfigEntries] = (0, react.useState)([]);
1855
+ const [dockerBusy, setDockerBusy] = (0, react.useState)(false);
1856
+ const flash = (0, react.useCallback)((n) => {
1857
+ setNotice(n);
1858
+ if (n) setTimeout(() => setNotice(void 0), 6e3);
1859
+ }, []);
1860
+ const reload = (0, react.useCallback)(() => setReloadKey((k) => k + 1), []);
1861
+ (0, react.useEffect)(() => {
1862
+ (async () => {
1863
+ try {
1864
+ const res = await fetch("/api/dshb/ssh-config");
1865
+ if (!res.ok) return;
1866
+ const data = await res.json();
1867
+ setSshConfigEntries(data.entries ?? []);
1868
+ } catch {}
1869
+ })();
1870
+ }, []);
1871
+ const selectNode = (0, react.useCallback)((id) => {
1872
+ setSelectedId(id);
1873
+ setTestResult(void 0);
1874
+ setTesting(false);
1875
+ setDockerBusy(false);
1876
+ if (id === "new") {
1877
+ setForm(EMPTY_FORM);
1878
+ return;
1879
+ }
1880
+ const node = nodes.find((n) => n.id === id);
1881
+ if (!node) return;
1882
+ setForm({
1883
+ name: node.name,
1884
+ type: node.type,
1885
+ host: node.ssh?.host ?? "",
1886
+ port: String(node.ssh?.port ?? 22),
1887
+ username: node.ssh?.username ?? "",
1888
+ authKind: node.ssh?.auth.kind ?? "password",
1889
+ password: "",
1890
+ privateKey: "",
1891
+ passphrase: "",
1892
+ keyPath: node.ssh?.auth.keyPath ?? "",
1893
+ dockerImage: node.docker?.image ?? "",
1894
+ dockerCpus: node.docker?.resources?.cpus != null ? String(node.docker?.resources?.cpus) : "",
1895
+ dockerMemory: node.docker?.resources?.memoryMB != null ? String(node.docker?.resources?.memoryMB) : ""
1896
+ });
1897
+ }, [nodes]);
1898
+ const importSshConfig = (0, react.useCallback)((entry) => {
1899
+ setForm((f) => ({
1900
+ ...f,
1901
+ type: "remote-ssh",
1902
+ host: entry.host,
1903
+ port: String(entry.port),
1904
+ username: entry.username,
1905
+ authKind: entry.hasIdentityFile ? "key" : f.authKind,
1906
+ keyPath: entry.identityFile ?? f.keyPath,
1907
+ name: f.name || entry.host
1908
+ }));
1909
+ setTestResult(void 0);
1910
+ flash({
1911
+ kind: "ok",
1912
+ text: `已从 ~/.ssh/config 导入 ${entry.host}`
1913
+ });
1914
+ }, [flash]);
1915
+ const save = (0, react.useCallback)(async () => {
1916
+ if (!form.name.trim()) {
1917
+ flash({
1918
+ kind: "error",
1919
+ text: "请填写节点名称"
1920
+ });
1921
+ return;
1922
+ }
1923
+ setBusy(true);
1924
+ try {
1925
+ const isRemote = form.type === "remote-ssh" || form.type === "remote-docker";
1926
+ const isDocker = form.type === "local-docker" || form.type === "remote-docker";
1927
+ const payload = {
1928
+ name: form.name.trim(),
1929
+ type: form.type,
1930
+ ssh: isRemote ? {
1931
+ host: form.host.trim(),
1932
+ port: Number(form.port) || 22,
1933
+ username: form.username.trim(),
1934
+ auth: {
1935
+ kind: form.authKind,
1936
+ ...form.keyPath.trim() ? { keyPath: form.keyPath.trim() } : {}
1937
+ }
1938
+ } : void 0,
1939
+ secrets: isRemote ? {
1940
+ ...form.password ? { password: form.password } : {},
1941
+ ...form.privateKey ? { privateKey: form.privateKey } : {},
1942
+ ...form.passphrase ? { passphrase: form.passphrase } : {}
1943
+ } : void 0,
1944
+ docker: isDocker ? {
1945
+ mode: "managed",
1946
+ ...form.dockerImage.trim() ? { image: form.dockerImage.trim() } : {},
1947
+ ...form.dockerCpus ? { resources: { cpus: Number(form.dockerCpus) || void 0 } } : {},
1948
+ ...form.dockerMemory ? { resources: { memoryMB: Number(form.dockerMemory) || void 0 } } : {},
1949
+ ...form.dockerCpus && form.dockerMemory ? { resources: {
1950
+ cpus: Number(form.dockerCpus) || void 0,
1951
+ memoryMB: Number(form.dockerMemory) || void 0
1952
+ } } : {}
1953
+ } : void 0
1954
+ };
1955
+ const url = selectedId === "new" ? "/api/dshb/nodes" : `/api/dshb/nodes/${selectedId}`;
1956
+ const res = await fetch(url, {
1957
+ method: selectedId === "new" ? "POST" : "PATCH",
1958
+ headers: { "content-type": "application/json" },
1959
+ body: JSON.stringify(payload)
1960
+ });
1961
+ const data = await res.json();
1962
+ if (res.ok && data.ok) {
1963
+ flash({
1964
+ kind: "ok",
1965
+ text: selectedId === "new" ? "节点已创建" : "节点已保存"
1966
+ });
1967
+ reload();
1968
+ if (data.node) setSelectedId(data.node.id);
1969
+ } else flash({
1970
+ kind: "error",
1971
+ text: data.error ?? "保存失败"
1972
+ });
1973
+ } catch {
1974
+ flash({
1975
+ kind: "error",
1976
+ text: "网络错误"
1977
+ });
1978
+ } finally {
1979
+ setBusy(false);
1980
+ }
1981
+ }, [
1982
+ form,
1983
+ selectedId,
1984
+ flash,
1985
+ reload
1986
+ ]);
1987
+ const remove = (0, react.useCallback)(async () => {
1988
+ if (selectedId === "new") return;
1989
+ setBusy(true);
1990
+ try {
1991
+ const node = nodes.find((n) => n.id === selectedId);
1992
+ if (node && (node.type === "local-docker" || node.type === "remote-docker") && node.docker?.containerId) try {
1993
+ await fetch(`/api/dshb/docker/${selectedId}/delete`, { method: "POST" });
1994
+ } catch {}
1995
+ if ((await fetch(`/api/dshb/nodes/${selectedId}`, { method: "DELETE" })).ok) {
1996
+ flash({
1997
+ kind: "ok",
1998
+ text: "节点已删除"
1999
+ });
2000
+ setSelectedId("new");
2001
+ setForm(EMPTY_FORM);
2002
+ reload();
2003
+ }
2004
+ } catch {
2005
+ flash({
2006
+ kind: "error",
2007
+ text: "删除失败"
2008
+ });
2009
+ } finally {
2010
+ setBusy(false);
2011
+ }
2012
+ }, [
2013
+ selectedId,
2014
+ nodes,
2015
+ flash,
2016
+ reload
2017
+ ]);
2018
+ const test = (0, react.useCallback)(async () => {
2019
+ if (form.type === "local-host") {
2020
+ flash({
2021
+ kind: "error",
2022
+ text: "本地节点无需测试连接"
2023
+ });
2024
+ return;
2025
+ }
2026
+ const isRemote = form.type === "remote-ssh" || form.type === "remote-docker";
2027
+ if (isRemote && !form.host.trim()) {
2028
+ flash({
2029
+ kind: "error",
2030
+ text: "请填写主机地址"
2031
+ });
2032
+ return;
2033
+ }
2034
+ setTesting(true);
2035
+ setTestResult(void 0);
2036
+ try {
2037
+ const payload = isRemote ? {
2038
+ ssh: {
2039
+ host: form.host.trim(),
2040
+ port: Number(form.port) || 22,
2041
+ username: form.username.trim(),
2042
+ auth: {
2043
+ kind: form.authKind,
2044
+ ...form.keyPath.trim() ? { keyPath: form.keyPath.trim() } : {}
2045
+ }
2046
+ },
2047
+ secrets: {
2048
+ ...form.password ? { password: form.password } : {},
2049
+ ...form.privateKey ? { privateKey: form.privateKey } : {},
2050
+ ...form.passphrase ? { passphrase: form.passphrase } : {}
2051
+ }
2052
+ } : {};
2053
+ const url = selectedId === "new" ? "/api/dshb/nodes/test-unsaved" : `/api/dshb/nodes/${selectedId}/test`;
2054
+ const r = (await (await fetch(url, {
2055
+ method: "POST",
2056
+ headers: { "content-type": "application/json" },
2057
+ body: JSON.stringify(payload)
2058
+ })).json()).report;
2059
+ if (r?.ok && r.reachable) setTestResult("连接成功");
2060
+ else {
2061
+ const cat = r?.category ? `(${r.category})` : "";
2062
+ setTestResult(`${r?.error ?? "连接失败"}${cat}`);
2063
+ }
2064
+ reload();
2065
+ } catch {
2066
+ setTestResult("网络错误");
2067
+ } finally {
2068
+ setTesting(false);
2069
+ }
2070
+ }, [
2071
+ form,
2072
+ selectedId,
2073
+ reload
2074
+ ]);
2075
+ const dockerAction = (0, react.useCallback)(async (action) => {
2076
+ if (selectedId === "new") return;
2077
+ if (action === "provision") {
2078
+ setDockerBusy(true);
2079
+ try {
2080
+ const res = await fetch(`/api/dshb/docker/${selectedId}/provision`, { method: "POST" });
2081
+ const data = await res.json();
2082
+ if (res.ok && data.ok) flash({
2083
+ kind: "ok",
2084
+ text: `容器已就绪:${data.containerId?.slice(0, 12) ?? ""}`
2085
+ });
2086
+ else flash({
2087
+ kind: "error",
2088
+ text: data.error ?? "拉起失败"
2089
+ });
2090
+ reload();
2091
+ } catch {
2092
+ flash({
2093
+ kind: "error",
2094
+ text: "网络错误"
2095
+ });
2096
+ } finally {
2097
+ setDockerBusy(false);
2098
+ }
2099
+ return;
2100
+ }
2101
+ setDockerBusy(true);
2102
+ try {
2103
+ const res = await fetch(`/api/dshb/docker/${selectedId}/${action}`, { method: "POST" });
2104
+ const data = await res.json();
2105
+ if (res.ok && data.ok) flash({
2106
+ kind: "ok",
2107
+ text: action === "restart" ? "容器已重启" : action === "stop" ? "容器已停止" : "容器已启动"
2108
+ });
2109
+ else flash({
2110
+ kind: "error",
2111
+ text: data.error ?? "操作失败"
2112
+ });
2113
+ reload();
2114
+ } catch {
2115
+ flash({
2116
+ kind: "error",
2117
+ text: "网络错误"
2118
+ });
2119
+ } finally {
2120
+ setDockerBusy(false);
2121
+ }
2122
+ }, [
2123
+ selectedId,
2124
+ flash,
2125
+ reload
2126
+ ]);
2127
+ const selectedNode = selectedId === "new" ? void 0 : nodes.find((n) => n.id === selectedId);
2128
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: `
2129
+ @media (max-width:767px){
2130
+ .dshb-node-section{flex-direction:column!important}
2131
+ .dshb-node-section>div:first-child{width:100%!important;border-right:none!important;border-bottom:1px solid var(--dsw-alias-border-l2);padding-right:0!important;padding-bottom:12px;margin-bottom:12px}
2132
+ .dshb-node-section>div:last-child{max-width:100%!important}
2133
+ .dshb-form-row{flex-direction:column!important;gap:8px!important}
2134
+ .dshb-form-row>label{width:100%!important;flex:none!important}
2135
+ }
2136
+ ` }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2137
+ className: "dshb-node-section",
2138
+ style: {
2139
+ display: "flex",
2140
+ gap: 20,
2141
+ minHeight: 320,
2142
+ overflow: "hidden",
2143
+ maxWidth: "100%",
2144
+ width: "100%",
2145
+ boxSizing: "border-box"
2146
+ },
2147
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2148
+ style: {
2149
+ width: 200,
2150
+ flexShrink: 0,
2151
+ borderRight: "1px solid var(--dsw-alias-border-l2)",
2152
+ paddingRight: 16
2153
+ },
2154
+ children: [
2155
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2156
+ type: "button",
2157
+ onClick: () => selectNode("new"),
2158
+ style: {
2159
+ ...primaryButtonStyle,
2160
+ width: "100%",
2161
+ marginBottom: 10
2162
+ },
2163
+ children: "新建节点"
2164
+ }),
2165
+ nodes.map((n) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2166
+ onClick: () => selectNode(n.id),
2167
+ style: {
2168
+ padding: "8px 10px",
2169
+ borderRadius: 6,
2170
+ cursor: "pointer",
2171
+ marginBottom: 4,
2172
+ background: selectedId === n.id ? "var(--dsw-alias-interactive-bg-active, var(--dsw-alias-interactive-bg-hover))" : "transparent",
2173
+ fontSize: 13
2174
+ },
2175
+ children: [
2176
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2177
+ style: { fontWeight: 500 },
2178
+ children: n.name
2179
+ }),
2180
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2181
+ style: {
2182
+ fontSize: 11,
2183
+ opacity: .7,
2184
+ display: "flex",
2185
+ alignItems: "center",
2186
+ gap: 4
2187
+ },
2188
+ children: [
2189
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: {
2190
+ width: 6,
2191
+ height: 6,
2192
+ borderRadius: "50%",
2193
+ background: n.status?.reachable === true ? "var(--dsw-alias-state-success-primary)" : n.status?.reachable === false ? "var(--dsw-alias-state-error-primary)" : "var(--dsw-alias-label-dimmed)"
2194
+ } }),
2195
+ n.type === "local-host" ? "默认环境(本地)" : n.type === "remote-ssh" ? "远程 SSH" : n.type === "local-docker" ? "本地 Docker" : n.type === "remote-docker" ? "远程 Docker" : n.type,
2196
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2197
+ style: {
2198
+ marginLeft: 4,
2199
+ color: n.status?.reachable === true ? "var(--dsw-alias-state-success-primary)" : n.status?.reachable === false ? "var(--dsw-alias-state-error-primary)" : "var(--dsw-alias-label-dimmed)"
2200
+ },
2201
+ children: n.status?.reachable === true ? "在线" : n.status?.reachable === false ? "离线" : "未知"
2202
+ })
2203
+ ]
2204
+ }),
2205
+ n.provision && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2206
+ style: {
2207
+ fontSize: 11,
2208
+ marginTop: 2,
2209
+ 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)"
2210
+ },
2211
+ children: n.provision.state === "provisioning" ? "容器拉起中…" : n.provision.state === "ready" ? "容器已就绪" : `容器拉起失败:${n.provision.error ?? "未知错误"}`
2212
+ })
2213
+ ]
2214
+ }, n.id)),
2215
+ nodes.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2216
+ style: {
2217
+ fontSize: 12,
2218
+ opacity: .6
2219
+ },
2220
+ children: "暂无节点"
2221
+ })
2222
+ ]
2223
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2224
+ style: {
2225
+ flex: 1,
2226
+ maxWidth: 480,
2227
+ minWidth: 0,
2228
+ overflow: "hidden"
2229
+ },
2230
+ children: [sshConfigEntries.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2231
+ style: { marginBottom: 14 },
2232
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
2233
+ style: {
2234
+ fontSize: 12,
2235
+ opacity: .75,
2236
+ display: "block",
2237
+ marginBottom: 4
2238
+ },
2239
+ children: "从 ~/.ssh/config 导入"
2240
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2241
+ style: inputStyle,
2242
+ value: "",
2243
+ onChange: (e) => {
2244
+ const entry = sshConfigEntries[Number(e.target.value)];
2245
+ if (entry) importSshConfig(entry);
2246
+ e.target.value = "";
2247
+ },
2248
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2249
+ value: "",
2250
+ children: "选择主机…"
2251
+ }), sshConfigEntries.map((entry, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
2252
+ value: i,
2253
+ children: [
2254
+ entry.host,
2255
+ ":",
2256
+ entry.port,
2257
+ entry.username ? ` (${entry.username})` : "",
2258
+ entry.hasProxyJump ? " 跳板" : ""
2259
+ ]
2260
+ }, entry.host))]
2261
+ })]
2262
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2263
+ style: {
2264
+ display: "flex",
2265
+ flexDirection: "column",
2266
+ gap: 12
2267
+ },
2268
+ children: [
2269
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2270
+ style: {
2271
+ display: "flex",
2272
+ flexDirection: "column",
2273
+ gap: 4,
2274
+ fontSize: 13
2275
+ },
2276
+ children: ["节点名称", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2277
+ style: inputStyle,
2278
+ value: form.name,
2279
+ onChange: (e) => setForm((f) => ({
2280
+ ...f,
2281
+ name: e.target.value
2282
+ })),
2283
+ placeholder: "例如:构建机"
2284
+ })]
2285
+ }),
2286
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2287
+ style: {
2288
+ display: "flex",
2289
+ flexDirection: "column",
2290
+ gap: 4,
2291
+ fontSize: 13
2292
+ },
2293
+ children: ["节点类型", /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2294
+ style: inputStyle,
2295
+ value: form.type,
2296
+ onChange: (e) => setForm((f) => ({
2297
+ ...f,
2298
+ type: e.target.value
2299
+ })),
2300
+ children: [
2301
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2302
+ value: "remote-ssh",
2303
+ children: "远程 SSH 宿主机"
2304
+ }),
2305
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2306
+ value: "remote-docker",
2307
+ children: "远程 Docker(SSH 通道)"
2308
+ }),
2309
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2310
+ value: "local-host",
2311
+ children: "默认环境(本地)"
2312
+ }),
2313
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2314
+ value: "local-docker",
2315
+ children: "本地 Docker 容器"
2316
+ })
2317
+ ]
2318
+ })]
2319
+ }),
2320
+ (form.type === "remote-ssh" || form.type === "remote-docker") && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2321
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2322
+ className: "dshb-form-row",
2323
+ style: {
2324
+ display: "flex",
2325
+ gap: 10
2326
+ },
2327
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2328
+ style: {
2329
+ display: "flex",
2330
+ flexDirection: "column",
2331
+ gap: 4,
2332
+ fontSize: 13,
2333
+ flex: 1,
2334
+ minWidth: 0
2335
+ },
2336
+ children: ["主机地址", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2337
+ style: inputStyle,
2338
+ value: form.host,
2339
+ onChange: (e) => setForm((f) => ({
2340
+ ...f,
2341
+ host: e.target.value
2342
+ })),
2343
+ placeholder: "192.168.1.10"
2344
+ })]
2345
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2346
+ style: {
2347
+ display: "flex",
2348
+ flexDirection: "column",
2349
+ gap: 4,
2350
+ fontSize: 13,
2351
+ width: 90
2352
+ },
2353
+ children: ["端口", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2354
+ style: inputStyle,
2355
+ value: form.port,
2356
+ onChange: (e) => setForm((f) => ({
2357
+ ...f,
2358
+ port: e.target.value
2359
+ }))
2360
+ })]
2361
+ })]
2362
+ }),
2363
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2364
+ style: {
2365
+ display: "flex",
2366
+ flexDirection: "column",
2367
+ gap: 4,
2368
+ fontSize: 13
2369
+ },
2370
+ children: ["用户名", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2371
+ style: inputStyle,
2372
+ value: form.username,
2373
+ onChange: (e) => setForm((f) => ({
2374
+ ...f,
2375
+ username: e.target.value
2376
+ })),
2377
+ placeholder: "root"
2378
+ })]
2379
+ }),
2380
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2381
+ style: {
2382
+ display: "flex",
2383
+ flexDirection: "column",
2384
+ gap: 4,
2385
+ fontSize: 13
2386
+ },
2387
+ children: ["认证方式", /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2388
+ style: inputStyle,
2389
+ value: form.authKind,
2390
+ onChange: (e) => setForm((f) => ({
2391
+ ...f,
2392
+ authKind: e.target.value
2393
+ })),
2394
+ children: [
2395
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2396
+ value: "password",
2397
+ children: "密码"
2398
+ }),
2399
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2400
+ value: "key",
2401
+ children: "私钥"
2402
+ }),
2403
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2404
+ value: "agent",
2405
+ children: "ssh-agent"
2406
+ })
2407
+ ]
2408
+ })]
2409
+ }),
2410
+ form.authKind === "password" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2411
+ style: {
2412
+ display: "flex",
2413
+ flexDirection: "column",
2414
+ gap: 4,
2415
+ fontSize: 13
2416
+ },
2417
+ children: [
2418
+ "密码",
2419
+ selectedNode?.hasSecret.hasPassword ? "(已保存,留空保持不变)" : "",
2420
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2421
+ style: inputStyle,
2422
+ type: "password",
2423
+ value: form.password,
2424
+ onChange: (e) => setForm((f) => ({
2425
+ ...f,
2426
+ password: e.target.value
2427
+ })),
2428
+ autoComplete: "new-password"
2429
+ })
2430
+ ]
2431
+ }),
2432
+ form.authKind === "key" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2433
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2434
+ style: {
2435
+ display: "flex",
2436
+ flexDirection: "column",
2437
+ gap: 4,
2438
+ fontSize: 13
2439
+ },
2440
+ children: [
2441
+ "私钥内容",
2442
+ selectedNode?.hasSecret.hasKey ? "(已保存,留空保持不变)" : "",
2443
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
2444
+ style: {
2445
+ ...inputStyle,
2446
+ minHeight: 90,
2447
+ resize: "vertical"
2448
+ },
2449
+ value: form.privateKey,
2450
+ onChange: (e) => setForm((f) => ({
2451
+ ...f,
2452
+ privateKey: e.target.value
2453
+ })),
2454
+ placeholder: "-----BEGIN OPENSSH PRIVATE KEY-----"
2455
+ })
2456
+ ]
2457
+ }),
2458
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2459
+ style: {
2460
+ display: "flex",
2461
+ flexDirection: "column",
2462
+ gap: 4,
2463
+ fontSize: 13
2464
+ },
2465
+ children: ["私钥 passphrase(可选)", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2466
+ style: inputStyle,
2467
+ type: "password",
2468
+ value: form.passphrase,
2469
+ onChange: (e) => setForm((f) => ({
2470
+ ...f,
2471
+ passphrase: e.target.value
2472
+ })),
2473
+ autoComplete: "new-password"
2474
+ })]
2475
+ }),
2476
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2477
+ style: {
2478
+ display: "flex",
2479
+ flexDirection: "column",
2480
+ gap: 4,
2481
+ fontSize: 13
2482
+ },
2483
+ children: ["或私钥文件路径(可选,优先使用内容)", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2484
+ style: inputStyle,
2485
+ value: form.keyPath,
2486
+ onChange: (e) => setForm((f) => ({
2487
+ ...f,
2488
+ keyPath: e.target.value
2489
+ })),
2490
+ placeholder: "~/.ssh/id_ed25519"
2491
+ })]
2492
+ })
2493
+ ] })
2494
+ ] }),
2495
+ (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", {
2496
+ style: {
2497
+ display: "flex",
2498
+ flexDirection: "column",
2499
+ gap: 4,
2500
+ fontSize: 13
2501
+ },
2502
+ children: ["镜像地址", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2503
+ style: inputStyle,
2504
+ value: form.dockerImage,
2505
+ onChange: (e) => setForm((f) => ({
2506
+ ...f,
2507
+ dockerImage: e.target.value
2508
+ })),
2509
+ placeholder: "alpine:latest"
2510
+ })]
2511
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2512
+ className: "dshb-form-row",
2513
+ style: {
2514
+ display: "flex",
2515
+ gap: 10
2516
+ },
2517
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2518
+ style: {
2519
+ display: "flex",
2520
+ flexDirection: "column",
2521
+ gap: 4,
2522
+ fontSize: 13,
2523
+ flex: 1,
2524
+ minWidth: 0
2525
+ },
2526
+ children: ["CPU 核数(可选)", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2527
+ style: inputStyle,
2528
+ value: form.dockerCpus,
2529
+ onChange: (e) => setForm((f) => ({
2530
+ ...f,
2531
+ dockerCpus: e.target.value
2532
+ })),
2533
+ placeholder: "2"
2534
+ })]
2535
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2536
+ style: {
2537
+ display: "flex",
2538
+ flexDirection: "column",
2539
+ gap: 4,
2540
+ fontSize: 13,
2541
+ flex: 1,
2542
+ minWidth: 0
2543
+ },
2544
+ children: ["内存 MB(可选)", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2545
+ style: inputStyle,
2546
+ value: form.dockerMemory,
2547
+ onChange: (e) => setForm((f) => ({
2548
+ ...f,
2549
+ dockerMemory: e.target.value
2550
+ })),
2551
+ placeholder: "512"
2552
+ })]
2553
+ })]
2554
+ })] }),
2555
+ testResult !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2556
+ style: {
2557
+ fontSize: 13,
2558
+ color: testResult === "连接成功" ? "var(--dsw-alias-state-success-primary)" : "var(--dsw-alias-state-error-primary)"
2559
+ },
2560
+ children: testResult
2561
+ }),
2562
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2563
+ style: {
2564
+ display: "flex",
2565
+ gap: 10,
2566
+ marginTop: 6,
2567
+ flexWrap: "wrap"
2568
+ },
2569
+ children: [
2570
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2571
+ type: "button",
2572
+ onClick: () => void save(),
2573
+ disabled: busy,
2574
+ style: primaryButtonStyle,
2575
+ children: selectedId === "new" ? "创建节点" : "保存"
2576
+ }),
2577
+ (form.type === "remote-ssh" || form.type === "remote-docker" || selectedId !== "new" && form.type === "local-docker") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2578
+ type: "button",
2579
+ onClick: () => void test(),
2580
+ disabled: busy || testing,
2581
+ style: secondaryButtonStyle,
2582
+ children: testing ? "测试中…" : "测试连接"
2583
+ }),
2584
+ selectedId !== "new" && (form.type === "local-docker" || form.type === "remote-docker") && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2585
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2586
+ type: "button",
2587
+ onClick: () => void dockerAction("provision"),
2588
+ disabled: busy || dockerBusy,
2589
+ style: primaryButtonStyle,
2590
+ children: dockerBusy ? "操作中…" : "拉起容器"
2591
+ }),
2592
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2593
+ type: "button",
2594
+ onClick: () => void dockerAction("restart"),
2595
+ disabled: busy || dockerBusy,
2596
+ style: secondaryButtonStyle,
2597
+ children: dockerBusy ? "操作中…" : "重启容器"
2598
+ }),
2599
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2600
+ type: "button",
2601
+ onClick: () => void dockerAction("stop"),
2602
+ disabled: busy || dockerBusy,
2603
+ style: secondaryButtonStyle,
2604
+ children: dockerBusy ? "操作中…" : "停止容器"
2605
+ })
2606
+ ] }),
2607
+ selectedId !== "new" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2608
+ type: "button",
2609
+ onClick: () => void remove(),
2610
+ disabled: busy,
2611
+ style: dangerOutlineButtonStyle,
2612
+ children: "删除"
2613
+ }),
2614
+ notice && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2615
+ style: {
2616
+ fontSize: 13,
2617
+ alignSelf: "center",
2618
+ color: notice.kind === "ok" ? "var(--dsw-alias-state-success-primary)" : "var(--dsw-alias-state-error-primary)"
2619
+ },
2620
+ children: notice.text
2621
+ })
2622
+ ]
2623
+ })
2624
+ ]
2625
+ })]
2626
+ })]
2627
+ })] });
2628
+ }
2629
+ function apply$1(ctx) {
2630
+ const globalStyle = document.createElement("style");
2631
+ globalStyle.textContent = `
2632
+ /* 仅在页面根阻止整页横向滚动。绝对定位浮层(模型菜单、上下文统计面板等)
2633
+ 依赖祖先 overflow:visible 才能展开到输入框之外,禁止给任何中间祖先加裁剪。 */
2634
+ html, body { overflow-x: hidden !important; max-width: 100vw !important }
2635
+ * { min-width: 0 !important; }
2636
+ table { display: block !important; overflow-x: auto !important; }
2637
+ img, video, canvas, svg { max-width: 100% !important; height: auto !important }
2638
+ pre, code { white-space: pre-wrap !important; word-break: break-all !important }
2639
+ input, select, textarea { max-width: 100% !important; }
2640
+ /* 上下文用量弹窗 .JObwrW_panel(ContextMeter):解除宽度塌缩并允许溢出显示。
2641
+ JObwrW_ 为上游 CSS 模块 hash,上游升级需同步。 */
2642
+ .JObwrW_panel { max-width: none !important; overflow: visible !important; }
2643
+ /* 子代理切换弹窗(dsh-client-ui-subagent):menu 是 flex column + max-height + overflow:auto,
2644
+ position:fixed 经 portal 渲染。子代理很多时 flex 压缩 node(flex-shrink:1 默认),
2645
+ 文字挤压且不触发滚动。给 node flex-shrink:0 让 menu 整体 overflow:auto 滚动。 */
2646
+ [class*="_menu"] [class*="_node"] { flex-shrink: 0 !important; }
2647
+ /* 模型选择弹窗(dsh-client-ui-model-selection):Ra_menu 上游 max-width 420px,
2648
+ 长模型名单行放不下被 ellipsis 截断。放宽到 720px 让模型名单行完整显示。
2649
+ [class*="Ra_menu"] 精确匹配模型弹窗,避免误伤其他菜单。Ra_ 为 hash,上游升级需同步。 */
2650
+ [class*="Ra_menu"] { max-width: 720px !important; width: auto !important; }
2651
+ /* 终端组件(dsh-web-frontend,CSS 模块前缀 1gdtu_)桌面端修复:上游 _command_ 默认
2652
+ overflow:hidden+text-overflow:ellipsis,LLM 调 bash 的长命令在桌面端被省略号
2653
+ 截断、看不到完整内容(输出区 _output_ 本就可横向滚动,唯命令区不行)。宽屏与
2654
+ mobile.tsx 的窄屏规则等效:行链 _prompt_/_promptLine_/_command_ 撑到 max-content、
2655
+ overflow 可见,长行溢出传入 _header_(本就 overflow-y:auto)的 scrollWidth,整个
2656
+ 命令区随 _header_ 横向滚动;copyButton 贴右缘保持可见。与窄屏规则共用 1gdtu_
2657
+ hash,上游升级需同步。桌面保留可见滚动条(不隐藏、不加 touch-action)。 */
2658
+ @media (min-width: 1024px) {
2659
+ [data-phase] [class*="_header_1gdtu_"] {
2660
+ min-width: 0 !important;
2661
+ max-width: 100% !important;
2662
+ overflow-x: auto !important;
2663
+ }
2664
+ [data-phase] [class*="_prompt_1gdtu_"],
2665
+ [data-phase] [class*="_promptLine_1gdtu_"],
2666
+ [data-phase] [class*="_command_1gdtu_"] {
2667
+ min-width: max-content !important;
2668
+ max-width: none !important;
2669
+ overflow: visible !important;
2670
+ }
2671
+ [data-phase] [class*="_command_1gdtu_"] {
2672
+ display: inline-block;
2673
+ text-overflow: clip !important;
2674
+ white-space: pre !important;
2675
+ }
2676
+ [data-phase] [class*="_copyButton_1gdtu_"] { position: sticky !important; right: 0; top: 0; }
2677
+ }
2678
+ `;
2679
+ document.head.appendChild(globalStyle);
2680
+ installMobile(ctx);
2681
+ ctx.plugin({
2682
+ inject: ["slots", "settingsScope"],
2683
+ apply: (sub) => {
2684
+ sub.slots.inject("settings.section", () => sub.slots.register({
2685
+ name: "settings.section",
2686
+ id: SECTION_ID,
2687
+ order: 110,
2688
+ label: () => "工作节点"
2689
+ }, NodeSection));
2690
+ sub.slots.inject("conversation.hero.workspace.directoryFlow", () => sub.slots.register({
2691
+ name: "conversation.hero.workspace.directoryFlow",
2692
+ priority: -10
2693
+ }, DirectoryFlowOccupant));
2694
+ sub.slots.inject("sidebar.workspaces.directoryFlow", () => sub.slots.register({
2695
+ name: "sidebar.workspaces.directoryFlow",
2696
+ priority: -10
2697
+ }, DirectoryFlowOccupant));
2698
+ }
2699
+ });
2700
+ const workspaces = ctx.workspaces;
2701
+ if (workspaces && typeof workspaces.openPath === "function") {
2702
+ const originalOpenPath = workspaces.openPath.bind(workspaces);
2703
+ workspaces.openPath = async (path) => {
2704
+ try {
2705
+ const res = await fetch(`/api/dshb/remote/bound?path=${encodeURIComponent(path)}`);
2706
+ if (res.ok) {
2707
+ const data = await res.json();
2708
+ if (data.bound) {
2709
+ if (data.kind !== "file") return originalOpenPath(path);
2710
+ const a = document.createElement("a");
2711
+ a.href = `/api/dshb/remote/file?path=${encodeURIComponent(path)}`;
2712
+ a.download = "";
2713
+ document.body.appendChild(a);
2714
+ a.click();
2715
+ a.remove();
2716
+ return;
2717
+ }
2718
+ }
2719
+ } catch {}
2720
+ return originalOpenPath(path);
2721
+ };
2722
+ }
2723
+ const ICON_SVG = {
2724
+ auth: `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M8 7C9.3807 7 10.5 5.8807 10.5 4.5C10.5 3.1193 9.3807 2 8 2C6.6193 2 5.5 3.1193 5.5 4.5C5.5 5.8807 6.6193 7 8 7Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M3 14C3 11.2386 5.2386 9 8 9C10.7614 9 13 11.2386 13 14" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
2725
+ "dshb-nodes": `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="3.5" cy="4" r="2" stroke="currentColor" stroke-width="1.5"/><circle cx="12.5" cy="4" r="2" stroke="currentColor" stroke-width="1.5"/><circle cx="8" cy="12" r="2" stroke="currentColor" stroke-width="1.5"/><path d="M3.5 6V8C3.5 9.1 4.4 10 5.5 10H8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M12.5 6V8C12.5 9.1 11.6 10 10.5 10H8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>`
2726
+ };
2727
+ const ICON_LABELS = {
2728
+ "认证": "auth",
2729
+ "工作节点": "dshb-nodes"
2730
+ };
2731
+ new MutationObserver(() => {
2732
+ const cells = document.querySelectorAll("button[class*=\"navCell\"]");
2733
+ for (const cell of Array.from(cells)) {
2734
+ const label = cell.querySelector("span[class*=\"navLabel\"]");
2735
+ if (!label) continue;
2736
+ const text = label.textContent ?? "";
2737
+ const iconKey = ICON_LABELS[text];
2738
+ if (!iconKey) continue;
2739
+ const iconSlot = cell.querySelector("[class*=\"navIcon\"]");
2740
+ if (!iconSlot) continue;
2741
+ if (iconSlot.getAttribute("data-dshb-icon") === iconKey) continue;
2742
+ iconSlot.innerHTML = ICON_SVG[iconKey] ?? "";
2743
+ iconSlot.setAttribute("data-dshb-icon", iconKey);
2744
+ }
2745
+ }).observe(document.body, {
2746
+ childList: true,
2747
+ subtree: true
2748
+ });
2749
+ }
2750
+ //#endregion
2751
+ //#region src/client/index.tsx
2752
+ const inject = [
2753
+ "connection",
2754
+ "workspaces",
2755
+ "slots",
2756
+ "layout",
2757
+ "conversation"
2758
+ ];
2759
+ function apply(ctx) {
2760
+ apply$2(ctx);
2761
+ apply$1(ctx);
2762
+ }
2763
+ //#endregion
2764
+ exports.apply = apply;
2765
+ exports.inject = inject;
2766
+ return module.exports;
2767
+ }
2768
+ });
2769
+
2770
+ //# sourceMappingURL=client.js.map