开发者文档
简体中文

机器人开发

语音机器人

有人在应用中给机器人打电话时,由 AI 语音模型接听。介绍如何开启、指令与音色、通话更新、工具、MCP 服务器、自有模型密钥、文字记录、音频流模式、Apple Watch 和保存对话——面向机器人所有者的指南。

语音机器人是人们可以在应用中给它打电话的机器人。接听电话的不是机器人所有者的代码,而是 Pabal 的 AI 语音模型;机器人所有者通过 Bot API 设定模型要遵循的指令、问候语、音色、语言和限额。每次通话开始和结束时,你都会收到 voice_call 更新;通话过程中,还可以让模型调用机器人的代码(工具)来查询订单或确认预约。此外还有音频流模式:由你自己的服务器直接接收声音,而不是 AI 模型。如果你是第一次创建机器人,请先阅读创建机器人。

当前状态

语音机器人需要在每台服务器上单独开启;本文更新时(2026-10-02),pabal.me 上是关闭的——正在开发服务器上测试

只有在下列条件全部满足的服务器上,才能给语音机器人打电话。即使服务器上关闭了语音,这些方法也依然存在,所以可以读取设置。但在这台服务器开启语音、且运营方允许该机器人之前,除了关闭类操作(关闭语音、关闭文字记录、关闭音频流模式、删除自有密钥、删除 MCP 服务器)以外,任何设置都不会被保存(403 VOICE_NOT_AVAILABLE)。你的机器人现在能否接听电话,可以看 getVoiceCallSettings 的 available 和 unavailable_reason。

功能开启条件(需全部满足)pabal.me 现状
语音机器人通话服务器接入了语音网关和通话中继(TURN) · 隐私政策中已发布机器人通话一节(bot-calls) · 运营方已允许该机器人 · 正式开放前,只有运营方指定的测试者可以拨打 · 来电者的应用是包含确认窗口的版本(桌面版 pabal-bc2——在确认窗口只有韩语和英语期间也接受 pabal-bc1;Android 62151;iPhone 版本号 5 及以上;Apple Watch 应用版本号 1 及以上)——这样的版本尚未发布关闭
从 Apple Watch 拨打语音机器人通话 + 服务器的手表 API + 手表音频入口(pabal-voice-edge)+ 接受直连通话(direct)的网关 · 手表应用版本号达到 TELEGRAM_VOICE_MIN_BUILD_WATCHOS 及以上 · 使用工具、MCP 服务器、自有密钥、音频流模式或机器人所有者文字记录的机器人暂时不行关闭——正在开发服务器上测试
保存对话(来电者的副本)语音机器人通话 + 政策中的 bot-calls-keep-text 一节 + 声明了 keep_text 的网关(0.9.0)+ 带复选框的应用版本 · 确认窗口文案的法律审查关闭——尚未完成法律审查
机器人打来的电话已发布隐私政策的 bot-calls-outbound 一节和服务条款的 voice-bot-owners-outbound 一节(机器人所有者要同意条款第 4 版,还需 voice-bot-owners 一节)· 法律审查后由运营方打开的全局开关 · 机器人所有者在 @BotFather 中开启(条款第 4 版、目的)· 运营方的外呼审核 · 接听者的许可(带许可窗口的应用版本)· 带有已批准的外呼提示录音的网关 · 机器人语言为韩语或英语关闭——等待法律审查
韩语、英语以外的机器人语言(7 种)语音机器人通话 + 能用该机器人的语言通话的网关(从 0.7.0 起支持全部 9 种语言) · 运营方已阅读该机器人的指令和问候语并允许(即使在接受所有机器人的服务器上也是如此)关闭——尚未部署
韩语、英语以外的确认窗口和提示语音(8 种语言)每种语言完成法律审查和母语者确认后,才会加入应用版本和网关镜像。在此之前,使用这些语言应用的来电者看到的也是英语确认窗口,听到的也是英语提示草稿——尚未审查
机器人所有者条款(第 4 版)服务条款中已发布机器人所有者一节(voice-bot-owners)。文字记录、自有密钥、音频流模式、MCP 服务器和机器人打电话都必须先同意此条款无法同意
工具语音机器人通话 + 支持工具的网关关闭
MCP 服务器MCP 组件 · 专用出站地址(运营条件——代码不检查) · 政策中的 bot-calls-mcp 一节和条款中的机器人所有者一节 · 同意机器人所有者条款 · 能在确认窗口中显示机器人 MCP 接收方的应用版本(在此之前可以注册,但任何通话都不会带上 MCP 工具)关闭
自有模型密钥服务器的主密钥 · 应用推出显示自有密钥机器人接收方的确认窗口(第 2 版)之后由运营方开启 · 机器人所有者条款 · 能运行所有者密钥的网关(默认只支持 Google 密钥)关闭——甚至不接受密钥
文字记录运营方完成法律审查后在管理页面中允许(默认关闭) · 机器人所有者条款 · 机器人所有者的开关 · 带有文字记录提示语音的网关 · 托管通话关闭
音频流模式服务器的主密钥 · 政策中的 bot-calls-stream 一节和条款中的机器人所有者一节 · 支持音频流通话的网关 · 包含音频流确认窗口的应用版本(尚无)关闭
  • 来电者(打电话的人)必须年满 19 周岁;拨打前会看到“An AI answers this call”(此通话由 AI 接听)确认窗口,接通后会先听到 AI 提示语音(来电者看到的内容)。没有确认窗口的应用(包括 Telegram 原版应用和 Telethon 之类的客户端)上,机器人没有通话按钮。
  • 托管通话的声音会发送给 Google——Vertex AI 的 Gemini Live API,位于美国艾奥瓦州(Google Cloud us-central1)。Pabal 不保存声音。Google 在生成回答后不保留声音和文字,但被自动检查怀疑为滥用的内容可能会被记录最长 90 天(与确认窗口的说明相同)。生产环境中只有这一个引擎,不会悄悄转给其他提供商(没有自动切换)。
  • 与人与人之间的通话不同,机器人通话不是端到端加密的:Pabal 的语音网关会解开声音并交给 AI 模型。来电者的确认窗口会说明这一点。

一通电话的流程

来电者 Pabal(运营方的服务器) Pabal 之外 Pabal 应用 确认窗口 · 19岁以上 打补丁的版本 pabal-server MTProto · Bot API 准入判定 · 限额 通话记录 机器人所有者的程序 Bot API (HTTPS) 你的代码 · 你的服务器 coturn 仅中继声音 pabal-voice AI 提示语音 · 指令 不保存声音 Google Vertex AI Gemini Live us-central1(美国) ① 呼叫 ② 链路 通话 · 设置 · 工具 ③ voice_call · 工具查询 设置 · 工具的回答 · endVoiceCall 声音(仅中继) ④ 声音 ↔ AI 的回答
托管通话:上方是信令,下方是声音

图示说明

  • 三个区域:来电者的应用 → Pabal 运营方的服务器(Pabal 服务器、语音网关、中继服务器) → Pabal 之外(Google 的 AI 模型、机器人所有者的程序)。机器人所有者的代码连一个字节的声音也看不到。
  • ① 呼叫:应用先把在确认窗口中获得的同意告知服务器,然后拨打。服务器会检查来电者(同意——自行确认年满 19 周岁 · 应用版本 · 测试者 · 限额)和机器人(允许 · 设置 · 限额),然后接听,或以忙音结束。
  • ② 链路:服务器把通话交给语音网关,同时发送机器人所有者的指令、问候语、音色、语言和工具。网关总是把 Pabal 的基本规则(例如表明自己是 AI)放在最前面,指令放在其后。
  • ③ Bot API:机器人所有者的程序设定设置;通话开始和结束时接收 voice_call;模型调用工具时接收 voice_call_tool_query 并作答。③ 的蓝色箭头是服务器发给机器人的内容,下方的灰色箭头是机器人发给服务器的内容。
  • ④ 声音(下面一行):应用和网关互不知道对方的地址,只通过中继服务器(coturn)交换声音;网关把声音交给 Google 的模型并取回回答。红色方框是声音离开 Pabal 的地方。
  • 在音频流模式下,④ 中 Google 的位置换成机器人所有者的服务器(音频流模式)。

从 Apple Watch 拨打的通话(准备中)

手表应用不使用 MTProto。手表通过服务器的手表 API(HTTPS JSON)登录、获取机器人列表和确认窗口并拨打电话。服务器进行与应用来电相同的准入判定后,把只能用于这通电话的一次性票据(15 秒)交给手表,并把通话交给网关。手表拿着这张票据连接音频入口(WSS,pabal-voice-edge),入口再把声音接到负责这通电话的网关工作进程。这里既没有密钥交换(DH),也没有通话中继(coturn)。但网关仍会解开声音交给模型,所以同样不是端到端加密,声音的去向也相同。机器人所有者会收到与应用来电完全相同的 voice_call(只有 started 的时间点不同,见下文)。

开启语音通话

开启语音通话要由人在 @BotFather 中操作——同意语音机器人规则是机器人所有者的承诺,而不是令牌的承诺。其余设置在 @BotFather 和 Bot API 中都可以完成。

  1. 请运营方允许你的机器人。在仅接受已允许机器人的服务器上(默认),在运营方于管理页面的语音标签页中按机器人 ID(令牌中 : 前面的数字)允许之前,@BotFather 里没有语音通话按钮,修改设置会返回 403 VOICE_NOT_AVAILABLE。如果还没正式开放,也请运营方把你用来试拨的账号加为测试者(你的用户 ID 是给机器人发消息后 getUpdates 中的 message.from.id)。
  2. 写好指令。这是说明机器人在电话里做什么的文字。可以用下面的 setVoiceCallSettings 设定,也可以在 @BotFather 的语音通话界面用指令(지시문)按钮发送。没有指令就无法开启(音频流模式除外)。
  3. 在 @BotFather 中:/mybots → 机器人 → 语音通话(음성 통화) → 开启(켜기)。第一次会显示语音机器人规则,点同意规则并开启(규칙에 동의하고 켜기)即可开启。之后只会问“要开启吗?”(켤까요?)。
  4. 等待再次允许。在仅接受已允许机器人的服务器上,允许的是当时的指令、问候语和工具,所以在允许之后再写指令,会变成 unavailable_reason: "awaiting_review"。运营方检查后再次允许,机器人才会接听。每次修改这三者中的任何一个(音频流模式下是音频流地址)都是如此。如果机器人的语言不是韩语或英语,即使在接受所有机器人的服务器上,也要等运营方阅读指令和问候语并允许后才会接听(音色和语言)。
  5. 确认 getVoiceCallSettings 的 available 为 true,然后在包含确认窗口的应用版本中,用机器人资料页的通话按钮试着拨打。
켜기
BotFather음성 통화를 켜기 전에 음성 봇 규칙을 확인해 주세요. 1. 봇은 언제나 AI 라고 밝혀요. 사람인 척하게 할 수 없어요. 2. 실제 기관·회사·사람을 사칭할 수 없어요. 3. 카드 번호·주민등록번호·비밀번호·인증 번호를 묻게 할 수 없어요. 4. 통화 소리는 파발이 저장하지 않고, 대답을 만들기 위해 Google(Vertex AI)로 보내져요. 5. 봇과의 음성 통화는 만 19세 이상만 할 수 있어요. 6. 규칙을 어기면 운영자가 음성 통화나 봇을 멈출 수 있어요.
규칙에 동의하고 켜기
BotFather음성 통화를 켰어요. 음성 통화 · @order_helper_bot 상태: 켜짐 목소리: Kore (단단한) · 언어: 한국어 … 전화를 받으면 먼저 "AI 음성 봇과 연결되었습니다" 안내가 나가고, 그다음 봇이 인사해요.

BotFather 用韩语回复。上面的规则意思是:1. 机器人始终表明自己是 AI,不能让它假装是人;2. 不能冒充真实存在的机构、公司或个人;3. 不能让它询问银行卡号、居民登记号、密码或验证码;4. 通话声音 Pabal 不保存,而是为了生成回答发送给 Google(Vertex AI);5. 只有年满 19 周岁的人才能与机器人语音通话;6. 违反规则时,运营方可以暂停语音通话或机器人。同意后,BotFather 会告诉你语音通话已开启,并显示机器人的语音通话状态(状态:开启,音色:Kore(坚定),语言:韩语),同时提醒:接听电话后会先播放“You're now talking to an AI voice bot.”(已接通 AI 语音机器人)提示,然后机器人再打招呼。

在仅接受已允许机器人的服务器上,状态可能显示为 "켜짐 — 바뀐 지시문·인사말을 운영자가 확인할 때까지 전화를 받지 않아요"(“开启——在运营方检查修改后的指令和问候语之前不接听电话”)(见上面第 4 步)。如果机器人的语言不是韩语或英语,那么无论在哪台服务器上,在运营方允许之前都会显示 "켜짐 — 한국어·영어가 아닌 봇은 운영자가 지시문·인사말을 읽고 허용할 때까지 전화를 받지 않아요 (자동 규칙 검사가 한국어와 영어만 읽어요)"(“开启——韩语、英语以外的机器人要等运营方阅读指令和问候语并允许后才会接听电话(自动规则检查只能读懂韩语和英语)”)。

语音通话界面上有开启/关闭、指令、问候语、音色、语言、限额、用量、文字记录、自有密钥、音频流模式,以及(在开启了 MCP 的服务器上)MCP 服务器按钮。即使语音被关闭或允许被撤销,关闭和删除也始终可以进行——关闭语音、删除自有密钥、关闭音频流模式、删除 MCP 服务器。

开启时和每次通话时,机器人的名字也会接受冒充检查。使用真实机构或公司名字的机器人会因 VOICE_CONTENT_REFUSED: name: impersonation 而无法开启。如果确实是该机构的机器人,请联系服务器的客服。

设置——setVoiceCallSettings

设定机器人的语音设置。这是只修改所发送字段的部分更新:先检查全部内容,然后一次写入,或者全部拒绝。如果没有任何变化,就什么都不写。

