dsh-pocket 2.6.3 → 2.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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_react3 = require("react");
40
+ var import_react2 = require("react");
41
41
 
42
42
  // client/api.js
43
43
  var POCKET_RPC_CHANNEL = "/dsh-pocket";
@@ -337,57 +337,100 @@ function MobileDrawerFooter({ useSessions, downloadSessionLog, toggleSidebar, t
337
337
  ));
338
338
  }
339
339
 
340
- // client/mobile/MobileComposerFullscreen.tsx
341
- var import_react2 = require("react");
342
- var FULLSCREEN_ATTR = "data-dsh-pocket-composer-fullscreen";
343
- function isFullscreen() {
344
- return typeof document !== "undefined" && document.body?.getAttribute(FULLSCREEN_ATTR) === "1";
340
+ // client/mobile/fileGuard.ts
341
+ var GUARD_MSG = "\u624B\u673A\u4E0A\u65E0\u6CD5\u76F4\u63A5\u6253\u5F00\u7535\u8111\u4E0A\u7684\u6587\u4EF6";
342
+ var WS_LABELS = ["\u6DFB\u52A0\u5DE5\u4F5C\u533A", "\u6DFB\u52A0\u5DE5\u4F5C\u533A\u2026", "Add workspace", "Add workspace\u2026"];
343
+ function looksLikeFilePath(text) {
344
+ const t = (text ?? "").trim();
345
+ if (t.length < 3 || t.length > 320) return false;
346
+ if (/^(\/|~\/|\.\.?\/|[A-Za-z]:\\)/.test(t)) return true;
347
+ if (/\/[\w.\-]+\.\w{1,12}$/.test(t)) return true;
348
+ if (/[\w.\-]+\/[\w.\-]+\.\w{1,12}/.test(t)) return true;
349
+ return false;
345
350
  }
346
- function setFullscreen(on) {
347
- if (typeof document === "undefined") return;
348
- if (on) document.body.setAttribute(FULLSCREEN_ATTR, "1");
349
- else document.body.removeAttribute(FULLSCREEN_ATTR);
351
+ function isInsidePocket(el) {
352
+ return el !== null && el.closest('[data-mobile-nav="frame"]') !== null;
350
353
  }
351
- function MobileComposerFullscreen() {
352
- const [on, setOn] = (0, import_react2.useState)(isFullscreen);
353
- (0, import_react2.useEffect)(() => {
354
- const update = () => setOn(isFullscreen());
355
- const observer = new MutationObserver(update);
356
- observer.observe(document.body, { attributes: true, attributeFilter: [FULLSCREEN_ATTR] });
357
- return () => observer.disconnect();
358
- }, []);
359
- return (0, import_react2.createElement)("button", {
360
- type: "button",
361
- "aria-label": on ? "\u6536\u8D77\u8F93\u5165" : "\u653E\u5927\u8F93\u5165",
362
- "aria-pressed": on,
363
- title: on ? "\u6536\u8D77\u8F93\u5165" : "\u653E\u5927\u8F93\u5165",
364
- "data-mobile-nav": "composer-fullscreen",
365
- "data-mobile-nav-keep": "1",
366
- // 自己的按钮不该被 #23 的 slot 通用隐藏规则误伤
367
- onClick: () => {
368
- const next = !isFullscreen();
369
- setFullscreen(next);
370
- setOn(next);
371
- },
372
- style: {
373
- appearance: "none",
374
- WebkitAppearance: "none",
375
- border: "none",
376
- background: "transparent",
377
- color: "inherit",
378
- cursor: "pointer",
379
- padding: "0 8px",
380
- height: 32,
381
- minWidth: 32,
382
- borderRadius: 6,
383
- display: "inline-flex",
384
- alignItems: "center",
385
- justifyContent: "center",
386
- font: "inherit",
387
- fontSize: 16,
388
- lineHeight: 1
354
+ function startFileGuard() {
355
+ let toastEl = null;
356
+ let toastTimer = null;
357
+ const showToast = (text) => {
358
+ if (toastEl === null) {
359
+ toastEl = document.createElement("div");
360
+ toastEl.setAttribute("data-mobile-nav", "file-guard-toast");
361
+ Object.assign(toastEl.style, {
362
+ position: "fixed",
363
+ left: "50%",
364
+ bottom: "64px",
365
+ transform: "translateX(-50%)",
366
+ maxWidth: "84vw",
367
+ zIndex: "9999",
368
+ padding: "10px 14px",
369
+ borderRadius: "10px",
370
+ background: "rgba(20,22,28,.92)",
371
+ color: "#fff",
372
+ fontSize: "13px",
373
+ lineHeight: "1.4",
374
+ textAlign: "center",
375
+ fontFamily: "inherit",
376
+ boxShadow: "0 4px 16px rgba(0,0,0,.28)",
377
+ pointerEvents: "none",
378
+ opacity: "0",
379
+ transition: "opacity .18s ease"
380
+ });
381
+ document.body.appendChild(toastEl);
389
382
  }
390
- }, on ? "\u2921" : "\u2922");
383
+ toastEl.textContent = text;
384
+ requestAnimationFrame(() => {
385
+ if (toastEl !== null) toastEl.style.opacity = "1";
386
+ });
387
+ if (toastTimer !== null) window.clearTimeout(toastTimer);
388
+ toastTimer = window.setTimeout(() => {
389
+ if (toastEl !== null) toastEl.style.opacity = "0";
390
+ }, 2600);
391
+ };
392
+ const onClick = (event) => {
393
+ const target = event.target;
394
+ if (target === null || isInsidePocket(target)) return;
395
+ const el = target.closest("button, a");
396
+ if (el === null) return;
397
+ if (!looksLikeFilePath(el.textContent)) return;
398
+ event.preventDefault();
399
+ event.stopImmediatePropagation();
400
+ showToast(GUARD_MSG);
401
+ };
402
+ document.addEventListener("click", onClick, true);
403
+ const hideWsEntries = () => {
404
+ const checkOne = (node) => {
405
+ if (node.nodeType !== 1) return;
406
+ const el = node;
407
+ const txt = (el.getAttribute("aria-label") ?? el.textContent ?? "").trim();
408
+ if (WS_LABELS.includes(txt)) {
409
+ el.style.display = "none";
410
+ el.setAttribute("data-mobile-nav-hide", "add-workspace");
411
+ }
412
+ };
413
+ const sel = '[role="menuitem"],[role="option"],li,button,a';
414
+ document.querySelectorAll(sel).forEach(checkOne);
415
+ const observer = new MutationObserver((mutations) => {
416
+ for (const m of mutations) {
417
+ m.addedNodes.forEach((n) => {
418
+ if (n.nodeType !== 1) return;
419
+ checkOne(n);
420
+ n.querySelectorAll?.(sel).forEach(checkOne);
421
+ });
422
+ }
423
+ });
424
+ observer.observe(document.body, { childList: true, subtree: true });
425
+ return () => observer.disconnect();
426
+ };
427
+ const disconnectWs = hideWsEntries();
428
+ return () => {
429
+ document.removeEventListener("click", onClick, true);
430
+ disconnectWs();
431
+ if (toastTimer !== null) window.clearTimeout(toastTimer);
432
+ toastEl?.remove();
433
+ };
391
434
  }
392
435
 
393
436
  // client/mobile/mobile.css.ts
@@ -1250,6 +1293,17 @@ var MOBILE_CSS = `
1250
1293
  [data-phase="hero"] [class$="_stack"] {
1251
1294
  gap: 0 !important;
1252
1295
  }
1296
+
1297
+ /* ---------- \u9690\u85CF\u300C\u6DFB\u52A0\u5DE5\u4F5C\u533A\u300D\u5165\u53E3\uFF08\u624B\u673A\u4E0A\u914D\u5DE5\u4F5C\u533A\u65E0\u610F\u4E49\uFF0Cissue #17 \u4FEE\u6B63\uFF09 ----------
1298
+ \u56FE\u6807\u6309\u94AE\u7684 aria-label \u968F\u8BED\u8A00\u53D8\u5316\uFF08zh\u300C\u6DFB\u52A0\u5DE5\u4F5C\u533A\u300D/ en\u300CAdd workspace\u300D\uFF09\uFF0C
1299
+ \u4E24\u79CD\u90FD\u8986\u76D6\uFF1B\u4E0B\u62C9\u83DC\u5355\u91CC\u7684\u300C\u6DFB\u52A0\u5DE5\u4F5C\u533A\u2026\u300D\u9879\u7531 fileGuard.ts \u7684 MutationObserver
1300
+ \u6309\u6587\u6848\u515C\u5E95\u9690\u85CF\uFF08CSS \u9009\u4E0D\u5230\u7EAF\u6587\u672C\u8282\u70B9\uFF09\u3002\u53EA\u5728\u7A84\u5C4F\u751F\u6548\u2014\u2014\u684C\u9762\u7AEF\u7167\u5E38\u4FDD\u7559\u3002 */
1301
+ button[aria-label="\u6DFB\u52A0\u5DE5\u4F5C\u533A"],
1302
+ button[aria-label="\u6DFB\u52A0\u5DE5\u4F5C\u533A\u2026"],
1303
+ button[aria-label="Add workspace"],
1304
+ button[aria-label="Add workspace\u2026"] {
1305
+ display: none !important;
1306
+ }
1253
1307
  }
1254
1308
 
1255
1309
  /* ---------- desktop: the mobile controls must never appear ---------- */
@@ -1264,52 +1318,8 @@ var MOBILE_CSS = `
1264
1318
  [data-mobile-nav="drawer-actions"] {
1265
1319
  display: none !important;
1266
1320
  }
1267
- /* \u684C\u9762\u7AEF\u6C38\u8FDC\u4E0D\u9700\u8981\u300C\u26F6 \u653E\u5927\u8F93\u5165\u300D\u6309\u94AE\uFF08composer \u5DF2\u7ECF\u6709\u5B8C\u6574\u5DE5\u5177\u884C\uFF09 */
1268
- [data-mobile-nav="composer-fullscreen"] {
1269
- display: none !important;
1270
- }
1271
1321
  }
1272
1322
 
1273
- /* ---------- \u653E\u5927\u8F93\u5165\uFF08issue #23\uFF09 ----------
1274
- \u7A84\u5C4F\u9ED8\u8BA4\u9690\u85CF\u63D2\u4EF6\u6CE8\u518C\u5230 conversation.input.* slot \u7684 UI\uFF08\u4EC5\u7559\u5B98\u65B9 resident
1275
- chrome\uFF09\uFF0C\u70B9\u300C\u26F6\u300D\u6309\u94AE\u628A composer \u5361\u7247\u56FA\u5B9A\u5230\u5168\u5C4F\u540E\u6240\u6709\u63D2\u4EF6 UI \u6062\u590D\u663E\u793A\u2014\u2014
1276
- \u65B0\u63D2\u4EF6\u96F6\u9002\u914D\u3002
1277
- \u5B9E\u73B0\u8BF4\u660E\uFF1A\u5F53\u524D dsh 0.1.1 \u6CA1\u628A input slot \u6E32\u67D3\u6210 [data-slot] \u5305\u88C5\uFF080.1.2+ \u624D\u6709\uFF09\uFF0C
1278
- \u6240\u4EE5\u8FD9\u91CC\u7528\u300C\u767D\u540D\u5355\u300D\u53CD\u9009\uFF1AmobileApply \u6CE8\u518C\u7684 mobile \u81EA\u8EAB\u8282\u70B9\u52A0
1279
- data-mobile-nav="composer-fullscreen" \u4E0D\u88AB\u9690\u85CF\uFF1B\u5176\u5B83\u63D2\u4EF6 UI \u9690\u85CF\u3002 */
1280
- @media (max-width: 1023px) {
1281
- /* \u9ED8\u8BA4\uFF08\u975E\u5168\u5C4F\uFF09\uFF1A\u9690\u85CF input slot \u5BB9\u5668\u4E0B\u7684\u6240\u6709\u76F4\u63A5\u5B50\u5143\u7D20\uFF08\u5305\u542B\u63D2\u4EF6\u548C\u5B98\u65B9\uFF09
1282
- \u2014\u2014\u5B98\u65B9\u81EA\u5DF1\u5982\u679C\u60F3\u5728\u7A84\u5C4F\u4E5F\u9732\u51FA\uFF0C\u6CE8 input slot \u65F6\u52A0 data-mobile-nav-keep="1" */
1283
- [data-dsh-pocket-composer-fullscreen]:not([data-dsh-pocket-composer-fullscreen="1"])
1284
- [class$="_card"]:has(textarea) [data-slot^="conversation.input."] > :not([data-mobile-nav-keep]),
1285
- [data-dsh-pocket-composer-fullscreen]:not([data-dsh-pocket-composer-fullscreen="1"])
1286
- [class$="_card"]:has(textarea) [data-slot^="conversation.input."]:empty {
1287
- display: none !important;
1288
- }
1289
- /* \u653E\u5927\u6309\u94AE\uFF1A\u5E38\u9A7B\u5728 conversation.input.right slot \u65C1\uFF0C\u684C\u9762\u7AEF\u5DF2\u88AB\u4E0A\u9762\u89C4\u5219\u9690\u85CF */
1290
- [data-mobile-nav="composer-fullscreen"] {
1291
- display: inline-flex;
1292
- }
1293
- /* \u5168\u5C4F\u6A21\u5F0F\uFF1Acomposer \u5361\u7247\u56FA\u5B9A\u5230\u89C6\u53E3\uFF0Ctextarea \u62C9\u9AD8\uFF0C\u5DE5\u5177\u884C\u53EF\u6362\u884C */
1294
- [data-dsh-pocket-composer-fullscreen="1"] [class$="_card"]:has(textarea) {
1295
- position: fixed !important;
1296
- inset: 0 !important;
1297
- z-index: 9999 !important;
1298
- border-radius: 0 !important;
1299
- margin: 0 !important;
1300
- height: 100dvh !important;
1301
- display: flex !important;
1302
- flex-direction: column !important;
1303
- }
1304
- [data-dsh-pocket-composer-fullscreen="1"] [class$="_card"]:has(textarea) textarea {
1305
- flex: 1 1 auto !important;
1306
- min-height: 50dvh !important;
1307
- max-height: none !important;
1308
- }
1309
- [data-dsh-pocket-composer-fullscreen="1"] [class$="_card"]:has(textarea) [data-slot^="conversation.input."] {
1310
- flex-wrap: wrap !important;
1311
- }
1312
- }
1313
1323
  `;
1314
1324
 
1315
1325
  // client/mobile/locales.ts
@@ -1525,6 +1535,11 @@ function mobileApply(ctx) {
1525
1535
  observer.disconnect();
1526
1536
  };
1527
1537
  }, "dsh-mobile-nav: sheet rise animation replay");
1538
+ ctx.effect(() => {
1539
+ if (!narrow.matches) return () => {
1540
+ };
1541
+ return startFileGuard();
1542
+ }, "dsh-mobile-nav: file open guard + hide add-workspace (issue #17)");
1528
1543
  ctx.slots.inject("conversation.session.header.actions", () => ctx.slots.register({
1529
1544
  name: "conversation.session.header.actions",
1530
1545
  id: "mobile-nav-toggle",
@@ -1534,12 +1549,6 @@ function mobileApply(ctx) {
1534
1549
  toggleSidebar: () => ctx.layout.toggleSidebar()
1535
1550
  })
1536
1551
  }, MobileNavToggle));
1537
- ctx.slots.inject("conversation.input.right", () => ctx.slots.register({
1538
- name: "conversation.input.right",
1539
- id: "mobile-composer-fullscreen",
1540
- order: 100,
1541
- locale: NS
1542
- }, MobileComposerFullscreen));
1543
1552
  ctx.slots.inject("shell.overlay", () => ctx.slots.register({
1544
1553
  name: "shell.overlay",
1545
1554
  id: "mobile-nav-overlay",
@@ -1779,15 +1788,15 @@ var styles = {
1779
1788
  warn: { color: "var(--dsw-alias-state-warn-primary,#b45309)", fontSize: 12, lineHeight: 1.5 }
1780
1789
  };
1781
1790
  function PocketSettingsTab({ rpcCall, t }) {
1782
- const [status, setStatus] = (0, import_react3.useState)(null);
1783
- const [busy, setBusy] = (0, import_react3.useState)(false);
1784
- const [error, setError] = (0, import_react3.useState)(null);
1785
- const [tunnelState, setTunnelState] = (0, import_react3.useState)(null);
1786
- const [restartNotice, setRestartNotice] = (0, import_react3.useState)(false);
1787
- const [updateInfo, setUpdateInfo] = (0, import_react3.useState)(null);
1788
- const [isDesktop, setIsDesktop] = (0, import_react3.useState)(false);
1789
- const [now, setNow] = (0, import_react3.useState)(Date.now());
1790
- (0, import_react3.useEffect)(() => {
1791
+ const [status, setStatus] = (0, import_react2.useState)(null);
1792
+ const [busy, setBusy] = (0, import_react2.useState)(false);
1793
+ const [error, setError] = (0, import_react2.useState)(null);
1794
+ const [tunnelState, setTunnelState] = (0, import_react2.useState)(null);
1795
+ const [restartNotice, setRestartNotice] = (0, import_react2.useState)(false);
1796
+ const [updateInfo, setUpdateInfo] = (0, import_react2.useState)(null);
1797
+ const [isDesktop, setIsDesktop] = (0, import_react2.useState)(false);
1798
+ const [now, setNow] = (0, import_react2.useState)(Date.now());
1799
+ (0, import_react2.useEffect)(() => {
1791
1800
  const t2 = setInterval(() => setNow(Date.now()), 1e3);
1792
1801
  return () => clearInterval(t2);
1793
1802
  }, []);
@@ -1819,18 +1828,18 @@ function PocketSettingsTab({ rpcCall, t }) {
1819
1828
  } catch {
1820
1829
  }
1821
1830
  };
1822
- (0, import_react3.useEffect)(() => {
1831
+ (0, import_react2.useEffect)(() => {
1823
1832
  load();
1824
1833
  const t2 = setInterval(load, 3e3);
1825
1834
  return () => clearInterval(t2);
1826
1835
  }, []);
1827
- (0, import_react3.useEffect)(() => {
1836
+ (0, import_react2.useEffect)(() => {
1828
1837
  try {
1829
1838
  sessionStorage.removeItem("dshp-auto-reloaded");
1830
1839
  } catch {
1831
1840
  }
1832
1841
  }, []);
1833
- (0, import_react3.useEffect)(() => {
1842
+ (0, import_react2.useEffect)(() => {
1834
1843
  if (isDesktop) return;
1835
1844
  let alive = true;
1836
1845
  const check = async () => {
@@ -1886,8 +1895,8 @@ function PocketSettingsTab({ rpcCall, t }) {
1886
1895
  setUpdateInfo((u) => ({ ...u, updating: false, result: "fail", output: err.message }));
1887
1896
  }
1888
1897
  };
1889
- const [disclaimerOpen, setDisclaimerOpen] = (0, import_react3.useState)(false);
1890
- const [disclaimerChecked, setDisclaimerChecked] = (0, import_react3.useState)(false);
1898
+ const [disclaimerOpen, setDisclaimerOpen] = (0, import_react2.useState)(false);
1899
+ const [disclaimerChecked, setDisclaimerChecked] = (0, import_react2.useState)(false);
1891
1900
  const doStartTunnel = async () => {
1892
1901
  const cfg = status?.tunnelConfig;
1893
1902
  if (cfg?.mode === "named" && (!cfg.hostname || !cfg.tokenSet)) {
@@ -1920,7 +1929,7 @@ function PocketSettingsTab({ rpcCall, t }) {
1920
1929
  } catch {
1921
1930
  }
1922
1931
  };
1923
- const [tunnelCfg, setTunnelCfg] = (0, import_react3.useState)(null);
1932
+ const [tunnelCfg, setTunnelCfg] = (0, import_react2.useState)(null);
1924
1933
  const switchToQuick = async () => {
1925
1934
  try {
1926
1935
  setStatus(await call(POCKET_ENDPOINTS.tunnelSetConfig, { mode: "quick" }));
@@ -1941,7 +1950,7 @@ function PocketSettingsTab({ rpcCall, t }) {
1941
1950
  setTunnelCfg((c) => ({ ...c, err: err.message }));
1942
1951
  }
1943
1952
  };
1944
- const [resetOpen, setResetOpen] = (0, import_react3.useState)(false);
1953
+ const [resetOpen, setResetOpen] = (0, import_react2.useState)(false);
1945
1954
  const doFactoryReset = async () => {
1946
1955
  setResetOpen(false);
1947
1956
  setBusy(true);
@@ -1973,7 +1982,7 @@ function PocketSettingsTab({ rpcCall, t }) {
1973
1982
  } catch {
1974
1983
  }
1975
1984
  };
1976
- const [lanToggleOpen, setLanToggleOpen] = (0, import_react3.useState)(null);
1985
+ const [lanToggleOpen, setLanToggleOpen] = (0, import_react2.useState)(null);
1977
1986
  const requestLanToggle = (on) => setLanToggleOpen(on);
1978
1987
  const confirmLanToggle = async () => {
1979
1988
  const on = lanToggleOpen;
@@ -1993,7 +2002,7 @@ function PocketSettingsTab({ rpcCall, t }) {
1993
2002
  setError(err.message);
1994
2003
  }
1995
2004
  };
1996
- const [customPin, setCustomPin] = (0, import_react3.useState)(null);
2005
+ const [customPin, setCustomPin] = (0, import_react2.useState)(null);
1997
2006
  const saveCustomPin = async (which) => {
1998
2007
  try {
1999
2008
  const r = await call(POCKET_ENDPOINTS.pinSetCustom, { which, value: customPin?.value ?? "" });
@@ -2009,11 +2018,11 @@ function PocketSettingsTab({ rpcCall, t }) {
2009
2018
  setCustomPin((c) => ({ ...c, err: err.message }));
2010
2019
  }
2011
2020
  };
2012
- const customPinRow = (which) => (0, import_react3.createElement)(
2021
+ const customPinRow = (which) => (0, import_react2.createElement)(
2013
2022
  "div",
2014
2023
  { style: { marginTop: 6, fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)", lineHeight: 1.5 } },
2015
2024
  t("customizing"),
2016
- (0, import_react3.createElement)("input", {
2025
+ (0, import_react2.createElement)("input", {
2017
2026
  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" },
2018
2027
  type: "password",
2019
2028
  maxLength: 8,
@@ -2025,11 +2034,11 @@ function PocketSettingsTab({ rpcCall, t }) {
2025
2034
  if (e.key === "Escape") setCustomPin(null);
2026
2035
  }
2027
2036
  }),
2028
- (0, import_react3.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12, marginLeft: 2 }, onClick: () => saveCustomPin(which) }, t("save")),
2029
- (0, import_react3.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12 }, onClick: () => setCustomPin(null) }, t("cancel")),
2030
- customPin?.err ? (0, import_react3.createElement)("div", { style: { color: "var(--dsw-alias-state-error-primary,#dc2626)", marginTop: 4 } }, errText(customPin.err)) : null
2037
+ (0, import_react2.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12, marginLeft: 2 }, onClick: () => saveCustomPin(which) }, t("save")),
2038
+ (0, import_react2.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12 }, onClick: () => setCustomPin(null) }, t("cancel")),
2039
+ customPin?.err ? (0, import_react2.createElement)("div", { style: { color: "var(--dsw-alias-state-error-primary,#dc2626)", marginTop: 4 } }, errText(customPin.err)) : null
2031
2040
  );
2032
- 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"));
2041
+ 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"));
2033
2042
  const lanUrl = status?.lanUrl;
2034
2043
  const tunnelUrl = status?.tunnelUrl;
2035
2044
  const tunnelPhase = tunnelState?.phase ?? "idle";
@@ -2045,14 +2054,14 @@ function PocketSettingsTab({ rpcCall, t }) {
2045
2054
  if (i < 0) return s;
2046
2055
  return (t("ok") === zh2.ok ? s.slice(0, i) : s.slice(i + 3)).trim();
2047
2056
  };
2048
- const [toast, setToast] = (0, import_react3.useState)(null);
2049
- const toastTimer = (0, import_react3.useRef)(null);
2057
+ const [toast, setToast] = (0, import_react2.useState)(null);
2058
+ const toastTimer = (0, import_react2.useRef)(null);
2050
2059
  const showToast = (text) => {
2051
2060
  setToast(text);
2052
2061
  clearTimeout(toastTimer.current);
2053
2062
  toastTimer.current = setTimeout(() => setToast(null), 2600);
2054
2063
  };
2055
- (0, import_react3.useEffect)(() => () => clearTimeout(toastTimer.current), []);
2064
+ (0, import_react2.useEffect)(() => () => clearTimeout(toastTimer.current), []);
2056
2065
  const modeBtnStyle = (active) => ({
2057
2066
  ...styles.btn,
2058
2067
  height: 28,
@@ -2062,49 +2071,49 @@ function PocketSettingsTab({ rpcCall, t }) {
2062
2071
  background: active ? "var(--dsw-alias-button-primary-fill, var(--dsw-alias-brand-primary,#4f6ef7))" : "var(--dsw-alias-bg-layer-1,#fff)",
2063
2072
  color: active ? "var(--dsw-alias-label-primary-foreground, #fff)" : "var(--dsw-alias-label-primary,inherit)"
2064
2073
  });
2065
- const Switch = (on, onClick) => (0, import_react3.createElement)("button", {
2074
+ const Switch = (on, onClick) => (0, import_react2.createElement)("button", {
2066
2075
  role: "switch",
2067
2076
  "aria-checked": !!on,
2068
2077
  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)" },
2069
2078
  onClick
2070
- }, (0, import_react3.createElement)("span", { style: { position: "absolute", top: 2, left: on ? 20 : 2, width: 18, height: 18, borderRadius: "50%", background: "#fff" } }));
2071
- const qrArea = (src, url, hint) => (0, import_react3.createElement)(
2079
+ }, (0, import_react2.createElement)("span", { style: { position: "absolute", top: 2, left: on ? 20 : 2, width: 18, height: 18, borderRadius: "50%", background: "#fff" } }));
2080
+ const qrArea = (src, url, hint) => (0, import_react2.createElement)(
2072
2081
  "div",
2073
2082
  { style: { background: "var(--dsw-alias-bg-layer-2,#f3f4f6)", borderRadius: 10, padding: "10px 12px", textAlign: "center", margin: "10px 0" } },
2074
- (0, import_react3.createElement)("img", { src, alt: "QR", style: styles.qr }),
2075
- (0, import_react3.createElement)("div", { style: styles.code }, url),
2076
- (0, import_react3.createElement)("div", { style: styles.muted }, hint)
2083
+ (0, import_react2.createElement)("img", { src, alt: "QR", style: styles.qr }),
2084
+ (0, import_react2.createElement)("div", { style: styles.code }, url),
2085
+ (0, import_react2.createElement)("div", { style: styles.muted }, hint)
2077
2086
  );
2078
- const row = (label, control, extra) => (0, import_react3.createElement)(
2087
+ const row = (label, control, extra) => (0, import_react2.createElement)(
2079
2088
  "div",
2080
2089
  { style: { borderTop: "1px solid var(--dsw-alias-border-l2,#e5e7eb)", paddingTop: 9, marginTop: 9 } },
2081
- (0, import_react3.createElement)(
2090
+ (0, import_react2.createElement)(
2082
2091
  "div",
2083
2092
  { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 } },
2084
- (0, import_react3.createElement)("span", { style: { fontSize: 13 } }, label),
2093
+ (0, import_react2.createElement)("span", { style: { fontSize: 13 } }, label),
2085
2094
  control
2086
2095
  ),
2087
2096
  extra ?? null
2088
2097
  );
2089
- const [advOpen, setAdvOpen] = (0, import_react3.useState)(false);
2090
- return (0, import_react3.createElement)(
2098
+ const [advOpen, setAdvOpen] = (0, import_react2.useState)(false);
2099
+ return (0, import_react2.createElement)(
2091
2100
  "div",
2092
2101
  { style: styles.card },
2093
- (0, import_react3.createElement)(
2102
+ (0, import_react2.createElement)(
2094
2103
  "div",
2095
2104
  { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 } },
2096
- (0, import_react3.createElement)(
2105
+ (0, import_react2.createElement)(
2097
2106
  "div",
2098
2107
  null,
2099
- (0, import_react3.createElement)("strong", null, t("title")),
2100
- (0, import_react3.createElement)("div", { style: styles.muted }, t("subtitle"))
2108
+ (0, import_react2.createElement)("strong", null, t("title")),
2109
+ (0, import_react2.createElement)("div", { style: styles.muted }, t("subtitle"))
2101
2110
  ),
2102
- (0, import_react3.createElement)(
2111
+ (0, import_react2.createElement)(
2103
2112
  "div",
2104
2113
  { style: { fontSize: 12, color: "var(--dsw-alias-label-tertiary,#8b93a1)", textAlign: "right" } },
2105
- (0, import_react3.createElement)("div", { style: { whiteSpace: "nowrap" } }, t("developer")),
2106
- (0, import_react3.createElement)("div", { style: { whiteSpace: "nowrap" } }, t("starAsk")),
2107
- (0, import_react3.createElement)(
2114
+ (0, import_react2.createElement)("div", { style: { whiteSpace: "nowrap" } }, t("developer")),
2115
+ (0, import_react2.createElement)("div", { style: { whiteSpace: "nowrap" } }, t("starAsk")),
2116
+ (0, import_react2.createElement)(
2108
2117
  "a",
2109
2118
  { 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" } },
2110
2119
  t("starCta")
@@ -2113,49 +2122,49 @@ function PocketSettingsTab({ rpcCall, t }) {
2113
2122
  ),
2114
2123
  // 桌面端不显示更新/重启横幅(更新由 DSH Desktop 管理),也不需要额外提示
2115
2124
  // 重启后提示(进程在后台运行,停止方法)——左侧蓝色色条(桌面端不会触发本插件的自重启)
2116
- !isDesktop && restartNotice ? (0, import_react3.createElement)(
2125
+ !isDesktop && restartNotice ? (0, import_react2.createElement)(
2117
2126
  "div",
2118
2127
  { 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" } },
2119
- (0, import_react3.createElement)(
2128
+ (0, import_react2.createElement)(
2120
2129
  "div",
2121
2130
  { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 } },
2122
- (0, import_react3.createElement)("div", { style: { fontWeight: 600, fontSize: 13 } }, t("restarted")),
2123
- (0, import_react3.createElement)("button", { style: styles.btn, onClick: () => setRestartNotice(false) }, t("ok"))
2131
+ (0, import_react2.createElement)("div", { style: { fontWeight: 600, fontSize: 13 } }, t("restarted")),
2132
+ (0, import_react2.createElement)("button", { style: styles.btn, onClick: () => setRestartNotice(false) }, t("ok"))
2124
2133
  ),
2125
- (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` }))
2134
+ (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` }))
2126
2135
  ) : null,
2127
2136
  // 更新提示——左侧黄色色条(提示有新版本);单状态:有更新/更新中/已更新自动重启,不并存
2128
2137
  // 桌面端不渲染(更新由 DSH Desktop 管理)
2129
- !isDesktop && updateInfo ? (0, import_react3.createElement)(
2138
+ !isDesktop && updateInfo ? (0, import_react2.createElement)(
2130
2139
  "div",
2131
2140
  { 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" } },
2132
- (0, import_react3.createElement)(
2141
+ (0, import_react2.createElement)(
2133
2142
  "div",
2134
2143
  { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 } },
2135
- (0, import_react3.createElement)(
2144
+ (0, import_react2.createElement)(
2136
2145
  "div",
2137
2146
  { style: { fontWeight: 600, fontSize: 13 } },
2138
2147
  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 })
2139
2148
  ),
2140
- 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"))
2149
+ 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"))
2141
2150
  ),
2142
- (0, import_react3.createElement)(
2151
+ (0, import_react2.createElement)(
2143
2152
  "div",
2144
2153
  { style: styles.muted, marginTop: 4 },
2145
2154
  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 })
2146
2155
  )
2147
2156
  ) : null,
2148
2157
  // 局域网:标题行自带总开关 → 二维码+地址 → 设置行(访问密码 / 高级·手动选地址)
2149
- (0, import_react3.createElement)(
2158
+ (0, import_react2.createElement)(
2150
2159
  "div",
2151
2160
  { style: styles.block },
2152
- (0, import_react3.createElement)(
2161
+ (0, import_react2.createElement)(
2153
2162
  "div",
2154
2163
  { style: { display: "flex", alignItems: "center", justifyContent: "space-between" } },
2155
- (0, import_react3.createElement)("span", { style: { fontWeight: 600, fontSize: 13 } }, t("lanAccess")),
2164
+ (0, import_react2.createElement)("span", { style: { fontWeight: 600, fontSize: 13 } }, t("lanAccess")),
2156
2165
  Switch(status?.lanEnabled !== false, () => requestLanToggle(status?.lanEnabled === false))
2157
2166
  ),
2158
- 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)(
2167
+ 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)(
2159
2168
  "div",
2160
2169
  null,
2161
2170
  qrArea(status.lanQr, lanUrl, t("lanHint")),
@@ -2163,100 +2172,100 @@ function PocketSettingsTab({ rpcCall, t }) {
2163
2172
  row(
2164
2173
  t("lanPin"),
2165
2174
  Switch(status?.lanAuthEnabled !== false, () => setLanAuth(status?.lanAuthEnabled === false)),
2166
- status?.lanAuthEnabled === false ? (0, import_react3.createElement)("div", { style: { ...styles.muted, marginTop: 6 } }, t("lanPinOff")) : customPin?.which === "lan" ? customPinRow("lan") : (0, import_react3.createElement)(
2175
+ status?.lanAuthEnabled === false ? (0, import_react2.createElement)("div", { style: { ...styles.muted, marginTop: 6 } }, t("lanPinOff")) : customPin?.which === "lan" ? customPinRow("lan") : (0, import_react2.createElement)(
2167
2176
  "div",
2168
2177
  { style: { marginTop: 6, display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" } },
2169
- (0, import_react3.createElement)("span", { style: { fontFamily: "ui-monospace,Menlo,monospace", fontSize: 13, letterSpacing: 1 } }, status.lanToken),
2170
- (0, import_react3.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12 }, onClick: refreshLanPin }, t("refresh")),
2178
+ (0, import_react2.createElement)("span", { style: { fontFamily: "ui-monospace,Menlo,monospace", fontSize: 13, letterSpacing: 1 } }, status.lanToken),
2179
+ (0, import_react2.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12 }, onClick: refreshLanPin }, t("refresh")),
2171
2180
  customBtn("lan"),
2172
- status?.lanPinCustom ? (0, import_react3.createElement)("span", { style: { fontSize: 11, color: "var(--dsw-alias-state-warn-primary,#b45309)" } }, t("pinCustomHint")) : null
2181
+ status?.lanPinCustom ? (0, import_react2.createElement)("span", { style: { fontSize: 11, color: "var(--dsw-alias-state-warn-primary,#b45309)" } }, t("pinCustomHint")) : null
2173
2182
  )
2174
2183
  ),
2175
2184
  // 高级:手动选地址(默认收起)
2176
2185
  row(
2177
2186
  t("advAddress"),
2178
- (0, import_react3.createElement)(
2187
+ (0, import_react2.createElement)(
2179
2188
  "button",
2180
2189
  { style: { border: "none", background: "none", font: "inherit", cursor: "pointer", fontSize: 12, color: "var(--dsw-alias-label-tertiary,#8b93a1)", padding: 0 }, onClick: () => setAdvOpen((v) => !v) },
2181
2190
  (status?.lanIpOverride || t("lanAddressAuto")) + " \u203A"
2182
2191
  ),
2183
- advOpen ? (0, import_react3.createElement)(
2192
+ advOpen ? (0, import_react2.createElement)(
2184
2193
  "div",
2185
2194
  { style: { marginTop: 8 } },
2186
- (0, import_react3.createElement)(
2195
+ (0, import_react2.createElement)(
2187
2196
  "label",
2188
2197
  { style: { display: "flex", alignItems: "center", gap: 8, fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)" } },
2189
2198
  t("lanAddress"),
2190
- (0, import_react3.createElement)(
2199
+ (0, import_react2.createElement)(
2191
2200
  "select",
2192
2201
  {
2193
2202
  value: status?.lanIpOverride || "",
2194
2203
  onChange: (e) => setLanAddress(e.target.value),
2195
2204
  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)" }
2196
2205
  },
2197
- (0, import_react3.createElement)("option", { value: "" }, t("lanAddressAuto")),
2198
- (status?.lanCandidates || []).map((ip) => (0, import_react3.createElement)("option", { key: ip, value: ip }, ip))
2206
+ (0, import_react2.createElement)("option", { value: "" }, t("lanAddressAuto")),
2207
+ (status?.lanCandidates || []).map((ip) => (0, import_react2.createElement)("option", { key: ip, value: ip }, ip))
2199
2208
  )
2200
2209
  )
2201
2210
  ) : null
2202
2211
  )
2203
- ) : (0, import_react3.createElement)("div", { style: styles.muted }, t("lanStarting"))
2212
+ ) : (0, import_react2.createElement)("div", { style: styles.muted }, t("lanStarting"))
2204
2213
  ),
2205
2214
  // 公网:标题行自带 开启/关闭 → 开启后:二维码+地址、地址模式行、访问密码行
2206
- (0, import_react3.createElement)(
2215
+ (0, import_react2.createElement)(
2207
2216
  "div",
2208
2217
  { style: styles.block },
2209
- (0, import_react3.createElement)(
2218
+ (0, import_react2.createElement)(
2210
2219
  "div",
2211
2220
  { style: { display: "flex", alignItems: "center", justifyContent: "space-between" } },
2212
- (0, import_react3.createElement)("span", { style: { fontWeight: 600, fontSize: 13 } }, t("wanAccess")),
2213
- 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"))
2221
+ (0, import_react2.createElement)("span", { style: { fontWeight: 600, fontSize: 13 } }, t("wanAccess")),
2222
+ 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"))
2214
2223
  ),
2215
- tunnelStarting ? (0, import_react3.createElement)(
2224
+ tunnelStarting ? (0, import_react2.createElement)(
2216
2225
  "div",
2217
2226
  { style: { marginTop: 8, fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)" } },
2218
2227
  tunnelPhase === "downloading" ? fmt(t, "downloading", { s: elapsed(tunnelStateStarted) }) : fmt(t, "connecting", { s: elapsed(tunnelStateStarted), suffix: elapsed(tunnelStateStarted) > 30 ? t("slowHint") : "" })
2219
- ) : tunnelPhase === "error" ? (0, import_react3.createElement)(
2228
+ ) : tunnelPhase === "error" ? (0, import_react2.createElement)(
2220
2229
  "div",
2221
2230
  { style: { marginTop: 8, fontSize: 12, color: "var(--dsw-alias-state-error-primary,#dc2626)" } },
2222
2231
  fmt(t, "error", { detail: errText(tunnelStateDetail) || t("unknownError") })
2223
- ) : !tunnelUrl && !isDesktop ? (0, import_react3.createElement)("div", { style: { ...styles.muted, marginTop: 8 } }, t("wanOffHint")) : null,
2224
- tunnelUrl ? (0, import_react3.createElement)(
2232
+ ) : !tunnelUrl && !isDesktop ? (0, import_react2.createElement)("div", { style: { ...styles.muted, marginTop: 8 } }, t("wanOffHint")) : null,
2233
+ tunnelUrl ? (0, import_react2.createElement)(
2225
2234
  "div",
2226
2235
  null,
2227
2236
  qrArea(status.tunnelQr, tunnelUrl, namedMode ? t("namedRunningHint") : t("wanHint")),
2228
2237
  // 地址模式行(随机/固定;固定域名选中或编辑时高亮)
2229
2238
  row(
2230
2239
  t("modeLabel"),
2231
- (0, import_react3.createElement)(
2240
+ (0, import_react2.createElement)(
2232
2241
  "span",
2233
2242
  { style: { display: "inline-flex", gap: 6 } },
2234
- (0, import_react3.createElement)("button", { style: modeBtnStyle(!namedActive), onClick: namedMode ? switchToQuick : tunnelCfg ? () => setTunnelCfg(null) : void 0 }, t("modeQuick")),
2235
- (0, import_react3.createElement)("button", { style: modeBtnStyle(namedActive), onClick: () => setTunnelCfg(tunnelCfg ? null : { hostname: tunnelModeView.hostname ?? "", token: "", err: null }) }, t("modeNamed"))
2243
+ (0, import_react2.createElement)("button", { style: modeBtnStyle(!namedActive), onClick: namedMode ? switchToQuick : tunnelCfg ? () => setTunnelCfg(null) : void 0 }, t("modeQuick")),
2244
+ (0, import_react2.createElement)("button", { style: modeBtnStyle(namedActive), onClick: () => setTunnelCfg(tunnelCfg ? null : { hostname: tunnelModeView.hostname ?? "", token: "", err: null }) }, t("modeNamed"))
2236
2245
  ),
2237
- (0, import_react3.createElement)(
2246
+ (0, import_react2.createElement)(
2238
2247
  "div",
2239
2248
  { style: { marginTop: 6 } },
2240
2249
  // 刚保存固定域名但当前连接仍是随机域名:需关闭后重新开启才生效
2241
- namedMode && /trycloudflare\.com/i.test(tunnelUrl ?? "") ? (0, import_react3.createElement)("div", { style: { ...styles.warn } }, t("namedTakeEffect")) : null,
2250
+ namedMode && /trycloudflare\.com/i.test(tunnelUrl ?? "") ? (0, import_react2.createElement)("div", { style: { ...styles.warn } }, t("namedTakeEffect")) : null,
2242
2251
  // 固定域名:已保存摘要 + 修改入口(非编辑态)
2243
- namedMode && !tunnelCfg ? (0, import_react3.createElement)(
2252
+ namedMode && !tunnelCfg ? (0, import_react2.createElement)(
2244
2253
  "div",
2245
2254
  { style: { ...styles.muted } },
2246
2255
  fmt(t, "namedSummary", { host: tunnelModeView.hostname || "\u2014", token: tunnelModeView.tokenSet ? t("namedTokenSet") : t("namedTokenMissing") }),
2247
- (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")),
2248
- (0, import_react3.createElement)("div", { style: { ...styles.muted, marginTop: 4 } }, t("namedHow")),
2249
- !tunnelModeView.tokenSet || !tunnelModeView.hostname ? (0, import_react3.createElement)("div", { style: { marginTop: 2, color: "var(--dsw-alias-state-error-primary,#dc2626)" } }, t("namedNeedCfg")) : null
2256
+ (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")),
2257
+ (0, import_react2.createElement)("div", { style: { ...styles.muted, marginTop: 4 } }, t("namedHow")),
2258
+ !tunnelModeView.tokenSet || !tunnelModeView.hostname ? (0, import_react2.createElement)("div", { style: { marginTop: 2, color: "var(--dsw-alias-state-error-primary,#dc2626)" } }, t("namedNeedCfg")) : null
2250
2259
  ) : null,
2251
2260
  // 固定域名:编辑表单(域名 + Tunnel Token,Token 留空保持不变)
2252
- tunnelCfg ? (0, import_react3.createElement)(
2261
+ tunnelCfg ? (0, import_react2.createElement)(
2253
2262
  "div",
2254
2263
  { style: { marginTop: 6, fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)", lineHeight: 1.6 } },
2255
- (0, import_react3.createElement)(
2264
+ (0, import_react2.createElement)(
2256
2265
  "div",
2257
2266
  null,
2258
2267
  t("namedHostnameLabel"),
2259
- (0, import_react3.createElement)("input", {
2268
+ (0, import_react2.createElement)("input", {
2260
2269
  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 },
2261
2270
  placeholder: "pocket.example.com",
2262
2271
  value: tunnelCfg.hostname ?? "",
@@ -2268,11 +2277,11 @@ function PocketSettingsTab({ rpcCall, t }) {
2268
2277
  }
2269
2278
  })
2270
2279
  ),
2271
- (0, import_react3.createElement)(
2280
+ (0, import_react2.createElement)(
2272
2281
  "div",
2273
2282
  { style: { marginTop: 6 } },
2274
2283
  t("namedTokenLabel"),
2275
- (0, import_react3.createElement)("input", {
2284
+ (0, import_react2.createElement)("input", {
2276
2285
  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" },
2277
2286
  type: "password",
2278
2287
  value: tunnelCfg.token ?? "",
@@ -2283,121 +2292,121 @@ function PocketSettingsTab({ rpcCall, t }) {
2283
2292
  }
2284
2293
  })
2285
2294
  ),
2286
- (0, import_react3.createElement)(
2295
+ (0, import_react2.createElement)(
2287
2296
  "div",
2288
2297
  { style: { marginTop: 6, display: "flex", gap: 8 } },
2289
- (0, import_react3.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12 }, onClick: saveNamedTunnel }, t("save")),
2290
- (0, import_react3.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12 }, onClick: () => setTunnelCfg(null) }, t("cancel"))
2298
+ (0, import_react2.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12 }, onClick: saveNamedTunnel }, t("save")),
2299
+ (0, import_react2.createElement)("button", { style: { ...styles.btn, height: 26, padding: "0 10px", fontSize: 12 }, onClick: () => setTunnelCfg(null) }, t("cancel"))
2291
2300
  ),
2292
- (0, import_react3.createElement)("div", { style: { ...styles.muted, marginTop: 6 } }, t("namedHow")),
2293
- (0, import_react3.createElement)("div", { style: { marginTop: 2, fontSize: 11, color: "var(--dsw-alias-state-warn-primary,#b45309)", lineHeight: 1.5 } }, t("namedSecurity")),
2294
- tunnelCfg.err ? (0, import_react3.createElement)("div", { style: { color: "var(--dsw-alias-state-error-primary,#dc2626)", marginTop: 4 } }, errText(tunnelCfg.err)) : null
2301
+ (0, import_react2.createElement)("div", { style: { ...styles.muted, marginTop: 6 } }, t("namedHow")),
2302
+ (0, import_react2.createElement)("div", { style: { marginTop: 2, fontSize: 11, color: "var(--dsw-alias-state-warn-primary,#b45309)", lineHeight: 1.5 } }, t("namedSecurity")),
2303
+ tunnelCfg.err ? (0, import_react2.createElement)("div", { style: { color: "var(--dsw-alias-state-error-primary,#dc2626)", marginTop: 4 } }, errText(tunnelCfg.err)) : null
2295
2304
  ) : null
2296
2305
  )
2297
2306
  ),
2298
2307
  // 访问密码行:值 + 自定义(自定义输入态整体替换)
2299
2308
  status.accessToken ? row(
2300
2309
  t("pinLabel"),
2301
- customPin?.which === "public" ? null : (0, import_react3.createElement)(
2310
+ customPin?.which === "public" ? null : (0, import_react2.createElement)(
2302
2311
  "span",
2303
2312
  { style: { display: "inline-flex", alignItems: "center", gap: 8 } },
2304
- (0, import_react3.createElement)("span", { style: { fontFamily: "ui-monospace,Menlo,monospace", fontSize: 13, letterSpacing: 1 } }, status.accessToken),
2313
+ (0, import_react2.createElement)("span", { style: { fontFamily: "ui-monospace,Menlo,monospace", fontSize: 13, letterSpacing: 1 } }, status.accessToken),
2305
2314
  customBtn("public")
2306
2315
  ),
2307
- (0, import_react3.createElement)(
2316
+ (0, import_react2.createElement)(
2308
2317
  "div",
2309
2318
  { style: { marginTop: 6 } },
2310
2319
  customPin?.which === "public" ? customPinRow("public") : null,
2311
- status?.publicPinCustom ? (0, import_react3.createElement)("div", { style: { ...styles.warn } }, t("pinCustomHint")) : null,
2312
- namedMode ? (0, import_react3.createElement)("div", { style: { ...styles.warn } }, t("namedSecurity")) : null
2320
+ status?.publicPinCustom ? (0, import_react2.createElement)("div", { style: { ...styles.warn } }, t("pinCustomHint")) : null,
2321
+ namedMode ? (0, import_react2.createElement)("div", { style: { ...styles.warn } }, t("namedSecurity")) : null
2313
2322
  )
2314
2323
  ) : null
2315
2324
  ) : null
2316
2325
  ),
2317
- error ? (0, import_react3.createElement)("div", { style: { color: "var(--dsw-alias-state-error-primary,#dc2626)", fontSize: 12, marginTop: 8 } }, `\u274C ${errText(error)}`) : null,
2326
+ error ? (0, import_react2.createElement)("div", { style: { color: "var(--dsw-alias-state-error-primary,#dc2626)", fontSize: 12, marginTop: 8 } }, `\u274C ${errText(error)}`) : null,
2318
2327
  // 恢复出厂设置:设置出问题时的临时兜底(最底部,避免误触)
2319
- (0, import_react3.createElement)(
2328
+ (0, import_react2.createElement)(
2320
2329
  "div",
2321
2330
  { style: styles.block },
2322
- (0, import_react3.createElement)(
2331
+ (0, import_react2.createElement)(
2323
2332
  "div",
2324
2333
  { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 } },
2325
- (0, import_react3.createElement)("span", { style: { fontWeight: 600, fontSize: 13 } }, t("resetFactory")),
2326
- (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"))
2334
+ (0, import_react2.createElement)("span", { style: { fontWeight: 600, fontSize: 13 } }, t("resetFactory")),
2335
+ (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"))
2327
2336
  ),
2328
- (0, import_react3.createElement)("div", { style: { ...styles.muted, marginTop: 6 } }, t("resetIntro"))
2337
+ (0, import_react2.createElement)("div", { style: { ...styles.muted, marginTop: 6 } }, t("resetIntro"))
2329
2338
  ),
2330
2339
  // 恢复出厂设置确认弹框
2331
- resetOpen ? (0, import_react3.createElement)(
2340
+ resetOpen ? (0, import_react2.createElement)(
2332
2341
  "div",
2333
2342
  { style: { position: "fixed", inset: 0, zIndex: 1e4, background: "rgba(0,0,0,.5)", display: "flex", alignItems: "center", justifyContent: "center", padding: 20 } },
2334
- (0, import_react3.createElement)(
2343
+ (0, import_react2.createElement)(
2335
2344
  "div",
2336
2345
  { 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)" } },
2337
- (0, import_react3.createElement)("div", { style: { fontWeight: 600, fontSize: 15, color: "var(--dsw-alias-state-warn-primary,#b45309)", marginBottom: 10 } }, t("resetTitle")),
2338
- (0, import_react3.createElement)("div", { style: { fontSize: 13, lineHeight: 1.7, color: "var(--dsw-alias-label-primary,inherit)", whiteSpace: "pre-line" } }, t("resetBody")),
2339
- (0, import_react3.createElement)(
2346
+ (0, import_react2.createElement)("div", { style: { fontWeight: 600, fontSize: 15, color: "var(--dsw-alias-state-warn-primary,#b45309)", marginBottom: 10 } }, t("resetTitle")),
2347
+ (0, import_react2.createElement)("div", { style: { fontSize: 13, lineHeight: 1.7, color: "var(--dsw-alias-label-primary,inherit)", whiteSpace: "pre-line" } }, t("resetBody")),
2348
+ (0, import_react2.createElement)(
2340
2349
  "div",
2341
2350
  { style: { display: "flex", gap: 8, marginTop: 16 } },
2342
- (0, import_react3.createElement)("button", { style: { ...styles.btn, flex: 1 }, onClick: () => setResetOpen(false) }, t("cancel")),
2343
- (0, import_react3.createElement)("button", { style: { ...styles.primary, flex: 1, background: "var(--dsw-alias-state-error-primary,#dc2626)" }, onClick: doFactoryReset }, t("resetConfirm"))
2351
+ (0, import_react2.createElement)("button", { style: { ...styles.btn, flex: 1 }, onClick: () => setResetOpen(false) }, t("cancel")),
2352
+ (0, import_react2.createElement)("button", { style: { ...styles.primary, flex: 1, background: "var(--dsw-alias-state-error-primary,#dc2626)" }, onClick: doFactoryReset }, t("resetConfirm"))
2344
2353
  )
2345
2354
  )
2346
2355
  ) : null,
2347
2356
  // Toast:重置等操作的即时反馈(固定屏幕正中央,2.6s 自动消失)
2348
- toast ? (0, import_react3.createElement)("div", {
2357
+ toast ? (0, import_react2.createElement)("div", {
2349
2358
  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)" }
2350
2359
  }, toast) : null,
2351
2360
  // 局域网访问开关确认弹框(关闭/打开时弹窗提醒)
2352
- lanToggleOpen !== null ? (0, import_react3.createElement)(
2361
+ lanToggleOpen !== null ? (0, import_react2.createElement)(
2353
2362
  "div",
2354
2363
  { style: { position: "fixed", inset: 0, zIndex: 1e4, background: "rgba(0,0,0,.5)", display: "flex", alignItems: "center", justifyContent: "center", padding: 20 } },
2355
- (0, import_react3.createElement)(
2364
+ (0, import_react2.createElement)(
2356
2365
  "div",
2357
2366
  { 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)" } },
2358
- (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")),
2359
- (0, import_react3.createElement)("div", { style: { fontSize: 13, lineHeight: 1.7, color: "var(--dsw-alias-label-primary,inherit)" } }, t(lanToggleOpen ? "lanToggleBodyOn" : "lanToggleBodyOff")),
2360
- (0, import_react3.createElement)(
2367
+ (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")),
2368
+ (0, import_react2.createElement)("div", { style: { fontSize: 13, lineHeight: 1.7, color: "var(--dsw-alias-label-primary,inherit)" } }, t(lanToggleOpen ? "lanToggleBodyOn" : "lanToggleBodyOff")),
2369
+ (0, import_react2.createElement)(
2361
2370
  "div",
2362
2371
  { style: { display: "flex", gap: 8, marginTop: 16 } },
2363
- (0, import_react3.createElement)("button", { style: { ...styles.btn, flex: 1 }, onClick: () => setLanToggleOpen(null) }, t("cancel")),
2364
- (0, import_react3.createElement)("button", { style: { ...styles.primary, flex: 1 }, onClick: confirmLanToggle }, t("confirm"))
2372
+ (0, import_react2.createElement)("button", { style: { ...styles.btn, flex: 1 }, onClick: () => setLanToggleOpen(null) }, t("cancel")),
2373
+ (0, import_react2.createElement)("button", { style: { ...styles.primary, flex: 1 }, onClick: confirmLanToggle }, t("confirm"))
2365
2374
  )
2366
2375
  )
2367
2376
  ) : null,
2368
2377
  // 安全免责声明弹框(issue #31):每次开启公网访问前确认
2369
- disclaimerOpen ? (0, import_react3.createElement)(
2378
+ disclaimerOpen ? (0, import_react2.createElement)(
2370
2379
  "div",
2371
2380
  { style: { position: "fixed", inset: 0, zIndex: 1e4, background: "rgba(0,0,0,.5)", display: "flex", alignItems: "center", justifyContent: "center", padding: 20 } },
2372
- (0, import_react3.createElement)(
2381
+ (0, import_react2.createElement)(
2373
2382
  "div",
2374
2383
  { 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)" } },
2375
- (0, import_react3.createElement)("div", { style: { fontWeight: 600, fontSize: 15, color: "var(--dsw-alias-state-warn-primary,#b45309)", marginBottom: 10 } }, t("disclaimerTitle")),
2376
- (0, import_react3.createElement)("div", { style: { fontSize: 13, lineHeight: 1.7, color: "var(--dsw-alias-label-primary,inherit)" } }, t("disclaimerBody")),
2377
- (0, import_react3.createElement)(
2384
+ (0, import_react2.createElement)("div", { style: { fontWeight: 600, fontSize: 15, color: "var(--dsw-alias-state-warn-primary,#b45309)", marginBottom: 10 } }, t("disclaimerTitle")),
2385
+ (0, import_react2.createElement)("div", { style: { fontSize: 13, lineHeight: 1.7, color: "var(--dsw-alias-label-primary,inherit)" } }, t("disclaimerBody")),
2386
+ (0, import_react2.createElement)(
2378
2387
  "label",
2379
2388
  { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 14, fontSize: 13, cursor: "pointer" } },
2380
- (0, import_react3.createElement)("input", { type: "checkbox", checked: disclaimerChecked, onChange: (e) => setDisclaimerChecked(e.target.checked), style: { width: 16, height: 16 } }),
2389
+ (0, import_react2.createElement)("input", { type: "checkbox", checked: disclaimerChecked, onChange: (e) => setDisclaimerChecked(e.target.checked), style: { width: 16, height: 16 } }),
2381
2390
  t("disclaimerAgree")
2382
2391
  ),
2383
- (0, import_react3.createElement)(
2392
+ (0, import_react2.createElement)(
2384
2393
  "div",
2385
2394
  { style: { display: "flex", gap: 8, marginTop: 16 } },
2386
- (0, import_react3.createElement)("button", { style: { ...styles.btn, flex: 1 }, onClick: () => setDisclaimerOpen(false) }, t("cancel")),
2387
- (0, import_react3.createElement)("button", {
2395
+ (0, import_react2.createElement)("button", { style: { ...styles.btn, flex: 1 }, onClick: () => setDisclaimerOpen(false) }, t("cancel")),
2396
+ (0, import_react2.createElement)("button", {
2388
2397
  style: { ...styles.primary, flex: 1, opacity: disclaimerChecked ? 1 : 0.5 },
2389
2398
  disabled: !disclaimerChecked,
2390
2399
  onClick: confirmDisclaimer
2391
2400
  }, t("disclaimerAgree"))
2392
2401
  ),
2393
- !disclaimerChecked ? (0, import_react3.createElement)("div", { style: { marginTop: 8, fontSize: 12, color: "var(--dsw-alias-state-error-primary,#dc2626)" } }, t("disclaimerHint")) : null
2402
+ !disclaimerChecked ? (0, import_react2.createElement)("div", { style: { marginTop: 8, fontSize: 12, color: "var(--dsw-alias-state-error-primary,#dc2626)" } }, t("disclaimerHint")) : null
2394
2403
  )
2395
2404
  ) : null,
2396
2405
  // 页面最底部:反馈入口
2397
- (0, import_react3.createElement)(
2406
+ (0, import_react2.createElement)(
2398
2407
  "div",
2399
2408
  { style: { ...styles.block, textAlign: "center" } },
2400
- (0, import_react3.createElement)(
2409
+ (0, import_react2.createElement)(
2401
2410
  "a",
2402
2411
  { href: "https://github.com/shaobeichen/dsh-pocket/issues", target: "_blank", rel: "noreferrer", style: { fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)", textDecoration: "none" } },
2403
2412
  t("feedback")
@@ -0,0 +1,118 @@
1
+ // 移动端文件守卫(issue #17 修正):dsh-web 在手机上点「文件链接」会触发桌面端
2
+ // workspaces.openPath(open <path>),既打不开(文件在电脑上),又会抛
3
+ // "path open failed: ...". 这里在捕获阶段拦截这类激活(点击 / 键盘),改为弹一个
4
+ // 提示;并隐藏「添加工作区」入口(手机上配工作区无意义)。
5
+ // 移植自 dsh-web-mobile(MIT)。
6
+ //
7
+ // 识别方式:不依赖 dsh-web 的 hash 类名(每次构建都变),只认「文本像文件路径的
8
+ // <button>/<a>」——文件链接按钮的文案就是路径(如 lib/proxy.mjs / /Users/.../x.ts)。
9
+
10
+ /** 手机上点击文件时弹出的提示。 */
11
+ const GUARD_MSG = '手机上无法直接打开电脑上的文件'
12
+ /** 「添加工作区」入口的文案(随语言变化),两种都覆盖。 */
13
+ const WS_LABELS = ['添加工作区', '添加工作区…', 'Add workspace', 'Add workspace…']
14
+
15
+ /** 文本是否像文件路径:绝对路径 / 相对路径 / 带扩展名的目录路径。 */
16
+ function looksLikeFilePath(text: string | null): boolean {
17
+ const t = (text ?? '').trim()
18
+ if (t.length < 3 || t.length > 320) return false
19
+ if (/^(\/|~\/|\.\.?\/|[A-Za-z]:\\)/.test(t)) return true
20
+ if (/\/[\w.\-]+\.\w{1,12}$/.test(t)) return true
21
+ if (/[\w.\-]+\/[\w.\-]+\.\w{1,12}/.test(t)) return true
22
+ return false
23
+ }
24
+
25
+ /** 节点是否落在 dsh-pocket 自身面板内(不拦截面板内的交互)。 */
26
+ function isInsidePocket(el: Element | null): boolean {
27
+ return el !== null && el.closest('[data-mobile-nav="frame"]') !== null
28
+ }
29
+
30
+ export function startFileGuard(): () => void {
31
+ // 轻量 toast:自包含,不依赖 dsh-pocket 面板的 React 状态。
32
+ let toastEl: HTMLElement | null = null
33
+ let toastTimer: number | null = null
34
+ const showToast = (text: string): void => {
35
+ if (toastEl === null) {
36
+ toastEl = document.createElement('div')
37
+ toastEl.setAttribute('data-mobile-nav', 'file-guard-toast')
38
+ Object.assign(toastEl.style, {
39
+ position: 'fixed',
40
+ left: '50%',
41
+ bottom: '64px',
42
+ transform: 'translateX(-50%)',
43
+ maxWidth: '84vw',
44
+ zIndex: '9999',
45
+ padding: '10px 14px',
46
+ borderRadius: '10px',
47
+ background: 'rgba(20,22,28,.92)',
48
+ color: '#fff',
49
+ fontSize: '13px',
50
+ lineHeight: '1.4',
51
+ textAlign: 'center',
52
+ fontFamily: 'inherit',
53
+ boxShadow: '0 4px 16px rgba(0,0,0,.28)',
54
+ pointerEvents: 'none',
55
+ opacity: '0',
56
+ transition: 'opacity .18s ease',
57
+ } as CSSStyleDeclaration)
58
+ document.body.appendChild(toastEl)
59
+ }
60
+ toastEl.textContent = text
61
+ requestAnimationFrame(() => {
62
+ if (toastEl !== null) toastEl.style.opacity = '1'
63
+ })
64
+ if (toastTimer !== null) window.clearTimeout(toastTimer)
65
+ toastTimer = window.setTimeout(() => {
66
+ if (toastEl !== null) toastEl.style.opacity = '0'
67
+ }, 2600)
68
+ }
69
+
70
+ // 捕获阶段拦截文件链接的激活。按钮的键盘激活(Enter/Space)会派发 click,
71
+ // 因此只拦 click 即可同时覆盖鼠标与键盘,避免重复处理。
72
+ const onClick = (event: MouseEvent): void => {
73
+ const target = event.target as HTMLElement | null
74
+ if (target === null || isInsidePocket(target)) return
75
+ const el = target.closest('button, a') as HTMLElement | null
76
+ if (el === null) return
77
+ if (!looksLikeFilePath(el.textContent)) return
78
+ event.preventDefault()
79
+ event.stopImmediatePropagation()
80
+ showToast(GUARD_MSG)
81
+ }
82
+ document.addEventListener('click', onClick, true)
83
+
84
+ // 隐藏「添加工作区」入口:图标按钮由 mobile.css.ts 按 aria-label 隐藏;
85
+ // 下拉菜单里的文本项 CSS 选不到,这里按文案兜底(只在新增节点时检查,省开销)。
86
+ const hideWsEntries = (): (() => void) => {
87
+ const checkOne = (node: Node): void => {
88
+ if (node.nodeType !== 1) return
89
+ const el = node as HTMLElement
90
+ const txt = (el.getAttribute('aria-label') ?? el.textContent ?? '').trim()
91
+ if (WS_LABELS.includes(txt)) {
92
+ el.style.display = 'none'
93
+ el.setAttribute('data-mobile-nav-hide', 'add-workspace')
94
+ }
95
+ }
96
+ const sel = '[role="menuitem"],[role="option"],li,button,a'
97
+ document.querySelectorAll(sel).forEach(checkOne)
98
+ const observer = new MutationObserver((mutations) => {
99
+ for (const m of mutations) {
100
+ m.addedNodes.forEach((n) => {
101
+ if (n.nodeType !== 1) return
102
+ checkOne(n)
103
+ ;(n as HTMLElement).querySelectorAll?.(sel).forEach(checkOne)
104
+ })
105
+ }
106
+ })
107
+ observer.observe(document.body, { childList: true, subtree: true })
108
+ return () => observer.disconnect()
109
+ }
110
+ const disconnectWs = hideWsEntries()
111
+
112
+ return () => {
113
+ document.removeEventListener('click', onClick, true)
114
+ disconnectWs()
115
+ if (toastTimer !== null) window.clearTimeout(toastTimer)
116
+ toastEl?.remove()
117
+ }
118
+ }
@@ -3,7 +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
+ import { startFileGuard } from './fileGuard.ts'
7
7
  import { MOBILE_CSS } from './mobile.css.ts'
8
8
  import { NS, en, zh } from './locales.ts'
9
9
  import type { MobileNavKey } from './locales.ts'
@@ -263,6 +263,15 @@ export function mobileApply(ctx): void {
263
263
  }
264
264
  }, 'dsh-mobile-nav: sheet rise animation replay')
265
265
 
266
+ // 移动端文件守卫(issue #17 修正):手机上点 dsh-web 渲染的文件链接会触发桌面
267
+ // 端 workspaces.openPath(open ...) —— 既打不开(路径在电脑上),又会抛
268
+ // "path open failed"。这里在捕获阶段拦截这类点击 / 键盘激活,改为弹一个提示,
269
+ // 并隐藏「添加工作区」入口(手机上配工作区无意义)。只挂窄屏。
270
+ ctx.effect(() => {
271
+ if (!narrow.matches) return () => {}
272
+ return startFileGuard()
273
+ }, 'dsh-mobile-nav: file open guard + hide add-workspace (issue #17)')
274
+
266
275
  ctx.slots.inject('conversation.session.header.actions', () => ctx.slots.register({
267
276
  name: 'conversation.session.header.actions',
268
277
  id: 'mobile-nav-toggle',
@@ -273,16 +282,6 @@ export function mobileApply(ctx): void {
273
282
  }),
274
283
  }, MobileNavToggle))
275
284
 
276
- // 「⛶ 放大输入」按钮(issue #23):注册到 conversation.input.right(发送键旁)
277
- // ——桌面端由 mobile.css.ts 隐藏(min-width:1024px)。点按切换 body 上的
278
- // data-dsh-pocket-composer-fullscreen 标记,由 CSS 把 composer 卡片全屏化。
279
- ctx.slots.inject('conversation.input.right', () => ctx.slots.register({
280
- name: 'conversation.input.right',
281
- id: 'mobile-composer-fullscreen',
282
- order: 100,
283
- locale: NS,
284
- }, MobileComposerFullscreen))
285
-
286
285
  ctx.slots.inject('shell.overlay', () => ctx.slots.register({
287
286
  name: 'shell.overlay',
288
287
  id: 'mobile-nav-overlay',
@@ -872,6 +872,17 @@ export const MOBILE_CSS = `
872
872
  [data-phase="hero"] [class$="_stack"] {
873
873
  gap: 0 !important;
874
874
  }
875
+
876
+ /* ---------- 隐藏「添加工作区」入口(手机上配工作区无意义,issue #17 修正) ----------
877
+ 图标按钮的 aria-label 随语言变化(zh「添加工作区」/ en「Add workspace」),
878
+ 两种都覆盖;下拉菜单里的「添加工作区…」项由 fileGuard.ts 的 MutationObserver
879
+ 按文案兜底隐藏(CSS 选不到纯文本节点)。只在窄屏生效——桌面端照常保留。 */
880
+ button[aria-label="添加工作区"],
881
+ button[aria-label="添加工作区…"],
882
+ button[aria-label="Add workspace"],
883
+ button[aria-label="Add workspace…"] {
884
+ display: none !important;
885
+ }
875
886
  }
