
Asterisk 的拨号计划Dialplan是所有 VoIP 管理员迟早要啃的硬骨头。我第一次接触它的时候面对/etc/asterisk/extensions.conf里那一堆exten 的行脑子里全是问号这到底是在写脚本还是在配电话交换机后来静下心逐个应用去试才发现真正的入门钥匙根本不是Dial()那类复杂应用而是最不起眼的三个基础应用Answer()、Playback()、Hangup()。毫不夸张地说把这三个应用吃透Dialplan 的骨架就立起来了后面再学路由、排队、IVR 只是往骨架上添肌肉。这篇文章就围绕这三个应用展开讲清楚它们各自干什么、为什么需要它们、组合起来能搭建什么再给出一套可以直接照着抄的完整配置和调试方法。适合刚接触 Asterisk 的运维新人、准备做电话机器人的开发者以及想系统梳理流程细节的入门者。我把原理、语法、踩坑经历全部揉在一起你跟着走一遍至少能独立写出第一个能跑通的自动应答流程。1. 先把地图摊开Dialplan 到底在解决什么问题1.1 从一次电话通话看 Asterisk 的角色想象一次最普通的呼叫用户拿起话筒拨了个号码。这通电话到了 Asterisk 之后会发生什么Asterisk 本身不会平白无故知道“该放语音”“该转给谁”“该挂断”它只有一套机制把通话交给 Dialplan 去执行。Dialplan 就是 Asterisk 的剧本每一通进来的呼叫都要按剧本往下走。这个剧本写在extensions.conf里由一个个“上下文context”组成。每个上下文相当于一个权限边界或一个功能场景比如[internal]管内部电话、[incoming]管外线来电。上下文里面躺着若干“分机extension”但这里的 extension 不只是电话号码它也可以是失配时的兜底标签s、超时标签t、无效按键标签i。每个 extension 后面跟一串优先级priority优先级从 1 开始用n表示“下一条”。Asterisk 按照“上下文 → 分机 → 优先级”的顺序逐条执行像是读一张写满指令的卡片。为什么理解这个模型很重要因为后续所有业务逻辑IVR、队列、语音信箱、外呼计划本质上都是在往这张卡片上添加指令行。如果你连最基本的执行顺序都搞不清后面一旦出现“明明写了播放却听不到声音”“电话一接就断”这类问题排查起来会特别痛苦。先把这个运行模型刻进脑子里再看具体应用就顺多了。1.2 为什么这仨应用是“最小开场组合”先问一个问题一通电进来Asterisk 最少要做哪几件事才算“有意义地处理”了答案是三件接通Answer→ 干点事Playback→ 收尾Hangup。接通用Answer()干的事先用最朴素的“放一段语音”也就是Playback()最后用Hangup()把通话资源释放掉。这三步拼起来就是所有电话流程的最小闭环。有朋友可能会说“我不用 Answer 直接 Playback 也能放声音啊。” 这种情况确实存在但结果不可控在某些通道上未接通时播放会导致音频路径没有完全建立拨号方听到的是半截话或者压根没声音更重要的是没有 Answer 时通道状态没有正确切换计费、话单、后续转接都会出问题。所以规范做法永远是先 Answer再操作最后 Hangup。这也是为什么官方示例几乎都是这个套路。再看Hangup()。很多人会觉得“挂断还需要专门写一个应用” 其实需要尤其是你要控制挂断原因码、要在挂断前执行清理操作的时候。就算不写它Dialplan 走到结尾 Asterisk 也会自动挂断但显式调用能让你掌握主动权话单上的原因码也更干净。这个三件套组合本质上是让你用最少的语法建立对通道生命周期——建立、使用、释放——的完整感知。1.3 配置文件的三个组成部分general、globals 与上下文打开extensions.conf通常会看到三个区域[general]全局设置区比如staticyes表示只允许通过文件修改拨号计划禁止在 CLI 里动态改。[globals]全局变量定义区可以定义类似EXT1000的变量后面用${EXT}引用。各业务上下文[incoming]、[default]、[internal]这类方括号片段真正放执行指令的地方。新手最容易犯的一个错是把[general]和[globals]当成普通上下文往里写路由结果发现怎么都不执行。正确理解是前两个是“元信息”只有业务上下文才是通道真正会进入的剧本。一个通道最终会进入哪个上下文通常由上游通道配置决定。比如 SIP 分机或中继配置里会指定contextincomingPBX 收到来电后就把呼叫交给[incoming]处理。如果这个映射没配好Dialplan 写得再漂亮也不会被触发。这也是很多人“配置了却没反应”的第一大原因——脚本没问题但通道根本就没走进来。2. 三个基础应用逐个拆开看2.1 Answer()接通动作背后的信号顺序Answer()的语法极简就是exten s,1,Answer()没有参数。它做的事情是把当前通道从未应答状态切换到已应答状态。对于 SIP 通道而言这一步等价于向对端回送200 OK响应同时建立双向媒体流对于模拟 FXS/FXO 通道则是真正摘机。这里有个关键点Answer 不是瞬间完成的也不会阻塞很长时间。执行完后Dialplan 立刻执行下一行。如果需要给对方一点反应时间常规做法是接一行Wait(1)或Wait(2)这在播放提示音之前尤其常用。Answer()还有返回值的概念成功返回 0失败返回 -1。虽然多数场景下你不需要关心返回值但在调试时通过core show application answer看帮助文档能确认细节。实际操作里要注意如果通道已经处于应答状态再次调用Answer()不会报错只会安静地立即返回。所以别担心多写一行 Answer 会把通话搞坏它是个幂等性很好的操作。另一个实际经验某些外呼场景下B 端电话振铃后由 Asterisk 发起呼叫并等待被叫应答这时候“接通”发生在被叫通道上而不是主叫通道。如果你在 Dialplan 里对主叫先执行 Answer行为会和来电场景略有差异。这也是为什么建议先跑通一个最简单的来电场景再考虑外呼。2.2 Playback()播音员的工作方式Playback()的核心作用就是播放一个或多个语音文件语法是exten s,n,Playback(/路径/文件名)需要注意Playback()执行期间不做任何按键收集。也就是说如果对方在播放中途按了数字键这些 DTMF 信号要么被忽略要么被暂存到通道里但 Dialplan 不会因此跳转。这和后面要讲的Background()有本质区别Background()是边放音边监听按键Playback()是只顾着放完为止。它还支持一次播多个文件用连接Playback(hello-worldbeep)Asterisk 会顺序播放。返回值同样是 0 或 -1放完正常返回 0在播放过程中通道挂断则返回 -1。这个返回值在做“挂断检测”时有用比如播放中途用户挂断你想立刻停止后续流程。文件路径上有个容易踩的坑不要随便加扩展名也不要写错路径基准。Asterisk 会基于系统声音目录/var/lib/asterisk/sounds去查找文件。比如写Playback(hello-world)它会尝试在声音目录下找hello-world.gsm、hello-world.wav、hello-world.ulaw等格式中的一种并选一个与当前通道编码匹配的进行播放。如果你自己放了一个/opt/mysounds/promo.wav路径要写完整且权限必须让 Asterisk 运行用户能读。个人经验是先用系统自带的hello-world、beep这类文件跑通链路再换自定义音频这样能把“配置问题”和“素材问题”分开排查。2.3 Hangup()如何优雅地结束通话Hangup()语法为exten s,n,Hangup(16)括号里的数字是挂断原因码cause code16表示“正常挂断Normal Clearing”这是最常用的值。如果你不传参数Asterisk 会使用默认原因码最终体现在话单或对端信令提示里可能不够精确。为什么要手动指定原因码因为在真实的对接场景里中继对端比如运营商或对方 IP-PBX会依据原因码判断通话是“正常结束”“被叫忙”“被叫无应答”还是“号码不存在”。如果你把一通报了忙的音的呼叫挂断原因码写成 16对端可能误以为用户已经正常说完挂了反过来如果明明是正常咨询转接结束却给了个 “17用户忙”对方话单统计就会很混乱。所以业务侧一旦出现状态区分需求显式挂断原因码就很有价值。另外Asterisk 还支持h扩展hangup handler也就是“挂断处理”上下文。当通话挂断时无论它是在 Dialplan 什么位置结束的Asterisk 都会执行当前上下文里h扩展下的指令。常见用途是记录通话时长、清除资源、写日志。注意h扩展的优先级从 1 开始写里面一般不放 Answer 或 Playback而是放记录类应用。这能让你把“挂断后的清理动作”统一收口代码结构会清爽很多。2.4 三个应用常用的可选参数与返回值细节Answer()本身没有参数但配合它的常用应用是Wait()。Wait()的多寡靠手感测试环境 1 秒足够真机环境考虑到链路建立时间2 到 3 秒更稳但也不要过长否则用户会觉得“接通了没动静”。Playback()带一个可选的第二个参数options在实际生产里比较有用的是skip如果通道尚未应答则跳过播放。适合某些优先放音再应答的广告场景。noanswer播放时不改变通道的应答状态也就是“前置播放”模式。这两个选项背后的思路是有时候你想在真正接通前先放一段“来电彩铃”或者品牌语音这时noanswer和skip就能灵活控制。Hangup()的可选参数就是原因码。常用原因码我整理成一张速查表原因码含义典型场景16正常挂断通话自然结束17用户忙被叫占线18用户无应答振铃超时无人接21呼叫被拒绝对方主动拒接27目的地不可达中继/号码不可用31未指定错误通用异常把原因码写对能省下很多和对方技术对接时“你们挂断码是啥”的扯皮时间。3. 实操搭建从零写一个能跑起来的 Dialplan3.1 环境准备和文件基本盘先交代一下环境。我这边用的是社区常见发行版里的 Asterisk版本号在 16 到 20 之间均可本文语法适用配置主文件统一在/etc/asterisk/目录。Dialplan 文件就是extensions.conf如果发行版默认把它拆分成多个*.conf通常在文件头部有#include语句你只需要往对应片段里加内容。在动手前建议先确认 Asterisk 服务状态。命令行执行sudo systemctl status asterisk正常情况下能看到服务处于 active 状态。如果你是从源码编译安装的那更熟悉一些直接用asterisk -c前台跑也行。为了调试方便我强烈建议在配置阶段直接连上 CLI 控制台sudo asterisk -rvv-r表示连接运行中的实例-vv提高详细程度。这一步后面排查问题时几乎天天用。3.2 第一个完整流程接听-播放-挂断先在extensions.conf里新增一个上下文我习惯起名叫[lab]表示测试实验区避免动到生产上下文[lab] exten s,1,Answer() same n,Wait(1) same n,Playback(hello-world) same n,Playback(beep) same n,Hangup(16)这段脚本的意思非常直白进来就接听等 1 秒播一句“hello world”问候再播一声“beep”提示音然后正常挂断。这里用到的是s扩展也就是无论呼叫方拨了什么号码只要进了[lab]上下文都执行这段流程。适合做“来电总机测试”。如果你的场景是“用户拨某个特定分机号才触发”就把s换成具体号码比如6001[cnlab] exten 6001,1,Answer() same n,Playback(hello-world) same n,Hangup(16)两种写法的区别只在匹配条件s是最强兜底6001则要求呼叫匹配到对应分机号。用哪个取决于业务入口后续做 IVR 时两种都会用到。写完保存文件后必须重新加载 Dialplan才能生效CLI dialplan reload再验证一下上下文是否加载成功CLI dialplan show cnlab看到输出里列出6001和对应优先级说明配置已经被 Asterisk 接受。这时候用软电话呼入绑定的上下文配成cnlab拨打6001应该就能听到完整流程。3.3 语音文件准备与格式选择系统自带语音在/var/lib/asterisk/sounds/下里面分en/、zh_CN/等语言目录大部分发行版会装默认的英文提示音。你可以先列一下ls /var/lib/asterisk/sounds/en/ | head如果要用中文提示音有些发行版需要额外安装语言包或者直接自己录音转格式。我个人常用的方案是用任意录音工具录一段 WAV16bit、PCM、采样率 8kHz 或 16kHz 均可Asterisk 能自动转码放到/var/lib/asterisk/sounds/zh/目录然后在 Dialplan 里写exten s,n,Playback(zh/欢迎致电)注意文件名不要带中文也可以ASCII 命名更稳比如zh/welcome-msg。文件权限要为 asterisk 用户可读否则播放时会报 “Unable to open” 的日志。格式上Asterisk 会在多个编码之间挑一个最合适的来播所以如果你的系统里有wav、gsm两种版本它能按当前通话编码自动选择。我的建议是至少保留一份wav16bit PCM和一份gsm前者音质好、后者体积小覆盖绝大多数通道。3.4 用 CLI 验证和应用信息查询想确认一个应用具体有什么参数与其翻文档不如直接在 CLI 里查CLI core show application answer CLI core show application playback CLI core show application hangup日常调试里还有两个命令很重要dialplan show 上下文名列出该上下文的所有分机和优先级。core set verbose 3临时把日志级别调高配合-vvv连接能看到 Dialplan 每一行执行的详细过程。举一个排查现场的例子。有一次我写了个流程明明已经dialplan reload但呼入后就是静音。连上 CLI 用-rvvv观察发现日志里根本没有走到Answer()而是卡在 “No application answr found” 这样的报错上。回看文件才发现拼写少了个 e。这种问题靠肉眼看配置几乎不可能发现直接在 CLI 里跑一遍立即原形毕露。所以我的习惯是改完配置先 reload再开 verbose再呼入测试三步一气呵成。3.5 进阶一步用 Background 和 DTMF 拼出最小 IVR掌握了三件套后最自然的下一步就是做一个能让用户按键选择的最小 IVR。这里把Playback()换成Background()[ivr] exten s,1,Answer() same n,Background(zh/press-1-for-sales) same n,WaitExten(3) exten 1,1,Playback(zh/sales-line) same n,Hangup(16) exten 2,1,Playback(zh/support-line) same n,Hangup(16) exten i,1,Playback(zh/invalid-option) same n,Goto(s,1) exten t,1,Playback(zh/please-try-again) same n,Goto(s,1)Background()在播放的同时监听按键用户按 1 或 2 后Dialplan 跳到对应分机1、2继续执行。WaitExten(3)的意思是等待按键最长 3 秒。i是用户按了无效键的处理t是等待超时的处理。这已经是生产级 IVR 的骨架了而它的底层逻辑依然建立在 Answer、Playback、Hangup 的生命周期上。4. 常见问题与排查技巧实录4.1 不写 Answer 直接播放为什么会出现“接听没声音”这个问题我遇到不止一次。有些场景下比如从某些 SIP 中继进来的来电通道在进入 Dialplan 前可能已经处于“部分建立”状态这时候直接Playback()偶尔也能出声音但更多情况下你会看到软电话显示已接通喇叭里却一片寂静。原因在于媒体流的方向和编码协商需要通道进入应答状态后才算完全就绪。Answer()会触发后续的信令和媒体协商让双方都进入可以发送音频的状态跳过这一步Asterisk 可能在单工或半协商状态下尝试播放声音就丢了。遇到这类问题第一件事不是换编码、调参数而是回到基础补上Answer()并在它后面加 1 秒左右Wait()让链路稳定。八成问题直接解决。4.2 语音文件“找不到”或格式不匹配典型的日志片段是Unable to open ... (No such file or directory)或者 “format not supported”。排查顺序确认文件在声音目录下路径写对。注意Playback(zh/welcome-msg)对应的是/var/lib/asterisk/sounds/zh/welcome-msg.*。确认文件有至少一种 Asterisk 支持的声音格式比如wav、gsm、ulaw、alaw。如果只有 MP3那多半不行因为版权和专利原因Asterisk 默认不带 MP3 解码强烈建议统一转成 WAV 或 GSM。确认权限sudo chown asterisk:asterisk /var/lib/asterisk/sounds/zh/welcome-msg.wav权限不对会导致“能列出文件但读不了内容”。改完格式或路径不需要 reload Dialplan因为声音文件是按需读取的但建议清一下缓存记忆。4.3 优先级顺序和扩展名冲突Dialplan 的优先级如果乱写容易出现“执行不到预期位置”。老式写法允许跳号比如exten 6001,1,Answer()后直接exten 6001,5,Playback(...)Asterisk 也能执行但中间的 2 到 4 是空的语义混乱。现代写法推荐exten 6001,1,Answer() same n,Playback(hello-world) same n,Hangup()same n是“延续前一个扩展名”的简写只要当前还属于6001这个扩展就一路n下去。如果中间换了扩展名就必须重新写exten 新号码,1,...。我见过有人把它和s混用导致匹配错乱——比如在[ivr]里用s,1起头然后某一行突然same n换到了别的扩展上下文结果按键跳转全部失效。记住一条同一个扩展内才能用same n连续跨扩展必须另起炉灶。4.4 重点检查清单日志、权限与上下文映射最后给一张我自己的“毛病速查表”照着走一遍能解决九成问题现象可能原因排查方法呼入后完全没反应通道绑定上下文错误检查 SIP/PJSIP 通道配置里的context有反应但没声音缺少 Answer加 Answer 和 Wait(1)报文件找不到路径/语言目录/权限ls 声音目录、chown 文件按键无效用了 Playback 而非 Background换 Background 监听 DTMF流程不按顺序优先级混用、same 跨扩展回归exten与same n写法话单原因码异常Hangup 未指定 cause使用Hangup(16)等显式原因码日志总入口在/var/log/asterisk/full。调试时用tail -f /var/log/asterisk/full配合呼入能实时看到每一行 Dialplan 的执行轨迹。Verbose 级别越低输出越少建议临时开到 4 甚至 5问题解决后再调回 3避免日志刷爆磁盘。最后聊一点我自己的体会。很多人学 Asterisk 一上来就搜“怎么实现自动外呼”“怎么对接话务机器人”结果被一堆高级应用绕晕。我走过的弯路是高级功能玩得飞起但一次简单的“响一声就播放”反而排查了半个多小时原因就是通道没 Answer。后来我把 Answer、Playback、Hangup 当成调试三件套任何新环境、新通道、新版本都先用这三件套跑通一条最小链路确认上下行媒体都正常之后再叠加业务逻辑。这套做法几乎让我绕开了所有“环境不确定性”带来的假故障。你也不妨试试拿到任何一套 Asterisk 环境先不要急着写业务先让三件套说话。它不响说明通道没通它响了后面的路就好走了。