mcpField
N
novu-framework-integration

novu-framework-integration

github.com/novuhq开发4466 次安装19 周前更新
代码仓库

关于 novu-framework-integration

面向使用 `@novu/framework` 在应用代码中定义 Novu 通知工作流的开发者。技能介绍类型化触发负载、渠道与操作步骤,以及供 Novu Cloud 获取工作流内容和数据的 Bridge Endpoint。

主要功能

  1. 01在应用代码中定义工作流
  2. 02校验并类型化触发负载
  3. 03构建渠道与操作步骤
  4. 04为可编辑步骤设置 Schema
  5. 05通过 Bridge Endpoint 暴露工作流

适用场景

  1. 01将通知工作流纳入源代码管理
  2. 02在执行期间获取最新数据
  3. 03允许团队成员在 Dashboard 编辑步骤内容
  4. 04组合延迟、摘要和 HTTP 请求

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/novuhq/skills --skill novu-framework-integration

安装后在 Claude Code 中输入/novu-framework-integration确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
bridge-endpoint.md
deployment.md
email-templates.md
examples.md
schema-validation.md
security.md
studio-and-cli.md
translations.md
workflow-and-steps.md

详细介绍

框架集成

使用 @novu/framework 通过代码构建通知工作流,并与应用源代码一同管理。工作流保存在你的代码仓库中,内容使用你现有的库(React Email、Vue Email、Svelte Email)进行渲染;单个 HTTP 端点(Bridge)即可让 Novu Cloud 使用来自你服务的即时数据执行工作流。

通过代码构建工作流时使用此技能。对于在 Novu Dashboard 中创建的工作流,只需通过 trigger-notification 触发即可——无需使用 Framework。

何时使用 Framework

使用 Framework使用 Dashboard 工作流
工作流必须纳入源代码管理 / GitOps所有内容都由非技术同事负责
需要从数据库 / API 获取即时数据所有数据都能放入触发载荷
使用 React/Vue/Svelte Email 渲染邮件区块编辑器已足够
执行自定义代码(LLM、第三方 API)纯发送流程
需要类型化载荷和步骤控制快速原型

两种方式可以共存——同一环境中可以同时使用代码定义的工作流和 Dashboard 定义的工作流。

工作原理

  1. 使用 @novu/framework 中的 workflow(...) 在代码中定义工作流。
  2. 在应用中公开单个 /api/novu HTTP 路由——即 Bridge 端点。
  3. 将 Bridge URL 同步到 Novu Cloud(通过 npx novu sync 或 GitHub Action)。
  4. 在工作流执行期间,Novu Cloud 会通过经过身份验证的隧道调用 Bridge,以获取包含最新数据的步骤内容。
Trigger ──► Novu Cloud Worker ──► Your Bridge (/api/novu) ──► Provider (SendGrid, FCM, …)

快速开始

1. 初始化项目
npx novu init --secret-key=<YOUR_NOVU_SECRET_KEY>

这会创建一个示例 Bridge 应用,其中包含工作流、环境文件和可正常工作的 /api/novu 路由。

2. 或添加到现有应用
npm install @novu/framework zod @react-email/components react-email
NOVU_SECRET_KEY=<YOUR_NOVU_SECRET_KEY>
3. 定义工作流
import { workflow } from "@novu/framework";
import { z } from "zod";

export const welcomeWorkflow = workflow(
  "welcome-email",
  async ({ step, payload, subscriber }) => {
    await step.email("send-email", async (controls) => {
      return {
        subject: controls.subject,
        body: `Welcome ${subscriber.firstName ?? payload.userName}!`,
      };
    }, {
      controlSchema: z.object({
        subject: z.string().default("Welcome to {{payload.appName}}"),
      }),
    });
  },
  {
    payloadSchema: z.object({
      userName: z.string(),
      appName: z.string().default("Acme"),
    }),
    name: "Welcome Email",
    description: "Sent when a new user signs up",
    tags: ["onboarding"],
  }
);
4. 挂载 Bridge 端点

