dsh-remote-mobile 1.7.0 → 1.9.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
@@ -5,6 +5,37 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.9.0] - 2026-10-09
9
+
10
+ ### Fixed (适配 DSH 0.1.7+ 配置存储变更,修复插件配置无法持久化)
11
+
12
+ - **配置持久化迁移**:DSH 自 **0.1.7** 起(最早见于 v0.1.7-alpha.1 的 release notes,0.2.x 延续)官方已弃用 `~/.dsh/settings.yaml` 作为实时配置源(启动时一次性迁移进当前 profile 的 `cordis.patch.yml` 并改名为 `settings.yaml.imported`)。原实现持续写回该文件,导致免密开关、长期密码哈希、限频/锁定阈值在升级后无法持久化(重启即回落默认):
13
+ - 新增插件自有配置 `~/.dsh/remote-mobile/settings.json`(JSON、临时文件原子替换、权限 `0o600`),由 `SessionStore` 读写;
14
+ - 首次启动自动从旧 `settings.yaml` / `settings.yaml.imported` 一次性迁移,救回既有长期密码与限频配置;
15
+ - 保留旧版 `ctx.settings.register` 集成作为旧版 DSH 的尽力兼容(0.1.7+ 的 `SettingsForms` 无该 API,会安全跳过)。
16
+ - **防止已清空的密码复活**:配置迁移仅在自有 `settings.json` 不存在时执行一次;一旦自有配置存在,旧 `settings.yaml.imported` 不再参与合并,避免用户清空长期密码后重启又被旧值救回。决策逻辑抽为纯函数 `resolveInitialStoreOptions()` 并由单测锁定。
17
+ - **语言偏好读取适配**:`readGlobalLocale()` 改为优先读取当前 profile 的 `cordis.patch.yml`(locale entry),回退旧 `settings.yaml` / `settings.yaml.imported`,修复升级后登录页与接口语言恒为中文的问题。
18
+ - **测试**:新增 `test/settings-persistence.test.js` 覆盖自有配置读写、旧版迁移合并与 locale 解析。
19
+ - **文档**:README(中 / 英)更新配置存储位置与新增 FAQ Q8,说明 DSH 0.1.7+ 的 settings.yaml 迁移行为。
20
+
21
+ ---
22
+
23
+ ## [1.8.0] - 2026-10-08
24
+
25
+ ### Fixed & Security (适配 NAS / 网关子路径前缀反代,修复静默失败与开放重定向)
26
+
27
+ - **子路径 / 反向代理前缀自适应**:
28
+ - **前端设置面板与 SSE 长连接**:新增基于 `document.baseURI` 动态解析的 API 基础路径 `RM_API_BASE`,替换原先硬编码的 15 处 `/api/remote-mobile/...` 绝对路径。经统一网关子路径(如飞牛 fnOS `/app/dsh/`)反代时,请求不再丢失前缀,修复「点保存 / 生成配对码毫无反应」的静默失败;
29
+ - **独立手机登录页**:`verifyApiUrl` 与 `successRedirectUrl` 改为按当前路径与 `<base>` 动态推导,登录校验及登录成功后的跳转均保持在网关子路径内;
30
+ - **服务端 302 重定向**:新增 `resolveRedirectPath` / `resolveForwardedPrefix` 工具,使安全门禁(`gate.ts`)与 `/auth` 路由的重定向自动拼接 `X-Forwarded-Prefix`,不再跳出到宿主根路径。
31
+ - **错误处理与反馈**:
32
+ - **请求层容错**:封装 `safeFetchJson`,统一捕获非 2xx、HTML 错误页等异常响应并透传服务端 `reason`,避免 `res.json()` 解析失败被 Promise 链静默吞掉;
33
+ - **交互反馈**:设置面板与样式卡片补齐 `.catch` 与失败分支,改用 `showToast` / `showFeedback` 提示具体原因,杜绝操作无反馈。
34
+ - **安全加固**:严格校验 `X-Forwarded-Prefix`(`src/auth/prefix.ts`),仅接受同站绝对路径,拒绝 `//` 协议相对 URL、反斜杠、控制字符与协议前缀,防御开放重定向与 CRLF 注入。
35
+ - **文档**:README(中 / 英)新增 Q7,说明统一网关子路径部署注意事项、`X-Forwarded-Prefix` 依赖,以及本地回环 (127.0.0.1) 放行机制。
36
+
37
+ ---
38
+
8
39
  ## [1.7.0] - 2026-09-15
9
40
 
10
41
  ### Fixed & Compatibility (解决 Issue #6 · 适配官方新版三栏布局与右侧边栏)
