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 配置参数

参数说明默认值
urlProvider 主端点(必需)-
headers附加 HTTP Headers无
max_retries最大重试次数3
retry_delay_ms重试间隔 (ms)1000
timeout_secsHTTP 超时 (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_responseAPI 调用返回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跳转到另一个 IVRivr_id: 目标 IVR
route_to_agent路由到坐席agent_id
voip_bridge桥接到外部 VoIPuri: SIP URI

非终端动作(执行后等待事件,再次调用 Provider)

type说明字段
prompt播放提示音/TTSfile / tts_text, interruptible, next
dtmf_menuDTMF 菜单prompt / tts_text, timeout_ms, max_retries, keys
collect_dtmf收集多位 DTMFnum_digits, timeout_ms, end_key, variable
input_phone收集电话号码max_digits, timeout_ms
input_voice语音输入timeout_ms, language
api调用外部 HTTP APIurl, 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 设置的 variable
  • torecord 设置的 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)