选择与你的框架匹配的封装方式——请参阅下方的 Bridge 端点设置。

5. 运行本地 Studio
npx novu@latest dev --port <YOUR_APP_PORT>

打开 http://localhost:2022,即可预览工作流、编辑控件并触发测试事件。Studio 会自动创建一个公共隧道,以便 Novu Cloud 访问你的本地 Bridge。

工作流结构

正在设计工作流本身?请参阅 design-workflow/,了解渠道选择、严重程度、critical、摘要默认值、步骤条件和 9 个参考模板(订单确认、付款失败、账户暂停、评论、试用期即将结束、密码重置、Webhook 扇出、先获取再通知)。Framework SKILL 介绍的是如何在代码中表达这些决策;design-workflow/ 介绍的是需要做出哪些决策。

workflow(workflowId, handler, options);
参数类型说明
workflowIdstring环境中的唯一标识符
handler({ step, payload, subscriber }) => Promise<void>工作流主体——按顺序调用各个步骤
optionsWorkflowOptions架构、名称、描述、标签和偏好设置
工作流选项
选项类型用途
payloadSchemaZodSchema | JsonSchema | ClassValidatorClass验证触发载荷,并推断 payload 的 TS 类型
namestring显示在 Dashboard / <Inbox /> 中的易读名称
descriptionstring显示在 Dashboard 中的描述
tagsstring[]用于分类筛选 / Inbox 标签页
severity'low' | 'medium' | 'high'在 Inbox 中进行视觉优先级排序。大多数工作流无需设置。
criticalboolean绕过订阅者偏好设置、跳过摘要且不等待延迟。仅用于必须送达的事件。
preferencesWorkflowPreferences默认渠道偏好设置和 readOnly 标记
工作流上下文

处理器接收 { step, payload, subscriber }:

  • step — 渠道和操作步骤构建器(step.email、step.delay、step.digest,等等)
  • payload — 在触发时传入的强类型数据,并根据 payloadSchema 进行验证
  • subscriber — 收件人的 { subscriberId, firstName?, lastName?, locale?, data?, ... }

渠道步骤

所有渠道步骤的结构相同:

await step.<channel>(stepId, resolver, options?);
步骤必需输出显著输出返回结果
step.emailsubject、bodyattachments、from、replyTo否
step.smsbody—否
step.pushtitle(或 subject)、bodydata、image、icon否
step.chatbody—(按提供商覆盖)否
step.inAppbodysubject、avatar、redirect、primaryAction、secondaryAction、data{ seen, read, lastSeenDate, lastReadDate }
Email 步骤
await step.email("welcome", async (controls) => ({
  subject: controls.subject,
  body: render(<WelcomeEmail name={subscriber.firstName} />),
  from: "[email protected]",
  replyTo: "[email protected]",
}));
In-App 步骤(富载荷)
await step.inApp("inbox", async () => ({
  subject: "Welcome to Acme!",
  body: "We are excited to have you on board.",
  avatar: "https://acme.com/avatar.png",
  redirect: { url: "/welcome", target: "_self" },
  primaryAction: {
    label: "Get Started",
    redirect: { url: "/get-started", target: "_self" },
  },
  data: { entityType: "user", entityId: payload.userId },
}));

In-App 步骤会返回 { seen, read, lastSeenDate, lastReadDate } — 使用该结果来决定后续步骤的 skip。

SMS / Push / Chat
await step.sms("verification", async () => ({
  body: `Your code is ${payload.code}`,
}));

await step.push("new-message", async () => ({
  title: "New message",
  body: payload.preview,
  data: { messageId: payload.id },
}));

await step.chat("notify", async () => ({
  body: `:rocket: Deploy ${payload.id} succeeded`,
}));

操作步骤

step.delay

在执行下一步骤前暂停工作流。

await step.delay("wait-a-day", async () => ({
  unit: "days",
  amount: 1,
}));

