跳至正文
App — Cal.diy 源码分析:功能版图、项目架构与预约调用链

Cal.diy 源码分析:功能版图、项目架构与预约调用链

Cal.diy 官方仓库

AI 参与说明(Agent:Codex):本文依据 Cal.diy 官方仓库、固定提交源码、官方拆分公告与部署文档辅助调研,初版曾使用项目 Anatomy 制作区间计算交互,该展示已撤下;中文正文使用 humanizer-zh 编辑。资料核对于 2026-10-15,源码基线为 54343aa685ae8f33159d2f485ec4a57bad5c574a。采用建议属于本文判断;源码核验、教学模型与真实部署的验证范围分别说明。运行记录:模型 gpt-6.1-sol,reasoning effort ultra,执行入口 Codex Desktop,提供方 openai;CLI 版本 0.162.0-alpha.17.2,不代表桌面 App 版本。

DeepWiki:calcom/cal.diy

标签整理(2026-10-16,Agent:Codex):本次仅补齐分类标签并核对元数据,未改写或重新校验原文技术结论。运行记录:模型 gpt-6.1-sol,reasoning effort ultra,执行入口 Codex Desktop,提供方 openai;CLI 版本 0.162.0-alpha.17.2(不代表桌面 App 版本)。

源码分析修订(2026-10-16,Agent:Codex):补充功能与代码入口映射、运行架构、模块依赖、数据实体及查询与提交的完整调用链;撤下原区间排版尺和 Labs 入口,保留原锚点。使用项目 Anatomy 的机制建模与教学复核方法,以同一组预约数据解释模块之间的转换,并使用 humanizer-zh 编辑正文。源码仍固定于 54343aa685ae8f33159d2f485ec4a57bad5c574a。运行记录:模型 gpt-6.1-sol,reasoning effort ultra,执行入口 Codex Desktop,提供方 openai;CLI 版本 0.162.0-alpha.17.2,不代表桌面 App 版本。

Cal.diy 是以个人预约为主线的 TypeScript monorepo:Next.js Web 与独立 NestJS API v2 提供入口,共享排期、Booking 和日历集成代码,PostgreSQL 保存配置与预订结果。 理解它的关键,是把“从规则算出可约时间”与“提交后建立一次预订”两条链分开,再看它们怎样复用可用性计算。

本文先回答三个代码问题:一个功能从哪个文件进入;请求经过哪些模块;这些模块读写哪些数据。项目拆分、许可与自托管配置放在后面。以下路径与行为均以固定提交为准,不把仍留在目录里的企业代码分支当作当前产品能力。

英文术语中文名称简要解释
Event Type预约类型一组预约规则,例如时长、Buffer、可选地点与是否需要确认
Schedule / Availability日程表 / 可用时间每周工作时间与日期覆盖,展开后才能得到某一天的绝对时间区间
Busy忙碌区间外部日历、已有 Booking 或限制占用的时间
Slot预约时段按时长、时间网格和提前量筛出的可选开始时间
Booking预订访客提交后保存的业务记录,具有状态、参与者及外部事件引用
Credential集成凭据连接日历、会议等外部服务所需的授权记录
App Store应用商店注册并接入日历、会议、支付等第三方能力的代码体系
API v2第二版接口与 Web 自身接口并列的独立 HTTP 服务

功能版图:从用户动作定位代码

Cal.diy 的个人主线包括 Event Type 配置、Availability 管理、日历连接、公开预约、Booking 管理,以及按配置触发的通知和集成。下表列出固定源码具有入口或实现的功能;本文没有把所有第三方服务逐一连接验收。

用户要做的事对应代码层核心数据与后续行为
创建、修改个人 Event TypeWeb 的 Event Types 页面;viewer/eventTypes tRPC Router写入 EventType,确定时长、间隔、Buffer、提前量、表单与 Schedule 关联
设置每周工作时间与日期例外Availability 页面;viewer/availability Router写入 Schedule 和 Availability;查询时再按时区展开
安装日历或会议集成Apps 页面;packages/app-store 各应用的授权和服务实现保存 Credential;选择冲突检查用的 SelectedCalendar 和写入用的 DestinationCalendar
分享个人预约链接apps/web/app/(booking-page-wrapper)/[user]/[type]/page.tsx;共享 Booker UI加载 Event Type,取得候选 Slots,收集访客资料
选择日期并查看 SlotsuseSchedule → Web slots tRPC → AvailableSlotsService组合 Schedule、Busy、限制和临时 Reservation,返回候选开始时间
提交一次预约POST /api/book/event → RegularBookingService重检可用性,创建 Booking、Attendee,再协调外部工作
改期、取消、确认或拒绝Booking 对应的 HTTP / tRPC 入口与服务更新 Booking 生命周期;按分支处理外部事件和通知
在自有客户端调用接口apps/api/v2 的 Controllers 与 Services提供 slots、bookings、schedules、event-types 等接口,复用共享业务实现
收取预约费用、发送邮件、发 WebhookBooking 服务中的条件分支及各集成依赖实际服务配置;源码中有路径不等于支付或投递已完成

