DSH 云部署 × 手机 APK 快速开始 →
实战记录 · 已上线运行

一台云服务器,
让手机也变成 DeepSeek Harness 工作台

从空的 Ubuntu 开始:装 DSH、加一层账号密码的登录墙、用 nginx 反代到公网, 再配一个 19 KB 的安卓 WebView 外壳。之后你升级 DSH、装插件、改主题, 手机端下拉刷新就同步——APK 永远不用重装。

Ubuntu 24.04 DSH 0.2.1-rc.2 Node 22.23 nginx 1.24 APK 19 KB
DSH Mobile 下载页
手机打开下载页 · 390 × 844
3条命令跑起服务端
19 KB安卓外壳体积
0日常升级要重装的次数
2 层热更:页面 + 外壳

文中所有命令、路径、端口都来自这台正在运行的服务器,不是示意代码。

0030 秒看懂

如果你只想复制粘贴,把下面三步跑完,再跳到「首次登录」。

①

服务器上跑起 dsh

只监听 127.0.0.1:13080,不直接暴露公网。

②

加一层带密码的入口

dsh-cloud-gateway 监听 3080,路径 /dsh,登录后再反代回本机。

③

nginx 终结 TLS

443 对外,WebSocket 长连接,静态资源可长缓存、入口一律不缓存。

④

手机装 19 KB 外壳

WebView 直接加载同一个网址,服务器变它就变。

最短路径 · 复制即用
# 1) 安装(Node 22 环境)
npm i -g @deepseek-ai/dsh

# 2) 首次启动,日志里会打印带 token 的地址
dsh web --host 127.0.0.1 --port 13080 --trusted-host dsh.example.com

# 3) 装网关 + 手机适配插件
dsh plugin --profile web add github:MrLukezy/dsh-cloud-gateway
dsh plugin --profile web add github:mexiaosqwq/dsh-web-mobile

# 4) nginx 反代到 3080,浏览器打开 https://dsh.example.com/dsh/
ℹ️

为什么不让 dsh web --host 0.0.0.0 直接对外?因为官方 Web 只有 launch token,没有账号体系、没有失败限速。公网裸奔等于把整台机器的文件系统交出去。

01整体架构

四层,每一层只做一件事。手机和电脑走的是同一条链路,所以看到的内容天然一致。

手机 App WebView · 19 KB 电脑浏览器 同一套页面 nginx :443 · TLS WebSocket 缓存策略分流 dsh-cloud-gateway :3080 · /dsh 账号 + 密码 登录墙 / 会话 cookie dsh web 127.0.0.1:13080 会话 / 工作区 插件 bundle 文件读写 / 终端 只绑回环,永不直连公网 HTTP loopback 插件:网关 / 上传 / 余额 / 主题 可选:Windows 隧道 8443 dsh-win-tunnel

一个入口

只有 /dsh 这一个公网前缀,assets、plugins、api 全部挂在它后面,少一个都白屏。

两端同源

手机和电脑打开的是同一个地址,插件与主题不存在“手机版另做一套”。

网关不碰业务

只做登录和转发,DSH 升级、插件替换都不需要改它。

回环兜底

13080 永远只绑 127.0.0.1;就算 nginx 配错,也不会把裸服务暴露出去。

02准备清单

一台 2C2G 的云服务器就够(本机 DSH 常驻内存约 600 MB)。

项目要求说明
系统Ubuntu 22.04 / 24.04本文在 24.04 上验证
Node22.xnvm 或 NodeSource 均可
域名一个 A 记录指向服务器HTTPS 需要,手机端也更省心
端口80 / 443 放行8443 仅在用 Windows 隧道时需要
软件nginx、certbotapt install nginx certbot python3-certbot-nginx
内存≥ 1 GB跑 DSH + nginx 实测 600–800 MB
⚠️

先想清楚谁能进。DSH 能读写服务器文件、执行命令,等价于一个带 shell 的账号。账号密码请用密码管理器生成,不要和别的服务复用。

