← 返回目录

第 2 卷

第 2 / 4 卷日期:

插件接手。

serial:异步地逐个询问

复制代码 1ctx.on('chat/intent', (text) => {

2if (text.includes('天气')) { 3return 'weather'// 我负责天气,返回非空值 = 我接单了

4} 5// 返回 undefined = 我不处理,继续问下一个*

6})

7ctx.on('chat/intent', (text) => { 8if (text.includes('时间')) {

9return 'time'// 我负责时间,返回非空值 = 我接单了 10}

11})

12 13const res = await ctx.serial('chat/intent', '帮我查一下天气')

14console.log('返回的结果:', res)// 'weather'

serial 按注册顺序逐个 await 监听器,一旦某个监听器返回了非 null/false/undefined 的值,就立刻停止,把这个值作为结果返回,后面的监听器不再

执行。这就是“短路竞争”。

bail:serial 的同步版本

复制代码 1const syncRes = ctx.bail('chat/intent', '现在的时间是几点')

2console.log('返回的结果:', syncRes)*// 'time'*

bail 和 serial 的竞争语义完全一样,唯一的区别是:bail 同步执行,不 await。当你的监听器都是同步函数时,用 bail 更简洁,省掉一个 await。

waterfall——洋葱模型,层层加工

waterfall 是五种模式里最强大、也最需要理解的一个。waterfall 模式下的监听器像洋葱一样层层包裹,可以加工下游的返回值,也可以短路。

复制代码

1//监听器一(外层):给下游返回的回复加上署名(加工下游返回值)
2ctx.on('chat/reply',(text, next) =>{
3constdownstream =next() //先拿到下游的结果
4return`【bot】${downstream}` //再加工:加上署名
5})

6

7//监听器二(内层):遇到敏感词就短路(不调用next)
8ctx.on('chat/reply',(text, next) =>{
9if(text.includes('脏话')) {
10return'(内容已拦截)' //直接返回,不调用next,短路!
11}
12returnnext() //正常情况,继续往下走
13})

14 15// 最内层的默认行为:真正的回复生成(占位)

16const reply1 = ctx.waterfall('chat/reply', '你好啊', () => '你好,很高兴见到你')

17console.log('正常回复:', reply1)

18 19const reply2 = ctx.waterfall('chat/reply', '这句包含脏话', () => '不应该出现')

20console.log('被拦截的回复:', reply2)

请仔细读这段代码,waterfall 的监听器签名比别的模式多了一个 next 参数,next() 就是 “调用下游”的开关:

调用 next():把控制权交给下一个监听器(最终到最内层的函数),拿到下游返回值后,你可以加工它再返回。

不调用 next(),直接返回:短路。下游全部跳过,你返回的值就是最终结果。

只负责观察 / 记录的 waterfall 监听器,必须调用 next(),否则会无意中短路,吞掉所有下游行为。短路应该是一个有意为之的动作,而不是“忘了调 next”的副作用。

serial/bail 的短路和 waterfall 的短路是两种完全不同的机制。前者靠返回值,返回非 null/false/undefined 的值就返回并停止后续监听器;后者靠是否调用

next(),不调用就否决下游,与返回值无关。别把这两个短路混为一谈。

监听器的执行顺序,能控制吗?

能。刚刚我们多次提到按注册顺序,但 ctx.on 还接受一个 prepend 选项,让监听器插到队首:

复制代码 1// 普通注册:排到队尾

2ctx.on('chat/message', normalListener)

3// prepend:插到队首,最先执行 4ctx.on('chat/message', priorityListener, { prepend: true })

这个选项在“别的插件已经注册了监听器,而你的监听器必须抢在它们之前运行”时很有用。不过要克制使用,大部分时候,依赖注册顺序本身,比用 prepend 硬插队更清晰。

分发原理

看到这里,你可能觉得这五种模式是五套独立的机制。其实不是,它们的源码实现极简,而且藏着同一个原理:五种模式共享同一个监听器列表,区别只是怎么遍历这个列表。

先看所有模式的共同起点——dispatch()(node_modules/@deepseekai/cordis/src/events.ts):

全站分卷索引 · 4 卷
  1. 1第 1 卷
  2. 2第 2 卷
  3. 3第 3 卷
  4. 4第 4 卷