dsh-pocket 2.6.0 → 2.6.2

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/README.en.md CHANGED
@@ -194,7 +194,7 @@ Such tools take over all traffic and often cut cloudflared's tunnel-edge connect
194
194
  ```sh
195
195
  npm install
196
196
  node client/build.mjs # rebuild after editing client/
197
- npm test # proxy / auth / compression / tunnel / service / RPC / settings (105 tests)
197
+ npm test # proxy / auth / compression / tunnel / service / RPC / settings (109 tests)
198
198
  ```
199
199
 
200
200
  **Want to try your changes locally without publishing?** Point the installed plugin at your local checkout with a symlink and restart dsh web. Full steps (including switching back to the npm release) are in [LOCAL-DEV.md](./LOCAL-DEV.md).
package/README.md CHANGED
@@ -195,7 +195,7 @@ npx @deepseek-ai/dsh web
195
195
  ```sh
196
196
  npm install
197
197
  node client/build.mjs # 改 client/ 后重新打包
198
- npm test # 代理 / 认证 / 压缩 / 隧道 / 服务 / RPC / 设置(105 测试)
198
+ npm test # 代理 / 认证 / 压缩 / 隧道 / 服务 / RPC / 设置(109 测试)
199
199
  ```
200
200
 
201
201
  **改完想在本机先试?** 不用发版:把插件换成指向本地仓库的软链,重启 dsh web 就是本地代码。完整步骤(含怎么换回 npm 官方版本)见 [LOCAL-DEV.md](./LOCAL-DEV.md)。
package/client/client.js CHANGED
@@ -37,7 +37,7 @@ __export(index_exports, {
37
37
  redactStatus: () => redactStatus
38
38
  });
39
39
  module.exports = __toCommonJS(index_exports);
40
- var import_react2 = require("react");
40
+ var import_react3 = require("react");
41
41
 
42
42
  // client/api.js
43
43
  var POCKET_RPC_CHANNEL = "/dsh-pocket";
@@ -340,6 +340,59 @@ function MobileDrawerFooter({ useSessions, downloadSessionLog, toggleSidebar, t
340
340
  ));
341
341
  }
342
342
 
343
+ // client/mobile/MobileComposerFullscreen.tsx
344
+ var import_react2 = require("react");
345
+ var FULLSCREEN_ATTR = "data-dsh-pocket-composer-fullscreen";
346
+ function isFullscreen() {
347
+ return typeof document !== "undefined" && document.body?.getAttribute(FULLSCREEN_ATTR) === "1";
348
+ }
349
+ function setFullscreen(on) {
350
+ if (typeof document === "undefined") return;
351
+ if (on) document.body.setAttribute(FULLSCREEN_ATTR, "1");
352
+ else document.body.removeAttribute(FULLSCREEN_ATTR);
353
+ }
354
+ function MobileComposerFullscreen() {
355
+ const [on, setOn] = (0, import_react2.useState)(isFullscreen);
356
+ (0, import_react2.useEffect)(() => {
357
+ const update = () => setOn(isFullscreen());
358
+ const observer = new MutationObserver(update);
359
+ observer.observe(document.body, { attributes: true, attributeFilter: [FULLSCREEN_ATTR] });
360
+ return () => observer.disconnect();
361
+ }, []);
362
+ return (0, import_react2.createElement)("button", {
363
+ type: "button",
364
+ "aria-label": on ? "\u6536\u8D77\u8F93\u5165" : "\u653E\u5927\u8F93\u5165",
365
+ "aria-pressed": on,
366
+ title: on ? "\u6536\u8D77\u8F93\u5165" : "\u653E\u5927\u8F93\u5165",
367
+ "data-mobile-nav": "composer-fullscreen",
368
+ "data-mobile-nav-keep": "1",
369
+ // 自己的按钮不该被 #23 的 slot 通用隐藏规则误伤
370
+ onClick: () => {
371
+ const next = !isFullscreen();
372
+ setFullscreen(next);
373
+ setOn(next);
374
+ },
375
+ style: {
376
+ appearance: "none",
377
+ WebkitAppearance: "none",
378
+ border: "none",
379
+ background: "transparent",
380
+ color: "inherit",
381
+ cursor: "pointer",
382
+ padding: "0 8px",
383
+ height: 32,
384
+ minWidth: 32,
385
+ borderRadius: 6,
386
+ display: "inline-flex",
387
+ alignItems: "center",
388
+ justifyContent: "center",
389
+ font: "inherit",
390
+ fontSize: 16,
391
+ lineHeight: 1
392
+ }
393
+ }, on ? "\u2921" : "\u2922");
394
+ }
395
+
343
396
  // client/mobile/mobile.css.ts
