首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >关闭终端也不断线:给 DeepSeek Harness(dsh)一键后台启动与停止

关闭终端也不断线:给 DeepSeek Harness(dsh)一键后台启动与停止

原创
作者头像
岳泽以
发布2026-09-03 13:24:50
发布2026-09-03 13:24:50
330
举报
文章被收录于专栏:Vibe CodingVibe Coding

摘要

dsh(DeepSeek Harness)本地 Web GUI 调试 Agent 时,最扫兴的莫过于:终端窗口一关,服务跟着退出,页面从此打不开。这篇文章先用一分钟讲清"为什么进程会随终端关闭而死",再给出一个只需配置一次、日常只敲两个 4 字符命令的完整方案——dshd 后台启动、dsht 彻底停止,从此关终端不慌,重启不丢会话。

一、场景:你大概率踩过的坑

dsh 是一款在本地运行的 AI Harness 工具(npm 包 @deepseek-ai/dsh),它自带一个 Web GUI,默认地址是:

代码语言:txt
复制
http://127.0.0.1:3080

很多人(包括我)第一次都是这样启动的:

代码语言:bash
复制
dsh web

然后打开浏览器开始干活。干到一半想合上电脑或者清理终端窗口,结果终端一关,Web 也打不开了——因为刚才那条命令是"前台"跑在终端里的。

二、原理:为什么会这样?

在类 Unix 系统(macOS/Linux)里,终端窗口关闭时,shell 退出并会给自己的子进程发送一个 SIGHUP(挂断)信号

  • 前台运行的进程收到 SIGHUP 后默认直接退出;
  • 于是你的 Web 服务跟着终端一起"殉葬"。

要让服务不随终端退出,思路只有一个:让进程脱离终端会话。常见手段:

手段

特点

nohup … &

忽略 SIGHUP,进程在后台跑,最简单

tmux / screen

独立会话,之后还能 attach 回去看

launchd / systemd

做成系统服务,可开机自启(最重)

判断一个进程是否已经"自由",有一个很直观的办法——看它的父进程(PPID):

代码语言:bash
复制
ps -o pid=,ppid=,command= -p <PID>

PPID 是 1(被 init/launchd 收养)说明已脱离终端;PPID 还是某个 shell,说明它仍挂在终端下,关终端会一起死。

三、一次性配置(约 1 分钟)

下面这套配置只做一次:创建一个启动脚本 + 在 ~/.zshrc 里定义两个命令。日常你只需要记 dshddsht

3.1 前置:确认 dsh 已全局安装

代码语言:bash
复制
dsh --version || npm i -g @deepseek-ai/dsh

提示:如果之前用 npx 临时跑过 dsh,全局安装后建议清理一下 npx 缓存里的旧副本(~/.npm/_npx 下对应目录),避免"一份程序两处存放"的困惑。数据不受影响——dsh 的用户数据统一放在 ~/.dsh

3.2 创建启动脚本 ~/start-dsh.sh

代码语言:bash
复制
#!/usr/bin/env bash
# =============================================================
# start-dsh.sh —— 以"脱离终端"方式启动 dsh Web GUI
# 关闭启动终端后 Web 仍可访问 (http://127.0.0.1:3080)
#
# 用法:
#   bash ~/start-dsh.sh           默认模式: nohup 后台运行(推荐)
#   bash ~/start-dsh.sh --tmux    改用 tmux 会话运行
#   DSH_PORT=8080 bash ~/start-dsh.sh   自定义端口(需与 dsh 配置一致)
#
# 依赖: dsh(全局安装); --tmux 模式另需 tmux
# =============================================================
set -u

PORT="${DSH_PORT:-3080}"
LOG_DIR="${HOME}/.dsh/logs"
LOG_FILE="${LOG_DIR}/web.log"

MODE="${1:-nohup}"
if [ "${MODE}" = "--tmux" ]; then
  MODE=tmux
  shift
else
  MODE=nohup
fi

# ---- 定位 dsh 可执行文件(优先 ~/.local/bin 下的全局安装) ----
DSH_BIN="${DSH_BIN:-}"
if [ -z "${DSH_BIN}" ] && [ -x "${HOME}/.local/bin/dsh" ]; then
  DSH_BIN="${HOME}/.local/bin/dsh"
fi
if [ -z "${DSH_BIN}" ] && command -v dsh >/dev/null 2>&1; then
  DSH_BIN="$(command -v dsh)"
fi
if [ -z "${DSH_BIN}" ]; then
  echo "✗ 未找到 dsh,请先全局安装: npm i -g @deepseek-ai/dsh" >&2
  exit 1
fi

# ---- 已在运行则只提示,避免重复实例 ----
if lsof -nP -iTCP:"${PORT}" -sTCP:LISTEN >/dev/null 2>&1; then
  echo "✓ dsh 已在运行: http://127.0.0.1:${PORT}"
  [ -f "${LOG_FILE}" ] && echo "  日志: tail -f ${LOG_FILE}"
  exit 0
fi

mkdir -p "${LOG_DIR}"
echo "→ 启动 dsh (${DSH_BIN}) 端口 ${PORT},模式: ${MODE}"

if [ "${MODE}" = "tmux" ]; then
  if ! command -v tmux >/dev/null 2>&1; then
    echo "✗ 未安装 tmux,可先安装: brew install tmux" >&2
    exit 1
  fi
  tmux new-session -d -s dsh "exec ${DSH_BIN} web $* >'${LOG_FILE}' 2>&1"
  echo "✓ 已在 tmux 会话 [dsh] 中启动"
  echo "  返回查看: tmux attach -t dsh   (脱离: Ctrl+B 然后 D)"
else
  nohup "${DSH_BIN}" web "$@" >"${LOG_FILE}" 2>&1 &
  echo "✓ 已在后台启动 (PID $!)"
fi

echo "  打开地址: http://127.0.0.1:${PORT}"
echo "  查看日志: tail -f ${LOG_FILE}"

3.3 赋予执行权限

代码语言:bash
复制
chmod +x ~/start-dsh.sh
bash -n ~/start-dsh.sh && echo "语法 OK"

3.4 在 ~/.zshrc 末尾追加两个命令

代码语言:bash
复制
# dshd: 后台启动/确认 dsh web(关闭终端不中断),由 ~/start-dsh.sh 实现
alias dshd='bash ~/start-dsh.sh'

# dsht: 彻底关闭 dsh web(按 3080 端口找进程)
dsht() {
  local p
  p=$(lsof -tiTCP:3080 -sTCP:LISTEN 2>/dev/null)
  if [ -n "$p" ]; then
    kill $p && echo "✓ 已停止 dsh (PID: $p)"
  else
    echo "dsh 未在运行"
  fi
}

3.5 让配置立即生效

代码语言:bash
复制
source ~/.zshrc     # 新开的终端不需要,自动加载

四、日常使用

启动(或确认在运行)

代码语言:bash
复制
dshd

第一次会看到:

代码语言:txt
复制
→ 启动 dsh (/Users/xxx/.local/bin/dsh) 端口 3080,模式: nohup
✓ 已在后台启动 (PID 12345)
  打开地址: http://127.0.0.1:3080
  查看日志: tail -f ~/.dsh/logs/web.log

如果它已经在运行,则只会提示地址并退出——天然防重复启动

验证"关终端也不断"

启动用的终端窗口直接关掉,然后新开一个终端验证:

代码语言:bash
复制
dshd
# 输出: ✓ dsh 已在运行: http://127.0.0.1:3080

或者不带任何依赖地验证:

代码语言:bash
复制
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:3080   # 200 即存活

彻底停止

代码语言:bash
复制
dsht
# 输出: ✓ 已停止 dsh (PID: 12345)

没在运行时执行 dsht,会友好地提示 dsh 未在运行,而不是报一堆 kill 用法错误。

查看日志

代码语言:bash
复制
tail -f ~/.dsh/logs/web.log

五、脚本设计解读

