@qzsy/dsh-bridge
v3.11.1
Published
DSH 远程隧道云官方客户端插件:一条命令把本机 DeepSeek Harness 接入云端隧道(设备码绑定、短期令牌、本地门禁代理、HTTP/SSE/WebSocket 反向隧道)。
Readme
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与 CSSurl(); - 只处理同源根绝对 URL:不改
<a>导航、跨域 URL、已带前缀的 URL 与/_gw网关内部路由; - 本地直连与 Host 子域形态前缀为空,不安装任何补丁;自定义隧道/自建反代挂在任意子路径时,
以
<base href="./">解析出的路径为准。
已知边界:ESM 动态
import()与element.style.backgroundImage这类单属性赋值走引擎内部路径, 无法在此拦截;插件自身仍应优先使用相对 URL。
手机访问
窄屏(≤ 767px)访问 DSH 时,插件会自动把设置中心改成两级钻取、在顶部显示项目与分支,并按宿主能力决定抽屉归属,不需要额外配置:
- 抽屉优先交给宿主:DSH ≥ 0.2.2(AppFrame 在 <768px 自带
data-sidebar-drawer抽屉、遮罩与「打开侧边栏」按钮)时,插件不再注入自己的菜单按钮 / 遮罩 / 侧滑手势,也不改写侧栏的position/transform/z-index;只在宿主明确不做的部分补齐:软键盘与 home indicator 的底部让位、刘海下移宿主的「打开侧边栏」、抽屉内的安全区内边距、输入框 ≥16px(iOS 聚焦不放大),并用宿主的--dsh-frame-top-clearance抵掉全局面板顶部的双倍留白。 - 旧宿主(DSH < 0.2.2)自动回退:没有抽屉能力的宿主上仍由插件自绘抽屉(内容占满屏宽、左上角菜单按钮、左缘右滑打开 / 抽屉内左滑关闭、拖动跟手松手吸附、点遮罩 / 点会话行 / Esc 收起),行为与 3.9.x 一致。页面加载后如果宿主才挂出抽屉标记(例如窗口从宽变窄),会在下一次 resize 时切换过去并拆掉自绘痕迹。
- 顶部项目栏左侧是项目标题,右侧是分支芯片(没有分支时不显示);有未提交改动时在分支右侧显示绿色
+N/ 红色−N行数;点芯片展开只读分支面板:项目目录名 + 路径、搜索、本地/远程分支、最近提交时间、当前分支勾选(分支数据来自本机只读 Git 快照,手机端不切换分支); - 设置中心先显示全宽分类列表(图标 + 名称 + ›,52px 触控行),点分类进入详情,左上角「‹ 分类名」返回;详情内容独占整宽,不再被 188px 导航挤成竖排文字;
- 模型切换器在手机上改为底部 sheet:全宽、抓手、遮罩,支持下拉/点遮罩关闭;默认不弹键盘(搜索框
readonly),需要搜索时再点(两种抽屉模式都生效); - 选完模型自动继续进入「推理等级」sheet,Off / Low / High / Max 选完才关闭,手机上和桌面菜单一样连贯;
- 全面屏自动避开安全区:刘海 / 状态栏 / 底部 home indicator 不遮挡内容;
- 桌面端保持 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 | 设备已吊销或令牌失效:解绑后重新绑定;被吊销需在控制台重新添加设备 |
| 最近错误 4004 time_exhausted | 账号连接时长已用完(按账号计费,不按设备):控制台「账单」购买时长或让后台调账,再回面板点「开启隧道」重连;不需要重新绑定 |
| 打开公网地址 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。
