第 6 章 动手实战(二):事件与 Waterfall

事件让插件无需知道谁在监听就能发出通知。harness 用事件处理工具结果、模型请求、审批决定等交互。本章对应官方教程第 4 章。

6.1 声明、发出与监听

创建 stats.ts——一项负责计数并在每次变化时发出通知的服务:

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

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)
}

要点:

  • interface Events 合并与 interface Context 合并相对应:它声明事件名及监听器签名,使 ctx.emit 与 ctx.on 都有完整类型。

  • namespace/action 命名约定(如 stats/report)让扁平的事件命名空间保持易读。

创建 reporter.ts:

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

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')
  ctx.stats.bump('tool_call')
  ctx.stats.bump('prompt')
}

注意 import type {} from './stats.ts':它不会在运行时导入任何内容,作用是让 TypeScript 看到声明合并。运行输出:

[stats] tool_call -> 1
[stats] tool_call -> 2
[stats] prompt -> 1

因为 ctx.on() 属于 effect,监听器会随插件一同消失,永远不需要手动 removeListener。

6.2 五种分发模式回顾

ctx.emit(name, ...args)               // 同步广播,不等待、不收集返回值
await ctx.parallel(name, ...args)     // 所有监听器并发运行并共同等待
await ctx.serial(name, ...args)       // 顺序执行;首个非 null/false/undefined 返回值胜出并停止
ctx.bail(name, ...args)               // serial 的同步版本
await ctx.waterfall(name, ...args, next) // 环绕中间件,见下文

每个 harness 事件的模式都记录在其所属子系统页面的自动生成参考中。

6.3 Waterfall:转换或短路

waterfall 是实现拦截的模式。每个监听器收到参数和一个 next() continuation:

  • 调用 next() → 执行下游,并可包装其返回值;

  • 不调用 next() 直接返回 → 短路(Cordis 文档称"否决"),下游与默认逻辑都不会运行。

创建 waterfall-demo.ts:

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()
  })

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

cordis.yml 只指向该文件,运行输出:

HELLO
** BLOCKED **

第二行的执行过程:监听器 1 先运行并调用 next() → 触发监听器 2 → 监听器 2 看到 blocked 后不调用 next() 直接返回 → 最内层默认逻辑(传给 ctx.waterfall 的函数)从未运行 → 返回途中监听器 1 把替换消息转为大写。

黄金纪律

只负责观察或标注的 waterfall 监听器必须调用 next()。不调用就直接返回代表有意短路。日志监听器若忘记调用 next(),会悄无声息地吞掉所有下游的默认行为。

harness 使用 waterfall 处理"协作插件可以包装或回答"的决策:

  • agent/request:替换模型调用配置;

  • approval/request:策略代替用户作答;

  • agent/pre-step、llm/stream、tools/* 三事件等(均为瀑布式);

  • 对比:agent/turn-stopping 是 serial 事件,没有 next()。


依据官方教程第 4 章(04-events)整理。

DeepSeek Harness / 第 6 章 动手实战(二):事件与 Waterfall 0 字 0 行 cosolar
2026-09-17T10:12:13.912562133Z 2026-09-17T10:39:03.524818061Z