dsh 是个 launcher。它先解析自己的 flag——--profile、--patch、--dump-config、--dump-default-config、--help——遇到第一个不认识的 token,之后全部交给它启动的 app。dsh web 是 --profile web 的简写;dsh --profile headless "任务" 跑一次就退出;dsh plugin --profile <名字> 转发给 pnpm。
dsh 是 launcher,不是应用。它解析一个 profile、组装出插件树、然后启动这份组装描述的东西。CLI 里几乎
所有让人困惑的行为都源于这一点——尤其是参数顺序那条规则。
四种入口
| 入口 | 调用方式 | 行为 |
|---|---|---|
| Profile | dsh --profile <名字> | 启动 $DSH_HOME/profiles/<名字> 下的 profile |
| Web | dsh web | 等价于 --profile web |
| Headless | dsh --profile headless "任务" | 执行单次会话,输出结果,终止 |
| 插件管理 | dsh plugin --profile <名字> <pnpm 参数> | 转发给 pnpm,管理该 profile 的插件 |
Launcher flag
| Flag | 用途 |
|---|---|
--profile <名字> | 加载哪个 profile |
--patch <文件> | 应用配置覆盖——最后一层、优先级最高的 patch |
--dump-config | 打印当前组装后的配置 |
--dump-default-config | 不初始化 profile,打印组装出的树 |
--help | launcher 自己的帮助 |
--port | 透传给启动的 app,不被 launcher 消费 |
--port 列在这里但要加注,因为它是最常被写错的那个:它属于 app,所以必须排在 launcher 的 flag 后面。
参数顺序规则
launcher 先解析自己的 flag,遇到第一个它不认识的 token,之后全部算 app 的参数序列。
dsh --profile web --port 3100 # 正确--port 之前的都是 launcher 的词汇,--port 3100 交给 web app。反过来写,launcher 会在 --port
处停止解析,于是 --profile web 被当成 app 不认识的参数丢过去。
在脚本里这比交互式更要命,因为变量展开为空会让那条分界线整体位移。把变量加引号。
退出码
无效命令、属于别的模式的选项、配置错误和启动失败,一律非零退出。
这个保证比听上去更强。自动化 agent 里你真正怕的不是崩溃,而是「悄悄成功但什么都没干」。一个放错位置 的 flag 会让 launcher 大声失败,意味着坏掉的流水线在第一次运行就暴露,而不是第十次。
if ! dsh --profile headless "$JOB"; then
echo "harness 执行失败" >&2
exit 1
fi路径与环境变量
| 路径 | 内容 |
|---|---|
$DSH_HOME | harness home 目录,你所有覆盖的根 |
$DSH_HOME/profiles/<名字> | 一个 profile:bundle 列表、out-of-tree 插件、patch 文件 |
$DSH_HOME/profiles/<名字>/cordis.patch.yml | profile 级 patch 层 |
$DSH_HOME/cordis.patch.yml | home 级 patch 层——对所有 profile 生效,且压过 profile 自己那份 |
$DSH_HOME/settings.yaml | 自定义服务商与模型覆盖 |
$DSH_HOME/.credentials.yaml | 凭证,单独存放,好让 settings.yaml 可以共享 |
检视组装结果
dsh --profile web --dump-config打印四层全部应用后、每一行可被 patch 命中的配置——bundle、profile patch 文件、home patch 文件、
--patch。行为和预期打架时,以这份输出为准,而不是你的源文件。
dsh --profile web --dump-default-config不初始化 profile,打印组装出的树。两者 diff 是看清自己改了什么最快的办法。
管理插件
dsh plugin --profile web add some-cordis-plugin
dsh plugin --profile web remove some-cordis-plugin子命令把参数转发给 pnpm,所以 pnpm 全套词汇可用,含版本号。插件装进 profile 自己的
node_modules——这也是两个 profile 能各持互不兼容插件集而互不干扰的原因。
bundle 的解析顺序:先 dsh 安装目录,再 profile 的 node_modules。
速查
# 交互式浏览器 agent
dsh web
# 同一件事,显式写法,换端口
dsh --profile web --port 3100
# CI 用的单次执行
dsh --profile headless "总结失败的测试"
# 我到底在跑什么?
dsh --profile web --dump-config
# 干净 profile 长什么样?
dsh --profile web --dump-default-config
# 不动任何文件试一个改动
dsh --profile web --patch ./experiments/strict-approval.yml
# 只给一个 profile 装插件
dsh plugin --profile web add some-cordis-plugin常见问题
为什么 flag 的顺序有影响?
launcher 先解析自己的 flag,遇到第一个不认识的 token 就把后面全部当作 app 的参数。所以 --profile 这类 launcher flag 必须排在 --port 这类 app flag 前面。
--dump-config 和 --dump-default-config 有什么区别?
--dump-config 显示当前配置状态,即你的 patch 层生效之后的结果。--dump-default-config 不初始化 profile,显示干净基线。两者 diff 一下就是你改了什么。
dsh 失败会非零退出吗?
会。无效命令、属于别的模式的选项、配置错误和启动失败,一律非零退出——这正是它能安全地被脚本包起来的原因。