Step IVR 对接指南
Step IVR 是 RustPBX 的外部化 IVR 模式:每个呼叫步骤通过 HTTP POST 调用你的 Provider API,由外部系统决定下一步操作。适用于需要动态逻辑、数据库查询、AI 决策等场景。
1. 工作原理
来电进入 IVR
│
▼
RustPBX 调用 POST /ivr/start(通知新会话)
│
▼
RustPBX 调用 POST /ivr/step(携带事件)
│
▼
Provider 返回 ActionNode(下一步操作)
│
├── prompt(播放提示音/TTS)→ 等待事件 → 再次 POST /ivr/step
├── dtmf_menu(收集按键)→ 等待 DTMF → POST /ivr/step
├── collect_dtmf(收集多位按键)→ POST /ivr/step
├── input_voice(语音输入)→ POST /ivr/step
├── api(调用外部 API)→ POST /ivr/step(携带 api_response)
├── torecord(录音)→ POST /ivr/step(携带 recording_complete)
├── transfer(转接)→ 结束
├── hangup(挂断)→ 结束
└── queue/voicemail(进队列/信箱)→ 结束
结束时会调用 POST /ivr/end(fire-and-forget)。
2. 配置
2.1 路由配置
在路由规则中将呼叫指向 Step IVR:
[[route]]
name = "to-ivr"
action = "application"
application = "ivr:smart-ivr"
[route.match]
to_user = "^4000$"
2.2 IVR 定义
创建 config/ivr/smart_ivr.toml:
[ivr]
name = "smart-ivr"
ivr_mode = "step"
[ivr.provider]
url = "http://10.0.0.50:8080/ivr/step"
max_retries = 3
retry_delay_ms = 1000
timeout_secs = 10
[ivr.provider.headers]
Authorization = "Bearer my-secret-key"
X-App-Id = "my-ivr-app"
# 可选:TTS 配置(用于 tts_text 字段)
[ivr.tts]
enabled = true
2.3 Provider 配置参数
| 参数 | 说明 | 默认值 |
|---|---|---|
url | Provider 主端点(必需) | - |
headers | 附加 HTTP Headers | 无 |
max_retries | 最大重试次数 | 3 |
retry_delay_ms | 重试间隔 (ms) | 1000 |
timeout_secs | HTTP 超时 (s) | 10 |
2.4 兜底恢复与本地 Step 端点(0.5+)
兜底恢复:当外部 Provider 无法继续服务(不可达、连续失败)时,RustPBX 可以把呼叫转入本地 IVR,而不是直接挂断。规则按 priority 降序评估,匹配语义与拨号路由一致:
[proxy.ivr_fallback]
default = "default"
[[proxy.ivr_fallback.rules]]
name = "vip"
priority = 100
match = { "from.user" = "^9" }
target = "builtin_vip_step"
部署本地 Step 端点:IVR Provider 的 URL 可以指向部署自身(主机占位符本地解析),同一套 IVR 树无需修改即可运行在任意节点。
TTS 提示音别名:第三方 IVR 树可以按别名(tts_text 名称)引用提示音,由部署的 TTS 配置解析,不再硬编码引擎专属字符串。
3. 协议详解
3.1 ProviderContext(RustPBX → Provider)
每次 POST 请求体:
{
"session_id": "call_abc123",
"caller": "1001",
"callee": "4000",
"direction": "inbound",
"tenant_id": "default",
"ivr_id": "smart-ivr",
"variables": { "key": "value" },
"sip_headers": { "X-Custom": "value" },
"event": { "type": "session_start" }
}
3.2 ProviderEvent 类型
| event.type | 触发时机 | 附加字段 |
|---|---|---|
session_start | 会话开始(首次调用) | - |
resume(0.5+) | 挂起流程的继续——voip_bridge 结束且无缓冲按键 | resume_from_step_id: 恢复起点步骤 |
dtmf | 用户按键 | digit: 按键值 |
dtmf_timeout | 按键超时 | - |
audio_complete | 音频播放完成 | interrupted: 是否被打断 |
api_response | API 调用返回 | status: HTTP 状态码, body: 响应体 |
phone_collected | 号码收集完成 | number: 收集到的号码 |
recording_complete | 录音完成 | url: 录音地址, duration_secs: 时长 |
input_voice | 语音识别结果 | text: 识别文本, confidence: 置信度 |
error | 错误 | reason: 错误原因 |
dtmf_menu_invalid | 无效按键 | digit: 按键值 |
dtmf_menu_timeout | 菜单超时 | - |
resume:是继续,不是重新进入(0.5+)
当 Step 流程在 voip_bridge(如咨询转)上挂起、呼叫返回且无缓冲按键时,RustPBX 发送显式的 resume 事件并携带 resume_from_step_id——而不是重放第二次 session_start。逻辑流程的 session_start 只在其真正的首次进入时发出一次;Provider 必须从引用的步骤恢复(还原变量),而不是重播菜单。这堵住了“菜单重放“漏洞——呼叫者不会重复听到欢迎语、之前的输入也不会被再次询问。
end 追踪还携带结构化的 end_reason,Provider 无需靠“事件缺失“去猜测流程为何终止(完成/转接/失败)。
3.3 ActionNode(Provider → RustPBX)
终端动作(执行后 IVR 结束)
| type | 说明 | 字段 |
|---|---|---|
transfer | 转接到分机/队列/外部号码 | target: 目标 |
hangup | 挂断 | - |
queue | 进入排队 | queue: 队列名 |
voicemail | 进入语音信箱 | extension: 分机号 |
play_and_hangup | 播放提示后挂断 | file / tts_text |
jump_ivr | 跳转到另一个 IVR | ivr_id: 目标 IVR |
route_to_agent | 路由到坐席 | agent_id |
voip_bridge | 桥接到外部 VoIP | uri: SIP URI |
非终端动作(执行后等待事件,再次调用 Provider)
| type | 说明 | 字段 |
|---|---|---|
prompt | 播放提示音/TTS | file / tts_text, interruptible, next |
dtmf_menu | DTMF 菜单 | prompt / tts_text, timeout_ms, max_retries, keys |
collect_dtmf | 收集多位 DTMF | num_digits, timeout_ms, end_key, variable |
input_phone | 收集电话号码 | max_digits, timeout_ms |
input_voice | 语音输入 | timeout_ms, language |
api | 调用外部 HTTP API | url, method, headers, body |
torecord | 录音 | max_duration, silence_timeout, variable |
3.4 动作链(next 字段)
非终端动作可通过 next 字段链式组合,减少 HTTP 往返:
{
"type": "prompt",
"tts_text": "请稍候",
"next": {
"type": "api",
"url": "http://crm/query",
"next": {
"type": "dtmf_menu",
"tts_text": "查询完成,按1继续",
"timeout_ms": 5000
}
}
}
3.5 变量替换
ActionNode 中的字符串支持 $var_name$ 变量替换:
{
"type": "transfer",
"target": "$collected_number$"
}
变量来源:
collect_dtmf/input_phone设置的variabletorecord设置的variable(录音 URL)api的响应存入api_response变量
4. 会话生命周期端点
除了主 url,RustPBX 还会调用两个通知端点(fire-and-forget):
| 端点 | 说明 | URL 规则 |
|---|---|---|
/start | 会话开始 | {url}/start(url 末尾追加 /start) |
/end | 会话结束 | {url}/end(url 末尾追加 /end) |
这两个端点都是 POST,请求体与主端点相同。Provider 可以在此初始化/清理会话状态。
5. 错误处理
Provider 不可达时的行为:
| 参数 | 说明 | 默认值 |
|---|---|---|
max_retries | 重试次数 | 3 |
retry_delay_ms | 重试间隔 | 1000 |
重试耗尽后执行 fallback 动作(如未配置则挂断)。
6. Python Provider 示例
from http.server import HTTPServer, BaseHTTPRequestHandler
import json, time
class IvrProvider(BaseHTTPRequestHandler):
sessions = {}
def do_POST(self):
body = json.loads(self.rfile.read(int(self.headers.get("Content-Length", 0))))
sid = body.get("session_id", "")
if self.path.endswith("/start"):
self.sessions[sid] = {"step": "welcome"}
self._json(200, {"status": "ok"})
return
if self.path.endswith("/end"):
self.sessions.pop(sid, None)
self._json(200, {"status": "ok"})
return
# Main step endpoint
event = body.get("event", {"type": "session_start"})
session = self.sessions.get(sid, {"step": "welcome"})
ev_type = event.get("type", "")
if session["step"] == "welcome":
session["step"] = "menu"
self._json(200, {
"type": "prompt",
"tts_text": "欢迎致电,按1查询余额,按2转人工",
"interruptible": True
})
elif ev_type == "dtmf" and event["digit"] == "1":
self._json(200, {"type": "play_and_hangup", "tts_text": "您的余额为100元"})
elif ev_type == "dtmf" and event["digit"] == "2":
self._json(200, {"type": "transfer", "target": "agent"})
else:
self._json(200, {"type": "hangup"})
def _json(self, status, data):
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(json.dumps(data).encode())
HTTPServer(("0.0.0.0", 8080), IvrProvider).serve_forever()
6.1 测试
# 模拟 session_start
curl -X POST http://localhost:8080/ivr/step \
-H "Content-Type: application/json" \
-d '{"session_id":"test","event":{"type":"session_start"},"caller":"1001","callee":"4000"}'
# 模拟按键 1
curl -X POST http://localhost:8080/ivr/step \
-H "Content-Type: application/json" \
-d '{"session_id":"test","event":{"type":"dtmf","digit":"1"},"caller":"1001","callee":"4000"}'
7. 与 Tree 模式对比
| 特性 | Tree 模式 | Step 模式 |
|---|---|---|
| 配置方式 | TOML 文件(静态) | HTTP API(动态) |
| 适用场景 | 固定菜单结构 | 动态逻辑、AI 决策 |
| 延迟 | 无(本地执行) | 每步一次 HTTP 往返 |
| 可视化编辑 | IVR Editor 支持 | 不支持 |
| 变量/状态 | 有限 | 完全自定义 |
| TTS | 支持 | 支持(通过 tts_text) |