curl -s "https://pabal.me/bot$BOT_TOKEN/setVoiceCallSettings" -H 'Content-Type: application/json' -d '{
  "instructions": "你是“订单助手”店铺的电话客服。请礼貌、简短地回答。营业时间为上午10点到晚上8点。",
  "greeting": "这里是订单助手,请问有什么可以帮您?",
  "voice": "kore",
  "language": "ko-KR",
  "max_call_duration": 180
}'
# {"ok":true,"result":true}
参数类型说明
enabledBoolean开启/关闭。开启前必须已在 @BotFather 中同意规则(否则 403 VOICE_RULES_NOT_ACCEPTED),托管模式还必须有指令。关闭随时可以——正在进行的通话会持续到结束,只是不再接听新电话
instructionsString模型要遵循的指令。UTF-8 最多 8,192 字节(中文约 2,700 字)。"" 表示删除——已开启的托管机器人不能清空。指令越长通话费用越高,而且来电者有可能套出内容,所以不要写入密码或密钥
greetingStringAI 提示之后机器人说的第一句话。最多 512 字节(中文约 170 字)。为 "" 时模型会自己简短地打招呼
voiceString音色 id(列表),默认 kore
languageString机器人说和听的语言,取 9 个 id 之一(默认 ko-KR,列表)。zh-CN 会保存为 cmn-CN;仅大小写不同的 id(如 ja-jp)会按列表中的写法保存。如果不是韩语或英语,要等运营方阅读指令和问候语并允许后才会接听电话。开头的 AI 提示语音不使用这个语言,而是使用来电者看到的确认窗口的语言
max_call_durationInteger一通电话的最长时间(秒)。不少于 60,不超过 limits.max_call_duration(运营方和服务器上限中较小的那个,默认 600)。机器人默认值 300
max_concurrent_callsInteger同时接听的通话数。不少于 1,不超过 limits.max_concurrent_calls(默认 2)。机器人默认值 2
toolsArray通话中模型可以调用的机器人工具(工具)。[] 表示全部删除
transcriptBoolean文字记录开关(文字记录)
mode, stream_url, stream_secretString音频流模式(音频流模式的设置)
model, api_key不接受——VOICE_FIELD_NOT_AVAILABLE。模型由服务器决定,密钥只能通过 setVoiceCallApiKey 设定
  • 内容规则:指令、问候语和工具说明在保存时和通话时都会被检查。让机器人说自己是人(claims_human)、让它隐瞒自己是 AI(hides_ai)、让它无视 Pabal 规则(overrides_rules)、冒充真实的机构或公司(impersonation)、自称官方机构(official_claim)、询问银行卡号、居民登记号、密码或验证码(asks_secrets)、看起来像机密密钥的文字(looks_like_secret)、不可见字符(hidden_text)都会以 VOICE_CONTENT_REFUSED: <字段>: <代码> 被拒绝。“绝对不要询问密码”这类否定句可以通过。
  • Pabal 的基本规则始终优先。每次通话时,模型按 Pabal 的规则 → 机器人所有者的指令 → Pabal 规则的结尾部分这个顺序阅读,指令会被放进标记为“机器人所有者的文字”的区块中。指令不能取代 Pabal 的规则,也不能优先于它。
  • 每小时最多可以修改 30 次(与 @BotFather 合计,只计算实际发生变化的修改)。超过时返回 429 Too Many Requests: retry after N 和 parameters.retry_after。
  • 用表单(-d name=value)发送时,tools 要以 JSON 字符串传入。
  • 只通过 MTProto 登录的机器人,用 bots.sendCustomRequest 发送同样的 JSON(MTProto 机器人)。

查看当前设置——getVoiceCallSettings

curl -s "https://pabal.me/bot$BOT_TOKEN/getVoiceCallSettings"
{
  "ok": true,
  "result": {
    "enabled": true,
    "available": true,
    "suspended": false,
    "rules_accepted": true,
    "instructions": "你是“订单助手”店铺的电话客服。…",
    "greeting": "这里是订单助手,请问有什么可以帮您?",
    "voice": "kore",
    "language": "ko-KR",
    "max_call_duration": 180,
    "max_concurrent_calls": 2,
    "limits": {"max_call_duration": 600, "max_concurrent_calls": 2, "bot_minutes_per_day": 60, "owner_minutes_per_day": 120},
    "voices": [{"id": "zephyr", "name": "Zephyr", "description": "Bright"}, …],
    "languages": ["ko-KR", "en-US", "ja-JP", "cmn-CN", "fr-FR", "es-US", "id-ID", "vi-VN", "ar-EG"],
    "testers_only": true,
    "mode": "managed",
    "has_stream_secret": false,
    "owner_terms_accepted": false,
    "stream_available": false,
    "updated_date": 1790380712,
    "tools": [],
    "tool_limits": {"max_tools": 16, "max_definition_bytes": 8192, "definition_bytes": 0, "mcp_tools": 0},
    "tool_delivery": "none",
    "transcript": false,
    "transcript_available": false,
    "transcript_delivery": "get_updates",
    "has_api_key": false,
    "own_keys_available": false,
    "mcp_available": false
  }
}
字段含义
enabled机器人所有者是否已开启
available, unavailable_reason无论谁来电,机器人现在能否接听;如果不能,原因是什么(见下表)
rules_accepted, owner_terms_accepted是否已同意语音机器人规则和当前的机器人所有者条款(第 4 版)
limits这台服务器上可以设定的最大值,以及每天的限额(分钟)
voices, languages可以选择的音色和语言
testers_only正式开放前、只有运营方指定的测试者可以拨打时为 true
tools, tool_limits, tool_delivery, tool_delivery_problem工具及其限额、工具查询现在会送到哪里(webhook·get_updates·mtproto·none),以及送不到的原因(接收工具查询)
mode, stream_url, stream_secret_fingerprint, has_stream_secret, stream_available, stream_ignores音频流模式——不显示机密值,只显示 8 位指纹
transcript, transcript_available, transcript_unavailable_reason, transcript_delivery, transcript_delivery_problem文字记录开关,以及现在是否真的会送达
has_api_key, api_key, own_keys_available自有模型密钥——不显示密钥,只显示提供商、指纹、设定日期以及能否使用
mcp_available, mcp_servers, mcp_unavailable_reasonMCP 服务器
outbound机器人打电话的状态(机器人打来的电话——设置与用量)
updated_date最后一次修改的时间(Unix 秒)
unavailable_reason含义
voice_off这台服务器上语音已关闭,或者隐私政策中还没有机器人通话一节
kill_switch运营方停止了所有语音通话
bot_voice_off机器人所有者没有开启
not_allowed运营方没有允许这个机器人
awaiting_review运营方还没有检查修改后的指令、问候语或工具(音频流模式下是音频流地址)。如果机器人的语言不是韩语或英语,即使在接受所有机器人的服务器上,也需要运营方阅读指令和问候语并允许
suspended运营方暂停了这个机器人(或其所有者账号)的语音
name_refused, text_refused机器人的名字,或已保存的指令、问候语、工具说明不符合当前的内容规则。机器人所选的语言从列表中移除、无法再使用时,也会返回 text_refused——请重新选择语言
no_relay, no_gateway服务器没有通话中继或语音网关
no_gateway_with_tools, no_own_key_gateway没有能运行这个机器人的工具或自有密钥的网关
no_gateway_with_language没有接入能用这个机器人的语言通话的网关(音色和语言)
own_key_unavailable, stream_unavailable已设定的自有密钥或音频流模式现在无法使用,因此不接听(不会改用平台密钥代接)

音色和语言

从 Gemini Live 预置的 30 种音色中选择(默认 kore)。不支持上传或克隆音色。在 @BotFather 的音色(목소리)按钮中,可以按名字和风格来选。

id风格id风格id风格
zephyr明亮puck轻快charon清楚
kore坚定fenrir兴奋leda稚嫩
orus坚定aoede清爽callirrhoe悠闲
autonoe明亮enceladus带气声iapetus清澈
umbriel悠闲algieba柔和despina柔和
erinome清澈algenib沙哑rasalgethi清楚
laomedeia轻快achernar轻柔alnilam坚定
schedar平稳gacrux成熟pulcherrima积极
achird亲切zubenelgenubi随和vindemiatrix温柔
sadachbia活泼sadaltager博学sulafat温暖

语言(language)取下面 9 种之一。这是机器人说和听的语言,机器人会用这种语言回答。在 @BotFather 中,用语音通话界面上的语言(언어)按钮选择。

id语言须知
ko-KR韩语默认
en-US英语—
ja-JP日语—
cmn-CN中文(普通话)不在 Google 官方支持的语言列表中,因此 Google 不保证质量(测试中理解和回答都很准确)。以 zh-CN 发送也会接受,并保存为 cmn-CN
fr-FR法语—
es-US西班牙语—
id-ID印度尼西亚语—
vi-VN越南语—
ar-EG阿拉伯语(标准)说现代标准阿拉伯语。代码是 Google 实时模型中唯一的阿拉伯语代码 ar-EG,但并不限定方言——只有指令要求某种方言时,才会用那种方言说话
  • 韩语、英语以外的机器人要等运营方阅读指令和问候语并允许后才会接听电话——即使在接受所有机器人的服务器上也是如此。这是因为保存时的自动规则检查(假装是人、冒充等)只能读懂韩语和英语。允许之前是 unavailable_reason: "awaiting_review";允许之后如果修改指令、问候语或工具,会再次等待允许。@BotFather 会在选择语言的界面上以及选择之后提示 "한국어·영어가 아닌 언어는 자동 규칙 검사가 읽지 못해서, 운영자가 지시문·인사말을 읽고 허용한 뒤에 전화를 받아요."(“自动规则检查读不懂韩语、英语以外的语言,所以要等运营方阅读指令和问候语并允许后才会接听电话。”)。在运营方管理页面的语音标签页中,该机器人会显示为 "한국어·영어가 아닌 봇 (자동 규칙 검사가 읽지 못함): 지시문·인사말을 읽어 주세요"(“韩语、英语以外的机器人(自动规则检查读不懂):请阅读指令和问候语”)。
  • 必须有能用机器人的语言通话的网关,机器人才会接听。网关接入服务器时会声明自己能用哪些机器人语言通话(从 0.7.0 起支持全部 9 种),没有声明语言的网关被视为只支持韩语和英语。如果没有能用机器人的语言通话的网关,就会返回 no_gateway_with_language,通话按钮也会隐藏。不会改用其他语言代为通话。
  • id 不区分大小写(ja-jp 也会接受),并按列表中的写法保存。不在列表中的值会被拒绝,返回列出可用 id 的 VOICE_LANGUAGE_INVALID: language is one of ko-KR, en-US, ja-JP, cmn-CN, fr-FR, es-US, id-ID, vi-VN, ar-EG。
  • 如果所选语言后来从列表中移除,该机器人会保留设置,但不接听电话(text_refused)。不会改用其他语言代为接听,请在 @BotFather 的语言(언어)中重新选择。
  • 目前还不能选择高棉语作为机器人语言。模型虽然能听懂高棉语,但它自由作答的内容还没有经过高棉语母语者的确认。

如果来电者一直用另一种语言说话,模型会用那种语言回答。AI 提示语音不使用机器人的 language,而是使用来电者看到的确认窗口的语言(来电者看到的内容)。

接收通话更新——voice_call

有人给机器人打电话时,会收到 voice_call 更新。三种语音更新(voice_call、voice_call_tool_query、voice_call_transcript)只有写出名称请求时才会送达:请把它们放进 getUpdates 或 setWebhook 的 allowed_updates。列表为空时也不会送达(其他类型在列表为空时全部送达)。

curl -s "https://pabal.me/bot$BOT_TOKEN/getUpdates" -H 'Content-Type: application/json' \
  -d '{"allowed_updates": ["message", "voice_call", "voice_call_tool_query"], "timeout": 30}'
{
  "update_id": 7,
  "voice_call": {
    "id": "4611686018427387905",
    "direction": "incoming",
    "event": "started",
    "from": {"id": 100002, "is_bot": false, "first_name": "敏智", "language_code": "ko"},
    "date": 1790380803,
    "max_duration": 180
  }
}
{
  "update_id": 9,
  "voice_call": {
    "id": "4611686018427387905",
    "direction": "incoming",
    "event": "ended",
    "from": {"id": 100002, "is_bot": false, "first_name": "敏智", "language_code": "ko"},
    "date": 1790380952,
    "start_date": 1790380803,
    "duration": 149,
    "max_duration": 180,
    "end_reason": "caller"
  }
}
  • direction:打给机器人的电话为 "incoming",机器人打出的电话为 "outgoing"(机器人打来的电话的更新)。像以前一样把缺少此字段的更新当作打给机器人的电话,依然正确。
  • allowed_updates 每个机器人只有一份,保留最后一次由 getUpdates 或 setWebhook 传入的值(不传则保持原值)。所以像上例那样传入列表后,没写进列表的默认类型(edited_message、callback_query)就不会再送达——请把机器人用到的类型全部写上。请求之前发生的通话的更新不会被保留,之后再请求也收不到。
  • 来电者(from)只包含 ID、名字和应用语言,不包含用户名和手机号码(确认窗口上就是这样承诺的)。已删除的账号显示为 first_name: "Deleted Account"。
  • 使用 Webhook 时,这些更新会和其他更新一样按顺序以 POST 送达。getUpdates 的队列在内存中为每个机器人保留最近 1,000 条,服务器重启后会丢失。
  • 不会向已暂停或已删除的机器人发送任何内容。
event何时附加字段
started来电者的应用确认了通话(密钥交换完成),且服务器已告知网关通话已接通时。服务器看不到声音,所以这是服务器所知的“已接通”。Apple Watch 的通话没有密钥交换,所以是网关告知手表的声音已接通时max_duration
ended已接通的通话结束时。如果要等待文字记录,最多会晚 10 秒送达。在开启了保存对话的服务器上,所有非音频流模式的通话无论来电者如何选择,都最多晚 10 秒送达——这样机器人所有者就无法知道来电者的选择start_date、duration(按服务器时钟计算的接通秒数)、max_duration、end_reason、(开启了文字记录时)transcript
missed接通之前就结束的通话(响铃时挂断等)end_reason
refused仅当来电者通过了所有检查,却因机器人一方的原因(同时通话数、每天的时长、网关)无法接听时(其他拒绝不会通知机器人所有者)refused_reason:busy(同时通话已满) · budget(机器人、所有者或服务器的每日时长已用完) · unavailable(可接听的网关已满)

