首页
/ cal.diy 中的 Salesforce 集成指南:把预订参与者自动写入 Sales Cloud 联系人

cal.diy 中的 Salesforce 集成指南:把预订参与者自动写入 Sales Cloud 联系人

2026-09-09 09:05:16作者:裘晴惠Vivianne

本篇技术指南以本仓库(cal.diy,Cal.com 调度基础设施项目)中 Salesforce 集成应用的官方描述文档 DESCRIPTION.md 为核心,结合其 OAuth 授权、CRM 服务、事件类型配置与 Salesforce Apex 扩展包的完整源码实现,系统讲解该集成"把活动参与者(attendees)自动创建为 Salesforce 联系人"的核心能力、可配置项与开发发布流程。读完本文,你将掌握该集成的安装授权链路、事件类型级配置项语义、底层 CRM 服务实现原理,以及基于 scratch org 的本地开发与 SFDC Unlocked Package 发布方法。

集成总览:Salesforce (Sales Cloud) 是什么、集成做什么

按照官方描述文档 DESCRIPTION.md 的定义,Salesforce (Sales Cloud) 是一款云端应用,它通过集中管理客户信息、记录客户与公司的交互、并自动化销售人员每天要完成的许多任务,来帮助销售团队更聪明、更快速地销售。

在该集成中,Cal.com 承担的角色是"调度基础设施":当客户通过你的 Cal.com 链接完成预订后,集成会把本次预订的参与者自动同步到 Salesforce,作为 Sales Cloud 中的联系人(Contact)记录,从而让销售数据与日程数据打通。其特性在文档中被明确概括为一条:

  • 将预订事件参与者(event attendees)创建为 Salesforce (Sales Cloud) 中的联系人。

config.json 可以看到该应用在 app-store 体系中的完整元信息:

{
  "name": "Salesforce",
  "slug": "salesforce",
  "type": "salesforce_crm",
  "logo": "icon.png",
  "variant": "crm",
  "categories": ["crm"],
  "extendsFeature": "EventType",
  "publisher": "Cal.com, Inc.",
  "isOAuth": true
}

关键点解读:

  • type: "salesforce_crm" 表明它实现了 Cal.com 的 CRM 集成契约,注册在 CRM 类型下;
  • variant: "crm"categories: ["crm"] 决定它在应用市场中归属于 CRM 分类;
  • extendsFeature: "EventType" 说明该应用会为每个事件类型(EventType)提供可独立配置的能力卡片(AppCard);
  • isOAuth: true 说明其安装走 OAuth 授权流程。

从项目整体结构看,这个集成由两部分组成:一部分是 Cal.com 侧基于 Node/TypeScript 的集成服务(位于 packages/app-store/salesforce),另一部分是部署在 Salesforce 组织内的 Apex 扩展包(sfdc-package),两者通过 OAuth 凭据与 Named Credential 协同工作。

OAuth 授权链路:从安装到令牌落地

Salesforce 集成的安装是一个标准 OAuth 2.0 授权码流程,由两个 Next.js API 端点完成:发起授权(api/add.ts)与回调换令牌(api/callback.ts)。

发起授权

api/add.ts 中,服务端先从应用密钥中读取 consumer_key,缺失时直接返回 400 Salesforce client id missing

const appKeys = await getAppKeysFromSlug("salesforce");
if (typeof appKeys.consumer_key === "string") consumerKey = appKeys.consumer_key;
if (!consumerKey) return res.status(400).json({ message: "Salesforce client id missing." });

随后基于 jsforce 构造 OAuth2 客户端,并以 refresh_token full 作为授权 scope(前者换取长期可用令牌,后者申请完整 API 权限):

const salesforceClient = new jsforce.Connection({
  oauth2: {
    clientId: consumerKey,
    redirectUri: `${WEBAPP_URL_FOR_OAUTH}/api/integrations/salesforce/callback`,
  },
});

