Cal.diy 快速入门与实战示例
Cal.diy v6.2.0 快速入门:首运行向导、首个管理员 SQL 提权、INACTIVE_ADMIN 语义、curl 全链路 NextAuth 登录、tRPC 建 API Key、API v2 会话通道实测(日程/事件类型/时段/预约 201 回显)、license 门槛结论与预约页时区验证。2026-10-19 全流程实测。
Cal.diy 快速入门与实战示例
目标:以一条真实可复现的操作链路完成 Cal.diy v6.2.0 的入门:管理员初始化、建立工作时间与事件类型、验证预约页与时区、使用 API Key 与 REST API 完成时段查询与预约创建。本文所有命令与回显均在 Docker Compose 部署(Web
http://192.168.146.128:8127、API v2:8129)实测取得,查询/实测日期 2026-10-19。
一、环境信息
- 部署形态:《Cal.diy Docker Compose 部署指南》实测产物,Web 应用 8127、API v2 8129、PostgreSQL 维护口 8128
- 软件版本:Cal.diy v6.2.0(2026-03-01 官方稳定版)
- 演示账号:
admin@example.com,密码Q1w2e3r4 - 工具:curl(HTTP 级全链路复现)、psql(数据库提权操作)
- 前置知识:Cal.diy 的角色模型中 ADMIN 激活与否由密码强度与 2FA 状态决定(详见 2.3),首日操作会频繁遇到该语义
二、管理员初始化
2.1 首次访问与初始化向导
部署完成后的第一次访问会强制进入初始化向导:
# curl -s -L -o /dev/null -w "%{url_effective} -> %{http_code}\n" http://192.168.146.128:8127/
http://192.168.146.128:8127/auth/setup?step=1 -> 200
向导分三步:设置显示名与用户名 → 设置管理员邮箱与密码 → 完成。实测密码策略要求至少 7 位且同时包含大写、小写与数字(源码 packages/lib/auth/isPasswordValid.ts):本文演示口令 Q1w2e3r4 满足策略;按常规惯例的 q1w2e3r4 会因缺少大写字母被拒绝。
注册完成后系统自动登录并进入个人工作台。此时账号仍是普通角色(role=USER),管理后台不可用,需要下一步提权。
2.2 首个管理员提权
自托管首个用户提权是标准数据库操作(官方未提供界面入口):
# docker exec cal-com-6-2-0-db psql -U cal_com -d cal_com \
-c "UPDATE users SET role='ADMIN' WHERE id=1 RETURNING id, username, role;"
id | username | role
----+----------+-------
1 | admin | ADMIN
(1 row)
UPDATE 1
提权后重新登录一次使新角色进入会话。
2.3 关于 INACTIVE_ADMIN
重新登录后,会话接口会暴露一个容易误解的状态:
# curl -s -b /tmp/cal_cookies.txt http://192.168.146.128:8127/api/auth/session
{"user":{"name":null,"email":"admin@example.com","id":1,"username":"admin",
"role":"INACTIVE_ADMIN","belongsToActiveTeam":false, ...},
"expires":"2026-11-18T12:03:42.099Z", ...}
role: "INACTIVE_ADMIN" 的含义(源码 packages/features/auth/lib/next-auth-options.ts 实查):生产模式下 ADMIN 角色要求密码超过 14 位并启用双因素认证才显示为完全激活。这不是权限故障——此时管理后台页面已可访问:
# curl -s -o /dev/null -w "GET /settings/admin/flags -> %{http_code}\n" \
-b /tmp/cal_cookies.txt http://192.168.146.128:8127/settings/admin/flags
GET /settings/admin/flags -> 200
若要消除 INACTIVE_ADMIN 提示,将密码改为超过 14 位的强口令并在设置中启用 2FA 即可;演示环境可忽略。
2.4 补全个人资料
初始化向导若跳过姓名设置,后续预约标题会出现 Nameless。本机实测用数据库补全(也可在「设置-个人资料」页面操作):
# docker exec cal-com-6-2-0-db psql -U cal_com -d cal_com \
-c "UPDATE users SET name='管理员', \"timeZone\"='Asia/Shanghai' WHERE id=1 RETURNING id, username, name, \"timeZone\";"
id | username | name | timeZone
----+----------+--------+----------------
1 | admin | 管理员 | Asia/Shanghai
(1 row)
(列名 timeZone 混合大小写,SQL 中必须带双引号;表结构命名规则详见《Cal.diy 日常运维与排障手册》。)
三、建立工作时间与事件类型
登录 Web 应用后按以下顺序配置(本节操作均可在界面完成,也可走第四节 API;两条路径数据互通):
- 工作时间:「设置 - 可用性(Availability)」按周定义可预约时段。实测为周一至周五 09:00-17:00(Asia/Shanghai)。
- 事件类型:「事件类型 - 新建」创建
技术交流,slug 为tech-talk,时长 30 分钟。创建后个人预约页地址为http://192.168.146.128:8127/admin/tech-talk。 - 时区:预约者打开预约页时按其浏览器时区展示时段;事件类型可锁定时区(锁定后所有人按固定时区计算)。
用 API 完成同样的两步见 4.4 与 4.5,结果与界面操作等价(实测数据互通)。
3.1 预约页与时区验证
# curl -s http://192.168.146.128:8127/admin/tech-talk -o /dev/null -w "%{http_code}\n"
200
# curl -s http://192.168.146.128:8127/admin/tech-talk | grep -o '<title>[^<]*</title>'
<title> 技术交流 | | Cal.com</title>
预约页 200 且标题正确。页面中时段按预约者浏览器时区换算;用不同时区访问可用 ?timezone= 参数显式指定,如 http://192.168.146.128:8127/admin/tech-talk?timezone=Europe/London,页面时段会切换为伦敦时间。
3.2 嵌入第三方站点
Cal.diy 提供官方嵌入脚本(embed.js),供把预约页 iframe 嵌入任意站点:
# curl -s -o /dev/null -w "%{http_code}\n" http://192.168.146.128:8127/embed/embed.js
200
嵌入代码从「事件类型 - Embed」界面复制,形如:
<!-- Cal inline embed code begins -->
<div style="width:100%;height:100%;overflow:scroll" id="cal-embed-sec"></div>
<script type="text/javascript">
(function (C, A, L) { /* 官方加载器,从界面复制完整片段 */ })
("https://app.cal.com/embed/embed.js", "cal-embed-sec", ...);
</script>
<!-- Cal inline embed code ends -->
注意把脚本中的官方云地址 app.cal.com/embed/embed.js 替换为自托管地址 192.168.146.128:8127/embed/embed.js,否则嵌入的是云服务而非本机。
四、API Key 与 REST API 实战
Cal.diy v6.2.0 有两套 API 形态,实战前必须分清:
- tRPC(Web 内部 API):Web 应用自用通道,路径
/api/trpc/...,认证靠 Web 登录会话 Cookie。适合运维脚本,无文档化契约。 - REST API v2:独立服务(8129),路径
/v2/...,文档化契约。认证支持 API Key(Bearer)与 Web 会话(NEXT_AUTH)两种凭据。
4.1 用 Web 会话登录(curl 全链路)
自托管 HTTP 站点用 NextAuth 凭证流登录,三步取会话 Cookie:
# 步骤 1:取 CSRF Token
# curl -s -c /tmp/cal_cookies.txt http://192.168.146.128:8127/api/auth/csrf
{"csrfToken":"c1c3706cc50ea116bee703cd9b3263735003cc16faa7c33b932e5ddf506f5726%7C..."}
# 步骤 2:提交凭证(-c/-b 同一 cookie 文件)
# curl -s -b /tmp/cal_cookies.txt -c /tmp/cal_cookies.txt -X POST \
http://192.168.146.128:8127/api/auth/callback/credentials \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "csrfToken=<步骤1的token>" \
--data-urlencode "email=admin@example.com" \
--data-urlencode "password=Q1w2e3r4" \
--data-urlencode "callbackUrl=http://192.168.146.128:8127/" \
-o /dev/null -w "%{http_code}\n"
302
# 步骤 3:验证会话
# curl -s -b /tmp/cal_cookies.txt http://192.168.146.128:8127/api/auth/session
{"user":{...,"role":"INACTIVE_ADMIN",...},"expires":"2026-11-18T12:03:42.099Z",...}
登录 302 后 cookie 文件中出现 next-auth.session-token 即成功;若 302 的 Location 指向 /api/auth/error?error=incorrect-email-password,检查邮箱与密码。
4.2 创建 API Key(tRPC 通道)
界面路径:「设置 - 安全 - API Keys」。curl 复现实测:
# curl -s -b /tmp/cal_cookies.txt -X POST \
'http://192.168.146.128:8127/api/trpc/apiKeys/create?batch=1' \
-H 'Content-Type: application/json' \
-d '{"0":{"json":{"note":"doc-quickstart","neverExpires":true}}}'
[{"result":{"data":{"json":"cal_8267888999723761c21784fcc9c1cf0c"}}}]
API Key 全量只在此刻返回一次,形如 cal_ 前缀 32 位十六进制。实测坑点:请求体里不要传 "teamId":null,zod 校验会以 teamId: Expected number, received null 拒绝;直接省略该字段。
4.3 API v2 认证通道实测(重要)
按官方语义 API Key 走 Bearer 头:
# curl -s -H "Authorization: Bearer cal_8267888999723761c21784fcc9c1cf0c" \
http://192.168.146.128:8129/v2/me
{"status":"error","path":"/v2/me","error":{"code":"UnauthorizedException",
"message":"ApiAuthStrategy - api key - Invalid or missing CALCOM_LICENSE_KEY environment variable",...}}
实测结论:Cal.diy 社区版的 API Key 通道在校验企业 license(CALCOM_LICENSE_KEY,需向 Cal.com 购买)不通过时整体 401,API Key 本身验证已经通过(错误发生在 key 校验之后的 license 检查)。改用 Web 会话通道(NEXT_AUTH)则完全可用:
# 同一份 cookie 文件直接调 API v2
# curl -s -b /tmp/cal_cookies.txt http://192.168.146.128:8129/v2/me
{"status":"success","data":{"id":1,"email":"admin@example.com","name":"管理员",
"timeFormat":12,"weekStart":"Sunday","timeZone":"Asia/Shanghai","username":"admin",...}}
HTTP 200 返回用户完整档案。下一节全部走该通道。
4.4 创建工作时间(POST /v2/schedules)
# curl -s -b /tmp/cal_cookies.txt -X POST http://192.168.146.128:8129/v2/schedules \
-H 'Content-Type: application/json' \
-d '{"name":"标准工作时间","timeZone":"Asia/Shanghai","isDefault":true,
"availability":[{"days":["Monday","Tuesday","Wednesday","Thursday","Friday"],
"startTime":"1970-01-01T01:00:00.000Z","endTime":"1970-01-01T11:00:00.000Z"}]}'
{"status":"success","data":{"id":1,"name":"标准工作时间","isManaged":false,
"workingHours":[{"days":[1,2,3,4,5],"startTime":540,"endTime":1020,"userId":1}],
"schedule":[{"id":1,...,"days":[1,2,3,4,5],"startTime":"1970-01-01T09:00:00.000Z",
"endTime":"1970-01-01T17:00:00.000Z",...}],
"timeZone":"Asia/Shanghai","isDefault":true,"isLastSchedule":true,"userId":1}}
HTTP 201。实测坑点:默认标志字段名是 isDefault(传 default:true 会收到 isDefault must be a boolean value 的 400 报错,按报错字段名修正即可)。
4.5 创建事件类型(POST /v2/event-types)
# curl -s -b /tmp/cal_cookies.txt -X POST http://192.168.146.128:8129/v2/event-types \
-H 'Content-Type: application/json' \
-d '{"title":"技术交流","slug":"tech-talk","description":"30 分钟技术交流会议","length":30}'
{"status":"success","data":{"id":1,"title":"技术交流","slug":"tech-talk",
"description":"30 分钟技术交流会议","length":30,"hidden":false,"userId":1,
"periodType":"UNLIMITED",...}}
HTTP 201。实测坑点:时长字段是 length(传 lengthInMinutes 会收到 length must be a number 400)。
4.6 查询可用时段(GET /v2/slots/available)
# curl -s -b /tmp/cal_cookies.txt \
'http://192.168.146.128:8129/v2/slots/available?eventTypeId=1&startTime=2026-10-20T00:00:00.000Z&endTime=2026-10-21T23:59:59.000Z&timeZone=Asia/Shanghai'
{"status":"success","data":{"slots":{
"2026-10-20":[{"time":"2026-10-20T09:00:00.000+08:00"},
{"time":"2026-10-20T09:30:00.000+08:00"},
{"time":"2026-10-20T10:00:00.000+08:00"},...],
"2026-10-21":[{"time":"2026-10-21T09:00:00.000+08:00"},...]}}}
HTTP 200。时段按 timeZone 参数的目标时区输出(+08:00),从工作时间的起始整点起按事件时长切片。注意:用 eventTypeSlug=tech-talk&username=admin 组合查询实测返回 404 TRPCError,用 eventTypeId 数字主键查询正常——脚本对接时优先取事件类型的数字 id。
4.7 创建预约(POST /v2/bookings)
以 2026-10-20 09:00(北京时间)= UTC 01:00 发起预约:
# curl -s -b /tmp/cal_cookies.txt -X POST http://192.168.146.128:8129/v2/bookings \
-H 'Content-Type: application/json' -H 'cal-api-version: 2024-08-13' \
-d '{"start":"2026-10-20T01:00:00.000Z","eventTypeId":1,
"attendee":{"name":"张三","timeZone":"Asia/Shanghai",
"email":"zhangsan@example.com","language":"zh-CN"},
"metadata":{}}'
{"status":"success","data":{"uid":"7Dx2ipiJWu984BmKbSNik2",
"title":"技术交流 between 管理员 and 张三","status":"accepted",
"location":"integrations:daily",...,"attendees":[{"name":"张三","email":"zhangsan@example.com",...}]}}
HTTP 201。三个实测坑点逐一说明:
- 必须带
cal-api-version: 2024-08-13头。不带该头请求会路由到旧版(2024-04-15)校验管道,报出的错误与新 schema 不对应(例如要求不存在的顶层 timeZone,甚至内部 500)。 - 新版 schema 顶层不要传
timeZone、language:报错形如timeZone property is wrong, property timeZone should not exist。时区放 attendee.timeZone,语言用 attendee.language。 - language 是受控枚举:传
zh会被拒绝并列出合法值(...zh-CN...zh-TW...),必须用zh-CN。
预约成功后验证落库:
# docker exec cal-com-6-2-0-db psql -U cal_com -d cal_com \
-c 'SELECT uid, title, status, "startTime" FROM "Booking" ORDER BY id DESC LIMIT 1;'
uid | title | status | startTime
----------------------+------------------------------------+----------+---------------------
7Dx2ipiJWu984BmKbSNik2 | 技术交流 between 管理员 and 张三 | accepted | 2026-10-20 01:00:00
(1 row)
(表名 Booking 混合大小写需双引号。)location: integrations:daily 表示默认 Cal Video 视频会议(Daily.co),预约确认链接中会带生成的会议地址。预约者收件箱会收到确认邮件(需部署配置 SMTP 后生效)。
五、实战示例:一条完整的对外预约链路
综合以上能力,自托管对外预约的推荐链路如下:
- 对外发布预约页:把
http://192.168.146.128:8127/admin/tech-talk(或自有域名形式)放入官网/邮件签名;或按 3.2 用 embed.js 嵌入站点。 - 预约者自助预约:选择时区、时段、填写表单提交,系统即时锁定时段并生成视频会议链接。
- 事件流出站集成:在「设置 - Webhook」注册接收地址,订阅
BOOKING_CREATED、BOOKING_CANCELLED、BOOKING_RESCHEDULED等触发事件;推送带X-Cal-Signature-256签名头(HMAC-SHA256,密钥为注册 webhook 时设置的 secret),接收端验签通过后驱动工单/IM 通知等下游动作。完整实测(含签名逐字节验证)见《Cal.diy 日常运维与排障手册》。 - 管理动作:改期用
POST /v2/bookings/<uid>/reschedule,取消用POST /v2/bookings/<uid>/cancel(同版本头);管理员可在工作台「预约记录」中查看全部预约。
六、常见问题
Q1:登录一直 incorrect-email-password? 确认用的是注册邮箱(向导第 2 步填的),不是用户名;密码区分大小写。
Q2:API Key 调用全部 401 Invalid or missing CALCOM_LICENSE_KEY?
社区版 license 限制,非 Key 错误。用本文 4.3 的会话通道,或采购官方 license 后在 .env 配置 CALCOM_LICENSE_KEY 重启。
Q3:/v2/bookings 返回 500 Required?
几乎总是漏了 cal-api-version: 2024-08-13 头落入旧版管道,或 body 缺 attendee 对象。先补版本头再看报错。
Q4:预约页时段比预期少或偏移 8 小时? 检查用户与事件类型的时区设置:用户资料时区(第三章 2.4)、工作时间 availability 的 timeZone、slots 查询的 timeZone 参数三者口径要一致。
Q5:create 接口报 Expected number, received null? v6.2.0 多处 zod schema 对显式 null 严格,传 null 会被拒;省略可选字段即可。
七、参考链接
- 官方 API v2 文档:
https://cal.com/docs/api-reference/v2/introduction - 官方自托管文档:
https://cal.diy - Webhook 触发事件枚举(源码):
packages/features/webhooks/lib/constants.ts - 站内同批文档:《Cal.diy 介绍与选型指南》《Cal.diy Docker Compose 部署指南》《Cal.diy 日常运维与排障手册》