跳转到内容

输入关键词开始搜索文档。

Salesforce Marketing Cloud

最后更新 查看 Markdown 源文件@flue/salesforce

快速开始

通过 Salesforce Marketing Cloud Engagement 蓝图,将经过验证的事件通知服务入口和应用自有 REST 行为添加到现有 Flue 项目中。在终端或你选择的编码智能体中运行以下命令:

flue add channel salesforce-marketing-cloud

概述

该蓝图安装 @flue/salesforce。它在 <source-root>/salesforce-marketing-cloud-client.ts 创建一个窄范围的 Fetch 客户端,在 <source-root>/salesforce-marketing-cloud-email.ts 创建系列身份辅助函数,并在 <source-root>/channels/salesforce-marketing-cloud.ts 创建命名的 channel 和项目自有的 client 导出。它还会创建或更新一个智能体,将回调查找工具绑定到经过验证的电子邮件事件身份。此集成用于 Marketing Cloud Engagement ENS,而非通用的 Salesforce API。

src/channels/salesforce-marketing-cloud.ts(节选)
import { createSalesforceMarketingCloudChannel } from '@flue/salesforce';
import { dispatch, useModel } from '@flue/runtime';
import { Assistant } from '../agents/assistant.ts';
import { createSalesforceMarketingCloudClient } from '../salesforce-marketing-cloud-client.ts';
import { emailEventInstanceId, emailRefFromEvent } from '../salesforce-marketing-cloud-email.ts';

const callbackId = process.env.SALESFORCE_MARKETING_CLOUD_CALLBACK_ID!;
export const client = createSalesforceMarketingCloudClient({
  restBaseUrl: process.env.SALESFORCE_MARKETING_CLOUD_REST_BASE_URL!,
  accessToken: process.env.SALESFORCE_MARKETING_CLOUD_ACCESS_TOKEN!,
});

export const channel = createSalesforceMarketingCloudChannel({
  signatureKey: process.env.SALESFORCE_MARKETING_CLOUD_SIGNATURE_KEY!,
  callbackId,
  async events({ c, batch }) {
    const usefulEvents = [];
    for (const event of batch.events) {
      if (event.eventCategoryType !== 'EngagementEvents.EmailOpen') continue;
      const ref = emailRefFromEvent(callbackId, event);
      if (!ref) return c.json({ error: 'Expected a supported email event.' }, 400);
      usefulEvents.push({ event, ref });
    }
    for (const { event, ref } of usefulEvents) {
      await dispatch(Assistant, {
        id: emailEventInstanceId(ref),
        message: {
          kind: 'signal',
          type: `salesforce-marketing-cloud.${event.eventCategoryType}`,
          // `info` 携带系列特定的事件字段;参与事件没有自然的消息文本。
          body: JSON.stringify(event.info ?? {}),
          attributes: {
            ...(typeof event.timestampUTC === 'string' ? { occurredAt: event.timestampUTC } : {}),
            callbackId: ref.callbackId,
            mid: ref.mid,
            eid: ref.eid,
            jobId: ref.jobId,
            batchId: ref.batchId,
            listId: ref.listId,
            subscriberId: ref.subscriberId,
          },
        },
      });
    }
    return c.body(null, 204);
  },
});

签名批次中每个有效的选定电子邮件事件都会被接收进绑定到其回调和电子邮件跟踪身份的智能体,然后批次收到 204。完整的生成模块处理额外的发送和参与系列,并让绑定的智能体检索配置的回调。回调注册、OAuth、令牌刷新和一次性 /ens-verify 调用仍然是应用自有的;Node 和 Cloudflare 目标使用相同的 Fetch 和 Web Crypto 实现。

挂载渠道

渠道仅在 app.ts 挂载的位置提供 HTTP 路由。挂载模块中命名导出的 channel:

src/app.ts
import { channel as salesforceMarketingCloud } from './channels/salesforce-marketing-cloud.ts';

app.route('/channels/salesforce-marketing-cloud', salesforceMarketingCloud.route());

