- 人工智能
- AI Agent
- Agent 框架
- DeepSeek
【免费下载链接】deepseek-harness
DeepSeek Harness: Everything is a Plugin.
dsh启动器只解析属于自己的参数(--profile、--patch、配置 dump),并把自身 flag 之后的一切原样交给引导起来的配置树;新包@deepseek-ai/dsh-cmdline提供ctx.cmdlineArgs/ctx.appExit/ctx.appReady三个启动器事实,让任何普通应用插件用自己的 commander program 解析 flag,并把解析结果作为应用自有服务发布。读完本文,你将理解这套「按位置切分命令行、由组合包持有应用 flag、由 Loader 持有求值顺序」的设计全貌,并能在树外插件中为已安装的 profile 新增一个 flag 而无需改动启动器。
背景与问题:profile 落地之后,组合可安装,命令行却不能
DeepSeek Harness 以 profile 为单位组织启动配置:每个 profile 是一个目录,其下cordis.yml是空根配置,真正的配置树由多层 patch 叠加而成(bundle 层 → profile 用户层 →$DSH_HOME级用户层 →--patch覆盖层 → telemetry 开关层)。profile 落地后,组合(composition)可以通过dsh plugin --profile <name> add <package>安装,但组合的命令行却无法被应用接管:
- 旧的
apps/cli仍自己声明 Web flag 家族(--host、--port、--dev、--workspace-root、--trusted-host)和一次性任务位置参数,再为硬编码的行 id(webserver、api-gateway、connection、web-runtime)派生 patch——启动器成了「flag 目标行」的知情人; - 树外应用(如 turtle-ui)能贡献行,却无处接受一个 flag:
dsh --profile tui --resume <session>没有地方可供解析; dsh --profile web --help打印的是启动器的 help,而不是 web 应用的 help。
也就是说,命令行本身被启动器垄断,应用的 flag 与它配置的行被拆散在了两处。
核心决策:按位置切分,启动器不认识的第一个 token 就是应用参数的起点
解决的方案是位置化切分:启动器只解析属于自己的部分(--profile、--patch、配置 dump),把自己 flag 之后的一切原样交给引导起来的配置树。切分点由位置决定——启动器不认识的第一个 token 就是应用参数的起点。
这一语义在 apps/cli/src/args.ts 中由 commander 的四个选项组合实现:
program .name('dsh') .version(version, '-V, --version', 'output the version number') .helpOption(false) // 应用拥有 -h:启动器关闭自己的 help 选项 .allowUnknownOption() // 不认识的 token 不再报错,而是成为内层参数起点 .passThroughOptions() // 选项透传:--patch 不吞掉后面的内层参数 .enablePositionalOptions() // 位置参数之后的选项原样透传 .argument('[args...]', 'arguments for the booted profile\'s app (see: dsh --profile <name> --help)') .option('--profile <name>', 'the profile under $DSH_HOME/profiles to boot') .option('--patch <path>', 'extra patch-list overlay applied after the profile layer (repeatable)', collect) .option('--dump-config', 'print the composed profile tree and exit') .option('--dump-default-config', 'print the profile tree without its user layer or --patch overlays and exit')其中collect是一个非变参的单值收集器(--patch a.yml --patch b.yml逐个收集),注释明确写道「变参的--patch会吞掉内层参数」——这保证了--patch不会把应用自己的参数误收走。
于是:
dsh --profile tui --resume abc引导 tui profile,并把['--resume', 'abc']原样交给配置树;dsh --profile web -h打印的是 web 应用的 help;- 裸的
dsh -h没有可交付的应用,仍打印启动器自己的 help(见 args.ts 中.action内对-h/--help的处理); web是--profile web的硬编码别名,plugin子命令负责把剩余参数转发给 profile 目录里的 pnpm。
在 apps/cli/src/bin.ts 中,parseDshArgs把 argv 解析成三种 invocation:profile、plugin、dump-config,其中 profile 模式的args字段就是「启动器 flag 之后的一切」。
@deepseek-ai/dsh-cmdline:持有交接的契约
新包@deepseek-ai/dsh-cmdline持有这次交接,源码位于 packages/boot/cmdline/src/index.ts。它的核心是provideCmdline:
export function provideCmdline(ctx: Context, host: CmdlineHost): void { const snapshot: readonly string[] = Object.freeze([...host.args]) ctx.provide('cmdlineArgs', { get: () => snapshot }) ctx.provide('appExit', host.exit) if (host.ready !== undefined) ctx.provide('appReady', host.ready) }启动器在任何条目挂载之前调用它,向宿主上下文注入三个事实:
| 服务 | 接口 | 语义 |
|---|---|---|
cmdlineArgs | get(): readonly string[] | 启动器 flag 之后的一切,按 argv 顺序、不可变快照(Object.freeze冻结副本);无参数时为空数组 |
appExit | (code: number) => void | 请求有边界的进程退出,由启动器接线到它的 shutdown 控制器 |
appReady | onReady(listener) | 成功启动信号,由启动器在 boot 与宿主设置全部成功后才提交 |
这三个服务通过模块扩充(declare module '@deepseek-ai/cordis')挂到Context上。在 apps/cli/src/profile-boot.ts 的runProfile中,provideCmdline(hostCtx, { args: options.args, exit: code => void shutdown.shutdown(code), ready: appReady.service })在boot()回调内、配置树条目挂载前完成注入——这也是注释强调的「plugins resolve all launch-time environment values from the same immutable provenance snapshot」的一部分。
解析侧对应parseCmdline:
export function parseCmdline(ctx: Context, program: Command): void { const args = ctx.get('cmdlineArgs') const exit = ctx.get('appExit') if (args === undefined || exit === undefined) { throw new Error(`${program.name()}: the launcher must provide ctx.cmdlineArgs and ctx.appExit before the tree mounts`) } if (!hasAction(program)) { /* 无 action 的程序会解析成功却什么都不发布 */ } configureExitAndOutput(program) // 递归为整棵命令树配置 exitOverride 与输出路由 try { program.parse(args.get(), { from: 'user' }) } catch (error) { if (!isCommanderError(error)) throw error exit(error.exitCode) // help、version、解析错误、program.error 统一走 appExit } }两个细节值得注意:
- 结构化识别 commander 控制流错误:
isCommanderError不依赖instanceof CommanderError,而是检查code是否以commander.开头且exitCode是数字。原因是树外插件会带来自己的一份 commander 副本,类身份因此不同,instanceof会把已经打印出来的--help重新抛成致命的加载失败。 - 递归配置输出:commander 只在注册时把
exitOverride与输出配置复制给子命令,因此configureExitAndOutput必须递归整棵命令树,否则已注册子命令的拒绝会绕过ctx.appExit直接写进程流并调用process.exit。
parseCmdline只跑同步 action:成功的解析在 action 内发布服务;help、version、语法拒绝时 action 不运行。hasAction守卫确保程序真的声明了 action,避免「解析成功、什么都没发布、依赖行因服务缺失而 pending」的静默失败。
组合包持有应用 flag:dsh-web-app与dsh-headless
已交付的各应用把 flag 搬进了组合包。Web 家族由 packages/bundle/web-app/src/startup.ts 的web-startup插件持有:
export const name = 'web-startup' export const inject = ['cmdlineArgs'] export const WEB_STARTUP_SERVICE = 'webStartup' function webCommand(): Command { return new Command() .name('dsh --profile web') .description('Serve the DeepSeek Harness browser UI.') .helpOption('-h, --help', 'show this help') .option('--host <host>', 'bind host') .option('--no-open', 'do not open the Web UI in the default browser') .option('--port <port>', 'listen port; pass 0 to let the OS pick a free one') .option('--trusted-host <authority...>', 'extra authority the /api browser-trust fence accepts (host or host:port; repeatable)') }它的 action 校验取值后把不可变结果发布为webStartup服务(openBrowser、host、port、trustedHosts),其中--host 0.0.0.0会被拒绝——「为了安全,暂不支持:它会把远程代码执行暴露到网络;请用 127.0.0.1」。
一次性任务位置参数由 packages/bundle/headless/src/startup.ts 的headless-startup插件持有:[task...]位置参数用空格 join 成任务文本,空任务按用法错误拒绝,action 发布headlessStartup服务({ task }),供一次性 runner 行消费。
这两个提供方行都不携带启动器标记或特殊类型——它们是普通应用插件,inject: ['cmdlineArgs'],用自己的 commander program 调parseCmdline,在 action 中把解析出的取值作为应用自有服务提供出去。启动器完全不检查组合中的所有者:多个插件可以读取同一份不可变快照,没有读取方的 profile 会忽略自己的应用参数(flag 无人消费,直接丢弃)。
由提供方配置的行注入其服务,并在惰性配置表达式中直接读取它,例如port: !!js ctx.webStartup.port ?? 3080——flag 胜过写在它旁边的值(用户 patch 中的默认值),且没有任何东西被写回任何一行。这与曾被否决的「把解析出的取值写进每一行」方案形成鲜明对比。
为什么由 Loader 持有顺序:三条框架事实
「启动器只提供 argv 与进程生命周期服务、Loader 只挂载一次组合」这套分工,由三条框架事实塑造(文档称之为 "Why Loader owns the ordering"):
- profile 的各行位于根 include 的
patches选项内部。Include 声明了EntryGroup.key树载体标记(与 Group 相同),因此 Loader 让它的配置——条目与 patch 列表,包括 Include 自己的path——保持字面值,而不是在 Include 上下文中递归求值嵌套的!!js节点;每个表达式都在其目标行的 fiber中解析。 - Cordis 只在所有声明的注入都已激活后才激活 fiber。每次激活前一刻,Cordis 会基于 fiber 自身上下文运行
internal/configwaterfall;Cordis 快照注入服务之后,Loader 的监听器再插值原始配置。因此--help会让提供方服务保持缺失,依赖行永不激活。 - 提供方替换与 HMR 必须保持相同契约。fiber 重新激活时会重跑 waterfall,HMR 会把原始配置带给替换 fiber,而待处理行可以接受选项变更,不会针对缺失服务提前求值表达式。活动 patch 重载会针对仍然在线的服务再次插值,所以已经服务中的端口不会被悄悄重置。
依赖顺序因此仍由 Cordis 激活与 Loader 插值流程处理:各行保留自己的inject和配置,Loader 只挂载一次整套组合,启动器只提供 argv 与进程生命周期。
文档还指出两条并发后果:
- Loader 会并发挂载兄弟行,一行可能已激活而另一行仍在挂载,或整次 boot 正在回滚;所以 Web 组合包只会在自身的 Loader 配置树结算后公布 URL(不能在插值完成前发布)。
- Web 组合包的运行时插件也持有 harness 源码提示词段,因此
dsh web与dsh --profile web无需 Web 专用启动器设置即可按完全相同的方式启动。
曾考虑的替代方案:为什么被否决
原文档记录了一次完整的架构决策过程,六种替代方案各有明确否决理由:
- 把解析出的取值写进每一行(逐行一次配置更新,外加交还给启动器的一层 patch,使重载无法撤销它):能工作,但 patch 在应用与启动器之间来回传递、同一件事有两套机制、回收重建的正确性依赖 Loader 重启内部细节。维护者否决了这次往返,供各行读取的服务取代了这一切。
- 通过清空行的
inject来放行:孤立测试可行,在真实 web 树上失败——清空inject恰恰会丢失插件的静态注入,而且失败是静默的,直到插件去读它声明过的服务才暴露。 - 由启动器管理两趟挂载:可以让提供方先于读取行激活,但会重复组合、把顺序变成启动器职责,还掩盖了 Loader 的缺陷——嵌套表达式在 include 上下文而不是目标行的注入上下文中求值。
- 由启动器在 boot 之前运行每个组合包的命令函数(完全不经过 Cordis):严格早于「先 boot 再 help」,但这让应用启动成为配置树之外的第二套插件协议;使用注入
cmdlineArgs的普通提供方只保留一套协议,并且仍可 dump、可 patch。 - 由启动器强制指定命令行所有者:拒绝零个或多个读取方可以裁决
-h等重叠项,但get()是不可变读取,普通组合也可能需要多个应用自有服务;因此插件共享该快照,并通过普通组合持有各自解析器的交互。 instanceof CommanderError:树外插件带来自己的一份 commander 副本,类身份不同,已打印的--help会被重新抛成致命加载失败;改为按结构识别commander 的控制流错误。
后果与使用边界
这套设计带来了一系列可观察的行为约束,其中前两条是设计目标本身:
- 应用的 flag、help 文本和用法错误与它们所配置的行放在一起;给已安装的插件加一个 flag不需要改动启动器——turtle-ui 以同样方式获得
--resume <session>/--session <id>,正是这套设计的真正验证。 - 启动器完全不识别任何应用行:telemetry 行仍是它唯一的组合探测(用于环境开关),SIGTERM 在所有 surface 上以 0 退出,每次启动都监视用户 patch 层,一次性 runner 像任何应用一样经
ctx.appExit退出。信号处理与有界关闭的具体实现在 apps/cli/src/process-shutdown.ts(createProcessShutdown:5 秒宽限、正常完成与信号中断合并、重复信号升级为强制退出)。 --help会让所有依赖提供方服务的行保持待处理并请求有边界的退出;无关行可能在拆除前并发激活。- 应用自有服务没有静态声明的提供方:交付了消费行却缺少对应提供方的组合包会在结算时失败,报出指向该服务的待处理条目,而不是在加载时失败。
- 用户 patch 若整体替换某行的
config,会连同其中的表达式一起丢掉,该行上 flag 的优先级也随之消失。 - 启动器的 flag 必须写在应用参数之前;如果应用的第一个参数恰好等于
web或plugin,会选择对应的子命令;-V/--version在该边界之前仍归启动器持有;而且启动器的解析器会消耗一个--,因此要给应用传一个字面量--需要写成-- --。 --dump-config从不运行应用命令行提供方,因此它在任何应用参数被解析之前打印组合,并拒绝携带应用参数的调用(见 args.ts 中resolveBoot对args.length > 0的报错)。
总结:一条协议,取代两套机制
ctx.cmdlineArgs架构的实质是:把命令行所有权从启动器移交给组合本身。启动器负责位置切分与三个进程级事实(argv 快照、有界退出、就绪信号),应用插件负责用自己的 commander program 解析自己的 flag 并把结果发布为普通服务,Loader 与 Cordis 负责按注入依赖确定求值顺序。应用启动不再是配置树之外的第二套插件协议——它仍然是可 dump、可 patch、可 HMR 的普通组合,只是多了一个所有插件都能注入的cmdlineArgs。对插件开发者而言,交付一个带新 flag 的应用就是交付一个普通组合包:inject: ['cmdlineArgs']、parseCmdline(ctx, program)、在 action 中ctx.provide(...),然后任何依赖行都可以用惰性!!js表达式读取它。
进一步阅读:核心契约见 packages/boot/cmdline/src/index.ts 与 packages/boot/cmdline/tests/cmdline.spec.ts;启动器侧见 apps/cli/src/args.ts、apps/cli/src/bin.ts、apps/cli/src/profile-boot.ts;两个已交付应用见 packages/bundle/web-app/src/startup.ts 与 packages/bundle/headless/src/startup.ts。
- 人工智能
- AI Agent
- Agent 框架
- DeepSeek
【免费下载链接】deepseek-harness
DeepSeek Harness: Everything is a Plugin.
相关推荐
DeepSeek Harness Desktop 启动资源所有权:DesktopStartupGeneration 生命周期治理架构解析
DeepSeek Harness Desktop 启动资源所有权:DesktopStartupGeneration 生命周期治理架构解析 导读 本文围绕 dee
人工智能AI 应用AI Agent桌面应用插件系统DeepSeekdsh-plugindeepseek-harness-desktop 崩溃证据生命周期所有权:active-run marker 的深度重构实践
deepseek harness desktop 崩溃证据生命周期所有权:active run marker 的深度重构实践 导读 本文基于 deepseek
人工智能AI 应用AI Agent桌面应用插件系统DeepSeekdsh-pluginDeepSeek Harness Desktop 生命周期证据代际所有权机制深度解析
DeepSeek Harness Desktop 生命周期证据代际所有权机制深度解析 本文围绕 DeepSeek Harness Desktop(DSH 桌面端
人工智能AI 应用AI Agent桌面应用插件系统DeepSeekdsh-plugin
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考