news 2026/9/10 15:45:25

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
cal.diy 中的 Salesforce 集成指南:把预订参与者自动写入 Sales Cloud 联系人

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

【免费下载链接】cal.diyScheduling infrastructure for absolutely everyone.项目地址: https://gitcode.com/GitHub_Trending/ca/cal.diy

本篇技术指南以本仓库(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.ts 的appDataSchema中(基于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 控件,字符串/记录型字段在配置卡片中编辑):

配置键类型默认值语义
roundRobinLeadSkipboolean轮询路由(Round Robin)分配时跳过已有匹配记录的参与者
roundRobinSkipCheckRecordOnContact/Lead/AccountContact轮询跳过时在哪种记录上检查归属
rrSkipFieldRules{field, value, action}[]字段级跳过规则,actionignore(命中则跳过)或must_include(必须命中才跳过)
ifFreeEmailDomainSkipOwnerCheckbooleanfalse免费邮箱域名(如 gmail.com)跳过 owner 检查
roundRobinSkipFallbackToLeadOwnerbooleanfalse找不到归属时回退到 Lead Owner
skipContactCreationboolean不自动创建联系人
createEventOnContact/Lead/AccountContact在哪种记录类型下创建 Salesforce Event
createNewContactUnderAccountboolean在 Account 下新建 Contact
createLeadIfAccountNullbooleanAccount 为空时创建 Lead
onBookingWriteToEventObjectbooleanfalse预订成功后写回 Event 对象
onBookingWriteToEventObjectMaprecord{}Event 对象字段映射
createEventOnLeadCheckForContactboolean以 Lead 创建 Event 前先检查 Contact
onBookingChangeRecordOwnerbooleanfalse预订后修改记录 Owner
onBookingChangeRecordOwnerNamestring[]新 Owner 名称
sendNoShowAttendeeDatabooleanfalse发送 No-Show 参与者数据
sendNoShowAttendeeDataFieldstring""No-Show 数据写入的字段
onBookingWriteToRecordbooleanfalse预订成功后写回记录
onBookingWriteToRecordFieldsrecord{}记录字段映射,值为writeToBookingEntry
ignoreGuestsbooleanfalse忽略参与者(Guests),只处理主预订人
onCancelWriteToEventRecordbooleanfalse取消预订时写回
onCancelWriteToEventRecordFieldsrecord{}取消时字段映射

其中写回条目的结构(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.ts 以BuildCrmService的名义导出,供 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.ts 与 lib/utils/getAllPossibleWebsiteValuesFromEmailDomain.ts 负责在多候选 Account 之间决策归属、从免费邮箱域名推导可能官网,这些均有配套单元测试(lib/utils/tests)。

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

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

Salesforce 侧 Apex 扩展包与用户同步

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

  • CalComCalloutQueueable.cls:以 Queueable 方式异步向 Cal.com 发起 Callout,避免在触发器同步链路中阻塞事务;
  • UserUpdateHandler.cls 与 UserUpdateTrigger.trigger:监听 Salesforce 侧 User 记录变更,触发同步请求;
  • CalComHttpMock.cls:测试用的 HTTP Mock,配合 UserUpdateHandlerTest.cls 与 CalComCalloutQueueableTest.cls 验证行为;
  • Named Credential(CalCom_Development.namedCredential-meta.xml 与CalCom_Production)用于把目标 Cal.com 实例地址配置化,开发环境指向本地实例,生产环境指向线上。

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.ts、graphql.config.ts 与 graphqlrc.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.ts、api/callback.ts)、二十余项事件类型级配置(zod.ts)、以 lib/CrmService.ts 为核心的写入与去重逻辑、Salesforce 侧 Apex 包的双向同步,以及从 scratch org 到 Unlocked Package 发布的一整套工程实践。如需深入源码,建议从 lib/tests/CrmService.integration.test.ts 的集成测试与 sfdc-package 的 Apex 触发器入手,逐条对照配置项与断言,即可快速掌握端到端的数据流。

【免费下载链接】cal.diyScheduling infrastructure for absolutely everyone.项目地址: https://gitcode.com/GitHub_Trending/ca/cal.diy

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 15:45:00

Docker容器化运维实战:核心命令与生产环境指南

1. Docker命令核心操作指南作为容器化技术的实际应用者&#xff0c;我整理了一份经过生产环境验证的Docker命令手册。这份指南不仅包含基础命令&#xff0c;更着重分享我在实际运维中积累的实战技巧和避坑经验。1.1 容器生命周期管理启动容器的标准命令是docker run&#xff0c…

作者头像 李华
网站建设 2026/9/10 15:40:32

昇腾/GE创建HCCL记录任务API

CreateHcomRecordTask 【免费下载链接】ge GE&#xff08;Graph Engine&#xff09;是面向昇腾的图编译器和执行器&#xff0c;提供了计算图优化、多流并行、内存复用和模型下沉等技术手段&#xff0c;加速模型执行效率&#xff0c;减少模型内存占用。 GE 提供对 PyTorch、Tens…

作者头像 李华
网站建设 2026/9/10 15:40:12

如何为 Go 项目 README 添加 Go Report Card、PkgGoDev 与 Release 徽章?

如何为 Go 项目 README 添加 Go Report Card、PkgGoDev 与 Release 徽章&#xff1f; 【免费下载链接】project-layout Standard Go Project Layout 项目地址: https://gitcode.com/GitHub_Trending/pr/project-layout 希望 Go 项目的 README 让读者一眼看到代码检查结果…

作者头像 李华
网站建设 2026/9/10 15:39:41

CANN/ge:昇腾图编译执行引擎

GE (Graph Engine) 【免费下载链接】ge GE&#xff08;Graph Engine&#xff09;是面向昇腾的图编译器和执行器&#xff0c;提供了计算图优化、多流并行、内存复用和模型下沉等技术手段&#xff0c;加速模型执行效率&#xff0c;减少模型内存占用。 GE 提供对 PyTorch、TensorF…

作者头像 李华