dsh-mobile 0.8.3 → 0.8.5

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 are recorded here. GitHub Releases provide the downloadable packages and generated commit notes.
4
4
 
5
+ ## 0.8.5 - 2026-11-08
6
+
7
+ - Update an existing Windows managed-LAN firewall rule in place and verify its resulting scope instead of deleting the previous rules first. Administrator cancellation or a policy rejection now retains the prior rule and records a bounded error in the plugin log. Thanks @lyc0110 for the reproduction in [#364](https://github.com/saya-ch/dsh-mobile/issues/364).
8
+ - Preserve the bounded stderr explanation from the iroh identity probe. An insecure Windows profile ancestry is now surfaced as a specific error, logged by the plugin, and shown with actionable guidance instead of a generic endpoint failure. Private-directory checks remain strict. Thanks @fgql for the diagnosis in [#365](https://github.com/saya-ch/dsh-mobile/issues/365).
9
+ - On macOS, warn when Full Disk Access is missing after a remote channel becomes ready, with a direct System Settings action and a dismiss control. The notice is advisory and never blocks pairing or remote access. Thanks @LunarFeller for the implementation and macOS testing in [#354](https://github.com/saya-ch/dsh-mobile/issues/354) and [#355](https://github.com/saya-ch/dsh-mobile/pull/355).
10
+ - Treat a missing or zero-byte per-module size probe as unmeasured. Such modules keep their original `/plugins` rows instead of being packed into one oversized mobile boot batch when an intercepting proxy returns empty 200 responses. Thanks @chenForbear for the measurements in [#363](https://github.com/saya-ch/dsh-mobile/issues/363).
11
+ - Add a narrow stale-while-revalidate cache for the two parameterless catalog RPCs (`llm/listProviders` and `session/modelCatalog`). Eligible warm requests share one bounded upstream fill, each caller keeps its own RPC ID, and catalog-affecting mutations invalidate the generation; `catalogCache: false` restores the ordinary proxy path. The cache does not patch upstream client internals or restore labels before the catalog is ready. Thanks @chintoleung for [#367](https://github.com/saya-ch/dsh-mobile/pull/367).
12
+ - Resolve an in-app plugin update against the profile that loaded the running module when a Desktop host omits the current-profile service. The install-path fallback remains limited to profiles below `$DSH_HOME/profiles`, so a launcher that omits `--profile` can no longer silently update `web` instead of `desktop`. Thanks @Kizaemon for the detailed reproduction in [#368](https://github.com/saya-ch/dsh-mobile/issues/368).
13
+ - Accept RFC 6598 `100.64.0.0/10` addresses as managed-LAN candidates, including networks whose router or ISP exposes CGNAT-style addressing for the local Ethernet or Wi-Fi segment. The admin Host-header check continues to reject CGNAT to preserve its DNS-rebinding defence. Thanks @Kizaemon for the diagnosis in [#369](https://github.com/saya-ch/dsh-mobile/issues/369).
14
+ - Plugin-only preparation candidate. Android 0.7.2 remains the published App; no native App change is included.
15
+
16
+ ## 0.8.4 - 2026-11-07
17
+
18
+ - Resolve query-only and relative upstream redirects against the URL that actually requested them. This fixes mobile `/api/file` image previews receiving the app document after a redirect, while external redirect targets remain unchanged. Thanks @owenzhao for the report in [#361](https://github.com/saya-ch/dsh-mobile/issues/361). Thanks @aaalzk for independently confirming it.
19
+ - Keep the conversation clear of the folded icon rail when the drawer is closed, without changing open-drawer, docked or stock DSH layouts. Thanks @LUgABeEEE for [#362](https://github.com/saya-ch/dsh-mobile/pull/362).
20
+ - Add browser access guidance for HarmonyOS and for using an existing virtual LAN such as StarVPN. Neither new environment is claimed as device- or end-to-end-tested.
21
+ - Plugin-only update. Android 0.7.2 remains the published App; this release contains no native App change or device-cache activation.
22
+
5
23
  ## 0.8.3 - 2026-11-05
6
24
 
7
25
  - Move the current DSH 0.2.2 compatibility test lane to 0.2.2-rc.1. Preserve the dedicated Mobile layout by default, and route App Back on a foreground upstream drawer to DSH's own dismissal handler.
package/CONTRIBUTORS.md CHANGED
@@ -4,6 +4,18 @@ GitHub 的 [Contributors 图表](https://github.com/saya-ch/dsh-mobile/graphs/co
4
4
 
5
5
  GitHub's Contributors graph counts commits on the default branch; issue reports and PR proposals cannot be manually added to it. This page credits community members by how they contributed. The repository maintainer and automated dependency updates remain visible in GitHub's history.
6
6
 
7
+ ## 0.8.5 贡献 / Contributions for 0.8.5
8
+
9
+ 感谢 [@lyc0110](https://github.com/lyc0110) 在 [#364](https://github.com/saya-ch/dsh-mobile/issues/364) 报告 Windows 防火墙先删后建的风险,[@fgql](https://github.com/fgql) 在 [#365](https://github.com/saya-ch/dsh-mobile/issues/365) 提供 iroh 私有目录失败的完整诊断,[@LunarFeller](https://github.com/LunarFeller) 在 [#355](https://github.com/saya-ch/dsh-mobile/pull/355) 提供 macOS 完全磁盘访问提示及真机验证,[@chenForbear](https://github.com/chenForbear) 在 [#363](https://github.com/saya-ch/dsh-mobile/issues/363) 提供模块批次测量,[@chintoleung](https://github.com/chintoleung) 提供 [#367](https://github.com/saya-ch/dsh-mobile/pull/367) 的 catalog 缓存,以及 [@Kizaemon](https://github.com/Kizaemon) 在 [#368](https://github.com/saya-ch/dsh-mobile/issues/368) 报告 Desktop 更新配置档错误、在 [#369](https://github.com/saya-ch/dsh-mobile/issues/369) 完成 CGNAT 局域网诊断。
10
+
11
+ Thanks [@lyc0110](https://github.com/lyc0110) for reporting the Windows firewall replacement risk in [#364](https://github.com/saya-ch/dsh-mobile/issues/364), [@fgql](https://github.com/fgql) for the detailed iroh private-directory diagnosis in [#365](https://github.com/saya-ch/dsh-mobile/issues/365), [@LunarFeller](https://github.com/LunarFeller) for the macOS Full Disk Access notice and device verification in [#355](https://github.com/saya-ch/dsh-mobile/pull/355), [@chenForbear](https://github.com/chenForbear) for the module-batch measurements in [#363](https://github.com/saya-ch/dsh-mobile/issues/363), [@chintoleung](https://github.com/chintoleung) for the catalog cache in [#367](https://github.com/saya-ch/dsh-mobile/pull/367), and [@Kizaemon](https://github.com/Kizaemon) for the Desktop profile-update reproduction in [#368](https://github.com/saya-ch/dsh-mobile/issues/368) and CGNAT LAN diagnosis in [#369](https://github.com/saya-ch/dsh-mobile/issues/369).
12
+
13
+ ## 0.8.4 贡献 / Contributions for 0.8.4
14
+
15
+ 感谢 [@owenzhao](https://github.com/owenzhao) 在 [#361](https://github.com/saya-ch/dsh-mobile/issues/361) 报告相对重定向导致移动图片预览失败。感谢 [@aaalzk](https://github.com/aaalzk) 独立确认,以及 [@LUgABeEEE](https://github.com/LUgABeEEE) 在 [#362](https://github.com/saya-ch/dsh-mobile/pull/362) 提供折叠栏布局修复。原作者提交保留;维护者修正另记。
16
+
17
+ Thanks [@owenzhao](https://github.com/owenzhao) for reporting the mobile image-preview redirect failure in [#361](https://github.com/saya-ch/dsh-mobile/issues/361). Thanks [@aaalzk](https://github.com/aaalzk) for independently confirming it, and [@LUgABeEEE](https://github.com/LUgABeEEE) for the folded-rail layout fix in [#362](https://github.com/saya-ch/dsh-mobile/pull/362). Original commits are retained, with maintainer corrections recorded separately.
18
+
7
19
  ## 0.8.3 贡献 / Contributions for 0.8.3
8
20
 
9
21
  感谢 [@xaxka](https://github.com/xaxka) 在 [#344](https://github.com/saya-ch/dsh-mobile/pull/344) 提供共享 FRP 用户名支持,[@LUgABeEEE](https://github.com/LUgABeEEE) 在 [#346](https://github.com/saya-ch/dsh-mobile/pull/346) 提供横屏布局判断,以及 [@chintoleung](https://github.com/chintoleung) 在 [#351](https://github.com/saya-ch/dsh-mobile/pull/351) 和 [#353](https://github.com/saya-ch/dsh-mobile/pull/353) 修复推送设置与插件页布局。原作者提交保留,维护者修正另记。
package/README.en.md CHANGED
@@ -28,7 +28,7 @@
28
28
  <a href="#compatibility">Compatibility</a> ·
29
29
  <a href="docs/README.md">Documentation</a> ·
30
30
  <a href="#contributors">Contributors</a> ·
31
- <a href="https://github.com/saya-ch/dsh-mobile/releases/tag/v0.8.3">Release notes</a> ·
31
+ <a href="https://github.com/saya-ch/dsh-mobile/releases/tag/v0.8.5">Release notes</a> ·
32
32
  <a href="README.md">简体中文</a>
33
33
  </p>
34
34
 
@@ -36,7 +36,7 @@
36
36
  >
37
37
  > **Using an iPhone or iPad?** Use Safari or [add DSH Mobile to your Home Screen as a web app (PWA)](<#iphone-and-ipad-home-screen-web-app-pwa>)—no native iOS download needed.
38
38
  >
39
- > **Current stable release: 0.8.3**. Adapt to DSH 0.2.2-rc.1, fix landscape, Plugins and Web Push settings layouts, support shared FRP usernames, and improve iroh pairing. Plugin and Android App releases remain independent. [Plugin release notes and downloads](https://github.com/saya-ch/dsh-mobile/releases/tag/v0.8.3) · [Full changelog](CHANGELOG.md).
39
+ > **Current stable release: 0.8.5**. Preserve existing Windows firewall rules, fix Desktop update targeting and CGNAT LAN discovery, and add a bounded catalog cache plus a macOS permission notice. Plugin and Android App releases remain independent. [Plugin release notes and downloads](https://github.com/saya-ch/dsh-mobile/releases/tag/v0.8.5) · [Full changelog](CHANGELOG.md).
40
40
  >
41
41
  > **Version policy**: since plugin **0.6.2**, the Android app and plugin use independent version numbers and releases. Their numbers do not need to match, and a plugin update does not imply an app update. [Compatibility notes](#compatibility).
42
42
 
@@ -108,7 +108,7 @@ Restart DSH, then search for **dsh-mobile** under **Settings → Plugin Market**
108
108
 
109
109
  </details>
110
110
 
111
- `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/`.
111
+ `setup` automatically selects and remembers the current LAN from RFC1918 and `100.64.0.0/10` candidates; Wi-Fi, hotspot, and IP changes normally recover without re-pairing. Use `--address 192.168.x.x` to name the actual address only when automatic selection fails. Settings, certificates, devices, and customization files live under `$DSH_HOME/mobile-access/`.
112
112
 
113
113
  ### Install the Android app
114
114
 
@@ -167,6 +167,8 @@ Browser pairing and reauthentication pages support Simplified Chinese, English a
167
167
 
168
168
  For manual browser pairing, enter the 43-character pairing code shown on the computer. Keys starting with `dsh1.` or `dsh2.` belong in the Android app, not the browser field. Prefer the pairing link copied from the computer, which fills the code automatically; generate a new link when it expires.
169
169
 
170
+ **HarmonyOS phones and tablets:** If you cannot install the Android APK, try opening the browser pairing link copied from the computer, preferably through a trusted HTTPS entry. There is no native HarmonyOS package, and browser use on physical HarmonyOS devices has not been verified. Check pairing, opening a conversation, and reconnecting. If one of these fails, report the device model, system and browser versions, and the failing step without sharing the original pairing link.
171
+
170
172
  ### Remote access
171
173
 
172
174
  Use this after the phone leaves the computer's network. Remote access is disabled by default, and the phone needs no separate Tailscale, cpolar, cloudflared, or FRP app.
@@ -263,8 +265,6 @@ Prefer DSH's built-in file upload for attachments. If an extension must simulate
263
265
 
264
266
  The 0.6.3 release adds optional [Worker execution](docs/EXTENSION_WORKERS.en.md) for trusted local JavaScript hosts. It is disabled by default, selectable by extension, and recovers explicitly without replaying interrupted actions. It is not a permission sandbox and does not require a matching App version.
265
267
 
266
- <sub>You can even use an extension to connect to SillyTavern running on the same computer, give it a lightweight mobile frontend, and open it from the same app.</sub>
267
-
268
268
  > `host.mjs` has the same privileges as a local program. Create and run only computer-side extensions that you understand and trust.
269
269
 
270
270
  The examples above, applied:
@@ -445,7 +445,9 @@ On macOS, local network, cloudflared, self-hosted FRP and the own reverse proxy
445
445
 
446
446
  | DSH Mobile plugin | Verified DeepSeek Harness version |
447
447
  | --- | --- |
448
- | `0.8.3` | The current DSH 0.2.2 test target is `0.2.2-rc.1`, alongside existing checks including `0.2.1-rc.5`. The upstream rc.1 npm CLI requires Node.js 24.9 or later |
448
+ | `0.8.5` | Maintenance and performance update on the `0.2.2-rc.1` packed-DSH target: catalog cache, Desktop update targeting, CGNAT LAN discovery, firewall protection and a macOS permission notice. The Android App remains 0.7.2 |
449
+ | `0.8.4` | Plugin maintenance update. The packed DSH test target remains `0.2.2-rc.1`, alongside existing compatibility checks. The upstream rc.1 npm CLI requires Node.js 24.9 or later |
450
+ | `0.8.3` | Adapted to DSH 0.2.2-rc.1 with landscape, Plugins and Web Push settings layout fixes, shared FRP usernames and iroh pairing improvements |
449
451
  | `0.8.2` | The DSH 0.2.2 preview target for this release was `0.2.2-alpha.3`. Fresh `0.2.2-alpha.2` installs can fail to save settings because of an upstream dependency mismatch; see the [independent reproduction](https://github.com/deepseek-ai/deepseek-harness/discussions/11994#discussioncomment-19373554). Users still on alpha.2 should upgrade to rc.1 |
450
452
  | `0.8.1` | Add packed boot, settings, directory selection, attachments, sidebar and compressed Workspace checks on `0.2.1-rc.5` and `0.2.2-alpha.2`; see [CI](https://github.com/saya-ch/dsh-mobile/actions/workflows/ci.yml) for the complete version matrix |
451
453
  | `0.8.0` | Push public-contract, settings, encrypted-payload and tap-ownership checks are pinned to `0.2.1-rc.2 / rc.3`; see [CI](https://github.com/saya-ch/dsh-mobile/actions/workflows/ci.yml) for the general boot/layout matrix. iOS device results accept contributor testing; browser checks do not establish every phone, plugin or public route |
package/README.md CHANGED
@@ -28,7 +28,7 @@
28
28
  <a href="#兼容性">兼容性</a> ·
29
29
  <a href="docs/README.md">文档</a> ·
30
30
  <a href="#贡献者">贡献者</a> ·
31
- <a href="https://github.com/saya-ch/dsh-mobile/releases/tag/v0.8.3">更新记录</a> ·
31
+ <a href="https://github.com/saya-ch/dsh-mobile/releases/tag/v0.8.5">更新记录</a> ·
32
32
  <a href="README.en.md">English</a>
33
33
  </p>
34
34
 
@@ -36,7 +36,7 @@
36
36
  >
37
37
  > **使用 iPhone 或 iPad?** 可以用 Safari 访问,或[添加到主屏幕作为 Web App(PWA)使用](<#iphone-与-ipad主屏幕-web-apppwa>),无需下载原生 iOS App。
38
38
  >
39
- > **当前正式版本:0.8.3**。适配 DSH 0.2.2-rc.1,修复横屏、插件页和网页推送设置布局,支持共享 FRP 用户名,并改进 iroh 配对。插件与 Android App 独立更新。[插件更新与下载](https://github.com/saya-ch/dsh-mobile/releases/tag/v0.8.3) · [完整更新记录](CHANGELOG.md)。
39
+ > **当前正式版本:0.8.5**。保留 Windows 防火墙旧规则,修复 Desktop 插件更新目标与 CGNAT 局域网识别,并加入有界目录缓存和 macOS 权限提示。插件与 Android App 独立更新。[插件更新与下载](https://github.com/saya-ch/dsh-mobile/releases/tag/v0.8.5) · [完整更新记录](CHANGELOG.md)。
40
40
  >
41
41
  > **版本说明**:自插件 **0.6.2** 起,Android App 与插件独立编号、独立发布,版本号无需一致;插件升级不代表 App 需要同步升级。[兼容说明](#兼容性)。
42
42
 
@@ -108,7 +108,7 @@ dsh plugin --profile web add dshmarket
108
108
 
109
109
  </details>
110
110
 
111
- `setup` 会自动选择并记住当前局域网,切换 Wi-Fi、热点或 IP 后通常自动恢复;仅在自动选择失败时使用 `--address 192.168.x.x`。设置、证书、设备和自定义文件保存在 `$DSH_HOME/mobile-access/`。
111
+ `setup` 会自动选择并记住当前局域网,候选覆盖 RFC1918 与 `100.64.0.0/10`;切换 Wi-Fi、热点或 IP 后通常自动恢复。仅在自动选择失败时使用 `--address 192.168.x.x` 指定实际地址。设置、证书、设备和自定义文件保存在 `$DSH_HOME/mobile-access/`。
112
112
 
113
113
  ### 安装 Android App
114
114
 
@@ -169,6 +169,8 @@ dsh plugin --profile web add dshmarket
169
169
 
170
170
  浏览器手动配对时填写电脑端显示的 43 位配对码;以 `dsh1.` 或 `dsh2.` 开头的是 Android App 密钥,不要填入浏览器配对框。优先打开电脑端复制的配对链接,它会自动填写配对码;过期后在电脑端重新生成。
171
171
 
172
+ **鸿蒙手机/平板**:如果无法安装 Android APK,可先用浏览器打开电脑端复制的网页配对链接,优先使用受信任的 HTTPS 入口。目前没有原生鸿蒙安装包,也未完成鸿蒙浏览器真机验收。请先确认能否配对、打开会话和重连;若失败,反馈时附机型、系统和浏览器版本,以及失败发生在哪一步,不要附原始配对链接。
173
+
172
174
  ### 远程访问
173
175
 
174
176
  适合手机离开电脑所在网络后使用。远程访问默认关闭,手机不需要另外安装 Tailscale、cpolar、cloudflared 或 FRP。
@@ -265,8 +267,6 @@ Funnel 的运行组件把公开监听生命周期绑定到父进程和受限控
265
267
 
266
268
  0.6.3 新增可选的 [Worker 执行模式](docs/EXTENSION_WORKERS.md),用于约束可信本地扩展的 JavaScript 阻塞;默认关闭,按扩展选择,故障后显式恢复,不自动重放操作。这不是权限沙箱,也不要求 App 同步版本。
267
269
 
268
- <sub>你甚至可以通过扩展连接电脑上运行的酒馆,并从同一 App 打开它的简易移动前端。</sub>
269
-
270
270
  > `host.mjs` 与本机程序拥有相同权限;仅创建和运行你理解并信任的电脑端扩展。
271
271
 
272
272
  示例的实际效果:
@@ -447,7 +447,9 @@ macOS 上局域网、cloudflared、自建 FRP 与自有反向代理可用;Funn
447
447
 
448
448
  | DSH Mobile 插件 | 已验证的 DeepSeek Harness 版本与范围 |
449
449
  | --- | --- |
450
- | `0.8.3` | 当前 DSH 0.2.2 验证目标为 `0.2.2-rc.1`,同时保留 `0.2.1-rc.5` 等既有版本检查。使用 rc.1 的 npm CLI 时,按上游要求使用 Node.js 24.9 或更新版本 |
450
+ | `0.8.5` | 插件维护与性能更新,继续使用 `0.2.2-rc.1` 的 DSH 打包验证目标;新增目录缓存、Desktop 更新目标修复、CGNAT 局域网识别、防火墙保护与 macOS 权限提示。Android App 仍为 0.7.2 |
451
+ | `0.8.4` | 插件维护更新,继续使用 `0.2.2-rc.1` 的 DSH 打包验证目标,并保留既有版本检查。使用 rc.1 的 npm CLI 时,按上游要求使用 Node.js 24.9 或更新版本 |
452
+ | `0.8.3` | 当时适配 DSH 0.2.2-rc.1,修复横屏、插件页和网页推送设置布局,支持共享 FRP 用户名,并改进 iroh 配对 |
451
453
  | `0.8.2` | 当时的 DSH 0.2.2 预览目标是 `0.2.2-alpha.3`。`0.2.2-alpha.2` 全新安装可能因上游依赖组合不一致而无法保存设置;[独立复现](https://github.com/deepseek-ai/deepseek-harness/discussions/11994#discussioncomment-19373554)。仍在使用 alpha.2 的用户建议升级到 rc.1 |
452
454
  | `0.8.1` | 新增 `0.2.1-rc.5`、`0.2.2-alpha.2` 的打包启动与设置、目录选择、附件、侧栏、压缩 Workspace 连接检查;完整版本矩阵见 [CI](https://github.com/saya-ch/dsh-mobile/actions/workflows/ci.yml) |
453
455
  | `0.8.0` | 网页推送的公共接口、设置、加密载荷与点击归属检查固定在 `0.2.1-rc.2 / rc.3`;通用启动与布局矩阵见 [CI](https://github.com/saya-ch/dsh-mobile/actions/workflows/ci.yml)。iOS 真机结果采信贡献者实测;浏览器检查不代表所有手机、第三方插件或公网线路已验收 |
@@ -49,6 +49,10 @@ macOS can ask on the computer whether DSH may access a protected location such a
49
49
 
50
50
  Before leaving the Mac unattended, start DSH the way you normally run it and open the workspace folder you intend to use remotely. Grant only the access you need, and review existing grants in **System Settings → Privacy & Security → Files and Folders**. You can also choose a location for new workspaces that does not require this permission. After addressing a prompt, retry with your existing pairing. Remote access does not require Full Disk Access. See [Apple's guide to file and folder access](https://support.apple.com/en-us/guide/mac-help/mchld5a35146/mac).
51
51
 
52
+ Before leaving, use an already paired device on the intended remote channel, open **the same folder** in the web directory picker, and confirm that it lists the contents. Gateway health and terminal `ls` do not test DSH's access to the folder. If macOS prompts, address it and retry with the existing pairing; do not reset system permissions or reinstall the plugin.
53
+
54
+ After a remote channel becomes ready, the Mobile Access panel may also offer Full Disk Access. This is optional guidance for unattended access to protected folders; it does not block pairing or remote access. The button only opens System Settings and the plugin never changes permissions itself.
55
+
52
56
  ## Share the report
53
57
 
54
58
  Reports retain versions, interface names, masked endpoint suffixes, failure stages, timings, DNS categories and measurement scope. They do not read conversations or include pairing secrets, Tokens, Cookies, complete hostnames/IPs or proxy credentials. Context such as an interface name can still be identifying; review before posting.
@@ -49,6 +49,10 @@ macOS 可能在电脑上要求 DSH 授权访问「桌面」「文稿」「下载
49
49
 
50
50
  无人值守前,在 Mac 上用实际启动方式运行 DSH,并打开远程会用到的工作区目录。若弹出提示,只在确有需要时授权;已有目录权限可在「系统设置 → 隐私与安全性 → 文件与文件夹」检查。也可将新的工作区放在不需要此类授权的目录。处理提示后,先用原有配对重试。远程访问不以开启「完全磁盘访问」为前提。参见 [Apple 的文件与文件夹权限指南](https://support.apple.com/zh-cn/guide/mac-help/mchld5a35146/mac)。
51
51
 
52
+ 离开前,用已配对设备走计划使用的远程通道,在网页目录选择器里进入**同一个目录**并确认能列出内容。只检查网关健康或在终端运行 `ls`,不能验证 DSH 有权访问该目录。如果 Mac 弹出提示,处理后用原有配对重试;不用重置系统权限或重装插件。
53
+
54
+ 远程通道就绪后,移动访问面板也可能提示开启「完全磁盘访问」。这是为无人值守访问减少受保护目录授权弹窗的建议,不会阻断配对或远程连接;按钮只打开系统设置,不会由插件修改权限。
55
+
52
56
  ## 分享报告
53
57
 
54
58
  报告保留版本、接口名称、脱敏端点后缀、失败阶段、耗时、系统 DNS 类别和测量范围。不读取会话正文,不包含配对密钥、Token、Cookie、完整域名、完整 IP 或代理凭据。接口名称等上下文仍可能具有辨识性,公开前应再检查一遍。
package/docs/IROH.en.md CHANGED
@@ -41,6 +41,7 @@ Before a component release, verify first use with the exact catalog-pinned execu
41
41
  | `iroh_companion_missing` | Nothing installed yet: press Install first, or point `DSH_MOBILE_IROH_COMPANION` at a companion absolute path to reuse |
42
42
  | `iroh_sidecar_invalid` | An installed file was modified; reinstalling overwrites it in place, or reinstall the companion for a damaged companion copy |
43
43
  | `iroh_platform_unsupported` | No sidecar is published for this OS/architecture |
44
+ | `iroh_data_directory_insecure` | A Windows parent directory of the state path grants write or replacement rights to another account. Move `DSH_HOME` to private storage or correct those ACLs; the plugin log retains the sidecar's bounded stderr explanation |
44
45
  | `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 |
45
46
  | `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 |
46
47
  | 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 |
package/docs/IROH.md CHANGED
@@ -41,6 +41,7 @@ iroh 身份和证书必须存放在仅当前用户或受信系统账户可写的
41
41
  | `iroh_companion_missing` | 还没点安装(或配套不在常规布局旁);先点安装官方组件,或用 `DSH_MOBILE_IROH_COMPANION` 指向配套绝对路径复用 |
42
42
  | `iroh_sidecar_invalid` | 已装文件被改动过;重新点安装即可覆盖修复,配套副本损坏则重装配套 |
43
43
  | `iroh_platform_unsupported` | 当前系统/架构没有 sidecar 发布 |
44
+ | `iroh_data_directory_insecure` | Windows 状态目录的某个上级目录允许其他账户写入或替换。请把 `DSH_HOME` 移到私有目录,或修正上级目录 ACL;插件日志会保留 sidecar 的脱敏 stderr 说明 |
44
45
  | `iroh_identity_invalid` / `iroh_identity_failed` | 先检查状态目录及父目录是否私有、已有身份是否损坏,保留原文件再排查;`id` 仅检查已有身份,首次创建使用 `tls-prepare`,不是靠复制或删除身份修复 |
45
46
  | `iroh_start_timeout` | 查看 DSH 日志中的 iroh 错误及网络连接,确认固定组件可运行;不要反复重新配对来替代进程排查 |
46
47
  | 通道 `ready` 但手机连不上 | 检查 App 是否为完整版、设备是否支持打包的 ABI,以及描述符是否有效;电脑就绪不等于手机已连通,连接中继服务也不等于数据一定经过中继 |
@@ -138,6 +138,14 @@ The gateway groups upstream application modules into start batches (`/mobile-acc
138
138
 
139
139
  Resources without upstream validators do not automatically acquire a reliable one. Compare the same complete resource URL during ordinary navigation, reload and app cold start: inspect `200`/`304`, cache hits, `If-None-Match`, encoding and actual transfer volume. Do not clear site data first and treat the result as normal cache behavior. Improved HTTP validation does not guarantee cache reuse in every WebView, and these headers alone do not establish a complete fix for [#107](https://github.com/saya-ch/dsh-mobile/issues/107).
140
140
 
141
+ ## Model selector on slow links
142
+
143
+ The gateway keeps a narrow stale-while-revalidate cache for exactly two catalog RPCs: `llm/listProviders` and `session/modelCatalog`. On an eligible warm request it answers from the cache immediately, so the corresponding gateway→DSH round trip and host-side catalog rebuild are skipped; concurrent callers share one upstream fill. A cached entry is served only while younger than 60 seconds since its fill completed; after that the next request waits for a fresh fill. Within that window a stale entry (older than 5 seconds) is still served immediately while one background refresh runs outside the caller's critical path. The catalog response still transfers to the phone on every request — this removes the upstream wait, not the phone↔gateway transfer, and it does not push an update into a page that has already rendered its selector.
144
+
145
+ Responses served this way carry the live caller's RPC ID, `Cache-Control: no-store`, and an `x-dsh-mobile-catalog: hit | stale | replay | miss-passthrough` marker for diagnostics. Supported gateway-visible changes that affect the catalog (credentials, provider settings, account sign-in state, plugin enablement, and a session's saved default model) invalidate the cache immediately; edits made outside this gateway are bounded by the 60-second ceiling and by the client's own refetch behavior. A response that cannot be safely shared (an error, unexpected headers, an oversized body) is never cached: the phone then performs one ordinary request, which can add a single extra read attempt and its latency on a cold uncacheable response.
146
+
147
+ Set `catalogCache: false` in the `mobile-access` entry and save, then restart DSH (or reload the plugin) to restore the unchanged proxy path. Immediate label restoration from the saved `modelSelection` projection while the catalog is unresolved remains an upstream limitation: the public label slot does not apply in that state, and this plugin does not patch DSH client internals ([#290](https://github.com/saya-ch/dsh-mobile/issues/290)).
148
+
141
149
  ## Manual compaction and long-running API requests (0.6.0)
142
150
 
143
151
  Commands such as `/compact` sent from the phone traverse the Mobile gateway's HTTP API. Automatic compaction inside DSH does not traverse that gateway request. Older gateways used the default 30-second `upstreamTimeoutMs` for every proxied request, so manual compaction could return `502 upstream_unavailable` while the model was still producing the summary. [#171](https://github.com/saya-ch/dsh-mobile/issues/171) records this case.
@@ -138,6 +138,14 @@ websocketCompression:
138
138
 
139
139
  上游未提供校验标识的资源不会因此自动得到一个可靠标识。检查重复下载时,比较同一完整资源 URL 在普通打开、重新加载和 App 冷启动中的 `200`/`304`、缓存命中、`If-None-Match`、编码与实际传输量;不要先清除站点数据再把结果当作正常缓存行为。HTTP 校验语义改善不保证每个 WebView 都会复用缓存,也不能仅凭这些响应头认定 [#107](https://github.com/saya-ch/dsh-mobile/issues/107) 已完全解决。
140
140
 
141
+ ## 慢速链路上的模型选择器
142
+
143
+ 网关仅对两个目录 RPC(`llm/listProviders` 和 `session/modelCatalog`)保留一个窄范围的 stale-while-revalidate 缓存。符合条件的暖请求会立即由缓存应答,跳过对应的网关→DSH 往返和主机端目录重建;并发请求共享一次上游填充。缓存条目自填充完成起 60 秒内才会被应答,超时后下一个请求会等待新填充;在这段时间内,超过 5 秒的陈旧条目仍会立即应答,同时在调用方关键路径之外执行一次后台刷新。目录响应仍会在每次请求时传输到手机——这里消除的是上游等待,不是手机↔网关的传输,也不会向已经渲染完选择器的页面推送更新。
144
+
145
+ 以这种方式应答的响应携带当前调用方的 RPC ID、`Cache-Control: no-store`,以及用于诊断的 `x-dsh-mobile-catalog: hit | stale | replay | miss-passthrough` 标记。网关可见且会影响目录的改动(凭据、提供方设置、账户登录状态、插件启用状态,以及会话保存的默认模型)会立即使缓存失效;绕过本网关的修改受 60 秒上限和客户端自身重新拉取行为的约束。无法安全共享的响应(错误、异常响应头、超限正文)不会被缓存:手机随后会发起一次普通请求,冷启动且不可缓存的响应因此可能多出一次读取尝试及其延迟。
146
+
147
+ 在 `mobile-access` 条目中设置 `catalogCache: false` 并保存,再重启 DSH(或重新加载插件),即可恢复未改动的代理路径。目录未就绪时立即从已保存的 `modelSelection` 投影恢复标签仍是上游限制:公开的标签插槽不适用于该状态,本插件也不修补 DSH 客户端内部实现([#290](https://github.com/saya-ch/dsh-mobile/issues/290))。
148
+
141
149
  ## 手动压缩会话与长时间 API 请求(0.6.0)
142
150
 
143
151
  手机发出的 `/compact` 等命令要通过移动网关的 HTTP API;DSH 内部触发的自动压缩不经过这次网关请求。旧版网关对所有上游代理请求都使用默认 30 秒的 `upstreamTimeoutMs`,因此模型仍在生成摘要时,手动压缩可能先收到 `502 upstream_unavailable`。[#171](https://github.com/saya-ch/dsh-mobile/issues/171) 记录了这一情况。
@@ -25,6 +25,12 @@ A routed network may not support LAN broadcast discovery. If the computer is not
25
25
 
26
26
  This setting controls source access to the LAN Gateway, not source CIDRs for an external reverse proxy. Configure that separately with the [own HTTPS reverse proxy guide](SELF_HOSTED_ORIGIN.en.md). Do not expose the LAN Gateway or private HTTP backend directly to the public internet.
27
27
 
28
+ ## Use an existing virtual LAN
29
+
30
+ A virtual LAN such as [StarVPN](https://doc.starvpn.cn/) can carry an existing Mobile connection. DSH Mobile does not install the provider's client, sign in to its account, or select direct or relayed paths. Join both devices to the same controlled network, then check that the phone can reach the **actual Mobile HTTPS listener at an address covered by its certificate**. Use that entry to test pairing, resource loading, opening a conversation, and reconnecting. If broadcast discovery is unavailable, use the pairing link copied from the computer. StarVPN has not been tested end to end.
31
+
32
+ Adding a trusted subnet only changes which sources are allowed. It does not bind the service to another virtual IP or add that IP to its certificate. If the pairing link's address cannot be routed, use an existing controlled route to the original HTTPS address, or configure a reachable address with a valid certificate using the [own HTTPS reverse proxy guide](SELF_HOSTED_ORIGIN.en.md). Do not rewrite the pairing link to an arbitrary virtual IP, bypass certificate validation, or forward the DSH administration port.
33
+
28
34
  ## Accessing the NAS administration page
29
35
 
30
36
  The administration page's `/api/mobile-access` and the phone Gateway are separate entries. The additional trusted networks above do not open the management API. By default, the management API still accepts only direct loopback peers.
@@ -25,6 +25,12 @@ DSH Mobile 的默认 Windows HTTPS / Discovery 入站规则只允许 `LocalSubne
25
25
 
26
26
  此设置用于局域网 Gateway 的来源访问范围,不改变外部反向代理的来源 CIDR。自有代理请按[自有 HTTPS 反向代理指南](SELF_HOSTED_ORIGIN.md)配置;不要把局域网 Gateway 或私有 HTTP 后端映射到公网。
27
27
 
28
+ ## 接入已有虚拟局域网
29
+
30
+ [星空组网](https://doc.starvpn.cn/)等虚拟局域网可作为现有通道的网络层;DSH Mobile 不负责安装客户端、登录账号或选择直连/中转。两端加入同一受控网络后,先验证手机能否访问电脑**实际监听且证书匹配**的 Mobile HTTPS 地址,再用该入口配对并验证资源加载、会话和重连。广播发现不通时用电脑端复制的配对链接。星空组网尚未做过端到端验收。
31
+
32
+ 额外可信网段只控制来源,不会让服务监听新的虚拟 IP,也不会把虚拟 IP 加进证书。如果配对链接里的地址不能跨网路由,优先用组网已有的受控路由访问原 HTTPS 地址,或按[自有 HTTPS 反向代理指南](SELF_HOSTED_ORIGIN.md)配置手机能访问的、证书有效的地址。不要把配对链接里的地址随意替换成虚拟 IP、绕过证书校验或转发 DSH 管理端口。
33
+
28
34
  ## NAS 的管理网页访问
29
35
 
30
36
  管理网页的 `/api/mobile-access` 与手机连接的 Gateway 是两个入口。上面的额外可信网段设置不会开放管理 API。默认情况下,管理 API 仍只允许直连来源为回环地址。
package/lib/cli.js CHANGED
@@ -176,7 +176,7 @@ const VIRTUAL_INTERFACE_MARKERS = [
176
176
  const ROUTE_COMMAND_TIMEOUT_MS = 5e3;
177
177
  function privateIpv4(value) {
178
178
  const parts = value.split(".").map(Number);
179
- return parts.length === 4 && parts.every((part) => Number.isInteger(part) && part >= 0 && part <= 255) && (parts[0] === 10 || parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31 || parts[0] === 192 && parts[1] === 168);
179
+ return parts.length === 4 && parts.every((part) => Number.isInteger(part) && part >= 0 && part <= 255) && (parts[0] === 10 || parts[0] === 100 && parts[1] >= 64 && parts[1] <= 127 || parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31 || parts[0] === 192 && parts[1] === 168);
180
180
  }
181
181
  function networkCidr(address, cidr) {
182
182
  const prefix = Number(cidr.slice(cidr.lastIndexOf("/") + 1));
@@ -512,28 +512,65 @@ async function refreshManagedServerCertificate(setup, address) {
512
512
  // src/lan-setup.ts
513
513
  const FIREWALL_TCP_RULE = "DSH Mobile HTTPS";
514
514
  const FIREWALL_UDP_RULE = "DSH Mobile Discovery";
515
+ const MIN_MANAGED_PORT = 1024;
516
+ const MAX_MANAGED_PORT = 65535;
517
+ function assertManagedPort(port, name) {
518
+ if (!Number.isSafeInteger(port) || port < MIN_MANAGED_PORT || port > MAX_MANAGED_PORT) throw new Error(`${name} must be from ${String(MIN_MANAGED_PORT)} through ${String(MAX_MANAGED_PORT)}`);
519
+ }
520
+ function powerShellFailureDetail(error) {
521
+ if (!(error instanceof Error)) return "unknown failure";
522
+ const code = "code" in error && typeof error.code === "string" ? ` (${error.code})` : "";
523
+ const stderr = "stderr" in error && typeof error.stderr === "string" ? error.stderr.trim().replace(/\s+/gu, " ").slice(0, 512) : "";
524
+ return `${code}${stderr === "" ? "" : `: ${stderr}`}`;
525
+ }
515
526
  async function runElevatedPowerShell(script) {
516
- await execFileText("powershell.exe", [
517
- "-NoProfile",
518
- "-NonInteractive",
519
- "-Command",
520
- [
521
- "$ErrorActionPreference = 'Stop'; $process = Start-Process -FilePath 'powershell.exe' -Verb RunAs -WindowStyle Hidden -Wait -PassThru",
522
- ` -ArgumentList @('-NoProfile','-NonInteractive','-EncodedCommand','${Buffer.from(script, "utf16le").toString("base64")}')`,
523
- "; exit $process.ExitCode"
524
- ].join(" ")
525
- ], { windowsHide: true });
526
- }
527
- /** Install the two Windows inbound rules used by the managed LAN listener. */
527
+ const launch = [
528
+ "$ErrorActionPreference = 'Stop'; $process = Start-Process -FilePath 'powershell.exe' -Verb RunAs -WindowStyle Hidden -Wait -PassThru",
529
+ ` -ArgumentList @('-NoProfile','-NonInteractive','-EncodedCommand','${Buffer.from(script, "utf16le").toString("base64")}')`,
530
+ "; exit $process.ExitCode"
531
+ ].join(" ");
532
+ try {
533
+ await execFileText("powershell.exe", [
534
+ "-NoProfile",
535
+ "-NonInteractive",
536
+ "-Command",
537
+ launch
538
+ ], { windowsHide: true });
539
+ } catch (error) {
540
+ throw new Error(`Windows firewall operation failed${powerShellFailureDetail(error)}`, { cause: error });
541
+ }
542
+ }
543
+ function firewallRuleScript(name, protocol, port) {
544
+ return [
545
+ `$rules = @(Get-NetFirewallRule -DisplayName '${name}' -ErrorAction SilentlyContinue)`,
546
+ "if ($rules.Count -eq 0) {",
547
+ ` New-NetFirewallRule -DisplayName '${name}' -Direction Inbound -Action Allow -Protocol ${protocol} -LocalPort ${String(port)} -RemoteAddress LocalSubnet -Profile Any | Out-Null`,
548
+ "} else {",
549
+ " foreach ($rule in $rules) {",
550
+ " $rule | Set-NetFirewallRule -Enabled True -Direction Inbound -Action Allow -Profile Any",
551
+ ` $rule | Get-NetFirewallPortFilter | Set-NetFirewallPortFilter -Protocol ${protocol} -LocalPort ${String(port)}`,
552
+ " $rule | Get-NetFirewallAddressFilter | Set-NetFirewallAddressFilter -RemoteAddress LocalSubnet",
553
+ " }",
554
+ "}",
555
+ `$verified = @(Get-NetFirewallRule -DisplayName '${name}' -Enabled True -Direction Inbound -Action Allow -Profile Any -ErrorAction Stop)`,
556
+ `$matchingPort = @($verified | Get-NetFirewallPortFilter | Where-Object { $_.Protocol -eq '${protocol}' -and $_.LocalPort -contains '${String(port)}' })`,
557
+ `$matchingAddress = @($verified | Get-NetFirewallAddressFilter | Where-Object { $_.RemoteAddress -contains 'LocalSubnet' })`,
558
+ `if ($verified.Count -eq 0 -or $matchingPort.Count -eq 0 -or $matchingAddress.Count -eq 0) { throw "DSH Mobile firewall rule '${name}' is not active with the expected scope" }`
559
+ ];
560
+ }
561
+ /** Build the elevated Windows firewall script without removing a working rule first. */
562
+ function windowsFirewallSetupScript(port) {
563
+ assertManagedPort(port, "Windows firewall port");
564
+ return [
565
+ "$ErrorActionPreference = 'Stop'",
566
+ ...firewallRuleScript(FIREWALL_TCP_RULE, "TCP", port),
567
+ ...firewallRuleScript(FIREWALL_UDP_RULE, "UDP", port)
568
+ ].join("\n");
569
+ }
570
+ /** Install or update the two Windows inbound rules used by the managed LAN listener. */
528
571
  async function configureWindowsFirewall(port) {
529
572
  if (process.platform !== "win32") return;
530
- await runElevatedPowerShell([
531
- "$ErrorActionPreference = 'Stop'",
532
- `Get-NetFirewallRule -DisplayName '${FIREWALL_TCP_RULE}' -ErrorAction SilentlyContinue | Remove-NetFirewallRule`,
533
- `Get-NetFirewallRule -DisplayName '${FIREWALL_UDP_RULE}' -ErrorAction SilentlyContinue | Remove-NetFirewallRule`,
534
- `New-NetFirewallRule -DisplayName '${FIREWALL_TCP_RULE}' -Direction Inbound -Action Allow -Protocol TCP -LocalPort ${String(port)} -RemoteAddress LocalSubnet -Profile Any | Out-Null`,
535
- `New-NetFirewallRule -DisplayName '${FIREWALL_UDP_RULE}' -Direction Inbound -Action Allow -Protocol UDP -LocalPort ${String(port)} -RemoteAddress LocalSubnet -Profile Any | Out-Null`
536
- ].join("; "));
573
+ await runElevatedPowerShell(windowsFirewallSetupScript(port));
537
574
  }
538
575
  /** Remove only the two Windows firewall rules owned by DSH Mobile. */
539
576
  async function removeWindowsFirewall() {
@@ -547,8 +584,8 @@ async function removeWindowsFirewall() {
547
584
  /** Generate private TLS material and persist a managed setup selected by the user. */
548
585
  async function prepareManagedLanSetup(options) {
549
586
  if (!isAbsolute(options.setupFile) || !isAbsolute(options.controlFile)) throw new Error("managed LAN setup paths must be absolute");
550
- if (!Number.isSafeInteger(options.listenPort) || options.listenPort < 1024 || options.listenPort > 65535) throw new Error("managed LAN listenPort must be from 1024 through 65535");
551
- if (!Number.isSafeInteger(options.dshPort) || options.dshPort < 1024 || options.dshPort > 65535) throw new Error("managed LAN dshPort must be from 1024 through 65535");
587
+ assertManagedPort(options.listenPort, "managed LAN listenPort");
588
+ assertManagedPort(options.dshPort, "managed LAN dshPort");
552
589
  const directory = dirname(options.setupFile);
553
590
  const tlsDirectory = join(directory, "tls");
554
591
  await mkdir(tlsDirectory, {