const url = salesforceClient.oauth2.getAuthorizationUrl({
  scope: "refresh_token full",
  ...(state && { state }),
});
res.status(200).json({ url });

注意 redirectUri 指向 WEBAPP_URL_FOR_OAUTH 下的 callback 端点,同时会把请求的 OAuth state 编码进授权 URL,用于回调时校验会话归属。

回调换令牌与令牌生命周期探测

api/callback.ts 在拿到 code 后,使用 consumer_keyconsumer_secret 调用 Salesforce 令牌端点换取访问令牌:

const conn = new jsforce.Connection({
  oauth2: { clientId: consumerKey, clientSecret: consumerSecret, redirectUri: ... },
});
const salesforceTokenInfo = await conn.oauth2.requestToken(code as string);

值得注意的实现细节是:集成并不会直接保存令牌,而是先调用 Salesforce 的 OIDC Token Introspection 端点(lib/getSalesforceTokenLifetime.ts)计算令牌有效时长,再把 token_lifetime 一并存入凭据:

const response = await fetch(`${instanceUrl}/services/oauth2/introspect`, {
  method: "POST",
  headers: {
    Authorization: `Basic ${Buffer.from(`${consumer_key}:${consumer_secret}`).toString("base64")}`,
    "Content-Type": "application/x-www-form-urlencoded",
  },
  body: new URLSearchParams({ token: accessToken, token_type_hint: "access_token" }),
});
const tokenLifetime = data.exp - data.iat; // exp 与 iat 均以秒为单位

最终通过 createOAuthAppCredential{ ...salesforceTokenInfo, token_lifetime } 落库,然后重定向回应用安装页(getInstalledAppPath)。凭据对象的结构在 lib/CrmService.ts 中有对应的 zod schema 约束,包含 instance_urlaccess_tokenscopetoken_lifetime 等字段。

事件类型级配置项:schema 定义与界面开关

得益于 extendsFeature: "EventType",每个事件类型都可以独立开关并配置 Salesforce 同步行为。配置结构定义在 zod.tsappDataSchema 中(基于 eventTypeAppCardZod 扩展),其配置 UI 实现在 components/EventTypeAppCardInterface.tsx

核心记录类型与写回枚举

lib/enums.ts 定义了所有配置项可用的枚举值:

  • Salesforce 记录类型 SalesforceRecordEnumContactLeadAccountEvent
  • 写回时机 WhenToWriteToRecordevery_booking(每次预订都写)、field_empty(仅当字段为空时写);
  • 字段类型 SalesforceFieldTypedatestringphonecustompicklistbooleandatetimetextarea
  • 日期数据来源 DateFieldTypeDatabooking_start_datebooking_created_datebooking_cancel_date
  • 轮询路由判定来源 RoutingReasonsaccount_lookup_fieldlead_ownercontact_owneraccount_owner

完整配置项清单

appDataSchema 中可配置项及语义如下(布尔开关均有对应的 UI 控件,字符串/记录型字段在配置卡片中编辑):

配置键 类型 默认值 语义
roundRobinLeadSkip boolean 轮询路由(Round Robin)分配时跳过已有匹配记录的参与者
roundRobinSkipCheckRecordOn Contact/Lead/Account Contact 轮询跳过时在哪种记录上检查归属
rrSkipFieldRules {field, value, action}[] 字段级跳过规则,actionignore(命中则跳过)或 must_include(必须命中才跳过)
ifFreeEmailDomainSkipOwnerCheck boolean false 免费邮箱域名(如 gmail.com)跳过 owner 检查
roundRobinSkipFallbackToLeadOwner boolean false 找不到归属时回退到 Lead Owner
skipContactCreation boolean 不自动创建联系人
createEventOn Contact/Lead/Account Contact 在哪种记录类型下创建 Salesforce Event
createNewContactUnderAccount boolean 在 Account 下新建 Contact
createLeadIfAccountNull boolean Account 为空时创建 Lead
onBookingWriteToEventObject boolean false 预订成功后写回 Event 对象
onBookingWriteToEventObjectMap record {} Event 对象字段映射
createEventOnLeadCheckForContact boolean 以 Lead 创建 Event 前先检查 Contact
onBookingChangeRecordOwner boolean false 预订后修改记录 Owner
onBookingChangeRecordOwnerName string [] 新 Owner 名称
sendNoShowAttendeeData boolean false 发送 No-Show 参与者数据
sendNoShowAttendeeDataField string "" No-Show 数据写入的字段
onBookingWriteToRecord boolean false 预订成功后写回记录
onBookingWriteToRecordFields record {} 记录字段映射,值为 writeToBookingEntry
ignoreGuests boolean false 忽略参与者(Guests),只处理主预订人
onCancelWriteToEventRecord boolean false 取消预订时写回
onCancelWriteToEventRecordFields record {} 取消时字段映射

