DeepSeek 在 8 月 13 日开源了 Harness(下面叫 dsh)。关于它和 Claude Code、Codex 谁更强,另有一篇横评;这里只挖一件事:dsh 官方架构文档第一句就写明,底层由 Cordis 驱动。
Cordis 名气不大,但它撑起了 dsh 最硬的一句承诺——“一切皆插件”。模型适配器、工具注册表、会话日志、agent 循环本身,全是可替换的插件。要兑现这句话,光有个 plugin() 接口远远不够:你得有一套能装能拔、拔完还能干净重来的运行时,还得让插件之间的依赖和副作用有明确边界。
Cordis 给自己六个字的定位:时空可组合性的元框架。下面这份梳理基于 Cordis 官方仓库、Koishi 文档和核心源码,把这套抽象拆开讲清楚。没有跑实测,不涉及性能排名。
01
PART
“时空可组合性”到底在说什么
绝大多数插件框架只解决一个问题:怎么把代码拆成模块。Cordis 想同时解两个,而且把这两个叫“空间”和“时间”。
空间组合,回答的是插件之间怎么搭:一个插件能看见哪些服务、依赖谁、它产生的副作用归到哪个边界里、会不会越界污染别人。Koishi 文档里有一句很直白的概括:代码之间能够有效声明和隔离依赖关系的能力。
时间组合,回答的是插件的生命周期:什么时候激活、什么时候停掉、停了之后资源回不回收、能不能重新加载,而且最终行为只取决于“现在启用了哪些插件”,跟中间加载卸载过几回、什么顺序无关。
把这两件事拆开看,Cordis 和传统插件库的区别就清楚了。传统插件库管的是“插件怎么注册、怎么被调用”;Cordis 管的是“一个插件实例在哪个上下文里跑、依赖什么、产生了哪些可追踪的副作用、这些副作用什么时候被撤销”。它把插件当成一个带作用域、带依赖状态、带可回收副作用的动态运行单元,而不是一段被调用的代码。
02
PART
元框架,不是插件库
Koishi 文档把 Cordis 定义为“用来造框架的框架”。它不绑定数据库、消息平台或网页应用这些具体领域,只提供插件、上下文、服务和资源回收这些通用能力;Koishi 在它上面定义消息、指令、适配器和控制台,dsh 在它上面定义模型适配器、工具管线和会话日志。两个完全不同领域的项目,共用同一套插件运行时。
这里有两层含义。
一层是,Cordis 里的插件是一个可部署的运行单元:它接收上下文和配置,可以创建子插件,可以声明服务依赖,并且应当能在退出时撤销它创建的资源。
另一层是,Cordis 的“组合”不是普通函数调用。普通函数调用只描述“执行什么”;Cordis 还要描述在哪个上下文执行、依赖什么服务、产生哪些可追踪副作用、这些副作用何时被撤销。这四件事,正是 dsh 那套 capability seam(provider / consumer / service definition 分离)能成立的地基。
具体形态上,一个插件可以是双参数函数、双参数类,或者带 apply 方法的对象。插件加载在语义上相当于调用这个函数或构造这个类。这种写法保留了 JS/TS 的低门槛,同时通过注册过程把调用纳入 Registry 和 Fiber 管理。大功能还能拆成多个嵌套子插件,每个子模块拿到相对独立的热重载边界。
03
PART
Cordis 的四个核心抽象
CORE
Cordis 运行时靠四个抽象撑起来:Context、Registry、Fiber、Effect。逐个说。
Context:插件的能力入口和空间边界
Context 是插件访问运行时能力的接口。整个应用是一个容器,Context 给插件提供数据库、控制台、浏览器这类服务的访问入口。
它和传统依赖注入容器不完全一样。Cordis 利用 TypeScript 的声明合并机制扩展上下文类型,再通过上下文层级和服务实现决定具体实例的可见范围。关键在于把“功能访问”和“功能实现”分开:插件用 ctx.database,但数据库具体是谁实现的它不管;插件也能定义新 Service,让后续插件通过上下文访问。业务插件不必绑死某个具体实现,空间组合能力就这么来的。dsh 把 shell、sandbox、文件系统都做成 capability seam,同一个能力可以挂多套实现互相替换,靠的就是这一层。
Registry:插件去重
RegistryService 以插件 callback 为 key 管理 Runtime。ctx.plugin() 先把函数、类或对象解析成可执行 callback,然后在 Registry 里查有没有现成的 Runtime,有就复用,没有才建,最后为本次调用创建一个 Fiber。
这解释了一个行为:同一插件默认只执行一次。callback 已经登记过,系统复用它的 Runtime,而不是无脑重复注册所有资源。插件确实需要多次启用时,可以声明可重用,或者通过 fork 事件把“一次性共享部分”和“每次调用部分”分开。
Fiber:一次调用的生命周期实例
Fiber 是理解 Cordis 的钥匙。它不是“插件对象”,而是某次插件调用在某个上下文里的生命周期实例。
源码里,Fiber 保存父 Context、派生 Context、配置、依赖映射、运行状态、资源清理器和异步任务。它的状态有 PENDING、LOADING、ACTIVE、FAILED、DISPOSED、UNLOADING 几种。同一个插件可以有多个 Fiber,每个带不同配置、不同依赖快照、不同副作用集合。于是 Cordis 能同时表达两件事:插件逻辑只初始化一次,但同一份逻辑能按不同配置跑多份。dsh 的 profile(命名组合)能在同一进程里并存多套运行配置,底层就是多 Fiber。
Effect:把副作用变成能回收的资源
Effect 支撑起 Cordis 的可逆性。核心源码里的 effect() 接收一个执行函数,允许它返回清理函数、清理函数的集合、异步结果或异步可迭代结果。Cordis 收集这些清理函数,在销毁时逆序执行;嵌套副作用还会保留元数据用于追踪。
说穿了,插件执行时除了产出值,还会带出一组可逆的资源变换。插件注册了事件监听,逆操作就是取消监听;开了定时器,逆操作就是清除;打开了端口,逆操作就是关闭。Koishi 文档也明确要求,凡是用 ctx 外部 API 引入的资源,都得在 dispose 事件里手动清理,比如关掉 HTTP 服务器。
可逆性,即回收资源的能力,可以为软件带来可组合性、可靠性和可访问性。(Koishi《可逆的插件系统》)
04
PART
空间组合:依赖声明、服务和子作用域
空间组合有四层。
第一层是 Context 层级:插件在某个 Context 里跑,能通过 ctx.plugin() 创建嵌套插件。第二层是 Fiber 层级:每次调用有独立运行实例和副作用集合。第三层是 Service 层级:插件通过服务名访问抽象能力,用 inject 声明必需或可选依赖。第四层是隔离和拦截层:派生 Context 能对某些服务实现做局部覆盖、筛选或拦截。
inject 的语义最值得细看。
对必需服务,插件体不会在服务值变成可用之前加载;服务实现一变,插件先回滚,等新实现有效了再重新加载。对可选服务,插件运行时能读它,但服务在不在不决定插件整体生命周期。如果只是某个局部功能依赖某服务,ctx.inject() 能把这部分逻辑拆成独立子插件,避免整个插件跟着可选能力反复重载。dsh 在工具调用管线上挂 pre-execute、guard、approval、post-execute 一串拦截点,每个拦截点都是这类局部能力,各自独立替换,不牵连调用方。
05
PART
时间组合:加载、停用、卸载和重载
Koishi 对可逆性有一条硬要求,叫路径无关:不管你怎么加载、卸载、来回折腾,最终行为只跟最终启用的插件集合有关,不该依赖中间重复加载过几次、顺序怎样。
工程上,这不等于所有外部世界状态都能自动复原。它指的是 Cordis 尽力管理插件注册的可追踪资源,并给插件留出显式清理的边界。
生命周期事件是时间组合的入口。ready 用于应用启动完成后执行异步操作或等其他插件就绪;dispose 用于回收插件停用时框架没自动管的资源;fork 用来区分一次性外层逻辑和可重用内层逻辑。
Fiber 把依赖变化也纳入了时间状态。它会给 inject 依赖存一份当前实现的快照;依赖的实现 ID 或可用状态一变,epoch 就变,系统触发 unload,清空当前副作用,再根据新 epoch 决定要不要 reload。所以服务替换走的是一次有序的生命周期转换:先卸载、清理,再按新依赖重新激活。dsh 那句“换沙箱后端就提供对应 capability”,背后跑的就是这条转换。
把上面这些拼起来,一个 Cordis 插件可以抽象成:
PluginInstance = (Context, Config, Dependencies, Effects, LifecycleState)
activate : Dependencies_available -> Effects_created + Active
unload : Effects_created -> Effects_disposed + Inactive
reload : Dependency_changed -> unload -> activate
Context 和 Dependencies 体现空间位置,Effects 和 LifecycleState 体现时间行为。真正的时空可组合性是个闭环:插件在局部 Context 里声明依赖;依赖解析结果决定 Fiber 能不能激活;激活过程产生可追踪 Effects;依赖变化或显式停用触发逆序清理;清理完了,新的依赖快照再驱动一次重新激活。
06
PART
和你见过的插件系统比,差在哪
下面这张表不是完整评测,只从“空间边界”和“时间可逆性”两个维度做概念比较。
一句话:多数系统把“装载”做完了,“拔出”要么没有,要么很糙。Cordis 把拔出和依赖关系绑在一起,让服务实现的变更能驱动局部插件回滚重载。
07
PART
适合谁,不适合谁
Cordis 适合几类场景:可选能力多、插件数量大、依赖关系复杂的长时间运行系统;需要不重启主进程就替换功能模块的开发工具或服务平台;要把核心服务、增强功能和前端扩展分开部署的框架;想靠资源追踪快速定位泄漏和异常来源的 TypeScript 应用。Koishi 是最直接的案例——它的插件能注册指令、中间件、事件监听、控制台入口、本地化和服务,大型插件生态的加载、卸载和更新靠 Cordis 的可逆性管理。DeepSeek Harness 是另一个:把模型、工具、审批、日志全做成插件,靠的就是这套运行时。
但对生态规模的数字要有来源意识:Koishi 文档自述的 3000 多个插件是特定时点的数字,不是这里重新统计的实时值。
工程上最要紧的,是把资源边界写清楚。核心功能声明为必需服务;非核心增强用可选服务或独立子插件;所有不经由 ctx 自动托管的外部资源都注册显式 dispose;插件需要多次启用就声明 reusable 或用 fork 语义;服务提供者通过 start、stop、fork 生命周期钩子管理自身状态。
08
PART
还没解决的坑
PITFALL
第一,API 和生态仍在演进。 GitHub README 明确写 Cordis 处于积极开发中,API 尚不稳定,核心包版本还带着 release candidate 标识(当前 4.0.0-rc.8)。生产使用得锁版本、建兼容性测试,别拿未稳定 API 当长期标准。dsh 自己也是 developer preview,会话持久化格式版本号还是 0,官方说不承诺兼容。
第二,可逆性不是全自动。 Cordis 能自动管理通过它自己 API 注册的副作用,但如果插件直接用了 Node.js、浏览器、数据库驱动或第三方 SDK,框架不一定知道怎么撤销。插件作者仍得写清理函数;否则逻辑上“插件已卸载”,外面可能还留着端口、文件句柄、监听器、事务、缓存或远程订阅。
第三,路径无关有边界。 对内存里的注册关系,逆序 dispose 可以接近路径无关;对发消息、写数据库、提交远程任务这类不可逆外部副作用,卸载不可能自动抹掉已经发生的事。Cordis 的可逆性准确说是运行时资源和局部行为的可逆性,不是整个外部世界状态的数学可逆。
第四,异步竞态要小心。 源码已经为异步 Effect、epoch 变化和 unload/reload 设了状态保护,但插件作者仍要避免在异步回调里用已失活的 Context、把旧 Fiber 的资源写进新 Fiber,以及确保取消请求、终止迭代器、关闭连接。服务热替换时错误使用外层 Context,官方文档明确指出可能造成内存泄漏。
第五,HMR 和可逆停用不是一回事。 Cordis 已有 @cordisjs/plugin-hmr,但官方 HMR 页面仍标注“实现原理”和“手动控制”未完成。不能把 fork.dispose() 等同于任意模块级代码的无缝热替换:前者主要是插件运行实例的资源回收,后者还涉及模块缓存、代码版本迁移、状态迁移和开发工具链协同。
第六,依赖图复杂度会变成运行时复杂度。 服务提供者一替换,可能导致多个依赖插件级联回滚重载;插件之间存在循环依赖、隐式全局状态或外部共享资源时,局部重载的影响范围可能超出直觉。大型应用需要对依赖图、Fiber 状态和 Effect 追踪建可视化和诊断工具。
∞
LAST
怎么看这套东西
Cordis 的设计可以浓缩成一句:以作用域组织空间,以 Effect 管理资源,以 Fiber 管理时间,以 Service 和 inject 管理依赖。它给你的远不止一个 plugin() 方法:把插件从静态代码模块提升成可观察、可依赖、可卸载、可重载的运行时对象。
一个判断值得单独拎出来:模块化要是没有撤销能力,很容易退化成只增不减的注册过程;真正的插件化必须同时回答怎么装载和怎么拔出。Cordis 把“拔出”和依赖关系绑在一起,让服务实现的变更驱动局部插件回滚重载。空间上组件有边界,时间上边界能进出和再进,空间依赖一变,时间状态有序转换。
按成熟度、表达能力和风险综合看,Cordis 更适合定位成面向动态 TypeScript 应用的可逆插件运行时与元框架。项目需要高频模块替换、细粒度插件隔离和资源可追踪时,它有吸引力;只需要静态扩展点、部署时一次性装载或跨进程隔离时,它的生命周期模型可能带来额外复杂度。真要采用,先建插件资源清单、依赖图和卸载测试,再逐步引入动态加载和热重载。
如果你在选 agent 底座,已经看过 dsh 的横评,现在想知道它底层这套“一切皆插件”到底靠什么撑起来:Cordis 这六个核心抽象(Context、Registry、Fiber、Effect、Service、inject)和一条“依赖驱动可逆”的主线,就是答案。它还不算冻结的标准,但设计方向清楚,值得做平台的人认真读一遍源码。
你团队的工具或 agent 里,插件停用之后有没有遇到过端口、监听器、缓存没清干净的坑?欢迎说说你是怎么处理的。
参考来源(均来自官方仓库与文档):
cordiverse/cordis GitHub 仓库及 README(项目状态、API 稳定性声明) Koishi 文档《可逆的插件系统》(路径无关、可逆性理念) Cordis 核心 package.json(版本 4.0.0-rc.8,包描述 Meta-Framework for Modern Applications) Cordis 官方指南《认识插件》(插件的三种形态、嵌套插件) Cordis 官方指南《服务与依赖》(Context、Service、inject 语义、内存泄漏警告) Cordis 核心源码 packages/core/src(RegistryService、Fiber 状态机、Effect 收集与逆序清理、epoch 机制) Cordis 官方指南《生命周期》(ready、dispose、fork 事件、手动清理要求) Cordis 官方指南《模块热替换》(@cordisjs/plugin-hmr,实现原理与手动控制标注未完成)
本文基于 Cordis 官方仓库、Koishi 文档和核心源码梳理,未做同任务实测;文中对 dsh 的关联引用来自 DeepSeek Harness 官方架构文档,不构成选型承诺。
夜雨聆风