> 适用场景：你有一台服务器，想把 DeepSeek Harness 的浏览器界面（Web GUI）跑在上面，让家里的电脑、公司的电脑、甚至手机浏览器都能登录使用，用 AI 帮你写代码、跑任务。本文基于 DSH `0.1.0-rc.6` + Ubuntu 22.04/24.04 + nginx + pm2 的实际部署经验整理。

## 为什么需要服务器 + 反代

DSH 的 Web 界面（`dsh --profile web`）默认只监听 `127.0.0.1:3080`，而且出于安全考虑**禁止 `--host 0.0.0.0`**（CLI 直接报错：防止把远程代码执行能力暴露到公网）。所以正确姿势是：

```
浏览器（手机/电脑）──► nginx（80/443）──► dsh web（127.0.0.1:3080）
```

nginx 做反向代理 + 登录保护 + 移动端适配，DSH 本身只在本机回环地址提供服务。

---

## 架构总览


- dsh web：实际服务，监听 `127.0.0.1:3080` 
- dsh-webui-auth 插件： 登录认证（未认证的浏览器拿不到任何资源） 
- pm2： 守护进程 + 开机自启 
- nginx： 对外入口，反代 + 移动端缩放 + manifest 直出 

---

## 第一步：安装 DSH

需要 Node.js `^22.19.0 || >=24.0.0`：

```sh
npm install -g @deepseek-ai/dsh
dsh --version   # 验证安装
```

## 第二步：初始化 web profile 并安装认证插件

`dsh --profile web` 首次运行时自动初始化 profile（目录在 `~/.dsh/profiles/web`）。先装登录认证插件（需要 pnpm，Node 自带 corepack）：

```sh
corepack enable pnpm
npx @deepseek-ai/dsh plugin --profile web add dsh-webui-auth
```

之后启动 web 服务，首次访问登录页时创建你的账号密码（存在 `~/.dsh/profiles/web/node_modules/dsh-webui-auth/dsh-webui-auth.json`，密码是 scrypt 哈希）：

```sh
dsh --profile web
```

### 补充：固定目录选择器为浏览器模式（服务器远程访问必做）

创建 `~/.dsh/patch.yml`：

```yaml
# 固定目录选择器为「浏览器浏览」模式。
# 默认的 directory-picker-auto 会根据服务器环境自动选择：
#   - native：系统目录对话框，需要在服务器本机的显示器上操作；
#   - browse：浏览器内的目录列表（可浏览/新建子目录）。
# 服务器通过 nginx 反代远程访问时，native 对话框会弹在服务器自己的屏幕上，
# 远程的手机/电脑根本看不到，必须固定为 browse。
# 注意：patch 无法直接改已有行的插件 name，所以先禁用 auto 行，
# 再插入 browse 的主机端 + 客户端插件对。

- id: directory-picker
  disabled: true

- insert:
    - id: directory-picker-browse
      name: '@deepseek-ai/dsh-host-directory-picker-browse'
    - id: directory-picker-browse-ui
      name: '@deepseek-ai/dsh-client-ui-directory-picker-browse'
```

启动时用 `--patch` 挂上（见第四步的启动参数）。如果服务器上确实没有
DISPLAY/chooser 环境，auto 也会选 browse，但显式固定可以保证任何环境下行为一致。

## 第三步：配置 API Key

在 `~/.dsh/.credentials.yaml` 写入：

```yaml
DEEPSEEK_API_KEY: sk-你的密钥
```

（或通过环境变量 `DEEPSEEK_API_KEY` 注入，二选一即可。）

## 第四步：pm2 守护 + 开机自启

写 pm2 配置 `~/.dsh/pm2.json`：

```json
{
  "apps": [
    {
      "name": "dsh-web",
      "script": "/usr/local/bin/dsh",
      "args": "--profile web --patch /root/.dsh/patch.yml --trusted-host 你的域名",
      "autorestart": true,
      "restart_delay": 3000
    }
  ]
}
```

> `--trusted-host 你的域名` 必须加：Web 界面调用 `/api` 时有浏览器信任围栏，通过域名访问时 Host 不再是 `127.0.0.1:3080`，不加这个参数 API 会被拒。

启动并设置开机自启：

```sh
pm2 start ~/.dsh/pm2.json
pm2 save
pm2 startup systemd -u root --hp /root   # 按提示执行输出的命令
```

## 第五步：nginx 反向代理

新建 `/etc/nginx/sites-available/dsh.apple.me`（域名换成你的），软链到 `sites-enabled` 并 reload：

```sh
ln -s /etc/nginx/sites-available/dsh.apple.me /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
```

下面这份是**完整最终配置**，包含两个必踩的坑的解法（见注释），gzip/缓存为可选优化：