876
887
 
877
888
  /* ---------- desktop: the mobile controls must never appear ---------- */
@@ -886,50 +897,6 @@ export const MOBILE_CSS = `
886
897
  [data-mobile-nav="drawer-actions"] {
887
898
  display: none !important;
888
899
  }
889
- /* 桌面端永远不需要「⛶ 放大输入」按钮(composer 已经有完整工具行) */
890
- [data-mobile-nav="composer-fullscreen"] {
891
- display: none !important;
892
- }
893
900
  }
894
901
 
895
- /* ---------- 放大输入(issue #23) ----------
896
- 窄屏默认隐藏插件注册到 conversation.input.* slot 的 UI(仅留官方 resident
897
- chrome),点「⛶」按钮把 composer 卡片固定到全屏后所有插件 UI 恢复显示——
898
- 新插件零适配。
899
- 实现说明:当前 dsh 0.1.1 没把 input slot 渲染成 [data-slot] 包装(0.1.2+ 才有),
900
- 所以这里用「白名单」反选:mobileApply 注册的 mobile 自身节点加
901
- data-mobile-nav="composer-fullscreen" 不被隐藏;其它插件 UI 隐藏。 */
902
- @media (max-width: 1023px) {
903
- /* 默认(非全屏):隐藏 input slot 容器下的所有直接子元素(包含插件和官方)
904
- ——官方自己如果想在窄屏也露出,注 input slot 时加 data-mobile-nav-keep="1" */
905
- [data-dsh-pocket-composer-fullscreen]:not([data-dsh-pocket-composer-fullscreen="1"])
906
- [class$="_card"]:has(textarea) [data-slot^="conversation.input."] > :not([data-mobile-nav-keep]),
907
- [data-dsh-pocket-composer-fullscreen]:not([data-dsh-pocket-composer-fullscreen="1"])
908
- [class$="_card"]:has(textarea) [data-slot^="conversation.input."]:empty {
909
- display: none !important;
910
- }
911
- /* 放大按钮:常驻在 conversation.input.right slot 旁,桌面端已被上面规则隐藏 */
912
- [data-mobile-nav="composer-fullscreen"] {
913
- display: inline-flex;
914
- }
915
- /* 全屏模式:composer 卡片固定到视口,textarea 拉高,工具行可换行 */
916
- [data-dsh-pocket-composer-fullscreen="1"] [class$="_card"]:has(textarea) {
917
- position: fixed !important;
918
- inset: 0 !important;
919
- z-index: 9999 !important;
920
- border-radius: 0 !important;
921
- margin: 0 !important;
922
- height: 100dvh !important;
923
- display: flex !important;
924
- flex-direction: column !important;
925
- }
926
- [data-dsh-pocket-composer-fullscreen="1"] [class$="_card"]:has(textarea) textarea {
927
- flex: 1 1 auto !important;
928
- min-height: 50dvh !important;
929
- max-height: none !important;
930
- }
931
- [data-dsh-pocket-composer-fullscreen="1"] [class$="_card"]:has(textarea) [data-slot^="conversation.input."] {
932
- flex-wrap: wrap !important;
933
- }
934
- }
935
902
  `
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.3"
83
+ "version": "2.7.1"
84
84
  }
