乐于分享
好东西不私藏

【Cordis学习第四课】事件——让插件在“微信群”里广播消息,而不用互相@

【Cordis学习第四课】事件——让插件在“微信群”里广播消息,而不用互相@
TUTORIAL · CORDIS 第四课2026.08

插件通信只能一对一?

发一条消息,所有插件都收到

事件系统实战

Events · emit / on · waterfall

DSH · Cordis 学习系列

第四课事件广播

📦 4 Parts + Conclusion

👉 滑动

PART 01

最简单的例子

统计 + 报告

PART 02

五种分发模式

不只是广播

PART 03

深入 Waterfall

洋葱模型

PART 04

事件 vs 服务

何时用哪个

PART ///

写在最后

四课知识树

服务是“点对点”的电话,事件是“群发”的朋友圈——你只管发,谁在看、谁回应,你不需要知道。

在第三课中,我们学会了通过“服务(Service)”让插件之间点对点协作——就像你拿起电话,拨通对方的号码,直接对话。

但现实中的系统往往更复杂:一个插件发出通知,可能有多个插件需要响应;一个插件产生数据,可能有多个插件需要消费。如果每个都要通过服务去“一对一”调用,那代码就会变成一团乱麻——就像你要通知100个人开会,不可能一个个打电话,更高效的作法是在群里发一条消息。

Cordis的事件系统(Events)就是为了解决这个场景而设计的。它让插件之间可以解耦地通信:发送方只负责“广播”,接收方只负责“收听”,双方不需要知道彼此的存在

今天这一课,我们就来学习如何在DSH插件中使用事件系统,实现“一对多”的松耦合通信

01

PART

先看一个最简单的例子:统计服务 + 报告监听器

FIRST EXAMPLE

想象一个场景:我们有一个“统计服务”,它记录某些事件(比如“工具被调用了”“Prompt被发送了”)发生的次数,每次计数变化时,它要通知其他人。而我们还有一个“报告插件”,它负责监听这些变化并打印出来。

这种“一产生变化,多出响应”的场景,是事件系统的典型用武之地

STEP 01

创建统计服务(stats.ts)——它提供服务,也发出事件

...typescript

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

// 声明事件:'stats/report'事件会携带两个参数:事件名称和当前计数

declare module '@deepseek-ai/cordis' {

  interface Context {

    stats: StatsService

  }

  interface Events {

    'stats/report'(name: string, count: number): void

  }

}

export class StatsService extends Service {

  private counts = new Map<string, number>()

  constructor(ctx: Context) {

    super(ctx, 'stats')

  }

  bump(name: string) {

    const next = (this.counts.get(name) ?? 0) + 1

    this.counts.set(name, next)

    // 关键:计数变化后,发出事件

    this.ctx.emit('stats/report', name, next)

  }

}

export const name = 'stats'

export function apply(ctx: Context) {

  ctx.plugin(StatsService)

}

STEP 02

创建报告插件(reporter.ts)——它消费服务,也监听事件

...typescript

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

import type {} from './stats.ts' // 仅用于类型声明

export const name = 'reporter'

export const inject = ['stats'] // 依赖stats服务

export function apply(ctx: Context) {

  // 监听'stats/report'事件

  ctx.on('stats/report', (name, count) => {

    console.log(`[stats] ${name} -> ${count}`)

  })

  // 触发几次统计

  ctx.stats.bump('tool_call')

  ctx.stats.bump('tool_call')

  ctx.stats.bump('prompt')

}

STEP 03

组合配置(cordis.yml)

...yaml

- name: './stats.ts'

- name: './reporter.ts'

运行后输出:

...

[stats] tool_call -> 1

[stats] tool_call -> 2

[stats] prompt -> 1

你看到的事件系统的核心机制:

1

声明事件:通过interface Events声明事件名称和参数类型,让TypeScript提供类型安全。

2

发送事件ctx.emit('stats/report', name, next)

3

监听事件ctx.on('stats/report', (name, count) => { ... })

4

自动清理ctx.on()本身是effect,当监听器插件卸载时,监听器自动移除,无需手动removeListener

这就好比你在群里发了一条消息(emit),群里的任何人(on)都能看到,不需要你单独 @ 他们。

监听器插件被卸载时会自动退群(effect 自动清理监听),不会留下“已读不回”的幽灵。

02

PART

五种事件分发模式:不只是“广播”

5 DISPATCH MODES

上面用的是最简单的emit——它只是把事件发出去,不关心监听者有没有收到、处理结果如何。但Cordis提供了5种分发模式,分别对应不同的通信需求:

模式
调用方式
特点
emitctx.emit(name, ...args)同步广播
,不等待、不收集返回值
parallelawait ctx.parallel(name, ...args)并发执行所有监听器
,等待全部完成
serialawait ctx.serial(name, ...args)
顺序执行监听器,第一个返回非空值的监听器“胜出”短路后续
bailctx.bail(name, ...args)serial
同步版本
waterfallawait ctx.waterfall(name, ...args, next)洋葱模型
,每个监听器可修改结果或短路
1

