给 DeepSeek Harness 加一个 workspace 包:官方实操手册在管什么

2026-08-17

先说清楚前提:DeepSeek Harness 仓库 README 有一节标题就叫 Developer preview,正文用加粗大写写着会有破坏兼容性的变更。下面提到的所有文件名、字段名、脚本路径与默认值都对应 0.1.0-rc.5 这个快照,随时可能变。我们没有安装也没有运行过它,全部结论只来自仓库里的文本本身。

从一个会失败的动作说起

你想给 dsh 加一个新包。仓库里有现成的指路文件 docs/cookbook/adding-a-package.md,开头一句就交代了它的定位:这是给 @deepseek-ai/dsh-<name> 新包用的逐文件清单,并且自述「以 bash 和适配器这两个包为模板进行验证」,还附了一句「如果清单与模板有出入,请在此修正」。

这句话值得认真对待。它等于承认清单本身可能漂移。而真正会在你跑 pnpm run constraints 时报错的,不是这份 Markdown,是 scripts/check-workspace-constraints.ts——package.json 里 constraints 这条脚本就是 tsx scripts/check-workspace-constraints.ts。所以下面我按脚本的判断顺序走,手册对到哪、差到哪,一处一处对。

第一关:目录必须恰好两层

手册第 1 节画的骨架是 packages/<group>/<pkg>/,并写明分组是纯容器:没有 package.json,没有源文件,包仍然恰好位于其下一层。

执行这条的是同一个脚本里的 checkHierarchyShape()。它遍历 packages/ 下每个目录:如果组目录自己有 package.json,报错说包应该在 packages/<group>/<pkg> 而不是直接挂在 packages/ 下;如果组目录下某个子目录(node_modules 除外,见 localArtifactDirs)没有 package.json,报错说层级恰好是两层、不许更深嵌套。两个方向都堵死了。

同一个「两层」在别处也写死过:脚本第 16 行起的 workspaceGlobs 里那条是 { dir: 'packages', depth: 2 }pnpm-workspace.yaml 里对应的是 packages/*/*

这就是数包时最容易翻车的地方。我们按 packages/*/package.json 数,结果是 0;按 packages/*/*/package.json 数,是 219 个包,分布在 49 个组目录下。把 49 当成包数就完全错了。

顺带一个并列的事实:手册说分组「没有源文件」,而实读下来 49 个组目录里有 48 个各自躺着 README.mdREADME.zh.mdREADME.i18n.yaml 三份文件(唯一的例外是 packages/runtime-diagnostics/,这个目录下一个文件都没有,只有子目录);packages/client/packages/schedule/packages/web/ 三个组目录还各有一份 AGENTS.mdpackages/client/ 下另有一个 tsdown.client.ts——手册自己在第 2 节也点名了这个文件,说 client 插件包要调用这个共享 tsdown preset。脚本拦的只有组目录里的 package.json,这些文件不在它的检查范围内。两处并列摆在这,我不替作者解释。

第二关:分组名清单对不上

手册第 1 节让你「当已有分组与包的角色匹配时选择该分组」,然后列了十个名字:corellmbashcompactsubagenttodosession-persistenceuiutilsupport

按这十个名字去 packages/ 下逐个找,能找到的是五个:core(8 个包)、llm(5 个)、subagent(11 个)、todo(1 个)、util(7 个)。bashcompactsession-persistenceuisupport 这五个目录在这个快照里不存在。名字里带 bash 的包实际都挂在 shell 组下,有 bash-localbash-sandboxtool-bashtool-bash-persistent 四个,另有一个 terminal/terminal-bash。跟 compact 形近的是 compaction 组。

按这个清单挑分组会挑到不存在的目录,只能以实际目录为准。差异陈述到这里为止。

第三关:private: true 这条会直接把你坑住

手册第 1 节列 package.json 不变式,第一条就是 private: true

实读的结果是反过来的:219 个包里,写了 "private": true 的是 0 个,带 publishConfig 的是 219 个。手册自己指定的复制模板 packages/core/tools/package.json 里同样没有 private 字段,有的是 "publishConfig": { "access": "public" }"license": "MIT"

机制在脚本第 51 行这个正则上:

const releaseMemberDirectory = /^(?:packages\/[^/]+\/[^/]+|apps\/[^/]+|vendor\/[^/]+)$/

恰好两层的 packages 目录整体落进 release member 这一支。进了这一支,第 254 行的判断是「release member 不得设 "private": true」,同时要求 publishConfig.accesspublic,还要求 repository 字段的 urldirectory 与包所在目录严格对上。只有既不是 Landlock 公开包、又不匹配这个正则的工作区成员,才走到第 265 行那条「必须设 "private": true」。

也就是说,你照手册加了 private: truepnpm run constraints 会当场拦下来。前面那个两层 glob 不只是数数用的,它在这里直接决定了你的包走哪条分支。

