Skip to content

使用 Backend Function 发送通知

本手册用于创建一个 ENDPOINT BFF:调用方提交订单业务数据,Runtime 自动注入当前应用和当前用户,BFF 再通过应用级渠道配置把最终消息发送给指定用户、角色或群机器人。

1. 准备信息

准备以下内容:

信息示例
应用编码<app-code>
BFF 名称sendOrderNotification
应用级渠道配置名称订单审批邮件
应用级渠道编码<config-code>
接收人用户 ID、用户名、邮箱地址或角色编码

同时确认:

  1. 目标 Runtime 已支持应用级 notification.send
  2. 本地已安装并登录 rabetbase
  3. 日常环境验证时已安装并登录 lovrabet
  4. 当前账号有目标应用的 BFF 和渠道配置权限。
  5. 应用中已经存在可用的通知渠道配置,或者可以新建一个配置。

本场景不需要创建 dataset 级通知通道,也不需要配置 datasetCodesceneCodetriggerType=MANUAL

2. 创建渠道并取得 configCode

2.1 configCode 是什么

configCode 是应用级渠道配置的稳定编码,格式类似:

text
ncc_7b5f6c2a4d8e4f1a9b0c123456789abc

它只负责标识“使用哪个发送渠道”。

  • Webhook、SMTP 地址、账号和密钥保存在渠道配置中。
  • BFF 只保存 configCode,不保存渠道密钥。
  • configCode 按应用隔离。Runtime 会用当前 BFF 的可信 appCode 查询配置。
  • configCode 可以写在 BFF 中,但不应由 Endpoint 调用方任意传入。

2.2 在页面创建渠道配置

进入应用的通知管理页面,创建应用级渠道配置。

图片展示的是启智云图 - 企业智能系统中应用管理下的通知配置页面。左侧导航栏中“通知配置”下有“渠道管理”选项被蓝色边框突出显示…

可以选择:

  • 飞书、钉钉或企业微信群机器人 Webhook。
  • 飞书应用机器人。
  • EMAIL 邮箱 SMTP。
  • 通用 Webhook。

EMAIL 渠道需要确认:

  • channelTypeEMAIL
  • SMTP endpoint 使用 smtp://smtps://
  • 发件人地址已配置。
  • 开启 SMTP 认证时,用户名和密码均已配置。
  • SSL 或 STARTTLS 与邮件服务商要求一致。

环境已经配置官方邮件账号时,可以选择官方 EMAIL 配置。页面不会把 SMTP 密码交给 BFF。

2.3 通过接口查询 configCode

如果页面只显示配置名称,可以按应用和渠道类型查询:

http
GET <编辑态服务地址>/notification/channel-config/list
    ?appCode=<app-code>
    &channelType=EMAIL

响应中的每条记录会包含:

json
{
  "id": 88,
  "appCode": "<app-code>",
  "configName": "订单审批邮件",
  "configCode": "ncc_7b5f6c2a4d8e4f1a9b0c123456789abc",
  "channelType": "EMAIL",
  "connectTimeout": 5000,
  "readTimeout": 10000
}

选择配置时同时核对 configNamechannelTypeappCode,不要只按列表顺序选择。

创建接口只返回记录 ID:

http
POST <编辑态服务地址>/notification/channel-config
Content-Type: application/json
json
{
  "appCode": "<app-code>",
  "configName": "订单审批邮件",
  "channelType": "EMAIL",
  "endpointUrl": "",
  "channelConfig": "{\"useOfficialConfig\":true}",
  "description": "订单审批结果通知",
  "connectTimeout": 5000,
  "readTimeout": 10000
}

创建成功后:

  1. 从响应 data 取得配置 ID。
  2. 调用列表接口,或调用 GET /notification/channel-config/{id}?appCode=<app-code>
  3. 从详情结果中取得服务端生成的 configCode
  4. 检查 EMAIL 配置是否已保存完整的脱敏 SMTP 快照。

不要把登录 Cookie、AccessKey、SMTP 密码或完整 channelConfig 写入文档、BFF 或 Git。

3. 创建 BFF

首次使用当前目录时执行:

bash
rabetbase auth login
rabetbase init --appcode <app-code>

先检查是否已有同名脚本:

bash
rabetbase bff list \
  --appcode <app-code> \
  --format json

创建 ENDPOINT 脚本:

