cal.diy 集成 Tandem 虚拟办公室:视频会议应用接入指南与源码解析
【免费下载链接】cal.diyScheduling infrastructure for absolutely everyone.项目地址: https://gitcode.com/GitHub_Trending/ca/cal.diy
本指南以开源调度平台 cal.diy 中 Tandem 应用集成模块(packages/app-store/tandemvideo)为核心,系统讲解如何为会议类型配置 Tandem 视频会议地点、完成 OAuth 授权与 Credential 存储,并深入剖析其视频适配器与 Tandem REST API 的底层调用细节。读完本文,你将掌握从应用密钥配置到会议创建/更新/删除的完整接入链路,以及该模块在 cal.diy 应用商店体系中的注册方式。
Tandem 集成是什么:虚拟办公室中的即时协作
Tandem 是一个全新的虚拟办公空间,它让团队即使在线也能像身处实体办公室一样轻松连接。通过共办公房间(co-working rooms)、可用状态(available statuses)、实时视频通话(live real-time video call)与聊天(chat)等能力,你可以一眼看到谁在办公室,一键发起沟通与协作。它支持跨平台使用,同时提供桌面端与移动端版本(该描述同时被 DESCRIPTION.md 与应用元数据 _metadata.ts 引用)。
在 cal.diy 中,Tandem 被封装为conferencing(视频会议)类别的应用集成,其应用元数据声明如下(packages/app-store/tandemvideo/_metadata.ts):
type/appId:tandem_video,用于在 Credential 表中区分该集成;slug:tandem,用于应用商店路由与应用密钥读取;variant/category/categories:conferencing,表明它属于视频会议类应用;isOAuth: true,走标准 OAuth 授权流程;appData.location:注册integrations:tandem动态链接类型,供会议类型作为"地点"选项使用。
该模块在应用商店中的注册入口为 index.ts,同时它会被应用商店的代码生成产物自动汇总(见 apps.metadata.generated.ts、apps.server.generated.ts 与 video.adapters.generated.ts),无需手写任何注册代码。
前置条件:配置应用密钥(App Keys)
Tandem 集成运行前必须完成三项应用密钥配置,其校验逻辑在 lib/VideoApiAdapter.ts 与 api/add.ts 中均有体现:
| 配置键 | 含义 | 缺失时的表现 |
|---|---|---|
client_id | Tandem OAuth 客户端 ID | 返回 400:Tandem client_id missing. |
client_secret | Tandem OAuth 客户端密钥 | 返回 400:Tandem client_secret missing. |
base_url | Tandem API 基础地址 | 返回 400:Tandem base_url missing. |
配置键的 Schema 约束定义在 zod.ts:三个键均为非空字符串(z.string().min(1)),而appDataSchema为空对象,即该应用本身无需额外数据字段。密钥通过getAppKeysFromSlug("tandem")从应用商店统一机制加载,并以typeof appKeys.xxx === "string"做类型守卫后使用。
OAuth 授权流程:从发起安装到保存凭据
Tandem 集成包含两个 API 端点,均挂在api/index.ts导出下,路由前缀为/api/integrations/tandemvideo/。
add:构造授权跳转地址
api/add.ts 处理 GET 请求,流程如下:
- 通过
prisma.user.findFirstOrThrow校验当前会话用户存在; - 读取
client_id与base_url应用密钥,缺失则返回 400; - 以
WEBAPP_URL + /api/integrations/tandemvideo/callback作为redirect_uri并做 URL 编码; - 使用
node:querystring的stringify构造client_id、redirect_uri参数; - 返回
{ url: ${baseUrl}/oauth/approval?${query} },引导用户跳转到 Tandem 授权页。
callback:换取令牌并落库
api/callback.ts 处理授权回调:
- 校验
code参数,缺失时返回 401;并用decodeOAuthState(req, "tandem")解析 OAuth state(Tandem 位于免 nonce 校验的应用白名单中,见 _utils/oauth/decodeOAuthState.ts); - 携带
code、client_id、client_secret向${baseUrl}/api/v1/oauth/v2/token发起 POST,换取访问令牌; - 将响应中的
expires_in转换为绝对过期时间戳expiry_date后删除原字段; - 先清理该用户已有的
tandem_video类型凭据(prisma.credential.deleteMany),再调用createOAuthAppCredential写入新凭据; - 重定向回 state 中的
returnTo地址,或应用商店的已安装 Conferencing 应用页面(getInstalledAppPath({ variant: "conferencing", slug: "tandem" }))。
视频适配器:Tandem REST API 的完整封装
核心实现位于 lib/VideoApiAdapter.ts,它实现 cal.diy 的VideoApiAdapter接口(packages/types/VideoApiAdapter.d.ts),并通过 video.adapters.generated.ts 被视频会议系统按 slug 动态加载。
令牌管理与自动刷新
tandemAuth封装令牌生命周期(lib/VideoApiAdapter.ts):
- 有效性判定:
isTokenValid检查access_token存在且expiry_date < Date.now()——注意这里的命名语义是"已过期"; - 过期时携带
refresh_token(即 credential 中存储的 code)向${baseUrl}/api/v1/oauth/v2/token重新换发; - 新令牌按
Date.now() + expires_in * 1000计算expiry_date,并立即通过prisma.credential.update持久化,避免重启后令牌失效; getToken()在令牌未过期时直接返回现有 token,否则先刷新再返回。
事件翻译与会议操作
_translateEvent将 cal.diy 的CalendarEvent翻译为 Tandem 接口所需的 JSON 载荷(lib/VideoApiAdapter.ts):
{ "meeting": { "title": "事件标题", "starts_at": "开始时间(Unix 秒)", "ends_at": "结束时间(Unix 秒)", "description": "事件描述", "conference_solution": "tandem", "type": 3 } }其中时间通过Date.parse(date) / 1000转为 Unix 秒级时间戳。适配器暴露三类核心操作:
- createMeeting:POST
${baseUrl}/api/v1/meetings,以 Bearer Token 认证,返回{ id, event_link }并翻译为{ type: "tandem_video", id, url: event_link, password: "" }供会议参与者使用(lib/VideoApiAdapter.ts); - updateMeeting:PUT
${baseUrl}/api/v1/meetings/${bookingRef.meetingId},用updates键携带更新后的载荷,用于改期/编辑场景(lib/VideoApiAdapter.ts); - deleteMeeting:DELETE
${baseUrl}/api/v1/meetings/${uid},用于取消会议时释放资源(lib/VideoApiAdapter.ts)。
特别地,getAvailability直接返回空数组(lib/VideoApiAdapter.ts),注释明确说明"Tandem 不需要返回忙碌时段",即 Tandem 会议不参与空闲度计算。
在会议类型中使用 Tandem 作为地点
安装并授权 Tandem 后,即可在事件类型(Event Type)中将会议地点设置为Tandem Video。这得益于 _metadata.ts 中注册的动态链接类型:
appData: { location: { linkType: "dynamic", type: "integrations:tandem", label: "Tandem Video", }, },linkType: "dynamic"表示会议链接由系统在预约创建时动态生成(而非用户手动粘贴固定链接)。一旦会议地点选择了该类型,Booking 创建时会经由视频适配器的createMeeting创建 Tandem 会议并回填event_link;会议改期/取消时则对应触发updateMeeting/deleteMeeting,从而保证预约与虚拟办公室会议的全生命周期同步。
依赖与模块边界
Tandem 模块保持极简依赖(package.json):
- 运行时依赖仅
@calcom/lib与@calcom/prisma(workspace 内部包); - 开发依赖
@calcom/types,用于引用VideoApiAdapter、CredentialPayload、CalendarEvent等类型; - 静态资源方面,
static/目录包含图标icon.svg与 6 张产品示意图(tandem1–tandem6.jpg,由 DESCRIPTION.md 的 frontmatter 引用)。
该模块不依赖任何第三方运行时库,网络请求全部基于 Node 原生fetch,令牌刷新、会议 CRUD 与配置校验均复用@calcom/lib的handleErrorsJson、handleErrorsRaw、HttpError与getAppKeysFromSlug等通用工具,保持了与其他视频会议应用一致的结构化开发方式。如需为 cal.diy 接入其他虚拟办公室/视频会议服务,可直接以本模块为模板,替换元数据、OAuth 端点与VideoApiAdapter中的 API 调用即可。
【免费下载链接】cal.diyScheduling infrastructure for absolutely everyone.项目地址: https://gitcode.com/GitHub_Trending/ca/cal.diy
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考