DSH 远程隧道云 · 官方客户端插件
@qzsy/dsh-bridge 是 DSH 远程隧道云的官方客户端插件:一条命令把本机 DeepSeek Harness 接入云端隧道,在手机、平板或任意设备上安全远程使用。
- 独立实现:包内所有代码均为本仓库自研(MIT,版权见 LICENSE),不依赖、不包含任何第三方 dsh 隧道插件。
- 独立身份:插件 id/name
qzsy-dsh-bridge、RPC 通道 /qzsy-dsh-bridge、设置项与 provide 服务均为本插件命名;数据目录 ~/.dsh/qzsy-dsh-bridge/,默认本地端口 3092(可配置)。
- 轻量:只做 SaaS 隧道 + 本地门禁代理 + 设置面板,不包含 IM 机器人、Cloudflare、自建隧道、多网卡等额外功能。
安装
<name> 换成你的 DSH profile(如 web / desktop):
dsh plugin --profile <name> add @qzsy/dsh-bridge
安装后在 设置 → 远程访问 打开面板(插件 id 为 qzsy-dsh-bridge,DSH 组件列表会显示本插件而非其它隧道插件)。
绑定设备(接入 DSH 远程隧道云)
无感授权流程(推荐,全程不需要输入设备码、也不需要设本地口令):
- 面板中确认 SaaS 网关地址:官方云为
wss://dsh.b.nps.qzsyzn.com/_gw/connect(自建部署填自己的网关地址),点「保存网关」。
- 点「立即登录授权」:插件自动申请设备码并在系统浏览器打开
https://<控制面>/device/authorize?code=...。
- 在浏览器登录 SaaS 账号 → 点「授权此设备」→ 回到 DSH 面板。
- 面板会自动轮询并完成绑定,绑定后自动开启隧道,随后出现「显示二维码」按钮。
绑定成功后,面板顶部会出现账号卡片:头像、昵称/邮箱/用户名、剩余时长、本月已用、
累计已用与授权时间;每 60 秒自动刷新,也可手动「刷新」。
- 手机扫码(
https://<控制面>/open/<publicId>)→ 登录 SaaS 账号 → 自动验证隧道归属并签发一次性
ticket,跳转进入本机 DSH。
需要手动输入设备码时,可展开面板「高级:手动设备码 / 局域网口令」,用「获取设备码」走旧流程。
绑定信息(deviceId / deviceSecret)只保存在本机 ~/.dsh/qzsy-dsh-bridge/config.json
(权限 0600);面板只显示掩码。解绑会清除本机凭证,设备本身需在控制台吊销。
公网地址形态
- Host(主):
https://<publicId>.b.nps.qzsyzn.com/ —— 多租户生产形态,根路径资源天然隔离。
- Path(兼容):
https://<平台域名>/<prefix>/t/<publicId>/... —— 单隧道演示/无泛域名环境。
面板会把 Host 路由(存在时)显示为主地址,并同时列出 Path 路由。
路径前缀下的插件兼容
Path 形态把 DSH 挂在 /t/<publicId>/ 下,DSH 前端本身用 <base href="./"> + 相对 URL,天然适配;
但第三方插件常写死根绝对路径(例如 dsh-genui 的 /dsh-genui/runtime.js、/api/dsh-genui/prompt),
根绝对请求会绕开前缀落到平台控制面 → 插件在远程模式下表现为「无法访问 / 白卡」。
插件会在 DSH 渲染 index.html 时(webserver/index-inject)把一段路径前缀兼容层内联进 <head>,
在任何插件脚本之前把同源根绝对 URL 补上隧道前缀:
- 覆盖
fetch / XMLHttpRequest / WebSocket / EventSource / navigator.sendBeacon;
- 覆盖
script / link / img / iframe / media 等的 src、href、data、poster、srcset、
form.action 与 CSS url();
- 只处理同源根绝对 URL:不改
<a> 导航、跨域 URL、已带前缀的 URL 与 /_gw 网关内部路由;
- 本地直连与 Host 子域形态前缀为空,不安装任何补丁;自定义隧道/自建反代挂在任意子路径时,
以
<base href="./"> 解析出的路径为准。
已知边界:ESM 动态 import() 与 element.style.backgroundImage 这类单属性赋值走引擎内部路径,
无法在此拦截;插件自身仍应优先使用相对 URL。
手机访问
窄屏(≤ 767px)访问 DSH 时,插件会自动把左侧栏改成抽屉、把设置中心改成两级钻取、在顶部显示项目与分支,不需要额外配置:
- 内容区占满屏宽,左上角「菜单」按钮打开会话 / 工作区 / 插件入口;
- 顶部项目栏左侧是项目标题,右侧是分支芯片(没有分支时不显示);有未提交改动时在分支右侧显示绿色
+N / 红色 −N 行数;点芯片展开只读分支面板:项目目录名 + 路径、搜索、本地/远程分支、最近提交时间、当前分支勾选(分支数据来自本机只读 Git 快照,手机端不切换分支);
- 设置中心先显示全宽分类列表(图标 + 名称 + ›,52px 触控行),点分类进入详情,左上角「‹ 分类名」返回;详情内容独占整宽,不再被 188px 导航挤成竖排文字;
- 支持手势:左缘向右滑打开、抽屉内向左滑关闭,拖动时跟手,松手吸附;
- 模型切换器在手机上改为底部 sheet:全宽、抓手、遮罩,支持下拉/点遮罩关闭;默认不弹键盘(搜索框
readonly),需要搜索时再点;
- 选完模型自动继续进入「推理等级」sheet,Off / Low / High / Max 选完才关闭,手机上和桌面菜单一样连贯;
- 全面屏自动避开安全区:刘海 / 状态栏 / 底部 home indicator 不遮挡内容;
- 点遮罩、点会话行、按 Esc 或转回桌面宽度都会自动收起;
- 桌面端保持 DSH 原生布局,不受影响。
本地门禁与安全语义
插件会在本机监听一个代理端口(默认 3092),把请求转发给 DSH 本体并注入 DSH 原生
loopback 会话 cookie。来源判定:
| 来源 | 判定 | 结果 |
|---|
| 物理本机回环 | 连接来自 127.0.0.1 / ::1 且未带隧道标记 | 免认证,享有本机特权 |
| SaaS 隧道 | 携带 x-dsh-internal-tunnel 标记,并带网关注入的 x-qzsy-gate: ts.uid.hmac | 验签通过(HMAC-SHA256、±60 秒)即认证;未验签一律 401,不再要求本地口令 |
| 局域网 / 其它直连 | 非回环且无有效隧道标记 | 需本地口令登录或 ?auth= 令牌换取会话(可选能力) |
/__qzsy_bridge__/loopback-token 只对物理回环签发;经隧道访问一律 403。
- 隧道访客由网关信任头认证:hello-ack 为每条连接下发随机 32 字节
gate.secret,网关在校验一次性 ticket
且账号为隧道 owner 后附加 x-qzsy-gate;插件验签通过即放行,不依赖本地口令。
x-qzsy-gate 由网关先剥掉客户端伪造值再附加;插件转发 DSH 前也会剥离该头与隧道标记。
- 本地口令是可选的局域网直连能力(面板「高级」里可设置),与 SaaS 扫码进入互不影响;
?auth= 固定令牌同样只用于局域网/直连,请只发给可信设备。
- 移动端项目栏的分支快照走本插件 RPC 的只读
gitState:不需要本机 adminToken(手机经隧道也能看),
但只执行 git rev-parse / git for-each-ref,路径需为存在的绝对目录,不切换分支、不写 ref、
分支名不进 shell;面板里也标注「手机端只读」。
配置
配置文件:~/.dsh/qzsy-dsh-bridge/config.json
| 字段 | 默认 | 说明 |
|---|
port | 3092 | 本地门禁代理端口 |
gatewayUrl | wss://dsh.b.nps.qzsyzn.com/_gw/connect | SaaS 网关地址(也接受 http(s)) |
autoStart | false | 随 DSH 启动自动开启隧道 |
deviceId / deviceSecret | — | 绑定后写入,勿手工复制到其它设备 |
tunnel / routesVersion | — | 云端下发的只读缓存 |
access.token / access.sessionSecret | 自动生成 | 本地访问令牌与会话签名密钥 |
也支持 cordis 配置(见 cordis.example.yml):port、targetPort、gatewayUrl。
故障排查
| 现象 | 排查 |
|---|
| 面板显示「未绑定」 | 确认网关地址可访问;重新点「立即登录授权」,确认浏览器里已点「授权此设备」 |
| 授权页提示设备码无效/过期 | 设备码 10 分钟有效,回面板重新点「立即登录授权」 |
| 开启隧道一直「连接中」 | 检查 wss://.../_gw/connect 是否可直连(公司代理/防火墙可能拦截 WebSocket) |
握手失败 auth_failed / revoked | 设备已吊销或令牌失效:解绑后重新绑定;被吊销需在控制台重新添加设备 |
| 打开公网地址 401 | 隧道来源只认网关 x-qzsy-gate 签名:确认控制台/网关已升级到支持无感授权(hello-ack 下发 gate.secret),并重新扫码获取新 ticket |
| 本机 3092 被占用 | 在 config.json 或 cordis 配置里改 port |
| 请求 502 | DSH 本体不可达:确认 dsh web 在运行,或配置 targetPort |
| WebSocket 打不开 | 确认网关与反代放行 WS Upgrade;插件会透传 ws-open/accept/frame/close |
已知限制(v3.2.0)
getStatus 为面板引导接口,免 adminToken,只返回脱敏状态(设备 id/secret 均掩码,不含访问令牌与二维码;openUrl 由 /open/ 二次校验归属)。
- adminToken 每次进程启动随机生成,只经物理回环
/__qzsy_bridge__/loopback-token 签发;重启 DSH 后需重新领取(面板自动完成)。
- 访问口令登录已内置限流(同一 IP 连续 5 次失败锁定 60 秒);请使用足够强的口令,并只在内网/可信网络暴露本地端口。
- 修改或清除访问口令会轮换会话签名密钥,所有已登录的远程访客会立即失效。
access.token 为固定本地令牌(?auth= 链接可重复使用),README 已说明泄露处置方式;吊销设备后请解绑并重新绑定以轮换设备密钥。
开发与测试
npm install
node client/build.mjs # 重建 client/client.js
node --test test/*.test.mjs # 全量单测
测试覆盖:v2 握手、心跳与心跳超时、HTTP/SSE/WebSocket 转发、route-update、drain、
4003 吊销终态、断线重连、门禁三类来源、打包身份(全包无第三方包名残留)。
协议与许可
- 控制通道协议 v2 契约见仓库
docs/architecture/saas-tunnel.md,共享类型见 packages/tunnel-protocol。
- 本包以 MIT 许可发布,版权归本仓库(qzsy)所有,详见 LICENSE。