其中写回条目的结构(writeToBookingEntry)为:

{
  value: string | boolean,      // 要写入的值
  fieldType: SalesforceFieldType, // 对应 SF 字段类型
  whenToWrite: WhenToWriteToRecord // every_booking | field_empty
}

UI 侧(components/EventTypeAppCardInterface.tsx)通过 useAppContextWithSchema 读写这些配置,并渲染三个记录类型下拉框:recordOptions(Contact/Lead/Account,决定 createEventOn)与 checkOwnerOptions(Contact/Lead/Account,决定 roundRobinSkipCheckRecordOn),以及 WriteToObjectSettingsFieldRulesSettings 两个子设置组件(见 components/components)。

核心实现:CRM 服务如何把参与者写进 Salesforce

整个集成的主逻辑集中在 lib/CrmService.ts(约 1956 行),它实现了 Cal.com 通用的 CRM 接口,并通过 lib/index.tsBuildCrmService 的名义导出,供 crmManager 统一调度。

SalesforceCRMService 的关键设计(lib/CrmService.ts):

  • 持有 appOptions(即上一节的事件类型配置),预订流程按配置逐项执行;
  • 基于 jsforce 维护惰性 Connection,用存储的令牌访问 Salesforce REST API;
  • 通过 graphql/SalesforceGraphQLClient.ts 使用 Salesforce GraphQL 端点(schema 定义见 src/gql)查询记录归属;
  • 额外扩展了 SalesforceCRM 接口,增加 Salesforce 特有的三个方法:findUserEmailFromLookupField(按查找字段反查用户邮箱与路由来源)、incompleteBookingWriteToRecord(预订信息不完整时的写回兜底)、getAllPossibleAccountWebsiteFromEmailDomain(从邮箱域名推断 Account 官网)。

联系人查找与去重是核心流程之一:代码中定义了 SalesforceDuplicateError 类型(lib/CrmService.ts),用于解析 Salesforce 重复规则(Duplicate Rule)返回的 matchResults,从而在写入前判断目标联系人是否已存在。工具函数 lib/utils/getDominantAccountId.tslib/utils/getAllPossibleWebsiteValuesFromEmailDomain.ts 负责在多候选 Account 之间决策归属、从免费邮箱域名推导可能官网,这些均有配套单元测试(lib/utils/tests)。

对轮询路由(Round Robin)场景,集成还引入了"Assignment Reason"记录:配置打开 roundRobinLeadSkip 后,服务会基于 rrSkipFieldRulesRoutingReasons 判定跳过逻辑,并通过 lib/repositories/PrismaAssignmentReasonRepository.ts 把路由原因持久化,便于审计。

集成测试(lib/tests/CrmService.integration.test.ts)配合 salesforceMock.tsgraphql/tests 中的 urqlMock 覆盖了创建联系人、写入 Event、GraphQL 查询等关键链路,是理解预期行为的最佳参考。

Salesforce 侧 Apex 扩展包与用户同步

集成并不止于 Cal.com 单向写数据,还包括部署在 Salesforce 组织内的 Unlocked Package(sfdc-package),实现"Salesforce 变化反向通知 Cal.com"的双向能力。Apex 源码位于 sfdc-package/force-app/main/default

