dsh 后台运行指南:终端关闭不断线,nohup/tmux/systemd 实战 📅 发布时间:2026/9/12 8:01:41 👁 浏览次数: 最近我把 DeepSeek Harness命令行工具 dsh当成主力智能体框架来用每天都要在上面起对话、挂多轮任务、跑连接外部环境的自动化流程。用得越深就越发现一个问题只要本地开的终端窗口被关掉或者 SSH 远程连接闪断一次dsh 进程跟着就没正在跑的任务也会直接中断。刚开始我以为是 dsh 自身不稳定后来才意识到这是典型的“前台进程跟着终端生命周期走”的问题。说白了终端是爹进程是儿子终端一死儿子只能陪葬。提示dsh 是 DeepSeek Harness 的命令行入口负责管理多智能体任务、插件、Web 会话等。你用终端敲 dsh 启动起来的服务默认都挂在当前终端上CtrlC、关窗口、断 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 最大的好处是你还能看到实时日志进去敲 CtrlC或者查看会话状态。不过 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配好 Restartalways即使服务器重启也能自动拉起。这样既不牺牲交互性又能保证稳定性。下面我会把两套方案都完整写出来你可以直接抄。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.shnohup 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 21; then echo 端口 3080 已被占用请先停止旧实例或修改端口 exit 1 fi # nohup 启动 dsh后台运行 nohup dsh web $LOGFILE 21 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 正在停止 dshPID$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 日志和输出重定向的注意事项后台运行最容易踩的坑就是“日志丢失”。启动脚本里如果漏了21错误输出不会写进日志文件到时候进程没起来你查日志发现只有几行空内容排查起来非常痛苦。我建议所有后台脚本统一把 stdout 和 stderr 合并写入同一份日志文件。日志文件本身也要定期关心因为 dsh 的调试日志有时候会很大跑几天能把磁盘撑满。简单做法是在脚本里定期 truncate或者交给 logrotate 处理。这个不做强制要求但我见过太多人后台跑着跑着磁盘满了所以提一句。实际上我有一次就是日志文件涨到 20 多 G直接把服务器磁盘占满dsh 连写日志都写不动整个服务卡死。从那以后我养成了定期清理日志的习惯。3.5 命令行参数分离别把启动方式写死你可能今天要启动 dsh web明天要启动 dsh 跑特定插件后天又要连远程环境。如果我把 dsh 的完整命令写死在脚本里换场景就得改脚本。所以我把命令拆成可配置变量DASH_ARGS${DASH_ARGS:-dsh web} nohup $DASH_ARGS $LOGFILE 21 这样启动不同模式可以这样用DASH_ARGSdsh web --port 3080 ./start-dsh.sh DASH_ARGSdsh dev ./start-dsh.sh安装插件市场则用dsh plugin --profile web add dshmarket当然shell 里变量展开再执行涉及引号问题如果参数特别复杂建议直接用函数或数组。脚本简单化维护的人才不累。我自己的习惯是对于开发模式、web 模式、评测模式各写一份独立的配置文件启动脚本读取配置文件里的参数这样团队协作时不会互相覆盖。4. 更进一步用 systemd 把 dsh 做成常驻服务4.1 为什么服务化更省心脚本方案最大的弱点是没有“自愈”能力。dsh 进程因为段错误、OOM 被杀、系统重启都不会自动恢复。如果你是想把 dsh 长期部署在服务器上比如给团队开一个 dsh web 管理界面那我建议你直接上 systemd。systemd 的好处开机自启服务器重启不用人管Restartalways能在进程异常退出后自动拉起journalctl统一收日志查错方便停止、重启、看状态都是标准命令团队协作零成本。这就像你把一个应用从“手动双击运行”升级成了“系统服务”体验完全不一样。4.2 写一个 dsh.service 单元文件以 Linux 为例在/etc/systemd/system/dsh.service写入[Unit] DescriptionDeepSeek Harness Service Afternetwork-online.target Wantsnetwork-online.target [Service] Typesimple Useryour_user WorkingDirectory/home/your_user/.dsh EnvironmentPATH/usr/local/bin:/usr/bin:/bin ExecStart/usr/local/bin/dsh web --host 0.0.0.0 --port 3080 Restartalways RestartSec5 StandardOutputjournal StandardErrorjournal LimitNOFILE65536 [Install] WantedBymulti-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 插件运行时需要访问外部 APIAPI Key 通常通过环境变量传入。在 systemd 服务里写环境变量有几种方式最简单的是直接在 [Service] 段里加EnvironmentOPENAI_API_KEYxxx EnvironmentDSH_CONFIG_DIR/etc/dsh注意不要在 ExecStart 里写一堆 exportsystemd 不认 shell 语法。这是很多人从脚本迁移到 systemd 时最容易踩的坑。另外如果你的 dsh 与外部服务有依赖可以在 [Unit] 段加 After 和 Requires让 systemd 按顺序启动服务。如果你跑 dsh 的插件需要访问 Mysql、Redis 这类持久化组件把依赖关系写清楚能避免不少启动时序问题。4.4 崩溃自愈和资源限制Restartalways的含义是无论什么原因退出都尝试拉起。配合RestartSec5可以避免进程启动失败后疯狂重启打满 CPU。如果你想精确限制只有非正常退出才自愈可以用Restarton-failure这个更讲究一些实际使用中需要根据自己的场景选。如果你的 dsh 需要处理大量并发插件任务建议在 [Service] 段加LimitNOFILE65536这是在放宽文件描述符限制否则高并发时可能碰到 too many open files 错误。这个错误的特点是dsh 刚开始跑正常跑着跑着突然某个插件报错看日志全是文件描述符不足。我刚遇到时还以为是 dsh 的 bug后来查系统日志才发现是 ulimit 限制。5. 常见问题与排查技巧实录5.1 dsh web 提示 authentication required重新打开 URLdsh 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.log5.2 端口 3080 报 listen EACCES: permission denieddsh 默认监听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想删除某个历史对话先列出会话再删除指定 IDdsh conversation list dsh conversation delete conversation_id命令可能因为你装的版本不同而略有差异以dsh --help输出为准。6. 我这套方案用了半年之后的几点心得6.1 日志是你最好的朋友后台运行之后你再也看不到 dsh 的前台输出日志文件就是唯一的信息来源。不夸张地说我排查 dsh 后台问题的 90% 时间都在翻日志。所以脚本里一定把日志路径固定下来并且在启动成功或失败时明确打印日志位置不然过两天你自己都忘了进程跑在哪。6.2 优先 SIGTERM不要无脑 kill -9dsh 的会话和任务状态如果没落盘强制杀掉就可能丢进度。我只在 SIGTERM 等 10 秒无效时才用 -9。这个习惯不仅适用于 dsh也适用于你管理的所有长任务服务。如果你在调试时确实需要快速清场可以先用脚本正常停止不行再强杀但心里要清楚强杀的代价。6.3 定期检查端口和进程数后台进程最怕堆积。同一个 dsh 端口被多个残留实例抢占、PID 文件里是旧进程这些问题都是日志看不出来的。我养成的习惯是每周末看一眼ps aux | grep -c dsh lsof -i :3080数量对不上就手动清理。这个动作成本很低但能避免很多“莫名其妙”的问题。最后再分享一个小技巧把 start-dsh.sh 和 stop-dsh.sh 加上执行权限然后在~/.bashrc里加两个 aliasalias dsh-start~/bin/start-dsh.sh alias dsh-stop~/bin/stop-dsh.sh以后敲 dsh-start、dsh-stop 就能一键管理后台服务。这就是我目前最顺手的 dsh 后台运行思路日常用脚本长期部署用 systemd。如果你也刚被“终端关闭任务中断”坑过建议先把我这套 start/stop 脚本跑起来五分钟后就能告别这个问题。