dsh-mobile 0.6.17 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +47 -11
  2. package/CONTRIBUTORS.md +12 -6
  3. package/README.en.md +22 -14
  4. package/README.md +21 -14
  5. package/SECURITY.md +1 -1
  6. package/THIRD_PARTY_NOTICES.md +33 -0
  7. package/apps/mobile/README.md +28 -3
  8. package/apps/mobile/README.zh-CN.md +28 -3
  9. package/apps/mobile/release.json +3 -3
  10. package/cordis.patch.yml +5 -9
  11. package/docs/ATTACH_EXISTING_FRPS.en.md +32 -2
  12. package/docs/ATTACH_EXISTING_FRPS.md +2 -2
  13. package/docs/CLIENT_MODULES.en.md +2 -0
  14. package/docs/CLIENT_MODULES.md +2 -0
  15. package/docs/CLOUDFLARE_TUNNEL.en.md +2 -0
  16. package/docs/CLOUDFLARE_TUNNEL.md +2 -0
  17. package/docs/IROH.en.md +44 -0
  18. package/docs/IROH.md +44 -0
  19. package/docs/MOBILE_PLUGIN_ADAPTATION.en.md +196 -0
  20. package/docs/MOBILE_PLUGIN_ADAPTATION.md +196 -0
  21. package/docs/README.md +9 -0
  22. package/docs/SELF_HOSTED_ORIGIN.en.md +26 -1
  23. package/docs/SELF_HOSTED_ORIGIN.md +27 -2
  24. package/docs/SKIN_COMPATIBILITY.en.md +2 -0
  25. package/docs/SKIN_COMPATIBILITY.md +2 -0
  26. package/docs/SLOW_CONNECTIONS.en.md +10 -2
  27. package/docs/SLOW_CONNECTIONS.md +10 -2
  28. package/docs/TRUSTED_NETWORKS.en.md +17 -0
  29. package/docs/TRUSTED_NETWORKS.md +17 -0
  30. package/lib/cli.js.map +1 -1
  31. package/lib/client.js +1081 -70
  32. package/lib/client.js.map +1 -1
  33. package/lib/index.d.mts +271 -2
  34. package/lib/index.mjs +1908 -245
  35. package/lib/index.mjs.map +1 -1
  36. package/lib/mobile-layout.js +14 -6
  37. package/lib/mobile-layout.js.map +1 -1
  38. package/native/iroh-host/LICENSE.dsh-mobile-iroh +21 -0
  39. package/native/iroh-host/LICENSE.dsh-tether +21 -0
  40. package/native/iroh-host/NOTICE.dsh-mobile-iroh +13 -0
  41. package/native/iroh-host/README.md +66 -0
  42. package/package.json +18 -5
  43. package/packages/question-fixes/package.json +1 -1
@@ -4,14 +4,14 @@
4
4
 
5
5
  DeepSeek Harness 是这个社区维护的 Android WebView App 的显示名称。它直接打开插件提供的已认证 HTTPS 页面,不打包另一套 DSH 前端;App 与手机浏览器获得相同的 DSH 功能,以及电脑端配置的移动页面定制。
6
6
 