这段写给想"知其所以然"的同学。

  1. 核心只有一行nohup "$DSH_BIN" web >"$LOG_FILE" 2>&1 &
    • nohup:让进程忽略 SIGHUP,这是"关终端不死"的关键;
    • >log 2>&1:输出重定向到文件,否则后台进程往已关闭的终端写输出会报错;
    • &:放到后台,shell 立即返回。
  2. 为什么用端口判断"是否在运行",而不是 pgrep dsh 一个进程可能对应多个命令名,pkill -f dsh 这种模糊匹配很容易误杀其他 node 进程。而 Web 服务一定占用 3080 端口,用 lsof -tiTCP:3080 -sTCP:LISTEN 精确定位,最安全。
  3. 为什么 dsht 用函数而不是 alias? alias 只是文本替换;函数能写逻辑——没找到进程时给出友好提示,而不是执行 kill(无参数会报 usage 错误)。
  4. 可执行文件定位为什么先写死 ~/.local/bin/dsh 全局 npm 安装后 bin 可能在 ~/.local/bin 等非标准路径。脚本先探测已知路径,再退回 PATH 里的 dsh,保证无论环境变量怎么配都能启动。
  5. 数据安全:dsh 的数据(会话、设置、凭据)统一存放在 ~/.dsh,与进程存活无关。所以进程怎么重启都不丢对话历史——这一点让"大胆杀进程"变得毫无心理负担。
  6. tmux 模式是加分项:某些场景(想随时 attach 回去看交互输出)用 dshd --tmux,之后 tmux attach -t dsh 即可,逻辑完全对称。

六、常见问题

Q1:我现在的 dsh 是前台跑的,怎么平滑切换到后台模式?

先在前台那个终端按 Ctrl+C 停掉旧进程(页面会短暂断开,正常),再执行 dshd,最后刷新 http://127.0.0.1:3080。历史会话不会丢。

Q2:想换端口怎么办?

DSH_PORT=8080 dshd。注意端口需与 dsh 自身配置保持一致;dsht 默认按 3080 找进程,如换端口需要同步修改。

Q3:电脑重启后呢?

进程会随关机结束,开机后执行一次 dshd 即可。想做到开机自启,可以把它包成 launchd(macOS)或 systemd service(Linux),这是另一篇文章的话题。

Q4:关终端后网页打不开,怎么排查?

依次检查:① dshd 是否提示"已在运行";② lsof -nP -iTCP:3080 -sTCP:LISTEN 是否有输出;③ tail -n 50 ~/.dsh/logs/web.log 看启动报错;④ 浏览器是否走了代理导致访问不到 127.0.0.1。

七、小结

场景

命令

后台启动 / 确认在运行

dshd

彻底关闭

dsht

看日志

tail -f ~/.dsh/logs/web.log

三个避坑提醒,写代码和运维时都用得上:

  1. 关终端 ≠ 停止服务:只要用 nohup/tmux 启动,进程就归 init 管,终端只是"遥控器";
  2. 杀进程按端口、不按名字lsof -tiTCP:<port> 定位永远比 pkill -f 安全;
  3. 数据与进程分离:会话在 ~/.dsh,进程随便重启,历史不丢。

配置一次,之后每次只需 dshd 四个字符——把时间留给真正的问题,而不是和终端窗口搏斗。


(本文配套脚本:~/start-dsh.sh~/.zshrc 中的 dshd/dsht 定义;正文中的脚本为实测运行的完整原文。文中地址与路径以 macOS 为例,Linux 用户把 ~/.local/bin 换成自己的全局 bin 路径即可。)

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 摘要
  • 一、场景:你大概率踩过的坑
  • 二、原理:为什么会这样?
  • 三、一次性配置(约 1 分钟)
    • 3.1 前置:确认 dsh 已全局安装
    • 3.2 创建启动脚本 ~/start-dsh.sh
    • 3.3 赋予执行权限
    • 3.4 在 ~/.zshrc 末尾追加两个命令
    • 3.5 让配置立即生效
  • 四、日常使用
    • 启动(或确认在运行)
    • 验证"关终端也不断"
    • 彻底停止
    • 查看日志
  • 五、脚本设计解读
  • 六、常见问题
  • 七、小结
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档