Cal.com 侧接收这些同步请求的端点是 api/user-sync.ts。该 POST 端点会校验请求体中的 instanceUrl 是否与已存储凭据匹配、orgId 是否与凭据 URL 中的组织 ID 一致、以及邮箱与团队用户是否对应,三重校验全部通过才返回 { success: true },防止跨组织伪造同步请求。

本地开发、测试与包发布流程

官方开发文档 README.md 给出了完整的工程化流程。

创建 Salesforce 测试组织(Scratch Org)

需先安装 Salesforce CLI,然后:

yarn scratch-org:create   # 按 project-scratch-def.json 配置创建 scratch org
yarn scratch-org:start    # 在浏览器中打开该 org

若要在本地联调,需要把 scratch org 中的 Named Credential 指向本地实例(将 CalCom_Development 指向 localhost)。

GraphQL 类型生成

该集成通过 GraphQL Codegen 从 Salesforce schema 生成查询与类型(文档见 README.md):

  • 由于 Salesforce GraphQL 端点 v63 在生成 Setup__JoinInput 类型时存在已知报错,schema 需从 Salesforce GraphQL introspection 结果转换而来(使用 graphql-introspection-json-to-sdl 工具);
  • 运行 yarn generate:schema 生成 SDL 文件;
  • 开发期间保持 yarn codegen:watch 后台运行,自动从 SDL 生成查询与类型;
  • 相关配置见 codegen.tsgraphql.config.tsgraphqlrc.config.ts

Apex 包部署与发布

Apex 侧开发(需 Salesforce CLI):

yarn sfdc:deploy:preview   # 预览将部署到 scratch org 的变更
yarn sfdc:deploy           # 实际部署到 scratch org

发布 Unlocked Package 时所有命令需在 sfdc-package 目录下执行。首次创建包(一次性):

sf package create \
  --name "calcom-sfdc-package" \
  --package-type Unlocked \
  --path force-app \
  --target-dev-hub team@cal.com

每次发布新版本:

sf package version create \
  --package "calcom-sfdc-package" \
  --installation-key-bypass \
  --wait 20 \
  --target-dev-hub team@cal.com

其中 --installation-key-bypass 允许免密码安装,--wait 20 表示最多等待 20 分钟构建完成;准备 promote 时需追加 --code-coverage(要求 Apex 测试覆盖率不低于 75%)。查看已发布版本:

sf package version list --target-dev-hub team@cal.com

安装 URL 格式为 https://login.salesforce.com/packaging/installPackage.apexp?p0=<04t_SUBSCRIBER_PACKAGE_VERSION_ID>。Beta 版只能安装到沙盒/scratch org,允许安装到生产组织的版本需先 promote:

sf package version promote \
  --package "calcom-sfdc-package@X.X.X-X" \
  --target-dev-hub team@cal.com

运行 Apex 测试与覆盖率统计:

sf project deploy start --target-org <org-alias>
sf apex run test --test-level RunLocalTests --wait 10 --target-org <org-alias>

小结

本文基于官方描述文档 DESCRIPTION.md 展开,完整梳理了 cal.diy 中 Salesforce 集成"将预订参与者创建为 Sales Cloud 联系人"这一核心能力的技术全貌:OAuth 授权与令牌生命周期管理(api/add.tsapi/callback.ts)、二十余项事件类型级配置(zod.ts)、以 lib/CrmService.ts 为核心的写入与去重逻辑、Salesforce 侧 Apex 包的双向同步,以及从 scratch org 到 Unlocked Package 发布的一整套工程实践。如需深入源码,建议从 lib/tests/CrmService.integration.test.ts 的集成测试与 sfdc-package 的 Apex 触发器入手,逐条对照配置项与断言,即可快速掌握端到端的数据流。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
docsdocs
暂无描述
Markdown
899
5.83 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
925
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.84 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
601
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.03 K
525
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
395