dsh-plugin-phone-access
Verifieddsh-plugin-phone-access · v0.2.10 · MIT · Web UI
Reach this Harness from your phone over a private network: a loopback-preserving gateway with one-time pairing codes, plus a portrait layout for narrow screens.
Install
dsh plugin add dsh-plugin-phone-access Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Tags
Creators
Readme
手机访问 · dsh-plugin-phone-access
在外面用手机打开这台电脑上的 Harness,操作同一个 agent;竖屏下还有一套为手机改的布局。
它不碰 dsh 的任何配置:dsh 仍然只监听 127.0.0.1,插件在私有网络地址上另起一个网关,把请求的 Host / Origin 改写成回环地址再转发进去。卸载插件即恢复原样,没有残留。
为什么需要它
dsh 的 Web 界面有三道墙,缺一不可:
- 只绑
127.0.0.1,--host 0.0.0.0在启动时被明确拒绝; /api和 WebSocket 有 Host/Origin 栅栏:Host 必须是回环或在trustedHosts里,Origin 还必须等于 Host,否则 403;- 认证是"每次启动随机生成 token → 换签名 cookie",cookie 绑定在
host:port上,30 天。桌面版把那条带 token 的 URL 自己吃掉用来开窗口了,所以你手上没有 token。
这个插件用一次性配对码绕开第 3 点,用改写头绕开第 2 点,用不改绑定绕开第 1 点。
前置条件:先有一个私有网络
这个插件不提供网络,它只负责"在已有的私有网络上开一个能配对的入口"。 默认只认 Tailscale。
电脑和手机都要装、都要登录同一个账号:
- 电脑装 Tailscale(官网 https://tailscale.com/download),登录;
- 手机装 Tailscale。没有 Google Play 也能装:官方直发 APK,列表在 https://pkgs.tailscale.com/stable/#android(文件名形如
tailscale-android-universal-<版本>.apk,旁边有.sha256可以校验),要求 Android 8+; - 手机上打开 Tailscale 登录同一个账号,把开关打开;
- 两边都登录后,Tailscale 的管理台里应该能看到两台设备。
手机没 Play 商店时用邮箱登录链接最稳(登录页走浏览器,不依赖 Google 账号)。 想用别的通道(ZeroTier、WireGuard、Radmin VPN 这类覆盖网络,或同一个局域网)也行:把配置里的
scope改成private或all,插件按适配器类型自动识别,不会误绑普通局域网。
安装
dsh plugin --profile <profile> add dsh-plugin-phone-access
或在插件管理页里填包名 dsh-plugin-phone-access。装完重启 Harness,右下角会出现手机图标按钮。
本地开发时可以直接装目录:dsh plugin --profile <profile> add ./dsh-plugin-phone-access。
卸载:dsh plugin --profile <profile> remove dsh-plugin-phone-access。
首次使用(五步)
装完重启 Harness,然后:
- 点右下角的蓝色小圆钮(手机图标)。
- 看状态:
- 显示"Tailscale 已安装但没连上" → 去把 Tailscale 开关打开,不用重启 Harness,30 秒内自动生效;
- 显示"没检测到 Tailscale" → 电脑上还没装或没登录;
- 列出地址 → 继续下一步。
- 手机连上同一个 Tailscale 账号,打开面板里那条
http://100.x.y.z:19387(扫码或手输;建议存书签)。 - 输入面板上的 6 位配对码。
- 进界面后,把这台手机固定用同一个入口(书签或主屏图标),之后 30 天免登录。
配对码 15 分钟轮换、用一次即失效。拒绝时明确区分三种情况:
| 提示 | 含义 | 怎么办 |
|---|---|---|
| 配对码不对,请核对电脑面板上的 6 位数字 | 码真的不匹配 | 重新看面板 |
| 配对码已过期,面板已换新码 | 提交时已超过 15 分钟 | 用面板上的新码 |
| 尝试次数过多 | 8 次错误后锁 30 秒 | 点"换一个码"立刻解锁 |
竖屏布局
视口宽度 ≤ 760px 时自动生效,桌面窗口完全不受影响(所有规则都挂在 html[data-dsh-mobile] 下,只有媒体查询会设这个属性)。
| 原来 | 改成 |
|---|---|
| 三栏网格,展开侧栏把中栏挤到只剩几十像素 | 单栏网格;侧栏不再占据布局宽度 |
| 侧栏展开 = 264–420px 的列 | 视口内固定抽屉(min(82vw, 320px))+ 半透明遮罩 |
| 关闭侧栏要点左上角的控件 | 点遮罩空白处也能关(只做这一件事,不去猜哪些点击算导航) |
| 抽屉盖住在它上面打开的页面 | 已知取舍:点"设置"后它会盖住设置面板,先点遮罩收起抽屉 |
| 列宽拖拽手柄 | 隐藏(手机上无法可靠拖拽) |
| 无安全区适配 | viewport-fit=cover + env(safe-area-inset-*),刘海和底部横条不挡内容 |
| 软键盘可能盖住输入框 | interactive-widget=resizes-content,键盘弹出时压缩视口而不是覆盖 |
| 正文 14px 左右 | 16px(覆盖 --dsh-content-font-size) |
实现边界:只用 ctx.layout.toggleSidebar() 这个公开动作,和布局自己发布在根节点的 data-sidebar-collapsed / data-shell-overlay;类名用 [class*="_sidebarCol"] 这种前缀匹配——CSS Modules 的输出是 hash_局部名,构建哈希变了也照样命中。不读取、不重写任何别的插件的组件或样式表。
配置
改 ~/.dsh/profiles/desktop/cordis.patch.yml 里 phone-access 那个 config(或改本包 cordis.patch.yml 后重启):
| 字段 | 默认 | 含义 |
|---|---|---|
port |
0 |
手机侧端口;0 表示跟 dsh 用同一个端口(桌面版是 19387) |
scope |
tailnet |
tailnet = 只绑 Tailscale;private = Tailscale + 覆盖网络适配器;all = 所有非回环地址(含普通局域网) |
extraAddresses |
[] |
额外要绑的 IPv4 字面量 |
exclude |
[] |
明确不绑的地址 |
codeTtlSeconds |
900 |
配对码有效期(15 分钟) |
和代理软件共存(Clash / 其它 TUN 模式)
插件本身和代理没有交集:它只监听入站,转发目标是 127.0.0.1,一个出站外网请求都不发。
真正会打架的是 TUN 模式的代理和 Tailscale:TUN 会接管默认路由,把 Tailscale 的出站打洞 / DERP 中继流量一起抓走;代理不支持 UDP 时,Tailscale 会拿不到地址,网卡停在 169.254.x。
修法是给这些流量走 DIRECT(Clash 系的 rules: 里加,顺序要在最前面):
- IP-CIDR,100.64.0.0/10,DIRECT,no-resolve
- DOMAIN-SUFFIX,tailscale.com,DIRECT
- DOMAIN-SUFFIX,tailscale.io,DIRECT
- DOMAIN-SUFFIX,ts.net,DIRECT
注意:直连打洞用到的对端公网 IP 不是域名,域名规则覆盖不到,所以最坏情况是被迫走 DERP 中继(仍然可用,只是慢一点)。
安全模型
- 网关只绑私有网络地址,默认不含普通局域网。
- 配对码就是入口凭证:6 位、15 分钟、一次性、失败锁定。只显示在电脑的面板和
~/.dsh/dsh-phone-access.json(权限 600)。 - 配对码不从网络里读得到:
/phone-access/*只服务本机;网关直接拒绝这个命名空间,路由本身也拒绝经网关进来的请求。 /pair永远由网关自己处理,不看 cookie——否则一枚过期 cookie 会把配对请求转发给 dsh,变成一个无法诊断的 405。- 不放宽 dsh 自己的信任栅栏:
trustedHosts一个字都没改。网关是唯一的信任边界。 - 明文 HTTP:私有网络本身加密(Tailscale/WireGuard 端到端),所以这里不再套 TLS。
- 能连上就能执行命令:这个 Harness 里的 agent 可以在你电脑上跑 shell。谁在私有网络里,等于谁能操作这台电脑。
注意点(按踩坑频率排序)
- 电脑不能睡。插电时设成"从不睡眠":
powercfg /change standby-timeout-ac 0;笔记本还要powercfg /change standby-timeout-dc 0(电池模式)。电脑一睡,手机就连不上,且没有任何提示,只会转圈。 - Tailscale 被关掉是持久的。
tailscale down或界面里关掉开关之后,重启电脑也不会自己连回来。面板会显示"已安装但没连上"——去把开关打开即可,不用重启 Harness。 - 入口要固定。登录态是浏览器里的一枚 cookie,按"哪个浏览器/哪个 App"隔离。在 A 浏览器配对过,换 B 浏览器或在微信里打开,会重新要配对码(这是正常的,不是坏了)。
- cookie 从配对那天起 30 天,不是"30 天不用才过期"——天天用也会到期。到期时会自动跳回配对页,输个新码即可。
- 清浏览器数据 / 无痕模式 = 要重新配对。
- 手机锁屏久了隧道会断。给 Tailscale 关掉电池优化,或系统里开"始终开启的 VPN"。
- 能连上 = 能跑命令。这条私有网络里的任何设备都能操作这台电脑;别把地址和配对码发给别人。
- 端口改动要同步书签。默认 19387 与 dsh 一致;改了
port之后手机上的地址也要改。 - 重启 Harness 不会让已配对设备失效(签名密钥持久化在
~/.dsh/.credentials.yaml)。
已知限制
- 竖屏布局没有在真机上做视觉验证。逻辑和样式都在自检脚本里覆盖了,但"看起来对不对"必须你自己看一眼。
:has()需要 Chrome 105+(2022 年起)。不支持时会静默回落到原桌面布局,不会坏,只是白改。- 右侧面板(文件/终端)在竖屏下仍按 45% 视口宽度浮在中栏之上,会显得挤;需要大面积看文件建议横屏。
- 抽屉宽度固定,横向拖动调宽在手机上被禁用。
- 没有推送通知:需要盯着页面,或用社区插件的 Web Push。
- 面板文案没走 locale 服务,按浏览器语言中/英二选一。
- 每 30 秒重新采样网卡:Tailscale 掉线再回来、地址变了、换了网络,都会自动关掉旧监听、绑上新地址,不需要重启 Harness。
169.254.x(APIPA,网卡在但没地址)一律跳过。 - cookie 可能被手机侧清掉:真丢了就得重新配对(服务端密钥是持久的,不会自己失效)。
排查
| 现象 | 处理 |
|---|---|
| 面板显示"未检测到" | 没识别到 Tailscale 地址。确认 Tailscale 已登录;换别的通道要改 scope;或把地址写进 extraAddresses |
| 手机打开地址超时 | 手机没连上同一个私有网络;或 Windows 防火墙拦了 |
| 输码提示"码已过期" | 面板已换新码,重新输一次;有效期 15 分钟 |
| 输码提示"尝试次数过多" | 点面板上的"换一个码"立刻解锁 |
| 输码提示"码不对" | 核对面板上的 6 位数字;状态文件里的 lastAttempt 记着服务端收到的码和判定原因 |
| 输码提示"连接被拒绝(HTTP 405)" | 0.2.5 之前的版本:浏览器带着过期 cookie 时网关把 /pair 转发给了 dsh。更新后不再出现 |
| 竖屏布局没生效 | 确认视口 ≤760px;确认浏览器支持 :has();刷新页面 |
| 手机上换壁纸/进设置被挡住 | 抽屉盖住了设置面板:先点遮罩空白处收起抽屉,再操作 |
| 右下角没有小圆钮 | 客户端 bundle 没加载:看插件管理页里 phone-access 是否启用 |
卸载
插件管理页里移除 phone-access,刷新页面即可。它没有写任何持久配置,唯一留下的是 ~/.dsh/dsh-phone-access.json(可以删)。
开发 / 自检
verify.mjs 是 63 项的检查,打在正在运行的本地 dsh 上(默认探测 127.0.0.1:19387,可用 DSH_VERIFY_UPSTREAM_PORT 改):
node verify.mjs
覆盖:清单与 BOM、Host/Origin 改写是否真的让栅栏从 403 变 401、配对网关的路由与三种拒绝原因、过期 cookie 仍能到达 /pair、Host-only 路由与状态文件、WebSocket upgrade 不挂住、竖屏层的样式范围与点击行为、tailnet 状态判定、30 秒重采样定时器、客户端模块的注册形状。
包里没有构建步骤:index.js 和 client.js 就是源码。