03安装 DSH

官方包名 @deepseek-ai/dsh,全局装一份即可。

1

装 Node 22

bash
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs
node -v   # v22.x
2

装 DSH 并确认版本

bash
sudo npm i -g @deepseek-ai/dsh
dsh --version   # 0.2.1-rc.2
3

第一次启动,记下 token 地址

前台跑一次,日志里会打印带 token 的本地地址。这个 token 是官方 Web 的唯一凭据,第一次进页面必须带它,之后浏览器会种下 cookie。

bash
dsh web --host 127.0.0.1 --port 13080 --trusted-host dsh.example.com

# 输出示例:
#   dsh web: http://127.0.0.1:13080/?token=xxxxxxxxxxxxxxxx
ℹ️

--trusted-host 不是可选项。DSH 会用 Host / Origin 做同源校验,域名没登记就会出现“页面能开、一选目录就 403”的怪事。域名带端口时,带端口和不带端口要各写一条。

04常驻 + 开机自启

用 systemd 用户服务,不占 root,日志走 journald。

~/.config/systemd/user/dsh.service
[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   # 关键:不 enable-linger,退出 SSH 服务会被杀

systemctl --user status dsh --no-pager
journalctl --user -u dsh -f    # 实时日志
⚠️

DSH_HOME 一旦定下就别搬。配置、插件、会话全在里面,挪目录会同时断掉 node_modules 里的绝对路径软链。

📁

上面的 /home/ubuntu 是本机路径。换一台机器部署时,把它换成你的用户目录,PATH 里的 Node 路径也要跟着改(用 which node 查)。

05加一层登录网关

dsh-cloud-gateway 在 3080 开一个带账号密码的入口,验证通过后反代到 13080。

bash
dsh plugin --profile web add github:MrLukezy/dsh-cloud-gateway
dsh plugin --profile web add github:mexiaosqwq/dsh-web-mobile

# 装完重启一次,让插件 bundle 生效
systemctl --user restart dsh

写配置

在 ~/.dsh/profiles/web/cordis.patch.yml 追加下面这段(没有就新建)。这里的值只存在你自己的机器上。

~/.dsh/profiles/web/cordis.patch.yml
- id: cloud-gateway
  config:
    listenHost: 0.0.0.0
    listenPort: 3080
    # ★ 必须显式写上游。0.2.x 里 ctx.webStartup.port 拿不到真实端口,
    #   不写会回落到 3080(网关自己),转发打回自己 → 永远登录失败。
    upstreamHost: 127.0.0.1
    upstreamPort: 13080
    basePath: /dsh
    username: your-name
    password: your-password
    trustProxy: true
🚫

原文照抄最常见的坑:漏了 upstreamHost / upstreamPort。表现是登录框怎么提交都回到登录页,nginx 日志里全是 127.0.0.1:3080 自己打自己。

改完重启,确认 3080 在监听:

bash
systemctl --user restart dsh
ss -lntp | grep -E '13080|3080'
# 期望看到 0.0.0.0:3080 和 127.0.0.1:13080

curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3080/dsh/
# 302(跳登录页)说明网关活着
https://你的域名/dsh/
DSH 登录墙页面
登录墙长这样:只有账号、密码,没有第三方登录,也没有注册入口。

06nginx 反向代理

这一段决定“能不能用”:WebSocket、缓存、Host 头、上传大小,四个都得对。

/etc/nginx/sites-enabled/dsh.conf
# WebSocket 升级(必须放在 http 上下文,站点文件就是)
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;        # 上传 PDF / 大文件

    # ① 前端构建产物:文件名内嵌内容哈希,可以缓存一年
    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";
    }

    # ② 插件 bundle:同样带哈希,同样长缓存
    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";
    }

    # ③ 入口 HTML / 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;
    }

    # ④ APK 与自更新清单
    location /dsh-apk/ {
        alias /var/www/dsh-apk/;
        add_header Cache-Control "no-store";
    }
}