支持的 unit 值:seconds、minutes、hours、days、weeks、months。

step.digest

在一段时间内将多个触发事件聚合为一条通知。

const { events } = await step.digest("daily", async () => ({
  unit: "days",
  amount: 1,
  digestKey: payload.projectId, // optional — group by custom key
}));

await step.email("summary", async () => ({
  subject: `${events.length} updates today`,
  body: render(<DigestEmail events={events} />),
}));

对于基于 cron 的摘要,请使用 cron: "0 0 * * *",而非 unit/amount。每个摘要事件包含 { id, time, payload }。每个工作流只能有一个摘要 — 如果需要两阶段摘要,请通过 step.custom 链接第二个工作流。

step.http

在工作流中调用外部 HTTP 端点 — 用于 webhook 扇出,或即时获取数据。

const plan = await step.http("fetch-plan", async () => ({
  method: "GET",
  url: `https://api.example.com/users/${payload.userId}/plan`,
  responseBodySchema: {
    type: "object",
    properties: { planName: { type: "string" }, renewalDate: { type: "string" } },
    required: ["planName", "renewalDate"],
  } as const,
}));

await step.email("notify", async () => ({
  subject: `Your ${plan.planName} plan`,
  body: `Renews on ${plan.renewalDate}.`,
}));

Webhook 风格:

await step.http("webhook", async () => ({
  method: "POST",
  url: payload.webhookUrl,
  headers: [{ key: "Content-Type", value: "application/json" }],
  body: [
    { key: "event", value: "payment_failed" },
    { key: "subscriberId", value: subscriber.subscriberId },
  ],
  continueOnFailure: true,
}));

后续步骤引用 HTTP 响应数据时,HTTP 步骤必须声明 responseBodySchema。只有在架构中声明的属性才能通过 {{ steps.<http-step-id>.<property> }} 访问。

step.custom

运行任意代码,并保存其结果以供后续步骤使用。

const task = await step.custom("fetch-task", async () => {
  const t = await db.fetchTask(payload.taskId);
  return { id: t.id, title: t.title, complete: t.complete };
}, {
  outputSchema: {
    type: "object",
    properties: {
      id: { type: "string" },
      title: { type: "string" },
      complete: { type: "boolean" },
    },
    required: ["id", "complete"],
  } as const,
});

await step.email("reminder", async () => ({
  subject: `Reminder: ${task.title}`,
  body: "Please complete your task.",
}), {
  skip: () => task.complete,
});

自定义步骤的结果只能在后续步骤的 resolver、providers 和 skip 函数中使用,不能用于步骤控件。

步骤选项

await step.email(stepId, resolver, {
  controlSchema,         // Zod | JSON Schema | Class-Validator class
  skip,                  // (controls) => boolean | Promise<boolean>
  providers,             // per-provider override callbacks
  disableOutputSanitization, // boolean — for raw HTML in Inbox
});
skip

有条件地跳过步骤。接收已解析的控件。

await step.email("follow-up", resolver, {
  skip: () => inAppNotification.read === true,
});
providers(按步骤覆盖提供商配置)

自定义发送给底层提供商的请求 — 例如 Slack blocks 或 SendGrid cc。

await step.email("alert", resolver, {
  providers: {
    sendgrid: ({ controls, outputs }) => ({
      from: "[email protected]",
      cc: ["[email protected]"],
      _passthrough: {
        body: { ip_pool_name: "transactional" },
        headers: { "X-Custom": "value" },
      },
    }),
  },
});

_passthrough 会深度合并到最终的提供商请求中 — 类型化的提供商键优先于 _passthrough。

disableOutputSanitization

允许输出中包含原始 HTML / 未转义字符(例如 In-App data.link 中的 &):

await step.inApp("link", async () => ({
  body: "Check it out",
  data: { link: "/p/123?active=true&env=prod" },
}), { disableOutputSanitization: true });

载荷架构

