dsh-remote-panel 0.3.1 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +11 -0
  2. package/lib/client.js +75 -65
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -64,6 +64,17 @@ Caddy 的 `pki/authorities/local/root.crt`)。
64
64
  - Android:设置 → 安全 → 加密与凭据 → 安装证书 → CA 证书。
65
65
  - iOS:下载后到 设置 → 通用 → 关于本机 → 证书信任设置 里打开信任。
66
66
 
67
+ ## 已知限制(有意为之 / 待后续)
68
+
69
+ | 项 | 现状 | 说明 |
70
+ |---|---|---|
71
+ | APK 签名 | debug keystore | 自己侧载够用;要公开分发需换正式签名并保留密钥 |
72
+ | APK 更新 | 重新下载覆盖安装 | 面板「手机 App」卡里有下载入口;没有应用内自动更新 |
73
+ | APK 与口令 | 壳内置门禁口令 | 换了口令要重新构建壳(`./android/build.sh [server] [key]`) |
74
+ | DSH 升级 | hash 类名可能变 | 有契约自检(面板「外壳适配」灯),失配会显红而不是静默错位 |
75
+ | 崩溃上报 | 无 | 壳里失败会显示错误文本;日志只有控制台 |
76
+ | 无障碍 | 部分 | 抽屉没有焦点陷阱;aria 只覆盖了注入的按钮 |
77
+
67
78
  ## 安全模型:令牌只在门禁之后
68
79
 
69
80
  手机端有两道门:Caddy 的口令门禁(`dsh_gate` Cookie)+ DSH 自己的会话令牌。两者的取用顺序是:
package/lib/client.js CHANGED
@@ -1639,63 +1639,6 @@ window.__ModuleLoader__.load({
1639
1639
  }
1640
1640
  }
1641
1641
 
