Skip to content

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 依赖 appLoggermessageStoreProducer 依赖 appLogger

Cordis 的价值就在这里:组合关系、事件关系、服务依赖和副作用清理都落在同一个 Context 里。

每个插件的职责

这个练习故意把能力拆细。拆细以后,插件边界更清楚。

插件职责用到的 Cordis 能力
loggerPlugin提供日志服务ctx.provide()
messageStorePlugin提供消息存储服务ctx.provide()
messagePrinterPlugin监听消息并写入存储injectctx.on()ctx.effect()
messageProducerPlugin定时产生消息injectctx.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() 如何创建 Fiberpackages/core/src/registry.ts
ctx.effect() 如何收集清理函数packages/core/src/fiber.ts
服务如何 provide / injectpackages/core/src/reflect.ts
事件如何自动清理packages/core/src/events.ts

也可以继续看 DeepSeek Harness,它把 Cordis 用在 Agent Harness 上,把模型适配器、工具系统、会话日志和 Web UI 都做成插件能力。

小结

Cordis 入门不是背 API,而是建立一种插件运行时思维:

text
能力做成插件
副作用交给 effect
通信走事件
依赖写进 inject
组合交给父插件

如果你能用这几句话解释刚才的 message-runtime,就已经跨过 Cordis 的入门门槛了。


系列导航