dsh-remote-panel 0.3.1 → 0.3.3

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 +85 -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
@@ -1170,6 +1170,16 @@ window.__ModuleLoader__.load({
1170
1170
  .pXSMma_previewBadge { display: none !important; }
1171
1171
  /* 原生把「问候语 + 输入条」一起居中;手机按原型拆开:
1172
1172
  问候语留在屏幕中间(原生居中),输入条钉到底部 */
1173
+ /* 输入条座位顶部有一段渐隐(DSH 原本渐到 #151517),在纯黑底上会显出一条灰带,
1174
+ 这里把终点色换成纯黑,观感才对得上;底部那行状态(轮次/token/缓存)同理 */
1175
+ body[data-ds-dark-theme] .wSkVaW_composerSeat,
1176
+ body[data-ds-dark-theme] .wSkVaW_composerHero {
1177
+ background-image: linear-gradient(rgba(0, 0, 0, 0) 0, #000 36px) !important;
1178
+ }
1179
+ body[data-ds-dark-theme] [class*="_bottomRow"] {
1180
+ background: #000 !important;
1181
+ }
1182
+
1173
1183
  /* 会话里的输入条座位:底部要让开手势条(WebView 里 env() 常为 0,由壳注入) */
1174
1184
  .wSkVaW_composerSeat {
1175
1185
  padding-bottom: max(env(safe-area-inset-bottom, 0px), var(--dsh-safe-bottom, 0px)) !important;
@@ -1639,63 +1649,6 @@ window.__ModuleLoader__.load({
1639
1649
  }
1640
1650
  }
1641
1651
 
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
1652
 
1700
1653
  /**
1701
1654
  * 外壳挂点自检:DSH 升级后这些 hash 类名可能全变,样式就整体失配。
@@ -2006,30 +1959,97 @@ window.__ModuleLoader__.load({
2006
1959
  * 做法:在 pointerdown 阶段记一个短窗口(900ms),窗口内如果输入框收到 focus 就立刻 blur 回去;
2007
1960
  * 点别的地方会立刻清掉窗口,不会影响正常输入。
2008
1961
  */
1962
+ /**
1963
+ * 输入条焦点守卫:这几种操作都不该把键盘顶起来——
1964
+ * 切工作区、切会话、打开面板、点「+」菜单。
1965
+ *
1966
+ * DSH 会在操作之后把焦点塞回输入条,且时机在我们之后,
1967
+ * 所以除了撤焦点,还要开一个短暂的压制窗口(focusin 里再撤一次)。
1968
+ * 只针对输入条里的可编辑区,不拦用户自己点输入条。
1969
+ */
2009
1970
  function installComposerFocusGuard() {
2010
1971
  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
1972
+ let suppressUntil = 0
1973
+ let labelObserver = null
1974
+ let observedLabel = null
1975
+ let lastLabel = ''
1976
+ let labelTimer = 0
1977
+
1978
+ const isComposer = (el) =>
1979
+ el instanceof Element &&
1980
+ el !== document.body &&
1981
+ (el.isContentEditable === true || el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') &&
1982
+ (el.closest('.uV2eYG_root') !== null || el.closest('[class*="_composer"]') !== null)
1983
+
1984
+ const release = () => {
1985
+ const el = document.activeElement
1986
+ if (isComposer(el)) el.blur()
1987
+ }
1988
+ // 分几拍撤一次,并留一段压制窗口覆盖 DSH 自己的 focus
1989
+ const scheduleRelease = (windowMs = 700) => {
1990
+ suppressUntil = Math.max(suppressUntil, Date.now() + windowMs)
1991
+ for (const delay of [0, 80, 200, 400, 700]) window.setTimeout(release, delay)
2016
1992
  }
1993
+
2017
1994
  const onFocusIn = (event) => {
2018
- if (Date.now() > until) return
1995
+ if (Date.now() > suppressUntil) return
2019
1996
  const el = event.target
2020
- if (!isComposerInput(el)) return
1997
+ if (!isComposer(el)) return
2021
1998
  const drop = () => {
2022
- if (Date.now() <= until && document.activeElement === el) el.blur?.()
1999
+ if (Date.now() <= suppressUntil && document.activeElement === el) el.blur?.()
2023
2000
  }
2024
2001
  drop()
2025
2002
  // 有的实现会在下一帧再 focus 一次,补一刀
2026
2003
  requestAnimationFrame(drop)
2027
2004
  }
2005
+
2006
+ // 「+」按钮弹菜单,别顺手把焦点交给输入条
2007
+ const onPointerDown = (event) => {
2008
+ const target = event.target
2009
+ if (target instanceof Element && target.closest('.uV2eYG_add') !== null) suppressUntil = Date.now() + 900
2010
+ }
2011
+
2012
+ // 抽屉里选会话 / 打开面板:切完别把键盘顶起来
2013
+ const onClick = (event) => {
2014
+ const target = event.target
2015
+ if (!(target instanceof Element)) return
2016
+ if (target.closest('[class*="_sessionRow"], [class*="_panelRow"]') !== null) scheduleRelease(900)
2017
+ }
2018
+
2019
+ // 工作区选择器关闭 = 里面那个搜索框失焦
2020
+ const onFocusOut = (event) => {
2021
+ const target = event.target
2022
+ if (target instanceof Element && target.closest('[class*="wsc-"]') !== null) scheduleRelease()
2023
+ }
2024
+
2025
+ const watchLabel = () => {
2026
+ const label = document.querySelector('[class*="_workspaceLabel"]')
2027
+ if (label === null || label === observedLabel) return
2028
+ labelObserver?.disconnect()
2029
+ observedLabel = label
2030
+ lastLabel = label.textContent ?? ''
2031
+ labelObserver = new MutationObserver(() => {
2032
+ const now = label.textContent ?? ''
2033
+ if (now === lastLabel) return
2034
+ lastLabel = now
2035
+ scheduleRelease()
2036
+ })
2037
+ labelObserver.observe(label, { childList: true, characterData: true, subtree: true })
2038
+ }
2039
+
2028
2040
  document.addEventListener('pointerdown', onPointerDown, true)
2029
2041
  document.addEventListener('focusin', onFocusIn, true)
2042
+ document.addEventListener('click', onClick, true)
2043
+ document.addEventListener('focusout', onFocusOut, true)
2044
+ labelTimer = window.setInterval(watchLabel, 800)
2045
+ watchLabel()
2030
2046
  return () => {
2031
2047
  document.removeEventListener('pointerdown', onPointerDown, true)
2032
2048
  document.removeEventListener('focusin', onFocusIn, true)
2049
+ document.removeEventListener('click', onClick, true)
2050
+ document.removeEventListener('focusout', onFocusOut, true)
2051
+ window.clearInterval(labelTimer)
2052
+ labelObserver?.disconnect()
2033
2053
  }
2034
2054
  }
2035
2055
 
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.3",
4
4
  "description": "DSH 远程访问控制面:部署向导、状态面板与一键配对(EasyTier + Caddy),数据面保持进程外。",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",