在云服务器部署 DeepSeek Harness:从安装到 HTTPS 访问

为什么把 DSH 放到云服务器上
DeepSeek Harness,简称 DSH,可以读取项目文件、执行命令,并辅助完成开发任务。我把它部署在云服务器上,是为了通过浏览器访问固定的工作区:无论换到哪台电脑,都能继续处理同一份项目。
这篇文章记录我在 Debian + 1Panel + OpenResty 环境中的部署过程。完成后,便可以通过 HTTPS 域名访问 DSH,登录、选择工作区和模型,再开始会话。

部署分为几步:安装并启动 DSH,通过终端配置 API Key,设置域名和 HTTPS 反代,完成外层认证与 cookie 自动同步,最后检查 WebSocket 和模型回复。常见问题放在文末,方便遇到故障时按现象查找。
1. 环境与部署架构
| 项目 | 本文环境 |
|---|---|
| 系统 | Debian GNU/Linux 12(bookworm) |
| 服务器资源 | 4 vCPU、3.7 GiB 内存、4 GiB swap |
| 面板 | 1Panel 社区版 v2.0.2(stable) |
| 反向代理 | OpenResty 1.31.1.1,Docker host 网络;镜像 1panel/openresty<1>1>.31.1.1-2-4-noble |
| DSH / Node | DSH 0.2.0-rc.2 / Node v22.23.2(/usr/bin/node) |
| DSH 监听地址 | 127.0.0.1<3080>3080> |
| 示例域名 | dsh.example.com,请替换为自己的域名 |
| HTTPS 证书 | 通过面板申请并配置 |
以上是我在 2026-10-01 核对的实际环境。下文使用独立的 dsh 用户和示例目录;我的现有实例则把凭据保存在 /root/.dsh。已有服务应沿用实际配置,迁移前先备份凭据和工作区。
这里调用远程模型 API,云服务器负责运行 DSH 及工作区任务。资源需求取决于任务内容,例如安装依赖、构建项目或转换文件。
本文的回环上游地址依赖 OpenResty 使用 host 网络。 如果容器采用 bridge 网络,容器内的 127.0.0.1 不指向宿主机,需要调整上游地址。
DSH 目前属于开发者预览软件。本文记录个人部署经验,参数和接口以实际安装版本为准。由于 DSH 能执行工作区命令,建议使用专门的运行用户和项目目录。官方项目 · 官方安全说明
2. 安装 Node 与 DSH
2.1 确认 Node 环境与 DSH 版本
以下操作在服务器宿主机的 SSH 终端中完成。先查看现有 Node 环境和 DSH 可用版本:
node -vnpm -vnpm view @deepseek-ai/dsh dist-tags选定 DSH 版本后,查询它要求的 Node 版本:
DSH_VERSION='替换为目标版本号'npm view "@deepseek-ai/dsh@$DSH_VERSION" engines --json如果尚未安装 Node,按 Node 官方安装说明 安装满足要求的版本。使用下载镜像时,核对版本、架构和安装位置。
2.2 安装并确认入口路径
sudo npm install -g "@deepseek-ai/dsh@$DSH_VERSION" \ --no-audit --no-fund
command -v nodecommand -v dshnpm root -gdsh --versiondsh web --help安装耗时取决于服务器资源和网络。官方 registry 无法访问时,可以用 --registry 指定可用镜像;出现 ETARGET 时,先检查包名、版本号,以及该 registry 是否已收录目标版本。
如果希望在后台安装并持续查看日志,可以使用:
# 将引号内的版本号替换为已经确认存在的版本。sudo sh -c 'nohup nice -n 10 ionice -c3 npm install -g \ "@deepseek-ai/dsh@替换为目标版本号" --no-audit --no-fund \ > /var/log/dsh-npm-install.log 2>&1 &'tail -f /var/log/dsh-npm-install.log按 Ctrl+C 会退出日志跟踪,后台安装仍会继续。安装完成后,用 dsh --version 确认结果,并记录 command -v dsh 返回的入口路径,后续创建服务时会用到。
3. 用 systemd 托管 DSH
3.1 创建运行用户与工作区
下面使用专门的 dsh 用户,家目录为 /var/lib/dsh,工作区为 /srv/dsh-workspace。如果已有运行用户,沿用它,并同步调整后续路径。
id dsh >/dev/null 2>&1 || sudo useradd \ --system --user-group --create-home --home-dir /var/lib/dsh dshsudo mkdir -p /srv/dsh-workspace /var/lib/dsh/.dshsudo chown dsh:dsh /srv/dsh-workspace /var/lib/dsh /var/lib/dsh/.dsh工作区可以替换为自己的项目目录,运行用户需要具备相应的读写权限。
3.2 创建服务
先通过 dsh web --help 确认当前版本支持下列参数,再创建服务。这里用 DSH_BIN 填入实际安装入口:
DSH_BIN="$(command -v dsh)"test -n "$DSH_BIN" || { echo '未找到 dsh,请先完成安装'; exit 1; }
sudo tee /etc/systemd/system/dsh.service > /dev/null <<EOF[Unit]Description=DeepSeek Harness WebAfter=network-online.targetWants=network-online.target
[Service]Type=simpleUser=dshGroup=dshWorkingDirectory=/srv/dsh-workspaceEnvironment=HOME=/var/lib/dshEnvironment=DSH_HOME=/var/lib/dsh/.dshEnvironment=PATH=/usr/local/bin:/usr/bin:/binEnvironment=NO_COLOR=1ExecStart=$DSH_BIN web --host 127.0.0.1 --port 3080 --trusted-host dsh.example.com --no-openRestart=on-failureRestartSec=5StandardOutput=journalStandardError=journal
[Install]WantedBy=multi-user.targetEOF
sudo systemctl daemon-reloadsudo systemctl enable --now dsh将 dsh.example.com 替换为实际域名。如果 Node 通过 nvm 等用户级工具安装,还要核对目录权限和 PATH,因为 systemd 不会自动加载交互式 shell 配置。
我的实例另设了 MemoryHigh=1536M、MemoryMax=2G。这是该机器的服务资源上限,设置时应给系统和反代留出内存,不能当作 DSH 的最低配置要求。
3.3 检查服务状态
sudo systemctl status dsh --no-pagersudo ss -lntp | grep ':3080'curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3080/
确认状态为 active (running),并且监听地址为 127.0.0.1:3080。本文版本启用了 DSH 自带认证,未登录访问首页返回 401 属于预期行为。至此可以确认服务已启动,模型调用和浏览器连接留到后面验证。
4. 通过终端配置 API Key
在我的远程访问环境中,打开模型设置页会看到:
加载提供商目录失败: settings are unavailable in this browser
因此,我通过 systemd 环境文件配置 API Key。这里记录的是当前实例的实际现象,其他版本和访问环境应以各自测试结果为准。
官方 DeepSeek API Key 插件默认使用 DEEPSEEK_API_KEY 作为凭据引用,启动环境变量的优先级高于 GUI 保存的同名凭据。后续换密钥时,需要更新环境文件并重启服务。官方插件说明 · 官方凭据包说明
4.1 写入环境文件
将 YOUR_API_KEY 换成实际密钥。下面的命令会覆盖环境文件;公开代码或截图时保留占位值。
sudo mkdir -p /etc/dshsudo tee /etc/dsh/env > /dev/null <<'EOF'DEEPSEEK_API_KEY=YOUR_API_KEYEOFsudo chown root:root /etc/dsh/envsudo chmod 600 /etc/dsh/env文件由 root 拥有,权限为 600。系统级 systemd 读取后将变量传给服务进程,DSH 的运行用户无需直接读取它。
4.2 通过 drop-in 注入
sudo mkdir -p /etc/systemd/system/dsh.service.dsudo tee /etc/systemd/system/dsh.service.d/api-key.conf > /dev/null <<'EOF'[Service]EnvironmentFile=-/etc/dsh/envEOF
sudo systemctl daemon-reloadsudo systemctl restart dshsudo systemctl status dsh --no-pager这里沿用实例中的 EnvironmentFile=-/etc/dsh/env。路径前的 - 表示文件缺失时仍允许服务启动,所以还要检查变量是否真正进入进程。
环境文件修改后需要重启 DSH;daemon-reload 只重新加载服务配置,不会更新已运行进程的环境。
4.3 验证注入结果,不显示密钥
PID=$(systemctl show -p MainPID --value dsh)if [ "$PID" -gt 0 ]; then sudo cat "/proc/$PID/environ" \ | tr '\0' '\n' \ | grep -c '^DEEPSEEK_API_KEY=.'else echo 'DSH 未运行,请先检查服务日志'fi输出 1,表示进程环境中存在非空的 DEEPSEEK_API_KEY。密钥是否有效、所选模型是否可用,还要通过实际回复验证。
这里用 sudo cat 读取进程环境,再统计匹配数量。若直接使用 sudo tr ... < /proc/$PID/environ,重定向会先由当前 shell 执行,普通用户可能在 sudo 生效前就遇到权限不足。
使用自定义供应商时,还需配置对应的凭据引用、模型和接口地址;DEEPSEEK_API_KEY 只覆盖使用这一凭据名的配置。
5. 配置域名与 HTTPS
5.1 添加 DNS 记录
以 dsh.example.com 为例,在 example.com 的 DNS 管理页面添加:
| 字段 | 示例 |
|---|---|
| 主机记录 | dsh |
| 记录类型 | A |
| 记录值 | 服务器公网 IPv4 地址 |