channel.route() 是一个纯路由器工厂,相对于挂载路径提供渠道声明的路由。本指南中的 webhook 路径假定使用常规的 /channels/salesforce-marketing-cloud 挂载;不同的挂载路径会相应调整。分发目标智能体模块携带 'use agent' 指令——该指令负责注册,因此仅用于分发的智能体不需要自己的 HTTP 挂载。

配置

变量用途
SALESFORCE_MARKETING_CLOUD_SIGNATURE_KEY必需 — 验证入站 ENS 批次。
SALESFORCE_MARKETING_CLOUD_CALLBACK_ID必需 — 限制并标识配置的 ENS 回调。
SALESFORCE_MARKETING_CLOUD_REST_BASE_URL必需 — 选择租户特定的 Marketing Cloud REST 源。
SALESFORCE_MARKETING_CLOUD_ACCESS_TOKEN必需 — 认证应用自有 REST 请求。

它安装 @flue/salesforce 并创建命名的 channel 和项目自有的 client 导出。该集成针对 Marketing Cloud Engagement 事件通知服务(ENS),而非通用的 Salesforce API。

注册完整的回调 URL:

https://example.com/channels/salesforce-marketing-cloud/events

签名密钥和出站访问令牌是单独的凭据。回调注册、OAuth、令牌刷新和令牌存储仍然是应用自有的。

渠道模块

src/channels/salesforce-marketing-cloud.ts
import {
  createSalesforceMarketingCloudChannel,
  type SalesforceMarketingCloudEvent,
} from '@flue/salesforce';
import { defineTool, dispatch, useModel } from '@flue/runtime';
import { Assistant } from '../agents/assistant.ts';
import { createSalesforceMarketingCloudClient } from '../salesforce-marketing-cloud-client.ts';
import {
  emailEventInstanceId,
  emailRefFromEvent,
  type SalesforceMarketingCloudEmailRef,
} from '../salesforce-marketing-cloud-email.ts';

const callbackId = requiredEnv('SALESFORCE_MARKETING_CLOUD_CALLBACK_ID');

export const client = createSalesforceMarketingCloudClient({
  restBaseUrl: requiredEnv('SALESFORCE_MARKETING_CLOUD_REST_BASE_URL'),
  accessToken: requiredEnv('SALESFORCE_MARKETING_CLOUD_ACCESS_TOKEN'),
});

export const channel = createSalesforceMarketingCloudChannel({
  signatureKey: requiredEnv('SALESFORCE_MARKETING_CLOUD_SIGNATURE_KEY'),
  callbackId,

  // 路径:/channels/salesforce-marketing-cloud/events
  async events({ c, batch }) {
    const usefulEvents: Array<{
      event: SalesforceMarketingCloudEvent;
      ref: SalesforceMarketingCloudEmailRef;
    }> = [];

    for (const event of batch.events) {
      switch (event.eventCategoryType) {
        case 'TransactionalSendEvents.EmailSent':
        case 'TransactionalSendEvents.EmailNotSent':
        case 'TransactionalSendEvents.EmailBounced':
        case 'EngagementEvents.EmailOpen':
        case 'EngagementEvents.EmailClick':
        case 'EngagementEvents.EmailUnsubscribe': {
          const ref = emailRefFromEvent(callbackId, event);
          if (!ref) {
            return c.json({ error: 'Expected a supported Marketing Cloud email event.' }, 400);
          }
          usefulEvents.push({ event, ref });
          break;
        }
        default:
          break;
      }
    }

    for (const { event, ref } of usefulEvents) {
      await dispatch(Assistant, {
        id: emailEventInstanceId(ref),
        message: {
          kind: 'signal',
          type: `salesforce-marketing-cloud.${event.eventCategoryType}`,
          // `info` 携带系列特定的事件字段;参与事件没有自然的消息文本。
          body: JSON.stringify(event.info ?? {}),
          attributes: {
            ...(typeof event.timestampUTC === 'string' ? { occurredAt: event.timestampUTC } : {}),
            callbackId: ref.callbackId,
            mid: ref.mid,
            eid: ref.eid,
            jobId: ref.jobId,
            batchId: ref.batchId,
            listId: ref.listId,
            subscriberId: ref.subscriberId,
          },
        },
      });
    }

    return c.body(null, 204);
  },
});