为什么必须四个前缀都代理

DSH 的入口 HTML 在 /dsh/ 下,但它内嵌的 boot manifest 里,资源 URL 是根绝对路径:/assets/…、/plugins/…。只代理 /dsh/ 的典型现象是:登录进去了,页面一片白,控制台一堆 404。

bash · 校验并重载
sudo nginx -t && sudo systemctl reload nginx
curl -sI https://dsh.example.com/dsh/ | head -5
ℹ️

入口 HTML 一定要 no-cache。它里面写死了当前 bundle 的 rev;被 CDN 或浏览器缓存住,升级后就会一直加载旧 bundle,表现为“服务端明明更新了,界面还是老样子”。

07域名与 HTTPS

手机端尤其需要 TLS:安卓 9 起默认禁止明文 HTTP,自签证书还要额外放行。

bash
# A 记录生效后
sudo certbot --nginx -d dsh.example.com
sudo certbot renew --dry-run   # 确认自动续期可用

如果你不想用域名,也可以直接对 IP 开放,但 HTTPS 就没有了,手机壳那边只能走明文或信任自签证书——体验和安全性都差一截,不推荐。

08验收检查

逐条过一遍,任何一条不对,先别急着装手机端。

检查期望不对时看哪里
ss -lntp | grep 13080只有 127.0.0.1启动参数 --host
curl -sI …/dsh/302 到 /dsh/loginnginx location、网关端口
浏览器登录后进入会话列表cookie 域、trustProxy
发一条消息流式逐字返回proxy_buffering off
DevTools 控制台无 404 资源/assets/ /plugins/ 代理
选一个工作目录不报 transport failure--trusted-host 是否带端口
笔记本 · 1440×900
DSH 桌面端界面
登录后的桌面端:会话、工作区、插件入口都在同一页。
手机 · 390×844
DSH 手机端界面
同一个地址在手机上(装了竖屏插件):单栏 + 抽屉。

09手机端:19 KB 的外壳

它不打包网页,只打包一个 WebView。所有内容实时来自服务器。

纯 WebView

不用 AndroidX、不引第三方库,APK 只有 19 KB,装完几乎不占空间。

登录墙记忆

第一次输入 basic auth 后存本地,之后不再弹框;菜单里可一键清除。

上传 / 下载

文件选择器接系统,下载走 DownloadManager,带 cookie 一起发。

自动更新

启动时拉 version.json,版本号更大才提示,日常不打扰。

✅

下载地址:https://www.lukezystudio.cn/dsh-apk/mobile/(页面会实时读 version.json,显示版本、体积、SHA256)

同一套页面,手机上自动变单栏

壳里没有第二套界面。窄屏布局由插件接管:侧栏收进抽屉、会话占满宽度、输入区贴底并避开刘海。所以电脑上装了什么插件,手机上就是什么插件。

  • 竖屏适配:dsh-web-mobile(抽屉 + 全宽会话)
  • 登录态与电脑共用同一份 cookie,不用重复授权
  • 窗口尺寸一变就重排,没有「手机版」的独立代码

它和服务器是怎么连上的

App 启动读取上次保存的地址,默认指向你的域名
→
WebView 加载 /dsh/遇到登录墙弹一次框,凭据存本地
之后每次打开cookie 直接复用,2 秒进工作台
⇄
服务器更新下拉刷新即同步,APK 不动

10构建与签名

需要 JDK 17 + Android SDK(platform 34)。源码就一个 Activity,逻辑全在 MainActivity.java。

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(约 19 KB)

三个要改的常量

都写在 MainActivity.java 顶部,换域名或换更新地址只改这里:

MainActivity.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";

/** 必须和 version.json 的 versionCode 对齐 */
private static final int VERSION_CODE = 2;
⚠️

