
简介呼叫中心项目是一套基于 Asterisk 开发的企业级 Web 呼叫中心系统主要用于毕业设计、课程设计及实训竞赛等场景。功能覆盖自动外呼包括电费、水费、话费、物业费催缴以及交通移车、违法通知等八类提醒业务且支持扩展对接多种电话服务。整个压缩包共 1202 个文件大小约 26.43MB代码与资源分类清晰251 个 Java 文件负责业务逻辑137 个 JSP 与 156 个 CSS 支撑 Web 界面128 个 JS 处理前端交互另有 99 个 WAV 和 87 个 VOX 用于语音播报并附带工程配置与说明文档。项目工程经过严格测试可正常运行包含完整源码与工程文件可轻松复现设计报告也能直接借鉴。目前已有 78 人学习下载适合需要完整呼叫中心解决方案的同学参考或二次开发。1. 呼叫中心 Web 项目用 Asterisk 做内核毕设答辩能拿得出手的关键在哪很多人的毕设选题写着「基于 Asterisk 的呼叫中心 Web 项目」真正动手才发现Asterisk 本身是个老牌开源 PBX装起来容易但「Web 项目」这四个字才是工作量的大头坐席工作台、通话状态、录音回放、话务报表每一块都要从 Asterisk 的接口里把数据抠出来。这套方案能解决的核心问题是用 Asterisk 承担所有语音交换和信令处理Web 端只做管理、监控和操作界面不需要碰任何硬件。适合做毕设、课设、实训和竞赛项目的人尤其适合想展示「前后端 实时通信」完整链路的团队。我先把丑话说在前面真正让你翻车的往往不是拨号计划而是 Web 层和 Asterisk 之间的状态同步——这也是我先帮你把坑指出来的原因。2. 让 Asterisk 先跑起来版本选型、最小 PJSIP 配置与一条可验证的呼叫链路在写任何 Web 代码之前先把语音链路做成「黑匣子里有光」一台 Ubuntu 虚拟机装上 Asterisk让两个软电话能互相打通。这一步跑不通后面所有 Web 功能都是空中楼阁。2.1 选型PJSIP 还是 chan_sip18 LTS 还是 21 LTS老教程里满屏都是 chan_sip 和 sip.conf那是 Asterisk 13 以前的主流写法。从 16 开始官方主推 PJSIP 通道驱动配置文件是 pjsip.conf模块是 chan_pjsip。两者在 Web 项目里的差异主要在配置结构和调试命令PJSIP 把 endpoint、aor、auth、transport 拆成四个独立小节逻辑更清楚排查时用 pjsip show endpoints 一眼能看到分机的注册 IP 和状态chan_sip 配置简单但后续做 WebRTC 支持、NAT 穿透都要绕路。版本上我一般会选 LTS 分支。当前常见的 LTS 是 18 和 2118 的教程量最大搜一个问题基本都有答案适合毕设21 更新加密传输、WebRTC 相关的默认行为更现代但遇到问题能搜到的案例明显少。用 apt 直接装发行版仓库里的 asterisk 包就能满足大部分课设想要新特性再去官网下载源码编译注意编译前把依赖装全否则 make 到一半报缺头文件很浪费时间。下表是我的选择习惯对比项chan_sip旧PJSIP推荐配置文件sip.confpjsip.conf分机模型单段配置endpoint/aor/auth 分离WebRTC 支持较弱原生支持调试命令sip show peerspjsip show endpoints毕设建议尽量别用默认选它2.2 最小 pjsip.conf 与 extensions.conf一个分机、一条拨号规则装好 Asterisk 后先别急着配复杂功能我一般会先写一个能注册、能互拨的最小集合。pjsip.conf 里放一个软电话分机和一个传输配置[transport-udp] typetransport protocoludp bind0.0.0.0:5060 [6001] typeendpoint contextfrom-internal disallowall allowulaw,alaw direct_mediano transporttransport-udp [6001] typeaor max_contacts1 remove_existing1 [6001] typeauth auth_typeuserpass username6001 passwordyour_password [6001] typeidentify endpoint6001 match192.168.0.0/16这段配置的逻辑是endpoint 定义分机 6001 的行为aor 定义它能注册几个联系地址auth 定义账号密码identify 限制允许从哪个网段注册。注意 Asterisk 靠「同名小节的类型不同」把它们关联成一个分机所以四个 type 的小节名必须一致。参数上disallowall 之后再 allowulaw,alaw 表示只允许这两种 G.711 编码呼叫中心里话质和兼容性都够用direct_mediano 强制媒体流经过 Asterisk 中转这样录音和状态监控才完整后面 WebRTC 坐席也依赖这个设置。extensions.conf 里给一个最简拨号计划来电接听播放一段语音内部分机互拨直接 Dial。[from-internal] exten 6001,1,Answer() same n,Wait(1) same n,Playback(hello-world) same n,Hangup() exten _6XXX,1,Dial(PJSIP/${EXTEN},30)这里 _6XXX 是匹配 60016999 任意四位分机的通配符${EXTEN} 是实际拨出的号码30 是振铃 30 秒没人接就放弃。from-internal 这个 context 名要和 pjsip.conf 里 endpoint 的 context 保持一致否则分机拨不出去这是新手第一个容易踩的坑。2.3 验证链路从 asterisk -r 到软电话注册配置写完后重启 Asterisk 让配置生效然后用控制台命令验证sudo asterisk -rvvvv pjsip reload pjsip show endpoints pjsip show contactspjsip show endpoints 能看到 6001 的 endpoint 是否存在但此时 Aor Contact 一栏是 0 个说明还没注册。接着在 Windows 或手机装一个 SIP 软电话填服务器 IP、分机号 6001 和密码观察控制台输出。注册成功后 pjsip show contacts 里会出现 6001/sip:6001你的IP并且 Status 是 Avail或者直接显示注册 IP 地址。验证通话我习惯拨 6001 听系统自带的 hello-world能听到就说明放音、编码、媒体路径都正常。再在软电话里拨 6002如果 6002 是另一个已注册分机两边就能互相听到声音。到这一步语音链路已经通了可以开始接 Web 层。如果注册失败优先检查 VMware/VirtualBox 的虚拟机网络是不是 NATNAT 模式下宿主机外的软电话访问不到改成桥接网络或者干脆在宿主机上装一个软电话用同一网段。3. 打通 Web 和 AsteriskAMI 是主通道WebSocket 做状态推送Asterisk 跑起来之后Web 项目要解决的核心问题是后端怎么告诉 Asterisk「发起呼叫、挂断、查状态」以及 Asterisk 怎么把「有人来电、坐席接起、通话结束」这些实时状态告诉 Web。答案是 AMI WebSocket 双通道AMI 负责命令和事件WebSocket 负责把事件推到浏览器。3.1 AMI 不是 REST 但也不难最小 Python 客户端能发 OriginateAMIAsterisk Manager Interface是 Asterisk 内置的 TCP 管理协议默认监听 5038 端口协议格式是文本行Action 发命令Event 收事件。说它像 REST 是因为每一条命令都有明确的 Action 和参数说它不像是因为它是长连接、事件是主动推送的初学者容易在「要不要每次请求都重新连接」上纠结。先看 manager.conf 怎么开权限[general] enabled yes port 5038 bindaddr 127.0.0.1 [webadmin] secret your_ami_password read call,cdr,user,agent write call,originate,agentbindaddr127.0.0.1 是安全底线意思是 AMI 只允许本机访问Web 后端和 Asterisk 在同一台机器时用这个配置最稳也避免把管理口暴露到外网。read 是你允许这个账号订阅哪些事件write 是允许执行哪些动作比如发 Originate 外呼就必须有 originate 权限。然后用 Python 的 socket 直接请求 AMI。很多教程会引入 pyst2 这类库但直接走 socket 能让你真正看懂协议排查问题也更快import socket def ami_action(action_lines): s socket.create_connection((127.0.0.1, 5038), timeout5) s.sendall(bAction: Login\r\nUsername: webadmin\r\nSecret: your_ami_password\r\n\r\n) s.recv(4096) # 吃掉登录响应 payload \r\n.join(action_lines) \r\n\r\n s.sendall(payload.encode()) s.settimeout(2) resp b try: while True: chunk s.recv(4096) if not chunk: break resp chunk except socket.timeout: pass s.close() return resp.decode(errorsignore) resp ami_action([ Action: Originate, Channel: PJSIP/6001, Context: from-internal, Exten: 6002, Priority: 1, Timeout: 30000, ]) print(resp)这段代码做了两件事登录后发起一次 Originate。Originate 的意思是主动从 PJSIP/6001 这个通道发起呼叫让它拨到 from-internal context 里的 6002相当于 Web 点了一个「外呼」按钮。参数里 Channel 是发起方Exten 是被叫方Timeout 是 30 秒内没人接就超时。实际项目里不会每次新建连接而是维护一条长连接持续收事件这个下面展开。字段之间的 \r\n 和结尾空行是 AMI 协议的边界符漏掉一个空行服务端就解析不出 Action这是最常见的低级错误。3.2 事件驱动把 Newchannel/Hangup 转成 WebSocket 推给前端Web 呼叫中心的核心体验是实时性电话还没响完坐席工作台已经弹出来电号码。实现方式不是前端去轮询 AMI而是后端保持一条 AMI 长连接订阅事件再把事件转成 WebSocket 推给浏览器。后端收到的是这样的原始事件流Event: Newchannel Channel: PJSIP/6001-00000001 CallerIDNum: 13800000000 ChannelState: 5典型做法是后端写一个事件循环按行读事件过滤出 Web 关心的类型。我一般会建一张事件过滤表只把这几类推给前端AMI 事件含义Web 端用途Newchannel新通道建立来电弹屏Newstate通道状态变化振铃/接通通话状态流转Hangup通道挂断结束通话、释放坐席AgentConnect坐席接起队列来电工作台分配记录CDR通话记录生成话单归档代码上后端事件循环可以这样组织Python 伪代码实际项目一般用 asyncio 或线程池while True: line ami_socket.recv_line() if line.startswith(Event: ): event_type line.split(: , 1)[1].strip() if event_type in web_events: payload collect_event_fields(ami_socket) websocket_broadcast({type: event_type, data: payload})逻辑是把 AMI 的一行事件头读出来命中白名单就把整个事件字段收齐推给所有已连接的前端。collect_event_fields 要读到空行才算一条完整事件这个细节依赖 AMI 的分隔规则。前端侧不管是 Vue 还是 React接收逻辑都是同一套const ws new WebSocket(ws:// location.host /ws/events); ws.onmessage (event) { const msg JSON.parse(event.data); if (msg.type Newstate msg.data.ChannelState 6) { setCallState(接听中); } if (msg.type Hangup) { setCallState(已结束); } };ChannelState 是 AMI 的状态码5 是振铃Ringing6 是接通Up1 是正在拨号。前端不用关心底层信令只要维护好这个映射就行。稳定性上有两个参数要调WebSocket 的心跳要小于 Nginx/Asterisk 的空闲超时常见 3060 秒发一次 Ping 帧浏览器断线重连后后端要重新推送一次当前所有分机的状态快照否则前端会一直停留在旧状态。这个「重连后的快照同步」是很多毕设项目遗漏的答辩时一断网就露馅。3.3 AMI vs ARI vs FastAGI毕设场景怎么选以及 manager.conf 的安全底线除了 AMIAsterisk 还提供 ARI 和 FastAGIARI 是 Asterisk 16 主推的 RESTful WebSocket 接口用 HTTP 发命令、WebSocket 收事件对后端是 Java/Spring Boot 或 Node 的团队更友好FastAGI 则是把拨号计划的控制权交给外部脚本适合做复杂业务逻辑查数据库再决定播放什么语音。毕设和课设怎么选我的建议是如果目标是「Web 管理界面 坐席工作台」AMI 足够且稳定springboot 后端用 AMI 也是社区里很成熟的方案如果团队对 REST 风格更熟愿意承担多一些配置工作选 ARI 也很合理但 ARI 对事件流的处理比 AMI 复杂一点。一个项目里也可以混用以 AMI 为主做话务控制和事件订阅FastAGI 只在 IVR 需要访问业务数据库时接一个。三种方式对比见表接口协议风格事件推送适合场景学习成本AMITCP 文本命令自带事件流呼叫控制、状态监控低ARIHTTP REST WebSocket需单独订阅复杂呼叫流程编排中FastAGITCP/FastAGI 协议无IVR 中查库、动态放音中关于 manager.conf 的安全底线再强调一次bindaddr 一定绑内网或 127.0.0.1密码用独立强密码别和系统 root 复用AMI 账号的 read/write 权限按最小集给Web 展示用不到 System 权限就不开。Web 后端是唯一能访问 AMI 的服务相当于把你的 Asterisk 管理口锁在了内网里这是 Web 安全里最基础的边界原则也是企业级 Web 开发里常被审计的点。端口都配好后用 telnet 127.0.0.1 5038 能通、外网访问不到就是合格状态。提示AMI 端口只允许 Web 后端所在主机访问是呼叫中心 Web 项目里最便宜的防线别为了省事把它绑到 0.0.0.0。4. 把呼叫中心业务装进 Web 后台IVR、队列、录音与 CDR呼叫中心和普通电话系统最大的区别是业务闭环客户打进来先听 IVR按 1 进销售队列坐席签入队列接电话通话自动录音结束后 CDR 生成话单Web 后台再把这些数据展示成报表。这一章从上到下把链路串起来。4.1 IVR 拨号计划按键转队列的正确写法IVR 的核心是一个「等待按键」的拨号计划。很多新手写成 Answer 后接 Playback播完就挂断了按键根本收不到。正确做法是用 Background 放音并启动 WaitExten[ivr-main] exten s,1,Answer() same n,Background(welcome) same n,WaitExten(5) exten 1,1,Queue(sales) exten 2,1,Queue(support) exten i,1,Playback(invalid-option) same n,Restart()Background 在放音的同时就开始监听按键WaitExten(5) 是放音结束后再等 5 秒按键 1/2 分别跳转到同一个 context 下的 1 或 2 分机定义也就是进对应队列。i 分支处理无效按键提示后 Restart 回到开头。welcome 和 invalid-option 是系统自带语音换成你自己的提示音时把录好的 wav 文件放进 /var/lib/asterisk/sounds 对应语言目录文件名不带扩展名写在配置里即可。放音文件建议用 8kHz 16bit 单声道 WAV 或 ulaw 格式否则 Asterisk 解码会出怪声。注意这里 Queue() 的参数对应 queues.conf 里的队列名如果你在 Web 后台做 IVR 配置本质就是动态生成这段 dialplan 文本然后 reload。竞赛项目做到「后台改按键、前台立刻生效」是个不错的加分点。4.2 坐席与队列queues.conf 参数和签入签出的 AMI 动作queues.conf 决定队列的振铃策略。坐席多的时候策略选不对要么全员被骚扰要么电话没人接。我一般先配两个队列做演示[sales] musicclassdefault strategyringall timeout20 ringinuseno member PJSIP/6001 member PJSIP/6002 member PJSIP/6003 [support] strategyleastrecent timeout30 ringinuseno member PJSIP/6001 member PJSIP/6002strategyringall 是多人同时振铃谁先接谁上适合销售团队leastrecent 是最近最少接电话的人优先适合技术支持负载更均匀。timeout 是坐席振铃多久没人接就退回队列层处理。ringinuseno 是核心参数坐席正在通话中就不再给他分配新电话如果不设这个坐席通话时手机会继续振铃体验很差。Web 端坐席工作台的「签入/签出」就是对这个队列的成员动态增删走后端 AMI 命令Action: QueueAdd Queue: sales Interface: PJSIP/6001Action: QueueRemove Queue: sales Interface: PJSIP/6001QueueAdd 把坐席加进队列QueueRemove 移出。这两个动作配合上一章的事件推送就能在 Web 上显示「当前队列 5 人在线、2 人通话中」。坐席接听队列来电时如果希望话务员先看到客户资料再接听可以在 Queue 应用里加预振铃参数常见做法是 Queue(,,,30) 让坐席振铃 30 秒期间 Web 弹屏加载号码对应的档案。4.3 录音与 CDR从 MixMonitor 到首页话务报表录音和话单是呼叫中心 Web 项目里最容易被评委追问的两块。录音用 MixMonitor放在 Dial 的 b() 选项里比较合适exten _6XXX,1,Dial(PJSIP/${EXTEN},30,b(mixmonitor-output-${UNIQUEID}.wav))Dial 的 b() 选项表示被叫应答后才执行括号里的应用这里就是给每条通话生成一个以通道唯一 ID 命名的录音文件落在默认的 /var/spool/asterisk/monitor/ 目录。用的时机很关键放在 Answer 之前启动录进去的是没建立媒体流的静音放在应答之后启动才录到完整通话。MixMonitor 默认把双向混成一个文件正好适合回放。录音文件的属主必须是运行 Asterisk 的用户通常是 asterisk:asterisk否则文件创建不出来或权限报错sudo chown -R asterisk:asterisk /var/spool/asterisk/monitorCDRCall Detail Record是 Asterisk 内置的通话详单默认写到 SQLite 或 CSV生产项目常用 cdr_adaptive_odbc 写进 MySQL。Web 后台的话务报表本质上就是查这张表SELECT calldate, src, dst, duration, disposition FROM cdr WHERE calldate CURDATE() - INTERVAL 7 DAY ORDER BY calldate DESC;duration 是通话时长disposition 是结果ANSWERED 表示接通NO ANSWER 表示振铃没人接FAILED 表示呼叫失败。报表页面按这个字段分桶统计就行。注意 CDR 的 duration 是按整条通道记录算的如果一行里有多次呼叫转移单条记录可能只代表一段做统计时先确认你的业务里是否存在转接。录音文件路径最好也存一张自定义表里因为默认 CDR 没有录音文件名字段需要你在创建 MixMonitor 时把文件名同步到数据库。这个表结构建议在写 Web 端时自己设计成 call_id、file_path、cdr_id 三列报表页点「播放录音」时才能很快定位文件而不是去文件系统里猜。注意录音和 CDR 是评委最喜欢深挖的地方把「MixMonitor 生成文件 → 数据库记录路径 → Web 播放」这条链路走通项目完整性会明显上台阶。5. 避坑排查SIP 注册、录音无声、AMI 掉线、NAT 单向音频的 5 个真实案例这一章全是血泪经验。每个问题我都按「现象 → 原因 → 解决」的顺序写前两个属于环境类后三个属于 Web 项目里最常见的逻辑类排查顺序也建议从上往下。5.1 分机注册不上先查 transport再查 identify现象软电话一直显示 RegisteringAsterisk 控制台没有任何注册请求日志。原因排查顺序首先看 pjsip.conf 里 endpoint 是否绑定了 transport如果 transport 名字写错Asterisk 会直接忽略这条注册其次是 identify 小节里的 match 网段没包含软电话所在网段注册会被判定为不合法最后是防火墙云主机安全组或 ufw 忘了放行 5060/udp会让注册包根本到不了 Asterisk。解决在 asterisk -r 控制台执行 pjsip show endpoint 6001重点看 Transport 和 Identify 两栏是否正常再用 tcpdump -i any udp port 5060 抓包能抓到注册请求说明网络通抓不到就先放防火墙。把 transport 的 bind 地址改成 0.0.0.0:5060identify 的 match 改成你实际网段基本能解决九成注册问题。还有一类情况是软电话和 Asterisk 不在同一网段却在 identify 里写了 /8这种属于配置过宽建议反过来改精确。5.2 录音 0 字节或单声道MixMonitor 时机和目录权限现象通话结束后录音文件存在但大小是 0或者播放出来只有一端的声音。原因MixMonitor 执行时机太早比如在 Answer() 之前就录此时通道还没建立媒体流录进去的是空权限问题则是 Asterisk 用户对录音目录没有写权限文件创建不出来或者写了一半单声道则多半是录音时编码路径不匹配浏览器 WebRTC 通话用 opus 编码Asterisk 转成 wav 时选错格式回放就会缺声道。解决把 MixMonitor 放到 Dial 之后或使用 Dial 的 b() 选项保证媒体流已建立检查 /var/spool/asterisk/monitor 的属主执行 chown -R asterisk:asterisk /var/spool/asterisk/monitor。录出来的 wav 想转成 mp3 给 Web 端播放用 sox 一行命令处理sox input.wav -r 8000 -c 1 output.mp3注意 mp3 编码需要 libsox-fmt-mp3 这个插件没装的话 sox 会报错。我发现很多项目直接拿 wav 给浏览器播放也是可以的但文件大网页端加载慢做话务报表页时最好转一下。5.3 AMI 连接被踢或发不出 Originate权限与心跳一起查现象Web 后端连着连着 AMI 就断开或者执行 Originate 返回 Error提示 no originate privilege / authentication failed。原因manager.conf 里 write 权限没给 originate属于最常见的权限遗漏另一种是 Asterisk 对连接数有上限反复重连会挤掉旧连接还有一种是长连接静默太久被服务端断开客户端没有心跳机制。解决先确认账号权限把 write 至少配成 call,originate,agent客户端做心跳每 30 秒发一条 Action: PingAsterisk 会返回 Pong顺便可以用来检测链路存活Action: Ping收到 Pong 说明 AMI 还活着连续 3 次收不到就主动重连。重连时要注意先 Login 再重新订阅事件否则会漏掉重连期间的通话状态。我在项目里会把 AMI 封装成单例服务所有 Web 请求只通过这个服务发命令避免多处连接把连接数耗尽。5.4 内网演示音频单向direct_media 和 NAT 参数一起改现象两个分机明明注册成功、控制台也显示通道建立但只有一方能听到声音或者完全不响。原因这是 Asterisk 最常见也最「玄学」的媒体问题。本质是 SIP 信令里携带的媒体地址与实际网络路径不一致。内网虚拟机演示时常见原因是 Asterisk 所在机器的 IP 和软电话所在网段跨了 NAT或者 direct_media 尝试让两端直接传媒体绕开了 Asterisk导致一端发来的 RTP 包另一端根本收不到。解决把所有 endpoint 统一加上 direct_mediano同时确认 Asterisk 的 NAT 参数pjsip.conf 的 endpoint 里可以设置 rtp_symmetricyes 和 force_rportyes这两个参数让媒体流强制回传到来源地址可以解决绝大部分单向音频。真机测试时再配合 extern_ip 指定 Asterisk 的对外 IP让它在 SIP 信令里宣告正确的地址。记住一条经验呼叫中心项目里让所有媒体都经过 Asterisk 中转录音和监控才稳定为了那点延迟优化去开直连最后都会回来改配置。5.5 前端状态不同步Hangup 不是终点要有兜底查询现象对方已经挂断Web 工作台上通话窗口还挂着「通话中」坐席状态也不复位。原因AMI 的 Hangup 事件确实有但如果你在事件处理里只订阅了 Newstate没有订阅 Hangup后端就根本不知道通话结束另一种情况是通道经历了转移或桥接后再挂断Hangup 事件的 Channel 字段和最初创建时不是同一个前端按旧通道号匹配状态就匹配不上还有可能是 WebSocket 断了后端事件虽然收到但推不到前端。解决订阅列表里明确加入 Hangup、BridgeDestroy、PeerStatus前端状态判定的原则改为「收到 Hangup 才结束」而不是「超过几秒没新状态就结束」——但为了容错还要加一个兜底后端每隔 30 秒用 Action: Status 全量拉一次活动通道和前端状态做对账发现不一致就推送纠正信息。这个对账机制写起来不复杂却是让演示变得「稳」的关键竞赛现场评委让反复打电话测试时状态一次都不飘比什么炫酷界面都有说服力。6. 进阶用浏览器软电话把坐席端搬进 WebRTC附验收步骤到这一步你的 Web 项目已经有 IVR、队列、录音和报表但坐席接电话还是靠第三方软电话答辩时说服力差一截。把坐席端做成浏览器软电话是让整个项目「看起来完整」的临门一脚。Asterisk 对 WebRTC 的支持是原生能力前端用 JsSIP 或 SIP.js 注册为 SIP 分机媒体走 WebSocket 和浏览器内置的音频流不需要安装任何客户端。配置上PJSIP 要加一个 wss transport 和 WebRTC 专用 endpoint关键参数是 transporttransport-wss、webrtcyes、ice_supportyes、direct_mediano另外确认 res_http_websocket 模块已加载。前端代码核心就两步const socket new JsSIP.WebSocketInterface(wss://your-server:8089/ws); const ua new JsSIP.UA({ sockets: [socket], uri: sip:6001your-server, password: your_password, }); ua.on(registered, () console.log(坐席在线)); const session ua.call(sip:6002your-server);这段逻辑是先建 WebSocket 传输层再注册分机注册成功后直接发起呼叫。实际工程里要把 ua 保存为全局对象坐席签入签出就对应注册和注销。验收步骤我一般按这个清单走第一打开 chrome://webrtc-internals 确认 audio 流有没有实际收发这一步能定位是麦克风权限问题还是编码问题第二用另一个软电话呼入队列确认浏览器坐席能听到 IVR 放音、能正常接起第三挂断后回放录音确认 WebRTC 通话也被录上——这一步经常有人漏因为 WebRTC 用的编码可能是 opus而录音格式是 wavAsterisk 会做转码转码失败录音就是坏的。如果录音有问题把 endpoint 的 allow 加上 ulaw强制避免转码路径。注意浏览器对 wss 的证书要求很严本地自签证书调试前先计划好演示机器的信任处理。我自己第一次把坐席搬进浏览器时以为最难的是 ICE 协商结果一晚上都在跟自签证书搏斗本地测试可以加例外但竞赛现场的评审机器不一定允许你提前装证书建议准备一台本地 HTTPS 反代或者把证书预装进评审机器。如果时间紧宁可用软电话演示也别在证书上翻车。这套「AMI 控制 WebSocket 推送 浏览器坐席」的架构做完你的项目从演示到问答都经得起追问。希望帮到你。本文还有配套的精品资源点击获取