fix(12-02): Update All flow — 9 bug fixes from UAT
Fixes discovered during BATCH-04/BATCH-05 UAT testing:
- Convert confirmation to HTTP Request (editMessageText for inline kb,
sendMessage for text command) with dynamic endpoint selection
- Fix data chain breaks: use named node refs ($('Parse Callback Data'),
$('Get Update All Data')) instead of $json after API calls
- Add infrastructure container exclusion (n8n, socket-proxy) by image
and container name to prevent bot self-destruction during updates
- Add batch responseMode to update sub-workflow (skip Telegram messages)
- Reorder infra check before :latest filter so sha256-digest images
appear in skipped list
- Add onError:continueRegularOutput to Answer Update All Start for
expired callback queries
- Show "Back to List" button in batch summary for update-all flow
- Add Prepare Update All Batch fallback in Prepare Batch Loop
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+22
-28
@@ -2397,7 +2397,7 @@
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Store progress message ID and prepare first iteration\nlet batchState;\ntry {\n batchState = $('Initialize Batch State').item.json;\n} catch (e) {\n throw new Error('Failed to get Initialize Batch State: ' + e.message);\n}\n\nif (!batchState || !batchState.containers || !Array.isArray(batchState.containers)) {\n throw new Error('Invalid batch state');\n}\n\n// Get the message ID for progress updates\n// For keyboard: use original messageId (we edit in place)\n// For text commands: use the new message_id from sendMessage response\nlet progressMessageId;\nif (batchState.fromKeyboard) {\n progressMessageId = batchState.messageId;\n} else {\n // Get message_id from Send Batch Start Message response\n const sendResponse = $json;\n progressMessageId = sendResponse.result?.message_id || batchState.messageId;\n}\n\n// Return single item with all data for manual loop\n// We'll process containers[currentIndex] and increment\nreturn {\n json: {\n containers: batchState.containers,\n currentIndex: 0,\n container: batchState.containers[0],\n action: batchState.action,\n totalCount: batchState.totalCount,\n chatId: batchState.chatId,\n progressMessageId: progressMessageId,\n successCount: 0,\n failureCount: 0,\n warningCount: 0,\n results: [],\n fromKeyboard: batchState.fromKeyboard || false\n }\n};"
|
||||
"jsCode": "// Store progress message ID and prepare first iteration\n// Support both regular batch path (Initialize Batch State) and update-all path (Prepare Update All Batch)\nlet batchState;\ntry {\n batchState = $('Initialize Batch State').item.json;\n} catch (e) {\n try {\n batchState = $('Prepare Update All Batch').item.json;\n } catch (e2) {\n throw new Error('Failed to get batch state from either source');\n }\n}\n\nif (!batchState || !batchState.containers || !Array.isArray(batchState.containers)) {\n throw new Error('Invalid batch state');\n}\n\n// Get the message ID for progress updates\n// For keyboard: use original messageId (we edit in place)\n// For text commands: use the new message_id from sendMessage response\nlet progressMessageId;\nif (batchState.fromKeyboard) {\n progressMessageId = batchState.messageId;\n} else {\n // Get message_id from Send Batch Start Message response\n const sendResponse = $json;\n progressMessageId = sendResponse.result?.message_id || batchState.messageId;\n}\n\n// Return single item with all data for manual loop\n// We'll process containers[currentIndex] and increment\nreturn {\n json: {\n containers: batchState.containers,\n currentIndex: 0,\n container: batchState.containers[0],\n action: batchState.action,\n totalCount: batchState.totalCount,\n chatId: batchState.chatId,\n progressMessageId: progressMessageId,\n successCount: 0,\n failureCount: 0,\n warningCount: 0,\n results: [],\n fromKeyboard: batchState.fromKeyboard || false,\n fromBatchUpdateAll: batchState.fromBatchUpdateAll || false\n }\n};"
|
||||
},
|
||||
"id": "code-prepare-batch-loop",
|
||||
"name": "Prepare Batch Loop",
|
||||
@@ -2629,7 +2629,7 @@
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Build batch summary message with failure emphasis\n// Input: final batch state with results array and counters\nconst data = $json;\n\nconst action = data.action;\nconst results = data.results || [];\nconst successCount = data.successCount || 0;\nconst failureCount = data.failureCount || 0;\nconst warningCount = data.warningCount || 0;\nconst totalCount = data.totalCount || results.length;\nconst chatId = data.chatId;\nconst progressMessageId = data.progressMessageId;\nconst fromKeyboard = data.fromKeyboard || false;\n\n// Build summary text - failures emphasized first\nlet summaryText = `<b>Batch ${action} Complete</b>\\n\\n`;\n\n// Show failures first and prominently\nconst failures = results.filter(r => r.status === 'error');\nif (failures.length > 0) {\n summaryText += `<b>\\u274c Failed (${failures.length}):</b>\\n`;\n for (const f of failures) {\n summaryText += `\\u2022 ${f.name}: ${f.reason || 'Unknown error'}\\n`;\n }\n summaryText += '\\n';\n}\n\n// Show warnings summary (not individual) per context discretion\nconst warnings = results.filter(r => r.status === 'warning');\nif (warnings.length > 0) {\n // If 3 or fewer warnings, show details; otherwise just count\n if (warnings.length <= 3) {\n summaryText += `<b>\\u26a0\\ufe0f Warnings (${warnings.length}):</b>\\n`;\n for (const w of warnings) {\n summaryText += `\\u2022 ${w.name}: ${w.reason}\\n`;\n }\n summaryText += '\\n';\n } else {\n summaryText += `\\u26a0\\ufe0f Warnings: ${warnings.length} (containers already in desired state)\\n\\n`;\n }\n}\n\n// Show success count\nif (successCount > 0) {\n summaryText += `<b>\\u2705 Successful:</b> ${successCount}/${totalCount}\\n`;\n} else if (failureCount === 0 && warningCount > 0) {\n // All warnings, no success/failures\n summaryText += `No changes needed for ${totalCount} container(s)\\n`;\n}\n\n// Only include Back to List button if user came from inline keyboard\nconst replyMarkup = fromKeyboard ? { inline_keyboard: [[{ text: 'Back to List', callback_data: 'list:0' }]] } : null;\n\nreturn {\n json: {\n chatId: chatId,\n messageId: progressMessageId,\n text: summaryText,\n hasFailures: failureCount > 0,\n reply_markup: replyMarkup\n }\n};"
|
||||
"jsCode": "// Build batch summary message with failure emphasis\n// Input: final batch state with results array and counters\nconst data = $json;\n\nconst action = data.action;\nconst results = data.results || [];\nconst successCount = data.successCount || 0;\nconst failureCount = data.failureCount || 0;\nconst warningCount = data.warningCount || 0;\nconst totalCount = data.totalCount || results.length;\nconst chatId = data.chatId;\nconst progressMessageId = data.progressMessageId;\nconst fromKeyboard = data.fromKeyboard || false;\n\n// Build summary text - failures emphasized first\nlet summaryText = `<b>Batch ${action} Complete</b>\\n\\n`;\n\n// Show failures first and prominently\nconst failures = results.filter(r => r.status === 'error');\nif (failures.length > 0) {\n summaryText += `<b>\\u274c Failed (${failures.length}):</b>\\n`;\n for (const f of failures) {\n summaryText += `\\u2022 ${f.name}: ${f.reason || 'Unknown error'}\\n`;\n }\n summaryText += '\\n';\n}\n\n// Show warnings summary (not individual) per context discretion\nconst warnings = results.filter(r => r.status === 'warning');\nif (warnings.length > 0) {\n // If 3 or fewer warnings, show details; otherwise just count\n if (warnings.length <= 3) {\n summaryText += `<b>\\u26a0\\ufe0f Warnings (${warnings.length}):</b>\\n`;\n for (const w of warnings) {\n summaryText += `\\u2022 ${w.name}: ${w.reason}\\n`;\n }\n summaryText += '\\n';\n } else {\n summaryText += `\\u26a0\\ufe0f Warnings: ${warnings.length} (containers already in desired state)\\n\\n`;\n }\n}\n\n// Show success count\nif (successCount > 0) {\n summaryText += `<b>\\u2705 Successful:</b> ${successCount}/${totalCount}\\n`;\n} else if (failureCount === 0 && warningCount > 0) {\n // All warnings, no success/failures\n summaryText += `No changes needed for ${totalCount} container(s)\\n`;\n}\n\n// Include Back to List button if user came from inline keyboard or update-all flow\nconst fromBatchUpdateAll = data.fromBatchUpdateAll || false;\nconst replyMarkup = (fromKeyboard || fromBatchUpdateAll) ? { inline_keyboard: [[{ text: 'Back to List', callback_data: 'list:0' }]] } : null;\n\nreturn {\n json: {\n chatId: chatId,\n messageId: progressMessageId,\n text: summaryText,\n hasFailures: failureCount > 0,\n reply_markup: replyMarkup\n }\n};"
|
||||
},
|
||||
"id": "code-build-batch-summary",
|
||||
"name": "Build Batch Summary",
|
||||
@@ -2674,7 +2674,7 @@
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Check which containers have available updates\n// Focus on containers using :latest tag for practical performance\n\nconst containers = $input.all();\n\n// Get chatId and messageId from either text command or callback origin\nlet chatId, messageId;\ntry {\n const kwData = $('Keyword Router').first().json;\n chatId = kwData.message.chat.id;\n messageId = kwData.message.message_id;\n} catch(e) {\n // Inline keyboard origin -- data from Parse Callback Data\n const cbData = $('Parse Callback Data').first().json;\n chatId = cbData.chatId;\n messageId = cbData.messageId;\n}\n\n// Extract container data from API response\nlet allContainers = [];\nfor (const item of containers) {\n if (Array.isArray(item.json)) {\n allContainers = allContainers.concat(item.json);\n } else {\n allContainers.push(item.json);\n }\n}\n\n// Filter to containers using :latest tag\n// For now, we'll mark all :latest containers as \"potentially need update\"\n// A full implementation would pull each image, but that's expensive\n// We'll show all :latest containers and let batch execution handle the actual check\n\nconst containersToCheck = allContainers\n .filter(c => {\n const image = c.Image || '';\n // Check if using :latest or no tag specified (defaults to :latest)\n return image.includes(':latest') || !image.includes(':');\n })\n .map(c => ({\n id: c.Id.substring(0, 12),\n name: (c.Names && c.Names[0]) ? c.Names[0].replace(/^\\//, '') : 'unknown',\n image: c.Image,\n state: c.State\n }));\n\nreturn {\n json: {\n containersToUpdate: containersToCheck,\n count: containersToCheck.length,\n chatId: chatId,\n messageId: messageId,\n allContainers: allContainers\n }\n};"
|
||||
"jsCode": "// Check which containers have available updates\n// Focus on containers using :latest tag for practical performance\n\nconst containers = $input.all();\n\n// Get chatId and messageId from either text command or callback origin\nlet chatId, messageId;\ntry {\n const kwData = $('Keyword Router').first().json;\n chatId = kwData.message.chat.id;\n messageId = kwData.message.message_id;\n} catch(e) {\n // Inline keyboard origin -- data from Parse Callback Data\n const cbData = $('Parse Callback Data').first().json;\n chatId = cbData.chatId;\n messageId = cbData.messageId;\n}\n\n// Extract container data from API response\nlet allContainers = [];\nfor (const item of containers) {\n if (Array.isArray(item.json)) {\n allContainers = allContainers.concat(item.json);\n } else {\n allContainers.push(item.json);\n }\n}\n\n// Filter to :latest containers, excluding infrastructure\n// Infrastructure containers would break the bot if updated mid-execution\n// Check both image AND container name (image may be sha256 digest)\nconst infraPatterns = ['n8n', 'socket-proxy'];\nconst skipped = [];\n\nconst containersToCheck = allContainers\n .filter(c => {\n const image = (c.Image || '').toLowerCase();\n const name = ((c.Names && c.Names[0]) ? c.Names[0].replace(/^\\//, '') : '').toLowerCase();\n // Check infrastructure FIRST so they always appear in skipped list\n const isInfra = infraPatterns.some(p => image.includes(p) || name.includes(p));\n if (isInfra) {\n skipped.push((c.Names && c.Names[0]) ? c.Names[0].replace(/^\\//, '') : 'unknown');\n return false;\n }\n // Then filter to :latest or untagged only\n if (!image.includes(':latest') && image.includes(':')) return false;\n return true;\n })\n .map(c => ({\n id: c.Id.substring(0, 12),\n name: (c.Names && c.Names[0]) ? c.Names[0].replace(/^\\//, '') : 'unknown',\n image: c.Image,\n state: c.State\n }));\n\n// isCallback = true when triggered via inline keyboard button\nlet isCallback = false;\ntry { $('Parse Callback Data').first(); isCallback = true; } catch(e) {}\n\nreturn {\n json: {\n containersToUpdate: containersToCheck,\n count: containersToCheck.length,\n chatId: chatId,\n messageId: messageId,\n isCallback: isCallback,\n allContainers: allContainers,\n skippedInfra: skipped\n }\n};"
|
||||
},
|
||||
"id": "code-check-available-updates",
|
||||
"name": "Check Available Updates",
|
||||
@@ -2718,7 +2718,7 @@
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Build confirmation message for update all\nconst data = $json;\nconst containers = data.containersToUpdate || [];\nconst count = data.count || 0;\n\n// Build container list (max 10 for display)\nconst displayContainers = containers.slice(0, 10);\nconst containerList = displayContainers.map(c => `\u2022 ${c.name}`).join('\\n');\nconst moreText = count > 10 ? `\\n...and ${count - 10} more` : '';\n\nconst message = `Update ${count} container${count !== 1 ? 's' : ''}?\\n\\n${containerList}${moreText}`;\n\n// Create inline keyboard\nconst timestamp = Math.floor(Date.now() / 1000);\nconst containerNames = containers.map(c => c.name).join(',');\n\n// Encode container names in callback (will need to lookup IDs later)\nreturn {\n json: {\n chatId: data.chatId,\n messageId: data.messageId,\n message: message,\n keyboard: {\n inline_keyboard: [\n [\n { text: '\u2705 Confirm', callback_data: `uall:confirm:${timestamp}` },\n { text: '\u274c Cancel', callback_data: 'uall:cancel' }\n ]\n ]\n },\n containerNames: containerNames,\n containers: containers,\n timestamp: timestamp\n }\n};"
|
||||
"jsCode": "// Build confirmation message for update all\nconst data = $json;\nconst containers = data.containersToUpdate || [];\nconst count = data.count || 0;\nconst skipped = data.skippedInfra || [];\n\n// Build container list (max 10 for display)\nconst displayContainers = containers.slice(0, 10);\nconst containerList = displayContainers.map(c => `\u2022 ${c.name}`).join('\\n');\nconst moreText = count > 10 ? `\\n...and ${count - 10} more` : '';\nconst skipText = skipped.length > 0 ? `\\n\\n\u26a0\ufe0f Skipped (infrastructure): ${skipped.join(', ')}` : '';\n\nconst message = `Update ${count} container${count !== 1 ? 's' : ''}?\\n\\n${containerList}${moreText}${skipText}`;\n\n// Create inline keyboard\nconst timestamp = Math.floor(Date.now() / 1000);\nconst containerNames = containers.map(c => c.name).join(',');\n\n// Encode container names in callback (will need to lookup IDs later)\nreturn {\n json: {\n chatId: data.chatId,\n messageId: data.messageId,\n text: message,\n reply_markup: {\n inline_keyboard: [\n [\n { text: '\u2705 Confirm', callback_data: `uall:confirm:${timestamp}` },\n { text: '\u274c Cancel', callback_data: 'uall:cancel' }\n ]\n ]\n },\n isCallback: data.isCallback || false,\n containerNames: containerNames,\n containers: containers,\n timestamp: timestamp\n }\n};"
|
||||
},
|
||||
"id": "code-build-update-all-confirmation",
|
||||
"name": "Build Update All Confirmation",
|
||||
@@ -2731,28 +2731,21 @@
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "message",
|
||||
"operation": "sendMessage",
|
||||
"chatId": "={{ $json.chatId }}",
|
||||
"text": "={{ $json.message }}",
|
||||
"replyMarkup": "inlineKeyboard",
|
||||
"inlineKeyboard": "={{ JSON.stringify($json.keyboard) }}",
|
||||
"method": "POST",
|
||||
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/{{ $json.isCallback ? 'editMessageText' : 'sendMessage' }}",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify(Object.assign({ chat_id: $json.chatId, text: $json.text, parse_mode: 'HTML', reply_markup: $json.reply_markup }, $json.isCallback ? { message_id: $json.messageId } : {})) }}",
|
||||
"options": {}
|
||||
},
|
||||
"id": "telegram-send-update-all-confirmation",
|
||||
"name": "Send Update All Confirmation",
|
||||
"type": "n8n-nodes-base.telegram",
|
||||
"typeVersion": 1.2,
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [
|
||||
2000,
|
||||
2100
|
||||
],
|
||||
"credentials": {
|
||||
"telegramApi": {
|
||||
"id": "I0xTTiASl7C1NZhJ",
|
||||
"name": "Telegram account"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
@@ -2806,8 +2799,8 @@
|
||||
"parameters": {
|
||||
"resource": "message",
|
||||
"operation": "deleteMessage",
|
||||
"chatId": "={{ $json.chatId }}",
|
||||
"messageId": "={{ $json.messageId }}"
|
||||
"chatId": "={{ $('Parse Callback Data').item.json.chatId }}",
|
||||
"messageId": "={{ $('Parse Callback Data').item.json.messageId }}"
|
||||
},
|
||||
"id": "telegram-delete-update-all-expired",
|
||||
"name": "Delete Update All Expired",
|
||||
@@ -2850,8 +2843,8 @@
|
||||
"parameters": {
|
||||
"resource": "message",
|
||||
"operation": "deleteMessage",
|
||||
"chatId": "={{ $json.chatId }}",
|
||||
"messageId": "={{ $json.messageId }}"
|
||||
"chatId": "={{ $('Parse Callback Data').item.json.chatId }}",
|
||||
"messageId": "={{ $('Parse Callback Data').item.json.messageId }}"
|
||||
},
|
||||
"id": "telegram-delete-update-all-cancel",
|
||||
"name": "Delete Update All Cancel",
|
||||
@@ -2938,8 +2931,8 @@
|
||||
"parameters": {
|
||||
"resource": "message",
|
||||
"operation": "deleteMessage",
|
||||
"chatId": "={{ $json.chatId }}",
|
||||
"messageId": "={{ $json.messageId }}"
|
||||
"chatId": "={{ $('Parse Callback Data').item.json.chatId }}",
|
||||
"messageId": "={{ $('Parse Callback Data').item.json.messageId }}"
|
||||
},
|
||||
"id": "telegram-delete-update-all-confirm",
|
||||
"name": "Delete Update All Confirm",
|
||||
@@ -2972,7 +2965,7 @@
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Prepare batch execution data for update all\n// Get all :latest containers and prepare for batch loop\n\nconst containers = $input.all();\nconst chatId = $json.chatId;\n\n// Extract container data\nlet allContainers = [];\nfor (const item of containers) {\n if (Array.isArray(item.json)) {\n allContainers = allContainers.concat(item.json);\n } else {\n allContainers.push(item.json);\n }\n}\n\n// Filter to :latest containers\nconst targetContainers = allContainers\n .filter(c => {\n const image = c.Image || '';\n return image.includes(':latest') || !image.includes(':');\n })\n .map(c => ({\n id: c.Id.substring(0, 12),\n name: (c.Names && c.Names[0]) ? c.Names[0].replace(/^\\//, '') : 'unknown',\n image: c.Image,\n state: c.State\n }));\n\n// Format for batch execution (compatible with existing batch infrastructure)\nreturn {\n json: {\n chatId: chatId,\n action: 'update',\n containers: targetContainers,\n items: targetContainers.map(c => ({ name: c.name, id: c.id })),\n currentIndex: 0,\n totalCount: targetContainers.length,\n results: []\n }\n};"
|
||||
"jsCode": "// Prepare batch execution data for update all\n// Output same shape as Initialize Batch State for batch loop compatibility\n\nconst containers = $input.all();\nconst prevData = $('Get Update All Data').item.json;\nconst chatId = prevData.chatId;\n\n// Extract container data\nlet allContainers = [];\nfor (const item of containers) {\n if (Array.isArray(item.json)) {\n allContainers = allContainers.concat(item.json);\n } else {\n allContainers.push(item.json);\n }\n}\n\n// Exclude infrastructure containers that would break the bot if updated:\n// - n8n: updating kills the running workflow mid-execution\n// - docker-socket-proxy: updating kills Docker API access\nconst infraPatterns = ['n8n', 'socket-proxy'];\nconst skipped = [];\n\nconst targetContainers = allContainers\n .filter(c => {\n const image = (c.Image || '').toLowerCase();\n const name = ((c.Names && c.Names[0]) ? c.Names[0].replace(/^\\//, '') : '').toLowerCase();\n // Check infrastructure FIRST so they always appear in skipped list\n const isInfra = infraPatterns.some(p => image.includes(p) || name.includes(p));\n if (isInfra) {\n skipped.push((c.Names && c.Names[0]) ? c.Names[0].replace(/^\\//, '') : 'unknown');\n return false;\n }\n // Then filter to :latest or untagged\n if (!image.includes(':latest') && image.includes(':')) return false;\n return true;\n })\n .map(c => ({\n Name: (c.Names && c.Names[0]) ? c.Names[0].replace(/^\\//, '') : 'unknown',\n Id: c.Id ? c.Id.substring(0, 12) : null,\n Image: c.Image\n }));\n\n// Match Initialize Batch State output shape exactly\nreturn {\n json: {\n containers: targetContainers,\n action: 'update',\n totalCount: targetContainers.length,\n successCount: 0,\n failureCount: 0,\n warningCount: 0,\n results: [],\n chatId: chatId,\n messageId: null,\n currentIndex: 0,\n progressMessageId: null,\n fromKeyboard: false,\n fromBatchUpdateAll: true,\n skippedInfra: skipped\n }\n};"
|
||||
},
|
||||
"id": "code-prepare-update-all-batch",
|
||||
"name": "Prepare Update All Batch",
|
||||
@@ -3210,7 +3203,7 @@
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Prepare input for Container Update sub-workflow\nconst data = $('Build Progress Message').item.json;\nconst container = data.container;\n\n// Extract container info\nconst containerId = container.id || container.Id || '';\nconst containerName = container.name || container.Name || '';\n\nreturn {\n json: {\n containerId: containerId,\n containerName: containerName,\n chatId: data.chatId,\n messageId: data.progressMessageId || 0,\n responseMode: \"inline\",\n correlationId: $input.item.json.correlationId || ''\n }\n};"
|
||||
"jsCode": "// Prepare input for Container Update sub-workflow\nconst data = $('Build Progress Message').item.json;\nconst container = data.container;\n\n// Extract container info\nconst containerId = container.id || container.Id || '';\nconst containerName = container.name || container.Name || '';\n\n// Use batch mode (suppresses sub-workflow Telegram messages) for update-all path\nconst responseMode = data.fromBatchUpdateAll ? 'batch' : 'inline';\n\nreturn {\n json: {\n containerId: containerId,\n containerName: containerName,\n chatId: data.chatId,\n messageId: data.progressMessageId || 0,\n responseMode: responseMode,\n correlationId: $input.item.json.correlationId || ''\n }\n};"
|
||||
},
|
||||
"id": "caeae5d6-f9ec-4aa3-83d3-198b6b55be65",
|
||||
"name": "Prepare Batch Update Input",
|
||||
@@ -4783,7 +4776,8 @@
|
||||
"id": "I0xTTiASl7C1NZhJ",
|
||||
"name": "Telegram account"
|
||||
}
|
||||
}
|
||||
},
|
||||
"onError": "continueRegularOutput"
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
|
||||
Reference in New Issue
Block a user