feat(03-02): implement suggestion flow for no-match cases
- Replace Format No Match with Find Closest Match code node - Add Check Suggestion IF node to route based on hasSuggestion - Add Build Suggestion Keyboard code node for inline button payload - Add Send Suggestion HTTP Request to Telegram API with inline_keyboard - Update Match Container to include allContainers for suggestion logic - Suggestion shown when score >= 2 (partial match found)
This commit is contained in:
+107
-18
@@ -327,7 +327,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"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\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 }\n}];"
|
"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",
|
"id": "code-match-container",
|
||||||
"name": "Match Container",
|
"name": "Match Container",
|
||||||
@@ -498,20 +498,80 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"jsCode": "// No container matched the query\nconst data = $input.item.json;\nreturn {\n json: {\n chatId: data.chatId,\n text: `No container found matching '<b>${data.containerQuery}</b>'`\n }\n};"
|
"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-no-match",
|
"id": "code-find-closest",
|
||||||
"name": "Format No Match",
|
"name": "Find Closest Match",
|
||||||
"type": "n8n-nodes-base.code",
|
"type": "n8n-nodes-base.code",
|
||||||
"typeVersion": 2,
|
"typeVersion": 2,
|
||||||
"position": [1780, 300]
|
"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": {
|
"parameters": {
|
||||||
"resource": "message",
|
"resource": "message",
|
||||||
"operation": "sendMessage",
|
"operation": "sendMessage",
|
||||||
"chatId": "={{ $json.chatId }}",
|
"chatId": "={{ $json.chatId }}",
|
||||||
"text": "={{ $json.text }}",
|
"text": "=No container found matching '<b>{{ $json.query }}</b>'",
|
||||||
"additionalFields": {
|
"additionalFields": {
|
||||||
"parse_mode": "HTML"
|
"parse_mode": "HTML"
|
||||||
}
|
}
|
||||||
@@ -520,7 +580,7 @@
|
|||||||
"name": "Send No Match",
|
"name": "Send No Match",
|
||||||
"type": "n8n-nodes-base.telegram",
|
"type": "n8n-nodes-base.telegram",
|
||||||
"typeVersion": 1.2,
|
"typeVersion": 1.2,
|
||||||
"position": [2000, 300],
|
"position": [2220, 400],
|
||||||
"credentials": {
|
"credentials": {
|
||||||
"telegramApi": {
|
"telegramApi": {
|
||||||
"id": "telegram-credential",
|
"id": "telegram-credential",
|
||||||
@@ -745,7 +805,7 @@
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"node": "Format No Match",
|
"node": "Find Closest Match",
|
||||||
"type": "main",
|
"type": "main",
|
||||||
"index": 0
|
"index": 0
|
||||||
}
|
}
|
||||||
@@ -767,6 +827,46 @@
|
|||||||
[]
|
[]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"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": {
|
"Build Action Command": {
|
||||||
"main": [
|
"main": [
|
||||||
[
|
[
|
||||||
@@ -800,17 +900,6 @@
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"Format No Match": {
|
|
||||||
"main": [
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"node": "Send No Match",
|
|
||||||
"type": "main",
|
|
||||||
"index": 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"Format Multiple Matches": {
|
"Format Multiple Matches": {
|
||||||
"main": [
|
"main": [
|
||||||
[
|
[
|
||||||
|
|||||||
Reference in New Issue
Block a user