bash
rabetbase bff create \
  --appcode <app-code> \
  --type ENDPOINT \
  --name sendOrderNotification \
  --description "发送订单审批通知" \
  --format json

脚本位置:

text
.rabetbase/bff/<appCode>/ENDPOINT/sendOrderNotification.js

4. 理解 params 和 context

BFF 的固定函数签名为:

javascript
export default async function sendOrderNotification(params, context) {
  // 业务逻辑
}

4.1 params 从哪里来

params 是调用方提交的业务 JSON。

调用方式params 来源
HTTP EndpointHTTP 请求体中的 JSON 对象
lovrabet bff exec--params 后面的 JSON
前端 SDKclient.bff.execute({ scriptName, params }) 中的 params

例如请求体:

json
{
  "orderNo": "SO-001",
  "receiver": "zhangsan",
  "siteUrl": "https://<当前站点域名>"
}

脚本中直接读取:

javascript
params.orderNo
params.receiver
params.siteUrl

请求体不要再包装一层 params,也不要传 context

json
{
  "params": {
    "orderNo": "SO-001"
  },
  "context": {}
}

上面的错误结构会让脚本只能通过 params.params.orderNo 取值。请求中的 context 也不会替代 Runtime 注入的可信上下文。

4.2 context 从哪里来

context 由 Runtime 在执行 BFF 前自动构造,调用方不传。

字段来源和用途
context.appCode从目标 Endpoint 所属应用取得,标识当前应用
context.userInfo从已认证的当前调用用户取得
context.appRoles当前用户在当前应用中的启用角色
context.tenantCode当前租户编码;没有租户时可能为空
context.appConfig当前应用运行态配置访问入口
context.client数据、SQL、事务、扩展和公共 BFF 调用入口

本场景只需要:

javascript
context.client.extension.execute(...)

通知扩展会从可信上下文取得当前 appCode 和当前用户。不要把 appCodecurrentUseroperator 或伪造的用户对象传给 notification.send

不要打印或返回完整 contextcontext.userInfo,其中可能包含会话相关字段。

5. 编写通知脚本

5.1 接收人由调用方提供

CONFIG_CODE 改成第 2 步取得的实际 configCode

javascript
const CONFIG_CODE = "<config-code>";

export default async function sendOrderNotification(params, context) {
  const orderNo = String(params?.orderNo || "").trim();
  const receiver = String(params?.receiver || "").trim();
  const siteUrl = String(params?.siteUrl || "").replace(/\/+$/, "");

  if (!orderNo) {
    throw new Error("orderNo 不能为空");
  }
  if (!receiver) {
    throw new Error("receiver 不能为空");
  }
  if (!siteUrl) {
    throw new Error("siteUrl 不能为空");
  }

  return await context.client.extension.execute(
    "notification",
    "send",
    {
      configCode: CONFIG_CODE,
      audiences: [
        {
          type: "USER",
          ids: [receiver]
        }
      ],
      message: {
        title: "订单审批通过",
        summary: `订单 ${orderNo} 已完成审批`,
        theme: "blue",
        detailMarkdown: "请及时处理后续业务。",
        facts: [
          { label: "订单号", value: orderNo },
          { label: "审批状态", value: "已通过" }
        ],
        actions: [
          {
            text: "查看详情",
            url: `${siteUrl}/orders/${encodeURIComponent(orderNo)}`
          }
        ]
      }
    }
  );
}

5.2 EMAIL 固定发送给指定用户

业务要求固定通知某个用户时,把用户标识固定在 BFF 中,不让 Endpoint 调用方改接收人:

javascript
const CONFIG_CODE = "<email-config-code>";
const TARGET_USER = "<user-name>";

export default async function sendEmailNotification(params, context) {
  const testNo = String(params?.testNo || "").trim();
  if (!testNo) {
    throw new Error("testNo 不能为空");
  }

  return await context.client.extension.execute(
    "notification",
    "send",
    {
      configCode: CONFIG_CODE,
      audiences: [
        {
          type: "USER",
          ids: [TARGET_USER]
        }
      ],
      message: {
        title: "BFF 邮件通知测试",
        summary: `测试任务 ${testNo} 已完成`,
        theme: "blue",
        detailMarkdown: "该邮件由 BFF 通过应用级 EMAIL 渠道发送。",
        facts: [
          { label: "测试编号", value: testNo },
          { label: "发送环境", value: "daily" }
        ]
      }
    }
  );
}

