Repository Wiki
deepseek-ai/deepseek-harness

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

Loading diagram...

Sources: args.ts, README.zh.md

图中的边界来自 CLI 的实际职责划分:parseDshArgs 产生四类已解析 invocation;profile 启动需要组合 profile 目录中的 manifest、bundle 和 patch,而应用参数不由启动器解释。desktop 是保留名称,CLI 不允许普通启动和配置 dump 管理它;Desktop 宿主拥有该 profile 的生命周期。

组合层的共享 base 可以进一步概括为:

Loading diagram...

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 集合,启动器无需知道每个应用的参数协议。

typescript
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 | PluginInvocation

Source: args.ts

这组 discriminated union 将“启动 profile”“dump 配置”和“管理插件”明确分开。调用方可以根据 mode 在后续阶段选择 boot、只读诊断或 pnpm 操作,避免把配置检查误当成一次真实启动。

参数解析规则

typescript
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 或插件操作的分流可以表示为:

Loading diagram...

Sources: args.ts, README.zh.md, plugin.ts

resolveBoot 中的关键顺序是:先收集 patch,校验空路径和模板名,再检查 dump flag 是否互斥;没有 dump 时才产生 ProfileInvocation。dump 模式明确禁止 app args,因为 dump 不会挂载应用的命令行 provider,允许它们存在会造成“dump 结果”和真实启动不一致。

typescript
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 的包管理流程加入组合。

配置优先级从低到高为:

  1. 空根配置树。
  2. dsh.profile.bundles 中按声明顺序排列的 bundle patch。
  3. profile 自身的 cordis.patch.yml。
  4. $DSH_HOME/cordis.patch.yml。
  5. 命令行按顺序提供的 --patch overlay。

这种顺序使官方能力先建立基础,profile 再表达模式选择,用户层最后覆盖行为。dsh-hmr 启用时会监听 profile manifest、profile patch 与 home patch,并以统一串行重载重新组合层;未启用 HMR 时,修改要等重启生效。串行重载的意义是避免多份 patch 同时编辑时出现部分应用、部分未应用的中间状态。

使用场景与示例

启动不同运行时

以下命令来自 CLI 文档,展示入口选择和“启动器参数在前、应用参数在后”的约定:

sh
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 生命周期。

检查组合后的配置

sh
dsh --profile web --dump-default-config dsh --profile web --dump-config dsh --profile web --dump-config-schema

Source: README.zh.md

--dump-default-config 只打印 bundle 层,不接受 --patch;--dump-config 观察包含用户层和 overlay 的组合树;--dump-config-schema 导入组合树中插件声明的 schema,打印 entry 和 patch 的 JSON Schema,而不是配置值。检查不受信任插件前应注意 schema dump 会导入插件声明。

管理插件

sh
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-configbooleanfalse;与其他 dump 互斥输出完整组合 profile tree 并退出。
--dump-default-configbooleanfalse;不接受 --patch只输出 bundle 层配置并退出。
--dump-config-schemabooleanfalse;不接受 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,并要求显式风险确认:

sh
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-exemptions

Source: 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 或不满足初始化条件的插件操作。

插件操作失败

typescript
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:

  1. 以随附模板初始化 profile,或在 profile 目录中维护自己的 dsh.profile。
  2. 通过 bundles 声明组合包顺序。
  3. 使用 profile patch、home patch 和 --patch 覆盖配置。
  4. 通过 dsh plugin 安装树外插件,让插件进入 profile 的 node_modules 解析范围。
  5. 对有意接受的版本不兼容使用精确的 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。本文没有读取测试实现,因此不能进一步断言每个测试的断言细节。

Sources

(4 files)