使用Twilio C#库及Voice JavaScript SDK时,<Dial><Number>场景下AMD流程控制与特定通话逻辑实现咨询
Twilio C#库及Voice JavaScript SDK时,场景下AMD流程控制与特定通话逻辑实现咨询
我太懂你这个困扰了!在<Dial><Number>标签里用AMD时,没法像用CallResource.CreateAsync那样靠asyncAMD来异步控制后续流程,确实会卡住你的业务逻辑实现。结合你提到的三个业务场景,我给你梳理一个可行的解决方案,完全适配Twilio C#库和Voice JavaScript SDK的环境:
核心思路:拆分呼叫流程,异步处理AMD结果
因为<Dial>默认是同步执行的,会直接尝试连接主被叫,没法等AMD回调返回结果再决策。所以我们换个思路:
- 浏览器端先发起呼叫到你的Twilio号码/应用,而非直接拨打被叫号码
- 后端(C#)让主叫端先保持等待状态
- 同时用
CallResource.CreateAsync单独发起对被叫的呼叫,开启asyncAMD并指定回调接口 - 根据AMD回调返回的结果,再动态桥接主被叫或执行对应逻辑
分场景具体实现步骤
1. 浏览器端(Voice JS SDK)初始化与发起呼叫
先让用户从浏览器呼叫到你的Twilio号码,而非直接打给目标被叫:
// 初始化Twilio Device const device = new Twilio.Device(yourTwilioToken); device.on('connect', (connection) => { console.log('已连接到Twilio,等待被叫AMD结果...'); }); device.on('disconnect', () => { console.log('通话已结束'); }); // 发起呼叫到你的Twilio号码 device.connect({ to: '你的Twilio号码' });
2. 后端(C#)处理主叫初始请求
当Twilio收到主叫请求后,返回TwiML让主叫保持等待,同时存储主叫的CallSid(后续AMD回调要用到):
[HttpPost("init-call")] public IActionResult HandleIncomingCall([FromForm] string CallSid) { // 把CallSid存到数据库/缓存,关联用户会话或标识 _cache.Set($"caller_{CallSid}", CallSid, TimeSpan.FromMinutes(30)); // 返回TwiML让主叫等待(可以播放提示音或静音等待) var twiml = new TwimlResponse(); twiml.Say("正在连接对方,请稍候...", voice: VoiceEnum.PollyMatthew); twiml.Hold(); return Content(twiml.ToString(), "application/xml"); }
3. 后端发起带asyncAMD的被叫呼叫
在处理主叫请求的同时,用CallResource.CreateAsync发起对被叫的呼叫,开启异步AMD:
// 可以和上面的接口同逻辑,或单独抽离方法 var outboundCall = CallResource.Create( to: new PhoneNumber("目标被叫号码"), from: new PhoneNumber("你的Twilio号码"), url: new Uri("https://你的后端地址/handle-callee-wait"), // 返回空等待TwiML asyncAmd: true, amdStatusCallback: new Uri("https://你的后端地址/amd-status-callback"), amdStatusCallbackMethod: HttpMethod.Post ); // 存储被叫CallSid和主叫CallSid的关联关系 _cache.Set($"callee_{outboundCall.Sid}", callerCallSid, TimeSpan.FromMinutes(30));
对应的被叫等待TwiML接口:
[HttpPost("handle-callee-wait")] public IActionResult HandleCalleeWait() { var twiml = new TwimlResponse(); // 返回空响应让被叫保持连接,直到AMD结果返回后再更新TwiML return Content(twiml.ToString(), "application/xml"); }
4. 处理AMD回调,分场景执行逻辑
当Twilio返回AMD结果到你的回调接口后,根据不同状态处理:
[HttpPost("amd-status-callback")] public IActionResult AmdCallback([FromForm] string CallSid, [FromForm] string AmdStatus) { // 取出关联的主叫CallSid if (!_cache.TryGetValue($"callee_{CallSid}", out string callerCallSid)) { return BadRequest("未找到关联的主叫通话"); } // 临时会议ID,用于桥接主被叫 var conferenceId = $"temp-conf_{Guid.NewGuid().ToString("N").Substring(0, 8)}"; switch (AmdStatus.ToLower()) { // 场景2.1:Human/Unknown - 播放同意提示后桥接 case "human": case "unknown": // 更新被叫通话:播放同意提示后加入会议 var calleeTwiml = new TwimlResponse(); calleeTwiml.Say("继续通话即表示您同意被录音", voice: VoiceEnum.PollyMatthew); calleeTwiml.Dial().Conference(conferenceId); CallResource.Update( twiml: new Twiml(calleeTwiml.ToString()), path: new CallResource.Path(callSid: CallSid) ); // 更新主叫通话:加入同一会议 var callerTwiml = new TwimlResponse(); callerTwiml.Dial().Conference(conferenceId); CallResource.Update( twiml: new Twiml(callerTwiml.ToString()), path: new CallResource.Path(callSid: callerCallSid) ); break; // 场景2.2:Machine-Start - 直接桥接让主叫留言 case "machine_start": var machineTwiml = new TwimlResponse(); machineTwiml.Dial().Conference(conferenceId); // 同时更新主被叫通话到会议 CallResource.Update( twiml: new Twiml(machineTwiml.ToString()), path: new CallResource.Path(callSid: CallSid) ); CallResource.Update( twiml: new Twiml(machineTwiml.ToString()), path: new CallResource.Path(callSid: callerCallSid) ); break; // 场景2.3:Fax - 挂断被叫,告知主叫 case "fax": // 挂断被叫通话 CallResource.Update( status: CallResource.StatusEnum.Completed, path: new CallResource.Path(callSid: CallSid) ); // 告知主叫对方是传真 var faxTwiml = new TwimlResponse(); faxTwiml.Say("对方是传真机,通话已断开", voice: VoiceEnum.PollyMatthew); CallResource.Update( twiml: new Twiml(faxTwiml.ToString()), path: new CallResource.Path(callSid: callerCallSid) ); break; } return Ok(); }
关键注意事项
- 一定要妥善存储主被叫
CallSid的关联关系,建议用Redis或内存缓存(短期存储即可) - 会议模式是桥接主被叫最灵活的方式,比直接
Dial更易控制流程 - 如果需要录音,可以在创建会议或呼叫时开启
record参数,满足合规需求
备注:内容来源于stack exchange,提问作者paul_glad
相关产品推荐
相关产品推荐