EMAIL 的 USER.ids 支持:

  • 数字用户 ID,例如 "1001"
  • 用户名或昵称,例如 "zhangsan"
  • 完整邮箱地址,例如 "user@example.com"

优先使用稳定的内部用户 ID 或用户名。直接使用邮箱时,邮箱会成为 BFF 源码或业务参数的一部分,应按组织的数据安全要求处理。

5.3 参数边界

notification.send 顶层只接受:

  • configCode
  • audiences
  • message

不要传入:

  • datasetCode
  • sceneCode
  • createdId
  • record
  • 顶层 titlesummary
  • appCode
  • currentUseroperator 或其他用户对象
  • ccbccreplyToemailOptions

第三个参数是通知扩展的发送参数,不等于 Endpoint 函数收到的 params

javascript
context.client.extension.execute(
  "notification", // 组件
  "send",         // 动作
  { /* 通知发送参数 */ }
);

6. 选择接收人

6.1 发送给用户

javascript
audiences: [
  {
    type: "USER",
    ids: ["1001", "user@example.com"]
  }
]

6.2 发送给角色

javascript
audiences: [
  {
    type: "ROLE",
    codes: ["ADMIN"]
  }
]

常用角色编码包括 ADMINDEVUSER。角色只在当前应用内解析。

第一版只支持 USERROLE,不支持 DEPT

EMAIL 和飞书应用机器人必须至少解析出一个接收人。Webhook 群机器人已经有固定群目标,可以省略 audiences

javascript
return await context.client.extension.execute(
  "notification",
  "send",
  {
    configCode: CONFIG_CODE,
    message: {
      title: "订单审批通过",
      summary: `订单 ${params.orderNo} 已完成审批`
    }
  }
);

7. 推送 BFF

检查 JavaScript 语法:

bash
node --check .rabetbase/bff/<appCode>/ENDPOINT/sendOrderNotification.js

检查本地状态:

bash
rabetbase bff status --format json

新增脚本应出现在 added 中,已有脚本修改后应出现在 modified 中。

预览推送:

bash
rabetbase bff push \
  --appcode <app-code> \
  --type ENDPOINT \
  --name sendOrderNotification \
  --dry-run \
  --format json

确认 modelockKey、应用和脚本名称正确后正式推送:

bash
rabetbase bff push \
  --yes \
  --appcode <app-code> \
  --type ENDPOINT \
  --name sendOrderNotification \
  --format json

推送成功后确认:

  • uploaded 中包含目标脚本。
  • failed 为空。
  • Runtime 脚本缓存已经清理。
  • 再次执行 rabetbase bff status 时脚本位于 unchanged

8. 调用 ENDPOINT BFF

8.1 使用 lovrabet CLI

bash
lovrabet bff exec \
  --env daily \
  --appcode <app-code> \
  --name sendOrderNotification \
  --params '{
    "orderNo": "SO-EXAMPLE-001",
    "receiver": "zhangsan",
    "siteUrl": "https://<当前站点域名>"
  }' \
  --format json

--params 后面的 JSON 会直接成为函数的 params

8.2 使用 HTTP

完整请求结构:

http
POST <运行态服务地址>/api/endpoint/<app-code>/sendOrderNotification HTTP/1.1
Content-Type: application/json
<平台支持的认证信息>

{
  "orderNo": "SO-EXAMPLE-001",
  "receiver": "zhangsan",
  "siteUrl": "https://<当前站点域名>"
}

URL 中的是“被调用的目标 BFF”,不是调用方 BFF。调用方身份由认证信息决定,不放在请求体中。

浏览器同站点调用示例:

javascript
const response = await fetch(
  "/api/endpoint/<app-code>/sendOrderNotification",
  {
    method: "POST",
    credentials: "include",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      orderNo: "SO-EXAMPLE-001",
      receiver: "zhangsan",
      siteUrl: window.location.origin
    })
  }
);

const result = await response.json();
if (!response.ok || result.success === false) {
  throw new Error(result.errorMsg || "通知发送失败");
}

const notificationResult = result.data;

HTTP Endpoint 受应用权限和登录态保护。不要把 Cookie 或 AccessKey 硬编码在页面源码或 BFF 中。

8.3 使用前端 SDK

javascript
const result = await client.bff.execute({
  scriptName: "sendOrderNotification",
  params: {
    orderNo: "SO-EXAMPLE-001",
    receiver: "zhangsan",
    siteUrl: window.location.origin
  }
});