end_reason:caller(来电者挂断) · bot(机器人挂断,endVoiceCall) · max_length(达到最长时间) · failed(连接失败) · operator(运营方或紧急停止开关) · suspended(机器人被暂停或删除)。

每一通交给网关的通话,都会在来电者与机器人的对话中留下通话记录(通话时长或“已取消的通话”;接听前就被拒绝的通话不会留下)。机器人一方的副本不会以 message 更新送达——机器人的记录是 voice_call。来电者选择保存对话时,对话文字只留在来电者的聊天中通话记录的正下方,机器人一方什么也不会留下。在开启了保存对话的服务器上,通话记录之后的 30 个消息编号会预先占用,所以通话记录之后那条消息的编号比记录大 31 以上(未保存对话的通话也一样)。

通话中

挂断——endVoiceCall

curl -s "https://pabal.me/bot$BOT_TOKEN/endVoiceCall" -d voice_call_id=4611686018427387905
  • farewell(Boolean,默认 true):如果通话已接通,模型会简短道别后挂断。10 秒内没有挂断的话,由服务器结束通话。为 false 时立即挂断。音频流模式的通话总是立即挂断。
  • 这样结束的通话,其 voice_call.end_reason 为 bot。如果不是这个机器人正在进行的通话,返回 400 VOICE_CALL_NOT_FOUND。

给模型留备注——sendVoiceCallContext

在通话中告诉模型它应该知道的事情。模型会把它当作机器人所有者的文字而不是来电者的话来阅读,Pabal 的规则依然优先。

curl -s "https://pabal.me/bot$BOT_TOKEN/sendVoiceCallContext" -d voice_call_id=4611686018427387905 \
  --data-urlencode "text=这位来电者刚刚取消了 A-1042 订单。如果对方问起,请这样告诉对方。" -d respond=false
  • text:最多 2,000 个字符(6,144 字节),会按内容规则检查。respond=true 时模型立即回应;默认为 false 时在下一轮使用。
  • 备注同样会发送给模型(托管通话时是 Google),模型也可能把它说给来电者听——只写可以让来电者知道的内容。
  • 接通前返回 400 VOICE_CALL_NOT_ACTIVE;音频流通话或不接受备注的网关返回 400 VOICE_CONTEXT_NOT_SUPPORTED;每通电话每分钟超过 10 条返回 429(retry_after 60)。

用量——getVoiceCallUsage

curl -s "https://pabal.me/bot$BOT_TOKEN/getVoiceCallUsage?period=today"
{"ok": true, "result": {"period": "today", "from_date": 1790348400, "to_date": 1790434799,
 "calls": 3, "seconds": 412, "missed": 1,
 "refused": {"busy": 0, "budget": 0, "unavailable": 0},
 "platform_seconds": 412, "own_key_seconds": 0,
 "remaining_seconds_today": {"bot": 3188, "owner": 6788}}}
  • period:today(默认)或 month。为 month 时可以像 month=2026-09 这样选择月份(未来的月份返回 VOICE_USAGE_PERIOD_INVALID)。
  • 天和月的分界以服务器的时区为准(默认 Asia/Seoul)。remaining_seconds_today 是每日限额减去已结束的通话和正在进行的通话所预留的时长后的值。
  • 每通电话的用量记录(不记录来电者)在当月结束后保留 180 天,每月的合计只要机器人还在就一直保留。

工具——在通话中调用机器人的代码

当来电者问“订单 A-1042 到哪儿了?”时,模型会调用机器人的 check_order 工具,服务器把这个查询交给机器人,模型再把机器人的回答用话语转达给来电者。工具由机器人所有者定义,由机器人的代码回答。

模型 · 语音网关 Pabal 服务器 机器人的代码 ① check_order {"order_id": "A-1042"} 检查参数 · 删除未声明的键 卡号·身份证号在此拦下 ② voice_call_tool_query · budget_ms 无声: "One moment, let me check." 5秒: "Just a moment longer, please." ③ answerVoiceCallToolQuery(result) 检查回答 · 拒绝机密和卡号 最多 16,384 字节 ④ tool_result(仅作为数据) 模型用话语转达结果 超时: "The tool did not answer in time."
一次工具查询:模型 → 服务器 → 机器人 → 服务器 → 模型

图示说明

  • 三条泳道:左边是模型和语音网关,中间是 Pabal 服务器,右边是机器人所有者的代码。竖着的虚线表示时间。模型不直接与机器人对话,所有查询和回答都经过服务器。
  • ① 处的灰色方框——服务器先检查参数:是否符合工具的 parameters、大小和深度、未声明的键(删除后再转交)、看起来像银行卡号或居民登记号的值(不会送到机器人,模型会听到拒绝)。
  • 机器人在 ② 收到的是查询 id、通话 id、来电者、工具名、参数(JSON 字符串),以及 budget_ms——距离回答必须送达服务器还剩的毫秒数。等待期间,网关会填补沉默(左边的方框)。
  • ③ 的回答也会被检查:看起来像机密的内容、会让模型违反规则的话、银行卡号或居民登记号都会被拒绝,模型会听到错误。第一次回答就是最终回答,所以即使回答被拒绝,查询也会关闭。
  • ④ 的蓝色实线是正常情况,红色虚线是超过期限的情况。模型只把工具的结果当作数据接收,不会当作指示;失败时会听到 Pabal 的一句简短说明,并向来电者解释情况。

定义工具

curl -s "https://pabal.me/bot$BOT_TOKEN/setVoiceCallSettings" -H 'Content-Type: application/json' -d '{
  "instructions": "你是“订单助手”店铺的电话客服。请礼貌、简短地回答。来电者说出订单号时,请用 check_order 工具查询配送状态并告诉对方。",
  "tools": [{
    "name": "check_order",
    "description": "按订单号查询配送状态。来电者说出订单号时调用。",
    "parameters": {"type": "object",
                   "properties": {"order_id": {"type": "string", "description": "订单号,例如 A-1042"}},
                   "required": ["order_id"]},
    "timeout_ms": 5000
  }]
}'
项目规则
键只有 name、description、parameters(可选)、timeout_ms(可选)这四个
数量和大小机器人的工具加上已固定的 MCP 工具共 16 个,定义(name·description·parameters)的 UTF-8 总和 8,192 字节。自有密钥机器人也一样
name[A-Za-z_][A-Za-z0-9_-]{0,63}。不能包含 __(用于 MCP 工具名),不能以 pabal 开头,忽略大小写后不能重复
description必填,最多 1,024 字节,按内容规则检查。模型根据它决定何时调用,所以要写具体
parameters最外层是 {"type": "object"}。省略表示“没有参数”
timeout_ms1,000~15,000,默认 5,000。模型等待回答的最长时间

parameters 只使用两种引擎都支持的 JSON Schema 的一部分。

  • 关键字只有 type、description、enum、properties、required、items、minimum、maximum、minItems、maxItems 这十个。type 只能是一个(string·number·integer·boolean·array·object)。
  • properties·required 用于 object,items·minItems·maxItems 用于 array(items 必填),minimum·maximum 用于数字,enum 只用于 string(1~32 个,每个 64 字节)。
  • 属性名为 [A-Za-z_][A-Za-z0-9_]{0,63},一个工具最多 24 个属性,object 最多 3 层、object 和 array 合计最多 5 层。参数说明最多 512 字节。
  • 询问机密信息的参数会被拒绝:名字中含有 pin·cvv·password·otp·ssn·rrn·cardnumber 之类的词,或说明中提到银行卡号、密码、验证码、居民登记号时,返回 asks_secrets。
  • 错误会连同路径报告第一个问题:VOICE_TOOL_INVALID: tools[0].parameters.properties.tags: items_missing、VOICE_TOOL_INVALID: tools[0].name: reserved、VOICE_CONTENT_REFUSED: tools[0].description: impersonation。
  • 工具只能通过 Bot API 定义。@BotFather 的语音通话界面只显示工具名。
  • 在仅接受已允许机器人的服务器上,添加或修改工具后,在运营方再次允许之前不会接听电话(awaiting_review,开启的第 4 步)。

接收工具查询

{
  "update_id": 813,
  "voice_call_tool_query": {
    "id": "8190448532776044545",
    "voice_call_id": "4611686018427387905",
    "from": {"id": 100002, "is_bot": false, "first_name": "敏智", "language_code": "ko"},
    "name": "check_order",
    "arguments": "{\"order_id\":\"A-1042\"}",
    "budget_ms": 4700
  }
}
  • arguments 是 JSON 对象的文本(不超过 4,096 字节、深度 8,服务器已检查过)。尽管如此,它仍是模型根据来电者的话生成的值,所以请在机器人一侧再检查一遍。不能出现“只要知道订单号就能听到别人的订单”的情况——只告诉 from.id(经 Pabal 确认的来电者)自己的订单。
  • budget_ms 是从机器人收到查询起,到回答必须送达服务器为止的毫秒数。它等于工具的 timeout_ms 减去回到模型的路程(200 ms)和来到机器人的路程(100 ms),所以通常比 timeout_ms 少约 300 ms。双方的时钟都不参与计算。

查询送达机器人的途径按以下顺序决定(getVoiceCallSettings.tool_delivery)。

  1. 如果有 Webhook,就通过 Webhook。Webhook 的 allowed_updates 中必须包含 voice_call_tool_query;如果没有,查询不会改走其他途径,也就送不到(tool_delivery_problem: "not_in_allowed_updates")。查询是单独发送的 POST,不会被排队中的更新挡住,也不会重发。
  2. 如果没有 Webhook,且最近 15 秒内调用过 getUpdates,就放在下一次 getUpdates 的最前面。同样必须在 allowed_updates 中。每个机器人最多有 8 个等待中的查询。
  3. 否则,如果机器人通过 MTProto 连接,就以 updateBotWebhookJSONQuery 发送(与 allowed_updates 无关)。
  4. 以上都不符合时,查询送不到(tool_delivery_problem: "not_listening"),模型会听到“The tool could not be reached.”(无法连接到工具)。

回答——answerVoiceCallToolQuery

参数说明
tool_query_id更新中的 voice_call_tool_query.id
result结果。可以是文本;如果请求体是 JSON,也可以是对象或数组(以文本发送但能解析为 JSON 对象或数组时,也按 JSON 处理)。UTF-8 最多 16,384 字节(运营方可能设得更低)
error机器人自己的失败说明,最多 512 字节。模型会把这句话转达给来电者
  • result 和 error 必须且只能有一个(两个都没有返回 TOOL_RESULT_EMPTY,两个都有返回 TOOL_RESULT_INVALID)。返回 True。
  • 工具的回答会发送给模型(托管通话时是 Google),模型会把内容说给来电者听——只写可以让来电者知道的内容。
  • 第一次回答就是最终回答。对已过期或已结束的查询返回 400 TOOL_QUERY_EXPIRED: <原因>——answered(已回答)、timeout、cancelled(来电者插话,模型撤回了查询)、call_ended、unreachable。未知的 id 或超过 10 分钟的 id 返回 TOOL_QUERY_ID_INVALID。
  • 回答也会被检查:去掉不可见字符;看起来像机密的内容(looks_like_secret)、会让模型违反规则的话(overrides_rules·hides_ai·claims_human·asks_secrets)、银行卡号或居民登记号(sensitive_data)返回 TOOL_RESULT_REFUSED: <代码>。店铺的地址、电话号码、URL 这类机器人所有者自己的数据可以通过。超出大小时返回 TOOL_RESULT_TOO_LONG·TOOL_ERROR_TOO_LONG。
  • 使用 Webhook 时可以用响应体回答——把这个放进对工具查询 POST 的 200 响应里即可(不执行其他方法)。回答之前务必检查 X-Telegram-Bot-Api-Secret-Token 请求头。如果不检查就把客户数据放进响应,任何向该地址发送伪造查询的人都能拿到这个回答。
{"method": "answerVoiceCallToolQuery", "tool_query_id": "8190448532776044545",
 "result": {"order_id": "A-1042", "status": "配送中", "eta": "明天下午送达"}}

示例——python-telegram-bot

这是一个回答上面 check_order 工具的机器人。它用 getUpdates 接收,并把查询与其他更新同时处理(concurrent_updates)——如果因为处理前面的更新而耽搁,budget_ms 就会过去。订单按下单人的 Pabal 用户 ID 查找,只有与查询中的 from.id 一致时才告知。

# order_voice_bot.py — pip install "python-telegram-bot==22.*"
# 运行:read -rs BOT_TOKEN && export BOT_TOKEN
#       CUSTOMER_ID=<下单人的 Pabal 用户 ID> python3 order_voice_bot.py
# 测试时把 CUSTOMER_ID 设为你自己的 ID——给机器人发消息后,getUpdates 中的 message.from.id
import json
import os
import re

from telegram import Update
from telegram.error import TelegramError
from telegram.ext import Application, ContextTypes, TypeHandler

SERVER = "https://pabal.me"
CUSTOMER = int(os.environ.get("CUSTOMER_ID", "0"))
ORDERS = {                                    # 示例用的小表:实际应是店铺的订单系统
    (CUSTOMER, "A-1042"): {"status": "配送中", "eta": "明天下午送达"},
}


def check_order(arguments, caller_id):
    """直接传给 answerVoiceCallToolQuery 的 {"result": …} 或 {"error": …}"""
    order_id = str(json.loads(arguments).get("order_id", "")).strip().upper()
    if not re.fullmatch(r"[A-Z]-\d{4}", order_id):     # 参数是模型生成的——再检查一遍
        return {"error": "没有听清订单号。例如:A-1042"}
    found = ORDERS.get((caller_id, order_id))          # 只查来电者本人的订单
    if found is None:                                  # 别人的订单也用与不存在的订单相同的说法
        return {"error": f"没有找到 {order_id} 订单。"}
    return {"result": json.dumps({"order_id": order_id, **found}, ensure_ascii=False)}