versionCode 要同时改 两处:app/build.gradle(安卓系统的版本号)和 MainActivity.VERSION_CODE(自更新比较用)。只改一处,要么装不上,要么永远提示更新。

签名

用自己的 keystore,保存好并且不要提交到公开仓库。升级时签名必须一致,否则安卓会拒绝覆盖安装,只能卸载重装(本地数据全丢)。

bash
keytool -genkeypair -v -keystore keystore/dsh-mobile.jks \
  -alias dsh -keyalg RSA -keysize 2048 -validity 10000

11发布与自更新

把 APK 和一份清单丢到静态目录,App 下次启动就知道有没有新版。

bash · 发布
sudo cp app/build/outputs/apk/release/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 内的大
url点击“下载”跳转的地址绝对地址,https
sha256下载页展示、人工校验改包就要重算
size下载页显示体积字节数
notes升级弹窗正文写人话,一行就够
ℹ️

日常更新 DSH(升级版本、装插件、换主题)完全不需要动 APK。只有“壳”的能力变了——比如新增了下载、上传、菜单项——才需要发新版本。

12首次登录

安卓壳第一次进,需要把“地址”和“凭据”各教一次,之后全自动。

1

下载并安装 APK

用手机浏览器打开 下载页,允许“安装未知来源应用”。

2

返回键 → 修改地址

填你的入口地址,例如 https://dsh.example.com/dsh/。如果服务器是首次启动,先粘一次带 token 的完整地址,让 cookie 种下来,再换回普通地址。

3

登录墙弹框 → 保存

填网关的账号密码。填一次就记住;要换账号,走 返回键 → 清除登录凭据。

4

把它当网页用

下拉刷新、长按选择、附件上传都正常。返回键在页面内是“后退”,到底了再按一次是“呼出菜单”,连按两次退出——和浏览器习惯一致。

13为什么不用重装

两层热更:页面层几乎是实时的,外壳层只在必要时动。

你改了什么手机端怎么拿到要做什么
DSH 升级刷新页面无
装 / 卸插件刷新页面服务端重启 dsh,手机下拉刷新
主题、字号、布局即时无
会话与工作区实时无
外壳加功能version.json 提示发新版 APK,用户点一次升级
服务器:改了插件bundle 内容哈希变化,rev 更新
→
手机:下拉刷新拿到新 index.html 与 rev,加载新 bundle

这也是为什么 nginx 上 /assets/ 和 /plugins/ 敢设一年缓存:它们的 URL 里带内容哈希,内容一变 URL 就变;而入口 HTML 必须 no-cache,否则新 rev 永远传不下去。

14排错手册

下面每一条都是这台机器上真实踩过的,不是假设。

现象真正原因处理
登录怎么提交都回登录页 网关 upstream 没写,转发打回自己 补 upstreamHost/Port,重启
能进页面,选目录报 transport failure Host/Origin 与 --trusted-host 不一致 把域名、域名:端口都加进去
白屏,控制台 404 只代理了 /dsh/ 补 /assets/、/plugins/、/api
升级后界面没变 入口 HTML 被缓存 /dsh/ 加 Cache-Control: no-cache
回复整段蹦出来,不流式 nginx 缓冲了 SSE proxy_buffering off
上传 PDF 失败 请求体超限 client_max_body_size 50m
统计条整行消失 插件引用了不存在的图标,宿主判定 slot crash 给图标加 fallback,见下
App 报 ERR_TOO_MANY_RETRIES 登录弹框先 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

# 链路通吗(登录前应是 302)
curl -sI https://dsh.example.com/dsh/ | head -4

# 网关和 dsh 各自在听吗
ss -lntp | grep -E '13080|3080'
🧩

插件把整行 UI 弄没了怎么查:宿主对插件崩溃是“整槽位静默移除”。打开 DevTools,找 slot entry crashed,再看插件 client.js 里 import 的图标名是否在当前 DSH 版本里存在——名字对不上就是 React #130,服务端接口却完全正常,很容易误判。

