从 Cordis 到 dsh:一切皆插件是怎么落到代码的 📅 2026/8/20 11:34:04 摘要DeepSeek Harness 喊出一切皆插件它不是营销口号而是靠底层 Cordis 框架撑起来的架构事实。本文从源码拆解 Cordis 的五个核心机制插件、上下文、依赖注入、类型化事件、可逆副作用并记录源码安装的三个真实踩坑。上周 DeepSeek Harness下称 dsh开源口号六个字一切皆插件。开源不到一周GitHub 上 152k star了但热度之下有个尴尬的现实多数人只记住了这句口号说不清它到底怎么实现。你问一切皆插件那插件之间怎么协作得到的回答往往是含糊的就是模块化呗。看过源码的开发者都承认这句话不是营销。模型适配器、工具注册表、会话日志、Agent Loop——连循环本身都是插件没有特权核心。想换什么就换对应插件不用 fork 源码。但一切皆插件到底怎么落到每一行代码上答案在它的地基里一个叫 Cordis 的插件框架。这篇文章拆的就是它——五个核心机制加源码安装的三个真实踩坑。Cordis 是什么dsh 没有自己造轮子。它的底层直接 vendor 了一个插件框架版本deepseek-ai/cordis4.0.1来源dsh 仓库 vendor/cordis/package.json。Cordis 的理论背景是一篇论文《A Programming Paradigm for Spatiotemporal Composability》讲时空可组合性——但对开发者来说只需要记住它做了什么负责插件的加载、卸载、依赖关系解析其他一概不管。为什么这个选择关键因为 Cordis 足够小、足够纯粹。它只解决插件怎么组织这一个问题具体插件干什么它不关心。dsh 的 40 能力包shell、fs、web、subagent……全部是建在这个地基上的。五个核心概念Cordis 的全部设计可以压缩成五个概念插件被加载的代码单位。可以是一个带apply(ctx, config)的函数也可以是一个继承 Service 的类。插件有生命周期——挂载时实例化卸载时回收它登记的一切。上下文服务的容器。一个服务占据一个稳定的ctx.xxx键比如ctx.tools、ctx.llm。其他插件通过键查找服务而不是 import 具体实现。依赖注入插件用inject声明它需要哪些服务声明之后会等这些服务就绪才启动。加载顺序由依赖表达而不是手动编排启动序列。类型化事件插件之间用事件通信。事件以 emit、waterfall、parallel、serial 四种方式分发分别对应监听者观察、包装、并行扇出、按序执行。可逆副作用插件注册的服务、监听的事件、创建的资源全部可逆。插件卸载时一切自动回滚。这是运行时热插拔的前提——换插件不用重启。代码写出来就直观了。一个最小插件importtype{Context}fromdeepseek-ai/cordis;exportconstnametool-plugin;exportconstinject[tools];exportfunctionapply(ctx:Context){ctx.tools.register(/* 工具定义 */);}inject: [tools]声明了这个插件需要 tools 服务Cordis 会等 tools 就绪才调用 apply。注册通过ctx.effect()完成卸载时自动撤销。一次启动五行代码引出全部机制dsh 启动时跑boot()核心就五行来源dsh 源码 packages/boot/app-boot/src/index.tsconstctxnewContext();// 创建插件树根ctx.provide(dshHomePath,dshHomePath);// 注册第一个服务awaitctx.plugin(Loader);// 挂载配置加载插件awaitmountRootInclude(ctx,configPath);// 展开配置成插件树awaitassertEntriesActivated(ctx,binName);// 审计谁没激活就报错五行代码对应五个机制new Context()创建插件树的根。Context 既是服务仓库也是树节点——每个插件挂载后有自己子 context父卸载时子树跟着卸载ctx.provide()注册第一个服务。把值挂到 ctx 上全局可读ctx.plugin(Loader)程序化挂载插件。Loader 负责读 YAML 配置mountRootInclude()把 cordis.yml 交给 Loader声明式展开成整棵插件树第四行最值得展开。它说明插件的组织不是写死在代码里的而是声明在 YAML 配置里。一份简化版 cordis.yml 长这样plugins:-id:llmentry:deepseek-ai/dsh-llm-id:toolsentry:deepseek-ai/dsh-tools-id:shellentry:deepseek-ai/dsh-bash-sandbox# 想换实现改这一行你改哪个插件、换哪个实现、开哪个能力都在配置层决定不用碰源码。这也是一切皆插件能在产品层成立的原因——插件树是配置叠出来的不是代码写死的。assertEntriesActivated()做审计哪个条目最终没激活比如依赖永远不存在在这里点名报错不会带着半棵没加载完的树跑起来注意一个细节挂载和启动是两个分离的动作。ctx.plugin(X)只负责建 fiber插件的一次挂载产生的运行时对象并挂到树上同步、立即、不执行插件代码要等 inject 依赖齐了fiber 才跑execute。这个分离是 Cordis 能正确编排复杂依赖的根基。三个关键机制服务提供方和消费方只认名字服务是一个插件通过名字共享给全系统的对象。提供方用provide把对象和一个服务名web、shell、llm关联消费方用同一个名字ctx.web读出对象。双方唯一的约定是服务名——web-search-exa 声明需要 web它不知道也不关心对象由哪个插件提供。inject 重载换实现依赖方自动重跑更妙的是等到就位不是一次性的。运行中如果把 shell 的实现插件从 bash-local 换成 pwshCordis 会重载所有声明需要 shell 的插件让它们在新插件树上重新执行 apply。为什么是重载而不是把新对象悄悄递给正在跑的插件因为插件的 apply 已经执行过了旧实现可能被捕获进事件监听、定时器的闭包里。只换对象插件手里握的还是旧引用。撤销它的全部 effect、在新实现上重跑 apply是让插件整体落到新实现上的唯一一致方式。可逆副作用热插拔的前提重载为什么能随时做靠的是 effect 全部可逆——不存在一半成功一半失败的中间态。ctx.effect()注册的撤销函数按注册顺序逆序压栈执行跟 React 的 useEffect cleanup 一个思路但做成了框架级保证。插件初始化中途抛错已应用的一半效应逆序清掉不残留部分状态。三段式分包一个能力三个包一个能力被拆成三个包契约包Definition定义服务本身实现包Provider提供实现消费方包Consumer使用能力。拿 shell 举例dsh-shell定义 ShellExecutor 抽象类dsh-bash-sandbox实现它dsh-tool-bash把它包装成模型可见的 bash 工具。实现包和消费方包互相完全不引用两边只认识契约包和服务名。换实现时消费方一行也不用动——高频的 provider 变动停在注册表层不惊动依赖这条服务的插件。源码安装的三个坑架构讲完说点实际的。dsh 目前是 Developer Preview0.1.0-rc 系列官方明示破坏性变更随时可能发生。源码安装会踩到几个坑其中两个跟环境版本强相关。坑 1git 版本要 2.26dsh 仓库的构建流程对 git 有硬性版本要求。低于 2.26 会在依赖安装阶段直接失败。解法先git --version确认老版本用包管理器升级。macOS 上注意系统自带 git 可能是老版本建议装最新版或通过 Homebrew 管理。坑 2pnpm 必须用 11.7.0高了就报错这是最折腾的一个。dsh 的 pnpm-lock.yaml 把 pnpm 原生二进制pnpm/exe钉在了 11.7.0。如果你的 pnpm 版本高于 11.7.0安装时直接报[ERROR] Cannot verify the identity of the pnpm/exe.darwin-x64 native binary: it is missing from pnpm-lock.yaml. For help, run: pnpm help install.根因pnpm 的 env 安装器会对原生二进制做完整性校验——它在 lockfile 里找pnpm/exe的 integrity 记录来验证下载的二进制没被篡改。但 lockfile 里 pin 的是 11.7.0 的记录你本地 pnpm 版本更高时它要下载对应版本的pnpm/exe却在 lockfile 里找不到这条记录校验无从谈起直接报错。这是lockfile 钉死版本与本地环境版本漂移的经典冲突。解法把 pnpm 切回 11.7.0。用 Corepack 最干净corepackenablecorepack prepare pnpm11.7.0--activatepnpm--version# 确认 11.7.0坑 3Node 大版本别追新dsh 依赖链里有包声明了 node 版本门槛实测 node 20/22。追最新大版本可能触发依赖的兼容性问题——比如某个包用了新版本才移除的 API装的时候不报跑的时候才炸。解法先cat package.json | grep engines看官方声明选一个满足范围但不最新的 LTS 版本。用 nvm 切nvminstall22nvm use22我的判断回头看dsh 选择 Cordis 是笔划算的账框架把复杂度吃进肚子用户才能把自由度握在手里。一切皆插件的代价是理解门槛——五个核心机制、三段式分包、fiber 状态机这套抽象对普通开发者不友好。但收益是真实的换模型适配器、换沙箱 Provider、甚至换 Agent Loop都变成改配置级别的事。对想自建 Agent 基础设施的团队Cordis 这套服务 依赖 可逆副作用的组合是比 LangGraph 更底层的参考样本。想深入研究的官方仓库在 github.com/deepseek-ai/deepseek-harnessvendor/cordis/ 目录就是完整的 Cordis 源码。至于安装坑说到底都是版本问题一个精确到 11.7.0 的 lockfile把用最新版的习惯挡在了门外。0.1 的版本号配得上这份折腾——它适合尝鲜和架构学习不适合指望它马上稳定。作者唐悦玮 | 从后端出发用 AI 拓展到全栈的工程师。