开源、符合HIPAA要求的 Twilio 替代方案
Source: Dev.to
注: 如需最新的设置说明,请参阅仓库中的open-telephony-stack/README.md。
为什么我们构建了它
去年夏天,我们为医疗机构构建 AI 语音代理。我们需要:
- 拨打和接听电话
- 实时流式音频
- 保持 HIPAA 合规
Twilio 看起来是显而易见的选择——直到我们遇到付费墙:每月 2,000 美元 的业务关联协议(BAA),而且在打出第一通电话之前就要付费。对于一家初创公司来说,这个价格是难以承受的。
于是我们自行搭建了技术栈:
| 组件 | 功能 |
|---|---|
| Asterisk | 开源 PBX(Docker 化) |
| AWS Chime SDK | SIP 中继和电话号码 |
| FastAPI shim | 将传统电话系统桥接到现代 WebSocket API |
最终得到的是一个 完整且安全的电话系统,能够处理入站 和 出站电话:
- 通过 AWS Chime Voice Connector(真实 PSTN 号码)接收来电
- 在 Asterisk(Docker)上终止 SIP/TLS
- 通过 RTP 将音频桥接到 WebSocket 连接
- 将 base64 μ‑law 音频流式传输到你的 AI 语音服务器
- 提供一个 类似 Twilio 的 WebSocket API(参考 Twilio Media Streams)
你只需提供自己的 AI;这套栈仅负责电话基础设施。
用例示例
| 场景 | 此技术栈的优势 |
|---|---|
| Healthcare AI – 需要 HIPAA 合规但不想承担 Twilio 的 BAA 成本 | 无需额外合规费用;您掌控数据 |
| Custom call handling – Twilio 限制了您 | 对拨号计划、媒体路由等拥有完整控制 |
| Full stack ownership – 想拥有每一层 | 自托管、开源、无供应商锁定 |
| Learning/experimenting – 了解电话系统内部 | 端到端,从 PSTN 到 WebSocket,全部代码实现 |
如符合以下情况,请考虑其他方案:
- 您仅需要用于副项目的基础语音功能(Twilio 更简单)。
- 您不想管理基础设施。
- 您没有特殊的合规要求。
权衡: 管理此技术栈需要时间和持续的维护。
Service ports
| 服务 | 端口 | 协议 | 描述 |
|---|---|---|---|
| Asterisk SIP | 5061 | TCP/TLS | 使用 AWS Chime 的 SIP 信令 |
| Asterisk ARI | 8088 | HTTP | Asterisk REST 接口(仅限本地主机) |
| Shim server | 8080 | HTTP | FastAPI 服务器,健康检查端点 |
| RTP media | 10000‑10299 | UDP | 与 Asterisk 的音频流 |
架构概览
1. AWS Chime Voice Connector
- PSTN 网关。您在此处配置电话号码。
- 呼叫以 SIP/TLS 方式在端口 5061 到达。
2. Asterisk PBX (Docker)
- 处理 SIP 信令、RTP 媒体、呼叫路由。
- 使用 ARI(Asterisk REST 接口)而非传统的拨号计划脚本。
3. Shim server (FastAPI)
| 功能 | 详情 |
|---|---|
| 通过 ARI WebSocket 连接到 Asterisk | 接收 StasisStart 事件 |
| 创建 ExternalMedia 通道 | 将 RTP 桥接到您的 AI 语音服务器 |
| 维持 20 ms RTP 节奏 | 隔离 WebSocket 抖动 |
| 转发音频 | 将 Base64 μ‑law 负载发送至下游语音服务器 |
4. 您的 AI 语音服务器
- 任意能够使用 Twilio 兼容的 WebSocket 媒体格式的服务器(例如 OpenAI Realtime、AWS Nova Sonic、定制 ASR/TTS)。
- 示例实现:
open-telephony-stack/src/servers/voice_agent_server.py.
DNS & TLS 设置
DNS 记录(在 TLS 之前必需)
| Record type | Name | Value | TTL |
|---|---|---|---|
| A | sip.yourdomain.com | 您的弹性 IP(例如 54.123.45.67) | 300(或默认) |
在以下操作之前创建此 A 记录:
- 请求 Let’s Encrypt 证书(Certbot 验证域所有权)
- 配置 AWS Chime Voice Connector 终止(Chime 必须解析主机名)
- 在
pjsip.conf中设置external_signaling_address(必须与 DNS 名称匹配)
添加记录后,等待传播(几分钟到 48 小时)。使用以下方式验证:
dig sip.yourdomain.com
# or
nslookup sip.yourdomain.com
使用 Let’s Encrypt 的 TLS
- Certbot 在 EC2 实例上运行,绑定到 80 端口。
- 证书颁发给
sip.yourdomain.com。 - Asterisk 通过 Docker 卷挂载从
/etc/letsencrypt/live/...读取证书。 - 续订钩子在证书轮换时重新加载 Asterisk。
- Chime 使用 Let’s Encrypt 的 CA 根证书进行验证——无需自签名证书、手动续订,也不会出现意外过期。
呼叫流程(当有人拨打您的号码时)
-
呼叫方 拨打您的 AWS Chime 电话号码。
-
Chime 向您的 Asterisk 服务器发送 SIP INVITE(
TLS:5061)。 -
Asterisk 在
extensions.conf中匹配呼叫Answer() Stasis(voice-agent) -
ARI 通过 WebSocket 向 shim 服务器发送
StasisStart事件。 -
Shim 服务器 执行以下步骤:
a. 打开到您的语音服务器的 WebSocket。
b. 创建一个 ARI 混音桥。
c. 将 PSTN 通道添加到桥中。
d. 为 RTP 分配一个 UDP 端口(10000‑10299;每个实时通话使用独立端口)。
e. 创建指向该端口的 ExternalMedia 通道。
f. 将 ExternalMedia 通道添加到桥中。 -
音频流向:
PSTN ↔ Bridge ↔ ExternalMedia ↔ Shim (RTP) ↔ Voice Server (WSS) -
呼叫结束(呼叫方挂断 或 AI 通过 ARI 工具调用结束通话):
- ARI 发送
ChannelHangupRequest/ChannelDestroyed。 - Shim 清理:关闭 WebSocket,删除桥,释放 RTP 端口。
- ARI 发送
配置文件
所有配置文件位于 deployment/asterisk-server/asterisk-config/ 目录下。Docker 容器会挂载此目录。
pjsip.conf – SIP 中继配置
这是最重要的文件。 它定义了指向 AWS Chime 的 SIP 中继,包括传输设置、TLS 证书、入站/出站端点,以及必须与您创建的 DNS 名称匹配的
external_signing_address。
(仓库的其余部分包含其他配置文件、Docker Compose 文件和示例脚本。)
AWS Chime SDK + Asterisk Shim – 快速入门指南
以下是原始 markdown 的清理版。所有标题、代码块、表格和项目符号均已重新排版以提升可读性,同时保留了原始内容。
概览
| 文件 | 用途 |
|---|---|
| pjsip.conf | SIP 传输、TLS 设置以及 Chime Voice Connector 主机。 |
| extensions.conf | 最小化拨号计划 – 将来电路由到 ARI Stasis 应用 (voice‑agent)。 |
| ari.conf | Asterisk REST 接口 (ARI) 的凭证。 |
| http.conf | 内置 HTTP 服务器用于 ARI(绑定到 localhost 以提升安全性)。 |
| rtp.conf | UDP 端口范围,用于 RTP 媒体流(默认 10000‑10299)。 |
| modules.conf | 仅加载所需模块:PJSIP、ARI 与 μ‑law 编码器。 |
注意
external_signaling_address必须与您的 DNS 名称 以及 TLS 证书保持一致。local_net用于告诉 Asterisk 哪些是 “内部” 与 “外部”,以便进行 NAT 处理。verify_server=no因为 Chime 不 提供客户端证书。- cert/key 文件是 Asterisk 在 TLS 握手期间向 Chime 提供的证书。
前置条件
- AWS 账户
- EC2 实例(推荐
t3.medium或更高,Amazon Linux 2023) - 弹性 IP(绑定到 EC2 虚拟机,Chime Voice Connector 需要静态 IP)
- 域名,其 A 记录指向弹性 IP
- Docker 与 Docker Compose 已在实例上安装
配置 Chime Voice Connector
- 打开 AWS Chime SDK 控制台。
- 创建一个 Voice Connector(或编辑已有的)。
| 设置 | 值 |
|---|---|
| Host | sip.yourdomain.com |
| Port | 5061 |
| Protocol | TLS |
- 记录 Voice Connector 主机名——稍后在
pjsip.conf中需要使用它。
获取 TLS 证书(Let’s Encrypt)
# 安装 certbot
sudo yum install -y certbot
# 申请证书(需要开放 80 端口)
sudo certbot certonly --standalone \
--preferred-challenges http \
-d sip.yourdomain.com \
--agree-tos -m your@email.com
# 启用自动续期
sudo systemctl enable --now certbot-renew.timer
# 创建一个续期钩子,在 Docker 中重新加载 Asterisk
sudo mkdir -p /etc/letsencrypt/renewal-hooks/deploy
sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-asterisk.sh > /dev/null
该仓库还提供了一个 Lambda 函数,可在 AWS 为服务 AMAZON、EC2 与 CHIME_VOICECONNECTOR 发布新 IP 范围时自动更新安全组。
部署 Asterisk 服务器(Docker)
# 切换到 Docker 部署目录
cd deployment/asterisk-server
# -------------------------------------------------
# 根据需要编辑配置文件:
# - pjsip.conf : 域名、证书路径、voice‑connector 主机
# - ari.conf : 安全的 ARI 用户名/密码
# - rtp.conf : 如有需要,调整端口范围
# -------------------------------------------------
# 启动 Asterisk 容器
docker-compose up -d
# 查看日志
docker logs -f asterisk-server
# 打开交互式 Asterisk CLI
docker exec -it asterisk-server asterisk -rvvvvv
准备 Shim 服务器
创建 .env 文件
cat > .env <<'EOF'
ARI_BASE=http://127.0.0.1:8088/ari
ARI_USER=ariuser
ARI_PASS=your-secure-password-here
ARI_APP=voice-agent
EXTERNAL_MEDIA_HOST=127.0.0.1
ECS_MEDIA_WSS_URL=wss://your-voice-server.internal/voice/voice
RTP_PORT_START=10000
RTP_PORT_END=10299
EOF
构建并运行 Shim
# 构建 shim Docker 镜像
docker build -t asterisk-shim -f deployment/shim-server/Dockerfile .
# 运行 shim(使用 host 网络,以便绑定 RTP 端口)
docker run -d --env-file .env --network host --name asterisk-shim asterisk-shim
# 验证 shim 健康检查端点
curl http://localhost:8080/health
测试端到端流程
- 拨打您的 AWS Chime 电话号码(即分配给该 Voice Connector 的号码),
- 确认来电能够通过 SIP/TLS 进入 Asterisk,并在 ARI
voice-agent应用中被接收, - 检查媒体是否通过 RTP 端口成功传输,
- 如有需要,使用
docker exec -it asterisk-server asterisk -rvvvvv查看详细日志并进行故障排除。
Source: …
e Voice Connector).
2. 监视日志:
# Asterisk logs (SIP/RTP activity)
docker logs -f asterisk-server
# Shim server logs (session lifecycle)
docker logs -f asterisk-shim
您应该会看到类似以下的条目:
INVITE received
CallSession created
ExternalMedia channel established
WebSocket API(Shim ↔ 语音服务器)
该 API 镜像 Twilio Media Streams —— 具有相同的事件结构和 μ‑law 音频格式。
音频格式
| 属性 | 值 |
|---|---|
| 编解码器 | μ‑law (PCMU) |
| 采样率 | 8000 Hz |
| 帧大小 | 160 bytes (20 ms) |
| 编码 | Base64 |
事件负载
start(shim → 语音服务器)
{
"event": "start",
"start": {
"streamSid": "unique-stream-id",
"callSid": "asterisk-channel-id",
"customParameters": {
"source": "asterisk-shim",
"format": "ulaw"
}
}
}
media(双向)
{
"event": "media",
"streamSid": "unique-stream-id",
"media": {
"payload": "base64-encoded-ulaw-audio",
"timestamp": 1234
}
}
clear(语音服务器 → shim)
{ "event": "clear" }
mark(双向)
{
"event": "mark",
"streamSid": "unique-stream-id",
"mark": { "name": "responsePart" }
}
stop(任意方向)
{
"event": "stop",
"streamSid": "unique-stream-id"
}
示例实现
一个最小示例 voice_agent_server.py 已包含在仓库中。它演示了:
- 处理上述 WebSocket 事件
- 实时音频处理