使用 Backend Function 发送通知
本手册用于创建一个 ENDPOINT BFF:调用方提交订单业务数据,Runtime 自动注入当前应用和当前用户,BFF 再通过应用级渠道配置把最终消息发送给指定用户、角色或群机器人。
1. 准备信息
准备以下内容:
| 信息 | 示例 |
|---|---|
| 应用编码 | <app-code> |
| BFF 名称 | sendOrderNotification |
| 应用级渠道配置名称 | 订单审批邮件 |
| 应用级渠道编码 | <config-code> |
| 接收人 | 用户 ID、用户名、邮箱地址或角色编码 |
同时确认:
- 目标 Runtime 已支持应用级
notification.send。 - 本地已安装并登录
rabetbase。 - 日常环境验证时已安装并登录
lovrabet。 - 当前账号有目标应用的 BFF 和渠道配置权限。
- 应用中已经存在可用的通知渠道配置,或者可以新建一个配置。
本场景不需要创建 dataset 级通知通道,也不需要配置 datasetCode、sceneCode 或 triggerType=MANUAL。
2. 创建渠道并取得 configCode
2.1 configCode 是什么
configCode 是应用级渠道配置的稳定编码,格式类似:
ncc_7b5f6c2a4d8e4f1a9b0c123456789abc它只负责标识“使用哪个发送渠道”。
- Webhook、SMTP 地址、账号和密钥保存在渠道配置中。
- BFF 只保存
configCode,不保存渠道密钥。 configCode按应用隔离。Runtime 会用当前 BFF 的可信appCode查询配置。configCode可以写在 BFF 中,但不应由 Endpoint 调用方任意传入。
2.2 在页面创建渠道配置
进入应用的通知管理页面,创建应用级渠道配置。

可以选择:
- 飞书、钉钉或企业微信群机器人 Webhook。
- 飞书应用机器人。
- EMAIL 邮箱 SMTP。
- 通用 Webhook。
EMAIL 渠道需要确认:
channelType为EMAIL。- SMTP endpoint 使用
smtp://或smtps://。 - 发件人地址已配置。
- 开启 SMTP 认证时,用户名和密码均已配置。
- SSL 或 STARTTLS 与邮件服务商要求一致。
环境已经配置官方邮件账号时,可以选择官方 EMAIL 配置。页面不会把 SMTP 密码交给 BFF。
2.3 通过接口查询 configCode
如果页面只显示配置名称,可以按应用和渠道类型查询:
GET <编辑态服务地址>/notification/channel-config/list
?appCode=<app-code>
&channelType=EMAIL响应中的每条记录会包含:
{
"id": 88,
"appCode": "<app-code>",
"configName": "订单审批邮件",
"configCode": "ncc_7b5f6c2a4d8e4f1a9b0c123456789abc",
"channelType": "EMAIL",
"connectTimeout": 5000,
"readTimeout": 10000
}选择配置时同时核对 configName、channelType 和 appCode,不要只按列表顺序选择。
创建接口只返回记录 ID:
POST <编辑态服务地址>/notification/channel-config
Content-Type: application/json{
"appCode": "<app-code>",
"configName": "订单审批邮件",
"channelType": "EMAIL",
"endpointUrl": "",
"channelConfig": "{\"useOfficialConfig\":true}",
"description": "订单审批结果通知",
"connectTimeout": 5000,
"readTimeout": 10000
}创建成功后:
- 从响应
data取得配置 ID。 - 调用列表接口,或调用
GET /notification/channel-config/{id}?appCode=<app-code>。 - 从详情结果中取得服务端生成的
configCode。 - 检查 EMAIL 配置是否已保存完整的脱敏 SMTP 快照。
不要把登录 Cookie、AccessKey、SMTP 密码或完整 channelConfig 写入文档、BFF 或 Git。
3. 创建 BFF
首次使用当前目录时执行:
rabetbase auth login
rabetbase init --appcode <app-code>先检查是否已有同名脚本:
rabetbase bff list \
--appcode <app-code> \
--format json创建 ENDPOINT 脚本:
rabetbase bff create \
--appcode <app-code> \
--type ENDPOINT \
--name sendOrderNotification \
--description "发送订单审批通知" \
--format json脚本位置:
.rabetbase/bff/<appCode>/ENDPOINT/sendOrderNotification.js4. 理解 params 和 context
BFF 的固定函数签名为:
export default async function sendOrderNotification(params, context) {
// 业务逻辑
}4.1 params 从哪里来
params 是调用方提交的业务 JSON。
| 调用方式 | params 来源 |
|---|---|
| HTTP Endpoint | HTTP 请求体中的 JSON 对象 |
lovrabet bff exec | --params 后面的 JSON |
| 前端 SDK | client.bff.execute({ scriptName, params }) 中的 params |
例如请求体:
{
"orderNo": "SO-001",
"receiver": "zhangsan",
"siteUrl": "https://<当前站点域名>"
}脚本中直接读取:
params.orderNo
params.receiver
params.siteUrl请求体不要再包装一层 params,也不要传 context:
{
"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 调用入口 |
本场景只需要:
context.client.extension.execute(...)通知扩展会从可信上下文取得当前 appCode 和当前用户。不要把 appCode、currentUser、operator 或伪造的用户对象传给 notification.send。
不要打印或返回完整 context、context.userInfo,其中可能包含会话相关字段。
5. 编写通知脚本
5.1 接收人由调用方提供
把 CONFIG_CODE 改成第 2 步取得的实际 configCode:
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 调用方改接收人:
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 顶层只接受:
configCodeaudiencesmessage
不要传入:
datasetCodesceneCodecreatedIdrecord- 顶层
title或summary appCodecurrentUser、operator或其他用户对象cc、bcc、replyTo或emailOptions
第三个参数是通知扩展的发送参数,不等于 Endpoint 函数收到的 params:
context.client.extension.execute(
"notification", // 组件
"send", // 动作
{ /* 通知发送参数 */ }
);6. 选择接收人
6.1 发送给用户
audiences: [
{
type: "USER",
ids: ["1001", "user@example.com"]
}
]6.2 发送给角色
audiences: [
{
type: "ROLE",
codes: ["ADMIN"]
}
]常用角色编码包括 ADMIN、DEV 和 USER。角色只在当前应用内解析。
第一版只支持 USER 和 ROLE,不支持 DEPT。
EMAIL 和飞书应用机器人必须至少解析出一个接收人。Webhook 群机器人已经有固定群目标,可以省略 audiences:
return await context.client.extension.execute(
"notification",
"send",
{
configCode: CONFIG_CODE,
message: {
title: "订单审批通过",
summary: `订单 ${params.orderNo} 已完成审批`
}
}
);7. 推送 BFF
检查 JavaScript 语法:
node --check .rabetbase/bff/<appCode>/ENDPOINT/sendOrderNotification.js检查本地状态:
rabetbase bff status --format json新增脚本应出现在 added 中,已有脚本修改后应出现在 modified 中。
预览推送:
rabetbase bff push \
--appcode <app-code> \
--type ENDPOINT \
--name sendOrderNotification \
--dry-run \
--format json确认 mode、lockKey、应用和脚本名称正确后正式推送:
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
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
完整请求结构:
POST <运行态服务地址>/api/endpoint/<app-code>/sendOrderNotification HTTP/1.1
Content-Type: application/json
<平台支持的认证信息>
{
"orderNo": "SO-EXAMPLE-001",
"receiver": "zhangsan",
"siteUrl": "https://<当前站点域名>"
}URL 中的是“被调用的目标 BFF”,不是调用方 BFF。调用方身份由认证信息决定,不放在请求体中。
浏览器同站点调用示例:
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
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 需要复用通知逻辑时:
- 把公共拼装或发送逻辑创建为
COMMONBFF。 - ENDPOINT BFF 和其他 BFF 通过
context.client.bff.execute调用该 COMMON 函数。 - 外部系统、页面或 CLI 仍然调用 ENDPOINT BFF。
9. 检查发送结果
9.1 CLI 或 SDK 返回
{
"sent": true,
"configCode": "<config-code>",
"channelType": "EMAIL",
"message": "通知发送成功"
}9.2 原始 HTTP 返回
Controller 会增加统一响应外层:
{
"success": true,
"msg": "",
"errorMsg": "",
"errorCode": "0000",
"data": {
"sent": true,
"configCode": "<config-code>",
"channelType": "EMAIL",
"message": "通知发送成功"
}
}CLI 或 SDK 可能自动解开外层,只显示 data。
9.3 确认通知已发送
- HTTP 状态为 200,或 CLI 正常结束。
- 返回
sent=true。 - 返回的
configCode与 BFF 固定值一致。 - 返回的
channelType与渠道配置一致。 - 目标接收人实际收到消息。
- EMAIL 测试同时检查收件箱和垃圾邮件目录。
- 发送日志中的
channelCode等于configCode。 - 发送日志中的
triggerType为BFF。 - 发送日志中的
triggerSource为BACKEND_FUNCTION。 - 发送日志中的
datasetCode为空。 - 发送日志不包含
endpointUrl、channelConfig、密码或渠道密钥。
sent=true 表示 Runtime 已完成发送,并且 EMAIL 场景下 SMTP 服务已经接受请求;是否最终进入收件箱仍以收件人的邮箱结果为准。
10. 常见问题
10.1 当前环境未提供此扩展点实现: notification
目标 Runtime 尚未部署支持应用级直接发送的版本。先部署 Runtime,再重新执行 BFF。
10.2 不知道 configCode 从哪里取得
按 appCode + channelType 查询应用级渠道配置列表。新建配置时,先取得创建接口返回的 ID,再查详情或列表获取服务端生成的 configCode。
不要使用 dataset 级 channelCode 代替应用级 configCode。
10.3 应用级通知渠道配置不存在或不可用
检查:
- BFF 中的
configCode是否完整。 - 配置是否属于 BFF 当前应用。
- 配置是否已逻辑删除。
- 目标 Runtime 是否已经读取到最新渠道配置。
10.4 未解析到有效的邮件或飞书接收人
检查:
audiences是否为非空数组。USER.ids中的用户 ID、用户名、昵称或邮箱是否有效。- 用户是否配置了有效邮箱。
ROLE.codes是否受支持。- 角色下是否存在启用用户。
EMAIL 和飞书应用机器人不能使用空接收人。
10.5 notification.send 不支持参数
顶层只保留 configCode、audiences 和 message。
删除 datasetCode、sceneCode、createdId、record、顶层 title/summary、appCode 和用户对象。
10.6 message 不支持模板表达式
传入的消息必须是最终文本。不要把 ${event.xxx} 或 ${record.xxx} 交给通知扩展。
BFF 可以先使用 JavaScript 模板字符串:
const summary = `订单 ${params.orderNo} 已完成审批`;这里的表达式会在 BFF 中执行,传给 notification.send 时已经是最终字符串。
10.7 HTTP 请求中 params 是 undefined
确认:
- 请求方式为
POST。 Content-Type为application/json。- 请求体直接是 JSON 对象,没有再包装
params。 - 请求路径为
/api/endpoint/<app-code>/<script-name>。
10.8 HTTP 返回有 data,CLI 返回没有 data
这是调用层差异。原始 HTTP 使用统一响应外层,BFF 业务结果位于 data;CLI 和 SDK 通常已经解开外层。
10.9 EMAIL 返回成功但没有收到邮件
依次检查:
- 收件箱和垃圾邮件目录。
USER.ids是否解析到预期用户。- 用户资料中的邮箱是否正确。
- 邮件服务商是否延迟、退信或拦截。
- SMTP 发件域名、SPF、DKIM 和反垃圾策略。
11. message 属性与三种通知形式的实际表现
notification.send 的 message 使用统一输入结构,但飞书、钉钉和 EMAIL 会按各自的消息协议进行渲染。当前只支持 title、summary、theme、detailMarkdown、facts 和 actions 六个属性;不在列表中的字段会被拒绝。
11.1 统一输入结构
message: {
title: "订单审批通过",
summary: `订单 ${orderNo} 已完成审批`,
theme: "blue",
detailMarkdown: "请及时处理后续业务。",
facts: [
{ label: "订单号", value: orderNo },
{ label: "审批状态", value: "已通过" }
],
actions: [
{
text: "查看详情",
url: `${siteUrl}/orders/${encodeURIComponent(orderNo)}`
}
]
}其中 title 和 summary 必填;theme、detailMarkdown、facts 和 actions 可选。动态值应先在 BFF 中计算完成,再把最终字符串传给通知扩展。
11.2 字段支持与消费结果
| 属性 | 输入约束 | 飞书 | 钉钉 | |
|---|---|---|---|---|
title | 必填,非空字符串 | 卡片头部标题 | markdown.title,并在正文中显示为三级标题 | 邮件主题,同时显示为正文标题 |
summary | 必填,非空字符串 | 卡片首个 Markdown 内容块 | Markdown 正文摘要 | HTML 正文概要段落,同时进入纯文本正文 |
theme | 可选,仅支持 blue、green、orange、red、grey | 作为卡片头部主题色 | 当前不消费,不改变消息样式 | 当前不消费,不改变邮件样式 |
detailMarkdown | 可选,字符串 | 与 facts 合并为 Markdown 内容块 | 追加到 Markdown 正文 | HTML 中转义后放入预格式化文本块,不渲染为富 Markdown;纯文本正文保留原内容 |
facts | 可选,最多 8 项;每项严格为 { label, value },两个字段均为非空字符串 | 按“加粗标签:值”逐行显示 | 按 Markdown 项目符号逐行显示 | HTML 中显示为标签和值的表格;纯文本中显示为“标签: 值” |
actions | 可选,最多 2 项;每项严格为 { text, url },两个字段均为非空字符串 | 渲染为卡片按钮 | 渲染为 Markdown 链接列表 | HTML 中渲染为超链接;纯文本中显示为“按钮文字: URL” |
11.3 飞书的实际消费方式
飞书群机器人和飞书应用机器人对这六个字段的消费结果一致:title 进入卡片头部,theme 控制头部主题色,summary 作为第一段内容,facts 与 detailMarkdown 组成后续 Markdown 内容,actions 转换为按钮。飞书是当前三个渠道中唯一实际消费 theme 的渠道。
11.4 钉钉的实际消费方式
钉钉统一发送 msgtype=markdown 的消息。title 同时用于 markdown.title 和正文标题,summary、facts、detailMarkdown 和 actions 按顺序拼接成 Markdown 正文。actions 不会显示为独立按钮,而是显示为链接列表。当前实现不会读取 theme。
11.5 EMAIL 的实际消费方式
EMAIL 会同时生成 HTML 正文和纯文本正文。title 用作邮件主题并显示为正文标题,summary 用作概要段落,facts 在 HTML 中显示为表格,actions 显示为超链接。detailMarkdown 会进行 HTML 转义并放入预格式化文本块,因此其中的 Markdown 标题、表格等语法不会转换成对应的 HTML 样式。当前实现不会读取 theme。
cc、ccList、bcc、replyTo 和附件都不是 message 属性。当前 BFF 的 notification.send 也没有暴露动态抄送参数;不要把这些字段放进 message 或发送参数顶层。
11.6 跨渠道编写建议
- 跨渠道共用一份消息时,把
title和summary作为完整信息主体,不要依赖theme表达业务状态。 detailMarkdown优先使用普通文本、换行和简单强调,不要依赖复杂 Markdown 表格、图片或渠道专属语法。- 结构化业务信息放入
facts,跳转入口放入actions;分别遵守最多 8 项和最多 2 项的限制。 actions.url应使用经过校验的可信 HTTPS 地址,不要让调用方直接传入任意跳转地址。- 同一个
message在不同渠道中的视觉结果不同,检查时应分别查看飞书卡片、钉钉 Markdown 消息和实际邮件。