Skip to content
Zegging's Tech Blog
Go back

阅读 DeepSeek Harness(一):vendored Cordis 的复杂性从哪里来

阅读 DeepSeek Harness 时,vendor/ 是一个很难绕开的目录。Cordis 及其基础库被复制进仓库,每个目录都有自己的 package.jsontsconfig.json;根目录又配置了 pnpm workspace、TypeScript paths、project references、tsdown、rescope 脚本、lockfile 检查和 pre-commit guard。对于一个普通使用第三方依赖的项目,这套结构明显过重。

问题在于,DeepSeek Harness 不只是使用 Cordis。它希望 Cordis 和 DSH 可以在同一个仓库、同一个 commit 和同一套测试中共同演进,同时还要把两者发布成普通用户能够安装的 npm package。

复杂度不是 pnpm 带来的,而是同一份 Cordis 必须同时具有“仓库内源码”和“外部 npm package”两个身份。

理解这些配置只需要沿三条主线阅读:源码如何维护,源码如何构建并发布,用户安装时又发生了什么。

主线一:源码如何维护

为什么不直接依赖 npm 上的 Cordis

DeepSeek Harness 项目启动之初,Cordis core 仍处于 4.0.0-rc.6。更重要的是,Harness 的 Agent Loop 不只依赖 Cordis 的公开类型,还依赖 fiber 生命周期、effect 释放和 waterfall 派发等内部行为。这些行为一旦变化,受影响的不是一个孤立功能,而是插件卸载、工具执行和 Agent 生命周期的正确性。

普通 lockfile 能固定“安装哪个版本”,却不能让项目直接修改这个版本里的框架实现。patch-package 或 pnpm patchedDependencies 适合少量、稳定的第三方补丁;当修改开始覆盖生命周期、配置事务、HMR 和跨平台可靠性时,补丁文件本身就会变成一份难以阅读的 fork。

DeepSeek Harness 因此把需要共同演进的框架层复制进仓库。js-yamlchokidar 等普通第三方依赖仍然来自 npm。这里的边界是:只有正确性依赖其内部实现、并且需要在仓库内修改的框架层才值得 vendor。

Git submodule 只能改变源码快照的保存方式。即使 Cordis 通过 submodule 检出,npm 包名如何解析、源码如何参与 TypeScript 构建、最终如何发布仍然需要另外解决。这里的核心问题不是 Git 如何下载代码,而是这份代码进入 monorepo 后以什么身份参与整个 package graph。

vendor 不是一个目录,而是一组 package

DeepSeek Harness 没有把 Cordis 合并成一个内部模块,而是保留了上游的 package 边界:

vendor/
├── cordis/
├── cosmokit/
├── schemastery/
├── loader/
├── include/
├── group/
├── timer/
├── hmr/
└── logger-console/

每个目录都有独立的 package.json、版本、依赖、exports 和 TypeScript 编译边界。Harness 中的代码不会通过 ../../vendor/cordis 引用框架,而是继续使用 package name:

import { Context } from "@deepseek-ai/cordis";

物理源码属于当前仓库,逻辑身份仍然是 npm package。保留这层身份,是后续 workspace 解析、构建和发布能够使用同一套 import 的前提。

上游 commit 与本地修改如何记录

vendor/cordis 不是一份来源不明的复制代码。vendor/README.md 记录了它的三个关键信息:

当前目录:vendor/cordis
来源仓库:github.com/cordiverse/cordis 中的 packages/core
来源 commit:56b3d4f...

同步 Cordis 时,DeepSeek Harness 先取出这个 commit 对应的 packages/core 源码,再应用自己长期维护的修改:

vendor/cordis 当前源码 = cordiverse/cordis 在 56b3d4f... 时的 packages/core 源码 + DeepSeek Harness 的本地修改

这里所谓的“上游基线”,就是等号右边第一项:尚未叠加 DeepSeek Harness 本地修改的原始参照版本。它让维护者能够回答两个问题:当前代码相对上游改了什么;下一次升级时,本地修改应该从哪个版本开始重新应用。

当前修改已经覆盖几类框架行为:

  • 加固 fiber 的重入释放、异步 cleanup 和子 fiber 激活;
  • 让 Loader、Include 和 Group 的配置更新具备事务性和回滚能力;
  • 修复 HMR watcher、Windows 文件句柄暂态占用和 teardown drain 的并发问题;
  • 为 lazy config、配置 dump、Node-compatible TypeScript 和发布构建补齐能力。

