
字节笔记本
2026年10月6日 · 约 8 分钟读完
Cordis Fiber 状态机:插件从加载到卸载的一生
DeepSeek 开源的 agent 框架 DeepSeek Harness 把 Cordis 用作插件内核,奉行「一切皆插件」的架构。对插件作者来说,比记住几个 API 更重要的,是弄清插件的一生:它什么时候被加载,什么时候被卸载,卸载时注册过的监听器、工具和连接由谁收拾。Cordis 的参考文档把这些问题的答案收敛成一台 Fiber 状态机,本文整理这份参考的核心内容:六种状态的含义与流转、依赖驱动的加载与自动重载、注册资源的自动清理与处置顺序、嵌套上下文、dispose 语义,以及基于 HMR 的热替换。
Fiber 状态机:六种状态与流转规则
每个被加载的插件都拥有一个 Fiber 作用域,状态按下面的路线推进:
PENDING → LOADING → ACTIVE
↘ FAILED
ACTIVE → UNLOADING → DISPOSED| 状态 | 含义 |
|---|---|
| PENDING | 已声明,但所需依赖未就绪 |
| LOADING | 依赖就绪,正在执行 apply |
| ACTIVE | 插件运行中 |
| FAILED | apply 抛出异常 |
| UNLOADING | 插件正在卸载并释放资源 |
| DISPOSED | 已完全卸载 |

这张表值得逐行读一遍。PENDING 是大多数插件的起点:你在配置里声明了它,但它所需的依赖还没就绪,此时它只能等待。依赖一就绪,框架开始执行它的 apply,插件进入 LOADING;apply 正常完成后进入 ACTIVE,正式开始工作。如果 apply 中途抛出异常,Fiber 会停在 FAILED。卸载也不是瞬间完成的:插件先进入 UNLOADING,把注册的资源逐个撤销,最后才落到 DISPOSED。
依赖驱动的加载与自动重载
声明了 inject 的插件会等待所有必需服务就绪:
export const inject = ['tools', 'llm']
export function apply(ctx: Context) {
// ctx.tools and ctx.llm are ready here.
}apply 被调用时,ctx.tools 和 ctx.llm 已经就绪,插件不需要自己判断依赖是否存在。这套机制还有下半场:如果依赖的服务消失,例如提供方被替换时,插件会被自动卸载,从 ACTIVE 直接转为 DISPOSED;待服务恢复后,它又会被重新加载。服务可用性就这样从插件作者手工协调的问题,变成了框架层面的保证。
自动清理:注册即被追踪
通过 ctx 做的任何注册,在插件卸载时都会自动撤销:
export function apply(ctx: Context) {
// Event listener: removed automatically on unload.
ctx.on('some-event', handler)
// Custom resource: the returned disposer runs on unload.
ctx.effect(() => {
const connection = createConnection()
return () => connection.close()
})
}会被自动追踪和清理的操作包括:
- ctx.on(event, handler):事件监听
- ctx.tools.register(tool):工具注册
- ctx.llm.registerAdapter(names, adapter):LLM(大语言模型)适配器注册
- ctx.effect(() => cleanup):自定义资源
清理的顺序有讲究。插件卸载时,处置器按注册顺序的逆序开始调用,但多个异步处置器会并发执行,框架不保证它们逐个完成。因此,存在顺序依赖的清理步骤必须放进同一个 ctx.effect() 返回的处置器里,由这个处置器自己负责串行等待,不能指望框架替你排队。
嵌套上下文:子 Fiber 独立生灭
ctx.plugin() 会创建子 Fiber:子插件继承父上下文,但拥有独立的生命周期,并且随父插件一起卸载。把一组功能挂成某个插件的子插件,父插件被卸载时整组子插件会一并退出,不会留下孤儿实例。这也是按功能拆分插件的底气:组合关系由框架维护,作者只需要声明父子关系。
dispose:手动终止的三条保证
需要提前终止一个插件实例时,可以拿到它对应的 Fiber 并调用 dispose:
import type { Context } from '@deepseek-ai/cordis'
declare const ctx: Context
declare function myPlugin(ctx: Context): void
const fiber = ctx.plugin(myPlugin)
// Dispose it manually later.
await fiber.dispose()dispose 提供三条保证:该插件拥有的所有注册均被移除;它的子插件也被递归卸载;返回的 Promise 会在所有异步清理完成后才兑现。也就是说,await fiber.dispose() 返回之后,你可以确信这个插件在系统里不留残余。
HMR:热替换不留残余
通过 cordis.yml 加载 @deepseek-ai/cordis-plugin-hmr 之后,修改插件源文件会触发三步:先卸载旧插件并清理它的所有注册,再重新加载新代码,最后执行新的 apply。正因为插件注册会被自动清理,热替换不会保留旧实例的任何注册,新旧实例之间不会互相污染。

一个可以亲眼看的最小示例
把上面的机制放进一个最小插件:
export function apply(ctx: Context) {
console.log('plugin loading')
ctx.effect(() => {
console.log('effect registered')
return () => console.log('effect cleaned up')
})
}加载时输出:
plugin loading
effect registered卸载时输出:
effect cleaned up
几行日志正好把状态机走了一遍:apply 执行时打印第一行,effect 注册时打印第二行;卸载时处置器被调用,打印最后一行。
小结
Fiber 状态机是 Cordis 各项自动化能力的骨架:从 PENDING 到 ACTIVE 的推进由依赖就绪驱动,从 UNLOADING 到 DISPOSED 的收尾由自动清理完成,dispose 与 HMR 都建立在这套保证之上。对插件作者来说,要遵守的纪律只有一条:所有资源注册都走 ctx,剩下的加载、卸载、重载与热替换就可以放心交给框架。服务如何向其他插件提供能力、插件之间如何通信,分别由服务与依赖、事件系统两份参考文档回答,与本文合读,正好拼出 Cordis 插件模型的完整图景。



