foodsnap/src/n8n-foodsnap-unified.json

713 lines
26 KiB
JSON
Raw Normal View History

{
"name": "FoodSnap - Unified (Food & Coach)",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "wa/inbound",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-680,
-940
],
"id": "webhook-unified",
"name": "Webhook (Whatsapp)"
},
{
"parameters": {
"jsCode": "const body = $json.body ?? $json;\nconst data = body.data ?? {};\n\n// =========================\n// RemoteJid (prioridade s.whatsapp.net)\n// =========================\nconst remoteJid =\n data?.key?.remoteJid?.includes('@s.whatsapp.net')\n ? data.key.remoteJid\n : data?.key?.remoteJidAlt || '';\n\n// número limpo (E.164 sem +)\nconst number = remoteJid.replace(/\\D/g, '');\n\n// =========================\n// Message ID\n// =========================\nconst message_id = data?.key?.id || '';\n\n// =========================\n// Texto e Caption\n// =========================\n// Verifica conversation, extendedTextMessage (text) e imageMessage (caption)\nconst text =\n data?.message?.conversation ||\n data?.message?.extendedTextMessage?.text ||\n data?.message?.imageMessage?.caption ||\n '';\n\n// =========================\n// Imagem\n// =========================\nconst imageMessage =\n data?.message?.imageMessage ||\n data?.message?.extendedTextMessage?.contextInfo?.quotedMessage?.imageMessage ||\n null;\n\n// =========================\n// Return normalizado\n// =========================\nreturn [\n {\n number,\n remoteJid,\n message_id,\n text,\n hasImage: !!imageMessage,\n imageMessage,\n pushName: data?.pushName || 'Atleta',\n timestamp: new Date().toISOString(),\n raw: body\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-460,
-940
],
"id": "normalize-inbound",
"name": "Normalizar Dados"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": false,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "check-coach",
"leftValue": "={{ $json.text }}",
"rightValue": "coach,treino,shape,biotipo,fisico,musculo",
"operator": {
"type": "string",
"operation": "contains",
"singleValue": true
}
}
],
"combinator": "or"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
-240,
-940
],
"id": "router-intent",
"name": "É Coach?"
},
{
"parameters": {
"operation": "executeQuery",
"query": "with u as (\n select id\n from public.profiles\n where phone_e164 = cast({{ $('Normalizar Dados').item.json.number }} as text)\n limit 1\n),\nent as (\n select ue.is_active, ue.entitlement_code, ue.valid_until\n from public.user_entitlements ue\n where ue.user_id = (select id from u)\n order by ue.valid_until desc nulls last\n limit 1\n),\nusage as (\n select count(*) as used_count\n from public.coach_analyses fa\n where fa.user_id = (select id from u)\n and fa.used_free_quota = true\n)\nselect\n (select id from u) as user_id,\n (select id from u) is not null as exists,\n coalesce((select used_count from usage), 0)::int as free_used,\n greatest(0, 3 - coalesce((select used_count from usage), 0))::int as free_remaining,\n coalesce((select is_active from ent), false) as plan_active,\n (coalesce((select is_active from ent), false) = true OR greatest(0, 3 - coalesce((select used_count from usage), 0)) > 0) as can_process",
"options": {}
},
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
20,
-1160
],
"id": "validate-coach",
"name": "Validação Coach",
"credentials": {
"postgres": {
"id": "2JDD2OJz4cAsWb0J",
"name": "foodsnap_supabase"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "with u as (\n select id\n from public.profiles\n where phone_e164 = cast({{ $('Normalizar Dados').item.json.number }} as text)\n limit 1\n),\nent as (\n select\n ue.user_id,\n ue.is_active,\n ue.entitlement_code\n from public.user_entitlements ue\n where ue.user_id = (select id from u)\n order by ue.valid_until desc nulls last\n limit 1\n),\nusage as (\n select count(*) filter (where fa.used_free_quota = true) as free_used\n from public.food_analyses fa\n where fa.user_id = (select id from u)\n)\nselect\n (select id from u) is not null as exists,\n (select id from u) as user_id,\n coalesce((select free_used from usage), 0)::int as free_used,\n greatest(0, 5 - coalesce((select free_used from usage), 0))::int as free_remaining,\n coalesce((select is_active from ent), false) as plan_active,\n (\n (select id from u) is not null\n and (\n coalesce((select is_active from ent), false) = true\n or greatest(0, 5 - coalesce((select free_used from usage), 0)) > 0\n )\n ) as can_process;",
"options": {}
},
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
20,
-740
],
"id": "validate-food",
"name": "Validação Food",
"credentials": {
"postgres": {
"id": "2JDD2OJz4cAsWb0J",
"name": "foodsnap_supabase"
}
}
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "check-process",
"leftValue": "={{ $json.can_process }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
260,
-1160
],
"id": "if-coach-quota",
"name": "Coach OK?"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "check-process-food",
"leftValue": "={{ $json.can_process }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
260,
-740
],
"id": "if-food-quota",
"name": "Food OK?"
},
{
"parameters": {
"resource": "messages-api",
"instanceName": "FoodSnap",
"remoteJid": "={{ $('Normalizar Dados').item.json.number }}",
"messageText": "🧐 *Coach AI*: Analisando seu biótipo e gerando seu treino... Aguarde!",
"options_message": {}
},
"type": "n8n-nodes-evolution-api.evolutionApi",
"typeVersion": 1,
"position": [
500,
-1260
],
"id": "msg-ack-coach",
"name": "Ack Coach",
"credentials": {
"evolutionApi": {
"id": "nGWBERcZoQWgdxgk",
"name": "FoodSnap"
}
}
},
{
"parameters": {
"resource": "chat-api",
"operation": "get-media-base64",
"instanceName": "FoodSnap",
"messageId": "={{ $('Normalizar Dados').item.json.message_id }}"
},
"type": "n8n-nodes-evolution-api.evolutionApi",
"typeVersion": 1,
"position": [
720,
-1260
],
"id": "get-image-coach",
"name": "Baixar IMG Coach",
"credentials": {
"evolutionApi": {
"id": "nGWBERcZoQWgdxgk",
"name": "FoodSnap"
}
}
},
{
"parameters": {
"operation": "toBinary",
"sourceProperty": "data.base64",
"options": {}
},
"type": "n8n-nodes-base.convertToFile",
"typeVersion": 1.1,
"position": [
940,
-1260
],
"id": "convert-binary-coach",
"name": "Binário Coach"
},
{
"parameters": {
"resource": "image",
"operation": "analyze",
"modelId": {
"__rl": true,
"value": "models/gemini-1.5-flash",
"mode": "list",
"cachedResultName": "models/gemini-1.5-flash"
},
"text": "=Você é um Treinador Físico de Elite e Nutricionista Esportivo.\nAnalise a imagem fornecida (foto de corpo inteiro/físico).\n\n1. Verifique se é uma foto de corpo humano válida para análise fitness. Se não, retorne \"valid_body\": false.\n2. Se for válida, estime:\n - Biótipo Predominante (Ectomorfo, Mesomorfo, Endomorfo)\n - Percentual de Gordura (BF% aproximado)\n - Nível de Massa Muscular (Baixo, Médio, Alto)\n3. Sugira:\n - Objetivo Principal (Cutting, Bulking, Manutenção)\n - Divisão de Treino Recomendada (ex: ABC, ABCDE, Upper/Lower)\n - Foco Dietético (ex: Carb Cycling, Alto Carbo, Keto)\n - Dica de Ouro (uma frase curta de impacto)\n\nResponda APENAS em JSON estrito (sem markdown):\n{\n \"valid_body\": true,\n \"biotype\": \"...\",\n \"estimated_body_fat\": 15,\n \"muscle_mass\": \"Médio\",\n \"goal\": \"Bulking\",\n \"workout\": \"...\",\n \"diet\": \"...\",\n \"tip\": \"...\"\n}",
"inputType": "binary",
"simplify": false,
"options": {}
},
"type": "@n8n/n8n-nodes-langchain.googleGemini",
"typeVersion": 1,
"position": [
1160,
-1260
],
"id": "gemini-coach",
"name": "Gemini Coach",
"credentials": {
"googlePalmApi": {
"id": "T2uIVBcjJ9h8BFCC",
"name": "Backup APIKEY"
}
}
},
{
"parameters": {
"jsCode": "const raw = $json?.candidates?.[0]?.content?.parts?.[0]?.text || '{}';\nconst clean = raw.replace(/```json/g, '').replace(/```/g, '').trim();\nlet data = {};\ntry { \n data = JSON.parse(clean); \n} catch(e) {\n data = { valid_body: false, error: 'Failed to parse AI' };\n}\n\nreturn [{ ...data }];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1380,
-1260
],
"id": "parse-coach-json",
"name": "Parse Coach JSON"
},
{
"parameters": {
"operation": "executeQuery",
"query": "insert into public.coach_analyses\n(\n user_id,\n source,\n image_url,\n ai_raw_response,\n ai_structured,\n biotype,\n estimated_body_fat,\n goal_suggestion,\n muscle_mass_level,\n used_free_quota\n)\nvalues\n(\n cast('{{ $(\"Validação Coach\").item.json.user_id }}' as uuid),\n 'whatsapp',\n null,\n '{{ JSON.stringify($json) }}',\n '{{ JSON.stringify($json) }}',\n '{{ $json.biotype }}',\n cast({{ $json.estimated_body_fat || 0 }} as numeric),\n '{{ $json.goal }}',\n '{{ $json.muscle_mass }}',\n CASE\n WHEN {{ $(\"Validação Coach\").item.json.plan_active }} = true THEN false\n ELSE true\n END\n)\nreturning id as analysis_id;",
"options": {}
},
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
1600,
-1260
],
"id": "save-coach-db",
"name": "Salvar Coach DB",
"credentials": {
"postgres": {
"id": "2JDD2OJz4cAsWb0J",
"name": "foodsnap_supabase"
}
}
},
{
"parameters": {
"resource": "messages-api",
"instanceName": "FoodSnap",
"remoteJid": "={{ $('Normalizar Dados').item.json.number }}",
"messageText": "=⚡ *Coach AI Report*\n\n🧬 *Biótipo*: {{$json.biotype}}\n⚖ *BF Estimado*: ~{{$json.estimated_body_fat}}%\n💪 *Massa Muscular*: {{$json.muscle_mass}}\n\n🎯 *Foco*: {{$json.goal}}\n🏋 *Treino*: {{$json.workout}}\n🥗 *Dieta*: {{$json.diet}}\n\n💡 *Dica*: {{$json.tip}}\n\n_Acesse o App para ver a ficha completa!_",
"options_message": {}
},
"type": "n8n-nodes-evolution-api.evolutionApi",
"typeVersion": 1,
"position": [
1820,
-1260
],
"id": "reply-coach",
"name": "Responder Coach",
"credentials": {
"evolutionApi": {
"id": "nGWBERcZoQWgdxgk",
"name": "FoodSnap"
}
}
},
{
"parameters": {
"resource": "messages-api",
"instanceName": "FoodSnap",
"remoteJid": "={{ $('Normalizar Dados').item.json.number }}",
"messageText": "📸 Recebi sua foto! Analisando o prato... ⏳",
"options_message": {}
},
"type": "n8n-nodes-evolution-api.evolutionApi",
"typeVersion": 1,
"position": [
500,
-740
],
"id": "ack-food",
"name": "Ack Food",
"credentials": {
"evolutionApi": {
"id": "nGWBERcZoQWgdxgk",
"name": "FoodSnap"
}
}
},
{
"parameters": {
"resource": "chat-api",
"operation": "get-media-base64",
"instanceName": "FoodSnap",
"messageId": "={{ $('Normalizar Dados').item.json.message_id }}"
},
"type": "n8n-nodes-evolution-api.evolutionApi",
"typeVersion": 1,
"position": [
720,
-740
],
"id": "get-image-food",
"name": "Baixar IMG Food",
"credentials": {
"evolutionApi": {
"id": "nGWBERcZoQWgdxgk",
"name": "FoodSnap"
}
}
},
{
"parameters": {
"operation": "toBinary",
"sourceProperty": "data.base64",
"options": {}
},
"type": "n8n-nodes-base.convertToFile",
"typeVersion": 1.1,
"position": [
940,
-740
],
"id": "convert-binary-food",
"name": "Binário Food"
},
{
"parameters": {
"resource": "image",
"operation": "analyze",
"modelId": {
"__rl": true,
"value": "models/gemini-1.5-flash",
"mode": "list",
"cachedResultName": "models/gemini-1.5-flash"
},
"text": "=Você é um assistente nutricional... (Prompt Original de Comida)... Retorne SOMENTE JSON.",
"inputType": "binary",
"simplify": false,
"options": {}
},
"type": "@n8n/n8n-nodes-langchain.googleGemini",
"typeVersion": 1,
"position": [
1160,
-740
],
"id": "gemini-food",
"name": "Gemini Food",
"credentials": {
"googlePalmApi": {
"id": "T2uIVBcjJ9h8BFCC",
"name": "Backup APIKEY"
}
}
},
{
"parameters": {
"jsCode": "// Limpeza de JSON da Comida (Original)\nconst raw = $json?.candidates?.[0]?.content?.parts?.[0]?.text || '{}';\n// ... lógica existente de parse do FoodSnap ...\nreturn [{\n items: [],\n total: { calories: 500, protein: 30 },\n tip: { text: \"Exemplo de análise de comida\" }\n}];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1380,
-740
],
"id": "parse-food",
"name": "Parse Food",
"notes": "Lógica completa de parse de comida aqui (resumida para o arquivo)"
},
{
"parameters": {
"operation": "executeQuery",
"query": "insert into public.food_analyses ... (SQL Original)",
"options": {}
},
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
1600,
-740
],
"id": "save-food-db",
"name": "Salvar Food DB",
"credentials": {
"postgres": {
"id": "2JDD2OJz4cAsWb0J",
"name": "foodsnap_supabase"
}
}
},
{
"parameters": {
"resource": "messages-api",
"instanceName": "FoodSnap",
"remoteJid": "={{ $('Normalizar Dados').item.json.number }}",
"messageText": "=🥗 *FoodSnap*: Calorias: {{$json.total.calories}} ... (Formato Original)",
"options_message": {}
},
"type": "n8n-nodes-evolution-api.evolutionApi",
"typeVersion": 1,
"position": [
1820,
-740
],
"id": "reply-food",
"name": "Responder Food",
"credentials": {
"evolutionApi": {
"id": "nGWBERcZoQWgdxgk",
"name": "FoodSnap"
}
}
},
{
"parameters": {
"resource": "messages-api",
"instanceName": "FoodSnap",
"remoteJid": "={{ $('Normalizar Dados').item.json.number }}",
"messageText": "⚠️ Por favor, envie uma *imagem* para análise.",
"options_message": {}
},
"type": "n8n-nodes-evolution-api.evolutionApi",
"typeVersion": 1,
"position": [
260,
-500
],
"id": "msg-no-image",
"name": "Sem Imagem",
"credentials": {
"evolutionApi": {
"id": "nGWBERcZoQWgdxgk",
"name": "FoodSnap"
}
}
}
],
"connections": {
"Webhook (Whatsapp)": {
"main": [
[
{
"node": "Normalizar Dados",
"type": "main",
"index": 0
}
]
]
},
"Normalizar Dados": {
"main": [
[
{
"node": "É Coach?",
"type": "main",
"index": 0
}
]
]
},
"É Coach?": {
"main": [
[
{
"node": "Validação Coach",
"type": "main",
"index": 0
}
],
[
{
"node": "Validação Food",
"type": "main",
"index": 0
}
]
]
},
"Validação Coach": {
"main": [
[
{
"node": "Coach OK?",
"type": "main",
"index": 0
}
]
]
},
"Validação Food": {
"main": [
[
{
"node": "Food OK?",
"type": "main",
"index": 0
}
]
]
},
"Coach OK?": {
"main": [
[
{
"node": "Ack Coach",
"type": "main",
"index": 0
}
]
]
},
"Food OK?": {
"main": [
[
{
"node": "Ack Food",
"type": "main",
"index": 0
}
]
]
},
"Ack Coach": {
"main": [
[
{
"node": "Baixar IMG Coach",
"type": "main",
"index": 0
}
]
]
},
"Ack Food": {
"main": [
[
{
"node": "Baixar IMG Food",
"type": "main",
"index": 0
}
]
]
},
"Baixar IMG Coach": {
"main": [
[
{
"node": "Binário Coach",
"type": "main",
"index": 0
}
]
]
},
"Baixar IMG Food": {
"main": [
[
{
"node": "Binário Food",
"type": "main",
"index": 0
}
]
]
},
"Binário Coach": {
"main": [
[
{
"node": "Gemini Coach",
"type": "main",
"index": 0
}
]
]
},
"Binário Food": {
"main": [
[
{
"node": "Gemini Food",
"type": "main",
"index": 0
}
]
]
},
"Gemini Coach": {
"main": [
[
{
"node": "Parse Coach JSON",
"type": "main",
"index": 0
}
]
]
},
"Gemini Food": {
"main": [
[
{
"node": "Parse Food",
"type": "main",
"index": 0
}
]
]
},
"Parse Coach JSON": {
"main": [
[
{
"node": "Salvar Coach DB",
"type": "main",
"index": 0
}
]
]
},
"Parse Food": {
"main": [
[
{
"node": "Salvar Food DB",
"type": "main",
"index": 0
}
]
]
},
"Salvar Coach DB": {
"main": [
[
{
"node": "Responder Coach",
"type": "main",
"index": 0
}
]
]
},
"Salvar Food DB": {
"main": [
[
{
"node": "Responder Food",
"type": "main",
"index": 0
}
]
]
}
}
}