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

Twilio呼叫接通后自动挂断?客户端提示应用错误排查

Twilio Voice Calling功能异常问题排查

问题描述

我正在搭建Twilio Voice Calling功能,客户端与服务端代码如下,但出现两个异常:

  • 客户端收到语音提示:"Sorry, an application error has occurred. Goodbye."
  • 被叫方手机响铃并播放TwiML文本后自动挂断。

客户端代码

function makeCall() {
    if (!window.twilioDevice) {
        console.error('Twilio Device not ready');
        return;
    }

    const params = {
        to: '+1555#######',
        from: '+1555#######'
    };

    const connection = window.twilioDevice.connect(params);
}

服务端代码

WebApp.handlers.use('/twilio_voice', async (req, res, next) => {
    try {
        const accountSid = Meteor.settings.twilio.accountSid;
        const authToken = Meteor.settings.twilio.authToken;
        const twilioClient = require('twilio')(accountSid, authToken);

        const to = '+1##########'
        const from = '+1##########'
        // const { to, from } = req.body; // Ensure your client sends these in the request body

        if (!to || !from) {
            let errorMsg = `twilio_voice endpoint: Missing "to" or "from" phone number. to: ${to} from: ${from}`
            console.log(errorMsg);
            res.writeHead(400, { 'Content-Type': 'application/json' });
            res.end(JSON.stringify({ success: false, error: errorMsg }));
            return;
        }

        const call = await twilioClient.calls.create({
            twiml: '<Response><Say>Hello, this is a call from a user. User, please go ahead and say hello.</Say></Response>',
            to: to,
            from: from
        });

        console.log('Call SID: ', call.sid);
        res.type('application/json');
        res.send({ success: true, callSid: call.sid });

    } catch (error) {
        console.error('Error in /twilio_voice:', error);
        res.writeHead(400, { 'Content-Type': 'application/json' });
        res.end(JSON.stringify({ success: false, error: error.message }));
    }
});

问题原因及修复方案

1. 客户端流程错误:未正确初始化Twilio Device

你的客户端直接调用twilioDevice.connect(),但Twilio Device需要先通过服务端获取**能力令牌(Capability Token)**完成初始化,且connect的参数是传递给TwiML App的参数,不是直接触发API呼叫。

修复后的客户端代码:

// 初始化Twilio Device
async function initTwilioDevice() {
  const response = await fetch('/get_twilio_token', { method: 'POST' });
  const data = await response.json();
  window.twilioDevice = new Twilio.Device(data.token, { logLevel: 1 });

  // 监听错误与连接事件
  window.twilioDevice.on('error', (err) => console.error('Device Error:', err));
  window.twilioDevice.on('connect', (conn) => console.log('Call Connected:', conn));
}

// 发起呼叫
function makeCall() {
  if (!window.twilioDevice) {
    console.error('Twilio Device not ready');
    return;
  }

  const params = { to: '+1555#######' }; // 仅传递被叫号码到服务端TwiML端点
  window.twilioDevice.connect(params);
}

2. 服务端逻辑混淆:误用Calls API替代TwiML路由

当前服务端直接调用twilioClient.calls.create()创建呼叫,这与Twilio Device的WebRTC通话流程不匹配。正确流程是:客户端发起connect后,Twilio会向你的TwiML App配置的URL发送请求,服务端需返回TwiML来桥接客户端与被叫方。

修复后的服务端代码需包含两个端点:

端点1:生成Capability Token

WebApp.handlers.use('/get_twilio_token', async (req, res) => {
  try {
    const { accountSid, authToken, twimlAppSid } = Meteor.settings.twilio;
    const twilioClient = require('twilio')(accountSid, authToken);

    const capability = new twilioClient.jwt.ClientCapability({ accountSid, authToken });
    // 允许客户端接收呼叫
    capability.addScope(new twilioClient.jwt.ClientCapability.IncomingClientScope('my_client'));
    // 允许客户端发起呼叫,关联TwiML App
    capability.addScope(new twilioClient.jwt.ClientCapability.OutgoingClientScope({
      applicationSid: twimlAppSid,
      params: { clientName: 'my_client' }
    }));

    res.json({ success: true, token: capability.toJwt() });
  } catch (err) {
    console.error('Token Generation Error:', err);
    res.status(500).json({ success: false, error: err.message });
  }
});

端点2:返回TwiML处理呼叫

WebApp.handlers.use('/twilio_voice_twiml', async (req, res) => {
  try {
    const { accountSid, authToken, fromNumber } = Meteor.settings.twilio;
    const twilioClient = require('twilio')(accountSid, authToken);
    const to = req.query.to || req.body.to; // 获取客户端传递的被叫号码

    if (!to) {
      res.type('text/xml').send('<Response><Say>Missing destination number.</Say></Response>');
      return;
    }

    // 生成TwiML桥接客户端与被叫方
    const twiml = new twilioClient.twiml.VoiceResponse();
    const dial = twiml.dial();
    dial.number(to);

    res.type('text/xml').send(twiml.toString());
  } catch (err) {
    console.error('TwiML Generation Error:', err);
    res.type('text/xml').send('<Response><Say>An error occurred.</Say></Response>');
  }
});

3. 缺少Twilio控制台关键配置

  • 在Twilio控制台创建TwiML App,将Voice配置中的Request URL设为你的服务端TwiML端点(如https://your-domain.com/twilio_voice_twiml)
  • 确保fromNumber是Twilio已验证或购买的号码
  • 检查账户余额与权限,确认可发起呼出呼叫

4. 被叫方自动挂断的原因

原代码中的TwiML仅包含<Say>指令,播放完成后呼叫自动终止。使用<Dial>指令桥接双方后,通话会保持直到任意一方挂断。


内容的提问来源于stack exchange,提问作者VikR

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 14:04:52