DSH 插件开发 · 第 4 章:事件与分发模式
/ 5 min read
本文对应官方教程第 4 章:事件,示例代码在其基础上改编。
学习目标:掌握事件的声明/发出/监听,分清五种分发模式的适用场景,理解 waterfall 的“必须调用 next”纪律。
服务是调用,事件是广播
服务解决“我知道要找谁”,事件解决“我不知道谁在听”。发出方只负责把事情喊出去,监听方各自响应——工具执行结果、模型请求、审批决定,在 harness 里都以事件流转。
声明、发出与监听
事件表通过 Events 接口的类型合并来扩展。定义一个自己的事件:
import { Context, Events } from '@cordisjs/core'
declare module '@cordisjs/core' { interface Events { 'my:alert': (message: string) => void }}
// 发出方ctx.emit('my:alert', '服务重启了')
// 监听方(同样是 effect,随插件卸载自动移除)ctx.on('my:alert', (message) => { ctx.log('收到警报:', message)})事件名建议带命名空间前缀(my:),和第 3 章的服务命名一个道理——扁平空间,先到先得。
五种分发模式
同一个事件表,Cordis 提供五种发出方式,区别在于等不等、等多久、要不要返回值:
| 方法 | 语义 | 典型场景 |
|---|---|---|
ctx.emit(name, ...args) |
同步通知,不等待 | 日志、埋点这类“喊完就走” |
ctx.parallel(name, ...args) |
并发等所有监听器完成 | 要确认所有方都处理完 |
ctx.serial(name, ...args) |
按注册顺序逐个等 | 后面的监听器依赖前面的副作用 |
ctx.bail(name, ...args) |
第一个非空返回值短路 | “谁能处理?找到就停” |
ctx.waterfall(name, state, next) |
中间件链传递状态 | 请求处理管线 |
bail 的直觉是“责任链”:多个插件都能处理某类请求,谁先返回非空结果就采用谁的。
waterfall:harness 的动脉
waterfall 值得单独一章的待遇,因为 harness 最核心的流程都建在它上面:Agent 的请求循环、审批请求的流转,本质都是一串监听器依次加工状态、决定放不放行。
ctx.waterfall('agent/request', session, async (session) => { // 拿到的 session 已被前面的监听器加工过 return session})两条铁律:
- 观察型监听器必须调用
next()。你的监听器如果只是“看看”,末尾一定要return next(),否则事件链在你这里断掉,后续监听器和最终处理全部失联——这是 waterfall 类框架(Koa、connect 同款)最经典的坑。 - 短路即否决。不调
next直接返回值,等于宣称“这个请求我处理完了/我否决了”。审批插件就是靠“不调 next 并返回拒绝”实现一票否决的。
踩坑提示
- 用错模式:需要确认完成的场景用了
emit(不等结果),是典型的时序 bug 来源。判断标准就一条:发完之后要不要“确定所有人都做完了”。 waterfall监听器里忘了return next(),下游全哑——症状是“某个功能突然不工作”,很难联想到是自己新加的监听器断了链。- 事件参数是按引用传的,监听器里改了 session 对象,后面所有人都看得到——这正是 waterfall 的工作方式,但用
emit/parallel时要小心共享可变状态。
练习
- 定义一个
my:audit事件,两个插件分别监听,用parallel发出并确认两个都完成。 - 实现一个
bail事件:三个监听器只有一个认识这种消息,验证短路行为。 - 写两个
waterfall监听器加工同一个对象,故意让第一个不调next(),观察第二个是否被执行。