DeepSeek Harness 的移动接入插件:网关(Cloudflare Worker, dsh-gateway-worker)只做 配对 + WebRTC 信令,业务流量全部手机 ↔ Mac 直连(DataChannel,DTLS 加密)—— 零中转、零隧道、零 ssh、零服务器。配合移动客户端 DeepseekHarnessApp 使用。
手机 App ──wss(vstream 帧中转;扫码配对启用 E2E 加密)──→ CF Worker 网关
│
本插件(宿主)──────── wss(vstream 帧中转)──────────────────┘
└── P2P DataChannel(备选直连:业务流量不经网关,网络允许时可用)
2026-09-10 起默认链路为网关中转(P2P 在办公网/蜂窝不可达,降为备选); 2026-09-12 起扫码配对默认启用端到端加密(PSK 经 QR 带外,网关只见 密文;协议与互操作向量见 PROTOCOL.md §5)。下文「零中转」「仅信令」等 段落为 P2P-only 时代历史描述,以 PROTOCOL.md 为准。
- 配对:扫码/手输(双向亮码防抢注);设备令牌由 Worker 签发/吊销, 令牌绑定的 host 路由键只是登记标识;
- 直连:Mac 是 offerer;手机
connect→ Worker 派 sid → offer/answer/ ICE 经信令面交换 → DataChanneldsh建立; - 虚拟流:DataChannel 上的 vstream 二进制帧(OPEN/DATA/CLOSE,
16KB 分片)多路复用 TCP 字节流,Mac 侧落地
127.0.0.1:<dsh web 端口>; 手机侧同样起本地回环代理 —— 两端 HTTP/WebSocket 栈零改动; - UI:侧栏 foot「移动接入」dialog:P2P 状态(信令通道/活跃会话/
虚拟流计数)+ 机器名 + 配对 + 设备管理(链路徽章)+ 安全事件横幅
(配对/吊销 OS 通知与角标)。入口在本机可打通的页面可见:回环 http(s)
源(
dsh web的浏览器页)与 dsh desktop 外壳(dsh-app://app/)都挂; 局域网/公网源不挂(管理面只对 loopback 开放,见「安全模型」)。
协议细节(信令消息 + vstream 帧格式)见 PROTOCOL.md。
前置:
- pnpm 在 PATH 上(
dsh plugin本体只是对 profile 目录的 pnpm 转发器, 没有 pnpm 直接报 127 退出); - 网关 Worker 已部署且含信令面(
/signal/*+/admin/signal/ticket); - 手机 App 为支持 P2P 的版本(探测
/signal/caps自动走直连)。
dsh plugin --profile web add github:iptton-ai/dsh-mobile # 或本地目录: add ./dsh-mobile本包声明了 dsh.bundle.patch(指向自带的 cordis.patch.yml),
因此它是一个组合包:装好后会出现在 Web 侧栏「插件」页的 Installed 组,
开关即启用状态;被选中时它自带的 patch 作为组合包层自动生效(禁用官方 native
目录选择器 + 挂 browse 双前端 + 插入 dsh-mobile 行)。也可以在插件页用
Add plugin 直接装。
老文档让人手工合并 insert 段 —— 现在不需要了,照做反而会让同一批 id 在 组合树里出现两次。已经手工合并过的,见第 3 步的迁移说明。
报
ERR_PNPM_ADDING_TO_ROOT? profile 目录里已有pnpm-workspace.yaml(比如已跑过一次 approve-builds),pnpm 会把它当 workspace、拒绝把依赖加到 workspace root。两个解法任选: ① 命令尾追加-w(dsh 转发器原样透传参数):dsh plugin --profile web add github:iptton-ai/dsh-mobile -w; ② 在 profile 的pnpm-workspace.yaml加一行ignoreWorkspaceRootCheck: true后重跑原命令(以后 add/update 其他插件也不再问)。
pnpm ≥ 10 默认不执行依赖的 install 脚本;本插件的 WebRTC 依赖
node-datachannel 全靠 install 脚本下载预编译原生二进制。跳过的后果是
安装期零报错:dsh web 启动时插件 import 崩,侧栏不出现「移动接入」
入口。在 profile 目录(缺省 ~/.dsh/profiles/web/;设了 DSH_HOME
则在其下)执行:
cd ~/.dsh/profiles/web
pnpm approve-builds # ⚠️ 交互式命令,不带包名(见下);勾选 node-datachannel
dsh plugin --profile web install # 重跑安装,补跑被跳过的脚本报
Command "approve-builds" not found? 两个常见原因: ① 给approve-builds传了包名 —— 它是交互式命令不接参数,带参会当成 "执行叫 approve-builds 的命令" 而报ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL; ② pnpm < 10 根本没有这个子命令(pnpm -v确认)。 跨版本最稳的手写法(不依赖该命令):直接编辑 profile 的pnpm-workspace.yaml,加入:onlyBuiltDependencies: - node-datachannel然后重跑
dsh plugin --profile web install。
审批写入 profile 的 pnpm-workspace.yaml 持久生效,后续
dsh plugin --profile web update 不再丢。
第 2 步做完别急着重启,先确认预编译二进制存在(路径里的版本号以实际为准):
ls node_modules/.pnpm/node-datachannel@*/node_modules/node-datachannel/build/Release/
# 应能看到 node_datachannel.node文件不存在时的排查(典型症状:dsh web 启动报
Cannot find module '../../../build/Release/node_datachannel.node'):
- pnpm < 10(如 8.x):没有脚本跳过机制,二进制缺失说明 install 脚本
跑了但下载失败——预编译包从 GitHub Releases 下载,国内网络常超时。
强制重跑并挂代理:
若它转而本地编译并报缺 cmake:
pnpm rebuild node-datachannel # 失败/卡住时: HTTPS_PROXY=http://127.0.0.1:<代理端口> pnpm rebuild node-datachannel
brew install cmake后重跑(较慢)。 - pnpm ≥ 10:检查 yaml 键名拼写与缩进,改完必须重跑 install (只改配置不会补跑脚本)。
行结构由组合包自带的 patch 提供,不需要手工合并 insert。你只需要在 profile
自己的 cordis.patch.yml 里按 id 覆盖部署相关的 config(用户层在组合包层之后,
同一行的 config 以用户层为准):
- id: dsh-mobile
name: "dsh-mobile"
config:
gateway: https://dsh.example.com
adminKey: '<ADMIN_KEY>'键位见下节「配置」。插件页里 dsh-mobile 的开关如果没打开,打开它即可 ——
它就是 profile package.json 的 dsh.profile.bundles 里的 dsh-mobile 行。
已按旧文档手工合并过 patch? 先删掉你自己
cordis.patch.yml里那段- insert:(directory-picker-browse/ui-directory-picker-browse/dsh-mobile)和前面的- id: directory-picker / disabled: true,只留上面这段 id 覆盖的 config,再去插件页把dsh-mobile开关打开。否则同一批 id 会在组合树 里出现两次(dsh --profile <name> --dump-config可见),宿主半边会被挂载两遍。
重启后 dsh web 侧栏底部出现**「移动接入」**入口即装好。没出现时按序查:
① dsh web 日志有无 node-datachannel import 报错(= 第 2 步被跳过);
② 组合包是否启用(插件页 Installed 组的 dsh-mobile 开关 / profile package.json
的 dsh.profile.bundles)+ config 是否填了 gateway;
③ 改完 patch/开关后是否重启了 dsh web(desktop 外壳还需重启应用:窗口的模块
清单是启动快照,且模块根不走 HMR)。
2026-10-05 起四项均可在面板直接编辑:「移动接入」面板新增**「中转网关」区** (gateway + host 输入框),连同既有的「机器名」(label)与「管理密钥」(adminKey) 栏,保存在本机 dsh 用户设置、即时生效 —— 自布中转服务器的使用者全程无需碰 cordis.patch.yml。优先级:env
DSH_MOBILE_*> 面板(settings)> 此处 config。 下方表格的 config 键仅作部署模板/回落位。
cordis.patch.yml 的 dsh-mobile 行 config(DSH_MOBILE_* 环境变量可覆盖):
| 键 | 说明 | 默认 |
|---|---|---|
gateway |
CF Worker 网关地址(如 https://dsh.example.com) |
必填 |
adminKey |
管理密钥(部署 Worker 时的 ADMIN_KEY,≥16 字符)。优先级:env DSH_MOBILE_ADMIN_KEY > 面板「管理密钥」栏(用户 settings)> 此处 config —— config 是明文模板位,profile yml 常随 dotfiles 同步,能不用就不用 |
必填* |
host |
宿主路由键(多宿主各占一个;仅登记标识,无隧道语义) | <短主机名>.host |
publicUrl |
扫码落地页 | <gateway>/pair |
label |
机器名(缺省 hostname;面板可改,持久化) | — |
iceServers |
ICE 服务器(JSON 数组字符串;缺省公共 STUN,无 TURN;只作用于 Mac 侧 gather,手机侧由网关 ICE_SERVERS 下发) |
公共 STUN |
* adminKey 三处任一有效即可;全缺时插件不再拒绝加载(2026-08-29 前 会在启动时 throw),配对/信令以运行时错误文案指路,面板「管理密钥」栏可补。
环境变量:DSH_MOBILE_GATEWAY / DSH_MOBILE_ADMIN_KEY / DSH_MOBILE_HOST /
DSH_MOBILE_PUBLIC_URL / DSH_MOBILE_LABEL / DSH_MOBILE_ICE_SERVERS
- 管理通道从「ssh 到网关服务器 loopback」换成「HTTPS+Bearer adminKey 直连
Worker」:不再需要
target/adminPort,也不再依赖任何服务器与 ssh key; - adminKey 迁移(2026-08-29 修复):旧版在 webui「管理密钥」栏保存过的 密钥存储在 dsh 用户 settings,升级后自动沿用(优先级 env > settings > config);此前一段时间(78974c0–6f1dd43)settings 值被静默忽略、缺 config 即加载失败 —— 升级到本版后两个问题都不存在,面板密钥栏同步恢复;
remotePort(服务器隧道口)→host(路由键):多宿主仍各占一个, 网关按它把手机信令路由到本宿主;- 移除 ssh -R / cloudflared / Rust 网关 / CF Worker 中转面 / Web 远程访问 (网关浏览器登录)/ 多租户免 ssh 通道 —— 服务器带宽占用归零;
- 旧(中转时代)配对的令牌绑定的是旧 tunnel_host,需重新配对一次。
打洞失败(对称 NAT 等)无 TURN 时连接失败;要 TURN 时给 iceServers 配
(或 Worker env ICE_SERVERS),经 /signal/caps 与 ack 下发。
- 管理 API(
/pair/api/*)仅接受 loopback Host + 同源三重门 (Sec-Fetch-Site/Origin 双检、写操作强制x-dsh-mobile头、JSON 体限application/json); - 网关管理面信任根 = ADMIN_KEY(Bearer);host ticket 为短时 JWT(900s);
- 业务通道安全 = DTLS(WebRTC 强制);信令安全 = 设备令牌 + ADMIN_KEY;
- 配对秘密只存在手机内存;令牌可随时吊销;安全事件(配对成交/吊销) OS 通知 + 面板横幅显性化。
npm install
GATEWAY=https://dsh.example.com ADMIN_KEY=<密钥> node tools/e2e-smoke.mjs # 独立脚本模拟两侧
node tools/plugin-live-smoke.mjs # 插件真实代码路径(读 /tmp/.dshgw_admin_key)通过判据:消息经 P2P 通道原样返回(业务流量不经过 Worker)。
已核对 dsh 0.1.6-alpha.2(上一记录版 0.1.6-alpha.1):本插件触点全部
兼容,无代码改动。上游这轮变化集中在本插件不触及的面:session-controller
client 半侧大重构(sessions service/projection 拆分,宿主 RPC schema 零变化)、
terminal-controller 新增 bindings/retention(纯增量 namespace)、新增
plugin-manager / office-to-pdf 两个 remote 挂载、client-modules 组合产物从
内容寻址改为 revision 寻址并支持包内懒加载 chunk(注册协议向后兼容:
ClientBundleRegistration.chunk 可选)、desktop boot 走 dshDesktopBoot 门控
(served web 路径不变)。逐面核对(对 alpha.1→alpha.2 全量 diff + 已装
alpha.2 安装体验证):
- packages/api RPC schema —— session-controller 改的是 client 半侧内部 (宿主 contract sessions.ts 形状零变化);terminal-controller/workspace-files 为纯增量或内部实现(readAll 改走 fs.readBytes,错误码不变);gateway/ remotes 仅新增挂载,既有 namespace 零变化;settings-controller 仅版本号;
- 插件/extension 接口 ——
webServer.register({kind:'prefix'})/.portgetter(webserver src 零 diff)、settings.register(ns, schema, {base})、connection.authenticatedUrl、dsh.client声明解析、/plugins/<id>/client.js下发路由、sidebar.footer.action槽位 (ui-sidebar contract 在位):触点全部在位且形状一致。connection 新增connection/requestwaterfall 事件与streamBaseUrlhook,均为可选增量; - cordis.patch 清单结构 —— vendor/loader 零 diff,
EntryOptions与disabled/insert写法不变,directory-picker-browse两包名不变, profile 合并块无需同步; - 客户端协议 ——
__ModuleLoader__.load({id,factory})+require('react')种子 +slots.inject/register协议不变;chunk字段为可选增量, 既有单文件 bundle 注册不受影响。
复验(2026-09-16 实测,对真实 @deepseek-ai/dsh@0.1.6-alpha.2 宿主,全部
exit 0):
node --check lib/index.js && node --check lib/client.js # 语法门
node tools/contract-smoke.mjs # 契约门:mock 宿主跑真实 apply(),18 项(零网络)
node tools/host-live-smoke.mjs # 真宿主门:已装 dsh 的 WebServer+FileSettingsProvider 起真实 HTTP,15 项(零外网)
node tools/crypto-vectors.mjs # E2E 加密向量自检,6 cases(零网络)
node tools/client-guard-smoke.mjs # 客户端门:入口可见性矩阵(回环/dsh-app 外壳挂,局域网/公网不挂),15 项(零网络)host-live-smoke.mjs 是升级后的第一道硬门:用已安装 dsh 的真实
cordis/WebServer/settings 服务跑本插件真实 apply(),真实 HTTP 逐项断言
(真实 prefix 路由、真实 .port、settings 磁盘落盘、管理面三重门、dispose
下线)。connection 服务因拖 credentials 持久化不起真身,给形状桩。定位
已装 dsh 用 DSH_GLOBAL_ROOT,缺省 npm root -g 下的
@deepseek-ai/dsh/node_modules。每次升级 dsh 先跑这四条,再按需跑
上面的端到端冒烟。
alpha 断层,变化量大但触点零变化,无代码改动:新增 terminal-controller /
permission-presets 两个 RPC namespace;vendor/loader Entry 大重构但
apply(ctx, config) 契约与 patch 清单写法不变;client-modules 双半边内部
重构(dsh.client 语义、client.js 下发、slot 协议不变);boot profile 行名
改 link 模式物化,升级后开一次 dsh web 确认 insert 行加载(已确认)。
本插件触点全部兼容,无代码改动。上游变化集中在自带 client UI 插件内部 (ui-message-feedback 提交语义重构、ui-primitives 图稿拆分、间距微调), packages/api 六包仅版本号 diff,apps/web 客户端 src 零 diff;profile 合并块 无需同步。
本插件触点全部兼容,无代码改动。上游这轮实质变化只有三处,均不触及
本插件:LLM 模型目录(deepseek-v4-flash→deepseek-flash;bundle 基线
cordis.patch.yml 改的是 agent-default-model 的 config 值,清单结构未动)、
web 客户端 UI 打磨(侧栏右栏/文档预览/代码块/统计 pill)、slot-catalog
一处 source 行号注释。逐面核对(对已安装 rc.1 安装体逐一验证):
packages/api 六包仅版本号 diff;插件/extension 接口触点(webServer
register/.port、settings.register get/watch/update、connection
authenticatedUrl、dsh.client 声明、client.js 下发路由、sidebar.footer.action
槽位)全部在位且形状一致;客户端协议 __ModuleLoader__.load({id,factory})
require('react')种子不变。
复验:node --check(语法)+ contract-smoke.mjs(mock 宿主 18 项)+
host-live-smoke.mjs(真宿主 15 项;该工具即本轮新增的升级后第一道硬门)。
本插件触点全部兼容,无代码改动。逐面结论:
- 宿主服务 ——
webServer.register({kind:'prefix',path,handler})、settings.register(ns, schema, {base})的get/watch/update、connection.authenticatedUrl():packages/host/webserver、packages/settings/settings在 rc.1→alpha.2 仅package.json版本号 diff,browser-auth.ts零 diff; - 客户端协议 ——
window.__ModuleLoader__.load({id,factory})+require('react')种子 +slots.inject/register+sidebar.footer.action槽位不变;dsh.client声明形状(platform/inject)不变(上游只是把接口挪到@deepseek-ai/dsh-package-manifest的DshClientManifest)。boot 图仍收录dsh-mobile条目,实机(0.1.3-alpha.2 GUI)侧栏入口 + dialog 正常; cordis.patch.yml行结构 ——id/name/config/disabled/insert不变; web-app bundle 仍有directory-picker行,browse 后端/前端两包名未变。 (上游新增行为:绝对路径的insert.name也会转 file URL;本插件用包名,不受影响。)
当时的复验门:node --check(语法)+ contract-smoke.mjs(mock 宿主 18 项)。
MIT License.