dsh-my-phone
Lets a phone reach the DeepSeek Harness desktop over a LAN address or a Cloudflare quick tunnel, with QR pairing, a live session feed, approval prompts and a floating desktop widget. Both entry points (LAN and the Cloudflare quick tunnel) start enabled by default; each phone pairs with its own token
Install
dsh plugin --profile web add dsh-my-phone
Sandbox test
This plugin has not been tested yet. Submit a test →
README
DSH My Phone
 
把家里电脑上的 DSH,装进口袋。 手机扫码即可继续对话,不用安装 App,也不注册账号。
## 🚨 开启前必读
打开这个入口,等于把这台电脑上的 DSH 账号密码贴在门框上。
任何拿到入口的人都能以你的身份读文件、跑命令、看全部对话。
局域网入口默认打开(装好就能用),用不到时请关掉;外网隧道默认关闭。
完整风险清单见 [SECURITY.md](./SECURITY.md)。
它解决什么
人在公司,DSH 在家里电脑上。想接着问两句,不用远程桌面、不用 VNC、不用装客户端——
手机上打开一个网页,看到的就是原生 DSH 界面。
手机浏览器 ──▶ DSH My Phone(配对凭据)──▶ 本机 DSH(官方会话)
- 手机端零安装:打开网页就是完整界面
- 支持局域网(同 Wi-Fi,不经第三方,最安全)
- 支持外网(Cloudflare 隧道,公司/外地可用)
- 一台设备一份凭据,可单独移除
- 更新记录:[CHANGELOG.md](./CHANGELOG.md)(一版一行)
快速开始
# 1. 装进当前 profile(已发布到 npm) dsh plugin add dsh-my-phone --profile desktop # 或者装本地源码: # dsh plugin add ./dsh-my-phone --profile desktop # 2. 重启 DSH,打开设置页 # http://127.0.0.1:8080/__myphone/ (只能在本机打开)
1. 打开设置页,点一下「生成二维码」(二维码不会自动生成,需要你点一下;屏幕上有码才说明它是「刚生成的」)
2. 手机扫码 → 手机上出现「信任这台电脑?」→ 点「信任这台电脑,进入 DSH」
3. 完成。之后手机直接打开那个地址就行
二维码里带的是一次性票据(3 分钟、用一次就作废),比手输 6 位码更难被猜到。
它不会自动刷新:3 分钟到了就失效,想连就再点一次「换一个二维码」。
手输已取消 —— 手机端只看得到「扫码」这一条路;电脑端也没有 6 位码了。
在 DSH 的任意界面右下角都有一个悬浮图标(图标 + 一行「连接 DSH」字),点开就是同一个二维码,不用去翻设置页。
连上以后那行字会自动消失;鼠标放上去会轻轻弹一下,按住可以拖着贴到任意一条边。
外网(在公司用家里电脑)
1. 第一次开启时插件会自己在后台下载 cloudflared(约 53MB,有进度,只下一次)
2. 等 10~30 秒出现 https://xxx.trycloudflare.com
3. 手机打开这个地址(或直接扫二维码),点一次「信任这台电脑」
⚠️ 快速隧道的地址每次重启都会变。变了你需要重新打开电脑上的设置页看一眼新地址。
想要固定地址,需要自己的域名 + Cloudflare 命名隧道(v0.2 支持)。
界面
手机上只有三个页签:消息/日志 / 连接 / 高级,电脑上点右下角悬浮图标弹出同一套面板。
「连接」页 · 「高级」页
 · 
局域网 / 外出两张码可切换,默认给局域网那张;只开一个入口就只出一张码 · 入口开关、已配对设备(可备注 / 断开 / 删除)、音效设置
气泡提醒(授权 / 超时)

