Bundle
dsh-plugin-remote-access
Expose this machine's DSH web GUI to a chosen CIDR range (e.g. Tailscale), with an optional numeric access code, a phone layout that only affects remote pages, and the official UI preferences stored in this plugin's own config.
- Source
- Mzy123l
- stars
- 2 stars
- License
- MIT
- Updated
- Updated 7 days ago
Readme
# dsh-plugin-remote-access
给 **DeepSeek Harness(DSH)桌面版**用的插件:把本机 DSH 的网页界面**只开放给你指定的网段**
(默认 Tailscale 的 `100.64.0.0/10`),于是手机或另一台电脑也能用上它。
- **不绑 `0.0.0.0`**:只监听「允许网段」里属于本机的地址,网段外的对端一律 `403`。
- **不写死地址**:网段、监听地址、端口、上游、落地文件全是配置项。
- **两种进入方式**:复制带令牌的网址;或者设一个密码,手机开裸地址输一次(之后 30 天免密)。
- **手机界面能看**:「远程UI布局」可选手机 / 电脑 / 自动;手机布局**只作用于远程页面**,本机界面一个像素都不动。
- **手机上的官方界面偏好能留下来**:「UI 设置」把「通用设置」里的外观 / 字号 / 工作步骤展示 / 显示代码工作视图 /
性能与用量存进本插件的配置(默认「出厂值」=不干预官方默认),远程页面每次打开都重新套一遍 ——
官方那几项在手机上是「改完刷新即还原」的。
- **在设置页里改**:保存即热重载(原地关掉旧监听、按新参数重开),不用重启 DSH。
- **失败可见**:无论成功失败都会写一份自诊断文件,看不到 DSH 日志时也能定位。
## 安装
三种方式,选一种即可;**装完重启一次 DSH**。
**① 插件页(推荐)** —— 打开 DSH →「设置 → 插件 → 添加插件」,填下面任一:
| 填什么 | 从哪里装 |
|---|---|
| `dsh-plugin-remote-access` | 从 npm(1.2.0 起已发布,装起来最快) |
| `github:Mzy123l/dsh-plugin-remote-access` | 直接从 GitHub(要与仓库同步的最新代码时) |
| 本机某个目录的绝对路径 | 自己 clone / 下载下来时 |
> ⚠️ npm 上那两个旧名字 —— `dsh-remote-access-cidr`(**我们自己发的,停在 1.0.0**)和
> `dsh-remote-access`(**别人的包**)—— 都别再用来安装。当前名字是 `dsh-plugin-remote-access`:
> <https://www.npmjs.com/package/dsh-plugin-remote-access>。
> (页面上那个 `0.0.0-stage` 是 npm 给新包名自动建的占位版本,`latest` 指向 1.2.0,不用管它。)
> 📛 **改名(1.2.0):`dsh-remote-access-cidr` → `dsh-plugin-remote-access`。**
> bundle 的插件行是按**包名**装载的,所以旧来源装的那份不会自己跟过来:到「设置 → 插件」里把旧条目
> **移除**,再按新名字添加一次。移除有可能一并清掉 profile 里 `cordis.patch.yml` 中 `id: remote-access`
> 那一行的 `config:`(网段 / 密码 / UI 设置都在里面)—— 想留参数,先把它抄下来,装好再粘回去。
**② 命令行**(等价于插件页那一步;DSH 正在运行时 profile 写锁会占住):
```powershell
dsh plugin --profile desktop add dsh-plugin-remote-access
# 想跟仓库最新代码:dsh plugin --profile desktop add github:Mzy123l/dsh-plugin-remote-access
# CLI 若不认这两种写法,就填本机目录的绝对路径
```
**③ GitHub 一键取代码** —— 想把源码放到本机再装:
```powershell
irm https://raw.githubusercontent.com/Mzy123l/dsh-plugin-remote-access/main/install.ps1 | iex
```
脚本只负责**把代码取到一个固定目录**(默认 `%LOCALAPPDATA%\dsh-plugins\dsh-plugin-remote-access`),
装的动作仍然交给 DSH 自己(插件页填那个目录)。需要代理时:
```powershell
$s = irm https://raw.githubusercontent.com/Mzy123l/dsh-plugin-remote-access/main/install.ps1
& ([scriptblock]::Create($s)) -Proxy http://127.0.0.1:7890
```
> 为什么不一步装完:DSH 的插件安装(写 profile 的 `package.json` / `cordis.patch.yml`、跑包管理器)
> 只能由 DSH 自己做(插件页,或让 agent 用 `plugin_manager` 工具),手工改 profile 容易把它弄坏。
之后改参数都是热重载。改代码分两种:`client.js`(浏览器半区)**刷新页面**就会重新拉取;
`index.js`(宿主半区,含**新增配置字段**)要**重启一次 DSH** 才会加载。
## 安卓 App(APK,可选)
不想在手机浏览器里手打地址的话,可以用配套的外壳 App:
**[下载 dsh-remote-1.1.1.apk](https://github.com/Mzy123l/dsh-plugin-remote-access/releases/download/v1.1.1/dsh-remote-1.1.1.apk)**
([Release 页](https://github.com/Mzy123l/dsh-plugin-remote-access/releases) · 源码在 [`android/`](android/) · 说明见 [`android/README.md`](android/README.md))
App 名字就叫 **DSH**,装完图标是那个蓝色看板娘(自适应图标,圆形/方形启动器都正常)。
- **首屏就是地址页**:只填 `IP或域名:端口`(**不用写协议**,例 `100.64.0.3:19388`),HTTPS 一个勾决定;
- 连不上 / 填错 / 电脑没开时**自动退回地址页**,顶栏也永远有「设置」——**断网也改得回来**;
- 网页仍是电脑上那份 DSH:手机布局、`远程UI布局`、`UI 设置` 全由本插件的配置决定;
- 只申请 `INTERNET` / `ACCESS_NETWORK_STATE`,自签(`CN=DSH Remote`),同签名的后续版本可直接覆盖安装。
装的时候要允许「安装未知应用」。电脑那端仍然是本插件在监听(默认 `19388`),App 只是把网页装进壳里。
## 使用
### 方式一:设访问密码(推荐,手机不用复制长串)
1. 「设置 → 远程访问 → 访问密码」填 **4–12 位数字或字母**,保存(写别的字符会被就地拦下并说明规则)。
2. 手机打开 `http://<地址>:<端口>/` —— 地址和端口见状态文件里的 `手机访问:` 那一行。
3. 输一次密码即可进入,之后 **30 天免密**(cookie)。
4. 密码**输错一次**,该地址就**自动写进「排除的网段」**(形如 `100.x.y.z/32`)、之后一律 `403`;
解封 = 到「设置 → 远程访问 → 排除的网段」里把那一项删掉。
### 方式二:用带令牌的网址(不设密码)
打开状态文件里的 `远程访问网址: http://…/?token=…` **一次**,DSH 会把 token 换成自己的签名
cookie(默认 30 天);之后直接访问 `http://<地址>:<端口>/` 即可。没票就是 `401`。
> **每次启动 DSH 都会换一张 token**(用另一个 Windows 账户跑 DSH,也有它自己的一张),所以旧网址、
> 别的账户/上一次运行留下的网址里那个 `?token=…` 已经作废。拿它访问不会再把 DSH 那句英文甩给你了:
> 从当前版本起会看到插件自己的中文说明页(讲清「票作废」还是「没带票」,以及去哪儿拿当前那条)。
> 省事的做法:设了 `accessCode` 就直接打开**裸地址**
> (`http://<地址>:<端口>/`)输一次密码;没设就复制状态文件里**当前**那条带票网址。
> 旧 cookie 同理不通用(DSH 的 cookie 绑 `hostname:port`)。
### 状态文件
`<DSH_HOME>\remote-access-url.txt`(默认在 `%USERPROFILE%\.dsh`,权限 `0600`)里写着:带票网址、
手机访问地址、是否开启了解锁密码、已拉黑的地址与「待补写」的地址、放行密钥、当前生效的配置、
上游自检结果。打不开时先看它。
> ⚠️ 文件里有**访问令牌,等于本机操作权限**,不要外传(访问密码在里面打码成 `***`)。
## 配置
### 设置页里能改的
「设置 → 远程访问」里是 8 个常用项(**启用 / 允许的网段 / 排除的网段 / 端口 / 并发上限 / 日志级别 /
访问密码 / 远程UI布局**),外加一层可折叠的下一级菜单 **「UI 设置」**(5 项界面偏好,见下)。
保存会写进 profile 的 `cordis.patch.yml`,插件随即热重载(端口会变,新网址以状态文件为准)。
### 访问密码的规则
**访问密码只收 4–12 位数字或字母**(大小写敏感)。写别的(空格、符号、太短太长)**不会生效**,
而且**一定看得见**:设置页点保存时就地拦下并说明规则,`cordis.patch.yml` 里手写的非法值会在
状态文件的诊断里留一条 `「访问密码」没生效…`。这条提示是故意做的 —— 静默失效会让人以为密码设好了,
然后在手机上撞见 DSH 那句英文 401(这曾经是最难查的一种「坏了」)。
**改密码不会把已经进来的设备踢下线**:放行 cookie 的密钥与 `accessCode` 无关,单独存在
`<DSH_HOME>\remote-access-secret`(`0600`),DSH 重启后也照样有效。想让所有设备重新输一次密码,
删掉那个文件即可。
### 手机上的同一页也能改
DSH 的客户端策略让 `configForms` 在非回环页面不可写,但 Remote 通道本身是通的,
所以那一页直接调 `ctx.remote.settings.describe()` / `mutate()`,写盘跑在 Host 网关自己的上下文里,
落盘位置与桌面**完全相同**,而且等写盘完成才回执。
### 「UI 设置」这一组是什么、为什么要有副本
设置页里「远程访问」下面有一层可折叠的**下一级菜单「UI 设置」**,放的是官方「通用设置」里的五项界面偏好
(外观、字号大小、工作步骤展示、显示代码工作视图、性能与用量)。
官方这几项在**非回环页面**上只能「本页有效、刷新即还原」:ui-settings 把设置通道的持久化降级成内存态
(`persistence = ctx.remote.$host.isLoopback ? 'host' : 'memory'`),读写都到不了宿主 —— 手机端在官方设置里
改完,一刷新就回到默认。本插件因此存了同一批值,并在**每次打开远程页面**时重新套一遍(桌面端改完、
手机切回来也会重读):
- 用的是官方自己的「本页」写入入口(`theme.setTheme` / `theme.setFontSize` /
`configForms.developerTools.setEnabled` / 官方那两行设置项的 `setTranscriptView`、`setPerformanceUsage`),
与你在设置页手点那几行是**同一条路径**;
- **宿主的官方配置一个字都不写**;本机(回环)页面一个像素都不动(与 `remoteLayout` 同一条硬约束);
- `default`(出厂值)不是「什么都不做」,而是**显式套回官方默认值** —— 这样从「完全展开」改回「出厂值」
也能真的还原。官方默认值取自官方源码:外观 `system`、字号 `14`、工作步骤展示 Web 端 `detailed`
(桌面客户端 `standard`)、显示代码工作视图 `true`、性能与用量 `detailed`;
- 因此**本插件的配置优先**:在官方「通用设置」里改这几项只对当前页面有效,下一次重读配置时会被本插件的值
覆盖。想用官方的默认行为,就把这几项留在「出厂值」;
- 这一组是**新增的 Host 配置字段**,而宿主的配置 schema 是在 DSH 启动时读进来的:**第一次升级到这一版需要重启
一次 DSH**(之后改值、保存都不必重启;浏览器半区刷新页面即可生效)。
### 其余参数
其余参数只在 `cordis.patch.yml` 里配(也不建议常用):
| 键 | 默认 | 说明 | 设置页 |
|---|---|---|---|
| `enabled` | `true` | 关掉它只需设 `false`,不必卸载 | ✅ |
| `allowCidrs` | `['100.64.0.0/10']` | 允许来访的网段;也是 `listen: auto` 挑选本机监听地址的依据 | ✅ |
| `denyCidrs` | `[]` | 白名单里的例外黑名单;输错访问密码的地址会**自动加到这里**,删掉该项即解封 | ✅ |
| `port` | `0` | 监听端口,`0` = 系统随机 | ✅ |
| `maxConnections` | `64` | 并发连接上限(浏览器会开好几条 keep-alive,别设太小) | ✅ |
| `logLevel` | `'info'` | `silent` / `info` / `debug` | ✅ |
| `accessCode` | `''` | 解锁密码(**4–12 位数字或字母**)。设了就**不用复制 token**:手机开裸地址输一次即可;输错一次即把该地址写进「排除的网段」 | ✅ |
| `remoteLayout` | `'auto'` | 远程页面的界面布局:`auto`(按视口/指针自动判断)/ `phone`(强制手机布局,横竖屏自适应)/ `desktop`(与桌面一致)。**只作用于远程页面** | ✅ |
| `remoteTheme` | `'default'` | 「UI 设置」一组里的**外观**:`default`(出厂值)/ `dark`(深色)/ `light`(浅色)/ `system`(跟随系统)。**只作用于远程页面**,且不写宿主设置文档 | ✅ |
| `remoteFontSize` | `0` | 正文字号(10–22 px):`0` = 出厂值(14) | ✅ |
| `remoteTranscriptView` | `'default'` | 工作步骤展示:`default` / `compact`(简洁)/ `standard`(标准)/ `detailed`(详细)/ `verbose`(完全展开) | ✅ |
| `remoteDeveloperTools` | `'default'` | 显示代码工作视图:`default`(出厂值=开启)/ `on` / `off` | ✅ |
| `remotePerformanceUsage` | `'default'` | 性能与用量:`default` / `compact`(简洁)/ `detailed`(详细) | ✅ |
| `listen` | `['auto']` | `auto` = 只监听上面网段里的本机地址;也可写 `['100.x.y.z']` | — |
| `upstream` | `'auto'` | DSH 界面地址;`auto` 读 `ctx.webServer.port` | — |
| `rewriteHost` | `true` | 把 `Host`/`Origin` 改写成上游 authority | — |
| `forwardClientHeaders` | `true` | 转发 `x-forwarded-for` / `x-forwarded-proto` | — |
| `timeoutMs` | `0` | 等上游**响应头**的超时(毫秒),`0` = 不超时。响应头一到就交棒,正文/流式传输不会被它掐断 | — |
| `allowWebSocket` | `true` | 透传 WebSocket(界面实时推送靠它) | — |
| `urlFile` | `''` → `<DSH_HOME>/remote-access-url.txt` | 状态文件写哪;`off` = 不写 | — |
| `printUrl` | `true` | 同时把带票网址打到 DSH 日志 | — |
| `banFile` | `''` → 与状态文件同目录的 `remote-access-bans.txt` | 拉黑**暂存**文件:只有「写进排除的网段」失败时才用得上,平时不用碰 | — |
## 手机上的「新建工作区」选目录
DSH 默认用 `directory-picker-auto`:它判定「回环绑定 + 非 SSH + 有显示会话」时会挑**原生**后端,
而原生后端是在**宿主屏幕上**弹系统对话框 —— 手机那边点下去只会一直等(表现就是「打不开文件管理器」)。
本插件的 bundle patch 因此把选目录器固定成**应用内浏览**:关掉 auto,改挂
`dsh-host-directory-picker-browse`(列目录 / 建目录的后端)+ `dsh-client-ui-directory-picker-browse`
(「选择工作区目录」对话框)。手机上点新建工作区就会弹出这个应用内对话框,桌面上也一样。
- 桌面端代价:系统原生对话框换成应用内对话框(功能等价)。
- 安全说明:已授权的远程端因此可以**列目录、建目录**。它本来就能通过 DSH 跑命令,所以这不是新的权限等级;
不想让远程端看到文件系统,就把 `cordis.patch.yml` 里 `disabled` 那段和两行 `-browse` 删掉
(代价:手机端不再有选目录能力)。
### 为什么只看到「主目录」,怎么去别的盘
对话框的面包屑**在主目录以内会折叠**:起点显示成「主目录」,`C:\`、`Users` 这些上层被有意隐掉,
所以「找不到上一级」是设计而不是坏了。跳出去很容易:
点面包屑**右端的铅笔按钮(「编辑路径」)**,它会变成输入框并预填当前路径,改成你要的路径回车即可:
| 输入 | 效果 |
|---|---|
| `C:\` | C 盘顶层,所有顶层文件夹都出来了 |
| `D:\projects` | 直接跳到目标目录 |
| `\\server\share` | UNC 路径 |
宿主只接受**完全限定**路径(Windows 上必须是 `C:\…` 或完整的 `\\server\share…`;`\foo`、相对路径会被拒绝)。
跳到主目录**以外**之后,面包屑就会显示真实完整路径。目标文件夹不存在时,先用对话框里的「新建文件夹」,
或直接输入一个已存在的目录再点「打开」。
## 手机布局(远程UI布局)
DSH 的界面是为桌面三栏设计的。在 390px 宽的手机上,它的表现是:
- 侧栏一旦展开就吃掉 280px,正文只剩 **110px**;
- 设置页的左右两栏里,右侧只剩 101px —— 中文会**一个字一行**竖排下来;
- 输入区工具条把模型名截成 `DeepSeek-V4…`。
「远程UI布局」就是针对这些做的,选 `手机` 或 `自动` 后,**只有远程页面**会变成:
| 位置 | 手机布局 |
|---|---|
| 侧栏 | 收起时保持 56px 图标栏;展开时改成**抽屉**浮在正文上(`min(86vw, 320px)`),正文保持满宽,点遮罩即收起 |
| 设置页 | 改成上下两段:导航变成顶部**横向滚动**的胶囊条,内容占满整宽并自行滚动 |
| 输入区 | 工具条允许换行、左右内边距收窄,模型名不再被截断 |
| 触控 | 设置项在手机上单列排布、输入框 16px(iOS 聚焦不放大整页)、按钮 ≥40px |
| 安全区 | 刘海/圆角/手势条按 `env(safe-area-inset-*)` 留白;横屏高度紧张时压薄设置页导航 |
三条实现上的约定:
1. **本机不动**。判据是 `ctx.remote.$host.isLoopback`(页面生命周期内固定),**不是窗口宽度** ——
本机窗口拖到 390px 也不会变成手机布局。所有样式都挂在 `html[data-ra-layout="phone"]` 之下,
本机页面拿不到这个属性,也就永远匹配不上任何一条规则。
2. **不改别人的渲染树**。只加 `data-ra-*` 标记和一张 `<style>`,靠 CSS 覆盖;
唯一挂在框架之外的是抽屉遮罩(`<body>` 下、有固定 id)。定位用的是 `data-slot` 这类稳定契约,
再用 `[class*="_xxx"]` 兜住 DSH 会变的 CSS-module 哈希前缀。
3. **改完立刻生效**。这一项走 Remote 通道热更,手机上改完不用刷新(`settings/document-updated` 会重算)。
已知限制:手机宽度下 DSH 本身不给右侧栏分配轨道(`available < 300` 时右栏宽度算成 0),
所以**文件/终端那类右侧面板在手机上仍然打不开** —— 这属于 DSH 布局自身的策略,
本插件只改样式、不动别人的布局计算,因此没有去绕它。
## 动态壁纸为什么常常是静态的
装了「壁纸引擎」这类插件时,手机端经常只看到一张静态首帧。根因在**壁纸插件**那边,不在代理:
它把动态载荷(`scene.pkg`、网页壁纸入口)的地址**写死**成自己另起的那个媒体服务
`http://127.0.0.1:<随机端口>`(只绑回环)。手机打开这个绝对地址,访问的是**手机自己的** 127.0.0.1,
必然连不上 → 渲染页等不到载荷 → 首帧预算(15s 起、封顶 90s)到期 → 判定实时渲染失败,
并把这次失败写进**宿主共享**的设置文件,于是手机和桌面**一起**退回静态帧,直到手动重开「场景实时渲染」。
静态预览图走的是相对路径、经代理可达,所以「有图但不动」正是这个组合的表现。
本插件这边确实有一个**放大器**,已修:默认 `timeoutMs=0` 时没有显式声明超时,
于是继承了 Node ≥19 默认 agent 的 **5 秒空闲超时** —— 上游 5 秒没吐字节就把正在传输的响应掐断,
而且日志里打印的 `cfg.timeoutMs` 还是 0,看起来像「没设超时却超时了」。现在:总是显式声明超时,
且 `timeoutMs` 只作用于「等响应头」,头一到就交棒;上游半路断掉时下游会被一起收掉,不会让手机一直挂着。
彻底治好动态壁纸需要壁纸插件改用相对路径(或让它感知反代)。在它能改之前,两个可用做法:
把壁纸插件的「适配」目标设成浏览器(回到相对路径),或者接受手机端只有静态壁纸。
## 安全边界
- 只监听 `allowCidrs` 里的本机地址,**永远不会绑 `0.0.0.0`**。
- 每个连进来的对端地址也必须在 `allowCidrs` 内,否则 `403`;被拉黑(在 `denyCidrs` / 拉黑暂存里)的一律 `403`。
- 没设 `accessCode` 时:DSH 自身的令牌 / cookie 仍然生效(没票 `401`),本插件不降低 DSH 的任何鉴权。
- **设了 `accessCode` 就别再把它当「双因子」**:那时「网段 + 数字密码」就是全部凭据 —— 解锁成功后由本插件
在**服务端**替请求补上 DSH 的令牌(浏览器始终看不到 token),token 在这条路径上不再是第二道门。
想回到「必须带票」的严格模式,把 `accessCode` 清空即可。
- 一次密码错误立即拉黑该地址(写进「排除的网段」`denyCidrs`;万一写盘失败,它仍会被立刻拦住、
暂存到 `banFile` 并在状态文件里写明),这是**故意的**:可暴力猜的数字密码需要一道硬刹车。
- 建议再在 Tailscale ACL 里限定设备;`tailscale funnel`(公网入口)不要开。
- 本插件把来自网段的请求改写 `Host`/`Origin` 后转给回环上的 DSH,因此**它就是这个边界的守门人**:
`allowCidrs` 写宽了,等于把本机命令执行权限放开。
## 常见问题
**显示 `dsh web authentication required`(DSH 自己那句英文)** — 说明这次访问用的是**已经作废的票**:
每次启动 DSH 都会换一张 token,别的账户/上一次运行留下的那条 `?token=…` 网址都不能再用了。
换个做法:设了 `accessCode` 就直接打开**裸地址** `http://<地址>:<端口>/` 输一次密码;
没设就复制状态文件里**当前**那条带票网址。
如果你**之前明明能进**、现在只看到这句:那是浏览器里那张 DSH cookie 失效了。
从 b0ec52a 起,插件遇到这种 401 会**自动带当前票重试一次**,DSH 顺手发一张新 cookie,你不用再手动清。
**从当前版本起,这句英文不会再出现在「页面导航」上了。** 地址栏打不开时看到的是插件自己的中文页,
它按情况说清两种可能:**票作废了**(每次启动 DSH 都会换一张)还是**根本没带票/没带密码**,
并告诉你下一步去哪儿拿当前那条网址(本机「设置 → 远程访问」或状态文件)。
两个刻意的设计:页面**绝不回显当前 token**(它发给的正是尚未通过校验的对端),
而且**只接管导航请求** —— RPC/子资源的 `401` 仍原样透传,机器可读的错误语义不变。
设了 `accessCode` 但凭据过期时,回的是解锁页(重新输一次密码即可),也不是那句英文。
**输错密码被挡在外面** — 该 IP 已被拉黑。到「设置 → 远程访问 → 排除的网段」里删掉对应那一项
(形如 `100.x.y.z/32`)即可。如果状态文件里「待补写」不为空,说明当时写盘失败、它还没进配置,
那就删掉 `<DSH_HOME>\remote-access-bans.txt` 里的那一行。
**前几天还好,现在又要重新验证** — 换过端口或重启过 DSH。DSH 的 cookie 绑定 `hostname:port`,所以旧的不通用;
按「使用」里的步骤重来一次。
**设置页点了保存没反应 / 报失败** — 跑 `node tools/check-config-schema.mjs`,它会检查写盘的两个硬前提
(`Config` 是原生 Schemastery schema 且字段都标了 `.volatile()`;`package.json` 把
`@deepseek-ai/schemastery` 声明成了 peerDependency)。改完 `package.json` 要重启一次 DSH 才生效。
**手机上在「通用设置」里改完,刷新就变回去了** — 那几项(外观 / 字号大小 / 工作步骤展示 / 显示代码工作视图 /
性能与用量)在非回环页面上是「本页有效、刷新即还原」的。要让它留住,就用本插件的
「设置 → 远程访问 → **UI 设置**」改:这份值存在宿主配置里,每次打开远程页面重新套一遍
(默认「出厂值」=不干预官方默认,行为与官方一致)。
**升级后保存「UI 设置」报 `Config field "…" is not volatile`** — 宿主还不认识这个新字段
(宿主的配置 schema 在 DSH 启动时读入,改 `index.js` 不会热生效)。**重启一次 DSH** 即可,
之后改值不必再重启。
**手机在选目录里只能看到主目录** — 见上面「为什么只看到主目录」。
**手机上界面挤成一条、中文一个字一行** — 那是「远程UI布局」还留在 `电脑`(或 `auto` 没判定成手机)。
改成 `手机` 或 `自动` 即可。注意这一项是**存服务端配置**的:在手机上改完立刻生效,不用重装也不用刷新。
**视频/大文件/动态壁纸传到一半卡住** — 老版本会在「上游 5 秒没吐字节」时掐断响应
(Node ≥19 的默认 agent 自带 5 秒空闲超时,而 `timeoutMs=0` 恰好没覆盖它)。已修:
现在总是显式声明超时,且 `timeoutMs` 只管「等响应头」,头一到就交棒。
若壁纸仍然只有静态画面,那是**壁纸插件自己**把载荷地址写死成 `http://127.0.0.1:<端口>` 造成的
(手机打开这个地址等于访问手机自己的回环),见下面「动态壁纸为什么常常是静态的」。
## 开发与测试
```bash
node --check index.js # 语法
node tools/test-remote-access.mjs # Host 功能测试(起一个假上游,不需要 DSH)
node tools/test-client.mjs # 客户端冒烟测试(极简 React / 宿主桩子真跑一遍设置页,不需要浏览器)
node tools/check-config-schema.mjs # 用安装包里的真 Schemastery 校验 Config(找不到安装包则 SKIP)
```
`test-remote-access.mjs` 覆盖转发、`Host`/`Origin` 改写、`x-forwarded-for`、WebSocket `101` 透传、
网段外 `403`、解锁页与「错一次自动写进排除的网段」(含写盘失败退回暂存)、改密码不踢人、热重载换端口,
以及本轮补上的:访问密码规则(字母数字、非法值必须可见)、没有凭据时的中文 401 页
(且**绝不回显 token**、非导航请求仍原样透传)、`timeoutMs=0` 不再继承 agent 的 5 秒空闲超时、
`timeoutMs` 只作用于等响应头、上游半路断掉时下游不悬挂。
> 那条 5 秒超时的用例**故意要跑 5.3 秒**:只有真的等过 5 秒才能证明没继承那个默认值
> (Node 18 上它恒过,因为那个默认值不存在)。
`test-client.mjs` 覆盖设置页注册与渲染、回环与手机两条读写通道、保存与失败提示
(Remote 通道返回的是 `{ ok, value }` 信封,这是最容易踩的一处),以及手机布局最关键的那条不变式:
**回环页面即使窗口只有 390px、配置又写着 `phone`,也不设 `data-ra-layout`、不注入任何样式**;
远程页面则按 `phone`/`desktop`/`auto`(窄视口、触摸设备)分别给出正确判定;以及「UI 设置」那一组:
按配置调用官方五个入口、`default` 显式套回官方默认值、**本机页面一个入口都不碰**、官方组件缺席时安静跳过、
菜单默认收起且展开后是那五档。
`check-config-schema.mjs` 覆盖设置页写盘的两道硬前提,并顺带对表 `client.js` 的表单字段与 `Config`。
改手机布局时必须真机看一眼:这里用的调试回路是「无头浏览器 + CDP 仿真视口」
(`Emulation.setDeviceMetricsOverride` 到 390×844),因为 DSH 的布局计算读的是 `window.innerWidth`,
光把容器改窄看不到真实效果。
## 目录
| 路径 | 说明 |
|---|---|
| `index.js` | 插件 Host 半区(零依赖,只用 `node:` 内置模块):反向代理、访问门、401 说明页、热重载 |
| `client.js` | 浏览器半区:设置页参数表单(含「UI 设置」子菜单)+ 只作用于远程页面的手机布局引擎 + 把官方那五项界面偏好套到远程页面 |
| `cordis.patch.yml` | bundle 的 patch:插入插件行、固定选目录器 |
| `package.json` | 清单(`dsh.bundle.patch` / `exports` / `icon` / `meta`) |
| `locale/{zh,en}.json` | 插件页显示用的标题与说明 |
| `icon.svg` | 插件页图标 |
| `tools/` | 测试(Host / 客户端 / Config 校验)与从 DSH 安装包里取文件的工具 |
| `android/` | 安卓外壳 App 的源码(首屏地址页 + WebView;Java、零第三方依赖,可离线构建) |
| `install.ps1` | GitHub 一键取代码(放到本机固定目录,安装仍由 DSH 自己做) |
| `LICENSE` | MIT |
## 更新日志
### 1.2.0
- **改名**:包名 `dsh-remote-access-cidr` → **`dsh-plugin-remote-access`**,仓库同为
[`Mzy123l/dsh-plugin-remote-access`](https://github.com/Mzy123l/dsh-plugin-remote-access)。
插件行的 `id`(`remote-access`)与设置命名空间没变,但 bundle 是按包名装载的,
所以已装的旧来源**不会自动跟过来** —— 按上面「安装」里的迁移说明重装一次即可。
- **发布到 npm**:新名字下已发布 [dsh-plugin-remote-access](https://www.npmjs.com/package/dsh-plugin-remote-access)@1.2.0
(旧的 `dsh-remote-access-cidr` 停在 1.0.0,不再更新)。装起来最快的写法从此变成直接填包名。
- 文档:README / `install.ps1` / 代码注释里的旧名字与旧链接全部换成新仓库名,APK 链接指向当前 release。
### 1.1.1 – 1.1.2
- 安卓外壳:地址页左上角加返回(从网页点设置进来时回得去);设置 / 刷新两枚小图标挪到左侧图标栏下方;
APK 版本号统一为 1.1.1。
- 手机端手势:起手落在画布 / 可横滚容器 / 可拖拽 / 壁纸节点上时不接管横滑
(拖动吉祥物、横拖表格代码块不再滑出面板);抽屉开着时向右滑也能收起。
- 代理:给内容寻址的静态产物(`/plugins/??rev=…` 与 `/assets/`)钉一年 `immutable` 缓存,
治「每次进去都要重下十几 MB」。
### 1.1.0
- **「UI 设置」**(新):设置页里多一层可折叠的下一级菜单,把官方「通用设置」里的
**外观 / 字号大小 / 工作步骤展示 / 显示代码工作视图 / 性能与用量** 存进本插件配置
(`remoteTheme` / `remoteFontSize` / `remoteTranscriptView` / `remoteDeveloperTools` / `remotePerformanceUsage`)。
这几项在非回环页面上本来是「改完刷新即还原」,现在每次打开远程页面都会重新套用;
`default` = 出厂值(**显式套回官方默认值**),且**只作用于远程页面**、不写宿主设置文档。
- **安卓 App**(新):[`android/`](android/) 是 DSH 的安卓外壳(首屏填 `IP:端口`,断网也能改地址),
Release 里有可直接安装的 APK(应用名 **DSH**)。
- **`install.ps1`**:下载后校验包名与必需文件、覆盖前自动备份旧版本、`-Install` 直接交给 `dsh` CLI
(DSH 正在跑时自动跳过),还会认出 profile 里钉死的 `github:` 来源并提示怎么换。
- 手机端:抽屉真正不透明(垫底用壁纸引擎自己的「可读性底色」)、右栏不再压到正文上、边缘滑动手势。
- 修:上个版本引入的「下游断开后上游才响应」会把 DSH 宿主整个带崩(`ERR_STREAM_UNABLE_TO_PIPE`)。
- 外观下拉的选项顺序照官方来(浅色在深色之前)。
### 1.0.0
- 首次发布:按网段开放 DSH 网页界面;带票网址 / 数字密码两种进入方式;`远程UI布局`(手机 / 电脑 / 自动);
状态文件与自诊断;手机上的「新建工作区」用应用内选目录。
## 许可
[MIT](LICENSE) © 2026 Mzy123l
Install
dsh plugin --profile web add github:Mzy123l/dsh-plugin-remote-access
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-plugin-remote-access from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.