@wenbin_wb/dsh-bridge 2.6.1 → 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
@@ -45,6 +45,11 @@ var BRIDGE_ENDPOINTS = {
45
45
  saveCustomTunnelConfig: "saveCustomTunnelConfig",
46
46
  checkVersion: "checkVersion",
47
47
  upgradePlugin: "upgradePlugin",
48
+ restartDsh: "restartDsh",
49
+ exportBackup: "exportBackup",
50
+ importBackup: "importBackup",
51
+ diagnoseNetwork: "diagnoseNetwork",
52
+ getSystemMetrics: "getSystemMetrics",
48
53
  // 访问安全认证(密码保护 / 扫码免密 Token)
49
54
  authGetStatus: "authGetStatus",
50
55
  authUpdateConfig: "authUpdateConfig",
@@ -187,34 +192,14 @@ var Icons = {
187
192
  "svg",
188
193
  { viewBox: "0 0 24 24", width: 12, height: 12, fill: "none", stroke: "currentColor", strokeWidth: 2.5, strokeLinecap: "round", strokeLinejoin: "round", ...props },
189
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" })
190
201
  )
191
202
  };
192
- function useCopy() {
193
- const [copied, setCopied] = React.useState(false);
194
- const copy = React.useCallback((text) => {
195
- const done = () => {
196
- setCopied(true);
197
- setTimeout(() => setCopied(false), 2e3);
198
- };
199
- if (navigator.clipboard?.writeText) {
200
- navigator.clipboard.writeText(text).then(done).catch(() => fallbackCopy());
201
- } else {
202
- fallbackCopy();
203
- }
204
- function fallbackCopy() {
205
- const textarea = document.createElement("textarea");
206
- textarea.value = text;
207
- textarea.style.position = "fixed";
208
- textarea.style.opacity = "0";
209
- document.body.appendChild(textarea);
210
- textarea.select();
211
- const success = document.execCommand("copy");
212
- document.body.removeChild(textarea);
213
- if (success) done();
214
- }
215
- }, []);
216
- return [copied, copy];
217
- }
218
203
  function StatusTag({ running, status }) {
219
204
  let bg = "var(--dsw-alias-bg-layer-2,#f3f4f6)";
220
205
  let color = "var(--dsw-alias-label-secondary,#6b7280)";
@@ -338,7 +323,12 @@ function QrBlock({ url, qr, onReset, auth, onNavigateSecurity }) {
338
323
  "div",
339
324
  { style: { marginTop: 10 } },
340
325
  React.createElement("img", { src: qr, alt: "QR", style: s.qr }),
341
- 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
+ )
342
332
  ),
343
333
  onReset && React.createElement(
344
334
  "div",
@@ -1683,30 +1673,416 @@ function PlatformCard({ platformId, platformName, platformDesc, rpcCall, onStatu
1683
1673
  )
1684
1674
  );
1685
1675
  }