7
- 当前正式 App 为 **0.6.7**;构建号为 83。npm 插件与 Android App 独立编号、独立更新;插件升级不代表需要重新安装 APK。此次改进见 [0.6.7 更新](#067-更新)。
7
+ 当前正式 App 为 **0.7.0**;构建号为 85。npm 插件与 Android App 独立编号、独立更新;插件升级不代表需要重新安装 APK。此次改进见 [0.7.0 更新](#070-更新)。
8
8
 
9
9
  支持 Android 10 及以上系统。iOS 本地实验不属于已发布或受支持的客户端。
10
10
 
11
11
  ## 安装与配对
12
12
 
13
13
  1. 按[项目快速开始](../../README.md#快速开始)安装并启用插件。局域网配置可通过命令行或电脑端的 **移动访问** 面板完成。
14
- 2. 从 [GitHub Releases](https://github.com/saya-ch/dsh-mobile/releases/tag/android-v0.6.7) 安装已签名 APK。正式版沿用相同发布签名,可原位升级并保留配对;不同签名的 Debug 包不能直接覆盖正式版,不要为绕过签名不匹配而卸载或清除数据。
14
+ 2. 从 [GitHub Releases](https://github.com/saya-ch/dsh-mobile/releases/tag/android-v0.7.0) 安装已签名 APK。原有 HTTPS 通道可选兼容版 `.apk`;arm64-v8a/x86_64 设备使用实验性 iroh 时选完整版 `-iroh.apk`。两包使用相同包名和正式签名,选一个原位升级即可保留配对;不同签名的 Debug 包不能直接覆盖正式版,不要为绕过签名不匹配而卸载或清除数据。
15
15
  3. 在电脑 **移动访问 → 局域网访问** 中生成密钥/配对链接,或配置远程通道并生成远程配对二维码。在 App 选择对应方式,扫码或粘贴链接。
16
16
  4. 为电脑命名。每条局域网或远程配对单独保存;手机无需安装 cpolar、Cloudflare 或 Tailscale App。
17
17
 
@@ -36,6 +36,16 @@ DeepSeek Harness 是这个社区维护的 Android WebView App 的显示名称。
36
36
 
37
37
  删除本机记录会移除保存的设备令牌和固定 CA,不等于撤销电脑授权或清除所有 WebView 短期 Cookie。需要立即终止访问时,请在电脑的 **移动访问** 中撤销设备。Android“清除应用数据”会删除全部配对和网页数据,不是普通重连的处理步骤。
38
38
 
39
+ ## 0.7.0 更新
40
+
41
+ App 0.7.0/构建 85 在完整版中增加可选的实验性 iroh 配对和原生传输,配合插件 0.7.0 及以上版本使用。兼容版不含原生库,保留现有 HTTPS 通道并继续支持 32 位设备。旧版更新入口默认下载兼容版;使用 iroh 时请主动选择完整版。两包均保留设备列表、HTTPS 配对和原有正式签名。独立实验 App 的记录不会自动迁移。
42
+
43
+ iroh 默认关闭。电脑端安装经过校验的组件后,在 **移动访问 → 远程 → iroh(实验性)** 中生成专属配对二维码。直连和中继可用性取决于两端网络,手机浏览器不能直接使用这条传输,详见 [iroh 使用说明](../../docs/IROH.md)。
44
+
45
+ ## 0.6.8 更新
46
+
47
+ App 0.6.8/构建 84 让首次页面下载沿用既有连接总期限,不再被较短页面计时器打断后自动重新下载。首次加载确实失败时保留明确重试;已经正常打开的页面仍可恢复连接,TLS 验证和取消能力不变。这项原生修复需要升级 App,不能仅靠更新插件获得。
48
+
39
49
  ## 0.6.7 更新
40
50
 
41
51
  App 0.6.7/构建 83 配合插件 0.6.16 及以上版本,新增可选的同电脑局域网/远程切换,默认仍为手动。分别配对两个入口后,在设备列表设置中选择切换方式;符合条件的同会话纯文本草稿可随自动切换迁移,但这不是完整页面状态迁移,详见[切换规则](#同电脑自动连接)。
@@ -147,12 +157,27 @@ Set-Location apps/mobile/android
147
157
 
148
158
  Debug APK 位于 `app/build/outputs/apk/debug/app-debug.apk`。正式发布沿用只保存在仓库 Secrets 中的签名密钥,本地未签名 Release 构建或 Debug 安装不能替代正式签名核验。
149
159
 
160
+ 每次构建默认自带实验性 iroh 传输。需要 Rust 1.99.1(含 `aarch64-linux-android` 与 `x86_64-linux-android` 目标)与明确指定的 NDK:
161
+
162
+ ```powershell
163
+ Set-Location apps/mobile/android
164
+ ./gradlew.bat :app:assembleDebug -PirohNdk=C:/Android/Sdk/ndk/25.2.9519653 -x :app:lintAnalyzeDebugUnitTest -x :app:lintAnalyzeDebugAndroidTest
165
+ ```
166
+
167
+ 传 `-PirohNative=false` 可走免工具链的快速通道,构建的是原 HTTPS 外壳:没有 iroh 配对入口,`IrohNative` 会报告自身不可用。传输库生成在 `app/build/generated/iroh/jniLibs/` 下,永不提交。启用原生库的构建不改变包名与既定正式签名;实验阶段的独立 alpha 签名在这里不使用。
168
+
169
+ 原生构建还会从固定提交的官方 Maven 归档引入与 `native/Cargo.lock` 匹配的 JVM TLS 校验组件。必须保留其 JNI/Proguard 规则,仅打包 `.so` 不足以完成接入。APK 验证会检查 Debug 和压缩后的 Release 是否保留所需 JVM 类和原生入口名称。
170
+
150
171
  当前源码的 Debug 构建使用包名 `io.github.sayach.dshmobile.debug` 与 `-debug` 版本后缀,可与正式 App 并存,存储和配对彼此独立。它不会升级或迁移旧版无后缀 Debug 应用;切换包名时应保留旧应用和数据,不要为此卸载或清除。Release 构建继续使用 `io.github.sayach.dshmobile` 与既定正式签名。
151
172
 
152
- 已核验的正式版本为 [Android 0.6.7](https://github.com/saya-ch/dsh-mobile/releases/tag/android-v0.6.7),构建 83,SHA-256:`919153244e6b84eb55c4701146f50454995330e080da9c91389e8816b2bd9b79`。下载的 APK 沿用正式发布签名及包名 `io.github.sayach.dshmobile`。旧版本历史集中在 [CHANGELOG](../../CHANGELOG.md)。
173
+ 已核验的正式版本为 [Android 0.7.0](https://github.com/saya-ch/dsh-mobile/releases/tag/android-v0.7.0),构建 85。兼容版与完整版分别附有对应的 `.sha256` 校验文件;两个公开下载均沿用正式发布签名及包名 `io.github.sayach.dshmobile`。旧版本历史集中在 [CHANGELOG](../../CHANGELOG.md)。
153
174
 
154
175
  ## 验收与已知限制
155
176
 
177
+ - App 0.7.0 的两份公开 APK 均通过校验值、Manifest、正式签名、许可文本和对齐检查;完整版另检查两个 ABI 及保留的 JVM TLS/JNI 入口,兼容版确认没有原生库。此前的 arm64 开发构建有不依赖 ADB 转发的原生 iroh 网关/定制资源真机链路证据;最后补齐 JVM TLS 依赖后尚未新增真机公网中继回归,不能仅凭中继连接存在宣称数据已走中继。
178
+
179
+ - App 0.6.8 的公开 APK 已核验身份、校验值和既定正式签名。Debug 包同签名原位升级后可打开 DSH,并保留原设备记录;受控慢链路故障注入在配对前中断,不计为通过。
180
+
156
181
  - App 0.6.7 Debug 构建在 PJW110、Android 16、WebView 153 上,通过 USB 映射和固定 CA 的 HTTPS 连接隔离的 DSH 0.2.1-rc.2。真实自动接管保留同会话 Unicode 草稿,目标原生缩放/续期可用且不额外刷新;还通过确认取消、目标加载中按返回取消与迟到响应、原生续期取消、键盘返回和选择器取消。临时配对与映射已清理;这不是实际 Wi-Fi/蜂窝切网,也不是正式签名 APK 的装机验收。
157
182
  - 连接控制的开发包使用 Debug 元数据 0.6.4/构建 80,在 PJW110、Android 16、Google WebView 153.0.8010.36 上,通过 USB 和固定 CA 的 HTTPS 连接隔离的 DSH 0.2.0-rc.2/插件 0.6.10。检查了配对/启动/已保存连接取消、系统返回、手动失败收尾,以及停止原生恢复后从失效页面返回;暂停模块响应时也验证了提前露出网页、非按钮区域触摸透传、取消和放行后完成。原配对保留,临时 QA 记录已清理。
158
183
  - App 0.6.4 的 Debug 构建在同一 WebView 中覆盖文档替换/刷新、拒绝旧选择、接收受控 PNG 字节并形成已解码草稿预览;还检查了暂停导航及 HTTP 204 后选择器的可用性。更早的检查覆盖模型菜单返回、旋转保留草稿/引用/图片、已知撤销、重新配对、图标选择与列表顺序,均未发送模型请求。
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.6.7",
3
- "versionCode": 83,
4
- "releaseTag": "android-v0.6.7"
2
+ "version": "0.7.0",
3
+ "versionCode": 85,
4
+ "releaseTag": "android-v0.7.0"
5
5
  }
package/cordis.patch.yml CHANGED
@@ -1,15 +1,11 @@
1
1
  # Community bundle for the stock DSH Web profile. Installation contributes a
2
2
  # Host gateway and a browser Client face; it does not patch DSH source files.
3
3
  #
4
- # The directory picker is deliberately NOT composed here. Since DSH 0.2.1 the
5
- # `directory-picker` row is `@deepseek-ai/dsh-host-directory-picker-auto`, which
6
- # resolves the backend once at boot and mounts both its Host and Client faces
7
- # itself; the official Desktop no longer inserts its own pair. Inserting a second
8
- # picker here therefore collides on the single `directoryFlow` slots and on the
9
- # `directoryPicker` service, which makes the "Add workspace" action and its
10
- # Ctrl+O shortcut disappear. The dedicated Mobile page registers its own in-page
11
- # flow from the client face instead (see src/client.ts), so the desktop keeps its
12
- # native OS chooser without a second Host entry.
4
+ # Leave DSH's adaptive Host/Client directory-picker pair intact. A second Host
5
+ # provider conflicts with its single service; disabling the default can leave
6
+ # workspace actions without a picker. The dedicated Mobile page reuses an
7
+ # upstream browse surface when present and otherwise registers its own web flow
8
+ # through public workspace slots, without replacing the Desktop's system chooser.
13
9
  - insert:
14
10
  - id: mobile-access
15
11
  name: dsh-mobile
@@ -28,18 +28,42 @@ Plugin 0.6.16 adds Chinese, English and Italian field-level feedback: submission
28
28
  ## Public-CA mode
29
29
 
30
30
  1. In **Self-hosted FRP**, choose **Attach to my existing frps** and **Public CA certificate**. Enter the VPS address, frps control port, high-entropy shared token, public domain or IPv4 HTTPS origin, and the frps instance's actual `vhostHTTPPort`. Do not assume the managed deployment's default of 7080.
31
- 2. Copy the attachment plan. Confirm the plaintext vhost is loopback-only, then add the generated Caddy snippet and `import` manually. Caddy manages domain certificates. For public IPv4, the current plan pins Certbot 5.8.0 and uses `--standalone --preferred-profile shortlived --ip-address` for a [roughly six-day Let's Encrypt IP certificate](https://letsencrypt.org/2026/03/11/shorter-certs-certbot). Standalone issuance briefly stops Caddy to free port 80 and may interrupt other sites on the VPS; schedule a maintenance window, choose a domain, or use the self-signed TCP mode in app 0.4.6 or later if that interruption is unacceptable. The attach path only checks an existing `certbot.timer`; it does not install renewal or a certificate-copy/reload hook. Arrange renewal, installation into Caddy, and reload before expiry yourself. Copying the plan does not change the VPS.
31
+ 2. Copy the attachment plan. Confirm the plaintext vhost is loopback-only, then add the generated Caddy snippet at `/etc/caddy/dsh-mobile-dsh.caddy` and its `import` line at the top of the Caddyfile manually. Caddy manages domain certificates. For public IPv4, the current plan pins Certbot 5.8.0 and uses `--standalone --preferred-profile shortlived --ip-address` for a [roughly six-day Let's Encrypt IP certificate](https://letsencrypt.org/2026/03/11/shorter-certs-certbot). Standalone issuance briefly stops Caddy to free port 80 and may interrupt other sites on the VPS; schedule a maintenance window, choose a domain, or use the self-signed TCP mode in app 0.4.6 or later if that interruption is unacceptable. The attach path only checks an existing `certbot.timer`; it does not install renewal or a certificate-copy/reload hook. Arrange renewal, installation into Caddy, and reload before expiry yourself. Copying the plan does not change the VPS.
32
32
  3. Install the pinned official `frpc` on the computer, save and verify the connection, and test `/mobile-access/discovery` from an independent external network with normal certificate validation. Confirm HTTP 200 and this computer's installation identifier before pairing through the app's **Remote access** flow or a phone browser. A local self-check alone does not verify public reachability.
33
33
 
34
34
  ## Self-signed passthrough mode
35
35
 
36
36
  The computer-side gateway terminates TLS with a five-year CA and 397-day public-IPv4 leaf. The leaf can renew under the same CA; an expired or replaced CA requires a new fingerprint check and pairing. QR links and App keys carry the CA-required `dsh2` marker, rejected by older apps. Supported apps keep this CA in their private connection records, never Android's system trust store.
37
37
 
38
+ > Topology: `phone ──HTTPS(:33080)──▶ frps TCP proxy (passthrough) ──encrypted tunnel──▶ local frpc ──▶ 127.0.0.1:<gateway HTTPS port> ──▶ dsh web 127.0.0.1:3080`
39
+
38
40
  1. In **Self-hosted FRP**, enter the VPS address, frps control port, token, and `https://YOUR_PUBLIC_IPV4`. Choose **Attach to my existing frps**, **Self-signed passthrough**, and the public TCP port (default 33080; not 3080, 3443, or 3444).
39
41
  2. Copy and review the masked `frpc.toml` and VPS checklist. This is read-only: finish the server checks before selecting **Save and verify connection**.
40
42
  3. On the VPS, allow the chosen TCP entry port in its host firewall and cloud security group; the existing frps control port must already be reachable from the computer. Confirm frps is running and the TCP proxy listens on an address reachable from the public network. The plugin does not change the server's frps configuration or firewall.
43
+
44
+ ```sh
45
+ ufw allow 33080/tcp
46
+ ufw status | grep 33080
47
+ ```
48
+
49
+ With firewalld or a cloud security group, allow the same **TCP 33080**. Then check the frps service and its listeners (use your own service manager if frps does not run under systemd):
50
+
51
+ ```sh
52
+ systemctl is-active frps || true
53
+ ss -lnt | grep -E ':(<your bindPort>|33080)\b' || true
54
+ ```
55
+
56
+ The control port should already be listening. A proxy entry such as 33080 usually appears only after frpc registers successfully, so check its actual address once the connection is up.
41
57
  4. Install official frpc and select **Save and verify connection**. The plugin now writes the config in private `remote/frp/config/` (Unix `0600`, restricted Windows ACL), then runs `frpc verify` before startup. Clear the clipboard if you explicitly copied a token-bearing config. A public TCP proxy port normally appears only after frpc registers; check its actual listener then.
42
- 5. From another network, check `curl -k -sS -o /dev/null -w '%{http_code}\n' https://YOUR_PUBLIC_IPV4:33080/mobile-access/discovery`, replacing the host/port. HTTP 200 proves reachability only: `-k` does not verify identity. Pair through **Remote access** in app 0.4.6 or later; it fetches the CA without device credentials, checks the `dsh2` fingerprint and pins the exact Origin. CA HTTP 404 cannot fall back to platform trust. An Origin or CA change requires new pairing.
58
+ 5. From another network, check the public entry end to end, replacing the host and port with your own:
59
+
60
+ ```sh
61
+ # Replace YOUR_PUBLIC_IPV4 and 33080, then run this from an external network.
62
+ curl -k -sS -o /dev/null -w '%{http_code}\n' https://YOUR_PUBLIC_IPV4:33080/mobile-access/discovery
63
+ # Expected output: 200. `-k` skips curl's certificate check and proves reachability only, not identity.
64
+ ```
65
+
66
+ HTTP 200 proves reachability only: `-k` does not verify identity. Pair through **Remote access** in app 0.4.6 or later; it fetches the CA without device credentials, checks the `dsh2` fingerprint and pins the exact Origin. CA HTTP 404 cannot fall back to platform trust. An Origin or CA change requires new pairing.
43
67
 
44
68
  ## Troubleshooting and cleanup
45
69
 
@@ -53,5 +77,11 @@ The computer-side gateway terminates TLS with a five-year CA and 397-day public-
53
77
  | `frp_proxy_name_in_use` | frps keeps proxy names unique across **all** its clients, and this installation's own name (`dsh-mobile-<first 12 characters of the installation identity>`) is already registered — usually another DSH computer, or this computer's previous session before the frps heartbeat expires it. Disable self-hosted FRP there, or remove that proxy on the frps. frpc keeps running without publishing the entry port, so check proxy registration rather than relying on process liveness. |
54
78
  | `frp_remote_port_in_use` | Another proxy on the same frps already owns the chosen entry port. Ports are first-come, first-served per server, so give this computer a different `publicPort` and allow it in the firewall. |
55
79
  | Public entry unreachable | Check the chosen port, cloud and host firewalls, frps listener address, running `frpc`, and the external-network probe. A computer-side probe is not proof that a phone can connect. |
80
+ | App reports "connected to another DSH" | The public entry points at a different DSH: check `remotePort`, and confirm frpc is running on this computer. |
81
+
82
+ ## Relation to the other channels
83
+
84
+ - The **`origin` own-reverse-proxy channel** (default 3444) is a separate path: TLS is terminated by **your own external proxy** and it is independent of this mode.
85
+ - **Deploy mode** (the plugin installs frps + Caddy) is unchanged and still needs SSH; attach mode needs **zero SSH**.
56
86
 
57
87
  **Remove FRP completely** deletes only plugin-managed FRP files on the computer, including the local `frpc` configuration and self-signed ingress material. The non-secret plugin `installation-id` remains so a later FRP connection keeps the same proxy name; removing all DSH Mobile private state removes that identifier too. The attach path never deletes or restarts your existing frps, Caddy site, certificates, or firewall rules on the VPS. Managed deployment and its separate server cleanup are described in the [self-hosted FRP guide](SELF_HOSTED_FRP.en.md).
@@ -77,7 +77,7 @@
77
77
 
78
78
  ### 3. 本机连接,再让手机配对
79
79
 
80
- 5. 按需安装官方 frpc,点击「保存并验证连接」。配置此时才写入私有 `remote/frp/config/frpc.toml`(Unix `0600`,Windows 受限 ACL),启动前运行 `frpc verify`。若明确复制过含 Token 的配置,请及时清理剪贴板。
80
+ 5. 按需安装官方 frpc,点击「保存配置并验证连接」。配置此时才写入私有 `remote/frp/config/frpc.toml`(Unix `0600`,Windows 受限 ACL),启动前运行 `frpc verify`。若明确复制过含 Token 的配置,请及时清理剪贴板。
81
81
 
82
82
  6. 在电脑面板「远程访问」生成**远程配对二维码 / 配对链接**(含 CA 指纹、一次性 Token 和必须固定 CA 的 `dsh2` 标记)。
83
83
  7. 用 Android App 扫码(或粘贴链接)完成配对,即可远程进入 DSH。
@@ -102,7 +102,7 @@ curl -k -sS -o /dev/null -w '%{http_code}\n' https://YOUR_PUBLIC_IPV4:33080/mobi
102
102
  | `frp_attach_cert_unknown` | 自签入口证书缺失或不可读;检查面板证书状态与私有目录的 `remote/ingress/`,不要绕过指纹验证。 |
103
103
  | `frp_ingress_ca_expired` | 自签 CA 已过期,插件不会静默替换受信任 CA;重新生成 CA 后,手机须核对新指纹并重新配对。Android App 0.6.3 及更新版本仍可读取、删除该设备记录,但 TLS 继续拒绝过期证书。若使用 0.6.2 或更早版本且设备库因此不可读,请保留 App 数据,并按 [App 手册](../apps/mobile/README.zh-CN.md)安装 0.6.3 或更新的同签名正式版,不要为一台电脑清除全部配对。 |
104
104
  | 手机浏览器提示证书不受信任 | 自签档的正常现象;请在 App 内配对使用 |
105
- | 自检显示「公网入口不可达」 | 端口未放行、frpc 未启动,或 frps 未把该 `remotePort` 转发出来 |
105
+ | 自检显示「公网入口 TCP 端口不可达」 | 端口未放行、frpc 未启动,或 frps 未把该 `remotePort` 转发出来 |
106
106
  | `frp_proxy_name_in_use` | frps 的代理名在**整台服务器**上唯一。本机安装名(`dsh-mobile-<安装标识前 12 位>`)已被占用:多半是另一台 DSH 电脑,或本机上一次连接尚未等到 frps 心跳超时。到那台机器上关闭自建 FRP,或在 frps 上移除该代理。frpc 此时仍在运行、只是入口端口没被发布,应检查代理是否注册成功,不能仅凭进程存在判断就绪。 |
107
107
  | `frp_remote_port_in_use` | 同一台 frps 上该入口端口已被别的代理占用(端口先到先得)。给这台电脑换一个 `publicPort`,并在防火墙/安全组放行。 |
108
108
  | App 报「连接到另一台 DSH」 | 公网入口指向了别的 DSH:检查 `remotePort` 与 frpc 是否在本机运行 |
@@ -8,6 +8,8 @@ Choose which client components the dedicated mobile page loads. This does not un
8
8
 
9
9
  Selection applies to whole module IDs, not individual buttons, cards or sidebars inside a module. A plugin may register several modules or share/depend on a module with other features. A feature not delivered as an optional client module may not be independently removable here.
10
10
 
11
+ The selection covers client modules only. A plugin may also inject page-level styles from the host into every page — wallpapers and whole-page palettes are the usual case. That content is not a client module, so deselecting one does not remove it: disable the whole plugin, or let the plugin adapt itself to the mobile layout. See [skins and mobile layout](SKIN_COMPATIBILITY.en.md) for skin diagnosis.
12
+
11
13
  ## Computer defaults and this device
12
14
 
13
15
  - On the **computer: Settings → General → Mobile page modules → Manage**, set defaults for this computer's mobile access. Enable a LAN or remote access entry first so the current DSH module catalog can be read.
@@ -8,6 +8,8 @@
8
8
 
9
9
  选择作用于完整模块 ID,不能分别隐藏同一模块中的按钮、卡片或侧栏。一个插件可能注册多个模块,也可能与其他功能共用或依赖某个模块;未拆为可选客户端模块的功能不一定能在这里单独关闭。
10
10
 
11
+ 模块选择只作用于客户端模块。插件还可以在服务端把样式直接注入每个页面——典型就是皮肤壁纸和整页配色;这类内容不是客户端模块,取消勾选不会让它消失。要彻底关掉请禁用整个插件,或由插件自己按移动布局适配。皮肤类插件的排查见[皮肤与移动布局](SKIN_COMPATIBILITY.md)。
12
+
11
13
  ## 电脑默认与本设备选择
12
14
 
13
15
  - **电脑端:设置 → 通用设置 → 移动页面模块 → 管理**,修改此电脑的移动访问默认加载项。先开启一个局域网或远程访问入口,才能读取当前 DSH 的模块目录。
@@ -2,6 +2,8 @@
2
2
 
3
3
  [中文指南](CLOUDFLARE_TUNNEL.md)
4
4
 
5
+ > Already running cloudflared or a system service? Use the [own-reverse-proxy mode](SELF_HOSTED_ORIGIN.en.md#when-you-already-run-cloudflared-yourself) and route that tunnel to the protected HTTP backend shown in the panel, without another component installation or a tunnel Token stored by the plugin. The instructions below describe the separate plugin-managed option.
6
+
5
7
  The built-in cloudflared provider runs in one of two modes, chosen in the panel under **Mobile access → Remote → cloudflared → Tunnel type**:
6
8
 
7
9
  | | Quick tunnel (default) | Named tunnel |
@@ -2,6 +2,8 @@
2
2
 
3
3
  [English guide](CLOUDFLARE_TUNNEL.en.md)
4
4
 
5
+ > 已经自己运行 cloudflared 或使用系统服务?可改用[自备反向代理模式](SELF_HOSTED_ORIGIN.md#已经自己运行-cloudflared-时),将现有隧道指向面板展示的受保护 HTTP 后端,无需再安装组件或在插件保存隧道 Token。以下步骤介绍由插件管理 cloudflared 的另一种方式。
6
+
5
7
  内置的 cloudflared 通道有两种模式,在面板 **移动访问 → 远程 → cloudflared → 隧道类型** 中切换:
6
8
 
7
9
  | | 快速隧道(默认) | 命名隧道 |
@@ -0,0 +1,44 @@
1
+ # Experimental iroh connection
2
+
3
+ iroh replaces public domains and tunnel accounts with an encrypted direct connection (relay fallback when direct fails): the phone finds the computer's cryptographic identity first, then traffic returns to the loopback gateway. You need no domain, port mapping, or separate HTTPS tunnel.
4
+
5
+ > Plugin 0.7.0 and the full App 0.7.0 integrate the computer provider, pinned TLS, pairing UI and Android native transport. The feature is off by default and remains experimental. Direct connectivity, relay use and speed depend on both networks; availability on every campus, corporate or carrier network is not guaranteed.
6
+ > The design lives in the companion's [integration seam](https://github.com/saya-ch/dsh-mobile-iroh/blob/main/docs/INTEGRATION_SEAM.md).
7
+
8
+ ## Prerequisites
9
+
10
+ - Windows x64 or Linux x64 (the only published sidecar targets).
11
+ - Android 10+ with the full `-iroh.apk`, supporting arm64-v8a and x86_64. The ordinary `.apk` is the compatibility build without native libraries: it retains existing HTTPS connections and serves older automatic-update links, but cannot use iroh. Mobile browsers cannot connect directly through iroh either. Both APKs have the same application ID and official signer; install only one.
12
+ - Press **Install official component** once to fetch the sidecar: the plugin downloads the pinned `dsh-mobile-iroh 0.1.0-alpha.1` release per platform (about 15 MB on Windows x64, 24 MB on Linux x64), verifies its SHA-256, and stores it in the plugin private directory. An installed companion plugin with a verified copy is reused instead, with no download. The Rust source lives with the main repository under `native/iroh-host` (shared core under `native/iroh-core`) and is built per platform in CI.
13
+ - DSH Mobile stays enabled; the iroh channel is off by default and never disturbs LAN or other remote channels.
14
+
15
+ ## Usage
16
+
17
+ 1. Under **Mobile access → Remote**, pick `iroh (Experimental)`.
18
+ 2. Press **Prepare iroh → Install official component** (labelled with the download size): the first run downloads and verifies the sidecar; later runs reuse the verified copy. A hash mismatch or an unsupported platform stops here with a reason instead of a half-started channel. Merely switching the channel on never triggers a download.
19
+ 3. Turn remote access on: the plugin resolves the endpoint identity, issues the endpoint gateway certificate (own CA, 90-day leaf, 30-day renewal headroom), starts a dedicated gateway, launches the sidecar, and waits for `ready`. `ready` means the sidecar is connected and the gateway serves its loopback socket.
20
+ 4. Press **Generate iroh pairing QR code** and scan it with the full App. The descriptor binds the sidecar's six-digit code, the Gateway's one-time token and pinned CA. Generating another descriptor replaces the previous pairing window; pairing a phone redeems the Gateway token through the normal pair endpoint over iroh.
21
+ 5. Switching off or switching providers stops the owned process and Gateway without deleting stored pairings. The independent App has a different package identity; its Keystore credentials are not automatically migrated into this App.
22
+
23
+ ## Security boundaries
24
+
25
+ - The sidecar proxies only to a `127.0.0.1`/`::1` loopback gateway with end-to-end TLS; commands and events are versioned, bounded, and rejected closed on unknown fields.
26
+ - A rotated fingerprint, replaced binary, or moved file fails verification and parks the channel at `unavailable` instead of silently running a stale one.
27
+ - Transport revocation and device-token revocation are separate authorizations; their unified management is Phase 2 work. Computer-side revocation keeps working through the existing device management.
28
+
29
+ ## Troubleshooting
30
+
31
+ Store the iroh identity and certificates in a directory tree writable only by the current user or trusted system accounts. DSH's default state under the Windows user profile is intended for this; custom locations also need private ancestors. Some AppData or shared locations grant other accounts write access and are rejected. Do not loosen ACLs, delete real identities or move state into untrusted storage to bypass the check. Back up and preserve existing identity files.
32
+
33
+ Before a component release, verify first use with the exact catalog-pinned executable, not only a locally compiled host or a directory initialized by hand. The controller invokes `tls-prepare` because `id` only reads existing identity state. On Windows, `DSH_IROH_PINNED_FIRST_USE=<verified executable> npx vitest run tests/iroh-pinned-first-use.test.ts` checks initialization, reuse and corruption refusal in owned private storage. A failure blocks component acceptance; do not loosen directory ACL checks or replace real identities to make it pass.
34
+
35
+ | Symptom | Meaning |
36
+ | --- | --- |
37
+ | `iroh_companion_missing` | Nothing installed yet: press Install first, or point `DSH_MOBILE_IROH_COMPANION` at a companion absolute path to reuse |
38
+ | `iroh_sidecar_invalid` | An installed file was modified; reinstalling overwrites it in place, or reinstall the companion for a damaged companion copy |
39
+ | `iroh_platform_unsupported` | No sidecar is published for this OS/architecture |
40
+ | `iroh_identity_invalid` / `iroh_identity_failed` | Check private permissions on the state directory and its ancestors and check for damaged identity files, preserving originals. `id` reads an existing identity; first use invokes `tls-prepare`, not copying or deleting an identity |
41
+ | `iroh_start_timeout` | Inspect iroh errors in the DSH log and network connectivity, and verify that the pinned component runs; repeated pairing is not a substitute for process diagnosis |
42
+ | Channel `ready` but no phone connects | Check that the App is the full build, the device supports a packaged ABI and the descriptor is fresh. Computer readiness does not establish phone connectivity; a relay-service connection alone does not establish that data uses the relay |
43
+
44
+ Chinese version: [IROH.md](IROH.md).
package/docs/IROH.md ADDED
@@ -0,0 +1,44 @@
1
+ # iroh 实验性连接
2
+
3
+ iroh 用加密直连(打不通时走中继)代替公网域名和隧道账号:手机先找到电脑的加密身份,再把流量送回本机网关。你不需要准备域名、端口映射或另一套 HTTPS 隧道。
4
+
5
+ > 插件 0.7.0 和 App 0.7.0 完整版整合电脑端通道、固定 TLS、配对界面和 Android 原生传输。功能默认关闭,仍标记为实验性;能否直连、是否经过中继及连接速度取决于两端网络,不保证在所有校园网、企业网或运营商环境下可用。
6
+ > 相关设计见独立仓的[集成接缝](https://github.com/saya-ch/dsh-mobile-iroh/blob/main/docs/INTEGRATION_SEAM.md)。
7
+
8
+ ## 前提
9
+
10
+ - Windows x64 或 Linux x64(sidecar 只发布这两个目标)。
11
+ - Android 10+,安装含 iroh 的完整版 `-iroh.apk`,支持 arm64-v8a 和 x86_64。普通 `.apk` 是无原生库的兼容版,保留原有 HTTPS 连接并供旧版自动更新使用;不支持 iroh,也不支持手机浏览器直接接入。两个 APK 使用相同包名和正式签名,只需安装一个。
12
+ - 点 **安装官方组件** 一次下好 sidecar:插件从固定的 `dsh-mobile-iroh 0.1.0-alpha.1` 发行版按平台下载(Windows x64 约 15 MB,Linux x64 约 24 MB),校验 SHA-256 后放进插件私有目录;已装配套插件且副本校验通过时会直接复用,不再下载。Rust 源码随主仓在 `native/iroh-host`(公共 core 在 `native/iroh-core`)维护,CI 按平台构建校验。
13
+ - DSH Mobile 保持启用;iroh 通道默认关闭,不影响局域网和其他远程通道。
14
+
15
+ ## 使用
16
+
17
+ 1. 在 **移动访问 → 远程** 选择 `iroh(实验性)`。
18
+ 2. 点 **准备 iroh → 安装官方组件**(带下载大小):首次下载并校验 sidecar,通过才算就绪;再次点击会复用已校验的副本。哈希不对、平台不支持都会直接给出原因,不会启动半截通道。注意只是点开通道开关不会触发下载。
19
+ 3. 打开远程开关:插件依次解析端点身份、签发端点专属网关证书(自有 CA,90 天叶子、30 天提前续)、起专用网关、拉起 sidecar 进程并等待 `ready`。`ready` 表示 sidecar 已连接且网关在回环监听。
20
+ 4. 点 **生成 iroh 配对二维码**,用完整版 App 扫描。描述符绑定 sidecar 的 6 位码、网关的一次性 token 和固定 CA;再次生成会替换旧配对窗口,手机通过 iroh 上的普通 pair 接口核销网关 token。
21
+ 5. 关闭或切换通道会停止自有进程与网关,不删除已保存的配对。独立实验 App 的包名不同,其 Keystore 凭据不会自动迁移到本 App。
22
+
23
+ ## 安全边界
24
+
25
+ - sidecar 只允许代理到 `127.0.0.1`/`::1` 的回环网关,TLS 仍端到端;命令与事件都是版本化、定长、未知字段直接拒绝。
26
+ - 换指纹/换二进制/挪文件都会让校验失败并停在 `unavailable`,不会静默跑旧通道。
27
+ - 传输层撤销与设备 Token 撤销是两套授权,统一管理界面是 Phase 2 内容;电脑端撤销仍按现有设备管理生效。
28
+
29
+ ## 排障
30
+
31
+ iroh 身份和证书必须存放在仅当前用户或受信系统账户可写的目录树。Windows 默认的用户 profile 下 DSH 状态目录符合此用途;自定义状态位置时也要检查父目录权限。某些 AppData 或共享目录允许其他账户写入,会被拒绝;不要通过放宽 ACL、删除真实身份或移入不可信目录来绕过检查。已有身份应先备份并保留。
32
+
33
+ 组件发布前必须使用目录中固定的二进制验证首次启用,不能只测本地编译的 host 或手工初始化过的目录。控制器调用 `tls-prepare`,因为 `id` 只读取既有身份。Windows 可设置 `DSH_IROH_PINNED_FIRST_USE=<已校验的程序>` 后运行 `npx vitest run tests/iroh-pinned-first-use.test.ts`,检查私有测试目录中的初始化、复用与损坏拒绝。失败应阻止组件验收,不得通过放松目录 ACL 检查或替换真实身份来让测试通过。
34
+
35
+ | 现象 | 含义 |
36
+ | --- | --- |
37
+ | `iroh_companion_missing` | 还没点安装(或配套不在常规布局旁);先点安装官方组件,或用 `DSH_MOBILE_IROH_COMPANION` 指向配套绝对路径复用 |
38
+ | `iroh_sidecar_invalid` | 已装文件被改动过;重新点安装即可覆盖修复,配套副本损坏则重装配套 |
39
+ | `iroh_platform_unsupported` | 当前系统/架构没有 sidecar 发布 |
40
+ | `iroh_identity_invalid` / `iroh_identity_failed` | 先检查状态目录及父目录是否私有、已有身份是否损坏,保留原文件再排查;`id` 仅检查已有身份,首次创建使用 `tls-prepare`,不是靠复制或删除身份修复 |
41
+ | `iroh_start_timeout` | 查看 DSH 日志中的 iroh 错误及网络连接,确认固定组件可运行;不要反复重新配对来替代进程排查 |
42
+ | 通道 `ready` 但手机连不上 | 检查 App 是否为完整版、设备是否支持打包的 ABI,以及描述符是否有效;电脑就绪不等于手机已连通,连接中继服务也不等于数据一定经过中继 |
43
+
44
+ 英文版见 [IROH.en.md](IROH.en.md)。
@@ -0,0 +1,196 @@
1
+ # Mobile plugin adaptation guide
2
+
3
+ [中文](MOBILE_PLUGIN_ADAPTATION.md) · [Documentation index](README.md)
4
+
5
+ This guide is for DSH plugin authors, aiming for **usable small screens without desktop regressions**. DSH Mobile provides connectivity, page layout and shared entry points; it does not rearrange every third-party plugin's internal controls. Start in a browser, then add device acceptance; an Android device is not required to begin.
6
+
7
+ ## Identify where the panel is mounted
8
+
9
+ | Current mobile-page behavior | What the plugin should do |
10
+ | --- | --- |
11
+ | Opening a panel in the `main` slot adds a “Back to conversation” bar above it | Use the container's actual available height. The bar is at least 56px high; safe areas and content can increase it. Do not subtract a fixed 56px from screen height |
12
+ | The narrow sidebar is a drawer no wider than `min(88vw, 340px)`; a wide layout can keep it persistent | Do not assume a fixed 340px sidebar. Let entry labels and buttons shrink |
13
+ | The mobile layout has `.dshm-shell`, `[data-pane="conversation"]` and `[data-pane="sidebar"]` markers | Use these to locate regions while diagnosing. Scope internal styles to the plugin's own root rather than private host CSS class names |
14
+ | The mobile shell gives inputs and editable areas a 16px font floor while retaining larger inherited sizes | Leave room for editing, rather than counteracting user preferences with a smaller fixed font |
15
+ | Adapted shared entry points do not guarantee arbitrary third-party hit areas | Plugin authors must still check buttons, close actions, checkboxes and menus for actual hit testing |
16
+
17
+ These describe the current layout, not fixed dimensions a plugin should copy. Overlays, sidebars and main panels have different available space; verify the actual mounting location.
18
+
19
+ ## Adapt to available space
20
+
21
+ Make the base layout shrink first: give flex/grid children `min-width: 0`, let toolbars wrap, and use content-driven height. Panel width is not always viewport width: a 400px panel in a desktop sidebar needs compact layout too.
22
+
23
+ For arrangement-only changes, CSS container queries are an option; full-page viewport adaptation can still use media queries. The `600px` below is an example breakpoint, to be chosen for the plugin's content:
24
+
25
+ ```css
26
+ .plugin-panel-host { container-type: inline-size; min-width: 0; }
27
+ .plugin-panel { min-width: 0; }
28
+ .plugin-panel .toolbar { display: flex; flex-wrap: wrap; gap: 8px; }
29
+
30
+ @container (max-width: 600px) {
31
+ .plugin-panel .toolbar { display: grid; grid-template-columns: minmax(0, 1fr); }
32
+ }
33
+ ```
34
+
35
+ Wrap `.plugin-panel` in `.plugin-panel-host`. On engines without container queries, the base wrapping and fluid widths should remain usable; do not put an essential action solely in the query branch.
36
+
37
+ When a script branch is necessary, observe the component's root width instead of classifying a phone by user agent:
38
+
39
+ ```js
40
+ const rootRef = useRef(null)
41
+ const [panelWidth, setPanelWidth] = useState(null)
42
+ const isNarrow = panelWidth !== null && panelWidth <= 600
43
+
44
+ useEffect(() => {
45
+ const element = rootRef.current
46
+ if (element === null) return undefined
47
+ const measure = () => setPanelWidth(element.getBoundingClientRect().width)
48
+ measure()
49
+ window.addEventListener('resize', measure)
50
+ const observer = typeof window.ResizeObserver === 'function' ? new window.ResizeObserver(measure) : null
51
+ observer?.observe(element)
52
+ return () => {
53
+ window.removeEventListener('resize', measure)
54
+ observer?.disconnect()
55
+ }
56
+ }, [])
57
+
58
+ // The component's returned root; keep a usable base layout before the first measurement.
59
+ h('section', { ref: rootRef, className: 'plugin-panel', 'data-narrow': isNarrow ? 'true' : 'false' }, contents)
60
+ ```
61
+
62
+ Without ResizeObserver, this fallback only measures again on window resize. If a panel resizes independently, also use the plugin's existing layout notifications. Dispose observers and listeners when the component unmounts.
63
+
64
+ ## Adaptation checklist
65
+
66
+ ### Touch targets and spacing
67
+
68
+ This project recommends at least a **44×44 CSS px** hit area for primary Web controls, with 48px for primary actions where space permits; these are not conversions from Android dp or iOS pt. Leave space between adjacent actions rather than extending targets over their neighbors.
69
+
70
+ ```css
71
+ .plugin-panel :is(button, select, input:not([type="checkbox"]):not([type="radio"])) { min-height: 44px; }
72
+ .plugin-panel .icon-button { min-width: 44px; }
73
+ .plugin-panel .actions { display: flex; flex-wrap: wrap; gap: 8px; }
74
+ .plugin-panel .check-label { box-sizing: border-box; display: inline-flex; align-items: center; gap: 8px; min-width: 44px; min-height: 44px; padding-inline: 8px; }
75
+ ```
76
+
77
+ Put a small checkbox inside its associated `label` to enlarge the clickable label without enlarging the icon. Use actual taps and `elementFromPoint()` to check edge hits; a declared `min-height` can still be shrunk, clipped or obscured by layout.
78
+
79
+ ### Filters, tabs and inline actions
80
+
81
+ Long filter strips can wrap or use their own horizontally scrollable container; do not make the whole page overflow horizontally. Retain visible focus and enough padding to bring the last item fully into view:
82
+
83
+ ```css
84
+ .plugin-panel .chips { display: flex; gap: 8px; overflow-x: auto; padding: 4px; }
85
+ .plugin-panel .chip { flex: 0 0 auto; min-height: 44px; }
86
+ ```
87
+
88
+ An unambiguous inline action can become an icon on narrow panels, provided it keeps an accessible name and, where needed, a text explanation in a menu. Hover text cannot be the only explanation on a phone.
89
+
90
+ ```js
91
+ h('button', { className: 'icon-button', 'aria-label': label }, h(Icon, { 'aria-hidden': true, name: 'play' }))
92
+ ```
93
+
94
+ A narrow panel does not imply the absence of a keyboard. Decorative shortcut badges can collapse, but retain functioning shortcuts, help, Tab order and visible focus; do not remove keyboard-accessible behavior solely by width.
95
+
96
+ ### Type, density and complete information
97
+
98
+ Inherit DSH's fonts and semantic colors. To follow the mobile page's text setting, a plugin can use `--dsh-content-font-size` on its content root and retain inheritance when the variable is absent:
99
+
100
+ ```css
101
+ .plugin-panel { font-size: var(--dsh-content-font-size, inherit); line-height: 1.5; }
102
+ .plugin-panel .meta { color: var(--dsw-alias-label-secondary, currentColor); }
103
+ ```
104
+
105
+ Compress decorative headings and whitespace, not touch targets. Let long titles wrap or offer an accessible way to view them in full. Errors, pending confirmations and essential action names must not exist only as truncated text. Check larger text, long paths and Chinese/English content.
106
+
107
+ ### Overlays, forms and scrolling
108
+
109
+ Constrain overlays to their actual container and visible area, rather than using fixed dimensions that push actions off-screen. Reuse DSH's existing overlay components first; a custom overlay needs an accessible close action, focus management and Escape behavior.
110
+
111
+ ```css
112
+ .plugin-panel .form-row { display: grid; grid-template-columns: minmax(0, 1fr); gap: 8px; }
113
+ .plugin-panel .scroll-body { min-height: 0; overflow: auto; overscroll-behavior: contain; }
114
+ .plugin-panel textarea { box-sizing: border-box; width: 100%; min-height: 96px; }
115
+ ```
116
+
117
+ Add `env(safe-area-inset-*)` only where content really reaches a screen edge and the host has not already handled it, avoiding duplicate space. Save, Cancel and errors must remain visible or reachable by scrolling with the keyboard open. An independent scroll area should not move the page underneath.
118
+
119
+ ### Overlay layering and portals
120
+
121
+ The dedicated mobile shell uses `isolation:isolate` so its drawer and panels share a local stacking context. A positioned menu portalled directly to `document.body` with a positive `z-index` can paint above that shell; its value does not have to exceed the drawer's internal value. The stock-layout adapter retains its existing layer values.
122
+
123
+ This is not a guarantee that every body child outranks every dialog. Negative values, zero/auto paint order, ancestor stacking contexts, and the browser's top layer affect the result. Menus inside the shell remain constrained by their ancestors. Prefer the host's overlay facilities where available, and dismiss a menu when its owning panel closes or a modal takes focus. Do not globally raise third-party widgets or change DSH's dialog layers.
124
+
125
+ Verify actual visibility, focus and `document.elementFromPoint()` while the drawer, detail panel and settings are open, in narrow and wide layouts. The owned `smoke:plugin-portal-layers` fixture checks positive-z-index body menus and a top-layer dialog; it is not acceptance of every community plugin.
126
+
127
+ ## Verification: what code checks, browsers and devices establish
128
+
129
+ ### Structural checks
130
+
131
+ Unit tests can check narrow branches, accessible names, retained state and unmount cleanup. Server rendering does not run browser layout measurement; supplying a test width or layout stub only establishes the chosen branch, not actual dimensions, hit testing or pixel-identical desktop output. A `44px` string in CSS is not touch acceptance.
132
+
133
+ ### Real browser checks
134
+
135
+ Mount the plugin in an actual DSH page and cover at least 375px and 390px narrow viewports, landscape, full-width desktop and a narrow desktop sidebar. If a static preview is maintained, load it in a browser too and describe its differences from the real host.
136
+
137
+ - Cover list, detail, settings, empty, long-content and error states;
138
+ - Inspect element bounds, clipping ancestors and actual hit targets, including the first and last actions;
139
+ - Use a keyboard to check Tab, Escape and focus restoration, with no accidental background action while an overlay is open;
140
+ - Change width and text size, checking that drafts, selections and expanded state survive;
141
+ - Check light/dark themes and desktop baselines. Explain screenshot changes rather than demanding pixel identity for intentional improvements.
142
+
143
+ For example, after opening the real panel through its existing entry point, Playwright can check a control's geometry and hit testing. Replace the following selector with the plugin's own stable marker:
144
+
145
+ ```js
146
+ const action = page.locator('[data-plugin-action="save"]')
147
+ await action.scrollIntoViewIfNeeded()
148
+ const box = await action.boundingBox()
149
+ if (box === null || box.width < 44 || box.height < 44) throw new Error('Save target is too small')
150
+ const hit = await action.evaluate(element => {
151
+ const box = element.getBoundingClientRect()
152
+ return element.contains(document.elementFromPoint(box.x + box.width / 2, box.y + box.height / 2))
153
+ })
154
+ if (!hit) throw new Error('Save target is obscured')
155
+ ```
156
+
157
+ A center hit is a starting point, not a substitute for edge taps, scrolling, focus or state checks. See [CONTRIBUTING](../CONTRIBUTING.md#select-behavior-checks) for launch and verification procedures.
158
+
159
+ ### Device follow-up
160
+
161
+ Browser emulation does not replace real WebView, keyboard, safe-area, momentum-scroll or external-keyboard acceptance. Record the device, App/WebView versions, connection and actual steps. State unverified parts directly instead of claiming all-platform success.
162
+
163
+ ## Templates for plugin authors
164
+
165
+ ### Issue
166
+
167
+ ```markdown
168
+ ## Plugin on a small screen
169
+ - Plugin name and version:
170
+ - DSH, DSH Mobile and App/browser versions:
171
+ - Panel location and available width (main / sidebar / overlay):
172
+ - Reproduction steps, expected and actual behavior:
173
+ - Screenshot and identifiable control (redact credentials and private content):
174
+ - Comparison with other optional frontend modules omitted, if tested:
175
+
176
+ ## Acceptance goals
177
+ - [ ] Essential actions are visible and usable; long content is not clipped
178
+ - [ ] Actual touch areas, adjacent spacing and first/last items checked
179
+ - [ ] Keyboard focus, back/close behavior and drafts retained
180
+ - [ ] Overlays and forms work at narrow widths and larger text
181
+ - [ ] No desktop workflow regressions; unverified device behavior identified
182
+ ```
183
+
184
+ ### PR
185
+
186
+ - [ ] Describe affected entry points and content, without hard-splitting phone/desktop by UA;
187
+ - [ ] Reuse components and state; avoid duplicating business logic when arrangements differ;
188
+ - [ ] Supply real-browser narrow and desktop comparisons, steps and relevant tests;
189
+ - [ ] Check focus, hit testing, long content, errors and cleanup, not only CSS text;
190
+ - [ ] Distinguish verified and unverified device behavior; do not copy another version's results.
191
+
192
+ ## Scope
193
+
194
+ This guide draws on community plugin adaptation and [discussion #279](https://github.com/saya-ch/dsh-mobile/discussions/279). It offers reusable implementation and acceptance ideas, not an independent verification of a new third-party release. For theme-specific questions, see [Skins and mobile layout](SKIN_COMPATIBILITY.en.md).
195
+
196
+ DSH Mobile does not maintain a universal override table keyed by third-party private class names, or guarantee that plugins requiring native windows work directly on remote pages. The plugin owning the component should maintain its adaptation through DSH's public extension points. Report missing host capabilities separately rather than modifying installed DSH artifacts to work around them.