事件发布与订阅
本教程共 32 篇 · 第 23 篇 · 更新于 2026-08-15 · 约 5 分钟阅读
本节目标:学会声明事件类型、发布与监听事件,掌握五种分发模式,并知道事件与服务怎么选。
事件是什么
服务支持直接调用;事件让插件不知道谁在监听就能发出通知。harness 用事件处理工具结果、模型请求、审批决定等交互。监听方通过 ctx.on 注册回调,触发方通过 ctx.emit 广播,两边互不引用。
发布与监听
// 触发方:广播一条消息
ctx.emit('my-plugin/ready', { id: 'worker-1' })
// 监听方:收到后处理
ctx.on('my-plugin/ready', ({ id }) => {
console.log(`${id} is ready`)
})
ctx.on 属于 effect,监听器随插件卸载自动移除,不需要手动维护 removeListener。
事件类型定义
事件名和监听器签名可以用声明合并注册,让 ctx.emit 与 ctx.on 都有完整类型,拼错事件名或参数类型会在编译期报错:
declare module '@deepseek-ai/cordis' {
interface Events {
'my-plugin/ready': (payload: { id: string }) => void
'my-plugin/check': (input: string) => boolean | undefined
'my-plugin/transform': (input: string, next: () => Promise<string>) => Promise<string>
}
}
命名遵循 namespace/action 约定,让扁平的事件命名空间保持易读。harness 的实例:agent/step、agent/request、tools/result、session/event。
五种分发模式
事件采用哪种模式是契约的一部分,决定监听器能否返回值、能否并发、能否短路:
| 模式 | 调用 | 语义 |
|---|---|---|
| emit | ctx.emit() | 同步广播,不等待、不收集返回值 |
| parallel | await ctx.parallel() | 所有监听器并发运行,一同等待 |
| serial | await ctx.serial() | 按序运行并等待;第一个非 null/false/undefined 返回值胜出 |
| bail | ctx.bail() | serial 的同步版本 |
| waterfall | await ctx.waterfall() | 环绕中间件,见下 |
每个 harness 事件都在所属子系统页面上记录它的分发模式。
waterfall:转换或短路
waterfall 是实现拦截的模式。每个监听器收到参数和一个 next() 续延,可以转换 next() 的返回值,也可以不调用 next() 直接返回——这会把链条其余部分短路,Cordis 称为否决:
ctx.on('demo/transform', async (input, next) => {
if (input.includes('blocked')) return '** blocked **'
return next()
})
Warning只负责观察或标注的 waterfall 监听器必须调用
next()。不调用代表有意短路——日志监听器忘记调next(),会悄无声息吞掉所有下游默认行为。
harness 用 waterfall 处理可包装或可回答的决策:agent/request 允许插件替换模型调用配置,approval/request 允许策略代替用户作答。
跨插件通信示例
一个统计服务在每次变化时发出事件,另一个插件负责打印。两个插件互不知道对方存在,由服务注册表和事件连接:
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 = 'reporter'
export const inject = ['stats']
export function apply(ctx: Context) {
ctx.on('stats/report', (name, count) => {
console.log(`[stats] ${name} -> ${count}`)
})
ctx.stats.bump('tool_call')
}
组合加载后输出 [stats] tool_call -> 1。想给统计加新消费者?再写一个监听插件即可,发布方一行不用改。
harness 的真实事件
三大事件域(会话 / Agent / 能力)在第 12 章讲过,这里不再复述。写代码时只要记住:harness 里每个事件都有明确的分发模式,写监听器前先查它属于哪一域、什么模式,再决定用 ctx.on 还是哪种 emit。
Note
turn/*、step/*、tool/call、compaction/*是持久化的会话事件类型,不是同名 Cordis 事件。要观察它们,监听session/event再检查event.type。
事件 vs 服务怎么选
两条通信通道各有定位:
- 直接调用、要拿返回值:用服务。调用方明确知道要谁的能力。
- 通知广播、不关心谁响应:用事件。发布方不知道监听者存在。
- 拦截/改写/决策链:用 waterfall 事件,这是 harness 扩展点的主形态。
- 可替换实现:用服务(换提供方),事件负责观察与策略。
一条经验:能力提供用服务,能力观察与策略用事件。两者经常组合——服务负责干活,事件负责把干完的活广播出去,并允许其他插件在干活的路上插手。
小结
- 事件 = 不知道谁在听也能广播的通知;
ctx.on订阅、ctx.emit发布。 - 事件类型用声明合并注册,拼错事件名编译期就报错。
- 五种分发模式:emit / parallel / serial / bail / waterfall。
- waterfall 是环绕中间件;观察型监听器必须调
next()。 - 能力提供用服务,能力观察与策略用事件。