☰
Cal.com Signal 集成深度解析:在 cal.diy 中配置聊天与视频通话地点的完整实现
2026/10/11 20:43:51 网站建设 项目流程
  • 后端
  • 前端
  • 企业应用

【免费下载链接】cal.diy

Scheduling infrastructure for absolutely everyone.

项目地址:https://gitcode.com/GitHub_Trending/ca/cal.diy
点击查看免费下载

Signal 集成是 Cal.com(本仓库 cal.diy)应用商店中的一个轻量级 messaging / conferencing 应用,其唯一能力由一行描述概括:Schedule a chat with your guests or have a Signal Video call.(与受邀者安排聊天,或发起 Signal 视频通话)。本文以该集成应用的描述文档为骨架,深入仓库源码,完整讲解其配置参数、安装流程、静态链接地点的运行时校验机制,以及在事件类型(Event Type)与 Platform API v2 中的使用方式。读完本文,你将掌握 Cal.com 应用商店中"静态链接型会议地点"这一类应用的完整工作原理,并具备自行扩展同类集成的能力。

Signal 集成:一句话描述背后的完整功能

DESCRIPTION.md 全篇只有一段描述文字,但它同时以 YAML frontmatter 的形式承载了应用商店的元信息:

--- description: Schedule a chat with your guests or have a Signal Video call. items: - 1.jpg - 2.jpg --- Schedule a chat with your guests or have a Signal Video call.
  • description:应用商店列表页中展示的一句话简介,会被 Cal.com 的应用市场直接使用;
  • items:应用详情页中的展示截图列表,对应 static/1.jpg 与 static/2.jpg 两张随包发布的截图;
  • 正文部分与 frontmatter 的description相同,用于渲染应用详情页的简介段落。
Signal 集成随包发布的展示截图之一
Signal 集成随包发布的展示截图之二

从功能实现看,Signal 集成本质上是把「一个可预约的 Signal 链接」作为事件类型(Event Type)的会议地点。它不需要 OAuth 授权、不需要同步日历,安装后组织者在创建事件类型时填写自己的signal.me个人链接,受邀者即可看到该链接并据此发起 Signal 通话。

应用目录结构:一个最小化 App 的完整骨架

Signal 集成位于 packages/app-store/signal/,目录结构非常精简,是整个应用商店中"最小可运行 App"的典型代表:

signal/ ├── api/ │ ├── add.ts # 安装入口(声明式 handler) │ └── index.ts # 导出 add handler ├── static/ │ ├── 1.jpg # 应用商店展示截图(DESCRIPTION.md 中声明) │ ├── 2.jpg │ └── icon.svg # 应用图标(蓝色圆角方形 + 白色 Signal 标志) ├── DESCRIPTION.md # 应用描述与展示图声明 ├── config.json # 应用核心配置(名称、类型、地点元信息) ├── index.ts # 包入口,重新导出 api └── package.json # 包名为 @calcom/signal
  • index.ts 仅一行export * as api from "./api",将安装 API 暴露给应用注册表;
  • api/index.ts 将addhandler 作为默认导出转发;
  • package.json 声明包名为@calcom/signal,主入口为./index.ts,依赖@calcom/lib工作区包;
  • static/icon.svg 是官方风格图标:以#3A76F0蓝色圆角方形为底,中央为白色 Signal 标志图形。

config.json:Signal 如何定义"会议地点"

config.json 是整个集成的配置中枢,所有应用商店展示信息与地点行为都由它驱动:

{ "name": "Signal", "slug": "signal", "type": "signal_video", "logo": "icon.svg", "url": "https://cal.com/", "variant": "messaging", "categories": ["messaging", "conferencing"], "publisher": "Cal.com, Inc.", "email": "support@cal.com", "description": "Schedule a chat with your guests or have a Signal Video call.", "appData": { "location": { "type": "integrations:signal_video", "label": "Signal", "linkType": "static", "organizerInputPlaceholder": "https://signal.me/#p/+11234567890", "urlRegExp": "^http(s)?:\\/\\/(www\\.)?signal.me\\/[a-zA-Z0-9]*" } }, "isOAuth": false }

核心字段解析:

字段值含义
name/slugSignal/signal应用名称与唯一标识(slug 禁止随意修改)
typesignal_video应用类型,同时作为 Credential 记录的type字段值
variantmessaging应用形态分类,影响应用商店的展示逻辑
categories["messaging", "conferencing"]同时归属消息与会议两个分类
isOAuthfalse非 OAuth 应用,安装无需外部授权跳转
appData.location.typeintegrations:signal_video事件类型中实际存储的"地点类型"标识,integrations:前缀表示来自应用商店
appData.location.linkTypestatic静态链接:地点是一个由组织者预先填写的固定 URL
appData.location.organizerInputPlaceholderhttps://signal.me/#p/+11234567890组织者填写链接时的输入框占位示例
appData.location.urlRegExp^http(s)?:\/\/(www\.)?signal.me\/[a-zA-Z0-9]*组织者输入的链接必须匹配的正则校验规则

其中urlRegExp限定了合法链接必须位于signal.me域名下(可带http/https与www.前缀),从源头保证事件类型中存储的确实是有效的 Signal 链接。

安装流程:声明式 Handler 与默认安装

Signal 的安装逻辑位于 api/add.ts,它采用 Cal.com 的声明式 App Handler 模式:

import type { AppDeclarativeHandler } from "@calcom/types/AppHandler"; import { createDefaultInstallation } from "../../_utils/installation"; import appConfig from "../config.json"; const handler: AppDeclarativeHandler = { appType: appConfig.type, variant: appConfig.variant, slug: appConfig.slug, supportsMultipleInstalls: false, handlerType: "add", createCredential: ({ appType, user, slug, teamId }) => createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }), }; export default handler;

要点:

  • AppDeclarativeHandler的类型定义见 packages/types/AppHandler.d.ts,要求提供appType、slug、variant、supportsMultipleInstalls、handlerType: "add"与createCredential回调;
  • supportsMultipleInstalls: false表示每个用户(或团队)只能安装一次;
  • createCredential直接调用 _utils/installation.ts 中的createDefaultInstallation,传入空key: {}——因为 Signal 不需要保存任何第三方凭据。

createDefaultInstallation的实际行为是在数据库中创建一条Credential记录:

const installation = await prisma.credential.create({ data: { type: appType, key, ...(teamId ? { teamId } : { userId: user.id }), appId: slug, subscriptionId, paymentStatus, billingCycleStart, }, });

可以看到,安装的本质只是记录「该用户/团队已启用 Signal 应用」,不涉及任何外部 API 交互。这也解释了isOAuth: false的含义:从点击安装到安装完成是瞬时且无跳转的。

注册表:生成的集成元数据

安装入口与元数据会被构建期脚本汇总进生成文件,Signal 在其中均有登记:

  • apps.server.generated.ts 第 65 行:signal: import("./signal/api"),使服务器端能够调用其安装 handler;
  • apps.metadata.generated.ts 第 200 行:signal: signal_config_json,将config.json注册为应用元数据;
  • bookerApps.metadata.generated.ts 第 82 行:同样注册 Signal 元数据,供预订(booking)页面在运行时读取其地点信息。

静态链接地点的运行时行为

Cal.com 将所有应用商店提供的地点统一汇总于 packages/app-store/locations.ts。该文件遍历appStoreMetadata(由各应用config.json生成),把appData.location转换为标准的地点类型对象,Signal 正是其中之一。

模板变量替换与字段补全

for (const [key, value] of Object.entries(location)) { if (typeof value === "string") { location[key] = value.replace(/{SLUG}/g, meta.slug).replace(/{TITLE}/g, meta.name); } } const newLocation = { ...location, messageForOrganizer: location.messageForOrganizer || `Set ${location.label} link`, iconUrl: meta.logo, variable: location.variable || "locationLink", defaultValueVariable: location.defaultValueVariable || "link", };
  • 地点字符串中的{SLUG}、{TITLE}占位符会被替换为实际的应用 slug 与名称(详见模板章节);
  • 若未显式提供messageForOrganizer,默认文案为Set Signal link;
  • 默认使用locationLink作为组织者输入字段、link作为存储变量。

静态链接强制要求组织者输入

if (newLocation.linkType === "static") { newLocation.organizerInputType = location.organizerInputType || "text"; if (newLocation.organizerInputPlaceholder?.match(/https?:\/\//)) { // HACK: Translation ends up removing https? if it's in the beginning :( newLocation.organizerInputPlaceholder = ` ${newLocation.organizerInputPlaceholder}`; } }

linkType === "static"的地点(Signal 属于此类)会在事件类型配置界面强制显示组织者输入框,并将以https://开头的占位符前加一个前导空格,避免 i18n 翻译处理误删协议头。

链接合法性校验

组织者填写的链接会在保存事件类型时通过 locations.ts 中的locationsResolver(zod schema)校验:

if ( eventLocationType && !eventLocationType.default && eventLocationType.linkType === "static" && eventLocationType.urlRegExp ) { const valid = z.string().regex(new RegExp(eventLocationType.urlRegExp)).safeParse(link).success; if (!valid) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: [eventLocationType?.defaultValueVariable ?? "link"], message: t("invalid_url_error_message", { label: eventLocationType.label, sampleUrl: sampleUrl ?? "https://cal.com", }), }); } }

即:静态链接类型的地点,其链接必须以urlRegExp正则(Signal 为signal.me域名规则)做匹配校验,不合法则给出带示例 URL 的报错信息。

地点类型与存储

  • AppStoreLocationType[appName] = newLocation.type将应用名映射到地点类型,随后LocationType枚举会包含integrations:signal_video;
  • getAppSlugFromLocationType支持从地点类型反查应用 slug;
  • 预订时getLocationValueForDB会把事件类型中的 Signal 链接值写入预订记录,作为本次预约的会议地点;
  • getHumanReadableLocationValue会在成功页/邮件中把integrations:signal_video渲染为可读的Signal标签或直接展示链接。

在事件类型与 Platform API v2 中使用 Signal

事件类型配置

组织者安装 Signal 后,在创建或编辑事件类型时,地点选项列表会动态加入Signal(数据来自bookerApps.metadata.generated.ts中的元数据)。选择后输入自己的signal.me链接(格式如https://signal.me/#p/+11234567890),受邀者在预订页即可看到该链接,确认预订后链接会出现在确认页与通知邮件中。

Platform API v2 的地点映射

如果你使用 Platform API 创建事件类型,Signal 地点对应一个公开的集成标识signal-video。相关转换逻辑位于 apps/api/v2/src/platform/event-types/event-types_2024_06_14/transformers/:

  • api-to-internal/locations.ts 第 27 行:"signal-video": "integrations:signal_video",将 API 入参转换为内部地点类型;
  • internal-to-api/locations.ts 第 29 行:"integrations:signal_video": "signal-video",反向转换;
  • internal/locations.ts 第 21 行:zod 校验中允许"signal-video"作为合法的集成地点标识。

因此,通过 API 创建带 Signal 地点的事件类型时,请求体中可声明type: "integration", integration: "signal-video"并附带 Signal 链接,服务端会将其规范化为integrations:signal_video存储。

复用模板开发同类集成

Signal 集成并非特例,Cal.com 为其提供了官方模板 packages/app-store/templates/event-type-location-video-static/,模板的 config.json 使用占位符定义同样的地点结构:

"appData": { "location": { "type": "integrations:{SLUG}_video", "label": "{TITLE}", "linkType": "static", "organizerInputPlaceholder": "https://video.app/mylink", "urlRegExp": "^http(s)?:\\/\\/(www\\.)?video.app\\/[a-zA-Z0-9]*" } }

模板的 api/add.ts 与 Signal 的 api/add.ts 逐行一致。对照可知,开发一个「静态链接型」集成只需四步:

  1. 复制模板目录,修改config.json中的name、slug、type、appData.location(特别是organizerInputPlaceholder与urlRegExp);
  2. 编写DESCRIPTION.md(描述 +items截图列表);
  3. 替换static/下的图标与展示截图;
  4. 通过 app-store CLI 生成注册表条目,即可出现在应用商店并作为事件类型地点使用。

小结

Signal 集成是理解 Cal.com 应用商店架构的最佳最小案例:DESCRIPTION.md负责展示信息,config.json定义地点行为(静态链接 + 正则校验),声明式 handler 完成无 OAuth 的即装即用安装,运行时由 locations.ts 统一驱动地点类型解析、校验与存储,并通过 API v2 转换器对 Platform 用户开放。掌握了这条链路,你就掌握了 Cal.com 中所有linkType: "static"类会议地点应用(如 Skype、Telegram、Facetime 等同类集成)的完整工作机制。

  • 后端
  • 前端
  • 企业应用

【免费下载链接】cal.diy

Scheduling infrastructure for absolutely everyone.

项目地址:https://gitcode.com/GitHub_Trending/ca/cal.diy
点击查看免费下载

相关推荐

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询