dsh-mobile 0.3.16 → 0.4.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  Notable changes to DSH Mobile are recorded here. GitHub Releases remain the source for downloadable packages and complete generated commit notes.
4
4
 
5
+ ## Unreleased
6
+
7
+ Future changes will be recorded here.
8
+
9
+ ## 0.4.0 - 2026-09-14
10
+
11
+ - Add multi-device management to the Android app: migrate existing LAN and remote credentials, show each paired computer with its Origin and HTTPS reachability state, connect the most recently used valid device on direct startup, and provide list, rename, re-pair, delete, and launch-behavior controls.
12
+ - Complete plugin-side localization for Chinese, English, and Italian, including diagnostic report copy, browser pairing pages, and reauthentication guidance selected from `Accept-Language`; Android resources remain key-complete across all three supported locales.
13
+ - Use a bounded session-free native probe for list reachability checks, with a renewal fallback for older plugins so status refreshes cannot evict an active DSH session.
14
+ - Preserve a local row after computer-side revocation, stop automatic retries for revoked credentials, and expose a short-lived undo action for local deletion without restoring a computer-side authorization.
15
+ - Follow the DSH conversation's actual nested scroll container when showing or hiding the Android toolbar, keeping task-notification settings reachable on current DSH Web layouts.
16
+ - Let cpolar choose its default route instead of forcing `cn`; explicit region settings remain available for advanced deployments.
17
+ - Add Android task-completion and pending-input reminders through authenticated Host events and the exact-origin native bridge; notification permission is enabled explicitly from the foreground app menu, lock-screen text stays generic, and each completed turn keeps a separate reminder (thanks @qzyqmzn for PR #75).
18
+ - Show and re-copy remote pairing links without invalidating the QR code's active one-time pairing window (thanks @qzyqmzn for PR #75).
19
+ - Detect plugin-market installations that have not completed LAN setup, prevent the loopback-only `127.0.0.1` fallback from being presented as phone access, and provide a localized in-panel network picker that creates private TLS material and LAN-only Windows firewall rules after explicit confirmation. The configured gateway starts after one DSH restart (thanks @cangming99 for #72).
20
+ - Keep native DSH file selection in the composer Add group, add only camera capture there, and reuse the native menu-row styling for the mobile action.
21
+ - Add a native-app-only Switch computer action to the WebView's DSH General settings page, keeping the Android device-list settings and DSH content toolbar focused on their own controls.
22
+ - Hide the Android shell toolbar above the WebView so DSH occupies the full safe viewport; the system status-bar color continues to follow the page background.
5
23
 
6
24
  ## 0.3.16 - 2026-09-12
7
25
 
package/README.en.md CHANGED
@@ -19,14 +19,14 @@
19
19
 
20
20
  > DSH Mobile is a DeepSeek Harness community plugin; the native app supports Android only.
21
21
  >
22
- > **0.3.16 update**: supports community task-board pane anchors, improves wide-screen rightbar docking and phone question cards, and fixes custom DSH Web ports still targeting the saved default upstream. [Details](CHANGELOG.md).
22
+ > **0.4.0 update**: adds multi-device management, computer-side revocation status sync, and session-free reachability checks; improves plugin-market LAN onboarding, task-state notifications, the immersive WebView layout, and three-language pairing pages. [Details](CHANGELOG.md).
23
23
  >
24
- > **Upgrade reminder**: update the plugin to **0.3.16** and restart DSH. Existing pairings and app 0.3.15 remain compatible; the 0.3.16 APK changes only Android version metadata, so the app update is optional. [Compatibility notes](#compatibility).
24
+ > **Upgrade reminder**: 0.4.0 requires the plugin and Android app to be updated together for the multi-device list. Older apps continue to use their existing single-device pairing. [Compatibility notes](#compatibility).
25
25
 
26
26
  <p align="center">
27
- <a href="https://github.com/saya-ch/dsh-mobile/releases/download/v0.3.16/dsh-mobile-android-v0.3.16.apk"><img src="assets/brand/app-icon-rounded.svg" alt="DSH Mobile Android app icon" width="72" height="72"></a><br>
28
- <a href="https://github.com/saya-ch/dsh-mobile/releases/download/v0.3.16/dsh-mobile-android-v0.3.16.apk"><strong>Download Android app 0.3.16</strong></a><br>
29
- <sub><a href="https://github.com/saya-ch/dsh-mobile/releases/tag/v0.3.16">Release notes and checksums</a></sub>
27
+ <a href="https://github.com/saya-ch/dsh-mobile/releases/download/v0.4.0/dsh-mobile-android-v0.4.0.apk"><img src="assets/brand/app-icon-rounded.svg" alt="DSH Mobile Android app icon" width="72" height="72"></a><br>
28
+ <a href="https://github.com/saya-ch/dsh-mobile/releases/download/v0.4.0/dsh-mobile-android-v0.4.0.apk"><strong>Download Android app 0.4.0</strong></a><br>
29
+ <sub><a href="https://github.com/saya-ch/dsh-mobile/releases/tag/v0.4.0">Release notes and checksums</a></sub>
30
30
  </p>
31
31
 
32
32
  DSH Mobile is a DeepSeek Harness plugin that lets a mobile browser or the Android app connect over a protected LAN or an optional Tailscale Funnel, cpolar, or self-hosted FRP remote path. Local and remote access keep the same sessions, Workspaces, messages, and tools while using separate switches and paired-device stores without modifying DeepSeek Harness source.
@@ -40,7 +40,7 @@ It also lets you customize the phone from a DSH conversation: `/mobile <what you
40
40
  - **Continue DSH work from a phone**: the same sessions, Workspaces, messages, and tools, in real time.
41
41
  - **Customize the phone UI by talking to DSH**: change the mobile layout, interactions, and features from a conversation; open pages refresh within seconds.
42
42
  - **A dedicated touch layout**: session drawer, tool details, settings, question cards, and composer reorganized for phones. Native app screens follow the Android system locale in Simplified Chinese, English, or Italian. Plugin-owned Web UI follows DSH's selected locale; Italian resources are ready for a future DSH Italian locale.
43
- - **Image attachments**: use the top row of the composer plus menu to select an image or take a photo; PNG, JPEG, WebP, and GIF files up to 8 MiB are supported, plus full-resolution JPEG capture.
43
+ - **Image attachments**: file selection uses DSH's native **Add** group; DSH Mobile only adds **Take photo** to that group, and captured images follow DSH's native attachment flow.
44
44
  - **Auto-discovery, no re-pairing**: Wi-Fi, hotspot, or IP changes normally recover automatically.
45
45
  - **One-click connection diagnostics**: check versions, gateway, network interface, firewall, and the remote path; stable reason codes are localized in the UI, and the copied report excludes credentials and complete addresses.
46
46
  - **One-click approval for third-party plugin WebSockets**: the diagnostics view groups blocked plugin connections by directory (with attempt counts); allowing a path unblocks that exact path while everything unapproved stays blocked, and a red badge marks the sidebar entry until reviewed (#47). If a plugin keeps failing to connect (for example a terminal reporting 1006), first look at the diagnostics view for blocked connections and approve them in one click — manual configuration is usually unnecessary.
@@ -74,7 +74,7 @@ Or via the plugin market (optional):
74
74
  dsh plugin --profile web add dshmarket
75
75
  ```
76
76
 
77
- Restart DSH, then search for **dsh-mobile** under **Settings → Plugin Market** and install it with one click.
77
+ Restart DSH, then search for **dsh-mobile** under **Settings → Plugin Market** and install it. On first opening Mobile access, the Local network page lists this computer's current networks; confirm one to create private certificates and LAN configuration, then restart DSH once as prompted. No terminal `setup` command is required for this path.
78
78
 
79
79
  `setup` automatically selects and remembers the current LAN; Wi-Fi, hotspot, and IP changes normally recover without re-pairing. Use `--address 192.168.x.x` only when automatic selection fails. Settings, certificates, devices, and customization files live under `$DSH_HOME/mobile-access/`.
80
80
 
@@ -103,6 +103,8 @@ Port note: `dsh web --port` changes the DSH Web upstream port (3080 by default),
103
103
 
104
104
  The app is optional: select **Copy pairing link** and open it in a mobile browser. The browser must manually trust the plugin certificate on the first visit.
105
105
 
106
+ Browser pairing and reauthentication pages use the browser's `Accept-Language` to show Simplified Chinese, English, or Italian. Android screens follow the system language, while the DSH plugin control panel follows the language selected by DSH.
107
+
106
108
  ### Remote access
107
109
 
108
110
  Use this after the phone leaves the computer's network. Remote access is disabled by default, and the phone needs no separate Tailscale, cpolar, or FRP app.
@@ -160,6 +162,54 @@ The examples above, applied:
160
162
  <img src="https://raw.githubusercontent.com/saya-ch/dsh-mobile/main/assets/screenshots/cyberpunk-monitor-1.png" width="22%" style="margin-left:8px" alt="Mobile UI customized into a cyberpunk computer monitor">
161
163
  </p>
162
164
 
165
+ ### Device management
166
+
167
+ The Android app keeps LAN, cpolar, Tailscale Funnel, and self-hosted FRP pairings in one **Paired computers** list. The first upgrade migrates the legacy LAN and remote credentials without requiring another pairing; when an address changes, the app merges the row by the DSH installation's stable `instanceId` and keeps its custom name. Device tokens and LAN CAs remain encrypted by Android Keystore and never appear in the list or QR code.
168
+
169
+ Each row shows its custom name, transport, Origin, live reachability, and last connection time. A green dot means **Reachable**; a gray dot means **Checking**, **Temporarily unreachable**, **Pairing expired**, or **Removed on computer**. The check validates the DSH Gateway over HTTPS instead of using ICMP, so a temporary network outage is not mistaken for computer-side revocation.
170
+
171
+ - **Startup behavior → Open DSH directly** (default): one device connects directly; with multiple devices, the app tries the last-used device first, then the still-valid device with the most recent connection. A bounded connection budget returns to the list instead of spinning forever.
172
+ - **Startup behavior → Show device list**: choose a computer on every launch, which is useful when switching between several machines. The option is in the list's top-right settings button and is saved immediately.
173
+ - Tap a row to connect. The overflow button and long press open the same action sheet for rename, check now, pair again, or delete the local record. Deletion has a second confirmation and a short undo window; undo restores only the local row and never restores a computer-side revocation.
174
+ - Open DSH **Settings → General** in the WebView and select **Switch computer** to return to the paired-device list; this action appears only in the Android app. If the computer revokes a device, the app keeps its row as **Removed on computer**, stops automatic reconnection, and offers **Pair again** or **Delete device**.
175
+
176
+ <table>
177
+ <tr>
178
+ <td align="center" valign="top" width="50%">
179
+ <img src="https://raw.githubusercontent.com/saya-ch/dsh-mobile/main/assets/screenshots/device-management.jpg" width="44%" alt="Paired computer list in the Android app"><br>
180
+ <sub>Device management: paired computers, transport, and reachability</sub>
181
+ </td>
182
+ <td align="center" valign="top" width="50%">
183
+ <img src="https://raw.githubusercontent.com/saya-ch/dsh-mobile/main/assets/screenshots/startup-behavior.jpg" width="44%" alt="Startup behavior settings in the Android app"><br>
184
+ <sub>Startup behavior: open DSH directly or show the device list</sub>
185
+ </td>
186
+ </tr>
187
+ </table>
188
+
189
+ ### Third-party plugin compatibility
190
+
191
+ The mobile adaptation keeps DSH's existing Workspace, task-management, terminal, and file-panel entry points instead of isolating third-party plugin content in a separate page. The wide-layout screenshot below shows the Android app in a wide viewport. The app adapts to the available width: phones use drawers and overlays, while wide screens use side-by-side panels; both layouts expose the same features and connection methods. DSH still loads third-party plugins itself—the mobile layer only adapts layout and access, without modifying DeepSeek Harness source.
192
+
193
+ Compatibility and WebSocket rules:
194
+
195
+ - This release is contract-checked against DSH `0.1.5-rc.2` (renderer-v2) and retains the `0.1.5-rc.1` LAN verification. The DSH page must expose the standard session, `main`/`panelInfo`, and `rightbar` slots; the community plugin must register its panel or sidebar content through DSH's standard entry points.
196
+ - The gateway allows first-party DSH WebSocket paths by default, including `/sidebar/ws/terminal`. Other paths used by community sidebar plugins are blocked by default and appear in Diagnostics; the `/sidebar/ws/agent-opens` and `/sidebar/ws/agent-terminals` paths in the image are examples that must be reviewed for the actual plugin.
197
+ - In **Connection diagnostics → Third-party WebSocket paths**, select **Allow** only for an exact path you have verified. Query strings and fuzzy prefixes are rejected; **Allow all** is not recommended. Approved paths can be removed at any time, and the same policy applies to LAN and remote connections.
198
+ - Approval only lets that path pass through the authenticated, same-origin DSH Mobile gateway. It does not open arbitrary TCP/UDP ports or bypass device pairing. If a community plugin still fails, check the path recorded by Diagnostics and approve one path at a time.
199
+
200
+ <table>
201
+ <tr>
202
+ <td align="center" valign="top" width="50%">
203
+ <img src="https://raw.githubusercontent.com/saya-ch/dsh-mobile/main/assets/screenshots/third-party-plugin-adaptation.png" width="96%" alt="Android app wide layout with a community sidebar plugin adapted"><br>
204
+ <sub>Community sidebar plugin compatibility in the Android app</sub>
205
+ </td>
206
+ <td align="center" valign="top" width="50%">
207
+ <img src="https://raw.githubusercontent.com/saya-ch/dsh-mobile/main/assets/screenshots/websocket-diagnostics.png" width="96%" alt="Third-party WebSocket path diagnostics in the Android app"><br>
208
+ <sub>Connection diagnostics: review and allow third-party WebSocket paths</sub>
209
+ </td>
210
+ </tr>
211
+ </table>
212
+
163
213
  ## App or mobile browser
164
214
 
165
215
  | Client | Best for | Notes |
@@ -203,7 +253,7 @@ The table below lists, for each plugin version, the DeepSeek Harness version it
203
253
 
204
254
  | DSH Mobile plugin | Verified DeepSeek Harness version |
205
255
  | --- | --- |
206
- | `0.3.15`, `0.3.16` | `0.1.5-rc.2` (contract check); `0.1.5-rc.1` (@idoall LAN verification) |
256
+ | `0.3.15`, `0.3.16`, `0.4.0` | `0.1.5-rc.2` (contract check); `0.1.5-rc.1` (@idoall LAN verification) |
207
257
  | `0.3.14` | `0.1.3-alpha.2` |
208
258
  | `0.3.9`-`0.3.12` | `0.1.3-alpha.1` |
209
259
  | `0.3.6`-`0.3.8` | `0.1.2-rc.1` |
@@ -211,7 +261,7 @@ The table below lists, for each plugin version, the DeepSeek Harness version it
211
261
  | `0.3.0`-`0.3.3` | `0.1.2-alpha.1` |
212
262
  | `0.1.4`, `0.2.x` | `0.1.1-rc.2` |
213
263
 
214
- Existing 0.3.3–0.3.16 apps do not need re-pairing. cpolar users should use app 0.3.15 or later because earlier apps may time out before a slow first load over the free route finishes; earlier apps also use a different status-bar strategy. App 0.1.3 or earlier requires reinstalling and pairing again.
264
+ Existing 0.3.3–0.4.0 apps do not need re-pairing. cpolar users should use app 0.3.15 or later because earlier apps may time out before a slow first load over the free route finishes; earlier apps also use a different status-bar strategy. The 0.4.0 app adds the multi-device list, startup behavior, and computer-side revocation status; older apps continue to connect to their saved single device. App 0.1.3 or earlier requires reinstalling and pairing again.
215
265
 
216
266
  ## Uninstall
217
267
 
package/README.md CHANGED
@@ -26,14 +26,14 @@
26
26
 
27
27
  > DSH Mobile 是 DeepSeek Harness 社区插件,原生 App 仅支持 Android。
28
28
  >
29
- > **0.3.16 更新**:兼容社区任务看板的移动布局锚点,优化宽屏右栏与手机提问卡片,并修复自定义 DSH Web 端口仍连接旧默认端口的问题。[详细记录](CHANGELOG.md)。
29
+ > **0.4.0 更新**:加入多设备管理、电脑端撤销状态同步、无 Session 可达性检查;完善插件市场局域网引导、任务状态通知、原生 WebView 沉浸布局和三语配对页。[详细记录](CHANGELOG.md)。
30
30
  >
31
- > **升级提醒**:建议将插件升级至 **0.3.16** 并重启 DSH;现有配对与 0.3.15 App 保持兼容,本次 Android App 仅同步版本号,可按需更新。[兼容说明](#兼容性)。
31
+ > **升级提醒**:0.4.0 需要同步更新插件与 Android App 才能使用多设备列表;旧 App 仍可使用原有单设备配对。[兼容说明](#兼容性)。
32
32
 
33
33
  <p align="center">
34
- <a href="https://github.com/saya-ch/dsh-mobile/releases/download/v0.3.16/dsh-mobile-android-v0.3.16.apk"><img src="assets/brand/app-icon-rounded.svg" alt="DSH Mobile 安卓应用图标" width="72" height="72"></a><br>
35
- <a href="https://github.com/saya-ch/dsh-mobile/releases/download/v0.3.16/dsh-mobile-android-v0.3.16.apk"><strong>下载 Android App 0.3.16</strong></a><br>
36
- <sub><a href="https://github.com/saya-ch/dsh-mobile/releases/tag/v0.3.16">版本说明与校验文件</a></sub>
34
+ <a href="https://github.com/saya-ch/dsh-mobile/releases/download/v0.4.0/dsh-mobile-android-v0.4.0.apk"><img src="assets/brand/app-icon-rounded.svg" alt="DSH Mobile 安卓应用图标" width="72" height="72"></a><br>
35
+ <a href="https://github.com/saya-ch/dsh-mobile/releases/download/v0.4.0/dsh-mobile-android-v0.4.0.apk"><strong>下载 Android App 0.4.0</strong></a><br>
36
+ <sub><a href="https://github.com/saya-ch/dsh-mobile/releases/tag/v0.4.0">版本说明与校验文件</a></sub>
37
37
  </p>
38
38
 
39
39
  DSH Mobile 是一个 DeepSeek Harness 插件,让手机浏览器或 Android App 通过局域网,或可选的 Tailscale Funnel、cpolar、自建 FRP 远程通道连接电脑,继续使用同一份会话、工作区、消息和工具。局域网与远程访问分别启停、分别管理设备,且都不修改 DeepSeek Harness 源码。
@@ -47,7 +47,7 @@ DSH Mobile 是一个 DeepSeek Harness 插件,让手机浏览器或 Android App
47
47
  - **在手机上继续电脑端的工作**:同一份会话、工作区、消息和工具,实时同步。
48
48
  - **用对话定制手机端**:直接在 DSH 对话里改手机页面的布局、交互和功能,几秒内刷新。
49
49
  - **专属触屏布局**:会话抽屉、工具详情、设置、提问卡片和输入栏都按手机重新组织。App 原生页面跟随系统显示简体中文、英文或意大利文;插件界面跟随 DSH 的语言设置,意大利语资源已为 DSH 后续支持预留。
50
- - **图片附件**:在已打开会话的输入栏加号菜单顶部选择图片或拍照;支持 PNG、JPEG、WebP、GIF(不超过 8 MiB)和完整分辨率 JPEG。
50
+ - **图片附件**:文件选择使用 DSH 原生“添加”组;DSH Mobile 只在该组补充“拍照”,拍摄结果按 DSH 原生附件流程发送。
51
51
  - **自动发现、无需重新配对**:切换 Wi-Fi、热点或 IP 后通常自动恢复。
52
52
  - **一键连接诊断**:检查版本、网关、网卡、防火墙和远程通道;稳定的原因码在界面中本地化,并生成不含凭据与完整地址的脱敏报告。
53
53
  - **第三方插件 WebSocket 一键放行**:诊断页按目录分组记录被拦截的插件连接(含次数),点允许即放行确切路径,未批准的一律拦截;有新拦截时侧栏红点提醒(#47)。若某插件的连接一直失败(如终端报 1006),先到诊断页看看有没有被拦的连接,一键放行即可,通常无需手动配置。
@@ -81,7 +81,7 @@ pnpm dsh --profile web
81
81
  dsh plugin --profile web add dshmarket
82
82
  ```
83
83
 
84
- 重启 DSH 后,在 **设置 → 插件市场** 里搜索 dsh-mobile,一键安装即可。
84
+ 重启 DSH 后,在 **设置 → 插件市场** 里搜索 dsh-mobile 并安装。首次打开“移动访问”时,局域网页会列出电脑当前网络;确认网卡后由插件生成私有证书并配置局域网,按提示重启一次 DSH 即可使用,无需再打开终端运行 `setup`。
85
85
 
86
86
  `setup` 会自动选择并记住当前局域网,切换 Wi-Fi、热点或 IP 后通常自动恢复;仅在自动选择失败时使用 `--address 192.168.x.x`。设置、证书、设备和自定义文件保存在 `$DSH_HOME/mobile-access/`。
87
87
 
@@ -110,6 +110,8 @@ dsh plugin --profile web add dshmarket
110
110
 
111
111
  不安装 App 也可以访问:点击 **复制配对链接**,在手机浏览器中打开;首次访问需要按浏览器提示手动信任插件证书。
112
112
 
113
+ 手机浏览器中的配对和重新连接页面会按浏览器的 `Accept-Language` 显示简体中文、英文或意大利文;Android App 则跟随系统语言。DSH 内的插件控制面板继续跟随 DSH 当前语言。
114
+
113
115
  ### 远程访问
114
116
 
115
117
  适合手机离开电脑所在网络后使用。远程访问默认关闭,手机不需要另外安装 Tailscale、cpolar 或 FRP。
@@ -167,6 +169,54 @@ Tailscale Funnel 覆盖范围广,但在中国大陆网络下可能不稳定。
167
169
  <img src="https://raw.githubusercontent.com/saya-ch/dsh-mobile/main/assets/screenshots/cyberpunk-monitor-1.png" width="22%" style="margin-left:8px" alt="/mobile 定制为赛博朋克监控面板">
168
170
  </p>
169
171
 
172
+ ### 设备管理
173
+
174
+ Android App 将局域网、cpolar、Tailscale Funnel 和自建 FRP 统一整理到“已配对设备”列表。首次升级会自动迁移旧版局域网与远程凭据,不要求重新配对;地址变化时按 DSH 安装的稳定 `instanceId` 合并原记录,保留自定义名称。设备 Token 和局域网 CA 继续由 Android Keystore 加密保存,不会显示在列表或二维码中。
175
+
176
+ 每条记录显示自定义名称、连接方式、Origin、实时可达状态和最近连接时间。绿色状态点表示“可达”,灰色状态点表示“检测中”“暂不可达”“配对已过期”或“电脑端已移除”;可达性检查直接验证 DSH Gateway,不依赖 ICMP,也不会把暂时断网误判成电脑端撤销。
177
+
178
+ - **启动时打开 → 直接进入 DSH**(默认):单设备直接连接;多设备优先连接上次使用的设备,其次按最近连接时间选择仍有效的设备。连接超过有限重试预算后自动回到列表,不会无限转圈。
179
+ - **启动时打开 → 显示设备列表**:每次启动先选择电脑,适合经常在多台设备之间切换。该选项位于列表右上角的设置按钮中,修改后立即保存。
180
+ - 点按设备行可连接;右侧“…”和长按提供相同的操作面板,可编辑名称、立即检测、重新配对或删除本地记录。删除前会二次确认,并提供短暂撤销;撤销只恢复本机记录,不恢复电脑端已经撤销的授权。
181
+ - 在 WebView 页面打开 DSH **设置 → 通用**,选择 **切换电脑** 可回到已配对设备列表;该动作仅在 Android App 中显示。电脑端撤销设备后,App 保留该条目并显示“电脑端已移除”,停止自动重连,同时提供 **重新配对** 和 **删除本地记录**。
182
+
183
+ <table>
184
+ <tr>
185
+ <td align="center" valign="top" width="50%">
186
+ <img src="https://raw.githubusercontent.com/saya-ch/dsh-mobile/main/assets/screenshots/device-management.jpg" width="44%" alt="Android App 已配对设备列表"><br>
187
+ <sub>设备管理列表:查看已配对电脑、连接方式和可达状态</sub>
188
+ </td>
189
+ <td align="center" valign="top" width="50%">
190
+ <img src="https://raw.githubusercontent.com/saya-ch/dsh-mobile/main/assets/screenshots/startup-behavior.jpg" width="44%" alt="Android App 启动行为设置"><br>
191
+ <sub>启动行为设置:选择直接进入 DSH 或显示设备列表</sub>
192
+ </td>
193
+ </tr>
194
+ </table>
195
+
196
+ ### 第三方插件适配
197
+
198
+ 移动适配保持 DSH 原有的工作区、任务管理、终端和文件面板入口,不会把第三方插件内容隔离成另一套页面。下面的宽屏截图展示 Android App 在宽屏下的布局。App 会根据屏幕宽度自适应:手机使用抽屉和浮层,宽屏使用并排面板;两种布局共享相同的功能和连接方式。第三方插件仍由 DSH 自己加载,移动层负责适配布局与连接,不修改 DeepSeek Harness 源码。
199
+
200
+ 兼容条件与 WebSocket 放行规则:
201
+
202
+ - 本版本已按 DSH `0.1.5-rc.2` 的 renderer-v2 合同检查,并保留对 `0.1.5-rc.1` 局域网路径的验证。DSH 页面需要提供标准的会话、`main`/`panelInfo` 和 `rightbar` 插槽;插件自身需要通过 DSH 的标准面板或侧边栏入口注册内容。
203
+ - 网关默认只允许 DSH 内置的第一方 WebSocket 路径。社区侧边栏插件使用的其他路径默认拦截,通常会在诊断页显示为待处理项目;截图中的`/sidebar/ws/agent-terminals` 就属于这类需要按实际插件确认的路径。
204
+ - 在 **连接诊断 → 第三方 WebSocket 路径** 中,只对确认过的精确路径点击 **允许**。系统不接受带查询字符串或模糊前缀的路径;不建议使用“全部允许”。已允许的路径可以随时移除,局域网和远程连接使用同一套规则。
205
+ - 放行只代表该路径可以通过已认证、同源的 DSH Mobile 网关,不会开放任意 TCP/UDP 端口,也不会绕过设备配对。若社区插件仍然连接失败,先看诊断页的实际拦截路径,再按一条路径放行。
206
+
207
+ <table>
208
+ <tr>
209
+ <td align="center" valign="top" width="50%">
210
+ <img src="https://raw.githubusercontent.com/saya-ch/dsh-mobile/main/assets/screenshots/third-party-plugin-adaptation.png" width="96%" alt="Android App 宽屏布局与社区侧边栏插件适配"><br>
211
+ <sub>Android App 下社区侧边栏插件的兼容</sub>
212
+ </td>
213
+ <td align="center" valign="top" width="50%">
214
+ <img src="https://raw.githubusercontent.com/saya-ch/dsh-mobile/main/assets/screenshots/websocket-diagnostics.png" width="96%" alt="Android App 第三方 WebSocket 路径诊断"><br>
215
+ <sub>连接诊断:检查第三方 WebSocket 路径并按需放行</sub>
216
+ </td>
217
+ </tr>
218
+ </table>
219
+
170
220
  ## App 与手机浏览器
171
221
 
172
222
 
@@ -210,17 +260,18 @@ flowchart LR
210
260
 
211
261
  下表列出各插件版本验证支持到的 DeepSeek Harness 版本(早于该版本的 0.1.x 均兼容)。0.3.6 起插件不再按版本号拒绝启动,未列出的更新版本由 CI 契约检查兜底。历史记录见 [CHANGELOG.md](CHANGELOG.md)。
212
262
 
213
- | DSH Mobile 插件 | 验证支持的 DeepSeek Harness 版本 |
214
- | --- | --- |
215
- | `0.3.15`、`0.3.16` | `0.1.5-rc.2`(契约检查);`0.1.5-rc.1`(@idoall 局域网实测) |
216
- | `0.3.14` | `0.1.3-alpha.2` |
217
- | `0.3.9`-`0.3.12` | `0.1.3-alpha.1` |
218
- | `0.3.6`-`0.3.8` | `0.1.2-rc.1` |
219
- | `0.3.4`、`0.3.5` | `0.1.2-alpha.2` |
220
- | `0.3.0`-`0.3.3` | `0.1.2-alpha.1` |
221
- | `0.1.4`、`0.2.x` | `0.1.1-rc.2` |
222
-
223
- 现有 0.3.3–0.3.16 App 无需重新配对;cpolar 用户应使用 0.3.15 或更新 App,较早版本可能在免费线路的慢速首次加载完成前超时;更早的 App 还使用不同的状态栏策略。App 0.1.3 及更早版本需卸载重装并重新配对。
263
+
264
+ | DSH Mobile 插件 | 验证支持的 DeepSeek Harness 版本 |
265
+ | ----------------------------------------- | -------------------------------------------------------------- |
266
+ | `0.3.15`、`0.3.16`、`0.4.0` | `0.1.5-rc.2`(契约检查);`0.1.5-rc.1`(@idoall 局域网实测) |
267
+ | `0.3.14` | `0.1.3-alpha.2` |
268
+ | `0.3.9`-`0.3.12` | `0.1.3-alpha.1` |
269
+ | `0.3.6`-`0.3.8` | `0.1.2-rc.1` |
270
+ | `0.3.4`、`0.3.5` | `0.1.2-alpha.2` |
271
+ | `0.3.0`-`0.3.3` | `0.1.2-alpha.1` |
272
+ | `0.1.4`、`0.2.x` | `0.1.1-rc.2` |
273
+
274
+ 现有 0.3.3–0.4.0 App 无需重新配对;cpolar 用户应使用 0.3.15 或更新 App,较早版本可能在免费线路的慢速首次加载完成前超时;更早的 App 还使用不同的状态栏策略。App 0.4.0 才支持多设备列表、启动行为设置和电脑端撤销状态同步;旧版 App 仍可连接已保存的单台设备。App 0.1.3 及更早版本需卸载重装并重新配对。
224
275
 
225
276
  ## 卸载
226
277
 
package/lib/cli.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { execFile } from "node:child_process";
3
2
  import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
4
3
  import { homedir, networkInterfaces } from "node:os";
5
- import { basename, dirname, join, resolve } from "node:path";
4
+ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
6
5
  import { X509Certificate, createPrivateKey, createPublicKey } from "node:crypto";
6
+ import { execFile } from "node:child_process";
7
7
  import { generate } from "selfsigned";
8
8
  import "@deepseek-ai/cordis";
9
9
  import "@deepseek-ai/schemastery";
@@ -324,6 +324,88 @@ async function refreshManagedServerCertificate(setup, address) {
324
324
  });
325
325
  await Promise.all([atomicWrite(setup.tls.certFile, server.cert), atomicWrite(setup.tls.keyFile, server.private)]);
326
326
  }
327
+ //#endregion
328
+ //#region src/lan-setup.ts
329
+ const FIREWALL_TCP_RULE = "DSH Mobile HTTPS";
330
+ const FIREWALL_UDP_RULE = "DSH Mobile Discovery";
331
+ async function runElevatedPowerShell(script) {
332
+ await execFileText("powershell.exe", [
333
+ "-NoProfile",
334
+ "-NonInteractive",
335
+ "-Command",
336
+ [
337
+ "$ErrorActionPreference = 'Stop'; $process = Start-Process -FilePath 'powershell.exe' -Verb RunAs -WindowStyle Hidden -Wait -PassThru",
338
+ ` -ArgumentList @('-NoProfile','-NonInteractive','-EncodedCommand','${Buffer.from(script, "utf16le").toString("base64")}')`,
339
+ "; exit $process.ExitCode"
340
+ ].join(" ")
341
+ ], { windowsHide: true });
342
+ }
343
+ /** Install the two Windows inbound rules used by the managed LAN listener. */
344
+ async function configureWindowsFirewall(port) {
345
+ if (process.platform !== "win32") return;
346
+ await runElevatedPowerShell([
347
+ "$ErrorActionPreference = 'Stop'",
348
+ `Get-NetFirewallRule -DisplayName '${FIREWALL_TCP_RULE}' -ErrorAction SilentlyContinue | Remove-NetFirewallRule`,
349
+ `Get-NetFirewallRule -DisplayName '${FIREWALL_UDP_RULE}' -ErrorAction SilentlyContinue | Remove-NetFirewallRule`,
350
+ `New-NetFirewallRule -DisplayName '${FIREWALL_TCP_RULE}' -Direction Inbound -Action Allow -Protocol TCP -LocalPort ${String(port)} -RemoteAddress LocalSubnet -Profile Any | Out-Null`,
351
+ `New-NetFirewallRule -DisplayName '${FIREWALL_UDP_RULE}' -Direction Inbound -Action Allow -Protocol UDP -LocalPort ${String(port)} -RemoteAddress LocalSubnet -Profile Any | Out-Null`
352
+ ].join("; "));
353
+ }
354
+ /** Remove only the two Windows firewall rules owned by DSH Mobile. */
355
+ async function removeWindowsFirewall() {
356
+ if (process.platform !== "win32") return;
357
+ await runElevatedPowerShell([
358
+ "$ErrorActionPreference = 'Stop'",
359
+ `Get-NetFirewallRule -DisplayName '${FIREWALL_TCP_RULE}' -ErrorAction SilentlyContinue | Remove-NetFirewallRule`,
360
+ `Get-NetFirewallRule -DisplayName '${FIREWALL_UDP_RULE}' -ErrorAction SilentlyContinue | Remove-NetFirewallRule`
361
+ ].join("; "));
362
+ }
363
+ /** Generate private TLS material and persist a managed setup selected by the user. */
364
+ async function prepareManagedLanSetup(options) {
365
+ if (!isAbsolute(options.setupFile) || !isAbsolute(options.controlFile)) throw new Error("managed LAN setup paths must be absolute");
366
+ if (!Number.isSafeInteger(options.listenPort) || options.listenPort < 1024 || options.listenPort > 65535) throw new Error("managed LAN listenPort must be from 1024 through 65535");
367
+ if (!Number.isSafeInteger(options.dshPort) || options.dshPort < 1024 || options.dshPort > 65535) throw new Error("managed LAN dshPort must be from 1024 through 65535");
368
+ const directory = dirname(options.setupFile);
369
+ const tlsDirectory = join(directory, "tls");
370
+ await mkdir(tlsDirectory, {
371
+ recursive: true,
372
+ mode: 448
373
+ });
374
+ const managedTls = {
375
+ mode: "managed",
376
+ caCertFile: join(tlsDirectory, "ca.pem"),
377
+ caKeyFile: join(tlsDirectory, "ca-key.pem"),
378
+ certFile: join(tlsDirectory, "server-cert.pem"),
379
+ keyFile: join(tlsDirectory, "server-key.pem")
380
+ };
381
+ const ca = await ensureManagedCa(managedTls, {
382
+ certFile: join(tlsDirectory, "cert.pem"),
383
+ keyFile: join(tlsDirectory, "key.pem")
384
+ });
385
+ const setup = {
386
+ version: 2,
387
+ networkInterface: options.network.name,
388
+ listenPort: options.listenPort,
389
+ upstreamOrigin: `http://127.0.0.1:${String(options.dshPort)}`,
390
+ tls: managedTls
391
+ };
392
+ await refreshManagedServerCertificate(setup, options.network.address);
393
+ const androidCertificate = join(tlsDirectory, "dsh-mobile-ca.cer");
394
+ await writeFile(androidCertificate, ca.raw, { mode: 384 });
395
+ await Promise.all([...Object.values(managedTls).filter((value) => value !== "managed").map((file) => restrictPrivateFile(file)), restrictPrivateFile(androidCertificate)]);
396
+ if (options.configureFirewall) await configureWindowsFirewall(options.listenPort);
397
+ await Promise.all([writeFile(options.setupFile, `${JSON.stringify({
398
+ ...setup,
399
+ tls: Object.fromEntries(Object.entries(setup.tls).map(([key, value]) => [key, typeof value === "string" ? value.replaceAll("\\", "/") : value]))
400
+ }, null, 2)}\n`, { mode: 384 }), writeFile(options.controlFile, "{\"version\":1,\"enabled\":true}\n", { mode: 384 })]);
401
+ await Promise.all([restrictPrivateFile(options.setupFile), restrictPrivateFile(options.controlFile)]);
402
+ return Object.freeze({
403
+ setup: Object.freeze(setup),
404
+ network: Object.freeze({ ...options.network }),
405
+ origin: `https://${options.network.address}:${String(options.listenPort)}`,
406
+ androidCertificate
407
+ });
408
+ }
327
409
  Object.freeze({
328
410
  manifest: 65536,
329
411
  script: 1048576,
@@ -351,8 +433,6 @@ function assertExtensionId(value) {
351
433
  }
352
434
  //#endregion
353
435
  //#region src/cli.ts
354
- const FIREWALL_TCP_RULE = "DSH Mobile HTTPS";
355
- const FIREWALL_UDP_RULE = "DSH Mobile Discovery";
356
436
  function parseOptions(args) {
357
437
  let address;
358
438
  let port = 3443;
@@ -394,84 +474,16 @@ function parseOptions(args) {
394
474
  function dshHome() {
395
475
  return resolve(process.env.DSH_HOME ?? join(homedir(), ".dsh"));
396
476
  }
397
- async function runElevatedPowerShell(script) {
398
- await execFileText("powershell.exe", [
399
- "-NoProfile",
400
- "-NonInteractive",
401
- "-Command",
402
- [
403
- "$ErrorActionPreference = 'Stop'; $process = Start-Process -FilePath 'powershell.exe' -Verb RunAs -WindowStyle Hidden -Wait -PassThru",
404
- ` -ArgumentList @('-NoProfile','-NonInteractive','-EncodedCommand','${Buffer.from(script, "utf16le").toString("base64")}')`,
405
- "; exit $process.ExitCode"
406
- ].join(" ")
407
- ], { windowsHide: true });
408
- }
409
- async function configureWindowsFirewall(port) {
410
- if (process.platform !== "win32") return;
411
- const script = [
412
- "$ErrorActionPreference = 'Stop'",
413
- `Get-NetFirewallRule -DisplayName '${FIREWALL_TCP_RULE}' -ErrorAction SilentlyContinue | Remove-NetFirewallRule`,
414
- `Get-NetFirewallRule -DisplayName '${FIREWALL_UDP_RULE}' -ErrorAction SilentlyContinue | Remove-NetFirewallRule`,
415
- `New-NetFirewallRule -DisplayName '${FIREWALL_TCP_RULE}' -Direction Inbound -Action Allow -Protocol TCP -LocalPort ${String(port)} -RemoteAddress LocalSubnet -Profile Any | Out-Null`,
416
- `New-NetFirewallRule -DisplayName '${FIREWALL_UDP_RULE}' -Direction Inbound -Action Allow -Protocol UDP -LocalPort ${String(port)} -RemoteAddress LocalSubnet -Profile Any | Out-Null`
417
- ].join("; ");
418
- console.log("Windows will request administrator approval for two LAN-only firewall rules.");
419
- await runElevatedPowerShell(script);
420
- }
421
- async function removeWindowsFirewall() {
422
- if (process.platform !== "win32") return;
423
- await runElevatedPowerShell([
424
- "$ErrorActionPreference = 'Stop'",
425
- `Get-NetFirewallRule -DisplayName '${FIREWALL_TCP_RULE}' -ErrorAction SilentlyContinue | Remove-NetFirewallRule`,
426
- `Get-NetFirewallRule -DisplayName '${FIREWALL_UDP_RULE}' -ErrorAction SilentlyContinue | Remove-NetFirewallRule`
427
- ].join("; "));
428
- }
429
477
  async function setup(args) {
430
478
  const options = parseOptions(args);
431
479
  const preferredInterfaces = options.address === void 0 ? await preferredLanInterfaceNames() : [];
432
480
  const network = selectLanNetwork(options.address, void 0, void 0, preferredInterfaces);
433
481
  const home = dshHome();
434
482
  const directory = join(home, "mobile-access");
435
- const tls = join(directory, "tls");
436
- await mkdir(tls, {
483
+ await mkdir(directory, {
437
484
  recursive: true,
438
485
  mode: 448
439
486
  });
440
- const legacyCertFile = join(tls, "cert.pem");
441
- const legacyKeyFile = join(tls, "key.pem");
442
- const certFile = join(tls, "server-cert.pem");
443
- const keyFile = join(tls, "server-key.pem");
444
- const caCertFile = join(tls, "ca.pem");
445
- const caKeyFile = join(tls, "ca-key.pem");
446
- const androidCertificate = join(tls, "dsh-mobile-ca.cer");
447
- const managedTls = {
448
- mode: "managed",
449
- caCertFile,
450
- caKeyFile,
451
- certFile,
452
- keyFile
453
- };
454
- const ca = await ensureManagedCa(managedTls, {
455
- certFile: legacyCertFile,
456
- keyFile: legacyKeyFile
457
- });
458
- const managedSetup = {
459
- version: 2,
460
- networkInterface: network.name,
461
- listenPort: options.port,
462
- upstreamOrigin: `http://127.0.0.1:${String(options.dshPort)}`,
463
- tls: managedTls
464
- };
465
- await refreshManagedServerCertificate(managedSetup, network.address);
466
- await writeFile(androidCertificate, ca.raw, { mode: 384 });
467
- await Promise.all([
468
- restrictPrivateFile(caCertFile),
469
- restrictPrivateFile(caKeyFile),
470
- restrictPrivateFile(certFile),
471
- restrictPrivateFile(keyFile),
472
- restrictPrivateFile(androidCertificate)
473
- ]);
474
- if (options.configureFirewall) await configureWindowsFirewall(options.port);
475
487
  const customCss = join(directory, "mobile.css");
476
488
  try {
477
489
  await readFile(customCss);
@@ -507,14 +519,17 @@ async function setup(args) {
507
519
  mode: 448
508
520
  });
509
521
  await createExtensionScaffold(extensions, "custom", "自定义移动扩展", false);
510
- const origin = `https://${network.address}:${String(options.port)}`;
511
- await Promise.all([writeFile(join(directory, "setup.json"), `${JSON.stringify({
512
- ...managedSetup,
513
- tls: Object.fromEntries(Object.entries(managedSetup.tls).map(([key, value]) => [key, typeof value === "string" ? value.replaceAll("\\", "/") : value]))
514
- }, null, 2)}\n`, { mode: 384 }), writeFile(join(directory, "control.json"), "{\"version\":1,\"enabled\":true}\n", { mode: 384 })]);
515
- await Promise.all([restrictPrivateFile(join(directory, "setup.json")), restrictPrivateFile(join(directory, "control.json"))]);
516
- console.log(`DSH Mobile follows ${network.name} and is currently configured for ${origin}`);
517
- console.log(`Install this CA certificate on Android once: ${androidCertificate}`);
522
+ if (options.configureFirewall && process.platform === "win32") console.log("Windows will request administrator approval for two LAN-only firewall rules.");
523
+ const result = await prepareManagedLanSetup({
524
+ setupFile: join(directory, "setup.json"),
525
+ controlFile: join(directory, "control.json"),
526
+ network,
527
+ listenPort: options.port,
528
+ dshPort: options.dshPort,
529
+ configureFirewall: options.configureFirewall
530
+ });
531
+ console.log(`DSH Mobile follows ${network.name} and is currently configured for ${result.origin}`);
532
+ console.log(`Install this CA certificate on Android once: ${result.androidCertificate}`);
518
533
  console.log(`Ask DSH to customize the mobile Web UI and features in: ${customCss} and ${customScript}`);
519
534
  console.log(`Additional extensions live in: ${extensions}`);
520
535
  console.log("Start DSH with: dsh --profile web");