async def on_update(update: Update, context: ContextTypes.DEFAULT_TYPE):
    q = update.api_kwargs.get("voice_call_tool_query")  # PTB 不认识的类型会放在 api_kwargs 里
    if q is None:
        return
    if q["name"] == "check_order":
        answer = check_order(q["arguments"], q["from"]["id"])
    else:
        answer = {"error": "不认识这个工具。"}
    try:
        await context.bot.do_api_request("answerVoiceCallToolQuery",
                                         api_kwargs={"tool_query_id": q["id"], **answer})
    except TelegramError as e:                         # TOOL_QUERY_EXPIRED:太晚了,或通话已结束
        print("回答没有被接受:", e, flush=True)


app = (Application.builder().token(os.environ["BOT_TOKEN"])
       .base_url(f"{SERVER}/bot").base_file_url(f"{SERVER}/file/bot")
       .concurrent_updates(True).build())
app.add_handler(TypeHandler(Update, on_update))
app.run_polling(allowed_updates=["message", "voice_call", "voice_call_tool_query"], poll_interval=0, timeout=10)

aiogram 会跳过它不认识的更新类型,所以需要在调度器的外层中间件(dp.update.outer_middleware)中先从 update.model_extra 取出 voice_call_tool_query。Pabal 代码仓库中有示例(scripts/bots/voice_tools_aiogram.py),需要时请向服务器运营方索取。

模型和来电者听到的内容

  • 成功:结果成为模型的函数响应(如果是 JSON,就作为 JSON 值)。结果中的文字只当作数据读取,不会成为指示。
  • 失败:模型会以通话语言听到 Pabal 的一句简短说明,并向来电者解释情况——超过期限时是“The tool did not answer in time.”(工具没有按时回答),送不到时是“The tool could not be reached.”(无法连接到工具),参数像银行卡号时是“Pabal does not pass on card numbers or national ID numbers.”(Pabal 不会传递银行卡号或身份证号之类的信息),机器人的 error 则原样转达。参数有误时,模型会收到哪里有误的提示,可以重新调用。
  • 等待期间,网关会填补沉默:安静 1.5 秒后说一次“One moment, let me check.”(请稍等,我查一下)(如果该工具最近响应慢则立即说),超过 5 秒后说一次“Just a moment longer, please.”(请再稍等一下)。这两句话使用的是机器人的语言,而不是来电者的语言;如果该语言没有审查完毕的录音,就用英语播放(目前审查完毕的只有韩语和英语)。
  • 如果来电者打断模型说话(插话),网关可能会撤回未完成的查询,之后的回答会返回 TOOL_QUERY_EXPIRED: cancelled。

工具的限额

同时未完成的查询每通电话 2 个、每个机器人 8 个。调用次数每通电话每分钟 30 次、每个机器人每分钟 120 次。超过时不会发送给机器人,模型会立即听到 too_many_open·rate_limited。

机器人所有者条款

文字记录、自有模型密钥、音频流模式、MCP 服务器和机器人打电话,必须同意机器人所有者条款(第 4 版)后才能开启。同意要由人在 @BotFather 的语音通话界面(文字记录、自有密钥、音频流模式、MCP 服务器、机器人打来的电话中任一界面均可)中完成——不能用令牌完成。即使同意过旧版本,也会再次询问。在服务器的服务条款发布机器人所有者一节之前,不会出现同意按钮(在此之前调用返回 403 VOICE_OWNER_TERMS_NOT_ACCEPTED)。

BotFather봇 주인 약관을 확인해 주세요. 대화 기록, 자기 모델 키, 원음 모드, MCP 서버, 봇이 거는 전화에 필요해요. 1. 대화 기록을 켜면, 통화가 끝날 때 AI가 적은 대화 글을 내가 받아요. 받은 글은 내가 처리하고 지켜요. 2. 대화 기록이 켜진 봇에 전화하면, 통화가 연결될 때 안내 음성이 대화 글이 봇 주인에게 간다고 먼저 알려요. 3. 받은 대화 글은 필요한 만큼만 두고, 발신자가 지워 달라고 하면 지워요. 모델 학습에 쓰지 않아요. 4. 자기 모델 키를 쓰면, 통화 소리와 글이 내 키의 제공자에게 내 계약으로 가요. 그 처리의 책임은 나에게 있고, 내 개인정보 안내에 그렇게 적어요. 5. 원음 모드에서는 전화 소리가 파발의 AI 모델이 아니라 내 서버로 가요. 그 소리를 받아 처리하는 건 나예요. 전화를 건 사람의 소리와 말한 내용을 법이 허락하는 범위에서만 쓰고, 필요 없으면 지워요. 6. 내 서버가 어디에 있는지, 무엇을 하는지 전화를 건 사람이 물으면 알려요. 봇 소개에 적어 두면 좋아요. 7. 봇은 언제나 AI 라고 밝혀요. 대답은 AI 가 만들어요. 사람이 직접 대답하거나, 사람의 녹음을 AI 대답처럼 틀지 않아요. 원음 모드에서는 파발이 통화 맨 앞에 "AI 음성 봇입니다. 통화 소리는 봇 개발자의 서버로 갑니다." 안내를 틀어요. 8. 카드 번호·주민등록번호·비밀번호·인증 번호를 묻지 않아요. 음성 봇 규칙도 그대로 지켜요. 9. 파발은 대화 글을 저장하지 않아요. 전해지면, 늦어도 24시간 뒤에는 서버에서 지워져요. 키, 원음 모드와 MCP 서버의 비밀은 암호화해 두고 어디에도 기록하지 않아요. 10. MCP 서버를 등록하면, 통화 중 모델이 그 서버의 도구를 부를 때 발신자가 말한 값이 그 서버로 가요. 그 서버와 그 서버가 넘기는 곳의 처리 책임은 나에게 있고, 받는 곳은 사실대로 신고해요. 11. 약관을 어기면 운영자가 음성 통화나 봇을 멈출 수 있어요. 봇이 거는 전화를 켜려면 아래도 지켜요. 12. 봇이 거는 전화는 선언하고 운영자가 허락한 목적(알림·깨우기·배달·예약)에만 써요. 광고·판매 권유·홍보를 하지 않아요. 배달·예약 안내 안에서도 마찬가지예요. 광고성 정보를 보내면 정보통신망법 제50조를 어긴 것이고 그 책임은 나에게 있어요. 파발은 그런 발신을 곧바로 멈추고 발신을 해지해요. 진료·채용·대출처럼 사람의 권리나 의무에 관한 판단을 이 전화로 알리지 않아요. 13. 전화를 건 봇은 첫 말에서 봇 이름과 AI 라는 것을 밝히고, 사람·기관·파발 직원을 사칭하지 않아요. 14. context 에 건강·금융(금액·계좌)·주소 같은 민감한 정보를 넣지 않고, 비밀번호·인증 번호·카드 번호를 묻거나 권하지 않아요. 15. 만 19세 미만인 사람에게 전화하려 하지 않아요. 16. 조용한 시간·쉬는 중·하루 한도를 피하려 하지 않아요(여러 봇·계정으로 나눠 걸기 포함). 사람이 그만 받겠다고 하면 다시 걸지 않아요. 17. 내 개인정보 처리방침을 두고, 통화 내용이 AI 제공자에게 간다는 것을 알려요. 봇이 거는 전화는 종단 간 암호화가 아니에요. 18. 사람에게 보이는 허락 창에 봇 주인인 내 이름이 나와요. 19. 어기면 그 봇의 발신이 멈추고, 음성 기능 전체가 멈출 수 있어요. 20. 발신으로 받는 사람 정보(파발 id·허락했는지·받았는지·통화 시각과 길이·누가 끊었는지)는 선언한 목적에만 쓰고, 남에게 주지 않고, 허락이 꺼지거나 그 사람이 지워 달라고 하면 지워요. 받은 시각으로 사람의 일과나 있는 곳을 짐작하지 않아요. 21. 허락을 서비스의 조건으로 걸거나 속여서 받지 않아요(허락 링크 메시지 포함). 22. 전화마다 적는 목적은 그 전화의 실제 목적이에요. 깨우기 창을 다른 용도로 쓰지 않아요(서버는 목적을 봇이 적은 대로 믿어요). 23. 봇 토큰을 지키고, 샌 것을 알면 곧바로 @BotFather 에서 재발급해요. 토큰이 곧 사람에게 전화할 권한이에요. 24. 봇이 건 전화에는 대화 기록(1항)이 오지 않아요. 봇에게 가는 것은 20항의 것이에요.
약관에 동의하기

BotFather 用韩语显示条款,按钮的意思是“同意条款”。要点如下:1. 开启文字记录后,通话结束时你会收到 AI 写下的对话文字,由你负责处理和保护;2. 给开启了文字记录的机器人打电话的人,会在接通时先听到提示语音,告知文字会发给机器人所有者;3. 收到的文字只保留必要的时间,来电者要求删除时予以删除,不用于模型训练;4. 使用自有模型密钥时,通话的声音和文字按你的合同发送给你的密钥提供商,这部分处理由你负责,并要写进你的隐私说明;5. 在音频流模式下,电话声音不是发给 Pabal 的 AI 模型,而是发给你的服务器,接收并处理这些声音的是你,来电者的声音和所说的内容只在法律允许的范围内使用,不再需要时删除;6. 来电者问起你的服务器在哪里、做什么时要告诉对方(最好写在机器人简介中);7. 机器人始终表明自己是 AI,回答由 AI 生成——不由真人直接回答,也不把真人录音当作 AI 回答播放;在音频流模式下,Pabal 会在通话一开始播放“This is an AI voice bot. The call's audio goes to the bot developer's server.”(这是 AI 语音机器人,通话声音会发送到机器人开发者的服务器)提示;8. 不询问银行卡号、居民登记号、密码或验证码,并遵守语音机器人规则;9. Pabal 不保存文字记录——送达后最迟 24 小时内从服务器删除,密钥以及音频流模式和 MCP 服务器的机密值加密保存,不记录在任何地方;10. 注册 MCP 服务器后,通话中模型调用该服务器的工具时,来电者所说的值会发送到该服务器,该服务器及其再转交方的处理由你负责,接收方要如实申报;11. 违反条款时,运营方可以暂停语音通话或机器人。

第 12–24 条是开启机器人打来的电话时要遵守的内容:只用于声明并经运营方许可的目的,不做广告;在第一句话中说明机器人的名称和自己是 AI;不在 context 中放入敏感信息;不试图给未满 19 岁的人打电话;不设法绕过免打扰时段、暂停和限额;接听者的信息只用于该目的,许可关闭后删除;保管好令牌。机器人打出的电话不会带来文字记录(第 24 条)。条款文字可能会在法律审查后修改。

MCP 服务器

MCP 服务器默认关闭,pabal.me 上也是关闭的

运营方需要准备好 MCP 组件(pabal-mcp、签名器、出站代理),并发布隐私政策中的 bot-calls-mcp 一节和服务条款中的机器人所有者一节,才会开启(为 MCP 请求单独设置专用出站地址是运营条件,代码不检查)。即使开启后,在应用能在确认窗口中显示机器人的 MCP 接收方之前,也只能注册,任何通话都不会带上 MCP 工具(mcp_unavailable_reason: "sheet")。在关闭了 MCP 的服务器上,setVoiceCallMcpServer 返回 403 VOICE_NOT_AVAILABLE: MCP tools are not available on this server,@BotFather 中也没有“MCP 服务器”(MCP 서버)按钮。

机器人所有者注册自己运营(或签约)的 MCP 服务器后,模型就能在通话中调用该服务器的工具。发起调用的不是机器人的代码,而是 Pabal——Pabal 的 MCP 客户端经由 Pabal 的出站代理向该服务器发出请求,并执行期限、大小和检查方面的约束。因此,来电者所说的值会发送到该服务器(机器人所有者条款第 10 条)。

MCP 服务器需要具备的条件

  • Streamable HTTP、POST(协议版本 2024-11-05 及以上)。https:// 并使用公共证书,不重定向,端口只能是服务器允许的(pabal.me 为 443)。
  • 使用凭据的服务器,在没有凭据时必须如实以 401 拒绝,并且不能接受错误的凭据。一份凭据只能由一个机器人使用。
  • 运营方文档:服务器的运营方要在 https://<主机>/.well-known/pabal-voice-mcp 放一份 JSON 文档(不超过 32KB,不重定向)——运营主体、国家、联系方式、目的、数据类型和所在地、接收方和再委托方、保存期限。这是显示在来电者确认窗口中的接收方信息;如果没有,由机器人所有者在注册时申报(见下文)。无需凭据、任何人都能调用的公开服务器(auth.type: "none")必须在这份文档的端点条目中写明 "accepts": "any",表明接受来自 Pabal 的调用。
  • Pabal 附加的内容:tools/call 的 _meta 中会带上剩余期限 me.pabal/budget_ms(毫秒)和结果大小上限 me.pabal/result_budget_bytes,还可能带上 Pabal 签名的断言(me.pabal/assertion,JWT)。断言的签发者、sub(bot:<机器人 id>)和验证密钥(JWKS)可以通过 getVoiceCallMcpBinding 获取。
  • tools/list 不超过 2MB、20 页。不能有需要人工参与的步骤(例如在浏览器中登录)。
  • 每个工具用 _meta["me.pabal/voice"] 给出分类——data(none·caller·third_party)、content(system·third_party_text)、effect(none·write·notify·financial)、max_ms(不超过 15,000)、max_result_bytes(不超过 65,536),以及可选的 p95_ms。如果服务器不提供,由机器人所有者在注册时用 tools 补充覆盖。
  • 规范 1.0 只开放 L1:只有 data: "none" 且 effect: "none" 的工具(只读取公开信息的工具)会被固定到通话中。处理来电者数据(L2)或执行某种操作(L3)的工具只会被记录,不会被使用。
  • 注册时,Pabal 会运行合规性检查(地址、TLS、版本、401、工具列表、工具结构、凭据范围、人工步骤)。不通过则以 MCP_CONFORMANCE_FAILED: CT-xx 拒绝。

