@wenbin_wb/dsh-bridge 2.6.0 → 2.7.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/client/client.js CHANGED
@@ -40,9 +40,16 @@ var BRIDGE_ENDPOINTS = {
40
40
  startCloudflared: "startCloudflared",
41
41
  stopCloudflared: "stopCloudflared",
42
42
  resetCloudflared: "resetCloudflared",
43
+ saveCloudflaredConfig: "saveCloudflaredConfig",
44
+ setTunnelAutoStart: "setTunnelAutoStart",
43
45
  saveCustomTunnelConfig: "saveCustomTunnelConfig",
44
46
  checkVersion: "checkVersion",
45
47
  upgradePlugin: "upgradePlugin",
48
+ restartDsh: "restartDsh",
49
+ exportBackup: "exportBackup",
50
+ importBackup: "importBackup",
51
+ diagnoseNetwork: "diagnoseNetwork",
52
+ getSystemMetrics: "getSystemMetrics",
46
53
  // 访问安全认证(密码保护 / 扫码免密 Token)
47
54
  authGetStatus: "authGetStatus",
48
55
  authUpdateConfig: "authUpdateConfig",
@@ -185,34 +192,14 @@ var Icons = {
185
192
  "svg",
186
193
  { viewBox: "0 0 24 24", width: 12, height: 12, fill: "none", stroke: "currentColor", strokeWidth: 2.5, strokeLinecap: "round", strokeLinejoin: "round", ...props },
187
194
  React.createElement("polyline", { points: "20 6 9 17 4 12" })
195
+ ),
196
+ ops: (props) => React.createElement(
197
+ "svg",
198
+ { viewBox: "0 0 24 24", width: 16, height: 16, fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", ...props },
199
+ React.createElement("circle", { cx: 12, cy: 12, r: 3 }),
200
+ React.createElement("path", { d: "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" })
188
201
  )
189
202
  };
190
- function useCopy() {
191
- const [copied, setCopied] = React.useState(false);
192
- const copy = React.useCallback((text) => {
193
- const done = () => {
194
- setCopied(true);
195
- setTimeout(() => setCopied(false), 2e3);
196
- };
197
- if (navigator.clipboard?.writeText) {
198
- navigator.clipboard.writeText(text).then(done).catch(() => fallbackCopy());
199
- } else {
200
- fallbackCopy();
201
- }
202
- function fallbackCopy() {
203
- const textarea = document.createElement("textarea");
204
- textarea.value = text;
205
- textarea.style.position = "fixed";
206
- textarea.style.opacity = "0";
207
- document.body.appendChild(textarea);
208
- textarea.select();
209
- const success = document.execCommand("copy");
210
- document.body.removeChild(textarea);
211
- if (success) done();
212
- }
213
- }, []);
214
- return [copied, copy];
215
- }
216
203
  function StatusTag({ running, status }) {
217
204
  let bg = "var(--dsw-alias-bg-layer-2,#f3f4f6)";
218
205
  let color = "var(--dsw-alias-label-secondary,#6b7280)";
@@ -336,7 +323,12 @@ function QrBlock({ url, qr, onReset, auth, onNavigateSecurity }) {
336
323
  "div",
337
324
  { style: { marginTop: 10 } },
338
325
  React.createElement("img", { src: qr, alt: "QR", style: s.qr }),
339
- React.createElement("div", { style: { ...s.muted, marginTop: 4 } }, "\u8BF7\u5728\u79C1\u5BC6\u73AF\u5883\u4E0B\u4F7F\u7528")
326
+ React.createElement("div", { style: { ...s.muted, marginTop: 4 } }, "\u8BF7\u5728\u79C1\u5BC6\u73AF\u5883\u4E0B\u4F7F\u7528"),
327
+ React.createElement(
328
+ "div",
329
+ { style: { ...s.muted, marginTop: 4, fontSize: 11, color: "var(--dsw-alias-brand-primary, #4f6ef7)" } },
330
+ "\u{1F4F1} \u63D0\u793A\uFF1A\u624B\u673A\u6D4F\u89C8\u5668\u626B\u7801\u6253\u5F00\u540E\uFF0C\u5728\u83DC\u5355\u70B9\u51FB\u300C\u6DFB\u52A0\u5230\u4E3B\u5C4F\u5E55\u300D\u5373\u53EF\u4F5C\u4E3A\u72EC\u7ACB\u5168\u5C4F App \u8FD0\u884C\u3002"
331
+ )
340
332
  ),
341
333
  onReset && React.createElement(
342
334
  "div",
@@ -447,7 +439,19 @@ var CustomTunnelConfigForm = React.memo(function CustomTunnelConfigForm2({ serve
447
439
  )
448
440
  );
449
441
  });
450
- var TunnelCard = React.memo(function TunnelCard2({ title, desc, data, onStart, onStop, onReset, auth, onNavigateSecurity, children }) {
442
+ var TunnelCard = React.memo(function TunnelCard2({
443
+ title,
444
+ desc,
445
+ data,
446
+ autoStart,
447
+ onToggleAutoStart,
448
+ onStart,
449
+ onStop,
450
+ onReset,
451
+ auth,
452
+ onNavigateSecurity,
453
+ children
454
+ }) {
451
455
  const { running, configured, url, qr, state } = data ?? {};
452
456
  const phase = state?.phase ?? "idle";
453
457
  return React.createElement(
@@ -462,7 +466,32 @@ var TunnelCard = React.memo(function TunnelCard2({ title, desc, data, onStart, o
462
466
  React.createElement("div", { style: s.label }, title),
463
467
  React.createElement("div", { style: { ...s.muted, marginTop: 2 } }, desc)
464
468
  ),
465
- React.createElement(StatusTag, { running })
469
+ React.createElement(
470
+ "div",
471
+ { style: { display: "flex", flexDirection: "column", alignItems: "flex-end", gap: 6 } },
472
+ React.createElement(StatusTag, { running }),
473
+ onToggleAutoStart && React.createElement(
474
+ "label",
475
+ {
476
+ style: {
477
+ display: "flex",
478
+ alignItems: "center",
479
+ gap: 4,
480
+ fontSize: 11,
481
+ color: "var(--dsw-alias-label-secondary,#6b7280)",
482
+ cursor: "pointer",
483
+ userSelect: "none"
484
+ },
485
+ title: "DSH \u542F\u52A8\u65F6\u81EA\u52A8\u6062\u590D\u8BE5\u96A7\u9053\u7684\u8FD0\u884C\u72B6\u6001"
486
+ },
487
+ React.createElement("input", {
488
+ type: "checkbox",
489
+ checked: Boolean(autoStart),
490
+ onChange: (e) => onToggleAutoStart(e.target.checked)
491
+ }),
492
+ "\u968F DSH \u542F\u52A8\u81EA\u52A8\u5F00\u542F"
493
+ )
494
+ )
466
495
  ),
467
496
  children,
468
497
  phase !== "idle" && phase !== "ready" && React.createElement("div", {
@@ -488,6 +517,107 @@ var TunnelCard = React.memo(function TunnelCard2({ title, desc, data, onStart, o
488
517
  )
489
518
  );
490
519
  });
520
+ var CloudflareConfigForm = React.memo(function CloudflareConfigForm2({ token, hostname, onSave }) {
521
+ const [open, setOpen] = React.useState(Boolean(token || hostname));
522
+ const [tokenVal, setTokenVal] = React.useState(token || "");
523
+ const [hostnameVal, setHostnameVal] = React.useState(hostname || "");
524
+ const [saving, setSaving] = React.useState(false);
525
+ const [msg, setMsg] = React.useState(null);
526
+ React.useEffect(() => {
527
+ setTokenVal(token || "");
528
+ setHostnameVal(hostname || "");
529
+ }, [token, hostname]);
530
+ const handleSave = async (e) => {
531
+ e.preventDefault();
532
+ setSaving(true);
533
+ setMsg(null);
534
+ try {
535
+ await onSave({ token: tokenVal, hostname: hostnameVal });
536
+ setMsg({ ok: true, text: "\u2713 \u56FA\u5B9A\u57DF\u540D\u914D\u7F6E\u5DF2\u4FDD\u5B58" });
537
+ } catch (err) {
538
+ setMsg({ ok: false, text: err.message || "\u4FDD\u5B58\u5931\u8D25" });
539
+ } finally {
540
+ setSaving(false);
541
+ }
542
+ };
543
+ return React.createElement(
544
+ "div",
545
+ {
546
+ style: {
547
+ ...s.block,
548
+ borderTop: "1px solid var(--dsw-alias-border-secondary, #e5e7eb)",
549
+ paddingTop: 10,
550
+ marginTop: 10
551
+ }
552
+ },
553
+ React.createElement(
554
+ "div",
555
+ {
556
+ style: { display: "flex", alignItems: "center", justifyContent: "space-between", cursor: "pointer", userSelect: "none" },
557
+ onClick: () => setOpen((v) => !v)
558
+ },
559
+ React.createElement(
560
+ "div",
561
+ { style: { fontSize: 12, fontWeight: 500, color: "var(--dsw-alias-brand-primary, #3b82f6)" } },
562
+ "\u2699\uFE0F \u9AD8\u7EA7\u914D\u7F6E\uFF1A\u56FA\u5B9A\u57DF\u540D (Cloudflare Token) ",
563
+ (token || hostname) && React.createElement("span", { style: { fontSize: 11, color: "var(--dsw-alias-state-success-primary, #059669)", fontWeight: 400 } }, "\u25CF \u5DF2\u914D\u7F6E\u56FA\u5B9A\u57DF\u540D")
564
+ ),
565
+ React.createElement("span", { style: { fontSize: 11, color: "var(--dsw-alias-label-secondary, #9ca3af)" } }, open ? "\u25B4 \u6298\u53E0" : "\u25BE \u5C55\u5F00")
566
+ ),
567
+ open && React.createElement(
568
+ "form",
569
+ { onSubmit: handleSave, style: { marginTop: 10 } },
570
+ React.createElement(
571
+ "div",
572
+ { style: { fontSize: 12, color: "var(--dsw-alias-label-secondary, #6b7280)", marginBottom: 8, lineHeight: 1.5 } },
573
+ "\u5728 Cloudflare Zero Trust \u63A7\u5236\u53F0\u521B\u5EFA Tunnel \u5373\u53EF\u83B7\u53D6\u4E13\u5C5E Token \u5E76\u7ED1\u5B9A\u81EA\u5DF1\u7684\u57DF\u540D\uFF08\u5982 dsh.yourname.com\uFF09\uFF0C\u6BCF\u6B21\u91CD\u542F URL \u6C38\u4E0D\u53D8\u66F4\u3002\u4E0D\u586B\u5219\u4F7F\u7528\u9ED8\u8BA4\u514D\u767B\u5F55\u4E34\u65F6\u968F\u673A\u57DF\u540D\u3002"
574
+ ),
575
+ React.createElement(
576
+ "div",
577
+ { style: { marginBottom: 8 } },
578
+ React.createElement("input", {
579
+ style: s.input,
580
+ type: "text",
581
+ placeholder: "\u81EA\u5B9A\u4E49\u56FA\u5B9A\u57DF\u540D (\u4F8B\u5982: dsh.yourdomain.com)",
582
+ value: hostnameVal,
583
+ onChange: (e) => setHostnameVal(e.target.value)
584
+ })
585
+ ),
586
+ React.createElement(
587
+ "div",
588
+ { style: { marginBottom: 8 } },
589
+ React.createElement("input", {
590
+ style: s.input,
591
+ type: "password",
592
+ placeholder: "Tunnel Token (\u4F8B\u5982: eyJhIjoi...)",
593
+ value: tokenVal,
594
+ onChange: (e) => setTokenVal(e.target.value)
595
+ })
596
+ ),
597
+ React.createElement(
598
+ "div",
599
+ { style: { display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" } },
600
+ React.createElement("button", {
601
+ type: "submit",
602
+ style: { ...s.btnPri, height: 28, fontSize: 12, padding: "0 12px" },
603
+ disabled: saving
604
+ }, saving ? "\u4FDD\u5B58\u4E2D\u2026" : "\u4FDD\u5B58\u56FA\u5B9A\u57DF\u540D\u914D\u7F6E"),
605
+ (tokenVal || hostnameVal) && React.createElement("button", {
606
+ type: "button",
607
+ style: { ...s.btnGhost, height: 28, fontSize: 12, padding: "0 10px" },
608
+ onClick: () => {
609
+ setTokenVal("");
610
+ setHostnameVal("");
611
+ onSave({ token: "", hostname: "" });
612
+ }
613
+ }, "\u6E05\u9664"),
614
+ msg && React.createElement("span", {
615
+ style: { fontSize: 12, color: msg.ok ? "var(--dsw-alias-state-success-primary, #059669)" : "var(--dsw-alias-state-error-primary, #dc2626)" }
616
+ }, msg.text)
617
+ )
618
+ )
619
+ );
620
+ });
491
621
  var AccessAuthCard = React.memo(function AccessAuthCard2({ auth, rpcCall, onUpdate }) {
492
622
  const [enabled, setEnabled] = React.useState(auth?.enabled ?? false);
493
623
  const [mode, setMode] = React.useState(auth?.mode ?? "token_and_password");
@@ -1543,30 +1673,416 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
1543
1673
  )
1544
1674
  );
1545
1675
  }
1546
- function UpgradeCommandRow({ cmd }) {
1547
- const [copied, copy] = useCopy();
1676
+ function SystemMetricsWidget({ metrics }) {
1677
+ if (!metrics) return null;
1678
+ const memUsedPercent = metrics.memory?.usedPercent ?? 0;
1679
+ const memUsedGb = (metrics.memory?.usedBytes / 1024 ** 3).toFixed(1);
1680
+ const memTotalGb = (metrics.memory?.totalBytes / 1024 ** 3).toFixed(1);
1681
+ const heapMb = Math.round((metrics.memory?.processHeapUsed || 0) / 1024 ** 2);
1682
+ const formatUptime = (sec = 0) => {
1683
+ const days = Math.floor(sec / 86400);
1684
+ const hrs = Math.floor(sec % 86400 / 3600);
1685
+ const mins = Math.floor(sec % 3600 / 60);
1686
+ if (days > 0) return `${days}\u5929 ${hrs}\u5C0F\u65F6 ${mins}\u5206`;
1687
+ if (hrs > 0) return `${hrs}\u5C0F\u65F6 ${mins}\u5206`;
1688
+ return `${mins}\u5206\u949F`;
1689
+ };
1690
+ const progressColor = memUsedPercent > 85 ? "#dc2626" : memUsedPercent > 70 ? "#d97706" : "#059669";
1548
1691
  return React.createElement(
1549
1692
  "div",
1550
- { style: { display: "flex", alignItems: "center", gap: 8 } },
1551
- React.createElement("code", {
1693
+ {
1552
1694
  style: {
1553
- ...s.code,
1554
- fontSize: 11,
1555
- color: "var(--dsw-alias-label-secondary,#6b7280)",
1556
- flex: 1,
1557
- minWidth: 0,
1558
- wordBreak: "break-all",
1559
- background: "var(--dsw-alias-bg-layer-1,#ffffff)",
1560
- padding: "4px 8px",
1695
+ ...s.card,
1696
+ marginBottom: 16
1697
+ }
1698
+ },
1699
+ React.createElement(
1700
+ "div",
1701
+ { style: { display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 12, flexWrap: "wrap", gap: 6 } },
1702
+ React.createElement(
1703
+ "div",
1704
+ { style: { ...s.label, fontSize: 13, display: "flex", alignItems: "center", gap: 6 } },
1705
+ "\u{1F4CA} \u5BBF\u4E3B\u7CFB\u7EDF\u4E0E\u8FD0\u884C\u770B\u677F"
1706
+ ),
1707
+ React.createElement(
1708
+ "div",
1709
+ { style: { fontSize: 11, color: "var(--dsw-alias-label-secondary, #6b7280)" } },
1710
+ `Node ${metrics.os?.nodeVersion || ""} \xB7 ${metrics.os?.platform || ""} ${metrics.os?.arch || ""}`
1711
+ )
1712
+ ),
1713
+ React.createElement(
1714
+ "div",
1715
+ { style: { display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(130px, 1fr))", gap: 12, marginBottom: 12 } },
1716
+ React.createElement(
1717
+ "div",
1718
+ null,
1719
+ React.createElement("div", { style: { color: "var(--dsw-alias-label-tertiary, #9ca3af)", fontSize: 11, marginBottom: 2 } }, "CPU \u6838\u5FC3\u4E0E\u578B\u53F7"),
1720
+ React.createElement(
1721
+ "div",
1722
+ { style: { fontWeight: 600, color: "var(--dsw-alias-label-primary, currentColor)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }, title: metrics.cpu?.model },
1723
+ `${metrics.cpu?.cores || 0} \u6838\u5FC3 (${(metrics.cpu?.model || "").split("@")[0].trim()})`
1724
+ )
1725
+ ),
1726
+ React.createElement(
1727
+ "div",
1728
+ null,
1729
+ React.createElement("div", { style: { color: "var(--dsw-alias-label-tertiary, #9ca3af)", fontSize: 11, marginBottom: 2 } }, "DSH \u8FD0\u884C\u65F6\u95F4 (Uptime)"),
1730
+ React.createElement(
1731
+ "div",
1732
+ { style: { fontWeight: 600, color: "var(--dsw-alias-state-success-primary, #059669)" } },
1733
+ formatUptime(metrics.uptime?.processSec)
1734
+ )
1735
+ ),
1736
+ React.createElement(
1737
+ "div",
1738
+ null,
1739
+ React.createElement("div", { style: { color: "var(--dsw-alias-label-tertiary, #9ca3af)", fontSize: 11, marginBottom: 2 } }, "Node \u8FDB\u7A0B\u5806\u5185\u5B58"),
1740
+ React.createElement(
1741
+ "div",
1742
+ { style: { fontWeight: 600, color: "var(--dsw-alias-label-primary, currentColor)" } },
1743
+ `${heapMb} MB`
1744
+ )
1745
+ )
1746
+ ),
1747
+ React.createElement(
1748
+ "div",
1749
+ null,
1750
+ React.createElement(
1751
+ "div",
1752
+ { style: { display: "flex", justifyContent: "space-between", fontSize: 11, color: "var(--dsw-alias-label-secondary, #6b7280)", marginBottom: 4 } },
1753
+ React.createElement("span", null, `\u7CFB\u7EDF\u5185\u5B58\u5360\u7528: ${memUsedGb} GB / ${memTotalGb} GB`),
1754
+ React.createElement("span", { style: { fontWeight: 600, color: progressColor } }, `${memUsedPercent}%`)
1755
+ ),
1756
+ React.createElement(
1757
+ "div",
1758
+ {
1759
+ style: {
1760
+ width: "100%",
1761
+ height: 6,
1762
+ background: "var(--dsw-alias-border-l2, #e5e7eb)",
1763
+ borderRadius: 999,
1764
+ overflow: "hidden"
1765
+ }
1766
+ },
1767
+ React.createElement("div", {
1768
+ style: {
1769
+ width: `${memUsedPercent}%`,
1770
+ height: "100%",
1771
+ background: progressColor,
1772
+ borderRadius: 999,
1773
+ transition: "width .3s ease"
1774
+ }
1775
+ })
1776
+ )
1777
+ )
1778
+ );
1779
+ }
1780
+ function NetworkDiagnosticWidget({ rpcCall }) {
1781
+ const [running, setRunning] = React.useState(false);
1782
+ const [result, setResult] = React.useState(null);
1783
+ const runDiagnose = React.useCallback(async () => {
1784
+ setRunning(true);
1785
+ try {
1786
+ const r = await rpcCall(BRIDGE_ENDPOINTS.diagnoseNetwork, {});
1787
+ if (r?.ok) setResult(r.value);
1788
+ } catch (e) {
1789
+ setResult({ overall: "warning", results: [{ item: "err", name: "\u8BCA\u65AD\u8BF7\u6C42\u5F02\u5E38", status: "fail", detail: e.message }] });
1790
+ } finally {
1791
+ setRunning(false);
1792
+ }
1793
+ }, [rpcCall]);
1794
+ return React.createElement(
1795
+ "div",
1796
+ { style: { ...s.card, marginBottom: 16 } },
1797
+ React.createElement(
1798
+ "div",
1799
+ { style: { marginBottom: 10 } },
1800
+ React.createElement(
1801
+ "div",
1802
+ { style: { ...s.label, fontSize: 13, display: "flex", alignItems: "center", gap: 6 } },
1803
+ "\u{1F50D} \u7F51\u7EDC\u8FDE\u901A\u6027\u4E00\u952E\u8BCA\u65AD"
1804
+ ),
1805
+ React.createElement(
1806
+ "div",
1807
+ { style: { ...s.muted, marginTop: 3 } },
1808
+ "\u4E00\u952E\u68C0\u6D4B\u672C\u5730\u53CD\u5411\u4EE3\u7406\u7AEF\u53E3\u3001\u5C40\u57DF\u7F51 IPv4\u3001Cloudflare Anycast \u5EF6\u8FDF\u4EE5\u53CA\u56FD\u5185 npmmirror \u955C\u50CF\u6E90\u8FDE\u901A\u6027\u3002"
1809
+ )
1810
+ ),
1811
+ React.createElement(
1812
+ "div",
1813
+ { style: { display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap", marginBottom: result ? 12 : 0 } },
1814
+ React.createElement(
1815
+ "button",
1816
+ {
1817
+ type: "button",
1818
+ style: { ...s.btnPri, height: 32, fontSize: 12, padding: "0 14px" },
1819
+ onClick: runDiagnose,
1820
+ disabled: running
1821
+ },
1822
+ running ? "\u6B63\u5728\u63A2\u6D4B\u8FDE\u901A\u6027\u2026" : result ? "\u{1F504} \u91CD\u65B0\u8BCA\u65AD\u7F51\u7EDC" : "\u{1F50D} \u5F00\u59CB\u4E00\u952E\u8BCA\u65AD"
1823
+ ),
1824
+ result && React.createElement("span", {
1825
+ style: {
1826
+ fontSize: 12,
1827
+ color: result.overall === "healthy" ? "var(--dsw-alias-state-success-primary, #059669)" : "var(--dsw-alias-state-warn-primary, #d97706)",
1828
+ fontWeight: 600
1829
+ }
1830
+ }, result.overall === "healthy" ? "\u2713 \u6240\u6709\u7F51\u7EDC\u63A2\u6D4B\u9879\u6B63\u5E38" : "\u25B2 \u68C0\u6D4B\u5230\u90E8\u5206\u5EF6\u8FDF\u8F83\u9AD8\u6216\u5F02\u5E38")
1831
+ ),
1832
+ running && !result && React.createElement(
1833
+ "div",
1834
+ {
1835
+ style: {
1836
+ marginTop: 10,
1837
+ padding: "10px 14px",
1838
+ borderRadius: 8,
1839
+ background: "var(--dsw-alias-bg-layer-2, #f9fafb)",
1840
+ display: "flex",
1841
+ alignItems: "center",
1842
+ gap: 8,
1843
+ color: "var(--dsw-alias-brand-primary, #4f6ef7)",
1844
+ fontSize: 12
1845
+ }
1846
+ },
1847
+ React.createElement("span", { style: { animation: "spin 1s linear infinite", display: "inline-flex" } }, React.createElement(Icons.refresh)),
1848
+ "\u6B63\u5728\u6267\u884C\u7F51\u7EDC\u7AEF\u53E3\u4E0E\u4E91\u7AEF\u8282\u70B9\u8FDE\u901A\u6027\u63A2\u6D4B\u2026"
1849
+ ),
1850
+ result?.results && React.createElement(
1851
+ "div",
1852
+ {
1853
+ style: {
1854
+ display: "flex",
1855
+ flexDirection: "column",
1856
+ gap: 8,
1857
+ marginTop: 10,
1858
+ paddingTop: 10,
1859
+ borderTop: "1px solid var(--dsw-alias-border-l2, #e5e7eb)"
1860
+ }
1861
+ },
1862
+ result.results.map((item, idx) => {
1863
+ const isPass = item.status === "pass";
1864
+ const isWarn = item.status === "warn";
1865
+ return React.createElement(
1866
+ "div",
1867
+ {
1868
+ key: idx,
1869
+ style: {
1870
+ display: "flex",
1871
+ alignItems: "flex-start",
1872
+ justifyContent: "space-between",
1873
+ gap: 8,
1874
+ padding: "8px 10px",
1875
+ borderRadius: 6,
1876
+ background: "var(--dsw-alias-bg-layer-1, rgba(255,255,255,0.7))",
1877
+ border: `1px solid ${isPass ? "var(--dsw-alias-state-success-border, #a7f3d0)" : isWarn ? "var(--dsw-alias-state-warn-border, #fde68a)" : "var(--dsw-alias-state-error-border, #fecaca)"}`,
1878
+ boxSizing: "border-box"
1879
+ }
1880
+ },
1881
+ React.createElement(
1882
+ "div",
1883
+ { style: { flex: 1, minWidth: 0 } },
1884
+ React.createElement(
1885
+ "div",
1886
+ { style: { fontWeight: 600, color: "var(--dsw-alias-label-primary, currentColor)", marginBottom: 2 } },
1887
+ isPass ? "\u2713 " : isWarn ? "\u25B2 " : "\u2715 ",
1888
+ item.name
1889
+ ),
1890
+ React.createElement("div", { style: { fontSize: 11, color: "var(--dsw-alias-label-secondary, #6b7280)" } }, item.detail)
1891
+ ),
1892
+ item.latencyMs != null && React.createElement("span", {
1893
+ style: {
1894
+ fontSize: 11,
1895
+ fontWeight: 600,
1896
+ flexShrink: 0,
1897
+ color: item.latencyMs < 500 ? "var(--dsw-alias-state-success-primary, #059669)" : "var(--dsw-alias-state-warn-primary, #d97706)"
1898
+ }
1899
+ }, `${item.latencyMs}ms`)
1900
+ );
1901
+ })
1902
+ )
1903
+ );
1904
+ }
1905
+ function BackupRestoreWidget({ rpcCall, onUpdate }) {
1906
+ const [exporting, setExporting] = React.useState(false);
1907
+ const [importing, setImporting] = React.useState(false);
1908
+ const [msg, setMsg] = React.useState(null);
1909
+ const fileInputRef = React.useRef(null);
1910
+ const handleExport = async () => {
1911
+ setExporting(true);
1912
+ setMsg(null);
1913
+ try {
1914
+ const r = await rpcCall(BRIDGE_ENDPOINTS.exportBackup, {});
1915
+ if (r?.ok && r.value) {
1916
+ const jsonStr = JSON.stringify(r.value, null, 2);
1917
+ const blob = new Blob([jsonStr], { type: "application/json" });
1918
+ const url = URL.createObjectURL(blob);
1919
+ const a = document.createElement("a");
1920
+ const now = /* @__PURE__ */ new Date();
1921
+ const dateStr = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, "0")}${String(now.getDate()).padStart(2, "0")}`;
1922
+ a.href = url;
1923
+ a.download = `dsh-bridge-backup-${dateStr}.json`;
1924
+ document.body.appendChild(a);
1925
+ a.click();
1926
+ document.body.removeChild(a);
1927
+ URL.revokeObjectURL(url);
1928
+ setMsg({ ok: true, text: "\u2713 \u5907\u4EFD\u6587\u4EF6\u5DF2\u6210\u529F\u5BFC\u51FA\u5E76\u4E0B\u8F7D\u5230\u672C\u5730\uFF01" });
1929
+ } else {
1930
+ setMsg({ ok: false, text: r?.error?.message || "\u5BFC\u51FA\u5907\u4EFD\u5931\u8D25" });
1931
+ }
1932
+ } catch (e) {
1933
+ setMsg({ ok: false, text: e.message || "\u5BFC\u51FA\u5F02\u5E38" });
1934
+ } finally {
1935
+ setExporting(false);
1936
+ }
1937
+ };
1938
+ const handleFileChange = async (e) => {
1939
+ const file = e.target?.files?.[0];
1940
+ if (!file) return;
1941
+ setImporting(true);
1942
+ setMsg(null);
1943
+ try {
1944
+ const text = await file.text();
1945
+ const backup = JSON.parse(text);
1946
+ const r = await rpcCall(BRIDGE_ENDPOINTS.importBackup, { backup });
1947
+ if (r?.ok) {
1948
+ setMsg({ ok: true, text: "\u2713 \u914D\u7F6E\u5DF2\u6210\u529F\u5BFC\u5165\u5E76\u5237\u65B0\u751F\u6548\uFF01" });
1949
+ onUpdate?.(r.value?.status);
1950
+ } else {
1951
+ setMsg({ ok: false, text: r?.error?.message || "\u5BFC\u5165\u914D\u7F6E\u5931\u8D25" });
1952
+ }
1953
+ } catch (err) {
1954
+ setMsg({ ok: false, text: `\u5BFC\u5165\u89E3\u6790\u5931\u8D25: ${err.message}` });
1955
+ } finally {
1956
+ setImporting(false);
1957
+ if (fileInputRef.current) fileInputRef.current.value = "";
1958
+ }
1959
+ };
1960
+ return React.createElement(
1961
+ "div",
1962
+ { style: { ...s.card, marginBottom: 16 } },
1963
+ React.createElement(
1964
+ "div",
1965
+ { style: { marginBottom: 10 } },
1966
+ React.createElement(
1967
+ "div",
1968
+ { style: { ...s.label, fontSize: 13, display: "flex", alignItems: "center", gap: 6 } },
1969
+ "\u{1F5C4}\uFE0F \u5168\u5C40\u914D\u7F6E\u5907\u4EFD\u4E0E\u6062\u590D"
1970
+ ),
1971
+ React.createElement(
1972
+ "div",
1973
+ { style: { ...s.muted, marginTop: 3 } },
1974
+ "\u652F\u6301\u4E00\u952E\u5BFC\u51FA\u6216\u5BFC\u5165\u6062\u590D\u672C\u63D2\u4EF6\u6240\u6709\u914D\u7F6E\uFF08\u5305\u542B\u5404 IM \u5E73\u53F0\u51ED\u8BC1\u3001\u6388\u6743\u767D\u540D\u5355\u3001\u516C\u7F51\u96A7\u9053\u4E0E\u5B89\u5168\u8BA4\u8BC1\u89C4\u5219\uFF09\u3002"
1975
+ )
1976
+ ),
1977
+ React.createElement(
1978
+ "div",
1979
+ { style: { display: "flex", gap: 10, alignItems: "center", flexWrap: "wrap" } },
1980
+ React.createElement("button", {
1981
+ type: "button",
1982
+ style: { ...s.btnPri, height: 32, fontSize: 12, padding: "0 14px" },
1983
+ onClick: handleExport,
1984
+ disabled: exporting || importing
1985
+ }, exporting ? "\u6B63\u5728\u5BFC\u51FA\u2026" : "\u{1F4E5} \u5BFC\u51FA\u914D\u7F6E\u5907\u4EFD (.json)"),
1986
+ React.createElement("button", {
1987
+ type: "button",
1988
+ style: { ...s.btnGhost, height: 32, fontSize: 12, padding: "0 14px" },
1989
+ onClick: () => fileInputRef.current?.click(),
1990
+ disabled: exporting || importing
1991
+ }, importing ? "\u6B63\u5728\u5BFC\u5165\u2026" : "\u{1F4E4} \u5BFC\u5165\u914D\u7F6E\u6062\u590D"),
1992
+ React.createElement("input", {
1993
+ type: "file",
1994
+ ref: fileInputRef,
1995
+ accept: ".json",
1996
+ style: { display: "none" },
1997
+ onChange: handleFileChange
1998
+ })
1999
+ ),
2000
+ msg && React.createElement("div", {
2001
+ style: {
2002
+ marginTop: 10,
2003
+ padding: "6px 12px",
1561
2004
  borderRadius: 6,
1562
- border: "1px solid var(--dsw-alias-border-l2,#e5e7eb)"
1563
- }
1564
- }, cmd),
1565
- React.createElement("button", {
1566
- style: { ...s.btnGhost, height: 26, padding: "0 10px", fontSize: 12, flexShrink: 0 },
1567
- onClick: () => copy(cmd),
1568
- title: "\u590D\u5236\u5347\u7EA7\u547D\u4EE4"
1569
- }, copied ? "\u2713 \u5DF2\u590D\u5236" : "\u590D\u5236")
2005
+ fontSize: 12,
2006
+ background: msg.ok ? "var(--dsw-alias-state-success-bg,#ecfdf5)" : "var(--dsw-alias-state-error-bg,#fef2f2)",
2007
+ color: msg.ok ? "var(--dsw-alias-state-success-primary,#059669)" : "var(--dsw-alias-state-error-primary,#dc2626)"
2008
+ }
2009
+ }, msg.text)
2010
+ );
2011
+ }
2012
+ function RestartDshCard({ rpcCall }) {
2013
+ const [restarting, setRestarting] = React.useState(false);
2014
+ const [status, setStatus] = React.useState(null);
2015
+ const handleRestart = async () => {
2016
+ setRestarting(true);
2017
+ setStatus({ phase: "restarting", text: "\u6B63\u5728\u5411 DSH \u670D\u52A1\u53D1\u9001\u91CD\u542F\u6307\u4EE4\u2026" });
2018
+ try {
2019
+ await rpcCall(BRIDGE_ENDPOINTS.restartDsh, {});
2020
+ } catch {
2021
+ }
2022
+ setStatus({ phase: "reconnecting", text: "DSH \u670D\u52A1\u6B63\u5728\u91CD\u542F\u4E2D\uFF0C\u6B63\u5728\u81EA\u52A8\u91CD\u65B0\u8FDE\u63A5\u2026" });
2023
+ await new Promise((r) => setTimeout(r, 2e3));
2024
+ let attempts = 0;
2025
+ const maxAttempts = 30;
2026
+ const pollHealth = setInterval(async () => {
2027
+ attempts++;
2028
+ try {
2029
+ const r = await rpcCall(BRIDGE_ENDPOINTS.checkVersion, {});
2030
+ if (r?.ok) {
2031
+ clearInterval(pollHealth);
2032
+ setStatus({ phase: "success", text: "\u{1F389} \u91CD\u542F\u6210\u529F\uFF01\u5DF2\u91CD\u65B0\u5EFA\u7ACB\u8FDE\u63A5\uFF0C\u6B63\u5728\u5237\u65B0\u9875\u9762\u2026" });
2033
+ setTimeout(() => {
2034
+ window.location.reload();
2035
+ }, 1e3);
2036
+ return;
2037
+ }
2038
+ } catch {
2039
+ }
2040
+ if (attempts >= maxAttempts) {
2041
+ clearInterval(pollHealth);
2042
+ setStatus({ phase: "timeout", text: "\u91CD\u8FDE\u7B49\u5F85\u8D85\u65F6\uFF0C\u8BF7\u624B\u52A8\u5237\u65B0\u9875\u9762\u3002" });
2043
+ setRestarting(false);
2044
+ }
2045
+ }, 1e3);
2046
+ };
2047
+ return React.createElement(
2048
+ "div",
2049
+ { style: { ...s.card, marginBottom: 16 } },
2050
+ React.createElement(
2051
+ "div",
2052
+ { style: { marginBottom: 10 } },
2053
+ React.createElement(
2054
+ "div",
2055
+ { style: { ...s.label, fontSize: 13, display: "flex", alignItems: "center", gap: 6 } },
2056
+ "\u{1F504} DSH \u670D\u52A1\u5E73\u6ED1\u91CD\u542F"
2057
+ ),
2058
+ React.createElement(
2059
+ "div",
2060
+ { style: { ...s.muted, marginTop: 3 } },
2061
+ "\u4F18\u96C5\u9000\u51FA\u5E76\u91CD\u65B0\u62C9\u8D77\u5F53\u524D DSH \u8FDB\u7A0B\u4E0E\u6240\u6709\u63D2\u4EF6\u670D\u52A1\uFF0C\u524D\u7AEF\u5C06\u5728\u51E0\u79D2\u540E\u81EA\u52A8\u63A2\u6D4B\u91CD\u8FDE\u5E76\u5237\u65B0\u9875\u9762\u3002"
2062
+ )
2063
+ ),
2064
+ !restarting && !status && React.createElement("button", {
2065
+ type: "button",
2066
+ style: { ...s.btnGhost, height: 32, fontSize: 12, padding: "0 14px" },
2067
+ onClick: handleRestart
2068
+ }, "\u{1F504} \u7ACB\u5373\u91CD\u542F DSH \u670D\u52A1"),
2069
+ (restarting || status) && React.createElement(
2070
+ "div",
2071
+ {
2072
+ style: {
2073
+ display: "flex",
2074
+ alignItems: "center",
2075
+ gap: 8,
2076
+ fontSize: 12,
2077
+ color: status?.phase === "success" ? "var(--dsw-alias-state-success-primary, #059669)" : status?.phase === "timeout" ? "var(--dsw-alias-state-error-primary, #dc2626)" : "var(--dsw-alias-state-info-primary, #2563eb)",
2078
+ fontWeight: 500
2079
+ }
2080
+ },
2081
+ status?.phase !== "success" && status?.phase !== "timeout" && React.createElement("span", {
2082
+ style: { animation: "spin 1s linear infinite", display: "inline-flex" }
2083
+ }, React.createElement(Icons.refresh)),
2084
+ status?.text || "\u6B63\u5728\u8C03\u5EA6\u2026"
2085
+ )
1570
2086
  );
1571
2087
  }
1572
2088
  function VersionBanner({ rpcCall }) {
@@ -1575,6 +2091,9 @@ function VersionBanner({ rpcCall }) {
1575
2091
  const [upgrading, setUpgrading] = React.useState(false);
1576
2092
  const [upgradeResult, setUpgradeResult] = React.useState(null);
1577
2093
  const [showManual, setShowManual] = React.useState(false);
2094
+ const [restarting, setRestarting] = React.useState(false);
2095
+ const [restartStatus, setRestartStatus] = React.useState(null);
2096
+ const [dismissRestart, setDismissRestart] = React.useState(false);
1578
2097
  const check = React.useCallback(async () => {
1579
2098
  setLoading(true);
1580
2099
  try {
@@ -1593,11 +2112,12 @@ function VersionBanner({ rpcCall }) {
1593
2112
  if (!info?.latest || upgrading) return;
1594
2113
  setUpgrading(true);
1595
2114
  setUpgradeResult(null);
2115
+ setDismissRestart(false);
2116
+ setRestartStatus(null);
1596
2117
  try {
1597
2118
  const r = await rpcCall(BRIDGE_ENDPOINTS.upgradePlugin, { version: info.latest });
1598
2119
  if (r?.ok && r.value?.ok) {
1599
- setUpgradeResult({ ok: true, message: `\u5DF2\u6210\u529F\u5347\u7EA7\u5230 v${info.latest}\uFF01\u8BF7\u91CD\u542F DSH \u670D\u52A1\u4F7F\u65B0\u7248\u672C\u751F\u6548\u3002` });
1600
- setTimeout(() => check(), 3e3);
2120
+ setUpgradeResult({ ok: true, message: `\u5DF2\u6210\u529F\u5347\u7EA7\u5230 v${info.latest}\uFF01` });
1601
2121
  } else {
1602
2122
  setUpgradeResult({ ok: false, message: r?.value?.error || r?.error?.message || "\u5347\u7EA7\u5931\u8D25" });
1603
2123
  setShowManual(true);
@@ -1608,10 +2128,42 @@ function VersionBanner({ rpcCall }) {
1608
2128
  } finally {
1609
2129
  setUpgrading(false);
1610
2130
  }
1611
- }, [info?.latest, upgrading, rpcCall, check]);
2131
+ }, [info?.latest, upgrading, rpcCall]);
2132
+ const handleRestart = React.useCallback(async () => {
2133
+ setRestarting(true);
2134
+ setRestartStatus({ phase: "restarting", text: "\u6B63\u5728\u8C03\u5EA6 DSH \u670D\u52A1\u91CD\u542F\u2026" });
2135
+ try {
2136
+ await rpcCall(BRIDGE_ENDPOINTS.restartDsh, {});
2137
+ } catch {
2138
+ }
2139
+ setRestartStatus({ phase: "reconnecting", text: "DSH \u670D\u52A1\u6B63\u5728\u91CD\u542F\u4E2D\uFF0C\u6B63\u5728\u81EA\u52A8\u91CD\u65B0\u8FDE\u63A5\u2026" });
2140
+ await new Promise((r) => setTimeout(r, 2e3));
2141
+ let attempts = 0;
2142
+ const maxAttempts = 30;
2143
+ const pollHealth = setInterval(async () => {
2144
+ attempts++;
2145
+ try {
2146
+ const r = await rpcCall(BRIDGE_ENDPOINTS.checkVersion, {});
2147
+ if (r?.ok) {
2148
+ clearInterval(pollHealth);
2149
+ setRestartStatus({ phase: "success", text: "\u{1F389} \u91CD\u542F\u6210\u529F\uFF01\u5DF2\u81EA\u52A8\u52A0\u8F7D\u6700\u65B0\u7248\u672C\u3002\u6B63\u5728\u5237\u65B0\u9875\u9762\u2026" });
2150
+ setTimeout(() => {
2151
+ window.location.reload();
2152
+ }, 1e3);
2153
+ return;
2154
+ }
2155
+ } catch {
2156
+ }
2157
+ if (attempts >= maxAttempts) {
2158
+ clearInterval(pollHealth);
2159
+ setRestartStatus({ phase: "timeout", text: "\u91CD\u8FDE\u7B49\u5F85\u8D85\u65F6\uFF0C\u8BF7\u624B\u52A8\u5237\u65B0\u9875\u9762\u3002" });
2160
+ setRestarting(false);
2161
+ }
2162
+ }, 1e3);
2163
+ }, [rpcCall]);
1612
2164
  const links = React.createElement(
1613
2165
  "div",
1614
- { style: { display: "flex", gap: 12, alignItems: "center", flexWrap: "wrap" } },
2166
+ { style: { display: "flex", gap: 10, alignItems: "center", flexWrap: "wrap" } },
1615
2167
  React.createElement("a", {
1616
2168
  href: GITHUB_URL,
1617
2169
  target: "_blank",
@@ -1686,7 +2238,7 @@ function VersionBanner({ rpcCall }) {
1686
2238
  gap: 4
1687
2239
  },
1688
2240
  onClick: check,
1689
- disabled: loading || upgrading,
2241
+ disabled: loading || upgrading || restarting,
1690
2242
  title: "\u91CD\u65B0\u68C0\u67E5 npm \u7EBF\u4E0A\u7248\u672C"
1691
2243
  },
1692
2244
  React.createElement(Icons.refresh),
@@ -1734,17 +2286,17 @@ function VersionBanner({ rpcCall }) {
1734
2286
  fontSize: 12,
1735
2287
  padding: "0 14px",
1736
2288
  background: upgradeResult?.ok ? "var(--dsw-alias-state-success-primary,#059669)" : "var(--dsw-alias-brand-primary,#4f6ef7)",
1737
- opacity: upgrading ? 0.6 : 1
2289
+ opacity: upgrading || restarting ? 0.6 : 1
1738
2290
  },
1739
2291
  onClick: handleUpgrade,
1740
- disabled: upgrading || upgradeResult?.ok
2292
+ disabled: upgrading || restarting || upgradeResult?.ok
1741
2293
  },
1742
2294
  upgrading ? React.createElement(
1743
2295
  "span",
1744
2296
  { style: { display: "inline-flex", alignItems: "center", gap: 6 } },
1745
2297
  React.createElement("span", { style: { animation: "spin 1s linear infinite", display: "inline-flex" } }, React.createElement(Icons.refresh)),
1746
2298
  "\u6B63\u5728\u81EA\u52A8\u5347\u7EA7\u2026"
1747
- ) : upgradeResult?.ok ? "\u2713 \u5DF2\u5B8C\u6210\u5347\u7EA7" : `\u4E00\u952E\u5347\u7EA7\u5230 v${info.latest}`
2299
+ ) : upgradeResult?.ok ? "\u2713 \u5347\u7EA7\u5B8C\u6210" : `\u4E00\u952E\u5347\u7EA7\u5230 v${info.latest}`
1748
2300
  )
1749
2301
  ),
1750
2302
  // 简短更新内容 / Release Notes 亮点展示
@@ -1753,9 +2305,9 @@ function VersionBanner({ rpcCall }) {
1753
2305
  {
1754
2306
  style: {
1755
2307
  fontSize: 12,
1756
- color: "var(--dsw-alias-label-secondary,#374151)",
1757
- background: "rgba(255, 255, 255, 0.75)",
1758
- border: "1px solid rgba(191, 219, 254, 0.7)",
2308
+ color: "var(--dsw-alias-label-primary, #374151)",
2309
+ background: "var(--dsw-alias-bg-layer-2, rgba(255, 255, 255, 0.85))",
2310
+ border: "1px solid var(--dsw-alias-state-info-border, rgba(191, 219, 254, 0.8))",
1759
2311
  borderRadius: 8,
1760
2312
  padding: "8px 12px",
1761
2313
  marginBottom: 10,
@@ -1763,14 +2315,78 @@ function VersionBanner({ rpcCall }) {
1763
2315
  whiteSpace: "pre-line"
1764
2316
  }
1765
2317
  },
1766
- React.createElement("div", { style: { fontWeight: 600, color: "var(--dsw-alias-state-info-primary,#1e40af)", marginBottom: 2 } }, "\u2728 \u66F4\u65B0\u4EAE\u70B9\uFF1A"),
2318
+ React.createElement("div", { style: { fontWeight: 600, color: "var(--dsw-alias-state-info-primary, #2563eb)", marginBottom: 2 } }, "\u2728 \u66F4\u65B0\u4EAE\u70B9\uFF1A"),
1767
2319
  info.releaseNotes
1768
2320
  ),
1769
- upgradeResult && React.createElement("div", {
2321
+ // 升级成功后:引导重启 DSH 操作卡片
2322
+ upgradeResult?.ok && !dismissRestart && React.createElement(
2323
+ "div",
2324
+ {
2325
+ style: {
2326
+ background: "var(--dsw-alias-bg-layer-2, rgba(255, 255, 255, 0.95))",
2327
+ border: "1px solid var(--dsw-alias-state-success-border, #a7f3d0)",
2328
+ borderRadius: 8,
2329
+ padding: "12px 14px",
2330
+ marginBottom: 10
2331
+ }
2332
+ },
2333
+ React.createElement(
2334
+ "div",
2335
+ { style: { display: "flex", alignItems: "center", gap: 8, marginBottom: 8 } },
2336
+ React.createElement("span", { style: { fontSize: 16 } }, "\u2728"),
2337
+ React.createElement("span", {
2338
+ style: { fontSize: 13, fontWeight: 600, color: "var(--dsw-alias-state-success-primary, #059669)" }
2339
+ }, `\u5DF2\u6210\u529F\u5347\u7EA7\u5230 v${info.latest}\uFF01\u9700\u8981\u91CD\u542F DSH \u670D\u52A1\u4F7F\u65B0\u7248\u672C\u751F\u6548`)
2340
+ ),
2341
+ !restarting && !restartStatus && React.createElement(
2342
+ "div",
2343
+ {
2344
+ style: { display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }
2345
+ },
2346
+ React.createElement("button", {
2347
+ style: {
2348
+ ...s.btnPri,
2349
+ height: 30,
2350
+ fontSize: 12,
2351
+ padding: "0 14px",
2352
+ background: "var(--dsw-alias-state-success-primary, #059669)"
2353
+ },
2354
+ onClick: handleRestart
2355
+ }, "\u{1F504} \u7ACB\u5373\u91CD\u542F DSH \u670D\u52A1"),
2356
+ React.createElement("button", {
2357
+ style: {
2358
+ ...s.btnGhost,
2359
+ height: 30,
2360
+ fontSize: 12,
2361
+ padding: "0 12px"
2362
+ },
2363
+ onClick: () => setDismissRestart(true)
2364
+ }, "\u7A0D\u540E\u624B\u52A8\u91CD\u542F")
2365
+ ),
2366
+ (restarting || restartStatus) && React.createElement(
2367
+ "div",
2368
+ {
2369
+ style: {
2370
+ display: "flex",
2371
+ alignItems: "center",
2372
+ gap: 8,
2373
+ fontSize: 12,
2374
+ color: restartStatus?.phase === "success" ? "var(--dsw-alias-state-success-primary, #059669)" : restartStatus?.phase === "timeout" ? "var(--dsw-alias-state-error-primary, #dc2626)" : "var(--dsw-alias-state-info-primary, #2563eb)",
2375
+ fontWeight: 500
2376
+ }
2377
+ },
2378
+ restartStatus?.phase !== "success" && restartStatus?.phase !== "timeout" && React.createElement("span", {
2379
+ style: { animation: "spin 1s linear infinite", display: "inline-flex" }
2380
+ }, React.createElement(Icons.refresh)),
2381
+ restartStatus?.text || "\u6B63\u5728\u5904\u7406\u2026"
2382
+ )
2383
+ ),
2384
+ // 失败提示
2385
+ upgradeResult && !upgradeResult.ok && React.createElement("div", {
1770
2386
  style: {
1771
- background: upgradeResult.ok ? "var(--dsw-alias-state-success-bg,#ecfdf5)" : "var(--dsw-alias-state-error-bg,#fef2f2)",
1772
- border: `1px solid ${upgradeResult.ok ? "var(--dsw-alias-state-success-border,#a7f3d0)" : "var(--dsw-alias-state-error-border,#fecaca)"}`,
1773
- color: upgradeResult.ok ? "var(--dsw-alias-state-success-primary,#065f46)" : "var(--dsw-alias-state-error-primary,#991b1b)",
2387
+ background: "var(--dsw-alias-state-error-bg,#fef2f2)",
2388
+ border: "1px solid var(--dsw-alias-state-error-border,#fecaca)",
2389
+ color: "var(--dsw-alias-state-error-primary,#991b1b)",
1774
2390
  padding: "8px 12px",
1775
2391
  borderRadius: 6,
1776
2392
  fontSize: 12,
@@ -1804,12 +2420,14 @@ var TABS = [
1804
2420
  { id: "lan", label: "\u5C40\u57DF\u7F51", icon: Icons.lan },
1805
2421
  { id: "tunnel", label: "\u516C\u7F51\u96A7\u9053", icon: Icons.tunnel },
1806
2422
  { id: "im", label: "IM \u673A\u5668\u4EBA", icon: Icons.bot },
1807
- { id: "security", label: "\u5B89\u5168\u8BA4\u8BC1", icon: Icons.security }
2423
+ { id: "security", label: "\u5B89\u5168\u8BA4\u8BC1", icon: Icons.security },
2424
+ { id: "ops", label: "\u8FD0\u7EF4\u76D1\u63A7", icon: Icons.ops }
1808
2425
  ];
1809
2426
  function TabBar({ active, onChange, dots }) {
1810
2427
  return React.createElement(
1811
2428
  "div",
1812
2429
  {
2430
+ className: "dsh-tabbar-container",
1813
2431
  style: {
1814
2432
  display: "flex",
1815
2433
  gap: 4,
@@ -1818,7 +2436,9 @@ function TabBar({ active, onChange, dots }) {
1818
2436
  overflowX: "auto",
1819
2437
  WebkitOverflowScrolling: "touch",
1820
2438
  maxWidth: "100%",
1821
- flexWrap: "nowrap"
2439
+ flexWrap: "nowrap",
2440
+ scrollbarWidth: "none",
2441
+ msOverflowStyle: "none"
1822
2442
  }
1823
2443
  },
1824
2444
  TABS.map(({ id, label, icon: TabIcon }) => {
@@ -2031,8 +2651,20 @@ function BridgePanel({ rpcCall }) {
2031
2651
  () => act(BRIDGE_ENDPOINTS.stopCloudflared).then(() => act(BRIDGE_ENDPOINTS.startCloudflared)),
2032
2652
  [act]
2033
2653
  );
2654
+ const onToggleCloudflaredAutoStart = React.useCallback(
2655
+ (autoStart) => act(BRIDGE_ENDPOINTS.setTunnelAutoStart, { tunnel: "cloudflared", autoStart }),
2656
+ [act]
2657
+ );
2658
+ const saveCloudflaredConfig = React.useCallback(
2659
+ ({ token, hostname }) => act(BRIDGE_ENDPOINTS.saveCloudflaredConfig, { token, hostname }),
2660
+ [act]
2661
+ );
2034
2662
  const onStartCustom = React.useCallback(() => act(BRIDGE_ENDPOINTS.startCustomTunnel), [act]);
2035
2663
  const onStopCustom = React.useCallback(() => act(BRIDGE_ENDPOINTS.stopCustomTunnel), [act]);
2664
+ const onToggleCustomAutoStart = React.useCallback(
2665
+ (autoStart) => act(BRIDGE_ENDPOINTS.setTunnelAutoStart, { tunnel: "customTunnel", autoStart }),
2666
+ [act]
2667
+ );
2036
2668
  const saveConfig = React.useCallback(
2037
2669
  (serverUrl, accessToken) => act(BRIDGE_ENDPOINTS.saveCustomTunnelConfig, { serverUrl, accessToken }),
2038
2670
  [act]
@@ -2066,21 +2698,31 @@ function BridgePanel({ rpcCall }) {
2066
2698
  tabContent = React.createElement(
2067
2699
  React.Fragment,
2068
2700
  null,
2069
- React.createElement(TunnelCard, {
2070
- title: "Cloudflare \u96A7\u9053",
2071
- desc: "\u4E00\u952E\u83B7\u53D6\u516C\u7F51\u5730\u5740\uFF08\u91CD\u542F\u540E URL \u4F1A\u53D8\u5316\uFF09",
2072
- data: {
2073
- running: status?.cloudflared?.running,
2074
- url: status?.cloudflared?.url,
2075
- qr: status?.cloudflared?.qr,
2076
- state: status?.cloudflared?.state
2701
+ React.createElement(
2702
+ TunnelCard,
2703
+ {
2704
+ title: "Cloudflare \u96A7\u9053",
2705
+ desc: status?.cloudflared?.tokenConfigured ? "\u56FA\u5B9A\u57DF\u540D\u6A21\u5F0F\uFF08Token \u8FD0\u884C \xB7 \u91CD\u542F URL \u4FDD\u6301\u4E0D\u53D8\uFF09" : "\u4E00\u952E\u83B7\u53D6\u516C\u7F51\u5730\u5740\uFF08\u514D\u767B\u5F55\u4E34\u65F6\u968F\u673A\u57DF\u540D\uFF09",
2706
+ data: {
2707
+ running: status?.cloudflared?.running,
2708
+ url: status?.cloudflared?.url,
2709
+ qr: status?.cloudflared?.qr,
2710
+ state: status?.cloudflared?.state
2711
+ },
2712
+ autoStart: status?.cloudflared?.autoStart,
2713
+ onToggleAutoStart: onToggleCloudflaredAutoStart,
2714
+ auth: status?.auth,
2715
+ onNavigateSecurity: navSecurity,
2716
+ onStart: onStartCloudflared,
2717
+ onStop: onStopCloudflared,
2718
+ onReset: status?.cloudflared?.running ? onResetCloudflared : null
2077
2719
  },
2078
- auth: status?.auth,
2079
- onNavigateSecurity: navSecurity,
2080
- onStart: onStartCloudflared,
2081
- onStop: onStopCloudflared,
2082
- onReset: status?.cloudflared?.running ? onResetCloudflared : null
2083
- }),
2720
+ React.createElement(CloudflareConfigForm, {
2721
+ token: status?.cloudflared?.token ?? "",
2722
+ hostname: status?.cloudflared?.hostname ?? "",
2723
+ onSave: saveCloudflaredConfig
2724
+ })
2725
+ ),
2084
2726
  React.createElement(
2085
2727
  TunnelCard,
2086
2728
  {
@@ -2093,6 +2735,8 @@ function BridgePanel({ rpcCall }) {
2093
2735
  qr: ct?.qr,
2094
2736
  state: ct?.state
2095
2737
  },
2738
+ autoStart: ct?.autoStart,
2739
+ onToggleAutoStart: onToggleCustomAutoStart,
2096
2740
  auth: status?.auth,
2097
2741
  onNavigateSecurity: navSecurity,
2098
2742
  onStart: onStartCustom,
@@ -2112,6 +2756,18 @@ function BridgePanel({ rpcCall }) {
2112
2756
  rpcCall: authRpcCall,
2113
2757
  onUpdate: () => load(true)
2114
2758
  });
2759
+ } else if (activeTab === "ops") {
2760
+ tabContent = React.createElement(
2761
+ React.Fragment,
2762
+ null,
2763
+ React.createElement(SystemMetricsWidget, { metrics: status?.system }),
2764
+ React.createElement(NetworkDiagnosticWidget, { rpcCall: authRpcCall }),
2765
+ React.createElement(BackupRestoreWidget, {
2766
+ rpcCall: authRpcCall,
2767
+ onUpdate: () => load(true)
2768
+ }),
2769
+ React.createElement(RestartDshCard, { rpcCall: authRpcCall })
2770
+ );
2115
2771
  } else if (activeTab === "im") {
2116
2772
  const IM_PLATFORMS = [
2117
2773
  { id: "wechat", label: "\u5FAE\u4FE1", icon: Icons.wechat, brandColor: "#07C160", desc: "ClawBot \u626B\u7801\u76F4\u8FDE \xB7 \u65E0\u9700\u516C\u7F51" },
@@ -2482,6 +3138,17 @@ function injectMobileStyles() {
2482
3138
  const style = document.createElement("style");
2483
3139
  style.id = "dsh-bridge-mobile-styles";
2484
3140
  style.textContent = `
3141
+ /* DSH Bridge \u9690\u85CF Tab \u680F\u539F\u751F\u6EDA\u52A8\u6761\u5E76\u4FDD\u6301\u5E73\u6ED1\u6ED1\u52A8 */
3142
+ .dsh-tabbar-container {
3143
+ scrollbar-width: none !important;
3144
+ -ms-overflow-style: none !important;
3145
+ }
3146
+ .dsh-tabbar-container::-webkit-scrollbar {
3147
+ display: none !important;
3148
+ width: 0 !important;
3149
+ height: 0 !important;
3150
+ }
3151
+
2485
3152
  /* DSH Bridge \u79FB\u52A8\u7AEF\u81EA\u9002\u5E94\u4E0E\u89E6\u63A7\u4EA4\u4E92\u589E\u5F3A\u6837\u5F0F */
2486
3153
  :root {
2487
3154
  --dsh-mobile-header-h: 52px;