截图中的 xxx.<ip>.com 是原始标注,实际应理解为自己的域名,例如 dsh.example.com。服务器 IP 填入“记录值”,无需拼进域名。首次调试可以先直接解析到服务器,减少 CDN 回源配置带来的影响。
5.2 在 1Panel 创建反向代理网站
进入“网站 → 网站 → 创建网站”,绑定实际域名,将反代目标设为 http://127.0.0.1:3080。随后在 HTTPS 配置中申请或选择证书,并启用 HTTPS。

上图展示创建入口,上游、证书和自定义配置在对应站点中设置。云安全组及主机防火墙应放行网站端口,DSH 的 3080 端口继续只监听回环地址。
6. 配置反代:Host 与 WebSocket
DSH 启动参数中的 --trusted-host 应与访问域名一致。反代原样透传浏览器的 Host,能够保留非标准 HTTPS 端口,避免 Host 与 Origin 不一致。
下面是反代的核心配置。请合并到现有站点的 location / 中,并保留面板生成的证书等设置:
location / { proxy_pass http://127.0.0.1:3080; proxy_http_version 1.1;
proxy_set_header Host $http_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_set_header X-Forwarded-Host $http_host;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s; proxy_send_timeout 3600s; proxy_buffering off; proxy_cache off;
client_max_body_size 300m;}Upgrade 和 Connection 用于 WebSocket 升级,读超时设为 3600 秒。Nginx 默认的 WebSocket 空闲超时为 60 秒,指长时间没有上游数据时断开连接;应用心跳也可以维持连接。Nginx WebSocket 文档
300m 是示例附件大小限制,可按需要调整。保存配置时核对面板的 include 规则,避免重复创建 location /,或让同一片段被加载多次。
检查并重载 OpenResty
先用 docker ps 找出本站使用的 OpenResty 容器,将容器名填入 ORESTY:
sudo docker ps --format '{{.Names}}'ORESTY='替换为实际 OpenResty 容器名'sudo docker exec "$ORESTY" /usr/local/openresty/nginx/sbin/nginx -t配置检查通过后,再执行重载:
sudo docker exec "$ORESTY" /usr/local/openresty/nginx/sbin/nginx -s reload7. 配置外层访问认证
本文采用“外层认证 + 服务器注入 DSH cookie”的登录方式。先保护 HTTP 与 WebSocket 入口,再按第 8 节配置自动兑换和同步,浏览器便只需完成外层登录。
7.1 配置 Basic Auth
为了给公网入口增加用户名和密码验证,我在反代中配置了 Basic Auth。浏览器访问时会显示:

Basic Auth 位于反代外层,DSH 的 token/cookie 属于内层认证。第 8 节通过 sync.sh 自动提供内层凭据,两层配合完成登录。
下面使用 openssl passwd -5 生成 SHA-256 crypt 密码记录:
DIR=/opt/1panel/www/sites/dsh.example.comread -rp '用户名: ' AUTH_USERread -rsp '密码: ' AUTH_PASSWORD; echoAUTH_HASH=$(printf '%s' "$AUTH_PASSWORD" | openssl passwd -5 -stdin)printf '%s:%s\n' "$AUTH_USER" "$AUTH_HASH" \ | sudo tee "$DIR/htpasswd" > /dev/nullunset AUTH_PASSWORD AUTH_HASH密码文件应放在无法通过网站直接下载的位置,并设置为 OpenResty 运行用户可读。接着在 location / 中加入:
auth_basic "DSH";auth_basic_user_file /www/sites/dsh.example.com/htpasswd;本文宿主机路径为 /opt/1panel/...,容器内对应路径为 /www/...。请根据自己的挂载关系填写 auth_basic_user_file。
密码工具的参数应按对应工具理解。例如,OpenSSL 的 -5 指 SHA-256 crypt;传统 DES crypt 的 8 位密码限制不适用于 apr1。Apache 密码工具文档
完成第 8 节的自动登录配置后,再检查 WebSocket。如果页面正常、WS 却返回 401,应核对 WS 入口的外层认证。sync.sh 只提供 DSH 内层凭据,独立的 WS location 也需要有效的外层访问控制。
7.2 WebSocket 通行证:一次认证绕过与修复
我的实例为 /api/remote.mux 配置了精确匹配的 location:普通页面使用 Basic Auth,WS 入口则检查静态 dshgate cookie,通过后再注入 DSH 会话 cookie。因此,dshgate 本身也是访问凭据,持有它就能建立新连接。
检查时发现,发放通行证的 Set-Cookie 指令带有 always。匿名请求虽然收到 401,响应仍发出了 dshgate;仅带这个值的 WS 握手随后得到 101,绕过了 Basic Auth。
修复时,我去掉了这条 Set-Cookie 的 always,保留 HSTS 的 always,并同时轮换通行证的发放值和比较值。下面只列相关配置行,部署时需合并到实际 location:
# location / 内;继续保留 auth_basic 和 auth_basic_user_fileadd_header Strict-Transport-Security "max-age=31536000" always;add_header Set-Cookie "dshgate=NEW_RANDOM_VALUE; Path=/; HttpOnly; Secure; SameSite=Lax";
# location = /api/remote.mux 内;与上面使用同一个新值if ($cookie_dshgate != "NEW_RANDOM_VALUE") { return 401; }不带 always 的 add_header 只对文档规定的状态码生效,其中不包含 401。错误页、内部跳转和响应头继承也应结合站点配置检查。Nginx 响应头文档
另外,$remote_user 非空不代表密码验证成功。它读取的是 Basic 请求头中解析出的用户名,错误密码或不存在的用户也可能让它有值,不能据此发放通行证。Nginx 变量源码 · Basic 头解析源码
修复后的验证结果如下:
- 在实际站点中,匿名、用户不存在和密码错误的请求均返回 401,且不再下发通行证;WS 使用空值、错误值或旧值也都被拒绝。
- 在同镜像隔离环境中,正确 Basic 凭据得到 200 和新通行证;新通行证通过模拟 mux 的校验,进入内容阶段后返回 404。这个测试验证了门禁逻辑,没有建立真实 DSH WebSocket。
- 最后,我刷新已登录页面,确认新建立的
remote.mux返回 101,并能收到模型回复,完成实际站点的合法访问验证。
静态通行证与 Basic Auth 密码相互独立,改密码时也应轮换通行证。会话 cookie 的浏览器属性不代表服务器端有效期;轮换只影响新握手,已有 WS 长连接仍可能由旧 worker 继续持有。本次重载保留了这些连接,没有强制中断会话。
重载是否生效,应结合日志和请求行为判断。本次通过新 worker 启动、旧通行证被拒绝确认了变化。nginx -T 适合检查磁盘配置和 include 链,不能读取运行中 worker 的配置;reload 命令返回 0,也只说明信号已送达。Nginx 命令行文档
8. 配置自动登录:用 sync.sh 兑换并注入 cookie
这是本文部署流程中的必做步骤。 浏览器通过外层认证后,OpenResty 需要为请求提供有效的 DSH 会话 cookie。sync.sh 负责在服务器上兑换凭据、写入反代片段,并配合定时器提前刷新,让日常访问无需手动输入启动 token。
sync.sh 是本实例自行配置的脚本。第 7 节的外层认证控制谁能访问,本节的 cookie 注入完成 DSH 内层登录,两部分都需要配置。
8.1 启动 token 与会话 cookie
启动 token 随进程启动变化,用于兑换 cookie;cookie 的签名密钥会持久保存。本地凭据提供方将其存于 $DSH_HOME/.credentials.yaml。
只要签名密钥、authority(规范化域名和端口)及有效期符合要求,既有 cookie 就能跨重启继续使用。 脚本用于首次兑换和周期刷新,重启后同步一次也是我的日常操作习惯。
默认 cookieMaxAgeDays 为 30 天,脚本在 20 天时刷新。调整有效期后,应同步缩短刷新周期。凭据删除或更换后,DSH 在下一次激活时加载新密钥,原有签名随之失效,需要重新兑换。DSH 浏览器认证设计
8.2 准备目录与 include
以下继续使用 dsh.example.com。将它换成自己的域名,并核对宿主机与容器的挂载路径:
sudo install -d -m 755 /usr/local/lib/dsh-authsudo install -d -m 700 /var/lib/dsh-auth
# 站点已在 1Panel 中创建后执行;已有文件时 touch 不会清空其内容。sudo mkdir -p /opt/1panel/www/sites/dsh.example.com/proxysudo touch /opt/1panel/www/sites/dsh.example.com/proxy/dsh-auth.inc在实际转发请求的 location 中,加载容器内对应的文件:
include /www/sites/dsh.example.com/proxy/dsh-auth.inc;如果 /api/remote.mux 使用独立 location,也要加载这条 include,并保留 Host 和 WebSocket 转发参数。占位文件必须在配置检查前存在,否则 Nginx 无法读取它。
自动注入会让反代请求携带有效的 DSH 凭据。因此,普通页面应保留 Basic Auth;采用通行证的 WS 入口,应只接受成功认证后发出的有效通行证。
8.3 服务器现用脚本
下面是服务器现用脚本,将域名和容器名统一改为示例值。使用前,把 dsh.example.com、your-openresty-container 及相关路径换成自己的配置。
该版本从 MainPID 对应日志中取 token,以成功状态文件的修改时间判断是否刷新。配置检查或重载命令失败时,会尝试恢复上一版 include,并返回非零退出码。
服务器上已验证未到刷新周期时的正常跳过;兑换、写入、回滚和失败重试在隔离环境完成测试,本次没有强制改写生产 cookie。
将代码保存为 /usr/local/lib/dsh-auth/sync.sh,以 root 运行:
#!/usr/bin/env bash# 用当前 dsh 进程的启动 token 换一个浏览器会话 cookie,注入给 OpenResty。# dsh 重启或 cookie 快到期时自动刷新;平时空转退出。## 2026-10-01 加固:# 1) token 按 dsh.service 当前 MainPID 过滤(journalctl _PID=),不再依赖日志行数窗口;# 2) 写文件/找容器/兑换 cookie/nginx -t/reload 任一步失败都非零退出,且不打印成功;# 3) TOKEN_FILE 只在 nginx -t 与 reload 都成功后才写,失败后下次执行必然重试;# 4) 刷新判定改用 TOKEN_FILE 的 mtime(= 上次成功时间),不看 include 的 mtime,# 避免「失败后 include 变新 → 下次被跳过」;# 5) 替换 include 前保留上一版到 $BACKUP,nginx -t / reload 失败时恢复并报告结果。set -uo pipefail
# 以下路径均可用环境变量覆盖,仅用于隔离测试/换站点;默认值即本站生产值。DOMAIN="${DSH_AUTH_DOMAIN:-dsh.example.com}"DSH_URL="${DSH_AUTH_DSH_URL:-http://127.0.0.1:3080}"INCLUDE="${DSH_AUTH_INCLUDE:-/opt/1panel/www/sites/dsh.example.com/proxy/dsh-auth.inc}"BACKUP="${DSH_AUTH_BACKUP:-/var/lib/dsh-auth/dsh-auth.inc.prev}"TOKEN_FILE="${DSH_AUTH_TOKEN_FILE:-/var/lib/dsh-auth/token}"SERVICE="${DSH_AUTH_SERVICE:-dsh.service}"ORESTY="${DSH_AUTH_ORESTY:-your-openresty-container}"NGINX_BIN=/usr/local/openresty/nginx/sbin/nginxREFRESH_DAYS=20
fail() { echo "sync.sh: $*" >&2; exit 1; }
# 1) 取「当前 dsh 进程」的启动 tokenMAIN_PID="$(systemctl show -p MainPID --value "$SERVICE" 2>/dev/null)"case "$MAIN_PID" in '' | 0) fail "$SERVICE 未运行或没有主进程(MainPID=${MAIN_PID:-空})" ;; *[!0-9]*) fail "无法解析 $SERVICE 的 MainPID:$MAIN_PID" ;;esacTOKEN="$(journalctl -u "$SERVICE" _PID="$MAIN_PID" --no-pager 2>/dev/null \ | grep -o 'token=[A-Za-z0-9_-]\+' | tail -1 | cut -d= -f2)"[ -n "$TOKEN" ] || fail "未在 $SERVICE 当前进程(PID $MAIN_PID)的日志里找到启动 token"
# 2) 是否需要刷新:以「上次成功同步时间」(TOKEN_FILE 的 mtime)为准NEED=1if [ -f "$TOKEN_FILE" ] && [ -f "$INCLUDE" ] && [ "$(cat "$TOKEN_FILE")" = "$TOKEN" ]; then AGE=$(( ( $(date +%s) - $(stat -c %Y "$TOKEN_FILE") ) / 86400 )) [ "$AGE" -lt "$REFRESH_DAYS" ] && NEED=0fi[ "$NEED" = 1 ] || exit 0
# 3) 用启动 token 换浏览器会话 cookieCOOKIE="$(curl -si -H "Host: $DOMAIN" "$DSH_URL/?token=$TOKEN" \ | grep -i '^set-cookie:' | head -1 \ | sed -E 's/^[Ss]et-[Cc]ookie:[[:space:]]*([^;]+).*/\1/' | tr -d '\r')"[ -n "$COOKIE" ] || fail "换取 cookie 失败(token 可能已过期,或 $DSH_URL 不可达)"
# 4) 写新 include(先保留上一版)printf 'proxy_set_header Cookie "%s";\n' "$COOKIE" > "$INCLUDE.tmp" \ || fail "写入 $INCLUDE.tmp 失败,未改动 $INCLUDE"HAVE_BACKUP=0if [ -f "$INCLUDE" ]; then cp -p "$INCLUDE" "$BACKUP" || fail "备份 $INCLUDE 到 $BACKUP 失败,未改动 $INCLUDE" HAVE_BACKUP=1else echo "sync.sh: 注意:$INCLUDE 原先不存在,本次首次写入,无上一版可回滚" >&2fimv "$INCLUDE.tmp" "$INCLUDE" || fail "替换 $INCLUDE 失败"
rollback() { if [ "$HAVE_BACKUP" = 1 ]; then if cp -p "$BACKUP" "$INCLUDE"; then echo "sync.sh: $1;已恢复上一版 include($BACKUP)" >&2 else echo "sync.sh: $1;恢复失败!请手工把 $BACKUP 复制回 $INCLUDE" >&2 fi else echo "sync.sh: $1;无上一版可恢复,请人工检查 $INCLUDE" >&2 fi}
# 5) 固定容器 + 配置检查 + 重载docker inspect -f '{{.State.Running}}' "$ORESTY" 2>/dev/null | grep -qx true \ || { rollback "OpenResty 容器 $ORESTY 不存在或未运行,未 reload"; exit 1; }docker exec "$ORESTY" "$NGINX_BIN" -t >/dev/null 2>&1 \ || { rollback "nginx -t 失败,未 reload"; exit 1; }docker exec "$ORESTY" "$NGINX_BIN" -s reload >/dev/null 2>&1 \ || { rollback "nginx -s reload 失败"; exit 1; }
# 6) 全部成功后才提交成功状态printf '%s' "$TOKEN" > "$TOKEN_FILE" \ || fail "写入 $TOKEN_FILE 失败(配置已生效,下次执行会重新兑换)"
echo "已同步 $DOMAIN 的 cookie"exit 0sudo chown root:root /usr/local/lib/dsh-auth/sync.shsudo chmod 700 /usr/local/lib/dsh-auth/sync.sh各参数的用途如下:
| 参数 | 用途 |
|---|---|
| DOMAIN | 实际访问的 authority;非标准端口也要包含端口,例如 dsh.example.com<8443>8443> |
| DSH_URL | DSH 本机地址;本文为 http://127.0.0.1<3080>3080> |
| INCLUDE | 宿主机片段路径;容器内 include 路径按挂载关系填写 |
| BACKUP | 上一版片段的备份位置;目录必须先存在 |
| TOKEN_FILE | 保存上次同步成功的 token,并以文件修改时间判断刷新周期 |
| SERVICE | DSH 的 systemd 服务名,本文为 dsh.service |
| ORESTY | 本站实际 OpenResty 容器名,必须替换示例值 |
| REFRESH_DAYS | 刷新周期天数;本文 20 天,小于默认 30 天有效期 |
脚本按以下顺序工作:
- 读取服务 MainPID,再查询对应 PID 的日志。服务未运行或找不到 token 时,返回失败。
- 如果 token 未变、include 存在,且距上次成功同步不足 20 天,直接退出。
- 携带实际 Host 兑换 cookie,取出 Set-Cookie 的
名字=值,用于上游 Cookie 请求头。 - 写入临时片段、保留上一版,再以
mv替换 include,避免读取半截文件。 - 检查容器状态与 Nginx 配置,再发送重载信号;失败时尝试恢复备份,并报告结果。
- 命令成功后才写 TOKEN_FILE。刷新失败不更新成功标记,下次执行仍会重试。
刷新时间取自成功状态文件,脚本没有解析 cookie 的到期 payload。“已同步”表示上述命令成功执行,浏览器验收仍需结合日志和实际请求;恢复磁盘文件也不等于确认运行配置已恢复。
8.4 配套 systemd 定时器
sudo tee /etc/systemd/system/dsh-auth-sync.service > /dev/null <<'EOF'[Unit]Description=Sync DSH browser cookie into nginxAfter=dsh.service
[Service]Type=oneshotExecStart=/usr/local/lib/dsh-auth/sync.shEOF
sudo tee /etc/systemd/system/dsh-auth-sync.timer > /dev/null <<'EOF'[Unit]Description=Periodically sync DSH browser cookie
[Timer]OnBootSec=60OnUnitActiveSec=10minAccuracySec=30s
[Install]WantedBy=timers.targetEOF
sudo systemctl daemon-reloadsudo systemctl enable --now dsh-auth-sync.timersudo /usr/local/lib/dsh-auth/sync.sh定时器在开机后检查一次,随后每 10 分钟运行;未到刷新时间时,脚本正常退出且没有输出。
After=dsh.service 只规定启动顺序,不会主动启动 DSH,也不保证它已经打印 token。首次执行过早时,先确认 DSH 就绪,再手动重试。
8.5 日常使用与验证
普通重启后,等待 DSH 就绪,再运行同步脚本:
sudo systemctl restart dsh# 检查本次启动日志,等待服务就绪后再执行。sudo /usr/local/lib/dsh-auth/sync.sh等待时间以服务状态和启动日志为准。旧 cookie 仍有效时,通常可以继续访问。
凭据重建后需要重新激活 DSH,新进程会生成新的 token,正常同步即可触发兑换。现用脚本没有强制刷新参数;token 未变且未到周期时,重复执行会正常跳过。
查看定时器、同步日志及片段中的指令数量:
sudo systemctl status dsh-auth-sync.timer --no-pagersudo journalctl -u dsh-auth-sync.service -n 30 --no-pager
# 只确认有 Cookie 注入指令,不显示有效 cookie 内容。sudo grep -c '^proxy_set_header Cookie ' \ /opt/1panel/www/sites/dsh.example.com/proxy/dsh-auth.inc匹配数为 1,说明文件中有一条 Cookie 注入指令。随后刷新浏览器,确认外层登录成功、新的 remote.mux 为 101,并能收到回复。
脚本的常见输出及处理方式如下:
| 现用脚本输出 | 含义与处理 |
|---|---|
| 已同步域名的 cookie | 命令流程成功,接着检查浏览器新连接与模型回复 |
| 无输出、退出码 0 | token 未变且未到刷新周期,正常跳过 |
| 未在日志里找到 token | 检查本次启动是否就绪,MainPID 与日志是否匹配 |
| 兑换请求失败或没有会话 cookie | 检查 Host、当前启动 token、DSH 状态和上游地址 |
| 配置校验或重载失败、退出码非 0 | 查看恢复上一版是否成功,修复配置、容器名或挂载路径,再重试 |
同步脚本负责 DSH 内层 cookie,外层页面与 WebSocket 的访问控制仍由反代配置完成,两部分应一起验证。
8.6 调试时手动使用 token 登录
需要排查 DSH 自带认证时,可以手动使用启动 token。本文的日常访问由上述同步脚本完成,下面的方法用于调试。先查看启动日志:
sudo journalctl -u dsh -n 50 --no-pager日志中的地址类似:
dsh web: http://127.0.0.1:3080/?token=REDACTED将地址部分换成实际 HTTPS 域名,保留当前启动 token,在浏览器中访问一次。兑换会话 cookie 后,就可以通过不带 token 的首页地址访问。
token 属于登录凭据,分享日志或截图时应遮盖。它随进程启动变化,而既有 cookie 在签名密钥、authority 和有效期满足要求时可以跨重启使用。
完成自动登录配置后,浏览器的日常访问无需执行这一步。
9. 选择工作区并验证首次会话
打开 DSH,添加服务器上的工作区,例如 /srv/dsh-workspace。这里填写的是服务器目录。
选择已配置的模型,创建会话,发一条简单消息,确认能够收到回复。再打开开发者工具的 Network 面板,找到 remote.mux:

101 Switching Protocols 表示 WebSocket 已完成升级。结合模型回复,可以确认浏览器连接和模型调用均正常。服务显示 running、首页返回 200,只完成了其中一部分检查。官方首次使用指南
10. 常见问题与排查方向
| 现象 | 优先检查 |
|---|---|
| systemd 启动失败 | dsh/Node 入口、PATH、目录权限、用户和启动参数 |
| 502 Bad Gateway | DSH 是否运行、监听端口、容器网络与上游地址 |
| API 返回 403 | trusted-host、透传 Host、浏览器 Origin 与端口 |
| DSH 提示 authentication required | 手动登录或反代注入是否生效,authority、签名密钥与有效期是否匹配 |
| 401 且有 WWW-Authenticate: Basic | 反代用户名密码、密码文件路径与可读权限 |
| 页面正常但 WebSocket 401 | WS 实际匹配的 location、外层认证、通行证是否有效 |
| 一直连接中或重连 | Upgrade/Connection、超时、后端日志和认证状态 |
| settings are unavailable in this browser | 核对版本与访问环境;本文通过 systemd 环境文件配置 API Key |
| 设置了 Key 但模型仍调用失败 | 变量是否进入进程、所选供应商与凭据引用、密钥有效性及 API 返回错误 |
| npm ETARGET | 版本号、dist-tags 与当前 registry 的包版本 |
| 上传 413 | Nginx client_max_body_size 与应用自身附件限制 |
| 服务退出,日志有 OOM | 主机可用内存、swap、服务资源上限及正在运行的工作区任务 |
11. 日常维护
# 查看状态sudo systemctl status dsh --no-pager
# 查看日志;分享前清理 token 等凭据sudo journalctl -u dsh -n 50 --no-pager
# 修改启动参数、环境文件或 drop-in 后重启sudo systemctl daemon-reloadsudo systemctl restart dsh升级前,备份实际的 DSH_HOME、工作区和 systemd 配置,并记录当前 DSH、Node 版本。备份可能包含会话和凭据,应作为私有数据保存。
执行 Office 文件转换、视频转码等任务时,注意内存和 swap 占用。我的实例曾遇到 Office 网页预览带来的资源问题,具体转换方式和插件配置应按当前版本核对。
写在最后
把 DSH 放到云服务器上后,我可以通过浏览器回到同一个工作区,继续处理项目。部署过程中最有用的经验,是按服务、反代、认证、模型调用逐层检查:每一步有明确结果,遇到故障时就更容易定位。
对我的环境来说,模型设置页不可用的问题通过 systemd 环境文件解决;HTTP 与 WebSocket 的认证则分别做了成功和拒绝测试。最后刷新页面,看到新的连接建立并收到模型回复,这套部署才算真正跑通。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!