注册

机密值只能放在请求体中发送(放在查询字符串中返回 400)。请把它写进文件再发送,以免机密值留在命令行里。

umask 077
cat > mcp-shop.json <<'EOF'
{"label": "shop",
 "url": "https://mcp.example.com/mcp",
 "auth": {"type": "bearer", "token": "(MCP 服务器给你的令牌)"}}
EOF
curl -s "https://pabal.me/bot$BOT_TOKEN/setVoiceCallMcpServer" -H 'Content-Type: application/json' -d @mcp-shop.json
  • label:[a-z][a-z0-9]{0,11},不能以 pabal 开头。每个机器人最多 2 个服务器。模型看到的工具名是 label__工具名。
  • auth.type:none、bearer(token)、headers(最多 3 个请求头)、client_credentials(token_url、client_id、client_secret,或 private_key_jwt: true——需先调用 createVoiceCallMcpClientKey)。暂不支持 authorization_code。none(公开服务器)只有在该服务器运营方的公开文档表明接受来自 Pabal 的调用时才能注册(否则返回 MCP_OPT_IN_REQUIRED)。
  • 会改变接收方的变更(新服务器、不同的地址或令牌主机、新的申报、更高的等级)的请求会返回 400 MCP_CHANGE_NEEDS_CONFIRMATION,机器人所有者需要在 5 分钟内按下 @BotFather 中出现的确认(확인 — 이 변경을 적용)按钮才会生效。这样即使令牌泄露,来电者的话也不会被悄悄发往别处。只更换机密值的轮换会立即生效。
  • 如果服务器运营方没有公开文档,机器人所有者要用 declaration 如实申报接收方(运营主体、国家、联系方式、目的、数据类型、所在地、再委托方、保存期限)。这些信息会显示在来电者的确认窗口中,发生变化时会再次询问来电者。
  • 注册和检查每个机器人每小时 10 次、每个所有者 20 次。其他方法:refreshVoiceCallMcpServer(重新检查)、deleteVoiceCallMcpServer(随时可以)、getVoiceCallMcpBinding(你的服务器用来识别 Pabal 调用的签发者、sub 和 JWKS)、createVoiceCallMcpClientKey。
  • 已注册的服务器和工具的状态显示在 getVoiceCallSettings.mcp_servers 中(不显示路径、查询和机密值)。规范全文(Pabal Voice MCP Profile 1.0)在 Pabal 代码仓库的 docs/app/voice-bot-mcp-profile.html 中,需要时请向服务器运营方索取。

自有模型密钥

自有密钥目前在任何生产服务器上都无法使用

用自有密钥接听的通话,来电者必须看到说明声音会发送给该密钥的提供商的确认窗口(第 2 版),但目前还没有显示这个窗口的应用。因此,除测试服务器外,在运营方确认应用已就绪并开启之前,服务器甚至不接受密钥(403 VOICE_OWN_KEY_NOT_AVAILABLE)。服务器还需要有主密钥。

让机器人的通话在机器人所有者自己的模型账号上运行。费用计入所有者的提供商账号,自有密钥的通话不占用平台对机器人、所有者和服务器的每日限额(来电者的限额照旧)。需要先同意机器人所有者条款。

curl -s "https://pabal.me/bot$BOT_TOKEN/setVoiceCallApiKey" -d provider=google \
  --data-urlencode "api_key@vertex-sa.json"
  • provider:google 是 Vertex AI 的服务账号 JSON 密钥(只保存必要的字段),openai 是以 sk- 开头的 API 密钥。其他值返回 VOICE_PROVIDER_INVALID。密钥只能放在请求体中(放在查询中返回 400)——像上面那样从文件读取发送,就不会留在命令行里。不要把它粘贴到 @BotFather 的对话中。
  • 服务器在接受密钥之前,会用网关的测试会话确认它确实能打开(最多 12 秒)。通过后保存并返回 True。提供商拒绝时返回 API_KEY_INVALID;能登录但没有实时 API 权限时返回 API_KEY_NO_REALTIME_ACCESS;没有可用于测试的网关或网关繁忙时返回 503 API_KEY_CHECK_UNAVAILABLE: …: the key was not kept。
  • 测试每个机器人每小时 5 次、每个所有者(所有机器人合计)10 次。只计算到达了网关的测试。
  • 密钥不会出现在任何地方——回答、错误、日志、界面中都没有。getVoiceCallSettings.api_key 中只有提供商、设定日期、指纹(8 位)和能否使用。
  • 如果已设定的密钥无法使用(条款变更、服务器无法打开密钥等),机器人不接听电话——不会悄悄换成平台密钥。用 deleteVoiceCallApiKey(随时可以)删除密钥后,机器人会改用平台密钥接听,正在使用该密钥的通话会结束。
  • 自有密钥的通话没有 Pabal 的每日限额——只受一通电话的时长、同时通话数和来电者的限额约束。费用计入你的账号,请在提供商的控制台设置预算提醒。
  • OpenAI 密钥需要网关运营方另行开启才会使用(网关默认只支持 Google 密钥)。如果没有开启了它的网关,测试 OpenAI 密钥会返回 503 … no_gateway。OpenAI 密钥这条路径还没有用真正的 OpenAI 测试过(只用假服务器测试过),用 OpenAI 密钥接听的通话会使用网关指定的 OpenAI 音色(默认 marin),与机器人的音色设置无关。Google 密钥在网关的 Vertex 区域(us-central1)中使用。
  • 在音频流模式下,密钥只保存,不使用。

文字记录

只有在运营方允许的服务器上才会送达文字记录

运营方必须完成法律审查,并在管理页面中允许(默认关闭)。在未允许的服务器上,即使打开开关,也会是 transcript_available: false、transcript_unavailable_reason: "not_allowed",文字不会送达。

开启后,机器人所有者会收到 AI 写下的通话对话文字。请先同意机器人所有者条款,再打开开关。

文字记录和保存对话不是一回事

文字记录(本节)由机器人所有者开启、由机器人所有者接收,来电者在接通时会听到提示语音。保存对话由打电话的人通过确认窗口中的复选框选择,通话结束后只以文字留在本人自己的聊天中——不会发给机器人或机器人所有者,也没有提示语音。一通电话可以同时有这两者。保存对话没有供机器人所有者打开的开关,只要服务器的隐私政策中有 bot-calls-keep-text 一节就会开启(准备中——使用应用——语音机器人)。

curl -s "https://pabal.me/bot$BOT_TOKEN/setVoiceCallSettings" -d transcript=true
  • 机器人打出的电话没有文字记录(机器人打来的电话)——即使打开了开关也是如此。
  • 给开启了文字记录的机器人打电话时,来电者会在 AI 提示之后紧接着听到“What is said on this call is written down and sent to the bot's owner.”(本次通话的对话内容会被写成文字并发送给机器人所有者)。只记录提示结束之后说的话。音频流模式的通话没有文字记录。
  • 已结束通话的 voice_call(event: "ended")中会带有 transcript。这是唯一的完整版本,所以请把 voice_call 放进 allowed_updates。如果想在通话中逐段接收,也要加上 voice_call_transcript(只通过 HTTP Bot API;MTProto 收不到)。
  • 包含银行卡号或居民登记号的发言,会把所有数字替换成 * 后再送达。分成两段说的数字或用词语念出的数字无法识别,所以收到的文字也请谨慎处理。
  • 每段发言最多 4,096 字节,每通电话最多 256KiB、4,000 段。超出部分会被丢弃,并带有 truncated: true。
  • Pabal 不保存文字记录:通话期间保存在内存中,交给机器人一次,没有取走的在 24 小时后删除。服务器重启时会丢失。收到之后的保存和处理由机器人所有者负责。
"transcript": {
  "turns": [
    {"role": "bot", "text": "这里是订单助手,请问有什么可以帮您?", "offset_ms": 5200},
    {"role": "caller", "text": "订单 A-1042 到哪儿了?", "offset_ms": 9800}
  ],
  "truncated": false
}
{"update_id": 21, "voice_call_transcript": {"voice_call_id": "4611686018427387905", "seq": 2,
 "role": "caller", "text": "订单 A-1042 到哪儿了?", "is_final": true, "offset_ms": 9800, "date": 1790380813}}

role 为 caller(来电者)或 bot(模型),offset_ms 是从通话声音开始时算起的毫秒数。

音频流模式

音频流模式目前还不能使用

来电者的应用必须显示说明“通话声音会发送到机器人开发者的服务器”的音频流确认窗口,才能给音频流机器人打电话,但包含这个窗口的应用版本还没有。服务器上也需要主密钥、隐私政策中的 bot-calls-stream 一节、机器人所有者一节,以及支持音频流的网关。缺少这些时,mode=stream 返回 403 VOICE_STREAM_NOT_AVAILABLE。

在音频流模式下,电话的原始声音不是发给 Pabal 的 AI 模型,而是发给机器人所有者的服务器。你的服务器收听声音,并把回答以声音的形式发回——使用哪种 AI 由你决定(其他提供商的实时模型,或你自己的语音识别与语音合成组合)。Pabal 仍然会在开头播放 AI 提示语音,并照样执行来电者的确认、限额、通话时长和同时通话数的限制。不需要指令,工具、自有密钥和文字记录只保存不使用(stream_ignores)。

来电者 Pabal(服务器 · 网关) 所有者的 wss 服务器 ① 音频流确认窗口 · 呼叫 ② 升级 + 签名 · pabal.voice.v1 ③ start {call_id, bot_id, format} ④ ready(3 秒内) AI 提示语音 · 此期间来电者的声音 不会发送到机器人服务器 ⑤ live 声音(中继) ⑥ PCM16 24 kHz · 20 ms 分片 ⑦ clear · mark · hangup ⑧ stop {reason} → 以 1000 关闭
音频流模式:机器人所有者的服务器取代了模型的位置

图示说明

  • 三条泳道:左边是来电者,中间是 Pabal(服务器和语音网关),右边是机器人所有者的服务器。机器人服务器既不直接与应用对话,也不直接与 Pabal 服务器对话,只与网关通过 WebSocket 交流。
  • ②~④ 发生在电话响铃期间:网关通过 TLS 连接它确认过的公网地址,带着签名请求头完成升级,然后发送 start。机器人服务器必须在 3 秒内发送 ready 才会接听电话,否则来电者会听到忙音。
  • 灰色方框是一道关卡:在 AI 提示语音(“This is an AI voice bot. The call's audio goes to the bot developer's server.”)播放完之前,来电者的声音不会发送到机器人服务器,机器人抢先发送的声音也不会播放。提示结束的那一刻,发送 ⑤ live。
  • ⑥ 声音双向都是 PCM16、24 kHz、单声道,来电者一侧总是 20 ms(960 字节)的分片。蓝色箭头是 Pabal 发送的,灰色是机器人服务器发送的。
  • ⑦ 和 ⑧:机器人可以发送插话(clear)、播放位置通知(mark)和挂断(hangup);通话结束时,Pabal 发送带有原因的 stop,并以 1000 关闭。

设置与探测

请先把下面的示例服务器运行在公网地址上(证书与其名称相符)。设置的那一刻,探测就会连接该服务器,连不上的失败也会计入每小时 5 次的限额。

同意机器人所有者条款后(@BotFather → 语音通话 → 音频流模式 → 同意机器人所有者条款(봇 주인 약관에 동의)),告诉 Pabal 服务器的地址和机密值。机密值是 32~128 个可打印的 ASCII 字符(不含空格),新的地址必须总是配上新的机密值一起发送。

umask 077
openssl rand -hex 32 | tr -d '\n' > stream-secret.txt      # 末尾不能有换行
curl -s "https://pabal.me/bot$BOT_TOKEN/setVoiceCallSettings" -d mode=stream \
  -d stream_url=wss://bot.example.com:8443/voice --data-urlencode "stream_secret@stream-secret.txt"
  1. 地址格式:只能是 wss://,不能有用户名、# 或空格,只能用 ASCII(其他字符需百分号编码),不超过 2,048 个字符。主机必须是带点的名称或公网 IP(不能用 localhost),端口为 443 或 1024~65535。否则返回 400 STREAM_URL_INVALID: …。
  2. 探测限额在请求的那一刻就计数:每个机器人每小时 5 次、每个所有者 10 次,同时进行的每个机器人 1 个、每个所有者 2 个。超过时返回 429。因网关繁忙而没有连接任何地方的探测不计数。
  3. 地址检查:名称只要指向任何一个私有或保留地址,或 Pabal 自身的地址,就会返回 400 STREAM_URL_INVALID: the host must resolve only to public addresses。
  4. 探测:网关会试着连接一次(start 中带 "probe": true)。收到 ready 就会开启,机密值会在服务器主密钥的保护下加密保存。失败时返回 400 STREAM_PROBE_FAILED: <代码>——unreachable(名称、连接、时间或 TLS 之一)、handshake_failed(返回的不是 101——通常是签名被拒绝)、redirect、no_subprotocol、no_ready、bad_ready、address_refused、busy、url 等——其他代码会原样返回网关给出的值。服务器最多等待 12 秒。
  5. 通过 Bot API(令牌)开启音频流模式或更改地址时,@BotFather 会向机器人所有者发送“音频流地址已改为 …(机密值 …)”(원음 주소가 … 로 바뀌었어요 (비밀 …))以及返回托管模式(관리형으로 돌아가기)按钮;返回托管模式时,会告知这一情况并附上语音通话(음성 통화)按钮。这是为了防止令牌泄露后声音被悄悄送往其他服务器。
  • 把 stream_secret 放进查询字符串,会先于其他一切检查直接返回 400。不能在 Webhook 响应中设置音频流模式。
  • 要返回托管模式,使用 mode=managed(会删除地址和机密值)。如果处于开启状态且指令为空,会返回 VOICE_INSTRUCTIONS_REQUIRED,所以请同时发送指令。
  • 在运营方仅接受已允许机器人的服务器上,要等运营方检查音频流地址并再次允许后才会接听电话。
  • getVoiceCallSettings 中只显示 stream_url 和机密值的指纹。音频流通话不能使用 sendVoiceCallContext(VOICE_CONTEXT_NOT_SUPPORTED),endVoiceCall 不道别,直接挂断。

