jxboop/dsh-plugin-mobile-bridge

dsh-plugin-mobile-bridge

给 DeepSeek Harness 用的手机端桥接插件:手机发图、下发任务、实时看输出,支持局域网 / USB 共享 / Cloudflare 隧道外网访问。

Uncategorizedcloudflare-tunneldeepseek-harnessdsh

dsh-plugin-mobile-bridge

给 DeepSeek Harness 用的手机端桥接插件: 在手机上导入图片、下发任务、实时看输出,从同一个 WiFi、USB 共享、或者外网隧道都行。

在电脑上跑 DSH,在手机上用。不用装 App,打开浏览器就是控制台。


它能做什么

  • 手机直接发图(截图、拍的照片)给 DSH,不用再经过微信/QQ 中转;动图、HEIC/BMP 等格式、视频也能发(图片先在手机上压到长边 1600,大文件分片上传)
  • 「文件」键:任意文件都能传(PDF / 文档 / 压缩包 / 代码 / 音频…),不限制类型 —— 原样传到电脑,agent 拿到的是路径(模型看不了这些格式,但 agent 能用工具读它、解压、转码、跑脚本)。一次最多 3 个(宿主的上限;手机上会拦住第 4 个并说清原因,不会让它悄悄消失)
  • 电脑给的素材在手机上直接看、直接存(agent 交付的「交付物」也会列出来:图片给缩略图):对话里出现图片/视频路径(脚本截的图、image_generate 出的图、导出的视频…)会自动变成缩略图,点开全屏,长按就能"存储到照片";附件也走真 URL(data: 在 iOS 上长按没有"存储到照片")
  • 自定义背景(手机上自己换):点工具栏「背景」→ 选一张图片 / 动图(GIF)/ 视频 → 传到电脑、当场铺满整个手机界面,不用碰电脑也不用重载插件;「去掉背景」一键还原。4.7.0 起面板里还多一个自带的 🌀 赛博朋克:纯 CSS 画的(霓虹地平线 + 透视网格 + 扫描线),不用传图、不占流量、任何分辨率都清晰;4.8.0 起它默认就在动,4.8.1 把动态感做足(六层:光晕漂 / 网格快速流 / 斜落的霓虹雨 / 地平线呼吸 / 扫描线 / 偶发故障闪,而且压暗层让开、气泡更透 —— 隔着气泡就看得见在动),配色跟着换成霓虹青 + 品红,面板上还有 🐢 让它静下来 的刹车(会把所有层一起停;系统开了"减弱动态效果"也会自动静) —— 它和图片背景二选一(设一个会摘掉另一个)。整套界面是毛玻璃风格(对话气泡透明、面板半透明 + blur,冷色冰蓝调子),跟你挑的背景同一个调子
  • 新建任务、下发指令、取消运行,实时看思考过程和工具调用
  • 它跑着的时候能插话:运行中右边的按钮变成「插话」,说的话排进队列、这一轮结束就接着处理,不用干等它跑完才敢打字(桌面端的 steer = 打断当前回合,手机上默认不用它:宁可等一轮,也不要把你正在跑的事打断)
  • 发出去的话立刻看得见、还能撤回:排队中的消息先在对话里显示成一条虚线气泡(带「排队中」状态),点「撤回」真的把它从队列里拿掉(DSH updateQueue 的 remove);已经开始处理的会如实告诉你撤不回来,绝不假装成功
  • 第一次连上会讲一遍怎么用(点「开始使用」收起);顶栏有「说明」键随时能再看。这篇说明按设备记"看过没有"(不是按 IP)—— 所以换一台手机、同学连进来都会各讲一遍,不会被人"代表"掉
  • 按会话回看历史;每条消息气泡外侧带一个看得见的「复制」(点一下复制这条的正文 —— agent 的回复只复制正文、不带思考过程;长按气泡也能复制,但那个入口藏得深,没人会去猜)
  • 会话能置顶、能标常用(2.5.0 新增):点顶部那一行打开列表 —— 📌 置顶(钉到最上面,重启手机还在)、⭐ 常用(自己打星)、以及手机按"你用得多的"自动排的前几个;点行切换、点 📌/⭐ 只改标记。这三份数据只存在你这台手机里,不碰电脑上的会话数据
  • 顶端就写着当前模型、列表能删会话(2.6.0 新增):最上面一行是当前模型(读宿主的真值,电脑端换过这里也跟着变,点开就能换);会话列表默认不摆 agent 子会话和自检会话(想看在底部勾「显示全部会话」),每行有 🗑 删除 —— 要输 PIN 才删得掉,正在跑的会话得再确认一次,删掉的记录在电脑端可以取消归档找回来
  • 余额查询(配合余额插件的同一份凭据)
  • 地址自动排序:USB 共享 / 蓝牙 PAN / 个人热点 / 局域网,哪个能通就把哪个排前面
  • 外网可达:推荐 Tailscale Funnel(一条固定的 https://<机器名>.<tailnet>.ts.net/ 地址、开机自恢复、手机不用装任何 App);也附带 Cloudflare 快速隧道脚本做临时方案
  • 手机上直接生图(1.0 新增):「生图」键 → 写一句英文提示词 → 它不是直连生图后端,而是把这条指令交给当前会话里的 image_generate 工具去出图 —— 图因此落在会话里,能像别的素材一样在对话中看、点开、长按存相册。第一次点会把那句话先放进输入框让你过目,之后照发。⚠️ 这需要电脑上装了 image_generate 工具(单独的插件,不在本仓库里),否则 agent 只会回一句"没有这个工具 / 没有生图模型" —— 那不是手机桥坏了,见下面《手机上直接生图》一节。
  • 手机上看 / 改权限档位(1.0 新增):权限面板 + 状态角标。读不需要额外证明;收紧到只读也不拦(往安全方向走不设障);但放开档位必须重新输 PIN —— 免得"捡到一台已解锁手机的人"直接把整台机器放开。3.5.0 起,新建任务那张纸里也能先选一档(建好的那一刻就按它跑;选「跟随默认」=什么都不改、不额外要 PIN)
  • 任何 App 的「分享」都能进电脑(3.6.0 新增):用 iOS 快捷指令做一个"分享表"动作 POST 到桥的一条短链,之后 Safari / 相册 / 备忘录里点「分享」→ 选它,内容就进了电脑上最近那个会话。页面上一个键都没加;默认关 + 每次带 PIN(见下面《分享到桥》一节)
  • 分享键还能给「网页版」(4.2.0):一份单文件、自带排版的 .html 安装页(GET /api/install?format=html ✓),同学直接浏览器打开就能读 ✓、不需要你电脑在线 ✓;手机上不能直接分享文件时会如实说并退到下载 ✓
  • 📋 那张纸上还多了两样(3.8.0,同样没加新键):「最近放进去的」 —— 你放进电脑剪贴板的文字在手机本地留最近 20 条,点一条只放进输入框(不替你按发送);⚠️ 从电脑读回来的不进历史(那可能是密码)。🔤 取字 —— 截一张电脑屏幕交给电脑上的 agent 认字、存成一份可搜索的笔记(桥自己不认字)
  • ⏰ 定时开机(4.13.0 新增):在「📊 状态盘」上写一个时刻,到点在睡眠里把机器叫醒 ✓。⚠️ 两条路必须分开说:睡眠 (S3) 系统还活着 → 计划任务的「唤醒定时器」能叫醒它 ✓;完全关机 (S5) 软件做不到 ✗(那时系统没在运行,任何软件都不存在 ✓)—— 那只能去主板 BIOS 里设「RTC 定时开机」✓,而"BIOS 里设没设"Windows 查不到 ✗,所以插件不摆那个键 ✗。默认关,跟 powerControl 那个开关走(见下面《定时开机》一节)
  • 🔌 蓝牙能「断开某一台」(4.14.0 新增):设备列表里已连接的那几行右边多一个「断开」✓。⚠️ 两条实话:断没断以电脑读回来的连接状态为准 ✗(实测音频/遥控两种服务返回"成功"却没断 ✓,真正断掉耳机的是免提那一种 ✓);有的设备就是断不掉 ✗(本机实测 iPhone 试了 14 种服务都不掉)→ 那就照实说"没断成" ✓ 并给两条替代 ✓。不会取消配对 ✓(只断连接);"连接"仍然只能去系统设置里点 ✗(Windows 没有可靠接口,实测 rc=87 ✓)

版本变更记录见 CHANGELOG.md。


想分享给同学?照这四步走(要能从外网连,Tailscale 必装)

  1. 同学先得有 DSH(这个插件是扩展,不是独立程序;Node 22+),还要有 Git(装插件那条命令要用)。

  2. 一条命令装插件,然后重启 DSH:

    dsh plugin --profile web add github:jxboop/dsh-plugin-mobile-bridge
    

    重启后日志里会打印他自己那台电脑的手机网址:

    [mobile-bridge] listening on 0.0.0.0:3081, PIN 123456
    [mobile-bridge] phone url: http://192.168.1.20:3081/ab12cd34ef56ab78/  (WLAN)
    
  3. 🔴 Tailscale 必装(电脑上) —— 这是「从外网也能连」的前提,不装就只能和电脑在同一个 WiFi 下用:

    • 去 https://tailscale.com/download 装、登录一次;
    • 开外网入口:tailscale funnel --bg 3081(第一次按它给的链接开一次 HTTPS);
    • 把它打印的 https://<电脑名>.<tailnet>.ts.net 写进 ~/.dsh/mobile-bridge.json 的 "publicUrl",再重启 DSH;
    • 手机关掉 WiFi、只用流量打开日志里新出现的 https 地址 —— 能看到登录页才算“外网通了”。 完整命令与排错见下面《外网访问(要分享给朋友 → 这一步必做)》。
  4. 手机浏览器打开那条网址,输 PIN 就能用。桌面 DSH 左下角的徽标上也写着完整网址 + PIN。

三点务必说清楚:

  • PIN 和网址里的随机段是每台电脑各自生成的(~/.dsh/mobile-bridge.json)——别把自己的网址/PIN 发给别人,那等于把你电脑的后门给对方。
  • 同一个 WiFi(宿舍/家里)一般直接可用;校园网常常禁止客户端互访 —— 分享给朋友基本都要走外网,Tailscale 是必装前置 ✓(不装 = 只能同一 WiFi ✗)。
  • Tailscale 只是通道:它不碰你的对话内容、也不经过第三方服务器;手机端仍然只认那台电脑自己的 PIN ✓。

装成「App」放在手机桌面上(推荐)

不需要 App Store、不需要越狱、不用装任何第三方东西 —— 用手机浏览器自带的 「添加到主屏幕」,就是一个带图标、全屏、能离线打开的 App:

iPhone(Safari):打开手机桥的网址 → 底部「分享」按钮 → 添加到主屏幕 → 添加。 Android(Chrome):打开网址 → 右上角菜单 → 安装应用 / 添加到主屏幕。

装完从桌面图标点开:没有地址栏、没有浏览器按钮,启动有加载进度条,界面是毛玻璃那套; 后台断网也能打开外壳(页面走 network-first,永远拿最新的)。

图标是 tools/make-icons.mjs 生成的真 PNG(iOS 不认 SVG 图标),改了图标设计重跑一次即可:

node tools/make-icons.mjs

说明:原生 App 做不了(iOS 打包签名要 Mac + 开发者账号 + 上架审核), 而 PWA 在体验上已经等价:独立图标、全屏、启动图、离线外壳、无浏览器界面。

安装

前置条件:你得先有一个能正常运行的 DSH —— 这是个扩展插件,不是独立程序。需要 Node 22 以上(用的都是内置模块,没有构建步骤)。

1. 装插件

两个会直接卡住你的坑(同学那边实测踩到,都写在这里):

  1. 这条命令需要 Git。 pnpm 对 github: / git+https: 一律走 git 解析,不会自动改走 tarball;没装 Git 就是 ERR_PNPM_GIT_RESOLVE_FAILED。装一次 Git for Windows 再重跑;不想装 Git 就用下面的 tar.gz 地址。
  2. 必须是 1.8.2 及以上。 1.8.0 / 1.8.1 的包里缺 lib/addresses.js(files 漏列), 装上去插件一加载就报找不到模块 —— 症状是手机页面打不开、一直转圈。 判断办法:看 <profile>\node_modules\dsh-plugin-mobile-bridge\lib 里有没有 addresses.js。
# 从 GitHub 安装(`link:` 只接受本地路径,网址要用 git+https 或 github: 简写)
dsh plugin --profile web add git+https://github.com/jxboop/dsh-plugin-mobile-bridge.git

# 等价写法
dsh plugin --profile web add github:jxboop/dsh-plugin-mobile-bridge

# 没有 Git?用 codeload 的 tar.gz 地址(这条路不需要 git)
dsh plugin --profile web add "https://codeload.github.com/jxboop/dsh-plugin-mobile-bridge/tar.gz/refs/heads/main"

# 从本地目录(改代码时用)
dsh plugin --profile web add link:/path/to/dsh-plugin-mobile-bridge

tar.gz 地址不会自己更新到最新提交:它的版本串是"分支"而不是提交号,add 时 pnpm 会跳过已装的那一份。要更新得先 remove 再 add:

dsh plugin --profile web remove dsh-plugin-mobile-bridge
dsh plugin --profile web add "https://codeload.github.com/jxboop/dsh-plugin-mobile-bridge/tar.gz/refs/heads/main"

(用 github: 那条则可以直接重跑 add 更新 —— pnpm 会重新解析到最新提交。)

装完重启 DSH,日志里会出现:

[mobile-bridge] listening on 0.0.0.0:3081, PIN 123456
[mobile-bridge] phone url: http://192.168.1.20:3081/ab12cd34ef56ab78/  (WLAN)

更新到最新版

把上面那条安装命令原样再跑一遍就行,不需要先卸载:

dsh plugin --profile web add github:jxboop/dsh-plugin-mobile-bridge

然后重启 DSH。(dsh plugin 是 pnpm 的转发器;对分支形式的 git 依赖,add 会重新解析到最新提交。实测 0.2.2 → 0.3.0 可直接升级。)想确认装到了哪一版:看 <profile>/node_modules/dsh-plugin-mobile-bridge/package.json 的 version。

版本号从 0.9.4 起才是准的。0.7.0~0.9.3 那一段只在提交标题里写了版本,package.json 一直停在 0.6.7 —— 所以那几版看不出新旧,照"重跑一遍安装命令 + 重启"来判断即可。

版本怎么排(2026-10-09 起):加功能 = 进一位小数(1.3.17 之后的下一个功能是 1.4.0, 再下一个 1.5.0…);修 bug / 只改文案 = 补丁位(1.4.1、1.4.2…)。一次提交里两者都有时按功能算。 每个版本都带 package.json 版本、CHANGELOG.md 一段、git tag v<版本> 和 Release。

2. 首次启动会自动生成配置

~/.dsh/mobile-bridge.json(Windows:%USERPROFILE%\.dsh\mobile-bridge.json):

{
  "version": 1,
  "port": 3081,
  "pin": "123456",
  "pathSecret": "ab12cd34ef56ab78",
  "tokenTtlHours": 12,
  "bindTokenToIp": false,
  "elevationMinutes": 60
}

pin 和 pathSecret 都是自动生成的,别外传。 在 DSH 界面左下角的徽标上能直接看到完整网址和 PIN。

字段默认说明
port3081监听端口
pin随机 6 位登录密码
pathSecret随机 16 位十六进制所有路由都藏在这个随机段后面,没有它一律 404
tokenTtlHours12登录令牌有效期(上限 14 天)
bindTokenToIpfalse是否严格绑定令牌的来源 IP。默认 false:地址变化只记录告警(在 /api/bootstrap 的 foreignUses 里可见),不吊销登录。设 true 则换地址即吊销 —— 移动网络下不可用(运营商 NAT、IPv6 隐私扩展会不停换地址,手机会被反复踢下线),只适合固定网络
elevationMinutes60输一次 PIN 后,多久内可以"指挥 DSH 干活"
crashProbefalse排查用,平时别开。 设为 true 会把宿主的 uncaughtException / exit 事件写进 ~/.dsh/dsh-crash.log(含每 30 秒一条内存心跳)。它跑在宿主进程里、会注册全局处理器并持续写盘,所以默认关闭。
answerOnPhonefalse让手机回答 agent 的提问(ask_user_question)和工具审批。默认关:开着时"正在看这个会话的手机"会抢在桌面前面拿到提问,桌面要等 120 秒才轮到。没有手机在看这个会话时,无论开关如何都是桌面先拿到。
pcControl关手机上控制电脑(4.5.0 / 4.5.1):静音 / 音量 / 亮度 / 开-关蓝牙 / 看电量与联网 / 看蓝牙设备 / 断开某一台蓝牙设备(4.14.0)。默认关,改完立刻生效、不用重启。开着时"让电脑动手"的动作仍要 PIN 提权。⚠️ 亮度只能调笔记本内置屏(台式机 / 外接屏会如实说调不了);蓝牙设备列表走系统原生接口(约 0.8 秒、带连接状态);「连接某台设备」底层接口实测一律被拒(rc=87),所以列表里点一台就是打开系统蓝牙设置;「断开」以回读到的连接状态为准(详见下面《蓝牙:能「断开」,连不回来》一节)。
powerControl关手机上让这台机器动手:关机 / 重启 / 解锁,以及 4.13.0 起的「定时开机」(到点在睡眠里唤醒 ✓)。默认关,改完立刻生效、不用重启;每次动作仍要 PIN 提权。⚠️ 完全关机后开机软件做不到 ✗(详见下面《定时开机》一节)。

为什么过一会儿又让我输 PIN?

这是设计如此,不是掉线。手机上有两种状态:

  • 已登录(有 cookie):能看会话列表和聊天记录。令牌默认 12 小时有效,所以重启电脑、重启 DSH 都不会把你踢出去。
  • 已提权(最近输过一次 PIN):才能让这台电脑干活 —— 下发任务、取消、新建会话。

提权默认保持一个小时(elevationMinutes —— 2026-10-14 用户要求:从 15 分钟改成 60 分钟),过期后第一次动手会重新弹出 PIN 门;宿主重启后恢复的会话同样不带提权。觉得还不够就把它调大,上限 240 分钟:

{ "elevationMinutes": 240 }

如果你看到"需要重新输入 PIN"却找不到输入框,那是 0.2.0 及更早版本的 bug(页面只认 401、不认服务器用来表达提权的 403 + needPin,于是提示了却没有门)。0.2.1 起已修复:过期时页面会直接把 PIN 门打开。

3. 放行防火墙(Windows)

首次启动会弹 Windows 防火墙提示。**只勾"专用网络"**即可(手机和电脑在同一 WiFi 时够用)。若要走外网隧道,勾"公用网络"。


外网访问(要分享给朋友 → 这一步必做)

要在局域网之外(流量 / 别的 WiFi)也能连,Tailscale 是必装的前置 ✓(不装就只能同一 WiFi ✗)。装好之后三条路,按这个顺序试:手机也装 Tailscale(最稳)→ Tailscale Funnel(手机不用装东西)→ Cloudflare 快速隧道(临时)。

📤 手机上的「分享 → 复制全文 / 网页版」给的那份零基础安装文案,5.0.0 起就是按这一节写的 ✓(Tailscale 必装 + Funnel + publicUrl + 关 WiFi 验一次 ✓)—— 分享给朋友时直接发那份就行。

📌 手机之前能打开、突然打不开了:先看是不是本机局域网 IP 变了(校园 WiFi 漫游 / DHCP 续租很常见 ✓ —— 手机上的旧网址会连不上,桥这边一条请求都收不到 ✓)。扫桌面徽标(左下角)的二维码重新打开一次即可 ✓; 想以后不再遇到:手机「加到主屏幕」时用固定的外网地址(*.ts.net 那条)✓,局域网 IP 会随网络变 ✗。

🔎 桥的「外网隧道」自检现在会真的从公网入口去测(不再被本机 Tailscale MagicDNS 骗 ✓)。 如果自检/徽标说「外网入口不通」,在电脑上跑 tailscale funnel --bg 3081 重开一次即可 ✓ (WiFi 漫游 / 换 IP 之后它偶尔会卡在这 ✓ —— 5.0.3 起自检会直接把这个命令写给你 ✓)。

方案 A:手机也装 Tailscale(最稳,推荐先试)

电脑和手机都装 Tailscale、用同一个账号登录,然后手机浏览器打开:

http://<电脑的 tailnet IP>:3081/<pathSecret>/

电脑的 tailnet IP 怎么拿:托盘图标点开就能看到(形如 100.122.188.30),或者命令行 tailscale ip -4。 <pathSecret> 是网址密钥段 —— DSH 窗口的日志里、或桌面 DSH 左下角徽标上都有完整网址。

为什么它最稳:流量走 tailnet(设备间端到端加密),完全不经过 Funnel 的公网入口。 2026-10-08 实测过一次:Funnel 的公网入口有两个 IP,其中一个偶发回不到机器 (外部 6 个国家的节点复测,一半报 Broken pipe)—— 手机就会间歇性"网络不通", 而同一时刻 tailnet 直连是好的。

代价:手机要多装一个 App(iPhone 需要非中国区 Apple ID),并且 Tailscale 要在后台常驻。

方案 B:Tailscale Funnel(地址永不变、开机自恢复、手机不用装东西)

为什么推荐:Cloudflare 免费隧道的域名每次重启都变,旧的永久失效;Funnel 给的是一条 https://<机器名>.<tailnet>.ts.net/ 的固定公网地址 —— Tailscale 服务随开机自启、配置存在 daemon 状态里,重启电脑后什么都不用做,网址照旧。而且它是标准 HTTPS,手机上不用装任何客户端(这点对 iPhone 尤其重要:Tailscale 的 iOS app 没有上架中国区 App Store)。

# 1. 装 Tailscale 并登录:https://tailscale.com/download

# 2. 首次开 Funnel 需要去控制台同意一次(只此一次),命令会把链接打出来:
tailscale funnel 3081
#    Funnel is not enabled on your tailnet.
#    To enable, visit: https://login.tailscale.com/f/funnel?node=xxxxx

# 3. 同意之后再执行一次,就常驻后台了:
tailscale funnel --bg 3081
#    Available on the internet:
#    https://<机器名>.<tailnet>.ts.net/
#    |-- proxy http://127.0.0.1:3081

# 4. 手机访问(结尾必须带密钥段):
#    https://<机器名>.<tailnet>.ts.net/<pathSecret>/

不用时关掉:tailscale funnel --https=443 off

⚠️ 选了一条路就别同时跑 tools/tunnel-start.ps1 —— 它会把配置里的 publicUrl 改回那个每次都变的 cloudflared 域名。

安全上如实说:Funnel 和免费隧道一样是公网地址,保护来自随机密钥段 + PIN 这两层;区别只是域名固定、不会更容易被猜到。要"只有自己的设备能进",就用方案 A(设备之间端到端加密、不经公网入口)。

Funnel 不稳时怎么办:今天实测到的症状是"手机说网络不通",但电脑这边一切正常(tools/tunnel-status.ps1 也是 OK)。 先 tailscale down 再 tailscale up(不需要管理员)常常能救回来;实在不行重启电脑,或直接用方案 A。

方案 C:Cloudflare 免费快速隧道(临时用)

tools/ 下有一套脚本:不需要公网 IP、不需要改路由器、不需要注册账号。代价是域名每次重启都变,旧的永久失效 —— 只适合临时开一下。

# 1. 下载 cloudflared.exe 放到 tools/ 目录
#    https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-windows-amd64.exe

# 2. 启动隧道(会打印完整网址并复制到剪贴板)
powershell -NoProfile -ExecutionPolicy Bypass -File tools\tunnel-start.ps1

# 3. 不用时关掉
powershell -NoProfile -ExecutionPolicy Bypass -File tools\tunnel-stop.ps1

其它工具:

脚本用途
tools/tunnel-status.ps1体检:隧道现在是死是活(进程、端口、外网实测),手机上出现错误页时先跑它
tools/show-phone-url.ps1一个可复制的小窗口,显示外网/局域网网址 + PIN(带刷新按钮)
tools/revoke-phone.ps1应急:一键吊销所有手机登录(两步确认,防误触)

在手机上回答 agent 的提问

agent 卡在 ask_user_question 上等你选,或者某个工具调用需要你点头时,手机上会直接出现一张卡片:选项按钮、可多选、也能自己写一段话;审批则是「允许一次 / 拒绝」两个键。答完 agent 立刻继续,不用你跑回电脑前面。

默认关闭,打开方式:

{ "answerOnPhone": true }

打开后改配置需重启 DSH(或重载插件)生效。

它怎么决定谁来答:

情况谁作答
有手机正在看着这个会话手机(桌面等 120 秒才轮到,防手机没答导致卡死)
没有手机在看这个会话桌面,和我们没插手时一模一样
手机答了,别的手机也开着先答的算数,其余卡片自动撤掉
提问发生时手机不在场(在别的会话 / 页面被系统挂起 / 正在重连)桌面先答;手机上事后会出现一条能点的横幅,切过去还能补答(4.14.1)

4.14.1「回来还能看见、还能答」(用户 2026-10-16 报「我看不见你的问题手机端」✓):

  • 上面那张表里第四条就是当时那两次的情形 —— 卡片只在"提问发生那一刻有手机盯着这个会话"时才推给手机, 手机不在场就一个字都到不了 ✗(而桌面照常,这是设计 ✓,不是坏了 ✗);
  • 现在宿主会把超时之后仍然收得住的提问(userQuestions.continued)也告诉手机(GET /api/waiting): 手机显示「另一条会话在等你回答」✓,点进去直接看到那张卡 ✓ 并且答得出去 ✓(走宿主自己的 callId ✓, 不是桥上那个 120 秒就失效的临时 id ✗);页面从后台回到前台时也会主动补问一次 ✓;
  • ⚠️ 仍然没做:提问的那一瞬间手机不在场,卡片不会"立刻"出现在手机上 ✗ —— 它只能事后看到并补答。 要"立刻"就得让桥在没人在看时也把提问扣给手机,代价是桌面那条会晚 120 秒 ✗,所以没做。
  • "读不到"和"没有"是两件事 ✓:宿主没有那两个服务时,服务端回 available:false ✓、页面上写「读不到」✗ —— 绝不说成"没有提问" ✗。
  • 出了这种事不再只能靠猜 ✓:桥会把每一次「为什么没交给手机」记进日志 ✓ (例:ask(question) -> 桌面:现在没有手机在看 session-xxx(此刻有人在看:session-yyy) ✓), 以及手机开始看 / 不看哪个会话 ✓。

几个已经处理好的细节:

  • 答案要提权:和下发任务一样,光有 cookie 不够,得最近输过 PIN —— 否则偷走 cookie 的人能替你在批准框上点"允许"。
  • 同一个提问不能答两次:用过的 id 立刻失效。
  • 手机刷新不会丢卡片:新开的流会把还没答的提问补推一次。
  • 问题文本来自模型,一律当纯文本渲染(textContent,不是 innerHTML),模型塞不进脚本。
  • 贴到别的页签也看得见:卡片挂在主区域外面,切到「余额」不会漏掉提问。
  • 提问作废了就撤卡:超时/已在别处回答后,卡片自己消失(而不是点一次报一次错)。
  • 你正开着面板时也看得见(2.5.1):卡片一到会自动收起当前打开的工具面板(会话列表 / 录音 / 生图 / 背景 / 权限 / 删除纸),振动一下并提示「agent 要你点头」—— 面板只是收起,里面写好的内容不会丢;卡片的层级也不再低于任何全屏面板。 (这条是用户报「审批没弹出来」查出来的真因:卡片本来是流内元素,被那些 z-index:50 的全屏纸整个盖住, 而宿主只等 120 秒就退回桌面 —— 桌面上两分钟后才冒出来,看起来就像"手机上没提示"。)

已知限制:加载页和断线整屏(那两屏比卡片高)时不会显示卡片 —— 那属于"还没连上/断开了", 连上以后卡片自然回来。

断了会告诉你,不用你自己发现

手机和电脑之间的连接有三种坏法,每一种都会在页面顶部弹出一条说明,而不是像以前那样只把一个 9 像素的圆点变红:

情况页面会说什么
连接被切断(隧道没了 / 网络断了)「和电脑断开了」+ 可能的原因
连接还在、但对面不再说话(电脑睡眠最典型)「好像和电脑失去联系了」+ 最后收到消息是多久之前
提问过期、或已在别处回答卡片自己撤掉,并说明原因

而且它会给出两条能点的出路:

  1. 改用局域网地址 —— 同一个 WiFi 下,这条链路完全不依赖隧道。地址由宿主在 bootstrap 里给(含密钥段的完整 URL)。注意换地址等于换站点,cookie 带不过去,要重输一次 PIN。
  2. 重试连接 —— 不换地址,直接重连。

桌面那一侧也不再装死:插件每 120 秒从外网探一次自己的隧道,连续两次探不通就把徽标上的外网地址标红成「⚠️ 本机自检没连上公网(手机可能仍能连)」, 并往 ~/.dsh/mobile-bridge.log 写一行。恢复时也会写一行。 ⚠️ 4.16.1 起措辞改准了 ✓:探针是在这台电脑上跑的 ✓,本机 DNS / 网络抖一下就会失败 ✓, 不等于手机连不上 ✗(旧文案写的是「隧道已断,手机连不上」✗ —— 2026-10-17 就是这么把用户误导的 ✓)。 要逐项结论就跑 tools\tunnel-status.ps1 ✓:它会先认路线(外网地址是 *.ts.net 就按 Tailscale Funnel 查 ✓, 不再去要求 cloudflared ✗),最后以外网实测为准 ✓。

手机上是 Cloudflare 错误页?(Error 1033 / 1016)

这两个错都不是插件的问题,而是隧道没了。

错误含义
1033 Cloudflare Tunnel error这个主机名曾经是一条隧道,但现在没有任何隧道连着它
1016 Origin DNS error这个主机名已经完全解析不到了(隧道早就没了)

最常见的两个原因:

  1. 免费快速隧道的网址,每次 cloudflared 重启都会换一个。 旧网址永久失效,救不回来 —— 不是"过一会儿就好",是那个域名再也不属于你了。手机上收藏的旧网址永远打不开。
  2. 跑隧道的电脑睡眠 / 休眠 / 合盖了,或者换了网络、被安全软件或加速器掐掉了连接 —— 隧道随之中断。

怎么办:

# 先体检:一眼看出是进程没了、端口没起、还是网址失效
powershell -NoProfile -ExecutionPolicy Bypass -File tools\tunnel-status.ps1

# 网址失效就重拿一个(先停再起,确保拿到全新的)
powershell -NoProfile -ExecutionPolicy Bypass -File tools\tunnel-stop.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File tools\tunnel-start.ps1

tunnel-start.ps1 会真的从外网打开一次再告诉你成功了 —— 如果它说"没能通过外网验证",那就是现在把网址给手机也没用,直接照它列的原因排查,别浪费时间。

想要一个永不变的网址,只有两条路:Cloudflare 命名隧道(要有自己的域名 + 账号),或者用 Tailscale / ZeroTier 这类组网工具(推荐:地址固定、端到端加密、且完全不暴露到公网)。

⚠️ 免费快速隧道的网址每次重启都会变,而且是一条公开网址 —— 拿到的人都能看到 PIN 登录页。手机桥有 PIN 认证和限速兜着,但别把网址发到群里。


安全模型

这个桥可以经隧道暴露到公网,所以"安全"必须是能验证的,不是一句承诺。

已实施的防护

防护说明
随机路径前缀所有路由藏在 /<pathSecret>/ 后面。无密钥路径一律 404
限速的钥匙不可伪造只在 socket 对端是 loopback(即自家 cloudflared)时才读转发头;cf-connecting-ip 优先,x-forwarded-for 取最后一段(自家代理追加的),绝不取第一段(那是调用方可控的)
两级登录限速每地址 12 次/10 分钟 + 所有人合计 60 次/10 分钟
令牌绑定来源 IP偷走的 cookie 换一台机器就是废纸
写操作需要再验 PINcookie 只能读;下发指令/取消/新建会话需要一小时内输过一次 PIN
Cookie Secure 条件化HTTPS 下带 Secure,明文 HTTP 下不带(否则局域网登录会静默失效)
令牌 12 小时过期上限 14 天,/api/logout 立即吊销
SSE 连接上限MAX_LIVE_STREAMS = 6,超限 503
网络层超时与连接上限headersTimeout 20s(挡 slowloris)、requestTimeout 180s、maxConnections 64
统一安全响应头nosniff、X-Frame-Options: DENY、cache-control: no-store、Referrer-Policy: no-referrer(防止密钥路径经 Referer 泄漏)
不回显内部错误细节只进日志,不回给调用方
探测留痕但密钥不落盘无密钥请求记为 GET <no-secret> <- ip;日志里永远不出现密钥路径
登录审计成功/失败的每一次登录都落一行(~/.dsh/mobile-bridge.logins.jsonl,超 256KB 时成功的那几行优先留 —— 否则约 1300 次错 PIN 就能把真记录挤光、面板假报"没有陌生设备")。失败尝试不算"已知设备" —— 否则攻击者先试错一次就把自己洗白;"新地址第一次成功登录"会在桌面角标上报警,并写明"走外网时这是隧道那端的地址"
看审计要现输 PINGET /api/logins 走 x-dsh-pin 头(绝不进 URL):这份账里有这台电脑的网络史,和"看剪贴板 / 截图"同一级。current("这台手机")按令牌判,不按 IP(IP 会被 warn-only 路径改写)
踢设备要现输 PIN手机端「踢掉其它设备」= 收回权限,仍需证明是本人(和"删除会话"同级);默认保留当前这台。桌面那个 /revoke 照旧只认本机(loopback),密钥泄漏也踢不掉你
诊断包脱敏GET /api/diag 生成的文本里,PIN 与 pathSecret 一律替换成 <PIN> / <密钥段>,另有 Bearer … / sk-… 两条通用规则;读不到配置时在包里写明"没能确认脱敏";每份日志只截最近 48KB —— 它是要发出去的东西
自检的隧道那一条是"现探"state.tunnel.ok 是三态(null = 没探过):点一次 🩺 就现探一次,免费隧道换域名后会重新探 —— 绝不说"没测过"就是"可达"

怎么验证(请自己跑)

node test/security.mjs

51 项断言:限速钥匙的单元断言、密钥路径门禁、每个接口的未授权拒绝、限速生效、cookie 标志、令牌有效期、注销吊销、响应头、SSE 上限、附件不泄漏、日志脱敏。退出码非 0 即有失败。

其中只读的那半(密钥门禁、未授权拒绝、响应头、日志脱敏)打的是你正在用的那台桥;破坏性的那一半(连猜 15 次 PIN、真登录拿 cookie、打满 SSE 上限)会在临时目录里另起一台一次性实例,跑完即拆 —— 因为登录限速有一份全局额度,是和手机共用的,在真身上打就是拿手机当靶子。早期版本正是这么把用户锁在门外的。

要连真实 Cloudflare 边缘一起验(会消耗本机出口 IP 的登录额度,10 分钟后自愈):

$env:BRIDGE_TUNNEL_URL='https://<你的隧道域名>'
node test/security.mjs

其它测试。先装一次测试依赖(jsdom,只有 UI 测试用它;插件本身零运行时依赖):

npm install
文件内容需要
test/crashprobe.mjs崩溃探针开关:默认不碰宿主全局状态、开启后能在配置重写中存活(17 项)—
test/harness.mjs桥的端到端行为:PIN 门、cookie、prompt 组装、SSE 扇出、取消、附件、新建会话、余额缓存、重启保活(含"恢复的会话可读但不可写 = 403+needPin")、注销、桌面角标注入与 start() 不抛错、普通请求取对话 /api/transcript(巨型历史裁剪 / 一堆短字符串拼成的 stream 也要丢 / 缺 sessionId 400 / 客户端半路掐断后桥还活着)、子会话的错误帧带 unsupported 且从列表里消失,以及手机作答全链路(提问推到流上 / POST api/answer 收答案 / 防重放 / 审批 outcome 校验 / 取消不惊动桌面 / 刷新补推)(608 项)—
test/ui.mjs手机页面的 DOM 与交互:含"没有请求跑出密钥段"、"提权过期时必须把 PIN 门打开"、运行中同时给出「插话」与「停止」并确认插话已收下、换会话后「在跑」状态跟着重置、首次说明页(自动弹出 / 讲清四件事 / 点了就记住 / 真有提问时让位)、子会话不许谎报断线、也不再重连、迟到的旧会话回包不许贴到新会话上、localStorage 写不进去(无痕模式)也不许把页面搞死、Ctrl+Enter 连按两下只发一次、提问与审批卡片(渲染、作答编码、防 XSS、重推保草稿、单选与自定义互斥、作废撤卡、跨页签可见),以及断线提示条(弹条、说清原因、给局域网兜底、心跳到达后收起)、面板主题注入 / 撤除(841 项)jsdom
test/addresses.mjs地址排序与分类(16 项)—
test/shot.mjs真浏览器截图 + 布局/滚动体检(scroll: 那行会报能不能滚到顶底、滚动中位重取后位置还在不在、有没有块在估算高度)Windows + Edge
test/switch-bench.mjs真浏览器量"切会话":切到没打开过的会话多久有内容、切回来是不是那一刻就有、这次切换下载了多少字节Windows + Edge
test/reopen-bench.mjs真浏览器量"从别的窗口点回来":把网络延迟拉到 2 秒再重载,报"对话出现"与"界面显示"两个时刻Windows + Edge
test/scroll-live.mjs真浏览器复现"回弹":开一个临时会话让它真的流式输出一千多行,一边往上滚一边采样 15 秒(回弹次数 / scrollHeight 变动 / 最长掉帧)Windows + Edge
test/phone-diag.mjs真浏览器诊断:抓页面异常、控制台、网络状态码Windows + Edge
test/boot-probe.mjs加载页进度条探针:网络节流下每 250ms 采样一次,回答"百分比会不会冻住"(这类 bug 本机复现不了,只有慢网络才看得见)Windows + Edge
node test/package.mjs && node test/harness.mjs && node test/qr.mjs && node test/ui.mjs
node test/security.mjs && node test/crashprobe.mjs && node test/addresses.mjs && node test/offline.mjs
node test/platform.mjs

九个套件共 1795 项(package 186 · harness 614 · qr 12 · ui 852 · security 54 · crashprobe 17 · addresses 16 · offline 17 · platform 27)。另有 test/voice.mjs 33 条语音草稿 —— 语音那一版 按用户要求先跳过,文件留在仓库里、能单独跑,但不并进这项门槛(tools/check-counts.mjs 只管上面九个)。 改完必跑全套 ✓ —— 只跑某一个会漏掉内容规矩与界面回归(2.7.2 就是这么漏了两版的 ✓)。

桌宠:已下线(4.17.0)

手机桌宠(小鲸 / DeepSeek酱)在 4.15.0 上线、在 4.17.0 下线 ✓(用户 2026-10-19: 「手机桌宠不要了,电脑桌宠不用动」✓):

  • 界面上那只浮层、「更多」里的 🐳 键、拖动 / 缩放 / 跑动、发成功 / 失败换表情 —— 全删了 ✓;
  • 宿主那两条只读接口(GET /api/pet / GET /api/pet/asset)也一起删了 ✓;
  • ⚠️ 用户本机的素材没删 ✓:~/.dsh/mobile-bridge-pets/<名字>/ 里的立绘 / 设定图 / pet.json 还在他自己的电脑上 ✓(那是他自己的东西 ✓ —— 删功能不删素材 ✓);
  • ⚠️ 电脑网页端(Harness Web UI)那只没做、也没动 ✓(用户明确说了不用动 ✓);
  • 想回看这一段历史,见 CHANGELOG.md 的 4.15.0 / 4.15.1 / 4.16.1 / 4.17.0 几节 ✓。

蓝牙:能「断开」,连不回来(4.14.0 起)

用户 2026-10-16 原话:「还有蓝牙功能的断开设备连接,现在只有连接设备」✓ —— 这一版补上了「断开」那一半 ✓。

动作能不能做说明
看设备列表 + 真实连接状态✅系统原生接口(约 0.8 秒)
断开某一台(4.14.0)✅ 能(部分设备)见下面两条 ⚠️
连上某一台❌ 不能Windows 没有可靠接口(BluetoothSetServiceState(ENABLE) 实测一律 rc=87);面板里点一台 = 打开系统蓝牙设置 ✓
取消配对(忘记设备)❌ 故意不做那是 BluetoothRemoveDevice:要重配一遍,代价太大;本插件不动配对关系 ✓

⚠️ 两条必须知道的实话(都是 2026-10-16 在真机上验出来的 ✓):

  1. "断没断"以电脑读回来的连接状态为准 ✗ ✗ —— 不是"命令返回成功" ✓。 实测:耳机的 A2DP / AVRCP 两种服务关下去,系统返回成功(rc=0)却没断 ✗; 真正让它断开的是免提(HFP)那一种 ✓。所以插件是逐个服务关、每关一次回读状态 ✓,断了才说断了 ✓。
  2. 有的设备就是断不掉 ✗ —— 同一台机器上的 iPhone 试了 14 种服务都不掉 ✓ (其中 4 种还回 rc=0 ✗)。这种情况面板会照实说"没能断开" ✗,并给两条真能用的替代 ✓: 点「打开蓝牙设置」在电脑上手动关 ✓;或者「蓝牙关掉再打开」—— ⚠️ 后者会把所有连接一起断掉(耳机、手机热点都会断 ✓),代价写在面板上 ✓。

断开成功之后那一行会变成"未连接" ✓(那一步是重新读一次设备列表得到的 ✓ —— 不是页面自己改的 ✗);要连回来仍然只能去系统蓝牙设置里点一下 ✓。

定时开机(4.13.0 起):睡眠能醒,关机只能靠 BIOS

用户 2026-10-16 原话:「既然有关机那么可以做一个对应的定时开机功能」✓。

这个名字最容易让人误会,所以先把两句话放前面(手机面板上写的也是这两句):

机器当时的状态到点能不能自己开
睡眠 (S3)✅ 能 —— 系统还活着,计划任务带「唤醒定时器」(WakeToRun)就有权把它叫醒
休眠 (S4)✅ 能(这台机器上休眠没启用 —— 面板会照实说,这是另一件事)
完全关机 (S5)❌ 不能 —— 系统没在运行,任何软件都不存在

⇒ 完全关机之后到点开机,只能靠主板 BIOS/UEFI 的 RTC 定时开机(各家叫法不同:RTC Alarm Power On / Resume by RTC Alarm / Wake on RTC / Power On By RTC),设好之后不需要任何软件。 而"BIOS 里设没设",Windows 这边没有任何接口能读到 ✗ —— 面板会明说查不到,不给假安心; 任何人拿这个动作来问(接口是公开的),服务端也会如实拒绝并把"为什么做不到"说出来 ✓。 这也是插件不摆那个键的原因 ✗(摆一个按了没反应的键 = 骗人)。

怎么用(在「📊 状态盘」上,紧挨着「电脑端一键动作」):

  1. 配置里要有 "powerControl": true(见上表 —— 定时开机和"关机 / 重启"共用一个开关:都是"让这台机器自己动";默认关,改完立刻生效);
  2. 写一个时刻:07:30(今天已经过了就定明天)/ 10-17 07:30(今年过了就定明年)/ 2026-10-17 07:30(必须是将来,过去的一律拒绝)→ 点「设它」;
  3. 面板会当场读回来给你看:那个任务的时刻、有没有「唤醒」标志、以及这台机器的唤醒定时器策略 (交流 / 电池两档)。不是「启用」时,面板会把改成启用的那条命令摆出来(要管理员 —— 插件不偷偷提权 ✗);电池那档是「禁用」时,还会提醒你插着电才稳。

只响一次 ✓(同一时刻只留一个 ✓,再设一个就换掉它);「取消这个定时唤醒」删完会回读确认才说删掉了 ✓。 powercfg /waketimers("系统里还排着哪些唤醒定时器")要管理员才能读 —— 读不到时面板说读不到 ✗, 不当成"没有" ✗(这两件事完全不一样)。

分享到桥:任何 App → 分享 → 电脑会话(3.6.0 起)

在手机上看见一段话、一个链接、一张图,想直接丢给电脑上的会话 —— 不用切到桥的界面,**在原来的 App 里点「分享」**就行。

为什么做成"快捷指令 + 一条短链":手机上这个网页已经很大了(10.8k 行), 再加按钮就要跟"面板重渲染"那套脾气搏斗(3.5.0 为了一个「解锁」按钮折腾了五轮 ✗)。 短链那条路完全不碰页面 —— 更稳,也更快。

怎么开(默认关 ✓)

  1. 在这台电脑的 ~/.dsh/mobile-bridge.json 里加一行:

    { "shareIntake": true }
    

    (改完立刻生效,不用重启 ✓;不想要了删掉/改回 false 就立刻关 ✓。)

  2. 手机上打开桥 → 工具键那排的 🩺 一键自检 → 找到**「分享到桥」那一行 → 抄下它给出的短链** (那条链里带着这段密钥,别发给别人 ✓)。

  3. 在 iPhone 的快捷指令里新建一个快捷指令:

    • 打开「在分享表单中显示」;

    • 「接收」选 文本、网址(想分享图片再加 图像);

    • 加一个「获取 URL 内容」:方法 POST,URL 填第 2 步抄来的短链, 请求体选 JSON,内容形如:

      { "pin": "你的六位 PIN", "text": "分享进来的文本" }
      

      (有网址就再加 "url": "分享进来的网址";有图片可先「Base64 编码」再放进 "images": [{"mediaType": "image/png", "data": "…"}]。)

    • 保存 ✓ —— 之后在