You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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回调返回结果再决策。所以我们换个思路:

  1. 浏览器端先发起呼叫到你的Twilio号码/应用,而非直接拨打被叫号码
  2. 后端(C#)让主叫端先保持等待状态
  3. 同时用CallResource.CreateAsync单独发起对被叫的呼叫,开启asyncAMD并指定回调接口
  4. 根据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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.16 12:10:28