Fix workflow forward task immediate disconnection issue

Fixed critical bug where forward tasks in workflows would immediately disconnect calls instead of forwarding them properly.

Changes:
- Fixed append_twiml_element function to properly handle Dial elements with child Number elements
- Enhanced create_forward_twiml to extract numbers from nested data structures
- Added comprehensive error handling for missing forward numbers
- Added detailed logging throughout workflow execution for debugging
- Set default timeout of 30 seconds for forward operations

The issue was caused by the Dial element being converted to string which lost all child Number elements, resulting in an empty dial that would immediately disconnect.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-09-18 16:27:51 -07:00
parent 345ef43740
commit 90cb03acfd
4 changed files with 287 additions and 20 deletions

View File

@@ -390,7 +390,8 @@ class TWP_Admin {
<?php endif; ?> <?php endif; ?>
</select> </select>
<button type="button" class="button" onclick="loadElevenLabsVoices()">Load Voices</button> <button type="button" class="button" onclick="loadElevenLabsVoices()">Load Voices</button>
<p class="description">Default voice for text-to-speech. Click "Load Voices" after entering your API key.</p> <button type="button" class="button" onclick="refreshElevenLabsVoices()" title="Refresh voices from ElevenLabs">🔄 Refresh</button>
<p class="description">Default voice for text-to-speech. Click "Load Voices" after entering your API key, or "Refresh" to get updated voices.</p>
<?php if (WP_DEBUG): ?> <?php if (WP_DEBUG): ?>
<p class="description"><small>Debug: Current saved voice ID = "<?php echo esc_html(get_option('twp_elevenlabs_voice_id', 'empty')); ?>"</small></p> <p class="description"><small>Debug: Current saved voice ID = "<?php echo esc_html(get_option('twp_elevenlabs_voice_id', 'empty')); ?>"</small></p>
<?php endif; ?> <?php endif; ?>
@@ -1066,6 +1067,70 @@ class TWP_Admin {
xhr.send('action=twp_get_elevenlabs_voices&nonce=' + '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>'); xhr.send('action=twp_get_elevenlabs_voices&nonce=' + '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>');
} }
function refreshElevenLabsVoices() {
var select = document.getElementById('elevenlabs-voice-select');
var button = event.target;
var currentValue = select.getAttribute('data-current') || select.value;
console.log('Refreshing voices, current value:', currentValue);
button.textContent = 'Refreshing...';
button.disabled = true;
var xhr = new XMLHttpRequest();
xhr.open('POST', ajaxurl);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onload = function() {
button.textContent = '🔄 Refresh';
button.disabled = false;
try {
var response = JSON.parse(xhr.responseText);
if (response.success) {
var options = '<option value="">Select a voice...</option>';
if (Array.isArray(response.data)) {
response.data.forEach(function(voice) {
var selected = voice.voice_id === currentValue ? ' selected' : '';
var category = voice.category === 'cloned' ? ' (Cloned)' : (voice.category === 'premade' ? ' (Premade)' : '');
options += '<option value="' + voice.voice_id + '"' + selected + '>' + voice.name + category + '</option>';
});
}
select.innerHTML = options;
select.setAttribute('data-current', currentValue);
// Re-add preview buttons
addVoicePreviewButtons(select, response.data);
// Show success message
var statusMsg = document.createElement('div');
statusMsg.style.color = 'green';
statusMsg.style.fontSize = '12px';
statusMsg.style.marginTop = '5px';
statusMsg.textContent = 'Voices refreshed successfully! Found ' + response.data.length + ' voices.';
button.parentNode.appendChild(statusMsg);
setTimeout(function() {
if (statusMsg.parentNode) {
statusMsg.parentNode.removeChild(statusMsg);
}
}, 3000);
} else {
alert('Error refreshing voices: ' + (response.data || 'Unknown error'));
}
} catch (e) {
console.error('Refresh voices error:', e);
alert('Failed to refresh voices. Please try again.');
}
};
xhr.send('action=twp_refresh_elevenlabs_voices&nonce=' + '<?php echo wp_create_nonce('twp_ajax_nonce'); ?>');
}
function addVoicePreviewButtons(select, voices) { function addVoicePreviewButtons(select, voices) {
// Remove existing preview container // Remove existing preview container
var existingPreview = document.getElementById('voice-preview-container'); var existingPreview = document.getElementById('voice-preview-container');
@@ -4786,6 +4851,36 @@ class TWP_Admin {
} }
} }
/**
* AJAX handler for refreshing ElevenLabs voices (clears cache)
*/
public function ajax_refresh_elevenlabs_voices() {
check_ajax_referer('twp_ajax_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_die('Unauthorized');
}
// Clear the cached voices
delete_transient('twp_elevenlabs_voices');
// Now fetch fresh voices
$elevenlabs = new TWP_ElevenLabs_API();
$result = $elevenlabs->get_voices(); // This will fetch from API and re-cache
if ($result['success']) {
wp_send_json_success($result['data']['voices']);
} else {
$error_message = 'Failed to refresh voices';
if (is_string($result['error'])) {
$error_message = $result['error'];
} elseif (is_array($result['error']) && isset($result['error']['detail'])) {
$error_message = $result['error']['detail'];
}
wp_send_json_error($error_message);
}
}
/** /**
* AJAX handler for getting ElevenLabs models * AJAX handler for getting ElevenLabs models
*/ */

