@zhengjunyao/dsh-restart 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.
package/lib/client.js ADDED
@@ -0,0 +1,1706 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@zhengjunyao/dsh-restart",
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_dom_client = require("react-dom/client");
9
+ let react_jsx_runtime = require("react/jsx-runtime");
10
+ //#region src/client/api.ts
11
+ /** Error carrying the route's JSON error message. */
12
+ var RestartApiError = class extends Error {
13
+ status;
14
+ constructor(message, status = 0) {
15
+ super(message);
16
+ this.status = status;
17
+ this.name = "RestartApiError";
18
+ }
19
+ };
20
+ /** One JSON request with a hard timeout (a dead server must not hang the UI). */
21
+ async function request(path, init = {}, timeoutMs = 6e3) {
22
+ const controller = new AbortController();
23
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
24
+ let response;
25
+ try {
26
+ response = await fetch(path, {
27
+ ...init,
28
+ signal: controller.signal,
29
+ cache: "no-store"
30
+ });
31
+ } catch (error) {
32
+ throw new RestartApiError(error instanceof Error && error.name === "AbortError" ? "请求超时(服务可能正在重启)" : "网络请求失败: " + String(error instanceof Error ? error.message : error));
33
+ } finally {
34
+ clearTimeout(timer);
35
+ }
36
+ let body;
37
+ try {
38
+ body = await response.json();
39
+ } catch {
40
+ throw new RestartApiError("HTTP " + response.status + ": 响应不是合法 JSON", response.status);
41
+ }
42
+ if (!response.ok) throw new RestartApiError(typeof body === "object" && body !== null && typeof body.error === "string" ? body.error : "HTTP " + response.status, response.status);
43
+ return body;
44
+ }
45
+ /** The dsh-restart panel API. */
46
+ var RestartApi = class {
47
+ /** Host + helper + config + history. */
48
+ async status() {
49
+ return request("/api/dsh-restart/status");
50
+ }
51
+ /** Liveness probe used while reconnecting (short timeout, tiny body). */
52
+ async probe(timeoutMs = 2500) {
53
+ return request("/api/dsh-restart/probe", {}, timeoutMs);
54
+ }
55
+ /** Ask for a restart; the host answers before it exits. */
56
+ async restart(reason, source = "web") {
57
+ return request("/api/dsh-restart/restart", {
58
+ method: "POST",
59
+ headers: { "Content-Type": "application/json" },
60
+ body: JSON.stringify({
61
+ reason,
62
+ source
63
+ })
64
+ }, 1e4);
65
+ }
66
+ /** Boot-log tail; `which: 'latest'` = newest log file on disk. */
67
+ async logs(which = "latest", lines = 200) {
68
+ return request(`/api/dsh-restart/logs?which=${encodeURIComponent(which)}&lines=${String(lines)}`, {}, 8e3);
69
+ }
70
+ /** Restart history, newest first. */
71
+ async history(limit = 20) {
72
+ return request(`/api/dsh-restart/history?limit=${String(limit)}`);
73
+ }
74
+ /** Patch (or reset) the plugin config. */
75
+ async setConfig(patch) {
76
+ return request("/api/dsh-restart/config", {
77
+ method: "POST",
78
+ headers: { "Content-Type": "application/json" },
79
+ body: JSON.stringify(patch)
80
+ });
81
+ }
82
+ /** Live helper state (through the host). */
83
+ async helper() {
84
+ return request("/api/dsh-restart/helper");
85
+ }
86
+ /** Ask a failed helper to try again. */
87
+ async helperRetry() {
88
+ return request("/api/dsh-restart/helper/retry", { method: "POST" }, 4e3);
89
+ }
90
+ };
91
+ /**
92
+ * Read the detached helper's live state straight from its console port.
93
+ *
94
+ * Used only while DSH itself is unreachable: the helper is a different origin
95
+ * (another port) but answers with `Access-Control-Allow-Origin: *`.
96
+ */
97
+ async function fetchHelperDirect(consoleUrl, timeoutMs = 2500) {
98
+ if (consoleUrl === "") return null;
99
+ const controller = new AbortController();
100
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
101
+ try {
102
+ const response = await fetch(`${consoleUrl}/status`, {
103
+ signal: controller.signal,
104
+ cache: "no-store"
105
+ });
106
+ if (!response.ok) return null;
107
+ return await response.json();
108
+ } catch {
109
+ return null;
110
+ } finally {
111
+ clearTimeout(timer);
112
+ }
113
+ }
114
+ /**
115
+ * Fetch the helper's copy-ready failure report.
116
+ *
117
+ * Served by the recovery console (a different port, CORS-open), so it is
118
+ * reachable exactly when the main server is not — which is when a failure
119
+ * report matters.
120
+ */
121
+ async function fetchHelperReport(consoleUrl, timeoutMs = 3e3) {
122
+ if (consoleUrl === "") return "";
123
+ const controller = new AbortController();
124
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
125
+ try {
126
+ const response = await fetch(`${consoleUrl}/report`, {
127
+ signal: controller.signal,
128
+ cache: "no-store"
129
+ });
130
+ return response.ok ? await response.text() : "";
131
+ } catch {
132
+ return "";
133
+ } finally {
134
+ clearTimeout(timer);
135
+ }
136
+ }
137
+ /** Ask the helper (direct) to relaunch after a failure. */
138
+ async function requestHelperRetry(consoleUrl, timeoutMs = 3e3) {
139
+ if (consoleUrl === "") return false;
140
+ const controller = new AbortController();
141
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
142
+ try {
143
+ return (await fetch(`${consoleUrl}/retry`, {
144
+ method: "POST",
145
+ signal: controller.signal
146
+ })).ok;
147
+ } catch {
148
+ return false;
149
+ } finally {
150
+ clearTimeout(timer);
151
+ }
152
+ }
153
+ //#endregion
154
+ //#region src/client/state.ts
155
+ /**
156
+ * dsh-restart — the browser-side restart state machine.
157
+ *
158
+ * One module-level store, subscribed to by every surface (settings card,
159
+ * sidebar popover, full-screen overlay). A restart is a process that outlives
160
+ * the page it started from, so the state is mirrored into sessionStorage: if
161
+ * the tab reloads mid-restart, the overlay picks the wait back up instead of
162
+ * leaving the user on a dead page with no explanation.
163
+ *
164
+ * The lifecycle:
165
+ *
166
+ * requesting ──POST /api/dsh-restart/restart──▶ waiting
167
+ * waiting ──probe /api/dsh-restart/probe every N ms──▶ ready ──▶ location.reload()
168
+ * waiting ──helper reports failure──▶ failed (keeps probing; the helper can
169
+ * be retried from the overlay)
170
+ */
171
+ /** Storage key for resuming across a reload. */
172
+ const STORAGE_KEY = "dsh-restart/pending";
173
+ /** A pending restart older than this is treated as stale and dropped. */
174
+ const RESUME_WINDOW_MS = 15 * 6e4;
175
+ /** Grace period before the helper is consulted (the old host needs to die first). */
176
+ const HELPER_PROBE_AFTER_MS = 5e3;
177
+ /** How long the page waits after the server answers before reloading. */
178
+ const RELOAD_DELAY_MS = 700;
179
+ const api$1 = new RestartApi();
180
+ let state = {
181
+ phase: "idle",
182
+ startedAt: 0,
183
+ elapsedMs: 0,
184
+ fallbackUrl: "",
185
+ port: 0,
186
+ logFile: "",
187
+ error: "",
188
+ note: "",
189
+ helper: null,
190
+ ack: null,
191
+ config: null,
192
+ reloadAt: null,
193
+ source: "web",
194
+ reason: "",
195
+ retrying: false
196
+ };
197
+ const listeners = /* @__PURE__ */ new Set();
198
+ let ticker = null;
199
+ let prober = null;
200
+ let probing = false;
201
+ /** Current snapshot (stable identity between mutations). */
202
+ function getState() {
203
+ return state;
204
+ }
205
+ /** Subscribe to state changes. */
206
+ function subscribe(listener) {
207
+ listeners.add(listener);
208
+ return () => {
209
+ listeners.delete(listener);
210
+ };
211
+ }
212
+ /** Merge a patch into the snapshot and notify subscribers. */
213
+ function setState(patch) {
214
+ state = {
215
+ ...state,
216
+ ...patch
217
+ };
218
+ for (const listener of listeners) listener();
219
+ }
220
+ /** Remember the essentials so a reload can resume the wait. */
221
+ function persist() {
222
+ try {
223
+ if (state.phase === "idle") {
224
+ sessionStorage.removeItem(STORAGE_KEY);
225
+ return;
226
+ }
227
+ sessionStorage.setItem(STORAGE_KEY, JSON.stringify({
228
+ phase: state.phase,
229
+ startedAt: state.startedAt,
230
+ fallbackUrl: state.fallbackUrl,
231
+ port: state.port,
232
+ logFile: state.logFile,
233
+ source: state.source,
234
+ reason: state.reason,
235
+ ack: state.ack
236
+ }));
237
+ } catch {}
238
+ }
239
+ /** Drop the persisted marker. */
240
+ function clearPersisted() {
241
+ try {
242
+ sessionStorage.removeItem(STORAGE_KEY);
243
+ } catch {}
244
+ }
245
+ /** Keep the two timers from stacking up. */
246
+ function stopLoops() {
247
+ if (ticker !== null) {
248
+ clearInterval(ticker);
249
+ ticker = null;
250
+ }
251
+ if (prober !== null) {
252
+ clearTimeout(prober);
253
+ prober = null;
254
+ }
255
+ probing = false;
256
+ }
257
+ /** Start the elapsed-time ticker (cheap; drives the overlay counter). */
258
+ function startTicker() {
259
+ if (ticker !== null) return;
260
+ ticker = setInterval(() => {
261
+ if (state.startedAt === 0) return;
262
+ setState({ elapsedMs: Date.now() - state.startedAt });
263
+ }, 250);
264
+ }
265
+ /** Reload as soon as the new host answered. */
266
+ function scheduleReload() {
267
+ if (state.reloadAt !== null) return;
268
+ setState({
269
+ reloadAt: Date.now(),
270
+ note: "已就绪,正在刷新页面…"
271
+ });
272
+ clearPersisted();
273
+ setTimeout(() => {
274
+ try {
275
+ location.reload();
276
+ } catch {}
277
+ }, RELOAD_DELAY_MS);
278
+ }
279
+ /** One reconnect probe; schedules the next one. */
280
+ async function probeOnce() {
281
+ if (probing) return;
282
+ probing = true;
283
+ let ok = false;
284
+ try {
285
+ await api$1.probe();
286
+ ok = true;
287
+ } catch {
288
+ ok = false;
289
+ }
290
+ probing = false;
291
+ if (ok) {
292
+ setState({
293
+ phase: "ready",
294
+ helper: null,
295
+ error: ""
296
+ });
297
+ stopLoops();
298
+ if (state.config?.autoReload === false) setState({ note: "新宿主已就绪,点击「刷新页面」加载新代码。" });
299
+ else scheduleReload();
300
+ persist();
301
+ return;
302
+ }
303
+ setState({
304
+ phase: state.phase === "failed" ? "failed" : "waiting",
305
+ note: state.phase === "failed" ? "启动失败,可重试或查看报错。" : "正在等待新宿主启动…"
306
+ });
307
+ const waited = Date.now() - state.startedAt;
308
+ if (waited >= HELPER_PROBE_AFTER_MS && state.fallbackUrl !== "") {
309
+ const helper = await fetchHelperDirect(state.fallbackUrl);
310
+ if (helper !== null) {
311
+ const failedNow = helper.phase === "failed";
312
+ setState({
313
+ helper,
314
+ error: failedNow ? helper.failure?.message ?? "启动失败(助手未给出原因)" : state.error,
315
+ phase: failedNow ? "failed" : "waiting",
316
+ note: failedNow ? "启动失败:新进程没能起来,下面是它的输出。" : "正在启动新宿主…(可通过恢复控制台查看日志)"
317
+ });
318
+ } else if (state.helper === null && waited > 2e4) setState({ note: "仍在等待新宿主;若长时间没有响应,请打开恢复控制台查看日志。" });
319
+ }
320
+ persist();
321
+ scheduleProbe(state.config?.probeIntervalMs ?? 1200);
322
+ }
323
+ /** Queue the next probe. */
324
+ function scheduleProbe(intervalMs) {
325
+ if (prober !== null) clearTimeout(prober);
326
+ prober = setTimeout(() => {
327
+ probeOnce();
328
+ }, Math.max(300, intervalMs));
329
+ }
330
+ /** Load the config once so the reconnect follows the user's preferences. */
331
+ async function refreshConfig() {
332
+ try {
333
+ const status = await api$1.status();
334
+ setState({ config: status.config });
335
+ return status.config;
336
+ } catch {
337
+ return null;
338
+ }
339
+ }
340
+ /**
341
+ * Start a restart and stay on top of it.
342
+ * @param reason - free-text reason recorded in the host's history.
343
+ * @param source - who asked (the panel passes 'web').
344
+ */
345
+ async function startRestart(reason = "", source = "web") {
346
+ if (state.phase === "requesting" || state.phase === "waiting") return;
347
+ stopLoops();
348
+ setState({
349
+ phase: "requesting",
350
+ startedAt: Date.now(),
351
+ elapsedMs: 0,
352
+ error: "",
353
+ note: "正在下发重启指令…",
354
+ helper: null,
355
+ ack: null,
356
+ reloadAt: null,
357
+ source,
358
+ reason,
359
+ retrying: false
360
+ });
361
+ persist();
362
+ try {
363
+ const ack = await api$1.restart(reason, source);
364
+ const config = state.config ?? await refreshConfig();
365
+ setState({
366
+ phase: "waiting",
367
+ ack,
368
+ fallbackUrl: ack.fallbackUrl,
369
+ port: ack.fallbackPort,
370
+ logFile: ack.logFile,
371
+ note: "旧进程正在退出,等待新宿主启动…",
372
+ config
373
+ });
374
+ persist();
375
+ startTicker();
376
+ scheduleProbe(1e3);
377
+ } catch (error) {
378
+ setState({
379
+ phase: "failed",
380
+ error: "重启指令下发失败:" + (error instanceof RestartApiError ? error.message : String(error instanceof Error ? error.message : error)),
381
+ note: "宿主没有接受重启请求,服务仍在运行。"
382
+ });
383
+ persist();
384
+ }
385
+ }
386
+ /** Probe right now (the overlay's "立即重试" button). */
387
+ async function checkNow() {
388
+ if (state.phase === "idle") return;
389
+ setState({ note: "正在检测…" });
390
+ await probeOnce();
391
+ }
392
+ /** Ask the helper to relaunch after a failed boot. */
393
+ async function retryBoot() {
394
+ if (state.phase !== "failed") return;
395
+ setState({
396
+ retrying: true,
397
+ note: "已请求重启助手再试一次…"
398
+ });
399
+ let ok = false;
400
+ try {
401
+ ok = (await api$1.helperRetry()).ok;
402
+ } catch {
403
+ ok = await requestHelperRetry(state.fallbackUrl);
404
+ }
405
+ if (!ok) ok = await requestHelperRetry(state.fallbackUrl);
406
+ setState({
407
+ retrying: false,
408
+ phase: ok ? "waiting" : "failed",
409
+ error: ok ? "" : state.error,
410
+ note: ok ? "重启助手正在重新拉起…" : "重试请求没有送达;请打开恢复控制台手动重试。"
411
+ });
412
+ if (ok) {
413
+ startTicker();
414
+ scheduleProbe(1e3);
415
+ }
416
+ }
417
+ /** Dismiss the overlay without touching the server. */
418
+ function dismiss() {
419
+ stopLoops();
420
+ clearPersisted();
421
+ setState({
422
+ phase: "idle",
423
+ startedAt: 0,
424
+ elapsedMs: 0,
425
+ error: "",
426
+ note: "",
427
+ helper: null,
428
+ ack: null,
429
+ reloadAt: null,
430
+ retrying: false
431
+ });
432
+ }
433
+ /**
434
+ * Resume a restart that was in flight when the page went away.
435
+ *
436
+ * Called once at mount by the overlay; a no-op when nothing is pending.
437
+ */
438
+ function resumeIfPending() {
439
+ if (state.phase !== "idle") return;
440
+ let raw = null;
441
+ try {
442
+ raw = sessionStorage.getItem(STORAGE_KEY);
443
+ } catch {
444
+ return;
445
+ }
446
+ if (raw === null) return;
447
+ let parsed = null;
448
+ try {
449
+ parsed = JSON.parse(raw);
450
+ } catch {
451
+ clearPersisted();
452
+ return;
453
+ }
454
+ const startedAt = typeof parsed?.startedAt === "number" ? parsed.startedAt : 0;
455
+ if (startedAt === 0 || Date.now() - startedAt > RESUME_WINDOW_MS) {
456
+ clearPersisted();
457
+ return;
458
+ }
459
+ if (parsed?.phase !== "waiting" && parsed?.phase !== "requesting" && parsed?.phase !== "failed") return;
460
+ setState({
461
+ phase: "waiting",
462
+ startedAt,
463
+ elapsedMs: Date.now() - startedAt,
464
+ fallbackUrl: typeof parsed.fallbackUrl === "string" ? parsed.fallbackUrl : "",
465
+ port: typeof parsed.port === "number" ? parsed.port : 0,
466
+ logFile: typeof parsed.logFile === "string" ? parsed.logFile : "",
467
+ source: typeof parsed.source === "string" ? parsed.source : "web",
468
+ reason: typeof parsed.reason === "string" ? parsed.reason : "",
469
+ ack: parsed.ack ?? null,
470
+ note: "检测到未完成的重启,继续等待新宿主…",
471
+ error: typeof parsed.error === "string" ? parsed.error : ""
472
+ });
473
+ refreshConfig().then(() => {
474
+ scheduleProbe(600);
475
+ });
476
+ startTicker();
477
+ }
478
+ /** React binding: re-renders the caller whenever the restart state changes. */
479
+ function useRestartState() {
480
+ return (0, react.useSyncExternalStore)(subscribe, getState, getState);
481
+ }
482
+ //#endregion
483
+ //#region src/client/RestartPanel.tsx
484
+ /**
485
+ * dsh-restart panel — the visible entry for the restart plugin.
486
+ *
487
+ * Rendered in two places from one component: as a settings-page section
488
+ * (`settings.section` slot, variant="settings") and inside the popover opened
489
+ * from the sidebar entry (variant="floating"). It shows what is running, offers
490
+ * the one-click restart, and — this is the point — surfaces the boot log and
491
+ * the lines that look like errors, so a plugin that fails to load is visible
492
+ * without going back to a terminal.
493
+ *
494
+ * Plain React, inline styles only, theme-agnostic, no emoji.
495
+ */
496
+ /** Module-level API client (stateless; the component closes over it). */
497
+ const api = new RestartApi();
498
+ /** Accent for primary actions. */
499
+ const ACCENT = "#2b6cb0";
500
+ /** Failure colour. */
501
+ const DANGER$1 = "#c0392b";
502
+ /** Healthy colour. */
503
+ const OK = "#2f9e5f";
504
+ /** One shared style sheet. */
505
+ const s = {
506
+ card: {
507
+ display: "flex",
508
+ flexDirection: "column",
509
+ gap: "12px",
510
+ maxWidth: "680px",
511
+ padding: "14px 16px",
512
+ borderRadius: "10px",
513
+ border: "1px solid rgba(128,128,128,0.3)",
514
+ fontSize: "13px",
515
+ color: "inherit",
516
+ boxSizing: "border-box"
517
+ },
518
+ floatCard: {
519
+ display: "flex",
520
+ flexDirection: "column",
521
+ gap: "10px",
522
+ width: "420px",
523
+ maxHeight: "74vh",
524
+ overflowY: "auto",
525
+ padding: "14px 16px",
526
+ borderRadius: "12px",
527
+ border: "1px solid rgba(128,128,128,0.3)",
528
+ fontSize: "13px",
529
+ color: "inherit",
530
+ boxSizing: "border-box"
531
+ },
532
+ head: {
533
+ display: "flex",
534
+ alignItems: "center",
535
+ gap: "8px"
536
+ },
537
+ dot: {
538
+ width: 8,
539
+ height: 8,
540
+ borderRadius: "50%",
541
+ flex: "none",
542
+ background: "#c9cdd4"
543
+ },
544
+ title: {
545
+ fontWeight: 600,
546
+ fontSize: "13px",
547
+ margin: 0,
548
+ flex: 1
549
+ },
550
+ grid: {
551
+ display: "grid",
552
+ gridTemplateColumns: "76px 1fr",
553
+ gap: "4px 12px",
554
+ fontSize: "12px"
555
+ },
556
+ label: { opacity: .6 },
557
+ value: { wordBreak: "break-all" },
558
+ primary: {
559
+ padding: "9px 14px",
560
+ borderRadius: "8px",
561
+ border: "1px solid #2b6cb0",
562
+ background: ACCENT,
563
+ color: "#fff",
564
+ cursor: "pointer",
565
+ fontSize: "13px",
566
+ fontWeight: 600
567
+ },
568
+ danger: {
569
+ padding: "9px 14px",
570
+ borderRadius: "8px",
571
+ border: "1px solid #c0392b",
572
+ background: DANGER$1,
573
+ color: "#fff",
574
+ cursor: "pointer",
575
+ fontSize: "13px",
576
+ fontWeight: 600
577
+ },
578
+ button: {
579
+ padding: "6px 11px",
580
+ borderRadius: "7px",
581
+ border: "1px solid rgba(128,128,128,0.35)",
582
+ background: "transparent",
583
+ color: "inherit",
584
+ cursor: "pointer",
585
+ fontSize: "12px"
586
+ },
587
+ row: {
588
+ display: "flex",
589
+ gap: "8px",
590
+ flexWrap: "wrap",
591
+ alignItems: "center"
592
+ },
593
+ log: {
594
+ margin: 0,
595
+ maxHeight: "180px",
596
+ overflow: "auto",
597
+ padding: "9px 11px",
598
+ borderRadius: "8px",
599
+ background: "rgba(128,128,128,0.10)",
600
+ border: "1px solid rgba(128,128,128,0.22)",
601
+ font: "11.5px/1.55 ui-monospace,SFMono-Regular,Menlo,monospace",
602
+ whiteSpace: "pre-wrap",
603
+ wordBreak: "break-word",
604
+ color: "inherit"
605
+ },
606
+ error: {
607
+ color: DANGER$1,
608
+ fontWeight: 600,
609
+ wordBreak: "break-word"
610
+ },
611
+ muted: {
612
+ opacity: .62,
613
+ fontSize: "12px"
614
+ },
615
+ section: {
616
+ display: "flex",
617
+ flexDirection: "column",
618
+ gap: "8px",
619
+ paddingTop: "10px",
620
+ borderTop: "1px solid rgba(128,128,128,0.22)"
621
+ },
622
+ field: {
623
+ display: "flex",
624
+ alignItems: "center",
625
+ gap: "8px",
626
+ justifyContent: "space-between"
627
+ },
628
+ input: {
629
+ width: "108px",
630
+ padding: "4px 7px",
631
+ borderRadius: "6px",
632
+ border: "1px solid rgba(128,128,128,0.35)",
633
+ background: "transparent",
634
+ color: "inherit",
635
+ fontSize: "12px",
636
+ boxSizing: "border-box"
637
+ },
638
+ badge: {
639
+ padding: "1px 7px",
640
+ borderRadius: "999px",
641
+ border: "1px solid rgba(128,128,128,0.35)",
642
+ fontSize: "11px",
643
+ opacity: .85
644
+ }
645
+ };
646
+ /** Human duration from milliseconds. */
647
+ function human$1(ms) {
648
+ if (!Number.isFinite(ms) || ms <= 0) return "—";
649
+ const total = Math.round(ms / 1e3);
650
+ if (total < 60) return `${total} 秒`;
651
+ const minutes = Math.floor(total / 60);
652
+ const seconds = total % 60;
653
+ if (minutes < 60) return `${minutes} 分 ${seconds} 秒`;
654
+ return `${Math.floor(minutes / 60)} 小时 ${minutes % 60} 分`;
655
+ }
656
+ /** Local time from an ISO string. */
657
+ function localTime(iso) {
658
+ if (iso === "") return "—";
659
+ const date = new Date(iso);
660
+ if (Number.isNaN(date.getTime())) return iso;
661
+ const pad = (value) => String(value).padStart(2, "0");
662
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
663
+ }
664
+ /** Phase label for the live helper. */
665
+ const PHASE_LABEL = {
666
+ "waiting-port-free": "等待旧进程退出",
667
+ starting: "正在启动",
668
+ "waiting-ready": "等待就绪",
669
+ ready: "已就绪",
670
+ retrying: "正在重试",
671
+ failed: "启动失败"
672
+ };
673
+ /** Copy text to the clipboard, reporting whether it worked. */
674
+ async function copyText(text) {
675
+ try {
676
+ await navigator.clipboard.writeText(text);
677
+ return true;
678
+ } catch {
679
+ return false;
680
+ }
681
+ }
682
+ /** One panel render. */
683
+ function RestartPanel(props) {
684
+ const variant = props.variant ?? "settings";
685
+ const live = useRestartState();
686
+ const [status, setStatus] = (0, react.useState)(null);
687
+ const [logs, setLogs] = (0, react.useState)(null);
688
+ const [draft, setDraft] = (0, react.useState)(null);
689
+ const [busy, setBusy] = (0, react.useState)(false);
690
+ const [error, setError] = (0, react.useState)("");
691
+ const [notice, setNotice] = (0, react.useState)("");
692
+ const [showLog, setShowLog] = (0, react.useState)(false);
693
+ const [showSettings, setShowSettings] = (0, react.useState)(false);
694
+ /** Load status + boot log. */
695
+ const load = (0, react.useCallback)(async () => {
696
+ setBusy(true);
697
+ try {
698
+ const next = await api.status();
699
+ setStatus(next);
700
+ setDraft((current) => current ?? next.config);
701
+ setError("");
702
+ const tail = await api.logs("latest", Math.max(60, next.config.logLines));
703
+ setLogs(tail);
704
+ } catch (caught) {
705
+ setError(caught instanceof RestartApiError ? caught.message : String(caught));
706
+ } finally {
707
+ setBusy(false);
708
+ }
709
+ }, []);
710
+ (0, react.useEffect)(() => {
711
+ load();
712
+ const timer = setInterval(() => {
713
+ if (live.phase === "idle") load();
714
+ }, 2e4);
715
+ return () => clearInterval(timer);
716
+ }, [load, live.phase]);
717
+ (0, react.useEffect)(() => {
718
+ if (!notice) return;
719
+ const timer = setTimeout(() => setNotice(""), 2600);
720
+ return () => clearTimeout(timer);
721
+ }, [notice]);
722
+ const restarting = live.phase === "requesting" || live.phase === "waiting";
723
+ const config = draft ?? status?.config ?? null;
724
+ /**
725
+ * Only ever show helper state that belongs to THIS host.
726
+ *
727
+ * A helper reports the pid it replaced (`oldPid`) and the pid it started
728
+ * (`childPid`); neither matching this process means it is somebody else's
729
+ * restart — a leftover helper on the fallback port, for instance. Rendering
730
+ * that as "重启助手 启动失败" claims a failure the user never had.
731
+ */
732
+ const ownedHelper = (0, react.useMemo)(() => {
733
+ const candidate = live.helper ?? status?.helper ?? null;
734
+ if (candidate === null) return null;
735
+ const hostPid = status?.host.pid;
736
+ if (hostPid === void 0) return live.phase !== "idle" ? candidate : null;
737
+ return candidate.oldPid === hostPid || candidate.childPid === hostPid ? candidate : null;
738
+ }, [
739
+ live.helper,
740
+ live.phase,
741
+ status?.helper,
742
+ status?.host.pid
743
+ ]);
744
+ const helper = ownedHelper;
745
+ const helperAlive = live.phase !== "idle" ? live.helper !== null : ownedHelper !== null && (status?.helperAlive ?? false);
746
+ const bootErrors = logs?.errorLines ?? [];
747
+ const consoleUrl = live.fallbackUrl !== "" ? live.fallbackUrl : status?.consoleUrl ?? "";
748
+ /** One-click restart. */
749
+ const onRestart = (0, react.useCallback)(async () => {
750
+ setNotice("");
751
+ if (restarting) return;
752
+ try {
753
+ await refreshConfig();
754
+ } catch {}
755
+ await startRestart("web 面板点击重启", "web");
756
+ }, [restarting]);
757
+ /**
758
+ * Copy a self-contained diagnosis report.
759
+ *
760
+ * The restart failure report (written by the helper) wins when it exists;
761
+ * otherwise compose the same shape from the status + boot log this panel
762
+ * already has, so the button is always useful.
763
+ */
764
+ const copyDiagnosis = (0, react.useCallback)(async () => {
765
+ const fromHelper = await fetchHelperReport(consoleUrl);
766
+ if (fromHelper !== "") {
767
+ const ok = await copyText(fromHelper);
768
+ setNotice(ok ? "已复制重启失败报告(来自恢复控制台),可直接粘贴给 AI" : "复制失败");
769
+ return;
770
+ }
771
+ const ok = await copyText([
772
+ "# DSH 重启插件诊断报告",
773
+ "",
774
+ `- 时间:${(/* @__PURE__ */ new Date()).toISOString()}`,
775
+ `- 宿主:pid ${status?.host.pid ?? "?"}|${status?.host.url ?? ""}|DSH ${status?.host.dshVersion ?? "?"}|Node ${status?.host.nodeVersion ?? "?"}`,
776
+ `- 已运行:${status === null ? "?" : human$1(status.host.uptimeMs)} 启动于 ${localTime(status?.host.startedAt ?? "")}`,
777
+ `- 启动命令:${status?.host.command ?? "?"}`,
778
+ `- 重启方式:${status?.host.launchd.managed === true ? `launchd ${status.host.launchd.label}(${status.host.launchd.state})` : "分离助手自拉起"}`,
779
+ `- 助手:${helperAlive ? `运行中(${PHASE_LABEL[helper?.phase ?? ""] ?? helper?.phase ?? "?"},第 ${helper?.attempt ?? 1}/${helper?.maxAttempts ?? 1} 次)` : "未运行"}`,
780
+ helper?.failure?.message !== void 0 ? `- 上次失败原因:${helper.failure.message}` : "",
781
+ helper?.childExit != null ? `- 退出码:${String(helper.childExit.code)}${helper.childExit.signal != null ? " / " + helper.childExit.signal : ""}` : "",
782
+ `- 启动日志:${logs?.file ?? "(无)"}`,
783
+ live.error !== "" ? `- 面板错误:${live.error}` : "",
784
+ error !== "" ? `- 接口错误:${error}` : "",
785
+ "",
786
+ "## 疑似报错行",
787
+ "",
788
+ "```",
789
+ bootErrors.length === 0 ? "(未识别出明显报错行)" : bootErrors.join("\n"),
790
+ "```",
791
+ "",
792
+ "## 启动日志(最后 80 行)",
793
+ "",
794
+ "```",
795
+ (logs?.lines ?? []).slice(-80).join("\n") || "(无日志)",
796
+ "```",
797
+ "",
798
+ "## 最近重启记录",
799
+ "",
800
+ ...(status?.history ?? []).slice(0, 5).map((record) => `- ${record.at}|${record.source}|${record.reason}|pid ${record.oldPid} → 助手 ${record.helperPid ?? "—"}`),
801
+ ""
802
+ ].filter((line) => line !== "").join("\n"));
803
+ setNotice(ok ? "已复制诊断报告,可直接粘贴给 AI" : "复制失败");
804
+ }, [
805
+ bootErrors,
806
+ consoleUrl,
807
+ error,
808
+ helper,
809
+ helperAlive,
810
+ live.error,
811
+ logs,
812
+ status
813
+ ]);
814
+ /** Persist the config patch. */
815
+ const saveConfig = (0, react.useCallback)(async (patch) => {
816
+ try {
817
+ const result = await api.setConfig(patch);
818
+ setDraft(result.config);
819
+ setStatus((current) => current === null ? current : {
820
+ ...current,
821
+ config: result.config
822
+ });
823
+ setNotice("已保存(下次重启生效的项会在重启后应用)");
824
+ setError("");
825
+ } catch (caught) {
826
+ setError(caught instanceof RestartApiError ? caught.message : String(caught));
827
+ }
828
+ }, []);
829
+ /** The boot-log block, shared by both variants. */
830
+ const logBlock = (0, react.useMemo)(() => {
831
+ if (logs === null) return null;
832
+ if (!logs.exists) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
833
+ style: s.muted,
834
+ children: "暂无启动日志(重启一次后,新宿主的输出会记录在这里)。"
835
+ });
836
+ const lines = logs.lines.slice(-80);
837
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
838
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
839
+ style: s.row,
840
+ children: [
841
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
842
+ style: s.muted,
843
+ children: logs.file
844
+ }),
845
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { marginLeft: "auto" } }),
846
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
847
+ type: "button",
848
+ style: s.button,
849
+ onClick: () => {
850
+ copyText([
851
+ ...bootErrors,
852
+ "",
853
+ ...lines
854
+ ].join("\n")).then((ok) => setNotice(ok ? "已复制启动日志" : "复制失败"));
855
+ },
856
+ children: "复制"
857
+ })
858
+ ]
859
+ }),
860
+ bootErrors.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
861
+ style: s.error,
862
+ children: [
863
+ "检测到 ",
864
+ bootErrors.length,
865
+ " 行疑似报错:"
866
+ ]
867
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
868
+ style: s.muted,
869
+ children: "未发现明显报错。"
870
+ }),
871
+ bootErrors.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
872
+ style: s.log,
873
+ children: bootErrors.join("\n")
874
+ }) : null,
875
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
876
+ style: s.log,
877
+ children: lines.join("\n")
878
+ })
879
+ ] });
880
+ }, [logs, bootErrors]);
881
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
882
+ style: variant === "settings" ? s.card : s.floatCard,
883
+ children: [
884
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
885
+ style: s.head,
886
+ children: [
887
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: {
888
+ ...s.dot,
889
+ background: error !== "" ? DANGER$1 : restarting ? "#e0a13a" : OK
890
+ } }),
891
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
892
+ style: s.title,
893
+ children: "重启 DSH"
894
+ }),
895
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
896
+ style: s.badge,
897
+ children: live.phase === "idle" ? "空闲" : PHASE_LABEL[live.phase] ?? live.phase
898
+ }),
899
+ props.onClose !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
900
+ type: "button",
901
+ style: s.button,
902
+ onClick: props.onClose,
903
+ children: "收起"
904
+ }) : null
905
+ ]
906
+ }),
907
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
908
+ style: s.grid,
909
+ children: [
910
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
911
+ style: s.label,
912
+ children: "宿主"
913
+ }),
914
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
915
+ style: s.value,
916
+ children: status === null ? "读取中…" : `pid ${status.host.pid} · ${status.host.url}`
917
+ }),
918
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
919
+ style: s.label,
920
+ children: "版本"
921
+ }),
922
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
923
+ style: s.value,
924
+ children: status === null ? "—" : `DSH ${status.host.dshVersion || "未知"} · Node ${status.host.nodeVersion}`
925
+ }),
926
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
927
+ style: s.label,
928
+ children: "已运行"
929
+ }),
930
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
931
+ style: s.value,
932
+ children: status === null ? "—" : `${human$1(status.host.uptimeMs)}(启动于 ${localTime(status.host.startedAt)})`
933
+ }),
934
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
935
+ style: s.label,
936
+ children: "重启方式"
937
+ }),
938
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
939
+ style: s.value,
940
+ children: [status === null ? "—" : status.host.launchd.managed ? `launchd 托管(${status.host.launchd.label}${status.host.launchd.state === "" ? "" : " · " + status.host.launchd.state})— 由 launchd 拉起,避免与自己拉起的进程抢端口` : "分离助手自拉起(等端口释放后用相同命令重启)", config !== null && config.restartMode !== "auto" ? ` · 配置强制:${config.restartMode}` : ""]
941
+ }),
942
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
943
+ style: s.label,
944
+ children: "启动命令"
945
+ }),
946
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
947
+ style: {
948
+ ...s.value,
949
+ fontFamily: "ui-monospace, Menlo, monospace",
950
+ fontSize: "11.5px"
951
+ },
952
+ children: status === null ? "—" : status.host.command
953
+ })
954
+ ]
955
+ }),
956
+ restarting || live.phase === "failed" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
957
+ style: live.phase === "failed" ? s.error : s.muted,
958
+ children: [live.note, live.phase === "failed" && live.error !== "" ? `|${live.error}` : ""]
959
+ }) : null,
960
+ error !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
961
+ style: s.error,
962
+ children: error
963
+ }) : null,
964
+ notice !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
965
+ style: s.muted,
966
+ children: notice
967
+ }) : null,
968
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
969
+ style: s.row,
970
+ children: [
971
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
972
+ type: "button",
973
+ style: restarting || busy ? {
974
+ ...s.primary,
975
+ opacity: .6,
976
+ cursor: "default"
977
+ } : s.primary,
978
+ disabled: restarting,
979
+ onClick: () => {
980
+ onRestart();
981
+ },
982
+ children: restarting ? "正在重启…" : "立即重启"
983
+ }),
984
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
985
+ type: "button",
986
+ style: s.button,
987
+ onClick: () => {
988
+ load();
989
+ },
990
+ children: "刷新状态"
991
+ }),
992
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
993
+ type: "button",
994
+ style: s.button,
995
+ onClick: () => {
996
+ copyDiagnosis();
997
+ },
998
+ children: "复制诊断报告"
999
+ }),
1000
+ consoleUrl !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1001
+ type: "button",
1002
+ style: s.button,
1003
+ onClick: () => window.open(consoleUrl, "_blank", "noopener"),
1004
+ children: "恢复控制台"
1005
+ }) : null
1006
+ ]
1007
+ }),
1008
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1009
+ style: s.muted,
1010
+ children: "点击后旧进程退出、分离的重启助手用完全相同的命令拉起新宿主,本页会自动重连并刷新;若新宿主启动失败,报错会直接显示在上方遮罩与恢复控制台。"
1011
+ }),
1012
+ helperAlive && helper !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1013
+ style: s.section,
1014
+ children: [
1015
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1016
+ style: s.row,
1017
+ children: [
1018
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "重启助手" }),
1019
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1020
+ style: s.badge,
1021
+ children: PHASE_LABEL[helper.phase ?? ""] ?? helper.phase ?? "未知"
1022
+ }),
1023
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1024
+ style: s.muted,
1025
+ children: [
1026
+ "第 ",
1027
+ helper.attempt ?? 1,
1028
+ "/",
1029
+ helper.maxAttempts ?? 1,
1030
+ " 次 · 已 ",
1031
+ human$1(helper.elapsedMs ?? 0)
1032
+ ]
1033
+ })
1034
+ ]
1035
+ }),
1036
+ helper.failure?.message !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1037
+ style: s.error,
1038
+ children: helper.failure.message
1039
+ }) : null,
1040
+ helper.errorLines !== void 0 && helper.errorLines.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
1041
+ style: s.log,
1042
+ children: helper.errorLines.map((entry) => entry.text).join("\n")
1043
+ }) : null,
1044
+ helper.logFile != null && helper.logFile !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1045
+ style: s.muted,
1046
+ children: ["日志:", helper.logFile]
1047
+ }) : null
1048
+ ]
1049
+ }) : null,
1050
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1051
+ style: s.section,
1052
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1053
+ style: s.row,
1054
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1055
+ type: "button",
1056
+ style: s.button,
1057
+ onClick: () => setShowLog((value) => !value),
1058
+ children: showLog ? "收起启动日志" : "上次启动日志"
1059
+ }), bootErrors.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1060
+ style: s.error,
1061
+ children: [bootErrors.length, " 行疑似报错"]
1062
+ }) : logs?.exists === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1063
+ style: s.muted,
1064
+ children: "无明显报错"
1065
+ }) : null]
1066
+ }), showLog ? logBlock : null]
1067
+ }),
1068
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1069
+ style: s.section,
1070
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1071
+ style: s.row,
1072
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "重启记录" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1073
+ style: s.muted,
1074
+ children: [status?.history.length ?? 0, " 条"]
1075
+ })]
1076
+ }), status === null || status.history.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1077
+ style: s.muted,
1078
+ children: "还没有通过本插件重启过。"
1079
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1080
+ style: {
1081
+ display: "flex",
1082
+ flexDirection: "column",
1083
+ gap: "4px"
1084
+ },
1085
+ children: status.history.slice(0, 6).map((record) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1086
+ style: s.muted,
1087
+ children: [
1088
+ localTime(record.at),
1089
+ " · ",
1090
+ record.source,
1091
+ record.reason === "" ? "" : `(${record.reason})`,
1092
+ " · pid ",
1093
+ record.oldPid,
1094
+ " →",
1095
+ " ",
1096
+ record.helperPid === null ? "—" : `助手 ${record.helperPid}`
1097
+ ]
1098
+ }, `${record.at}-${record.helperPid ?? 0}`))
1099
+ })]
1100
+ }),
1101
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1102
+ style: s.section,
1103
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1104
+ style: s.row,
1105
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1106
+ type: "button",
1107
+ style: s.button,
1108
+ onClick: () => setShowSettings((value) => !value),
1109
+ children: showSettings ? "收起设置" : "插件设置"
1110
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1111
+ style: s.muted,
1112
+ children: ["配置文件:", status?.configFile ?? "—"]
1113
+ })]
1114
+ }), showSettings && config !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1115
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1116
+ style: s.field,
1117
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "重启方式(auto 自动识别 launchd)" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1118
+ style: s.input,
1119
+ value: config.restartMode,
1120
+ onChange: (event) => void saveConfig({ restartMode: event.target.value }),
1121
+ children: [
1122
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1123
+ value: "auto",
1124
+ children: "auto"
1125
+ }),
1126
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1127
+ value: "launchd",
1128
+ children: "launchd"
1129
+ }),
1130
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1131
+ value: "helper",
1132
+ children: "helper"
1133
+ })
1134
+ ]
1135
+ })]
1136
+ }),
1137
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1138
+ style: s.field,
1139
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "新宿主应答后自动刷新页面" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1140
+ type: "checkbox",
1141
+ checked: config.autoReload,
1142
+ onChange: (event) => void saveConfig({ autoReload: event.target.checked })
1143
+ })]
1144
+ }),
1145
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1146
+ style: s.field,
1147
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "重启时显示全屏遮罩" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1148
+ type: "checkbox",
1149
+ checked: config.showOverlay,
1150
+ onChange: (event) => void saveConfig({ showOverlay: event.target.checked })
1151
+ })]
1152
+ }),
1153
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1154
+ style: s.field,
1155
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "启动超时(毫秒,3000-900000)" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1156
+ type: "number",
1157
+ style: s.input,
1158
+ defaultValue: config.bootTimeoutMs,
1159
+ onBlur: (event) => void saveConfig({ bootTimeoutMs: Number(event.target.value) })
1160
+ })]
1161
+ }),
1162
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1163
+ style: s.field,
1164
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "自动重试次数(1-5)" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1165
+ type: "number",
1166
+ style: s.input,
1167
+ defaultValue: config.maxAttempts,
1168
+ onBlur: (event) => void saveConfig({ maxAttempts: Number(event.target.value) })
1169
+ })]
1170
+ }),
1171
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1172
+ style: s.field,
1173
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "恢复控制台端口(默认 3099)" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1174
+ type: "number",
1175
+ style: s.input,
1176
+ defaultValue: config.fallbackPort,
1177
+ onBlur: (event) => void saveConfig({ fallbackPort: Number(event.target.value) })
1178
+ })]
1179
+ }),
1180
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1181
+ style: s.row,
1182
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1183
+ type: "button",
1184
+ style: s.button,
1185
+ onClick: () => {
1186
+ api.setConfig({ reset: true }).then((result) => {
1187
+ setDraft(result.config);
1188
+ setNotice("已恢复默认设置");
1189
+ }).catch((caught) => setError(String(caught)));
1190
+ },
1191
+ children: "恢复默认设置"
1192
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1193
+ style: s.muted,
1194
+ children: "重启助手每次重启都会重新读取这些设置。"
1195
+ })]
1196
+ })
1197
+ ] }) : null]
1198
+ })
1199
+ ]
1200
+ });
1201
+ }
1202
+ //#endregion
1203
+ //#region src/client/floating.tsx
1204
+ /**
1205
+ * Sidebar entry for dsh-restart.
1206
+ *
1207
+ * The restart control belongs next to the other plugin entries in the left
1208
+ * sidebar's settings area (`[class*="settingsArea"]`), not in a corner of its
1209
+ * own — my other plugins already mount there, so this entry joins their
1210
+ * horizontal row when it exists and creates the row when it does not.
1211
+ *
1212
+ * The button itself only opens the panel; the actual restart is the panel's
1213
+ * primary button, so a stray click in the sidebar can never kill the session.
1214
+ * The icon reflects live state (idle / restarting / failed) so the sidebar is
1215
+ * enough to tell whether something went wrong.
1216
+ *
1217
+ * If the settings area never appears (a shell without it), the entry falls back
1218
+ * to a fixed bottom-right ball. A MutationObserver re-places it whenever the
1219
+ * anchor changes, because the sidebar is React-rendered and may be recreated.
1220
+ */
1221
+ /** Container id, so a hot reload does not stack copies. */
1222
+ const CONTAINER_ID$1 = "dsh-restart-entry";
1223
+ /** Stylesheet id. */
1224
+ const STYLE_ID$1 = "dsh-restart/entry.css";
1225
+ /** Class marking the inline (sidebar) placement. */
1226
+ const INLINE_CLASS = "dshrst-inline";
1227
+ /** The settings area my other plugin entries mount into. */
1228
+ const SETTINGS_AREA_SELECTOR = "[class*=\"settingsArea\"]";
1229
+ /** The WeChat bridge's ball — used to seed a shared row when no row exists yet. */
1230
+ const ANCHOR_BALL_SELECTOR = ".dshwx-ball";
1231
+ /** Existing shared row (created by dsh-zhihu); reused when present. */
1232
+ const EXISTING_ROW_SELECTOR = ".dsh-zhihu-row";
1233
+ /** Row this plugin creates when nothing else provides one. */
1234
+ const ROW_CLASS = "dshrst-row";
1235
+ const ROW_SELECTOR = ".dshrst-row";
1236
+ /** Debounce for the placement observer (ms). */
1237
+ const PLACEMENT_DEBOUNCE_MS = 250;
1238
+ const BUSY_COLOR = "#e0a13a";
1239
+ const FAIL_COLOR = "#c0392b";
1240
+ const CSS$1 = [
1241
+ "#dsh-restart-entry .dshrst-fab{position:fixed;right:24px;bottom:24px;width:50px;height:50px;",
1242
+ "border-radius:50%;border:none;outline:none;cursor:pointer;z-index:2147483000;",
1243
+ "background:#2b6cb0;color:#fff;display:flex;align-items:center;justify-content:center;",
1244
+ "box-shadow:0 6px 20px rgba(43,108,176,.32);transition:transform .15s,background .2s}",
1245
+ "#dsh-restart-entry .dshrst-fab:hover{transform:scale(1.06)}",
1246
+ ".dshrst-row{display:flex;align-items:center;justify-content:flex-start}",
1247
+ "#dsh-restart-entry.dshrst-inline{display:flex;align-items:center;flex:none}",
1248
+ "#dsh-restart-entry.dshrst-inline .dshrst-fab{position:static;width:36px;height:36px;",
1249
+ "margin:0 0 0 8px;border-radius:8px;background:transparent;color:inherit;box-shadow:none;",
1250
+ "border:1px solid rgba(128,128,128,.35);opacity:.78;transition:opacity .15s,border-color .15s,color .2s}",
1251
+ "#dsh-restart-entry.dshrst-inline .dshrst-fab:hover{opacity:1;border-color:rgba(128,128,128,.7);transform:none}",
1252
+ "#dsh-restart-entry .dshrst-spin{animation:dshrst-side-spin 1s linear infinite}",
1253
+ "@keyframes dshrst-side-spin{to{transform:rotate(360deg)}}",
1254
+ "#dsh-restart-entry .dshrst-pop{position:fixed;right:24px;bottom:86px;z-index:2147483001;",
1255
+ "border-radius:12px;box-shadow:0 14px 44px rgba(0,0,0,.22);overflow:hidden;color:inherit}",
1256
+ "#dsh-restart-entry.dshrst-inline .dshrst-pop{right:auto;left:24px;bottom:92px}"
1257
+ ].join("");
1258
+ /** Inject the stylesheet once. */
1259
+ function injectStyles$1() {
1260
+ if (document.querySelector("style[data-plugin-css=" + JSON.stringify(STYLE_ID$1) + "]") !== null) return;
1261
+ const style = document.createElement("style");
1262
+ style.dataset.plugin = "dsh-restart";
1263
+ style.dataset.pluginCss = STYLE_ID$1;
1264
+ style.textContent = CSS$1;
1265
+ document.head.appendChild(style);
1266
+ }
1267
+ /** Sample the shell's surface colour so the popover matches the active theme. */
1268
+ function surfaceColor() {
1269
+ const isOpaque = (value) => value !== "" && value !== "transparent" && value !== "rgba(0, 0, 0, 0)";
1270
+ const body = getComputedStyle(document.body).backgroundColor;
1271
+ if (isOpaque(body)) return body;
1272
+ const html = getComputedStyle(document.documentElement).backgroundColor;
1273
+ if (isOpaque(html)) return html;
1274
+ const root = document.documentElement;
1275
+ return root.classList.contains("dark") || root.dataset.theme === "dark" || window.matchMedia?.("(prefers-color-scheme: dark)").matches === true ? "#1c1c1e" : "#ffffff";
1276
+ }
1277
+ /** The circular-arrow glyph (monochrome, no emoji). */
1278
+ function RestartIcon(props) {
1279
+ return (0, react.createElement)("svg", {
1280
+ viewBox: "0 0 24 24",
1281
+ width: 17,
1282
+ height: 17,
1283
+ fill: "none",
1284
+ stroke: "currentColor",
1285
+ strokeWidth: 2,
1286
+ strokeLinecap: "round",
1287
+ strokeLinejoin: "round",
1288
+ "aria-hidden": true,
1289
+ className: props.spinning ? "dshrst-spin" : void 0
1290
+ }, (0, react.createElement)("path", { d: "M21 12a9 9 0 1 1-2.64-6.36" }), (0, react.createElement)("polyline", { points: "21 3 21 9 15 9" }));
1291
+ }
1292
+ /** The entry button plus its popover. */
1293
+ function Entry(props) {
1294
+ const [open, setOpen] = (0, react.useState)(false);
1295
+ const live = useRestartState();
1296
+ const busy = live.phase === "requesting" || live.phase === "waiting";
1297
+ const failed = live.phase === "failed";
1298
+ const colour = failed ? FAIL_COLOR : busy ? BUSY_COLOR : void 0;
1299
+ return (0, react.createElement)("div", null, open ? (0, react.createElement)("div", {
1300
+ className: "dshrst-pop",
1301
+ style: { background: surfaceColor() }
1302
+ }, (0, react.createElement)(RestartPanel, {
1303
+ variant: "floating",
1304
+ onClose: () => setOpen(false)
1305
+ })) : null, (0, react.createElement)("button", {
1306
+ type: "button",
1307
+ className: "dshrst-fab",
1308
+ style: colour === void 0 ? void 0 : {
1309
+ background: props.mode === "ball" ? colour : void 0,
1310
+ color: props.mode === "ball" ? "#fff" : colour
1311
+ },
1312
+ title: failed ? "DSH 重启失败 — 点击查看报错" : busy ? "DSH 正在重启…" : "重启 DSH",
1313
+ "aria-label": "重启 DSH",
1314
+ onClick: () => setOpen((value) => !value)
1315
+ }, (0, react.createElement)(RestartIcon, { spinning: busy })));
1316
+ }
1317
+ /** React root handle. */
1318
+ let root$1 = null;
1319
+ /** Place the entry into the sidebar (or fall back to a fixed ball). */
1320
+ function place(container, mode) {
1321
+ if (mode === "ball") {
1322
+ if (container.parentElement !== document.body) document.body.appendChild(container);
1323
+ container.classList.remove(INLINE_CLASS);
1324
+ return;
1325
+ }
1326
+ const settingsArea = document.querySelector(SETTINGS_AREA_SELECTOR);
1327
+ if (settingsArea === null) {
1328
+ if (container.parentElement !== document.body) document.body.appendChild(container);
1329
+ container.classList.remove(INLINE_CLASS);
1330
+ return;
1331
+ }
1332
+ let row = document.querySelector(EXISTING_ROW_SELECTOR) ?? document.querySelector(ROW_SELECTOR);
1333
+ const anchor = document.querySelector(ANCHOR_BALL_SELECTOR);
1334
+ if (row === null && anchor !== null && anchor.parentElement !== null) {
1335
+ const parent = anchor.parentElement;
1336
+ if (parent === settingsArea || settingsArea.contains(parent)) {
1337
+ const created = document.createElement("div");
1338
+ created.className = ROW_CLASS;
1339
+ parent.insertBefore(created, anchor);
1340
+ created.appendChild(anchor);
1341
+ row = created;
1342
+ }
1343
+ }
1344
+ const target = row ?? settingsArea;
1345
+ if (container.parentElement !== target) target.appendChild(container);
1346
+ container.classList.add(INLINE_CLASS);
1347
+ }
1348
+ /** Mount the sidebar entry (idempotent). */
1349
+ async function mountRestartEntry() {
1350
+ if (typeof document === "undefined") return;
1351
+ injectStyles$1();
1352
+ let config = null;
1353
+ try {
1354
+ config = (await new RestartApi().status()).config;
1355
+ } catch {
1356
+ config = null;
1357
+ }
1358
+ const entryMode = config?.entry ?? "sidebar";
1359
+ if (entryMode === "off") return;
1360
+ let container = document.getElementById(CONTAINER_ID$1);
1361
+ if (container === null) {
1362
+ container = document.createElement("div");
1363
+ container.id = CONTAINER_ID$1;
1364
+ container.dataset.plugin = "dsh-restart";
1365
+ }
1366
+ const mode = entryMode === "ball" ? "ball" : "sidebar";
1367
+ place(container, mode);
1368
+ if (root$1 === null) {
1369
+ root$1 = (0, react_dom_client.createRoot)(container);
1370
+ root$1.render((0, react.createElement)(Entry, { mode }));
1371
+ }
1372
+ if (entryMode === "both") {
1373
+ const ballId = "dsh-restart-entry-ball";
1374
+ if (document.getElementById(ballId) === null) {
1375
+ const ball = document.createElement("div");
1376
+ ball.id = ballId;
1377
+ ball.dataset.plugin = "dsh-restart";
1378
+ document.body.appendChild(ball);
1379
+ (0, react_dom_client.createRoot)(ball).render((0, react.createElement)(Entry, { mode: "ball" }));
1380
+ }
1381
+ }
1382
+ let timer = null;
1383
+ new MutationObserver(() => {
1384
+ if (timer !== null) clearTimeout(timer);
1385
+ timer = setTimeout(() => {
1386
+ if (entryMode !== "ball") place(container, "sidebar");
1387
+ }, PLACEMENT_DEBOUNCE_MS);
1388
+ }).observe(document.body, {
1389
+ childList: true,
1390
+ subtree: true
1391
+ });
1392
+ }
1393
+ //#endregion
1394
+ //#region src/client/overlay.tsx
1395
+ /**
1396
+ * dsh-restart — the full-screen restart overlay.
1397
+ *
1398
+ * Restarting DSH kills the very server this page is talking to, so without an
1399
+ * overlay the tab just goes dead: no spinner, no progress, no explanation.
1400
+ * This layer covers the shell while the handoff happens, then reloads the page
1401
+ * by itself. When the new host fails to boot, it shows the failing output — the
1402
+ * helper streams it to the recovery console, which is still reachable even
1403
+ * though DSH is not.
1404
+ *
1405
+ * Rendered from its own React root so it survives any shell re-render.
1406
+ */
1407
+ /** Container id, so a hot reload does not stack overlays. */
1408
+ const CONTAINER_ID = "dsh-restart-overlay-root";
1409
+ /** Stylesheet id. */
1410
+ const STYLE_ID = "dsh-restart/overlay.css";
1411
+ const DANGER = "#c0392b";
1412
+ const CSS = [
1413
+ "#dsh-restart-overlay-root .dshrst-mask{position:fixed;inset:0;z-index:2147483100;",
1414
+ "background:rgba(12,14,18,.42);backdrop-filter:blur(3px);-webkit-backdrop-filter:blur(3px);",
1415
+ "display:flex;align-items:flex-start;justify-content:center;padding:8vh 20px 40px;overflow:auto}",
1416
+ "#dsh-restart-overlay-root .dshrst-card{width:100%;max-width:680px;border-radius:14px;overflow:hidden;",
1417
+ "box-shadow:0 24px 70px rgba(0,0,0,.35);border:1px solid rgba(128,128,128,.28);color:inherit;",
1418
+ "background:var(--dshrst-surface,#fff);display:flex;flex-direction:column}",
1419
+ "#dsh-restart-overlay-root .dshrst-head{display:flex;align-items:center;gap:10px;padding:16px 20px;",
1420
+ "border-bottom:1px solid rgba(128,128,128,.2)}",
1421
+ "#dsh-restart-overlay-root .dshrst-spin{width:15px;height:15px;border-radius:50%;flex:none;",
1422
+ "border:2px solid rgba(128,128,128,.35);border-top-color:#2b6cb0;animation:dshrst-spin .8s linear infinite}",
1423
+ "@keyframes dshrst-spin{to{transform:rotate(360deg)}}",
1424
+ "#dsh-restart-overlay-root .dshrst-body{padding:16px 20px;display:flex;flex-direction:column;gap:12px}",
1425
+ "#dsh-restart-overlay-root .dshrst-log{margin:0;max-height:220px;overflow:auto;padding:10px 12px;",
1426
+ "border-radius:8px;background:rgba(128,128,128,.10);border:1px solid rgba(128,128,128,.22);",
1427
+ "font:12px/1.55 ui-monospace,SFMono-Regular,Menlo,monospace;white-space:pre-wrap;word-break:break-word}",
1428
+ "#dsh-restart-overlay-root .dshrst-actions{display:flex;gap:8px;flex-wrap:wrap}",
1429
+ "#dsh-restart-overlay-root button{font:inherit;font-size:13px;padding:7px 13px;border-radius:7px;",
1430
+ "border:1px solid rgba(128,128,128,.35);background:transparent;color:inherit;cursor:pointer}",
1431
+ "#dsh-restart-overlay-root button:hover{border-color:rgba(128,128,128,.65)}",
1432
+ "#dsh-restart-overlay-root button.primary{background:#2b6cb0;border-color:#2b6cb0;color:#fff}",
1433
+ "#dsh-restart-overlay-root button.danger{background:#c0392b;border-color:#c0392b;color:#fff}",
1434
+ "#dsh-restart-overlay-root .dshrst-err{color:#c0392b;font-weight:600}",
1435
+ "#dsh-restart-overlay-root .dshrst-muted{opacity:.66;font-size:12px}",
1436
+ "#dsh-restart-overlay-root .dshrst-steps{display:flex;gap:6px;flex-wrap:wrap;font-size:12px}",
1437
+ "#dsh-restart-overlay-root .dshrst-step{padding:2px 9px;border-radius:999px;border:1px solid rgba(128,128,128,.28)}",
1438
+ "#dsh-restart-overlay-root .dshrst-step.on{border-color:#2b6cb0;color:#2b6cb0}"
1439
+ ].join("");
1440
+ /** Inject the overlay stylesheet once. */
1441
+ function injectStyles() {
1442
+ if (document.querySelector("style[data-plugin-css=" + JSON.stringify(STYLE_ID) + "]") !== null) return;
1443
+ const style = document.createElement("style");
1444
+ style.dataset.plugin = "dsh-restart";
1445
+ style.dataset.pluginCss = STYLE_ID;
1446
+ style.textContent = CSS;
1447
+ document.head.appendChild(style);
1448
+ }
1449
+ /** The four steps shown as a progress strip. */
1450
+ const STEPS = [
1451
+ {
1452
+ key: "requesting",
1453
+ label: "下发指令"
1454
+ },
1455
+ {
1456
+ key: "restarting",
1457
+ label: "旧进程退出"
1458
+ },
1459
+ {
1460
+ key: "booting",
1461
+ label: "新宿主启动"
1462
+ },
1463
+ {
1464
+ key: "ready",
1465
+ label: "已就绪"
1466
+ }
1467
+ ];
1468
+ /** Which step is active for a given phase. */
1469
+ function stepIndex(state) {
1470
+ if (state.phase === "requesting") return 0;
1471
+ if (state.phase === "ready") return 3;
1472
+ if (state.phase === "failed") return 1;
1473
+ const waited = state.elapsedMs;
1474
+ return waited < 2500 ? 0 : waited < 6e3 ? 1 : 2;
1475
+ }
1476
+ /** Title for the overlay header. */
1477
+ function titleOf(state) {
1478
+ if (state.phase === "requesting") return "正在重启 DSH…";
1479
+ if (state.phase === "waiting") return "正在重启 DSH…";
1480
+ if (state.phase === "ready") return "DSH 已就绪";
1481
+ return "DSH 启动失败";
1482
+ }
1483
+ /** Human duration. */
1484
+ function human(ms) {
1485
+ const total = Math.max(0, Math.round(ms / 1e3));
1486
+ const minutes = Math.floor(total / 60);
1487
+ const seconds = total % 60;
1488
+ return minutes > 0 ? `${minutes} 分 ${seconds} 秒` : `${seconds} 秒`;
1489
+ }
1490
+ /** One overlay render. */
1491
+ function Overlay() {
1492
+ const state = useRestartState();
1493
+ const logRef = (0, react.useRef)(null);
1494
+ const [copied, setCopied] = (0, react.useState)(false);
1495
+ const visible = state.phase !== "idle" && (state.config === null || state.config.showOverlay !== false);
1496
+ const tail = (0, react.useMemo)(() => {
1497
+ const lines = state.helper?.tail ?? [];
1498
+ return (state.helper === null && state.error !== "" ? [state.error] : lines).slice(-40);
1499
+ }, [state.helper, state.error]);
1500
+ (0, react.useEffect)(() => {
1501
+ if (logRef.current !== null) logRef.current.scrollTop = logRef.current.scrollHeight;
1502
+ }, [tail]);
1503
+ (0, react.useEffect)(() => {
1504
+ if (!copied) return;
1505
+ const timer = setTimeout(() => setCopied(false), 1800);
1506
+ return () => clearTimeout(timer);
1507
+ }, [copied]);
1508
+ if (!visible) return null;
1509
+ const active = stepIndex(state);
1510
+ const errorText = state.error !== "" ? state.error : state.helper?.failure?.message ?? "";
1511
+ const exit = state.helper?.childExit;
1512
+ const copy = async () => {
1513
+ const report = await fetchHelperReport(state.fallbackUrl);
1514
+ if (report !== "") try {
1515
+ await navigator.clipboard?.writeText(report);
1516
+ setCopied(true);
1517
+ return;
1518
+ } catch {}
1519
+ const text = [
1520
+ `DSH 重启${state.phase === "failed" ? "失败" : ""}报告`,
1521
+ `时间:${new Date(state.startedAt).toISOString()}`,
1522
+ `已等待:${human(state.elapsedMs)}`,
1523
+ errorText === "" ? "" : `错误:${errorText}`,
1524
+ exit != null ? `退出码:${String(exit.code)}${exit.signal != null ? " / " + exit.signal : ""}` : "",
1525
+ state.logFile !== "" ? `日志:${state.logFile}` : "",
1526
+ state.helper?.errorLines?.length ? "\n—— 疑似报错 ——\n" + state.helper.errorLines.map((e) => e.text).join("\n") : "",
1527
+ tail.length > 0 ? "\n—— 启动输出 ——\n" + tail.join("\n") : ""
1528
+ ].filter((line) => line !== "").join("\n");
1529
+ try {
1530
+ await navigator.clipboard?.writeText(text);
1531
+ setCopied(true);
1532
+ } catch {
1533
+ setCopied(false);
1534
+ }
1535
+ };
1536
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1537
+ className: "dshrst-mask",
1538
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1539
+ className: "dshrst-card",
1540
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1541
+ className: "dshrst-head",
1542
+ children: [
1543
+ state.phase === "ready" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1544
+ className: "dshrst-spin",
1545
+ style: state.phase === "failed" ? { borderTopColor: DANGER } : void 0
1546
+ }),
1547
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
1548
+ style: { fontSize: 15 },
1549
+ children: titleOf(state)
1550
+ }),
1551
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1552
+ className: "dshrst-muted",
1553
+ style: { marginLeft: "auto" },
1554
+ children: ["已等待 ", human(state.elapsedMs)]
1555
+ })
1556
+ ]
1557
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1558
+ className: "dshrst-body",
1559
+ children: [
1560
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1561
+ className: "dshrst-steps",
1562
+ children: STEPS.map((step, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1563
+ className: "dshrst-step" + (index <= active ? " on" : ""),
1564
+ style: state.phase === "failed" && index === active ? {
1565
+ borderColor: DANGER,
1566
+ color: DANGER
1567
+ } : void 0,
1568
+ children: step.label
1569
+ }, step.key))
1570
+ }),
1571
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1572
+ className: "dshrst-muted",
1573
+ children: state.note
1574
+ }),
1575
+ errorText !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1576
+ className: "dshrst-err",
1577
+ children: errorText
1578
+ }) : null,
1579
+ state.helper !== null || tail.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
1580
+ className: "dshrst-log",
1581
+ ref: logRef,
1582
+ children: tail.length > 0 ? tail.join("\n") : "(等待新进程输出…)"
1583
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1584
+ className: "dshrst-muted",
1585
+ children: "等待新进程输出…(旧进程退出后,重启助手会接管并记录日志)"
1586
+ }),
1587
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1588
+ className: "dshrst-muted",
1589
+ style: {
1590
+ display: "flex",
1591
+ gap: 12,
1592
+ flexWrap: "wrap"
1593
+ },
1594
+ children: [
1595
+ state.logFile !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: ["日志:", state.logFile] }) : null,
1596
+ state.helper?.childPid != null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: ["新进程 pid:", state.helper.childPid] }) : null,
1597
+ state.ack !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: ["助手 pid:", state.ack.helperPid ?? "—"] }) : null
1598
+ ]
1599
+ }),
1600
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1601
+ className: "dshrst-actions",
1602
+ children: [
1603
+ state.phase === "ready" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1604
+ type: "button",
1605
+ className: "primary",
1606
+ onClick: () => location.reload(),
1607
+ children: "刷新页面"
1608
+ }) : null,
1609
+ state.phase === "failed" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1610
+ type: "button",
1611
+ className: "danger",
1612
+ onClick: () => void retryBoot(),
1613
+ disabled: state.retrying,
1614
+ children: state.retrying ? "正在重试…" : "让助手重试启动"
1615
+ }) : null,
1616
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1617
+ type: "button",
1618
+ onClick: () => void checkNow(),
1619
+ children: "立即检测"
1620
+ }),
1621
+ state.fallbackUrl !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1622
+ type: "button",
1623
+ onClick: () => window.open(state.fallbackUrl, "_blank", "noopener"),
1624
+ children: "打开恢复控制台"
1625
+ }) : null,
1626
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1627
+ type: "button",
1628
+ onClick: () => void copy(),
1629
+ children: copied ? "已复制" : "复制完整报告"
1630
+ }),
1631
+ state.phase === "failed" || state.phase === "ready" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1632
+ type: "button",
1633
+ onClick: () => {
1634
+ dismiss();
1635
+ },
1636
+ children: "关闭遮罩"
1637
+ }) : null
1638
+ ]
1639
+ }),
1640
+ state.phase !== "failed" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1641
+ className: "dshrst-muted",
1642
+ children: "重启期间这个页面会自动重连;新宿主一旦应答,页面会自动刷新加载新代码。"
1643
+ }) : null
1644
+ ]
1645
+ })]
1646
+ })
1647
+ });
1648
+ }
1649
+ /** React root handle, so mounting twice is a no-op. */
1650
+ let root = null;
1651
+ /** Mount the overlay root (called once by the client entry). */
1652
+ function mountRestartOverlay() {
1653
+ if (root !== null) return;
1654
+ if (typeof document === "undefined") return;
1655
+ injectStyles();
1656
+ let container = document.getElementById(CONTAINER_ID);
1657
+ if (container === null) {
1658
+ container = document.createElement("div");
1659
+ container.id = CONTAINER_ID;
1660
+ container.dataset.plugin = "dsh-restart";
1661
+ document.body.appendChild(container);
1662
+ }
1663
+ root = (0, react_dom_client.createRoot)(container);
1664
+ root.render(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Overlay, {}));
1665
+ resumeIfPending();
1666
+ }
1667
+ //#endregion
1668
+ //#region src/client/index.ts
1669
+ /** Required services. */
1670
+ const inject = ["slots"];
1671
+ /**
1672
+ * Register the settings card, mount the sidebar entry and the overlay.
1673
+ * @param ctx - client root context.
1674
+ */
1675
+ function apply(ctx) {
1676
+ try {
1677
+ ctx.slots.inject("settings.section", () => ctx.slots.register({
1678
+ name: "settings.section",
1679
+ id: "restart",
1680
+ order: 338,
1681
+ label: () => "重启"
1682
+ }, RestartPanel));
1683
+ } catch (error) {
1684
+ console.warn("[dsh-restart] settings panel registration failed:", error);
1685
+ }
1686
+ try {
1687
+ mountRestartOverlay();
1688
+ } catch (error) {
1689
+ console.warn("[dsh-restart] overlay mount failed:", error);
1690
+ }
1691
+ try {
1692
+ mountRestartEntry().catch((error) => {
1693
+ console.warn("[dsh-restart] sidebar entry mount failed:", error);
1694
+ });
1695
+ } catch (error) {
1696
+ console.warn("[dsh-restart] sidebar entry mount failed:", error);
1697
+ }
1698
+ }
1699
+ //#endregion
1700
+ exports.apply = apply;
1701
+ exports.inject = inject;
1702
+ return module.exports;
1703
+ }
1704
+ });
1705
+
1706
+ //# sourceMappingURL=client.js.map