1686
- function UpgradeCommandRow({ cmd }) {
1687
- 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";
1691
+ return React.createElement(
1692
+ "div",
1693
+ {
1694
+ style: {
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
+ };
1688
1960
  return React.createElement(
1689
1961
  "div",
1690
- { style: { display: "flex", alignItems: "center", gap: 8 } },
1691
- React.createElement("code", {
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", {
1692
2001
  style: {
1693
- ...s.code,
1694
- fontSize: 11,
1695
- color: "var(--dsw-alias-label-secondary,#6b7280)",
1696
- flex: 1,
1697
- minWidth: 0,
1698
- wordBreak: "break-all",
1699
- background: "var(--dsw-alias-bg-layer-1,#ffffff)",
1700
- padding: "4px 8px",
2002
+ marginTop: 10,
2003
+ padding: "6px 12px",
1701
2004
  borderRadius: 6,
1702
- border: "1px solid var(--dsw-alias-border-l2,#e5e7eb)"
1703
- }
1704
- }, cmd),
1705
- React.createElement("button", {
1706
- style: { ...s.btnGhost, height: 26, padding: "0 10px", fontSize: 12, flexShrink: 0 },
1707
- onClick: () => copy(cmd),
1708
- title: "\u590D\u5236\u5347\u7EA7\u547D\u4EE4"
1709
- }, 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
+ )
1710
2086
  );
1711
2087
  }
1712
2088
  function VersionBanner({ rpcCall }) {
@@ -1715,6 +2091,9 @@ function VersionBanner({ rpcCall }) {
1715
2091
  const [upgrading, setUpgrading] = React.useState(false);
1716
2092
  const [upgradeResult, setUpgradeResult] = React.useState(null);
1717
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);
1718
2097
  const check = React.useCallback(async () => {
1719
2098
  setLoading(true);
1720
2099
  try {
@@ -1733,11 +2112,12 @@ function VersionBanner({ rpcCall }) {
1733
2112
  if (!info?.latest || upgrading) return;
1734
2113
  setUpgrading(true);
1735
2114
  setUpgradeResult(null);
2115
+ setDismissRestart(false);
2116
+ setRestartStatus(null);
1736
2117
  try {
1737
2118
  const r = await rpcCall(BRIDGE_ENDPOINTS.upgradePlugin, { version: info.latest });
1738
2119
  if (r?.ok && r.value?.ok) {
1739
- 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` });
1740
- setTimeout(() => check(), 3e3);
2120
+ setUpgradeResult({ ok: true, message: `\u5DF2\u6210\u529F\u5347\u7EA7\u5230 v${info.latest}\uFF01` });
1741
2121
  } else {
1742
2122
  setUpgradeResult({ ok: false, message: r?.value?.error || r?.error?.message || "\u5347\u7EA7\u5931\u8D25" });
1743
2123
  setShowManual(true);
@@ -1748,10 +2128,42 @@ function VersionBanner({ rpcCall }) {
1748
2128
  } finally {
1749
2129
  setUpgrading(false);
1750
2130
  }
1751
- }, [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]);
1752
2164
  const links = React.createElement(
1753
2165
  "div",
1754
- { style: { display: "flex", gap: 12, alignItems: "center", flexWrap: "wrap" } },
2166
+ { style: { display: "flex", gap: 10, alignItems: "center", flexWrap: "wrap" } },
1755
2167
  React.createElement("a", {
1756
2168
  href: GITHUB_URL,
1757
2169
  target: "_blank",
@@ -1826,7 +2238,7 @@ function VersionBanner({ rpcCall }) {
1826
2238
  gap: 4
1827
2239
  },
1828
2240
  onClick: check,
1829
- disabled: loading || upgrading,
2241
+ disabled: loading || upgrading || restarting,
1830
2242
  title: "\u91CD\u65B0\u68C0\u67E5 npm \u7EBF\u4E0A\u7248\u672C"
1831
2243
  },
1832
2244
  React.createElement(Icons.refresh),
@@ -1874,17 +2286,17 @@ function VersionBanner({ rpcCall }) {
1874
2286
  fontSize: 12,
1875
2287
  padding: "0 14px",
1876
2288
  background: upgradeResult?.ok ? "var(--dsw-alias-state-success-primary,#059669)" : "var(--dsw-alias-brand-primary,#4f6ef7)",
1877
- opacity: upgrading ? 0.6 : 1
2289
+ opacity: upgrading || restarting ? 0.6 : 1
1878
2290
  },
1879
2291
  onClick: handleUpgrade,
1880
- disabled: upgrading || upgradeResult?.ok
2292
+ disabled: upgrading || restarting || upgradeResult?.ok
1881
2293
  },
1882
2294
  upgrading ? React.createElement(
1883
2295
  "span",
1884
2296
  { style: { display: "inline-flex", alignItems: "center", gap: 6 } },
1885
2297
  React.createElement("span", { style: { animation: "spin 1s linear infinite", display: "inline-flex" } }, React.createElement(Icons.refresh)),
1886
2298
  "\u6B63\u5728\u81EA\u52A8\u5347\u7EA7\u2026"
1887
- ) : 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}`
1888
2300
  )
1889
2301
  ),
1890
2302
  // 简短更新内容 / Release Notes 亮点展示
@@ -1893,9 +2305,9 @@ function VersionBanner({ rpcCall }) {
1893
2305
  {
1894
2306
  style: {
1895
2307
  fontSize: 12,
1896
- color: "var(--dsw-alias-label-secondary,#374151)",
1897
- background: "rgba(255, 255, 255, 0.75)",
1898
- 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))",
1899
2311
  borderRadius: 8,
1900
2312
  padding: "8px 12px",
1901
2313
  marginBottom: 10,
@@ -1903,14 +2315,78 @@ function VersionBanner({ rpcCall }) {
1903
2315
  whiteSpace: "pre-line"
1904
2316
  }
1905
2317
  },
1906
- 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"),
1907
2319
  info.releaseNotes
1908
2320
  ),
1909
- 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", {
1910
2386
  style: {
1911
- background: upgradeResult.ok ? "var(--dsw-alias-state-success-bg,#ecfdf5)" : "var(--dsw-alias-state-error-bg,#fef2f2)",
1912
- border: `1px solid ${upgradeResult.ok ? "var(--dsw-alias-state-success-border,#a7f3d0)" : "var(--dsw-alias-state-error-border,#fecaca)"}`,
1913
- 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)",
1914
2390
  padding: "8px 12px",
1915
2391
  borderRadius: 6,
1916
2392
  fontSize: 12,
@@ -1944,12 +2420,14 @@ var TABS = [
1944
2420
  { id: "lan", label: "\u5C40\u57DF\u7F51", icon: Icons.lan },
1945
2421
  { id: "tunnel", label: "\u516C\u7F51\u96A7\u9053", icon: Icons.tunnel },
1946
2422
  { id: "im", label: "IM \u673A\u5668\u4EBA", icon: Icons.bot },
1947
- { 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 }
1948
2425
  ];
1949
2426
  function TabBar({ active, onChange, dots }) {
1950
2427
  return React.createElement(
1951
2428
  "div",
1952
2429
  {
2430
+ className: "dsh-tabbar-container",
1953
2431
  style: {
1954
2432
  display: "flex",
1955
2433
  gap: 4,
@@ -1958,7 +2436,9 @@ function TabBar({ active, onChange, dots }) {
1958
2436
  overflowX: "auto",
1959
2437
  WebkitOverflowScrolling: "touch",
1960
2438
  maxWidth: "100%",
1961
- flexWrap: "nowrap"
2439
+ flexWrap: "nowrap",
2440
+ scrollbarWidth: "none",
2441
+ msOverflowStyle: "none"
1962
2442
  }
1963
2443
  },
1964
2444
  TABS.map(({ id, label, icon: TabIcon }) => {
@@ -2276,6 +2756,18 @@ function BridgePanel({ rpcCall }) {
2276
2756
  rpcCall: authRpcCall,
2277
2757
  onUpdate: () => load(true)
2278
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
+ );
2279
2771
  } else if (activeTab === "im") {
2280
2772
  const IM_PLATFORMS = [
2281
2773
  { id: "wechat", label: "\u5FAE\u4FE1", icon: Icons.wechat, brandColor: "#07C160", desc: "ClawBot \u626B\u7801\u76F4\u8FDE \xB7 \u65E0\u9700\u516C\u7F51" },
@@ -2646,6 +3138,17 @@ function injectMobileStyles() {
2646
3138
  const style = document.createElement("style");
2647
3139
  style.id = "dsh-bridge-mobile-styles";
2648
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
+
2649
3152
  /* DSH Bridge \u79FB\u52A8\u7AEF\u81EA\u9002\u5E94\u4E0E\u89E6\u63A7\u4EA4\u4E92\u589E\u5F3A\u6837\u5F0F */
2650
3153
  :root {
2651
3154
  --dsh-mobile-header-h: 52px;