export function retrieveCallback(ref: SalesforceMarketingCloudEmailRef) {
  if (ref.callbackId !== callbackId) {
    throw new TypeError('Expected the configured Marketing Cloud callback.');
  }
  return defineTool({
    name: 'retrieve_salesforce_marketing_cloud_callback',
    description: '检索绑定到此智能体的 Marketing Cloud ENS 回调。',
    async run() {
      return { output: await client.getCallback(callbackId) };
    },
  });
}

function requiredEnv(name: string): string {
  const value = process.env[name];
  if (!value) throw new Error(`${name} is required.`);
  return value;
}

路由固定为 POST /events。该示例对选定的电子邮件事件系列进行分组,同时保持 ENS 分类法开放。emailRefFromEvent() 是应用代码,用于验证 mid、eid 和 event.composite 下选定系列的跟踪字段。它使用 callbackId 将这些值规范化为本地智能体 ID,并拒绝格式错误的事件。

ENS 不提供通用的交付或对话 ID。此电子邮件身份仅对应用验证的系列有效。compositeId 是可选的,且对事务性电子邮件已弃用,因此不要将其用作通用键。

项目自有客户端

使用窄范围的 Fetch 客户端,并在附加 Bearer 令牌之前验证租户源:

src/salesforce-marketing-cloud-client.ts
export function createSalesforceMarketingCloudClient({
  restBaseUrl,
  accessToken,
  fetcher = globalThis.fetch,
}: {
  restBaseUrl: string;
  accessToken: string;
  fetcher?: typeof globalThis.fetch;
}) {
  const origin = salesforceMarketingCloudRestOrigin(restBaseUrl);
  if (!accessToken || accessToken.trim() !== accessToken) {
    throw new TypeError('Marketing Cloud access token must be non-empty and trimmed.');
  }

  return {
    async getCallback(callbackId: string) {
      if (!callbackId || callbackId.trim() !== callbackId) {
        throw new TypeError('Marketing Cloud callback id must be non-empty and trimmed.');
      }
      const response = await fetcher(
        `${origin}/platform/v1/ens-callbacks/${encodeURIComponent(callbackId)}`,
        {
          method: 'GET',
          headers: {
            accept: 'application/json',
            authorization: `Bearer ${accessToken}`,
          },
        },
      );
      if (!response.ok) {
        throw new Error(`Marketing Cloud API request failed with ${response.status}.`);
      }
      const value: unknown = await response.json();
      if (!value || typeof value !== 'object' || Array.isArray(value)) {
        throw new TypeError('Marketing Cloud returned an invalid callback response.');
      }
      return value;
    },
  };
}

function salesforceMarketingCloudRestOrigin(value: string): string {
  const url = new URL(value);
  const suffix = '.rest.marketingcloudapis.com';
  if (
    url.protocol !== 'https:' ||
    url.username !== '' ||
    url.password !== '' ||
    url.port !== '' ||
    url.pathname !== '/' ||
    url.search !== '' ||
    url.hash !== '' ||
    !url.hostname.endsWith(suffix) ||
    url.hostname.length === suffix.length
  ) {
    throw new TypeError('Expected an HTTPS tenant origin ending in .rest.marketingcloudapis.com.');
  }
  return url.origin;
}

不要接受来自模型或事件的任意 API 源、回调 ID 或令牌。上面显示的工具在可信应用代码中绑定所有三个值,仅执行:

GET /platform/v1/ens-callbacks/{callbackId}
Authorization: Bearer <access token>

不需要 Salesforce SDK。回调注册、OAuth、令牌刷新、订阅生命周期、令牌存储和更广泛的出站 API 行为仍然是应用自有的。

绑定智能体

src/agents/assistant.ts
'use agent';
import { type AgentProps, useModel, useTool } from '@flue/runtime';
import { retrieveCallback } from '../channels/salesforce-marketing-cloud.ts';
import { parseEmailEventInstanceId } from '../salesforce-marketing-cloud-email.ts';