规范

  • TLS 1.2 及以上,证书必须是与 URL 中的名称相符的公共证书。网关连接的是它确认过的 IP,但 SNI 和证书校验都使用名称。不跟随重定向(3xx)。
  • 必须选择子协议 pabal.voice.v1(Sec-WebSocket-Protocol)。不压缩。每条消息最多 65,536 字节,文本消息最多 4,096 字节(UTF-8)。
  • 网关每 15 秒发送一次 ping。15 秒内没有收到 pong 就断开(大多数库会自动回应)。
  • 文本消息是 JSON 事件,二进制消息是声音。声音格式只有一种:PCM16 小端序、单声道、24,000 Hz。call_id·bot_id 是十进制字符串。
方向消息何时 · 含义
Pabal → 机器人{"event":"start","protocol":1,"call_id":"…","bot_id":"…","probe":false,"caller":{"lang":"ko"},"format":{"encoding":"pcm_s16le","sample_rate":24000,"channels":1,"frame_ms":20},"max_seconds":300}升级完成后立即发送一次。不含来电者的名字、号码或 ID(只有 caller.lang——人的信息通过 voice_call 更新获取)
Pabal → 机器人{"event":"live"}AI 提示已结束。从此开始传输声音
Pabal → 机器人二进制来电者的声音,总是 960 字节(20 ms)。只在 live 之后
Pabal → 机器人{"event":"mark","name":"…"}机器人发送的该 mark 之前的声音已全部播放
Pabal → 机器人{"event":"cleared","played_ms":N,"dropped_ms":N,"marks":["…"]}对 clear 的回应
Pabal → 机器人{"event":"buffer_full","dropped_ms":N}机器人的声音积压超过 30 秒,溢出的部分已丢弃
Pabal → 机器人{"event":"stop","reason":"…","played_ms":N,"duration_ms":N}通话已结束。随后以 1000 关闭
机器人 → Pabal{"event":"ready"}start 之后 3 秒内,最先发送
机器人 → Pabal二进制机器人的声音:偶数字节,2~65,536 字节。live 之前的会被丢弃
机器人 → Pabal{"event":"clear"}插话:立即停止排队中的机器人声音
机器人 → Pabal{"event":"mark","name":"[A-Za-z0-9_.:-]{1,64}"}“播放到这里时告诉我”。每通电话最多有 256 个在等待
机器人 → Pabal{"event":"hangup"}结束通话。先把排队中的声音最多播放 5 秒,然后挂断
  • 未知的 event 会被忽略。不是 JSON 的文本、超过 4KiB 的文本、字节数为奇数或超过 64KiB 的声音属于违反规范,会结束通话(来电者会听到致歉提示)。
  • 发送量上限:文本事件平均每秒 50 个(瞬时 100 个),声音平均为实时的 4 倍(瞬时 60 秒的量)。如果机器人的套接字不读取,导致来电者的声音积压超过 30 秒(1,500 个),也会结束通话。

签名

网关会在升级请求中加上这个请求头。

X-Pabal-Voice-Signature: t=<Unix 秒>,nonce=<16 字节 hex>,call=<call_id>,v1=<hex>
v1 = HMAC-SHA256(stream_secret, "pabal-voice-stream/1\n<t>\n<nonce>\n<call>\n<host>\n<path?query>")
  • host 是你告诉 Pabal 的 URL 的主机(小写,不含端口)。不要用请求中的 Host 请求头来计算——那个值是发送方写的,起不到绑定主机的作用。path?query 是 URL 的路径和查询原样(没有时为 /)。探测时,call 是探测的编号。
  • 机器人服务器必须检查:|当前时间 − t| ≤ 300 秒,同一个 nonce 只能用一次,HMAC 要用恒定时间的函数逐字节比较。有误时返回 401 之类的非 101 响应。
  • 然后检查 start.call_id 是否与签名中的 call 相同。不同则以 1008 关闭——防止已签名的升级被重用于其他通话。只服务一个机器人的服务器,还要检查 start.bot_id 是否是自己的机器人。
  • 测试用的值:机密值 example-stream-secret-of-the-bot-developer-0001、t=1790000000、nonce 00112233445566778899aabbccddeeff、call 4611686018427387905、host bot.example.com、路径 /voice?bot=100042 → v1=807517ff96ac75223bde35b48c775f49a2eda2fb4832a02c0ea832b914d70d0c。

声音 · 插话 · mark · 结束

  • 机器人 → 来电者的缓冲区:网关按播放速度发出声音,所以可以一次性全部发送,但积压超过 30 秒时,溢出部分会被丢弃并发送 buffer_full。机器人的声音中断 300 ms,就视为一个“块”结束(mark·clear 的单位)。
  • clear:丢弃所有尚未播放的机器人声音,从下一个 10 ms 开始静音(实测不到 50 ms)。回应 cleared 中,played_ms 是本次通话到目前为止实际播放的机器人声音总量,dropped_ms 是这次丢弃的量,marks 是被丢弃的块上挂着的 mark 名称——可用于告诉你的模型“在哪里被打断了”。
  • mark:它之前的声音播放完后,会以同样的名称返回。mark 之前的声音最好以 480 字节(10 ms)的整数倍结束(剩余的零头会用静音补齐)。
  • 结束:机器人发送 hangup,或以 1000·1001 干净地关闭。其他方式的关闭或 TCP 断开,会像违反规范一样结束通话(向来电者播放致歉提示)。Pabal 结束通话时,stop.reason 是一个简短的代码——hangup(来电者挂断)、max_length、bot_ended、disconnect、drain、kill_switch、operator、link_lost、media_failed … 遇到未知的值请当作“已结束”处理。
  • 探测的 start 带有 "probe": true、空的 caller 和 max_seconds: 0。发送 ready 后会收到 {"event":"stop","reason":"probe"} 并关闭。探测时不要打开模型会话之类会产生费用的东西。

示例——最小的音频流服务器

这是一个不用 AI 就能检验整个规范的服务器:收到 live 时播放提示音,之后把来电者的声音原样送回(回声)。它会检查签名、时间、nonce、通话和机器人。先用它确认通话能从头到尾正常进行,再把回声的部分换成你的 AI。Pabal 代码仓库中还有 scripts/bots/stream_echo_bot.py(含插话演示)和 scripts/bots/stream_openai_relay.mjs(用你的 OpenAI 密钥中继到 Realtime,一个没有实际运行过的示例),需要时请向服务器运营方索取。

# stream_bot.py — pip install "websockets>=13"
# 运行:STREAM_SECRET_FILE=stream-secret.txt TLS_CERT=fullchain.pem TLS_KEY=privkey.pem \
#       PUBLIC_HOST=bot.example.com BOT_ID=<机器人 ID——令牌中 : 前面的数字> python3 stream_bot.py
import asyncio
import hashlib
import hmac
import json
import math
import os
import ssl
import struct
import time

from websockets.asyncio.server import serve

