
用 dsh(DeepSeek Harness)本地 Web GUI 调试 Agent 时,最扫兴的莫过于:终端窗口一关,服务跟着退出,页面从此打不开。这篇文章先用一分钟讲清"为什么进程会随终端关闭而死",再给出一个只需配置一次、日常只敲两个 4 字符命令的完整方案——dshd 后台启动、dsht 彻底停止,从此关终端不慌,重启不丢会话。
dsh 是一款在本地运行的 AI Harness 工具(npm 包 @deepseek-ai/dsh),它自带一个 Web GUI,默认地址是:
http://127.0.0.1:3080很多人(包括我)第一次都是这样启动的:
dsh web然后打开浏览器开始干活。干到一半想合上电脑或者清理终端窗口,结果终端一关,Web 也打不开了——因为刚才那条命令是"前台"跑在终端里的。
在类 Unix 系统(macOS/Linux)里,终端窗口关闭时,shell 退出并会给自己的子进程发送一个 SIGHUP(挂断)信号:
要让服务不随终端退出,思路只有一个:让进程脱离终端会话。常见手段:
手段 | 特点 |
|---|---|
| 忽略 SIGHUP,进程在后台跑,最简单 |
| 独立会话,之后还能 attach 回去看 |
launchd / systemd | 做成系统服务,可开机自启(最重) |
判断一个进程是否已经"自由",有一个很直观的办法——看它的父进程(PPID):
ps -o pid=,ppid=,command= -p <PID>PPID 是 1(被 init/launchd 收养)说明已脱离终端;PPID 还是某个 shell,说明它仍挂在终端下,关终端会一起死。
下面这套配置只做一次:创建一个启动脚本 + 在 ~/.zshrc 里定义两个命令。日常你只需要记 dshd 和 dsht。
dsh --version || npm i -g @deepseek-ai/dsh提示:如果之前用
npx临时跑过 dsh,全局安装后建议清理一下 npx 缓存里的旧副本(~/.npm/_npx下对应目录),避免"一份程序两处存放"的困惑。数据不受影响——dsh 的用户数据统一放在~/.dsh。
~/start-dsh.sh#!/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}"chmod +x ~/start-dsh.sh
bash -n ~/start-dsh.sh && echo "语法 OK"~/.zshrc 末尾追加两个命令# 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
}source ~/.zshrc # 新开的终端不需要,自动加载dshd第一次会看到:
→ 启动 dsh (/Users/xxx/.local/bin/dsh) 端口 3080,模式: nohup
✓ 已在后台启动 (PID 12345)
打开地址: http://127.0.0.1:3080
查看日志: tail -f ~/.dsh/logs/web.log如果它已经在运行,则只会提示地址并退出——天然防重复启动。
启动用的终端窗口直接关掉,然后新开一个终端验证:
dshd
# 输出: ✓ dsh 已在运行: http://127.0.0.1:3080或者不带任何依赖地验证:
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:3080 # 200 即存活dsht
# 输出: ✓ 已停止 dsh (PID: 12345)没在运行时执行 dsht,会友好地提示 dsh 未在运行,而不是报一堆 kill 用法错误。
tail -f ~/.dsh/logs/web.log这段写给想"知其所以然"的同学。
nohup "$DSH_BIN" web >"$LOG_FILE" 2>&1 &nohup:让进程忽略 SIGHUP,这是"关终端不死"的关键;>log 2>&1:输出重定向到文件,否则后台进程往已关闭的终端写输出会报错;&:放到后台,shell 立即返回。pgrep dsh?
一个进程可能对应多个命令名,pkill -f dsh 这种模糊匹配很容易误杀其他 node 进程。而 Web 服务一定占用 3080 端口,用 lsof -tiTCP:3080 -sTCP:LISTEN 精确定位,最安全。dsht 用函数而不是 alias?
alias 只是文本替换;函数能写逻辑——没找到进程时给出友好提示,而不是执行 kill(无参数会报 usage 错误)。~/.local/bin/dsh?
全局 npm 安装后 bin 可能在 ~/.local/bin 等非标准路径。脚本先探测已知路径,再退回 PATH 里的 dsh,保证无论环境变量怎么配都能启动。~/.dsh,与进程存活无关。所以进程怎么重启都不丢对话历史——这一点让"大胆杀进程"变得毫无心理负担。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。
场景 | 命令 |
|---|---|
后台启动 / 确认在运行 |
|
彻底关闭 |
|
看日志 |
|
三个避坑提醒,写代码和运维时都用得上:
lsof -tiTCP:<port> 定位永远比 pkill -f 安全;~/.dsh,进程随便重启,历史不丢。配置一次,之后每次只需 dshd 四个字符——把时间留给真正的问题,而不是和终端窗口搏斗。
(本文配套脚本:~/start-dsh.sh 与 ~/.zshrc 中的 dshd/dsht 定义;正文中的脚本为实测运行的完整原文。文中地址与路径以 macOS 为例,Linux 用户把 ~/.local/bin 换成自己的全局 bin 路径即可。)
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。