结论:能塞进脚本。答案走 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。命令、输出片段、耗时、退出码均为本机实测。
文 / 程实
广告


