最近我把 DeepSeek Harness(命令行工具 dsh)当成主力智能体框架来用,每天都要在上面起对话、挂多轮任务、跑连接外部环境的自动化流程。用得越深就越发现一个问题:只要本地开的终端窗口被关掉,或者 SSH 远程连接闪断一次,dsh 进程跟着就没,正在跑的任务也会直接中断。刚开始我以为是 dsh 自身不稳定,后来才意识到,这是典型的“前台进程跟着终端生命周期走”的问题。说白了,终端是爹,进程是儿子,终端一死,儿子只能陪葬。
提示:dsh 是 DeepSeek Harness 的命令行入口,负责管理多智能体任务、插件、Web 会话等。你用终端敲 dsh 启动起来的服务,默认都挂在当前终端上,Ctrl+C、关窗口、断 SSH,都会把整个进程连锅端。
这篇文章不是官方文档的复读,是我自己踩完坑以后整理的一套方案:怎么让 dsh 在终端关闭后继续跑,怎么一键启动、一键停止,以及怎么在遇到 dsh web authentication required、端口权限报错、进程假死这类问题时快速止血。适合所有正在用 dsh、或者打算把 dsh 部署到远程服务器上的开发者。顺便说一句,很多人会拿 dsh 和 AgentScope 之类的多智能体框架对比,但选型是另一个话题,我这里只聊运行管理,不聊功能对比。
1. 为什么必须解决“关闭终端就断线”的问题
1.1 真实现状:你的 dsh 是“挂在终端上”的
无论 dsh 是用来跑对话式任务、在本地环境里执行代码,还是通过 dsh web 提供服务,它的进程模型都和绝大多数命令行工具一样:由 shell 派生,父进程是你的终端模拟器(bash、zsh、Windows Terminal 等)。终端退出时,系统会给这个会话里的进程发送 SIGHUP 信号,进程收到信号后默认行为就是退出。
这个“挂断信号”的英文是 HUP,也就是 hangup。你想想上古时期的有线电话,电话一挂,线路就断了。Unix 把这套逻辑继承下来,意思就是:终端都没了,你也没必要活。对普通命令没问题,但对 dsh 这种要长时间跑任务的工具就是灾难。
我实际遇到的情况是:本地 Windows 电脑开一个 Git Bash 窗口,用 dsh 起了一个多智能体任务,计划跑个半小时。中途我去开会,回来顺手把终端关了,结果任务跑到 60% 就中断,重开 dsh 一切归零,还要重新配上下文。这种挫败感,用一次就忘不了。
1.2 谁最需要后台运行方案
不是所有用 dsh 的人都需要后台运行,但下面这几类人基本逃不掉:
- 通过 SSH 连到远程服务器跑 dsh,人在本地,SSH 一断任务就断;
- 把 dsh 当常驻服务用,比如开 dsh web 管理界面,需要长时间在线;
- 夜间挂批量任务,跑模型对比或者评测,第二天早上看结果;
- 本地开发时经常开多个终端,随手关错窗口就把服务带走了。
如果你只是临时敲几条命令,前台启动完全没问题。但只要出现过一次“任务白跑”的情况,你就应该把后台运行当成基础设施来做,而不是每次手工救火。
1.3 dsh 为什么比普通命令更怕终端关闭
普通命令比如 ls、grep,执行完就退出,终端关不关无所谓。但 dsh 是常驻型进程。它启动后会拉起会话管理、插件系统,可能还会监听本地端口给 dsh web 用。进程不退出,任务就不结束。这导致终端关闭时,正在执行的插件调用、正在等待模型返回的请求,都会跟着进程一起被结束。更麻烦的是,有些中间状态写在内存里,进程一死就彻底丢了。
所以在给 dsh 做后台化之前,先搞清楚它的进程特征,后面写脚本才有的放矢。dsh 这个工具本身设计得不算复杂,但插件多了以后,进程之间的依赖关系会变复杂。如果你直接在前台跑 dsh web,再开几个插件插件目录,一旦进程被挂断,插件状态、会话缓存全部丢光。这也是为什么我强烈建议尽早做后台化。
2. 后台运行方案怎么选:nohup、tmux、systemd 对比
2.1 nohup 的局限与适用场景
最简单的做法是nohup dsh &,它的作用是忽略 SIGHUP 信号,让进程在终端关闭后继续存活。大多数 Linux 发行版和 macOS 自带,Windows 的 Git Bash 环境也能用。
但 nohup 有个问题:它只管“不被挂断”,不管进程的完整生命周期管理。如果你之前跑过一个 dsh,再启动第二个,会撞端口;停止进程时你还得手动找 PID。也就是说,nohup 适合临时应急,不适合当长期方案。我见过不少用户用 nohup 跑起来就再也不管,等到想重启的时候,满世界找进程,最后只能重启机器。
2.2 tmux 和 screen:交互体验最好
tmux(以及更老的 screen)是一个终端复用器,相当于给进程开了一个“虚拟终端”,你随时可以重新接上去看输出。对 dsh 来说,tmux 最大的好处是你还能看到实时日志,进去敲 Ctrl+C,或者查看会话状态。
不过 tmux 也有学习成本,第一次用的人容易按错快捷键,而且系统重启之后会话就消失了,除非你配合一些自动恢复的配置。我个人的建议是:如果你是交互式使用 dsh,比如要经常看对话输出,用 tmux 会更顺手。但如果是无人值守地跑任务,tmux 反而有点多余。
2.3 systemd:服务化管理的终极方案
如果你把 dsh 当成一个常驻服务,最好用 systemd 来管。systemd 不只是后台化,它还能做到开机自启、崩溃自动重启、统一日志收集,停止和启动都是systemctl start dsh这种标准命令,非常干净。
缺点是配置相对重,Windows 原生没有 systemd,如果你在 Windows 上用 dsh,这套方案就用不上。另外 systemd 对环境变量的管理和 shell 语法差异,也需要花一点时间适应。我第一次迁移脚本到 systemd 的时候,就在 Environment 配置上踩了坑。
2.4 我的选择:按平台双轨并行
说了这么多,我最后实际落地的是双轨方案:
- Linux、macOS、Git Bash:用“启动脚本 + 停止脚本 + PID 文件”的方式,脚本内部封装 nohup,再加端口冲突检查;
- 长期服务器部署:用 systemd service,配好 Restart=always,即使服务器重启也能自动拉起。
这样既不牺牲交互性,又能保证稳定性。下面我会把两套方案都完整写出来,你可以直接抄。
3. 一键后台启动与停止:脚本实战
3.1 先确认 dsh 命令安装好了
脚本写到一半,发现 dsh 压根没装,或者路径不对,那就尴尬了。先执行:
which dsh dsh --version如果提示“dsh 不是内部或外部命令,也不是可运行的程序或批处理文件”,大概率是安装后没有把可执行文件所在目录加到 PATH。常见处理办法是:
- 检查安装目录,比如
~/.deepseek-harness/bin或 npm 全局目录; - 在
~/.bashrc或~/.zshrc里加export PATH="$HOME/.deepseek-harness/bin:$PATH"; - 重新 source 配置文件。
确认命令能用之后,再进入下一步。这个步骤看起来基础,但后台启动时报“command not found”的概率其实很高,尤其是通过 SSH 登录时,PATH 可能和本地终端不完全一样。我之前就是没注意,SSH 进去后一直提示找不到 dsh,后来才发现是登录 shell 只加载了 /etc/profile,没加载 .bashrc。
3.2 start-dsh.sh:nohup + PID 文件版本
原理不复杂:nohup 启动 dsh,把标准输出和错误输出都重定向到日志文件,进程 ID 记到 PID 文件。这样关闭终端不影响进程,后面停止脚本也能用 PID 文件定位进程。
我实际用的启动脚本是这样:
#!/usr/bin/env bash # start-dsh.sh - 后台启动 dsh,终端关闭不断线 set -euo pipefail LOGDIR="$HOME/.dsh/logs" LOGFILE="$LOGDIR/dsh.log" PIDFILE="$LOGDIR/dsh.pid" if [ ! -d "$LOGDIR" ]; then mkdir -p "$LOGDIR" fi if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then echo "dsh 已经在运行,PID=$(cat "$PIDFILE")" exit 0 fi # 端口冲突检查,默认端口 3080 if lsof -i :3080 -sTCP:LISTEN >/dev/null 2>&1; then echo "端口 3080 已被占用,请先停止旧实例或修改端口" exit 1 fi # nohup 启动 dsh,后台运行 nohup dsh web >"$LOGFILE" 2>&1 & echo $! > "$PIDFILE" sleep 2 if kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then echo "dsh 已后台启动,PID=$(cat "$PIDFILE")" echo "日志文件: $LOGFILE" else echo "dsh 启动失败,请查看日志: $LOGFILE" exit 1 fi几个细节说一下:
kill -0不是杀进程,而是检查进程是否存在,非常常用;lsof -i :3080检查端口,避免旧实例还在,新实例启动直接报权限或占用错误;nohup dsh web只是示例,如果你要带其他参数,比如启用特定插件目录,自己并到命令行后面即可。
3.3 stop-dsh.sh:按 PID 精确停止
停止脚本的核心是按 PID 停止,同时清理 PID 文件。如果 PID 文件不存在,就回退按端口 3080 找进程。
#!/usr/bin/env bash # stop-dsh.sh - 停止后台运行的 dsh set -euo pipefail LOGDIR="$HOME/.dsh/logs" PIDFILE="$LOGDIR/dsh.pid" if [ -f "$PIDFILE" ]; then PID=$(cat "$PIDFILE") if kill -0 "$PID" 2>/dev/null; then echo "正在停止 dsh,PID=$PID" kill "$PID" for i in {1..10}; do if ! kill -0 "$PID" 2>/dev/null; then break fi sleep 1 done # 如果优雅停止超时,强制 kill if kill -0 "$PID" 2>/dev/null; then echo "优雅停止超时,强制结束" kill -9 "$PID" fi else echo "PID 文件里的进程不存在,可能已经退出" fi rm -f "$PIDFILE" else echo "没有找到 PID 文件,尝试按端口停止" PID=$(lsof -ti :3080 -sTCP:LISTEN || true) if [ -n "$PID" ]; then echo "按端口找到进程 $PID,正在停止" kill "$PID" || kill -9 "$PID" else echo "没有找到正在运行的 dsh 进程" fi fi这里有一个很重要的事情:不要一上来就kill -9。dsh 有会话状态需要落盘,直接强制杀掉容易丢数据。先用kill发 SIGTERM 让进程自己清理,等 10 秒还不停,再用kill -9。这个 10 秒的等待时间看起来增加了脚本耗时,但能避免很多数据丢失的问题,值得。
3.4 日志和输出重定向的注意事项
后台运行最容易踩的坑就是“日志丢失”。启动脚本里如果漏了2>&1,错误输出不会写进日志文件,到时候进程没起来,你查日志发现只有几行空内容,排查起来非常痛苦。我建议所有后台脚本统一把 stdout 和 stderr 合并写入同一份日志文件。
日志文件本身也要定期关心,因为 dsh 的调试日志有时候会很大,跑几天能把磁盘撑满。简单做法是在脚本里定期 truncate,或者交给 logrotate 处理。这个不做强制要求,但我见过太多人后台跑着跑着磁盘满了,所以提一句。实际上,我有一次就是日志文件涨到 20 多 G,直接把服务器磁盘占满,dsh 连写日志都写不动,整个服务卡死。从那以后我养成了定期清理日志的习惯。
3.5 命令行参数分离:别把启动方式写死
你可能今天要启动 dsh web,明天要启动 dsh 跑特定插件,后天又要连远程环境。如果我把 dsh 的完整命令写死在脚本里,换场景就得改脚本。所以我把命令拆成可配置变量:
DASH_ARGS="${DASH_ARGS:-dsh web}" nohup $DASH_ARGS >"$LOGFILE" 2>&1 &这样启动不同模式可以这样用:
DASH_ARGS="dsh web --port 3080" ./start-dsh.sh DASH_ARGS="dsh dev" ./start-dsh.sh安装插件市场则用:
dsh plugin --profile web add dshmarket当然,shell 里变量展开再执行涉及引号问题,如果参数特别复杂,建议直接用函数或数组。脚本简单化,维护的人才不累。我自己的习惯是,对于开发模式、web 模式、评测模式各写一份独立的配置文件,启动脚本读取配置文件里的参数,这样团队协作时不会互相覆盖。
4. 更进一步:用 systemd 把 dsh 做成常驻服务
4.1 为什么服务化更省心
脚本方案最大的弱点是没有“自愈”能力。dsh 进程因为段错误、OOM 被杀、系统重启,都不会自动恢复。如果你是想把 dsh 长期部署在服务器上,比如给团队开一个 dsh web 管理界面,那我建议你直接上 systemd。
systemd 的好处:
- 开机自启,服务器重启不用人管;
Restart=always能在进程异常退出后自动拉起;journalctl统一收日志,查错方便;- 停止、重启、看状态都是标准命令,团队协作零成本。
这就像你把一个应用从“手动双击运行”升级成了“系统服务”,体验完全不一样。
4.2 写一个 dsh.service 单元文件
以 Linux 为例,在/etc/systemd/system/dsh.service写入:
[Unit] Description=DeepSeek Harness Service After=network-online.target Wants=network-online.target [Service] Type=simple User=your_user WorkingDirectory=/home/your_user/.dsh Environment=PATH=/usr/local/bin:/usr/bin:/bin ExecStart=/usr/local/bin/dsh web --host 0.0.0.0 --port 3080 Restart=always RestartSec=5 StandardOutput=journal StandardError=journal LimitNOFILE=65536 [Install] WantedBy=multi-user.target写完之后执行:
sudo systemctl daemon-reload sudo systemctl enable dsh sudo systemctl start dsh systemctl status dsh journalctl -u dsh -f然后 dsh 就变成系统级服务了。即使你退出 SSH,服务照样跑;服务器没电重启了,它也会在开机后自动拉起。用systemctl status查看时,如果有 Active: active (running) 字样,就说明服务状态正常。
4.3 环境变量和依赖顺序
有的 dsh 插件运行时需要访问外部 API,API Key 通常通过环境变量传入。在 systemd 服务里写环境变量有几种方式,最简单的是直接在 [Service] 段里加:
Environment=OPENAI_API_KEY=xxx Environment=DSH_CONFIG_DIR=/etc/dsh注意不要在 ExecStart 里写一堆 export,systemd 不认 shell 语法。这是很多人从脚本迁移到 systemd 时最容易踩的坑。另外,如果你的 dsh 与外部服务有依赖,可以在 [Unit] 段加 After 和 Requires,让 systemd 按顺序启动服务。如果你跑 dsh 的插件需要访问 Mysql、Redis 这类持久化组件,把依赖关系写清楚能避免不少启动时序问题。
4.4 崩溃自愈和资源限制
Restart=always的含义是无论什么原因退出,都尝试拉起。配合RestartSec=5,可以避免进程启动失败后疯狂重启打满 CPU。如果你想精确限制只有非正常退出才自愈,可以用Restart=on-failure,这个更讲究一些,实际使用中需要根据自己的场景选。
如果你的 dsh 需要处理大量并发插件任务,建议在 [Service] 段加LimitNOFILE=65536,这是在放宽文件描述符限制,否则高并发时可能碰到 too many open files 错误。这个错误的特点是:dsh 刚开始跑正常,跑着跑着突然某个插件报错,看日志全是文件描述符不足。我刚遇到时还以为是 dsh 的 bug,后来查系统日志才发现是 ulimit 限制。
5. 常见问题与排查技巧实录
5.1 dsh web 提示 authentication required,重新打开 URL
dsh web 启动后,终端会打印一个带 token 的 URL,第一次访问时需要打开这个 URL 完成认证。如果后台启动后你来不及点,或者 URL 滚出屏幕了,重启又要重来,怎么办?
我的经验是:启动 dsh web 后,把完整 URL 从终端输出里复制到日志文件保存好。后台启动脚本里重定向了日志,就方便多了。日志里通常会打印类似:
dsh web authentication required; reopen the url printed by dsh web.这时候去日志文件里找初始 URL,或者干脆停掉 dsh 重新在前台跑一次,把 URL 复制出来再后台运行。千万别在认证完成前关掉终端,否则 token 链接还没点就过期了,日志会一直提示认证要求。
这里有一个非常实用的小技巧:用 grep 从日志里把 URL 过滤出来。
grep -E "http://127.0.0.1:[0-9]+/auth|token" "$HOME/.dsh/logs/dsh.log"5.2 端口 3080 报 listen EACCES: permission denied
dsh 默认监听127.0.0.1:3080,如果启动时提示 EACCES permission denied,多半是以下原因:
- 端口已经被其他进程占用,而且那个进程属于别的用户;
- 当前用户没有绑定这个本地地址的权限(少见,但 SELinux 或容器里会遇到);
- 之前后台启动过一次 dsh,残留实例还占着端口。
处理方式:
lsof -i :3080 ps aux | grep dsh如果发现有残留 dsh 进程,用 kill 清理;如果占用的不是 dsh,就换个端口。dsh 一般支持--port参数,改成 3081、9090 都行。切勿上来就 sudo kill,先看清楚是谁占的,不然可能把别人的服务误杀了。我自己就干过这种事,最后被同事追着问是不是动了测试环境的端口。
5.3 dsh 关掉之后怎么再启动
很多人后台启动一次之后再想启动新实例,发现端口被占,或者不知道进程在哪。这时先看 PID 文件和日志。
如果确认旧进程已经退出,但端口还是无法绑定,可以等几秒再试,因为端口可能处于 TIME_WAIT 状态。如果确认进程死了,端口却还在,那就是系统少了端口回收时间,一般不影响再次监听,只是体现在报错上比较多。
更省事的办法是直接用我上面的 start-dsh.sh 脚本,脚本里已经做了端口检查和 PID 文件校验,重复启动时会提示“已经在运行”,不会产生第二个实例。这比你自己手动敲 nohup 放心多了。
5.4 Windows 下双击应用无窗口、后台进程却在
有搜索热词指向这个问题:安装的 GUI 应用双击后无法显示窗口,但后台进程显示已经启动。dsh Desktop 这类应用偶尔也会这样。遇到这种情况:
- 先打开任务管理器,看进程是否真的在跑;
- 如果是,右键结束进程树,再重新启动;
- 检查是否有单实例锁,比如
.lock文件,把它删掉; - 如果是图形界面初始化失败,时钟、桌面会话异常都可能导致无窗口,重启桌面最直接。
这不是 dsh 独有的问题,Windows 应用常见,关键是别反复双击,那只会堆出一堆僵尸进程。我有一次就是双击了七八次,任务管理器里一排同名进程,全在那空转,最后只能挨个结束。
5.5 插件树加载失败
还有热词提到error: dsh: plugin tree failed to load: failed to apply loader entry include。这类报错一般出现在 dsh 配置目录下插件文件格式不对,或者 include 的目录不存在。
排查顺序:
- 找到 dsh 插件配置目录,通常在
~/.config/dsh/或~/.dsh/; - 检查 include 路径指向的文件是否存在;
- 用
dsh plugin list看插件加载情况; - 临时把可疑插件改名,缩小范围;
- 必要的时候重新加载插件市场,比如
dsh plugin --profile web add dshmarket。
这类问题跟后台运行关系不大,但如果你在后台启动时遇到,会很难定位,因为日志里信息不完整。所以我建议配置阶段先在前台跑,确认无误后再后台化。
5.6 常用补充:读 md 文件、删除对话
顺带说两个 dsh 高频操作,因为不少人在后台跑着 dsh 时也会用到。想读 md 文件,可以在对话里直接指定文件路径,一般用@file或-f参数,具体语法看版本帮助:
dsh run --file README.md想删除某个历史对话,先列出会话,再删除指定 ID:
dsh conversation list dsh conversation delete <conversation_id>命令可能因为你装的版本不同而略有差异,以dsh --help输出为准。
6. 我这套方案用了半年之后的几点心得
6.1 日志是你最好的朋友
后台运行之后,你再也看不到 dsh 的前台输出,日志文件就是唯一的信息来源。不夸张地说,我排查 dsh 后台问题的 90% 时间都在翻日志。所以脚本里一定把日志路径固定下来,并且在启动成功或失败时明确打印日志位置,不然过两天你自己都忘了进程跑在哪。
6.2 优先 SIGTERM,不要无脑 kill -9
dsh 的会话和任务状态如果没落盘,强制杀掉就可能丢进度。我只在 SIGTERM 等 10 秒无效时才用 -9。这个习惯不仅适用于 dsh,也适用于你管理的所有长任务服务。如果你在调试时确实需要快速清场,可以先用脚本正常停止,不行再强杀,但心里要清楚强杀的代价。
6.3 定期检查端口和进程数
后台进程最怕堆积。同一个 dsh 端口被多个残留实例抢占、PID 文件里是旧进程,这些问题都是日志看不出来的。我养成的习惯是每周末看一眼:
ps aux | grep -c dsh lsof -i :3080数量对不上,就手动清理。这个动作成本很低,但能避免很多“莫名其妙”的问题。最后再分享一个小技巧:把 start-dsh.sh 和 stop-dsh.sh 加上执行权限,然后在~/.bashrc里加两个 alias:
alias dsh-start="~/bin/start-dsh.sh" alias dsh-stop="~/bin/stop-dsh.sh"以后敲 dsh-start、dsh-stop 就能一键管理后台服务。这就是我目前最顺手的 dsh 后台运行思路:日常用脚本,长期部署用 systemd。如果你也刚被“终端关闭任务中断”坑过,建议先把我这套 start/stop 脚本跑起来,五分钟后就能告别这个问题。