View File

@@ -413,6 +413,7 @@ jQuery(document).ready(function($) {
html += '</select>'; html += '</select>';
html += '<input type="hidden" name="voice_name" value="' + (data.voice_name || '') + '">'; html += '<input type="hidden" name="voice_name" value="' + (data.voice_name || '') + '">';
html += '<button type="button" class="button button-small" onclick="loadWorkflowVoices(this)">Load Voices</button>'; html += '<button type="button" class="button button-small" onclick="loadWorkflowVoices(this)">Load Voices</button>';
html += '<button type="button" class="button button-small" onclick="refreshWorkflowVoices(this)" title="Refresh voices">🔄</button>';
html += '</div>'; html += '</div>';
// Audio File Options // Audio File Options
@@ -454,6 +455,7 @@ jQuery(document).ready(function($) {
html += '</select>'; html += '</select>';
html += '<input type="hidden" name="voice_name" value="' + (data.voice_name || '') + '">'; html += '<input type="hidden" name="voice_name" value="' + (data.voice_name || '') + '">';
html += '<button type="button" class="button button-small" onclick="loadWorkflowVoices(this)">Load Voices</button>'; html += '<button type="button" class="button button-small" onclick="loadWorkflowVoices(this)">Load Voices</button>';
html += '<button type="button" class="button button-small" onclick="refreshWorkflowVoices(this)" title="Refresh voices">🔄</button>';
html += '</div>'; html += '</div>';
// Audio File Options // Audio File Options
@@ -520,6 +522,7 @@ jQuery(document).ready(function($) {
html += '</select>'; html += '</select>';
html += '<input type="hidden" name="voice_name" value="' + (data.voice_name || '') + '">'; html += '<input type="hidden" name="voice_name" value="' + (data.voice_name || '') + '">';
html += '<button type="button" class="button button-small" onclick="loadWorkflowVoices(this)">Load Voices</button>'; html += '<button type="button" class="button button-small" onclick="loadWorkflowVoices(this)">Load Voices</button>';
html += '<button type="button" class="button button-small" onclick="refreshWorkflowVoices(this)" title="Refresh voices">🔄</button>';
html += '</div>'; html += '</div>';
// Audio File Options // Audio File Options
@@ -559,6 +562,7 @@ jQuery(document).ready(function($) {
html += '</select>'; html += '</select>';
html += '<input type="hidden" name="voice_name" value="' + (data.voice_name || '') + '">'; html += '<input type="hidden" name="voice_name" value="' + (data.voice_name || '') + '">';
html += '<button type="button" class="button button-small" onclick="loadWorkflowVoices(this)">Load Voices</button>'; html += '<button type="button" class="button button-small" onclick="loadWorkflowVoices(this)">Load Voices</button>';
html += '<button type="button" class="button button-small" onclick="refreshWorkflowVoices(this)" title="Refresh voices">🔄</button>';
html += '</div>'; html += '</div>';
// Audio File Options // Audio File Options
@@ -1774,6 +1778,97 @@ jQuery(document).ready(function($) {
}); });
}; };
// Refresh voices function for workflow
window.refreshWorkflowVoices = function(buttonOrSelect) {
var $button, $select;
// Check if we were passed a button or a select element
if ($(buttonOrSelect).is('button')) {
$button = $(buttonOrSelect);
$select = $button.prev('select.voice-select');
} else if ($(buttonOrSelect).is('select')) {
$select = $(buttonOrSelect);
$button = $select.next('button');
} else {
// Fallback - assume it's a button
$button = $(buttonOrSelect);
$select = $button.prev('select.voice-select');
}
// Read the current value
var currentValue = '';
var selectedOption = $select.find('option:selected');
if (selectedOption.length && selectedOption.val()) {
currentValue = selectedOption.val();
} else {
currentValue = $select.attr('data-current') || '';
}
if ($button.length) {
$button.text('Refreshing...').prop('disabled', true);
}
$.post(twp_ajax.ajax_url, {
action: 'twp_refresh_elevenlabs_voices',
nonce: twp_ajax.nonce
}, function(response) {
if ($button.length) {
$button.text('🔄').prop('disabled', false);
}
if (response.success) {
var options = '<option value="">Default voice</option>';
response.data.forEach(function(voice) {
var selected = (voice.voice_id === currentValue) ? ' selected' : '';
var description = voice.labels ? Object.values(voice.labels).join(', ') : '';
var optionText = voice.name + (description ? ' (' + description + ')' : '');
options += '<option value="' + voice.voice_id + '" data-voice-name="' + voice.name + '"' + selected + '>' + optionText + '</option>';
});
$select.html(options);
// If we had a current value, make sure it's selected
if (currentValue) {
$select.val(currentValue);
// Update the voice name field with the selected voice's name
var $voiceNameInput = $select.siblings('input[name="voice_name"]');
if ($voiceNameInput.length) {
var selectedVoice = $select.find('option:selected');
var voiceName = selectedVoice.data('voice-name') || selectedVoice.text() || '';
if (selectedVoice.val() === '') {
voiceName = '';
}
$voiceNameInput.val(voiceName);
}
}
// Show success message
var $statusMsg = $('<span style="color: green; font-size: 12px; margin-left: 5px;">Refreshed!</span>');
$button.after($statusMsg);
setTimeout(function() {
$statusMsg.remove();
}, 3000);
} else {
var errorMessage = 'Error refreshing voices: ';
if (typeof response.data === 'string') {
errorMessage += response.data;
} else if (response.data && response.data.message) {
errorMessage += response.data.message;
} else {
errorMessage += 'Unknown error';
}
alert(errorMessage);
}
}).fail(function() {
if ($button.length) {
$button.text('🔄').prop('disabled', false);
}
alert('Failed to refresh voices. Please check your API key.');
});
};
// Toggle audio type options visibility // Toggle audio type options visibility
$(document).on('change', 'input[name="audio_type"]', function() { $(document).on('change', 'input[name="audio_type"]', function() {
var $container = $(this).closest('.step-config-section'); var $container = $(this).closest('.step-config-section');

View File

@@ -161,6 +161,7 @@ class TWP_Core {
// Eleven Labs AJAX // Eleven Labs AJAX
$this->loader->add_action('wp_ajax_twp_get_elevenlabs_voices', $plugin_admin, 'ajax_get_elevenlabs_voices'); $this->loader->add_action('wp_ajax_twp_get_elevenlabs_voices', $plugin_admin, 'ajax_get_elevenlabs_voices');
$this->loader->add_action('wp_ajax_twp_refresh_elevenlabs_voices', $plugin_admin, 'ajax_refresh_elevenlabs_voices');
$this->loader->add_action('wp_ajax_twp_get_elevenlabs_models', $plugin_admin, 'ajax_get_elevenlabs_models'); $this->loader->add_action('wp_ajax_twp_get_elevenlabs_models', $plugin_admin, 'ajax_get_elevenlabs_models');
$this->loader->add_action('wp_ajax_twp_preview_voice', $plugin_admin, 'ajax_preview_voice'); $this->loader->add_action('wp_ajax_twp_preview_voice', $plugin_admin, 'ajax_preview_voice');

View File

@@ -79,7 +79,9 @@ class TWP_Workflow {
break; break;
case 'forward': case 'forward':
error_log('TWP Workflow: Processing forward step: ' . json_encode($step));
$step_twiml = self::create_forward_twiml($step); $step_twiml = self::create_forward_twiml($step);
error_log('TWP Workflow: Forward step TwiML generated: ' . $step_twiml);
$stop_after_step = true; // Forward ends the workflow $stop_after_step = true; // Forward ends the workflow
break; break;
@@ -135,6 +137,9 @@ class TWP_Workflow {
// Add step TwiML to combined response // Add step TwiML to combined response
if ($step_twiml) { if ($step_twiml) {
error_log('TWP Workflow: Appending step TwiML to combined response');
error_log('TWP Workflow: Step TwiML before append: ' . $step_twiml);
// Parse the step TwiML and append to combined response // Parse the step TwiML and append to combined response
$step_xml = simplexml_load_string($step_twiml); $step_xml = simplexml_load_string($step_twiml);
if ($step_xml) { if ($step_xml) {
@@ -142,10 +147,15 @@ class TWP_Workflow {
self::append_twiml_element($response, $element); self::append_twiml_element($response, $element);
} }
$has_response = true; $has_response = true;
error_log('TWP Workflow: Combined response after append: ' . $response->asXML());
} else {
error_log('TWP Workflow: ERROR - Failed to parse step TwiML: ' . $step_twiml);
} }
// Stop processing if this step type should end the workflow // Stop processing if this step type should end the workflow
if ($stop_after_step) { if ($stop_after_step) {
error_log('TWP Workflow: Stopping after this step (stop_after_step = true)');
break; break;
} }
} }
@@ -153,10 +163,13 @@ class TWP_Workflow {
// Return combined response or default // Return combined response or default
if ($has_response) { if ($has_response) {
return $response->asXML(); $final_twiml = $response->asXML();
error_log('TWP Workflow: Final workflow TwiML response: ' . $final_twiml);
return $final_twiml;
} }
// Default response // Default response
error_log('TWP Workflow: No response generated, returning default response');
return self::create_default_response(); return self::create_default_response();
} }
@@ -196,7 +209,23 @@ class TWP_Workflow {
$response->record($attributes); $response->record($attributes);
break; break;
case 'Dial': case 'Dial':
$response->dial((string) $element, $attributes); // Create dial instance
$dial = $response->dial('', $attributes);
// Add child Number elements
foreach ($element->children() as $child) {
$child_name = $child->getName();
if ($child_name === 'Number') {
$dial->number((string) $child, self::get_attributes($child));
} elseif ($child_name === 'Client') {
$dial->client((string) $child, self::get_attributes($child));
} elseif ($child_name === 'Queue') {
$dial->queue((string) $child, self::get_attributes($child));
} elseif ($child_name === 'Conference') {
$dial->conference((string) $child, self::get_attributes($child));
} elseif ($child_name === 'Sip') {
$dial->sip((string) $child, self::get_attributes($child));
}
}
break; break;
case 'Queue': case 'Queue':
$response->queue((string) $element, $attributes); $response->queue((string) $element, $attributes);
@@ -485,25 +514,72 @@ class TWP_Workflow {
* Create forward TwiML * Create forward TwiML
*/ */
private static function create_forward_twiml($step) { private static function create_forward_twiml($step) {
error_log('TWP Workflow Forward: Creating forward TwiML with step data: ' . print_r($step, true));
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>'); $twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
// Check if data is nested (workflow steps have data nested)
$step_data = isset($step['data']) ? $step['data'] : $step;
// Get the forward number(s) from the proper location
$forward_numbers = array();
// Check various possible locations for forward numbers
if (isset($step_data['forward_numbers']) && is_array($step_data['forward_numbers'])) {
$forward_numbers = $step_data['forward_numbers'];
error_log('TWP Workflow Forward: Found forward_numbers array in data: ' . print_r($forward_numbers, true));
} elseif (isset($step_data['forward_number']) && !empty($step_data['forward_number'])) {
$forward_numbers = array($step_data['forward_number']);
error_log('TWP Workflow Forward: Found single forward_number in data: ' . $step_data['forward_number']);
} elseif (isset($step_data['number']) && !empty($step_data['number'])) {
$forward_numbers = array($step_data['number']);
error_log('TWP Workflow Forward: Found number in data: ' . $step_data['number']);
} elseif (isset($step['forward_numbers']) && is_array($step['forward_numbers'])) {
$forward_numbers = $step['forward_numbers'];
error_log('TWP Workflow Forward: Found forward_numbers array: ' . print_r($forward_numbers, true));
} elseif (isset($step['forward_number']) && !empty($step['forward_number'])) {
$forward_numbers = array($step['forward_number']);
error_log('TWP Workflow Forward: Found single forward_number: ' . $step['forward_number']);
} elseif (isset($step['number']) && !empty($step['number'])) {
$forward_numbers = array($step['number']);
error_log('TWP Workflow Forward: Found number: ' . $step['number']);
}
// Filter out empty numbers
$forward_numbers = array_filter($forward_numbers, function($num) {
return !empty($num);
});
if (empty($forward_numbers)) {
error_log('TWP Workflow Forward: ERROR - No forward numbers found in step data');
// Return error message instead of empty dial
$say = $twiml->addChild('Say', 'Sorry, the forwarding destination is not configured. Please try again later.');
$say->addAttribute('voice', 'alice');
$twiml->addChild('Hangup');
return $twiml->asXML();
}
error_log('TWP Workflow Forward: Forwarding to numbers: ' . implode(', ', $forward_numbers));
$dial = $twiml->addChild('Dial'); $dial = $twiml->addChild('Dial');
$dial->addAttribute('answerOnBridge', 'true'); $dial->addAttribute('answerOnBridge', 'true');
if (isset($step['timeout'])) { // Set timeout (default to 30 seconds if not specified)
$dial->addAttribute('timeout', $step['timeout']); $timeout = isset($step_data['timeout']) ? $step_data['timeout'] :
(isset($step['timeout']) ? $step['timeout'] : '30');
$dial->addAttribute('timeout', $timeout);
error_log('TWP Workflow Forward: Using timeout: ' . $timeout);
// Add all forward numbers
foreach ($forward_numbers as $number) {
error_log('TWP Workflow Forward: Adding number to Dial: ' . $number);
$dial->addChild('Number', $number);
} }
if (isset($step['forward_numbers']) && is_array($step['forward_numbers'])) { $result = $twiml->asXML();
// Sequential forwarding error_log('TWP Workflow Forward: Final Forward TwiML: ' . $result);
foreach ($step['forward_numbers'] as $number) {
$dial->addChild('Number', $number);
}
} elseif (isset($step['forward_number'])) {
$dial->addChild('Number', $step['forward_number']);
}
return $twiml->asXML(); return $result;
} }
/** /**