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

Node.js+Twilio自动呼叫状态异常:始终返回complete求助

问题根源分析

核心问题出在Twilio的状态判定逻辑:只有当呼叫振铃超时无人接听,且未执行任何应用层Twiml逻辑时,才会触发no-answer/failed等状态。而你的代码中,不管对方是否接听,Twilio都会直接执行初始的play/redirect操作,这会让Twilio判定呼叫已被"处理完成",因此始终返回completed状态,而非实际的未接听/失败状态。

具体修复方案

1. 先判断呼叫是否被接听,再执行业务逻辑

修改/initiateCall的Twiml生成逻辑,用<Dial>组件先处理呼叫振铃,只有当呼叫被接听后,才进入后续的音频播放/姓名收集流程:

router.post('/initiateCall', async (req, res) => {
    const { text, to, contactName, pulseId, linkedPulseId } = req.body;

    console.log('Request:', req.body);

    if (!text || !to) {
        return res
            .status(400)
            .send('Please provide both "text" and "to" in the request body.');
    }

    try {
        const twiml = new twilio.twiml.VoiceResponse();
        // 使用Dial组件处理呼叫振铃,指定接听后的回调端点
        const dial = twiml.dial({
            action: `${baseUrl}/twilio/handleCallAnswer?text=${encodeURIComponent(text)}&contactName=${contactName}`,
            method: 'POST',
            timeout: 30, // 振铃超时时间,按需调整
        });
        dial.number(to);

        const call = await client.calls.create({
            from: process.env.TWILIO_PHONE_NUMBER,
            to: to,
            twiml: twiml.toString(),
            statusCallback: `${baseUrl}/twilio/callStatus?pulseId=${pulseId}&linkedPulseId=${linkedPulseId}`,
            statusCallbackEvent: [
                'initiated',
                'ringing',
                'answered',
                'completed',
                'no-answer',
                'busy',
                'failed',
                'canceled',
            ],
            statusCallbackMethod: 'POST', // 明确指定POST方法
        });

        res.send({ message: 'Call initiated successfully', callSid: call.sid });
    } catch (error) {
        console.error('Error during call initiation:', error.message);
        res.status(500).send('Failed to initiate call.');
    }
});

新增/handleCallAnswer端点,处理呼叫被接听后的业务逻辑:

router.post('/handleCallAnswer', async (req, res) => {
    const { DialCallStatus, text, contactName } = req.body;
    const twiml = new twilio.twiml.VoiceResponse();

    // 仅当呼叫被接听时,执行后续音频播放/姓名收集
    if (DialCallStatus === 'answered') {
        const response = await axios.post(`${baseUrl}/googleTTS/synthesize`, {
            text: decodeURIComponent(text),
        });
        const audioUrl = response.data.url;

        if (contactName) {
            twiml.play(audioUrl);
            twiml.pause({ length: 1 });
        } else {
            twiml.play(audioUrl);
            twiml.pause({ length: 1 });
            twiml.redirect(`${baseUrl}/twilio/askName`);
        }
    } else {
        // 未接听则直接挂断
        twiml.hangup();
    }

    res.type('text/xml');
    res.send(twiml.toString());
});

2. 简化/askName的状态检查逻辑

因为该端点只会在呼叫被接听后触发,所以可以简化状态判断:

router.post('/askName', async (req, res) => {
    const twiml = new twilio.twiml.VoiceResponse();
    const retry = req.query.retry;

    if (req.body.CallStatus !== 'in-progress') {
        twiml.hangup();
        res.type('text/xml');
        res.send(twiml.toString());
        return;
    }

    twiml
        .gather({
            input: 'speech',
            action: '/twilio/confirmName',
            method: 'POST',
            speechTimeout: '2',
            language: 'en-US',
        })
        .play(
            retry ? `${awsS3Url}/askNameRetry.wav` : `${awsS3Url}/askName.wav`
        );

    twiml.redirect('/twilio/askName?retry=true');

    res.type('text/xml');
    res.send(twiml.toString());
});

3. 确保/callStatus正确接收状态

在状态回调端点中,直接使用Twilio发送的CallStatus参数,不要自行推断:

router.post('/callStatus', async (req, res) => {
    const { CallStatus, pulseId, linkedPulseId } = req.body;
    console.log('Received Call Status:', CallStatus);

    // 根据实际状态更新Monday.com
    switch(CallStatus) {
        case 'no-answer':
            // 处理未接听逻辑
            break;
        case 'failed':
            // 处理呼叫失败逻辑
            break;
        case 'completed':
            // 处理呼叫完成逻辑
            break;
        // 其他状态按需处理
    }

    res.sendStatus(200); // 必须返回200,否则Twilio会重复回调
});
关键注意事项
  • 务必通过Twilio控制台的呼叫日志查看实际状态,确认Twilio是否发送了预期的状态值。
  • 状态回调端点必须返回200 OK,否则Twilio会重复发送回调请求。
  • no-answer仅在呼叫振铃超时且未进入任何应用逻辑时触发,一旦Twilio执行了你的Twiml,就会标记为completed。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 12:06:12