dshb-ui 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js ADDED
@@ -0,0 +1,1868 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dshb-ui",
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/directory-flow.tsx
10
+ const inputStyle$1 = {
11
+ width: "100%",
12
+ boxSizing: "border-box",
13
+ padding: "0 10px",
14
+ height: 32,
15
+ border: "1px solid var(--dsw-alias-border-l2)",
16
+ borderRadius: 8,
17
+ fontSize: 14,
18
+ fontFamily: "inherit",
19
+ background: "var(--dsw-alias-bg-layer-1)",
20
+ color: "var(--dsw-alias-label-primary)",
21
+ outline: "none"
22
+ };
23
+ const buttonStyle$1 = {
24
+ boxSizing: "border-box",
25
+ height: 36,
26
+ padding: "0 14px",
27
+ borderRadius: 18,
28
+ fontSize: 14,
29
+ lineHeight: "22px",
30
+ fontFamily: "inherit",
31
+ cursor: "pointer",
32
+ border: "none",
33
+ display: "inline-flex",
34
+ justifyContent: "center",
35
+ alignItems: "center",
36
+ gap: 4
37
+ };
38
+ const primaryButtonStyle$1 = {
39
+ ...buttonStyle$1,
40
+ background: "var(--dsw-alias-button-primary-fill)",
41
+ color: "var(--dsw-alias-label-primary-foreground)"
42
+ };
43
+ const secondaryButtonStyle$1 = {
44
+ ...buttonStyle$1,
45
+ border: "1px solid var(--dsw-alias-border-l2)",
46
+ color: "var(--dsw-alias-label-primary)",
47
+ background: "transparent"
48
+ };
49
+ const overlayStyle = {
50
+ position: "fixed",
51
+ inset: 0,
52
+ background: "var(--dsw-alias-overlay-mask, rgba(0,0,0,0.45))",
53
+ display: "flex",
54
+ alignItems: "center",
55
+ justifyContent: "center",
56
+ zIndex: 1e3
57
+ };
58
+ const dialogStyle = {
59
+ width: "min(92vw, 560px)",
60
+ maxWidth: "100%",
61
+ maxHeight: "80vh",
62
+ overflow: "auto",
63
+ overflowX: "hidden",
64
+ background: "var(--dsw-alias-bg-layer-1)",
65
+ border: "1px solid var(--dsw-alias-border-l2)",
66
+ borderRadius: 12,
67
+ padding: 20,
68
+ boxSizing: "border-box",
69
+ color: "var(--dsw-alias-label-primary)"
70
+ };
71
+ function joinPath(base, name) {
72
+ return `${base.replace(/\/+$/, "")}/${name}`;
73
+ }
74
+ function DirectoryFlowOccupant(props) {
75
+ const [nodes, setNodes] = (0, react.useState)([]);
76
+ const [nodeId, setNodeId] = (0, react.useState)("");
77
+ const [path, setPath] = (0, react.useState)("");
78
+ const [entries, setEntries] = (0, react.useState)([]);
79
+ const [error, setError] = (0, react.useState)(void 0);
80
+ const [busy, setBusy] = (0, react.useState)(false);
81
+ const [newDirName, setNewDirName] = (0, react.useState)("");
82
+ (0, react.useEffect)(() => {
83
+ if (!props.open) return;
84
+ (async () => {
85
+ try {
86
+ const res = await fetch("/api/dshb/nodes");
87
+ if (!res.ok) return;
88
+ const data = await res.json();
89
+ setNodes((data.nodes ?? []).filter((n) => n.type === "local-host" || n.status?.reachable === true));
90
+ } catch {}
91
+ })();
92
+ }, [props.open]);
93
+ const browse = (0, react.useCallback)(async (id, dir) => {
94
+ setError(void 0);
95
+ try {
96
+ const res = await fetch(`/api/dshb/nodes/${id}/browse?path=${encodeURIComponent(dir)}`);
97
+ const data = await res.json();
98
+ if (!res.ok || !data.ok) {
99
+ setError(data.error ?? "目录读取失败");
100
+ return;
101
+ }
102
+ setPath(data.path ?? dir);
103
+ setEntries((data.entries ?? []).filter((e) => e.isDirectory));
104
+ } catch {
105
+ setError("网络错误");
106
+ }
107
+ }, []);
108
+ const selectNode = (0, react.useCallback)((id) => {
109
+ setNodeId(id);
110
+ setPath("");
111
+ setEntries([]);
112
+ setError(void 0);
113
+ browse(id, "/");
114
+ }, [browse]);
115
+ const mkdir = (0, react.useCallback)(async () => {
116
+ if (!newDirName.trim()) return;
117
+ setBusy(true);
118
+ setError(void 0);
119
+ try {
120
+ const target = joinPath(path, newDirName.trim());
121
+ const res = await fetch(`/api/dshb/nodes/${nodeId}/mkdir`, {
122
+ method: "POST",
123
+ headers: { "content-type": "application/json" },
124
+ body: JSON.stringify({ path: target })
125
+ });
126
+ const data = await res.json();
127
+ if (!res.ok || !data.ok) {
128
+ setError(data.error ?? "创建目录失败");
129
+ return;
130
+ }
131
+ setNewDirName("");
132
+ browse(nodeId, target);
133
+ } catch {
134
+ setError("网络错误");
135
+ } finally {
136
+ setBusy(false);
137
+ }
138
+ }, [
139
+ newDirName,
140
+ nodeId,
141
+ path,
142
+ browse
143
+ ]);
144
+ const confirm = (0, react.useCallback)(async () => {
145
+ if (!nodeId || !path) {
146
+ setError("请选择节点与目录");
147
+ return;
148
+ }
149
+ setBusy(true);
150
+ setError(void 0);
151
+ try {
152
+ const node = nodes.find((n) => n.id === nodeId);
153
+ if (!node) return;
154
+ if (node.type === "local-host") {
155
+ props.onPicked(path);
156
+ return;
157
+ }
158
+ const res = await fetch("/api/dshb/workspaces/bind", {
159
+ method: "POST",
160
+ headers: { "content-type": "application/json" },
161
+ body: JSON.stringify({
162
+ nodeId,
163
+ remotePath: path
164
+ })
165
+ });
166
+ const data = await res.json();
167
+ if (!res.ok || !data.ok || !data.mirrorPath) {
168
+ setError(data.error ?? "工作区注册失败");
169
+ return;
170
+ }
171
+ props.onPicked(data.mirrorPath);
172
+ } catch {
173
+ setError("网络错误");
174
+ } finally {
175
+ setBusy(false);
176
+ }
177
+ }, [
178
+ nodeId,
179
+ path,
180
+ nodes,
181
+ props
182
+ ]);
183
+ if (!props.open) return null;
184
+ const node = nodes.find((n) => n.id === nodeId);
185
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
186
+ style: overlayStyle,
187
+ role: "dialog",
188
+ "aria-label": "添加工作区",
189
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
190
+ style: dialogStyle,
191
+ children: [
192
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
193
+ style: {
194
+ display: "flex",
195
+ justifyContent: "space-between",
196
+ alignItems: "center",
197
+ marginBottom: 14
198
+ },
199
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
200
+ style: {
201
+ margin: 0,
202
+ fontSize: 16,
203
+ color: "var(--dsw-alias-label-primary)"
204
+ },
205
+ children: "添加工作区"
206
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
207
+ type: "button",
208
+ onClick: props.onCancel,
209
+ style: secondaryButtonStyle$1,
210
+ children: "关闭"
211
+ })]
212
+ }),
213
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
214
+ style: {
215
+ display: "block",
216
+ fontSize: 12,
217
+ color: "var(--dsw-alias-label-secondary)",
218
+ marginBottom: 4
219
+ },
220
+ children: "工作节点"
221
+ }),
222
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
223
+ style: {
224
+ ...inputStyle$1,
225
+ marginBottom: 14
226
+ },
227
+ value: nodeId,
228
+ onChange: (e) => selectNode(e.target.value),
229
+ disabled: props.busy || busy,
230
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
231
+ value: "",
232
+ children: "选择节点…"
233
+ }), nodes.map((n) => {
234
+ const typeLabel = n.type === "local-host" ? "本地" : n.type === "remote-ssh" ? "远程 SSH" : n.type === "local-docker" ? "本地 Docker" : n.type === "remote-docker" ? "远程 Docker" : n.type;
235
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
236
+ value: n.id,
237
+ children: [
238
+ n.name,
239
+ "(",
240
+ typeLabel,
241
+ ")"
242
+ ]
243
+ }, n.id);
244
+ })]
245
+ }),
246
+ nodeId !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
247
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
248
+ style: {
249
+ display: "flex",
250
+ justifyContent: "space-between",
251
+ alignItems: "center",
252
+ marginBottom: 6
253
+ },
254
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
255
+ style: {
256
+ fontSize: 12,
257
+ color: "var(--dsw-alias-label-secondary)"
258
+ },
259
+ children: [
260
+ "当前目录(",
261
+ node?.type === "local-host" ? "本机" : "远端",
262
+ ")"
263
+ ]
264
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
265
+ type: "button",
266
+ onClick: () => void browse(nodeId, dirnameOf(path)),
267
+ disabled: busy,
268
+ style: secondaryButtonStyle$1,
269
+ children: "上一级"
270
+ })]
271
+ }),
272
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
273
+ style: {
274
+ ...inputStyle$1,
275
+ marginBottom: 8,
276
+ height: "auto",
277
+ minHeight: 32,
278
+ display: "flex",
279
+ alignItems: "center",
280
+ flexWrap: "wrap",
281
+ gap: 2,
282
+ cursor: "pointer",
283
+ lineHeight: "32px",
284
+ overflow: "hidden"
285
+ },
286
+ onClick: () => void browse(nodeId, path),
287
+ children: path.split("/").filter(Boolean).length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
288
+ style: { opacity: .5 },
289
+ children: "根目录"
290
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
291
+ onClick: (ev) => {
292
+ ev.stopPropagation();
293
+ browse(nodeId, "/");
294
+ },
295
+ style: {
296
+ cursor: "pointer",
297
+ color: "var(--dsw-alias-label-secondary)"
298
+ },
299
+ children: "/"
300
+ }), path.split("/").filter(Boolean).map((seg, i, arr) => {
301
+ const dir = "/" + arr.slice(0, i + 1).join("/");
302
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
303
+ style: {
304
+ display: "inline-flex",
305
+ alignItems: "center",
306
+ gap: 2
307
+ },
308
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
309
+ onClick: (ev) => {
310
+ ev.stopPropagation();
311
+ browse(nodeId, dir);
312
+ },
313
+ style: { cursor: "pointer" },
314
+ children: seg
315
+ }), i < arr.length - 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
316
+ style: { opacity: .4 },
317
+ children: "/"
318
+ })]
319
+ }, i);
320
+ })] })
321
+ }),
322
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
323
+ style: {
324
+ maxHeight: 200,
325
+ overflow: "auto",
326
+ border: "1px solid var(--dsw-alias-border-l2)",
327
+ borderRadius: 6,
328
+ marginBottom: 10
329
+ },
330
+ children: [entries.map((e) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
331
+ onClick: () => void browse(nodeId, e.path),
332
+ style: {
333
+ padding: "7px 10px",
334
+ cursor: "pointer",
335
+ fontSize: 13,
336
+ borderRadius: 6,
337
+ color: "var(--dsw-alias-label-primary)",
338
+ display: "flex",
339
+ alignItems: "center",
340
+ gap: 6,
341
+ overflow: "hidden"
342
+ },
343
+ onMouseEnter: (ev) => ev.currentTarget.style.background = "var(--dsw-alias-interactive-bg-hover)",
344
+ onMouseLeave: (ev) => ev.currentTarget.style.background = "transparent",
345
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
346
+ style: { flexShrink: 0 },
347
+ children: "📁"
348
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
349
+ style: {
350
+ overflow: "hidden",
351
+ textOverflow: "ellipsis",
352
+ whiteSpace: "nowrap"
353
+ },
354
+ children: e.name
355
+ })]
356
+ }, e.path)), entries.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
357
+ style: {
358
+ padding: 12,
359
+ fontSize: 12,
360
+ color: "var(--dsw-alias-label-tertiary)"
361
+ },
362
+ children: "无子目录"
363
+ })]
364
+ }),
365
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
366
+ style: {
367
+ display: "flex",
368
+ gap: 8,
369
+ marginBottom: 12
370
+ },
371
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
372
+ style: {
373
+ ...inputStyle$1,
374
+ flex: 1,
375
+ minWidth: 0
376
+ },
377
+ value: newDirName,
378
+ onChange: (e) => setNewDirName(e.target.value),
379
+ placeholder: "在当前目录下新建子目录…",
380
+ onKeyDown: (e) => {
381
+ if (e.key === "Enter") mkdir();
382
+ }
383
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
384
+ type: "button",
385
+ onClick: () => void mkdir(),
386
+ disabled: busy || !newDirName.trim(),
387
+ style: {
388
+ ...secondaryButtonStyle$1,
389
+ flexShrink: 0,
390
+ whiteSpace: "nowrap"
391
+ },
392
+ children: "新建"
393
+ })]
394
+ })
395
+ ] }),
396
+ error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
397
+ style: {
398
+ fontSize: 13,
399
+ color: "var(--dsw-alias-state-error-primary)",
400
+ marginBottom: 10
401
+ },
402
+ children: error
403
+ }),
404
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
405
+ style: {
406
+ display: "flex",
407
+ justifyContent: "flex-end",
408
+ gap: 10
409
+ },
410
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
411
+ type: "button",
412
+ onClick: props.onCancel,
413
+ style: secondaryButtonStyle$1,
414
+ children: "取消"
415
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
416
+ type: "button",
417
+ onClick: () => void confirm(),
418
+ disabled: busy || props.busy || !nodeId || !path,
419
+ style: primaryButtonStyle$1,
420
+ children: "设为工作区"
421
+ })]
422
+ })
423
+ ]
424
+ })
425
+ });
426
+ }
427
+ function dirnameOf(path) {
428
+ const parts = path.split("/").filter(Boolean);
429
+ parts.pop();
430
+ return parts.length === 0 ? "/" : `/${parts.join("/")}`;
431
+ }
432
+ //#endregion
433
+ //#region src/client/mobile.tsx
434
+ /**
435
+ * dshb 移动端屏幕适配(参考 dsh-web-mobile 实现,仅保留页面适配与目录抽屉
436
+ * 切换,不含文件浏览 / 导出会话日志等功能按钮)。
437
+ *
438
+ * 机制与上游一致:dsh 客户端在窄屏会自动给 AppFrame 加 data-sidebar-collapsed
439
+ * 折叠侧边栏;这里把侧边栏列(frame 的第一个 grid 子元素)用 CSS 抽屉化
440
+ * (绝对定位 + transform 移出/滑入),并在会话头部注入一个切换按钮调用
441
+ * ctx.layout.toggleSidebar()。窄屏判定与上游一致(<1024 且粗指针,避免桌面
442
+ * 小窗误触发)。
443
+ */
444
+ const MOBILE_QUERY = "(max-width: 1023px) and (pointer: coarse)";
445
+ const FRAME_ATTR = "data-dshb-mobile";
446
+ const COLLAPSED_ATTR = "data-sidebar-collapsed";
447
+ const MOBILE_CSS = `
448
+ @media (max-width: 1023px) and (pointer: coarse) {
449
+ html, body { touch-action: pan-y pinch-zoom !important; overscroll-behavior-x: none !important; }
450
+
451
+ [${FRAME_ATTR}="frame"] {
452
+ box-sizing: border-box !important;
453
+ position: relative !important;
454
+ grid-template-columns: minmax(0, 1fr) 0 0 !important;
455
+ padding-top: env(safe-area-inset-top, 0px) !important;
456
+ }
457
+
458
+ /* 侧边栏列 -> 抽屉:默认移出屏幕,展开(无 collapsed)时滑入 */
459
+ [${FRAME_ATTR}="frame"] > :first-child {
460
+ position: absolute !important;
461
+ inset: 0 auto 0 0 !important;
462
+ width: max-content;
463
+ max-width: 92vw;
464
+ z-index: 40 !important;
465
+ transform: translateX(-110%);
466
+ transition: transform .28s ease-in-out;
467
+ background: var(--dsw-alias-bg-base, #ffffff);
468
+ padding-top: env(safe-area-inset-top, 0px) !important;
469
+ border-right: none !important;
470
+ touch-action: pan-y pinch-zoom !important;
471
+ }
472
+ [${FRAME_ATTR}="frame"]:not([${COLLAPSED_ATTR}]) > :first-child { transform: none !important; }
473
+ @media (prefers-reduced-motion: reduce) {
474
+ [${FRAME_ATTR}="frame"] > :first-child { transition: none !important; }
475
+ }
476
+
477
+ /* 拖拽手柄在触屏上无用 */
478
+ [data-side="sidebar"], [data-side="details"] { display: none !important; }
479
+
480
+ /* 对话区:去桌面滚动条占位,收窄 gutter,字号降一档 */
481
+ [data-phase] [class*="_scrollBody"] { scrollbar-gutter: auto !important; scrollbar-width: none; }
482
+ [data-phase] [class*="_scrollBody"]::-webkit-scrollbar { display: none !important; width: 0; height: 0; }
483
+ [data-phase] [class*="_scroll"]:not([class*="_scrollBody"]):has(p) { padding-left: 20px; padding-right: 20px; font-size: 15px !important; }
484
+ [data-phase] [class*="_scroll"]:not([class*="_scrollBody"]):has(p) p,
485
+ [data-phase] [class*="_scroll"]:not([class*="_scrollBody"]):has(p) li,
486
+ [data-phase] [class*="_scroll"]:not([class*="_scrollBody"]):has(p) [class*="_text_"] { font-size: 15px !important; }
487
+ [data-phase] table { width: 100%; max-width: 100%; }
488
+ [data-phase] th, [data-phase] td { max-width: none; min-width: 0; }
489
+ [data-phase] [class*="_scroll"]:not([class*="_scrollBody"]) img { width: auto !important; max-width: 100% !important; height: auto !important; max-height: 220px !important; }
490
+ [data-phase] [class*="_userStack"], [data-phase] [class*="_userStack"] [class*="_bubble"] { box-sizing: border-box; width: fit-content; max-width: 100%; }
491
+ [data-phase] [class*="_actions"] { overflow: hidden; }
492
+
493
+ /* 统计行(turns/steps/LLM/TPS):窄屏横向滚动,指标可滑动看全 */
494
+ [${FRAME_ATTR}="stats"] {
495
+ display: flex !important;
496
+ flex-wrap: nowrap !important;
497
+ align-items: center;
498
+ gap: 6px;
499
+ overflow-x: auto !important;
500
+ max-width: 100% !important;
501
+ scrollbar-width: none;
502
+ -webkit-overflow-scrolling: touch;
503
+ }
504
+ [${FRAME_ATTR}="stats"]::-webkit-scrollbar { display: none; }
505
+ [${FRAME_ATTR}="stats"] > * { flex-shrink: 0 !important; white-space: nowrap !important; }
506
+ [${FRAME_ATTR}="stats"] { gap: 6px !important; }
507
+ [class*="composerStack"] { gap: 4px !important; }
508
+ /* stats line 竖线分隔符(|)两边 margin 10px 太松,适度收紧 */
509
+ [data-phase] [class*="_sep"] { margin: 0 3px !important; }
510
+
511
+ /* 每轮回复下方的统计文字(footer):收紧 actions 内部 gap 与气泡到 footer 间距,减少松散感 */
512
+ [data-phase] [class*="_actions"] { gap: 4px !important; }
513
+ [data-phase] [class*="_actions"] [class*="_timeEnd"] { gap: 4px !important; padding-left: 6px !important; }
514
+ [data-phase] [class*="userRow"], [data-phase] [class*="assistantRow"] { gap: 4px !important; }
515
+ /* 点号(·)左右 margin 10px 太松,收紧并抵消前后空格 */
516
+ [data-phase] [class*="_runTimeDot"] { margin: 0 -2px !important; }
517
+ /* footer 容器各项间距 16px 偏松 */
518
+ [data-phase] [class*="_root"][class*="osXY9a"] { gap: 8px !important; }
519
+ /* footer 统计行窄屏可横向滑动看全,不溢出页面 */
520
+ [data-phase] [class*="_actions"] { max-width: 100% !important; overflow-x: auto !important; scrollbar-width: none; touch-action: pan-x !important; }
521
+ [data-phase] [class*="_actions"]::-webkit-scrollbar { display: none; }
522
+
523
+ /* 上下文用量弹窗(ContextMeter .JObwrW_panel):上游 dsh-client-ui-trajectory 的
524
+ [class*="panel"] 全局规则给它加了 max-width:100%!important,命中本弹窗,使宽度
525
+ 从 264px 塌缩到包含块(.root 28px)宽,内容竖排成竖条。窄屏解除该误伤。 */
526
+ .JObwrW_panel { max-width: none !important; overflow-x: visible !important; }
527
+
528
+ /* 头部:标题省略,tab 条横向滚动 */
529
+ [${FRAME_ATTR}="frame"] [data-phase] header { padding-left: 44px; padding-right: 8px; }
530
+ [${FRAME_ATTR}="frame"] [data-phase] header [class*="_crumbs"] { flex: 1 1 0; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap !important; }
531
+ [${FRAME_ATTR}="frame"] [data-phase] header [role="tablist"] { flex-wrap: nowrap; gap: 0 16px; overflow-x: auto; overscroll-behavior-x: contain; scrollbar-width: none; }
532
+ [${FRAME_ATTR}="frame"] [data-phase] header [role="tablist"]::-webkit-scrollbar { display: none; }
533
+ [${FRAME_ATTR}="frame"] [data-phase] header [role="tablist"] > button { flex-shrink: 0; white-space: nowrap; }
534
+
535
+ /* 设置对话框:窄屏近全宽 sheet(排除导出等普通对话框与目录选择器) */
536
+ [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"])):not(:has([class*="ZuhsRW"])) {
537
+ position: absolute !important;
538
+ left: 8px !important;
539
+ top: calc(env(safe-area-inset-top, 0px) + 12px) !important;
540
+ width: calc(100vw - 16px) !important;
541
+ max-width: calc(100vw - 16px) !important;
542
+ height: auto !important;
543
+ max-height: calc(100dvh - 24px - env(safe-area-inset-top, 0px)) !important;
544
+ flex-direction: column !important;
545
+ border-radius: 14px !important;
546
+ }
547
+ [aria-modal="true"]:not(:has(> :first-child > :last-child > button)) { max-width: calc(100vw - 32px) !important; }
548
+ /* nav:横排换行(避免 CJK 标签竖排楼梯状),隐藏冗余 caption */
549
+ [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"])):not(:has([class*="ZuhsRW"])) > :first-child {
550
+ width: 100%;
551
+ flex-direction: row !important;
552
+ align-items: center;
553
+ gap: 6px;
554
+ padding: 10px 12px 8px;
555
+ }
556
+ [aria-modal="true"]:has(> :first-child > :last-child > button):not(:has([role="navigation"])):not(:has([class*="ZuhsRW"])) > :first-child > :first-child { display: none !important; }
557
+ [aria-modal="true"] [class*="_navList"] { flex: 1 1 auto; min-width: 0; flex-direction: row !important; flex-wrap: wrap; gap: 6px; overflow: visible; }
558
+ [aria-modal="true"] [class*="_navList"] > button { flex-shrink: 0; white-space: nowrap; }
559
+ /* 外观模式卡片横排 */
560
+ [aria-modal="true"] [class*="_cubeRow"] { gap: 6px; }
561
+ [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; }
562
+ /* section 填充 sheet 宽度 */
563
+ [aria-modal="true"] [class*="_section"] { width: 100% !important; max-width: none !important; }
564
+ [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; }
565
+ [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; }
566
+
567
+ /* 切换按钮:固定头部左侧 */
568
+ [${FRAME_ATTR}="toggle"] {
569
+ position: absolute !important;
570
+ left: 8px !important;
571
+ top: 12px !important;
572
+ z-index: 2 !important;
573
+ display: inline-flex !important;
574
+ align-items: center; justify-content: center;
575
+ width: 28px; height: 28px;
576
+ padding: 0; border: none; background: none; cursor: pointer; color: inherit;
577
+ -webkit-tap-highlight-color: transparent;
578
+ }
579
+
580
+ /* 首页/hero(无活跃会话,session header 不存在)的抽屉切换 FAB:
581
+ 浮在顶部相机带下方,避免与 hero 头部内容重叠 */
582
+ [${FRAME_ATTR}="fab"] {
583
+ position: absolute !important;
584
+ top: calc(env(safe-area-inset-top, 0px) + 72px) !important;
585
+ left: 10px !important;
586
+ z-index: 21 !important;
587
+ display: inline-flex !important;
588
+ align-items: center; justify-content: center;
589
+ width: 38px; height: 38px;
590
+ padding: 0;
591
+ border: 1px solid var(--dsw-alias-border-l1, rgba(0, 0, 0, .12)) !important;
592
+ border-radius: 50% !important;
593
+ background: var(--dsw-alias-button-floating-fill, #ffffff) !important;
594
+ color: var(--dsw-alias-label-primary, inherit) !important;
595
+ cursor: pointer !important;
596
+ box-shadow: 0 2px 12px rgba(0, 0, 0, .18) !important;
597
+ -webkit-tap-highlight-color: transparent;
598
+ }
599
+ }
600
+
601
+ /* 宽屏 / 鼠标指针:隐藏切换按钮 */
602
+ @media (min-width: 1024px), (pointer: fine) {
603
+ [${FRAME_ATTR}="toggle"], [${FRAME_ATTR}="fab"] { display: none !important; }
604
+ }
605
+ `;
606
+ function findFrame() {
607
+ return document.querySelector("[data-shell-overlay]")?.parentElement ?? null;
608
+ }
609
+ function getFrame() {
610
+ return document.querySelector(`[${FRAME_ATTR}="frame"]`) ?? findFrame();
611
+ }
612
+ /** viewport-fit=cover:让 safe-area-inset 生效,内容避开刘海/状态栏。 */
613
+ function installViewport(ctx) {
614
+ ctx.effect(() => {
615
+ const narrow = window.matchMedia(MOBILE_QUERY);
616
+ const viewport = document.querySelector("meta[name=\"viewport\"]");
617
+ if (viewport === null) return () => {};
618
+ const original = viewport.content;
619
+ const sync = () => {
620
+ viewport.content = narrow.matches ? "width=device-width, initial-scale=1, viewport-fit=cover" : original;
621
+ };
622
+ sync();
623
+ narrow.addEventListener("change", sync);
624
+ return () => {
625
+ narrow.removeEventListener("change", sync);
626
+ viewport.content = original;
627
+ };
628
+ }, "dshb-mobile: viewport");
629
+ }
630
+ function installStyles(ctx) {
631
+ ctx.effect(() => {
632
+ const tag = document.createElement("style");
633
+ tag.dataset.plugin = "dshb-mobile";
634
+ tag.textContent = MOBILE_CSS;
635
+ document.head.appendChild(tag);
636
+ setTimeout(() => {
637
+ if (tag.isConnected) document.head.appendChild(tag);
638
+ }, 0);
639
+ return () => {
640
+ tag.remove();
641
+ };
642
+ }, "dshb-mobile: styles");
643
+ }
644
+ /** 给 AppFrame 打标记(drawer CSS 选择器挂载点),窄屏才打。 */
645
+ function installFrameMarker(ctx) {
646
+ ctx.effect(() => {
647
+ const narrow = window.matchMedia(MOBILE_QUERY);
648
+ let frame = null;
649
+ const clear = () => {
650
+ if (frame !== null) frame.removeAttribute(FRAME_ATTR);
651
+ frame = null;
652
+ };
653
+ const ensure = () => {
654
+ if (!narrow.matches) return;
655
+ frame = findFrame();
656
+ if (frame !== null && !frame.hasAttribute(FRAME_ATTR)) frame.setAttribute(FRAME_ATTR, "frame");
657
+ };
658
+ ensure();
659
+ const mo = new MutationObserver(ensure);
660
+ mo.observe(document.documentElement, {
661
+ childList: true,
662
+ subtree: true
663
+ });
664
+ const onChange = () => {
665
+ if (narrow.matches) ensure();
666
+ else clear();
667
+ };
668
+ narrow.addEventListener("change", onChange);
669
+ return () => {
670
+ mo.disconnect();
671
+ narrow.removeEventListener("change", onChange);
672
+ clear();
673
+ };
674
+ }, "dshb-mobile: frame marker");
675
+ }
676
+ /**
677
+ * 抽屉打开时覆盖内容区的半透明遮罩,点击关闭;Escape 同样关闭;
678
+ * 点击抽屉内的会话/导航项后关闭抽屉。
679
+ */
680
+ function installDrawerInteractions(ctx, toggleSidebar) {
681
+ ctx.effect(() => {
682
+ const narrow = window.matchMedia(MOBILE_QUERY);
683
+ let backdrop = null;
684
+ const drawerOpen = () => {
685
+ const frame = getFrame();
686
+ return narrow.matches && frame !== null && !frame.hasAttribute(COLLAPSED_ATTR);
687
+ };
688
+ const sync = () => {
689
+ if (drawerOpen() && backdrop === null) {
690
+ backdrop = document.createElement("div");
691
+ backdrop.setAttribute(`${FRAME_ATTR}-backdrop`, "");
692
+ backdrop.style.cssText = "position:fixed;inset:0;z-index:39;background:rgba(0,0,0,.4);-webkit-tap-highlight-color:transparent;";
693
+ backdrop.addEventListener("click", () => toggleSidebar());
694
+ document.body.appendChild(backdrop);
695
+ } else if (!drawerOpen() && backdrop !== null) {
696
+ backdrop.remove();
697
+ backdrop = null;
698
+ }
699
+ };
700
+ const onKeyDown = (event) => {
701
+ if (event.key !== "Escape") return;
702
+ if (document.querySelector("[aria-modal=\"true\"]") !== null) return;
703
+ if (drawerOpen()) toggleSidebar();
704
+ };
705
+ const onClick = (event) => {
706
+ if (!drawerOpen()) return;
707
+ const target = event.target;
708
+ if (!(target instanceof Element)) return;
709
+ const drawer = document.querySelector(`[${FRAME_ATTR}="frame"] > :first-child`);
710
+ if (drawer === null || !drawer.contains(target)) return;
711
+ if (target.closest("[class*=\"sessionRow\"] button") !== null) return;
712
+ if (target.closest("[class*=\"newSession\"], [class*=\"sessionRow\"], [role=\"treeitem\"], [class*=\"searchResult\"]") !== null) toggleSidebar();
713
+ };
714
+ sync();
715
+ const mo = new MutationObserver(sync);
716
+ mo.observe(document.documentElement, {
717
+ childList: true,
718
+ subtree: true,
719
+ attributes: true,
720
+ attributeFilter: [COLLAPSED_ATTR]
721
+ });
722
+ document.addEventListener("keydown", onKeyDown, true);
723
+ document.addEventListener("click", onClick, true);
724
+ narrow.addEventListener("change", sync);
725
+ return () => {
726
+ mo.disconnect();
727
+ document.removeEventListener("keydown", onKeyDown, true);
728
+ document.removeEventListener("click", onClick, true);
729
+ narrow.removeEventListener("change", sync);
730
+ backdrop?.remove();
731
+ };
732
+ }, "dshb-mobile: drawer interactions");
733
+ }
734
+ /** 对话/输入框下方的统计行(turns/steps/LLM/TPS):hashed class 无法直接选,按文本特征标记后由 CSS 横向滚动,避免溢出。 */
735
+ function installStatsLine(ctx) {
736
+ ctx.effect(() => {
737
+ const narrow = window.matchMedia(MOBILE_QUERY);
738
+ const STATS_LINE_RE = /(\d+\s*轮|\d+\s*步|\bturns\b|\bsteps\b)/;
739
+ const isStatsText = (el) => {
740
+ const text = (el.textContent ?? "").trim();
741
+ if (text.length === 0 || text.length > 160) return false;
742
+ if (!STATS_LINE_RE.test(text)) return false;
743
+ if (el.querySelector("textarea, input, button, select, a") !== null) return false;
744
+ return true;
745
+ };
746
+ const mark = () => {
747
+ if (!narrow.matches) return;
748
+ document.querySelectorAll(`[${FRAME_ATTR}="stats"]`).forEach((el) => {
749
+ if (!el.isConnected || !STATS_LINE_RE.test((el.textContent ?? "").trim())) el.removeAttribute(FRAME_ATTR);
750
+ });
751
+ const cands = Array.from(document.querySelectorAll("span, div")).filter((el) => {
752
+ if (!isStatsText(el)) return false;
753
+ if (getComputedStyle(el).display === "contents") return false;
754
+ return true;
755
+ });
756
+ for (const el of cands) {
757
+ if (el.hasAttribute(FRAME_ATTR)) continue;
758
+ let anc = el.parentElement;
759
+ let hasOuterCand = false;
760
+ while (anc) {
761
+ if (cands.includes(anc)) {
762
+ hasOuterCand = true;
763
+ break;
764
+ }
765
+ anc = anc.parentElement;
766
+ }
767
+ if (!hasOuterCand) {
768
+ if (!el.hasAttribute(FRAME_ATTR)) el.setAttribute(FRAME_ATTR, "stats");
769
+ }
770
+ }
771
+ };
772
+ mark();
773
+ const mo = new MutationObserver(() => {
774
+ if (narrow.matches) mark();
775
+ });
776
+ mo.observe(document.documentElement, {
777
+ childList: true,
778
+ subtree: true
779
+ });
780
+ narrow.addEventListener("change", mark);
781
+ return () => {
782
+ mo.disconnect();
783
+ narrow.removeEventListener("change", mark);
784
+ };
785
+ }, "dshb-mobile: stats line");
786
+ }
787
+ /** 目录抽屉切换按钮(仅切换,不含文件浏览/日志导出等功能按钮)。 */
788
+ function DrawerToggle({ toggleSidebar }) {
789
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
790
+ type: "button",
791
+ "data-dshb-mobile": "toggle",
792
+ "aria-label": "打开目录",
793
+ title: "打开目录",
794
+ onClick: () => toggleSidebar(),
795
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
796
+ width: "16",
797
+ height: "16",
798
+ viewBox: "0 0 16 16",
799
+ fill: "none",
800
+ xmlns: "http://www.w3.org/2000/svg",
801
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
802
+ x: "1.5",
803
+ y: "2.5",
804
+ width: "13",
805
+ height: "11",
806
+ rx: "2",
807
+ stroke: "currentColor",
808
+ strokeWidth: "1.5"
809
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
810
+ d: "M6 2.5v11",
811
+ stroke: "currentColor",
812
+ strokeWidth: "1.5"
813
+ })]
814
+ })
815
+ });
816
+ }
817
+ 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>";
818
+ /**
819
+ * 首页 / hero(无活跃会话)没有 session header,header 上的切换按钮不渲染;
820
+ * 这里在 frame 上注入一个浮动切换按钮(FAB),仅 hero 且抽屉收起时显示。
821
+ */
822
+ function installFab(ctx, toggleSidebar) {
823
+ ctx.effect(() => {
824
+ const narrow = window.matchMedia(MOBILE_QUERY);
825
+ const FAB_ATTR = "fab";
826
+ const heroPhase = () => document.querySelector("[data-phase=\"active\"]") === null;
827
+ const drawerOpen = () => {
828
+ const frame = getFrame();
829
+ return frame !== null && !frame.hasAttribute(COLLAPSED_ATTR);
830
+ };
831
+ let fab = null;
832
+ const sync = () => {
833
+ if (!narrow.matches || !heroPhase() || drawerOpen()) {
834
+ if (fab !== null) {
835
+ fab.remove();
836
+ fab = null;
837
+ }
838
+ return;
839
+ }
840
+ const frame = getFrame();
841
+ if (frame === null) return;
842
+ if (fab !== null && fab.parentElement === frame) return;
843
+ fab?.remove();
844
+ fab = document.createElement("button");
845
+ fab.type = "button";
846
+ fab.setAttribute(FRAME_ATTR, FAB_ATTR);
847
+ fab.setAttribute("aria-label", "打开目录");
848
+ fab.setAttribute("title", "打开目录");
849
+ fab.innerHTML = PANEL_ICON_SVG;
850
+ fab.addEventListener("click", toggleSidebar);
851
+ frame.appendChild(fab);
852
+ };
853
+ sync();
854
+ const mo = new MutationObserver(sync);
855
+ mo.observe(document.documentElement, {
856
+ childList: true,
857
+ subtree: true,
858
+ attributes: true,
859
+ attributeFilter: [
860
+ "data-phase",
861
+ "data-sidebar-collapsed",
862
+ "class"
863
+ ]
864
+ });
865
+ narrow.addEventListener("change", sync);
866
+ return () => {
867
+ mo.disconnect();
868
+ narrow.removeEventListener("change", sync);
869
+ fab?.remove();
870
+ };
871
+ }, "dshb-mobile: hero fab");
872
+ }
873
+ /** 安装移动端适配:样式 + viewport + 抽屉标记 + 抽屉交互 + 切换按钮。 */
874
+ function installMobile(ctx) {
875
+ installStyles(ctx);
876
+ installViewport(ctx);
877
+ installFrameMarker(ctx);
878
+ installStatsLine(ctx);
879
+ const layout = ctx.layout;
880
+ const slots = ctx.slots;
881
+ if (layout && typeof layout.toggleSidebar === "function") {
882
+ const toggleSidebar = () => layout.toggleSidebar();
883
+ installDrawerInteractions(ctx, toggleSidebar);
884
+ installFab(ctx, toggleSidebar);
885
+ }
886
+ if (slots && layout) slots.inject("conversation.session.header.actions", () => slots.register({
887
+ name: "conversation.session.header.actions",
888
+ id: "dshb-mobile-toggle",
889
+ order: 10,
890
+ inject: () => ({ toggleSidebar: () => layout.toggleSidebar() })
891
+ }, DrawerToggle));
892
+ }
893
+ //#endregion
894
+ //#region src/client/index.tsx
895
+ const inject = [
896
+ "connection",
897
+ "workspaces",
898
+ "slots",
899
+ "layout",
900
+ "conversation"
901
+ ];
902
+ const SECTION_ID = "dshb-nodes";
903
+ const EMPTY_FORM = {
904
+ name: "",
905
+ type: "remote-ssh",
906
+ host: "",
907
+ port: "22",
908
+ username: "",
909
+ authKind: "password",
910
+ password: "",
911
+ privateKey: "",
912
+ passphrase: "",
913
+ keyPath: "",
914
+ dockerImage: "",
915
+ dockerCpus: "",
916
+ dockerMemory: ""
917
+ };
918
+ const inputStyle = {
919
+ width: "100%",
920
+ boxSizing: "border-box",
921
+ padding: "0 10px",
922
+ height: 32,
923
+ border: "1px solid var(--dsw-alias-border-l2)",
924
+ borderRadius: 8,
925
+ fontSize: 14,
926
+ fontFamily: "inherit",
927
+ background: "var(--dsw-alias-bg-layer-1)",
928
+ color: "var(--dsw-alias-label-primary)",
929
+ outline: "none"
930
+ };
931
+ const buttonStyle = {
932
+ boxSizing: "border-box",
933
+ height: 36,
934
+ padding: "0 14px",
935
+ borderRadius: 18,
936
+ fontSize: 14,
937
+ lineHeight: "22px",
938
+ fontFamily: "inherit",
939
+ cursor: "pointer",
940
+ border: "none",
941
+ display: "inline-flex",
942
+ justifyContent: "center",
943
+ alignItems: "center",
944
+ gap: 4
945
+ };
946
+ const primaryButtonStyle = {
947
+ ...buttonStyle,
948
+ background: "var(--dsw-alias-button-primary-fill)",
949
+ color: "var(--dsw-alias-label-primary-foreground)"
950
+ };
951
+ const secondaryButtonStyle = {
952
+ ...buttonStyle,
953
+ border: "1px solid var(--dsw-alias-border-l2)",
954
+ color: "var(--dsw-alias-label-primary)",
955
+ background: "transparent"
956
+ };
957
+ const dangerOutlineButtonStyle = {
958
+ ...buttonStyle,
959
+ border: "1px solid var(--dsw-alias-state-error-primary)",
960
+ color: "var(--dsw-alias-state-error-primary)",
961
+ background: "transparent"
962
+ };
963
+ function useNodes(reloadKey) {
964
+ const [nodes, setNodes] = (0, react.useState)([]);
965
+ (0, react.useEffect)(() => {
966
+ let cancelled = false;
967
+ (async () => {
968
+ try {
969
+ const res = await fetch("/api/dshb/nodes");
970
+ if (!res.ok) return;
971
+ const data = await res.json();
972
+ if (!cancelled) setNodes(data.nodes ?? []);
973
+ } catch {}
974
+ })();
975
+ return () => {
976
+ cancelled = true;
977
+ };
978
+ }, [reloadKey]);
979
+ return nodes;
980
+ }
981
+ function NodeSection(_props) {
982
+ const [reloadKey, setReloadKey] = (0, react.useState)(0);
983
+ const nodes = useNodes(reloadKey);
984
+ const [selectedId, setSelectedId] = (0, react.useState)("new");
985
+ const [form, setForm] = (0, react.useState)(EMPTY_FORM);
986
+ const [notice, setNotice] = (0, react.useState)(void 0);
987
+ const [busy, setBusy] = (0, react.useState)(false);
988
+ const [testing, setTesting] = (0, react.useState)(false);
989
+ const [testResult, setTestResult] = (0, react.useState)(void 0);
990
+ const [sshConfigEntries, setSshConfigEntries] = (0, react.useState)([]);
991
+ const [dockerBusy, setDockerBusy] = (0, react.useState)(false);
992
+ const flash = (0, react.useCallback)((n) => {
993
+ setNotice(n);
994
+ if (n) setTimeout(() => setNotice(void 0), 6e3);
995
+ }, []);
996
+ const reload = (0, react.useCallback)(() => setReloadKey((k) => k + 1), []);
997
+ (0, react.useEffect)(() => {
998
+ (async () => {
999
+ try {
1000
+ const res = await fetch("/api/dshb/ssh-config");
1001
+ if (!res.ok) return;
1002
+ const data = await res.json();
1003
+ setSshConfigEntries(data.entries ?? []);
1004
+ } catch {}
1005
+ })();
1006
+ }, []);
1007
+ const selectNode = (0, react.useCallback)((id) => {
1008
+ setSelectedId(id);
1009
+ setTestResult(void 0);
1010
+ setTesting(false);
1011
+ setDockerBusy(false);
1012
+ if (id === "new") {
1013
+ setForm(EMPTY_FORM);
1014
+ return;
1015
+ }
1016
+ const node = nodes.find((n) => n.id === id);
1017
+ if (!node) return;
1018
+ setForm({
1019
+ name: node.name,
1020
+ type: node.type,
1021
+ host: node.ssh?.host ?? "",
1022
+ port: String(node.ssh?.port ?? 22),
1023
+ username: node.ssh?.username ?? "",
1024
+ authKind: node.ssh?.auth.kind ?? "password",
1025
+ password: "",
1026
+ privateKey: "",
1027
+ passphrase: "",
1028
+ keyPath: node.ssh?.auth.keyPath ?? "",
1029
+ dockerImage: node.docker?.image ?? "",
1030
+ dockerCpus: node.docker?.resources?.cpus != null ? String(node.docker?.resources?.cpus) : "",
1031
+ dockerMemory: node.docker?.resources?.memoryMB != null ? String(node.docker?.resources?.memoryMB) : ""
1032
+ });
1033
+ }, [nodes]);
1034
+ const importSshConfig = (0, react.useCallback)((entry) => {
1035
+ setForm((f) => ({
1036
+ ...f,
1037
+ type: "remote-ssh",
1038
+ host: entry.host,
1039
+ port: String(entry.port),
1040
+ username: entry.username,
1041
+ authKind: entry.hasIdentityFile ? "key" : f.authKind,
1042
+ keyPath: entry.identityFile ?? f.keyPath,
1043
+ name: f.name || entry.host
1044
+ }));
1045
+ setTestResult(void 0);
1046
+ flash({
1047
+ kind: "ok",
1048
+ text: `已从 ~/.ssh/config 导入 ${entry.host}`
1049
+ });
1050
+ }, [flash]);
1051
+ const save = (0, react.useCallback)(async () => {
1052
+ if (!form.name.trim()) {
1053
+ flash({
1054
+ kind: "error",
1055
+ text: "请填写节点名称"
1056
+ });
1057
+ return;
1058
+ }
1059
+ setBusy(true);
1060
+ try {
1061
+ const isRemote = form.type === "remote-ssh" || form.type === "remote-docker";
1062
+ const isDocker = form.type === "local-docker" || form.type === "remote-docker";
1063
+ const payload = {
1064
+ name: form.name.trim(),
1065
+ type: form.type,
1066
+ ssh: isRemote ? {
1067
+ host: form.host.trim(),
1068
+ port: Number(form.port) || 22,
1069
+ username: form.username.trim(),
1070
+ auth: {
1071
+ kind: form.authKind,
1072
+ ...form.keyPath.trim() ? { keyPath: form.keyPath.trim() } : {}
1073
+ }
1074
+ } : void 0,
1075
+ secrets: isRemote ? {
1076
+ ...form.password ? { password: form.password } : {},
1077
+ ...form.privateKey ? { privateKey: form.privateKey } : {},
1078
+ ...form.passphrase ? { passphrase: form.passphrase } : {}
1079
+ } : void 0,
1080
+ docker: isDocker ? {
1081
+ mode: "managed",
1082
+ ...form.dockerImage.trim() ? { image: form.dockerImage.trim() } : {},
1083
+ ...form.dockerCpus ? { resources: { cpus: Number(form.dockerCpus) || void 0 } } : {},
1084
+ ...form.dockerMemory ? { resources: { memoryMB: Number(form.dockerMemory) || void 0 } } : {},
1085
+ ...form.dockerCpus && form.dockerMemory ? { resources: {
1086
+ cpus: Number(form.dockerCpus) || void 0,
1087
+ memoryMB: Number(form.dockerMemory) || void 0
1088
+ } } : {}
1089
+ } : void 0
1090
+ };
1091
+ const url = selectedId === "new" ? "/api/dshb/nodes" : `/api/dshb/nodes/${selectedId}`;
1092
+ const res = await fetch(url, {
1093
+ method: selectedId === "new" ? "POST" : "PATCH",
1094
+ headers: { "content-type": "application/json" },
1095
+ body: JSON.stringify(payload)
1096
+ });
1097
+ const data = await res.json();
1098
+ if (res.ok && data.ok) {
1099
+ flash({
1100
+ kind: "ok",
1101
+ text: selectedId === "new" ? "节点已创建" : "节点已保存"
1102
+ });
1103
+ reload();
1104
+ if (data.node) setSelectedId(data.node.id);
1105
+ } else flash({
1106
+ kind: "error",
1107
+ text: data.error ?? "保存失败"
1108
+ });
1109
+ } catch {
1110
+ flash({
1111
+ kind: "error",
1112
+ text: "网络错误"
1113
+ });
1114
+ } finally {
1115
+ setBusy(false);
1116
+ }
1117
+ }, [
1118
+ form,
1119
+ selectedId,
1120
+ flash,
1121
+ reload
1122
+ ]);
1123
+ const remove = (0, react.useCallback)(async () => {
1124
+ if (selectedId === "new") return;
1125
+ setBusy(true);
1126
+ try {
1127
+ const node = nodes.find((n) => n.id === selectedId);
1128
+ if (node && (node.type === "local-docker" || node.type === "remote-docker") && node.docker?.containerId) try {
1129
+ await fetch(`/api/dshb/docker/${selectedId}/delete`, { method: "POST" });
1130
+ } catch {}
1131
+ if ((await fetch(`/api/dshb/nodes/${selectedId}`, { method: "DELETE" })).ok) {
1132
+ flash({
1133
+ kind: "ok",
1134
+ text: "节点已删除"
1135
+ });
1136
+ setSelectedId("new");
1137
+ setForm(EMPTY_FORM);
1138
+ reload();
1139
+ }
1140
+ } catch {
1141
+ flash({
1142
+ kind: "error",
1143
+ text: "删除失败"
1144
+ });
1145
+ } finally {
1146
+ setBusy(false);
1147
+ }
1148
+ }, [
1149
+ selectedId,
1150
+ nodes,
1151
+ flash,
1152
+ reload
1153
+ ]);
1154
+ const test = (0, react.useCallback)(async () => {
1155
+ if (form.type === "local-host") {
1156
+ flash({
1157
+ kind: "error",
1158
+ text: "本地节点无需测试连接"
1159
+ });
1160
+ return;
1161
+ }
1162
+ const isRemote = form.type === "remote-ssh" || form.type === "remote-docker";
1163
+ if (isRemote && !form.host.trim()) {
1164
+ flash({
1165
+ kind: "error",
1166
+ text: "请填写主机地址"
1167
+ });
1168
+ return;
1169
+ }
1170
+ setTesting(true);
1171
+ setTestResult(void 0);
1172
+ try {
1173
+ const payload = isRemote ? {
1174
+ ssh: {
1175
+ host: form.host.trim(),
1176
+ port: Number(form.port) || 22,
1177
+ username: form.username.trim(),
1178
+ auth: {
1179
+ kind: form.authKind,
1180
+ ...form.keyPath.trim() ? { keyPath: form.keyPath.trim() } : {}
1181
+ }
1182
+ },
1183
+ secrets: {
1184
+ ...form.password ? { password: form.password } : {},
1185
+ ...form.privateKey ? { privateKey: form.privateKey } : {},
1186
+ ...form.passphrase ? { passphrase: form.passphrase } : {}
1187
+ }
1188
+ } : {};
1189
+ const url = selectedId === "new" ? "/api/dshb/nodes/test-unsaved" : `/api/dshb/nodes/${selectedId}/test`;
1190
+ const r = (await (await fetch(url, {
1191
+ method: "POST",
1192
+ headers: { "content-type": "application/json" },
1193
+ body: JSON.stringify(payload)
1194
+ })).json()).report;
1195
+ if (r?.ok && r.reachable) setTestResult("连接成功");
1196
+ else {
1197
+ const cat = r?.category ? `(${r.category})` : "";
1198
+ setTestResult(`${r?.error ?? "连接失败"}${cat}`);
1199
+ }
1200
+ reload();
1201
+ } catch {
1202
+ setTestResult("网络错误");
1203
+ } finally {
1204
+ setTesting(false);
1205
+ }
1206
+ }, [
1207
+ form,
1208
+ selectedId,
1209
+ reload
1210
+ ]);
1211
+ const dockerAction = (0, react.useCallback)(async (action) => {
1212
+ if (selectedId === "new") return;
1213
+ if (action === "provision") {
1214
+ setDockerBusy(true);
1215
+ try {
1216
+ const res = await fetch(`/api/dshb/docker/${selectedId}/provision`, { method: "POST" });
1217
+ const data = await res.json();
1218
+ if (res.ok && data.ok) flash({
1219
+ kind: "ok",
1220
+ text: `容器已就绪:${data.containerId?.slice(0, 12) ?? ""}`
1221
+ });
1222
+ else flash({
1223
+ kind: "error",
1224
+ text: data.error ?? "拉起失败"
1225
+ });
1226
+ reload();
1227
+ } catch {
1228
+ flash({
1229
+ kind: "error",
1230
+ text: "网络错误"
1231
+ });
1232
+ } finally {
1233
+ setDockerBusy(false);
1234
+ }
1235
+ return;
1236
+ }
1237
+ setDockerBusy(true);
1238
+ try {
1239
+ const res = await fetch(`/api/dshb/docker/${selectedId}/${action}`, { method: "POST" });
1240
+ const data = await res.json();
1241
+ if (res.ok && data.ok) flash({
1242
+ kind: "ok",
1243
+ text: action === "restart" ? "容器已重启" : action === "stop" ? "容器已停止" : "容器已启动"
1244
+ });
1245
+ else flash({
1246
+ kind: "error",
1247
+ text: data.error ?? "操作失败"
1248
+ });
1249
+ reload();
1250
+ } catch {
1251
+ flash({
1252
+ kind: "error",
1253
+ text: "网络错误"
1254
+ });
1255
+ } finally {
1256
+ setDockerBusy(false);
1257
+ }
1258
+ }, [
1259
+ selectedId,
1260
+ flash,
1261
+ reload
1262
+ ]);
1263
+ const selectedNode = selectedId === "new" ? void 0 : nodes.find((n) => n.id === selectedId);
1264
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: `
1265
+ @media (max-width:767px){
1266
+ .dshb-node-section{flex-direction:column!important}
1267
+ .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}
1268
+ .dshb-node-section>div:last-child{max-width:100%!important}
1269
+ .dshb-form-row{flex-direction:column!important;gap:8px!important}
1270
+ .dshb-form-row>label{width:100%!important;flex:none!important}
1271
+ }
1272
+ ` }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1273
+ className: "dshb-node-section",
1274
+ style: {
1275
+ display: "flex",
1276
+ gap: 20,
1277
+ minHeight: 320,
1278
+ overflow: "hidden",
1279
+ maxWidth: "100%",
1280
+ width: "100%",
1281
+ boxSizing: "border-box"
1282
+ },
1283
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1284
+ style: {
1285
+ width: 200,
1286
+ flexShrink: 0,
1287
+ borderRight: "1px solid var(--dsw-alias-border-l2)",
1288
+ paddingRight: 16
1289
+ },
1290
+ children: [
1291
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1292
+ type: "button",
1293
+ onClick: () => selectNode("new"),
1294
+ style: {
1295
+ ...primaryButtonStyle,
1296
+ width: "100%",
1297
+ marginBottom: 10
1298
+ },
1299
+ children: "新建节点"
1300
+ }),
1301
+ nodes.map((n) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1302
+ onClick: () => selectNode(n.id),
1303
+ style: {
1304
+ padding: "8px 10px",
1305
+ borderRadius: 6,
1306
+ cursor: "pointer",
1307
+ marginBottom: 4,
1308
+ background: selectedId === n.id ? "var(--dsw-alias-interactive-bg-active, var(--dsw-alias-interactive-bg-hover))" : "transparent",
1309
+ fontSize: 13
1310
+ },
1311
+ children: [
1312
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1313
+ style: { fontWeight: 500 },
1314
+ children: n.name
1315
+ }),
1316
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1317
+ style: {
1318
+ fontSize: 11,
1319
+ opacity: .7,
1320
+ display: "flex",
1321
+ alignItems: "center",
1322
+ gap: 4
1323
+ },
1324
+ children: [
1325
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: {
1326
+ width: 6,
1327
+ height: 6,
1328
+ borderRadius: "50%",
1329
+ 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)"
1330
+ } }),
1331
+ n.type === "local-host" ? "默认环境(本地)" : n.type === "remote-ssh" ? "远程 SSH" : n.type === "local-docker" ? "本地 Docker" : n.type === "remote-docker" ? "远程 Docker" : n.type,
1332
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1333
+ style: {
1334
+ marginLeft: 4,
1335
+ 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)"
1336
+ },
1337
+ children: n.status?.reachable === true ? "在线" : n.status?.reachable === false ? "离线" : "未知"
1338
+ })
1339
+ ]
1340
+ }),
1341
+ n.provision && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1342
+ style: {
1343
+ fontSize: 11,
1344
+ marginTop: 2,
1345
+ 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)"
1346
+ },
1347
+ children: n.provision.state === "provisioning" ? "容器拉起中…" : n.provision.state === "ready" ? "容器已就绪" : `容器拉起失败:${n.provision.error ?? "未知错误"}`
1348
+ })
1349
+ ]
1350
+ }, n.id)),
1351
+ nodes.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1352
+ style: {
1353
+ fontSize: 12,
1354
+ opacity: .6
1355
+ },
1356
+ children: "暂无节点"
1357
+ })
1358
+ ]
1359
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1360
+ style: {
1361
+ flex: 1,
1362
+ maxWidth: 480,
1363
+ minWidth: 0,
1364
+ overflow: "hidden"
1365
+ },
1366
+ children: [sshConfigEntries.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1367
+ style: { marginBottom: 14 },
1368
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1369
+ style: {
1370
+ fontSize: 12,
1371
+ opacity: .75,
1372
+ display: "block",
1373
+ marginBottom: 4
1374
+ },
1375
+ children: "从 ~/.ssh/config 导入"
1376
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1377
+ style: inputStyle,
1378
+ value: "",
1379
+ onChange: (e) => {
1380
+ const entry = sshConfigEntries[Number(e.target.value)];
1381
+ if (entry) importSshConfig(entry);
1382
+ e.target.value = "";
1383
+ },
1384
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1385
+ value: "",
1386
+ children: "选择主机…"
1387
+ }), sshConfigEntries.map((entry, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
1388
+ value: i,
1389
+ children: [
1390
+ entry.host,
1391
+ ":",
1392
+ entry.port,
1393
+ entry.username ? ` (${entry.username})` : "",
1394
+ entry.hasProxyJump ? " 跳板" : ""
1395
+ ]
1396
+ }, entry.host))]
1397
+ })]
1398
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1399
+ style: {
1400
+ display: "flex",
1401
+ flexDirection: "column",
1402
+ gap: 12
1403
+ },
1404
+ children: [
1405
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1406
+ style: {
1407
+ display: "flex",
1408
+ flexDirection: "column",
1409
+ gap: 4,
1410
+ fontSize: 13
1411
+ },
1412
+ children: ["节点名称", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1413
+ style: inputStyle,
1414
+ value: form.name,
1415
+ onChange: (e) => setForm((f) => ({
1416
+ ...f,
1417
+ name: e.target.value
1418
+ })),
1419
+ placeholder: "例如:构建机"
1420
+ })]
1421
+ }),
1422
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1423
+ style: {
1424
+ display: "flex",
1425
+ flexDirection: "column",
1426
+ gap: 4,
1427
+ fontSize: 13
1428
+ },
1429
+ children: ["节点类型", /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1430
+ style: inputStyle,
1431
+ value: form.type,
1432
+ onChange: (e) => setForm((f) => ({
1433
+ ...f,
1434
+ type: e.target.value
1435
+ })),
1436
+ children: [
1437
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1438
+ value: "remote-ssh",
1439
+ children: "远程 SSH 宿主机"
1440
+ }),
1441
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1442
+ value: "remote-docker",
1443
+ children: "远程 Docker(SSH 通道)"
1444
+ }),
1445
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1446
+ value: "local-host",
1447
+ children: "默认环境(本地)"
1448
+ }),
1449
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1450
+ value: "local-docker",
1451
+ children: "本地 Docker 容器"
1452
+ })
1453
+ ]
1454
+ })]
1455
+ }),
1456
+ (form.type === "remote-ssh" || form.type === "remote-docker") && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1457
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1458
+ className: "dshb-form-row",
1459
+ style: {
1460
+ display: "flex",
1461
+ gap: 10
1462
+ },
1463
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1464
+ style: {
1465
+ display: "flex",
1466
+ flexDirection: "column",
1467
+ gap: 4,
1468
+ fontSize: 13,
1469
+ flex: 1,
1470
+ minWidth: 0
1471
+ },
1472
+ children: ["主机地址", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1473
+ style: inputStyle,
1474
+ value: form.host,
1475
+ onChange: (e) => setForm((f) => ({
1476
+ ...f,
1477
+ host: e.target.value
1478
+ })),
1479
+ placeholder: "192.168.1.10"
1480
+ })]
1481
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1482
+ style: {
1483
+ display: "flex",
1484
+ flexDirection: "column",
1485
+ gap: 4,
1486
+ fontSize: 13,
1487
+ width: 90
1488
+ },
1489
+ children: ["端口", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1490
+ style: inputStyle,
1491
+ value: form.port,
1492
+ onChange: (e) => setForm((f) => ({
1493
+ ...f,
1494
+ port: e.target.value
1495
+ }))
1496
+ })]
1497
+ })]
1498
+ }),
1499
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1500
+ style: {
1501
+ display: "flex",
1502
+ flexDirection: "column",
1503
+ gap: 4,
1504
+ fontSize: 13
1505
+ },
1506
+ children: ["用户名", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1507
+ style: inputStyle,
1508
+ value: form.username,
1509
+ onChange: (e) => setForm((f) => ({
1510
+ ...f,
1511
+ username: e.target.value
1512
+ })),
1513
+ placeholder: "root"
1514
+ })]
1515
+ }),
1516
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1517
+ style: {
1518
+ display: "flex",
1519
+ flexDirection: "column",
1520
+ gap: 4,
1521
+ fontSize: 13
1522
+ },
1523
+ children: ["认证方式", /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1524
+ style: inputStyle,
1525
+ value: form.authKind,
1526
+ onChange: (e) => setForm((f) => ({
1527
+ ...f,
1528
+ authKind: e.target.value
1529
+ })),
1530
+ children: [
1531
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1532
+ value: "password",
1533
+ children: "密码"
1534
+ }),
1535
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1536
+ value: "key",
1537
+ children: "私钥"
1538
+ }),
1539
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1540
+ value: "agent",
1541
+ children: "ssh-agent"
1542
+ })
1543
+ ]
1544
+ })]
1545
+ }),
1546
+ form.authKind === "password" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1547
+ style: {
1548
+ display: "flex",
1549
+ flexDirection: "column",
1550
+ gap: 4,
1551
+ fontSize: 13
1552
+ },
1553
+ children: [
1554
+ "密码",
1555
+ selectedNode?.hasSecret.hasPassword ? "(已保存,留空保持不变)" : "",
1556
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1557
+ style: inputStyle,
1558
+ type: "password",
1559
+ value: form.password,
1560
+ onChange: (e) => setForm((f) => ({
1561
+ ...f,
1562
+ password: e.target.value
1563
+ })),
1564
+ autoComplete: "new-password"
1565
+ })
1566
+ ]
1567
+ }),
1568
+ form.authKind === "key" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1569
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1570
+ style: {
1571
+ display: "flex",
1572
+ flexDirection: "column",
1573
+ gap: 4,
1574
+ fontSize: 13
1575
+ },
1576
+ children: [
1577
+ "私钥内容",
1578
+ selectedNode?.hasSecret.hasKey ? "(已保存,留空保持不变)" : "",
1579
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
1580
+ style: {
1581
+ ...inputStyle,
1582
+ minHeight: 90,
1583
+ resize: "vertical"
1584
+ },
1585
+ value: form.privateKey,
1586
+ onChange: (e) => setForm((f) => ({
1587
+ ...f,
1588
+ privateKey: e.target.value
1589
+ })),
1590
+ placeholder: "-----BEGIN OPENSSH PRIVATE KEY-----"
1591
+ })
1592
+ ]
1593
+ }),
1594
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1595
+ style: {
1596
+ display: "flex",
1597
+ flexDirection: "column",
1598
+ gap: 4,
1599
+ fontSize: 13
1600
+ },
1601
+ children: ["私钥 passphrase(可选)", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1602
+ style: inputStyle,
1603
+ type: "password",
1604
+ value: form.passphrase,
1605
+ onChange: (e) => setForm((f) => ({
1606
+ ...f,
1607
+ passphrase: e.target.value
1608
+ })),
1609
+ autoComplete: "new-password"
1610
+ })]
1611
+ }),
1612
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1613
+ style: {
1614
+ display: "flex",
1615
+ flexDirection: "column",
1616
+ gap: 4,
1617
+ fontSize: 13
1618
+ },
1619
+ children: ["或私钥文件路径(可选,优先使用内容)", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1620
+ style: inputStyle,
1621
+ value: form.keyPath,
1622
+ onChange: (e) => setForm((f) => ({
1623
+ ...f,
1624
+ keyPath: e.target.value
1625
+ })),
1626
+ placeholder: "~/.ssh/id_ed25519"
1627
+ })]
1628
+ })
1629
+ ] })
1630
+ ] }),
1631
+ (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", {
1632
+ style: {
1633
+ display: "flex",
1634
+ flexDirection: "column",
1635
+ gap: 4,
1636
+ fontSize: 13
1637
+ },
1638
+ children: ["镜像地址", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1639
+ style: inputStyle,
1640
+ value: form.dockerImage,
1641
+ onChange: (e) => setForm((f) => ({
1642
+ ...f,
1643
+ dockerImage: e.target.value
1644
+ })),
1645
+ placeholder: "alpine:latest"
1646
+ })]
1647
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1648
+ className: "dshb-form-row",
1649
+ style: {
1650
+ display: "flex",
1651
+ gap: 10
1652
+ },
1653
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1654
+ style: {
1655
+ display: "flex",
1656
+ flexDirection: "column",
1657
+ gap: 4,
1658
+ fontSize: 13,
1659
+ flex: 1,
1660
+ minWidth: 0
1661
+ },
1662
+ children: ["CPU 核数(可选)", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1663
+ style: inputStyle,
1664
+ value: form.dockerCpus,
1665
+ onChange: (e) => setForm((f) => ({
1666
+ ...f,
1667
+ dockerCpus: e.target.value
1668
+ })),
1669
+ placeholder: "2"
1670
+ })]
1671
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1672
+ style: {
1673
+ display: "flex",
1674
+ flexDirection: "column",
1675
+ gap: 4,
1676
+ fontSize: 13,
1677
+ flex: 1,
1678
+ minWidth: 0
1679
+ },
1680
+ children: ["内存 MB(可选)", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1681
+ style: inputStyle,
1682
+ value: form.dockerMemory,
1683
+ onChange: (e) => setForm((f) => ({
1684
+ ...f,
1685
+ dockerMemory: e.target.value
1686
+ })),
1687
+ placeholder: "512"
1688
+ })]
1689
+ })]
1690
+ })] }),
1691
+ testResult !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1692
+ style: {
1693
+ fontSize: 13,
1694
+ color: testResult === "连接成功" ? "var(--dsw-alias-state-success-primary)" : "var(--dsw-alias-state-error-primary)"
1695
+ },
1696
+ children: testResult
1697
+ }),
1698
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1699
+ style: {
1700
+ display: "flex",
1701
+ gap: 10,
1702
+ marginTop: 6,
1703
+ flexWrap: "wrap"
1704
+ },
1705
+ children: [
1706
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1707
+ type: "button",
1708
+ onClick: () => void save(),
1709
+ disabled: busy,
1710
+ style: primaryButtonStyle,
1711
+ children: selectedId === "new" ? "创建节点" : "保存"
1712
+ }),
1713
+ (form.type === "remote-ssh" || form.type === "remote-docker" || selectedId !== "new" && form.type === "local-docker") && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1714
+ type: "button",
1715
+ onClick: () => void test(),
1716
+ disabled: busy || testing,
1717
+ style: secondaryButtonStyle,
1718
+ children: testing ? "测试中…" : "测试连接"
1719
+ }),
1720
+ selectedId !== "new" && (form.type === "local-docker" || form.type === "remote-docker") && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1721
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1722
+ type: "button",
1723
+ onClick: () => void dockerAction("provision"),
1724
+ disabled: busy || dockerBusy,
1725
+ style: primaryButtonStyle,
1726
+ children: dockerBusy ? "操作中…" : "拉起容器"
1727
+ }),
1728
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1729
+ type: "button",
1730
+ onClick: () => void dockerAction("restart"),
1731
+ disabled: busy || dockerBusy,
1732
+ style: secondaryButtonStyle,
1733
+ children: dockerBusy ? "操作中…" : "重启容器"
1734
+ }),
1735
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1736
+ type: "button",
1737
+ onClick: () => void dockerAction("stop"),
1738
+ disabled: busy || dockerBusy,
1739
+ style: secondaryButtonStyle,
1740
+ children: dockerBusy ? "操作中…" : "停止容器"
1741
+ })
1742
+ ] }),
1743
+ selectedId !== "new" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1744
+ type: "button",
1745
+ onClick: () => void remove(),
1746
+ disabled: busy,
1747
+ style: dangerOutlineButtonStyle,
1748
+ children: "删除"
1749
+ }),
1750
+ notice && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1751
+ style: {
1752
+ fontSize: 13,
1753
+ alignSelf: "center",
1754
+ color: notice.kind === "ok" ? "var(--dsw-alias-state-success-primary)" : "var(--dsw-alias-state-error-primary)"
1755
+ },
1756
+ children: notice.text
1757
+ })
1758
+ ]
1759
+ })
1760
+ ]
1761
+ })]
1762
+ })]
1763
+ })] });
1764
+ }
1765
+ function apply(ctx) {
1766
+ const globalStyle = document.createElement("style");
1767
+ globalStyle.textContent = `
1768
+ html, body { overflow-x: hidden !important; max-width: 100vw !important }
1769
+ * { min-width: 0 !important; }
1770
+ table { display: block !important; overflow-x: auto !important; }
1771
+ img, video, canvas, svg { max-width: 100% !important; height: auto !important }
1772
+ pre, code { white-space: pre-wrap !important; word-break: break-all !important }
1773
+ input, select, textarea { max-width: 100% !important; }
1774
+ [class*="content"], [class*="main"], [class*="panel"], [class*="page"], [class*="wrapper"], [class*="container"] { max-width: 100% !important; overflow-x: hidden !important }
1775
+ body > div { max-width: 100vw !important; overflow-x: hidden !important; }
1776
+ body > div > div { max-width: 100vw !important; overflow-x: hidden !important; }
1777
+ `;
1778
+ document.head.appendChild(globalStyle);
1779
+ const fixOverflow = () => {
1780
+ const vw = document.documentElement.clientWidth;
1781
+ const walk = (el) => {
1782
+ if (!(el instanceof HTMLElement)) return;
1783
+ if (el.scrollWidth > vw && el !== document.documentElement && el !== document.body) {
1784
+ el.style.maxWidth = "100vw";
1785
+ el.style.overflowX = "hidden";
1786
+ }
1787
+ for (const child of el.children) walk(child);
1788
+ };
1789
+ walk(document.documentElement);
1790
+ };
1791
+ fixOverflow();
1792
+ new MutationObserver(() => requestAnimationFrame(fixOverflow)).observe(document.body, {
1793
+ childList: true,
1794
+ subtree: true
1795
+ });
1796
+ installMobile(ctx);
1797
+ ctx.plugin({
1798
+ inject: ["slots", "settingsScope"],
1799
+ apply: (sub) => {
1800
+ sub.slots.inject("settings.section", () => sub.slots.register({
1801
+ name: "settings.section",
1802
+ id: SECTION_ID,
1803
+ order: 110,
1804
+ label: () => "工作节点"
1805
+ }, NodeSection));
1806
+ sub.slots.inject("conversation.hero.workspace.directoryFlow", () => sub.slots.register({ name: "conversation.hero.workspace.directoryFlow" }, DirectoryFlowOccupant));
1807
+ sub.slots.inject("sidebar.workspaces.directoryFlow", () => sub.slots.register({ name: "sidebar.workspaces.directoryFlow" }, DirectoryFlowOccupant));
1808
+ }
1809
+ });
1810
+ const workspaces = ctx.workspaces;
1811
+ if (workspaces && typeof workspaces.openPath === "function") {
1812
+ const originalOpenPath = workspaces.openPath.bind(workspaces);
1813
+ workspaces.openPath = async (path) => {
1814
+ try {
1815
+ const res = await fetch(`/api/dshb/remote/bound?path=${encodeURIComponent(path)}`);
1816
+ if (res.ok) {
1817
+ const data = await res.json();
1818
+ if (data.bound) {
1819
+ if (data.kind !== "file") return originalOpenPath(path);
1820
+ const a = document.createElement("a");
1821
+ a.href = `/api/dshb/remote/file?path=${encodeURIComponent(path)}`;
1822
+ a.download = "";
1823
+ document.body.appendChild(a);
1824
+ a.click();
1825
+ a.remove();
1826
+ return;
1827
+ }
1828
+ }
1829
+ } catch {}
1830
+ return originalOpenPath(path);
1831
+ };
1832
+ }
1833
+ const ICON_SVG = {
1834
+ 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>`,
1835
+ "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>`
1836
+ };
1837
+ const ICON_LABELS = {
1838
+ "认证": "auth",
1839
+ "工作节点": "dshb-nodes"
1840
+ };
1841
+ new MutationObserver(() => {
1842
+ const cells = document.querySelectorAll("button[class*=\"navCell\"]");
1843
+ for (const cell of Array.from(cells)) {
1844
+ const label = cell.querySelector("span[class*=\"navLabel\"]");
1845
+ if (!label) continue;
1846
+ const text = label.textContent ?? "";
1847
+ const iconKey = ICON_LABELS[text];
1848
+ if (!iconKey) continue;
1849
+ const iconSlot = cell.querySelector("[class*=\"navIcon\"]");
1850
+ if (!iconSlot) continue;
1851
+ if (iconSlot.getAttribute("data-dshb-icon") === iconKey) continue;
1852
+ iconSlot.innerHTML = ICON_SVG[iconKey] ?? "";
1853
+ iconSlot.setAttribute("data-dshb-icon", iconKey);
1854
+ }
1855
+ }).observe(document.body, {
1856
+ childList: true,
1857
+ subtree: true
1858
+ });
1859
+ }
1860
+ //#endregion
1861
+ exports.NodeSection = NodeSection;
1862
+ exports.apply = apply;
1863
+ exports.inject = inject;
1864
+ return module.exports;
1865
+ }
1866
+ });
1867
+
1868
+ //# sourceMappingURL=client.js.map