载荷是在触发时传入的数据。定义架构即可获得类型化的 payload 并进行运行时验证。

使用 Zod
import { z } from "zod";

workflow("comment", handler, {
  payloadSchema: z.object({
    postId: z.number(),
    authorName: z.string(),
    comment: z.string().max(200),
  }),
});
使用 JSON Schema
workflow("comment", handler, {
  payloadSchema: {
    type: "object",
    properties: {
      postId: { type: "number" },
      authorName: { type: "string" },
      comment: { type: "string", maxLength: 200 },
    },
    required: ["postId", "comment"],
    additionalProperties: false,
  } as const,
});

TypeScript 需要 as const 才能从 JSON Schema 推断载荷类型。

使用 Class Validator
import { IsString, IsNumber } from "class-validator";

class CommentPayload {
  @IsNumber() postId!: number;
  @IsString() authorName!: string;
  @IsString() comment!: string;
}

workflow("comment", handler, { payloadSchema: CommentPayload });

需要 class-validator、class-validator-jsonschema、reflect-metadata。请参阅 references/schema-validation.md。

步骤控件 — 团队成员无需编写代码即可使用

控件是步骤级输入,非技术同事可以在 Novu Dashboard UI 中编辑,而无需修改代码。控件会通过你定义的架构进行验证(Zod / JSON Schema / Class-Validator)。

await step.email("welcome", async (controls) => ({
  subject: controls.subject,
  body: render(<EmailTemplate hideBanner={controls.hideBanner} />),
}), {
  controlSchema: z.object({
    hideBanner: z.boolean().default(false),
    subject: z.string().default("Hi {{subscriber.firstName | capitalize}}"),
  }),
});
控件中的变量

控件值支持 LiquidJS 模板:

  • {{subscriber.firstName}} — 任意订阅者属性
  • {{payload.userId}} — payloadSchema 中定义的任意载荷字段
  • {{payload.invoiceDate | date: '%a, %b %d, %y'}} — Liquid 过滤器
  • {{subscriber.firstName | append: ': ' | append: payload.status | capitalize}} — 链式过滤器

