可逆效应 · 反应式共效应 · 插件框架 · 热更新 · TypeScript —— cordiverse/cordis(Cordis)是一个把「动态组合」做成运行时能力的元框架。 它不绑定任何业务领域,只提供一套通用的组合语义: 组件装上即生效、卸载零残留、改动立刻热更。
这份能力不是纸上谈兵。Koishi 生态的插件体系构建在它之上, 2026 年 8 月开源的 DeepSeek Harness(仓库描述:Everything is a Plugin) 也把它作为底层插件框架 vendor 进来。 理解 Cordis,等于提前看到插件化架构的下一站。
动态组合的三道坎
插件化架构早已是常态:机器人平台挂功能模块,IDE 装扩展, Agent 框架注册工具链。但「能装」和「装得干净」是两回事, 多数团队卡在三道坎上。
• 卸载残留:监听器、定时器、服务注册散落在各模块里, 插件卸载后它们仍在运行。轻则内存缓慢增长, 重则出现「卸载了的指令还在响应」的幽灵行为。
• 依赖编排:插件 B 要用插件 A 提供的服务,谁先启动? 手动排启动顺序,插件一多就变成牵一发动全身。
• 热更新缺失:改一行配置要重启整个进程, 长连接断开、内存状态清零,调试一个参数要等几十秒。
想象一个机器人平台的具体事故:上午装了「签到」插件, 下午嫌它吵而卸载。结果第二天凌晨,定时任务照常发消息, 用户以为平台被入侵。根因只有一个—— 卸载路径上漏了一处清理,而这种事防不胜防。
这三道坎的共同根源,是组件的副作用没有「归属」。 没人知道哪些资源属于哪个插件,自然没人能负责回收; 依赖关系没有声明式表达,加载顺序只能靠人肉维护。 平台越成功、插件越多,欠下的技术债就越重。
Cordis 的目标用户,正是被这些问题反复折磨的人: 插件平台与 Agent harness 的开发者、微内核架构的实践者、 以及每天和几十上百个插件打交道的生态作者。 他们需要的不是又一个 DI 容器, 而是一套把「组合」本身变成安全操作的运行时。 衡量标准也很朴素:敢不敢在生产环境直接装卸插件。
三步上手与真实样例
上手路径短得惊人:安装、建上下文、挂插件,三条语句进正题。
npm install cordis
import { Context } from 'cordis'
const root = new Context()
root.plugin(myPlugin, config)
样例一:可逆的副作用。 Cordis 里所有资源注册都走 ctx.effect(),
它要求你在创建资源的同时给出「逆操作」:
root.plugin((ctx) => ctx.effect(() => {
const timer = setInterval(tick, 1000)
return () => clearInterval(timer)
}))
输入是一个创建函数,动作是注册定时器并返回清理函数,
结果是:插件被卸载时,运行时按 LIFO 顺序自动执行全部逆操作,
定时器、监听器、服务注册一次清空,无需手写任何清理逻辑。
事件监听用 ctx.on() 注册,本质也是 effect,
退出时自动摘除,从机制上杜绝了泄漏。
样例二:声明式依赖注入。 插件用 inject 声明所需服务,
框架保证依赖就绪后才执行插件主体:
const plugin = {
inject: ['database'],
apply(ctx) {
const db = ctx.database // 此处一定可用
},
}
输入是依赖清单,动作是等待所有依赖激活,结果是:
启动顺序由依赖图自动推导,删插件、加插件都不会打乱秩序。
类插件还可以用 @Inject() 装饰器声明,
配置项则交给 Standard Schema 校验,类型即契约。
样例三:配置驱动加载与热更。 配合 @cordisjs/plugin-loader 与 @cordisjs/plugin-hmr,插件树写在 YAML 配置里,支持 disabled 开关、config 插值与按需隔离。保存文件后, 框架只重载受影响的插件,其余保持运行,改完即生效。 HMR 会先收集模块依赖图,被改动波及的插件局部重载, 外部依赖变更才触发全量重启,避免无谓的连锁重启。
常见误区有两个:在 ctx.effect 之外创建资源,
等于主动放弃自动回收;声明了 inject 却无人提供该服务,
访问时会得到明确的报错提示,而不是静默的 undefined。
另外要注意,配置校验是同步的,异步校验暂不支持,
别在 Config 里写 async 函数。
可逆效应与反应式依赖
Cordis 的设计围绕两个正交维度展开, 对应论文《A Programming Paradigm for Spatiotemporal Composability》。
• 时间维:可逆效应。 每个上下文变换都携带一个由运行时 追踪的逆操作,组件移除即「倒带」,系统状态完整还原, 这是框架保证的不变量,而非开发者的编码纪律。
• 空间维:反应式共效应。 组件声明自己的依赖(coeffect), 环境一有变化,运行时按声明规格通知并重激活相关组件。
实现上,Context 是一个 Proxy 容器,属性访问经由 ReflectService 拦截,强制走 provide / get 协议; 每个插件对应一个 Fiber,内部是六态状态机 (PENDING、LOADING、ACTIVE、FAILED、UNLOADING、DISPOSED)。 Fiber 用一串 epoch 标记自己依赖的服务实例: 依赖被卸载或替换,epoch 变化,Fiber 自动进入重载流程, 下游再级联刷新——这正是反应式依赖的落地形态。 reload 与 unload 之间用 inertia 衔接, 让「热更」与「卸载」可以无缝交替,不会出现中间空窗。
Loader 层做增量对账:配置 diff 之后只重挂变化的分支, 不动的插件原样保留;事件系统提供五种分发模式 (emit、parallel、serial、bail、waterfall), 从观察、并行扇出到中间件链式包装, 一张 API 覆盖协作的全部形态。
对照常见方案,创新点很清晰:
• 对比 ZIO、Effect-TS 这类 monadic 效应库, Cordis 是覆盖在普通宿主代码上的叠加层,业务无需重写;
• 对比 RAII、STM 的静态作用域,撤销范围不预先固定, 组件装多久,运行时就追踪多久;
• 对比手写清理与 React useEffect, 逆操作由「创建与回收结构配对」保证,天然防漏。
代价同样真实:Proxy 与符号魔法抬高学习曲线; 项目处于 4.0.0-rc.8,API 仍在变动; 副作用追踪也带来一定的运行时开销。 它适合把「可组合性」当作核心价值的架构, 而不是所有应用的默认选择。
落地场景与演进空间
两个方向可以立即落地。
• Agent harness 的插件层:DeepSeek Harness 已验证这条路。 工具注册、模型供应商切换、提示词片段全部插件化, 「卸载干净」意味着试错零成本——会话开始前注册一组工具, 会话结束后整组卸载,下一个会话从干净状态起步, 换掉一个坏工具不会污染后续对话。
• 多租户与微前端:用 ctx.isolate() 隔离不同租户的
服务实例,一个进程内互不干扰,配置变更只作用于
自己的隔离域,扩容租户如同安装插件。
更远的想象在论文的 future work 里:自演化 Agent—— 让 AI 在无人监督下持续生成、替换自己的运行时组件。 可逆效应让每一次「换组件」都变成可回滚的事务, 反应式依赖让替换自动传导到下游,加载器负责落盘与对账, 整套机制恰好构成安全演化的闭环。 如果动态组合范式有一个终点, 那大概就是系统自己学会安全地重构自己。
夜雨聆风