15常见问题

一定要云服务器吗?家里电脑行不行

行。用 dsh-win-tunnel 把 Windows 上的 DSH 通过 SSH 反向隧道暴露出去,手机照样能用,只是电脑得开着。

能不能只给自己用,不加登录墙

可以,但不建议。至少加 nginx basic auth + IP 白名单,否则等于把 shell 挂公网。网关的优势是能自己改密码、退出登录、看在线状态。

APK 会收集数据吗

不会。它没有联网 SDK、没有统计,只做 WebView 加载和一次版本检查。源码只有一个 Activity,可以自己审。

换手机要重新配吗

要重装、重填一次地址和密码;会话和历史都在服务器上,换设备不影响。

多个域名 / 反代套反代

把每一层的外层域名都写进 --trusted-host,并在网关开启 trustProxy。Host 头必须原样透传(用 $http_host,别用 $host,后者会丢端口)。

16GitHub 仓库

这套东西不是一个仓库,是“壳 + 网关 + 插件”的组合。

安卓 WebView 外壳:菜单、上传、下载、自更新
私有
登录墙 + 反向代理,公网入口就靠它
公开
手机端上传文件到工作区的紧凑按钮
公开
余额与套餐额度面板(DeepSeek / NewAPI / OpenCode Go)
公开
Windows 本机 DSH 经 SSH 反向隧道出公网(8443)
公开
把上面整套流程写成 AI Skill,一句话复刻
公开
液态玻璃主题
公开
生图插件(OpenAI 兼容网关)
公开
社区竖屏适配插件(窄屏抽屉、全宽会话)
公开
ℹ️

主仓库 dsh-mobile 目前是私有的。想直接拿 APK 用,走上面的下载页;想要源码,仓库地址就是 github.com/MrLukezy 下的同名项目。

17给 AI 读的版本

人和模型看同一页,但模型更适合下面这种结构化摘要;也可以直接拉纯文本。

MACHINE-READABLE ENDPOINTS

同一份内容的纯文本与结构化版本,方便 agent 抓取、diff、复述:

facts.json · 关键事实
{
  "service": {
    "name": "DeepSeek Harness (dsh web)",
    "version": "0.2.1-rc.2",
    "bind": "127.0.0.1:13080",
    "home": "/home/ubuntu/.dsh",
    "unit": "dsh.service (systemd --user, linger enabled)"
  },
  "gateway": {
    "plugin": "dsh-cloud-gateway",
    "listen": "0.0.0.0:3080",
    "basePath": "/dsh",
    "upstream": "http://127.0.0.1:13080",
    "auth": "form login → session cookie"
  },
  "edge": {
    "server": "nginx 1.24 (Ubuntu)",
    "tls": "Let's Encrypt, 443",
    "proxyPrefixes": ["/dsh/", "/assets/", "/plugins/", "/api"],
    "cache": "hashed assets immutable; entry html no-cache"
  },
  "client": {
    "apk": "https://www.lukezystudio.cn/dsh-apk/mobile/dsh-mobile.apk",
    "size": 19344,
    "versionCode": 2,
    "manifest": "https://www.lukezystudio.cn/dsh-apk/mobile/version.json",
    "runtime": "WebView shell, no bundled assets"
  },
  "repos": [
    "github.com/MrLukezy/dsh-mobile",
    "github.com/MrLukezy/dsh-cloud-gateway",
    "github.com/MrLukezy/dsh-mobile-files",
    "github.com/MrLukezy/dsh-win-tunnel"
  ],
  "checks": [
    "systemctl --user status dsh",
    "curl -sI https://HOST/dsh/  # expect 302",
    "ss -lntp | grep -E '13080|3080'"
  ]
}

给 agent 的建议:先读 facts.json 建立事实,再用 tutorial.md 取步骤,需要原文时回来看这一页的排错表。