这解释了为什么一个简单 patch 文件已经不够。DeepSeek Harness 实际维护的是一个紧贴上游、但拥有明确本地语义的 Cordis fork,只是这个 fork 被嵌在 monorepo 中。

仓库如何强制使用 vendored packages

源码复制进 vendor/ 并不代表所有 import 都会自动使用它。根 pnpm-workspace.yaml 先把这些目录注册成 workspace package:

packages:
  - vendor/*
  - packages/*/*
  - apps/*

linkWorkspacePackages: true

overrides:
  "@deepseek-ai/cosmokit": "link:vendor/cosmokit"
  "@deepseek-ai/schemastery": "link:vendor/schemastery"

仓库内的 package 通过 workspace:^ 声明本地依赖:

{
  "dependencies": {
    "@deepseek-ai/cordis": "workspace:^"
  }
}

workspace: 协议表示这个依赖必须解析到当前 workspace,找不到匹配的本地 package 就直接失败。linkWorkspacePackages 让符合版本范围的 workspace package 自动参与链接;overrides 进一步强制 cosmokit 和 schemastery 在整个依赖图中指向 vendor/,防止传递依赖下载 registry 上的另一份实现。

安装完成后,pnpm-lock.yaml 记录的是目录链接,而不是 registry 版本:

"@deepseek-ai/cordis":
  specifier: workspace:^
  version: link:../../../vendor/cordis

TypeScript 还有自己的源码解析配置:

{
  "paths": {
    "@deepseek-ai/cordis": ["./vendor/cordis/src"],
    "@deepseek-ai/cosmokit": ["./vendor/cosmokit/src"]
  }
}

pnpm 负责把 package name 链接到 vendor/cordis,TypeScript paths 负责把源码检查和相应测试指向 vendor/cordis/srcscripts/verify-vendored-links.ts 再检查 lockfile:每个 vendored package 的解析结果必须以 link: 开头,packagessnapshots 中不得出现同名 registry 副本。

这就是 pnpm 在整套设计中的职责:维护“一个 package name 最终解析到哪个目录”。pnpm 不知道源码来自哪个上游 commit,也不知道 DeepSeek Harness 修改了什么;这些信息属于 vendor/README.md 和源码同步流程。

上游更新如何进入仓库

更新一个 vendored package 时,维护者需要:

  1. 记录新的上游 commit;
  2. 复制对应 package 的源码;
  3. 逐项重新应用本地修改,或者删除已经被上游吸收的修改;
  4. 更新 vendor/README.md 的 manifest 和修改日志;
  5. 重新执行命名转换、安装、测试和构建。

scripts/check-vendor-manifest.sh 会拒绝只修改 vendor/*/src、却没有在同一个 staged commit 中更新 vendor/README.md 的变更。scripts/rescope-vendor.ts 则负责重新应用 package name 的 scope 转换,并检查转换结果没有遗漏。

因此,仓库维护的不是一份静态副本,而是“从一个明确上游 commit 重建当前 Cordis fork”的能力。

主线二:如何打包并发布

为什么 package name 要改成 @deepseek-ai

Vendored packages 包含 DeepSeek Harness 的本地修改,不能继续用 cordis@cordisjs/plugin-loader 等上游名字发布,否则会占用不属于 DeepSeek 的 registry 名称。它们因此被统一发布到 @deepseek-ai scope:

cordis                   -> @deepseek-ai/cordis
@cordisjs/plugin-loader  -> @deepseek-ai/cordis-plugin-loader

改名只作用于 package identity。cordis: 内置协议、cordis.yml 配置文件族、目录名和 Symbol.for("schemastery") 等运行时标识保持不变。rescope-vendor.ts 集中维护这份映射,使同步上游后可以机械地重新应用。

tsc 生成 JavaScript 和类型声明

仓库外的用户不会下载 DeepSeek Harness 的 Git 仓库,也不会在自己的机器上编译 vendor/cordis/src。DeepSeek 需要先把 TypeScript 源码构建成 JavaScript 和类型声明。

Host 构建从下面这条命令开始:

tsc -b tsconfig.host.json && tsdown --env.DSH_BUILD_FACE host

tsc 是 TypeScript 编译器,-b 表示按照 project references 构建一组互相依赖的 TypeScript project。project reference 是一个 tsconfig.json 对另一个 tsconfig.json 的引用:它既保留每个 package 的独立编译边界,也告诉编译器构建顺序。tsconfig.host.json 明确引用了 vendor/cosmokitvendor/schemasteryvendor/cordis 等 package,所以编译器会先处理被依赖的 package。

vendor/cordis 为例,它的 tsconfig.json 指定:

{
  "compilerOptions": {
    "rootDir": "src",
    "outDir": "lib/types"
  },
  "references": [{ "path": "../cosmokit" }]
}

执行 tsc 后,一个 src/index.ts 会产生:

vendor/cordis/src/index.ts

    ├── lib/types/index.js        JavaScript 中间产物
    ├── lib/types/index.js.map    JavaScript sourcemap
    ├── lib/types/index.d.ts      TypeScript 类型声明
    └── lib/types/index.d.ts.map  类型声明 sourcemap

.js 是 Node.js 真正能够执行的代码;.d.ts 不参与运行,它只告诉 TypeScript 和编辑器这个 package 导出了哪些类、函数和类型。两个 .map 文件把生成文件的位置映射回原始源码,供调试器和编辑器定位。

vendored 源码中的相对 import 使用显式 .ts 扩展名,例如 ./context.ts。根 TypeScript 配置开启 rewriteRelativeImportExtensions,因此编译出的 JavaScript 会把它改成 ./context.js,避免 Node.js 在运行时寻找不存在的 .ts 文件。

tsdown 生成最终运行时代码

tsc 解决了类型检查、项目依赖顺序和声明文件,但 lib/types/*.js 还只是按源码文件展开的中间模块。tsdown 接着读取这些 JavaScript:

输入:vendor/cordis/lib/types/index.js
输出:vendor/cordis/lib/index.js

tsdown.config.tsvendor/* 纳入 workspace build。对 Cordis 来说,tsdown 将 package 内部模块整理成 lib/index.js。它是面向 Node.js、以 ES2024 语法级别为目标的 ESM runtime bundle:ESM 指使用 import/export 的 JavaScript 模块格式,bundle 表示多个内部模块被整理为对外运行入口。

类型声明已经由 tsc 生成,所以 tsdown 配置 dts: falseclean: false 则保留已有的声明文件。两个工具的职责由此分开:

tsc     -> 检查类型、按 project references 编译、生成 .d.ts
tsdown  -> 整理并打包最终运行时 JavaScript

Schemastery 需要同时支持 ESM 和 CommonJS,因此它提供单独的 tsdown.config.ts,将同一个入口构建为 lib/index.mjslib/index.cjs。CommonJS 是使用 require/module.exports 的另一种 JavaScript 模块格式。

package.json 连接 package name 与产物

构建完成后,vendor/cordis/package.json 告诉 Node.js、TypeScript 和 npm 各自应该读取什么:

{
  "name": "@deepseek-ai/cordis",
  "version": "4.0.1",
  "main": "lib/index.js",
  "types": "lib/types/index.d.ts",
  "exports": {
    ".": {
      "types": "./lib/types/index.d.ts",
      "default": "./lib/index.js"
    },
    "./src/*": "./src/*"
  },
  "files": [
    "lib/index.js",
    "lib/types/**/*.d.ts",
    "lib/types/**/*.d.ts.map",
    "bin.js",
    "src"
  ]
}