export function Assistant({ id }: AgentProps) {
  useModel('anthropic/claude-haiku-4-5');
  const email = parseEmailEventInstanceId(id);
  useTool(retrieveCallback(email));
  return '审查入站的 Salesforce Marketing Cloud 电子邮件生命周期事件。在回调状态或交付配置相关时检索配置的 ENS 回调。';
}

该工具不接受来自模型的租户源、回调 ID、访问令牌或资源 ID。解析的本地 ID 仍然是标识符,不是授权;该工具在选择凭据之前再次检查其回调 ID。

回调验证

在回调设置期间,Marketing Cloud 发送一个未签名的 JSON 正文,包含:

{
  "callbackId": "provider-callback-id",
  "verificationKey": "one-time-verification-key"
}

未签名的设置请求仅在渠道有 verification 处理程序时被接受。限制 callbackId,从应用代码调用 POST /platform/v1/ens-verify,并在设置后禁用该处理程序。没有该处理程序时,未签名的请求会收到 401。

Flue 验证形状并在处理程序完成后返回所需的空 200。它不会自动注册回调、获取令牌或调用验证 API。除非应用明确需要,否则将此设置调用与上面的 GET 专用客户端分开。

签名和事件批次

签名通知包含:

x-sfmc-ens-signature: <base64 HMAC-SHA256 digest>

Marketing Cloud 对精确的正文字节进行签名。signatureKey 是必需的:它是回调创建期间返回的不透明字符串,直接作为 UTF-8 HMAC 密钥材料导入。不要对其进行 base64 解码。只有签名头是 base64 解码的。

签名的负载是一个有序的非空数组,最多包含 1000 个事件。每个事件以其 Marketing Cloud 自己的字段名称和嵌套传递——没有 raw 包装器,也没有字段投影。入口仅需要每个事件上有非空的 eventCategoryType;这一个字段使批次可转发。其他一切都按照 ENS 发送的方式交付:

  • timestampUTC,提供者 UTC 纪元(毫秒),原样转发且不验证(某些系列省略它或使用不同的表示);
  • composite({ jobId, batchId, listId, … })、definitionKey 和 definitionId,在携带它们的电子邮件发送和参与系列上;
  • info,系列特定的详细信息;
  • mid 和 eid,在某些系列上作为 number 到达,在其他系列上作为 string;
  • compositeId,扁平化的跟踪 ID,对事务性电子邮件已弃用。

顶层索引签名转发建模类型未命名的任何已认证字段。批次还暴露 rawBody,即签名验证后的确切 UTF-8 正文。该包不关闭事件分类法,也不推断通用的资源、参与者、交付或对话身份。在 eventCategoryType 上缩窄,并验证你读取的每个系列特定字段。

响应和交付

不返回任何内容会产生空的 200。JSON 兼容的值会成为 JSON 200。普通的 Hono 或 Fetch Response 会原样传递。

ENS 仅确认 200 到 204 的状态。渠道失败和不支持的(不可序列化的)返回值会产生 500。确认范围之外的自定义 Response 会原样传递,并可能导致重发。

Flue 不施加路由超时。处理程序被等待,其结果被序列化。唯一的 ENS 截止时间在设置时:未签名的验证 POST 必须在 30 秒内响应 200,否则回调创建失败。稳态交付没有每请求截止时间,但 ENS 会重试任何未被确认的批次。

ENS 交付是至少一次的,重试可能持续最多七天。快速承认持久工作——分发,然后返回——而不是在慢操作上阻塞处理程序,并依赖幂等性。该包不进行去重或持久化事件;在非幂等工作之前使用应用自有的持久状态和系列适当的键。

Cloudflare Workers

入口和项目自有的客户端使用基于标准的 Fetch、URL 和 Web Crypto API。它们在 Flue 的规范 nodejs_compat 配置下的 workerd 中执行;包的 workerd 测试练习精确正文 HMAC 验证。

使用原始合成事件批次和本地密钥进行测试。使用注入的失败关闭 Fetch 在 Node 和 workerd 中测试真实客户端,断言精确的租户主机、回调路径、方法和 Bearer 头。永远不要注册实时回调、执行 OAuth、调用 /ens-verify 或从测试中联系 Salesforce。

参阅 @flue/salesforce README。