@@ -1,77 +0,0 @@
1
- // 手机端「⛶ 放大输入」开关(issue #23)
2
- //
3
- // 行为:
4
- // - 点击切换 body 属性 `data-dsh-pocket-composer-fullscreen`;
5
- // - mobile.css.ts 监听这个属性,把 composer 卡片固定到全屏(textarea 拉高、工具行
6
- // 全部展开),插件 UI 注册到 conversation.input.* slot 的按钮全部恢复显示;
7
- // - 默认(无 fullscreen 属性)下,CSS 把这些 slot 全部隐藏,只保留官方 resident
8
- // chrome(权限/plan/模型/发送)。新插件只要注册到 input slot 就自动遵守此规则,
9
- // 无需逐个适配。
10
- //
11
- // 实现细节:
12
- // - 不依赖 dsh web 把 data-slot 包装暴露给选择器(0.1.1 没,0.1.2+ 才有)。我们用
13
- // `:scope > :not([data-mobile-nav-keep])` 这样的「保留」标记:所有插件如果
14
- // 想在手机未放大时也露出,加 data-mobile-nav-keep="1" 即可。默认隐藏的是
15
- // slot 容器下的所有直接子元素,**包含 dsh web 官方自己注入的**(这些是
16
- // 我们想隐藏的"插件 UI"——只要不冲突)。所以需要 dsh web 在官方自己的 UI 上
17
- // 加 `data-mobile-nav-keep` 标记——等 dsh 0.1.2+ data-slot 上线后改用那个
18
- // 更优雅(CSS 选择器变成 `[data-slot=...] > :not([data-mobile-nav-keep])`)。
19
- import { createElement as h, useEffect, useState } from 'react';
20
-
21
- const FULLSCREEN_ATTR = 'data-dsh-pocket-composer-fullscreen';
22
-
23
- function isFullscreen() {
24
- return typeof document !== 'undefined' && document.body?.getAttribute(FULLSCREEN_ATTR) === '1';
25
- }
26
-
27
- function setFullscreen(on) {
28
- if (typeof document === 'undefined') return;
29
- if (on) document.body.setAttribute(FULLSCREEN_ATTR, '1');
30
- else document.body.removeAttribute(FULLSCREEN_ATTR);
31
- }
32
-
33
- /**
34
- * 放大输入按钮:注册到 `conversation.input.right`,id `mobile-composer-fullscreen`。
35
- * 仅在窄屏(max-width:1023px)显示,桌面端由 CSS 隐藏。
36
- */
37
- export function MobileComposerFullscreen() {
38
- const [on, setOn] = useState(isFullscreen);
39
- useEffect(() => {
40
- const update = () => setOn(isFullscreen());
41
- // 与其它 effect 协调:监听任何对 body 属性的修改即可(设置项自己改也算)
42
- const observer = new MutationObserver(update);
43
- observer.observe(document.body, { attributes: true, attributeFilter: [FULLSCREEN_ATTR] });
44
- return () => observer.disconnect();
45
- }, []);
46
- return h('button', {
47
- type: 'button',
48
- 'aria-label': on ? '收起输入' : '放大输入',
49
- 'aria-pressed': on,
50
- title: on ? '收起输入' : '放大输入',
51
- 'data-mobile-nav': 'composer-fullscreen',
52
- 'data-mobile-nav-keep': '1', // 自己的按钮不该被 #23 的 slot 通用隐藏规则误伤
53
- onClick: () => {
54
- const next = !isFullscreen();
55
- setFullscreen(next);
56
- setOn(next);
57
- },
58
- style: {
59
- appearance: 'none',
60
- WebkitAppearance: 'none',
61
- border: 'none',
62
- background: 'transparent',
63
- color: 'inherit',
64
- cursor: 'pointer',
65
- padding: '0 8px',
66
- height: 32,
67
- minWidth: 32,
68
- borderRadius: 6,
69
- display: 'inline-flex',
70
- alignItems: 'center',
71
- justifyContent: 'center',
72
- font: 'inherit',
73
- fontSize: 16,
74
- lineHeight: 1,
75
- },
76
- }, on ? '⤡' : '⤢');
77
- }