Bundle
dsh-relay-host
DeepSeek Harness relay: mirror the home dsh web UI to the public internet through an outbound wire trunk (cloud relay + out-of-tree host plugin).
- Source
- SunNull
- stars
- 1 stars
- License
- MIT
- Updated
- Updated 13 days ago
Readme
# dsh-relay
[English](README.en.md) | 中文
**把家里运行的 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(dsh)完整映射到公网** —— 在任何设备、任何地点,通过一个网址获得与你家中电脑完全一致的 dsh 体验:会话实时同步、流式输出、工具卡片、审批弹窗、工作区浏览,一个不少。
```
手机 / 外网电脑 ──HTTPS──→ 云端中继(dsh-relay-cloud,一台 VPS)
│ 出站长连接干线(家端主动拨出)
▼
家中 dsh(保持 127.0.0.1 回环绑定,零端口暴露)
└─ dsh-relay-host 插件:进程内"隐形浏览器",
以回环身份复放全部请求
```
## 它是如何工作的(Wire-Trunk 架构)
- **家端是一个树外插件**(~200 行,零构建):它作为"dsh 进程里的隐形浏览器",对本机 `127.0.0.1:3080` 发起与真实浏览器完全相同的协议连接(POST `/api/*` + 两条下行 WebSocket),并把它们原封不动复用到云端。
- **云端是纯转发面**:对远端浏览器呈现与 dsh 原版逐字节一致的 wire;静态资源由家端实时上报、云端缓存——前端与宿主永远同版本,不存在协议漂移。
- **不修改 dsh 仓库任何一行代码**:插件挂在 `~/.dsh/profiles/web/` 用户补丁层,`git pull`、自动更新、随时卸载互不干扰。
- **安全性**:dsh 本身无认证层且特权方法钉死回环——本方案恰好让请求以回环身份进入,同时把认证(配对码 → 长期 Cookie)补在云端入口;家端零入站端口,天然穿 NAT。
---
## 完整部署教程
三个角色:**云端**(一台公网服务器)、**家端**(运行 dsh 的电脑)、**远端**(手机/任何浏览器)。按顺序做完约 15 分钟。
### 前置条件
| 角色 | 要求 |
|---|---|
| 云端 | 公网 IP 的 Linux 服务器(1 核 1G 起步即可),能 SSH 登录,开放一个 TCP 端口(下文用 8443) |
| 家端 | 已能运行 `dsh web`(默认 127.0.0.1:3080),Node ≥ 22 |
| 远端 | 任何现代浏览器 |
### 第 1 步:准备凭据(家端电脑上执行)
生成两个强随机值,后面所有步骤都用它们:
```sh
# 干线令牌(家端 ↔ 云端的身份凭证,32 位)
node -e "console.log(require('crypto').randomBytes(24).toString('hex'))"
# 记作 <TOKEN>,例如 9f86d081884c7d65...
# 配对码(远端浏览器首次访问输入,8 位)
node -e "console.log(require('crypto').randomBytes(4).toString('hex'))"
# 记作 <CODE>,例如 a1b2c3d4
```
> ⚠️ 这两个值就是整套系统的钥匙,生成后妥善保存,不要提交进任何仓库。
### 第 2 步:部署云端
SSH 登录你的服务器:
```sh
ssh root@<你的服务器IP>
```
**2.1 安装 Node 22**(已装可跳过;`node -v` 检查):
```sh
ARCH=$(uname -m); case "$ARCH" in x86_64) NARCH=x64;; aarch64) NARCH=arm64;; esac
curl -fsSL https://cdn.npmmirror.com/binaries/node/v22.14.0/node-v22.14.0-linux-$NARCH.tar.xz -o /tmp/node.tar.xz
tar -xJf /tmp/node.tar.xz -C /opt
ln -sfn /opt/node-v22.14.0-linux-$NARCH /opt/node
ln -sf /opt/node/bin/node /usr/local/bin/node
ln -sf /opt/node/bin/npm /usr/local/bin/npm
node -v # 应显示 v22.14.0
```
**2.2 获取代码并安装依赖**:
```sh
git clone https://github.com/SunNull/dsh-relay.git /opt/dsh-relay
cd /opt/dsh-relay/cloud
npm install --registry=https://registry.npmmirror.com
```
**2.3 创建 systemd 常驻服务**(把 `<TOKEN>`/`<CODE>` 换成第 1 步生成的值):
```sh
cat > /etc/systemd/system/dsh-relay.service <<EOF
[Unit]
Description=dsh-relay cloud
After=network-online.target
[Service]
Environment=DSH_RELAY_BIND=0.0.0.0
Environment=DSH_RELAY_PORT=8443
Environment=DSH_RELAY_TOKEN=<TOKEN>
Environment=DSH_RELAY_PAIRING_CODE=<CODE>
WorkingDirectory=/opt/dsh-relay/cloud
ExecStart=/usr/local/bin/node server.mjs
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now dsh-relay
systemctl status dsh-relay # 应为 active (running)
curl -s http://127.0.0.1:8443/healthz # 应返回 {"ok":true,...,"trunkReady":false}
```
> `trunkReady:false` 是正常的——家端还没连。
**2.4 云防火墙/安全组放行 8443**(TCP 入方向,源 0.0.0.0/0):
- **阿里云/腾讯云**:控制台 → 实例 → 安全组/防火墙 → 添加规则 TCP 8443
- 服务器自身若开了 ufw:`ufw allow 8443/tcp`
在**家端电脑**浏览器打开 `http://<服务器IP>:8443/healthz` 验证,应返回 JSON——通了。
### 第 3 步:安装家端插件
在家端电脑(dsh 所在机器),两种安装方式。**要管理面板 UI 请用方式 A**;方式 B 功能等效但侧栏不出现面板入口(见其说明)。
**方式 A:标准 bundle 安装**(仓库声明 `dsh.bundle`,一条命令挂载;管理面板 UI 依赖此方式的 `dsh.client` 发现机制):
```sh
dsh plugin --profile web add "github:SunNull/dsh-relay"
```
然后在 `~/.dsh/profiles/web/cordis.patch.yml` 追加干线配置(bundle 层不携带凭据,用户层覆盖):
```yaml
- id: dsh-relay-host
config:
relayUrl: ws://<你的服务器IP>:8443/trunk
token: <TOKEN>
```
**方式 B:安装器一条龙**(克隆仓库并自动写好配置):
```sh
git clone https://github.com/SunNull/dsh-relay.git
cd dsh-relay
node install.mjs --relay-url ws://<你的服务器IP>:8443/trunk --token <TOKEN>
```
安装器会:
1. 把插件的三个文件(`index`/`admin`/`client`)平铺复制到 `~/.dsh/profiles/web/plugins/`(`index.mjs` 以相对路径导入 `./admin.mjs`,三者必须同目录)
2. 在 `~/.dsh/profiles/web/cordis.patch.yml` 追加托管配置块(dsh 的补丁层**热生效**)
> 方式 B 说明:中继与管理路由完整可用,但 dsh web **侧栏不会出现「中继管理」入口**——client 面板要走 bundle 的 `dsh.client` 发现机制(方式 A)。需要面板 UI 请改装方式 A。
两种方式都需要**重启一次 dsh web**(模块代码需要进程加载;之后仅改配置不再需要重启)。卸载:方式 A 用 `dsh plugin --profile web remove dsh-relay-host`,方式 B 用 `node install.mjs --uninstall`。
```sh
# 停掉现有 dsh web,重新启动,例如:
pnpm dsh web
```
**验证干线已连**——在服务器上执行:
```sh
curl -s http://127.0.0.1:8443/healthz
# "trunkReady":true 即成功;false 则检查插件配置与令牌是否一致
```
### 第 4 步:远端访问
手机(或任何设备)浏览器打开:
```
http://<你的服务器IP>:8443
```
1. 首次出现配对页 → 输入 `<CODE>` → 点"配对"
2. 自动进入完整 dsh 界面(与家中电脑一模一样:会话、模型、工作区全同步)
3. 建议"添加到主屏幕",即得类原生 App 体验
多台设备重复第 4 步即可(默认上限 10 台,见环境变量)。
### 第 5 步(强烈推荐):远程可选工作区
dsh 默认在宿主桌面弹**原生目录选择框**,远程设备看不到。追加此补丁让所有人走**网页内目录浏览**:
编辑 `~/.dsh/profiles/web/cordis.patch.yml`,在末尾追加:
```yaml
- id: directory-picker
disabled: true
- insert:
- id: directory-picker-browse-host
name: '@deepseek-ai/dsh-host-directory-picker-browse'
- id: directory-picker-browse-ui
name: '@deepseek-ai/dsh-client-ui-directory-picker-browse'
```
保存即热生效(这是 dsh 官方为远程部署场景设计的标准换法)。之后"添加工作区"会打开网页目录浏览器,手机上可任意选择家中路径。
### 第 6 步(可选但强烈建议):HTTPS
明文 HTTP 有被运营商劫持/窃听风险。有一个域名即可上自动 HTTPS:
```sh
# 服务器上安装 Caddy(以官方 apt 源为例)
apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
apt update && apt install caddy
```
把 DNS A 记录指向服务器 IP,然后:
```sh
cp /opt/dsh-relay/cloud/Caddyfile.template /etc/caddy/Caddyfile
# 编辑文件,把 dsh.example.com 换成你的域名
nano /etc/caddy/Caddyfile
systemctl reload caddy
```
之后用 `https://<你的域名>` 访问;家端重新指向(热生效,无需重启 dsh):
```sh
cd dsh-relay
node install.mjs --relay-url wss://<你的域名>/trunk --token <TOKEN>
```
---
## 管理面板
dsh web 侧栏的 **⚙ 中继管理** 入口打开内置管理面板——本机直连和经中继配对的远程浏览器都能用,也就是说**躺在沙发上用手机就能管**。
四个区域:
1. **状态卡**:干线在线状态、桥接下行数、缓存条数、云端运行时长
2. **设备表**:每台已配对设备的备注/配对时间/最后活跃,逐台剔除(立即吊销其 Cookie 并断开活动连接),或一键"全部剔除"
3. **配对码表**:多枚带备注的配对码——新增、点击显示码值、启用/停用、换值、删除
4. **审计**:云端操作尾部记录(配对成败、剔除、码管理等)
安全模型一句话:**管理权跟随 dsh web 的可达性**——谁能打开你的 dsh web,谁就能管理中继;手机丢了,从家里电脑或任何其他已配对设备上把它剔除即可。
> 升级要求:面板功能需要**云端与家端插件同时更新**到 0.2.0+(家端重跑 `dsh plugin --profile web add "github:SunNull/dsh-relay"` 或 `node install.mjs`,随后重启 dsh web)。
---
## 日常运维
| 操作 | 命令/位置 |
|---|---|
| 查看云端状态与审计 | 配对后访问 `http://<服务器>:8443/__relay`(JSON) |
| 健康检查 | `curl http://<服务器>:8443/healthz`(无需认证) |
| 云端日志 | `journalctl -u dsh-relay -f` |
| 踢掉某台/所有已配对设备 | dsh web 侧栏「中继管理」面板直接剔除(无需 SSH;见上文[管理面板](#管理面板)) |
| 更新云端代码 | `cd /opt/dsh-relay && git pull && systemctl restart dsh-relay`(各版本变化见 [CHANGELOG](CHANGELOG.md)) |
| 卸载家端插件 | 家端:`node install.mjs --uninstall`(再删掉仓库目录即完全清除) |
| 家端 dsh 重启后 | 无需任何操作——插件自动重连云端(断线指数退避重试) |
## 云端环境变量
| 变量 | 默认 | 说明 |
|---|---|---|
| `DSH_RELAY_PORT` | 3081 | 监听端口 |
| `DSH_RELAY_BIND` | 127.0.0.1 | 绑定地址;公网部署设 0.0.0.0 |
| `DSH_RELAY_TOKEN` | dev-token | 家端干线令牌(与插件 config.token 一致) |
| `DSH_RELAY_PAIRING_CODE` | 随机生成并打印 | 浏览器配对码;建议显式设置 |
| `DSH_RELAY_STATE` | ./relay-state.json | 设备令牌 + 审计日志持久化文件 |
| `DSH_RELAY_MAX_TOKENS` | 10 | 最多配对设备数 |
| `DSH_RELAY_MAX_WS` | 16 | 并发浏览器 WebSocket 上限 |
| `DSH_RELAY_BLOCK_PRIVILEGED` | 0 | `1` = 云端拦截设置/凭据等特权方法(更保守) |
| `DSH_RELAY_HEARTBEAT_MS` | 20000 | 干线与浏览器下行连接的心跳周期(毫秒)。浏览器错过一个整周期的 pong 即判定为死连接断开(前端会自动重连并重放基线),心跳流量同时防止 NAT 空闲超时 |
## 验收测试(probe/)
部署完成后跑一遍,确保全链路健康:
```sh
# 1) 干线三要素(静态/RPC/WS 桥),在任意机器:
node probe/e2e-probe.mjs # 先编辑脚本顶部 BASE 为你的服务器地址
# 2) 浏览器全流程(配对→UI→WS),需 pip install playwright && playwright install chromium:
DSH_RELAY_BASE=http://<服务器>:8443 DSH_RELAY_PAIRING_CODE=<CODE> python probe/browser-acceptance-m1.py
```
## 安全须知(务必阅读)
- **配对码即钥匙**:通过配对的设备 ≈ 坐在你家电脑前使用 dsh(能聊天、跑命令、看文件)。公网部署必须用强配对码 + HTTPS。
- **特权面提醒**:远程默认可访问 dsh 的设置/凭据面。保守起见可设 `DSH_RELAY_BLOCK_PRIVILEGED=1`(远端将无法改设置/看凭据,聊天与文件功能不受影响)。
- 云端零业务落盘:`relay-state.json` 只存设备令牌与审计计数,**不含任何会话内容**。
- 令牌轮换:换 `<TOKEN>` 需同步改云端 systemd 环境 + 家端重跑 `install.mjs --token`。
## 故障排查
| 症状 | 原因与解法 |
|---|---|
| 手机打开显示"家端不在线" | 家端 dsh 没跑 / 插件没连上。服务器 `curl localhost:8443/healthz` 看 `trunkReady`;false 则查家端配置与网络 |
| 配对提示"设备数已达上限" | 之前测试占满 10 坑。在「中继管理」面板"全部剔除"后重新配对 |
| **忘记配对码** | 在 dsh web「中继管理」面板中点击码值即显示;0.2.0 之前部署的实例可读云端 `relay-state.json`(或 systemd 单元/启动日志中的旧值) |
| **更换配对码** | 在面板中对目标码**换值**(或删除后新建)。注意:改 systemd 里的 `DSH_RELAY_PAIRING_CODE` 再重启**不会**轮换已有配对码——状态文件存在时环境变量只在首次播种配对码表,旧码继续有效;要作废某码请用面板的换值/停用/删除 |
| 首次打开很慢(10-20 秒) | 正常:云端冷缓存逐个拉取静态资源,第二遍起快 |
| 远程"添加工作区"无反应 | 没做第 5 步补丁(原生弹窗弹在了家里屏幕上) |
| 界面空白/一直转圈 | 先强刷新/清缓存;再跑 probe 脚本定位是静态、RPC 还是 WS 环节 |
## 已知限制
- HTTP 响应整包缓冲(SSE 下载流除外),超大导出需等待完整传输
- dsh web 协议无版本协商(client/host 同船发布)——前端由家端上报天然同版本;dsh 大版本升级后建议全链路回归跑一遍 probe
- 家端插件模块更新需重启 dsh web(补丁配置热生效,模块代码不热替换)
## 适用版本
基于 DeepSeek Harness `0.1.0-rc.5`(2026-08)验证。dsh 处于 developer preview,wire 变化时以 probe 验收为准。
## License
[MIT](LICENSE)
Install
dsh plugin --profile web add github:SunNull/dsh-relay
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-relay-host from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.