1642
- /**
1643
- * 切换工作区后别自动聚焦输入框。
1644
- *
1645
- * DSH 打开工作区选择器时会先把焦点交给搜索框;关掉后焦点会回到输入条,
1646
- * 手机上就是键盘自己弹出来。这里在选择器关闭、或 chip 文案变化后,
1647
- * 把焦点从输入条上撤走(只撤输入条里的可编辑区,不动选择器自己的搜索框)。
1648
- */
1649
- function installComposerFocusGuard() {
1650
- if (typeof document === 'undefined' || typeof MutationObserver === 'undefined') return () => {}
1651
- let labelObserver = null
1652
- let observedLabel = null
1653
- let lastLabel = ''
1654
-
1655
- const isComposer = (el) =>
1656
- el !== null &&
1657
- el !== document.body &&
1658
- (el.isContentEditable === true || el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') &&
1659
- (el.closest('.uV2eYG_root') !== null || el.closest('[class*="_composer"]') !== null)
1660
-
1661
- const release = () => {
1662
- const el = document.activeElement
1663
- if (isComposer(el)) el.blur()
1664
- }
1665
- // DSH 可能在我们之后又把焦点塞回去,所以分几拍各撤一次
1666
- const scheduleRelease = () => {
1667
- for (const delay of [0, 80, 200, 400]) window.setTimeout(release, delay)
1668
- }
1669
-
1670
- const watchLabel = () => {
1671
- const label = document.querySelector('[class*="_workspaceLabel"]')
1672
- if (label === null || label === observedLabel) return
1673
- labelObserver?.disconnect()
1674
- observedLabel = label
1675
- lastLabel = label.textContent ?? ''
1676
- labelObserver = new MutationObserver(() => {
1677
- const now = label.textContent ?? ''
1678
- if (now === lastLabel) return
1679
- lastLabel = now
1680
- scheduleRelease()
1681
- })
1682
- labelObserver.observe(label, { childList: true, characterData: true, subtree: true })
1683
- }
1684
-
1685
- // 选择器关闭 = 里面那个搜索框失焦。用事件判断,别在每次 DOM 变动时全量 querySelector。
1686
- const onFocusOut = (event) => {
1687
- const target = event.target
1688
- if (target instanceof Element && target.closest('[class*="wsc-"]') !== null) scheduleRelease()
1689
- }
1690
- document.addEventListener('focusout', onFocusOut, true)
1691
- const ensureLabel = window.setInterval(watchLabel, 800)
1692
- watchLabel()
1693
- return () => {
1694
- document.removeEventListener('focusout', onFocusOut, true)
1695
- window.clearInterval(ensureLabel)
1696
- labelObserver?.disconnect()
1697
- }
1698
- }
1699
1642
 
1700
1643
  /**
1701
1644
  * 外壳挂点自检:DSH 升级后这些 hash 类名可能全变,样式就整体失配。
@@ -2006,30 +1949,97 @@ window.__ModuleLoader__.load({
2006
1949
  * 做法:在 pointerdown 阶段记一个短窗口(900ms),窗口内如果输入框收到 focus 就立刻 blur 回去;
2007
1950
  * 点别的地方会立刻清掉窗口,不会影响正常输入。
2008
1951
  */
1952
+ /**
1953
+ * 输入条焦点守卫:这几种操作都不该把键盘顶起来——
1954
+ * 切工作区、切会话、打开面板、点「+」菜单。
1955
+ *
1956
+ * DSH 会在操作之后把焦点塞回输入条,且时机在我们之后,
1957
+ * 所以除了撤焦点,还要开一个短暂的压制窗口(focusin 里再撤一次)。
1958
+ * 只针对输入条里的可编辑区,不拦用户自己点输入条。
1959
+ */
2009
1960
  function installComposerFocusGuard() {
2010
1961
  if (typeof document === 'undefined') return () => {}
2011
- let until = 0
2012
- const isComposerInput = (el) => el instanceof Element && el.closest?.('.uV2eYG_input') !== null
2013
- const onPointerDown = (event) => {
2014
- const target = event.target
2015
- until = target instanceof Element && target.closest('.uV2eYG_add') !== null ? Date.now() + 900 : 0
1962
+ let suppressUntil = 0
1963
+ let labelObserver = null
1964
+ let observedLabel = null
1965
+ let lastLabel = ''
1966
+ let labelTimer = 0
1967
+
1968
+ const isComposer = (el) =>
1969
+ el instanceof Element &&
1970
+ el !== document.body &&
1971
+ (el.isContentEditable === true || el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') &&
1972
+ (el.closest('.uV2eYG_root') !== null || el.closest('[class*="_composer"]') !== null)
1973
+
1974
+ const release = () => {
1975
+ const el = document.activeElement
1976
+ if (isComposer(el)) el.blur()
1977
+ }
1978
+ // 分几拍撤一次,并留一段压制窗口覆盖 DSH 自己的 focus
1979
+ const scheduleRelease = (windowMs = 700) => {
1980
+ suppressUntil = Math.max(suppressUntil, Date.now() + windowMs)
1981
+ for (const delay of [0, 80, 200, 400, 700]) window.setTimeout(release, delay)
2016
1982
  }
1983
+
2017
1984
  const onFocusIn = (event) => {
2018
- if (Date.now() > until) return
1985
+ if (Date.now() > suppressUntil) return
2019
1986
  const el = event.target
2020
- if (!isComposerInput(el)) return
1987
+ if (!isComposer(el)) return
2021
1988
  const drop = () => {
2022
- if (Date.now() <= until && document.activeElement === el) el.blur?.()
1989
+ if (Date.now() <= suppressUntil && document.activeElement === el) el.blur?.()
2023
1990
  }
2024
1991
  drop()
2025
1992
  // 有的实现会在下一帧再 focus 一次,补一刀
2026
1993
  requestAnimationFrame(drop)
2027
1994
  }
1995
+
1996
+ // 「+」按钮弹菜单,别顺手把焦点交给输入条
1997
+ const onPointerDown = (event) => {
1998
+ const target = event.target
1999
+ if (target instanceof Element && target.closest('.uV2eYG_add') !== null) suppressUntil = Date.now() + 900
2000
+ }
2001
+
2002
+ // 抽屉里选会话 / 打开面板:切完别把键盘顶起来
2003
+ const onClick = (event) => {
2004
+ const target = event.target
2005
+ if (!(target instanceof Element)) return
2006
+ if (target.closest('[class*="_sessionRow"], [class*="_panelRow"]') !== null) scheduleRelease(900)
2007
+ }
2008
+
2009
+ // 工作区选择器关闭 = 里面那个搜索框失焦
2010
+ const onFocusOut = (event) => {
2011
+ const target = event.target
2012
+ if (target instanceof Element && target.closest('[class*="wsc-"]') !== null) scheduleRelease()
2013
+ }
2014
+
2015
+ const watchLabel = () => {
2016
+ const label = document.querySelector('[class*="_workspaceLabel"]')
2017
+ if (label === null || label === observedLabel) return
2018
+ labelObserver?.disconnect()
2019
+ observedLabel = label
2020
+ lastLabel = label.textContent ?? ''
2021
+ labelObserver = new MutationObserver(() => {
2022
+ const now = label.textContent ?? ''
2023
+ if (now === lastLabel) return
2024
+ lastLabel = now
2025
+ scheduleRelease()
2026
+ })
2027
+ labelObserver.observe(label, { childList: true, characterData: true, subtree: true })
2028
+ }
2029
+
2028
2030
  document.addEventListener('pointerdown', onPointerDown, true)
2029
2031
  document.addEventListener('focusin', onFocusIn, true)
2032
+ document.addEventListener('click', onClick, true)
2033
+ document.addEventListener('focusout', onFocusOut, true)
2034
+ labelTimer = window.setInterval(watchLabel, 800)
2035
+ watchLabel()
2030
2036
  return () => {
2031
2037
  document.removeEventListener('pointerdown', onPointerDown, true)
2032
2038
  document.removeEventListener('focusin', onFocusIn, true)
2039
+ document.removeEventListener('click', onClick, true)
2040
+ document.removeEventListener('focusout', onFocusOut, true)
2041
+ window.clearInterval(labelTimer)
2042
+ labelObserver?.disconnect()
2033
2043
  }
2034
2044
  }
2035
2045
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-remote-panel",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "DSH 远程访问控制面:部署向导、状态面板与一键配对(EasyTier + Caddy),数据面保持进程外。",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",