emit:就像你在群里发了一条消息,发完就不管了,爱回不回

2

parallel:就像你在群里发了一份问卷,等所有人填完才汇总

3

serial:就像你挨个打电话问问题,第一个人回答了,后面的人就不用再问了

4

waterfall:就像你在群里发了一句话,每个人都可以在上面修改,最后变成一个完整的结果(类似中间件)。

03

PART

深入 Waterfall:理解“洋葱模型”与短路机制

WATERFALL DEEP DIVE

waterfall是Cordis中最强大、也最需要理解的一种模式。它在Harness中被广泛用于决策流程,比如agent/request事件(插件可以修改模型调用配置)和approval/request事件(插件可以代替用户审批)。

我们来看一个演示waterfall的完整例子:

waterfall-demo.ts

...typescript

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

declare module '@deepseek-ai/cordis' {

  interface Events {

    'demo/transform'(input: string, next: () => Promise<string>): Promise<string>

  }

}

export const name = 'waterfall-demo'

export function apply(ctx: Context) {

  // 监听器1:后注册,但它是“外层”

  ctx.on('demo/transform', async (input, next) => {

    const downstream = await next()

    return downstream.toUpperCase()

  })

  // 监听器2:先注册,但它是“内层”

  ctx.on('demo/transform', async (input, next) => {

    if (input.includes('blocked')) return '** blocked **'

    return next()

  })

  // 触发两次waterfall

  void (async () => {

    console.log(await ctx.waterfall('demo/transform', 'hello', async () => 'hello'))

    console.log(await ctx.waterfall('demo/transform', 'blocked words', async () => 'hello'))

  })()

}

运行结果:

...

HELLO

** BLOCKED **

为什么输入变了?这背后是waterfall的“洋葱模型”

1

监听器的执行顺序从先注册到后注册。但关键在于每个监听器中的next()调用会“跳转”到下一个监听器。

2

形成了层层包裹的结构:监听器1(外层)包裹监听器2(内层);外层通过await next()等待内层执行完,然后对结果做后处理(转大写);内层如果检测到'blocked',直接返回拦截信息,不调用next(),从而短路了更内层的逻辑(本例中最内层是async () => 'hello')。

3

即使内层短路,外层依然能捕获结果,并做统一处理(转大写)。

这个模式在Harness中的实际应用:

1

权限校验:外层负责记录日志,内层负责校验权限。校验不通过时,内层直接返回错误,外层记录日志后也返回错误。

2

缓存拦截:外层负责查缓存,内层负责查数据库。缓存命中时,外层直接返回,不再调用内层。

!踩坑提示 🕳

在 waterfall 监听器中,如果你只是想“观察”数据,必须调用 next()。忘记调用,下游监听器就得不到执行,链条被“悄无声息地截断”——这是最容易踩的坑。

04

PART

事件 vs 服务:什么时候用哪个?

EVENT VS SERVICE

这是一个很实际的问题,我总结了一个简单原则:

1

服务(Service):用于“我需要一个特定的能力”。比如“我需要一个能统计的服务”,你会明确知道你要调用ctx.stats.bump()

2

事件(Event):用于“我想通知别人发生了什么事”“我想在某个时机插入逻辑”。比如“用户提交了表单,我想让所有对此感兴趣的插件都能收到通知”。

一个直观的区别:服务是“主动调用”,事件是“被动触发”。你用服务时,是你主动去“拿”东西;你用事件时,是“扔”出去一个消息,等别人响应。

///

LAST

写在最后

CONCLUSION

这一课我们学习了Cordis的事件系统。如果说服务是“有线电话”(点对点),那事件就是“微信群聊”(一对多广播)。加上waterfall提供的“洋葱模型”,事件不仅能广播,还能让监听器像中间件一样层层处理、按需短路

回顾四课的知识树:

1

第一课:插件是什么——导出一个apply函数。

2

第二课:如何善后——用effect管理资源清理。

3

第三课:如何协作——用Service提供能力,用inject消费能力。

4

第四课:如何通知——用Event广播消息,用waterfall实现中间件链。

下一课,我们将进入“配置(Config)”的世界——如何让插件通过cordis.yml接收外部参数,变得可定制、可复用

最后,我想问一个问题:在你看过的软件或系统中,有没有哪里用到了“事件驱动”的设计?比如“当文件变化时自动重新编译”“当新用户注册时发送欢迎邮件”?

欢迎在评论区分享你观察到的事件驱动案例。点赞最高的3个案例,我会在下期分析它如何用Cordis的事件机制实现。

我是 violetdream,热衷于分享 AI 观察与干货。

关注本公众号,下周更新【Cordis学习第五课】——配置:让你的插件通过YAML“定制”行为,不再写死。

既然看到这里了,如果觉得有用,随手点个赞、在看、转发三连吧。

点赞
在看
转发

THANKS FOR READING