在前面的基础篇中,我们学会了如何写一个插件、给它添加工具、配置参数,最后把它打包成一个可分享的组合包。这一路走来,你一直在“用”插件——但你可能还不清楚,当框架加载你的插件时,背后到底发生了什么。
这一章,我们将揭开这层幕布。
每一个 Harness 插件,从被框架发现到最终被卸载,都会经历一条完整的状态流转路径。这条路径由 Cordis 框架精确管理,确保插件在正确的时机加载、在依赖就绪后启动、在需要时安全卸载。
理解这条路径,不仅是深入 Harness 开发的必经之路,更是你排查问题、设计健壮插件的核心能力。读完本章,你将知道:为什么我的插件没加载?为什么它打印不出日志?HMR为什么能工作? ——这些问题的答案,都在插件的生命周期里。
什么是 Fiber?——每个插件都有的“身份证”
在 Cordis 框架中,每一个被加载的插件实例都拥有一个 Fiber。
Fiber 是什么?简单来说,它是插件在运行时的一个记录对象,里面存着这个插件的所有重要信息:
当前处于什么状态(PENDING、LOADING、ACTIVE……)。
父上下文是谁(这个插件是由谁创建的)。
依赖了哪些服务(inject 中声明的那些)。
配置的快照(加载时使用的配置值)。
所有需要清理的资源(通过 ctx 注册的一切)。
可以把 Fiber 想象成插件的“身份证”和“档案袋”——它记录了插件从诞生到消失的全过程。框架通过 Fiber 来管理每个插件,就像学校通过学生档案来管理每个学生一样。
给初中级开发者的提示:你通常不需要直接操作 Fiber——框架替你管理好了。但理解 Fiber 的存在,能帮你理解“框架是怎么知道插件当前在干什么的”。
六种状态:插件的一生
一个插件从被声明到最终消失,会经历六个状态。我们用一条主线串起来看:
PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED
↘ FAILEDPENDING(等待中)——插件被发现了,但还不能启动
这是插件的“出生”状态。框架已经知道了这个插件的存在——比如在 cordis.yml 中声明了它——但还不能调用它的 apply 函数。
为什么不能?最常见的原因是:依赖的服务还没准备好。
比如你的插件写了 inject = ['tools', 'llm'],那么框架会等到 tools 服务和 llm 服务都成功加载并进入 ACTIVE 状态后,才会把你的插件从 PENDING 移入 LOADING。
给初中级开发者的提示:如果你启动 Harness 后发现某个插件“没反应”,日志里也找不到它的加载信息——先去检查它的 inject 声明的那些服务是不是都正常加载了。这是 PENDING 状态最常见的场景。
LOADING(加载中)——apply 正在执行
依赖已经就绪了,框架开始调用你的 apply 函数。
在这个状态下:
你的 apply 函数中的代码正在执行。
你在 apply 里通过 ctx 注册的所有东西——工具、事件监听、服务——正在被一一注册。
如果 apply 执行过程中抛出了异常,插件会直接进入 FAILED 状态。
LOADING 是一个瞬时状态——它只在 apply 函数执行期间存在。一旦 apply 正常执行完毕,插件就进入了 ACTIVE。
ACTIVE(运行中)——插件正常工作
这是插件的“黄金时期”。apply 已经执行完毕,所有注册都已完成,插件正在正常运行。
在这个状态下:
插件注册的工具可以被模型调用。
插件注册的事件监听器可以接收事件。
插件提供的服务可以被其他插件消费。
插件会一直停留在 ACTIVE 状态,直到触发卸载——可能是配置变更、HMR、手动卸载,或者它依赖的某个服务消失了。
FAILED(失败)——apply 执行出错
如果 apply 函数抛出了错误,或者配置校验失败,插件会进入 FAILED 状态。
进入 FAILED 后,插件不会再被重试。框架会记录错误信息,并继续加载其他插件。你需要修复代码或配置后,重新启动(或触发HMR)来让插件重新加载。
重要提示:FAILED 和 UNLOADING 是两条不同的路径。FAILED 是从 LOADING 直接跳转过来的——插件还没完全启动就失败了。而 UNLOADING 是从 ACTIVE 跳转过来的——插件正常运行后被卸载。
UNLOADING(卸载中)——清理正在进行
当插件需要被卸载时——无论是因为配置变更、HMR、手动调用 fiber.dispose(),还是因为依赖的服务消失了——框架会将插件从 ACTIVE 移入 UNLOADING。
在这个状态下:
所有通过 ctx 注册的资源开始被清理。
事件监听器被移除。
工具被注销。
ctx.effect() 中注册的清理函数被执行。
清理的顺序是:反向注册顺序——后注册的先清理。但多个异步清理函数是并发执行的,没有串行完成的保证。如果你有依赖顺序的清理逻辑,应该把它们放在同一个 ctx.effect() 中,在清理函数内部按顺序执行。
DISPOSED(已销毁)——完全消失
所有清理工作都完成了。插件已经从框架中完全移除,所有资源都已释放。
DISPOSED 是插件的终点状态。到了这里,插件就像从来没有存在过一样——没有残留的监听器、没有泄露的内存、没有悬空的定时器。
状态流转图
把上面六个状态串起来,就是完整的生命周期:
┌─────────── ──────────┐
│ │
▼ │
PENDING ──依赖就绪──▶ LOADING ──apply成功──▶ ACTIVE ──触发卸载──▶ UNLOADING ──清理完成──▶ DISPOSED
│ │
└──apply失败──▶ FAILED ◀─────────┘
│
└──(修复后重新加载,回到 PENDING)两条关键路径:
正常路径:PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED。
失败路径:PENDING → LOADING → FAILED(从 LOADING 直接跳转)。
依赖驱动的加载:为什么 inject 如此重要
我们在之前的章节中用过 inject=['tools'],但当时只是简单提了一句“框架会确保服务就绪”。现在有了生命周期的视角,我们可以更深入地理解它的工作原理。
当一个插件声明了 inject,框架会做这样几件事:
检查所有依赖的服务:看看 tools、llm 这些服务是否已经在 ACTIVE 状态。
如果都就绪:把插件从 PENDING 移入 LOADING,调用 apply。
如果有任何一个未就绪:插件停留在 PENDING,等待。
如果某个依赖服务在运行时消失了(比如被HMR了):插件自动从 ACTIVE 回到 DISPOSED(注意,这里不走 UNLOADING 的完整路径,而是直接销毁),然后当服务恢复时再重新加载。
第4点尤其重要:依赖服务的变化会自动触发依赖方插件的重新加载。你不需要写任何代码来“监听服务变化”或“重新初始化”——框架替你做了。
给初中级开发者的理解:可以把 inject 理解成一种“契约”。你告诉框架“我需要什么”,框架保证“在你开始工作之前,这些东西都准备好了;如果这些东西中途出了问题,我会先让你休息,等它们恢复后再让你重新上班”。
这种设计让插件之间的协作变得极其稳健。你永远不需要担心“我调用 ctx.tools 的时候它还没准备好”这种问题。
自动清理:为什么你不用手动写 cleanup
我们在第一章就接触过“自动清理”这个概念——通过 ctx 注册的任何东西,在插件卸载时都会被自动清理。
现在,结合生命周期,我们可以更完整地理解这个机制。
什么是 Effect?
在 Cordis 中,所有通过 ctx API 进行的注册都被称为 Effect。Effect 的核心特征是:注册时绑定到当前插件的 Fiber,卸载时自动撤销。
以下这些操作都是 Effect:
操作 | 卸载时自动发生 |
|---|---|
ctx.on(event, handler) | 事件监听器被移除 |
ctx.tools.register(tool) | 工具被注销 |
ctx.llm.registerAdapter(...) | LLM 适配器被注销 |
ctx.plugin(childPlugin) | 子插件被递归卸载 |
ctx.effect(() => cleanup) | 返回的清理函数被执行 |
你几乎不需要手动调用 ctx.effect() ——因为内置的注册 API 本身就已经是 Effect 了。只有在管理框架不感知的资源时——比如原生 Node.js 的定时器、TCP 连接、文件监听器——才需要用 ctx.effect() 包装。
清理的顺序
卸载时,清理按反向注册顺序执行——后注册的先清理。但要注意:多个异步清理函数是并发执行的,没有串行完成的保证。
如果你的清理逻辑有顺序依赖(比如必须先关闭数据库连接、再关闭连接池),应该把它们放在同一个 ctx.effect() 中:
ctx.effect(() => {
const conn = createConnection()
const pool = createPool(conn)
return async () => {
// 按顺序清理:先关 pool,再关 conn
await pool.close()
await conn.close()
}
})这样就能保证清理顺序可控。
嵌套上下文:插件里面套插件
我们在基础篇中写的所有插件,都是直接通过 cordis.yml 加载的——它们是顶层插件。
但 Cordis 还支持一种更灵活的模式:在插件内部动态加载子插件。
import type { Context } from '@deepseek-ai/cordis'
export const name = 'parent-plugin'
export function apply(ctx: Context) {
// 在父插件中动态加载一个子插件
const fiber = ctx.plugin(function childPlugin(ctx: Context) {
console.log('child plugin loaded!')
})
// fiber 是子插件的运行时句柄
// 可以通过 fiber.dispose() 手动卸载它
}ctx.plugin() 做了两件事:
创建子 Fiber:子插件拥有自己独立的 Fiber 和生命周期。
继承父上下文:子插件可以访问父插件的上下文——包括父插件注册的服务。
子插件和父插件的关系
子插件有自己的生命周期状态(PENDING → LOADING → ACTIVE → ...)。
子插件可以有自己的 inject 依赖。
当父插件被卸载时,所有子插件会被递归卸载。
你也可以通过 fiber.dispose() 手动提前卸载子插件。
这种嵌套机制让插件可以动态组合——一个插件可以根据运行时的条件,决定是否加载某些子功能。
手动卸载:fiber.dispose()
有时候,你可能想在插件生命周期结束之前主动卸载它。比如:
某个功能在特定条件下不再需要了。
你想在测试中验证插件的清理逻辑。
你需要手动控制资源的释放时机。
这时可以用 fiber.dispose():
import type { Context } from '@deepseek-ai/cordis'
export function apply(ctx: Context) {
// 加载一个子插件,拿到它的 fiber
const fiber = ctx.plugin(somePlugin)
// 某个条件满足时,手动卸载它
ctx.on('some-event', async () => {
await fiber.dispose()
console.log('plugin manually unloaded')
})
}fiber.dispose() 的保证:
插件拥有的所有注册都被移除。
子插件被递归卸载。
返回的 Promise 在所有异步清理完成后才 resolve。
这意味着 await fiber.dispose() 之后,你可以确信这个插件已经完全消失了。
HMR:生命周期的实战应用
我们在基础篇的打包与安装中简单提过HMR。现在,结合生命周期,你可以真正理解它是怎么工作的了。
当 @deepseek-ai/cordis-plugin-hmr 插件被加载后,它会在后台监控你的源文件变化。当你保存一个文件时,HMR 插件会做这样几件事:
卸载旧插件:将旧版本的插件从 ACTIVE 移入 UNLOADING,执行所有清理。
清除模块缓存:清除 Node.js 中对应模块的缓存。
加载新代码:重新加载修改后的文件。
运行新的 apply:用新代码创建一个新插件,走一遍完整的生命周期。
为什么 HMR 能工作得这么干净?
因为所有注册都是 Effect。卸载旧插件时,所有通过 ctx 注册的资源都被自动清理了。新插件加载时,一切从零开始注册。不会有“旧监听器残留”或“工具重复注册”的问题。
你不需要为 HMR 写任何特殊代码——你写的插件天然就支持 HMR。
给初中级开发者的提示:HMR 是开发阶段的神器。修改代码后保存,Harness 会自动重新加载你的插件,你不需要手动重启服务。这在调试和迭代时能节省大量时间。
总结
本章我们深入了 Cordis 框架的核心——插件的生命周期管理。核心收获如下:
概念 | 说明 |
|---|---|
Fiber | 每个插件实例的运行句柄,记录状态、依赖、配置和资源 |
六种状态 | PENDING → LOADING → FAILED/ACTIVE → UNLOADING → DISPOSED |
PENDING | 依赖未就绪,等待中 |
LOADING | 依赖就绪,apply 正在执行 |
ACTIVE | 插件正常运行 |
FAILED | apply 执行出错 |
UNLOADING | 正在清理资源 |
DISPOSED | 完全卸载 |
依赖驱动加载 | 声明 inject 后,框架确保依赖服务就绪才加载 |
Effect | 通过 ctx 的注册操作,自动绑定到当前 Fiber,卸载时自动撤销 |
嵌套上下文 | ctx.plugin() 创建子插件,继承父上下文但有独立生命周期 |
fiber.dispose() | 手动卸载插件,返回的 Promise 在清理完成后 resolve |
HMR | 利用生命周期机制,修改代码后自动卸载旧插件、加载新插件 |
理解生命周期,你就理解了 Harness 插件系统的“骨架”。无论是排查问题、设计插件架构,还是理解热更新、依赖注入等高级特性,这些知识都是基础中的基础。
下一章,我们将学习如何让插件对外提供服务,以及如何通过依赖注入构建可扩展的插件体系。