前端 SDK 返回的是 BFF 的业务结果,不需要读取 HTTP 外层的 data

8.4 不要用 context.client.bff.execute 调 ENDPOINT

BFF 内的 context.client.bff.execute 用于调用 COMMON 类型公共函数,不用于调用另一个 ENDPOINT BFF。

多个 BFF 需要复用通知逻辑时:

  1. 把公共拼装或发送逻辑创建为 COMMON BFF。
  2. ENDPOINT BFF 和其他 BFF 通过 context.client.bff.execute 调用该 COMMON 函数。
  3. 外部系统、页面或 CLI 仍然调用 ENDPOINT BFF。

9. 检查发送结果

9.1 CLI 或 SDK 返回

json
{
  "sent": true,
  "configCode": "<config-code>",
  "channelType": "EMAIL",
  "message": "通知发送成功"
}

9.2 原始 HTTP 返回

Controller 会增加统一响应外层:

json
{
  "success": true,
  "msg": "",
  "errorMsg": "",
  "errorCode": "0000",
  "data": {
    "sent": true,
    "configCode": "<config-code>",
    "channelType": "EMAIL",
    "message": "通知发送成功"
  }
}

CLI 或 SDK 可能自动解开外层,只显示 data

9.3 确认通知已发送

  1. HTTP 状态为 200,或 CLI 正常结束。
  2. 返回 sent=true
  3. 返回的 configCode 与 BFF 固定值一致。
  4. 返回的 channelType 与渠道配置一致。
  5. 目标接收人实际收到消息。
  6. EMAIL 测试同时检查收件箱和垃圾邮件目录。
  7. 发送日志中的 channelCode 等于 configCode
  8. 发送日志中的 triggerTypeBFF
  9. 发送日志中的 triggerSourceBACKEND_FUNCTION
  10. 发送日志中的 datasetCode 为空。
  11. 发送日志不包含 endpointUrlchannelConfig、密码或渠道密钥。

sent=true 表示 Runtime 已完成发送,并且 EMAIL 场景下 SMTP 服务已经接受请求;是否最终进入收件箱仍以收件人的邮箱结果为准。

10. 常见问题

10.1 当前环境未提供此扩展点实现: notification

目标 Runtime 尚未部署支持应用级直接发送的版本。先部署 Runtime,再重新执行 BFF。

10.2 不知道 configCode 从哪里取得

appCode + channelType 查询应用级渠道配置列表。新建配置时,先取得创建接口返回的 ID,再查详情或列表获取服务端生成的 configCode

不要使用 dataset 级 channelCode 代替应用级 configCode

10.3 应用级通知渠道配置不存在或不可用

检查:

  1. BFF 中的 configCode 是否完整。
  2. 配置是否属于 BFF 当前应用。
  3. 配置是否已逻辑删除。
  4. 目标 Runtime 是否已经读取到最新渠道配置。

10.4 未解析到有效的邮件或飞书接收人

检查:

  1. audiences 是否为非空数组。
  2. USER.ids 中的用户 ID、用户名、昵称或邮箱是否有效。
  3. 用户是否配置了有效邮箱。
  4. ROLE.codes 是否受支持。
  5. 角色下是否存在启用用户。

EMAIL 和飞书应用机器人不能使用空接收人。

10.5 notification.send 不支持参数

顶层只保留 configCodeaudiencesmessage

删除 datasetCodesceneCodecreatedIdrecord、顶层 title/summaryappCode 和用户对象。

10.6 message 不支持模板表达式

传入的消息必须是最终文本。不要把 ${event.xxx}${record.xxx} 交给通知扩展。

BFF 可以先使用 JavaScript 模板字符串:

javascript
const summary = `订单 ${params.orderNo} 已完成审批`;

这里的表达式会在 BFF 中执行,传给 notification.send 时已经是最终字符串。

10.7 HTTP 请求中 params 是 undefined

确认:

  • 请求方式为 POST
  • Content-Typeapplication/json
  • 请求体直接是 JSON 对象,没有再包装 params
  • 请求路径为 /api/endpoint/<app-code>/<script-name>

10.8 HTTP 返回有 data,CLI 返回没有 data

这是调用层差异。原始 HTTP 使用统一响应外层,BFF 业务结果位于 data;CLI 和 SDK 通常已经解开外层。

10.9 EMAIL 返回成功但没有收到邮件

