Cordis:把卸载做完备的插件框架
内容提要
Cordis是一个插件化框架,其核心是通过effect树管理插件生命周期,确保卸载的完备性。它利用Fiber状态机、依赖快照和代理归属改写,实现可逆注册与资源清理,支持热重载、配置双向同步及事件自举。设计强调可撤销性,为agent运行时提供稳定基础,但存在Proxy开销和调试复杂性。
延伸解读
卸载完备性是插件化的真正分水岭
文章指出,插件化的难点不在加载而在卸载。Cordis 通过 effect 树管理生命周期,确保卸载时子插件和资源自动清理,避免泄漏。这种设计使得热重载和动态配置成为可能,而不会积累污染。对于依赖插件化的系统,卸载路径的可靠性比加载功能更重要。
依赖快照与归属改写:避免过期引用
Cordis 在激活时对依赖做快照,卸载后访问会报错,从机制上杜绝了持有过期引用的问题。同时,通过代理改写服务的 this.ctx,使资源归属自动绑定到调用方,服务无需自管理清理。这种设计减少了插件间的耦合,但增加了调试复杂度,因为代理嵌套可能让断点中的对象与源码不一致。
热重载的脆弱性与回滚保障
Cordis 的热重载依赖 Node 私有 API,且已为不同 Node 版本分叉,是系统中最脆弱的部分。但实现中采用全量备份和整体回滚,确保失败不留半吊子状态。这种设计权衡了稳定性与风险,使用时需注意 Node 版本兼容性。
配置即代码:便利与风险并存
Cordis 的配置支持 !!js 表达式,在插件上下文中执行 eval,极大提升了灵活性,但也将配置文件的信任级别提升到与源码相同。分发第三方配置时需谨慎,因为配置可能执行任意代码。这种设计适合可信环境,但对外部输入需严格校验。
Q&A
Cordis 是什么?它的核心设计目标是什么?
Cordis 是一个插件化框架,最初从聊天机器人框架 Koishi 中抽取,现被用于 agent 运行时。其核心设计目标是确保插件卸载的完备性,通过 effect 树管理插件生命周期,实现可逆注册与资源清理,避免热重载后残留垃圾。
Cordis 中插件和 Service 有什么区别?
插件可以是函数、构造器或带 apply 的对象,而 Service 是一个基类,构造时通过 ctx.reflect.provide() 注册为服务。插件可以不提供服务,服务也不必是插件。依赖图上的节点是 Fiber,而不是 Service。
Cordis 如何实现插件卸载的完备性?
Cordis 通过 effect 树管理插件生命周期:子插件是父 fiber 的一个 effect,父 fiber 卸载时按后进先出顺序执行 disposer 列表,子插件随之卸载。effect 创建时检查 fiber 是否活跃,已卸载则抛错。支持生成器部分回滚,确保资源清理。
Cordis 的 Fiber 状态机是如何工作的?
Fiber 有六个状态,通过 epoch 字符串驱动转换。epoch 由依赖的 impl 的 fiber.uid 拼接而成,依赖缺失或变化时 epoch 改变,触发状态转换。inertia 字段处理并发,转换途中新目标只记录,完成后再次比较,保证最终收敛。
Cordis 如何实现依赖快照和归属改写?
依赖快照:激活时复制 store 为快照,ctx.foo 解析的是激活时的值,卸载后访问抛错。归属改写:通过 Proxy 将服务内部的 this.ctx 替换为调用方的上下文,使资源注册到调用方 fiber 上,服务无需知道调用者。
Cordis 的访问控制是如何实现的?
访问控制通过 Proxy 的 get 陷阱实现:未 inject 的属性无法获取,祖先 inject 的可继承,隔离符号必须一致。隔离用 symbol 做命名空间,不同隔离域互不可见,共享域通过共享 symbol 实现。
Cordis 支持哪些事件派发模式?
支持五种:emit(只观察)、parallel(并行等全部)、serial(按序可中断)、bail(一有返回就停)、waterfall(环绕式中间件)。其中 bail 常被忽略,但核心内部使用。
Cordis 的配置系统有什么特点?
配置系统双向同步:配置变化触发 fiber.update() 或卸载,插件更新配置会写回文件。支持表达式(!!js 标签),在插件上下文中求值。自卸载检测通过六个条件区分主动卸载和被动卸载。
Cordis 的热重载机制是如何实现的?
热重载基于 Vite 的传播算法,清空模块缓存(ESM 和 CJS),失败时整体回滚。依赖 Node 私有 API,需要 --expose-internals,且已为 Node 22/23 和 24 分叉。
Cordis 的代价和局限性有哪些?
代价包括:Proxy 嵌套导致性能开销,调试体验被代理污染,热重载依赖私有 API 脆弱,配置可执行代码带来安全风险,API 不稳定。