Twenty Call Recorder 自托管部署完全指南:Recall.ai 凭据、服务器变量与 Webhook 全链路配置
相关服务:德国大带宽服务器租用
Call Recorder 是 Twenty 生态中面向 CRM 的会议录制应用:它能自动派机器人(bot)进入 Google Meet、Zoom、Microsoft Teams、Webex 与 GoTo Meeting,录制、转写并把音频/视频/文字稿存进 Twenty 自己的存储。本文面向 Twenty 服务器管理员(server admin),系统讲解如何将 Call Recorder 连接到录制服务 Recall.ai——包括它读取的环境变量、Webhook 的接线步骤、录制生命周期如何流转,以及常见故障的定位与修复。读完你将掌握一套可复现的自托管接线流程,并能对照源码理解签名校验、状态推进与补偿机制(reconciliation)等底层实现。
本文主体来自 Call Recorder 自托管安装指南,并结合该应用在本仓库 packages/twenty-apps/public/call-recorder 目录下的源码、测试与常量定义逐条佐证。
适用前提:你是哪种部署形态
Call Recorder 的对外连接是"单向且简单"的:应用向 Recall.ai 发起 API 请求(调度/更新/取消 bot),同时暴露一个服务端 Webhook 路由接收 Recall.ai 的回调。官方文档首先区分了两类用户:
- Twenty 自托管(self-hosted):本指南的核心场景。以下所有服务器变量都必须由你手动配置在应用注册(application registration)上。
- Twenty Cloud 用户:这些凭据"可能已经配置好"(already configured)——在动手设置前,请先与你的 workspace 确认,避免重复覆盖。
从 package.json 可以看到应用版本为 1.12.0,要求 Twenty 运行时不低于 2.37.0("twenty": ">=2.37.0"),Node 需 ^24.5.0 且使用 Yarn(>=4.0.2)。在接线前请确认你的自托管部署满足这些版本下限。
接线前必须理解的两件事
Call Recorder 通过 Recall.ai 完成"派 bot 进会 + 收回录制"。SETUP 文档明确指出,只有两样东西必须配置:
- 一个 Recall.ai API Key(可选配 Region),用于让应用能调度(schedule)、更新(update)与取消(cancel)bot;
- 一条从 Recall.ai 回到你部署的 Webhook,让应用能感知"录制已就绪"并完成摄取(ingest)。
关于转写(Transcription)的默认与例外
转写在默认情况下运行在 Recall.ai 自带的转写服务商上,因此不需要任何额外凭据。唯一的例外是:如果某个 workspace 把 CALL_RECORDER_TRANSCRIPT_PROVIDER 切换成了 Gladia,那么管理员必须在 Recall.ai 控制台为每一个在用 Region 都配置一个 Gladia API Key(路径:Recall.ai 控制台 → Transcription → Gladia),否则该 workspace 的转写会失败。
这条默认行为与源码常量一致:仓库中存在默认转写服务商常量 default-call-recorder-transcript-provider.ts 与转写服务商环境变量名常量 call-recorder-transcript-provider-env-var-name.ts,说明转写服务商是一个由 workspace 可调的应用级配置而非服务器级硬编码。
服务器变量(Server Variables)完整清单
安装应用后(Settings → Applications → Call Recorder),请在应用注册(application registration)上设置以下服务器变量:
| 服务器变量 | 必填 | 用途 |
|---|---|---|
RECALL_API_KEY | 是 | 对应 Region 的 Recall.ai API Key;用于调度、更新和取消 bot。 |
RECALL_REGION | 否 | Recall.ai API 请求所使用的区域。默认为 eu-central-1(欧洲 / 法兰克福)。 |
CALL_RECORDER_RECORDING_RETENTION_HOURS | 否 | Recall.ai 在处理完成后保留源媒体(source media)的时长。默认为 166 小时(6 天 22 小时),略低于 Recall 的 168 小时免费存储窗口。超过 168 可能产生 Recall 存储费用。Twenty 侧摄入副本不受影响。 |
RECALL_WEBHOOK_SECRET | 是 | Svix 签名密钥(whsec_…),用于校验进入的 Recall Webhook。 |
以上默认值均有源码佐证:
- 默认 Region 定义在 default-recall-region.ts:
export const DEFAULT_RECALL_REGION = 'eu-central-1'; - Region 与 Key 的变量名常量分别见 recall-region-env-var-name.ts 与 recall-api-key-env-var-name.ts。
- Webhook 签名密钥变量名常量见 recall-webhook-secret-env-var-name.ts。
- 保留时长默认值常量见 default-call-recorder-recording-retention-hours.ts。
不要混淆:哪些是"应用变量"而不是"服务器变量"
Bot 行为设置(显示名称、录制提示语、提前入会时间、等候室与离会超时)、转写服务商(
CALL_RECORDER_TRANSCRIPT_PROVIDER)以及摘要设置(CALL_RECORDER_SUMMARY_ENABLED、CALL_RECORDER_ADDITIONAL_SUMMARY_PROMPT)都是应用变量(application variables),由 workspace 管理员在应用内部调整——不是服务器变量,不要在服务器层配置它们。
这两类配置在源码中是硬性区分的。以 universal-identifiers.ts 为索引可以看到:bot 名称、等候室超时(CALL_RECORDER_WAITING_ROOM_TIMEOUT_SECONDS_APP_VARIABLE_…)、无人入会超时(CALL_RECORDER_NOONE_JOINED_TIMEOUT_SECONDS_APP_VARIABLE_…)、全员离会超时(CALL_RECORDER_EVERYONE_LEFT_TIMEOUT_SECONDS_APP_VARIABLE_…)、摘要开关、额外摘要提示词、录制提示语等,全部属于 APP_VARIABLE 命名空间,对应着 workspace 级可调参数;而 Webhook 密钥、Region、API Key 走的是"环境变量 + 应用注册"路径,在 Webhook 处理器中通过 getApplicationVariableValue(...) 读取(见下文 Webhook 源码分析)。
配置 Recall Webhook:URL、事件订阅与四步接线
应用暴露一个服务端 Webhook 路由(server webhook route)。该路由负责三件事:
- 校验 Recall/Svix 签名;
- 推进匹配的录制生命周期状态:
JOINING→RECORDING→PROCESSING,或FAILED; - 录制一旦完成,摄入(ingest)音频、视频与文字稿。
它永远不会把状态往回退(never moves a status backward),因此乱序或重复投递(out-of-order / duplicate deliveries)都是安全的;而一旦签名校验失败,它会返回非 2xx,让 Recall 侧自动重试。
在你的部署上使用下面这个 URL,只替换其中的主机名(host):
https://<your-twenty-host>/webhooks/server/9215afe6-1497-4149-a49d-e608e239bbaf
URL 中这段 ID(9215afe6-…)就是 Recall Webhook 逻辑函数(logic function) 的通用标识符——它并不是随机路径,而是应用内逻辑函数注册出来的稳定路由。在源码 universal-identifiers.ts 中可确认:
export const RECALL_WEBHOOK_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER =
'9215afe6-1497-4149-a49d-e608e239bbaf';
该逻辑函数的实际定义位于 recall-webhook.ts,其处理器 recallWebhookRouteHandler 的实现逻辑与 SETUP 文档的描述一一对应:
// A thrown error becomes a non-2xx, which makes Svix retry; a returned result dispatches to the target.
export const recallWebhookRouteHandler = (
routePayload: RoutePayload<RecallWebhookBody>,
): RecallWebhookResolverResult => {
const webhookSecret = getApplicationVariableValue(
RECALL_WEBHOOK_SECRET_ENV_VAR_NAME,
);
if (!isNonEmptyString(webhookSecret)) {
throw new Error(
'RECALL_WEBHOOK_SECRET server variable is not set. …',
);
}
// … verifyRecallWebhookSignature({ rawBody, headers, secret }) …
if (!signatureCheck.valid) {
throw new Error(`Invalid webhook signature: ${signatureCheck.error}`);
}
// … 解析 body,从 Recall bot metadata 中提取 Twenty workspaceId …
};
值得注意的实现细节(源码 82-91 行):该逻辑函数在 serverRouteTriggerSettings 中声明了需要服务器原样转发的请求头:webhook-id、webhook-timestamp、webhook-signature、svix-id、svix-timestamp、svix-signature。签名校验依赖 rawBody(未加工的原始请求体),若服务器没有转发原始请求体,处理器会直接抛错(Raw request body was not forwarded by the server; cannot verify the webhook signature)。timeoutSeconds 为 30,说明从签名校验到状态推进必须在该窗口内完成。签名校验通过后,处理器解析出 bot metadata 中的 workspaceId,并把负载派发(dispatch)给 process-recall-webhook 逻辑函数(见 process-recall-webhook.ts,其中调用 handleRecallWebhook 完成状态推进)。
在 Recall.ai 控制台的接线步骤
- 在 Recall.ai 控制台创建一个 webhook endpoint,指向你部署的上述 Webhook URL,并订阅以下事件:bot 状态变更、录制完成/失败、转写完成/失败(对应事件名
bot.status_change、recording.done、recording.failed、transcript.done、transcript.failed)。- 其中 状态变更 驱动录制生命周期(lifecycle);
- 录制与转写事件 触发媒体与文字稿的摄入(ingestion)。
- 文档特别提醒:只订阅状态变更是不够的——如果只订阅
bot.status_change,媒体摄入将只能依赖对账补偿任务(reconciliation backstop)兜底,而不是实时触发。
- 复制该 endpoint 的签名密钥(signing secret)——它以
whsec_开头。 - 把它设置为 Call Recorder 应用注册上的
RECALL_WEBHOOK_SECRET服务器变量。 - 用同样的方式设置
RECALL_API_KEY(以及可选的RECALL_REGION)。
对应测试可在 process-recall-webhook.test.ts 中找到,可用于在改配置后快速验证预期行为。
录制生命周期(Recording Lifecycle)与状态机
每一条录制都以一条 CallRecording 记录存储,并在以下生命周期状态中流转:
SCHEDULED → JOINING → RECORDING → PROCESSING → COMPLETED,或转为 FAILED(有失败原因时附带 failure reason)。
该状态集合在源码 call-recording-status.ts 中被定义为枚举,注释说明其与核心 select 选项保持一致、并由 schema 集成测试守护:
export enum CallRecordingStatus {
SCHEDULED = 'SCHEDULED',
JOINING = 'JOINING',
RECORDING = 'RECORDING',
PROCESSING = 'PROCESSING',
COMPLETED = 'COMPLETED',
FAILED = 'FAILED',
NOT_RECORDED = 'NOT_RECORDED',
}
状态机中有三个对自托管排障至关重要的规则:
- 只有"音频 + 视频"两者都完成摄入,录制才会到达
COMPLETED。Recall 在创建 bot 时只会产出你请求的产物(mixed MP3 + MP4);如果其中某个产物处理失败,录制会被标记为FAILED。这与 SETUP 排障表中"Recording never reaches COMPLETED"的成因直接对应。 - Recall.ai 只在有限窗口内保留源媒体(默认约 7 天),以保持在免费存储窗口内。Twenty 会把视频、音频、文字稿摄入并存储在自己的存储中,因此即使 Recall 侧的媒体过期,你的副本依然可用——这也是 README 中"Your copy is yours"承诺的实现基础。
- 一个周期性的对账任务(reconciliation job)作为安全网:即使实时 Webhook 更新被遗漏,它也会定期从 Recall 拉取最新状态,让 CallRecording 与 Recall 端的 bot 保持同步。这解释了为什么"漏掉一次 webhook"通常不会造成永久性失败——几分钟内补偿任务会自动纠正。
从本目录 logic-functions 的命名即可看出该补偿体系的组件构成:reconcile-upcoming-calendar-events.ts(入会前的事件对账)、reconcile-stale-bot-state.ts(陈旧 bot 状态对账)、sweep-upcoming-calendar-events.ts(清扫)、cleanup-orphaned-recall-bots.ts 与 cancel-recall-bots-on-uninstall.ts 等,配合各自的 cron pattern 常量(如 upcoming-calendar-events-sweep-cron-pattern.ts、cleanup-orphaned-recall-bots-cron-pattern.ts),构成了一套"实时 Webhook 为主、周期补偿为底"的双通道一致性设计。
关于录制的入会前提(附带说明)
Call Recorder 只对"同步过日历且带会议链接"的事件录制:adhoc 的、从未出现在 Google/Outlook/CalDAV 日历上的临时呼叫不会被录制。bot 支持的平台为 Google Meet、Zoom、Microsoft Teams、Webex、GoTo Meeting;指向其他平台(如 ro.am、Daily、Whereby)或仅有拨入号码的事件会被忽略(不会调度 bot)。产品能力、计费与"每条会议是否录制"的开关细节见应用 README.md。
故障排查速查表(Troubleshooting)
以下是 SETUP 文档提供的完整排障表。由于它同时覆盖"配置错误"与"业务预期行为",建议在开排查单前先通读一遍:
| 症状 | 可能原因 | 修复方式 |
|---|---|---|
| 没有 bot 进入会议 | Recording 处于关闭状态、事件没有会议链接、事件不是来自已同步日历,或 RECALL_API_KEY 未设置 | 确认事件处于开启状态、即将到来、带视频链接且来自已同步日历;确认 RECALL_API_KEY 已设置 |
录制永远到不了 COMPLETED | 漏掉了 Recall Webhook,或音频/视频只产出了一个 | 对账任务会在几分钟内从 Recall 拉取最新状态;若已标记 FAILED,到 Recall 控制台检查该 bot |
| 文字稿为空,或标记为 pending/failed | Recall 的异步转写尚未完成,或该通呼叫的转写失败 | 等待对账任务摄入文字稿;若持续失败,文字稿中会留下失败标记 |
Webhook 被拒并返回 500(Invalid webhook signature,Recall 持续重试) | RECALL_WEBHOOK_SECRET 与 Recall endpoint 的签名密钥不一致 | 把 Recall webhook endpoint 的 whsec_… 密钥重新复制到 RECALL_WEBHOOK_SECRET 服务器变量 |
Webhook 被拒并返回 500(RECALL_WEBHOOK_SECRET … not set) | RECALL_WEBHOOK_SECRET 未设置 | 在应用注册上设置它 |
| bot 几乎立刻离开 | 没有人在等候室/空会超时前被放进会议,或所有人都离开了 | 如果超时设置过激进,可在应用设置中调整等候室/空会超时(见 README 中的 Customize the bot) |
| bot 进入了你本不想录制的会议 | 录制默认开启 | 将该事件的 Recording 字段设为 Off;已调度的 bot 会被取消 |
| 文字稿到达后摘要仍是空的 | 摘要被禁用,或摘要生成运行失败(例如 AI 额度耗尽) | 确认 CALL_RECORDER_SUMMARY_ENABLED 不为 false,且 workspace 有 AI 额度 |
| 摘要显示 "No summary available." | 文字稿为空或质量差到无法理解 | 无需处理;这是低质量文字稿的预期结果 |
针对表中 Webhook 报错的两行,源码层面有精确的抛错与之对应:recall-webhook.ts 在密钥缺失时抛出 'RECALL_WEBHOOK_SECRET server variable is not set. …',在签名不匹配时抛出 `Invalid webhook signature: ${signatureCheck.error}`。两个错误都会被转成非 2xx 响应,从而触发 Svix 重试——这正是"修复密钥后会自动恢复"的机制来源。
关键结论速览
- 自托管 Call Recorder 只需四步核心接线:建 Webhook endpoint → 复制
whsec_…密钥 → 设置RECALL_WEBHOOK_SECRET→ 设置RECALL_API_KEY(可选RECALL_REGION)。 - 服务器变量与应用变量是两个不同的配置面:前者由服务器管理员在应用注册上维护,后者由 workspace 管理员在应用内调整,二者不可混配。
- 录制的
COMPLETED以"音频与视频均已摄入"为前提;状态只会前进、不会回退,乱序与重复投递是安全的。 - Recall 侧媒体约 7 天后过期,但 Twenty 自存副本永久可用;周期对账任务是"漏掉 Webhook"时的最后防线。
- 排查 500 报错时,优先检查
RECALL_WEBHOOK_SECRET是否设置、是否与 Recall endpoint 的签名密钥一致——源码中的两个抛错文案可以直接作为 grep 线索。
项目地址: https://gitcode.com/GitHub_Trending/tw/twenty
项目地址: https://gitcode.com/GitHub_Trending/tw/twenty