第 2 卷
插件接手。
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 | //监听器一(外层):给下游返回的回复加上署名(加工下游返回值) |
|---|---|
| 2 | ctx.on('chat/reply',(text, next) =>{ |
| 3 | constdownstream =next() //先拿到下游的结果 |
| 4 | return`【bot】${downstream}` //再加工:加上署名 |
| 5 | }) |
6
| 7 | //监听器二(内层):遇到敏感词就短路(不调用next) |
|---|---|
| 8 | ctx.on('chat/reply',(text, next) =>{ |
| 9 | if(text.includes('脏话')) { |
| 10 | return'(内容已拦截)' //直接返回,不调用next,短路! |
| 11 | } |
| 12 | returnnext() //正常情况,继续往下走 |
| 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):