Node.js 找到 package 目录后,会按 exports 选择入口;普通运行时执行 lib/index.js。TypeScript 或编辑器分析同一个 import 时则读取 lib/types/index.d.tsfiles 字段决定哪些文件可以进入 npm tarball:lib/types/*.js 和 JavaScript sourcemap 只是构建中间产物,不会发布。

pnpm pack 生成 tarball

仓库里的 @deepseek-ai/dsh 直接依赖 Cordis:

{
  "dependencies": {
    "@deepseek-ai/cordis": "workspace:^"
  }
}

这个写法不能原样发布,因为 npm 用户的电脑上没有 DeepSeek Harness workspace。执行 pnpm pack 时,pnpm 会读取 Cordis 的实际版本,并在 tarball 内改写成普通 semver:

仓库中的 @deepseek-ai/dsh:
"@deepseek-ai/cordis": "workspace:^"

发布到 npm 的 @deepseek-ai/dsh:
"@deepseek-ai/cordis": "^4.0.1"

^4.0.1 表示允许安装 >=4.0.1 <5.0.0 的兼容版本。scripts/release/pack.ts 会对每个待发布 package 执行 pnpm pack,生成 .tgz 格式的压缩包并检查其中的文件。

@deepseek-ai/dsh CLI 自己也经过 tsc + tsdownapps/cli 的 package 级 tsdown 配置以 lib/types/bin.js 为入口,生成 apps/cli/lib/bin.js。它的 package.json 再通过 bin.dsh = lib/bin.js 声明安装后应创建一个名为 dsh 的命令,并通过 files: ["lib/*.js", "config"] 把 CLI 代码放进 tarball。

tarball 上传到 npm registry

scripts/release/publish.ts 按依赖顺序把已经生成的 tarball 上传到 npm registry,也就是 npm 客户端查询和下载 package 的公共服务。发布步骤读取 tarball 自己声明的 name、version 和完整性,不再根据工作区临时决定上传内容。

@deepseek-ai/cordis 因此不是一个只在仓库里生效的别名。截至本文,它已经作为 @deepseek-ai/cordis@4.0.1 真实发布在 npm 上;npm 上的 @deepseek-ai/dsh@0.1.0-rc.6 也真实声明了对 @deepseek-ai/cordis@^4.0.1 的依赖。

主线三:安装时发生了什么

用户运行:

npx @deepseek-ai/dsh web

npx 不会去 GitHub 克隆仓库,而是完成下面的工作:

1. 从 npm registry 查询 @deepseek-ai/dsh
2. 下载 @deepseek-ai/dsh 的 tarball
3. 读取 tarball 中的 package.json
4. 发现 dependencies 里的 @deepseek-ai/cordis@^4.0.1
5. 继续下载 Cordis、Cordis plugins 和其他 dsh packages
6. 根据 bin.dsh = lib/bin.js 创建并执行 dsh 命令
7. lib/bin.js import "@deepseek-ai/cordis"
8. Node.js 沿 node_modules 查找并命中 @deepseek-ai/cordis 目录
9. Node.js 读取该目录的 package.json,通过 exports 加载 lib/index.js

如果使用 npm install @deepseek-ai/dsh,这些 package 通常可以在项目的 node_modules/ 中看到;使用 npx 时,它们通常位于 npm 的执行缓存中。具体物理目录会随 npm 的去重结果变化,但逻辑依赖始终包含:

@deepseek-ai/dsh
├── @deepseek-ai/cordis
├── @deepseek-ai/cordis-plugin-loader
├── @deepseek-ai/cordis-plugin-include
├── @deepseek-ai/cordis-plugin-hmr
└── 其他 @deepseek-ai/dsh-* packages

CLI 将 @deepseek-ai/cordis 放在普通 dependencies 中,所以用户不需要手动安装 Cordis。内部的 @deepseek-ai/dsh-* 插件则主要通过 peer dependency 声明自己需要 Cordis。peer dependency 的含义是“由安装这个插件的上层应用提供兼容版本”;当版本范围兼容时,包管理器可以把这些要求归并到 CLI 安装的同一份 Cordis,而不是让每个插件各带一份框架。

同一个 package name 最终贯穿了源码、构建和安装:

源码检查:TypeScript paths -> vendor/cordis/src
仓库内产物:pnpm workspace link -> vendor/cordis -> exports -> lib/index.js
npm 用户:npm registry -> node_modules/@deepseek-ai/cordis -> exports -> lib/index.js

DeepSeek 没有把 vendor/ 直接交给用户,而是把自己维护的 Cordis 源码构建并发布为正常的 npm package,再让 @deepseek-ai/dsh 像依赖普通 package 一样依赖它。

三条主线是如何闭合的

主线需要保持的事实主要机制
源码维护当前代码来自哪个上游 commit、本地修改了什么、仓库是否只使用这份实现vendor/README.md、workspace links、TypeScript paths、修改日志与门禁
打包发布TypeScript 源码能否生成稳定的 JavaScript、类型声明和 npm tarballproject references、tsc、tsdown、exportsfiles、pack/publish scripts
用户安装DSH 是否自动安装 Cordis、Node.js 最终执行哪个文件、插件是否共享兼容框架普通 semver dependencies、binnode_modules 解析、peer dependencies

如果 DeepSeek Harness 只是一个不发布 package 的内部应用,或者从不修改 Cordis,这套设计明显过重。直接依赖 npm、使用少量 patchedDependencies,或者维护一个单独发布的 Cordis fork 都会更简单。

当前方案承担这些复杂度,是因为它同时选择了三件事:框架源码必须与 Harness 一起演进,插件 package 要独立发布,源码检查与用户最终执行的 JavaScript 必须来自同一套 Cordis 实现。按这三条主线阅读后,分散在 Git、pnpm、TypeScript、构建脚本和 npm 中的配置就不再是同一个问题的重复解法,而是三个阶段各自需要维护的事实。

参考


Share this post on:

Previous Post
特修斯之船:DeepSeek Harness 与模型的协同进化
Next Post
Agent 的发展与未来:框架与运行时的过去和未来