第四关:files 是算出来的,不是大致包含

手册说 files 列表「精确包含」四类内容,并补了一句:如果包的运行时 export 指向输出树,还要包含 lib/types/**/*.js。这条在脚本里对应 expectedDshPackageFiles(manifest),返回值再交给 sameStringList 做严格比对——顺序和内容都得一致,不是「包含即可」。

拿模板包对一下就清楚了。packages/core/tools/package.jsonfiles 是这四行,顺序如下:

"files": [
  "lib/index.js",
  "lib/invariant.js",
  "lib/types/**/*.js",
  "lib/types/**/*.d.ts"
]

lib/types/**/*.js 之所以在里面,是因为 usesEmittedTreeDefaults() 会扫 exports 里有没有 default./lib/types/ 开头的条目。这个包的 ./types./presentation 两个子路径正好都指向 ./lib/types/ 下的 JS,所以这一项被算进来。你的包如果没有这类子路径,加了这一行反而会被判定与期望列表不符。

expectedDshPackageFiles 里还有一串条件项,都能在源码里读到触发条件:有 bin 的加 lib/bin.jsexports['./worker'] 存在则加 lib/worker.cjs./client 的 default 恰好是 ./lib/client.js 才加 lib/client.js./loader./store./startup 各有自己的判定。手册没有把这些逐条列出来,只写了一句「门禁认可的包专用运行时产物」——具体清单只能去脚本里读。

bin 那一项还有个细节值得对一眼。手册的措辞是「带有 bin 的 CLI 应用包在 files 中将 lib/bin.js 紧跟在 lib/index.js 之后」,而脚本里 lib/bin.js 是排在 lib/index.jslib/invariant.js 两项之后拼进去的。仓库里两个带 bin 的包——packages/examples/acp-demopackages/examples/jsonrpc-demo——实际写的都是 lib/index.jslib/invariant.jslib/bin.js 这个顺序。既然比对用的是严格的顺序相等,抄手册那句话就会被判不符。

第五关:tsconfig 那句括号里的条件

手册给的 tsconfig.json 骨架是:extends ../../../tsconfig.base.jsonrootDirsrcoutDirlib/types,references 里放 vendor/cosmokitvendor/cordis,括号里补了「如果你用 Config 就加 vendor/schemastery」,再加每个 dsh 依赖。

前三项在手册指定的复制模板 packages/core/tools 和 bash 侧的 packages/shell/tool-bash 里都照做了。第四项在这两个包之间不一致:packages/shell/tool-bash/tsconfig.json 的 references 里有 ../../../vendor/schemastery,而 packages/core/tools/tsconfig.json 里没有——尽管 packages/core/tools/src/index.ts 第 8 行写的是 import z from '@deepseek-ai/schemastery',它的 package.json 也把 @deepseek-ai/schemastery 放在 dependencies 里(手册原话是这个包是运行时校验器,因此进 dependencies,并注明与 agent-loop 一致)。两处摆在一起,就这样。

包的形态倒是很统一。以 packages/shell/tool-bash/src/index.ts 为例,第 30 行 export const name = 'tool-bash',第 31 行 export const inject = ['tools', 'shell', 'systemPrompt', 'shellEnv'],第 34 行是 interface Config,第 40 行是同名的 Config schema,第 190 行是 apply(ctx, config)。全仓 packages/*/*/src/index.ts 里,顶层 export const name 出现在 76 个文件、export const inject 出现在 72 个。手册说的「service default export 或 plugin(name/inject/apply/Config)」在这里能对上。

还有一条容易忘的:包内相对导入在源码里写显式 .ts 后缀,比如 tool-bash 那个 index 结尾从 './background.ts''./render.ts' 导入。这种写法在 packages/*/*/src/index.ts 里出现在 128 个文件。写成 .js 或者省略后缀都是跟仓库习惯拧着来。

第六关:host 与 client 只能选一个(多数情况下)

手册第 2 节要求把新包加进 tsconfig.host.jsontsconfig.client.jsonreferences,并强调「普通包恰好属于一个 aggregate,绝不两个都加」,只点名 api/remotes 因为顺序依赖用了仓库专属拆分,新包不得仿照。

实读两份聚合配置:tsconfig.host.json 有 187 条 references,tsconfig.client.json 有 49 条,两边路径完全相同的有 3 条——./packages/compaction/compaction./packages/host/webserver./packages/typert/registry。这三个包目录下都只有一个 tsconfig.json,没有分面文件。

执行这条的 scripts/project-reference-faces.ts 写的规则跟手册措辞不完全一样:注释里说「单一 config 的项目是中立的,可以参与任一张图;一旦一个包声明了两个分面 config,从某张聚合图出发能到达的每条引用就必须命名与出发点匹配的那一叶」。也就是说,脚本管的是分面包别串图,中立包被两边都引用它不报错。