会话需要你批准、或者一轮跑太久没结束,悬浮球旁边会自己弹一条气泡,同时响一声;点球打开「消息/日志」看详情。
同类事件短时间内不会重复弹(去抖),不会一直吵。
配置
状态存在 ~/.dsh/.dsh-my-phone/state.json:
字段 · 默认 · 说明
lanEnabled · true · 局域网入口(装好即可用;不用时请关掉)
lanPort · 8080 · 管理页端口(仅 127.0.0.1;被占用时自动往后试 12 个端口)
publicPort · 8081 · 公网入口端口(仅在开启时监听,同样自动避让)
tunnelEnabled · true · 外网隧道(手机不在同一 WiFi 时才有用;不用请关掉)
cloudflaredPath · '' · cloudflared 可执行文件路径
maxDevices · 8 · 最多可配对设备数
autoOffMinutes · 30 · 连续多久没有访问就自动收回入口,0 = 不自动关
这个文件含本机签名密钥,等同于密码,不要外传。
安全设计(三条原则)
1. 不碰密钥文件。插件不解析 .credentials.yaml,而是调用宿主官方
ctx.connection.authenticatedUrl() 换一枚官方会话。宿主改版也炸不了。
2. 管理面与访问面物理分离。管理页只绑 127.0.0.1,公网监听器收到该路径直接 404。
3. 凭据可吊销。一台设备一枚 HMAC 令牌,删掉设备立刻失效,不需要换全局口令。
详见 [SECURITY.md](./SECURITY.md),含逐环节攻击面与 12 项实测结果。
已知限制(v1.5.0)
- ❌ 隧道断线不自动重连
- ❌ 快速隧道地址不固定
- ❌ 手机端不能自动锁横屏(浏览器限制:只有全屏 + 系统允许时才让锁方向,iOS 基本不行)——提供「全屏横屏」按钮试一次,失败就手动转手机
- ✅ 右下角悬浮按钮(
shell.overlay插槽)做成手机 App 图标的样子:60px 白圆角方块(里面一条鲸尾)+ 下面一行胶囊字「连接 DSH」 - 连上(有设备被信任)以后那行字自动消失,只剩图标;没连上就一直挂着,随时提醒你它还开着
- 鼠标悬停 0.3 秒 / 长按 0.42 秒 → 一个只展示不带按钮的气泡,几秒后自己消失;点一下 = 打开弹窗(电脑和手机上完全一致)
- 悬停/按下的形变更大也更 Q 弹:
cubic-bezier(.34,1.56,.64,1)回弹,hover 抬起 7px + 放大 1.14 + 微转,press 压到 0.86 再弹回;系统开了「减少动态效果」就自动关掉 - 可以拖着走,松手贴最近的一条边(上/下/左/右都行);位置记成「贴哪条边 + 该方向上的偏移」(不是绝对坐标),
所以窗口缩放、父容器有 transform 都不会算歪;弹窗右下角有一行纯文字的「回到图标默认位置」(无背景无边框,悬停变蓝),点完顺手把弹窗关掉
z-index: 2147483000(弹窗 2147483200):别的插件的浮球/浮层压不住它- 默认位置=贴右边、离底边四个图标的高度(≈400px),不压右下角那一排常用按钮
- 图标用插件自己的
icon.png(透明鲸尾原图,/__myphone/icon.png同源直出;取不到才退回内联鲸尾) - ✅ 点开只有两页:「连接」=两排说明(「相机或者浏览器扫码连接」(加粗)/「扫码连接 DeepSeek Harness 桌面版」(不加粗))+ 居中的大按钮(生成/换一个二维码)+ 链接行(复制做小了跟在链接后面)+ 右下角一行纯文字「回到图标默认位置」;
「高级」=局域网/外网分组(设置项做小)+ 自动关闭入口(分段控件 关/15/30/60/120)+ 已配对设备 已连/上限(可滚动、可全部撤销、可备注)+ 排查(清空 / 复制诊断)
- ✅ 外网通道断了会自己重连:cloudflared 被网络掐掉、被 Cloudflare 回收、自己崩溃,后台每 10 秒看一次,断了就退避重连(5s→10s→20s→40s→60s),「排查日志」里能看到「断了 / 正在第 N 次重连 / 已重新连上」;连上有新域名时会告诉你手机要重扫。
- ✅ 设备上限是硬限制:连满 8 台后再扫会直接说「请先在电脑上删掉一台」,不会默默把最早那台踢下线;删除设备会先撤销信任再移除,并且要你在弹窗里确认一次。
- ✅ 二维码不会自己换新的:只有两种情况会自动生成 —— ①从来还没生成过;②你打开弹窗时那张已经失效了。其余情况它就一直显示手上这张(3 分钟到点变灰盖「已失效」戳),想换必须手点「换一个二维码」;弹窗开着放到过期也不会偷偷刷新。电脑端网页更严格:一律手动点「生成二维码」
- ✅ 设备能一直区分:手机浏览器都自称「Android 设备」,所以每台可以起个备注(最长 24 字,例:嘉然今天吃什么),列表里显示成「嘉然今天吃什么(Android 设备)」
- ✅ 重启 / 地址变了会主动说:DSH 重启过、或入口地址(每次重开外网都会变)跟上次不一样时,
下次你一在界面里动鼠标/按键,球上会弹一次引导气泡「DSH 刚重启过 / 入口地址变了 · 手机重新扫一次即可,不用重新信任」。一次重启只弹一次。
- ✅ Esc 关弹窗
- ✅ 手机打开是另一副面孔:同一份前端代码先试管理口,够不着(手机上必然够不着,管理口只绑 127.0.0.1)就切成手机形态 ——
「连接」显示已连接/这台设备/入口地址/复制地址,「高级」是只读的这台设备 + 排查事件;
手机端不给开关、不给换码、不给撤销别的设备(这些必须在电脑上做)。数据来自只读口 GET /__myphone/state(要设备凭据)
- ✅ 手机屏幕窄时有两个放大招:「放大成桌面宽度」(viewport 撑到 1280,可左右滑)、「全屏横屏」(尽力锁方向)
- ✅ 入口没有密码、也没有手输配对码的页面:未持有效凭据的浏览器只会看到一页「用手机扫电脑上的码」
- ✅ 二维码带一次性票据(144 bit、3 分钟、用一次即废),扫完整屏只有一个「信任这台电脑」按钮
- ✅ 手机必须主动点一次「信任」才建立设备;设备列表能单独撤销信任(撤销立刻断开)
- ✅ 两个入口默认都开(
lanEnabled/tunnelEnabled都是 true,装好就能用),端口被占用时自动往后试 12 个 - ✅ 宿主 Web UI 里有原生设置卡片(
settings.plugin.item插槽):开关、地址、二维码、跳转 - ✅ 自动关闭按通道分开算:局域网闲了只关局域网,外网闲了只关外网,不会因为你在家用 Wi-Fi 就把出门要用的隧道一起关掉(默认都是关 0)
(v0.4.3 起改了这个默认值——以前默认 30 分钟,用户过一会儿回来看到两个开关全关,还以为是插件坏了)
- ✅ 官方会话路径已在真实 DSH 进程内验证通过(见
SECURITY.md第 3 节) - ✅ 实时通道已验证:DSH 不用 WebSocket(客户端 bundle 里 0 处引用、服务端不响应 101),走普通 HTTP 流式响应;代理按 chunked 原样透传,首块 7ms 到达(见
SECURITY.md3.5 节) - ✅ 公网全流程实测:cloudflared 快速隧道 → 配对 → 带凭据访问到真正的 DSH,连跑 3 轮 18/18(见
SECURITY.md3.7 节) - ✅ 外网只需点一下「下载 cloudflared」:自动走镜像 + 断点续传(52.8MB 约 30 秒),免注册、免安装
连不上怎么查(v0.4 起,不用猜)
按这个顺序看,基本三步内能定位:
1. 悬浮窗 → 高级 → 排查日志 → 展开:这里直接列出手最近 8 条动静(配对成功/被拒的原因、连不上本机 DSH、宿主报错),
点「复制诊断」可把端口、开关状态、设备表、事件一起拷出来;旁边「清空」把这份列表和 pair.log / error.log 一起清掉
(两者各先存一份 .1 备份,清完又出问题还能捞上一次的堆栈)。
手机上也有同一份只读的排查(GET /__myphone/state),看得到事件但改不了任何开关。
2. ~/.dsh/.dsh-my-phone/pair.log:每一次配对尝试都有一行(时间、方式、失败原因、来源、UA、剩余次数)。
3. ~/.dsh/.dsh-my-phone/error.log:出 500 时这里躺着一整条堆栈 + 当时的请求行与 UA。
500 页面本身也会把异常第一行显示出来(例如 内部错误:Invalid URL)。
已修的一个典型例子:手机浏览器 POST 表单会发 Origin: null(不透明来源),旧代码对它做
new URL() 直接抛 TypeError: Invalid URL,于是配对一路 500、谁都连不上。
现在解析不出 host 时只记一条日志、不拦(这一步只下发本机凭据,跳过没有 CSRF 收益)。
界面是怎么长出来的(v1.5.
…(truncated)
Rendered from the README published on npm. Formatted only, not edited.
Versions
Published on npm::5,最后更新 2026-11-04