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 <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user