package/README.md CHANGED
@@ -8,9 +8,17 @@
8
8
  [![npm downloads](https://img.shields.io/npm/dm/dsh-remote-mobile.svg?style=flat-square&color=22c55e)](https://www.npmjs.com/package/dsh-remote-mobile)
9
9
  [![node](https://img.shields.io/badge/node-%3E%3D18.0.0-8b5cf6?style=flat-square)](https://nodejs.org)
10
10
  [![license](https://img.shields.io/npm/l/dsh-remote-mobile.svg?style=flat-square&color=10b981)](https://github.com/IceApriler/dsh-remote-mobile/blob/master/LICENSE)
11
+ [![DSH](https://img.shields.io/badge/DSH-Web%20Profile-0ea5e9?style=flat-square)](https://github.com/IceApriler/dsh-remote-mobile)
12
+ [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://github.com/IceApriler/dsh-remote-mobile/pulls)
11
13
 
12
14
  <p align="center">
13
- <b>零改动底层代码 · 突破本地限制 · 扫码直连 · 工作区全功能复用 · 传输层加密</b>
15
+ <b>零改动底层代码 · 局域网 / Tailscale 安全接入 · 扫码配对 · 移动端样式适配 · 工作区能力复用 · 实时状态推送</b>
16
+ </p>
17
+
18
+ <p align="center">
19
+ 📱 <b>桌面级移动工作台</b>:手机上也能新建 / 切换工作区、执行终端命令<br>
20
+ 🛡️ <b>多重安全认证</b>:扫码配对 · 长期密码 · 免密直连 + RSA 加密 + 防爆破封禁<br>
21
+ 🎨 <b>移动端样式片段</b>:内置三段预设,不满意可自定义 CSS
14
22
  </p>
15
23
 
16
24
  [English Documentation](./README_EN.md) · [简体中文](./README.md)
@@ -23,6 +31,8 @@
23
31
  <a href="#preview">界面预览</a> •
24
32
  <a href="#features">功能特性</a> •
25
33
  <a href="#install">安装与更新</a> •
34
+ <a href="#advanced">高级配置</a> •
35
+ <a href="#storage">本地文件存储</a> •
26
36
  <a href="#faq">常见问题</a>
27
37
  </p>
28
38
 
@@ -37,15 +47,15 @@
37
47
 
38
48
  DSH 核心服务出于安全考虑默认仅监听本地回环地址(`127.0.0.1`),手机、平板或其他电脑无法从外部访问 Web 控制台。本项目通过**访问控制中间件**与**请求隔离技术**,安全地开放了 **Tailscale 虚拟私网** 以及 **本地局域网 (Wi-Fi/LAN)** 访问能力,并提供传输加密、扫码配对、长期密码认证与防暴力破解审计体系。
39
49
 
40
- 手机端访问时,插件还针对 DSH 界面做了**移动端样式适配**:侧边栏抽屉折叠与可拖拽展开把手、新版官方三栏 Grid 布局与右侧边栏无缝兼容、设置弹窗居中微缩、对话正文高密度排版等,让手机上的使用体验更贴近桌面端。
50
+ 手机端访问时,插件还针对 DSH 界面做了**移动端样式适配**:侧边栏抽屉折叠与可拖拽展开把手、新版官方三栏 Grid 布局与右侧边栏兼容、设置弹窗居中微缩、对话正文高密度排版等,让手机上的使用体验更贴近桌面端。
41
51
 
42
52
  ---
43
53
 
44
54
  <span id="advantages"></span>
45
55
  ## ⚡ 核心优势
46
56
 
47
- * 🚀 **突破网络壁垒,工作区全功能复用**:解决局域网和 Tailscale 无法访问 DSH Web 的问题。在移动端或外部设备访问时,**支持新建工作区、切换工作区、执行终端命令等桌面端核心功能**。
48
- * 📱 **手机端专属适配与同源复用**:直接复用 DSH Web 官方同源底座与生态插件(如 `dsh-pet` 宠物、任务看板等),无需单独维护手机端后台。插件已针对移动端做好了基础响应式适配(如侧边栏抽屉折叠、设置弹窗居中、对话正文字号与行距精细调小以提升一屏信息量),并内置**样式片段覆写功能**,支持开箱即用或按需注入自定义 CSS 样式小插件。
57
+ * 🚀 **突破网络壁垒,复用桌面端能力**:用于在局域网 / Tailscale 下访问 DSH Web。在移动端或外部设备访问时,**支持新建工作区、切换工作区、执行终端命令等常用桌面端操作**。
58
+ * 📱 **手机端专属适配与同源复用**:直接复用 DSH 官方 Web 前端(手机与桌面是同一套界面)与各类生态插件(如侧边栏、任务看板、终端面板等),无需单独维护手机端后台。插件已针对移动端做好了基础响应式适配(如侧边栏抽屉折叠、设置弹窗居中、对话正文字号与行距精细调小以提升一屏信息量),并内置**样式片段覆写功能**,支持开箱即用或按需注入自定义 CSS 样式小插件。
49
59
  * 🛡️ **安全门禁与传输加密**:内置 RSA 非对称公钥加密、`scrypt` 加盐慢哈希密码落盘、连续输错自动熔断锁定 IP,拦截公网/局域网未经授权的访问。
50
60
  * 📲 **扫码快速配对**:自动识别并生成 Tailscale CGNAT 与局域网专属访问链接与二维码,手机扫码即可完成长效授权。
51
61
  * 🔄 **SSE 实时状态推送**:设备上线、下线、注销、IP 锁定等安全事件即时推送,无需前端轮询。
@@ -65,7 +75,7 @@ dsh plugin --profile web add dsh-remote-mobile
65
75
  ```
66
76
 
67
77
  > [!IMPORTANT]
68
- > **生态插件安装建议**:**不建议安装 `@linxin666/dsh-web-all` 全家桶**,更推荐**按需安装单体功能插件**。全家桶将多个前端模块合并打包,可能引发移动端悬浮入口重复与手势交互冲突。如需任务看板、宠物等生态功能,建议直接安装对应的单体包(详见下方说明)。
78
+ > **生态插件安装建议**:**不建议安装 `@linxin666/dsh-web-all` 全家桶**,更推荐**按需安装单体功能插件**。全家桶将多个前端模块合并打包,可能引发移动端悬浮入口重复与手势交互冲突;如需任务看板、宠物等生态功能,建议直接安装对应的单体包(原因、推荐命令与共存说明见 [常见问题 Q6](#faq))。
69
79
 
70
80
  ---
71
81
 
@@ -94,22 +104,7 @@ dsh plugin --profile web add dsh-remote-mobile
94
104
  > **💡 说明**:插件自身注册已由 DSH Bundle 体系全自动处理,**无需**在 `cordis.patch.yml` 中额外添加 `id: remote-mobile`。Windows 用户可按 `Win + R` 键输入 `%USERPROFILE%\.dsh\profiles\web` 快速直达配置目录。
95
105
 
96
106
  > [!WARNING]
97
- > **⚠️ 插件兼容与共存说明:不建议安装聚合全家桶(`@linxin666/dsh-web-all`),更推荐「按需单独安装所需插件」**
98
- >
99
- > * **为什么不建议安装 `@linxin666/dsh-web-all` 全家桶?**
100
- > 1. **避免功能冲突与重复入口**:该全家桶将多个子插件的前端逻辑编译打包在同一个 `client.js` 中,其移动端自适应逻辑在模块加载时会自执行。即使在 `cordis.patch.yml` 中配置了 `web-ui-remote-web-ui: disabled: true`,浏览器加载脚本后依然会渲染出小鲸鱼悬浮按钮(`#dshRemoteWhale`),导致两个插件的移动端入口与手势交互出现重复和冲突。
101
- > 2. **体积更轻量、升级更解耦**:全家桶将多个功能捆绑在一起,按需安装单体插件可以保持环境精简,也便于未来各插件独立升级与维护。
102
- >
103
- > * **推荐的最佳实践(按需单独安装单体插件)**:
104
- > 建议按需安装各自独立的功能单体包,例如:
105
- > ```bash
106
- > # 推荐方式:需要哪个装哪个,职责清晰、互不干扰
107
- > dsh plugin --profile web add @linxin666/dsh-client-ui-task-board
108
- > dsh plugin --profile web add @linxin666/dsh-client-ui-web-ui-settings
109
- > dsh plugin --profile web add @linxin666/dsh-pet
110
- > dsh plugin --profile web add @linxin666/dsh-ssh
111
- > ```
112
- > *(注:自 v1.6.0 起,`dsh-remote-mobile` 已内置冲突防护机制,会自动前置注入 `dsh-remote-force-desktop` 并协同 CSS 避免小鲸鱼悬浮按钮 `#dshRemoteWhale` 重复挂载,保障已安装全家桶环境下的共存体验;若追求更清爽的环境,按需安装单体包依然是最佳实践)*。
107
+ > **不建议安装聚合全家桶(`@linxin666/dsh-web-all`)**:它会把多个子插件的前端逻辑合并打包到同一个 `client.js` 中,加载时自执行,可能与本插件产生移动端入口重复与手势冲突。推荐**按需安装所需单体插件**(如任务看板、宠物等)。完整原因、推荐命令与共存说明见 [常见问题 Q6](#faq)。
113
108
 
114
109
  ---
115
110
 
@@ -138,7 +133,7 @@ dsh web --no-open
138
133
 
139
134
  ---
140
135
 
141
- ### 手机端实机演示(同源免登与工作区全功能)
136
+ ### 手机端实机演示(同源免登与工作区能力)
142
137
 
143
138
  | 登录 | 对话列表字号缩放 | 侧边栏移动端样式 | 官方轨迹查看 |
144
139
  | :---: | :---: | :---: | :---: |
@@ -161,18 +156,20 @@ dsh web --no-open
161
156
  - **设备会话管理**:实时查看已连接设备的类型、操作系统、浏览器、来源 IP 及最近活跃时间,支持单设备注销与一键注销全部设备。
162
157
 
163
158
  ### 3. 安全防护
164
- - **传输层 RSA 非对称加密**:登录认证接口支持客户端 RSA 加密。在 HTTPS 等安全上下文下优先使用 Web Crypto 原生 `RSA-OAEP-SHA256`;在 DSH 默认的 HTTP 局域网/Tailscale 访问(非安全上下文)下回退至纯 JS 垫片,并使用 `crypto.getRandomValues()` 进行密码学安全随机数填充。敏感密码与配对码在客户端加密后再通过网络传输。
159
+ - **传输层 RSA 非对称加密**:登录认证接口在客户端即完成 RSA 加密,敏感密码与配对码加密后才经网络传输。
160
+ - **HTTPS 等安全上下文**:优先使用 Web Crypto 原生 `RSA-OAEP-SHA256`;
161
+ - **DSH 默认的 HTTP 局域网 / Tailscale 访问(非安全上下文)**:回退至纯 JS 垫片,并使用 `crypto.getRandomValues()` 做密码学安全随机数填充。
165
162
  - **scrypt 慢哈希存储**:服务端采用 `scrypt` 加盐慢哈希算法(`scrypt:${salt}:${hash}`)对密码进行落盘存储,比对过程采用 `crypto.timingSafeEqual` 恒定时间比较以防范时序侧信道攻击。
166
163
  - **防暴力破解与限频保护**:
167
164
  - 连续输错凭证达到阈值(默认 5 次)自动锁定该 IP 15 分钟,拦截后续验证请求并返回 HTTP 429;
168
165
  - 滑动窗口限频(默认 60 次/分钟),防范高频恶意刷量探测;
169
166
  - 访问审计与锁定状态持久化落盘,服务重启后自动恢复;
170
167
  - 支持管理员在管理面板中一键解锁指定 IP。
171
- - **智能静态资源放行机制**:安全门禁内置智能前端静态扩展名识别(放行 `.js`、`.css`、`.png`、`.svg`、`.woff2` 等 20+ 种合法资源),彻底拦截无扩展名或动态管理 API 请求(如 `/plugins/xxx/admin`),确保第三方生态插件前端正常展示的同时严密保护后台动态接口,无需用户在设置界面手动维护白名单。
172
- - **高可靠原子持久化与防抖**:采用毫秒级写盘防抖节流保护磁盘 I/O(会话数据 500ms、样式片段 300ms),配合 `beforeExit` 进程退出 Flush 保证数据不丢失;`settings.yaml` 与 `style-snippets.json` 采用临时文件原子替换(`renameSync`),防止异常中断损坏数据。
168
+ - **智能静态资源放行机制**:安全门禁内置智能前端静态扩展名识别(放行 `.js`、`.css`、`.png`、`.svg`、`.woff2` 等 20 种合法资源),拦截无扩展名或动态管理 API 请求(如 `/plugins/xxx/admin`),确保第三方生态插件前端正常展示的同时严密保护后台动态接口,无需用户在设置界面手动维护白名单。(DSH 核心静态目录 `/assets/` 属官方资源,单独无条件放行。)
169
+ - **高可靠原子持久化与防抖**:采用毫秒级写盘防抖节流保护磁盘 I/O(会话数据 500ms、样式片段 300ms),配合 `beforeExit` 进程退出 Flush 保证数据不丢失;`settings.json` 与 `style-snippets.json` 采用临时文件原子替换(`renameSync`),防止异常中断损坏数据。
173
170
  - **真实 IP 安全提取**:仅信任底层 Socket 真实连接地址,防范伪造的 `X-Forwarded-For` 欺骗攻击。
174
171
  - **本机回环 CSRF 防御**:插件的变更类 API 会校验浏览器同源信号(`Origin` / `Sec-Fetch-Site`),恶意网页驱使浏览器向 `127.0.0.1` 发起的跨站写请求将被直接拒绝;curl / 本机脚本等无浏览器信号的客户端不受影响。
175
- - **回环上下文不洗白**:上下文虚拟化仅作用于外部来源流量;本机回环请求保留原始 Host / Origin,使 DSH 底层自带的 DNS-Rebinding 与同源校验对外部域名继续生效。SSE 事件流仅限同源连接,不开放跨域读取。
172
+ - **本机回环请求不做上下文改写**:上下文虚拟化仅作用于外部来源流量;本机回环请求保留原始 Host / Origin(不"洗白"),使 DSH 自带的 DNS-Rebinding 与同源校验对外部域名继续生效。SSE 事件流仅限同源连接,不开放跨域读取。
176
173
 
177
174
  ### 4. 实时状态同步与国际化
178
175
  - **SSE 实时事件推送**:基于 Server-Sent Events 实现新设备接入、设备重连、会话撤销及安全告警的实时通知,内置连接双向关闭监听与幂等清理。
@@ -183,14 +180,18 @@ dsh web --no-open
183
180
 
184
181
  DSH Web 界面在手机上仍有不少沿袭桌面端的样式问题,插件内置「移动端样式片段」模块,把移动端适配拆分为可独立启停的 CSS 片段:
185
182
 
186
- - **按界面区域划分的三段内置预设**:`preset-sidebar` 侧边栏抽屉导航(折叠 0 宽度、可拖拽悬浮把手、自适应抽屉宽度与 44px 触控友好区)、`preset-settings` 设置面板适配(上下堆叠自适应、顶部导航横滑、内容纵向自然滚览)、`preset-main` 对话正文**高密度排版**(小字号 12.5px + 紧凑行距 + 收紧边距 → 每行展示更多内容;基于稳定 HTML 元素与 localName 后缀,不做布局缩放,无右侧留白),默认移动端启用、PC 关闭;
183
+ - **按界面区域划分的三段内置预设**(默认移动端启用、PC 关闭):
184
+ - `preset-sidebar` **侧边栏抽屉导航**:折叠时侧边栏 0 宽度占用、可拖拽悬浮把手、自适应抽屉宽度、44px 触控友好区;
185
+ - `preset-settings` **设置面板适配**:上下堆叠自适应、顶部导航横滑、内容纵向自然滚览;
186
+ - `preset-main` **对话正文高密度排版**:小字号 12.5px + 紧凑行距 + 收紧边距,每行展示更多内容;基于稳定 HTML 元素与 localName 后缀,不做布局缩放,无右侧留白;
187
187
  - **PC / 移动端分别启停(按视口宽度判定,与设备无关)**:每段预设和每个自定义片段都有独立的「🖥️ PC」「📱 移动端」两个开关;「移动端」= 窄视口(≤900px)生效——**PC 浏览器拉小窗口也会生效**,「PC」= 宽视口(>900px)生效,两端都开 = 全宽度生效;
188
+ - **关于 `html[data-dsh-mobile]` 选择器**:该标记仅在**移动端 UA** 请求时写入。若自定义 CSS 以 `html[data-dsh-mobile]` 打头,则只在手机等移动端 UA 下命中;希望「窄屏桌面窗口也生效」时请直接写选择器(无需 `html[data-dsh-mobile]` 前缀),注入层会自动包裹媒体查询。
188
189
  - **用户自定义(样式小插件)**:在 **设置 ⚙️ → 远程与移动端 → 🎨 移动端样式片段** 中粘贴自己的 CSS 即可新增片段,支持编辑/启停/删除/**一键复制**(每个片段「查看 CSS」旁都有「📋 复制样式」按钮),持久化于 `~/.dsh/remote-mobile/style-snippets.json`,保存后下一次页面加载即生效,无需重启;
189
- - **按 UA 打标记 + 按宽度生效**:样式按视口宽度档生效(见上),与设备 UA 无关;移动端 UA 请求额外给 `<html>` 打上 `data-dsh-mobile="1"` 标记作为作用域钩子,并注入可拖拽的展开把手脚本(全端注入,运行时仅在侧边栏折叠时生效)。
190
+ - **按 UA 打标记 + 按宽度生效**:样式按视口宽度档生效(见上),与设备 UA 无关;移动端 UA 请求额外给 `<html>` 打上 `data-dsh-mobile="1"` 标记作为作用域钩子(以其作为选择器时仅移动端 UA 命中,详见上条),并注入可拖拽的展开把手脚本(全端注入,运行时仅在侧边栏折叠时生效)。
190
191
 
191
192
  ### 6. 插件通用共存保护
192
193
 
193
- - **启动零冲突**:`remoteWebUiPairing` 配对共享服务被其他远程/Web 接入类插件占用时,本插件自动让出(延迟裁决:等待激活窗口结束后检测服务名归属),彻底避免服务重名导致的整树回滚致命崩溃——从插件市场安装后可直接启动,无需任何手动配置;
194
+ - **启动零冲突**:`remoteWebUiPairing` 配对共享服务被其他远程/Web 接入类插件占用时,本插件自动让出(延迟裁决:等待激活窗口结束后检测服务名归属),避免服务重名导致的整树回滚崩溃——从插件市场安装后可直接启动,无需任何手动配置;
194
195
  - **冲突状态可见化**:检测到共存时,设置页顶部展示可关闭的警示横幅(中英文),说明让出行为与「只保留其一」的建议;处理完成后刷新页面即不再出现;
195
196
  - **一键诊断报告**:横幅内 **「📋 复制诊断与修复信息」** 按钮可复制完整诊断报告——运行时自动识别占用方插件包名与 loader entry id,报告含精确到行的修复配置与命令(禁用对方 / 卸载本插件二选一),粘贴给 AI 助手即可按步骤处理。
196
197
 
@@ -279,20 +280,24 @@ dsh plugin --profile web remove dsh-remote-mobile
279
280
 
280
281
  ---
281
282
 
283
+ <span id="advanced"></span>
282
284
  ## ⚙️ 高级配置
283
285
 
284
- 插件已完全接入 DSH 官方 Settings 体系,配置项支持在 Web 界面中直观调整,也可在 `~/.dsh/settings.yaml` 的 `dsh-remote-mobile` 命名空间下手动修改:
286
+ 配置持久化于插件自有文件 `~/.dsh/remote-mobile/settings.json`,可在 Web 界面中直观调整,也可直接编辑该文件:
285
287
 
286
- ```yaml
287
- dsh-remote-mobile:
288
- allowTailscale: false # boolean,默认 false:是否允许 Tailscale 虚拟私网免密访问
289
- allowLan: false # boolean,默认 false:是否允许局域网免密访问(高危警示)
290
- secretHash: "" # string,默认空:长期访问密码的 scrypt 加盐哈希值
291
- maxVisitsPerMinute: 60 # number,默认 60:单 IP 每分钟最大访问登录页次数
292
- maxFailedAttempts: 5 # number,默认 5:触发封禁的连续认证失败最大次数
293
- lockDurationMs: 900000 # number,默认 900000 (15分钟):IP 锁定持续时间(毫秒)
288
+ ```jsonc
289
+ {
290
+ "allowTailscale": false, // boolean,默认 false:是否允许 Tailscale 虚拟私网免密访问
291
+ "allowLan": false, // boolean,默认 false:是否允许局域网免密访问(高危警示)
292
+ "secretHash": "", // string,默认空:长期访问密码的 scrypt 加盐哈希值
293
+ "maxVisitsPerMinute": 60, // number,默认 60:单 IP 每分钟最大访问登录页次数
294
+ "maxFailedAttempts": 5, // number,默认 5:触发封禁的连续认证失败最大次数
295
+ "lockDurationMs": 900000 // number,默认 900000 (15分钟):IP 锁定持续时间(毫秒)
296
+ }
294
297
  ```
295
298
 
299
+ > **关于配置存储**:DSH 自 **0.1.7** 起(最早见于 v0.1.7-alpha.1 的 release notes,0.2.x 延续)官方已弃用 `~/.dsh/settings.yaml` 作为**实时配置源**——该文件会在启动时被一次性迁移进当前 profile 的 `cordis.patch.yml`,并改名为 `settings.yaml.imported`(详见 [Q8](#faq))。因此本插件不再写入 settings.yaml,而是持久化到上述自有 `settings.json`;首次启动时会自动从旧 `settings.yaml` / `settings.yaml.imported` 迁移一次(把旧长期密码、限频等配置救回)。
300
+
296
301
  ### 🎨 移动端样式片段(可选)
297
302
 
298
303
  自定义样式片段(样式小插件)与启停状态持久化于 `~/.dsh/remote-mobile/style-snippets.json`,可在设置面板图形化管理,也可直接编辑该文件:
@@ -303,7 +308,7 @@ dsh-remote-mobile:
303
308
  "presetStates": {
304
309
  "preset-sidebar": { "pc": false, "mobile": true },
305
310
  "preset-settings": { "pc": false, "mobile": true },
306
- "preset-main": { "pc": true, "mobile": true }
311
+ "preset-main": { "pc": false, "mobile": true }
307
312
  },
308
313
  "custom": [
309
314
  {
@@ -322,13 +327,14 @@ dsh-remote-mobile:
322
327
 
323
328
  ---
324
329
 
330
+ <span id="storage"></span>
325
331
  ## 📂 本地文件存储位置
326
332
 
327
333
  > **💡 路径提示**:在 macOS / Linux 下根目录为 `~/.dsh/`;在 Windows 下对应为 `C:\Users\<你的用户名>\.dsh\`(可在文件资源管理器地址栏直接输入 `%USERPROFILE%\.dsh` 直达)。
328
334
 
329
335
  | 文件路径 | 说明 | 安全级别 |
330
336
  |---|---|---|
331
- | `~/.dsh/settings.yaml` | 全局安全策略与免密开关配置 | 用户级读写 |
337
+ | `~/.dsh/remote-mobile/settings.json` | 插件配置:免密开关、长期密码哈希与限频/锁定策略 | 本地落盘,权限 `0o600`(仅当前用户可读写) |
332
338
  | `~/.dsh/remote-mobile/devices.json` | 已授权设备会话、IP 访问计数与安全审计数据(内含长效 Token,权限 `0o600`) | 本地落盘,仅当前用户可读写 |
333
339
  | `~/.dsh/remote-mobile/rsa-keys.json` | 服务端 RSA 密钥对文件(含公钥与私钥) | 本地落盘,权限 `0o600`(仅当前用户可读写) |
334
340
  | `~/.dsh/remote-mobile/style-snippets.json` | 移动端样式片段(内置预设启停状态 + 用户自定义 CSS 小插件) | 本地落盘持久化 |
@@ -388,7 +394,8 @@ dsh-remote-mobile:
388
394
  <details>
389
395
  <summary><b>Q4: 手机端样式还是不满意,想完全自己定制?</b></summary>
390
396
 
391
- **答**:打开 **设置 ⚙️ → 远程与移动端 → 🎨 移动端样式片段**,先尝试启停三段内置预设(侧边栏 / 设置面板 / 正文,每段都有独立的 PC 与移动端开关);还不够就在「自定义片段」里粘贴自己的 CSS(例如 `html[data-dsh-mobile] .xxx { ... }`)。保存后手机端刷新页面立即可见,改动全部落在 `~/.dsh/remote-mobile/style-snippets.json`,升级插件不会丢失。
397
+ **答**:打开 **设置 ⚙️ → 远程与移动端 → 🎨 移动端样式片段**,先尝试启停三段内置预设(侧边栏 / 设置面板 / 正文,每段都有独立的 PC 与移动端开关);还不够就在「自定义片段」里粘贴自己的 CSS(例如 `.我的选择器 { ... }`)。保存后手机端刷新页面立即可见,改动全部落在 `~/.dsh/remote-mobile/style-snippets.json`,升级插件不会丢失。
398
+ > 小提示:若选择器写成 `html[data-dsh-mobile] .xxx`,则只在移动端 UA 下生效,窄屏桌面窗口不会命中;希望两端窄屏都生效,请直接写选择器。
392
399
  </details>
393
400
 
394
401
  <details>
@@ -406,8 +413,39 @@ dsh-remote-mobile:
406
413
 
407
414
  **答**:主要是基于**避免功能冲突**与**环境轻量解耦**的考量:
408
415
  1. **避免移动端入口重复与手势交互冲突**:全家桶插件会将十几个独立功能插件的前端代码编译打包在同一个 `client.js` 中,其内置的移动端自适应逻辑在模块加载时会自执行。即使在 `cordis.patch.yml` 中配置了 `web-ui-remote-web-ui: disabled: true`,浏览器加载脚本后仍可能渲染出小鲸鱼悬浮按钮(`#dshRemoteWhale`),导致两个插件的移动端悬浮入口重叠以及触控手势冲突。
409
- 2. **轻量可控与独立升级**:按需单独安装所需插件(例如只需任务看板和宠物时,仅安装 `@linxin666/dsh-client-ui-task-board` 与 `@linxin666/dsh-pet`),既能保证环境轻盈,也便于各个插件独立升级、回滚与维护,避免依赖绑定。
410
- *(注:自 v1.6.0 起,`dsh-remote-mobile` 已内置无缝兼容处理,通过前置参数与 CSS 协同规避 `#dshRemoteWhale` 重复挂载,保障已安装全家桶环境下的正常使用;若追求更简洁稳定的开发与运行环境,按需安装单体包仍是最佳实践)*。
416
+ 2. **轻量可控与独立升级**:按需单独安装所需插件,既能保证环境轻盈,也便于各个插件独立升级、回滚与维护,避免依赖绑定。推荐按需安装各自独立的单体包,例如:
417
+ ```bash
418
+ # 需要哪个装哪个,职责清晰、互不干扰
419
+ dsh plugin --profile web add @linxin666/dsh-client-ui-task-board
420
+ dsh plugin --profile web add @linxin666/dsh-client-ui-web-ui-settings
421
+ dsh plugin --profile web add @linxin666/dsh-pet
422
+ dsh plugin --profile web add @linxin666/dsh-ssh
423
+ ```
424
+ *(注:自 v1.6.0 起,`dsh-remote-mobile` 已内置兼容防护——注入抑制脚本向 `sessionStorage` 写入 `dsh-remote-force-desktop=1` 并协同 CSS,避免 `#dshRemoteWhale` 被**重复挂载**;但小鲸鱼入口本身仍可能由对方渲染,入口重叠的根源在对方,故仍建议按需安装单体包)*。
425
+ </details>
426
+
427
+ <details>
428
+ <summary><b>Q7: 部署在 NAS / 统一网关 / Nginx 反向代理下(如飞牛 fnOS <code>/app/dsh/</code>)有什么注意事项?</b></summary>
429
+
430
+ **答**:
431
+ 1. **子路径与前缀自适应**:自 v1.8.0 起,前端已原生适配 `<base href="...">`,服务端重定向已支持 `X-Forwarded-Prefix` 请求头。经网关子路径代理访问时(如 `http://<ip>:5666/app/dsh/`),各项请求都会自动保持在当前子路径下:
432
+ * **设置面板与 SSE 长连接**:基于 `<base>` 动态解析 API 路径,保存配置、生成配对码、事件推送均不会丢失 `/app/dsh` 前缀(旧版本此处会表现为「点击毫无反应」);
433
+ * **独立手机登录页与 302 重定向**:登录校验、登录成功后的跳转,以及门禁 / `/auth` 的重定向都会保留当前子路径,不再跳回宿主根目录;
434
+ * **注意**:服务端 302 重定向依赖反向代理透传 `X-Forwarded-Prefix`;若代理未透传该头,重定向将回退到安全默认路径 `/auth`。
435
+ 2. **网关回环 (127.0.0.1) 放行机制**:
436
+ * 出于严密的安全防护考量(防止外部恶意客户端伪造 `X-Forwarded-For` 请求头绕过认证),插件门禁严格基于底层物理 Socket 连接判断客户端真实来源。
437
+ * 当反向代理/宿主网关直接从本机 `127.0.0.1` 代理至 DSH 时,请求会被识别为本地回环连接并直接放行——此时由网关/NAS 宿主自身的登录与权限隔离机制进行安全兜底。
438
+ * 若你希望通过外部统一域名/网关访问时仍强制要求输入本插件的动态配对码或长期访问密码,建议配置反向代理透明透传客户端源 IP,或直接通过 DSH 开放的外部端口(如默认的 `3080` 或自定义映射端口)直连访问。
439
+ </details>
440
+
441
+ <details>
442
+ <summary><b>Q8: 升级 DSH 0.1.7+(含 0.2.x)后 <code>~/.dsh/settings.yaml</code> 不见了 / 我的配置丢了?</b></summary>
443
+
444
+ **答**:这是 **DSH 官方自 0.1.7 起的行为变更**(最早见于 v0.1.7-alpha.1 的 release notes,0.2.x 延续),与本插件无关:
445
+
446
+ - DSH 自 **0.1.7** 起已**不再把 `~/.dsh/settings.yaml` 当作实时配置源**。启动时它由官方 `dsh-settings` 的 `importLegacyDocument()` **一次性导入**到当前 profile 的 `~/.dsh/profiles/<profile>/cordis.patch.yml`,随后原文件被**改名为 `settings.yaml.imported`**(先改名、再导入;若某个 section 找不到对应 entry,则会跳过,仅保留在该文件中,且不会重试)。官方 release notes 原文:「设置改由当前 Profile 的插件配置保存,支持声明过的实时更新字段;旧 settings.yaml 仅尝试导入一次,自定义设置插件需适配。」
447
+ - 因此升级后 `settings.yaml` 消失、只剩 `settings.yaml.imported` 属**正常现象**;模型 provider、locale 等应改为在 profile 的 `cordis.patch.yml` 中查看/编辑。
448
+ - **本插件自 v1.9.0 起**已改为把自身配置持久化到 `~/.dsh/remote-mobile/settings.json`,并在首次启动时**自动从旧 `settings.yaml` / `settings.yaml.imported` 迁移一次**(把长期密码哈希、免密开关、限频阈值等救回),不再依赖 DSH 的 settings.yaml。若你曾发现"长期密码失效/限频重置",升级到 v1.9.0 后会自动恢复。
411
449
  </details>
412
450
 
413
451
  ---
package/README_EN.md CHANGED
@@ -8,9 +8,17 @@
8
8
  [![npm downloads](https://img.shields.io/npm/dm/dsh-remote-mobile.svg?style=flat-square&color=22c55e)](https://www.npmjs.com/package/dsh-remote-mobile)
9
9
  [![node](https://img.shields.io/badge/node-%3E%3D18.0.0-8b5cf6?style=flat-square)](https://nodejs.org)
10
10
  [![license](https://img.shields.io/npm/l/dsh-remote-mobile.svg?style=flat-square&color=10b981)](https://github.com/IceApriler/dsh-remote-mobile/blob/master/LICENSE)
11
+ [![DSH](https://img.shields.io/badge/DSH-Web%20Profile-0ea5e9?style=flat-square)](https://github.com/IceApriler/dsh-remote-mobile)
12
+ [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://github.com/IceApriler/dsh-remote-mobile/pulls)
11
13
 
12
14
  <p align="center">
13
- <b>Zero Core Modifications · Break Loopback Limits · QR Quick Pairing · Full Workspace Parity · Transport Encryption</b>
15
+ <b>Zero Core Modifications · Secure LAN / Tailscale Access · QR Pairing · Mobile UI Adaptation · Workspace Parity · Real-Time Push</b>
16
+ </p>
17
+
18
+ <p align="center">
19
+ 📱 <b>Desktop-grade mobile workspace</b>: create / switch workspaces and run terminal commands on your phone<br>
20
+ 🛡️ <b>Multi-tier auth</b>: QR pairing · persistent password · passwordless bypass + RSA encryption + brute-force lockout<br>
21
+ 🎨 <b>Mobile style snippets</b>: three built-in presets, fully customizable with your own CSS
14
22
  </p>
15
23
 
16
24
  English Documentation · [简体中文](./README.md)
@@ -23,6 +31,8 @@ English Documentation · [简体中文](./README.md)
23
31
  <a href="#preview">UI Preview</a> •
24
32
  <a href="#features">Features</a> •
25
33
  <a href="#install">Installation & Update</a> •
34
+ <a href="#advanced">Advanced Configuration</a> •
35
+ <a href="#storage">Local File Storage</a> •
26
36
  <a href="#faq">FAQ</a>
27
37
  </p>
28
38
 
@@ -37,15 +47,15 @@ English Documentation · [简体中文](./README.md)
37
47
 
38
48
  By default, DSH strictly listens only to the local loopback address (`127.0.0.1`), preventing mobile devices, tablets, or external computers from accessing the Web console. This plugin uses **access control middleware** and **request isolation** to securely open up **Tailscale Private Network** and **Local Area Network (LAN / Wi-Fi)** access, complete with RSA transmission encryption, QR code pairing, persistent password auth, and automated brute-force defense.
39
49
 
40
- When accessed from a phone, the plugin also adapts the DSH interface for **mobile styles**: a collapsible sidebar drawer with a draggable floating toggle, seamless compatibility with the official 3-column Grid layout & right sidebar, a centered/scaled settings dialog, and dense conversation typography — so the mobile experience feels closer to the desktop.
50
+ When accessed from a phone, the plugin also adapts the DSH interface for **mobile styles**: a collapsible sidebar drawer with a draggable floating toggle, compatibility with the official 3-column Grid layout & right sidebar, a centered/scaled settings dialog, and dense conversation typography — so the mobile experience feels closer to the desktop.
41
51
 
42
52
  ---
43
53
 
44
54
  <span id="advantages"></span>
45
55
  ## ⚡ Key Advantages
46
56
 
47
- * 🚀 **Break Network Barriers with Full Workspace Parity**: Solves the core issue of accessing DSH Web over LAN and Tailscale. When accessed on mobile devices, **creating workspaces, switching workspaces, and executing terminal commands are supported** without feature degradation.
48
- * 📱 **Dedicated Mobile Adaptations & Same-Origin Reuse**: Directly reuses the official DSH Web base and complete plugin ecosystem (such as `dsh-pet`, task boards, etc.) without maintaining a separate mobile backend. Includes out-of-the-box mobile adaptations (sidebar drawer toggle, centered settings modal, and compact chat font scaling for higher information density), alongside a built-in **Style Snippet Override system** to toggle presets or inject custom CSS snippets on demand.
57
+ * 🚀 **Break Network Barriers with Desktop-Grade Workspace**: For accessing DSH Web over LAN / Tailscale. On mobile devices, **common desktop operations such as creating / switching workspaces and running terminal commands are supported**.
58
+ * 📱 **Dedicated Mobile Adaptations & Same-Origin Reuse**: Directly reuses the official DSH Web frontend (the very same UI as desktop) and the wider plugin ecosystem (sidebars, task boards, terminal panels, etc.) without maintaining a separate mobile backend. Includes out-of-the-box mobile adaptations (sidebar drawer toggle, centered settings modal, and compact chat font scaling for higher information density), alongside a built-in **Style Snippet Override system** to toggle presets or inject custom CSS snippets on demand.
49
59
  * 🛡️ **Security Gateway & Transport Encryption**: Built-in client-side RSA asymmetric public key encryption, `scrypt` salted slow-hashing password persistence, and automated IP lockout upon consecutive brute-force failures, blocking unauthorized access over public/LAN networks.
50
60
  * 📲 **Quick QR Pairing**: Auto-detects Tailscale CGNAT and LAN IP addresses to generate dedicated pairing QR codes. Scan the code to authenticate and obtain persistent credentials.
51
61
  * 🔄 **SSE Real-Time Push**: Device connection, reconnection, revocation, and security alert events are pushed in real time via Server-Sent Events without polling.
@@ -65,7 +75,7 @@ dsh plugin --profile web add dsh-remote-mobile
65
75
  ```
66
76
 
67
77
  > [!IMPORTANT]
68
- > **Ecosystem Plugin Advice**: **Installing `@linxin666/dsh-web-all` (all-in-one package) is not recommended**. We suggest **installing individual standalone plugins on demand**. The all-in-one package bundles multiple frontend modules together, which may result in duplicated mobile floating buttons and conflicting gesture handlers. If you need task boards, pet widgets, or other tools, install their standalone packages directly (see details below).
78
+ > **Ecosystem Plugin Advice**: **Installing `@linxin666/dsh-web-all` (all-in-one package) is not recommended**. We suggest **installing individual standalone plugins on demand**. The all-in-one package bundles multiple frontend modules together, which may result in duplicated mobile floating buttons and conflicting gesture handlers. If you need task boards, pet widgets, or other tools, install their standalone packages directly (see [FAQ Q6](#faq) for the rationale, recommended commands, and coexistence notes).
69
79
 
70
80
  ---
71
81
 
@@ -95,22 +105,7 @@ Because DSH defaults to `127.0.0.1`, ensure your `~/.dsh/profiles/web/cordis.pat
95
105
  > **💡 Note**: Plugin self-registration is automatically handled by the DSH Bundle system. You do **not** need to add `id: remote-mobile` manually. Windows users can press `Win + R` and enter `%USERPROFILE%\.dsh\profiles\web` to quickly jump to the configuration folder.
96
106
 
97
107
  > [!WARNING]
98
- > **⚠️ Plugin Compatibility & Coexistence Note: Prefer installing standalone plugins over the all-in-one bundle (`@linxin666/dsh-web-all`)**
99
- >
100
- > * **Why do we recommend avoiding `@linxin666/dsh-web-all`?**
101
- > 1. **Avoid duplicated entries and gesture conflicts**: The all-in-one package compiles the frontend logic of multiple plugins into a single `client.js`. Its mobile adaptation logic executes upon script load. Even if you set `web-ui-remote-web-ui: disabled: true` in `cordis.patch.yml`, the browser may still render the floating whale button (`#dshRemoteWhale`), leading to duplicated mobile entry points and conflicting touch gestures.
102
- > 2. **Lightweight and independently upgradable**: Installing standalone packages keeps the environment minimal and lets you upgrade, rollback, or configure each component independently.
103
- >
104
- > * **Recommended Best Practice (Install Standalone Plugins)**:
105
- > Install only the specific standalone packages you actually need, for example:
106
- > ```bash
107
- > # Recommended: clear responsibilities, decoupled, fully controllable
108
- > dsh plugin --profile web add @linxin666/dsh-client-ui-task-board
109
- > dsh plugin --profile web add @linxin666/dsh-client-ui-web-ui-settings
110
- > dsh plugin --profile web add @linxin666/dsh-pet
111
- > dsh plugin --profile web add @linxin666/dsh-ssh
112
- > ```
113
- > *(Note: Starting from v1.6.0, `dsh-remote-mobile` includes automated compatibility handling that prepends `dsh-remote-force-desktop` to sessionStorage and coordinates with CSS to prevent the `#dshRemoteWhale` button from mounting twice. For the cleanest setup, installing standalone packages remains the best practice)*.
108
+ > **Installing the all-in-one bundle (`@linxin666/dsh-web-all`) is not recommended**: it compiles the frontend logic of multiple sub-plugins into a single `client.js` that self-executes on load, which may duplicate mobile entry points and cause gesture conflicts with this plugin. Install only the standalone plugins you need (task board, pet, etc.). See [FAQ Q6](#faq) for the full rationale, recommended commands, and coexistence notes.
114
109
 
115
110
  ---
116
111
 
@@ -137,7 +132,7 @@ Open the DSH Web Console in your browser, navigate to **Settings ⚙️ -> Remot
137
132
 
138
133
  ---
139
134
 
140
- ### Mobile Live Demonstration (Same-Origin Auth & Full Workspace Control)
135
+ ### Mobile Live Demonstration (Same-Origin Auth & Workspace Control)
141
136
 
142
137
  | Login & Auth | Chat Font Scaling | Mobile Sidebar Styling | Official Trajectory / Trace |
143
138
  | :---: | :---: | :---: | :---: |
@@ -155,23 +150,25 @@ Open the DSH Web Console in your browser, navigate to **Settings ⚙️ -> Remot
155
150
 
156
151
  ### 2. Multi-Tier Authentication
157
152
  - **Dynamic 6-Digit Pairing Codes**: Generates 6-digit short codes (5-minute validity, single-use) that exchange for 365-day persistent authentication cookies on mobile.
158
- - **Persistent Access Passwords**: Set customizable persistent passwords (minimum 6 characters with letters and numbers) for seamless long-term logins across multiple devices.
153
+ - **Persistent Access Passwords**: Set customizable persistent passwords (minimum 6 characters with letters and numbers) for convenient long-term logins across multiple devices.
159
154
  - **Direct Bypass Mode**: Enable passwordless direct bypass independently for Tailscale or LAN. Disabling bypass immediately cleans up temporary credentials and resets device state.
160
155
  - **Device Session Management**: Real-time inspection of connected device types, OS, browser, source IP, and last active timestamp, with single-device revocation and one-click bulk revocation.
161
156
 
162
157
  ### 3. Security & Cryptography
163
- - **Transport RSA Asymmetric Encryption**: Login endpoints support client-side RSA encryption. In secure contexts (HTTPS) this prioritizes native Web Crypto `RSA-OAEP-SHA256`; in DSH's default HTTP LAN/Tailscale access (non-secure context) it falls back to a pure JS shim using `crypto.getRandomValues()` for cryptographically secure random padding. Sensitive passwords and pairing codes are encrypted on the client before network transmission.
158
+ - **Transport RSA Asymmetric Encryption**: The login endpoint encrypts on the client first, so sensitive passwords and pairing codes travel the network only in ciphertext.
159
+ - **Secure contexts (HTTPS)**: prioritizes native Web Crypto `RSA-OAEP-SHA256`;
160
+ - **DSH default HTTP LAN/Tailscale access (non-secure context)**: falls back to a pure JS shim using `crypto.getRandomValues()` for cryptographically secure random padding.
164
161
  - **scrypt Slow Hash Storage**: Server persists passwords with `scrypt` salted slow-hashing (`scrypt:${salt}:${hash}`). Verification uses `crypto.timingSafeEqual` constant-time comparison to prevent timing side-channel attacks.
165
162
  - **Brute-Force Defense & Rate Limiting**:
166
163
  - Automatically locks out IPs for 15 minutes after reaching consecutive failed attempt thresholds (default: 5), returning HTTP 429;
167
164
  - Sliding-window rate limiting (default: 60 visits/min) to prevent high-frequency brute-force scraping;
168
165
  - Audit logs and lockout states persist to disk and restore across restarts;
169
166
  - Administrators can manually unlock blocked IPs with one click in the panel.
170
- - **Intelligent Static Asset Whitelisting**: The authentication gate automatically allows verified frontend asset extensions (`.js`, `.css`, `.png`, `.svg`, `.woff2`, etc., 20+ types) while strictly blocking extensionless or dynamic API routes (e.g. `/plugins/xxx/admin`), ensuring smooth third-party plugin rendering without manual whitelist configuration.
171
- - **Resilient Atomic Persistence & Debounce**: Millisecond-level debounce write-throttling to safeguard disk I/O (500ms for session data, 300ms for style snippets), coupled with `beforeExit` process flush hooks and atomic temporary file replacement (`renameSync`) for `settings.yaml` and `style-snippets.json`.
167
+ - **Intelligent Static Asset Whitelisting**: The authentication gate automatically allows verified frontend asset extensions (`.js`, `.css`, `.png`, `.svg`, `.woff2`, etc., 20 types) while blocking extensionless or dynamic API routes (e.g. `/plugins/xxx/admin`), ensuring smooth third-party plugin rendering without manual whitelist configuration. (The DSH core static directory `/assets/` is an official resource and is allowed unconditionally.)
168
+ - **Resilient Atomic Persistence & Debounce**: Millisecond-level debounce write-throttling to safeguard disk I/O (500ms for session data, 300ms for style snippets), coupled with `beforeExit` process flush hooks and atomic temporary file replacement (`renameSync`) for `settings.json` and `style-snippets.json`.
172
169
  - **Real Socket IP Extraction**: Relies strictly on underlying Socket connection addresses, preventing spoofed `X-Forwarded-For` header attacks.
173
170
  - **Loopback CSRF Defense**: Mutating plugin APIs validate browser same-origin signals (`Origin` / `Sec-Fetch-Site`). Cross-site write requests to `127.0.0.1` driven by malicious webpages are rejected outright; non-browser clients (curl / local scripts) are unaffected.
174
- - **No Context Laundering for Loopback**: Context virtualization applies only to external traffic. Loopback requests keep their original Host / Origin, so the built-in DNS-rebinding and same-origin checks of DSH keep working against external domains. The SSE event stream accepts same-origin connections only, with no cross-origin reads.
171
+ - **No Context Rewriting for Loopback Requests**: Context virtualization applies only to external traffic. Loopback requests keep their original Host / Origin (no "laundering"), so DSH's built-in DNS-rebinding and same-origin checks keep working against external domains. The SSE event stream accepts same-origin connections only, with no cross-origin reads.
175
172
 
176
173
  ### 4. Real-Time Sync & Internationalization
177
174
  - **SSE Real-Time Push**: Push notifications for new device pairing, reconnections, revocations, and security alerts via Server-Sent Events, with bidirectional close listeners and idempotent cleanup.
@@ -182,14 +179,18 @@ Open the DSH Web Console in your browser, navigate to **Settings ⚙️ -> Remot
182
179
 
183
180
  The DSH web UI still carries many desktop-oriented styles on phones. This plugin ships a built-in **Mobile Style Snippets** module that splits mobile adaptation into toggleable CSS snippets:
184
181
 
185
- - **Three area-based built-in presets**: `preset-sidebar` (collapsed sidebar becomes a 0-width drawer with draggable floating toggle, adaptive width, and touch-friendly 44px tap targets), `preset-settings` (vertically stacked layout with horizontally scrollable nav tabs and natural vertical scrolling), `preset-main` (dense conversation typography with scrollable code blocks). Mobile on / PC off by default;
182
+ - **Three area-based built-in presets** (mobile on / PC off by default):
183
+ - `preset-sidebar` **Sidebar Drawer**: the collapsed sidebar occupies 0 width, with a draggable floating toggle, adaptive drawer width, and touch-friendly 44px tap targets;
184
+ - `preset-settings` **Settings Panel**: vertically stacked layout with horizontally scrollable nav tabs and natural vertical scrolling;
185
+ - `preset-main` **Dense Conversation Typography**: compact 12.5px text, tight line-height and margins for more content per line; built on stable HTML elements and localName suffixes, with no layout scaling and no right-side gaps;
186
186
  - **Separate PC / Mobile toggles (viewport-width based, device-independent)**: every preset and custom snippet has independent PC and Mobile switches; "Mobile" = applies at narrow viewports (≤900px) — **a PC browser with a narrow window gets it too**; "PC" = applies at wide viewports (>900px); both on = applies at all widths;
187
+ - **About the `html[data-dsh-mobile]` selector**: that marker is only written for **mobile-UA** requests. If your custom CSS starts with `html[data-dsh-mobile]`, it only matches phone/mobile-UA clients; to also target a narrow desktop window, write the selector directly (without the `html[data-dsh-mobile]` prefix) and the injection layer wraps it in the media query automatically.
187
188
  - **Custom snippets (your own mini-plugins)**: paste your CSS in **Settings ⚙️ → Remote & Mobile → 🎨 Mobile Style Snippets** to add a snippet, with edit/toggle/delete/**one-click copy** support (every snippet has a "📋 Copy CSS" button next to "View CSS"). Persisted to `~/.dsh/remote-mobile/style-snippets.json`; changes apply on the next page load without restart;
188
- - **UA-based marker + width-based styling**: styles apply by viewport-width band (see above), independent of device UA; mobile UA requests additionally add `data-dsh-mobile="1"` to `<html>` as a scope hook, and the draggable toggle script is injected for all clients (it only activates while the sidebar is collapsed).
189
+ - **UA-based marker + width-based styling**: styles apply by viewport-width band (see above), independent of device UA; mobile UA requests additionally add `data-dsh-mobile="1"` to `<html>` as a scope hook (using it as a selector only matches mobile UA — see the note above), and the draggable toggle script is injected for all clients (it only activates while the sidebar is collapsed).
189
190
 
190
191
  ### 6. Generic Plugin Coexistence Protection
191
192
 
192
- - **Conflict-free startup**: when the shared pairing service `remoteWebUiPairing` is already registered by another remote/Web access plugin, this plugin yields automatically (deferred arbitration: it waits until the activation window settles, then checks who owns the service name), completely avoiding the fatal whole-tree rollback caused by service-name collisions — installing from the plugin market works out of the box with zero manual configuration;
193
+ - **Conflict-free startup**: when the shared pairing service `remoteWebUiPairing` is already registered by another remote/Web access plugin, this plugin yields automatically (deferred arbitration: it waits until the activation window settles, then checks who owns the service name), avoiding the whole-tree rollback caused by service-name collisions — installing from the plugin market works out of the box with zero manual configuration;
193
194
  - **Visible coexistence status**: when a conflict is detected, a dismissible warning banner (Chinese/English) appears at the top of the settings panel, explaining the yield behavior and the "keep only one" recommendation; refresh after resolving and it disappears;
194
195
  - **One-click diagnosis report**: the **"📋 Copy diagnosis & fix"** button in the banner copies a full report — the conflicting plugin's package name and loader entry id are auto-detected at runtime, and the report includes line-exact fix configuration and commands (disable the other / remove this one), ready to paste to an AI assistant.
195
196
 
@@ -278,20 +279,24 @@ dsh plugin --profile web remove dsh-remote-mobile
278
279
 
279
280
  ---
280
281
 
282
+ <span id="advanced"></span>
281
283
  ## ⚙️ Advanced Configuration
282
284
 
283
- Fully integrated with the DSH official Settings system. Configurations can be adjusted in the Web UI or edited in `~/.dsh/settings.yaml` under the `dsh-remote-mobile` namespace:
285
+ Configurations persist in the plugin-owned file `~/.dsh/remote-mobile/settings.json`; adjust them in the Web UI or edit the file directly:
284
286
 
285
- ```yaml
286
- dsh-remote-mobile:
287
- allowTailscale: false # boolean, default false: allow passwordless access via Tailscale
288
- allowLan: false # boolean, default false: allow passwordless access via LAN (High Risk)
289
- secretHash: "" # string, default empty: scrypt salted hash of the persistent password
290
- maxVisitsPerMinute: 60 # number, default 60: max login page visits per minute per IP
291
- maxFailedAttempts: 5 # number, default 5: max consecutive failed attempts before IP lockout
292
- lockDurationMs: 900000 # number, default 900000 (15 mins): IP lockout duration in ms
287
+ ```jsonc
288
+ {
289
+ "allowTailscale": false, // boolean, default false: allow passwordless access via Tailscale
290
+ "allowLan": false, // boolean, default false: allow passwordless access via LAN (High Risk)
291
+ "secretHash": "", // string, default empty: scrypt salted hash of the persistent password
292
+ "maxVisitsPerMinute": 60, // number, default 60: max login page visits per minute per IP
293
+ "maxFailedAttempts": 5, // number, default 5: max consecutive failed attempts before IP lockout
294
+ "lockDurationMs": 900000 // number, default 900000 (15 mins): IP lockout duration in ms
295
+ }
293
296
  ```
294
297
 
298
+ > **About config storage**: Since DSH **0.1.7** (first noted in the v0.1.7-alpha.1 release notes; continued in 0.2.x), `~/.dsh/settings.yaml` is no longer a live configuration source — on boot it is imported once into the active profile's `cordis.patch.yml` and renamed to `settings.yaml.imported` (see [Q8](#faq)). This plugin therefore no longer writes to settings.yaml and persists to its own `settings.json` above; on first launch it automatically migrates once from the legacy `settings.yaml` / `settings.yaml.imported` (recovering your old persistent password, rate limits, etc.).
299
+
295
300
  ### 🎨 Mobile Style Snippets (optional)
296
301
 
297
302
  Custom snippets (style mini-plugins) and their enabled states are persisted in `~/.dsh/remote-mobile/style-snippets.json`. Manage them from the settings panel or edit the file directly:
@@ -302,7 +307,7 @@ Custom snippets (style mini-plugins) and their enabled states are persisted in `
302
307
  "presetStates": {
303
308
  "preset-sidebar": { "pc": false, "mobile": true },
304
309
  "preset-settings": { "pc": false, "mobile": true },
305
- "preset-main": { "pc": true, "mobile": true }
310
+ "preset-main": { "pc": false, "mobile": true }
306
311
  },
307
312
  "custom": [
308
313
  {
@@ -321,13 +326,14 @@ Custom snippets (style mini-plugins) and their enabled states are persisted in `
321
326
 
322
327
  ---
323
328
 
329
+ <span id="storage"></span>
324
330
  ## 📂 Local File Storage Locations
325
331
 
326
332
  > **💡 Path Note**: On macOS / Linux, the base path is `~/.dsh/`; on Windows, it resolves to `C:\Users\<username>\.dsh\` (you can directly paste `%USERPROFILE%\.dsh` into the File Explorer address bar).
327
333
 
328
334
  | Path | Description | Security Level |
329
335
  |---|---|---|
330
- | `~/.dsh/settings.yaml` | Global security policies & bypass toggles | User R/W |
336
+ | `~/.dsh/remote-mobile/settings.json` | Plugin config: bypass toggles, persistent-password hash, rate-limit & lockout policy | Local storage, `0o600` (Restricted to current user) |
331
337
  | `~/.dsh/remote-mobile/devices.json` | Authorized devices, sessions & IP audit statistics (contains long-lived tokens, `0o600`) | Local storage, restricted to current user |
332
338
  | `~/.dsh/remote-mobile/rsa-keys.json` | Server RSA keypair (public & private) | Local storage, `0o600` (Restricted to current user) |
333
339
  | `~/.dsh/remote-mobile/style-snippets.json` | Mobile style snippets (built-in toggles + custom CSS mini-plugins) | Local persistent storage |
@@ -381,7 +387,8 @@ If your log instead shows `service "remoteWebUiPairing" has been registered`, pl
381
387
  <details>
382
388
  <summary><b>Q4: Not happy with the mobile styles and want full control?</b></summary>
383
389
 
384
- **A**: Open **Settings ⚙️ → Remote & Mobile → 🎨 Mobile Style Snippets**. First try toggling the three area presets (sidebar / settings / conversation), each with independent PC and Mobile switches; if that is not enough, paste your own CSS into the "Custom Snippets" area (e.g. `html[data-dsh-mobile] .xxx { ... }`). Refresh the mobile page to see the effect immediately. Everything lives in `~/.dsh/remote-mobile/style-snippets.json` and survives plugin upgrades.
390
+ **A**: Open **Settings ⚙️ → Remote & Mobile → 🎨 Mobile Style Snippets**. First try toggling the three area presets (sidebar / settings / conversation), each with independent PC and Mobile switches; if that is not enough, paste your own CSS into the "Custom Snippets" area (e.g. `.my-selector { ... }`). Refresh the mobile page to see the effect immediately. Everything lives in `~/.dsh/remote-mobile/style-snippets.json` and survives plugin upgrades.
391
+ > Tip: if your selector starts with `html[data-dsh-mobile]`, it only applies to mobile-UA clients and will not match a narrow desktop window; drop the prefix if you want both.
385
392
  </details>
386
393
 
387
394
  <details>
@@ -405,8 +412,39 @@ If your log instead shows `service "remoteWebUiPairing" has been registered`, pl
405
412
 
406
413
  **Answer**: This is primarily based on **avoiding functional conflicts** and **keeping the environment lightweight and decoupled**:
407
414
  1. **Avoid duplicated mobile entries and touch gesture conflicts**: All-in-one packages compile the frontend logic of dozens of sub-plugins into a single `client.js`. Its mobile adaptation logic executes upon script load. Even if you set `web-ui-remote-web-ui: disabled: true` in `cordis.patch.yml`, the browser may still render the floating whale button (`#dshRemoteWhale`), causing duplicated mobile floating entry points and touch gesture interference.
408
- 2. **Lightweight and independently upgradable**: Installing standalone packages on demand (e.g. installing only `@linxin666/dsh-client-ui-task-board` and `@linxin666/dsh-pet` when needed) maintains a minimal dependency footprint and allows independent upgrading, rollback, and maintenance.
409
- *(Note: As of v1.6.0, `dsh-remote-mobile` provides built-in compatibility handling to prevent `#dshRemoteWhale` from mounting repeatedly, ensuring smooth coexistence when an all-in-one package is present; for the cleanest environment, installing standalone plugins remains the recommended practice)*.
415
+ 2. **Lightweight and independently upgradable**: Installing standalone packages on demand keeps a minimal dependency footprint and allows independent upgrading, rollback, and maintenance. Install only the standalone packages you need, for example:
416
+ ```bash
417
+ # Install only what you need — clear responsibilities, no interference
418
+ dsh plugin --profile web add @linxin666/dsh-client-ui-task-board
419
+ dsh plugin --profile web add @linxin666/dsh-client-ui-web-ui-settings
420
+ dsh plugin --profile web add @linxin666/dsh-pet
421
+ dsh plugin --profile web add @linxin666/dsh-ssh
422
+ ```
423
+ *(Note: As of v1.6.0, `dsh-remote-mobile` includes compatibility protection — an injected suppression script writes `dsh-remote-force-desktop=1` into `sessionStorage` and coordinates with CSS to prevent `#dshRemoteWhale` from being **mounted twice**. The whale entry itself may still be rendered by the other plugin, and the root cause of the overlap lies there, so installing standalone packages is still recommended)*.
424
+ </details>
425
+
426
+ <details>
427
+ <summary><b>Q7: What should I keep in mind when deploying behind a NAS / Unified Gateway / Nginx reverse proxy (e.g. fnOS <code>/app/dsh/</code>)?</b></summary>
428
+
429
+ **Answer**:
430
+ 1. **Sub-path & Prefix Adaptive**: As of v1.8.0, the frontend natively adapts to `<base href="...">`, and server-side redirects support the `X-Forwarded-Prefix` header. When accessed through a sub-path gateway (e.g. `http://<ip>:5666/app/dsh/`), requests automatically stay scoped within the current sub-path:
431
+ * **Settings panel & SSE live connection**: API paths are resolved dynamically from `<base>`, so saving config, generating pairing codes, and receiving live events never lose the `/app/dsh` prefix (on earlier versions this showed up as “clicking does nothing”);
432
+ * **Standalone mobile login page & 302 redirects**: login verification, post-login navigation, and the gate / `/auth` redirects all stay within the current sub-path instead of jumping out to the host root;
433
+ * **Note**: server-side 302 redirects depend on the reverse proxy forwarding `X-Forwarded-Prefix`; if it is not forwarded, redirects fall back to the safe default path `/auth`.
434
+ 2. **Gateway Loopback (127.0.0.1) Bypass Mechanism**:
435
+ * For rigorous security (preventing external attackers from forging `X-Forwarded-For` headers to bypass authentication), the plugin security gate strictly evaluates client authenticity based on the physical socket connection.
436
+ * When a reverse proxy / host gateway relays traffic directly from localhost `127.0.0.1` to DSH, the request is identified as a local loopback connection and allowed without pairing code prompts—backed by the NAS/host gateway's own authentication and permission boundary.
437
+ * If you wish to enforce the plugin's pairing code or persistent password even when entering through an external reverse proxy, configure the proxy to transparently pass client IPs or connect directly via the exposed DSH listening port (e.g. default port `3080` or custom mapped ports).
438
+ </details>
439
+
440
+ <details>
441
+ <summary><b>Q8: After upgrading to DSH 0.1.7+ (including 0.2.x), <code>~/.dsh/settings.yaml</code> is gone / my settings were lost?</b></summary>
442
+
443
+ **Answer**: This is an **official DSH behavior change introduced in 0.1.7** (first noted in the v0.1.7-alpha.1 release notes; continued in 0.2.x), unrelated to this plugin:
444
+
445
+ - Since DSH **0.1.7**, `~/.dsh/settings.yaml` is **no longer treated as a live configuration source**. On boot it is imported **once** by the official `dsh-settings` `importLegacyDocument()` into the active profile's `~/.dsh/profiles/<profile>/cordis.patch.yml`, then the original file is **renamed to `settings.yaml.imported`** (it renames first, then imports; a section with no matching entry is skipped, kept only in that file, and never retried). The official v0.1.7-alpha.1 release notes state that settings are now persisted as the current profile's plugin configuration (with declared live-update fields), and the legacy `settings.yaml` is imported only once — custom settings plugins must adapt.
446
+ - So after upgrading, `settings.yaml` disappearing and only `settings.yaml.imported` remaining is **expected**; model providers, locale, etc. should now be viewed/edited in the profile's `cordis.patch.yml`.
447
+ - **As of v1.9.0**, this plugin persists its own config to `~/.dsh/remote-mobile/settings.json` and, on first launch, **migrates once from the legacy `settings.yaml` / `settings.yaml.imported`** (recovering the persistent-password hash, bypass toggles, rate limits, etc.). It no longer depends on DSH's settings.yaml. If you saw a "persistent password stopped working / limits reset" symptom, upgrading to v1.9.0 restores it automatically.
410
448
  </details>
411
449
 
412
450
  ---
package/lib/auth/gate.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { isTailscaleIp, isLanIp, getClientIp } from './tailscale.js';
2
+ import { resolveRedirectPath } from './prefix.js';
2
3
  /**
3
4
  * 判断请求是否来自本机回环
4
5
  */
@@ -117,8 +118,9 @@ export function createGlobalAuthGate(store) {
117
118
  const accept = req.headers['accept'] || '';
118
119
  const isHtmlRequest = req.method === 'GET' && (accept.includes('text/html') || req.url === '/' || req.url?.startsWith('/?'));
119
120
  if (isHtmlRequest) {
121
+ const authLocation = resolveRedirectPath(req.headers, '/auth');
120
122
  res.writeHead(302, {
121
- Location: '/auth',
123
+ Location: authLocation,
122
124
  'Cache-Control': 'no-cache, no-store, must-revalidate',
123
125
  });
124
126
  res.end();