依次检查:

  1. 收件箱和垃圾邮件目录。
  2. USER.ids 是否解析到预期用户。
  3. 用户资料中的邮箱是否正确。
  4. 邮件服务商是否延迟、退信或拦截。
  5. SMTP 发件域名、SPF、DKIM 和反垃圾策略。

11. message 属性与三种通知形式的实际表现

notification.sendmessage 使用统一输入结构,但飞书、钉钉和 EMAIL 会按各自的消息协议进行渲染。当前只支持 titlesummarythemedetailMarkdownfactsactions 六个属性;不在列表中的字段会被拒绝。

11.1 统一输入结构

javascript
message: {
  title: "订单审批通过",
  summary: `订单 ${orderNo} 已完成审批`,
  theme: "blue",
  detailMarkdown: "请及时处理后续业务。",
  facts: [
    { label: "订单号", value: orderNo },
    { label: "审批状态", value: "已通过" }
  ],
  actions: [
    {
      text: "查看详情",
      url: `${siteUrl}/orders/${encodeURIComponent(orderNo)}`
    }
  ]
}

其中 titlesummary 必填;themedetailMarkdownfactsactions 可选。动态值应先在 BFF 中计算完成,再把最终字符串传给通知扩展。

11.2 字段支持与消费结果

属性输入约束飞书钉钉EMAIL
title必填,非空字符串卡片头部标题markdown.title,并在正文中显示为三级标题邮件主题,同时显示为正文标题
summary必填,非空字符串卡片首个 Markdown 内容块Markdown 正文摘要HTML 正文概要段落,同时进入纯文本正文
theme可选,仅支持 bluegreenorangeredgrey作为卡片头部主题色当前不消费,不改变消息样式当前不消费,不改变邮件样式
detailMarkdown可选,字符串与 facts 合并为 Markdown 内容块追加到 Markdown 正文HTML 中转义后放入预格式化文本块,不渲染为富 Markdown;纯文本正文保留原内容
facts可选,最多 8 项;每项严格为 { label, value },两个字段均为非空字符串按“加粗标签:值”逐行显示按 Markdown 项目符号逐行显示HTML 中显示为标签和值的表格;纯文本中显示为“标签: 值”
actions可选,最多 2 项;每项严格为 { text, url },两个字段均为非空字符串渲染为卡片按钮渲染为 Markdown 链接列表HTML 中渲染为超链接;纯文本中显示为“按钮文字: URL”

11.3 飞书的实际消费方式

飞书群机器人和飞书应用机器人对这六个字段的消费结果一致:title 进入卡片头部,theme 控制头部主题色,summary 作为第一段内容,factsdetailMarkdown 组成后续 Markdown 内容,actions 转换为按钮。飞书是当前三个渠道中唯一实际消费 theme 的渠道。

11.4 钉钉的实际消费方式

钉钉统一发送 msgtype=markdown 的消息。title 同时用于 markdown.title 和正文标题,summaryfactsdetailMarkdownactions 按顺序拼接成 Markdown 正文。actions 不会显示为独立按钮,而是显示为链接列表。当前实现不会读取 theme

11.5 EMAIL 的实际消费方式

EMAIL 会同时生成 HTML 正文和纯文本正文。title 用作邮件主题并显示为正文标题,summary 用作概要段落,facts 在 HTML 中显示为表格,actions 显示为超链接。detailMarkdown 会进行 HTML 转义并放入预格式化文本块,因此其中的 Markdown 标题、表格等语法不会转换成对应的 HTML 样式。当前实现不会读取 theme

ccccListbccreplyTo 和附件都不是 message 属性。当前 BFF 的 notification.send 也没有暴露动态抄送参数;不要把这些字段放进 message 或发送参数顶层。

11.6 跨渠道编写建议

  1. 跨渠道共用一份消息时,把 titlesummary 作为完整信息主体,不要依赖 theme 表达业务状态。
  2. detailMarkdown 优先使用普通文本、换行和简单强调,不要依赖复杂 Markdown 表格、图片或渠道专属语法。
  3. 结构化业务信息放入 facts,跳转入口放入 actions;分别遵守最多 8 项和最多 2 项的限制。
  4. actions.url 应使用经过校验的可信 HTTPS 地址,不要让调用方直接传入任意跳转地址。
  5. 同一个 message 在不同渠道中的视觉结果不同,检查时应分别查看飞书卡片、钉钉 Markdown 消息和实际邮件。

基于飞书知识库同步生成,内容以飞书源文档为准