真正同时有 tsconfig.host.jsontsconfig.client.json 的包,实读是三个:packages/api/gatewaypackages/api/remotespackages/client/connection。手册点名的是其中一个。

第七关:paths 里没有你的组怎么办

手册说 tsconfig.base.json 对已有分组无需编辑,新分组要给 @deepseek-ai/dsh-* 通配符加一条 ./packages/<group>/*/src 候选。

tsconfig.base.jsonpaths 实读 143 条。其中 @deepseek-ai/dsh-* 有 47 条候选路径,@deepseek-ai/dsh-*/invariant 有 43 条。跟磁盘上 49 个组目录逐一对照:通配符候选里有 ./packages/prompt/*/src,但这个快照里没有 packages/prompt 目录;磁盘上的 apiruntime-diagnosticstypert 三个组不在通配符候选里,它们下面的包在 paths 里是逐个显式列出的,比如 @deepseek-ai/dsh-api-gateway 直接指向 ./packages/api/gateway/src/index.ts@deepseek-ai/dsh-invariants 指向 ./packages/runtime-diagnostics/invariants/src/index.ts。另外 terminallsphostclient 四个组只出现在主通配符里,不在 /invariant 那条通配符里。

结论只到这一步:你要落哪个组,先去 paths 里确认那个组是走通配符还是走显式条目,两种情况要改的地方不一样。

第八关:README 不是文档要求,是门禁

这是最容易被当成「有空再补」的一节,实际上有两个独立的脚本在扫。

scripts/verify-package-readme-limitations.ts 要求每个包 README 里有一个逐字为 ## Known Limitations and Deferred Work 的二级标题。它的 isLimitationsLike() 会把 LimitationsDeferred workWhat is not here、开头的 DeferredNon-goals 这几类写法都识别成「像 limitations 但漂移了」,然后要求你改成 canonical 写法。允许没有这一节的白名单常量叫 NO_LIMITATIONS,这个快照里只有一条:packages/util/brand,理由写明是纯类型的 nominal-branding 原语、没有运行时行为与遗留工作。白名单条目还必须带非空理由,且必须指向一个真实存在的包,否则同样报错。

scripts/verify-package-readme-model-experience.ts 管 Model Experience 那一节。它有两份白名单:NO_MODEL_EXPERIENCE_SECTION 是完全不写这一节的包,实读 4 条;SENTENCE_MODEL_EXPERIENCE 是可以只写一句加一个 KV Cache effect 字段的包,实读 121 条,其中 kind: 'none' 66 条、kind: 'indirect' 55 条。同一个包不许同时出现在两份白名单里,脚本会专门报这个错。219 个包减掉这 125 个,剩下的就得按手册第 4 节那套 H3/H4 结构逐条写:What the model seesToken effectKV Cache effect,顺序也是定死的。

新包默认落在「得写完整结构」那一档。手册里那句「或在 scripts/verify-package-readme-limitations.ts 里加一条白名单」不是随手开的口子——从白名单里那 4 条与 1 条的理由措辞看,每一条都带审计说明。

最后那四行命令

手册第 5 节给的验证序列是:

pnpm install        # registers the workspace
pnpm run doc-sync
pnpm run constraints && pnpm run typecheck && pnpm run lint
pnpm run build && pnpm run hygiene

这几条在根 package.jsonscripts 里都能查到对应实现,doc-synctsx scripts/run-gates.ts doc-syncconstraints 是那个约束脚本,hygiene&& 串了十来项检查、constraints 也在其中(也就是说它会被跑第二遍)。

顺序这件事我只把两处原文摆在一起:constraints 脚本读的是 package.json 与目录结构,不依赖编译产物;而 typecheck 在根 package.json 里写的是 npm run build:lib:host && npm run typecheck:contracts-ready,前半截要先把 host 侧的 lib 编出来。手册把 constraints 排在 typecheck 之前,至于为什么这么排,文档里没有写,我不替作者补理由。

再重复一次那层限定:这是开发者预览阶段的仓库,README 自己写明会有破坏兼容性的变更。上面每一个数字、每一条正则、每一份白名单都对应 47f9438 这个快照。要动手之前,最稳的做法是先打开 scripts/check-workspace-constraints.ts 看当下的判断分支,再回头看手册——毕竟手册自己也写了,与模板有出入时要修的是手册。


本文依据 DeepSeek Harness 官方仓库(github.com/deepseek-ai/deepseek-harness)的 README、docs/ 下的 架构与子系统文档、以及 packages/ 下的源码整理,核对日 2026-08-17,对应仓库快照 47f9438(版本 0.1.0-rc.5)。 本文内容为仓库源码与文档口径,我们没有安装、也没有运行过这个项目, 因此不涉及界面外观、操作手感与运行速度的任何描述。 该仓库 README 自述处于开发者预览阶段并明确说明未来会有破坏兼容性的变更, 文中出现的命令、配置与默认值随时可能变动,请以仓库最新内容为准。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。