Novu 群发实战:使用 Topic 向订阅者组批量触发工作流通知
2026/9/11 5:40:41 网站建设 项目流程

Novu 群发实战:使用 Topic 向订阅者组批量触发工作流通知

【免费下载链接】novuThe open-source communication infrastructure for agents and products项目地址: https://gitcode.com/GitHub_Trending/no/novu

Topic 是 Novu 提供的"订阅者分组"机制:先把一批订阅者按业务语义(如"项目 Alpha 的关注者""工程团队")归入一个带唯一 key 的 Topic,之后只需向这个 Topic 触发一次事件,所有成员就会各自收到通知。本文以 Novu 官方示例 topic-trigger-examples.md 为骨架,结合仓库内 Topic 控制器、事件触发解析与 e2e 测试源码,完整演示 Topic 的创建、订阅者添加、单 Topic 触发、多 Topic 触发及 cURL 调用,并解释 fan-out 与去重等底层行为,读完后你可以直接在自己的业务中实现"一次触发、全员通知"的群发方案。

Topic 是什么:为群发而生的订阅者分组

在日常业务中,很多通知并不是发给单个订阅者,而是发给"一群人":一个开源项目的 watcher、一个公司的全体员工、某个版本的灰度用户群。Novu 的 Topic 正是为这类场景设计的一等公民抽象——它是一个有名字、有唯一 key 的订阅者集合:

  • 创建 Topic:只需提供key(唯一标识,用于后续触发)和name(可读名称);
  • 添加订阅者:把 subscriberId 加入该 Topic;
  • 触发通知:向to传入{ type: "Topic", topicKey },Novu 会自动把消息扇出(fan out)给 Topic 内的每一个订阅者。

从仓库源码看,Novu 对 Topic 的 REST 支持分为两代实现。当前推荐使用的是 v2 接口,控制器定义在 topics.controller.ts,路由为/topics(version 2),提供创建/查询/更新/删除 Topic 及订阅关系管理的完整 CRUD;早期 v1 实现则保留在 topics-v1.controller.ts。本文 SDK 示例走的是官方@novu/api客户端封装,最终都会落到这些 REST 接口上。

创建 Topic 并添加订阅者

官方示例首先演示了最基础的两步:创建 Topic,然后把订阅者加进去。

import { Novu } from "@novu/api"; const novu = new Novu({ secretKey: process.env.NOVU_SECRET_KEY, }); // Create a topic await novu.topics.create({ key: "project-alpha-watchers", name: "Project Alpha Watchers", }); // Add subscribers to the topic await novu.topics.subscriptions.create( { subscriptions: ["user-1", "user-2", "user-3"] }, "project-alpha-watchers" );

几个值得展开的细节

1.key是触发时的寻址依据,name只是展示名。在 v2 控制器中,POST /v2/topics对应的UpsertTopicCommand会携带keynamedatafailIfExists四个字段(见 topics.controller.ts)。其中key是触发阶段topicKey必须匹配的标识,name仅用于在控制台/列表中人类可读,data是可选的自定义元数据。

2. v2 的创建是"幂等 upsert"语义。接口文档明确说明:POST /v2/topics在 Topic 不存在时创建,已存在时则更新(见控制器上的 ApiOperation 描述,topics.controller.ts)。如果你希望"已存在则报错",可以追加查询参数?failIfExists=true,此时若 key 冲突会返回409。该能力由 upsert-topic.usecase.ts 实现。

3. 添加订阅者的入参更灵活。topics.subscriptions.create的第一个参数既可以是字符串数组(如上例,直接传 subscriberId),也支持对象数组{ identifier, subscriberId, name? }。控制器在 mapSubscriptions 中对字符串形式做了归一化。响应体包含data(成功创建的订阅)、meta(成功/失败计数)与errors(失败明细);若所有订阅均失败,接口会返回400(topics.controller.ts),因此建议对响应中的meta.failed做防御性处理。

4. 一个订阅者可以同时属于多个 Topic。订阅关系是独立存储的,同一 subscriberId 加入多个 Topic 互不影响,这为下文"一个用户同时命中多个群组"的触发场景提供了数据基础。

触发到单个 Topic:一次触发,全员通知

创建好 Topic 并加入订阅者后,触发通知的方式与普通触发几乎一致,区别仅在to字段:

const result = await novu.trigger({ workflowId: "project-update", to: { type: "Topic", topicKey: "project-alpha-watchers", }, payload: { projectName: "Alpha", update: "New release v2.0 deployed", }, });

to从"单个订阅者标识"换成了{ type: "Topic", topicKey },Novu 服务端会解析出该 Topic 下的全部订阅者,并为每个订阅者独立生成一份通知(各自拥有独立的 transaction 与消息记录)。

这一点在仓库的 e2e 测试中得到直接印证:trigger-event-topic.e2e.ts 中,先创建 Topictopic-key-trigger-event并加入两名订阅者,再以[{ type: TriggerRecipientsTypeEnum.Topic, topicKey: createdTopicDto.key }]触发(L62),断言响应status === "processed"acknowledged === truetransactionId存在。后续用例进一步按订阅者逐一查询,验证每个订阅者都生成了一条通知记录(L1169-L1176)——这就是"扇出到所有订阅成员"的服务端行为。

