# DSH 云服务器部署 + 手机 APK 联动 · 完整教程

> 环境：Ubuntu 24.04 / Node 22.23 / DSH 0.2.1-rc.2 / nginx 1.24 / APK 19 KB
> 在线版（带截图与架构图）：https://www.lukezystudio.cn/dsh-mobile-doc/
> 机器可读事实：https://www.lukezystudio.cn/dsh-mobile-doc/facts.json

目标：在一台云服务器上跑起 DeepSeek Harness，用一个带账号密码的网关把它安全地放到公网，
再用一个 19 KB 的安卓 WebView 外壳让手机也能进同一个工作台。服务器更新后手机刷新即同步，APK 不用重装。

---

## 0. 架构

```
手机 App (WebView)  ─┐
                     ├─ HTTPS :443 → nginx → dsh-cloud-gateway :3080 (登录墙) → dsh web :13080
电脑浏览器          ─┘                          （转发）                        （只绑 127.0.0.1）
```

四层，各自只做一件事：

| 层 | 职责 | 关键约束 |
|---|---|---|
| nginx | TLS、WebSocket、缓存分流 | 入口 HTML no-cache，哈希资源长缓存 |
| dsh-cloud-gateway | 登录 + 反代 | 必须显式写上游，否则打回自己 |
| dsh web | 会话、工作区、插件、文件与终端 | 只监听回环，永不直连公网 |
| dsh-mobile | WebView 壳 | 不打包网页，只做壳能力 |

---

## 1. 准备

- Ubuntu 22.04 / 24.04，内存 ≥ 1 GB（实测 DSH 常驻 600–800 MB）
- Node 22.x
- 一个域名 A 记录指向服务器（HTTPS 需要）
- 安全组放行 80 / 443（8443 仅 Windows 隧道需要）
- `apt install nginx certbot python3-certbot-nginx`

> 安全前提：DSH 能读写服务器文件、执行命令，等价于一个带 shell 的账号。密码用密码管理器生成，不要复用。

---

## 2. 安装 DSH

```bash
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs
sudo npm i -g @deepseek-ai/dsh
dsh --version        # 0.2.1-rc.2
```

第一次启动（前台），日志会打印带 token 的本地地址：

```bash
dsh web --host 127.0.0.1 --port 13080 --trusted-host dsh.example.com
# dsh web: http://127.0.0.1:13080/?token=xxxxxxxx
```

- token 是官方 Web 的唯一凭据，第一次访问必须带它，之后浏览器种 cookie。
- `--trusted-host` 必填。DSH 用 Host / Origin 做同源校验，域名带端口时，带端口和不带端口各写一条。

---

## 3. systemd 常驻

`~/.config/systemd/user/dsh.service`：

```ini
[Unit]
Description=DeepSeek Harness Web UI
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
WorkingDirectory=/home/ubuntu
Environment=HOME=/home/ubuntu
Environment=DSH_HOME=/home/ubuntu/.dsh
Environment=PATH=/home/ubuntu/.nvm/versions/node/v22.23.0/bin:/usr/local/bin:/usr/bin:/bin
ExecStart=/home/ubuntu/.nvm/versions/node/v22.23.0/bin/dsh web \
  --host 127.0.0.1 --port 13080 \
  --trusted-host dsh.example.com --trusted-host dsh.example.com:3080
Restart=always
RestartSec=5

[Install]
WantedBy=default.target
```

```bash
systemctl --user daemon-reload
systemctl --user enable --now dsh
loginctl enable-linger $USER     # 不设置的话，退出 SSH 服务会被杀
journalctl --user -u dsh -f
```

---

## 4. 登录网关

```bash
dsh plugin --profile web add github:MrLukezy/dsh-cloud-gateway
dsh plugin --profile web add github:mexiaosqwq/dsh-web-mobile
systemctl --user restart dsh
```

`~/.dsh/profiles/web/cordis.patch.yml`：