在 Dashboard UI 中输入 {{,即可自动补全可用变量。

控制项与负载
控制项负载
编辑者仪表板中的非技术同事代码中的开发者
架构每个步骤的 controlSchema每个工作流的 payloadSchema
持久化按环境存储在 Novu Cloud 中在触发时发送
用例主题、文案、样式、行为开关每次触发时的动态数据

工作流偏好设置

在代码中定义默认渠道偏好设置。有关完整的偏好设置解析模型,请参阅 manage-preferences。

workflow("system-alert", handler, {
  preferences: {
    all: { enabled: true, readOnly: false },
    channels: {
      email: { enabled: true },
      sms: { enabled: false },
      inApp: { enabled: true },
    },
  },
});
  • all.readOnly: true 会将工作流设为关键工作流——订阅者无法禁用它。
  • 对于 channels 中未指定的任何渠道,all.enabled 是回退值。
  • 省略时的默认值:所有渠道均为 enabled: true、readOnly: false。

Bridge 端点设置

Bridge 是一个 HTTP 路由(默认为 /api/novu),Novu Cloud 会通过它调用你的应用,以便:

  • 发现已注册的工作流(GET)
  • 为指定的订阅者和负载解析步骤内容(POST)
  • 验证请求中的 HMAC 签名

每个框架都提供了一个 serve 包装器,用于处理解析、HMAC 验证和响应格式化。

Next.js(App Router)
import { serve } from "@novu/framework/next";
import { welcomeWorkflow } from "@/novu/workflows";

export const { GET, POST, OPTIONS } = serve({
  workflows: [welcomeWorkflow],
});
Next.js(Pages Router)
import { serve } from "@novu/framework/next";
import { welcomeWorkflow } from "../../novu/workflows";

export default serve({ workflows: [welcomeWorkflow] });
Express
import express from "express";
import { serve } from "@novu/framework/express";
import { welcomeWorkflow } from "./novu/workflows";

const app = express();
app.use(express.json()); // required
app.use("/api/novu", serve({ workflows: [welcomeWorkflow] }));
app.listen(4000);
NestJS
import { Module } from "@nestjs/common";
import { NovuModule } from "@novu/framework/nest";
import { welcomeWorkflow } from "./novu/workflows";

@Module({
  imports: [
    NovuModule.register({
      apiPath: "/api/novu",
      workflows: [welcomeWorkflow],
    }),
  ],
})
export class AppModule {}

如需依赖注入,请使用 NovuModule.registerAsync——参阅 references/bridge-endpoint.md。

Remix
import { serve } from "@novu/framework/remix";
import { welcomeWorkflow } from "../novu/workflows";

const handler = serve({ workflows: [welcomeWorkflow] });
export { handler as action, handler as loader };
SvelteKit
import { serve } from "@novu/framework/sveltekit";
import { welcomeWorkflow } from "$lib/novu/workflows";

export const { GET, POST, OPTIONS } = serve({ workflows: [welcomeWorkflow] });
Nuxt
import { serve } from "@novu/framework/nuxt";
import { welcomeWorkflow } from "~/novu/workflows";

export default defineEventHandler(serve({ workflows: [welcomeWorkflow] }));
H3
import { createApp, eventHandler, toNodeListener } from "h3";
import { createServer } from "node:http";
import { serve } from "@novu/framework/h3";
import { welcomeWorkflow } from "./novu/workflows";

const app = createApp();
app.use("/api/novu", eventHandler(serve({ workflows: [welcomeWorkflow] })));
createServer(toNodeListener(app)).listen(4000);
AWS Lambda
import { serve } from "@novu/framework/lambda";
import { welcomeWorkflow } from "./novu/workflows";

export const novu = serve({ workflows: [welcomeWorkflow] });
自定义(任意框架)
import { NovuRequestHandler, ServeHandlerOptions } from "@novu/framework";

export const serve = (options: ServeHandlerOptions) =>
  new NovuRequestHandler({
    frameworkName: "my-framework",
    ...options,
    handler: (req, res) => ({ /* method, headers, body, url, transformResponse */ }),
  }).createHandler();

完整的自定义处理程序签名请参阅 references/bridge-endpoint.md。

本地 Studio

通过公共隧道提供工作流实时预览,让 Novu Cloud 可以访问你的计算机。

npx novu@latest dev
# Defaults: --port 4000  --route /api/novu  --studio-port 2022

然后打开 http://localhost:2022(仅限 Chrome)。

CLI 标志
标志默认值用途
-p、--port4000你的应用端口
-r、--route/api/novuBridge 路由路径
-o、--originhttp://localhostBridge 源站
-d、--dashboard-urlhttps://dashboard.novu.co仪表板 URL——欧盟地区请使用 https://eu.dashboard.novu.co
-sp、--studio-port2022Studio UI 端口
-t、--tunnelauto可自托管的隧道 URL(例如 ngrok)
-H、--headlessfalse跳过 Studio UI
npx novu@latest dev --port 3002 --dashboard-url https://eu.dashboard.novu.co

Studio:

  • 自动创建稳定的隧道 URL,例如 https://<id>.novu.sh/api/novu
  • 允许你编辑步骤控制项和负载,以预览不同状态
  • 在 process.env.NODE_ENV=development 下运行——关闭 HMAC 验证,以允许 Studio 访问
  • 提供“Sync”按钮,可将状态推送到 Cloud(实际部署请使用 CI/CD)

触发工作流

通过代码定义的工作流与通过仪表板定义的工作流触发方式相同——在触发端(服务器、队列工作进程、Webhook 处理程序)使用 @novu/api:

import { Novu } from "@novu/api";
const novu = new Novu({ secretKey: process.env.NOVU_SECRET_KEY });

await novu.trigger({
  workflowId: "welcome-email",
  to: { subscriberId: "user-123", email: "[email protected]" },
  payload: { userName: "Jane", appName: "Acme" },
});

你也可以在另一个工作流的 step.custom 中触发工作流:

await step.custom("trigger-summary", async () => {
  return await summaryWorkflow.trigger({
    to: subscriber.subscriberId,
    payload: { events: events.map(e => e.payload) },
  });
});

有关完整的触发选项(批量、广播、主题、覆盖项、transactionId、取消),请参阅 trigger-notification。

React Email(及相关工具)

使用现有组件库渲染电子邮件。

React Email
npm install @react-email/components react-email
import { Body, Container, Head, Html, render } from "@react-email/components";

export const WelcomeEmail = ({ name }: { name: string }) => (
  <Html>
    <Head />
    <Body>
      <Container>Hello {name}, welcome!</Container>
    </Body>
  </Html>
);

export const renderWelcome = (name: string) => render(<WelcomeEmail name={name} />);
await step.email("welcome", async () => ({
  subject: "Welcome",
  body: renderWelcome(payload.userName),
}));

也支持 Vue Email、Svelte Email 和 Remix + React Email。请参阅 references/email-templates.md。

翻译(i18n)

对于基于框架的工作流,翻译内容保存在代码中(而非面向仪表板工作流的 Novu Translation 系统中)。你可以使用任意 i18n 库(例如 i18next),并在解析器中通过 subscriber.locale 获取内容。

import { workflow } from "@novu/framework";
import i18n from "./i18n";

export const localizedWorkflow = workflow(
  "welcome-localized",
  async ({ step, subscriber }) => {
    await step.email("email", async (controls) => {
      const t = i18n.getFixedT([subscriber.locale ?? controls.defaultLocale]);
      return {
        subject: t("welcomeEmailSubject", { username: subscriber.firstName }),
        body: render(<Welcome subject={t("subject")} body={t("body")} />),
      };
    }, {
      controlSchema: z.object({
        defaultLocale: z.string().default("en_US"),
      }),
    });
  },
);

完整的 i18next + React Email 示例请参阅 references/translations.md。

标签

为工作流添加标签,以便将其与相关通知归为一组(供收件箱标签页和仪表盘筛选使用):

workflow("login-alert", handler, { tags: ["security"] });
workflow("password-change", handler, { tags: ["security"] });

在收件箱中,使用 tabs={[{ label: "Security", filter: { tags: ["security"] } }]} 渲染一个“安全”标签页(参见 inbox-integration)。

部署

通过 CLI 同步

将工作流推送到 Novu Cloud:

npx novu@latest sync \
  --bridge-url https://api.acme.com/api/novu \
  --secret-key $NOVU_SECRET_KEY \
  --api-url https://api.novu.co  # use https://eu.api.novu.co for EU
GitHub Actions
name: Sync Novu Workflows
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: novuhq/actions-novu-sync@v2
        with:
          secret-key: ${{ secrets.NOVU_SECRET_KEY }}
          bridge-url: ${{ secrets.NOVU_BRIDGE_URL }}
          api-url: https://api.novu.co
GitOps 工作流
  1. 在本地使用 Studio 对接自己的机器进行开发。
  2. 创建 PR — CI 会对 Development 环境运行 npx novu sync,以进行端到端测试。
  3. 合并到 main — CI 会对 Production 环境运行 npx novu sync。

GitLab CI、Jenkins、CircleCI、Bitbucket、Azure DevOps 和 Travis CI 均可通过 CLI 使用。

生产与安全

  • Bridge URL 必须可通过 HTTPS 从公网访问。 Novu Cloud 会自动扩缩容 — 不会公布 IP 允许列表。
  • 默认启用 HMAC 验证(当 NODE_ENV !== "development" 时)。serve 包装器会处理此事 — 无需编写任何代码。每个请求都包含一个 Novu-Signature 标头(t=timestamp,v1=signature),系统会使用 NOVU_SECRET_KEY 对其进行验证。
  • 本地开发时通过 NODE_ENV=development 自动禁用 HMAC。 请勿在生产环境中禁用。
  • Vercel Preview URL 默认受保护 — 启用 自动化保护绕过,并在 Bridge URL 中通过 ?x-vercel-protection-bypass=<token> 传入绕过令牌。
自定义客户端

全局覆盖默认设置:

import { Client as NovuFrameworkClient } from "@novu/framework";
import { serve } from "@novu/framework/next";

export const { GET, POST, OPTIONS } = serve({
  client: new NovuFrameworkClient({
    secretKey: process.env.NOVU_SECRET_KEY,
    strictAuthentication: false, // disables HMAC — only for local dev
  }),
  workflows: [/* … */],
});

客户端读取的环境变量:

  • NOVU_SECRET_KEY — 你的密钥
  • NOVU_API_URL — 默认为 https://api.novu.co(欧盟地区使用 https://eu.api.novu.co)

常见问题

  1. Bridge URL 必须可通过公网访问 — Novu Cloud 无法使用 localhost。本地使用 Studio 隧道;生产环境则部署到公网。
  2. workflowId 是触发标识符 — 也是传给 novu.trigger({ workflowId }) 的同一个 ID。请使用 kebab-case,并保持稳定。
  3. 工作流中的步骤 id 必须唯一 — 重复的 ID 会在注册时抛出错误。
  4. JSON Schema 上的 as const — 如果没有它,TS 会将类型推断为 string,而不是字面量类型,导致 payload 变为 unknown。
  5. 每个工作流只能有一个 step.digest — 若需实现两阶段摘要模式,可通过 step.custom 链接第二个工作流。
  6. 同一触发产生的摘要 / 延迟结果不会影响其他触发 — 这些结果仅适用于各自的工作流运行。
  7. 自定义步骤的结果不能用于步骤控制 — 只能在后续步骤的 resolver、providers 或 skip 回调中使用。
  8. 每次更改工作流后都要同步 — Novu Cloud 需要了解新增 / 重命名的工作流以及更新后的控制架构。请将 npx novu sync 添加到 CI/CD 中。
  9. 当 NODE_ENV !== "development" 时,HMAC 在本地会验证失败 — 将其设为 development,让 Studio 能够访问你的 Bridge,或者在 Client 中禁用严格身份验证。
  10. 不要将 secretKey 存储在客户端包中 — 它只能在服务端使用。请将工作流和 Bridge 路由保留在服务端代码中,不要放在任何 "use client" 模块中。
  11. 提供商覆盖项 _passthrough 不会经过验证 — 拼写错误不会在编译时触发错误。请尽可能使用已知的、带类型的提供商键。
  12. 更改延迟 / 摘要步骤的内容不会影响已安排的事件 — 内容会在原始触发时保存。
  13. 工作流处理程序在重试时必须具有确定性 — Novu 会重新调用 Bridge 来解析步骤内容。请避免在 step.custom 之外产生副作用(只有 custom 步骤的结果会被持久化保存)。
  14. @novu/framework 要求 Node.js ≥ 20。

代码风格提示

  • 每个工作流对应 src/novu/workflows/<workflow-id>.ts 下的一个文件,并从总导出文件 src/novu/workflows/index.ts 重新导出。
  • 优先使用 Zod 架构,自动补全和类型推断效果最佳。仅在需要 Zod 未提供的功能(oneOf、if/then/else、$ref)时使用 JSON Schema。
  • 将 React Email 模板与使用它的工作流放在一起(src/novu/workflows/welcome/template.tsx)。
  • 将共享的 step.custom 逻辑封装为辅助函数(fetchUser(payload.userId)),以便复用。
  • 对于 NestJS,使用 NovuModule.registerAsync 和 NotificationService,以便工作流定义可以注入服务。

参考资料

评论

支持 Markdown