SECRET = open(os.environ["STREAM_SECRET_FILE"], "rb").read().strip()
PUBLIC_HOST = os.environ["PUBLIC_HOST"].lower()   # 告诉 Pabal 的 stream_url 的主机(不是 Host 请求头)
BOT_ID = os.environ.get("BOT_ID", "")             # 留空则接听任何机器人的通话
PATH = os.environ.get("STREAM_PATH", "/voice")
PORT = int(os.environ.get("PORT", "8443"))
CHIME = b"".join(struct.pack("<h", int(8000 * math.sin(2 * math.pi * 880 * i / 24000)))
                 for i in range(24000 * 6 // 10))  # 880 Hz 0.6 秒,PCM16 24 kHz
seen = {}                                         # 用过一次的 nonce


def signed_call(header, target):
    """检查 X-Pabal-Voice-Signature,返回签名中的 call id;有误时返回 None。"""
    try:
        parts = dict(p.split("=", 1) for p in header.split(","))
        t, nonce, call, v1 = int(parts["t"]), parts["nonce"], parts["call"], parts["v1"]
    except (KeyError, ValueError):
        return None
    now = time.time()
    if abs(now - t) > 300 or nonce in seen:
        return None
    text = f"pabal-voice-stream/1\n{t}\n{nonce}\n{call}\n{PUBLIC_HOST}\n{target}"
    expected = hmac.new(SECRET, text.encode("utf-8", "surrogateescape"), hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected.encode(), v1.encode("utf-8", "surrogateescape")):  # 逐字节比较
        return None
    for old in [n for n, at in seen.items() if at < now - 600]:
        del seen[old]
    seen[nonce] = now
    return call


def check(connection, request):
    if request.path.split("?", 1)[0] != PATH:
        return connection.respond(404, "not here\n")
    call = signed_call(request.headers.get("X-Pabal-Voice-Signature", ""), request.path)
    if call is None:
        return connection.respond(401, "bad signature\n")
    connection.signed_call = call                 # start 必须指向这通电话
    return None


async def handle(ws):
    if ws.subprotocol != "pabal.voice.v1":
        await ws.close(1002, "pabal.voice.v1 only")
        return
    start = json.loads(await asyncio.wait_for(ws.recv(), 5))
    if (start.get("event") != "start" or start.get("call_id") != ws.signed_call
            or (BOT_ID and start.get("bot_id") != BOT_ID)):
        await ws.close(1008, "not this call")
        return
    await ws.send(json.dumps({"event": "ready"}))    # 3 秒内,最先发送
    live = False
    async for message in ws:
        if isinstance(message, bytes):
            if live:
                await ws.send(message)            # 把来电者的 20 ms 原样送回(回声)
            continue
        event = json.loads(message)
        if event.get("event") == "live" and not start.get("probe"):   # AI 提示已结束
            live = True
            await ws.send(CHIME)
            await ws.send(json.dumps({"event": "mark", "name": "chime"}))
        elif event.get("event") == "stop":
            print("结束:", start["call_id"], event.get("reason"), flush=True)
            break


async def main():
    tls = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
    tls.minimum_version = ssl.TLSVersion.TLSv1_2
    tls.load_cert_chain(os.environ["TLS_CERT"], os.environ["TLS_KEY"])
    async with serve(handle, "0.0.0.0", PORT, ssl=tls, subprotocols=["pabal.voice.v1"],
                     process_request=check, max_size=65536, compression=None):
        print(f"正在监听: wss://{PUBLIC_HOST}:{PORT}{PATH}", flush=True)
        await asyncio.get_running_loop().create_future()


asyncio.run(main())

来电者看到的内容

  • 通话按钮:语音机器人的对话顶部和资料页上会出现电话按钮。只有当服务器判断这个人、这台设备的通话可以被接听时才会显示(正式开放前只对测试者显示)。没有视频通话。
  • 确认窗口(先于麦克风权限):标题“An AI answers this call”(此通话由 AI 接听);机器人不是人,声音也是 AI 生成的;通话不是端到端加密的;接收声音的一方(Google,Vertex AI 的 Gemini Live API)以及国家和地区(United States (Iowa, Google Cloud us-central1));发送的内容(通话中的声音、对话文字和 AI 的回答、机器人在通话中加入的备注以及机器人功能的结果);保存(Pabal 不保存声音,只保留通话记录 180 天;Google 在生成回答后不保留,但被自动检查怀疑为滥用的内容可能会被记录最长 90 天);机器人所有者收到的内容(名字、账号 ID、语言、通话时间和时长、工具用到的值——不包括手机号码和声音);以及年满 19 周岁。按钮是“I'm 19 or older · Call”(我已年满 19 周岁 · 呼叫)和取消。年龄是自行确认,不是实名验证。通话按钮是主按钮(用强调色填充),取消是次要按钮。在开启了保存对话的服务器上,说明之后会附有复选框“Keep this call's conversation as text in my chat”(将本次通话的对话以文字保存在我的聊天中,默认关闭)。
  • 多久问一次:桌面版和 Android 在第一次给该机器人打电话时询问(接收方或窗口版本变化时会再次询问),iPhone 和 Apple Watch 每次都问。手表每次都显示完整的窗口,保存对话开关的初始值是给该机器人拨打的上一通电话中的选择。应用每次通话都会把同意发送给服务器,服务器只接受同一设备、同一机器人、2 分钟内的一份同意,并且只用于这一通电话。
  • 接通后:AI 提示语音“You're now talking to an AI voice bot.”(音频流机器人则是“This is an AI voice bot. The call's audio goes to the bot developer's server.”)→(文字记录机器人)文字记录提示 → 机器人的问候语。机器人所有者不能关闭这些提示,提示结束之前来电者说的话不会发送给模型。保存对话没有提示语音——因为这是来电者自己在屏幕上选择的。
  • 提示的语言:提示语音使用的是来电者读过的确认窗口的语言,而不是机器人的 language。一通电话的所有提示都用同一种语言——AI 提示、文字记录提示,以及连接中断时的致歉(音频流机器人则是音频流提示和致歉)。其中只要有一条没有该语言审查完毕的录音,就全部用英语播放。目前审查完毕的语言只有韩语和英语。其余 8 种语言(日语、中文、法语、西班牙语、印度尼西亚语、越南语、高棉语、阿拉伯语)的确认窗口和提示还是等待法律审查和母语者确认的草稿,所以使用这些语言应用的来电者,目前看到的也是英语确认窗口,听到的也是英语提示。高棉语录音和日语版“Just a moment longer, please.”(请再稍等一下)的录音,还在等人试听后挑选。通话的第一条提示和文字记录提示之前,始终有至少 300 ms 的静音——以免电话线路吞掉第一个词。
  • 通话界面:名字下方显示“@{username} · AI · Bot”和“AI-generated voice · Call audio is processed by Google”(AI 生成的声音 · 通话声音由 Google 处理)。没有表情符号密钥和加密说明。
  • 连接出现问题时,会听到“Sorry, we've lost the connection. Ending the call now.”(抱歉,连接出现问题,通话即将结束)后挂断。所有拒绝都以忙音结束。

面向应用用户的说明在使用应用——给语音机器人打电话中。

机器人打来的电话

pabal.me 上处于关闭状态——等待法律审查

服务器已经实现了这项功能,但在隐私政策的 bot-calls-outbound 一节和服务条款的 voice-bot-owners-outbound 一节完成法律审查并发布之前,任何机器人都不能给人打电话。带许可窗口的应用版本也还没有发布(当前状态)。下面说的是在开启了这项功能的服务器上的行为。

语音机器人也可以主动给人打电话:确认预约、在约定的时间叫醒、通知配送。机器人所有者的代码用 startVoiceCall 发起请求,服务器判定后让对方的设备响铃;对方接听后,先播放 Pabal 的提示语音,再由 AI 语音模型按机器人的指令说话。通话如何结束,会通过 voice_call 更新告知。

  • 只打给许可了的人。对方必须在 Pabal 应用中许可该机器人给自己打电话(接听者的许可),并且可以随时关闭。
  • 不是广告电话。目的只有四种——提醒、叫醒、配送、预约;不能做广告、推销或宣传(机器人所有者条款第 12–24 条)。
  • 机器人打来的电话不是端到端加密的——声音会经过 Pabal 的服务器和 AI 提供方。对方接听后,通话期间声音会发往 Google——Vertex AI 的 Gemini Live API,美国艾奥瓦(Google Cloud us-central1)。Pabal 不保存声音。
  • 对话文字不会发给机器人所有者。机器人打出的电话没有文字记录。机器人所有者收到的是接听者看到和听到的内容中列出的五项。

流程

机器人的代码 Pabal 服务器 网关 · 模型 对方的应用 ① startVoiceCall 判定:机器人 · 许可 · 限额 免打扰时段 · 暂停中 ② 200 {voice_call_id} ③ dial ④ 响铃:名称 (AI) ⑤ 接听 ⑥ Pabal 的提示 ⑦ 机器人:名称 · AI · 目的 通话——声音经过网关和 AI 模型 ⑧ voice_call: started · ended 未接听 · 未响铃:not_answered
一通机器人打来的电话:请求 → 判定 → 响铃 → 接听 → 提示 → 第一句话 → 结束通知

图示说明

  • 四条泳道:从左到右依次是机器人所有者的代码、Pabal 服务器、语音网关和 AI 模型、接听者的应用。竖直的虚线表示时间;蓝色实线是通话推进的路径,灰色线是给机器人的回答。
  • ① 处的灰色框——判定有三次:回答前检查机器人一方(是否开启、运营方审核、条款、限额)以及是否有许可;返回 200 之后检查接听者一方(免打扰时段、暂停中、接听者的每日限额和间隔、可响铃的设备);响铃前再检查一次。
  • ② 的 200 只表示“已受理”。是否响铃、是否接听,只能从 ⑧ 的更新中得知。
  • ④–⑦ 接听者一方:响铃时机器人名称后带“(AI)”,接听后 Pabal 的提示语音(⑥)先于机器人播放。机器人在第一句话(⑦)中说明自己的名称、是 AI,以及来电目的。绿色框期间,声音经过网关和 AI 模型——不是端到端加密。
  • 红色虚线是关键:因接听者一方的判定而没有响铃的请求,与响了却没人接的请求,会在同一时间以同样的 not_answered 结束。机器人无法知道对方当时是在睡觉还是在另一通电话中。

开启——@BotFather

机器人打电话只能由机器人所有者在 @BotFather 中开启,不能用令牌开启(向 setVoiceCallSettings 发送外呼字段会返回 400 VOICE_FIELD_NOT_AVAILABLE)。必须先开启语音通话。/mybots → 机器人 → 语音通话(음성 통화)→ 机器人打来的电话(봇이 거는 전화)→ 开启(켜기)。

켜기
BotFather@order_helper_bot 이 사람에게 전화를 걸 수 있게 할까요? • 이 봇에게 전화를 허락한 사람에게만 걸어요. 허락은 그 사람이 파발 앱에서 하고, 언제든 끌 수 있어요. • 그 사람의 조용한 시간과 쉬는 중에는 울리지 않아요. • 하루 상한(지금 50번)과 서버의 한도가 있어요. • 운영자가 이 봇의 이름·사진·지시문·목소리·목적을 검토하고 허락해야 걸려요. 무엇이든 바뀌면 다시 검토해요. • 봇이 거는 전화는 종단 간 암호화가 아니에요. 통화 소리는 파발 서버와 AI 제공자를 거쳐요.
켜기
BotFather봇이 거는 전화를 켰어요. 운영자가 이 봇을 검토하고 허락하면 Bot API 의 startVoiceCall 로 걸 수 있어요. 봇이 거는 전화 · @order_helper_bot 상태: 켜짐 운영자 허락: 대기 목적: 배달 하루 상한: 50번 오늘: 요청 0 · 받음 0 · 안 받음 0 이 봇에게 전화를 허락한 사람에게만, 그 사람의 조용한 시간과 쉬는 중을 피해 걸어요. 봇이 거는 전화는 종단 간 암호화가 아니에요 — 통화 소리는 파발 서버와 AI 제공자를 거쳐요.

BotFather 用韩语回答。第一条消息询问是否让这个机器人给人打电话:只打给许可了的人,不在对方的免打扰时段和暂停期间响铃,有每日上限(现在是 50 次)和服务器的限额,运营方审核并许可机器人的名称、照片、指令、音色和目的后才能拨打(任何一项变化都要重新审核);机器人打来的电话不是端到端加密的。第二条是开启后的界面:已开启、等待运营方许可、目的为配送、上限 50、今天的请求数。

  • 条款第 4 版:如果还没有同意,开启前会先显示机器人所有者条款。第 12–24 条是关于机器人打电话的约定。
  • 目的:至少从 reminder(提醒)、wake_up(叫醒)、delivery(配送)、appointment(预约)中选一个才能开启。每次请求写明其中之一;没选的目的返回 400。更改目的后,会再次询问已许可的人,运营方也会重新审核。
  • 每日上限:这个机器人每天能发出的请求数,可选 10、20、50 次(默认 50)。
  • 运营方审核:运营方在管理页面查看机器人的名称、@用户名、照片、指令、问候语、工具、音色、语言、模式、目的和所有者姓名并许可后,才能拨打。其中任何一项变化,在重新获得许可前都不能拨打(403 VOICE_OUTBOUND_OFF: review)。
  • 令牌:用令牌更改语音设置(关闭语音除外)会让外呼暂停 24 小时(frozen)。如果是你自己改的,在 @BotFather 中点是我改的(내가 바꿨어요,vdk、vdK)解除。重新签发令牌也会暂停 24 小时,进行中的外呼会结束,开启了外呼的机器人的 Webhook 会被删除。
  • 只有语言为韩语或英语(ko-KR、en-US)的机器人可以拨打。使用音频流模式、自有密钥、工具或 MCP 服务器的机器人不能拨打(mode)。

接听者的许可

机器人无法自己取得许可。对方必须在 Pabal 应用中打开该机器人的许可窗口,点按“I am 19 or older · Allow”(我已满 19 岁 · 许可)。窗口会显示由 AI 拨打、目的、机器人所有者的名字、声音的去向和免打扰时段(使用应用——许可机器人给我打电话)。打开窗口的地方有三个。

  • 机器人菜单:机器人聊天或资料页中的“Allow calls…”(许可来电…)。
  • 一对一对话中的链接:机器人在与对方的一对一对话中,以正文链接或内联 URL 按钮发送 https://pabal.me/<机器人用户名>?callme=1。只有该机器人在该对话中发送的消息才会打开窗口——转发到其他对话的链接、预览或网页只会打开聊天。每人每天只能通过链接打开一次窗口(所有机器人合计),从未与机器人对话过则不会打开。
  • 设置中的列表“Bots allowed to call me”(许可来电的机器人):查看已许可的机器人,关闭或重新许可。
curl -s "https://pabal.me/bot$BOT_TOKEN/sendMessage" -d chat_id=$USER_ID \
  --data-urlencode "text=订单送达前要不要打电话通知您?" \
  --data-urlencode 'reply_markup={"inline_keyboard": [[{"text": "许可来电", "url": "https://pabal.me/order_helper_bot?callme=1"}]]}'
  • 自行托管的服务器:应用只认识 pabal.me 的链接。如果是其他服务器,必须把该服务器的主机加入应用版本的链接规则,链接才会打开窗口(菜单和设置列表照常可用)。
  • 目前的范围:只有 +82 号码的账号、设备时区为韩国(首尔)、登录满 24 小时的设备才能开启许可(国家由运营方设定)。回答未满 19 岁的人一年内不能与机器人通话。
  • 机器人的名称、所有者或目的改变,或许可满一年后,会再次询问;在此之前不能拨打。

拨打——startVoiceCall

curl -s "https://pabal.me/bot$BOT_TOKEN/startVoiceCall" -d user_id=$USER_ID -d purpose=delivery \
  --data-urlencode "context=订单 A-1042 今天下午 3 点左右送到。请问对方到时能否签收。" \
  -d max_duration=180 -d ring_timeout=30
{"ok": true, "result": {"voice_call_id": "8812345678901234"}}
参数含义
user_id要拨打的人的 ID。此人必须已许可这个机器人
purpose这个机器人所选目的之一:reminder、wake_up、delivery、appointment
context只给模型的、关于这通电话的说明,1–1,000 个字符、3,072 字节。会按内容规则检查(冒充、隐瞒 AI 身份、索要机密、广告)。模型在第一句话(名称、AI、目的)之后、对方回答之后才使用它
max_duration接听后的最长时长(秒),30 到 300 与机器人上限中较小者。默认 180
ring_timeout响铃时长(秒),30–45。默认 40
回答含义处理
200 {"voice_call_id": "…"}已受理。是否响铃、是否接听见更新—
400 Bad Request: <内容>参数错误、没选的目的(purpose is not declared)、机器人语言不是韩语或英语修正
400 VOICE_CONTEXT_REFUSED: <代码>context 违反内容规则修正
403 VOICE_OUTBOUND_OFF: <原因>机器人一方:owner_off(未开启)、terms(条款第 4 版)、review(等待运营方审核)、suspended(自动停止)、frozen(令牌暂停)、owner_name(所有者姓名未通过冒充检查)、mode,或语音设置(unavailable_reason)中机器人一方的原因:bot_voice_off、not_allowed、suspended、name_refused、text_refused、awaiting_review、stream_unavailable、own_key_unavailable、no_gateway、no_gateway_with_tools、no_gateway_with_language、no_own_key_gateway。服务器一方的原因 voice_off、kill_switch、no_relay、outbound_off 不返回 403,而是返回 503@BotFather、运营方
403 USER_UNAVAILABLE无法拨打此人:没有许可、已关闭或需重新许可、已拉黑、不是人、超出范围、未满 19 岁、不是测试者。无论原因,响应体始终相同请求对方许可
429 retry after N机器人或所有者一方的限额:请求频率、机器人或所有者的每日请求数、机器人的同时通话数、所有者的外呼分钟数、机器人或所有者的每日通话时长(与打给机器人的电话共用)N 秒后
503 VOICE_UNAVAILABLE服务器一方:外呼开关关闭、服务器的语音已关闭(包括隐私政策中没有机器人通话一节时)、紧急停止开关、没有通话中继、服务器限额和外呼份额、没有能拨打的网关(或网关都已满)、无法记录请求稍后
{"ok": false, "error_code": 403, "description": "Forbidden: USER_UNAVAILABLE"}

接听者一方的判定在 200 之后进行。即使有许可,只要处于免打扰时段、暂停中、超过与此人的每日限额或此人的每日限额与间隔、没有可响铃的设备或对方正在通话,就不会响铃,并在请求后约 ring_timeout + 12 秒收到 not_answered——与响了没人接无法区分。有许可也不会改变机器人一方的判定(403、429、503)。

设置与用量

"outbound": {
  "enabled": true,
  "purposes": ["delivery"],
  "daily_cap": 50,
  "reviewed": true,
  "frozen_until": 0,
  "suspended": false,
  "available": true
}
"outbound": {
  "today": {"requests": 4, "answered": 2, "not_answered": 2, "minutes": 3},
  "limits": {"daily": 50, "owner_daily": 100, "owner_minutes": 30}
}
  • getVoiceCallSettings 中的 outbound 总是存在:在 @BotFather 中设定的内容(enabled、purposes、daily_cap)、运营方审核(reviewed)、令牌暂停的解除时间(frozen_until,未暂停时为 0)、自动停止(suspended)以及现在能否拨打(available)。只有 available 为 false 时才有 unavailable_reason(机器人一方的取值与 403 VOICE_OUTBOUND_OFF 的原因相同,服务器一方则是 voice_off、kill_switch、no_relay、outbound_off——这四个对应 503 VOICE_UNAVAILABLE)。
  • getVoiceCallUsage 中的 outbound 只出现在曾经开启过外呼的机器人上:今天的请求、接听、未接听和分钟数,以及适用于这个机器人的每日限额。不提供响铃次数。calls、seconds、missed、refused 不计入机器人打出的电话。但机器人打出的电话与打给机器人的电话共用机器人和所有者的每日通话时长:接听的通话按秒计入,remaining_seconds_today 的 bot、owner 随之减少;响铃期间也会先减去预留的部分,没有接听则退回。
  • 不能用 setVoiceCallSettings 修改(400 VOICE_FIELD_NOT_AVAILABLE)。

更新

与打给机器人的通话一样,通过 voice_call 更新送达(在 allowed_updates 中写 voice_call)。direction 为 "outgoing",对方只以 to 中的 ID 出现(没有名字、用户名和电话号码)。

{
  "update_id": 83,
  "voice_call": {
    "id": "8812345678901234",
    "direction": "outgoing",
    "event": "not_answered",
    "to": {"id": 100005},
    "purpose": "delivery",
    "date": 1790000123
  }
}
{
  "update_id": 81,
  "voice_call": {
    "id": "8812345678901234",
    "direction": "outgoing",
    "event": "started",
    "to": {"id": 100005},
    "purpose": "delivery",
    "date": 1790000130,
    "max_duration": 180
  }
}
{
  "update_id": 82,
  "voice_call": {
    "id": "8812345678901234",
    "direction": "outgoing",
    "event": "ended",
    "to": {"id": 100005},
    "purpose": "delivery",
    "date": 1790000250,
    "start_date": 1790000130,
    "duration": 120,
    "max_duration": 180,
    "end_reason": "callee"
  }
}
{
  "update_id": 84,
  "voice_call": {
    "id": "8812345678901234",
    "direction": "outgoing",
    "event": "failed",
    "to": {"id": 100005},
    "purpose": "delivery",
    "date": 1790000110
  }
}
event何时附加字段
started对方接听且通话接通时。在 Apple Watch 上接听的通话,是手表的声音接上时(接不上则为 0 秒的 ended、failed)max_duration
ended接听的通话结束时。无论服务器如何设置,都要等待网关的用量,最多晚 10 秒送达start_date、duration、max_duration、end_reason
not_answered未接听的请求——不区分响了没接、拒接、因接听者一方的判定而没有响铃。请求后约 ring_timeout + 12 秒无
failed受理请求的网关在接听前断开无

end_reason:callee(对方挂断)、bot(机器人挂断、令牌重新签发)、max_length(最长时长)、failed(接听后连接失败)、operator(运营方、紧急停止开关)、suspended(机器人或所有者被暂停)。机器人打出的电话没有 missed、refused、ringing——以免从几秒后接听推断对方是否在手机旁——也没有 transcript(设计上关闭)。

通话中

  • endVoiceCall:在告知结果之前,无论正在响铃还是尚未响铃,都会撤回请求(true,之后为 not_answered)。已接听的通话会被结束(end_reason bot)。请求后 15 秒内撤回,会让与此人的配对暂停 1 小时。
  • sendVoiceCallContext:只能在对方接听后使用(之前为 VOICE_CALL_NOT_FOUND)。每通 3 次,1,000 个字符、3,072 字节,内容规则与 context 相同。网关不接收备注时为 VOICE_CONTEXT_NOT_SUPPORTED。

接听者看到和听到的内容

  • 响铃:机器人名称后带“(AI)”,并显示“AI voice bot · Pabal never asks for codes”(AI 语音机器人 · Pabal 从不索要验证码)。
  • 接听后先播放提示语音:“This is a Pabal AI voice call. We never ask for passwords, codes or money transfers. To stop these calls, turn off call permission in the bot's chat.”(这是 Pabal 的 AI 语音电话。我们绝不会索要密码、验证码或转账。要停止来电,请在机器人的聊天中关闭来电许可。)——使用对方读过的许可窗口的语言(韩语或英语);其他语言的应用为英语。机器人所有者不能关闭它。
  • 机器人的第一句话:机器人的名称、是 AI,以及来电目的。context 在此之后使用。
  • 关闭的方法:菜单中的“Stop calls from this bot”(停止此机器人的来电)、拉黑机器人、举报,随时都能关闭。通话中明确说出“别再给我打电话”之类的话,也会当场关闭(只说“停”则取决于模型的判断)。
  • 暂停中:拒接、响了没接,或接听后 10 秒内挂断,该机器人 24 小时内不能再给此人打电话。
  • 未接来电记录:设备确实响了铃、对方没有拉黑机器人、且该机器人的聊天中没有未读消息时,会留下“Missed AI bot call”(AI 机器人未接来电——已读,无通知)。有未读消息,或因免打扰时段、暂停而没有响铃时,没有记录。机器人发出的每个请求都会无一遗漏地保留在举报证据中(30 天)。
  • 机器人所有者收到的内容(与许可窗口的顺序相同):
    1. 对方的 Pabal ID
    2. 许可是否开启(从来电请求的回答可以得知)
    3. 是否接听
    4. 通话开始时间和时长
    5. 谁挂断的
    未接与拒接无法区分。通话内容不会发出。
  • 共用设备:任何已登录的设备都可以接听;无论谁接,机器人都会说明目的和提示内容。多人共用的电脑请提醒大家退出登录。
  • 锁屏:在 Android 14 及以上,如果没有允许全屏通知,锁屏上会以通知而不是来电界面出现。iPhone 和 Apple Watch 会像电话一样响铃。
  • 保存对话:对方在许可窗口中勾选后,只保存在对方自己的聊天中。在 Pabal 手表应用中开启用手表接听后,Apple Watch 也会一起响铃。
  • 机器人打来的电话不是端到端加密的——声音会经过 Pabal 的服务器和 AI 提供方。

限额 · 免打扰时段 · 暂停 · 自动停止

限额默认在生产服务器上
一个机器人打给一个人(每天)3 通运营方只能调低
打给一个人(每天,所有机器人)6 通只能调低
打给同一人的间隔10 分钟只能调高
一个机器人的请求(每天)50 通所有者在 @BotFather 中从 10、20、50 中选择
一位机器人所有者(每天,所有机器人)100 通 · 30 分钟通数只能调低
机器人发出下一个请求前10 秒只能调高
服务器的外呼分钟数服务器每日分钟数的 30%只能调低
免打扰时段21:00–08:00运营方和接听者只能扩大。以设备时区为准
叫醒时段(仅 wake_up)最多 120 分钟接听者在免打扰时段内选择
响铃(ring_timeout)30–45 秒请求未指定时为 40 秒
暂停——拒接、响了没接、接听后 10 秒内挂断24 小时只能调高
暂停——机器人在 15 秒内撤回1 小时只能调高
  • “每天”指服务器时区(TELEGRAM_VOICE_TIME_ZONE)的一天。通话时长不超过 300 秒。
  • 机器人打出的电话也与打给机器人的电话共用每日通话时长(机器人 60 分钟、所有者 120 分钟)。用完后 startVoiceCall 返回 429,打给该机器人的电话也会以 refused(budget)被拒绝。
  • 暂停解除的时机:时间到了、接听者给该机器人打了电话、接听者点按“I can take calls now”(现在可以接听)、重新许可。机器人的消息或按钮不能解除暂停。
  • 自动停止只停止该机器人的外呼(403 VOICE_OUTBOUND_OFF: suspended),持续到运营方确认并解除为止。条件:最近 20 通的接听率低于 20%、7 天内被响铃过的人举报 3 次、关闭许可的人达到 30% 及以上(10 人及以上时)、通话中触犯规则 2 次、15 秒内撤回 5 次、接听的人不超过 2 人但接听分钟数达 60 分钟及以上。

限额

限额默认由谁设定
一通电话的最长时间机器人 300 秒(可设定范围为 60 ~ 运营方和服务器的上限,默认 600)机器人所有者在运营方和服务器上限内设定。@BotFather 的按钮为 1、3、5、10 分钟
机器人的同时通话数2(1 ~ 运营方和服务器的上限,默认 2)机器人所有者在运营方和服务器上限内设定
单个机器人的每日通话时长60 分钟运营方
单个机器人所有者(所有机器人合计)的每日时长120 分钟运营方
整个服务器的每日时长300 分钟运营方
单个来电者每天 60 分钟,每小时 20 通运营方
接听的最短通话60 秒——剩余时长少于这个值时不接听运营方
新账号注册不满 7 天的人不能拨打——测试者,以及机器人一天以前曾给其发过消息的人除外运营方
修改设置每个机器人每小时 30 次—
通话中的备注每通电话每分钟 10 条—
工具16 个、8KB;未完成的查询每通电话 2 个、每个机器人 8 个;每分钟每通电话 30 次、每个机器人 120 次—
自有密钥测试 · 音频流探测每个机器人每小时 5 次,每个所有者 10 次—
MCP 注册和检查每个机器人每小时 10 次,每个所有者 20 次,每个机器人 2 个服务器—

从 Apple Watch 不能给使用工具、MCP 服务器、自有密钥、音频流模式或机器人所有者文字记录的机器人打电话。这种拒绝只发给来电者(手表应用的 403 WATCH_NOT_YET),机器人不会收到 refused 更新。

一通电话的时长取来电者、机器人、所有者和服务器今天剩余时长以及通话上限中最小的那个。接听时会在四个限额中都预留这么多时长,结束后只按服务器时钟的实际秒数计费。自有密钥的通话不占用机器人、所有者和服务器的限额(来电者的限额照旧)。

错误

错误的形式与其他方法相同:{"ok": false, "error_code": 400, "description": "Bad Request: VOICE_…"}。403 的形式是 Forbidden: <代码>: <说明>(只有 USER_UNAVAILABLE 不带说明,始终是 Forbidden: USER_UNAVAILABLE),429 会附带 parameters.retry_after。

代码含义
403 VOICE_RULES_NOT_ACCEPTED需要先在 @BotFather 中同意语音机器人规则
403 VOICE_NOT_AVAILABLE在这台服务器上,这个机器人不能使用语音(语音已关闭,或运营方未允许)——不能修改设置,只能关闭和删除
403 VOICE_SUSPENDED运营方暂停了这个机器人的语音。只能关闭
403 VOICE_OWNER_TERMS_NOT_ACCEPTED需要先同意机器人所有者条款(第 4 版)
403 VOICE_OWN_KEY_NOT_AVAILABLE这台服务器不接受自有密钥
403 VOICE_STREAM_NOT_AVAILABLE这台服务器现在不能使用音频流模式
403 VOICE_OUTBOUND_OFF: <原因>这个机器人现在不能给人打电话:owner_off、terms、review、suspended、frozen、owner_name、mode,或语音设置中机器人一方的原因(拨打)。voice_off、kill_switch、no_relay、outbound_off 返回 503
403 USER_UNAVAILABLE无法拨打此人——包括没有许可在内,无论原因,响应体始终相同
503 VOICE_UNAVAILABLE服务器一方的原因,现在不能给人打电话(外呼开关、服务器的语音已关闭、紧急停止开关、没有通话中继、服务器限额、网关、请求记录失败)——请稍后再试
Bad Request: purpose is not declared、Bad Request: the bot's voice language must be ko or en for outbound callsstartVoiceCall:没选的目的、不是韩语或英语的机器人语言
VOICE_INSTRUCTIONS_REQUIRED, VOICE_INSTRUCTIONS_TOO_LONG, VOICE_GREETING_TOO_LONG需要指令 · 指令超过 8,192 字节 · 问候语超过 512 字节
VOICE_INVALID, VOICE_LANGUAGE_INVALID, VOICE_LIMIT_INVALID, VOICE_MODE_INVALID不存在的音色 · 语言 · 超出范围的限额 · 不是 managed/stream 的模式
VOICE_CONTENT_REFUSED: <字段>: <代码>、VOICE_CONTEXT_REFUSED: <代码>内容规则(设置、startVoiceCall 的 context)
VOICE_TOOL_INVALID: <路径>: <原因>工具定义(定义工具)
VOICE_FIELD_NOT_AVAILABLE: …不接受的字段(model、api_key),托管机器人上的音频流字段,或机器人打电话的字段(outbound、purposes、daily_cap——只能在 @BotFather 中设置)
VOICE_CALL_NOT_FOUND, VOICE_CALL_NOT_ACTIVE, VOICE_CONTEXT_NOT_SUPPORTED, VOICE_CONTEXT_TOO_LONG通话中的方法(通话中)
VOICE_USAGE_PERIOD_INVALIDgetVoiceCallUsage 的期间
TOOL_QUERY_ID_INVALID, TOOL_QUERY_EXPIRED: …, TOOL_RESULT_…, TOOL_ERROR_TOO_LONG工具的回答(回答)
API_KEY_INVALID, API_KEY_NO_REALTIME_ACCESS, 503 API_KEY_CHECK_UNAVAILABLE, VOICE_PROVIDER_INVALID自有密钥(自有模型密钥)
STREAM_URL_INVALID, STREAM_SECRET_REQUIRED, STREAM_SECRET_INVALID, STREAM_PROBE_FAILED: …音频流模式(设置与探测)
MCP_…, MCP_OPT_IN_REQUIREDMCP 服务器(注册)。503 VOICE_NOT_AVAILABLE: mcp: … 请稍后重试,409 MCP_CHECK_IN_PROGRESS 请在检查结束后重试
429 Too Many Requests: retry after N每小时限额(设置、密钥测试、探测、MCP)、备注限额或机器人打电话的限额(限额)

通过 MTProto 连接的机器人

  • 只通过 MTProto 登录的机器人,用 bots.sendCustomRequest 调用语音方法(方法名和同样的 JSON)。支持:setVoiceCallSettings、getVoiceCallSettings、answerVoiceCallToolQuery、endVoiceCall、sendVoiceCallContext、getVoiceCallUsage、startVoiceCall 以及五个 MCP 方法。其他方法返回 METHOD_INVALID。startVoiceCall 的回答是 {"voice_call_id": "…"},拒绝以 RPC 错误返回(USER_UNAVAILABLE、VOICE_OUTBOUND_OFF: review、FLOOD_WAIT_n、VOICE_UNAVAILABLE)。
  • 不能通过 Webhook 响应调用的方法:setVoiceCallApiKey、startVoiceCall,以及 MCP 的注册和密钥方法。放在 Webhook POST 的响应体中也不会执行——以免一个人发来的一条消息经由 Webhook 响应直接变成一通电话。请另外发请求调用。
  • 自有密钥只能通过 HTTPS:setVoiceCallApiKey·deleteVoiceCallApiKey 不能用 sendCustomRequest 调用。要删除密钥,请使用 HTTP Bot API 或 @BotFather。
  • 对于没有 Webhook、且最近 15 秒内没有调用 getUpdates 的机器人,voice_call(包括机器人打出的电话的更新)会以 updateBotWebhookJSON 推送一次(与 allowed_updates 无关,不重发)。工具查询以 updateBotWebhookJSONQuery 送达,用 bots.answerWebhookJSONQuery 回答 {"result": …} 或 {"error": "…"}。voice_call_transcript 不会通过 MTProto 送达(已结束更新中的 transcript 会送达)。
  • RPC 错误中,429 变为 FLOOD_WAIT_n,其余保持原来的大写代码。