```yaml
- id: cloud-gateway
  config:
    listenHost: 0.0.0.0
    listenPort: 3080
    upstreamHost: 127.0.0.1     # ★ 必须写
    upstreamPort: 13080         # ★ 必须写
    basePath: /dsh
    username: your-name
    password: your-password
    trustProxy: true
```

验证：

```bash
ss -lntp | grep -E '13080|3080'
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3080/dsh/   # 302
```

常见坑：漏 `upstreamHost/Port` → 网关转发给自己 → 登录永远回登录页。

---

## 5. nginx 反代

```nginx
map $http_upgrade $connection_upgrade { default upgrade; '' close; }

server {
    listen 443 ssl http2;
    server_name dsh.example.com;

    ssl_certificate     /etc/letsencrypt/live/dsh.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/dsh.example.com/privkey.pem;

    client_max_body_size 50m;

    # 哈希资源：长缓存
    location ^~ /assets/ {
        proxy_pass http://127.0.0.1:3080;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        add_header Cache-Control "public, max-age=31536000, immutable";
    }
    location ^~ /plugins/ {
        proxy_pass http://127.0.0.1:3080;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        add_header Cache-Control "public, max-age=31536000, immutable";
    }

    # 入口 / API / WebSocket：no-cache + 不缓冲
    location ^~ /dsh/ {
        proxy_pass http://127.0.0.1:3080;
        proxy_http_version 1.1;
        proxy_set_header Upgrade           $http_upgrade;
        proxy_set_header Connection        $connection_upgrade;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_buffering off;
        proxy_read_timeout 86400;
        proxy_send_timeout 86400;
        add_header Cache-Control "no-cache";
    }

    location /api {
        proxy_pass http://127.0.0.1:3080;
        proxy_http_version 1.1;
        proxy_set_header Upgrade    $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_set_header Host       $host;
        proxy_read_timeout 86400;
    }

    location /dsh-apk/ {
        alias /var/www/dsh-apk/;
        add_header Cache-Control "no-store";
    }
}
```

为什么四个前缀都要代理：入口 HTML 在 `/dsh/` 下，但 boot manifest 里的资源 URL 是根绝对路径
`/assets/…`、`/plugins/…`。只代理 `/dsh/` 的典型现象是登录进去了但白屏、控制台 404。

```bash
sudo nginx -t && sudo systemctl reload nginx
```

---

## 6. HTTPS

```bash
sudo certbot --nginx -d dsh.example.com
sudo certbot renew --dry-run
```

安卓 9+ 默认禁止明文 HTTP，手机端务必用 HTTPS。

---

## 7. 验收

| 检查 | 期望 |
|---|---|
| `ss -lntp \| grep 13080` | 只绑 127.0.0.1 |
| `curl -sI https://host/dsh/` | 302 → /dsh/login |
| 登录后 | 进入会话列表 |
| 发消息 | 流式逐字返回（不是整段蹦） |
| DevTools | 无 404 资源 |
| 选工作目录 | 不报 transport failure |

---

## 8. 手机端（19 KB 外壳）

下载：https://www.lukezystudio.cn/dsh-apk/mobile/

它是纯 WebView 壳，不含网页资源：

- 登录墙凭据记忆（basic auth）
- 系统文件选择器上传、DownloadManager 下载（带 cookie）
- 启动时拉 `version.json` 自更新
- 返回键菜单：刷新 / 修改地址 / 清除凭据 / 关于

### 构建

```bash
git clone https://github.com/MrLukezy/dsh-mobile.git
cd dsh-mobile
export ANDROID_HOME=$HOME/android-sdk
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
./gradlew assembleRelease       # app/build/outputs/apk/release/app-release.apk
```

三个常量在 `MainActivity.java` 顶部：

```java
private static final String DEFAULT_URL = "https://dsh.example.com/dsh/";
private static final String VERSION_CHECK_URL = "https://dsh.example.com/dsh-apk/mobile/version.json";
private static final int VERSION_CODE = 2;
```

### 发布