在服务端接收端,事件触发请求的to会进入 parse-event-request.usecase.ts 的parseRecipients流程(L572-L593)做合法性校验:单个toto数组均会被验证,其中携带topicKey的对象是合法的接收者类型之一(L563),且该topicKey需匹配SUBSCRIBER_ID_REGEX约束。

触发到多个 Topic:一次请求,命中多群组

通知目标并不限定为单个 Topic。你可以在一次 trigger 中同时指定多个 Topic,甚至混合 Topic 与单个订阅者

const result = await novu.trigger({ workflowId: "company-announcement", to: [ { type: "Topic", topicKey: "engineering-team" }, { type: "Topic", topicKey: "product-team" }, ], payload: { announcement: "Company all-hands at 3pm", }, });

混合接收者的真实验证

仓库 e2e 对"多个 Topic + 多个订阅者"组合进行了完整覆盖:trigger-event-topic.e2e.ts 构造了to = [Topic1, Topic2, subscriberId, {subscriberId, firstName, lastName, email}]的混合数组——两个 Topic 各含 2 名订阅者,另加 2 个直接订阅者,共 6 个去重后的接收者。断言最终消息总数为 12(L1144-L1149),即 6 个订阅者 × 工作流中的 2 个步骤,说明:

  • 多个 Topic 的成员会被合并展开;
  • Topic 成员与直接指定的订阅者可以共存于同一个to数组;
  • 服务端按"接收者 × 工作流步骤"正确生成消息,没有遗漏也没有多余。

另外,测试中还向to追加了一个不存在的 topic key 再触发(L1132),请求依然返回status: "processed"acknowledged: true,且消息总数不变——这说明不存在的 Topic 不会阻塞整个触发流程(建议仍以meta/errors或业务日志确认异常订阅)。

使用 cURL 直接调用 REST API

不依赖 SDK 时,可以直接调用 Novu 的 REST 接口。官方示例使用的是事件触发端点/v1/events/trigger

curl -X POST https://api.novu.co/v1/events/trigger \ -H "Authorization: ApiKey $NOVU_SECRET_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "project-update", "to": { "type": "Topic", "topicKey": "project-alpha-watchers" }, "payload": { "projectName": "Alpha", "update": "New release v2.0 deployed" } }'

要点说明:

  • 请求体中的name对应 SDK 里的workflowId,即工作流的外部标识;
  • to与 SDK 写法完全一致,可以是单个{ type: "Topic", topicKey }对象,也可以是数组(多 Topic、或 Topic 与订阅者混合);
  • payload会透传给工作流步骤模板中的变量;
  • Topic 的管理接口(创建、添加订阅者等)在 v2 路径下,如POST /v2/topicsPOST /v2/topics/:topicKey/subscriptions,对应 OpenAPI 定义见 create-a-topic.mdx;完整接口清单可参考 docs/api-reference/topics 目录下的各端点文档。

Important Notes:四条关键行为约定

原文档以四条注意事项收尾,它们直接决定了使用 Topic 的正确姿势,这里结合源码逐条展开:

  1. Topics 必须先创建,再触发。topicKey是触发时的寻址标识,服务端在触发解析阶段按 key 查找订阅关系。因此生产代码中请确保"创建 Topic + 添加订阅者"先于任何 trigger 执行,或在触发前对 Topic 是否存在做显式检查。

  2. 一个订阅者可以属于多个 Topic。订阅关系按"Topic × 订阅者"独立存储,同一用户加入"工程团队"和"产品团队"两个 Topic 完全合法。这正是多 Topic 触发、以及按兴趣/角色分组发送通知的基础。

  3. Topic 触发会对所有订阅成员逐个扇出通知。每个订阅者获得独立的消息实体与执行链路(如上面的 e2e 断言所示),因此一条 Topic 触发在活动流(Activity Feed)中会呈现为多条独立发送记录,便于按订阅者维度追踪送达状态。

  4. 同一触发请求中跨 Topic 的重复订阅者会被自动去重。例如用户 A 同时属于engineering-teamproduct-team,在一次company-announcement触发中,A 只会收到一份通知而不是两份。这一保证由服务端在展开接收者时统一去重,用户无需在业务侧处理"一人多群组"的重复投递问题。

进一步探索

  • 查看 Topic v2 完整 CRUD 与订阅管理实现:topics.controller.ts、topics-v2/usecases;
  • 了解事件触发接收者解析与校验逻辑:parse-event-request.usecase.ts;
  • 阅读 Topic 触发全链路 e2e 测试:trigger-event-topic.e2e.ts;
  • 浏览 Topic REST API 参考文档:docs/api-reference/topics。

掌握了 Topic 的创建、订阅与触发三板斧,你就可以把"逐个遍历订阅者触发"的循环逻辑,替换成一句trigger+ 一个topicKey,让 Novu 替你完成群发扇出与跨群组去重。

【免费下载链接】novuThe open-source communication infrastructure for agents and products项目地址: https://gitcode.com/GitHub_Trending/no/novu

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询