给 DeepSeek Harness 塞一个 vendored 包:为什么要这么麻烦

2026-08-17

先把前提摆在前面:DeepSeek Harness 仓库的 README 自述该项目处于 developer preview(开发者预览) 阶段,并且原文用全大写写了「THERE WILL BE COMPATIBILITY-BREAKING CHANGES」。下面提到的每一个文件名、字段名、脚本名都是仓库快照里当下的样子,随时可能变。

起点:为什么不能一句 pnpm add 了事

假设你在这个仓库里干活,需要再用一个上游 Cordis 插件——docs/cookbook/adding-a-vendored-package.md 开篇举的例子就是 @cordisjs/plugin-http。直觉动作是往 package.json 里加一行依赖,装上完事。这个仓库不让你这么干:cookbook 第一句就写明,这类包要作为固定版本的源码 vendor 到 vendor/ 下,而不是作为 npm 依赖添加。

理由不需要我们猜,仓库里有一份决策笔记,路径就写在 cookbook 的链接里:.agents/notes/implemented/process/2026-06-11-vendor-cordis-as-source.md。文档自述的原因有两条:这个仓库启动时 Cordis core 还是 release candidate;而 harness 依赖框架内部行为(笔记里点名了 fiber 生命周期、effect 释放、waterfall 分发),这些行为的确切表现关系到 agent 循环的正确性保证。笔记还写了被否掉的方案——直接依赖 npm 包,以及把传递依赖全部 vendor。后者被否的口径是:真正的第三方依赖(js-yamlchokidar@standard-schema/spec 之类)留在 npm,只有内部行为要紧的框架层才自己拿住。

所以 vendor/ 目录下现在是 9 个包,我们实际数了一遍目录:cordiscosmokitschemasteryloaderincludegrouptimerhmrlogger-console。这 9 个目录对应的 npm 名(改 scope 之后的 @deepseek-ai/cordis@deepseek-ai/cordis-plugin-loader 这一串)也硬编码在 scripts/check-workspace-constraints.ts 顶部的 vendoredPackages 集合里,可以拿去对照。

第一步:复制进来,然后马上要动三样东西

cookbook 给的目录形状很短:package.jsontsconfig.jsonsrc/,以及上游若带了就保留的 README.mdLICENSE。我们逐个目录看过,9 个包全都有 LICENSEREADME.md;只有 vendor/cordis/ 多一个 bin.js,这个细节等下讲守卫脚本时会再出现一次。