```bash
sudo cp app-release.apk /var/www/dsh-apk/mobile/dsh-mobile.apk
sha=$(sha256sum /var/www/dsh-apk/mobile/dsh-mobile.apk | cut -d' ' -f1)
size=$(stat -c%s /var/www/dsh-apk/mobile/dsh-mobile.apk)

sudo tee /var/www/dsh-apk/mobile/version.json >/dev/null <<EOF
{ "versionCode": 3, "versionName": "1.1.0",
  "url": "https://dsh.example.com/dsh-apk/mobile/dsh-mobile.apk",
  "sha256": "$sha", "size": $size, "notes": "更新说明" }
EOF
```

`versionCode` 要同时改 `app/build.gradle` 和 `MainActivity.VERSION_CODE`。

### 首次登录

1. 手机打开下载页安装 APK
2. 返回键 → 修改地址 → `https://dsh.example.com/dsh/`
   （服务器首次启动时先粘一次带 token 的地址，让 cookie 落地）
3. 登录墙弹框填账号密码，填一次记住
4. 换账号：返回键 → 清除登录凭据

---

## 9. 为什么不用重装

| 改动 | 手机端 | 操作 |
|---|---|---|
| DSH 升级 | 刷新页面 | 无 |
| 装/卸插件 | 刷新页面 | 服务端重启 dsh + 手机下拉刷新 |
| 主题/布局 | 即时 | 无 |
| 会话/工作区 | 实时 | 无 |
| 外壳加功能 | version.json 提示 | 发新版 APK |

机制：入口 HTML 内嵌 bundle rev；资源 URL 带内容哈希，内容变 URL 就变。
所以 `/assets/`、`/plugins/` 可以一年缓存，而 `/dsh/` 必须 no-cache。

---

## 10. 排错

| 现象 | 原因 | 修复 |
|---|---|---|
| 登录怎么提交都回登录页 | 网关 upstream 未配置 | 补 upstreamHost/Port |
| 选目录报 transport failure | Host/Origin 与 --trusted-host 不一致 | 域名、域名:端口都加 |
| 白屏 + 404 | 只代理 /dsh/ | 补 /assets/ /plugins/ /api |
| 升级后界面没变 | 入口 HTML 被缓存 | no-cache |
| 回复整段蹦 | nginx 缓冲 SSE | proxy_buffering off |
| 上传 PDF 失败 | 请求体超限 | client_max_body_size 50m |
| 统计条整行消失 | 插件引用不存在图标 → slot crash | 图标 fallback |
| App 报 ERR_TOO_MANY_RETRIES | auth handler 先 cancel 再 proceed | 一次性 handler 直接 proceed（1.0.1 已修） |
| APK 一直提示升级 | versionCode 只改一处 | build.gradle + MainActivity |
| 手机连 HTTP 失败 | Android 9+ 禁明文 | 上 HTTPS |

三个万能命令：

```bash
systemctl --user status dsh --no-pager | head -12
curl -sI https://dsh.example.com/dsh/ | head -4
ss -lntp | grep -E '13080|3080'
```

---

## 11. 仓库

- 壳：https://github.com/MrLukezy/dsh-mobile （私有）
- 网关：https://github.com/MrLukezy/dsh-cloud-gateway
- 手机上传：https://github.com/MrLukezy/dsh-mobile-files
- 余额面板：https://github.com/MrLukezy/dsh-balance
- Windows 隧道：https://github.com/MrLukezy/dsh-win-tunnel
- 一键复刻 Skill：https://github.com/MrLukezy/dsh-mobile-cloud-deploy
- 竖屏插件（社区）：https://github.com/mexiaosqwq/dsh-web-mobile

---

## 12. 常见问题

**一定要云服务器吗？** 不用。家里 Windows 可以用 dsh-win-tunnel 反向隧道到公网，代价是电脑得开着。

**能不加登录墙吗？** 不建议。最少也要 basic auth + IP 白名单。

**APK 收集数据吗？** 不。没有统计 SDK，只做 WebView 和一次版本检查。

**换手机要重配吗？** 要重装、重填地址密码；会话都在服务器上。

**多级反代？** 每层外层域名都进 `--trusted-host`，网关开 `trustProxy`，Host 用 `$http_host`（别用 `$host`，会丢端口）。
