From deeefe2b40228fba0c6200ddebac5688fa375adc Mon Sep 17 00:00:00 2001 From: Eric Date: Fri, 21 Aug 2026 10:16:09 -0300 Subject: [PATCH] feat(n8n): reconstroi o fluxo em cima do Webhook do Canal (resolve imagem) O 'Webhook do Canal' do Z-PRO (fora do ChatFlow, opcao 'Mensagens recebidas') manda o payload completo da mensagem, incluindo msg.image.url para fotos -- resolve a limitacao que bloqueava imagem. Novo endpoint orcamento-exames-canal-zpro substitui o antigo orcamento-exames-zpro (removido). Download da imagem autenticado com Bearer usando o bmToken do proprio payload. Volta ao envio assincrono via sub-workflow enviar_texto, ja que esse webhook e uma notificacao (nao e capturado/reenviado pelo Z-PRO como no ChatFlow). Prompt volta a aceitar foto normalmente. Co-Authored-By: Claude Sonnet 5 --- ...26-08-20-orcamento-exames-imagem-design.md | 62 +++++- n8n/orcamento-exames-imagem.workflow.json | 206 +++++++++++------- 2 files changed, 182 insertions(+), 86 deletions(-) diff --git a/docs/superpowers/specs/2026-08-20-orcamento-exames-imagem-design.md b/docs/superpowers/specs/2026-08-20-orcamento-exames-imagem-design.md index 60f555a..ddd1acf 100644 --- a/docs/superpowers/specs/2026-08-20-orcamento-exames-imagem-design.md +++ b/docs/superpowers/specs/2026-08-20-orcamento-exames-imagem-design.md @@ -281,15 +281,63 @@ ferramenta nesse ponto. Corrigido o prompt do `AI Agent` pra não prometer mais "mande uma foto" ao cliente (antes disso ele pedia foto mesmo sem nunca conseguir processar nenhuma). +### Descoberta 5: "Webhook do Canal" resolve a imagem (solução final) + +O Z-PRO tem, nas configurações do canal, uma opção **"Webhook do Canal"** +independente do ChatFlow: `Habilitar Webhook do Canal` + `URL de destino` + +toggle `Mensagens recebidas` (dispara a cada mensagem recebida pelo canal, +"em paralelo ao webhook geral do tenant"). Payload real capturado: + +```json +{ + "method": "message", + "msg": { + "from": "5575...", + "id": "wamid....", + "timestamp": "...", + "type": "image", + "image": { "mime_type": "image/jpeg", "id": "...", "url": "https://lookaside.fbsbx.com/whatsapp_business/attachments/?..." } + }, + "ticket": { + "protocol": "...", "id": 469774, "status": "pending", + "contact": { "name": "...", "number": "..." }, + "whatsapp": { "bmToken": "EAA..." } + } +} +``` + +Pra mensagem de texto, `msg` vem como `{ "type": "text", "text": { "body": "..." } }` +em vez de `image`. O workflow foi reconstruído em cima desse webhook: + +- Novo endpoint: `orcamento-exames-canal-zpro` (path completamente separado + do antigo `orcamento-exames-zpro`, que foi removido). +- Campos extraídos: `sessionId` (`ticket.protocol`), `telefone` (`msg.from`), + `nomeContato` (`ticket.contact.name`), `entrada` (`msg.text.body`), + `imagemUrl` (`msg.image.url`), `tipoMidia` (`msg.type`), `mediaToken` + (`ticket.whatsapp.bmToken`). +- A URL da imagem é do WhatsApp/Meta (`lookaside.fbsbx.com`) e exige + autenticação — o node "Baixar imagem" manda `Authorization: Bearer + {{ mediaToken }}` usando o `bmToken` do próprio payload. +- Como esse webhook é uma notificação assíncrona (não é capturado e + reenviado pelo Z-PRO como no mecanismo do ChatFlow), o envio da resposta + voltou a ser feito via API, reaproveitando o sub-workflow + `enviar_texto` (mesma lógica da primeira versão do desenho). +- **Importante:** esse webhook dispara em paralelo a qualquer coisa que o + ChatFlow também dispare para a mesma mensagem. Por isso as interações + "Webhook Avançado" e "Mensagem" dos passos `Menu Orçamento` / `Menu + Orçamento 2` no ChatFlow precisam ser removidas (ação do usuário, fora do + n8n) — senão o cliente recebe duas respostas pra cada mensagem. +- O prompt do `AI Agent` voltou a aceitar/pedir foto normalmente, já que a + imagem agora chega de verdade. + ### Ainda em aberto -Falta o usuário confirmar se existe, fora do construtor de ChatFlow (em -Configurações/Integrações do canal ou da empresa), um webhook de propósito -geral que dispare para toda mensagem recebida no canal — mecanismo -diferente do "Webhook Avançado" dentro de um ChatFlow. Se não existir, -verificar com o suporte do Z-PRO se há algum outro caminho suportado pela -plataforma. Até lá, o workflow funciona **somente com texto** — o cliente -digita os nomes dos exames, e a IA monta o orçamento normalmente. +- Confirmar, depois de remover os passos antigos do ChatFlow, que não há + mais resposta duplicada. +- Validar ponta a ponta com uma foto real de pedido de exames (a extração + do `imagemUrl`/`mediaToken` foi validada com uma imagem de teste, mas o + fluxo completo — download autenticado + IA de visão — ainda não foi + testado de ponta a ponta após esta reconstrução). ## Fora de escopo (por enquanto) diff --git a/n8n/orcamento-exames-imagem.workflow.json b/n8n/orcamento-exames-imagem.workflow.json index 02d3df4..069b14a 100644 --- a/n8n/orcamento-exames-imagem.workflow.json +++ b/n8n/orcamento-exames-imagem.workflow.json @@ -4,21 +4,21 @@ "active": true, "nodes": [ { - "parameters": { - "httpMethod": "POST", - "path": "orcamento-exames-zpro", - "responseMode": "responseNode", - "options": {} - }, - "id": "webhook-orcamento", - "name": "Webhook - Foto Exame", + "id": "webhook-canal-zpro", + "name": "Webhook - Canal Z-PRO", "type": "n8n-nodes-base.webhook", "typeVersion": 2.1, "position": [ -608, - 0 + 320 ], - "webhookId": "5ea5f89e-7351-4d45-a881-0055009423a0", + "parameters": { + "httpMethod": "POST", + "path": "orcamento-exames-canal-zpro", + "responseMode": "onReceived", + "options": {} + }, + "webhookId": "a00a2a36-f7ec-4e8e-a98f-93d3f93dfe45", "onError": "continueRegularOutput" }, { @@ -29,37 +29,43 @@ "id": "campo-sessionid", "name": "sessionId", "type": "string", - "value": "={{ $json.body.protocol }}" + "value": "={{ $json.body.ticket.protocol }}" }, { "id": "campo-telefone", "name": "telefone", "type": "string", - "value": "={{ $json.body.number }}" + "value": "={{ $json.body.msg.from }}" }, { "id": "campo-nomecontato", "name": "nomeContato", "type": "string", - "value": "={{ $json.body.contactName }}" + "value": "={{ $json.body.ticket.contact.name }}" }, { "id": "campo-entrada", "name": "entrada", "type": "string", - "value": "={{ String($json.body.entrada || $json.body.text || '').trim() }}" + "value": "={{ ($json.body.msg.text && $json.body.msg.text.body) || '' }}" }, { "id": "campo-imagemurl", "name": "imagemUrl", "type": "string", - "value": "={{ $json.body.mediaUrl || $json.body.fileUrl || $json.body.file || '' }}" + "value": "={{ ($json.body.msg.image && $json.body.msg.image.url) || '' }}" }, { "id": "campo-tipomidia", "name": "tipoMidia", "type": "string", - "value": "={{ $json.body.mediaType || $json.body.type || '' }}" + "value": "={{ $json.body.msg.type || '' }}" + }, + { + "id": "campo-mediatoken", + "name": "mediaToken", + "type": "string", + "value": "={{ ($json.body.ticket.whatsapp && $json.body.ticket.whatsapp.bmToken) || '' }}" } ] }, @@ -118,6 +124,15 @@ } }, "timeout": 20000 + }, + "sendHeaders": true, + "headerParameters": { + "parameters": [ + { + "name": "Authorization", + "value": "=Bearer {{ $json.mediaToken }}" + } + ] } }, "id": "baixar-imagem", @@ -158,9 +173,9 @@ { "parameters": { "promptType": "define", - "text": "={{ $('Extrair campos do webhook').item.json.entrada || 'O cliente mandou algo que não chegou como texto (provavelmente uma foto ou áudio, que ainda não conseguimos processar). Peça pra ele digitar os nomes dos exames.' }}", + "text": "={{ $('Extrair campos do webhook').item.json.entrada || 'O cliente enviou uma imagem. Veja a imagem anexada, que é o pedido médico dele.' }}", "options": { - "systemMessage": "Você é o assistente de orçamentos de exames de uma clínica, atendendo automaticamente pelo WhatsApp, numa conversa com várias mensagens (não só uma).\n\nIMPORTANTE (limitação técnica atual): você NUNCA recebe fotos/imagens nesta conversa, mesmo que o cliente mande uma — só texto chega até você. Por isso, NUNCA peça pro cliente mandar foto ou imagem do pedido. Se o cliente mandar uma foto (você vai perceber isso porque a mensagem dele chega vazia, sem texto), peça educadamente pra ele DIGITAR os nomes dos exames que estão no pedido médico, sem mencionar foto.\n\nTABELA DE EXAMES E VALORES (lista de exemplo — será substituída pela lista real depois):\n- Hemograma completo — R$ 35,00\n- Glicemia de jejum — R$ 20,00\n- Colesterol total e frações — R$ 45,00\n- Triglicérideos — R$ 25,00\n- TSH (hormônio tireoidiano) — R$ 40,00\n- Exame de urina (EAS) — R$ 30,00\n- Ultrassonografia abdominal — R$ 150,00\n- Raio-X de tórax — R$ 90,00\n- Eletrocardiograma (ECG) — R$ 80,00\n- Papanicolau — R$ 60,00\n\nCOMO CONVERSAR:\n1. O cliente pode digitar os nomes dos exames de uma vez ou aos poucos, em mensagens separadas — use também o histórico da conversa (mensagens anteriores) pra juntar as informações, não só a mensagem atual.\n2. Se o cliente ainda não digitou, nem agora nem antes nesta conversa, nenhum nome de exame reconhecível, NÃO invente orçamento — cumprimente rapidamente e peça pra ele digitar os nomes dos exames conforme o pedido médico.\n3. Assim que tiver pelo menos um exame identificável (considerando o texto atual E o histórico da conversa), identifique cada exame pedido.\n4. Para cada exame identificado que corresponde (mesmo com sinônimo, abreviação comum ou pequena diferença de grafia) a um item da tabela acima, inclua na resposta o nome do exame e o valor.\n5. Para exames identificados que NÃO correspondem a nenhum item da tabela, ou que você não conseguiu entender com confiança, liste-os separadamente avisando que um atendente vai confirmar o valor.\n6. Nunca deixe de orçar por causa de um item não reconhecido — sempre orçe o que for possível reconhecer.\n7. Calcule e mostre o valor total somando apenas os itens com preço confirmado na tabela.\n8. Responda sempre em português, tom cordial e objetivo, formatado para WhatsApp: texto simples, sem markdown (nada de ** ou #), pode usar emojis com moderação e quebras de linha para organizar.\n9. Sua resposta deve conter SOMENTE o texto a ser enviado ao cliente nesta mensagem — nada de comentários extras, explicações do seu raciocínio, ou texto fora da conversa com o cliente.", + "systemMessage": "Você é o assistente de orçamentos de exames de uma clínica, atendendo automaticamente pelo WhatsApp, numa conversa com várias mensagens (não só uma).\n\nTABELA DE EXAMES E VALORES (lista de exemplo — será substituída pela lista real depois):\n- Hemograma completo — R$ 35,00\n- Glicemia de jejum — R$ 20,00\n- Colesterol total e frações — R$ 45,00\n- Triglicérideos — R$ 25,00\n- TSH (hormônio tireoidiano) — R$ 40,00\n- Exame de urina (EAS) — R$ 30,00\n- Ultrassonografia abdominal — R$ 150,00\n- Raio-X de tórax — R$ 90,00\n- Eletrocardiograma (ECG) — R$ 80,00\n- Papanicolau — R$ 60,00\n\nCOMO CONVERSAR:\n1. O cliente pode mandar uma foto do pedido médico (com ou sem legenda), digitar os nomes dos exames, ou fazer as duas coisas em mensagens separadas, em qualquer ordem — use também o histórico da conversa (mensagens anteriores) pra juntar as informações, não só a mensagem atual. Uma imagem anexada a esta mensagem (quando houver) é o pedido médico do cliente.\n2. Se o cliente ainda não mandou, nem agora nem antes nesta conversa, nenhuma foto de pedido nem nomes de exames, NÃO invente orçamento — cumprimente rapidamente e peça pra ele mandar a foto do pedido médico ou digitar os nomes dos exames.\n3. Assim que tiver pelo menos um exame identificável (considerando a imagem atual, o texto atual E o histórico da conversa), identifique cada exame pedido.\n4. Para cada exame identificado que corresponde (mesmo com sinônimo, abreviação comum ou pequena diferença de grafia) a um item da tabela acima, inclua na resposta o nome do exame e o valor.\n5. Para exames identificados que NÃO correspondem a nenhum item da tabela, ou que você não conseguiu ler/entender com confiança, liste-os separadamente avisando que um atendente vai confirmar o valor.\n6. Nunca deixe de orçar por causa de um item não reconhecido — sempre orçe o que for possível reconhecer.\n7. Calcule e mostre o valor total somando apenas os itens com preço confirmado na tabela.\n8. Responda sempre em português, tom cordial e objetivo, formatado para WhatsApp: texto simples, sem markdown (nada de ** ou #), pode usar emojis com moderação e quebras de linha para organizar.\n9. Sua resposta deve conter SOMENTE o texto a ser enviado ao cliente nesta mensagem — nada de comentários extras, explicações do seu raciocínio, ou texto fora da conversa com o cliente.", "passthroughBinaryImages": true } }, @@ -179,10 +194,28 @@ "assignments": { "assignments": [ { - "id": "resp-output", - "name": "output", + "id": "resp-number", + "name": "number", + "type": "string", + "value": "={{ $('Extrair campos do webhook').item.json.telefone }}" + }, + { + "id": "resp-body", + "name": "body", "type": "string", "value": "={{ $json.output ? $json.output : 'Recebemos sua solicitação de exames! Nossa equipe vai confirmar os valores e te retornar em breve.' }}" + }, + { + "id": "resp-externalkey", + "name": "externalKey", + "type": "string", + "value": "={{ 'orcamento-' + $execution.id }}" + }, + { + "id": "resp-isclosed", + "name": "isClosed", + "type": "boolean", + "value": false } ] }, @@ -213,60 +246,75 @@ "name": "Simple Memory" }, { - "parameters": { - "respondWith": "text", - "responseBody": "={{ $json.output }}", - "options": {} - }, - "id": "responder-webhook", - "name": "Responder Webhook (texto)", - "type": "n8n-nodes-base.respondToWebhook", - "typeVersion": 1.5, + "id": "enviar-orcamento-zpro", + "name": "Enviar orçamento (Z-PRO)", + "type": "n8n-nodes-base.executeWorkflow", + "typeVersion": 1.3, "position": [ - 912, + 900, -128 - ] - }, - { - "id": "webhook-canal-zpro", - "name": "Webhook - Canal Z-PRO (diagnóstico)", - "type": "n8n-nodes-base.webhook", - "typeVersion": 2.1, - "position": [ - -608, - 320 ], "parameters": { - "httpMethod": "POST", - "path": "orcamento-exames-canal-zpro", - "responseMode": "onReceived", - "options": {} + "workflowId": { + "__rl": true, + "mode": "id", + "value": "Y4ndG7GuIUrmVRxV", + "cachedResultName": "MCP Server - Mensageria ZPRO - enviar_texto" + }, + "workflowInputs": { + "mappingMode": "defineBelow", + "value": { + "number": "={{ $json.number }}", + "body": "={{ $json.body }}", + "externalKey": "={{ $json.externalKey }}", + "isClosed": "={{ $json.isClosed }}" + }, + "matchingColumns": [], + "schema": [ + { + "id": "number", + "displayName": "number", + "required": false, + "defaultMatch": false, + "display": true, + "canBeUsedToMatch": true, + "type": "string" + }, + { + "id": "body", + "displayName": "body", + "required": false, + "defaultMatch": false, + "display": true, + "canBeUsedToMatch": true, + "type": "string" + }, + { + "id": "externalKey", + "displayName": "externalKey", + "required": false, + "defaultMatch": false, + "display": true, + "canBeUsedToMatch": true, + "type": "string" + }, + { + "id": "isClosed", + "displayName": "isClosed", + "required": false, + "defaultMatch": false, + "display": true, + "canBeUsedToMatch": true, + "type": "boolean" + } + ], + "attemptToConvertTypes": false, + "convertFieldsToString": false + } } - }, - { - "id": "noop-canal-zpro", - "name": "Capturar payload (diagnóstico)", - "type": "n8n-nodes-base.noOp", - "typeVersion": 1, - "position": [ - -368, - 320 - ], - "parameters": {} } ], "connections": { - "Webhook - Foto Exame": { - "main": [ - [ - { - "node": "Extrair campos do webhook", - "type": "main", - "index": 0 - } - ] - ] - }, "É mensagem de imagem?": { "main": [ [ @@ -343,17 +391,6 @@ ] ] }, - "Montar corpo da resposta": { - "main": [ - [ - { - "node": "Responder Webhook (texto)", - "type": "main", - "index": 0 - } - ] - ] - }, "Extrair campos do webhook": { "main": [ [ @@ -365,11 +402,22 @@ ] ] }, - "Webhook - Canal Z-PRO (diagnóstico)": { + "Webhook - Canal Z-PRO": { "main": [ [ { - "node": "Capturar payload (diagnóstico)", + "node": "Extrair campos do webhook", + "type": "main", + "index": 0 + } + ] + ] + }, + "Montar corpo da resposta": { + "main": [ + [ + { + "node": "Enviar orçamento (Z-PRO)", "type": "main", "index": 0 }