一个应用小的时候,所有功能写在一起问题不大。入口文件启动服务,业务代码直接调用数据库,事件监听随手注册,配置和日志也可以全局引用。
但应用一旦变成插件化系统,问题就会完全不同。
插件可能由不同人维护,加载顺序可能变化,有些功能可以启用或禁用,有些服务要被多个插件共享,有些插件卸载后还要把事件监听、定时器和连接清理干净。如果没有一层统一的运行时,插件系统很快会变成一堆互相引用、互相等待、互相污染的模块。
Cordis 解决的就是这个问题。
它不是一个面向具体业务的框架,而是一个插件化应用的内核。它关心的不是“机器人怎么收消息”或“数据库怎么查询”,而是更底层的事情:插件如何加载,服务如何共享,依赖如何等待,事件如何传播,资源如何卸载,作用域如何隔离。
可以先用一句话概括:
Cordis 是一个用 Context 组织插件、服务、事件和生命周期的微内核。
为什么插件系统需要内核
插件化听起来很简单:把每个功能写成一个插件,再把插件加载进来就行。
真正麻烦的是插件之间的关系。
比如一个聊天机器人应用里,可能有这些插件:
- 平台适配器插件负责接入 Telegram、Discord、QQ 或飞书;
- 命令插件负责解析
/help、/weather; - 数据库插件提供用户数据读写;
- 日志插件记录消息和错误;
- 权限插件决定谁能调用哪些命令;
- 控制台插件提供网页管理界面。
这些插件不是孤立的。命令插件可能需要数据库,权限插件可能要监听消息,控制台插件可能要读取其他插件的配置,平台适配器收到消息后还要通知一批插件。
如果每个插件都直接 import 另一个插件,系统会越来越难维护:
插件 A 依赖插件 B
插件 B 又依赖服务 C
插件 C 启动顺序不确定
插件 D 卸载后监听还留着
插件 E 重载后逻辑执行两遍
Cordis 的作用,就是把这些关系统一放到内核里管理。
Context 是插件运行的环境
Cordis 最核心的对象是 Context,通常写成 ctx。
它可以理解成插件运行时的环境。插件拿到 ctx 后,不是自己到处找全局对象,而是通过 ctx 接入系统:
ctx.plugin(myPlugin)
ctx.provide('database', db)
ctx.inject(['database'], (ctx) => {})
ctx.on('message', callback)
ctx.emit('message', data)
这些方法分别对应 Cordis 的几项核心能力:
plugin:加载插件
provide:提供服务
inject:声明依赖
on / emit:监听和触发事件
所以 ctx 不是普通参数,而是整个插件系统的入口。插件通过它声明“我提供什么、我需要什么、我监听什么、我产生了哪些副作用”。
这让 Cordis 能够掌握插件运行时的结构,而不是让插件私下互相纠缠。
插件只是接收 Context 的函数
Cordis 里的插件形态很朴素:
function plugin(ctx) {
// 注册服务、监听事件、加载其他插件
}
加载插件就是:
ctx.plugin(plugin)
这种设计的好处是门槛低。插件不需要继承复杂基类,也不需要实现一大套接口。它只要拿到当前上下文,就可以把自己的能力注册进系统。
但简单不代表随意。插件所有重要操作都通过 ctx 完成,Cordis 才能记录这些操作,并在之后处理依赖、事件和清理。
Service 让插件共享能力
插件之间最常见的关系,是一个插件提供能力,另一个插件使用能力。
例如数据库插件提供数据库服务:
ctx.provide('database', db)
其他插件需要数据库时,不直接引用数据库插件,而是声明依赖这个服务:
ctx.inject(['database'], (ctx) => {
ctx.database.query(...)
})
这就是 Cordis 的 Service 机制。
它解决的是耦合问题。使用方只需要知道“我需要 database”,不需要知道 database 是谁创建的、什么时候创建的、具体实现是哪一个。
这样一来,服务可以被替换,插件可以调整加载顺序,测试时也可以提供另一套实现。
Inject 让插件等待依赖就绪
依赖注入最重要的地方,不只是“把对象传进去”,而是“等依赖存在以后再运行”。
假设用户插件依赖数据库:
function userPlugin(ctx) {
ctx.inject(['database'], (ctx) => {
// database 可用后才执行
})
}
如果 userPlugin 先加载,而数据库插件后加载,普通代码很容易报错:数据库还不存在。
Cordis 不要求所有插件都按固定顺序加载。它会先记录依赖关系。等某个插件提供了 database 服务,依赖这个服务的逻辑再被执行。
运行过程可以这样理解:
加载 userPlugin
↓
发现它需要 database
↓
database 暂时不存在,先等待
↓
加载 databasePlugin
↓
提供 database 服务
↓
userPlugin 的依赖满足,开始运行相关逻辑
这就是插件系统和普通模块系统的差别。普通模块通常假设依赖在 import 时已经存在;插件系统则要面对启用、禁用、重载和顺序变化。
Event 让插件松耦合通信
服务解决的是“共享能力”,事件解决的是“通知发生了什么”。
例如一个平台适配器收到消息后,可以触发事件:
ctx.emit('message', session)
命令插件、日志插件、统计插件都可以监听这个事件:
ctx.on('message', (session) => {
// 处理消息
})
触发事件的一方不需要知道谁在监听,监听事件的一方也不需要知道事件从哪里来。
这让插件可以像挂钩一样接入系统流程:
收到消息
↓
触发 message 事件
↓
命令插件处理命令
日志插件记录日志
权限插件检查权限
统计插件更新指标
如果没有事件机制,这些插件就只能互相调用,系统会重新变成一团依赖网。
Lifecycle 保证插件可以干净卸载
插件系统不能只考虑加载,还必须考虑卸载。
一个插件运行时可能会做很多有副作用的事情:
- 注册事件监听;
- 创建定时器;
- 提供服务;
- 打开数据库连接;
- 启动后台任务;
- 创建子上下文。
如果插件卸载时这些东西没有清掉,就会出现旧插件继续响应事件、重载后逻辑执行两次、资源泄漏等问题。
Cordis 的生命周期机制就是为了解决这个问题。插件通过 ctx 注册副作用时,Cordis 可以把这些副作用归到当前插件上下文里。等插件卸载时,相关资源也能跟着释放。
这也是插件热重载能够成立的基础:旧版本退出干净,新版本才不会和旧逻辑叠在一起。
Scope 让上下文可以分层
很多插件系统不只有一个全局环境。
一个插件可能只对某个机器人实例生效,只对某个频道生效,或者只在某个局部功能里提供服务。如果所有东西都挂在全局上下文里,隔离会变得很困难。
Cordis 的 Context 可以派生出子上下文。子上下文可以继承父上下文的一部分能力,同时拥有自己的作用域和生命周期。
可以把它想成一棵树:
根 Context
├─ Bot A 的 Context
│ ├─ 某个频道的 Context
│ └─ 某个局部插件的 Context
└─ Bot B 的 Context
这样一来,局部插件的事件监听、服务和资源可以被限制在自己的范围内。卸载某个子上下文时,也不必影响整个应用。
一条最小运行链路
把这些概念放在一起,Cordis 的运行模型大致是:
创建 Context
↓
加载 Plugin
↓
Plugin 提供 Service 或声明 Inject
↓
依赖满足后执行插件逻辑
↓
插件通过 Event 通信
↓
卸载时按 Lifecycle 清理资源
一个简单例子:
function databasePlugin(ctx) {
const db = createDatabase()
ctx.provide('database', db)
}
function userPlugin(ctx) {
ctx.inject(['database'], (ctx) => {
ctx.on('message', async (session) => {
const user = await ctx.database.getUser(session.userId)
console.log(user)
})
})
}
即使 userPlugin 比 databasePlugin 更早加载,也没有关系。它声明自己需要 database,Cordis 会等数据库服务出现后再执行相关逻辑。
这个例子里,几个核心机制刚好连起来:
databasePlugin 提供 Service
userPlugin 通过 Inject 等待 Service
userPlugin 通过 Event 监听 message
插件卸载时监听应随生命周期清理
Koishi 为什么会用 Cordis
Koishi 是一个聊天机器人框架。它要接入不同聊天平台,处理消息,注册命令,管理中间件、数据库、控制台和插件市场。
这些能力天然适合插件化:
平台适配器是插件
命令系统是插件
数据库服务是插件
控制台是插件
具体功能也是插件
Koishi 需要的不是一个简单的插件数组,而是一套能处理依赖、服务、事件、作用域和生命周期的运行时。Cordis 正好提供这层基础。
可以这样分层理解:
Cordis:插件、服务、事件、生命周期、上下文
Koishi:消息、命令、Session、适配器、控制台
具体插件:天气、AI 对话、数据库、平台接入
Cordis 不直接关心聊天机器人业务,但它让 Koishi 可以把机器人能力拆成可组合、可启停、可依赖、可清理的插件。
背后的理论视角
从工程角度看,Cordis 是插件、服务、事件和生命周期的运行时内核。再往下一层看,它其实在处理一个更一般的问题:动态组件如何在运行时安全地加入、退出和互相依赖。
Cordis 论文把这个问题称为 spatiotemporal composability,也就是“时空可组合性”。时间维度关心组件卸载后副作用能不能撤回;空间维度关心组件之间的依赖能不能随环境变化自动重算。
这套理论视角解释了为什么 Cordis 要把 Context、effect tracking、dependency injection 和 lifecycle 放在同一个内核里:它们不是彼此独立的功能点,而是共同服务于同一个目标——让动态组件在运行时既能安全退出,也能稳定协作。
总结
Cordis 的核心概念可以压缩成一组关系:
Context 是插件运行环境
Plugin 是挂载进环境的功能模块
Service 是插件共享能力的方式
Inject 负责等待依赖就绪
Event 负责插件之间通信
Lifecycle 负责卸载和清理
Scope 负责局部隔离
它像一个微内核:自己不做太多具体业务,但提供稳定的组织方式。只要一个应用开始走向插件化,开始需要动态加载、依赖等待、服务共享、事件广播和安全卸载,Cordis 这样的内核就会变得必要。
一句话概括:
Cordis 不是用来写某个具体功能的框架,而是用来组织一组插件,让它们可以有秩序地协作、隔离和退出。