入口依据:公开预约页面、Event Types Router、Availability Router、Web 提交接口、API v2 模块。下文展开各入口实际调用的函数。

当前产品范围与代码残留要分开读

官方已移除 Teams、Organizations、Routing Forms、Workflows、Instant Booking、AI Phone、SAML/SSO、Insights、Booking Audit、Impersonation 和 API v1。个人预约主线仍然经过一些来自上游的通用数据结构与分支;例如 Prisma 中还能找到团队字段,Slots 服务仍有聚合逻辑,部分 platform 导出则已改成占位实现。文件存在、类型存在、实际入口可用,是三个不同证据层次。官方删除清单

一个具体例子是 packages/platform/libraries/slots.ts 中的 validateRoundRobinSlotAvailability:移除相关企业模块后,这里的实现直接返回 true。不能据此说系统已经完成团队轮转校验。阅读本文时,先沿个人、单主持人的真实路径建立模型,再单独检查残留分支。platform Slots 导出

项目架构:两套服务入口,共享业务包

The personal booking flow uses the Web application’s own endpoints. API v2 is a separate entry point into shared business code.

公开页面、管理后台和 Web 接口都在 apps/web 内。个人 Event Type 查询 Slots 走 Web 的 tRPC;提交走 Web 的 /api/book/event。独立 API v2 则面向其他客户端,使用自己的认证、Controller、参数转换和服务编排。它不是个人预约页面必须逐次经过的中间层。useSchedule · Web 提交接口

flowchart TB
  browser["浏览器"] --> web["apps/web<br/>页面 / tRPC / HTTP"]
  client["API 客户端"] --> api["apps/api/v2<br/>Controllers / Services"]
  web --> features["共享预约业务<br/>features / Slots util.ts"]
  api --> platform["packages/platform<br/>共享导出与适配"]
  platform --> features
  features --> prisma["packages/prisma"]
  prisma --> pg["PostgreSQL"]
  features --> integrations["集成协调<br/>CalendarManager<br/>EventManager"]
  integrations --> external["外部日历 / 会议<br/>支付 / 通知"]

图表示依赖关系,不表示所有请求都会访问所有节点,也不表示这些模块是独立部署的微服务。packages/features 中很多代码由 Web 或 API 进程直接执行。Redis 出现在独立 API v2 的配置与队列、限流等实现中,不能因此把 Web 的每次预约都画成经 Redis 排队完成。

源码还留有团队 Event Type 的 API v2 查询分流:useSchedule 只有在 useApiV2 && isTeamEvent && enabled 时启用该分支,个人路径则启用 trpc.viewer.slots.getSchedule。这是代码现状;结合当前删除清单,不能反推团队产品能力已恢复。查询分流条件

前端为什么不只在 apps/web 里

公开预约入口位于 Next.js App Router,但入口仍复用旧的 getServerSideProps 适配层和共享 View:page.tsx → users-type-public-view → BookerWebWrapper → Booker。主要视图、日期选择、表单和 Slots 列表在 apps/web/modules/bookings/components/;packages/features/bookings/Booker 主要提供共享状态、Provider、Hooks、配置与工具。查询和提交 Hooks 也分布在 Web 业务模块中。管理页面同样常把路由壳、View 与业务代码拆开。只扫描 apps/web/app 会低估实际实现规模。

管理端 /event-types 则先检查会话,再通过 createRouterCaller 直接调用 Event Types Router 取得服务端数据;这一步不必向本机发一次 HTTP 请求。管理页

服务端也有类似组织:packages/trpc 定义 Web RPC 边界,Handler 再调用 packages/features 中的服务;apps/web/pages/api 中仍保留直接 HTTP 路由。这里同时使用 App Router 和 Pages API 路由,应从具体请求入口追踪,不能按一种框架目录约定猜出全部链路。公开页面 · Booker 主视图 · 共享 Booker 状态 · Web API 目录

代码架构:目录怎样承担职责

text
apps/
  web/                         页面、会话、Web tRPC 与 HTTP 入口
  api/v2/                      独立 API 的认证、Controller 与 Service
packages/
  features/
    bookings/                  共享状态与 Hooks、创建/改期/取消编排
    availability/              用户可用性计算
    schedules/                 工作时间展开、区间运算、Slots 生成
    busyTimes/                 日历与已有 Booking 的占用整理
  trpc/                        Web Router、输入 Schema 与 Handler
  prisma/                      schema、数据库访问与迁移
  app-store/                   应用注册、授权及具体集成
  platform/                    API 可复用的业务导出和适配
  lib/                         跨功能工具与公共实现
  ui/                          共享界面组件

根 package.json 通过 Yarn Workspaces 纳入应用、features、App Store 等包,再由 Turborepo 组织任务。这是部署入口分开、业务代码大量共享的 monorepo。根 package.json

模块名不等于严格的分层边界。features/bookings 同时包含前端共享状态、Hooks、部分组件和服务端 Booking 编排;schedules 包含更接近算法的函数;trpc 中的 Slots util.ts 又承担了较多业务组合。判断一个文件属于哪层,应看它是否依赖请求上下文、数据库、外部服务或 React,而不能只看它叫 lib 还是 service。

层次具体职责更适合在这里修改的内容
页面与 UI展示、表单、选择日期、触发查询和提交Booker 布局、交互、字段呈现
协议入口解析输入、取得身份与上下文、调用服务、映射错误tRPC 输入、Web HTTP 字段、API v2 DTO / Controller
业务编排选 Schedule、读 Busy、校验限制、建立 Booking、协调副作用可预约条件、预约流程与分支
计算函数区间展开、差集、Slots 时间网格时段算法;需特别核对时区和边界
持久化与集成保存实体、读写日历、创建会议、通知与支付数据约束、新 Provider、失败恢复

RegularBookingService.ts 最能体现当前代码的集中程度:核心 Handler 约两千行,公开的 createBooking 与 rescheduleBooking 再委托给它。数据库写入、通知、外部事件和多种历史分支集中于此。它便于沿单一路径追踪业务,但修改一处时需要辨认当前请求会进入哪些条件分支,不能把文件里的所有分支都理解为固定顺序。共享 Handler 与公开方法

核心数据模型:规则、占用与结果分别保存

Prisma schema 使用 PostgreSQL。核心模型可分成四组:预约规则、外部连接、预订结果、短期 Reservation。Prisma schema

erDiagram
  Schedule o|--o{ EventType : configures
  Schedule o|--o{ Availability : contains
  EventType o|--o{ Booking : describes
  Booking o|--o{ Attendee : includes
  Booking o|--o{ BookingReference : references

这张图聚焦规则如何关联到 Booking,再关联参与者与外部事件引用;可选关系保留其可选性。用户、Credential、日历选择与 Payment 的职责见下表,团队、座位和历史兼容字段未画入。

模型保存什么查询或提交时怎样使用
EventTypelength、slotInterval、Buffer、minimumBookingNotice、日期范围、确认和支付相关配置规定候选怎样生成、提交怎样校验
Schedule / AvailabilitySchedule 时区;每周 days、工作起止时间,或指定日期覆盖展开为具体日期的绝对起止时间;不能直接当成已预订事件
Credential用户与集成的授权记录为 App Store 中对应的日历、会议等实现提供授权
SelectedCalendar哪些日历参与冲突检查Busy 读取范围;与写入目标并非同一概念
DestinationCalendar新事件应写到哪里外部 Calendar 创建或更新时的目标
Bookinguid、绝对 startTime/endTime、状态、Event Type 与关联记录本地一次预订的事实;可在外部服务失败时仍然存在
Attendee / BookingReference参与者;外部日历或会议的标识、链接与凭据关联通知参与者,后续更新或删除对应的外部事件
PaymentBooking 关联的支付记录和支付状态与 Booking 状态一起判断后续流程,不能用“有 Booking”代替“已付款”
SelectedSlots被临时选择的时间、客户端标识和 releaseAt查询时排除其他访客尚未过期的占用;与最终 Booking 分开

例如,Availability 中的“每周一 09:00–12:00”需要结合 Schedule 时区和目标日期展开;Booking 的 2026-10-19T11:30:00Z 则已经是某次事件的绝对开始时间。前者产生候选,后者会成为后续查询的 Busy 来源。Schedule 与 Availability · Booking

Booking 生命周期不等于一个布尔值

创建路径会根据是否需要确认、是否需要支付等条件,决定初始状态;正常已接受的预约使用 ACCEPTED,待处理路径可能是 PENDING。支付还有独立数据,不能仅凭一个状态字段推导所有流程已完成。创建状态分支

改期复用 RegularBookingService 的主要 Handler,并把原 Booking 上下文带入;普通非 Seats 改期中,createBooking.ts 的局部事务会取消旧记录并创建新记录,fromReschedule 保存业务关联,Seats 另有处理。取消则先把本地 Booking 标为 CANCELLED,随后删除对应外部事件;外部删除失败会记录错误。由此可见,Booking 生命周期和第三方事件状态需要分别观察。改期与创建委托 · 局部事务 · 取消服务

待确认记录的后续操作可从 viewer/bookings/confirm.handler.ts 进入:服务核对操作者,再分别将 PENDING 确认成 ACCEPTED,或拒绝成 REJECTED;需要确认的创建分支不会立即建立外部 Calendar 事件。确认与拒绝 · 待确认创建分支

查询链:一个日期怎样变成候选 Slots

个人 Event Type 的请求从公开页面进入,经 Booker 与 useSchedule 查询 Web 的 slots tRPC。服务端再取得 Event Type、相关用户、Schedule 与 Busy,计算可用区间,并生成 Slots。

flowchart TB
  page["/[user]/[type]<br/>页面与 Booker"] --> hook["useSchedule<br/>个人 Event Type"]
  hook --> trpc["Web tRPC<br/>viewer.slots.getSchedule"]
  trpc --> service["AvailableSlotsService<br/>取得配置并组合结果"]
  service --> availability["UserAvailabilityService<br/>工作时间、Busy、限制"]
  availability --> ranges["扣除占用后的 dateRanges"]
  ranges --> slots["getSlots<br/>时长、频率、提前量、对齐"]
  slots --> reserved["过滤他人 SelectedSlots<br/>及其他条件"]
  reserved --> result["返回候选开始时间"]

精确文件链以入口、Router、Handler、Service 和计算函数为索引,避免从大目录盲读:

顺序文件或符号要观察的输入 / 输出
1apps/web/app/(booking-page-wrapper)/[user]/[type]/page.tsx路由参数与页面初始化数据
2apps/web/modules/schedules/hooks/useSchedule.ts日期范围、Event Type、时区与是否启用查询
3apps/web/pages/api/trpc/slots/[trpc].ts → slots _router.tsx → getSchedule.handler.ts挂载 Router,校验输入,取得服务实例
4packages/trpc/server/routers/viewer/slots/util.tsAvailableSlotsService 组合 Event Type、用户和时段
5packages/features/availability/lib/getUserAvailability.tsSchedule 与 Busy → dateRanges,还返回时区和相关元数据
6packages/features/schedules/lib/slots.tsdateRanges 与时长、频率、提前量 → Slots
7Slots util.ts 的 Reservation 过滤去掉其他访客有效占用,组织返回结果

来源:useSchedule · Web tRPC 挂载 · Slots Router · getSchedule Handler · AvailableSlotsService · Slots 生成函数。

第一步:把时间规则展开为绝对区间

buildDateRanges 及其工作时间处理读取每周规则、日期覆盖与时区等条件,产生目标日期内的起止时间。这个过程与“扣除 Busy”分开。夏令时、旅行时区与覆盖规则会影响这一步,不能把字符串 09:00 直接当成任意日期的 UTC 09:00。工作时间展开

第二步:读取 Busy,并把 Buffer 计入冲突区间

getBusyTimes 读取本地主持或参加的 Booking,并通过所选日历和凭据取得外部 Calendar Busy。已有 Booking 还带有它自身 Event Type 的 Buffer;改期时会排除对应原预订,带座位的情况也有不同处理。Busy 实现

这里有一个容易看反的细节:对于外部 Calendar Busy,系统向前扩展的是待预约 Event Type 的 afterEventBuffer,向后扩展的是 beforeEventBuffer。原因是待约活动若安排在 Busy 前,它的结束后 Buffer 不能碰到 Busy;若安排在 Busy 后,它的开始前 Buffer 不能碰到 Busy。已有本地 Booking 还会叠加该 Booking 自己前后的 Buffer。Buffer 计算

第三步:用 subtract 得到剩余 Availability

UserAvailabilityService 整理日历 Busy、Booking Busy 和限制后,调用 subtract(dateRanges, formattedBusyTimes)。subtract 按 Busy 开始时间排序,在每段可用区间中推进游标:跳过已结束占用,输出占用前的空隙,再跳过占用覆盖;最后输出尾段。它输出区间,并保留源区间附带的其他字段。可用性汇总 · subtract

第四步:把区间离散成 Slots

getSlots 还要处理完整预约时长、slotInterval 对应的频率、minimumBookingNotice、时区中的网格对齐、可选偏移和 Out of Office 元数据。它读取当前时间与环境变量,因此不是只由两组区间决定的纯差集函数。Slots 服务还会继续过滤临时 Reservation。调用参数 · getSlots · Reservation 过滤

贯穿示例:同一个请求在各模块中的数据

本节解剖的是完整查询链中几个关键转换:工作时间 → 扩展后的 Busy → 剩余区间 → 候选开始时间 → 排除 Reservation。固定为单主持人、UTC 日期 2026-10-19,当前时间固定在 08:00;外部日历成功返回,没有本地已有 Booking、Out of Office、偏移或优化 Slots 分支。

json
{
  "schedule": ["09:00", "12:00"],
  "calendarBusy": [["10:00", "10:30"]],
  "length": 30,
  "slotInterval": 30,
  "beforeEventBuffer": 10,
  "afterEventBuffer": 5,
  "minimumBookingNotice": 60,
  "otherReservation": ["11:00", "11:30"]
}

这是为了阅读而压缩的教学输入,不是某个 HTTP 接口的完整请求体。以下时间均在同一 UTC 日期;区间写成 [开始,结束),用于说明占用边界。

阶段传出的具体数据为什么得到这个结果
buildDateRanges[09:00,12:00)本例时区固定为 UTC,没有日期覆盖
getBusyTimes[09:55,10:40)Calendar Busy 10:00–10:30,前减 after 5 分钟、后加 before 10 分钟
subtract[09:00,09:55)、[10:40,12:00)从允许工作时间中扣除扩展后的占用
提前量下界09:00固定 now 08:00 + 60 分钟
getSlots09:00、11:00、11:3009:30 放不下完整 30 分钟;第二段起点 10:40 对齐到 11:00
Reservation 过滤09:00、11:30别人的有效 11:00–11:30 占用排除了 11:00

这组数据也说明 length 与 slotInterval 的分工:前者决定一次活动要装下多少分钟,后者影响开始时间的频率与对齐。空闲总分钟数无法直接除以时长得到最终可约次数。

可以用两次输入变化检查是否读懂了规则:

  • 把 afterEventBuffer 从 5 改成 0:Busy 左界变为 10:00,09:30–10:00 恰能容纳,候选增加 09:30。变化来自 Buffer 层,随后通过差集影响 Slots。
  • 把 length 从 30 改成 45,slotInterval 保持 30:11:30 放不下完整活动;11:00 又被上述 Reservation 排除,因此最终只剩 09:00。变化来自完整时长检查,时间网格仍按 30 分钟。

本例按固定源码的分钟级输入说明主要规则。原 getSlots 的结束判断包含减去 1 秒的实现细节,不能将本例概括为适用于任意秒级输入的数学契约;时区、DST、座位、多人聚合及外部故障也不在这组输入中。

提交链:选中的 Slot 怎样成为 Booking

访客提交 11:30–12:00 时,前一次查询结果不会直接成为数据库锁。Web 接口把请求交给 RegularBookingService,服务重新检查,再持久化并处理后续工作。

flowchart TB
  post["POST /api/book/event<br/>解析请求"] --> regular["RegularBookingService<br/>创建或改期的共享 Handler"]
  regular --> inputs["检查时间、时长与相关限制"]
  inputs --> available["ensureAvailableUsers<br/>以 booking 模式重读 Availability"]
  available --> decision{"整个请求区间仍可用?"}
  decision -->|否| reject["拒绝当前请求"]
  decision -->|是| create["createBooking<br/>本地数据库写入"]
  create --> event["按条件创建或更新<br/>外部 Calendar / 会议"]
  event --> later["按分支处理通知、Payment、Webhook"]

这张图展示本文选择的正常个人创建路径。支付、待确认、重复活动、座位与改期都会改变分支,不能把后半段理解为每次都执行、且必须全部成功的一条固定流水线。

阶段代码位置实际作用
解析 HTTP 请求apps/web/pages/api/book/event.ts调用 Booking 服务,映射响应或异常
取得业务上下文RegularBookingService 共享 Handler加载 Event Type、用户、凭据与可能的改期信息
检查输入与限额同文件约 764–821 行校验开始结束、活动时长和 Booking / duration limits
重检可用性约 933–946 行 → ensureAvailableUsers重新取得 Availability;正常路径不能仅信任先前候选
本地建立记录约 1705–1733 行 → createBooking建立 Booking、参与者等关联;改期事务处理旧记录
外部事件约 2050 行起的对应分支通过 EventManager 创建 Calendar / 会议事件
通知与其他后续工作同文件后续条件分支处理邮件、支付请求与 Webhook 等

来源:HTTP 入口 · 提交检查 · 完整区间重检 · 数据库写入 · 外部事件与通知 · 支付 · Webhook。

重检检查的是完整区间,不是重新展示候选列表

ensureAvailableUsers 在 booking 读取模式下重新取得可用性,显式保留日历 Busy 检查,并要求整个请求区间落入允许范围,再检查冲突。RegularBookingService 还设置 Booking fetch mode 绕过日历缓存。这里复用了可用性业务;它没有把前端显示过的 getSlots 结果当成提交许可。读取模式 · ensureAvailableUsers

例如,在显示 11:30 后,外部日历新增 11:40–12:10 事件,提交时读到的 Availability 就可能无法容纳原来的完整请求。反过来,仅改变页面排列或把 11:30 从界面藏起来,也不能代替服务端拒绝非法提交。

Reservation、幂等键与事务分别解决什么

SelectedSlots 带有 releaseAt,查询时会清除过期项并排除其他访客的有效占用。它的唯一键包含客户端标识,预约前的 reserve 路径也有检查再写入步骤;这不构成“全局时间范围原子互斥”的证明。Reservation 读取与过滤 · reserve Handler · 模型

Booking 还包含可选且唯一的 idempotencyKey,字段用于重复请求识别,但普通 Web 创建路径并未据此提供完整幂等保证;不能由字段存在推导重复提交一定只创建一条记录。这个键也不约束两个不同请求的时间区间重叠。普通非 Seats 改期时的数据库事务仅覆盖旧 Booking 更新和新 Booking 创建,也没有把外部 Calendar、邮件或支付一起纳入事务。Booking 模型 · createBooking 事务

日历读取失败与外部写入失败要分别看

读取 Busy 时,源码根据 silentlyHandleCalendarFailures 决定抛出异常还是记录并继续;UserAvailabilityService 的异常处理又会影响返回结果。因此不能把所有集成失败概括为一律拒绝,或一律忽略。Busy 异常分支 · 可用性异常处理

写入侧则可能已经有本地 Booking,再创建外部事件或发送通知。数据库有记录、外部日历有事件、邮件已送达,是三项不同结果。需要验证某个集成时,应分别观察这些结果及对应错误路径;本文没有据源码中存在重检或事务,就宣称并发竞态、重试和外部副作用已经得到端到端保证。

集成架构:如何接入一个日历或会议服务

集成可以沿三个层次阅读:App Store 组织应用和授权;CalendarManager 统一日历读写;EventManager 协调一次 Booking 对应的日历与会议工作。

Credential 保存用户授权,SelectedCalendar 决定读取哪些日历来检查冲突,DestinationCalendar 决定新事件写到哪里,BookingReference 再保存创建结果的外部标识。这条数据链解释了为什么“登录 Google”“连接 Google Calendar”“把 Booking 写入 Google Calendar”需要分开配置和核对。

新增 Provider 时,先看现有同类 App 的元数据、授权回调与服务实现如何接入管理层,再看 Busy 读取和 EventManager 写入怎样使用它。只新增 App 图标和安装页面,无法完成时间冲突读取、事件写入和取消改期。App Store · CalendarManager · EventManager

任务调度也要分清实现:packages/features/tasker 的通用后台任务默认使用 InternalTasker 与数据库任务记录,RedisTasker 仍标为未实现;普通预约邮件有直接调用 emailsAndSmsHandler.send 的路径,另有受配置与功能开关控制的同步 / Trigger.dev tasker。它们不能合并画成一套 Redis 消息队列。通用 Tasker · 普通通知调用 · 另一套 Tasker 策略

API v2:复用业务,而不是复制整个 Web 流程

API v2 的 Controller 负责路径、认证及输入输出;Service 通过共享包和 platform 导出接入排期与 Booking 实现。阅读它时,先定位具体 endpoint,再追踪调用,避免从 Web Hook 反猜 API 行为。

接口范围固定源码中的入口需要注意的区别
Slots/v2/slots;slots-2024-09-04 Controller查询可用时段,另有 Reservation 相关方法
Bookings/v2/bookings;2024-08-13 Controller创建、列出、改期、取消、确认与拒绝有不同方法和认证条件
Schedules版本化的 schedules Controller管理 Schedule;不是直接创建 Booking
Event Types版本化的 event-types Controller管理预约规则;不是候选 Slots 本身

API 创建路径也能追到同一业务类:BookingsController → 版本化 BookingsService → inputService 转换 body → API 侧 RegularBookingService。最后这个类通过 extends 继承 platform-libraries/bookings 导出的共享类,再由 Nest 注入仓库、Prisma 和任务依赖;Web 则通过自己的容器装配。因此两种协议入口具有不同的认证和字段形式,同时复用核心创建逻辑。API 输入转换 · 继承与依赖注入 · 共享导出

源码中的接口版本是按资源演进的,不能用一个日期概括所有 API v2 端点。访问控制也要按具体方法查看,不能由“支持 API Key”推导所有操作要求相同。Slots Controller · Bookings Controller

源码阅读与二次开发路线

如果目标是理解代码,建议按下表完成一次纵向阅读,再横向扩展集成和异常分支。每一步都应能指认输入、输出和下一跳,而不是只记住目录名。

阅读顺序读完应能回答的问题关键入口
1. 认识实体哪些是规则,哪些是绝对时间,哪些保存外部结果?schema.prisma 中 EventType、Schedule、Booking、Credential
2. 看个人页面选日期会触发哪次请求,提交走哪个接口?公开页面、Booker、useSchedule、/api/book/event
3. 看可用性工作时间、Busy、Buffer 和限制怎样组合?getUserAvailability、getBusyTimes、date-ranges
4. 看 Slots空闲区间为什么只生成这些开始时间?Slots util.ts、schedules/lib/slots.ts
5. 看写入服务端重检什么,本地事务覆盖什么?RegularBookingService、ensureAvailableUsers、createBooking
6. 看外部结果日历、会议、通知失败后会留下什么状态?EventManager、CalendarManager、取消与通知分支
7. 看另一套入口API v2 如何转换输入并复用上述业务?对应 Controller → Service → platform / features

实际改动也应沿职责定位:改预约表单先找 Booker;改时段对齐先看 getSlots;改“能否预约”先看读取与提交两边的可用性;新增日历先追凭据、Busy 与外部事件的完整链。修改查询端而遗漏提交端,容易造成页面显示与服务端判断不一致。

以下命令仅用于检索固定源码,不会连接真实服务。前置条件是已安装 Git 与 ripgrep:

sh
git clone https://github.com/calcom/cal.diy.git
cd cal.diy
git checkout --detach 54343aa685ae8f33159d2f485ec4a57bad5c574a

rg -n 'class AvailableSlotsService|UserAvailabilityService|getSlots\(' \
  packages/trpc/server/routers/viewer/slots packages/features
rg -n 'createBooking|rescheduleBooking|ensureAvailableUsers' \
  apps/web/pages/api/book packages/features/bookings/lib
rg -n '^model (EventType|Schedule|Availability|Booking|Credential|SelectedSlots)' \
  packages/prisma/schema.prisma

预期能分别定位 Slots 组合、Booking 创建与核心模型。安装和运行则应继续按根 packageManager、锁文件及该提交的 Dockerfile 核对,不能把检索成功视为项目已启动。

项目背景、许可与采用边界

2026 年 4 月,Cal.com 将生产代码迁入私有仓库,原公共仓库 calcom/cal.com 改名为 calcom/cal.diy。它承接了原仓库历史,Stars、Forks 和 Releases 因此混有拆分前的积累;评估社区版维护应看拆分后的具体提交与处理记录。官方技术拆分公告

当前根 LICENSE 是 MIT;历史标签需分别检查,例如 v6.2.0 根 LICENSE 仍为 AGPLv3。当前 MIT 不能追溯套用所有旧版本。官方 README 推荐个人、非生产用途,这是当前维护与适用范围提示;源码许可本身不等于官方提供托管、SLA 或第三方账户。当前 LICENSE · 历史 LICENSE · README

截至本次核查,所分析 main 提交日期为 2026-09-20,最新正式 Release v6.2.0 发布于 2026-03-01,早于拆分。历史 Release 不能替代当前主线的可复现构建物;也不能仅由提交间隔推导项目已停止维护。固定提交 · Releases

自托管要核对哪些实际条件

固定 Compose 包含 PostgreSQL、Redis、Web、独立 API v2 与 Prisma Studio;个人 Web 主线不以独立 API v2 容器为必经节点。原文件还映射了 Studio 与 Redis 端口,镜像没有完整固定版本,不应直接当作可重复、适合公开部署的清单。Compose

条件对应配置与核验
数据库与迁移DATABASE_URL、DATABASE_DIRECT_URL;Compose 另用 POSTGRES_* 和 DATABASE_HOST 拼接连接串
公开地址与登录NEXT_PUBLIC_WEBAPP_URL、NEXTAUTH_URL;构建与运行地址需一致
会话与凭据NEXTAUTH_SECRET、CALENDSO_ENCRYPTION_KEY;真实密钥需安全保存并纳入恢复
日历与通知自己的 OAuth 客户端、授权范围、EMAIL_SERVER_* 与 EMAIL_FROM
Web PushNEXT_PUBLIC_VAPID_PUBLIC_KEY、VAPID_PRIVATE_KEY
独立 API v2另核对 Redis、JWT 和启动所需变量;无支付配置时的占位值不能证明支付可用

Google Calendar callback 为 /api/integrations/googlecalendar/callback,Google 登录 callback 为 /api/auth/callback/google,用途不同。需要在自己的客户端配置真实域名与授权,仓库不提供公共 OAuth 凭据。环境样例 · API v2 排障 · Google 集成文档

首次评估应固定源码或实际镜像 digest,在隔离环境完成创建 Event Type、设置 Availability、连接日历、访客预约、取消和改期,分别核对本地 Booking、外部事件与通知。再增加跨时区、并发和外部失败场景,并演练数据库及密钥恢复。本文建议从个人预约与源码学习场景评估;需要团队、组织、路由、自动工作流或托管支持时,当前产品范围并不匹配。

本次验证范围

本文固定一份源码读取,核对了 Web 与 API v2 入口、共享服务、核心 Prisma 模型和日历集成调用关系;产品删除范围、许可与部署条件另与官方资料交叉核对。此前已通过 Context7 CLI 查询 Cal.diy 部署与 Google 集成文档;本次代码分析以固定源码为主。

贯穿示例从固定提交抽取原 subtract、getSlots 及辅助函数、Calendar Busy Buffer 映射表达式和 checkForConflicts,固定 UTC 时钟与环境变量运行了基准输入及两组参数变化,结果与正文一致。主验证使用仓库指定的 Dayjs 1.11.4 及其 timezone patch,只隔离监控包装;另以现有 Dayjs 1.11.23 交叉运行,结果相同。工作时间展开后的区间和日历 Busy 为测试输入,没有执行完整 Schedule 展开、AvailableSlotsService 或 Reservation 数据库路径。

原局部 subtract 教学图曾完成 1,900 组局部对照,这不能证明完整项目或图形的讲解效果;本次已撤下该展示。当前静态正文保留每层输入、输出、计算原因与参数变化,旧 cal-diy-availability 锚点仍指向贯穿示例。

本次未启动完整 Cal.diy、连接真实 Calendar / SMTP、创建真实 Booking,也未完成支付、并发或恢复验收。文中的路径与行为分析可以帮助定位代码,不能替代针对实际部署和集成的运行验证。

博客助手

正在打开博客助手…