ByteNoteByteNote
Cordis 插件生命周期与 effect 资源释放
字

字节笔记本

2026年10月6日 · 约 8 分钟读完

Cordis 插件生命周期与 effect 资源释放

API中转
¥120

本篇是 Cordis 插件开发系列教程的一章,主题是插件的生命周期:从加载到卸载会经历什么,插件作者又该如何保证自己创建的资源被干净地释放。示例代码与结论来自 DeepSeek 开源 agent 框架 DeepSeek Harness 官方文档中的 Cordis 教程(MIT 许可证),本文在保留技术事实的前提下做了改写与补充。

Cordis 是 DeepSeek Harness 的插件内核,奉行「一切皆插件」的架构。插件可能因为修改配置、热重载、显式资源释放或所需服务消失而被卸载。通过 Cordis API 建立的注册属于 effect,会在所属插件卸载时自动撤销;在这些 API 之外管理的资源,必须包装在 ctx.effect() 中。

用 ctx.effect 包装外部资源

对于 Cordis 尚未管理的资源,例如定时器、连接或 watcher,应将其包装在 ctx.effect() 中并返回 disposer,也就是资源释放函数。

创建 lifecycle.ts,将它放在 tmp/cordis-tutorial 中:

ts
import type { Context } from '@deepseek-ai/cordis'

export const name = 'lifecycle-demo'

function heartbeat(ctx: Context) {
  console.log('heartbeat plugin loading')
  ctx.effect(() => {
    const timer = setInterval(() => console.log('tick'), 200)
    return () => {
      clearInterval(timer)
      console.log('heartbeat cleaned up')
    }
  })
}

export function apply(ctx: Context) {
  // Mount a child plugin and keep its fiber to dispose it later.
  const fiber = ctx.plugin(heartbeat)
  // The demo timer is itself an effect: if THIS plugin is unloaded first,
  // the pending callback is cancelled instead of firing on a dead app.
  ctx.effect(() => {
    const timer = setTimeout(async () => {
      await fiber.dispose()
      console.log('disposed')
      process.exit(0)
    }, 700)
    return () => clearTimeout(timer)
  })
}

让 cordis.yml 指向该文件:

yaml
- name: './lifecycle.ts'

教程给出的运行命令是 node --import tsx ../../vendor/cordis/bin.js,路径指向仓库内 vendor 的 Cordis 入口。运行后会得到如下输出:

text
heartbeat plugin loading
tick
tick
tick
heartbeat cleaned up
disposed

请留意三点:

  • ctx.plugin(heartbeat) 会把一个来自代码的函数挂载为插件,这与 YAML loader 为每个配置项执行的操作相同。函数插件不需要 apply 方法:Cordis 会直接调用该函数,其名称只用于诊断。只有对象形态才要求 apply 方法,例如 ctx.plugin({ apply(ctx) { /* ... */ } })。调用会返回一个 fiber,即一个已加载插件实例的运行时句柄。
  • effect 主体在加载期间运行,它返回的 disposer 在卸载期间运行。对于生命周期与插件一致的资源,你绝不需要自行调用 disposer。
  • fiber.dispose() 会等该插件的所有清理工作(包括异步 disposer)完成后才结束,并递归卸载它挂载的所有子插件。

Fiber 状态机

每个已加载插件实例都拥有一个 fiber,并在以下状态之间转换:

text
PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED
                 ↘ FAILED
  • PENDING:插件已经声明,但所需服务尚不可用。
  • LOADING / ACTIVE:apply 正在运行,或者已经完成。
  • FAILED:apply 或配置校验抛出异常。
  • UNLOADING / DISPOSED:disposer 正在运行,或者一切均已拆除。

Cordis fiber 状态机:PENDING 到 DISPOSED 的完整流转

PENDING 状态值得单独解释。教程特别指出,它通常是「为什么我的插件没有任何输出」这类问题的答案:插件已经声明,但依赖的服务迟迟没有就绪,它就会停在等待阶段,看起来像被静默忽略。遇到这种情况,先检查依赖服务是否已经注册、是否按正确顺序声明。

已经属于 effect 的操作

你很少需要亲自编写 ctx.effect(),因为内置注册 API 本身已经是 effect:

  • ctx.on(event, listener):监听器会在卸载时自动移除。
  • ctx.plugin(child):子插件会随父插件一同 dispose(资源释放)。
  • 服务注册属于 effect。ctx.tools.register(...) 等 harness 注册表也会把返回的 disposer 附着到调用插件上,因此会自动撤销。

也就是说,只要资源是通过 Cordis 的注册 API 进入系统的,卸载时的清理就不需要你操心;ctx.effect() 留给那些 Cordis 不认识的资源,例如直接创建的定时器或第三方连接。把它们放进 effect 并返回 disposer 后,Cordis 会在卸载期间调用该释放逻辑,热重载时也不例外。

ctx.effect 的加载、运行与卸载三个阶段

卸载顺序的两个细节

disposer 的执行顺序有一条明确规则:按注册顺序的逆序启动,后申请的资源先释放。但另一个细节容易踩坑:多个异步 disposer 会并发运行,彼此不等待。

如果拆除步骤必须按顺序执行,教程的建议是把它们放在同一个 disposer 中,并在其中依次 await 每一步完成,而不是注册成多个独立的异步 disposer,否则完成次序不可控。

小结

Cordis 的生命周期模型可以浓缩成两句话:凡是内置 API 注册的东西,天然是 effect,卸载时自动撤销;凡是自己获取的外部资源,用 ctx.effect() 包装并返回 disposer。fiber 状态机则提供了一张排查地图,插件行为不符合预期时,先看它停在哪一个状态,多数问题就能定位。

相关文章

分享: