DeepSeek Harness 的定位与使用场景
DeepSeek Harness 是一个以 profile 和插件组合为核心的开发者预览运行时:通过统一的 dsh 启动器选择运行模式,将多个 bundle 的 patch 层按顺序组合,再叠加用户覆盖配置,并把应用自身的参数交给对应 profile 处理。
Purpose and Scope
本文说明 DeepSeek Harness 的整体定位、dsh CLI 的入口模型、profile 组合方式,以及 Web、headless、SDK、SDK minimal、ACP 和插件管理等主要使用场景。重点是“如何选择并启动一个 Harness 运行时,以及配置和插件如何进入该运行时”。
本文不展开某个具体插件的业务实现、Web UI、Agent 协议细节、Desktop Electron 宿主或底层存储实现;这些属于相应的 profile、插件和宿主页面。需要了解具体层级的精确 flag 和启动失败矩阵时,应进一步参阅 CLI 行为参考和 app-boot 文档。
Overview
Harness 的基本抽象不是一个固定功能的单体应用,而是一组可组合的运行时 profile。调用者首先使用 dsh <profile> 或 dsh --profile <profile> 选择要启动的 profile;启动器只负责解析自身拥有的参数,例如 profile、patch overlay 和配置 dump。第一个无法被启动器识别的 token 标志着应用参数的开始,后续参数原样交给已启动的 profile,因此 Web、TUI 或 headless 应用可以拥有自己的参数空间。
profile 目录由 package.json、dsh.profile manifest、按顺序排列的 bundles 和用户自己的 cordis.patch.yml 组成。组合过程从空配置树开始,依次应用 bundle patch、profile patch、$DSH_HOME/cordis.patch.yml,最后应用命令行 --patch overlay。这个模型让同一套 base 能够服务多个入口,同时保留每个 profile 的模式差异和用户定制能力。
从使用场景看:
- 交互式应用:
dsh web启动 Web profile;其他已安装 profile 可以接收自己的应用参数。 - 一次性自动化任务:
dsh --profile headless "run the tests"创建持久化会话,打印最终答案并退出。 - 集成客户端:
dsh --profile sdk通过 JSON-RPC stdio 服务 SDK 客户端;sdk-minimal使用独立的极简配置树。 - 自动化协议接入:
dsh --profile acp通过 ACP stdio 持续服务,直到客户端断开。 - 运行时扩展:
dsh plugin --profile <name> <pnpm args>在 profile 目录中转发 pnpm 参数,管理树外插件。 - 配置诊断:
--dump-default-config、--dump-config和--dump-config-schema在不启动 profile 的情况下检查组合结果或 schema。
Architecture
Sources: args.ts, README.zh.md
图中的边界来自 CLI 的实际职责划分:parseDshArgs 产生四类已解析 invocation;profile 启动需要组合 profile 目录中的 manifest、bundle 和 patch,而应用参数不由启动器解释。desktop 是保留名称,CLI 不允许普通启动和配置 dump 管理它;Desktop 宿主拥有该 profile 的生命周期。
组合层的共享 base 可以进一步概括为:
Source: composition.md
该组合关系由 composition.md 的定位和 bundle patch 图支持:dsh-base 是 web、headless、sdk、acp 共享的基础组合,模式 bundle 和用户层覆盖它;sdk-minimal 则维护独立配置树。
Sources: composition.md, README.zh.md, args.ts
启动器职责与参数边界
parseDshArgs 的职责
parseDshArgs 是 CLI 的边界解析器,而不是应用参数解析器。它使用 Commander 配置 --profile、--from-default-profile、--patch 和三种 dump 操作;同时启用未知选项放行和 pass-through,使第一个不属于启动器的 token 及其后续内容保留在 args 中。这样设计的关键原因是不同 profile 的应用可以拥有互不冲突的 flag 集合,启动器无需知道每个应用的参数协议。
1interface ProfileInvocation {
2 mode: 'profile'
3 profile: string
4 fromDefaultProfile?: string | undefined
5 patches: string[]
6 args: string[]
7}
8
9interface DumpConfigInvocation {
10 mode: 'dump-config'
11 profile: string
12 fromDefaultProfile?: string | undefined
13 defaultOnly: boolean
14 patches: string[]
15}
16
17interface DumpConfigSchemaInvocation {
18 mode: 'dump-config-schema'
19 profile: string
20 fromDefaultProfile?: string | undefined
21 patches: string[]
22}
23
24interface PluginInvocation {
25 mode: 'plugin'
26 profile: string
27 args: string[]
28}
29
30export type DshInvocation = ProfileInvocation | DumpConfigInvocation | DumpConfigSchemaInvocation | PluginInvocationSource: args.ts
这组 discriminated union 将“启动 profile”“dump 配置”和“管理插件”明确分开。调用方可以根据 mode 在后续阶段选择 boot、只读诊断或 pnpm 操作,避免把配置检查误当成一次真实启动。
参数解析规则
1const collect = (value: string, previous: string[] = []): string[] => [...previous, value]
2
3function selectProfile(value: string, previous?: string): string {
4 if (previous !== undefined) throw new InvalidArgumentError('select a profile only once')
5 return value
6}
7
8function rejectElectronProfile(program: Command, profile: string): void {
9 if (profile.toLowerCase() === 'desktop') {
10 program.error('error: profile "desktop" is managed exclusively by the Electron application')
11 }
12}Source: args.ts
--patch 是可重复的单值选项,而不是 variadic 选项;这避免它吞掉后续应用参数。profile 只能选择一次。desktop 的拒绝则体现了宿主边界:Desktop profile 必须由 Electron 应用管理,普通 npm CLI 不能把它当作普通 profile 启动。
Core Flow
一次 profile 启动、配置 dump 或插件操作的分流可以表示为:
Sources: args.ts, README.zh.md, plugin.ts
resolveBoot 中的关键顺序是:先收集 patch,校验空路径和模板名,再检查 dump flag 是否互斥;没有 dump 时才产生 ProfileInvocation。dump 模式明确禁止 app args,因为 dump 不会挂载应用的命令行 provider,允许它们存在会造成“dump 结果”和真实启动不一致。
1const dumps = [options.dumpConfig, options.dumpDefaultConfig, options.dumpConfigSchema].filter(Boolean)
2if (dumps.length === 0) {
3 return { mode: 'profile', profile, fromDefaultProfile: options.fromDefaultProfile, patches, args }
4}
5if (dumps.length > 1) {
6 program.error('error: --dump-config, --dump-default-config, and --dump-config-schema are mutually exclusive')
7}
8if (args.length > 0) {
9 program.error(`error: config dumps take no app arguments, got ${args.map(argument => JSON.stringify(argument)).join(' ')}`)
10}Source: args.ts
Profile 组合与配置层
profile 初始化时,内置模板可在首次使用时创建;--from-default-profile 允许从随附模板创建一个新的非内置 profile。profile 的 bundle 解析先查找 DSH 安装目录中的官方 bundle,再查找 profile 自身 node_modules 中的 bundle,因此树外插件可以通过 profile 的包管理流程加入组合。
配置优先级从低到高为:
- 空根配置树。
dsh.profile.bundles中按声明顺序排列的 bundle patch。- profile 自身的
cordis.patch.yml。 $DSH_HOME/cordis.patch.yml。- 命令行按顺序提供的
--patchoverlay。
这种顺序使官方能力先建立基础,profile 再表达模式选择,用户层最后覆盖行为。dsh-hmr 启用时会监听 profile manifest、profile patch 与 home patch,并以统一串行重载重新组合层;未启用 HMR 时,修改要等重启生效。串行重载的意义是避免多份 patch 同时编辑时出现部分应用、部分未应用的中间状态。
使用场景与示例
启动不同运行时
以下命令来自 CLI 文档,展示入口选择和“启动器参数在前、应用参数在后”的约定:
1dsh web
2dsh --profile acp
3dsh --profile headless "run the tests"
4dsh --profile sdk
5dsh --profile sdk-minimal
6dsh --profile web --port 8080
7dsh --profile tui --resume <id>Source: README.zh.md, README.zh.md
其中 --port、--resume 和 headless 的任务文本都属于应用层输入;CLI 只负责识别 profile。SDK 和 ACP 是 profile,而不是另立的公开可执行命令,因此它们仍遵循同一个启动器和 profile 生命周期。
检查组合后的配置
dsh --profile web --dump-default-config
dsh --profile web --dump-config
dsh --profile web --dump-config-schemaSource: README.zh.md
--dump-default-config 只打印 bundle 层,不接受 --patch;--dump-config 观察包含用户层和 overlay 的组合树;--dump-config-schema 导入组合树中插件声明的 schema,打印 entry 和 patch 的 JSON Schema,而不是配置值。检查不受信任插件前应注意 schema dump 会导入插件声明。
管理插件
dsh plugin --profile tui add <package>
dsh plugin --profile <name> <pnpm args>Source: README.zh.md, plugin.ts
runPlugin 为插件操作构造带 execution: 'cli'、输出上限和锁等待时间的 PackageOperationOptions,普通 profile 通过 runPluginCommand 执行;Desktop profile 则在持有 package.json 文件锁的情况下调用 runProfilePnpm。因此 profile 包操作与并发启动之间具有明确的文件锁边界。
配置、兼容性与插件生命周期
配置选项
| 选项 | 类型 | 默认/约束 | 作用 |
|---|---|---|---|
--profile <name> | string | 必填(除非命令本身提供 profile) | 选择 $DSH_HOME/profiles/<name> 下的 profile;只能选择一次。 |
--from-default-profile <name> | string | 可选 | 在缺少自定义 profile 时,从随附模板初始化它。 |
--patch <path> | string[] | 空数组;可重复 | 在 profile 层之后按 argv 顺序应用额外 patch。 |
--dump-config | boolean | false;与其他 dump 互斥 | 输出完整组合 profile tree 并退出。 |
--dump-default-config | boolean | false;不接受 --patch | 只输出 bundle 层配置并退出。 |
--dump-config-schema | boolean | false;不接受 app args | 输出 profile entry 和 patch 的 JSON Schema,不挂载 profile。 |
dsh plugin ... | pnpm args | 由 pnpm 解释 | 在指定 profile 目录中安装、移除或管理插件。 |
CLI 自身的 help 和 version 也由启动器处理;profile 应用的 --help 在第一个应用参数开始后由应用插件处理。
版本兼容性批准
安装和 profile 启动会依据声明的 DSH peer 范围检查运行时版本。对不兼容插件的允许不是宽泛开关,而是精确绑定到 package version 与 DSH runtime version,并要求显式风险确认:
dsh plugin --profile <name> allow-version <package@version> --dsh-version <exact> --accept-risk
dsh plugin --profile <name> revoke-version <package@version> --dsh-version <exact>
dsh plugin --profile <name> version-exemptionsSource: plugin.ts
allow-version 会向 stderr 写入风险警告;批准记录在 profile 的兼容性信息中,并受 package.json 文件锁保护。这样做的设计意图是把“暂时接受不兼容”的决定变成可审计、可撤销且不会意外放大到其他版本组合的操作。
失败模式、边界条件与并发
参数和模式错误
- 缺少 profile 时,裸启动不会继续;只有
dsh -h或dsh --help这种无 profile 的帮助请求可以直接输出 launcher help。 - 重复
--profile会触发InvalidArgumentError。 - 空的
--patch或--from-default-profile值会被拒绝。 - 多个 dump flag 互斥;任何 dump 携带 app args 都会失败。
desktop由 Electron 专属管理,普通 CLI 会拒绝启动、dump 或不满足初始化条件的插件操作。
插件操作失败
1if (result.exitCode === 127) process.stderr.write('dsh: pnpm was not found; install pnpm and make it available on PATH.\n')
2for (const { name, version, runtimeVersion } of result.incompatible ?? []) {
3 process.stderr.write(`dsh: to accept the risk, run: dsh plugin --profile ${profile} allow-version ${name}@${version} --dsh-version ${runtimeVersion} --accept-risk\n`)
4}
5if (result.exitCode !== 0) process.stderr.write(`dsh: plugin command failed; diagnostics: ${result.logPath}\n`)Source: plugin.ts
退出码 127 被明确解释为找不到 pnpm;不兼容结果会输出精确的批准命令;其他非零退出则输出诊断日志路径。对于 git-hosted plugin,安装失败时还会提示 pnpm 的 allowBuilds 配置要求,而不是把构建批准静默完成。
锁与一致性
插件管理器与 dsh plugin 共享包操作和 profile 写锁。版本豁免路径使用 withFileLock(join(dir, 'package.json'), ..., { waitMs: 120000 });Desktop 的实际 pnpm 操作也在同一类文件锁中执行。锁等待上限为 120 秒,普通包操作还把 lookup timeout 设置为 120 秒。由此可以确认的并发保证是 profile 包元数据更新避免并发写入;源码材料没有证明运行中应用是否能无缝看到依赖变更,因此依赖更新后的生效行为应按 profile 重载或重启处理。
性能与运维注意事项
- 启动时组合层按声明顺序应用;bundle 数量和 patch 复杂度会影响配置构建成本,但当前材料没有提供基准数据。
- HMR 通过串行重载保持层组合的一致顺序;未启用 HMR 时修改需要重启,运维上应明确选择即时迭代还是稳定重启模型。
- 包操作会继承认证环境和终端描述符,并捕获诊断;service 调用则使用清理后的环境。
- 生产运行需要已构建的包和前端产物;仓库文档要求在根目录运行
pnpm run build,再使用pnpm dsh <args...>运行 TypeScript 入口。 SAFETY.md将 Harness 定义为尚未经过安全审计的 experimental developer-preview software;不应把它当作生产就绪软件或不可信 workload 的唯一安全控制。
Extension Points
Harness 的主要扩展点是 profile manifest、patch overlay 和 profile-local plugin package:
- 以随附模板初始化 profile,或在 profile 目录中维护自己的
dsh.profile。 - 通过
bundles声明组合包顺序。 - 使用 profile patch、home patch 和
--patch覆盖配置。 - 通过
dsh plugin安装树外插件,让插件进入 profile 的node_modules解析范围。 - 对有意接受的版本不兼容使用精确的
allow-version,并在不再需要时revoke-version。
这种扩展方式把官方 base 与用户定制分离:官方 bundle 提供共同能力,profile 和 patch 表达部署/模式差异,插件包提供额外能力。源码没有显示一个要求所有社区插件继承的单一基类;因此扩展时应以 profile manifest、插件声明 schema 和 peer version 约束为契约,而不是假设存在未在源码中出现的公共继承关系。
测试与验证边界
现有 CLI 文档指出,Web failure matrix 使用构建后的 CLI 验证启动失败、认证 HTTP 响应、诊断、恢复、进程退出和 dispose,并覆盖原生配置 HMR 的 awaitWriteFinish;Web best-effort startup 还覆盖必需依赖和端口冲突。这些测试关注启动/恢复契约,不调用模型 API。本文没有读取测试实现,因此不能进一步断言每个测试的断言细节。