tsconfig.json 不是照抄上游的。cookbook 给了模板:rootDir: srcoutDir: lib/types,加上一串严格性放宽项,再给它导入的每个其他 vendored 包写一条 references。想看真身,翻 vendor/include/tsconfig.json:它的 references../cosmokit../cordis../loader 三条,compilerOptions 里除了模板列的那几项,还多一个 noImplicitAny: false——cookbook 的示例块里没有这一项。vendor/AGENTS.md 对这类改动的口径是:vendor/*/tsconfig.json 是唯一可以随手改的例外,因为它们本来就是为适配 monorepo 构建重新生成的,允许因类型检查策略调整而改动(原文举的例子正是 noImplicitAny)。我们实读了 9 个 tsconfig,带 noImplicitAny: false 的是 cordisincludeloader 三个,其余六个没有。

第二样是名字。这些包全部被改到 @deepseek-ai scope 下:cordis@deepseek-ai/cordis@cordisjs/plugin-<x>@deepseek-ai/cordis-plugin-<x>。映射表在 docs/rescope.md,那页同时列了「不动的东西」:目录名不动、版本号不动、Loader 的 cordis: 内建前缀不动(cordis:includecordis:group 是协议前缀不是包名)、cordis.yml 这一族配置文件名不动、Schemastery 的 Symbol.for('schemastery') 这类上游运行时标识符不动。改名不用手工做,scripts/rescope-vendor.ts 持有映射,pnpm run rescope-vendor --apply 重写全部引用,pnpm run rescope-vendor:check 断言结果——后者挂在根 package.jsonhygiene 脚本第一位。

第三样最容易漏:本地相对导入要写成显式 .ts 后缀。打开 vendor/cordis/src/index.ts,除去每条导出上方的一行 JSDoc 注释,正文就是七条 export *./context.ts./events.ts./fiber.ts./logger.ts./registry.ts./service.ts./utils.ts,一条不落全带后缀。这不是上游的写法,是这个仓库的本地差异。机制在 tsconfig.base.json 里:allowImportingTsExtensionsrewriteRelativeImportExtensions 两个开关都是 true,cookbook 的说法是编译后运行时导入被改写成 .js,而声明文件保留显式 .ts,让 NodeNext/Node16 的 TypeScript 消费方能解析。

第二步:只有两处根配置需要手动登记

这是这套设计里我觉得最省心的一段。cookbook 的表格列了要改的文件,真正的手工活只有两处:

文件加什么
tsconfig.base.jsonpaths 里加 "<npm-name>": ["./vendor/<dir>/src"]
tsconfig.host.jsonreferences 里加 { "path": "./vendor/<dir>" }
vendor/README.mdmanifest 表加一行,并登记本地修改
scripts/publint-all.ts通常跳过

tsconfig.base.json 里那 9 条 paths 是连着写的一段,指向的都是 ./vendor/<dir>/src——注意是 src 而不是构建产物。tsconfig.host.json 里那 9 条 references 排在所有 packages/* 条目之前,cookbook 给的理由是 vendored 代码只经 host 聚合进入项目图。cookbook 结尾还把这条重复了一遍,说重要的隔离边界是 project-reference 图:vendored 源码必须通过它自己的 vendor/<dir>/tsconfig.json 被引用,而不是被拉进某个聚合项目开了严格检查的 TypeScript 程序里。

剩下的靠 glob 自动覆盖,不用你动手。根 package.jsonworkspaces 第一项就是 vendor/*tsdown.config.tsworkspace 数组是 ['vendor/*', 'packages/*/*', 'apps/cli'].oxlintrc.json 的忽略列表里有 vendor/**,注释写的是「vendored source keeps upstream style and idioms」;vitest.config.ts 的覆盖率 include 只写了 packages/*/*/src/**/*.{ts,tsx} 一条,注释明说 vendor/examples/ 不在范围内。也就是说,vendored 源码既不受本仓库 lint 规则约束,也不进覆盖率统计——这两处配置各自都在注释里写明了这是有意排除,不是漏配。

至于 scripts/publint-all.ts,我们看了它的 glob,是 packages/*/*/package.json,确实不含 vendor

第三步:两道机械守卫

pre-commit 那道scripts/check-vendor-manifest.sh,挂在 lefthook.yml 的 pre-commit 里,job 名字就叫 vendor manifest guard。逻辑短到可以整段读:它取 git diff --cached --name-only,匹配 ^vendor/[^/]+/(src/|bin\.js),再看 vendor/README.md 有没有一起被 stage;前者有、后者没有,就打印被改的文件并 exit 1。注意那个 bin\.js——前面说过全仓库只有 vendor/cordis/ 有这个文件,这条正则就是为它留的。