```nginx
server {
    listen 80;
    server_name dsh.apple.me;

    # ── 可选：gzip 压缩（纯反代站点，上游不压缩，统一由 nginx 压）──
    gzip on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_min_length 1k;
    gzip_vary on;   # 必须：与下面的强缓存配合，区分压缩/未压缩变体
    gzip_types text/plain text/css application/json application/javascript
               application/x-javascript text/javascript application/xml
               application/xml+rss image/svg+xml application/manifest+json;

    # ── 可选：静态资源缓存（Vite 产物带内容哈希，可放心长缓存）──
    location ~* ^/assets/.*\.(js|css|woff2?|ttf|eot|svg|png|jpe?g|gif|ico|webp|json|map)$ {
        proxy_pass http://127.0.0.1:3080;
        proxy_http_version 1.1;
        proxy_set_header Host 127.0.0.1:3080;
        add_header Cache-Control "public, max-age=31536000, immutable";
    }

    # ── 坑 2 解法：manifest 由 nginx 直接输出（见下文详解）──
    location = /manifest.webmanifest {
        default_type application/manifest+json;
        return 200 '{
  "id": "/",
  "name": "DeepSeek Harness",
  "short_name": "DSH",
  "start_url": "/",
  "scope": "/",
  "display": "standalone",
  "icons": [
    {
      "src": "/favicon.svg",
      "sizes": "any",
      "type": "image/svg+xml",
      "purpose": "any"
    }
  ]
}';
    }

    # ── 主入口：反代 + 坑 1 解法（移动端缩放）──
    location / {
        proxy_pass http://127.0.0.1:3080;
        proxy_http_version 1.1;
        proxy_set_header Host 127.0.0.1:3080;
        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_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";   # WebSocket/SSE 长连接
        proxy_read_timeout 86400s;
        proxy_send_timeout 86400s;

        # ── 坑 1 解法：手机端 viewport 缩放 ──
        proxy_set_header Accept-Encoding "";     # 必须：让 sub_filter 拿到未压缩 body
        sub_filter 'name="viewport" content="width=device-width, initial-scale=1"'
                   'name="viewport" content="width=device-width, initial-scale=0.7"';
        sub_filter_once on;
    }
}
```

---

## 坑 1：手机浏览器内容区域太小

**现象**：手机上打开 Web 界面， 工作区域宽度太窄，设置页面几乎没法用。

**原因**：页面 HTML 里的 viewport meta 是 `initial-scale=1`， 文字太大。

**解法**：在 nginx 上用 `sub_filter` 把响应里的 viewport 改成 `0.7`，让页面在手机屏上缩小显示。两个要点：

1. 必须先 `proxy_set_header Accept-Encoding "";` 把发给上游的压缩请求头置空，否则上游返回 gzip 压缩的 body，`sub_filter` 的字符串替换会失效；
2. `sub_filter` 默认只处理 `text/html`，HTML 页面正好符合。

改完 `nginx -t && systemctl reload nginx`，手机**硬刷新**（清缓存）后就能看到效果。

---

## 坑 2：manifest.webmanifest 请求永远是 302，PWA 装不上

**现象**：浏览器加载 `<link rel="manifest">` 时请求 `/manifest.webmanifest` 一直被重定向到登录页（302），DevTools 里 manifest 解析失败，手机"添加到主屏幕"不可用，`display` 模式也控制不了。

**原因**：`dsh-webui-auth` 插件在 HTTP 层拦截了**所有**未认证请求（包括静态资源）并 302 到登录页。而浏览器拉取 manifest 的请求不会携带登录 Cookie，于是永远 302，拿不到 manifest 内容。

**解法**：让 nginx 绕过认证，**直接输出**这份 manifest 文件——内容照抄 DSH dist 里的 `manifest.webmanifest`，并把 `"display": "fullscreen"` 改成 `"standalone"`（standalone 是"普通应用窗口"，手机 PWA 更合适；fullscreen 会全屏隐藏浏览器 UI）。

要点：

1. 用 `location = ` 精确匹配，只接管这一个路径，不影响其他资源；
2. 内容**硬编码**在 nginx 里，以后 DSH 升级如果改了 manifest 结构需要手动同步；
3. 不用改 DSH 源码，也不用给认证插件开白名单，改动全部在 nginx 层，升级 DSH 不会丢。

---

## 访问与验证

- 电脑：浏览器打开 `http://你的域名`，登录后正常使用；
- 手机：同样地址登录。验证 PWA：Chrome 菜单 →「添加到主屏幕」；DevTools → Application → Manifest，`display` 应为 `standalone`；
- 如果手机或电脑上看到的是旧页面，记得硬刷新清缓存。

## 常见问题


1. 想用 HTTPS?
> 用 certbot 申请证书，`listen 443 ssl`，并把 80 重定向到 443（注意 443 的 location 也要带上同样的 sub_filter 和 manifest 直出配置）
2. 改完 nginx 不生效?
> `nginx -t` 先查语法，再 `systemctl reload nginx`，浏览器硬刷新
3. 远程访问时"选择目录"没反应，或对话框弹在服务器本机屏幕上?
> 检查启动参数里是否带了 `--patch /root/.dsh/patch.yml` 固定 browse 目录选择器；不固定时 auto 可能选中 native（系统对话框弹在服务器屏幕上，远程用户看不到也点不了）
