Skip to content

dsh-plugin-phone-access

Verified

dsh-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 界面有三道墙,缺一不可:

  1. 只绑 127.0.0.1,--host 0.0.0.0 在启动时被明确拒绝;
  2. /api 和 WebSocket 有 Host/Origin 栅栏:Host 必须是回环或在 trustedHosts 里,Origin 还必须等于 Host,否则 403;
  3. 认证是"每次启动随机生成 token → 换签名 cookie",cookie 绑定在 host:port 上,30 天。桌面版把那条带 token 的 URL 自己吃掉用来开窗口了,所以你手上没有 token。

这个插件用一次性配对码绕开第 3 点,用改写头绕开第 2 点,用不改绑定绕开第 1 点。

前置条件:先有一个私有网络

这个插件不提供网络,它只负责"在已有的私有网络上开一个能配对的入口"。 默认只认 Tailscale。

电脑和手机都要装、都要登录同一个账号:

  1. 电脑装 Tailscale(官网 https://tailscale.com/download),登录;
  2. 手机装 Tailscale。没有 Google Play 也能装:官方直发 APK,列表在 https://pkgs.tailscale.com/stable/#android(文件名形如 tailscale-android-universal-<版本>.apk,旁边有 .sha256 可以校验),要求 Android 8+;
  3. 手机上打开 Tailscale 登录同一个账号,把开关打开;
  4. 两边都登录后,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,然后:

  1. 点右下角的蓝色小圆钮(手机图标)。
  2. 看状态:
    • 显示"Tailscale 已安装但没连上" → 去把 Tailscale 开关打开,不用重启 Harness,30 秒内自动生效;
    • 显示"没检测到 Tailscale" → 电脑上还没装或没登录;
    • 列出地址 → 继续下一步。
  3. 手机连上同一个 Tailscale 账号,打开面板里那条 http://100.x.y.z:19387(扫码或手输;建议存书签)。
  4. 输入面板上的 6 位配对码。
  5. 进界面后,把这台手机固定用同一个入口(书签或主屏图标),之后 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。谁在私有网络里,等于谁能操作这台电脑。

注意点(按踩坑频率排序)

  1. 电脑不能睡。插电时设成"从不睡眠":powercfg /change standby-timeout-ac 0;笔记本还要 powercfg /change standby-timeout-dc 0(电池模式)。电脑一睡,手机就连不上,且没有任何提示,只会转圈。
  2. Tailscale 被关掉是持久的。tailscale down 或界面里关掉开关之后,重启电脑也不会自己连回来。面板会显示"已安装但没连上"——去把开关打开即可,不用重启 Harness。
  3. 入口要固定。登录态是浏览器里的一枚 cookie,按"哪个浏览器/哪个 App"隔离。在 A 浏览器配对过,换 B 浏览器或在微信里打开,会重新要配对码(这是正常的,不是坏了)。
  4. cookie 从配对那天起 30 天,不是"30 天不用才过期"——天天用也会到期。到期时会自动跳回配对页,输个新码即可。
  5. 清浏览器数据 / 无痕模式 = 要重新配对。
  6. 手机锁屏久了隧道会断。给 Tailscale 关掉电池优化,或系统里开"始终开启的 VPN"。
  7. 能连上 = 能跑命令。这条私有网络里的任何设备都能操作这台电脑;别把地址和配对码发给别人。
  8. 端口改动要同步书签。默认 19387 与 dsh 一致;改了 port 之后手机上的地址也要改。
  9. 重启 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 就是源码。