Cordis 入门 06:做一个迷你插件运行时
前 5 篇已经分别练过插件、effect、事件、服务和插件组合。最后一篇把这些能力合在一起,做一个迷你运行时。
这个项目不追求功能复杂,而是帮你形成 Cordis 的完整手感:
text
Context 创建运行时
Plugin 提供能力
Event 传递消息
Service 复用能力
Effect 管理副作用
Composition 组织功能包最终效果
我们要做一个 message-runtime:
loggerPlugin提供日志服务。messageStorePlugin提供内存消息仓库。messagePrinterPlugin监听消息事件并打印。messageProducerPlugin定时产生消息。messageRuntimePlugin组合以上插件。- 主程序运行 3 秒后卸载整个运行时。
项目结构仍然保持简单:
text
cordis-demo
├─ package.json
└─ index.mjs写完整代码
把 index.mjs 改成下面这样:
js
import { Context } from 'cordis'
const ctx = new Context()
function loggerPlugin(ctx) {
ctx.provide('appLogger', {
info(message) {
console.log(`[info] ${message}`)
},
})
}
function messageStorePlugin(ctx) {
const messages = []
ctx.provide('messageStore', {
add(message) {
messages.push(message)
},
list() {
return [...messages]
},
})
}
const messagePrinterPlugin = Object.assign((ctx) => {
ctx.on('message/created', (message) => {
ctx.messageStore.add(message)
ctx.appLogger.info(`new message: ${message.text}`)
})
ctx.effect(() => {
ctx.appLogger.info('message printer active')
return () => ctx.appLogger.info('message printer disposed')
})
}, {
inject: ['appLogger', 'messageStore'],
})
function messageProducerPlugin(ctx, config) {
ctx.effect(() => {
let count = 1
const timer = setInterval(() => {
ctx.emit('message/created', {
id: count,
text: `hello #${count}`,
})
count++
}, config.interval)
return () => {
clearInterval(timer)
ctx.appLogger.info('message producer disposed')
}
}, 'message producer')
}
messageProducerPlugin.inject = ['appLogger']
function messageRuntimePlugin(ctx, config) {
ctx.plugin(loggerPlugin)
ctx.plugin(messageStorePlugin)
ctx.plugin(messagePrinterPlugin)
ctx.plugin(messageProducerPlugin, {
interval: config.interval,
})
}
const runtimeFiber = ctx.plugin(messageRuntimePlugin, {
interval: 800,
})
await runtimeFiber
setTimeout(async () => {
await runtimeFiber.dispose()
console.log('runtime disposed')
}, 3000)运行:
bash
node index.mjs你会看到类似输出:
text
[info] message printer active
[info] new message: hello #1
[info] new message: hello #2
[info] new message: hello #3
[info] message producer disposed
[info] message printer disposed
runtime disposed把链路画出来
这个迷你运行时的关系如下。
这个图里有两种关系:
- 实线组合关系:
messageRuntimePlugin安装子插件。 - 能力依赖关系:
Printer依赖appLogger和messageStore,Producer依赖appLogger。
Cordis 的价值就在这里:组合关系、事件关系、服务依赖和副作用清理都落在同一个 Context 里。
每个插件的职责
这个练习故意把能力拆细。拆细以后,插件边界更清楚。
| 插件 | 职责 | 用到的 Cordis 能力 |
|---|---|---|
loggerPlugin | 提供日志服务 | ctx.provide() |
messageStorePlugin | 提供消息存储服务 | ctx.provide() |
messagePrinterPlugin | 监听消息并写入存储 | inject、ctx.on()、ctx.effect() |
messageProducerPlugin | 定时产生消息 | inject、ctx.effect()、ctx.emit() |
messageRuntimePlugin | 装配子插件 | ctx.plugin() |
这也是写 Cordis 插件时推荐的思路:一个插件只负责一个稳定能力。
故意改坏:删掉 loggerPlugin
把 messageRuntimePlugin 里的 ctx.plugin(loggerPlugin) 注释掉:
js
function messageRuntimePlugin(ctx, config) {
// ctx.plugin(loggerPlugin)
ctx.plugin(messageStorePlugin)
ctx.plugin(messagePrinterPlugin)
ctx.plugin(messageProducerPlugin, {
interval: config.interval,
})
}再次运行,你会发现消费者插件不会正常开始输出。原因是:
messagePrinterPlugin声明了inject: ['appLogger', 'messageStore']。messageProducerPlugin.inject = ['appLogger']。appLogger没有被提供。- 依赖不满足时,Cordis 不会激活这些插件。
这不是坏事。它比“运行到一半才发现 ctx.appLogger 是空”更可控。
继续扩展的练习
现在你已经有了一个可以继续扩展的小框架。可以试试这些练习:
- 增加
messageStatsPlugin,统计收到的消息数量。 - 增加
messageFilterPlugin,过滤掉偶数 id 的消息。 - 把
messageStore改成最多只保留最近 5 条消息。 - 增加一个
ctx.on('runtime/stop')监听器,在停止前打印消息列表。 - 把
interval改成命令行参数。
每做一个练习,都优先想清楚三个问题:
- 这个能力是否应该是独立插件?
- 这个插件依赖哪些服务?
- 这个插件有没有需要清理的副作用?
系列回顾
这套入门系列实际围绕一条主线展开。
现在回看 Cordis,几个概念就不再散了:
Context是插件共享的运行时上下文。Plugin是可安装的能力单元。Fiber是插件运行后的生命周期实例。Effect让副作用能被清理。Event让插件低耦合通信。Service + inject让插件声明式依赖能力。ctx.plugin()让小插件组合成大功能。
下一步怎么学
如果你想继续深入,可以按这个顺序看源码:
| 学习目标 | 源码入口 |
|---|---|
Context 如何创建运行时表面 | packages/core/src/context.ts |
ctx.plugin() 如何创建 Fiber | packages/core/src/registry.ts |
ctx.effect() 如何收集清理函数 | packages/core/src/fiber.ts |
| 服务如何 provide / inject | packages/core/src/reflect.ts |
| 事件如何自动清理 | packages/core/src/events.ts |
也可以继续看 DeepSeek Harness,它把 Cordis 用在 Agent Harness 上,把模型适配器、工具系统、会话日志和 Web UI 都做成插件能力。
小结
Cordis 入门不是背 API,而是建立一种插件运行时思维:
text
能力做成插件
副作用交给 effect
通信走事件
依赖写进 inject
组合交给父插件如果你能用这几句话解释刚才的 message-runtime,就已经跨过 Cordis 的入门门槛了。
系列导航:
