结论:能塞进脚本。答案走 stdout,诊断走 stderr,失败时 0.6 秒退 1,不会挂着不动。

前两期看过它怎么装、怎么跑任务,这期只回答一个问题:它的无头模式能不能像 curl 一样,被脚本直接调用、把结果接走。答案是能,但得认准两条规矩。

本次实测

环境:WSL2(Ubuntu)、Node 22.23.2、dsh 0.2.0-rc.2。全部在 /tmp/dsh_20261009/ 沙箱里跑,每条命令都套 timeout 保护。

六个检查点,一次跑完:

# 测试项 命令要点 结果 耗时
1 stdout 与 stderr 是否分离 ... > out 2> err exit 0;stdout 108 字节、1 行(只有答案),stderr 31 字节(推理) 6.0 s
2 任务从 stdin 传入 echo "任务" \| dsh --profile headless exit 0,文件正常落盘 6.8 s
3 机器可读事件流 --json stdout 8 行 JSON,末行 {"type":"final","text":"好"} 2.8 s
4 密钥无效 DEEPSEEK_API_KEY=sk-invalid exit 1,stderr 一行 AUTH 报错 0.56 s
5 超时硬杀 timeout 3 dsh ... exit 124,进程数未增长 3.3 s
6 无任务 + 关 stdin dsh --profile headless < /dev/null exit 1,stderr a task is required 1.5 s

答案是答案,日志是日志。成功时 stdout 只有最后一句答复,推理过程全在 stderr;两路分开写,日志单独存、结果单独取,互不干扰。日志的体积还很飘:同样是建一个文件,两次运行的 stderr 一次 31 字节,一次 983 字节。这一点直接决定了它能不能进管道。

--json 是给脚本用的口子。一次运行被拆成 8 行事件(thinking / text / status / session / final),取末行 final 的 text 字段就是结果,不用去猜文本格式。

失败快,而且带状态码。密钥无效 0.56 秒退出、退出码 1;超时被杀是 124;缺任务 1.5 秒退 1。三种情况都不挂起,适合放进 cron 或流水线。

一个坑:两路一合,答案就脏了

现象:图省事,把两路输出合并成一份:out=$(dsh --profile headless "任务" 2>&1)。那份"结果"里会混进推理,一次文件创建任务实际返回 169 字节,其中两行以 dsh: reasoning: 开头,真正的答复只在最后一行。下游要是按行取,很容易把思考文本当成答案。

复现:

sh combined=$(dsh --profile headless "创建 /tmp/dsh_20261009/c.txt,内容一行:x" 2>&1) printf '%s\n' "$combined"

解法:两路分开,并且把退出码一起管住。可直接抄这三行:

sh set -euo pipefail ans=$(timeout 280 dsh --profile headless "$TASK" 2>/dev/null) echo "结果:$ans"

2>/dev/null 把推理挡在门外,timeout 兜住偶发的长任务,set -e 让脚本在 dsh 退出码非 0 时当场停下;赋值里的命令失败就会触发它,这一步我单独验过。

还有第二层:别只看 stdout、不看退出码。密钥无效时 stdout 是空的。一个只判断"输出非空"、或者压根不查 $? 的脚本,会把一次失败当成一次空答复,默默放过去。取到答案之前,先确认退出码是 0。

一句话总评

4.5 / 5,只评"能不能进流水线"这一项。 分离输出、退出码、超时、stdin、JSON 都按 Unix 的老规矩给齐了;剩下的雷都在写脚本的人手上:合并输出、不查退出码,是这次要记住的两件事。

测试环境:WSL2(Ubuntu)、Node 22.23.2、dsh 0.2.0-rc.2。命令、输出片段、耗时、退出码均为本机实测。

文 / 程实