d842efbf17
Parse and Match now returns formatted text directly, so Format Response node is redundant. Connect Parse and Match directly to Send Docker Response to avoid 'Unexpected response format' error. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
3401 lines
118 KiB
JSON
3401 lines
118 KiB
JSON
{
|
|
"name": "Docker Manager Bot",
|
|
"nodes": [
|
|
{
|
|
"parameters": {
|
|
"updates": [
|
|
"message",
|
|
"callback_query"
|
|
]
|
|
},
|
|
"id": "telegram-trigger",
|
|
"name": "Telegram Trigger",
|
|
"type": "n8n-nodes-base.telegramTrigger",
|
|
"typeVersion": 1.1,
|
|
"position": [
|
|
240,
|
|
300
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"rules": {
|
|
"values": [
|
|
{
|
|
"id": "route-message",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "has-message",
|
|
"leftValue": "={{ $json.message?.text }}",
|
|
"rightValue": "",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "notEmpty"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "message"
|
|
},
|
|
{
|
|
"id": "route-callback",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "has-callback",
|
|
"leftValue": "={{ $json.callback_query?.id }}",
|
|
"rightValue": "",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "notEmpty"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "callback_query"
|
|
}
|
|
]
|
|
},
|
|
"options": {
|
|
"fallbackOutput": "none"
|
|
}
|
|
},
|
|
"id": "switch-update-type",
|
|
"name": "Route Update Type",
|
|
"type": "n8n-nodes-base.switch",
|
|
"typeVersion": 3.2,
|
|
"position": [
|
|
460,
|
|
300
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "user-auth-condition",
|
|
"leftValue": "={{ $json.message.from.id.toString() }}",
|
|
"rightValue": "563878771",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "if-auth",
|
|
"name": "IF User Authenticated",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
680,
|
|
200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "callback-auth-condition",
|
|
"leftValue": "={{ $json.callback_query.from.id.toString() }}",
|
|
"rightValue": "563878771",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "if-callback-auth",
|
|
"name": "IF Callback Authenticated",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
680,
|
|
500
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"rules": {
|
|
"values": [
|
|
{
|
|
"id": "docker-query-route",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": false,
|
|
"leftValue": "",
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "contains-status",
|
|
"leftValue": "={{ $json.message.text.toLowerCase() }}",
|
|
"rightValue": "status",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "contains"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "or"
|
|
},
|
|
"renameOutput": false
|
|
},
|
|
{
|
|
"id": "action-command-route",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": false,
|
|
"leftValue": "",
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "starts-with-start",
|
|
"leftValue": "={{ $json.message.text.toLowerCase().trim() }}",
|
|
"rightValue": "start ",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "startsWith"
|
|
}
|
|
},
|
|
{
|
|
"id": "starts-with-stop",
|
|
"leftValue": "={{ $json.message.text.toLowerCase().trim() }}",
|
|
"rightValue": "stop ",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "startsWith"
|
|
}
|
|
},
|
|
{
|
|
"id": "starts-with-restart",
|
|
"leftValue": "={{ $json.message.text.toLowerCase().trim() }}",
|
|
"rightValue": "restart ",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "startsWith"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "or"
|
|
},
|
|
"renameOutput": false
|
|
},
|
|
{
|
|
"id": "update-command-route",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": false,
|
|
"leftValue": "",
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "starts-with-update",
|
|
"leftValue": "={{ $json.message.text.toLowerCase().trim() }}",
|
|
"rightValue": "update ",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "startsWith"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "or"
|
|
},
|
|
"renameOutput": false
|
|
},
|
|
{
|
|
"id": "logs-command-route",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": false,
|
|
"leftValue": "",
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "matches-logs",
|
|
"leftValue": "={{ $json.message.text.toLowerCase().trim() }}",
|
|
"rightValue": "^(show\\s+)?logs\\s+",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "regex"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "or"
|
|
},
|
|
"renameOutput": false
|
|
}
|
|
]
|
|
},
|
|
"options": {
|
|
"fallbackOutput": "extra"
|
|
}
|
|
},
|
|
"id": "switch-route",
|
|
"name": "Route Message",
|
|
"type": "n8n-nodes-base.switch",
|
|
"typeVersion": 3.2,
|
|
"position": [
|
|
900,
|
|
200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "curl -s --unix-socket /var/run/docker.sock 'http://localhost/v1.47/containers/json?all=true'",
|
|
"options": {}
|
|
},
|
|
"id": "exec-docker-list",
|
|
"name": "Docker List Containers",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
1120,
|
|
100
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Get Docker API response and intent data\nconst dockerOutput = $input.item.json.stdout;\nconst intent = $('Parse Intent').item.json;\nconst chatId = intent.original_message.chat.id;\n\n// Parse JSON response - only error if we can't parse valid JSON from stdout\nlet containers;\ntry {\n if (!dockerOutput || dockerOutput.trim() === '') {\n throw new Error('Empty response');\n }\n containers = JSON.parse(dockerOutput);\n} catch (e) {\n return [{\n json: {\n chatId: chatId,\n error: true,\n text: \"Can't reach Docker - check if n8n has socket access\"\n }\n }];\n}\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '') // Remove leading slash\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '') // Remove common prefixes\n .toLowerCase();\n}\n\n// Check if user requested a specific container or general list\nconst requestedName = intent.container;\n\n// If no container name specified, return summary\nif (!requestedName || requestedName === '' || intent.action === 'list_containers') {\n const counts = containers.reduce((acc, c) => {\n acc[c.State] = (acc[c.State] || 0) + 1;\n return acc;\n }, {});\n \n const total = containers.length;\n const running = counts['running'] || 0;\n const stopped = counts['exited'] || 0;\n const other = total - running - stopped;\n \n let summary = `<b>Docker Containers</b>\\n\\n`;\n summary += `Running: ${running}/${total}\\n`;\n if (stopped > 0) summary += `Stopped: ${stopped}\\n`;\n if (other > 0) summary += `Other: ${other}\\n`;\n \n // List running containers\n const runningContainers = containers\n .filter(c => c.State === 'running')\n .map(c => {\n const name = normalizeName(c.Names[0]);\n const uptime = c.Status;\n return ` \u2022 ${name} - ${uptime}`;\n });\n \n if (runningContainers.length > 0) {\n summary += `\\n<b>Running:</b>\\n${runningContainers.join('\\n')}`;\n }\n \n return [{\n json: {\n chatId: chatId,\n text: summary\n }\n }];\n}\n\n// Find matching containers\nconst matches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n return containerName.includes(requestedName);\n});\n\nif (matches.length === 0) {\n return [{\n json: {\n chatId: chatId,\n error: true,\n text: `No container found matching \"${requestedName}\"`\n }\n }];\n}\n\nif (matches.length === 1) {\n const container = matches[0];\n const name = normalizeName(container.Names[0]);\n const state = container.State;\n const status = container.Status;\n const image = container.Image;\n \n let text = `<b>${name}</b>\\n`;\n text += `State: ${state}\\n`;\n text += `Status: ${status}\\n`;\n text += `Image: ${image}`;\n \n return [{\n json: {\n chatId: chatId,\n text: text\n }\n }];\n}\n\n// Multiple matches - list them\nconst matchList = matches.map(c => {\n const name = normalizeName(c.Names[0]);\n return ` \u2022 ${name} (${c.State})`;\n}).join('\\n');\n\nreturn [{\n json: {\n chatId: chatId,\n text: `Multiple matches for \"${requestedName}\":\\n${matchList}`\n }\n}];"
|
|
},
|
|
"id": "code-parse-match",
|
|
"name": "Parse and Match",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1340,
|
|
100
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Get the data from previous node\nconst data = $input.item.json;\nconst chatId = data.chatId;\n\n// If error or summary, pass through as-is\nif (data.error || data.summary || data.multipleMatches) {\n return [{\n json: {\n chatId: chatId,\n text: data.text\n }\n }];\n}\n\n// Format single container details\nif (data.singleMatch) {\n const container = data.container;\n\n // State indicator mapping\n const stateIndicator = {\n 'running': '[OK]',\n 'exited': '[STOPPED]',\n 'paused': '[PAUSED]',\n 'restarting': '[RESTARTING]',\n 'dead': '[DEAD]'\n };\n\n const indicator = stateIndicator[container.state] || '[?]';\n\n // Format detailed response\n const text = `${indicator} <b>${container.name}</b>\\n\\n` +\n `<b>State:</b> ${container.state}\\n` +\n `<b>Status:</b> ${container.status}\\n` +\n `<b>Image:</b> ${container.image}\\n` +\n `<b>ID:</b> ${container.id.substring(0, 12)}`;\n\n return [{\n json: {\n chatId: chatId,\n text: text\n }\n }];\n}\n\n// Fallback\nreturn [{\n json: {\n chatId: chatId,\n text: \"Unexpected response format\"\n }\n}];"
|
|
},
|
|
"id": "code-format-response",
|
|
"name": "Format Response",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1560,
|
|
100
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "message",
|
|
"operation": "sendMessage",
|
|
"chatId": "={{ $json.chatId }}",
|
|
"text": "={{ $json.text }}",
|
|
"additionalFields": {
|
|
"parse_mode": "HTML"
|
|
}
|
|
},
|
|
"id": "telegram-send-docker",
|
|
"name": "Send Docker Response",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
1560,
|
|
200
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const message = $input.item.json.message;\nconst timestamp = new Date().toISOString();\nconst text = message.text || '(no text)';\n\nreturn {\n json: {\n chatId: message.chat.id,\n text: `Got: ${text}\\n\\nProcessed: ${timestamp}`\n }\n};"
|
|
},
|
|
"id": "code-format-echo",
|
|
"name": "Format Echo",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
900,
|
|
800
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "message",
|
|
"operation": "sendMessage",
|
|
"chatId": "={{ $json.chatId }}",
|
|
"text": "={{ $json.text }}",
|
|
"additionalFields": {
|
|
"parse_mode": "HTML"
|
|
}
|
|
},
|
|
"id": "telegram-send",
|
|
"name": "Send Echo",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
1120,
|
|
800
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Parse action from intent\n// Intent structure: { action: \"container_action\", container: \"name\", parameters: { action: \"restart\" } }\nconst text = $json.original_message.text.toLowerCase().trim();\nconst chatId = $json.original_message.chat.id;\nconst messageId = $json.original_message.message_id;\n\n// If intent provided an action, use it\nlet requestedAction = $json.parameters.action || null;\nlet containerQuery = $json.container;\n\n// If no action in parameters, try to infer from the original text\nif (!requestedAction) {\n if (text.includes('start') && !text.includes('restart')) {\n requestedAction = 'start';\n } else if (text.includes('stop')) {\n requestedAction = 'stop';\n } else if (text.includes('restart')) {\n requestedAction = 'restart';\n } else if (text.includes('update')) {\n requestedAction = 'update';\n }\n}\n\nif (!requestedAction || !containerQuery) {\n return {\n json: {\n error: true,\n errorMessage: 'Please specify an action (start/stop/restart/update) and container name.',\n chatId: chatId\n }\n };\n}\n\n// Map action names\nconst actionMap = {\n 'start': 'start',\n 'stop': 'stop',\n 'restart': 'restart',\n 'update': 'update'\n};\n\nconst action = actionMap[requestedAction.toLowerCase()];\n\nif (!action) {\n return {\n json: {\n error: true,\n errorMessage: 'Unknown action. Valid actions: start, stop, restart, update',\n chatId: chatId\n }\n };\n}\n\nreturn {\n json: {\n action: action,\n containerQuery: containerQuery,\n chatId: chatId,\n messageId: messageId\n }\n};"
|
|
},
|
|
"id": "code-parse-action",
|
|
"name": "Parse Action",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
900,
|
|
400
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "curl -s --unix-socket /var/run/docker.sock 'http://localhost/v1.47/containers/json?all=true'",
|
|
"options": {}
|
|
},
|
|
"id": "exec-docker-list-action",
|
|
"name": "Docker List for Action",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
1120,
|
|
400
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Get Docker API response and action info from Parse Action\nconst dockerOutput = $input.item.json.stdout;\nconst actionData = $('Parse Action').item.json;\nconst action = actionData.action;\nconst containerQuery = actionData.containerQuery;\nconst chatId = actionData.chatId;\n\n// Parse JSON response\nlet containers;\ntry {\n if (!dockerOutput || dockerOutput.trim() === '') {\n throw new Error('Empty response');\n }\n containers = JSON.parse(dockerOutput);\n} catch (e) {\n return [{\n json: {\n matchCount: -1,\n error: true,\n chatId: chatId,\n text: \"Can't reach Docker - check if n8n has socket access\"\n }\n }];\n}\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '') // Remove leading slash\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '') // Remove common prefixes\n .toLowerCase();\n}\n\n// Find matching containers using fuzzy matching\nconst matches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n const normalized = containerQuery.toLowerCase();\n return containerName.includes(normalized) || normalized.includes(containerName);\n});\n\n// Return match results with all necessary context\n// Include allContainers for suggestion finding when no matches\nreturn [{\n json: {\n matches: matches.map(c => ({\n Id: c.Id,\n Name: c.Names[0].replace(/^\\//, ''),\n State: c.State\n })),\n matchCount: matches.length,\n action: action,\n containerQuery: containerQuery,\n chatId: chatId,\n allContainers: containers\n }\n}];"
|
|
},
|
|
"id": "code-match-container",
|
|
"name": "Match Container",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1340,
|
|
400
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"rules": {
|
|
"values": [
|
|
{
|
|
"id": "docker-error",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "count-negative",
|
|
"leftValue": "={{ $json.matchCount }}",
|
|
"rightValue": "0",
|
|
"operator": {
|
|
"type": "number",
|
|
"operation": "lt"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": false
|
|
},
|
|
{
|
|
"id": "no-match",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "count-zero",
|
|
"leftValue": "={{ $json.matchCount }}",
|
|
"rightValue": "0",
|
|
"operator": {
|
|
"type": "number",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": false
|
|
},
|
|
{
|
|
"id": "single-match",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "count-one",
|
|
"leftValue": "={{ $json.matchCount }}",
|
|
"rightValue": "1",
|
|
"operator": {
|
|
"type": "number",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": false
|
|
},
|
|
{
|
|
"id": "multiple-matches",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "count-gt-one",
|
|
"leftValue": "={{ $json.matchCount }}",
|
|
"rightValue": "1",
|
|
"operator": {
|
|
"type": "number",
|
|
"operation": "gt"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": false
|
|
}
|
|
]
|
|
},
|
|
"options": {
|
|
"fallbackOutput": "extra"
|
|
}
|
|
},
|
|
"id": "switch-match-count",
|
|
"name": "Check Match Count",
|
|
"type": "n8n-nodes-base.switch",
|
|
"typeVersion": 3.2,
|
|
"position": [
|
|
1560,
|
|
400
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build the curl command for the action\nconst data = $input.item.json;\nconst containerId = data.matches[0].Id;\nconst action = data.action;\nconst containerName = data.matches[0].Name;\nconst chatId = data.chatId;\n\n// stop and restart use ?t=10 for graceful timeout\nconst timeout = (action === 'stop' || action === 'restart') ? '?t=10' : '';\n\n// Build curl command that returns HTTP status code\nconst cmd = `curl -s -o /dev/null -w \"%{http_code}\" --unix-socket /var/run/docker.sock -X POST 'http://localhost/v1.47/containers/${containerId}/${action}${timeout}'`;\n\nreturn {\n json: {\n cmd: cmd,\n containerId: containerId,\n containerName: containerName,\n action: action,\n chatId: chatId\n }\n};"
|
|
},
|
|
"id": "code-build-action-cmd",
|
|
"name": "Build Action Command",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1780,
|
|
400
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "={{ $json.cmd }}",
|
|
"options": {}
|
|
},
|
|
"id": "exec-action",
|
|
"name": "Execute Action",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
2000,
|
|
400
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Parse the HTTP status code from curl output\nconst stdout = $input.item.json.stdout;\nconst stderr = $input.item.json.stderr;\nconst actionData = $('Build Action Command').item.json;\nconst containerName = actionData.containerName;\nconst action = actionData.action;\nconst chatId = actionData.chatId;\n\n// Check for curl-level errors first\nif (stderr && stderr.trim()) {\n return {\n json: {\n success: false,\n chatId: chatId,\n text: `Failed to ${action} ${containerName}: ${stderr.trim()}`\n }\n };\n}\n\nconst statusCode = parseInt(stdout.trim());\n\n// 204: Success, 304: Already in state (also success for user)\nif (statusCode === 204 || statusCode === 304) {\n const verb = action === 'start' ? 'started' :\n action === 'stop' ? 'stopped' : 'restarted';\n return {\n json: {\n success: true,\n chatId: chatId,\n text: `<b>${containerName}</b> ${verb} successfully`\n }\n };\n}\n\n// Handle error codes\nlet errorMsg;\nswitch (statusCode) {\n case 404:\n errorMsg = 'Container not found';\n break;\n case 409:\n errorMsg = 'Container is in a conflicting state';\n break;\n case 500:\n errorMsg = 'Docker server error';\n break;\n default:\n errorMsg = `HTTP ${statusCode}`;\n}\n\nreturn {\n json: {\n success: false,\n chatId: chatId,\n text: `Failed to ${action} ${containerName}: ${errorMsg}`\n }\n};"
|
|
},
|
|
"id": "code-parse-action-result",
|
|
"name": "Parse Action Result",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2220,
|
|
400
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "message",
|
|
"operation": "sendMessage",
|
|
"chatId": "={{ $json.chatId }}",
|
|
"text": "={{ $json.text }}",
|
|
"additionalFields": {
|
|
"parse_mode": "HTML"
|
|
}
|
|
},
|
|
"id": "telegram-send-action-result",
|
|
"name": "Send Action Result",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
2440,
|
|
400
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Find closest match for suggestion\nconst query = $json.containerQuery.toLowerCase();\nconst containers = $json.allContainers;\nconst action = $json.action;\nconst chatId = $json.chatId;\n\n// Simple closest match: longest common substring or starts-with\nlet bestMatch = null;\nlet bestScore = 0;\n\nfor (const container of containers) {\n const name = container.Names[0].replace(/^\\//, '').toLowerCase();\n // Score by: contains query, or query contains name, or matching chars\n let score = 0;\n if (name.includes(query)) score = query.length * 2;\n else if (query.includes(name)) score = name.length * 1.5;\n else {\n // Simple: count matching starting characters\n for (let i = 0; i < Math.min(query.length, name.length); i++) {\n if (query[i] === name[i]) score++;\n else break;\n }\n }\n if (score > bestScore) {\n bestScore = score;\n bestMatch = container;\n }\n}\n\n// Require minimum score of 2 to suggest\nif (!bestMatch || bestScore < 2) {\n return { json: { hasSuggestion: false, query, action, chatId } };\n}\n\nconst suggestedName = bestMatch.Names[0].replace(/^\\//, '');\nconst suggestedId = bestMatch.Id.substring(0, 12); // Short ID for callback_data\n\nreturn {\n json: {\n hasSuggestion: true,\n query,\n action,\n chatId,\n suggestedName,\n suggestedId,\n timestamp: Date.now()\n }\n};"
|
|
},
|
|
"id": "code-find-closest",
|
|
"name": "Find Closest Match",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1780,
|
|
300
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "has-suggestion",
|
|
"leftValue": "={{ $json.hasSuggestion }}",
|
|
"rightValue": true,
|
|
"operator": {
|
|
"type": "boolean",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "if-has-suggestion",
|
|
"name": "Check Suggestion",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2000,
|
|
300
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build suggestion keyboard for Telegram\nconst { chatId, query, action, suggestedName, suggestedId, timestamp } = $json;\n\n// callback_data must be <=64 bytes - use short keys\n// a=action (1 char: s=start, t=stop, r=restart)\n// c=container short ID\n// t=timestamp\nconst actionCode = action === 'start' ? 's' : action === 'stop' ? 't' : 'r';\nconst callbackData = JSON.stringify({ a: actionCode, c: suggestedId, t: timestamp });\n\nreturn {\n json: {\n chat_id: chatId,\n text: `No container '<b>${query}</b>' found.\\n\\nDid you mean <b>${suggestedName}</b>?`,\n parse_mode: \"HTML\",\n reply_markup: {\n inline_keyboard: [\n [\n { text: `Yes, ${action} ${suggestedName}`, callback_data: callbackData },\n { text: \"Cancel\", callback_data: '{\"a\":\"x\"}' }\n ]\n ]\n }\n }\n};"
|
|
},
|
|
"id": "code-build-suggestion",
|
|
"name": "Build Suggestion Keyboard",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2220,
|
|
200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $credentials.telegramApi.accessToken }}/sendMessage",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify($json) }}",
|
|
"options": {}
|
|
},
|
|
"id": "http-send-suggestion",
|
|
"name": "Send Suggestion",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
2440,
|
|
200
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "message",
|
|
"operation": "sendMessage",
|
|
"chatId": "={{ $json.chatId }}",
|
|
"text": "=No container found matching '<b>{{ $json.query }}</b>'",
|
|
"additionalFields": {
|
|
"parse_mode": "HTML"
|
|
}
|
|
},
|
|
"id": "telegram-send-no-match",
|
|
"name": "Send No Match",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
2220,
|
|
400
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build batch confirmation keyboard for multiple matches\nconst matches = $json.matches;\nconst action = $json.action;\nconst chatId = $json.chatId;\nconst query = $json.containerQuery;\n\n// List matched container names\nconst names = matches.map(m => m.Name);\nconst shortIds = matches.map(m => m.Id.substring(0, 12));\n\n// Build callback_data - must be <=64 bytes\n// For batch: a=action code, c=array of short IDs, t=timestamp\nconst actionCode = action === 'start' ? 's' : action === 'stop' ? 't' : 'r';\nconst timestamp = Date.now();\n\n// Check size - if too many containers, callback_data might exceed 64 bytes\n// Each short ID is 12 chars, plus overhead. Max ~3-4 containers safely\nlet callbackData;\nlet limitedCount = shortIds.length;\nif (shortIds.length <= 4) {\n callbackData = JSON.stringify({ a: actionCode, c: shortIds, t: timestamp });\n} else {\n // Too many containers - limit to first 4\n callbackData = JSON.stringify({ a: actionCode, c: shortIds.slice(0, 4), t: timestamp });\n limitedCount = 4;\n}\n\n// Format container list\nconst listText = names.map(n => ` \\u2022 ${n}`).join('\\n');\n\n// Build action verb for button\nconst actionVerb = action.charAt(0).toUpperCase() + action.slice(1);\n\nreturn {\n json: {\n chat_id: chatId,\n text: `Found <b>${matches.length}</b> containers matching '<b>${query}</b>':\\n\\n${listText}\\n\\n${actionVerb} all?`,\n parse_mode: \"HTML\",\n reply_markup: {\n inline_keyboard: [\n [\n { text: `Yes, ${action} ${limitedCount} containers`, callback_data: callbackData },\n { text: \"Cancel\", callback_data: '{\"a\":\"x\"}' }\n ]\n ]\n },\n // Store metadata for summary\n _meta: {\n action,\n containers: matches,\n timestamp,\n limitedCount\n }\n }\n};"
|
|
},
|
|
"id": "code-build-batch-keyboard",
|
|
"name": "Build Batch Keyboard",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1780,
|
|
500
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $credentials.telegramApi.accessToken }}/sendMessage",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ chat_id: $json.chat_id, text: $json.text, parse_mode: $json.parse_mode, reply_markup: $json.reply_markup }) }}",
|
|
"options": {}
|
|
},
|
|
"id": "http-send-batch-confirm",
|
|
"name": "Send Batch Confirmation",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
2000,
|
|
500
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "message",
|
|
"operation": "sendMessage",
|
|
"chatId": "={{ $json.chatId }}",
|
|
"text": "={{ $json.text }}",
|
|
"additionalFields": {
|
|
"parse_mode": "HTML"
|
|
}
|
|
},
|
|
"id": "telegram-send-docker-error",
|
|
"name": "Send Docker Error",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
1780,
|
|
200
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Parse callback data from button click (single suggestion or batch)\nconst callback = $json.callback_query;\nlet data;\ntry {\n data = JSON.parse(callback.data);\n} catch (e) {\n data = { a: 'x' }; // Treat parse error as cancel\n}\n\nconst queryId = callback.id;\nconst chatId = callback.message.chat.id;\nconst messageId = callback.message.message_id;\n\n// Check 2-minute timeout (120000ms)\nconst TWO_MINUTES = 120000;\nconst isExpired = data.t && (Date.now() - data.t > TWO_MINUTES);\n\n// Decode action: s=start, t=stop, r=restart, x=cancel\nconst actionMap = { s: 'start', t: 'stop', r: 'restart', x: 'cancel' };\nconst action = actionMap[data.a] || 'cancel';\n\n// Detect batch (c is array) vs single suggestion (c is string)\nconst isBatch = Array.isArray(data.c);\nconst containerIds = isBatch ? data.c : (data.c ? [data.c] : []);\n\nreturn {\n json: {\n queryId,\n chatId,\n messageId,\n action,\n containerIds, // Array for batch support\n containerId: containerIds[0] || null, // For single-container compat\n expired: isExpired,\n isBatch,\n isCancel: action === 'cancel'\n }\n};"
|
|
},
|
|
"id": "code-parse-callback",
|
|
"name": "Parse Callback Data",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
900,
|
|
500
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"rules": {
|
|
"values": [
|
|
{
|
|
"id": "is-cancel",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "cancel-true",
|
|
"leftValue": "={{ $json.isCancel }}",
|
|
"rightValue": true,
|
|
"operator": {
|
|
"type": "boolean",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "cancel"
|
|
},
|
|
{
|
|
"id": "is-expired",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "expired-true",
|
|
"leftValue": "={{ $json.expired }}",
|
|
"rightValue": true,
|
|
"operator": {
|
|
"type": "boolean",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "expired"
|
|
},
|
|
{
|
|
"id": "is-batch",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "batch-true",
|
|
"leftValue": "={{ $json.isBatch }}",
|
|
"rightValue": true,
|
|
"operator": {
|
|
"type": "boolean",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "batch"
|
|
}
|
|
]
|
|
},
|
|
"options": {
|
|
"fallbackOutput": "extra"
|
|
}
|
|
},
|
|
"id": "switch-route-callback",
|
|
"name": "Route Callback",
|
|
"type": "n8n-nodes-base.switch",
|
|
"typeVersion": 3.2,
|
|
"position": [
|
|
1120,
|
|
500
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Prepare cancel response - answer callback query and delete message\nconst { queryId, chatId, messageId } = $json;\nreturn {\n json: {\n queryId,\n chatId,\n messageId,\n answerText: 'Cancelled'\n }\n};"
|
|
},
|
|
"id": "code-handle-cancel",
|
|
"name": "Handle Cancel",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1340,
|
|
500
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $credentials.telegramApi.accessToken }}/answerCallbackQuery",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId, text: $json.answerText }) }}",
|
|
"options": {}
|
|
},
|
|
"id": "http-answer-cancel",
|
|
"name": "Answer Cancel Query",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
1560,
|
|
500
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $credentials.telegramApi.accessToken }}/deleteMessage",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ chat_id: $('Handle Cancel').item.json.chatId, message_id: $('Handle Cancel').item.json.messageId }) }}",
|
|
"options": {}
|
|
},
|
|
"id": "http-delete-cancel-msg",
|
|
"name": "Delete Cancel Message",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
1780,
|
|
500
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Prepare expired response\nconst { queryId, chatId, messageId } = $json;\nreturn {\n json: {\n queryId,\n chatId,\n messageId,\n answerText: 'Confirmation expired. Please try again.'\n }\n};"
|
|
},
|
|
"id": "code-handle-expired",
|
|
"name": "Handle Expired",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1340,
|
|
600
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $credentials.telegramApi.accessToken }}/answerCallbackQuery",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId, text: $json.answerText, show_alert: true }) }}",
|
|
"options": {}
|
|
},
|
|
"id": "http-answer-expired",
|
|
"name": "Answer Expired Query",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
1560,
|
|
600
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $credentials.telegramApi.accessToken }}/deleteMessage",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ chat_id: $('Handle Expired').item.json.chatId, message_id: $('Handle Expired').item.json.messageId }) }}",
|
|
"options": {}
|
|
},
|
|
"id": "http-delete-expired-msg",
|
|
"name": "Delete Expired Message",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
1780,
|
|
600
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build curl command for callback action execution\nconst data = $input.item.json;\nconst containerId = data.containerId;\nconst action = data.action;\nconst chatId = data.chatId;\nconst messageId = data.messageId;\nconst queryId = data.queryId;\n\n// stop and restart use ?t=10 for graceful timeout\nconst timeout = (action === 'stop' || action === 'restart') ? '?t=10' : '';\n\n// Build curl command that returns HTTP status code\nconst cmd = `curl -s -o /dev/null -w \"%{http_code}\" --unix-socket /var/run/docker.sock -X POST 'http://localhost/v1.47/containers/${containerId}/${action}${timeout}'`;\n\nreturn {\n json: {\n cmd,\n containerId,\n action,\n chatId,\n messageId,\n queryId\n }\n};"
|
|
},
|
|
"id": "code-build-callback-cmd",
|
|
"name": "Build Callback Action",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1340,
|
|
700
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "={{ $json.cmd }}",
|
|
"options": {}
|
|
},
|
|
"id": "exec-callback-action",
|
|
"name": "Execute Callback Action",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
1560,
|
|
700
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Parse callback action result and get container name\nconst stdout = $input.item.json.stdout;\nconst stderr = $input.item.json.stderr;\nconst cmdData = $('Build Callback Action').item.json;\nconst containerId = cmdData.containerId;\nconst action = cmdData.action;\nconst chatId = cmdData.chatId;\nconst messageId = cmdData.messageId;\nconst queryId = cmdData.queryId;\n\n// Check for curl-level errors first\nif (stderr && stderr.trim()) {\n return {\n json: {\n success: false,\n chatId,\n messageId,\n queryId,\n text: `Failed to ${action}: ${stderr.trim()}`,\n answerText: 'Action failed'\n }\n };\n}\n\nconst statusCode = parseInt(stdout.trim());\n\n// 204: Success, 304: Already in state (also success)\nif (statusCode === 204 || statusCode === 304) {\n const verb = action === 'start' ? 'started' :\n action === 'stop' ? 'stopped' : 'restarted';\n return {\n json: {\n success: true,\n chatId,\n messageId,\n queryId,\n containerId,\n text: `Container ${verb} successfully`,\n answerText: `Container ${verb}`\n }\n };\n}\n\n// Handle error codes\nlet errorMsg;\nswitch (statusCode) {\n case 404:\n errorMsg = 'Container not found';\n break;\n case 409:\n errorMsg = 'Container is in a conflicting state';\n break;\n case 500:\n errorMsg = 'Docker server error';\n break;\n default:\n errorMsg = `HTTP ${statusCode}`;\n}\n\nreturn {\n json: {\n success: false,\n chatId,\n messageId,\n queryId,\n text: `Failed to ${action}: ${errorMsg}`,\n answerText: 'Action failed'\n }\n};"
|
|
},
|
|
"id": "code-parse-callback-result",
|
|
"name": "Parse Callback Result",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1780,
|
|
700
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $credentials.telegramApi.accessToken }}/answerCallbackQuery",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId, text: $json.answerText }) }}",
|
|
"options": {}
|
|
},
|
|
"id": "http-answer-action",
|
|
"name": "Answer Action Query",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
2000,
|
|
700
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $credentials.telegramApi.accessToken }}/deleteMessage",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ chat_id: $('Parse Callback Result').item.json.chatId, message_id: $('Parse Callback Result').item.json.messageId }) }}",
|
|
"options": {}
|
|
},
|
|
"id": "http-delete-suggestion-msg",
|
|
"name": "Delete Suggestion Message",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
2220,
|
|
700
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "message",
|
|
"operation": "sendMessage",
|
|
"chatId": "={{ $('Parse Callback Result').item.json.chatId }}",
|
|
"text": "={{ $('Parse Callback Result').item.json.text }}",
|
|
"additionalFields": {
|
|
"parse_mode": "HTML"
|
|
}
|
|
},
|
|
"id": "telegram-send-callback-result",
|
|
"name": "Send Callback Result",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
2440,
|
|
700
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Execute batch action on all containers sequentially\nconst containerIds = $json.containerIds;\nconst action = $json.action;\nconst chatId = $json.chatId;\nconst messageId = $json.messageId;\nconst queryId = $json.queryId;\n\nconst timeout = (action === 'stop' || action === 'restart') ? '?t=10' : '';\nconst results = [];\n\n// Execute each container action sequentially using fetch\nfor (const containerId of containerIds) {\n try {\n // Use n8n's built-in $http or construct command for later execution\n // Since we can't use execSync easily, we'll build commands for chained execution\n results.push({\n containerId,\n cmd: `curl -s -o /dev/null -w \"%{http_code}\" --unix-socket /var/run/docker.sock -X POST 'http://localhost/v1.47/containers/${containerId}/${action}${timeout}'`\n });\n } catch (err) {\n results.push({ containerId, error: err.message });\n }\n}\n\nreturn {\n json: {\n commands: results,\n action,\n queryId,\n chatId,\n messageId,\n totalCount: containerIds.length\n }\n};"
|
|
},
|
|
"id": "code-build-batch-commands",
|
|
"name": "Build Batch Commands",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1340,
|
|
800
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Execute all batch commands and collect results\nconst { commands, action, queryId, chatId, messageId, totalCount } = $json;\nconst results = [];\n\n// Build a single shell command that runs all curl commands sequentially\n// and outputs results in a parseable format\nconst allCommands = commands.map((c, i) => \n `echo \"RESULT_${i}:$(${c.cmd})\"`\n).join(' && ');\n\nreturn {\n json: {\n batchCmd: allCommands,\n containerIds: commands.map(c => c.containerId),\n action,\n queryId,\n chatId,\n messageId,\n totalCount\n }\n};"
|
|
},
|
|
"id": "code-prepare-batch-exec",
|
|
"name": "Prepare Batch Execution",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1560,
|
|
800
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "={{ $json.batchCmd }}",
|
|
"options": {}
|
|
},
|
|
"id": "exec-batch-action",
|
|
"name": "Execute Batch Action",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
1780,
|
|
800
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Parse batch execution results\nconst stdout = $input.item.json.stdout || '';\nconst stderr = $input.item.json.stderr || '';\nconst prevData = $('Prepare Batch Execution').item.json;\nconst { containerIds, action, queryId, chatId, messageId, totalCount } = prevData;\n\n// Parse results from output like: RESULT_0:204 RESULT_1:204 RESULT_2:304\nconst results = [];\nfor (let i = 0; i < containerIds.length; i++) {\n const match = stdout.match(new RegExp(`RESULT_${i}:(\\\\d+)`));\n if (match) {\n const statusCode = parseInt(match[1]);\n results.push({\n containerId: containerIds[i],\n success: statusCode === 204 || statusCode === 304,\n statusCode\n });\n } else {\n results.push({\n containerId: containerIds[i],\n success: false,\n error: 'No response'\n });\n }\n}\n\nconst successCount = results.filter(r => r.success).length;\nconst failCount = results.length - successCount;\n\nreturn {\n json: {\n results,\n successCount,\n failCount,\n totalCount: results.length,\n action,\n queryId,\n chatId,\n messageId\n }\n};"
|
|
},
|
|
"id": "code-parse-batch-result",
|
|
"name": "Parse Batch Result",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2000,
|
|
800
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Format batch result message\nconst { successCount, failCount, totalCount, action } = $json;\nconst verb = action === 'start' ? 'started' :\n action === 'stop' ? 'stopped' : 'restarted';\n\nlet message;\nif (failCount === 0) {\n message = `Successfully ${verb} ${successCount} container${successCount > 1 ? 's' : ''}`;\n} else if (successCount === 0) {\n message = `Failed to ${action} all ${totalCount} containers`;\n} else {\n message = `${verb.charAt(0).toUpperCase() + verb.slice(1)} ${successCount}/${totalCount} containers (${failCount} failed)`;\n}\n\nreturn {\n json: {\n message,\n chatId: $json.chatId,\n queryId: $json.queryId,\n messageId: $json.messageId\n }\n};"
|
|
},
|
|
"id": "code-format-batch-result",
|
|
"name": "Format Batch Result",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2220,
|
|
800
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $credentials.telegramApi.accessToken }}/answerCallbackQuery",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ callback_query_id: $json.queryId }) }}",
|
|
"options": {}
|
|
},
|
|
"id": "http-answer-batch-query",
|
|
"name": "Answer Batch Query",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
2440,
|
|
800
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "=https://api.telegram.org/bot{{ $credentials.telegramApi.accessToken }}/deleteMessage",
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ chat_id: $('Format Batch Result').item.json.chatId, message_id: $('Format Batch Result').item.json.messageId }) }}",
|
|
"options": {}
|
|
},
|
|
"id": "http-delete-batch-confirm-msg",
|
|
"name": "Delete Batch Confirm Message",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
2660,
|
|
800
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "message",
|
|
"operation": "sendMessage",
|
|
"chatId": "={{ $('Format Batch Result').item.json.chatId }}",
|
|
"text": "={{ $('Format Batch Result').item.json.message }}",
|
|
"additionalFields": {
|
|
"parse_mode": "HTML"
|
|
}
|
|
},
|
|
"id": "telegram-send-batch-result",
|
|
"name": "Send Batch Result",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
2880,
|
|
800
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Parse update command from message\nconst text = $json.message.text.toLowerCase().trim();\nconst chatId = $json.message.chat.id;\nconst messageId = $json.message.message_id;\n\n// Match update pattern: update followed by container name\nconst match = text.match(/^update\\s+(.+)$/i);\n\nif (!match) {\n return {\n json: {\n error: true,\n errorMessage: 'Invalid update format. Use: update <container-name>',\n chatId: chatId\n }\n };\n}\n\nreturn {\n json: {\n containerQuery: match[1].trim(),\n chatId: chatId,\n messageId: messageId\n }\n};"
|
|
},
|
|
"id": "code-parse-update",
|
|
"name": "Parse Update Command",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
900,
|
|
1000
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "curl -s --unix-socket /var/run/docker.sock 'http://localhost/v1.47/containers/json?all=true'",
|
|
"options": {}
|
|
},
|
|
"id": "exec-docker-list-update",
|
|
"name": "Docker List for Update",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
1120,
|
|
1000
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Get Docker API response and update info\nconst dockerOutput = $input.item.json.stdout;\nconst updateData = $('Parse Update Command').item.json;\nconst containerQuery = updateData.containerQuery;\nconst chatId = updateData.chatId;\n\n// Parse JSON response\nlet containers;\ntry {\n if (!dockerOutput || dockerOutput.trim() === '') {\n throw new Error('Empty response');\n }\n containers = JSON.parse(dockerOutput);\n} catch (e) {\n return [{\n json: {\n matchCount: -1,\n error: true,\n chatId: chatId,\n text: \"Can't reach Docker - check if n8n has socket access\"\n }\n }];\n}\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '')\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '')\n .toLowerCase();\n}\n\n// Find matching containers using fuzzy matching\nconst matches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n const normalized = containerQuery.toLowerCase();\n return containerName.includes(normalized) || normalized.includes(containerName);\n});\n\n// Return match results\nreturn [{\n json: {\n matches: matches.map(c => ({\n Id: c.Id,\n Name: c.Names[0].replace(/^\\//, ''),\n State: c.State\n })),\n matchCount: matches.length,\n containerQuery: containerQuery,\n chatId: chatId,\n allContainers: containers\n }\n}];"
|
|
},
|
|
"id": "code-match-update-container",
|
|
"name": "Match Update Container",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1340,
|
|
1000
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"rules": {
|
|
"values": [
|
|
{
|
|
"id": "update-docker-error",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "count-negative",
|
|
"leftValue": "={{ $json.matchCount }}",
|
|
"rightValue": "0",
|
|
"operator": {
|
|
"type": "number",
|
|
"operation": "lt"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "error"
|
|
},
|
|
{
|
|
"id": "update-no-match",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "count-zero",
|
|
"leftValue": "={{ $json.matchCount }}",
|
|
"rightValue": "0",
|
|
"operator": {
|
|
"type": "number",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "no-match"
|
|
},
|
|
{
|
|
"id": "update-single-match",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "count-one",
|
|
"leftValue": "={{ $json.matchCount }}",
|
|
"rightValue": "1",
|
|
"operator": {
|
|
"type": "number",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "single"
|
|
},
|
|
{
|
|
"id": "update-multiple-matches",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "count-gt-one",
|
|
"leftValue": "={{ $json.matchCount }}",
|
|
"rightValue": "1",
|
|
"operator": {
|
|
"type": "number",
|
|
"operation": "gt"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "multiple"
|
|
}
|
|
]
|
|
},
|
|
"options": {
|
|
"fallbackOutput": "extra"
|
|
}
|
|
},
|
|
"id": "switch-update-match-count",
|
|
"name": "Check Update Match Count",
|
|
"type": "n8n-nodes-base.switch",
|
|
"typeVersion": 3.2,
|
|
"position": [
|
|
1560,
|
|
1000
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "message",
|
|
"operation": "sendMessage",
|
|
"chatId": "={{ $json.chatId }}",
|
|
"text": "={{ $json.text }}",
|
|
"additionalFields": {
|
|
"parse_mode": "HTML"
|
|
}
|
|
},
|
|
"id": "telegram-send-update-error",
|
|
"name": "Send Update Error",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
1780,
|
|
900
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Update requires exact container name - multiple matches not allowed\nconst matches = $json.matches;\nconst query = $json.containerQuery;\nconst chatId = $json.chatId;\n\nconst names = matches.map(m => m.Name).join(', ');\n\nreturn {\n json: {\n chatId: chatId,\n text: `Update requires an exact container name.\\n\\nFound ${matches.length} matches: ${names}`\n }\n};"
|
|
},
|
|
"id": "code-update-multiple-handler",
|
|
"name": "Handle Update Multiple",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1780,
|
|
1100
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "message",
|
|
"operation": "sendMessage",
|
|
"chatId": "={{ $json.chatId }}",
|
|
"text": "={{ $json.text }}",
|
|
"additionalFields": {
|
|
"parse_mode": "HTML"
|
|
}
|
|
},
|
|
"id": "telegram-send-update-multiple",
|
|
"name": "Send Update Multiple",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
2000,
|
|
1100
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "message",
|
|
"operation": "sendMessage",
|
|
"chatId": "={{ $json.chatId }}",
|
|
"text": "=No container found matching '<b>{{ $json.containerQuery }}</b>'",
|
|
"additionalFields": {
|
|
"parse_mode": "HTML"
|
|
}
|
|
},
|
|
"id": "telegram-send-update-no-match",
|
|
"name": "Send Update No Match",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
1780,
|
|
1000
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build inspect command for the matched container\nconst containerId = $json.matches[0].Id;\nconst containerName = $json.matches[0].Name;\nconst chatId = $json.chatId;\n\nreturn {\n json: {\n cmd: `curl -s --unix-socket /var/run/docker.sock 'http://localhost/v1.47/containers/${containerId}/json'`,\n containerId,\n containerName,\n chatId\n }\n};"
|
|
},
|
|
"id": "code-build-inspect-cmd",
|
|
"name": "Build Inspect Command",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1780,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "={{ $json.cmd }}",
|
|
"options": {}
|
|
},
|
|
"id": "exec-inspect-container",
|
|
"name": "Inspect Container",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
2000,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Parse container inspect output and extract config\nconst stdout = $input.item.json.stdout;\nconst cmdData = $('Build Inspect Command').item.json;\nconst containerId = cmdData.containerId;\nconst containerName = cmdData.containerName;\nconst chatId = cmdData.chatId;\n\nlet inspect;\ntry {\n inspect = JSON.parse(stdout);\n} catch (e) {\n return {\n json: {\n error: true,\n chatId,\n text: `Failed to inspect ${containerName}: ${e.message}`\n }\n };\n}\n\nconst imageName = inspect.Config.Image;\nconst currentImageId = inspect.Image;\n\n// Extract version from labels if available\nconst labels = inspect.Config.Labels || {};\nconst currentVersion = labels['org.opencontainers.image.version']\n || labels['version']\n || currentImageId.substring(7, 19);\n\nreturn {\n json: {\n imageName,\n currentImageId,\n currentVersion,\n containerConfig: inspect.Config,\n hostConfig: inspect.HostConfig,\n networkSettings: inspect.NetworkSettings,\n containerName,\n containerId,\n chatId\n }\n};"
|
|
},
|
|
"id": "code-parse-container-config",
|
|
"name": "Parse Container Config",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2220,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build pull image command\nconst imageName = $json.imageName;\n\nreturn {\n json: {\n cmd: `curl -s --unix-socket /var/run/docker.sock -X POST 'http://localhost/v1.47/images/create?fromImage=${encodeURIComponent(imageName)}'`,\n imageName,\n currentImageId: $json.currentImageId,\n currentVersion: $json.currentVersion,\n containerConfig: $json.containerConfig,\n hostConfig: $json.hostConfig,\n networkSettings: $json.networkSettings,\n containerName: $json.containerName,\n containerId: $json.containerId,\n chatId: $json.chatId\n }\n};"
|
|
},
|
|
"id": "code-build-pull-cmd",
|
|
"name": "Build Pull Command",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2440,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "={{ $json.cmd }}",
|
|
"options": {}
|
|
},
|
|
"id": "exec-pull-image",
|
|
"name": "Pull Image",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
2660,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build inspect image command to get the new image ID\nconst pullData = $('Build Pull Command').item.json;\nconst imageName = pullData.imageName;\n\nreturn {\n json: {\n cmd: `curl -s --unix-socket /var/run/docker.sock 'http://localhost/v1.47/images/${encodeURIComponent(imageName)}/json'`,\n imageName,\n currentImageId: pullData.currentImageId,\n currentVersion: pullData.currentVersion,\n containerConfig: pullData.containerConfig,\n hostConfig: pullData.hostConfig,\n networkSettings: pullData.networkSettings,\n containerName: pullData.containerName,\n containerId: pullData.containerId,\n chatId: pullData.chatId\n }\n};"
|
|
},
|
|
"id": "code-build-inspect-image-cmd",
|
|
"name": "Build Image Inspect",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2880,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "={{ $json.cmd }}",
|
|
"options": {}
|
|
},
|
|
"id": "exec-inspect-image",
|
|
"name": "Inspect New Image",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
3100,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Compare old and new image IDs to detect if update is needed\nconst stdout = $input.item.json.stdout;\nconst prevData = $('Build Image Inspect').item.json;\nconst currentImageId = prevData.currentImageId;\nconst chatId = prevData.chatId;\n\nlet newImage;\ntry {\n newImage = JSON.parse(stdout);\n} catch (e) {\n return {\n json: {\n error: true,\n chatId,\n text: `Failed to inspect new image: ${e.message}`\n }\n };\n}\n\nconst newImageId = newImage.Id;\n\nif (currentImageId === newImageId) {\n // No update needed - stay silent per CONTEXT.md\n return { json: { needsUpdate: false, chatId } };\n}\n\n// Extract new version from labels\nconst labels = newImage.Config?.Labels || {};\nconst newVersion = labels['org.opencontainers.image.version']\n || labels['version']\n || newImageId.substring(7, 19);\n\nreturn {\n json: {\n needsUpdate: true,\n currentImageId,\n newImageId,\n currentVersion: prevData.currentVersion,\n newVersion,\n containerConfig: prevData.containerConfig,\n hostConfig: prevData.hostConfig,\n networkSettings: prevData.networkSettings,\n containerName: prevData.containerName,\n containerId: prevData.containerId,\n chatId\n }\n};"
|
|
},
|
|
"id": "code-compare-digests",
|
|
"name": "Compare Digests",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
3320,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "needs-update",
|
|
"leftValue": "={{ $json.needsUpdate }}",
|
|
"rightValue": true,
|
|
"operator": {
|
|
"type": "boolean",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "if-needs-update",
|
|
"name": "Check If Update Needed",
|
|
"type": "n8n-nodes-base.if",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
3540,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build stop container command\nconst containerId = $json.containerId;\n\nreturn {\n json: {\n cmd: `curl -s -o /dev/null -w \"%{http_code}\" --unix-socket /var/run/docker.sock -X POST 'http://localhost/v1.47/containers/${containerId}/stop?t=10'`,\n containerId,\n containerName: $json.containerName,\n currentVersion: $json.currentVersion,\n newVersion: $json.newVersion,\n containerConfig: $json.containerConfig,\n hostConfig: $json.hostConfig,\n networkSettings: $json.networkSettings,\n chatId: $json.chatId\n }\n};"
|
|
},
|
|
"id": "code-build-stop-cmd",
|
|
"name": "Build Stop Command",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
3760,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "={{ $json.cmd }}",
|
|
"options": {}
|
|
},
|
|
"id": "exec-stop-container",
|
|
"name": "Stop Container",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
3980,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Verify container stopped and build remove command\nconst stdout = $input.item.json.stdout;\nconst prevData = $('Build Stop Command').item.json;\nconst containerId = prevData.containerId;\nconst chatId = prevData.chatId;\n\nconst statusCode = parseInt(stdout.trim());\n\n// 204: stopped, 304: already stopped - both OK\nif (statusCode !== 204 && statusCode !== 304) {\n return {\n json: {\n error: true,\n chatId,\n text: `Failed to stop container: HTTP ${statusCode}`\n }\n };\n}\n\nreturn {\n json: {\n cmd: `curl -s -o /dev/null -w \"%{http_code}\" --unix-socket /var/run/docker.sock -X DELETE 'http://localhost/v1.47/containers/${containerId}'`,\n containerId,\n containerName: prevData.containerName,\n currentVersion: prevData.currentVersion,\n newVersion: prevData.newVersion,\n containerConfig: prevData.containerConfig,\n hostConfig: prevData.hostConfig,\n networkSettings: prevData.networkSettings,\n chatId\n }\n};"
|
|
},
|
|
"id": "code-verify-stop-build-remove",
|
|
"name": "Verify Stop Build Remove",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
4200,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "={{ $json.cmd }}",
|
|
"options": {}
|
|
},
|
|
"id": "exec-remove-container",
|
|
"name": "Remove Container",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
4420,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build container create request body from saved config\nconst prevData = $('Verify Stop Build Remove').item.json;\nconst config = prevData.containerConfig;\nconst hostConfig = prevData.hostConfig;\nconst networkSettings = prevData.networkSettings;\nconst containerName = prevData.containerName;\nconst chatId = prevData.chatId;\n\n// Build NetworkingConfig from NetworkSettings\nconst networks = {};\nfor (const [name, netConfig] of Object.entries(networkSettings.Networks || {})) {\n networks[name] = {\n IPAMConfig: netConfig.IPAMConfig,\n Links: netConfig.Links,\n Aliases: netConfig.Aliases\n };\n}\n\nconst createBody = {\n ...config,\n HostConfig: hostConfig,\n NetworkingConfig: {\n EndpointsConfig: networks\n }\n};\n\n// Remove fields that shouldn't be in create request\ndelete createBody.Hostname;\ndelete createBody.Domainname;\n\nreturn {\n json: {\n createBody: JSON.stringify(createBody),\n containerName,\n currentVersion: prevData.currentVersion,\n newVersion: prevData.newVersion,\n chatId\n }\n};"
|
|
},
|
|
"id": "code-build-create-body",
|
|
"name": "Build Create Body",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
4640,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build the create container command using a temp file approach\nconst createBody = $json.createBody;\nconst containerName = $json.containerName;\nconst chatId = $json.chatId;\n\n// Write body to command using here-doc to avoid shell escaping issues\nconst cmd = `curl -s -X POST --unix-socket /var/run/docker.sock -H \"Content-Type: application/json\" -d '${createBody.replace(/'/g, \"'\\\\''\")}' 'http://localhost/v1.47/containers/create?name=${encodeURIComponent(containerName)}'`;\n\nreturn {\n json: {\n cmd,\n containerName,\n currentVersion: $json.currentVersion,\n newVersion: $json.newVersion,\n chatId\n }\n};"
|
|
},
|
|
"id": "code-build-create-cmd",
|
|
"name": "Build Create Command",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
4860,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "={{ $json.cmd }}",
|
|
"options": {}
|
|
},
|
|
"id": "exec-create-container",
|
|
"name": "Create Container",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
5080,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Parse create response and extract new container ID\nconst stdout = $input.item.json.stdout;\nconst prevData = $('Build Create Command').item.json;\nconst chatId = prevData.chatId;\n\nlet response;\ntry {\n response = JSON.parse(stdout);\n} catch (e) {\n return {\n json: {\n error: true,\n chatId,\n text: `Create failed: ${stdout}`\n }\n };\n}\n\nif (response.message) {\n // Error response from Docker\n return {\n json: {\n error: true,\n chatId,\n text: `Create failed: ${response.message}`\n }\n };\n}\n\nreturn {\n json: {\n newContainerId: response.Id,\n containerName: prevData.containerName,\n currentVersion: prevData.currentVersion,\n newVersion: prevData.newVersion,\n chatId\n }\n};"
|
|
},
|
|
"id": "code-parse-create-response",
|
|
"name": "Parse Create Response",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
5300,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build start command for new container\nconst newContainerId = $json.newContainerId;\n\nreturn {\n json: {\n cmd: `curl -s -o /dev/null -w \"%{http_code}\" --unix-socket /var/run/docker.sock -X POST 'http://localhost/v1.47/containers/${newContainerId}/start'`,\n newContainerId,\n containerName: $json.containerName,\n currentVersion: $json.currentVersion,\n newVersion: $json.newVersion,\n chatId: $json.chatId\n }\n};"
|
|
},
|
|
"id": "code-build-start-cmd",
|
|
"name": "Build Start Command",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
5520,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "={{ $json.cmd }}",
|
|
"options": {}
|
|
},
|
|
"id": "exec-start-new-container",
|
|
"name": "Start New Container",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
5740,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Parse start result and format success message\nconst stdout = $input.item.json.stdout;\nconst prevData = $('Build Start Command').item.json;\nconst containerName = prevData.containerName;\nconst currentVersion = prevData.currentVersion;\nconst newVersion = prevData.newVersion;\nconst chatId = prevData.chatId;\n\nconst statusCode = parseInt(stdout.trim());\n\n// 204: started, 304: already running\nif (statusCode !== 204 && statusCode !== 304) {\n return {\n json: {\n chatId,\n text: `Container created but failed to start: HTTP ${statusCode}`\n }\n };\n}\n\nconst message = `<b>${containerName}</b> updated: ${currentVersion} \\u2192 ${newVersion}`;\n\nreturn {\n json: {\n chatId,\n text: message\n }\n};"
|
|
},
|
|
"id": "code-format-update-result",
|
|
"name": "Format Update Result",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
5960,
|
|
1200
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "message",
|
|
"operation": "sendMessage",
|
|
"chatId": "={{ $json.chatId }}",
|
|
"text": "={{ $json.text }}",
|
|
"additionalFields": {
|
|
"parse_mode": "HTML"
|
|
}
|
|
},
|
|
"id": "telegram-send-update-result",
|
|
"name": "Send Update Result",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
6180,
|
|
1200
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Parse logs command from intent\n// Intent structure: { action: \"view_logs\", container: \"name\", parameters: { lines: 50 } }\nconst chatId = $json.original_message.chat.id;\nconst messageId = $json.original_message.message_id;\nconst containerQuery = $json.container;\nconst lines = $json.parameters.lines || 50;\n\nif (!containerQuery) {\n return {\n json: {\n error: true,\n errorMessage: 'Please specify which container logs you want to see.',\n chatId: chatId\n }\n };\n}\n\n// Validate line count (reasonable limits)\nconst validatedLines = Math.min(Math.max(lines, 1), 1000);\n\nreturn {\n json: {\n containerQuery: containerQuery,\n lines: validatedLines,\n chatId: chatId,\n messageId: messageId\n }\n};"
|
|
},
|
|
"id": "code-parse-logs",
|
|
"name": "Parse Logs Command",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
900,
|
|
600
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "curl -s --unix-socket /var/run/docker.sock 'http://localhost/v1.47/containers/json?all=true'",
|
|
"options": {}
|
|
},
|
|
"id": "exec-docker-list-logs",
|
|
"name": "Docker List for Logs",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
1120,
|
|
600
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Get Docker API response and logs info from Parse Logs\nconst dockerOutput = $input.item.json.stdout;\nconst logsData = $('Parse Logs Command').item.json;\nconst containerQuery = logsData.containerQuery;\nconst lines = logsData.lines;\nconst chatId = logsData.chatId;\n\n// Parse JSON response\nlet containers;\ntry {\n if (!dockerOutput || dockerOutput.trim() === '') {\n throw new Error('Empty response');\n }\n containers = JSON.parse(dockerOutput);\n} catch (e) {\n return [{\n json: {\n matchCount: -1,\n error: true,\n chatId: chatId,\n text: \"Can't reach Docker - check if n8n has socket access\"\n }\n }];\n}\n\n// Function to normalize container names\nfunction normalizeName(name) {\n return name\n .replace(/^\\//, '') // Remove leading slash\n .replace(/^(linuxserver[-_]|binhex[-_])/i, '') // Remove common prefixes\n .toLowerCase();\n}\n\n// Find matching containers using fuzzy matching\nconst matches = containers.filter(c => {\n const containerName = normalizeName(c.Names[0]);\n const normalized = containerQuery.toLowerCase();\n return containerName.includes(normalized) || normalized.includes(containerName);\n});\n\n// Return match results with all necessary context\nreturn [{\n json: {\n matches: matches.map(c => ({\n Id: c.Id,\n Name: c.Names[0].replace(/^\\//, ''),\n State: c.State\n })),\n matchCount: matches.length,\n containerQuery: containerQuery,\n lines: lines,\n chatId: chatId\n }\n}];"
|
|
},
|
|
"id": "code-match-logs-container",
|
|
"name": "Match Logs Container",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1340,
|
|
600
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"rules": {
|
|
"values": [
|
|
{
|
|
"id": "logs-docker-error",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "count-negative",
|
|
"leftValue": "={{ $json.matchCount }}",
|
|
"rightValue": 0,
|
|
"operator": {
|
|
"type": "number",
|
|
"operation": "lt"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": false
|
|
},
|
|
{
|
|
"id": "logs-no-match",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "count-zero",
|
|
"leftValue": "={{ $json.matchCount }}",
|
|
"rightValue": 0,
|
|
"operator": {
|
|
"type": "number",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": false
|
|
},
|
|
{
|
|
"id": "logs-single-match",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "count-one",
|
|
"leftValue": "={{ $json.matchCount }}",
|
|
"rightValue": 1,
|
|
"operator": {
|
|
"type": "number",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": false
|
|
},
|
|
{
|
|
"id": "logs-multiple-matches",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"typeValidation": "loose"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "count-many",
|
|
"leftValue": "={{ $json.matchCount }}",
|
|
"rightValue": 1,
|
|
"operator": {
|
|
"type": "number",
|
|
"operation": "gt"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": false
|
|
}
|
|
]
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "switch-logs-match-count",
|
|
"name": "Check Logs Match Count",
|
|
"type": "n8n-nodes-base.switch",
|
|
"typeVersion": 3.2,
|
|
"position": [
|
|
1560,
|
|
600
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build Docker logs API curl command\nconst matchData = $input.item.json;\nconst container = matchData.matches[0]; // Single match verified by switch\nconst lines = matchData.lines;\n\nconst cmd = `curl -s --unix-socket /var/run/docker.sock \"http://localhost/v1.47/containers/${container.Id}/logs?stdout=1&stderr=1&tail=${lines}×tamps=1\"`;\n\nreturn {\n json: {\n command: cmd,\n containerName: container.Name,\n containerId: container.Id,\n lines: lines,\n chatId: matchData.chatId\n }\n};"
|
|
},
|
|
"id": "code-build-logs-cmd",
|
|
"name": "Build Logs Command",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1780,
|
|
500
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"command": "={{ $json.command }}",
|
|
"options": {}
|
|
},
|
|
"id": "exec-logs",
|
|
"name": "Execute Logs",
|
|
"type": "n8n-nodes-base.executeCommand",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
2000,
|
|
500
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Format Docker logs for Telegram\nconst rawOutput = $input.item.json.stdout || '';\nconst containerName = $('Build Logs Command').item.json.containerName;\nconst requestedLines = $('Build Logs Command').item.json.lines;\nconst chatId = $('Build Logs Command').item.json.chatId;\n\n// Handle empty logs\nif (!rawOutput || rawOutput.trim() === '') {\n return {\n json: {\n chatId: chatId,\n text: `No logs available for <b>${containerName}</b>`\n }\n };\n}\n\n// Docker API with timestamps returns text lines when using tail parameter\n// But may have 8-byte binary headers we need to strip\nconst lines = rawOutput.split('\\n')\n .filter(line => line.length > 0)\n .map(line => {\n // Check if line starts with binary header (non-printable chars in first 8 bytes)\n if (line.length > 8 && line.charCodeAt(0) <= 2) {\n return line.substring(8);\n }\n return line;\n })\n .join('\\n');\n\n// Truncate for Telegram (4096 char limit, leave room for header)\nconst maxLen = 3800;\nconst truncated = lines.length > maxLen\n ? lines.substring(0, maxLen) + '\\n... (truncated)'\n : lines;\n\nconst lineCount = lines.split('\\n').length;\nconst header = `Logs for <b>${containerName}</b> (last ${lineCount} lines):\\n\\n`;\n\nreturn {\n json: {\n chatId: chatId,\n text: header + '<pre>' + truncated + '</pre>'\n }\n};"
|
|
},
|
|
"id": "code-format-logs",
|
|
"name": "Format Logs",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
2220,
|
|
500
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "message",
|
|
"operation": "sendMessage",
|
|
"chatId": "={{ $json.chatId }}",
|
|
"text": "={{ $json.text }}",
|
|
"additionalFields": {
|
|
"parse_mode": "HTML"
|
|
}
|
|
},
|
|
"id": "telegram-send-logs",
|
|
"name": "Send Logs Response",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
2440,
|
|
500
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"resource": "message",
|
|
"operation": "sendMessage",
|
|
"chatId": "={{ $json.chatId }}",
|
|
"text": "={{ $json.text || 'Error retrieving logs' }}",
|
|
"additionalFields": {
|
|
"parse_mode": "HTML"
|
|
}
|
|
},
|
|
"id": "telegram-send-logs-error",
|
|
"name": "Send Logs Error",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
1780,
|
|
700
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const data = $input.item.json;\nreturn {\n json: {\n chatId: data.chatId,\n text: `No container found matching \"<b>${data.containerQuery}</b>\".\\n\\nTry \"status\" to see all containers.`\n }\n};"
|
|
},
|
|
"id": "code-format-logs-no-match",
|
|
"name": "Format Logs No Match",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1780,
|
|
650
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const data = $input.item.json;\nconst names = data.matches.map(m => m.Name).join('\\n- ');\nreturn {\n json: {\n chatId: data.chatId,\n text: `Found ${data.matches.length} containers matching \"<b>${data.containerQuery}</b>\":\\n\\n- ${names}\\n\\nPlease be more specific.`\n }\n};"
|
|
},
|
|
"id": "code-format-logs-multiple",
|
|
"name": "Format Logs Multiple",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1780,
|
|
600
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Build Claude API request body\nconst userMessage = $json.message?.text || '';\n\nconst systemPrompt = `You are a Docker container management assistant. Parse user requests and return ONLY valid JSON.\n\nValid actions:\n- view_logs: User wants to see container logs\n- query_stats: User asks about resource usage (memory, CPU)\n- container_action: User wants to start/stop/restart/update a container\n- container_status: User asks about container status\n- list_containers: User wants to see all containers\n- unknown: Cannot determine intent\n\nRespond with JSON: {\"action\": \"<action>\", \"container\": \"<name or null>\", \"parameters\": {}}\n\nExamples:\n- \"show me plex logs\" -> {\"action\": \"view_logs\", \"container\": \"plex\", \"parameters\": {\"lines\": 50}}\n- \"what's using the most memory?\" -> {\"action\": \"query_stats\", \"container\": null, \"parameters\": {\"metric\": \"memory\", \"sort\": \"desc\"}}\n- \"restart nginx\" -> {\"action\": \"container_action\", \"container\": \"nginx\", \"parameters\": {\"action\": \"restart\"}}\n- \"how's sonarr doing?\" -> {\"action\": \"container_status\", \"container\": \"sonarr\", \"parameters\": {}}\n- \"hello\" -> {\"action\": \"unknown\", \"container\": null, \"parameters\": {\"message\": \"I can help with Docker containers. Try: 'show logs', 'restart plex', or 'what's using memory?'\"}}`;\n\nconst body = {\n model: 'claude-sonnet-4-5-20250929',\n max_tokens: 256,\n system: [{\n type: 'text',\n text: systemPrompt,\n cache_control: { type: 'ephemeral' }\n }],\n messages: [{\n role: 'user',\n content: userMessage\n }]\n};\n\nreturn {\n claudeBody: JSON.stringify(body),\n message: $json.message\n};"
|
|
},
|
|
"id": "code-prepare-claude-body",
|
|
"name": "Prepare Claude Request",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
900,
|
|
300
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "https://api.anthropic.com/v1/messages",
|
|
"authentication": "genericCredentialType",
|
|
"genericAuthType": "httpHeaderAuth",
|
|
"sendHeaders": true,
|
|
"headerParameters": {
|
|
"parameters": [
|
|
{
|
|
"name": "anthropic-version",
|
|
"value": "2023-06-01"
|
|
}
|
|
]
|
|
},
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ $json.claudeBody }}",
|
|
"options": {
|
|
"timeout": 30000,
|
|
"retry": {
|
|
"enabled": true,
|
|
"maxTries": 3
|
|
}
|
|
}
|
|
},
|
|
"id": "http-claude-intent",
|
|
"name": "Claude Intent Parser",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
1010,
|
|
300
|
|
],
|
|
"credentials": {
|
|
"httpHeaderAuth": {
|
|
"id": "anthropic-api-key",
|
|
"name": "Anthropic API Key"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Parse and validate Claude's intent response\nconst response = $input.item.json;\n\n// Claude response structure: { content: [{ type: \"text\", text: \"...\" }] }\nlet intentText = '';\ntry {\n intentText = response.content[0].text;\n} catch (e) {\n return {\n action: 'error',\n error: 'Invalid Claude response structure',\n raw: JSON.stringify(response)\n };\n}\n\n// Parse JSON from Claude's response\nlet intent;\ntry {\n // Claude might wrap JSON in markdown code blocks, strip them\n const cleaned = intentText.replace(/```json\\n?/g, '').replace(/```\\n?/g, '').trim();\n intent = JSON.parse(cleaned);\n} catch (e) {\n return {\n action: 'error',\n error: 'Could not parse intent JSON',\n raw: intentText\n };\n}\n\n// Validate required fields\nconst validActions = ['view_logs', 'query_stats', 'container_action', 'container_status', 'list_containers', 'unknown'];\nif (!intent.action || !validActions.includes(intent.action)) {\n return {\n action: 'unknown',\n error: 'Invalid or missing action',\n parameters: { message: 'I didn\\'t understand that. Try: \"show logs plex\" or \"restart nginx\"' }\n };\n}\n\n// Normalize container name if present\nif (intent.container) {\n intent.container = intent.container.toLowerCase().trim();\n}\n\n// Set defaults for parameters\nintent.parameters = intent.parameters || {};\n\n// Preserve original message for fallback - get from Prepare Claude Request node\nintent.original_message = $('Prepare Claude Request').item.json.message || {};\n\nreturn intent;"
|
|
},
|
|
"id": "code-parse-intent",
|
|
"name": "Parse Intent",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
1120,
|
|
300
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"rules": {
|
|
"values": [
|
|
{
|
|
"id": "route-view-logs",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "is-view-logs",
|
|
"leftValue": "={{ $json.action }}",
|
|
"rightValue": "view_logs",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "view_logs"
|
|
},
|
|
{
|
|
"id": "route-query-stats",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "is-query-stats",
|
|
"leftValue": "={{ $json.action }}",
|
|
"rightValue": "query_stats",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "query_stats"
|
|
},
|
|
{
|
|
"id": "route-container-action",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "is-container-action",
|
|
"leftValue": "={{ $json.action }}",
|
|
"rightValue": "container_action",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "container_action"
|
|
},
|
|
{
|
|
"id": "route-container-status",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "is-container-status",
|
|
"leftValue": "={{ $json.action }}",
|
|
"rightValue": "container_status",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "container_status"
|
|
},
|
|
{
|
|
"id": "route-list-containers",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "is-list-containers",
|
|
"leftValue": "={{ $json.action }}",
|
|
"rightValue": "list_containers",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "list_containers"
|
|
},
|
|
{
|
|
"id": "route-unknown",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "is-unknown",
|
|
"leftValue": "={{ $json.action }}",
|
|
"rightValue": "unknown",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "unknown"
|
|
},
|
|
{
|
|
"id": "route-error",
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict"
|
|
},
|
|
"conditions": [
|
|
{
|
|
"id": "is-error",
|
|
"leftValue": "={{ $json.action }}",
|
|
"rightValue": "error",
|
|
"operator": {
|
|
"type": "string",
|
|
"operation": "equals"
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "error"
|
|
}
|
|
]
|
|
},
|
|
"options": {
|
|
"fallbackOutput": "none"
|
|
}
|
|
},
|
|
"id": "switch-intent-router",
|
|
"name": "Intent Router",
|
|
"type": "n8n-nodes-base.switch",
|
|
"typeVersion": 3.2,
|
|
"position": [
|
|
1340,
|
|
300
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"chatId": "={{ $json.original_message.chat.id }}",
|
|
"text": "={{ $json.parameters.message || \"I can help manage your Docker containers. Try:\\n- 'show logs plex'\\n- 'restart sonarr'\\n- 'what containers are running?'\\n- 'what's using the most memory?'\" }}",
|
|
"additionalFields": {
|
|
"parse_mode": "HTML"
|
|
}
|
|
},
|
|
"id": "telegram-send-unknown",
|
|
"name": "Send Unknown Intent",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
1560,
|
|
100
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"chatId": "={{ $json.original_message.chat.id }}",
|
|
"text": "Stats queries coming soon! For now, try 'status' to see running containers.",
|
|
"additionalFields": {
|
|
"parse_mode": "HTML"
|
|
}
|
|
},
|
|
"id": "telegram-send-stats-placeholder",
|
|
"name": "Send Stats Placeholder",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
1560,
|
|
200
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"parameters": {
|
|
"chatId": "={{ $json.original_message.chat.id }}",
|
|
"text": "Sorry, I encountered an error understanding your request. Please try again.",
|
|
"additionalFields": {
|
|
"parse_mode": "HTML"
|
|
}
|
|
},
|
|
"id": "telegram-send-intent-error",
|
|
"name": "Send Intent Error",
|
|
"type": "n8n-nodes-base.telegram",
|
|
"typeVersion": 1.2,
|
|
"position": [
|
|
1560,
|
|
500
|
|
],
|
|
"credentials": {
|
|
"telegramApi": {
|
|
"id": "telegram-credential",
|
|
"name": "Telegram API"
|
|
}
|
|
}
|
|
}
|
|
],
|
|
"connections": {
|
|
"Telegram Trigger": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Route Update Type",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Route Update Type": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "IF User Authenticated",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "IF Callback Authenticated",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"IF User Authenticated": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Prepare Claude Request",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[]
|
|
]
|
|
},
|
|
"Prepare Claude Request": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Claude Intent Parser",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"IF Callback Authenticated": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Parse Callback Data",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[]
|
|
]
|
|
},
|
|
"Parse Callback Data": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Route Callback",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Route Callback": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Handle Cancel",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Handle Expired",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Build Batch Commands",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Build Callback Action",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Handle Cancel": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Answer Cancel Query",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Answer Cancel Query": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Delete Cancel Message",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Handle Expired": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Answer Expired Query",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Answer Expired Query": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Delete Expired Message",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Callback Action": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Execute Callback Action",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Execute Callback Action": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Parse Callback Result",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Parse Callback Result": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Answer Action Query",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Answer Action Query": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Delete Suggestion Message",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Delete Suggestion Message": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Callback Result",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Route Message": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Docker List Containers",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Parse Action",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Parse Update Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Format Echo",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Docker List Containers": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Parse and Match",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Parse and Match": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Docker Response",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Format Response": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Docker Response",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Format Echo": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Echo",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Parse Action": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Docker List for Action",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Docker List for Action": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Match Container",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Match Container": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Check Match Count",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Check Match Count": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Docker Error",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Find Closest Match",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Build Action Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Build Batch Keyboard",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[]
|
|
]
|
|
},
|
|
"Find Closest Match": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Check Suggestion",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Check Suggestion": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Build Suggestion Keyboard",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Send No Match",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Suggestion Keyboard": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Suggestion",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Action Command": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Execute Action",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Execute Action": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Parse Action Result",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Parse Action Result": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Action Result",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Batch Keyboard": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Batch Confirmation",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Batch Commands": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Prepare Batch Execution",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Prepare Batch Execution": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Execute Batch Action",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Execute Batch Action": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Parse Batch Result",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Parse Batch Result": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Format Batch Result",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Format Batch Result": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Answer Batch Query",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Answer Batch Query": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Delete Batch Confirm Message",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Delete Batch Confirm Message": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Batch Result",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Parse Update Command": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Docker List for Update",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Docker List for Update": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Match Update Container",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Match Update Container": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Check Update Match Count",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Check Update Match Count": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Update Error",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Send Update No Match",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Build Inspect Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Handle Update Multiple",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[]
|
|
]
|
|
},
|
|
"Handle Update Multiple": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Update Multiple",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Inspect Command": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Inspect Container",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Inspect Container": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Parse Container Config",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Parse Container Config": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Build Pull Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Pull Command": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Pull Image",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Pull Image": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Build Image Inspect",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Image Inspect": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Inspect New Image",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Inspect New Image": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Compare Digests",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Compare Digests": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Check If Update Needed",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Check If Update Needed": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Build Stop Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[]
|
|
]
|
|
},
|
|
"Build Stop Command": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Stop Container",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Stop Container": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Verify Stop Build Remove",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Verify Stop Build Remove": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Remove Container",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Remove Container": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Build Create Body",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Create Body": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Build Create Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Create Command": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Create Container",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Create Container": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Parse Create Response",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Parse Create Response": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Build Start Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Start Command": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Start New Container",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Start New Container": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Format Update Result",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Format Update Result": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Update Result",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Parse Logs Command": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Docker List for Logs",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Docker List for Logs": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Match Logs Container",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Match Logs Container": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Check Logs Match Count",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Check Logs Match Count": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Logs Error",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Format Logs No Match",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Build Logs Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Format Logs Multiple",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build Logs Command": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Execute Logs",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Execute Logs": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Format Logs",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Format Logs": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Logs Response",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Format Logs No Match": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Logs Error",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Format Logs Multiple": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Send Logs Error",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Claude Intent Parser": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Parse Intent",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Parse Intent": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Intent Router",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Intent Router": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Parse Logs Command",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Send Stats Placeholder",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Docker List for Action",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Docker List Containers",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Docker List Containers",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Send Unknown Intent",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Send Intent Error",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
}
|
|
},
|
|
"pinData": {},
|
|
"settings": {
|
|
"executionOrder": "v1"
|
|
},
|
|
"staticData": null,
|
|
"tags": [],
|
|
"triggerCount": 1,
|
|
"active": false
|
|
} |