DeepSeek Harness 的模块依赖图:哪个包被依赖得最狠、哪个是叶子

2026-08-17

翻一个陌生的大仓库,最先想搞明白的通常不是「它有什么功能」,而是「我动这一处,会波及谁」。DeepSeek Harness 仓库里有一份现成的答案叫 docs/module-graph.md,一张 Mermaid 图加一张表。问题是,这张图第一眼给你的印象,和你真正要的答案不是一回事——照着它做影响面判断,会漏。

下面沿着这张图的生成路径走一遍,把口径先钉死,再看数字。

需要先说明:该仓库 README 自述处于开发者预览(developer preview)阶段,并用大写明写「未来将出现破坏兼容性的变更」。本文提到的包名、目录、脚本名与默认值都随时可能变,回源核对是唯一可靠的做法。

先把口径钉死:这张图是谁生成的、只读什么

docs/module-graph.md 的头两行是一条注释,写明由 scripts/gen-module-graph.ts 生成、不要手改,重新生成用 pnpm run gen-module-graph。仓库根 package.json 的 scripts 里还有一条 verify-module-graph,值是 tsx scripts/gen-module-graph.ts --check;这条脚本在 scripts/run-gates.ts 里被注册成一个标签为 module graph 的 gate。也就是说这张图的「新鲜度」是有闸门管的,不是谁想起来才跑一次。

真正决定图长什么样的是 scripts/package-graph.ts 里的 collectPackageGraph。它做三件事:globSync('packages/*/*/package.json') 收集清单;丢掉 name 不以 @deepseek-ai/dsh- 开头的;然后只取 peerDependencies 的键,再过滤出同样带 dsh- 前缀的那些,作为边。

请把最后半句读两遍。dependenciesdevDependencies 它一个字都不看。文档正文自己也写了这个口径,说 peerDependencies 是「规范的运行时依赖信号」。这句是文档自述,它是不是覆盖了全部真实依赖,后面会用具体数字说。

另外 collectPackageGraph 末尾调的 topoSort,一旦发现某一轮没有任何包的依赖全部就位,就直接抛 dependency cycle among ...。所以只要这张图能生成出来,图里就没有环——这不是文档的承诺,是生成器跑不过去的硬约束。

数量:49 是组,219 才是包

packages/ 下面直接 ls 会看到 49 个目录,但那一层是,不是包。真正的包在下一层,packages/*/* 这个两层 glob 才对得上。我们逐个数 packages/*/*/package.json,得到 219 个包;它们的 version 字段全部是 0.1.0-rc.5,与仓库根 package.json 的版本一致。这和文档里 Mermaid 的 219 个节点、文末依赖表的 219 行完全对得上。

边我们数出 1089 条docs/module-graph.md 里带 --> 的行有 1090 行,多出来的那一行是正文里解释「边 a --> b 表示……」的说明句,不是边)。

组里最大的是 client,39 个包;其后是 session 13 个、subagent 11 个、shell 9 个,corehost 并列 8 个。

顺带一个能省你半小时的坑:图里的短名和目录名不是一回事。生成器把 @deepseek-ai/dsh- 前缀剥掉当短名,目录却是 packages/<组>/<叶>。所以图上写 client-locale,目录是 packages/client/localeapi-remotes 对应 packages/api/remotesclient-ui-slots 对应 packages/client/ui-slots。拿短名当路径去 ls 一定扑空。文末那张表给的是带链接的相对路径,认路径别认名字。

唯一的叶子:invariants

peerDependencies 算出边,219 个包里出边为零的只有一个@deepseek-ai/dsh-invariants,位置在 packages/runtime-diagnostics/invariants。它的 peerDependencies 里只有一项 @deepseek-ai/cordis,不带 dsh- 前缀,被 SCOPE 过滤规则挡在图外了,所以它在图上干干净净。

反过来,其余 218 个包全部直接依赖它。这不是巧合:我们逐个检查过,219 个包里每一个都有 src/invariant.ts,每一个的 exports 都声明了 ./invariant 子路径;scripts/check-workspace-constraints.ts 第 329 到 338 行专门校验这个子路径——types 必须是 ./lib/types/invariant.d.tsdefault 必须是 ./lib/invariant.js,两个都写了才算数。docs/cookbook/adding-a-package.md 把这一类 package.json 约束统称为「package.json invariants」,并注明由 pnpm run constraints(即上面那个脚本)执行;同一段清单里也写明每个新增包要打包发布的 files 必须包含 lib/invariant.js

包本体很小:src/index.ts 200 行、src/invariant.ts 30 行。index.ts 里的 Config 有三个字段——enabledpackage_allowlistpackage_blocklist,后两个是大小写敏感的正则字符串数组,allowlist 为空表示全放行,blocklist 在 allowlist 命中之后再排除。enabled 在第 96 行写的是 z.boolean().default(true)。这是配置默认值,不是「你跑起来一定会怎样」的保证:谁在什么时机装这个 service、上层有没有覆盖这个配置,得另看装配代码。

对读者的实际意义是:invariants 是唯一叶子,意味着「排除 invariants 之后再看图」才有信息量。它的入度 218 只说明了一条仓库约定,不说明它有多重要。

直接入度榜:会骗人的那张榜

去掉 invariants,按直接入度排下来是这样:

直接被依赖数传递影响面
session80155
llm78168
agent58124
tools4388
client-runtime3234
client-ui-slots3134
brand29177
system-prompt29127
client-locale2630
client-ui-primitives2632
timeout23171
attachment5170
code-runtime290

左边这一列是图上直接连过来的边数,右边是沿 peerDependencies 一路往上追的传递闭包大小,也就是「改它以后,声明链上会牵扯到多少个包」。两列的排序完全不一样。

client-runtime 直接被 32 个包依赖,看着挺唬人,传递闭包只有 34;client-locale 是 26 对 30。原因不神秘:它们本来就坐在 DAG 靠顶的位置,上面没什么东西了。

真正要小心的是最后两行。attachment 只被 5 个包直接依赖——attachment-localclient-ui-conversationllmllm-pi-aitool-fs——但它的传递闭包是 170。code-runtime 更极端,直接依赖者只有两个(code-runtime-worker-threadtools),闭包 90。因为其中一个直接依赖者是 llmtools 这种腰部枢纽,一脚踩下去整条链都在下面。

所以那个最初的问题——「改这一处会波及谁」——用直接入度回答会得到错的答案。按传递闭包排,前几名是 invariants 218、brand 177、timeout 171、attachment 170、llm 168、scope 161、typert-protocol 157、session 155。闭包超过 100 的一共 10 个包。

另一头也值得看:108 个包没有任何 in-repo 包依赖它们,接近总数的一半。其中 client 组占 20 个,subagent 组 9 个,test-support 组 6 个。这些是图的出口,改它们在声明层面不牵连别人。

沿最长的一条链走一遍

拓扑排下来一共 17 层,最长的一条链正好也是 17 个节点:

client-ui-model-selection -> client-ui-commands -> client-ui-conversation
-> client-ui-input-trigger -> client-locale -> client-ui-settings
-> client-runtime -> api-remotes -> cordis-host-runner -> tools
-> user-approval -> agent -> session -> llm -> attachment -> brand -> invariants

挑链上几处把原始声明摊开看,比看图直观:

  • packages/core/toolspeerDependencies 里有 8 个 dsh 包:agentcode-runtimeinvariantsllmscopesessionsystem-promptuser-approval
  • packages/core/agent 有 6 个:invariantsllmscopesessionsystem-prompttypert-protocol
  • packages/core/session 有 5 个:brandinvariantsllmscopetypert-protocol
  • packages/llm/llm 有 4 个:attachmentbrandinvariantstimeout
  • packages/attachment/attachment 只有 2 个:brandinvariants
  • packages/util/brand 只剩 1 个:invariants

链走到这里就到底了。所有这些条目的版本范围写的都是 workspace:^,另外每个包的 peerDependencies 里都还有一条 @deepseek-ai/cordis——它不带 dsh- 前缀,同样不进图。

这张图看不见什么

这是全篇最该记住的一段。既然生成器只读 peerDependencies,那么写在 dependencies 里的 in-repo 依赖就完全不在图上。

我们数了一遍:219 个包里有 22 个在 dependencies 中声明了 in-repo 的 @deepseek-ai/dsh-*。最极端的是 packages/bundle/base:它的 peerDependencies 只有 @deepseek-ai/dsh-invariants@deepseek-ai/cordis 两项,所以在图上它是个只有一条出边的节点,正向传递闭包算出来是 1;但同一个 package.jsondependencies 有 77 项,其中 75 项是 in-repo 的 dsh 包。packages/bundle/web-app 同理,peerDependencies 里的 dsh 包只有 shell-envinvariantssystem-prompt 三个,dependencies 59 项里有 57 项带 @deepseek-ai/dsh- 前缀——其中 56 项落在 packages/*/* 里,剩下那项是 @deepseek-ai/dsh-web-frontend,它的 package.jsonapps/web,本来就不在生成器的 glob 采样范围内,图上从来不会出现。顺带说一句,packages/bundle/ 下一共只有三个包:baseheadlessweb-app

两处口径不一致:图按 peerDependencies 画,装配层的实际清单写在 dependencies 里。以各自 package.json 的原文为准,我们不推断这样写的原因,也不由此评价什么。要注意的只有一条实操结论——看 bundle 层的包时,别信图,直接打开它的 package.json

什么时候用它,什么时候别用

用它的场景:判断某个中低层包的改动会不会捅穿声明链;确认某两个包之间到底有没有声明依赖;看某个新包插在第几层。这些图给得又快又准,而且有 gate 保证不过期。

别用它的场景:任何涉及 packages/bundle/ 下三个包的判断;任何需要知道「实际打包进产物的是什么」的判断;任何需要知道「运行时到底谁调用谁」的判断——peerDependencies 是声明,不是调用关系,两个包声明了依赖不代表代码路径上真的走到。

自己核一遍的成本也不高:仓库 package.json 的 scripts 里现成写着 gen-module-graphverify-module-graph 两条(值分别是 tsx scripts/gen-module-graph.tstsx scripts/gen-module-graph.ts --check)。以上为仓库 package.json 中 scripts 字段的原文,未经实测,实际以仓库最新内容与命令自身的输出为准;再次提醒该项目处于开发者预览阶段,脚本名与行为都可能变。至于本文那些直接入度、传递闭包、层数,全部是我们逐个读 packages/*/*/package.jsonpeerDependencies 字段算出来的,你按同样的口径能复算出同样的数。


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

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