关于 courier-notification-skills
用于设计和实现多渠道 Courier 通知的指导技能,面向开发者提供按任务查找资料的流程,以及 SDK 用法和通知安全规则。
主要功能
- 01按任务定位相关指南文件
- 02涵盖 Courier 的 Node.js 和 Python SDK 用法
- 03制定跨渠道通知安全规则
- 04提供模板、journey 和路由指导
适用场景
- 01实现邮件、短信或推送通知
- 02编码前核对 Courier SDK 调用
- 03规划通知路由与错误处理
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ trycourier/ courier-skills --skill courier-notification-skills安装后在 Claude Code 中输入/courier-notification-skills确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
LICENSEREADME.mdresources/channels/email.mdinbox-v7-legacy.mdsms.mdms-teams.mdpush.mdwhatsapp.mdinbox.mdslack.mdresources/growth/adoption.mdengagement.mdcampaigns.mdindex.mdreferral.mdreengagement.mdresources/guides/mcp.mdcli.mdelemental.mdjourneys.mdcatalog.mdbatching.mdquickstart.mdmulti-channel.mdproviders.mdpreferences.mdmigrate-from-knock.mdmigrate-general.mdreliability.mdmigrate-from-novu.md详细介绍
Courier 通知技能
关于如何在所有渠道中构建易于传递且引人入胜的通知的指南。
如何使用此技能
- 识别任务 — 用户正在处理哪个渠道、通知类型或跨领域问题?
- 只阅读必要内容 — 使用下面的路由表,找到与任务相关的 1-2 个文件。不要阅读所有文件。
- 检查最新文档 — 如需当前 API 签名和 SDK 方法,请获取
https://www.courier.com/docs/llms.txt - 先综合,再编码 — 编写代码前,规划完整实现(渠道、路由、错误处理)。
- 应用规则 — 每个资源文件都以“快速参考”部分开头,其中包含硬性规则。请将其视为约束,而非建议。
- 检查通用规则 — 生成任何通知代码前,请确认其未违反下方的通用规则。
处理模糊请求
如果用户的请求无法明确对应到特定渠道、通知类型或指南,请先提出澄清问题,再阅读任何资源文件。不要猜测——路由错误会浪费时间,并产生无关代码。
根据需要提出以下问题:
- 哪个渠道? — “你要通过哪个渠道发送:电子邮件、SMS、推送通知、应用内通知、Slack、Teams 还是 WhatsApp?”
- 什么类型? — “这是事务性通知(由用户操作触发,例如密码重置或订单确认),还是营销/增长通知(主动发送,例如功能公告)?”
- 全新项目还是已有项目? — “你是从头开始,还是已有 Courier 代码?如果已有,安装了哪些 SDK 包?”
- 使用什么语言? — “你使用的是 TypeScript/Node.js、Python,还是其他语言?”
无需提问全部四项——只问将请求路由到正确的 1-2 个文件所需的问题。如果请求明确涉及特定主题(例如“帮我处理 SMS”),则跳过提问,直接查看相关资源。
问题 3(“全新项目还是已有项目?”)对路由的影响:
| 答案 | 跳过 | 加载 |
|---|---|---|
| 刚开始使用 Courier/没有现有代码 | (无) | quickstart.md + 相关渠道或类型文件 |
已有项目 — 已安装 @trycourier/courier 或 trycourier | quickstart.md 中的安装和环境设置部分 | 直接跳转到渠道或类型文件;假设 client 已构建。若有帮助,可提供 courier messages list 作为单行健康检查。 |
已有项目 — Inbox v7(@trycourier/react-*) | v8 指南 | 请参阅下方的“Courier Inbox 版本检测”部分,然后查看 inbox-v7-legacy.md |
规范 SDK 结构
编写或评估任何 Courier 代码前,请以此结构为准。如果下方任何文件中的内容看似与之矛盾,请以本节为准,并获取最新文档以厘清——不要粘贴有冲突的代码片段。
Node.js(@trycourier/courier):
import Courier from "@trycourier/courier";
// Reads process.env.COURIER_API_KEY by default
const client = new Courier();
await client.send.message({
message: {
to: { user_id: "user-123" }, // or { email }, { phone_number }, { list_id }, { tenant_id }, etc.
template: "nt_01kmrbq6ypf25tsge12qek41r0", // OR content: { title, body } / { version, elements }
data: { /* merge variables */ },
},
}, {
// Pass the Idempotency-Key via headers. Always set it explicitly here —
// that is the one path guaranteed to be sent to the API across SDK
// versions. Verify against your installed SDK version before relying on
// any other `idempotencyKey` request option.
headers: { "Idempotency-Key": "order-confirmation-12345" },
});
Python(trycourier):
from courier import Courier
# Reads COURIER_API_KEY from env by default
client = Courier()
client.send.message(
message={
"to": {"user_id": "user-123"},
"template": "nt_01kmrbq6ypf25tsge12qek41r0",
"data": {},
},
# Pass the Idempotency-Key via extra_headers. Python does not accept
# idempotency_key= as a keyword argument — the header is the only way.
extra_headers={"Idempotency-Key": "order-confirmation-12345"},
)
方法命名快速查询(两个 SDK 遵循相同结构,Node 使用 camelCase,Python 使用 snake_case):
| 操作 | Node | Python |
|---|---|---|
| 发送消息 | client.send.message({ message }) | client.send.message(message=...) |
| 创建模板 | client.notifications.create({ notification, state }) → 在顶层返回 { id, name, content, … } | client.notifications.create(notification=..., state=...) → response.id |
| 发布模板 | client.notifications.publish(templateId) | client.notifications.publish(template_id) |
| 检索消息 | client.messages.retrieve(id) | client.messages.retrieve(id) |
| 列出消息 | client.messages.list({ ... }) | client.messages.list(...) |
| 订阅用户到列表(增量添加) | client.lists.subscriptions.subscribeUser(userId, { list_id }) | client.lists.subscriptions.subscribe_user(user_id, list_id=...) |
| 替换列表订阅者 | client.lists.subscriptions.subscribe(listId, { recipients }) | client.lists.subscriptions.subscribe(list_id, recipients=...) |
| 创建/替换租户 | client.tenants.update(tenantId, body) | client.tenants.update(tenant_id, ...) |
| 将用户添加到租户 | client.users.tenants.addSingle(tenantId, { user_id }) | client.users.tenants.add_single(tenant_id, user_id=...) |
| 创建批量任务 | client.bulk.createJob({ message: { event } })(必需提供 event) | client.bulk.create_job(message={"event": ...}) |
| 创建/更新资料(合并) | client.profiles.create(userId, { profile }) | client.profiles.create(user_id, profile=...) |
| 获取用户偏好设置 | client.users.preferences.retrieve(userId) | client.users.preferences.retrieve(user_id) |
| 更新用户对某个主题的偏好设置 | client.users.preferences.updateOrCreateTopic(topicId, { user_id, topic: { status, ... } }) | client.users.preferences.update_or_create_topic(topic_id, user_id=..., topic=...) |
| 注册用户设备令牌 | client.users.tokens.addSingle(token, { user_id, provider_key, device }) | client.users.tokens.add_single(token, user_id=..., provider_key=..., device=...) |
| 创建旅程 | client.journeys.create({ name, nodes, enabled }) | client.journeys.create(name=..., nodes=..., enabled=...) |
| 替换旅程(草稿) | client.journeys.replace(id, { name, nodes, enabled }) | client.journeys.replace(id, name=..., nodes=..., enabled=...) |
| 发布旅程 | client.journeys.publish(id) | client.journeys.publish(id) |
| 调用旅程(开始一次运行) | client.journeys.invoke(id, { user_id, data, profile }) → { runId } | client.journeys.invoke(template_id=id, user_id=..., data=..., profile=...) → .run_id |
| 创建旅程专用模板 | POST /journeys/{id}/templates(无 SDK 辅助方法) | POST /journeys/{id}/templates(无 SDK 辅助方法) |
| 发布旅程专用模板 | POST /journeys/{id}/templates/{templateId}/publish(无 SDK 辅助方法) | POST /journeys/{id}/templates/{templateId}/publish(无 SDK 辅助方法) |
| 触发自动化(旧版) | client.automations.invoke.invokeByTemplate(templateId, { recipient, data }) | client.automations.invoke.invoke_by_template(template_id, recipient=..., data=...) |
| 触发临时自动化(旧版) | client.automations.invoke.invokeAdHoc({ recipient, automation }) | client.automations.invoke.invoke_ad_hoc(recipient=..., automation=...) |
| 创建路由策略 | client.routingStrategies.create({ name, routing, channels?, providers? }) → 返回 { id: "rs_...", ... } | client.routing_strategies.create(name=..., routing=..., ...) |
| 替换路由策略(完整 PUT) | client.routingStrategies.replace(id, { name, routing, ... }) | client.routing_strategies.replace(id, name=..., routing=..., ...) |
| 配置提供商 | client.providers.create({ provider, settings, title?, alias? }) | client.providers.create(provider=..., settings=..., ...) |
列出提供商目录(必需的 settings 架构) | client.providers.catalog.list({ keys?, name?, channel? }) | client.providers.catalog.list(keys=..., channel=...) |
| 取消消息 | client.messages.cancel(messageId) | client.messages.cancel(message_id) |
| 检索模板 | client.notifications.retrieve(templateId) | client.notifications.retrieve(template_id) |
| 列出模板 | client.notifications.list() | client.notifications.list() |
| 替换模板(完整 PUT) | client.notifications.replace(templateId, { notification, state }) | client.notifications.replace(template_id, notification=..., state=...) |
| 归档模板 | client.notifications.archive(templateId) | client.notifications.archive(template_id) |
| 获取已发布模板内容 | client.notifications.retrieveContent(templateId) | client.notifications.retrieve_content(template_id) |
上表涵盖了最常见的操作。journeys.md、templates.md、routing-strategies.md 和 providers.md 各自都包含对应资源的完整 SDK CRUD 形状表(包括
list、retrieve、replace、archive)。对于新的多步骤流程,请使用 Journeys,而不是 Automations——参见 Journeys。
不存在的形状(请勿臆造):
client.messages.archive(...)— 归档操作仅支持 REST:POST /messages/{id}/archive。注意:client.notifications.archive(id)和client.routingStrategies.archive(id)/client.providers.archive(id)确实存在——此限制仅适用于 messages 命名空间。client.tenants.createOrReplace(...)— 请使用client.tenants.updateclient.lists.subscribe(listId, userId)— 请使用subscriptions.subscribeUser或subscriptions.subscribe- 不含
event的批量createJob({ message: { template } })—event为必填项 client.users.preferences.update(...)— 请使用client.users.preferences.updateOrCreateTopic(topicId, { user_id, topic })。client.automations.invoke(templateId, ...)— 正确的形状是client.automations.invoke.invokeByTemplate(...)或client.automations.invoke.invokeAdHoc(...)。注意:对于新的多步骤流程,优先使用 Journeys(POST /journeys),而不是 Automations。- Journey 管理 SDK 方法(
client.journeys.create/replace/publish/invoke)确实存在,应优先于原始 REST。Journey 作用域内的模板操作(POST /journeys/{id}/templates、.../publish)目前没有 SDK 辅助方法——请对此类操作使用 REST/curl。Journeys 尚未加入 MCP。 client.routing.create(...)/client.strategies.*— 正确的命名空间是client.routingStrategies.*(Node)/client.routing_strategies.*(Python)。client.integrations.*— 不存在integrations命名空间;服务商配置位于client.providers.*下,服务商类型目录位于client.providers.catalog.*下。
存在但不应作为默认选项的形状:
client.profiles.update(userId, { patch: [...] })— 此方法确实存在,可应用 JSON Patch(RFC 6902)。仅当用户明确需要原子字段级操作(针对特定路径执行add/remove/replace/test)时才使用。对于常见的“将这些字段合并到个人资料中”场景,请使用client.profiles.create(userId, { profile })(POST,深度合并)。client.profiles.replace(userId, { profile })— 此方法确实存在,是一个完整的 PUT,会覆盖个人资料。仅当需要将个人资料重置为已知的良好状态时才使用。日常写入请使用client.profiles.create(合并),这样更安全,因为它不会悄悄丢弃字段。
通用规则
- 绝不批量发送或延迟发送 OTP、密码重置或安全警报通知
- 对重复发送可能造成危害的发送场景(支付、安全警报、OTP),请使用幂等键
- 安全变更通知中绝不显示完整的电子邮件地址/电话号码(请进行掩码处理)
- 安全相关邮件始终包含“并非由我请求”链接
- 电话号码始终使用 E.164 格式
- 仅向用户要求的渠道或适用于该用例的渠道发送——不要默认向所有渠道群发
- 发送模板时,请将 Courier 生成的
nt_...ID 作为规范 ID;将 ID 视为不透明的工作区专用值,并在发送前将别名解析为nt_...
另请参阅(此处不再重复)
- 静默时段(非 OTP、非安全相关):resources/guides/patterns.md 和 resources/guides/throttling.md
- 429 / 提供商速率限制和重试:resources/guides/throttling.md 和 resources/guides/reliability.md
- 合规性(GDPR、CAN-SPAM、TCPA、10DLC):属于应用层关注事项——有关发件人身份验证和选择加入机制,请参阅渠道指南(resources/channels/email.md、resources/channels/sms.md);有关司法管辖区要求,请咨询法律顾问
- 测试与生产工作区以及安全部署:resources/guides/quickstart.md(每个环境使用单独的 API 密钥)和 resources/guides/reliability.md
Courier Inbox 版本检测
提供 Inbox 指南前,请先确定用户使用的 SDK 版本:
- 检查 v7 标识——查找以下任意项:
@trycourier/react-provider、@trycourier/react-inbox、@trycourier/react-toast、@trycourier/react-hooks、<CourierProvider>、useInbox()、useToast()、<Inbox />(不是<CourierInbox />)、clientKey属性、renderMessage属性。如果可以,请检查package.json。 - 检查 v8 标识——查找以下任意项:
@trycourier/courier-react、@trycourier/courier-react-17、@trycourier/courier-ui-inbox、useCourier()、<CourierInbox />、<CourierToast />、courier.shared.signIn()、registerFeeds、listenForUpdates。 - 如果不明确,请询问——“你使用的是哪个版本的 Courier Inbox SDK?如果你的 package.json 中有
@trycourier/react-inbox,那就是 v7。如果有@trycourier/courier-react,那就是 v8。”
**新项目一律使用 v8——v7 已是旧版。**如果用户使用的是 v7:
- **不要编写新的 v7 代码。**正确做法是升级到 v8。
- 在修改 v7 代码前,请阅读 resources/channels/inbox-v7-legacy.md——其中介绍了识别模式和迁移路径。
- 引导他们按照分步指南进行迁移:
https://www.courier.com/docs/sdk-libraries/courier-react-v8-migration-guide - v8 的打包体积更小、没有第三方依赖、内置深色模式,并且 UI 更现代。
- v7 和 v8 的 API 完全不同——v7 代码无法与 v8 配合使用,反之亦然。
- **唯一的例外:**v8 暂不支持 Tags 或 Pins。如果用户依赖这些功能,可能需要暂时继续使用 v7,但应计划在 v8 添加支持后迁移。
Courier 官方文档
需要查询当前 API 签名、SDK 方法或这些资源未涵盖的功能时:
- 获取
https://www.courier.com/docs/llms.txt——该文件提供 Courier 全部文档页面的结构化 Markdown 索引,包含 URL 和说明 - 在索引中查找相关页面,然后获取该页面的 URL 以查看完整详情
- 最佳实践优先采用此技能中的模式;API 细节则使用 llms.txt
何时使用 llms.txt:
- 需要查询这些资源中未列出的方法的确切签名(例如
client.audiences.create()) - 开发者询问此技能未涵盖的 Courier 功能(例如 Audiences、Brands、Translations)
- 需要确认此技能中的代码示例是否与当前 SDK 版本相符
何时不使用 llms.txt:
- 这些资源文件中已有答案(应优先采用此技能中经过实践验证的模式,而不是原始文档)
- 问题涉及最佳实践或通知设计(llms.txt 无法提供帮助)
架构概览
[User Action / System Event]
│
▼
┌───────────────┐
│ Notification │
│ Trigger │
└───────┬───────┘
│
▼
┌───────────────┐
│ Routing │──── User Preferences
│ Decision │──── Channel Availability
└───────┬───────┘──── Urgency Level
│
▼
┌───────────────────────────────────────┐
│ Channel Selection │
├───────┬───────┬───────┬───────┬──────┤
│ Email │ SMS │ Push │ Inbox │ Chat │
└───┬───┴───┬───┴───┬───┴───┬───┴───┬──┘
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
[Delivery] [Delivery] [Delivery] [Delivery] [Delivery]
│ │ │ │ │
└───────┴───────┴───────┴───────┘
│
▼
┌───────────────┐
│ Webhooks │
│ & Events │
└───────────────┘
快速参考
按渠道
| 需要…… | 适用于…… | 查看 |
|---|---|---|
| 发送邮件、改善送达率、设置 SPF/DKIM/DMARC | 你需要一份持久、详尽的记录。收据、确认信息、长篇内容、附件和丰富格式。送达率取决于发件人信誉(SPF/DKIM/DMARC);不适用于实时场景。 | 邮件 |
| 发送短信、处理 10DLC 注册 | 你需要快速触达用户,发送简短且时效性强的信息。一次性密码、预约提醒、配送更新。在美国需要注册 10DLC;字符数有限;按条计费。 | 短信 |
| 发送推送通知、处理 iOS/Android 差异 | 你需要提醒积极使用应用的用户。活动通知、实时提醒、重新互动。需要设备令牌和操作系统权限;iOS 和 Android 的权限模式不同;关闭权限的用户不会收到通知。 | 推送 |
| 构建应用内通知中心 | 你需要持久的应用内通知,支持已读状态、跨设备同步和收件箱界面。仅在应用内可见。需要 Courier Inbox SDK(v7 和 v8 的差异很重要——请参阅文件标题和上文的 Inbox 版本检测部分)。 | Inbox(v8) — v8 为主要版本。如果你已有 v7 代码(@trycourier/react-inbox、<CourierProvider>、useInbox),请先参阅 Inbox v7 旧版,再进行修改。 |
| 使用 Block Kit 发送 Slack 消息 | 接收者是 Slack 用户或频道。内部提醒、团队通知、ChatOps。需要 OAuth 和机器人设置;Block Kit 有自己的 JSON 结构;每个工作区都有速率限制。 | Slack |
| 发送 Microsoft Teams 消息 | 接收者使用 Microsoft Teams。适用场景与 Slack 相同,但组织不同。需要连接器或机器人;Adaptive Cards 有自己的结构。 | MS Teams |
| 使用模板发送 WhatsApp 消息 | 适用于受监管市场、客户支持和高互动地区(LATAM、EU、IN)。支持富媒体和模板。超出 24 小时客户服务时限后,必须使用获批的消息模板;按类别收取会话费用。 |
按事务类型
| 需要…… | 查看 |
|---|---|
| 构建密码重置、一次性密码、验证和安全提醒 | 身份验证 |
| 构建订单确认、配送和送达更新 | 订单 |
| 构建收据、发票、催款和订阅通知 | 账单 |
| 构建预约确认、提醒和改期通知 | 预约 |
| 构建欢迎信息、个人资料更新和设置变更通知 | 账户 |
| 了解事务性通知原则 | 事务性通知概览 |
按增长类型
| 需要…… | 查看 |
|---|---|
| 构建激活流程、设置指引和首次价值体验 | 引导 |
| 构建功能公告、功能发现和使用教育 | 功能采用 |
| 构建活动通知、留存和习惯养成循环 | 用户互动 |
| 构建用户召回、非活跃用户唤回和购物车弃购挽回流程 | 重新互动 |
| 构建推荐邀请、奖励和病毒式传播循环 | 推荐 |
| 构建促销、销售和升级活动 | 营销活动 |
| 了解增长通知原则 | 增长概览 |
跨主题指南
| 需求 | 参阅 |
|---|---|
| 开始发送第一条通知 | 快速入门 |
| 跨多个渠道路由,设置备用方案 | 多渠道 |
| 管理用户通知偏好 | 偏好设置 |
| 处理重试、幂等性和错误恢复 | 可靠性 |
| 合并通知,构建摘要 | 批处理 |
| 控制频率,防止疲劳 | 限流 |
| 为你的应用类型规划通知 | 目录 |
| 使用 CLI 执行临时操作、调试和 Agent 工作流 | CLI |
| 使用 MCP 服务,让 AI Agent 以结构化方式访问 API | MCP 服务 |
| 通过 API 管理模板(创建、发布、版本控制) | 模板 |
通过 API 创建路由策略(rs_...、提供商优先级) | 路由策略 |
| 通过 API 配置提供商(SendGrid、Twilio 等,目录发现) | 提供商 |
| 了解 Elemental 内容格式(元素类型、控制流、本地化) | Elemental |
| 构建多步骤通知旅程(延迟、分支、序列) | 旅程 |
| 可复用的代码模式(同意、静默时段、屏蔽、重试) | 模式 |
| 从任何通知系统迁移到 Courier | 通用迁移 |
| 从 Knock 迁移到 Courier | 从 Knock 迁移 |
| 从 Novu 迁移到 Courier | 从 Novu 迁移 |
未深入介绍的主题(从官方文档获取)
此技能目前尚未为以下领域提供专门指南。用户询问时,请通过 WebFetch 获取下方页面;不要凭记忆臆造 API 结构。如有疑问,请先获取 https://www.courier.com/docs/llms.txt,并使用其中返回的网址。
| 主题 | 获取 |
|---|---|
| 受众(基于属性的定向) | https://www.courier.com/docs/platform/users/audiences |
| 自动化(旧版仪表板工作流——对于新的多步骤流程,请使用旅程) | https://www.courier.com/docs/automations/overview |
| 品牌(徽标、颜色、可复用的视觉形象) | https://www.courier.com/docs/platform/content/brands |
| 租户(多租户 B2B、每个租户的品牌/偏好设置) | https://www.courier.com/docs/platform/tenants/tenants-overview(代码另请参阅[模式](./resources/guides/patterns.md)中的“租户”部分) |
| 事件 / 事件映射 | https://www.courier.com/docs/platform/automations/inbound-events(以及[发送 API](https://www.courier.com/docs/reference/send/message)中的 event 字段) |
翻译 / i18n(超出每个模板的 locales 块的范围) | https://www.courier.com/docs/platform/content/elemental/locales(元素级)或 https://www.courier.com/docs/api-reference/translations/get-a-translation(API) |
按任务划分的最小文件集
对于常见任务,你只需阅读以下特定文件:
决策指南
你要构建什么?
-
特定通知(OTP、订单确认、密码重置等) → 使用上方的按任务划分的最小文件集表,准确找出需要阅读的 1-2 个文件。
-
新的通知渠道(电子邮件、SMS、推送、Slack 等) → 查看按渠道划分,了解对应渠道的指南。
-
多步骤通知序列(新用户引导邮件序列、升级处理、提醒序列、购物车弃单挽回、召回) → 从用户旅程开始。用户旅程让你可以通过 API 将完整的 DAG(延迟、分支、发送)定义为代码。
-
通知基础设施(路由、偏好设置、可靠性、批处理) → 查看跨领域指南,找到相关指南。
-
增长 / 生命周期通知(新用户引导、互动、推荐) → 先阅读增长概览了解同意要求,再查看具体类型。
-
刚接触 Courier或准备发送首条通知 → 从快速入门开始。
-
排查投递问题 → 务必先从 CLI(
courier messages list、courier messages content)开始,查看实际投递状态,而不是猜测。然后:电子邮件进入垃圾邮件?查看电子邮件。SMS 未送达?查看SMS。常见故障?查看可靠性。 -
临时操作、CI/CD 或 AI Agent 工作流 → 如果你的编辑器支持 MCP(Cursor、Claude Code、Claude Desktop、Windsurf、VSCode),请使用它 — 参见 MCP 服务。对于仅使用 shell 的环境、CI/CD 或无法使用 MCP 的情况,请使用 CLI — 参见 CLI。两者使用相同的 API 密钥,支持相同的 API 功能。
-
以编程方式管理模板或了解 Elemental(Courier 的 JSON 模板语言) → 查看模板,了解完整的 CRUD 生命周期(创建、发布、版本管理、本地化)。查看 Elemental,了解逐个元素的参考(
text、action、image、meta、channel、group)、控制流(if、loop、ref)和区域设置处理。 -
可复用的代码模式(同意检查、免打扰时段、幂等性、回退) → 查看模式,获取 TypeScript、Python、CLI 和 curl 的可复制粘贴实现。
-
从其他通知系统迁移到 Courier → 从 Knock 迁移: 从 Knock 迁移。从 Novu 迁移: 从 Novu 迁移。从任何其他系统迁移(自建系统、直接使用 SendGrid、直接使用 Twilio 等):通用迁移。