这道守卫要保的东西写在 vendor/README.md 里:一份「Local modifications」清单,开头一句是「Keep this log exhaustive」,要求每一处与上游的偏离都登记。这份清单现在有 18 条,内容从「移除 hmr 的 locale YAML 导入」这种一句话说清的,到 cordis/src/fiber.ts 的可重入释放加固、Loader/Include 配置事务化协调这种整段描述的都有。vendor/AGENTS.md 的措辞更直接:不要随手改 vendor/*/src/

hygiene 那道scripts/verify-vendored-links.ts。它先读 vendor/ 下每个目录的 package.json 收集包名(读不到 manifest 的目录跳过),再解析 pnpm-lock.yaml,查两类违规:一是 importers 下任何指向 vendored 名字的依赖项,其 version 必须以 link: 开头;二是 packagessnapshots 两个段的键里,绝不允许出现 vendored 包名——出现就意味着有一份 registry 副本。脚本注释把后果写得很明白:同名的 registry 副本与 vendored 副本共存,会悄悄把框架层分叉。

支撑这件事的是 pnpm-workspace.yaml 里的 linkWorkspacePackages: true,以及 overrides 里给 @deepseek-ai/cosmokit@deepseek-ai/schemastery 显式写死的 link:vendor/... 两行。

三处口径对不上的地方

按仓库的规矩,cookbook 自己在开头写了「已对照现有 vendored 集合验证;如有偏差,请在此修正」。我们照着核了一遍,有三处对不上,只陈述位置,不推断原因。

第一处是 "private": true cookbook 的 package.json 不变式第一条写的是 "private": true(括号里注明「vendored 包永不发布」)。而实读 9 个 vendor/*/package.json,没有一个带 private 字段,全部带 publishConfig: { "access": "public" }。更硬的一处在 scripts/check-workspace-constraints.ts:正则 releaseMemberDirectory 明确把 vendor/[^/]+ 算作发布成员,随后的分支里 manifest.private === true 会直接报错「release member must not set “private”: true」,publishConfig.access !== 'public' 也报错。vendor/README.md 开头一段与 docs/rescope.md 的口径则指向发布:因为每个 harness 包都把框架声明为 peer dependency,发布 harness 就会把这层一起发布出去,用上游名字发就等于抢注。要补一句的是,同一份 vendor/README.md 的「Local modifications」第 2 条里,写的又是所有 package.json 重新生成时「added private: true」——这一条与我们实读的 9 个 manifest 也对不上。三处摆在一起就是:cookbook 与 README 的修改日志说要 private,README 开头段、docs/rescope.md 与约束脚本说要可发布。只陈述到这里,以我们实读的源码与约束脚本为准。

第二处是版本号。 vendor/README.md 的 manifest 表与 docs/rescope.md 的映射表都记着一组版本:cordis 4.0.0-rc.7、cosmokit 1.8.1、schemastery 3.18.0、include 1.0.4、hmr 1.0.15。而实读 vendor/*/package.json 得到的是 cordis 4.0.1、cosmokit 1.8.2、schemastery 3.18.1、include 1.0.6、hmr 1.0.16。两份文档都写了「版本号不动,所以这份 manifest 仍然读起来像上游快照」,实际数值与之不符。说完就停。

第三处是 tsdown 的例子。 cookbook 说只有构建形态与根默认值不同时才需要 per-package 的 vendor/<dir>/tsdown.config.ts,举的例子是 vendor/schemasteryvendor/logger-console。实际 vendor/ 下有三个:多出一个 vendor/loader/tsdown.config.ts

这套麻烦换来了什么,以及什么时候别用它

决策笔记里写明的收益是:框架层可审计、可打补丁、版本固定;构建产物执行的是与源码测试同一份 vendored Cordis。代价也在笔记里——上游同步是手工的,流程写在 vendor/README.md 末尾那五步(记上游 HEAD、覆盖 src/、重新施加本地修改、更新表格里的版本与 commit、在根目录跑 pnpm install && pnpm run test && pnpm run build)。

反过来,什么不该 vendor,vendor/README.md 也直接列了:reggol@cordisjs/utils@cordisjs/element@cordisjs/unyaml 被标为「Intentionally not vendored(verified unused by this set)」;js-yamlchokidarpicomatch@babel/code-framesupports-color 这些第三方依赖留在 npm。判断线不是「重不重要」,而是文档给的那条:内部行为你要不要拿住。

最后照抄 cookbook 的验证段,原样三行:

pnpm install        # registers the workspace
pnpm run typecheck
pnpm run build && pnpm run constraints

以上命令原样抄自仓库文档,我们没有运行过;实际以仓库当下的脚本定义与 --help 输出为准。另外提醒一次,这个项目处于开发者预览阶段并明说会有破坏性变更,上面每一处路径和脚本名都可能在下一个快照里换掉。


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

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