344
397
  var MOBILE_CSS = `
345
398
  /* ---------- base control styles (rendered at any width, hidden where unused) ---------- */
@@ -1767,15 +1820,15 @@ var styles = {
1767
1820
  warn: { color: "var(--dsw-alias-state-warn-primary,#b45309)", fontSize: 12, lineHeight: 1.5 }
1768
1821
  };
1769
1822
  function PocketSettingsTab({ rpcCall, t }) {
1770
- const [status, setStatus] = (0, import_react2.useState)(null);
1771
- const [busy, setBusy] = (0, import_react2.useState)(false);
1772
- const [error, setError] = (0, import_react2.useState)(null);
1773
- const [tunnelState, setTunnelState] = (0, import_react2.useState)(null);
1774
- const [restartNotice, setRestartNotice] = (0, import_react2.useState)(false);
1775
- const [updateInfo, setUpdateInfo] = (0, import_react2.useState)(null);
1776
- const [isDesktop, setIsDesktop] = (0, import_react2.useState)(false);
1777
- const [now, setNow] = (0, import_react2.useState)(Date.now());
1778
- (0, import_react2.useEffect)(() => {
1823
+ const [status, setStatus] = (0, import_react3.useState)(null);
1824
+ const [busy, setBusy] = (0, import_react3.useState)(false);
1825
+ const [error, setError] = (0, import_react3.useState)(null);
1826
+ const [tunnelState, setTunnelState] = (0, import_react3.useState)(null);
1827
+ const [restartNotice, setRestartNotice] = (0, import_react3.useState)(false);
1828
+ const [updateInfo, setUpdateInfo] = (0, import_react3.useState)(null);
1829
+ const [isDesktop, setIsDesktop] = (0, import_react3.useState)(false);
1830
+ const [now, setNow] = (0, import_react3.useState)(Date.now());
1831
+ (0, import_react3.useEffect)(() => {
1779
1832
  const t2 = setInterval(() => setNow(Date.now()), 1e3);
1780
1833
  return () => clearInterval(t2);
1781
1834
  }, []);
@@ -1807,18 +1860,18 @@ function PocketSettingsTab({ rpcCall, t }) {
1807
1860
  } catch {
1808
1861
  }
1809
1862
  };
1810
- (0, import_react2.useEffect)(() => {
1863
+ (0, import_react3.useEffect)(() => {
1811
1864
  load();
1812
1865
  const t2 = setInterval(load, 3e3);
1813
1866
  return () => clearInterval(t2);
1814
1867
  }, []);
1815
- (0, import_react2.useEffect)(() => {
1868
+ (0, import_react3.useEffect)(() => {
1816
1869
  try {
1817
1870
  sessionStorage.removeItem("dshp-auto-reloaded");
1818
1871
  } catch {
1819
1872
  }
1820
1873
  }, []);
1821
- (0, import_react2.useEffect)(() => {
1874
+ (0, import_react3.useEffect)(() => {
1822
1875
  if (isDesktop) return;
1823
1876
  let alive = true;
1824
1877
  const check = async () => {
@@ -1874,11 +1927,11 @@ function PocketSettingsTab({ rpcCall, t }) {
1874
1927
  setUpdateInfo((u) => ({ ...u, updating: false, result: "fail", output: err.message }));
1875
1928
  }
1876
1929
  };
1877
- const [tempPinForm, setTempPinForm] = (0, import_react2.useState)({ kind: "lan", expiresInSec: 86400, label: "" });
1878
- const [tempPinsList, setTempPinsList] = (0, import_react2.useState)([]);
1879
- const [tempPinLast, setTempPinLast] = (0, import_react2.useState)(null);
1880
- const [tempPinBusy, setTempPinBusy] = (0, import_react2.useState)(false);
1881
- const [tempPinError, setTempPinError] = (0, import_react2.useState)(null);
1930
+ const [tempPinForm, setTempPinForm] = (0, import_react3.useState)({ kind: "lan", expiresInSec: 86400, label: "" });
1931
+ const [tempPinsList, setTempPinsList] = (0, import_react3.useState)([]);
1932
+ const [tempPinLast, setTempPinLast] = (0, import_react3.useState)(null);
1933
+ const [tempPinBusy, setTempPinBusy] = (0, import_react3.useState)(false);
1934
+ const [tempPinError, setTempPinError] = (0, import_react3.useState)(null);
1882
1935
  const loadTempPins = async () => {
1883
1936
  try {
1884
1937
  const r = await call(POCKET_ENDPOINTS.tempPinList, {});
@@ -1886,10 +1939,10 @@ function PocketSettingsTab({ rpcCall, t }) {
1886
1939
  } catch {
1887
1940
  }
1888
1941
  };
1889
- (0, import_react2.useEffect)(() => {
1942
+ (0, import_react3.useEffect)(() => {
1890
1943
  loadTempPins();
1891
1944
  }, []);
1892
- (0, import_react2.useEffect)(() => {
1945
+ (0, import_react3.useEffect)(() => {
1893
1946
  if (tempPinsList.length === 0 && !tempPinLast) return;
1894
1947
  const t2 = setInterval(() => {
1895
1948
  loadTempPins();
@@ -1936,13 +1989,13 @@ function PocketSettingsTab({ rpcCall, t }) {
1936
1989
  if (s < 60) return `${s}s`;
1937
1990
  const m = Math.floor(s / 60);
1938
1991
  if (m < 60) return `${m}m`;
1939
- const h2 = Math.floor(m / 60);
1940
- if (h2 < 24) return `${h2}h`;
1941
- const d = Math.floor(h2 / 24);
1992
+ const h3 = Math.floor(m / 60);
1993
+ if (h3 < 24) return `${h3}h`;
1994
+ const d = Math.floor(h3 / 24);
1942
1995
  return `${d}d`;
1943
1996
  };
1944
- const [disclaimerOpen, setDisclaimerOpen] = (0, import_react2.useState)(false);
1945
- const [disclaimerChecked, setDisclaimerChecked] = (0, import_react2.useState)(false);
1997
+ const [disclaimerOpen, setDisclaimerOpen] = (0, import_react3.useState)(false);
1998
+ const [disclaimerChecked, setDisclaimerChecked] = (0, import_react3.useState)(false);
1946
1999
  const doStartTunnel = async () => {
1947
2000
  const cfg = status?.tunnelConfig;
1948
2001
  if (cfg?.mode === "named" && (!cfg.hostname || !cfg.tokenSet)) {
@@ -1975,7 +2028,7 @@ function PocketSettingsTab({ rpcCall, t }) {
1975
2028
  } catch {
1976
2029
  }
1977
2030
  };
1978
- const [tunnelCfg, setTunnelCfg] = (0, import_react2.useState)(null);
2031
+ const [tunnelCfg, setTunnelCfg] = (0, import_react3.useState)(null);
1979
2032
  const switchToQuick = async () => {
1980
2033
  try {
1981
2034
  setStatus(await call(POCKET_ENDPOINTS.tunnelSetConfig, { mode: "quick" }));
@@ -1996,7 +2049,7 @@ function PocketSettingsTab({ rpcCall, t }) {
1996
2049
  setTunnelCfg((c) => ({ ...c, err: err.message }));
1997
2050
  }
1998
2051
  };
1999
- const [resetOpen, setResetOpen] = (0, import_react2.useState)(false);
2052
+ const [resetOpen, setResetOpen] = (0, import_react3.useState)(false);
2000
2053
  const doFactoryReset = async () => {
2001
2054
  setResetOpen(false);
2002
2055
  setBusy(true);
@@ -2028,7 +2081,7 @@ function PocketSettingsTab({ rpcCall, t }) {
2028
2081
  } catch {
2029
2082
  }
2030
2083
  };
2031
- const [lanToggleOpen, setLanToggleOpen] = (0, import_react2.useState)(null);
2084
+ const [lanToggleOpen, setLanToggleOpen] = (0, import_react3.useState)(null);
2032
2085
  const requestLanToggle = (on) => setLanToggleOpen(on);
2033
2086
  const confirmLanToggle = async () => {
2034
2087
  const on = lanToggleOpen;
@@ -2048,7 +2101,7 @@ function PocketSettingsTab({ rpcCall, t }) {
2048
2101
  setError(err.message);
2049
2102
  }
2050
2103
  };
2051
- const [customPin, setCustomPin] = (0, import_react2.useState)(null);
2104
+ const [customPin, setCustomPin] = (0, import_react3.useState)(null);
2052
2105
  const saveCustomPin = async (which) => {
2053
2106
  try {
2054
2107
  const r = await call(POCKET_ENDPOINTS.pinSetCustom, { which, value: customPin?.value ?? "" });
@@ -2064,11 +2117,11 @@ function PocketSettingsTab({ rpcCall, t }) {
2064
2117
  setCustomPin((c) => ({ ...c, err: err.message }));
2065
2118
  }
2066
2119
  };
2067
- const customPinRow = (which) => (0, import_react2.createElement)(
2120
+ const customPinRow = (which) => (0, import_react3.createElement)(
2068
2121
  "div",
2069
2122
  { style: { marginTop: 6, fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)", lineHeight: 1.5 } },
2070
2123
  t("customizing"),
2071
- (0, import_react2.createElement)("input", {
2124
+ (0, import_react3.createElement)("input", {
2072
2125
  style: { width: 130, margin: "0 6px", padding: "4px 8px", fontSize: 14, letterSpacing: 1, textAlign: "center", border: "1px solid var(--dsw-alias-border-l2,#d1d5db)", borderRadius: 6, outline: "none" },
2073
2126
  type: "password",
2074
2127
  maxLength: 8,
@@ -2080,11 +2133,11 @@ function PocketSettingsTab({ rpcCall, t }) {
2080
2133
  if (e.key === "Escape") setCustomPin(null);
2081
2134
  }
2082
2135
  }),
2083
- (0, import_react2.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12, marginLeft: 2 }, onClick: () => saveCustomPin(which) }, t("save")),
2084
- (0, import_react2.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12 }, onClick: () => setCustomPin(null) }, t("cancel")),
2085
- customPin?.err ? (0, import_react2.createElement)("div", { style: { color: "var(--dsw-alias-state-error-primary,#dc2626)", marginTop: 4 } }, errText(customPin.err)) : null
2136
+ (0, import_react3.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12, marginLeft: 2 }, onClick: () => saveCustomPin(which) }, t("save")),
2137
+ (0, import_react3.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12 }, onClick: () => setCustomPin(null) }, t("cancel")),
2138
+ customPin?.err ? (0, import_react3.createElement)("div", { style: { color: "var(--dsw-alias-state-error-primary,#dc2626)", marginTop: 4 } }, errText(customPin.err)) : null
2086
2139
  );
2087
- const customBtn = (which) => (0, import_react2.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12, marginLeft: 8 }, onClick: () => setCustomPin({ which, value: "", err: null }) }, t("customize"));
2140
+ const customBtn = (which) => (0, import_react3.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12, marginLeft: 8 }, onClick: () => setCustomPin({ which, value: "", err: null }) }, t("customize"));
2088
2141
  const lanUrl = status?.lanUrl;
2089
2142
  const tunnelUrl = status?.tunnelUrl;
2090
2143
  const tunnelPhase = tunnelState?.phase ?? "idle";
@@ -2100,14 +2153,14 @@ function PocketSettingsTab({ rpcCall, t }) {
2100
2153
  if (i < 0) return s;
2101
2154
  return (t("ok") === zh2.ok ? s.slice(0, i) : s.slice(i + 3)).trim();
2102
2155
  };
2103
- const [toast, setToast] = (0, import_react2.useState)(null);
2104
- const toastTimer = (0, import_react2.useRef)(null);
2156
+ const [toast, setToast] = (0, import_react3.useState)(null);
2157
+ const toastTimer = (0, import_react3.useRef)(null);
2105
2158
  const showToast = (text) => {
2106
2159
  setToast(text);
2107
2160
  clearTimeout(toastTimer.current);
2108
2161
  toastTimer.current = setTimeout(() => setToast(null), 2600);
2109
2162
  };
2110
- (0, import_react2.useEffect)(() => () => clearTimeout(toastTimer.current), []);
2163
+ (0, import_react3.useEffect)(() => () => clearTimeout(toastTimer.current), []);
2111
2164
  const modeBtnStyle = (active) => ({
2112
2165
  ...styles.btn,
2113
2166
  height: 28,
@@ -2117,49 +2170,49 @@ function PocketSettingsTab({ rpcCall, t }) {
2117
2170
  background: active ? "var(--dsw-alias-button-primary-fill, var(--dsw-alias-brand-primary,#4f6ef7))" : "var(--dsw-alias-bg-layer-1,#fff)",
2118
2171
  color: active ? "var(--dsw-alias-label-primary-foreground, #fff)" : "var(--dsw-alias-label-primary,inherit)"
2119
2172
  });
2120
- const Switch = (on, onClick) => (0, import_react2.createElement)("button", {
2173
+ const Switch = (on, onClick) => (0, import_react3.createElement)("button", {
2121
2174
  role: "switch",
2122
2175
  "aria-checked": !!on,
2123
2176
  style: { flexShrink: 0, width: 40, height: 22, borderRadius: 11, border: "none", padding: 0, position: "relative", cursor: "pointer", font: "inherit", background: on ? "var(--dsw-alias-button-primary-fill, var(--dsw-alias-brand-primary,#4f6ef7))" : "var(--dsw-alias-border-l2,#d1d5db)" },
2124
2177
  onClick
2125
- }, (0, import_react2.createElement)("span", { style: { position: "absolute", top: 2, left: on ? 20 : 2, width: 18, height: 18, borderRadius: "50%", background: "#fff" } }));
2126
- const qrArea = (src, url, hint) => (0, import_react2.createElement)(
2178
+ }, (0, import_react3.createElement)("span", { style: { position: "absolute", top: 2, left: on ? 20 : 2, width: 18, height: 18, borderRadius: "50%", background: "#fff" } }));
2179
+ const qrArea = (src, url, hint) => (0, import_react3.createElement)(
2127
2180
  "div",
2128
2181
  { style: { background: "var(--dsw-alias-bg-layer-2,#f3f4f6)", borderRadius: 10, padding: "10px 12px", textAlign: "center", margin: "10px 0" } },
2129
- (0, import_react2.createElement)("img", { src, alt: "QR", style: styles.qr }),
2130
- (0, import_react2.createElement)("div", { style: styles.code }, url),
2131
- (0, import_react2.createElement)("div", { style: styles.muted }, hint)
2182
+ (0, import_react3.createElement)("img", { src, alt: "QR", style: styles.qr }),
2183
+ (0, import_react3.createElement)("div", { style: styles.code }, url),
2184
+ (0, import_react3.createElement)("div", { style: styles.muted }, hint)
2132
2185
  );
2133
- const row = (label, control, extra) => (0, import_react2.createElement)(
2186
+ const row = (label, control, extra) => (0, import_react3.createElement)(
2134
2187
  "div",
2135
2188
  { style: { borderTop: "1px solid var(--dsw-alias-border-l2,#e5e7eb)", paddingTop: 9, marginTop: 9 } },
2136
- (0, import_react2.createElement)(
2189
+ (0, import_react3.createElement)(
2137
2190
  "div",
2138
2191
  { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 } },
2139
- (0, import_react2.createElement)("span", { style: { fontSize: 13 } }, label),
2192
+ (0, import_react3.createElement)("span", { style: { fontSize: 13 } }, label),
2140
2193
  control
2141
2194
  ),
2142
2195
  extra ?? null
2143
2196
  );
2144
- const [advOpen, setAdvOpen] = (0, import_react2.useState)(false);
2145
- return (0, import_react2.createElement)(
2197
+ const [advOpen, setAdvOpen] = (0, import_react3.useState)(false);
2198
+ return (0, import_react3.createElement)(
2146
2199
  "div",
2147
2200
  { style: styles.card },
2148
- (0, import_react2.createElement)(
2201
+ (0, import_react3.createElement)(
2149
2202
  "div",
2150
2203
  { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 } },
2151
- (0, import_react2.createElement)(
2204
+ (0, import_react3.createElement)(
2152
2205
  "div",
2153
2206
  null,
2154
- (0, import_react2.createElement)("strong", null, t("title")),
2155
- (0, import_react2.createElement)("div", { style: styles.muted }, t("subtitle"))
2207
+ (0, import_react3.createElement)("strong", null, t("title")),
2208
+ (0, import_react3.createElement)("div", { style: styles.muted }, t("subtitle"))
2156
2209
  ),
2157
- (0, import_react2.createElement)(
2210
+ (0, import_react3.createElement)(
2158
2211
  "div",
2159
2212
  { style: { fontSize: 12, color: "var(--dsw-alias-label-tertiary,#8b93a1)", textAlign: "right" } },
2160
- (0, import_react2.createElement)("div", { style: { whiteSpace: "nowrap" } }, t("developer")),
2161
- (0, import_react2.createElement)("div", { style: { whiteSpace: "nowrap" } }, t("starAsk")),
2162
- (0, import_react2.createElement)(
2213
+ (0, import_react3.createElement)("div", { style: { whiteSpace: "nowrap" } }, t("developer")),
2214
+ (0, import_react3.createElement)("div", { style: { whiteSpace: "nowrap" } }, t("starAsk")),
2215
+ (0, import_react3.createElement)(
2163
2216
  "a",
2164
2217
  { href: "https://github.com/shaobeichen/dsh-pocket", target: "_blank", rel: "noreferrer", style: { color: "var(--dsw-alias-brand-primary,#4f6ef7)", fontSize: 12, lineHeight: 1.6, textDecoration: "underline" } },
2165
2218
  t("starCta")
@@ -2168,49 +2221,49 @@ function PocketSettingsTab({ rpcCall, t }) {
2168
2221
  ),
2169
2222
  // 桌面端不显示更新/重启横幅(更新由 DSH Desktop 管理),也不需要额外提示
2170
2223
  // 重启后提示(进程在后台运行,停止方法)——左侧蓝色色条(桌面端不会触发本插件的自重启)
2171
- !isDesktop && restartNotice ? (0, import_react2.createElement)(
2224
+ !isDesktop && restartNotice ? (0, import_react3.createElement)(
2172
2225
  "div",
2173
2226
  { style: { ...styles.block, borderLeft: "4px solid var(--dsw-alias-brand-primary,#4f6ef7)", borderRadius: 8, background: "var(--dsw-alias-bg-layer-2,#f3f4f6)", padding: "10px 12px" } },
2174
- (0, import_react2.createElement)(
2227
+ (0, import_react3.createElement)(
2175
2228
  "div",
2176
2229
  { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 } },
2177
- (0, import_react2.createElement)("div", { style: { fontWeight: 600, fontSize: 13 } }, t("restarted")),
2178
- (0, import_react2.createElement)("button", { style: styles.btn, onClick: () => setRestartNotice(false) }, t("ok"))
2230
+ (0, import_react3.createElement)("div", { style: { fontWeight: 600, fontSize: 13 } }, t("restarted")),
2231
+ (0, import_react3.createElement)("button", { style: styles.btn, onClick: () => setRestartNotice(false) }, t("ok"))
2179
2232
  ),
2180
- (0, import_react2.createElement)("div", { style: styles.muted, marginTop: 4, wordBreak: "break-all" }, fmt(t, "bgHint", { cmd: status?.killHint ?? `lsof -ti :${status?.dshPort ?? 3080} | xargs kill -9` }))
2233
+ (0, import_react3.createElement)("div", { style: styles.muted, marginTop: 4, wordBreak: "break-all" }, fmt(t, "bgHint", { cmd: status?.killHint ?? `lsof -ti :${status?.dshPort ?? 3080} | xargs kill -9` }))
2181
2234
  ) : null,
2182
2235
  // 更新提示——左侧黄色色条(提示有新版本);单状态:有更新/更新中/已更新自动重启,不并存
2183
2236
  // 桌面端不渲染(更新由 DSH Desktop 管理)
2184
- !isDesktop && updateInfo ? (0, import_react2.createElement)(
2237
+ !isDesktop && updateInfo ? (0, import_react3.createElement)(
2185
2238
  "div",
2186
2239
  { style: { ...styles.block, borderLeft: "4px solid var(--dsw-alias-state-warn-primary,#b45309)", borderRadius: 8, background: "var(--dsw-alias-bg-layer-2,#f3f4f6)", padding: "10px 12px" } },
2187
- (0, import_react2.createElement)(
2240
+ (0, import_react3.createElement)(
2188
2241
  "div",
2189
2242
  { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 } },
2190
- (0, import_react2.createElement)(
2243
+ (0, import_react3.createElement)(
2191
2244
  "div",
2192
2245
  { style: { fontWeight: 600, fontSize: 13 } },
2193
2246
  updateInfo.updated ? fmt(t, "updatedRestart", { ver: updateInfo.current }) : updateInfo.result === "ok" ? updateInfo.autoRestart ? fmt(t, "updateAutoRestarting", { ver: updateInfo.latest }) : fmt(t, "updatedOk", { ver: updateInfo.latest }) : fmt(t, "updateAvailable", { ver: updateInfo.latest })
2194
2247
  ),
2195
- updateInfo.result !== "ok" ? (0, import_react2.createElement)("button", { style: styles.primary, onClick: runUpdate, disabled: updateInfo.updating }, updateInfo.updating ? t("updating") : fmt(t, "updateTo", { ver: updateInfo.latest })) : updateInfo.autoRestart ? (0, import_react2.createElement)("button", { style: styles.btn, disabled: true }, t("restartingNow")) : (0, import_react2.createElement)("button", { style: styles.primary, onClick: restartPocket, disabled: updateInfo.restarting }, updateInfo.restarting ? t("restarting") : t("restartNow"))
2248
+ updateInfo.result !== "ok" ? (0, import_react3.createElement)("button", { style: styles.primary, onClick: runUpdate, disabled: updateInfo.updating }, updateInfo.updating ? t("updating") : fmt(t, "updateTo", { ver: updateInfo.latest })) : updateInfo.autoRestart ? (0, import_react3.createElement)("button", { style: styles.btn, disabled: true }, t("restartingNow")) : (0, import_react3.createElement)("button", { style: styles.primary, onClick: restartPocket, disabled: updateInfo.restarting }, updateInfo.restarting ? t("restarting") : t("restartNow"))
2196
2249
  ),
2197
- (0, import_react2.createElement)(
2250
+ (0, import_react3.createElement)(
2198
2251
  "div",
2199
2252
  { style: styles.muted, marginTop: 4 },
2200
2253
  updateInfo.updating ? fmt(t, "updatingDetail", { s: elapsed(updateInfo.startedAt) }) : updateInfo.restarting ? fmt(t, "restartingDetail", { s: elapsed(updateInfo.startedAt) }) : updateInfo.result === "ok" ? updateInfo.autoRestart ? t("updatedAutoDetail") : t("updatedRestartDetail") : updateInfo.result === "fail" ? fmt(t, "updateFailed", { err: errText(updateInfo.output) || t("unknownError") }) : fmt(t, "versionRange", { cur: updateInfo.current, latest: updateInfo.latest })
2201
2254
  )
2202
2255
  ) : null,
2203
2256
  // 局域网:标题行自带总开关 → 二维码+地址 → 设置行(访问密码 / 高级·手动选地址)
2204
- (0, import_react2.createElement)(
2257
+ (0, import_react3.createElement)(
2205
2258
  "div",
2206
2259
  { style: styles.block },
2207
- (0, import_react2.createElement)(
2260
+ (0, import_react3.createElement)(
2208
2261
  "div",
2209
2262
  { style: { display: "flex", alignItems: "center", justifyContent: "space-between" } },
2210
- (0, import_react2.createElement)("span", { style: { fontWeight: 600, fontSize: 13 } }, t("lanAccess")),
2263
+ (0, import_react3.createElement)("span", { style: { fontWeight: 600, fontSize: 13 } }, t("lanAccess")),
2211
2264
  Switch(status?.lanEnabled !== false, () => requestLanToggle(status?.lanEnabled === false))
2212
2265
  ),
2213
- status?.lanEnabled === false ? (0, import_react2.createElement)("div", { style: { marginTop: 8, fontSize: 12, color: "var(--dsw-alias-state-warn-primary,#b45309)", lineHeight: 1.5 } }, t("lanDisabledHint")) : lanUrl ? (0, import_react2.createElement)(
2266
+ status?.lanEnabled === false ? (0, import_react3.createElement)("div", { style: { marginTop: 8, fontSize: 12, color: "var(--dsw-alias-state-warn-primary,#b45309)", lineHeight: 1.5 } }, t("lanDisabledHint")) : lanUrl ? (0, import_react3.createElement)(
2214
2267
  "div",
2215
2268
  null,
2216
2269
  qrArea(status.lanQr, lanUrl, t("lanHint")),
@@ -2218,100 +2271,100 @@ function PocketSettingsTab({ rpcCall, t }) {
2218
2271
  row(
2219
2272
  t("lanPin"),
2220
2273
  Switch(status?.lanAuthEnabled !== false, () => setLanAuth(status?.lanAuthEnabled === false)),
2221
- status?.lanAuthEnabled === false ? (0, import_react2.createElement)("div", { style: { ...styles.muted, marginTop: 6 } }, t("lanPinOff")) : customPin?.which === "lan" ? customPinRow("lan") : (0, import_react2.createElement)(
2274
+ status?.lanAuthEnabled === false ? (0, import_react3.createElement)("div", { style: { ...styles.muted, marginTop: 6 } }, t("lanPinOff")) : customPin?.which === "lan" ? customPinRow("lan") : (0, import_react3.createElement)(
2222
2275
  "div",
2223
2276
  { style: { marginTop: 6, display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" } },
2224
- (0, import_react2.createElement)("span", { style: { fontFamily: "ui-monospace,Menlo,monospace", fontSize: 13, letterSpacing: 1 } }, status.lanToken),
2225
- (0, import_react2.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12 }, onClick: refreshLanPin }, t("refresh")),
2277
+ (0, import_react3.createElement)("span", { style: { fontFamily: "ui-monospace,Menlo,monospace", fontSize: 13, letterSpacing: 1 } }, status.lanToken),
2278
+ (0, import_react3.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12 }, onClick: refreshLanPin }, t("refresh")),
2226
2279
  customBtn("lan"),
2227
- status?.lanPinCustom ? (0, import_react2.createElement)("span", { style: { fontSize: 11, color: "var(--dsw-alias-state-warn-primary,#b45309)" } }, t("pinCustomHint")) : null
2280
+ status?.lanPinCustom ? (0, import_react3.createElement)("span", { style: { fontSize: 11, color: "var(--dsw-alias-state-warn-primary,#b45309)" } }, t("pinCustomHint")) : null
2228
2281
  )
2229
2282
  ),
2230
2283
  // 高级:手动选地址(默认收起)
2231
2284
  row(
2232
2285
  t("advAddress"),
2233
- (0, import_react2.createElement)(
2286
+ (0, import_react3.createElement)(
2234
2287
  "button",
2235
2288
  { style: { border: "none", background: "none", font: "inherit", cursor: "pointer", fontSize: 12, color: "var(--dsw-alias-label-tertiary,#8b93a1)", padding: 0 }, onClick: () => setAdvOpen((v) => !v) },
2236
2289
  (status?.lanIpOverride || t("lanAddressAuto")) + " \u203A"
2237
2290
  ),
2238
- advOpen ? (0, import_react2.createElement)(
2291
+ advOpen ? (0, import_react3.createElement)(
2239
2292
  "div",
2240
2293
  { style: { marginTop: 8 } },
2241
- (0, import_react2.createElement)(
2294
+ (0, import_react3.createElement)(
2242
2295
  "label",
2243
2296
  { style: { display: "flex", alignItems: "center", gap: 8, fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)" } },
2244
2297
  t("lanAddress"),
2245
- (0, import_react2.createElement)(
2298
+ (0, import_react3.createElement)(
2246
2299
  "select",
2247
2300
  {
2248
2301
  value: status?.lanIpOverride || "",
2249
2302
  onChange: (e) => setLanAddress(e.target.value),
2250
2303
  style: { font: "inherit", height: 30, padding: "0 8px", borderRadius: 8, border: "1px solid var(--dsw-alias-border-l2,#d1d5db)", background: "var(--dsw-alias-bg-layer-1,#fff)", color: "var(--dsw-alias-label-primary,inherit)" }
2251
2304
  },
2252
- (0, import_react2.createElement)("option", { value: "" }, t("lanAddressAuto")),
2253
- (status?.lanCandidates || []).map((ip) => (0, import_react2.createElement)("option", { key: ip, value: ip }, ip))
2305
+ (0, import_react3.createElement)("option", { value: "" }, t("lanAddressAuto")),
2306
+ (status?.lanCandidates || []).map((ip) => (0, import_react3.createElement)("option", { key: ip, value: ip }, ip))
2254
2307
  )
2255
2308
  )
2256
2309
  ) : null
2257
2310
  )
2258
- ) : (0, import_react2.createElement)("div", { style: styles.muted }, t("lanStarting"))
2311
+ ) : (0, import_react3.createElement)("div", { style: styles.muted }, t("lanStarting"))
2259
2312
  ),
2260
2313
  // 公网:标题行自带 开启/关闭 → 开启后:二维码+地址、地址模式行、访问密码行
2261
- (0, import_react2.createElement)(
2314
+ (0, import_react3.createElement)(
2262
2315
  "div",
2263
2316
  { style: styles.block },
2264
- (0, import_react2.createElement)(
2317
+ (0, import_react3.createElement)(
2265
2318
  "div",
2266
2319
  { style: { display: "flex", alignItems: "center", justifyContent: "space-between" } },
2267
- (0, import_react2.createElement)("span", { style: { fontWeight: 600, fontSize: 13 } }, t("wanAccess")),
2268
- tunnelUrl ? (0, import_react2.createElement)("button", { style: { ...styles.btn, height: 28, padding: "0 12px", fontSize: 12, color: "var(--dsw-alias-state-error-primary,#dc2626)" }, onClick: stopTunnel }, t("stopTunnel")) : (0, import_react2.createElement)("button", { style: { ...styles.primary, height: 28, padding: "0 14px", fontSize: 12 }, onClick: startTunnel, disabled: busy || tunnelStarting }, busy || tunnelStarting ? t("opening") : t("enable"))
2320
+ (0, import_react3.createElement)("span", { style: { fontWeight: 600, fontSize: 13 } }, t("wanAccess")),
2321
+ tunnelUrl ? (0, import_react3.createElement)("button", { style: { ...styles.btn, height: 28, padding: "0 12px", fontSize: 12, color: "var(--dsw-alias-state-error-primary,#dc2626)" }, onClick: stopTunnel }, t("stopTunnel")) : (0, import_react3.createElement)("button", { style: { ...styles.primary, height: 28, padding: "0 14px", fontSize: 12 }, onClick: startTunnel, disabled: busy || tunnelStarting }, busy || tunnelStarting ? t("opening") : t("enable"))
2269
2322
  ),
2270
- tunnelStarting ? (0, import_react2.createElement)(
2323
+ tunnelStarting ? (0, import_react3.createElement)(
2271
2324
  "div",
2272
2325
  { style: { marginTop: 8, fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)" } },
2273
2326
  tunnelPhase === "downloading" ? fmt(t, "downloading", { s: elapsed(tunnelStateStarted) }) : fmt(t, "connecting", { s: elapsed(tunnelStateStarted), suffix: elapsed(tunnelStateStarted) > 30 ? t("slowHint") : "" })
2274
- ) : tunnelPhase === "error" ? (0, import_react2.createElement)(
2327
+ ) : tunnelPhase === "error" ? (0, import_react3.createElement)(
2275
2328
  "div",
2276
2329
  { style: { marginTop: 8, fontSize: 12, color: "var(--dsw-alias-state-error-primary,#dc2626)" } },
2277
2330
  fmt(t, "error", { detail: errText(tunnelStateDetail) || t("unknownError") })
2278
- ) : !tunnelUrl && !isDesktop ? (0, import_react2.createElement)("div", { style: { ...styles.muted, marginTop: 8 } }, t("wanOffHint")) : null,
2279
- tunnelUrl ? (0, import_react2.createElement)(
2331
+ ) : !tunnelUrl && !isDesktop ? (0, import_react3.createElement)("div", { style: { ...styles.muted, marginTop: 8 } }, t("wanOffHint")) : null,
2332
+ tunnelUrl ? (0, import_react3.createElement)(
2280
2333
  "div",
2281
2334
  null,
2282
2335
  qrArea(status.tunnelQr, tunnelUrl, namedMode ? t("namedRunningHint") : t("wanHint")),
2283
2336
  // 地址模式行(随机/固定;固定域名选中或编辑时高亮)
2284
2337
  row(
2285
2338
  t("modeLabel"),
2286
- (0, import_react2.createElement)(
2339
+ (0, import_react3.createElement)(
2287
2340
  "span",
2288
2341
  { style: { display: "inline-flex", gap: 6 } },
2289
- (0, import_react2.createElement)("button", { style: modeBtnStyle(!namedActive), onClick: namedMode ? switchToQuick : tunnelCfg ? () => setTunnelCfg(null) : void 0 }, t("modeQuick")),
2290
- (0, import_react2.createElement)("button", { style: modeBtnStyle(namedActive), onClick: () => setTunnelCfg(tunnelCfg ? null : { hostname: tunnelModeView.hostname ?? "", token: "", err: null }) }, t("modeNamed"))
2342
+ (0, import_react3.createElement)("button", { style: modeBtnStyle(!namedActive), onClick: namedMode ? switchToQuick : tunnelCfg ? () => setTunnelCfg(null) : void 0 }, t("modeQuick")),
2343
+ (0, import_react3.createElement)("button", { style: modeBtnStyle(namedActive), onClick: () => setTunnelCfg(tunnelCfg ? null : { hostname: tunnelModeView.hostname ?? "", token: "", err: null }) }, t("modeNamed"))
2291
2344
  ),
2292
- (0, import_react2.createElement)(
2345
+ (0, import_react3.createElement)(
2293
2346
  "div",
2294
2347
  { style: { marginTop: 6 } },
2295
2348
  // 刚保存固定域名但当前连接仍是随机域名:需关闭后重新开启才生效
2296
- namedMode && /trycloudflare\.com/i.test(tunnelUrl ?? "") ? (0, import_react2.createElement)("div", { style: { ...styles.warn } }, t("namedTakeEffect")) : null,
2349
+ namedMode && /trycloudflare\.com/i.test(tunnelUrl ?? "") ? (0, import_react3.createElement)("div", { style: { ...styles.warn } }, t("namedTakeEffect")) : null,
2297
2350
  // 固定域名:已保存摘要 + 修改入口(非编辑态)
2298
- namedMode && !tunnelCfg ? (0, import_react2.createElement)(
2351
+ namedMode && !tunnelCfg ? (0, import_react3.createElement)(
2299
2352
  "div",
2300
2353
  { style: { ...styles.muted } },
2301
2354
  fmt(t, "namedSummary", { host: tunnelModeView.hostname || "\u2014", token: tunnelModeView.tokenSet ? t("namedTokenSet") : t("namedTokenMissing") }),
2302
- (0, import_react2.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12, marginLeft: 8 }, onClick: () => setTunnelCfg({ hostname: tunnelModeView.hostname ?? "", token: "", err: null }) }, t("namedEdit")),
2303
- (0, import_react2.createElement)("div", { style: { ...styles.muted, marginTop: 4 } }, t("namedHow")),
2304
- !tunnelModeView.tokenSet || !tunnelModeView.hostname ? (0, import_react2.createElement)("div", { style: { marginTop: 2, color: "var(--dsw-alias-state-error-primary,#dc2626)" } }, t("namedNeedCfg")) : null
2355
+ (0, import_react3.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12, marginLeft: 8 }, onClick: () => setTunnelCfg({ hostname: tunnelModeView.hostname ?? "", token: "", err: null }) }, t("namedEdit")),
2356
+ (0, import_react3.createElement)("div", { style: { ...styles.muted, marginTop: 4 } }, t("namedHow")),
2357
+ !tunnelModeView.tokenSet || !tunnelModeView.hostname ? (0, import_react3.createElement)("div", { style: { marginTop: 2, color: "var(--dsw-alias-state-error-primary,#dc2626)" } }, t("namedNeedCfg")) : null
2305
2358
  ) : null,
2306
2359
  // 固定域名:编辑表单(域名 + Tunnel Token,Token 留空保持不变)
2307
- tunnelCfg ? (0, import_react2.createElement)(
2360
+ tunnelCfg ? (0, import_react3.createElement)(
2308
2361
  "div",
2309
2362
  { style: { marginTop: 6, fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)", lineHeight: 1.6 } },
2310
- (0, import_react2.createElement)(
2363
+ (0, import_react3.createElement)(
2311
2364
  "div",
2312
2365
  null,
2313
2366
  t("namedHostnameLabel"),
2314
- (0, import_react2.createElement)("input", {
2367
+ (0, import_react3.createElement)("input", {
2315
2368
  style: { margin: "4px 0 0 6px", padding: "4px 8px", fontSize: 13, border: "1px solid var(--dsw-alias-border-l2,#d1d5db)", borderRadius: 6, outline: "none", width: 200 },
2316
2369
  placeholder: "pocket.example.com",
2317
2370
  value: tunnelCfg.hostname ?? "",
@@ -2323,11 +2376,11 @@ function PocketSettingsTab({ rpcCall, t }) {
2323
2376
  }
2324
2377
  })
2325
2378
  ),
2326
- (0, import_react2.createElement)(
2379
+ (0, import_react3.createElement)(
2327
2380
  "div",
2328
2381
  { style: { marginTop: 6 } },
2329
2382
  t("namedTokenLabel"),
2330
- (0, import_react2.createElement)("input", {
2383
+ (0, import_react3.createElement)("input", {
2331
2384
  style: { margin: "4px 0 0 6px", padding: "4px 8px", fontSize: 13, border: "1px solid var(--dsw-alias-border-l2,#d1d5db)", borderRadius: 6, outline: "none", width: 240, fontFamily: "ui-monospace,Menlo,monospace" },
2332
2385
  type: "password",
2333
2386
  value: tunnelCfg.token ?? "",
@@ -2338,70 +2391,70 @@ function PocketSettingsTab({ rpcCall, t }) {
2338
2391
  }
2339
2392
  })
2340
2393
  ),
2341
- (0, import_react2.createElement)(
2394
+ (0, import_react3.createElement)(
2342
2395
  "div",
2343
2396
  { style: { marginTop: 6, display: "flex", gap: 8 } },
2344
- (0, import_react2.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12 }, onClick: saveNamedTunnel }, t("save")),
2345
- (0, import_react2.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12 }, onClick: () => setTunnelCfg(null) }, t("cancel"))
2397
+ (0, import_react3.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12 }, onClick: saveNamedTunnel }, t("save")),
2398
+ (0, import_react3.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12 }, onClick: () => setTunnelCfg(null) }, t("cancel"))
2346
2399
  ),
2347
- (0, import_react2.createElement)("div", { style: { ...styles.muted, marginTop: 6 } }, t("namedHow")),
2348
- (0, import_react2.createElement)("div", { style: { marginTop: 2, fontSize: 11, color: "var(--dsw-alias-state-warn-primary,#b45309)", lineHeight: 1.5 } }, t("namedSecurity")),
2349
- tunnelCfg.err ? (0, import_react2.createElement)("div", { style: { color: "var(--dsw-alias-state-error-primary,#dc2626)", marginTop: 4 } }, errText(tunnelCfg.err)) : null
2400
+ (0, import_react3.createElement)("div", { style: { ...styles.muted, marginTop: 6 } }, t("namedHow")),
2401
+ (0, import_react3.createElement)("div", { style: { marginTop: 2, fontSize: 11, color: "var(--dsw-alias-state-warn-primary,#b45309)", lineHeight: 1.5 } }, t("namedSecurity")),
2402
+ tunnelCfg.err ? (0, import_react3.createElement)("div", { style: { color: "var(--dsw-alias-state-error-primary,#dc2626)", marginTop: 4 } }, errText(tunnelCfg.err)) : null
2350
2403
  ) : null
2351
2404
  )
2352
2405
  ),
2353
2406
  // 访问密码行:值 + 自定义(自定义输入态整体替换)
2354
2407
  status.accessToken ? row(
2355
2408
  t("pinLabel"),
2356
- customPin?.which === "public" ? null : (0, import_react2.createElement)(
2409
+ customPin?.which === "public" ? null : (0, import_react3.createElement)(
2357
2410
  "span",
2358
2411
  { style: { display: "inline-flex", alignItems: "center", gap: 8 } },
2359
- (0, import_react2.createElement)("span", { style: { fontFamily: "ui-monospace,Menlo,monospace", fontSize: 13, letterSpacing: 1 } }, status.accessToken),
2412
+ (0, import_react3.createElement)("span", { style: { fontFamily: "ui-monospace,Menlo,monospace", fontSize: 13, letterSpacing: 1 } }, status.accessToken),
2360
2413
  customBtn("public")
2361
2414
  ),
2362
- (0, import_react2.createElement)(
2415
+ (0, import_react3.createElement)(
2363
2416
  "div",
2364
2417
  { style: { marginTop: 6 } },
2365
2418
  customPin?.which === "public" ? customPinRow("public") : null,
2366
- status?.publicPinCustom ? (0, import_react2.createElement)("div", { style: { ...styles.warn } }, t("pinCustomHint")) : null,
2367
- namedMode ? (0, import_react2.createElement)("div", { style: { ...styles.warn } }, t("namedSecurity")) : null
2419
+ status?.publicPinCustom ? (0, import_react3.createElement)("div", { style: { ...styles.warn } }, t("pinCustomHint")) : null,
2420
+ namedMode ? (0, import_react3.createElement)("div", { style: { ...styles.warn } }, t("namedSecurity")) : null
2368
2421
  )
2369
2422
  ) : null
2370
2423
  ) : null
2371
2424
  ),
2372
- error ? (0, import_react2.createElement)("div", { style: { color: "var(--dsw-alias-state-error-primary,#dc2626)", fontSize: 12, marginTop: 8 } }, `\u274C ${errText(error)}`) : null,
2425
+ error ? (0, import_react3.createElement)("div", { style: { color: "var(--dsw-alias-state-error-primary,#dc2626)", fontSize: 12, marginTop: 8 } }, `\u274C ${errText(error)}`) : null,
2373
2426
  // 临时 PIN(issue #69):带过期的访问密码,给访客用
2374
- (0, import_react2.createElement)(
2427
+ (0, import_react3.createElement)(
2375
2428
  "div",
2376
2429
  { style: styles.block },
2377
- (0, import_react2.createElement)("div", { style: { fontWeight: 600, fontSize: 13, marginBottom: 4 } }, t("tempPinTitle")),
2378
- (0, import_react2.createElement)("div", { style: { ...styles.muted, marginBottom: 10 } }, t("tempPinSubtitle")),
2430
+ (0, import_react3.createElement)("div", { style: { fontWeight: 600, fontSize: 13, marginBottom: 4 } }, t("tempPinTitle")),
2431
+ (0, import_react3.createElement)("div", { style: { ...styles.muted, marginBottom: 10 } }, t("tempPinSubtitle")),
2379
2432
  // 生成表单:分类 + 时长 + 备注 + 生成
2380
- (0, import_react2.createElement)(
2433
+ (0, import_react3.createElement)(
2381
2434
  "div",
2382
2435
  { style: { display: "flex", flexWrap: "wrap", gap: 8, alignItems: "center" } },
2383
- (0, import_react2.createElement)(
2436
+ (0, import_react3.createElement)(
2384
2437
  "select",
2385
2438
  {
2386
2439
  value: tempPinForm.kind,
2387
2440
  onChange: (e) => setTempPinForm((f) => ({ ...f, kind: e.target.value })),
2388
2441
  style: { font: "inherit", height: 30, padding: "0 8px", borderRadius: 8, border: "1px solid var(--dsw-alias-border-l2,#d1d5db)", background: "var(--dsw-alias-bg-layer-1,#fff)", color: "var(--dsw-alias-label-primary,inherit)" }
2389
2442
  },
2390
- (0, import_react2.createElement)("option", { value: "public" }, t("tempPinKindPublic")),
2391
- (0, import_react2.createElement)("option", { value: "lan" }, t("tempPinKindLan"))
2443
+ (0, import_react3.createElement)("option", { value: "public" }, t("tempPinKindPublic")),
2444
+ (0, import_react3.createElement)("option", { value: "lan" }, t("tempPinKindLan"))
2392
2445
  ),
2393
- (0, import_react2.createElement)(
2446
+ (0, import_react3.createElement)(
2394
2447
  "select",
2395
2448
  {
2396
2449
  value: String(tempPinForm.expiresInSec),
2397
2450
  onChange: (e) => setTempPinForm((f) => ({ ...f, expiresInSec: Number(e.target.value) })),
2398
2451
  style: { font: "inherit", height: 30, padding: "0 8px", borderRadius: 8, border: "1px solid var(--dsw-alias-border-l2,#d1d5db)", background: "var(--dsw-alias-bg-layer-1,#fff)", color: "var(--dsw-alias-label-primary,inherit)" }
2399
2452
  },
2400
- (0, import_react2.createElement)("option", { value: "3600" }, t("tempPinDuration1h")),
2401
- (0, import_react2.createElement)("option", { value: "86400" }, t("tempPinDuration24h")),
2402
- (0, import_react2.createElement)("option", { value: "604800" }, t("tempPinDuration7d"))
2453
+ (0, import_react3.createElement)("option", { value: "3600" }, t("tempPinDuration1h")),
2454
+ (0, import_react3.createElement)("option", { value: "86400" }, t("tempPinDuration24h")),
2455
+ (0, import_react3.createElement)("option", { value: "604800" }, t("tempPinDuration7d"))
2403
2456
  ),
2404
- (0, import_react2.createElement)("input", {
2457
+ (0, import_react3.createElement)("input", {
2405
2458
  style: { flex: 1, minWidth: 120, height: 30, padding: "0 10px", borderRadius: 8, border: "1px solid var(--dsw-alias-border-l2,#d1d5db)", fontSize: 13, outline: "none" },
2406
2459
  placeholder: t("tempPinLabelPh"),
2407
2460
  value: tempPinForm.label,
@@ -2410,120 +2463,120 @@ function PocketSettingsTab({ rpcCall, t }) {
2410
2463
  if (e.key === "Enter") createTempPinNow();
2411
2464
  }
2412
2465
  }),
2413
- (0, import_react2.createElement)("button", { style: { ...styles.btn, height: 30 }, onClick: createTempPinNow, disabled: tempPinBusy }, t("tempPinCreate"))
2466
+ (0, import_react3.createElement)("button", { style: { ...styles.btn, height: 30 }, onClick: createTempPinNow, disabled: tempPinBusy }, t("tempPinCreate"))
2414
2467
  ),
2415
2468
  // 错误展示
2416
- tempPinError ? (0, import_react2.createElement)("div", { style: { color: "var(--dsw-alias-state-error-primary,#dc2626)", fontSize: 12, marginTop: 6 } }, errText(tempPinError)) : null,
2469
+ tempPinError ? (0, import_react3.createElement)("div", { style: { color: "var(--dsw-alias-state-error-primary,#dc2626)", fontSize: 12, marginTop: 6 } }, errText(tempPinError)) : null,
2417
2470
  // 刚生成的那一个(高亮,下一秒会消失提示用户立刻复制)
2418
- tempPinLast ? (0, import_react2.createElement)(
2471
+ tempPinLast ? (0, import_react3.createElement)(
2419
2472
  "div",
2420
2473
  { style: { marginTop: 10, padding: "8px 10px", borderRadius: 8, background: "var(--dsw-alias-bg-layer-2,#f3f4f6)", display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8, flexWrap: "wrap" } },
2421
- (0, import_react2.createElement)(
2474
+ (0, import_react3.createElement)(
2422
2475
  "div",
2423
2476
  null,
2424
- (0, import_react2.createElement)("div", { style: { fontSize: 11, color: "var(--dsw-alias-state-success-primary,#16a34a)", fontWeight: 600, marginBottom: 2 } }, `${t("tempPinCreated")} \xB7 ${tempPinLast.kind === "public" ? t("tempPinKindPublic") : t("tempPinKindLan")} \xB7 ${formatRemaining(tempPinLast.expiresAt)}`),
2425
- (0, import_react2.createElement)("span", { style: { fontFamily: "ui-monospace,Menlo,monospace", fontSize: 16, letterSpacing: 2, fontWeight: 600 } }, tempPinLast.value)
2477
+ (0, import_react3.createElement)("div", { style: { fontSize: 11, color: "var(--dsw-alias-state-success-primary,#16a34a)", fontWeight: 600, marginBottom: 2 } }, `${t("tempPinCreated")} \xB7 ${tempPinLast.kind === "public" ? t("tempPinKindPublic") : t("tempPinKindLan")} \xB7 ${formatRemaining(tempPinLast.expiresAt)}`),
2478
+ (0, import_react3.createElement)("span", { style: { fontFamily: "ui-monospace,Menlo,monospace", fontSize: 16, letterSpacing: 2, fontWeight: 600 } }, tempPinLast.value)
2426
2479
  ),
2427
- (0, import_react2.createElement)("button", { style: { ...styles.btn, height: 28, padding: "0 12px", fontSize: 12 }, onClick: () => copyTempPin(tempPinLast.value) }, t("tempPinCopy"))
2480
+ (0, import_react3.createElement)("button", { style: { ...styles.btn, height: 28, padding: "0 12px", fontSize: 12 }, onClick: () => copyTempPin(tempPinLast.value) }, t("tempPinCopy"))
2428
2481
  ) : null,
2429
2482
  // 列表
2430
- (0, import_react2.createElement)(
2483
+ (0, import_react3.createElement)(
2431
2484
  "div",
2432
2485
  { style: { marginTop: 12, borderTop: "1px solid var(--dsw-alias-border-l2,#e5e7eb)", paddingTop: 8 } },
2433
- tempPinsList.length === 0 ? (0, import_react2.createElement)("div", { style: { ...styles.muted, padding: "6px 0" } }, t("tempPinEmpty")) : tempPinsList.map((p) => (0, import_react2.createElement)(
2486
+ tempPinsList.length === 0 ? (0, import_react3.createElement)("div", { style: { ...styles.muted, padding: "6px 0" } }, t("tempPinEmpty")) : tempPinsList.map((p) => (0, import_react3.createElement)(
2434
2487
  "div",
2435
2488
  { key: p.value, style: { display: "flex", alignItems: "center", gap: 8, padding: "6px 0", borderBottom: "1px solid var(--dsw-alias-border-l2,#e5e7eb)" } },
2436
- (0, import_react2.createElement)("span", { style: { fontSize: 11, color: "var(--dsw-alias-label-tertiary,#8b93a1)", minWidth: 50 } }, p.kind === "public" ? t("tempPinKindPublic") : t("tempPinKindLan")),
2437
- (0, import_react2.createElement)("span", { style: { fontFamily: "ui-monospace,Menlo,monospace", fontSize: 13, letterSpacing: 1, flexShrink: 0 } }, p.value),
2438
- (0, import_react2.createElement)("span", { style: { ...styles.muted, flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, p.label || "\u2014"),
2439
- (0, import_react2.createElement)("span", { style: { fontSize: 11, color: "var(--dsw-alias-label-tertiary,#8b93a1)" } }, `${t("tempPinExpiresIn")} ${formatRemaining(p.expiresAt)}`),
2440
- (0, import_react2.createElement)("button", { style: { ...styles.btn, height: 24, padding: "0 10px", fontSize: 11 }, onClick: () => revokeTempPinNow(p) }, t("tempPinRevoke"))
2489
+ (0, import_react3.createElement)("span", { style: { fontSize: 11, color: "var(--dsw-alias-label-tertiary,#8b93a1)", minWidth: 50 } }, p.kind === "public" ? t("tempPinKindPublic") : t("tempPinKindLan")),
2490
+ (0, import_react3.createElement)("span", { style: { fontFamily: "ui-monospace,Menlo,monospace", fontSize: 13, letterSpacing: 1, flexShrink: 0 } }, p.value),
2491
+ (0, import_react3.createElement)("span", { style: { ...styles.muted, flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, p.label || "\u2014"),
2492
+ (0, import_react3.createElement)("span", { style: { fontSize: 11, color: "var(--dsw-alias-label-tertiary,#8b93a1)" } }, `${t("tempPinExpiresIn")} ${formatRemaining(p.expiresAt)}`),
2493
+ (0, import_react3.createElement)("button", { style: { ...styles.btn, height: 24, padding: "0 10px", fontSize: 11 }, onClick: () => revokeTempPinNow(p) }, t("tempPinRevoke"))
2441
2494
  ))
2442
2495
  )
2443
2496
  ),
2444
2497
  // 恢复出厂设置:设置出问题时的临时兜底(最底部,避免误触)
2445
- (0, import_react2.createElement)(
2498
+ (0, import_react3.createElement)(
2446
2499
  "div",
2447
2500
  { style: styles.block },
2448
- (0, import_react2.createElement)(
2501
+ (0, import_react3.createElement)(
2449
2502
  "div",
2450
2503
  { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 } },
2451
- (0, import_react2.createElement)("span", { style: { fontWeight: 600, fontSize: 13 } }, t("resetFactory")),
2452
- (0, import_react2.createElement)("button", { style: { ...styles.btn, height: 28, padding: "0 12px", fontSize: 12, color: "var(--dsw-alias-state-error-primary,#dc2626)" }, onClick: () => setResetOpen(true) }, t("resetGo"))
2504
+ (0, import_react3.createElement)("span", { style: { fontWeight: 600, fontSize: 13 } }, t("resetFactory")),
2505
+ (0, import_react3.createElement)("button", { style: { ...styles.btn, height: 28, padding: "0 12px", fontSize: 12, color: "var(--dsw-alias-state-error-primary,#dc2626)" }, onClick: () => setResetOpen(true) }, t("resetGo"))
2453
2506
  ),
2454
- (0, import_react2.createElement)("div", { style: { ...styles.muted, marginTop: 6 } }, t("resetIntro"))
2507
+ (0, import_react3.createElement)("div", { style: { ...styles.muted, marginTop: 6 } }, t("resetIntro"))
2455
2508
  ),
2456
2509
  // 恢复出厂设置确认弹框
2457
- resetOpen ? (0, import_react2.createElement)(
2510
+ resetOpen ? (0, import_react3.createElement)(
2458
2511
  "div",
2459
2512
  { style: { position: "fixed", inset: 0, zIndex: 1e4, background: "rgba(0,0,0,.5)", display: "flex", alignItems: "center", justifyContent: "center", padding: 20 } },
2460
- (0, import_react2.createElement)(
2513
+ (0, import_react3.createElement)(
2461
2514
  "div",
2462
2515
  { style: { background: "var(--dsw-alias-bg-layer-1,#fff)", borderRadius: 12, maxWidth: 440, width: "100%", padding: "20px 22px", boxShadow: "0 8px 32px rgba(0,0,0,.18)" } },
2463
- (0, import_react2.createElement)("div", { style: { fontWeight: 600, fontSize: 15, color: "var(--dsw-alias-state-warn-primary,#b45309)", marginBottom: 10 } }, t("resetTitle")),
2464
- (0, import_react2.createElement)("div", { style: { fontSize: 13, lineHeight: 1.7, color: "var(--dsw-alias-label-primary,inherit)", whiteSpace: "pre-line" } }, t("resetBody")),
2465
- (0, import_react2.createElement)(
2516
+ (0, import_react3.createElement)("div", { style: { fontWeight: 600, fontSize: 15, color: "var(--dsw-alias-state-warn-primary,#b45309)", marginBottom: 10 } }, t("resetTitle")),
2517
+ (0, import_react3.createElement)("div", { style: { fontSize: 13, lineHeight: 1.7, color: "var(--dsw-alias-label-primary,inherit)", whiteSpace: "pre-line" } }, t("resetBody")),
2518
+ (0, import_react3.createElement)(
2466
2519
  "div",
2467
2520
  { style: { display: "flex", gap: 8, marginTop: 16 } },
2468
- (0, import_react2.createElement)("button", { style: { ...styles.btn, flex: 1 }, onClick: () => setResetOpen(false) }, t("cancel")),
2469
- (0, import_react2.createElement)("button", { style: { ...styles.primary, flex: 1, background: "var(--dsw-alias-state-error-primary,#dc2626)" }, onClick: doFactoryReset }, t("resetConfirm"))
2521
+ (0, import_react3.createElement)("button", { style: { ...styles.btn, flex: 1 }, onClick: () => setResetOpen(false) }, t("cancel")),
2522
+ (0, import_react3.createElement)("button", { style: { ...styles.primary, flex: 1, background: "var(--dsw-alias-state-error-primary,#dc2626)" }, onClick: doFactoryReset }, t("resetConfirm"))
2470
2523
  )
2471
2524
  )
2472
2525
  ) : null,
2473
2526
  // Toast:重置等操作的即时反馈(固定屏幕正中央,2.6s 自动消失)
2474
- toast ? (0, import_react2.createElement)("div", {
2527
+ toast ? (0, import_react3.createElement)("div", {
2475
2528
  style: { position: "fixed", left: "50%", top: "50%", transform: "translate(-50%, -50%)", zIndex: 10001, width: "auto", maxWidth: 280, background: "rgba(17,24,39,.92)", color: "#fff", border: "none", borderRadius: 10, padding: "10px 16px", fontSize: 13, lineHeight: 1.5, textAlign: "center", boxShadow: "0 8px 24px rgba(0,0,0,.22)" }
2476
2529
  }, toast) : null,
2477
2530
  // 局域网访问开关确认弹框(关闭/打开时弹窗提醒)
2478
- lanToggleOpen !== null ? (0, import_react2.createElement)(
2531
+ lanToggleOpen !== null ? (0, import_react3.createElement)(
2479
2532
  "div",
2480
2533
  { style: { position: "fixed", inset: 0, zIndex: 1e4, background: "rgba(0,0,0,.5)", display: "flex", alignItems: "center", justifyContent: "center", padding: 20 } },
2481
- (0, import_react2.createElement)(
2534
+ (0, import_react3.createElement)(
2482
2535
  "div",
2483
2536
  { style: { background: "var(--dsw-alias-bg-layer-1,#fff)", borderRadius: 12, maxWidth: 420, width: "100%", padding: "20px 22px", boxShadow: "0 8px 32px rgba(0,0,0,.18)" } },
2484
- (0, import_react2.createElement)("div", { style: { fontWeight: 600, fontSize: 15, color: lanToggleOpen ? "var(--dsw-alias-brand-primary,#4f6ef7)" : "var(--dsw-alias-state-warn-primary,#b45309)", marginBottom: 10 } }, t(lanToggleOpen ? "lanToggleTitleOn" : "lanToggleTitleOff")),
2485
- (0, import_react2.createElement)("div", { style: { fontSize: 13, lineHeight: 1.7, color: "var(--dsw-alias-label-primary,inherit)" } }, t(lanToggleOpen ? "lanToggleBodyOn" : "lanToggleBodyOff")),
2486
- (0, import_react2.createElement)(
2537
+ (0, import_react3.createElement)("div", { style: { fontWeight: 600, fontSize: 15, color: lanToggleOpen ? "var(--dsw-alias-brand-primary,#4f6ef7)" : "var(--dsw-alias-state-warn-primary,#b45309)", marginBottom: 10 } }, t(lanToggleOpen ? "lanToggleTitleOn" : "lanToggleTitleOff")),
2538
+ (0, import_react3.createElement)("div", { style: { fontSize: 13, lineHeight: 1.7, color: "var(--dsw-alias-label-primary,inherit)" } }, t(lanToggleOpen ? "lanToggleBodyOn" : "lanToggleBodyOff")),
2539
+ (0, import_react3.createElement)(
2487
2540
  "div",
2488
2541
  { style: { display: "flex", gap: 8, marginTop: 16 } },
2489
- (0, import_react2.createElement)("button", { style: { ...styles.btn, flex: 1 }, onClick: () => setLanToggleOpen(null) }, t("cancel")),
2490
- (0, import_react2.createElement)("button", { style: { ...styles.primary, flex: 1 }, onClick: confirmLanToggle }, t("confirm"))
2542
+ (0, import_react3.createElement)("button", { style: { ...styles.btn, flex: 1 }, onClick: () => setLanToggleOpen(null) }, t("cancel")),
2543
+ (0, import_react3.createElement)("button", { style: { ...styles.primary, flex: 1 }, onClick: confirmLanToggle }, t("confirm"))
2491
2544
  )
2492
2545
  )
2493
2546
  ) : null,
2494
2547
  // 安全免责声明弹框(issue #31):每次开启公网访问前确认
2495
- disclaimerOpen ? (0, import_react2.createElement)(
2548
+ disclaimerOpen ? (0, import_react3.createElement)(
2496
2549
  "div",
2497
2550
  { style: { position: "fixed", inset: 0, zIndex: 1e4, background: "rgba(0,0,0,.5)", display: "flex", alignItems: "center", justifyContent: "center", padding: 20 } },
2498
- (0, import_react2.createElement)(
2551
+ (0, import_react3.createElement)(
2499
2552
  "div",
2500
2553
  { style: { background: "var(--dsw-alias-bg-layer-1,#fff)", borderRadius: 12, maxWidth: 420, width: "100%", padding: "20px 22px", boxShadow: "0 8px 32px rgba(0,0,0,.18)" } },
2501
- (0, import_react2.createElement)("div", { style: { fontWeight: 600, fontSize: 15, color: "var(--dsw-alias-state-warn-primary,#b45309)", marginBottom: 10 } }, t("disclaimerTitle")),
2502
- (0, import_react2.createElement)("div", { style: { fontSize: 13, lineHeight: 1.7, color: "var(--dsw-alias-label-primary,inherit)" } }, t("disclaimerBody")),
2503
- (0, import_react2.createElement)(
2554
+ (0, import_react3.createElement)("div", { style: { fontWeight: 600, fontSize: 15, color: "var(--dsw-alias-state-warn-primary,#b45309)", marginBottom: 10 } }, t("disclaimerTitle")),
2555
+ (0, import_react3.createElement)("div", { style: { fontSize: 13, lineHeight: 1.7, color: "var(--dsw-alias-label-primary,inherit)" } }, t("disclaimerBody")),
2556
+ (0, import_react3.createElement)(
2504
2557
  "label",
2505
2558
  { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 14, fontSize: 13, cursor: "pointer" } },
2506
- (0, import_react2.createElement)("input", { type: "checkbox", checked: disclaimerChecked, onChange: (e) => setDisclaimerChecked(e.target.checked), style: { width: 16, height: 16 } }),
2559
+ (0, import_react3.createElement)("input", { type: "checkbox", checked: disclaimerChecked, onChange: (e) => setDisclaimerChecked(e.target.checked), style: { width: 16, height: 16 } }),
2507
2560
  t("disclaimerAgree")
2508
2561
  ),
2509
- (0, import_react2.createElement)(
2562
+ (0, import_react3.createElement)(
2510
2563
  "div",
2511
2564
  { style: { display: "flex", gap: 8, marginTop: 16 } },
2512
- (0, import_react2.createElement)("button", { style: { ...styles.btn, flex: 1 }, onClick: () => setDisclaimerOpen(false) }, t("cancel")),
2513
- (0, import_react2.createElement)("button", {
2565
+ (0, import_react3.createElement)("button", { style: { ...styles.btn, flex: 1 }, onClick: () => setDisclaimerOpen(false) }, t("cancel")),
2566
+ (0, import_react3.createElement)("button", {
2514
2567
  style: { ...styles.primary, flex: 1, opacity: disclaimerChecked ? 1 : 0.5 },
2515
2568
  disabled: !disclaimerChecked,
2516
2569
  onClick: confirmDisclaimer
2517
2570
  }, t("disclaimerAgree"))
2518
2571
  ),
2519
- !disclaimerChecked ? (0, import_react2.createElement)("div", { style: { marginTop: 8, fontSize: 12, color: "var(--dsw-alias-state-error-primary,#dc2626)" } }, t("disclaimerHint")) : null
2572
+ !disclaimerChecked ? (0, import_react3.createElement)("div", { style: { marginTop: 8, fontSize: 12, color: "var(--dsw-alias-state-error-primary,#dc2626)" } }, t("disclaimerHint")) : null
2520
2573
  )
2521
2574
  ) : null,
2522
2575
  // 页面最底部:反馈入口
2523
- (0, import_react2.createElement)(
2576
+ (0, import_react3.createElement)(
2524
2577
  "div",
2525
2578
  { style: { ...styles.block, textAlign: "center" } },
2526
- (0, import_react2.createElement)(
2579
+ (0, import_react3.createElement)(
2527
2580
  "a",
2528
2581
  { href: "https://github.com/shaobeichen/dsh-pocket/issues", target: "_blank", rel: "noreferrer", style: { fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)", textDecoration: "none" } },
2529
2582
  t("feedback")
@@ -3,6 +3,7 @@ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
3
3
  import { MobileNavToggle } from './MobileNavToggle.tsx'
4
4
  import { MobileNavOverlay } from './MobileNavOverlay.tsx'
5
5
  import { MobileDrawerFooter } from './MobileDrawerFooter.tsx'
6
+ import { MobileComposerFullscreen } from './MobileComposerFullscreen.tsx'
6
7
  import { MOBILE_CSS } from './mobile.css.ts'
7
8
  import { NS, en, zh } from './locales.ts'
8
9
  import type { MobileNavKey } from './locales.ts'
package/lib/tunnel.mjs CHANGED
@@ -23,16 +23,41 @@ import { createWriteStream } from 'node:fs';
23
23
  export const QUICK_TUNNEL_URL_RE = /https:\/\/(?!api\.)[a-z0-9-]+\.trycloudflare\.com/i;
24
24
 
25
25
  function platformBinary() {
26
- const archMap = { x64: 'amd64', arm64: 'arm64' };
26
+ const archMap = { x64: 'amd64', arm64: 'arm64', ia32: '386', arm: 'arm' };
27
27
  const a = archMap[process.arch] ?? process.arch;
28
28
  const os = process.platform === 'darwin' ? 'darwin' : process.platform === 'win32' ? 'windows' : 'linux';
29
29
  return { os, a, ext: os === 'windows' ? '.exe' : '' };
30
30
  }
31
31
 
32
+ /**
33
+ * 候选发布资产名,按优先级排列(issue #45)。
34
+ *
35
+ * cloudflared 现在的发布布局是**按平台分**:
36
+ * - linux:只有裸二进制 `cloudflared-linux-amd64`(没有 .tgz)
37
+ * - darwin:只有 `cloudflared-darwin-<arch>.tgz`
38
+ * - windows:只有 `cloudflared-windows-<arch>.exe`
39
+ *
40
+ * 我们以前给 linux 拼的是 `cloudflared-linux-amd64.tgz`,而它**根本不存在**
41
+ * (GitHub 返回 404),五个镜像全指向同一个 404 地址,于是必然"所有源都不通"
42
+ * —— 表现就是 Linux 服务器上开了公网访问一直报"无法安装",而 macOS/Windows
43
+ * 一切正常。Linux 下裸二进制还有个额外好处:不用解压,也就不再依赖系统装了
44
+ * `tar`(Alpine / slim 镜像 / 容器里常常没有)。
45
+ *
46
+ * linux 仍把 .tgz 留在候选里作为回退,万一上游改回打包方式也不会直接躺平。
47
+ *
48
+ * @returns {string[]} 资产名列表,越靠前越优先。
49
+ */
50
+ export function platformAssets() {
51
+ const { os, a } = platformBinary();
52
+ if (os === 'windows') return [`cloudflared-windows-${a}.exe`];
53
+ if (os === 'darwin') return [`cloudflared-darwin-${a}.tgz`];
54
+ return [`cloudflared-linux-${a}`, `cloudflared-linux-${a}.tgz`];
55
+ }
56
+
32
57
  /**
33
58
  * cloudflared 下载源。
34
- * 优先:清华 TUNA 镜像的 Homebrew bottle(国内 CDN,实测 ~3MB/s)——仅 macOS/Linux
35
- * 且有对应 bottle 时可用(Windows Homebrew,自动跳过)。
59
+ * 平台策略:macOS 优先清华 TUNA 镜像的 Homebrew bottle(国内 CDN,实测 ~3MB/s),
60
+ * 拿不到再回退;Linux / Windows 直接走官方源优先(见 downloadCloudflared 里的说明)。
36
61
  * 兜底:官方 GitHub + 国内加速源(ghproxy.net / gh.ddlc.top / gh-proxy.com,2026-08
37
62
  * 实测可达)。npmmirror(淘宝)没有 cloudflared 镜像(已实测 404)。
38
63
  */
@@ -157,7 +182,7 @@ export async function downloadFile(url, dest, { signal, segments = PARALLEL_SEGM
157
182
  * 清华 TUNA 镜像的 cloudflared Homebrew bottle URL(国内 CDN,实测 ~3MB/s)。
158
183
  * **仅 macOS**——Linux 的 Homebrew bottle 其 ELF 解释器是 `@@HOMEBREW_PREFIX@@`
159
184
  * 占位符(需 brew install 时 patchelf 替换),没装 Homebrew 的机器直接 spawn 会
160
- * ENOENT(issue #22);Linux 走官方 GitHub tgz(解压即用)+ 加速源。
185
+ * ENOENT(issue #22);Linux 走官方 GitHub 裸二进制(无需解压)+ 加速源。
161
186
  * 匹配按 CPU 架构取清华目录里版本号最新的 bottle——Homebrew 构建时部署目标
162
187
  * 设得较老、向后兼容,所以旧系统(如 Ventura)也能用新一点的 bottle。
163
188
  * 抓目录失败/无匹配 → null(调用方回退 GitHub/加速源,不影响可用性)。
@@ -189,51 +214,65 @@ async function downloadCloudflared(binPath, signal) {
189
214
  const dir = dirname(binPath);
190
215
  const tmpFile = join(dir, `cloudflared.download`);
191
216
  const isWindows = os === 'windows';
192
- // 发布资产:Windows 是 .exe(下载即二进制),macOS/Linux 是 .tgz(需解压)
193
- const asset = isWindows ? `cloudflared-windows-${a}.exe` : `cloudflared-${os}-${a}.tgz`;
194
217
  const fetchSignal = signal
195
218
  ? AbortSignal.any([signal, AbortSignal.timeout(120_000)])
196
219
  : AbortSignal.timeout(120_000);
197
220
 
198
- // 构建有序源列表:[{url, host}];清华(如有,仅 macOS/Linux)排第一,再官方 + 加速源
199
- const sources = [];
200
- if (!isWindows) {
201
- const tua = await tsinghuaBottleUrl({ os, a }).catch(() => null);
202
- if (tua) sources.push({ url: tua, host: 'mirrors.tuna.tsinghua.edu.cn' });
203
- }
204
- for (const m of CLOUDFLARED_MIRRORS) sources.push({ url: m(asset), host: hostOf(m(asset)) });
221
+ // 源的顺序按平台定(issue #45):
222
+ // - macOS:清华 Homebrew bottle 排第一(国内 CDN 实测 ~3MB/s,给家用网络省几分钟),
223
+ // 拿不到再退回官方源 + 国内加速源。
224
+ // - Linux / Windows:不走 bottle,直接官方源优先。这两类环境网络差异太大
225
+ // (服务器、容器、 corporate 网络),多一层镜像就多一层失败模式;官方源
226
+ // 拿不到时,再用国内加速源兜底。
227
+ // bottle 与资产名无关,只算一次。
228
+ const bottle = os === 'darwin' ? await tsinghuaBottleUrl({ os, a }).catch(() => null) : null;
205
229
 
230
+ // 逐个资产试(linux 首选裸二进制,.tgz 只是回退),每个资产再逐个源试
231
+ const assets = platformAssets();
206
232
  let lastErr = null;
207
- for (let i = 0; i < sources.length; i++) {
208
- const { url, host } = sources[i];
209
- console.log(`⬇️ 下载 cloudflared(${i + 1}/${sources.length}:${host})…`);
210
- try {
211
- // 多线程分块(官方 GitHub 支持 Range,Windows 50MB 从几分钟降到几十秒);
212
- // 不支持 Range 的源自动回退单线程
213
- await downloadFile(url, tmpFile, { signal: fetchSignal });
214
- // 简单校验:空文件/极小文件视为下载失败(可能是镜像返回了错误页)
215
- const st = await stat(tmpFile);
216
- if (st.size < 1024 * 1024) throw new Error(`文件异常小(${st.size} 字节),疑似镜像错误页`);
217
- lastErr = null;
218
- break; // 下载成功
219
- } catch (err) {
220
- lastErr = err;
221
- await rm(tmpFile, { force: true }).catch(() => {}); // 清掉半截文件
222
- console.warn(` ⚠️ ${i + 1} 失败:${err?.message ?? err},尝试下一个…`);
233
+ let usedAsset = null;
234
+
235
+ for (let ai = 0; ai < assets.length && usedAsset === null; ai++) {
236
+ const asset = assets[ai];
237
+ const sources = [];
238
+ if (bottle && asset.endsWith('.tgz')) {
239
+ sources.push({ url: bottle, host: 'mirrors.tuna.tsinghua.edu.cn' });
240
+ }
241
+ for (const m of CLOUDFLARED_MIRRORS) sources.push({ url: m(asset), host: hostOf(m(asset)) });
242
+
243
+ for (let i = 0; i < sources.length; i++) {
244
+ const { url, host } = sources[i];
245
+ console.log(`⬇️ 下载 cloudflared(${asset},源 ${i + 1}/${sources.length}:${host})…`);
246
+ try {
247
+ // 多线程分块(官方 GitHub 支持 Range,Windows 50MB 从几分钟降到几十秒);
248
+ // 不支持 Range 的源自动回退单线程
249
+ await downloadFile(url, tmpFile, { signal: fetchSignal });
250
+ // 简单校验:空文件/极小文件视为下载失败(可能是镜像返回了错误页)
251
+ const st = await stat(tmpFile);
252
+ if (st.size < 1024 * 1024) throw new Error(`文件异常小(${st.size} 字节),疑似镜像错误页`);
253
+ usedAsset = asset;
254
+ lastErr = null;
255
+ break; // 下载成功
256
+ } catch (err) {
257
+ lastErr = err;
258
+ await rm(tmpFile, { force: true }).catch(() => {}); // 清掉半截文件
259
+ console.warn(` ⚠️ 源 ${i + 1} 失败:${err?.message ?? err},尝试下一个…`);
260
+ }
223
261
  }
224
262
  }
225
- if (lastErr) {
263
+ if (usedAsset === null) {
226
264
  throw new Error(
227
265
  `cloudflared 下载失败:所有源都不通(最后错误:${lastErr?.message ?? lastErr})。`
228
266
  + (isWindows
229
- ? `Windows 可手动安装后重试:winget install cloudflared;或下载 ${asset} 放到 ${dir} 目录 | download failed — try: winget install cloudflared, or put the exe into ${dir}`
230
- : `可手动安装后重试:npm i -g cloudflared(装好命令行 cloudflared 即可,无需下载);或开启代理/换网络后重试 | all mirrors failed — install cloudflared manually: npm i -g cloudflared, then retry`),
267
+ ? `Windows 可手动安装后重试:winget install cloudflared;或下载 ${assets[0]} 放到 ${dir} 目录 | download failed — try: winget install cloudflared, or put the exe into ${dir}`
268
+ : `也可以自己装好后在 settings.json "cloudflaredPath": "/path/to/cloudflared" 跳过下载(或用环境变量 DSH_POCKET_CLOUDFLARED);或用包管理器安装:apt/dnf install cloudflared | all mirrors failed — install cloudflared manually and set "cloudflaredPath" in settings.json, or: apt/dnf install cloudflared`),
231
269
  );
232
270
  }
233
271
 
234
272
  let extracted = join(dir, `cloudflared${ext}`);
235
- if (isWindows) {
236
- // Windows:exe 直接就是二进制,无需解压
273
+ // 只有 .tgz 才需要解压:Windows 的 .exe 和 linux 的裸二进制本身就可直接执行
274
+ // (linux 用裸二进制还有个好处——不依赖系统装了 tar,见 platformAssets 的说明)
275
+ if (!usedAsset.endsWith('.tgz')) {
237
276
  await rename(tmpFile, extracted).catch(async () => {
238
277
  await cp(tmpFile, extracted).catch(() => {});
239
278
  });
@@ -246,7 +285,13 @@ async function downloadCloudflared(binPath, signal) {
246
285
  await new Promise((resolve, reject) => {
247
286
  const child = spawn('tar', ['-xzf', tmpFile, '-C', extractDir], { stdio: 'ignore' });
248
287
  child.once('exit', (code) => code === 0 ? resolve() : reject(new Error(`cloudflared 解压失败(code=${code})`)));
249
- child.once('error', reject);
288
+ // spawn 失败(Alpine / slim 镜像 / 容器里常见:根本没有 tar)时 err.code
289
+ // ENOENT,直接抛出来只有一句 "spawn tar ENOENT",看不出该怎么办
290
+ child.once('error', (err) => reject(
291
+ err?.code === 'ENOENT'
292
+ ? new Error(`系统里没有 tar 命令,无法解压 ${usedAsset} —— 可改用 linux 裸二进制(默认已如此)或手动安装后设置 cloudflaredPath | no tar on this system`)
293
+ : err,
294
+ ));
250
295
  });
251
296
  // 找真实的二进制**文件**(排除目录):
252
297
  // - GitHub tgz:extractDir/cloudflared
package/package.json CHANGED
@@ -80,5 +80,5 @@
80
80
  "access": "public",
81
81
  "registry": "https://registry.npmjs.org/"
82
82
  },
83
- "version": "2.6.0"
83
+ "version": "2.6.2"
84
84
  }