From a692795cd4da3ac9a198618603174f2180b9b5e6 Mon Sep 17 00:00:00 2001 From: Isaac_Aracena Date: Sun, 5 Jul 2026 04:00:29 +0000 Subject: [PATCH] =?UTF-8?q?Update=20backup:=20Chat=20de=20WhatsApp=20-=20P?= =?UTF-8?q?ropuestas=20Ejecutadas=20-=20Versi=C3=B3n=20Final?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...p-propuestas-ejecutadas-version-final.json | 134 +++++++++--------- 1 file changed, 67 insertions(+), 67 deletions(-) diff --git a/chat-de-whatsapp-propuestas-ejecutadas-version-final.json b/chat-de-whatsapp-propuestas-ejecutadas-version-final.json index 9c1ce5a..1e7a700 100644 --- a/chat-de-whatsapp-propuestas-ejecutadas-version-final.json +++ b/chat-de-whatsapp-propuestas-ejecutadas-version-final.json @@ -1,5 +1,5 @@ { - "updatedAt": "2026-06-25T19:30:58.479Z", + "updatedAt": "2026-07-04T22:33:50.467Z", "createdAt": "2026-06-24T14:10:15.435Z", "id": "CdosDIWBgnDMnPdr", "name": "Chat de WhatsApp - Propuestas Ejecutadas - Versión Final", @@ -4150,9 +4150,9 @@ "operation": "analyze", "modelId": { "__rl": true, - "value": "models/gemini-3.1-flash-lite", + "value": "models/gemini-3.1-pro-preview", "mode": "list", - "cachedResultName": "models/gemini-3.1-flash-lite" + "cachedResultName": "models/gemini-3.1-pro-preview" }, "text": "={{ $json.gemini_prompt_audio }}", "inputType": "binary", @@ -4166,17 +4166,19 @@ ], "id": "353aba0e-8ed5-484a-b8fc-65ee7829a951", "name": "Gemini - Analizar audio WhatsApp TEST", + "retryOnFail": true, + "waitBetweenTries": 5000, "credentials": { "googlePalmApi": { - "id": "6Jdcd9FONxG35YuJ", - "name": "Isaac - Gemini Api Gratis" + "id": "jvsXYwL6IOoY2DBU", + "name": "Isaac - Gemini Api Pago" } - } + }, + "onError": "continueRegularOutput" }, { "parameters": { - "mode": "runOnceForEachItem", - "jsCode": "const gemini = $input.item.json || {};\n\nlet fuente = {};\n\ntry {\n fuente = $('Code - Preparar Gemini audio WhatsApp TEST').item.json || {};\n} catch (error) {\n fuente = {};\n}\n\nconst extraerTexto = (obj) => {\n if (!obj || typeof obj !== 'object') return '';\n\n if (typeof obj.text === 'string') return obj.text;\n if (typeof obj.output === 'string') return obj.output;\n if (typeof obj.response === 'string') return obj.response;\n if (typeof obj.content === 'string') return obj.content;\n\n if (obj.content?.parts && Array.isArray(obj.content.parts)) {\n return obj.content.parts\n .map((p) => p?.text || '')\n .filter(Boolean)\n .join('\\n');\n }\n\n if (Array.isArray(obj.parts)) {\n return obj.parts\n .map((p) => p?.text || '')\n .filter(Boolean)\n .join('\\n');\n }\n\n if (Array.isArray(obj.candidates)) {\n return JSON.stringify(obj.candidates);\n }\n\n return JSON.stringify(obj);\n};\n\nconst textoAnalisis = extraerTexto(gemini);\n\nreturn {\n json: {\n ...fuente,\n\n session_id: fuente.session_id || '',\n ejecucion_id: fuente.ejecucion_id || '',\n media_event_id: fuente.media_event_id || '',\n media_type: 'audio',\n media_file_name: fuente.media_file_name || '',\n\n analisis_texto: textoAnalisis,\n fecha_analisis: new Date().toISOString(),\n estado: 'ANALISIS_AUDIO_COMPLETADO',\n\n media_index: fuente.media_index || 1,\n media_total: fuente.media_total || fuente.media_total_esperado || 1,\n media_total_esperado: fuente.media_total_esperado || fuente.media_total || 1\n }\n};" + "jsCode": "const geminiItems = $input.all();\nconst prepItems = $('Code - Preparar Gemini audio WhatsApp TEST').all();\n\nfunction clean(value) {\n return String(value ?? '').trim();\n}\n\nfunction nowIso() {\n return new Date().toISOString();\n}\n\nfunction safeJson(value) {\n try {\n return JSON.stringify(value);\n } catch (e) {\n return String(value ?? '');\n }\n}\n\nfunction extractTextFromGemini(obj) {\n if (!obj) return '';\n\n if (typeof obj === 'string') {\n return clean(obj);\n }\n\n if (typeof obj.text === 'string') return clean(obj.text);\n if (typeof obj.output === 'string') return clean(obj.output);\n if (typeof obj.response === 'string') return clean(obj.response);\n if (typeof obj.content === 'string') return clean(obj.content);\n\n const possibleParts =\n obj?.content?.parts ||\n obj?.candidates?.[0]?.content?.parts ||\n obj?.response?.candidates?.[0]?.content?.parts ||\n obj?.parts ||\n [];\n\n if (Array.isArray(possibleParts)) {\n const text = possibleParts\n .map(part => clean(part?.text))\n .filter(Boolean)\n .join('\\n');\n\n if (text) return text;\n }\n\n const jsonText = safeJson(obj);\n return jsonText && jsonText !== '{}' ? jsonText : '';\n}\n\nfunction detectGeminiError(obj) {\n if (!obj || typeof obj !== 'object') return '';\n\n const candidates = [\n obj.error,\n obj.message,\n obj.description,\n obj.fullMessage,\n obj.full_message,\n obj.errorMessage,\n obj.error_message,\n obj?.error?.message,\n obj?.error?.description,\n obj?.error?.cause,\n obj?.json?.error,\n obj?.json?.message,\n ];\n\n const found = candidates\n .map(clean)\n .filter(Boolean)\n .join(' | ');\n\n const serialized = safeJson(obj);\n\n if (\n found ||\n serialized.includes('Internal error') ||\n serialized.includes('INTERNAL') ||\n serialized.includes('500') ||\n serialized.includes('service was not able to process')\n ) {\n return found || serialized;\n }\n\n return '';\n}\n\nconst output = [];\n\nconst total = Math.max(geminiItems.length, prepItems.length);\n\nfor (let i = 0; i < total; i++) {\n const geminiItem = geminiItems[i] || {};\n const prepItem = prepItems[i] || {};\n\n const geminiJson = geminiItem.json || {};\n const mediaJson = prepItem.json || geminiJson || {};\n\n const sessionId = clean(mediaJson.session_id);\n const ejecucionId = clean(mediaJson.ejecucion_id || sessionId);\n const mediaEventId = clean(mediaJson.media_event_id || mediaJson.event_id);\n const mediaFileName = clean(mediaJson.media_file_name || mediaJson.file_name || mediaJson.nombre_archivo);\n const driveFileId = clean(mediaJson.drive_file_id || mediaJson.file_id);\n const driveFileUrl = clean(mediaJson.drive_file_url || mediaJson.media_drive_url || mediaJson.webViewLink);\n\n const errorGemini = detectGeminiError(geminiJson);\n\n let analisisTexto = '';\n let estado = '';\n let requiereRevision = false;\n\n if (errorGemini) {\n requiereRevision = true;\n estado = 'ANALISIS_AUDIO_ERROR_GEMINI';\n\n analisisTexto = [\n 'ERROR CONTROLADO DE ANÁLISIS DE AUDIO',\n '',\n 'Gemini no pudo procesar esta nota de voz.',\n 'No inventar datos provenientes del audio.',\n 'Usar únicamente las imágenes, videos y demás evidencias disponibles.',\n 'Marcar el reporte para revisión manual si el audio era necesario para identificar propuesta, cliente, marca, país, ubicación o contexto.',\n '',\n `Detalle técnico: ${errorGemini}`,\n ].join('\\n');\n } else {\n analisisTexto = extractTextFromGemini(geminiJson);\n\n if (!analisisTexto) {\n requiereRevision = true;\n estado = 'ANALISIS_AUDIO_VACIO';\n\n analisisTexto = [\n 'ERROR CONTROLADO DE ANÁLISIS DE AUDIO',\n '',\n 'Gemini respondió, pero no devolvió texto útil para esta nota de voz.',\n 'No inventar datos provenientes del audio.',\n 'Usar únicamente las imágenes, videos y demás evidencias disponibles.',\n 'Marcar el reporte para revisión manual si el audio era necesario para identificar la propuesta.',\n ].join('\\n');\n } else {\n estado = 'ANALISIS_AUDIO_COMPLETADO';\n }\n }\n\n if (!sessionId) {\n throw new Error('No llegó session_id al normalizar análisis de audio.');\n }\n\n if (!mediaEventId) {\n throw new Error('No llegó media_event_id al normalizar análisis de audio.');\n }\n\n output.push({\n json: {\n ...mediaJson,\n\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n media_event_id: mediaEventId,\n media_type: 'audio',\n media_file_name: mediaFileName,\n drive_file_id: driveFileId,\n drive_file_url: driveFileUrl,\n\n analisis_texto: analisisTexto,\n fecha_analisis: nowIso(),\n estado,\n\n gemini_audio_error: errorGemini || '',\n audio_requiere_revision: requiereRevision,\n\n media_index: Number(mediaJson.media_index || 0),\n media_total: Number(mediaJson.media_total || mediaJson.media_total_esperado || 0),\n },\n });\n}\n\nreturn output;" }, "type": "n8n-nodes-base.code", "typeVersion": 2, @@ -4207,9 +4209,9 @@ "operation": "analyze", "modelId": { "__rl": true, - "value": "models/gemini-3.1-flash-lite", + "value": "models/gemini-3.1-pro-preview", "mode": "list", - "cachedResultName": "models/gemini-3.1-flash-lite" + "cachedResultName": "models/gemini-3.1-pro-preview" }, "text": "={{ $json.gemini_prompt_imagen }}", "inputType": "binary", @@ -4225,8 +4227,8 @@ "name": "Gemini - Analizar imagen WhatsApp TEST", "credentials": { "googlePalmApi": { - "id": "6Jdcd9FONxG35YuJ", - "name": "Isaac - Gemini Api Gratis" + "id": "jvsXYwL6IOoY2DBU", + "name": "Isaac - Gemini Api Pago" } } }, @@ -4264,9 +4266,9 @@ "operation": "analyze", "modelId": { "__rl": true, - "value": "models/gemini-3.1-flash-lite", + "value": "models/gemini-3.1-pro-preview", "mode": "list", - "cachedResultName": "models/gemini-3.1-flash-lite" + "cachedResultName": "models/gemini-3.1-pro-preview" }, "text": "={{ $json.gemini_prompt_video }}", "inputType": "binary", @@ -4282,8 +4284,8 @@ "name": "Analyze video", "credentials": { "googlePalmApi": { - "id": "6Jdcd9FONxG35YuJ", - "name": "Isaac - Gemini Api Gratis" + "id": "jvsXYwL6IOoY2DBU", + "name": "Isaac - Gemini Api Pago" } } }, @@ -4496,9 +4498,9 @@ "parameters": { "modelId": { "__rl": true, - "value": "models/gemini-3.1-flash-lite", + "value": "models/gemini-3.1-pro-preview", "mode": "list", - "cachedResultName": "models/gemini-3.1-flash-lite" + "cachedResultName": "models/gemini-3.1-pro-preview" }, "messages": { "values": [ @@ -4520,8 +4522,8 @@ "name": "Gemini - Generar JSON final propuesta ejecutada TEST", "credentials": { "googlePalmApi": { - "id": "6Jdcd9FONxG35YuJ", - "name": "Isaac - Gemini Api Gratis" + "id": "jvsXYwL6IOoY2DBU", + "name": "Isaac - Gemini Api Pago" } } }, @@ -5253,9 +5255,7 @@ "name": "Code - Preparar mensaje final WhatsApp TEST" }, { - "parameters": { - "amount": 7 - }, + "parameters": {}, "type": "n8n-nodes-base.wait", "typeVersion": 1.1, "position": [ @@ -5349,7 +5349,7 @@ }, { "parameters": { - "jsCode": "const data = $('Code - Normalizar JSON final propuesta ejecutada TEST').first().json || {};\n\nconst templateId = '10PZtoMFuiXczGM9Itbcjce4EKBUhiztFbXZiTWX2DKc';\nconst folderId = '1q92b3lMw_fcjD1YUtp5YcJKne49zOfpc';\n\nconst limpiar = (valor) =>\n String(valor || '')\n .replace(/[\\\\/:*?\"<>|]/g, '-')\n .replace(/\\s+/g, ' ')\n .trim();\n\nconst marca = limpiar(data.marca || 'Marca no identificada');\nconst referencia = limpiar(data.propuesta_referencia || 'Propuesta ejecutada');\nconst fecha = limpiar(data.fecha_ejecucion || new Date().toISOString().slice(0, 10));\nconst sessionId = limpiar(data.session_id || data.ejecucion_id || Date.now());\n\nconst presentationName = `${fecha} - ${marca} - ${referencia} - ${sessionId}`;\n\nreturn [\n {\n json: {\n ...data,\n\n slides_template_id: templateId,\n slides_folder_id: folderId,\n presentation_name: presentationName,\n\n estado_presentacion: 'PRESENTACION_PREPARADA'\n }\n }\n];" + "jsCode": "const data = $('Code - Normalizar JSON final propuesta ejecutada TEST').first().json || {};\n\nconst templateId = '1gHgQBDL2uFaa7yNwcyWrtE24B_UrCU1Dy-n63TPurPE';\nconst folderId = '1q92b3lMw_fcjD1YUtp5YcJKne49zOfpc';\n\nconst limpiar = (valor) =>\n String(valor || '')\n .replace(/[\\\\/:*?\"<>|]/g, '-')\n .replace(/\\s+/g, ' ')\n .trim();\n\nconst marca = limpiar(data.marca || 'Marca no identificada');\nconst referencia = limpiar(data.propuesta_referencia || 'Propuesta ejecutada');\nconst fecha = limpiar(data.fecha_ejecucion || new Date().toISOString().slice(0, 10));\nconst sessionId = limpiar(data.session_id || data.ejecucion_id || Date.now());\n\nconst presentationName = `${fecha} - ${marca} - ${referencia} - ${sessionId}`;\n\nreturn [\n {\n json: {\n ...data,\n\n slides_template_id: templateId,\n slides_folder_id: folderId,\n presentation_name: presentationName,\n\n estado_presentacion: 'PRESENTACION_PREPARADA'\n }\n }\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, @@ -5814,7 +5814,7 @@ }, { "parameters": { - "jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst jsonFinal = getNodeJson('Code - Normalizar JSON final propuesta ejecutada TEST');\nconst match = getNodeJson('Code - Match propuesta banco Fulgencio TEST');\nconst enriquecido = getNodeJson('Code - Enriquecer datos con match banco TEST');\n\nconst carpetaPreparada = getNodeJson('Code - Preparar carpeta ejecución TEST');\nconst carpetaDrive = getNodeJson('Drive - Crear carpeta ejecución TEST');\nconst carpetaNormalizada = getNodeJson('Code - Normalizar carpeta ejecución TEST');\n\nconst copiaPresentacion = getNodeJson('Drive - Copiar plantilla presentación ejecución TEST');\nconst linkPresentacionNormalizado = getNodeJson('Code - Normalizar link presentación ejecución TEST');\n\nconst data = {\n ...jsonFinal,\n ...carpetaPreparada,\n ...carpetaDrive,\n ...carpetaNormalizada,\n ...copiaPresentacion,\n ...linkPresentacionNormalizado,\n ...actual,\n ...match,\n ...enriquecido\n};\n\nconst limpiarTexto = (valor, fallback = 'No identificado') => {\n const texto = limpiar(valor);\n\n if (!texto) return fallback;\n if (texto.toLowerCase() === 'undefined') return fallback;\n if (texto.toLowerCase() === 'null') return fallback;\n if (texto.toLowerCase() === 'no disponible') return fallback;\n\n return texto;\n};\n\nconst cortar = (valor, max = 900) => {\n const texto = limpiarTexto(valor, 'No disponible');\n return texto.length > max ? texto.slice(0, max - 3) + '...' : texto;\n};\n\nconst normalizarPlano = (valor) => {\n return limpiar(valor)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, ' ')\n .trim();\n};\n\nconst limpiarReferenciaVisual = (valor) => {\n let texto = limpiar(valor);\n\n if (!texto) return 'No identificado';\n\n texto = texto\n .replace(/_/g, ' ')\n .replace(/\\s+/g, ' ')\n .replace(/\\bQUATE\\b/gi, 'GUATE')\n .replace(/\\bQUATEWMC\\b/gi, 'GUATE WMC')\n .replace(/\\bGUATEWMC\\b/gi, 'GUATE WMC')\n .replace(/(\\d{4})(GUATE|QUATE|WMC)/gi, '$1 $2')\n .replace(/(\\d)(WMC)/gi, '$1 $2')\n .replace(/\\s+/g, ' ')\n .trim();\n\n return texto || 'No identificado';\n};\n\nconst matchEstado = normalizarPlano(\n match.propuesta_match_estado ||\n data.propuesta_match_estado ||\n ''\n);\n\nconst matchConfianza = Number(\n match.propuesta_match_confianza ||\n data.propuesta_match_confianza ||\n 0\n);\n\nconst matchAlta =\n matchEstado === 'MATCH_ALTA_CONFIANZA' ||\n matchConfianza >= 85;\n\nconst propuestaNombreBanco = limpiar(\n match.propuesta_nombre_banco ||\n data.propuesta_nombre_banco ||\n ''\n);\n\nconst propuestaReferenciaEnriquecida = limpiar(\n enriquecido.propuesta_referencia ||\n data.propuesta_referencia ||\n jsonFinal.propuesta_referencia ||\n ''\n);\n\nconst propuestaReferenciaSlides = limpiarReferenciaVisual(\n matchAlta && propuestaNombreBanco\n ? propuestaNombreBanco\n : propuestaReferenciaEnriquecida\n);\n\nconst ubicacionSlides = limpiarTexto(\n enriquecido.ubicacion ||\n data.ubicacion ||\n jsonFinal.ubicacion,\n 'No identificado'\n);\n\nconst fechaEjecucionSlides = limpiarTexto(\n enriquecido.fecha_ejecucion ||\n data.fecha_ejecucion ||\n jsonFinal.fecha_ejecucion,\n 'No identificado'\n);\n\n// --------------------------------------------------\n// Resolver carpeta de evidencias\n// --------------------------------------------------\n\nconst mediaFolderId = limpiar(\n data.media_folder_id ||\n data.execution_folder_id ||\n data.folder_id ||\n data.id ||\n carpetaNormalizada.media_folder_id ||\n carpetaNormalizada.execution_folder_id ||\n carpetaNormalizada.folder_id ||\n carpetaDrive.id ||\n ''\n);\n\nlet mediaFolderUrl = limpiar(\n data.media_folder_url ||\n data.execution_folder_url ||\n data.folder_url ||\n carpetaNormalizada.media_folder_url ||\n carpetaNormalizada.execution_folder_url ||\n carpetaNormalizada.folder_url ||\n carpetaDrive.webViewLink ||\n ''\n);\n\nif (!mediaFolderUrl && mediaFolderId) {\n mediaFolderUrl = `https://drive.google.com/drive/folders/${mediaFolderId}`;\n}\n\n// --------------------------------------------------\n// Resolver link de presentación\n// --------------------------------------------------\n\nconst presentationId = limpiar(\n data.presentation_id ||\n data.presentacion_id ||\n linkPresentacionNormalizado.presentation_id ||\n copiaPresentacion.id ||\n actual.presentation_id ||\n ''\n);\n\nlet presentationUrl = limpiar(\n data.presentacion_ejecucion_url ||\n data.presentation_url ||\n data.presentationUrl ||\n data.link_presentacion ||\n linkPresentacionNormalizado.presentacion_ejecucion_url ||\n linkPresentacionNormalizado.presentation_url ||\n linkPresentacionNormalizado.webViewLink ||\n copiaPresentacion.webViewLink ||\n actual.presentacion_ejecucion_url ||\n actual.presentation_url ||\n actual.webViewLink ||\n ''\n);\n\nif (!presentationUrl && presentationId) {\n presentationUrl = `https://docs.google.com/presentation/d/${presentationId}/edit`;\n}\n\nif (!presentationId) {\n throw new Error('No llegó presentation_id para reemplazar textos en Slides.');\n}\n\nconst replacements = {\n '{{MARCA}}': limpiarTexto(data.marca || jsonFinal.marca),\n '{{CLIENTE}}': limpiarTexto(data.cliente || jsonFinal.cliente),\n '{{PAIS}}': limpiarTexto(data.pais || jsonFinal.pais),\n '{{UBICACION}}': ubicacionSlides,\n\n '{{FECHA_EJECUCION}}': fechaEjecucionSlides,\n '{{PROPUESTA_REFERENCIA}}': propuestaReferenciaSlides,\n\n '{{EJECUCION_ID}}': '',\n '{{SESSION_ID}}': '',\n\n '{{MANAGER_NOMBRE}}': limpiarTexto(data.manager_nombre || jsonFinal.manager_nombre),\n '{{MANAGER_TELEFONO}}': limpiarTexto(data.manager_telefono || jsonFinal.manager_telefono),\n\n '{{RESUMEN_IA}}': cortar(data.resumen_ia || jsonFinal.resumen_ia, 1000),\n '{{DESCRIPCION_EJECUCION}}': cortar(data.descripcion_ejecucion || jsonFinal.descripcion_ejecucion, 1000),\n '{{ELEMENTOS_DETECTADOS}}': cortar(data.elementos_detectados || jsonFinal.elementos_detectados, 1000),\n '{{TAGS}}': cortar(data.tags || jsonFinal.tags, 500),\n\n '{{FOTOS_COUNT}}': limpiarTexto(data.fotos_count ?? jsonFinal.fotos_count, '0'),\n '{{VIDEOS_COUNT}}': limpiarTexto(data.videos_count ?? jsonFinal.videos_count, '0'),\n '{{AUDIOS_COUNT}}': limpiarTexto(data.audios_count ?? jsonFinal.audios_count, '0'),\n\n '{{MEDIA_FOLDER_URL}}': limpiarTexto(mediaFolderUrl, 'No disponible'),\n '{{LINK_PRESENTACION}}': limpiarTexto(presentationUrl, 'No disponible')\n};\n\nconst slidesReplaceRequests = Object.entries(replacements).map(([placeholder, value]) => {\n return {\n replaceAllText: {\n containsText: {\n text: placeholder,\n matchCase: true\n },\n replaceText: value\n }\n };\n});\n\nreturn [\n {\n json: {\n ...data,\n\n presentation_id: presentationId,\n\n media_folder_id: mediaFolderId,\n media_folder_url: mediaFolderUrl,\n execution_folder_id: mediaFolderId,\n execution_folder_url: mediaFolderUrl,\n\n presentacion_ejecucion_url: presentationUrl,\n presentation_url: presentationUrl,\n\n propuesta_referencia: propuestaReferenciaSlides,\n ubicacion: ubicacionSlides,\n fecha_ejecucion: fechaEjecucionSlides,\n\n slides_replacements: replacements,\n slides_replace_requests: slidesReplaceRequests,\n\n estado_presentacion: 'REEMPLAZOS_SLIDES_PREPARADOS',\n\n slides_replacements_debug: {\n match_estado: matchEstado,\n match_confianza: matchConfianza,\n match_alta: matchAlta,\n\n propuesta_nombre_banco: propuestaNombreBanco,\n propuesta_referencia_enriquecida: propuestaReferenciaEnriquecida,\n propuesta_referencia_slides: propuestaReferenciaSlides,\n\n media_folder_id: mediaFolderId,\n media_folder_url: mediaFolderUrl,\n\n presentation_id: presentationId,\n presentation_url: presentationUrl,\n\n fuentes_links: {\n data_media_folder_url: data.media_folder_url || '',\n data_execution_folder_url: data.execution_folder_url || '',\n carpeta_normalizada_media_folder_url: carpetaNormalizada.media_folder_url || '',\n carpeta_normalizada_execution_folder_url: carpetaNormalizada.execution_folder_url || '',\n carpeta_drive_webViewLink: carpetaDrive.webViewLink || '',\n\n data_presentacion_ejecucion_url: data.presentacion_ejecucion_url || '',\n data_presentation_url: data.presentation_url || '',\n link_normalizado_presentacion: linkPresentacionNormalizado.presentacion_ejecucion_url || linkPresentacionNormalizado.presentation_url || '',\n copia_presentacion_webViewLink: copiaPresentacion.webViewLink || ''\n }\n }\n }\n }\n];" + "jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst jsonFinal = getNodeJson('Code - Normalizar JSON final propuesta ejecutada TEST');\nconst match = getNodeJson('Code - Match propuesta banco Fulgencio TEST');\nconst enriquecido = getNodeJson('Code - Enriquecer datos con match banco TEST');\n\nconst carpetaPreparada = getNodeJson('Code - Preparar carpeta ejecución TEST');\nconst carpetaDrive = getNodeJson('Drive - Crear carpeta ejecución TEST');\nconst carpetaNormalizada = getNodeJson('Code - Normalizar carpeta ejecución TEST');\n\nconst copiaPresentacion = getNodeJson('Drive - Copiar plantilla presentación ejecución TEST');\nconst linkPresentacionNormalizado = getNodeJson('Code - Normalizar link presentación ejecución TEST');\n\nconst data = {\n ...jsonFinal,\n ...carpetaPreparada,\n ...carpetaDrive,\n ...carpetaNormalizada,\n ...copiaPresentacion,\n ...linkPresentacionNormalizado,\n ...actual,\n ...match,\n ...enriquecido\n};\n\nconst limpiarTexto = (valor, fallback = 'No identificado') => {\n const texto = limpiar(valor);\n\n if (!texto) return fallback;\n if (texto.toLowerCase() === 'undefined') return fallback;\n if (texto.toLowerCase() === 'null') return fallback;\n if (texto.toLowerCase() === 'no disponible') return fallback;\n\n return texto;\n};\n\nconst cortar = (valor, max = 900) => {\n const texto = limpiarTexto(valor, 'No disponible');\n return texto.length > max ? texto.slice(0, max - 3) + '...' : texto;\n};\n\nconst normalizarPlano = (valor) => {\n return limpiar(valor)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, ' ')\n .trim();\n};\n\nconst limpiarReferenciaVisual = (valor) => {\n let texto = limpiar(valor);\n\n if (!texto) return 'No identificado';\n\n texto = texto\n .replace(/_/g, ' ')\n .replace(/\\s+/g, ' ')\n .replace(/\\bQUATE\\b/gi, 'GUATE')\n .replace(/\\bQUATEWMC\\b/gi, 'GUATE WMC')\n .replace(/\\bGUATEWMC\\b/gi, 'GUATE WMC')\n .replace(/(\\d{4})(GUATE|QUATE|WMC)/gi, '$1 $2')\n .replace(/(\\d)(WMC)/gi, '$1 $2')\n .replace(/\\s+/g, ' ')\n .trim();\n\n return texto || 'No identificado';\n};\n\nconst matchEstado = normalizarPlano(\n match.propuesta_match_estado ||\n data.propuesta_match_estado ||\n ''\n);\n\nconst matchConfianza = Number(\n match.propuesta_match_confianza ||\n data.propuesta_match_confianza ||\n 0\n);\n\nconst matchAlta =\n matchEstado === 'MATCH_ALTA_CONFIANZA' ||\n matchConfianza >= 85;\n\nconst propuestaNombreBanco = limpiar(\n match.propuesta_nombre_banco ||\n data.propuesta_nombre_banco ||\n ''\n);\n\nconst propuestaReferenciaEnriquecida = limpiar(\n enriquecido.propuesta_referencia ||\n data.propuesta_referencia ||\n jsonFinal.propuesta_referencia ||\n ''\n);\n\nconst propuestaReferenciaSlides = limpiarReferenciaVisual(\n matchAlta && propuestaNombreBanco\n ? propuestaNombreBanco\n : propuestaReferenciaEnriquecida\n);\n\nconst ubicacionSlides = limpiarTexto(\n enriquecido.ubicacion ||\n data.ubicacion ||\n jsonFinal.ubicacion,\n 'No identificado'\n);\n\nconst fechaEjecucionSlides = limpiarTexto(\n enriquecido.fecha_ejecucion ||\n data.fecha_ejecucion ||\n jsonFinal.fecha_ejecucion,\n 'No identificado'\n);\n\n// --------------------------------------------------\n// Resolver carpeta de evidencias\n// --------------------------------------------------\n\nconst mediaFolderId = limpiar(\n data.media_folder_id ||\n data.execution_folder_id ||\n data.folder_id ||\n data.id ||\n carpetaNormalizada.media_folder_id ||\n carpetaNormalizada.execution_folder_id ||\n carpetaNormalizada.folder_id ||\n carpetaDrive.id ||\n ''\n);\n\nlet mediaFolderUrl = limpiar(\n data.media_folder_url ||\n data.execution_folder_url ||\n data.folder_url ||\n carpetaNormalizada.media_folder_url ||\n carpetaNormalizada.execution_folder_url ||\n carpetaNormalizada.folder_url ||\n carpetaDrive.webViewLink ||\n ''\n);\n\nif (!mediaFolderUrl && mediaFolderId) {\n mediaFolderUrl = `https://drive.google.com/drive/folders/${mediaFolderId}`;\n}\n\n// --------------------------------------------------\n// Resolver link de presentación\n// --------------------------------------------------\n\nconst presentationId = limpiar(\n data.presentation_id ||\n data.presentacion_id ||\n linkPresentacionNormalizado.presentation_id ||\n copiaPresentacion.id ||\n actual.presentation_id ||\n ''\n);\n\nlet presentationUrl = limpiar(\n data.presentacion_ejecucion_url ||\n data.presentation_url ||\n data.presentationUrl ||\n data.link_presentacion ||\n linkPresentacionNormalizado.presentacion_ejecucion_url ||\n linkPresentacionNormalizado.presentation_url ||\n linkPresentacionNormalizado.webViewLink ||\n copiaPresentacion.webViewLink ||\n actual.presentacion_ejecucion_url ||\n actual.presentation_url ||\n actual.webViewLink ||\n ''\n);\n\nif (!presentationUrl && presentationId) {\n presentationUrl = `https://docs.google.com/presentation/d/${presentationId}/edit`;\n}\n\nif (!presentationId) {\n throw new Error('No llegó presentation_id para reemplazar textos en Slides.');\n}\n\nconst objetivoSlides = cortar(\n data.objetivo ||\n jsonFinal.objetivo ||\n data.descripcion_ejecucion ||\n jsonFinal.descripcion_ejecucion ||\n data.resumen_ia ||\n jsonFinal.resumen_ia ||\n 'No disponible',\n 900\n);\n\nconst replacements = {\n '{{MARCA}}': limpiarTexto(data.marca || jsonFinal.marca),\n '{{CLIENTE}}': limpiarTexto(data.cliente || jsonFinal.cliente),\n '{{PAIS}}': limpiarTexto(data.pais || jsonFinal.pais),\n '{{UBICACION}}': ubicacionSlides,\n\n '{{FECHA_EJECUCION}}': fechaEjecucionSlides,\n '{{PROPUESTA_REFERENCIA}}': propuestaReferenciaSlides,\n '{{OBJETIVO}}': objetivoSlides,\n\n '{{EJECUCION_ID}}': '',\n '{{SESSION_ID}}': '',\n\n '{{MANAGER_NOMBRE}}': limpiarTexto(data.manager_nombre || jsonFinal.manager_nombre),\n '{{MANAGER_TELEFONO}}': limpiarTexto(data.manager_telefono || jsonFinal.manager_telefono),\n\n '{{RESUMEN_IA}}': cortar(data.resumen_ia || jsonFinal.resumen_ia, 1000),\n '{{DESCRIPCION_EJECUCION}}': cortar(data.descripcion_ejecucion || jsonFinal.descripcion_ejecucion, 1000),\n '{{ELEMENTOS_DETECTADOS}}': cortar(data.elementos_detectados || jsonFinal.elementos_detectados, 1000),\n '{{TAGS}}': cortar(data.tags || jsonFinal.tags, 500),\n\n '{{FOTOS_COUNT}}': limpiarTexto(data.fotos_count ?? jsonFinal.fotos_count, '0'),\n '{{VIDEOS_COUNT}}': limpiarTexto(data.videos_count ?? jsonFinal.videos_count, '0'),\n '{{AUDIOS_COUNT}}': limpiarTexto(data.audios_count ?? jsonFinal.audios_count, '0'),\n\n '{{MEDIA_FOLDER_URL}}': limpiarTexto(mediaFolderUrl, 'No disponible'),\n '{{LINK_PRESENTACION}}': limpiarTexto(presentationUrl, 'No disponible')\n};\n\nconst slidesReplaceRequests = Object.entries(replacements).map(([placeholder, value]) => {\n return {\n replaceAllText: {\n containsText: {\n text: placeholder,\n matchCase: true\n },\n replaceText: value\n }\n };\n});\n\nreturn [\n {\n json: {\n ...data,\n\n presentation_id: presentationId,\n\n media_folder_id: mediaFolderId,\n media_folder_url: mediaFolderUrl,\n execution_folder_id: mediaFolderId,\n execution_folder_url: mediaFolderUrl,\n\n presentacion_ejecucion_url: presentationUrl,\n presentation_url: presentationUrl,\n\n propuesta_referencia: propuestaReferenciaSlides,\n ubicacion: ubicacionSlides,\n fecha_ejecucion: fechaEjecucionSlides,\n\n slides_replacements: replacements,\n slides_replace_requests: slidesReplaceRequests,\n\n estado_presentacion: 'REEMPLAZOS_SLIDES_PREPARADOS',\n\n slides_replacements_debug: {\n match_estado: matchEstado,\n match_confianza: matchConfianza,\n match_alta: matchAlta,\n\n propuesta_nombre_banco: propuestaNombreBanco,\n propuesta_referencia_enriquecida: propuestaReferenciaEnriquecida,\n propuesta_referencia_slides: propuestaReferenciaSlides,\n\n media_folder_id: mediaFolderId,\n media_folder_url: mediaFolderUrl,\n\n presentation_id: presentationId,\n presentation_url: presentationUrl,\n\n fuentes_links: {\n data_media_folder_url: data.media_folder_url || '',\n data_execution_folder_url: data.execution_folder_url || '',\n carpeta_normalizada_media_folder_url: carpetaNormalizada.media_folder_url || '',\n carpeta_normalizada_execution_folder_url: carpetaNormalizada.execution_folder_url || '',\n carpeta_drive_webViewLink: carpetaDrive.webViewLink || '',\n\n data_presentacion_ejecucion_url: data.presentacion_ejecucion_url || '',\n data_presentation_url: data.presentation_url || '',\n link_normalizado_presentacion: linkPresentacionNormalizado.presentacion_ejecucion_url || linkPresentacionNormalizado.presentation_url || '',\n copia_presentacion_webViewLink: copiaPresentacion.webViewLink || ''\n }\n }\n }\n }\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, @@ -8043,7 +8043,7 @@ }, { "parameters": { - "amount": 10 + "amount": 4 }, "type": "n8n-nodes-base.wait", "typeVersion": 1.1, @@ -8195,7 +8195,7 @@ }, { "parameters": { - "amount": 8 + "amount": 4 }, "type": "n8n-nodes-base.wait", "typeVersion": 1.1, @@ -8341,12 +8341,12 @@ "parameters": { "content": "## 📥 ENTRADA WHATSAPP Y GRUPO OFICIAL\n\nEste bloque recibe eventos desde Evolution API y normaliza el mensaje recibido.\n\nResponsabilidades:\n- Detectar si viene de grupo o chat individual.\n- Identificar sender real en grupo.\n- Resolver whatsapp_to para responder al grupo correcto.\n- Detectar texto, audio, imagen, video o documento.\n- Normalizar comandos oficiales.\n- Validar que el mensaje venga de un grupo permitido.\n\nRegla principal:\nsolo se procesa si el grupo está autorizado.", "height": 528, - "width": 832 + "width": 848 }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ - -9552, + -9568, -1344 ], "id": "d86e235c-45ef-48c5-a816-6c32f33fd9ae", @@ -10087,9 +10087,9 @@ "meta": { "templateCredsSetupCompleted": true }, - "versionId": "a72fa72c-6f91-4fca-9982-2b2438e339eb", - "activeVersionId": "a72fa72c-6f91-4fca-9982-2b2438e339eb", - "versionCounter": 175, + "versionId": "135d4b16-b87e-4b14-8234-57a60538586e", + "activeVersionId": "135d4b16-b87e-4b14-8234-57a60538586e", + "versionCounter": 218, "triggerCount": 1, "shared": [ { @@ -10112,9 +10112,9 @@ ], "tags": [], "activeVersion": { - "updatedAt": "2026-06-25T19:31:03.000Z", - "createdAt": "2026-06-25T19:30:58.490Z", - "versionId": "a72fa72c-6f91-4fca-9982-2b2438e339eb", + "updatedAt": "2026-07-04T22:34:01.000Z", + "createdAt": "2026-07-04T22:33:50.475Z", + "versionId": "135d4b16-b87e-4b14-8234-57a60538586e", "workflowId": "CdosDIWBgnDMnPdr", "nodes": [ { @@ -14260,9 +14260,9 @@ "operation": "analyze", "modelId": { "__rl": true, - "value": "models/gemini-3.1-flash-lite", + "value": "models/gemini-3.1-pro-preview", "mode": "list", - "cachedResultName": "models/gemini-3.1-flash-lite" + "cachedResultName": "models/gemini-3.1-pro-preview" }, "text": "={{ $json.gemini_prompt_audio }}", "inputType": "binary", @@ -14276,17 +14276,19 @@ ], "id": "353aba0e-8ed5-484a-b8fc-65ee7829a951", "name": "Gemini - Analizar audio WhatsApp TEST", + "retryOnFail": true, + "waitBetweenTries": 5000, "credentials": { "googlePalmApi": { - "id": "6Jdcd9FONxG35YuJ", - "name": "Isaac - Gemini Api Gratis" + "id": "jvsXYwL6IOoY2DBU", + "name": "Isaac - Gemini Api Pago" } - } + }, + "onError": "continueRegularOutput" }, { "parameters": { - "mode": "runOnceForEachItem", - "jsCode": "const gemini = $input.item.json || {};\n\nlet fuente = {};\n\ntry {\n fuente = $('Code - Preparar Gemini audio WhatsApp TEST').item.json || {};\n} catch (error) {\n fuente = {};\n}\n\nconst extraerTexto = (obj) => {\n if (!obj || typeof obj !== 'object') return '';\n\n if (typeof obj.text === 'string') return obj.text;\n if (typeof obj.output === 'string') return obj.output;\n if (typeof obj.response === 'string') return obj.response;\n if (typeof obj.content === 'string') return obj.content;\n\n if (obj.content?.parts && Array.isArray(obj.content.parts)) {\n return obj.content.parts\n .map((p) => p?.text || '')\n .filter(Boolean)\n .join('\\n');\n }\n\n if (Array.isArray(obj.parts)) {\n return obj.parts\n .map((p) => p?.text || '')\n .filter(Boolean)\n .join('\\n');\n }\n\n if (Array.isArray(obj.candidates)) {\n return JSON.stringify(obj.candidates);\n }\n\n return JSON.stringify(obj);\n};\n\nconst textoAnalisis = extraerTexto(gemini);\n\nreturn {\n json: {\n ...fuente,\n\n session_id: fuente.session_id || '',\n ejecucion_id: fuente.ejecucion_id || '',\n media_event_id: fuente.media_event_id || '',\n media_type: 'audio',\n media_file_name: fuente.media_file_name || '',\n\n analisis_texto: textoAnalisis,\n fecha_analisis: new Date().toISOString(),\n estado: 'ANALISIS_AUDIO_COMPLETADO',\n\n media_index: fuente.media_index || 1,\n media_total: fuente.media_total || fuente.media_total_esperado || 1,\n media_total_esperado: fuente.media_total_esperado || fuente.media_total || 1\n }\n};" + "jsCode": "const geminiItems = $input.all();\nconst prepItems = $('Code - Preparar Gemini audio WhatsApp TEST').all();\n\nfunction clean(value) {\n return String(value ?? '').trim();\n}\n\nfunction nowIso() {\n return new Date().toISOString();\n}\n\nfunction safeJson(value) {\n try {\n return JSON.stringify(value);\n } catch (e) {\n return String(value ?? '');\n }\n}\n\nfunction extractTextFromGemini(obj) {\n if (!obj) return '';\n\n if (typeof obj === 'string') {\n return clean(obj);\n }\n\n if (typeof obj.text === 'string') return clean(obj.text);\n if (typeof obj.output === 'string') return clean(obj.output);\n if (typeof obj.response === 'string') return clean(obj.response);\n if (typeof obj.content === 'string') return clean(obj.content);\n\n const possibleParts =\n obj?.content?.parts ||\n obj?.candidates?.[0]?.content?.parts ||\n obj?.response?.candidates?.[0]?.content?.parts ||\n obj?.parts ||\n [];\n\n if (Array.isArray(possibleParts)) {\n const text = possibleParts\n .map(part => clean(part?.text))\n .filter(Boolean)\n .join('\\n');\n\n if (text) return text;\n }\n\n const jsonText = safeJson(obj);\n return jsonText && jsonText !== '{}' ? jsonText : '';\n}\n\nfunction detectGeminiError(obj) {\n if (!obj || typeof obj !== 'object') return '';\n\n const candidates = [\n obj.error,\n obj.message,\n obj.description,\n obj.fullMessage,\n obj.full_message,\n obj.errorMessage,\n obj.error_message,\n obj?.error?.message,\n obj?.error?.description,\n obj?.error?.cause,\n obj?.json?.error,\n obj?.json?.message,\n ];\n\n const found = candidates\n .map(clean)\n .filter(Boolean)\n .join(' | ');\n\n const serialized = safeJson(obj);\n\n if (\n found ||\n serialized.includes('Internal error') ||\n serialized.includes('INTERNAL') ||\n serialized.includes('500') ||\n serialized.includes('service was not able to process')\n ) {\n return found || serialized;\n }\n\n return '';\n}\n\nconst output = [];\n\nconst total = Math.max(geminiItems.length, prepItems.length);\n\nfor (let i = 0; i < total; i++) {\n const geminiItem = geminiItems[i] || {};\n const prepItem = prepItems[i] || {};\n\n const geminiJson = geminiItem.json || {};\n const mediaJson = prepItem.json || geminiJson || {};\n\n const sessionId = clean(mediaJson.session_id);\n const ejecucionId = clean(mediaJson.ejecucion_id || sessionId);\n const mediaEventId = clean(mediaJson.media_event_id || mediaJson.event_id);\n const mediaFileName = clean(mediaJson.media_file_name || mediaJson.file_name || mediaJson.nombre_archivo);\n const driveFileId = clean(mediaJson.drive_file_id || mediaJson.file_id);\n const driveFileUrl = clean(mediaJson.drive_file_url || mediaJson.media_drive_url || mediaJson.webViewLink);\n\n const errorGemini = detectGeminiError(geminiJson);\n\n let analisisTexto = '';\n let estado = '';\n let requiereRevision = false;\n\n if (errorGemini) {\n requiereRevision = true;\n estado = 'ANALISIS_AUDIO_ERROR_GEMINI';\n\n analisisTexto = [\n 'ERROR CONTROLADO DE ANÁLISIS DE AUDIO',\n '',\n 'Gemini no pudo procesar esta nota de voz.',\n 'No inventar datos provenientes del audio.',\n 'Usar únicamente las imágenes, videos y demás evidencias disponibles.',\n 'Marcar el reporte para revisión manual si el audio era necesario para identificar propuesta, cliente, marca, país, ubicación o contexto.',\n '',\n `Detalle técnico: ${errorGemini}`,\n ].join('\\n');\n } else {\n analisisTexto = extractTextFromGemini(geminiJson);\n\n if (!analisisTexto) {\n requiereRevision = true;\n estado = 'ANALISIS_AUDIO_VACIO';\n\n analisisTexto = [\n 'ERROR CONTROLADO DE ANÁLISIS DE AUDIO',\n '',\n 'Gemini respondió, pero no devolvió texto útil para esta nota de voz.',\n 'No inventar datos provenientes del audio.',\n 'Usar únicamente las imágenes, videos y demás evidencias disponibles.',\n 'Marcar el reporte para revisión manual si el audio era necesario para identificar la propuesta.',\n ].join('\\n');\n } else {\n estado = 'ANALISIS_AUDIO_COMPLETADO';\n }\n }\n\n if (!sessionId) {\n throw new Error('No llegó session_id al normalizar análisis de audio.');\n }\n\n if (!mediaEventId) {\n throw new Error('No llegó media_event_id al normalizar análisis de audio.');\n }\n\n output.push({\n json: {\n ...mediaJson,\n\n session_id: sessionId,\n ejecucion_id: ejecucionId,\n media_event_id: mediaEventId,\n media_type: 'audio',\n media_file_name: mediaFileName,\n drive_file_id: driveFileId,\n drive_file_url: driveFileUrl,\n\n analisis_texto: analisisTexto,\n fecha_analisis: nowIso(),\n estado,\n\n gemini_audio_error: errorGemini || '',\n audio_requiere_revision: requiereRevision,\n\n media_index: Number(mediaJson.media_index || 0),\n media_total: Number(mediaJson.media_total || mediaJson.media_total_esperado || 0),\n },\n });\n}\n\nreturn output;" }, "type": "n8n-nodes-base.code", "typeVersion": 2, @@ -14317,9 +14319,9 @@ "operation": "analyze", "modelId": { "__rl": true, - "value": "models/gemini-3.1-flash-lite", + "value": "models/gemini-3.1-pro-preview", "mode": "list", - "cachedResultName": "models/gemini-3.1-flash-lite" + "cachedResultName": "models/gemini-3.1-pro-preview" }, "text": "={{ $json.gemini_prompt_imagen }}", "inputType": "binary", @@ -14335,8 +14337,8 @@ "name": "Gemini - Analizar imagen WhatsApp TEST", "credentials": { "googlePalmApi": { - "id": "6Jdcd9FONxG35YuJ", - "name": "Isaac - Gemini Api Gratis" + "id": "jvsXYwL6IOoY2DBU", + "name": "Isaac - Gemini Api Pago" } } }, @@ -14374,9 +14376,9 @@ "operation": "analyze", "modelId": { "__rl": true, - "value": "models/gemini-3.1-flash-lite", + "value": "models/gemini-3.1-pro-preview", "mode": "list", - "cachedResultName": "models/gemini-3.1-flash-lite" + "cachedResultName": "models/gemini-3.1-pro-preview" }, "text": "={{ $json.gemini_prompt_video }}", "inputType": "binary", @@ -14392,8 +14394,8 @@ "name": "Analyze video", "credentials": { "googlePalmApi": { - "id": "6Jdcd9FONxG35YuJ", - "name": "Isaac - Gemini Api Gratis" + "id": "jvsXYwL6IOoY2DBU", + "name": "Isaac - Gemini Api Pago" } } }, @@ -14606,9 +14608,9 @@ "parameters": { "modelId": { "__rl": true, - "value": "models/gemini-3.1-flash-lite", + "value": "models/gemini-3.1-pro-preview", "mode": "list", - "cachedResultName": "models/gemini-3.1-flash-lite" + "cachedResultName": "models/gemini-3.1-pro-preview" }, "messages": { "values": [ @@ -14630,8 +14632,8 @@ "name": "Gemini - Generar JSON final propuesta ejecutada TEST", "credentials": { "googlePalmApi": { - "id": "6Jdcd9FONxG35YuJ", - "name": "Isaac - Gemini Api Gratis" + "id": "jvsXYwL6IOoY2DBU", + "name": "Isaac - Gemini Api Pago" } } }, @@ -15363,9 +15365,7 @@ "name": "Code - Preparar mensaje final WhatsApp TEST" }, { - "parameters": { - "amount": 7 - }, + "parameters": {}, "type": "n8n-nodes-base.wait", "typeVersion": 1.1, "position": [ @@ -15459,7 +15459,7 @@ }, { "parameters": { - "jsCode": "const data = $('Code - Normalizar JSON final propuesta ejecutada TEST').first().json || {};\n\nconst templateId = '10PZtoMFuiXczGM9Itbcjce4EKBUhiztFbXZiTWX2DKc';\nconst folderId = '1q92b3lMw_fcjD1YUtp5YcJKne49zOfpc';\n\nconst limpiar = (valor) =>\n String(valor || '')\n .replace(/[\\\\/:*?\"<>|]/g, '-')\n .replace(/\\s+/g, ' ')\n .trim();\n\nconst marca = limpiar(data.marca || 'Marca no identificada');\nconst referencia = limpiar(data.propuesta_referencia || 'Propuesta ejecutada');\nconst fecha = limpiar(data.fecha_ejecucion || new Date().toISOString().slice(0, 10));\nconst sessionId = limpiar(data.session_id || data.ejecucion_id || Date.now());\n\nconst presentationName = `${fecha} - ${marca} - ${referencia} - ${sessionId}`;\n\nreturn [\n {\n json: {\n ...data,\n\n slides_template_id: templateId,\n slides_folder_id: folderId,\n presentation_name: presentationName,\n\n estado_presentacion: 'PRESENTACION_PREPARADA'\n }\n }\n];" + "jsCode": "const data = $('Code - Normalizar JSON final propuesta ejecutada TEST').first().json || {};\n\nconst templateId = '1gHgQBDL2uFaa7yNwcyWrtE24B_UrCU1Dy-n63TPurPE';\nconst folderId = '1q92b3lMw_fcjD1YUtp5YcJKne49zOfpc';\n\nconst limpiar = (valor) =>\n String(valor || '')\n .replace(/[\\\\/:*?\"<>|]/g, '-')\n .replace(/\\s+/g, ' ')\n .trim();\n\nconst marca = limpiar(data.marca || 'Marca no identificada');\nconst referencia = limpiar(data.propuesta_referencia || 'Propuesta ejecutada');\nconst fecha = limpiar(data.fecha_ejecucion || new Date().toISOString().slice(0, 10));\nconst sessionId = limpiar(data.session_id || data.ejecucion_id || Date.now());\n\nconst presentationName = `${fecha} - ${marca} - ${referencia} - ${sessionId}`;\n\nreturn [\n {\n json: {\n ...data,\n\n slides_template_id: templateId,\n slides_folder_id: folderId,\n presentation_name: presentationName,\n\n estado_presentacion: 'PRESENTACION_PREPARADA'\n }\n }\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, @@ -15924,7 +15924,7 @@ }, { "parameters": { - "jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst jsonFinal = getNodeJson('Code - Normalizar JSON final propuesta ejecutada TEST');\nconst match = getNodeJson('Code - Match propuesta banco Fulgencio TEST');\nconst enriquecido = getNodeJson('Code - Enriquecer datos con match banco TEST');\n\nconst carpetaPreparada = getNodeJson('Code - Preparar carpeta ejecución TEST');\nconst carpetaDrive = getNodeJson('Drive - Crear carpeta ejecución TEST');\nconst carpetaNormalizada = getNodeJson('Code - Normalizar carpeta ejecución TEST');\n\nconst copiaPresentacion = getNodeJson('Drive - Copiar plantilla presentación ejecución TEST');\nconst linkPresentacionNormalizado = getNodeJson('Code - Normalizar link presentación ejecución TEST');\n\nconst data = {\n ...jsonFinal,\n ...carpetaPreparada,\n ...carpetaDrive,\n ...carpetaNormalizada,\n ...copiaPresentacion,\n ...linkPresentacionNormalizado,\n ...actual,\n ...match,\n ...enriquecido\n};\n\nconst limpiarTexto = (valor, fallback = 'No identificado') => {\n const texto = limpiar(valor);\n\n if (!texto) return fallback;\n if (texto.toLowerCase() === 'undefined') return fallback;\n if (texto.toLowerCase() === 'null') return fallback;\n if (texto.toLowerCase() === 'no disponible') return fallback;\n\n return texto;\n};\n\nconst cortar = (valor, max = 900) => {\n const texto = limpiarTexto(valor, 'No disponible');\n return texto.length > max ? texto.slice(0, max - 3) + '...' : texto;\n};\n\nconst normalizarPlano = (valor) => {\n return limpiar(valor)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, ' ')\n .trim();\n};\n\nconst limpiarReferenciaVisual = (valor) => {\n let texto = limpiar(valor);\n\n if (!texto) return 'No identificado';\n\n texto = texto\n .replace(/_/g, ' ')\n .replace(/\\s+/g, ' ')\n .replace(/\\bQUATE\\b/gi, 'GUATE')\n .replace(/\\bQUATEWMC\\b/gi, 'GUATE WMC')\n .replace(/\\bGUATEWMC\\b/gi, 'GUATE WMC')\n .replace(/(\\d{4})(GUATE|QUATE|WMC)/gi, '$1 $2')\n .replace(/(\\d)(WMC)/gi, '$1 $2')\n .replace(/\\s+/g, ' ')\n .trim();\n\n return texto || 'No identificado';\n};\n\nconst matchEstado = normalizarPlano(\n match.propuesta_match_estado ||\n data.propuesta_match_estado ||\n ''\n);\n\nconst matchConfianza = Number(\n match.propuesta_match_confianza ||\n data.propuesta_match_confianza ||\n 0\n);\n\nconst matchAlta =\n matchEstado === 'MATCH_ALTA_CONFIANZA' ||\n matchConfianza >= 85;\n\nconst propuestaNombreBanco = limpiar(\n match.propuesta_nombre_banco ||\n data.propuesta_nombre_banco ||\n ''\n);\n\nconst propuestaReferenciaEnriquecida = limpiar(\n enriquecido.propuesta_referencia ||\n data.propuesta_referencia ||\n jsonFinal.propuesta_referencia ||\n ''\n);\n\nconst propuestaReferenciaSlides = limpiarReferenciaVisual(\n matchAlta && propuestaNombreBanco\n ? propuestaNombreBanco\n : propuestaReferenciaEnriquecida\n);\n\nconst ubicacionSlides = limpiarTexto(\n enriquecido.ubicacion ||\n data.ubicacion ||\n jsonFinal.ubicacion,\n 'No identificado'\n);\n\nconst fechaEjecucionSlides = limpiarTexto(\n enriquecido.fecha_ejecucion ||\n data.fecha_ejecucion ||\n jsonFinal.fecha_ejecucion,\n 'No identificado'\n);\n\n// --------------------------------------------------\n// Resolver carpeta de evidencias\n// --------------------------------------------------\n\nconst mediaFolderId = limpiar(\n data.media_folder_id ||\n data.execution_folder_id ||\n data.folder_id ||\n data.id ||\n carpetaNormalizada.media_folder_id ||\n carpetaNormalizada.execution_folder_id ||\n carpetaNormalizada.folder_id ||\n carpetaDrive.id ||\n ''\n);\n\nlet mediaFolderUrl = limpiar(\n data.media_folder_url ||\n data.execution_folder_url ||\n data.folder_url ||\n carpetaNormalizada.media_folder_url ||\n carpetaNormalizada.execution_folder_url ||\n carpetaNormalizada.folder_url ||\n carpetaDrive.webViewLink ||\n ''\n);\n\nif (!mediaFolderUrl && mediaFolderId) {\n mediaFolderUrl = `https://drive.google.com/drive/folders/${mediaFolderId}`;\n}\n\n// --------------------------------------------------\n// Resolver link de presentación\n// --------------------------------------------------\n\nconst presentationId = limpiar(\n data.presentation_id ||\n data.presentacion_id ||\n linkPresentacionNormalizado.presentation_id ||\n copiaPresentacion.id ||\n actual.presentation_id ||\n ''\n);\n\nlet presentationUrl = limpiar(\n data.presentacion_ejecucion_url ||\n data.presentation_url ||\n data.presentationUrl ||\n data.link_presentacion ||\n linkPresentacionNormalizado.presentacion_ejecucion_url ||\n linkPresentacionNormalizado.presentation_url ||\n linkPresentacionNormalizado.webViewLink ||\n copiaPresentacion.webViewLink ||\n actual.presentacion_ejecucion_url ||\n actual.presentation_url ||\n actual.webViewLink ||\n ''\n);\n\nif (!presentationUrl && presentationId) {\n presentationUrl = `https://docs.google.com/presentation/d/${presentationId}/edit`;\n}\n\nif (!presentationId) {\n throw new Error('No llegó presentation_id para reemplazar textos en Slides.');\n}\n\nconst replacements = {\n '{{MARCA}}': limpiarTexto(data.marca || jsonFinal.marca),\n '{{CLIENTE}}': limpiarTexto(data.cliente || jsonFinal.cliente),\n '{{PAIS}}': limpiarTexto(data.pais || jsonFinal.pais),\n '{{UBICACION}}': ubicacionSlides,\n\n '{{FECHA_EJECUCION}}': fechaEjecucionSlides,\n '{{PROPUESTA_REFERENCIA}}': propuestaReferenciaSlides,\n\n '{{EJECUCION_ID}}': '',\n '{{SESSION_ID}}': '',\n\n '{{MANAGER_NOMBRE}}': limpiarTexto(data.manager_nombre || jsonFinal.manager_nombre),\n '{{MANAGER_TELEFONO}}': limpiarTexto(data.manager_telefono || jsonFinal.manager_telefono),\n\n '{{RESUMEN_IA}}': cortar(data.resumen_ia || jsonFinal.resumen_ia, 1000),\n '{{DESCRIPCION_EJECUCION}}': cortar(data.descripcion_ejecucion || jsonFinal.descripcion_ejecucion, 1000),\n '{{ELEMENTOS_DETECTADOS}}': cortar(data.elementos_detectados || jsonFinal.elementos_detectados, 1000),\n '{{TAGS}}': cortar(data.tags || jsonFinal.tags, 500),\n\n '{{FOTOS_COUNT}}': limpiarTexto(data.fotos_count ?? jsonFinal.fotos_count, '0'),\n '{{VIDEOS_COUNT}}': limpiarTexto(data.videos_count ?? jsonFinal.videos_count, '0'),\n '{{AUDIOS_COUNT}}': limpiarTexto(data.audios_count ?? jsonFinal.audios_count, '0'),\n\n '{{MEDIA_FOLDER_URL}}': limpiarTexto(mediaFolderUrl, 'No disponible'),\n '{{LINK_PRESENTACION}}': limpiarTexto(presentationUrl, 'No disponible')\n};\n\nconst slidesReplaceRequests = Object.entries(replacements).map(([placeholder, value]) => {\n return {\n replaceAllText: {\n containsText: {\n text: placeholder,\n matchCase: true\n },\n replaceText: value\n }\n };\n});\n\nreturn [\n {\n json: {\n ...data,\n\n presentation_id: presentationId,\n\n media_folder_id: mediaFolderId,\n media_folder_url: mediaFolderUrl,\n execution_folder_id: mediaFolderId,\n execution_folder_url: mediaFolderUrl,\n\n presentacion_ejecucion_url: presentationUrl,\n presentation_url: presentationUrl,\n\n propuesta_referencia: propuestaReferenciaSlides,\n ubicacion: ubicacionSlides,\n fecha_ejecucion: fechaEjecucionSlides,\n\n slides_replacements: replacements,\n slides_replace_requests: slidesReplaceRequests,\n\n estado_presentacion: 'REEMPLAZOS_SLIDES_PREPARADOS',\n\n slides_replacements_debug: {\n match_estado: matchEstado,\n match_confianza: matchConfianza,\n match_alta: matchAlta,\n\n propuesta_nombre_banco: propuestaNombreBanco,\n propuesta_referencia_enriquecida: propuestaReferenciaEnriquecida,\n propuesta_referencia_slides: propuestaReferenciaSlides,\n\n media_folder_id: mediaFolderId,\n media_folder_url: mediaFolderUrl,\n\n presentation_id: presentationId,\n presentation_url: presentationUrl,\n\n fuentes_links: {\n data_media_folder_url: data.media_folder_url || '',\n data_execution_folder_url: data.execution_folder_url || '',\n carpeta_normalizada_media_folder_url: carpetaNormalizada.media_folder_url || '',\n carpeta_normalizada_execution_folder_url: carpetaNormalizada.execution_folder_url || '',\n carpeta_drive_webViewLink: carpetaDrive.webViewLink || '',\n\n data_presentacion_ejecucion_url: data.presentacion_ejecucion_url || '',\n data_presentation_url: data.presentation_url || '',\n link_normalizado_presentacion: linkPresentacionNormalizado.presentacion_ejecucion_url || linkPresentacionNormalizado.presentation_url || '',\n copia_presentacion_webViewLink: copiaPresentacion.webViewLink || ''\n }\n }\n }\n }\n];" + "jsCode": "const actual = $json || {};\n\nconst getNodeJson = (nodeName) => {\n try {\n return $(nodeName).first().json || {};\n } catch (error) {\n return {};\n }\n};\n\nconst limpiar = (valor) => String(valor ?? '').trim();\n\nconst jsonFinal = getNodeJson('Code - Normalizar JSON final propuesta ejecutada TEST');\nconst match = getNodeJson('Code - Match propuesta banco Fulgencio TEST');\nconst enriquecido = getNodeJson('Code - Enriquecer datos con match banco TEST');\n\nconst carpetaPreparada = getNodeJson('Code - Preparar carpeta ejecución TEST');\nconst carpetaDrive = getNodeJson('Drive - Crear carpeta ejecución TEST');\nconst carpetaNormalizada = getNodeJson('Code - Normalizar carpeta ejecución TEST');\n\nconst copiaPresentacion = getNodeJson('Drive - Copiar plantilla presentación ejecución TEST');\nconst linkPresentacionNormalizado = getNodeJson('Code - Normalizar link presentación ejecución TEST');\n\nconst data = {\n ...jsonFinal,\n ...carpetaPreparada,\n ...carpetaDrive,\n ...carpetaNormalizada,\n ...copiaPresentacion,\n ...linkPresentacionNormalizado,\n ...actual,\n ...match,\n ...enriquecido\n};\n\nconst limpiarTexto = (valor, fallback = 'No identificado') => {\n const texto = limpiar(valor);\n\n if (!texto) return fallback;\n if (texto.toLowerCase() === 'undefined') return fallback;\n if (texto.toLowerCase() === 'null') return fallback;\n if (texto.toLowerCase() === 'no disponible') return fallback;\n\n return texto;\n};\n\nconst cortar = (valor, max = 900) => {\n const texto = limpiarTexto(valor, 'No disponible');\n return texto.length > max ? texto.slice(0, max - 3) + '...' : texto;\n};\n\nconst normalizarPlano = (valor) => {\n return limpiar(valor)\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toUpperCase()\n .replace(/\\s+/g, ' ')\n .trim();\n};\n\nconst limpiarReferenciaVisual = (valor) => {\n let texto = limpiar(valor);\n\n if (!texto) return 'No identificado';\n\n texto = texto\n .replace(/_/g, ' ')\n .replace(/\\s+/g, ' ')\n .replace(/\\bQUATE\\b/gi, 'GUATE')\n .replace(/\\bQUATEWMC\\b/gi, 'GUATE WMC')\n .replace(/\\bGUATEWMC\\b/gi, 'GUATE WMC')\n .replace(/(\\d{4})(GUATE|QUATE|WMC)/gi, '$1 $2')\n .replace(/(\\d)(WMC)/gi, '$1 $2')\n .replace(/\\s+/g, ' ')\n .trim();\n\n return texto || 'No identificado';\n};\n\nconst matchEstado = normalizarPlano(\n match.propuesta_match_estado ||\n data.propuesta_match_estado ||\n ''\n);\n\nconst matchConfianza = Number(\n match.propuesta_match_confianza ||\n data.propuesta_match_confianza ||\n 0\n);\n\nconst matchAlta =\n matchEstado === 'MATCH_ALTA_CONFIANZA' ||\n matchConfianza >= 85;\n\nconst propuestaNombreBanco = limpiar(\n match.propuesta_nombre_banco ||\n data.propuesta_nombre_banco ||\n ''\n);\n\nconst propuestaReferenciaEnriquecida = limpiar(\n enriquecido.propuesta_referencia ||\n data.propuesta_referencia ||\n jsonFinal.propuesta_referencia ||\n ''\n);\n\nconst propuestaReferenciaSlides = limpiarReferenciaVisual(\n matchAlta && propuestaNombreBanco\n ? propuestaNombreBanco\n : propuestaReferenciaEnriquecida\n);\n\nconst ubicacionSlides = limpiarTexto(\n enriquecido.ubicacion ||\n data.ubicacion ||\n jsonFinal.ubicacion,\n 'No identificado'\n);\n\nconst fechaEjecucionSlides = limpiarTexto(\n enriquecido.fecha_ejecucion ||\n data.fecha_ejecucion ||\n jsonFinal.fecha_ejecucion,\n 'No identificado'\n);\n\n// --------------------------------------------------\n// Resolver carpeta de evidencias\n// --------------------------------------------------\n\nconst mediaFolderId = limpiar(\n data.media_folder_id ||\n data.execution_folder_id ||\n data.folder_id ||\n data.id ||\n carpetaNormalizada.media_folder_id ||\n carpetaNormalizada.execution_folder_id ||\n carpetaNormalizada.folder_id ||\n carpetaDrive.id ||\n ''\n);\n\nlet mediaFolderUrl = limpiar(\n data.media_folder_url ||\n data.execution_folder_url ||\n data.folder_url ||\n carpetaNormalizada.media_folder_url ||\n carpetaNormalizada.execution_folder_url ||\n carpetaNormalizada.folder_url ||\n carpetaDrive.webViewLink ||\n ''\n);\n\nif (!mediaFolderUrl && mediaFolderId) {\n mediaFolderUrl = `https://drive.google.com/drive/folders/${mediaFolderId}`;\n}\n\n// --------------------------------------------------\n// Resolver link de presentación\n// --------------------------------------------------\n\nconst presentationId = limpiar(\n data.presentation_id ||\n data.presentacion_id ||\n linkPresentacionNormalizado.presentation_id ||\n copiaPresentacion.id ||\n actual.presentation_id ||\n ''\n);\n\nlet presentationUrl = limpiar(\n data.presentacion_ejecucion_url ||\n data.presentation_url ||\n data.presentationUrl ||\n data.link_presentacion ||\n linkPresentacionNormalizado.presentacion_ejecucion_url ||\n linkPresentacionNormalizado.presentation_url ||\n linkPresentacionNormalizado.webViewLink ||\n copiaPresentacion.webViewLink ||\n actual.presentacion_ejecucion_url ||\n actual.presentation_url ||\n actual.webViewLink ||\n ''\n);\n\nif (!presentationUrl && presentationId) {\n presentationUrl = `https://docs.google.com/presentation/d/${presentationId}/edit`;\n}\n\nif (!presentationId) {\n throw new Error('No llegó presentation_id para reemplazar textos en Slides.');\n}\n\nconst objetivoSlides = cortar(\n data.objetivo ||\n jsonFinal.objetivo ||\n data.descripcion_ejecucion ||\n jsonFinal.descripcion_ejecucion ||\n data.resumen_ia ||\n jsonFinal.resumen_ia ||\n 'No disponible',\n 900\n);\n\nconst replacements = {\n '{{MARCA}}': limpiarTexto(data.marca || jsonFinal.marca),\n '{{CLIENTE}}': limpiarTexto(data.cliente || jsonFinal.cliente),\n '{{PAIS}}': limpiarTexto(data.pais || jsonFinal.pais),\n '{{UBICACION}}': ubicacionSlides,\n\n '{{FECHA_EJECUCION}}': fechaEjecucionSlides,\n '{{PROPUESTA_REFERENCIA}}': propuestaReferenciaSlides,\n '{{OBJETIVO}}': objetivoSlides,\n\n '{{EJECUCION_ID}}': '',\n '{{SESSION_ID}}': '',\n\n '{{MANAGER_NOMBRE}}': limpiarTexto(data.manager_nombre || jsonFinal.manager_nombre),\n '{{MANAGER_TELEFONO}}': limpiarTexto(data.manager_telefono || jsonFinal.manager_telefono),\n\n '{{RESUMEN_IA}}': cortar(data.resumen_ia || jsonFinal.resumen_ia, 1000),\n '{{DESCRIPCION_EJECUCION}}': cortar(data.descripcion_ejecucion || jsonFinal.descripcion_ejecucion, 1000),\n '{{ELEMENTOS_DETECTADOS}}': cortar(data.elementos_detectados || jsonFinal.elementos_detectados, 1000),\n '{{TAGS}}': cortar(data.tags || jsonFinal.tags, 500),\n\n '{{FOTOS_COUNT}}': limpiarTexto(data.fotos_count ?? jsonFinal.fotos_count, '0'),\n '{{VIDEOS_COUNT}}': limpiarTexto(data.videos_count ?? jsonFinal.videos_count, '0'),\n '{{AUDIOS_COUNT}}': limpiarTexto(data.audios_count ?? jsonFinal.audios_count, '0'),\n\n '{{MEDIA_FOLDER_URL}}': limpiarTexto(mediaFolderUrl, 'No disponible'),\n '{{LINK_PRESENTACION}}': limpiarTexto(presentationUrl, 'No disponible')\n};\n\nconst slidesReplaceRequests = Object.entries(replacements).map(([placeholder, value]) => {\n return {\n replaceAllText: {\n containsText: {\n text: placeholder,\n matchCase: true\n },\n replaceText: value\n }\n };\n});\n\nreturn [\n {\n json: {\n ...data,\n\n presentation_id: presentationId,\n\n media_folder_id: mediaFolderId,\n media_folder_url: mediaFolderUrl,\n execution_folder_id: mediaFolderId,\n execution_folder_url: mediaFolderUrl,\n\n presentacion_ejecucion_url: presentationUrl,\n presentation_url: presentationUrl,\n\n propuesta_referencia: propuestaReferenciaSlides,\n ubicacion: ubicacionSlides,\n fecha_ejecucion: fechaEjecucionSlides,\n\n slides_replacements: replacements,\n slides_replace_requests: slidesReplaceRequests,\n\n estado_presentacion: 'REEMPLAZOS_SLIDES_PREPARADOS',\n\n slides_replacements_debug: {\n match_estado: matchEstado,\n match_confianza: matchConfianza,\n match_alta: matchAlta,\n\n propuesta_nombre_banco: propuestaNombreBanco,\n propuesta_referencia_enriquecida: propuestaReferenciaEnriquecida,\n propuesta_referencia_slides: propuestaReferenciaSlides,\n\n media_folder_id: mediaFolderId,\n media_folder_url: mediaFolderUrl,\n\n presentation_id: presentationId,\n presentation_url: presentationUrl,\n\n fuentes_links: {\n data_media_folder_url: data.media_folder_url || '',\n data_execution_folder_url: data.execution_folder_url || '',\n carpeta_normalizada_media_folder_url: carpetaNormalizada.media_folder_url || '',\n carpeta_normalizada_execution_folder_url: carpetaNormalizada.execution_folder_url || '',\n carpeta_drive_webViewLink: carpetaDrive.webViewLink || '',\n\n data_presentacion_ejecucion_url: data.presentacion_ejecucion_url || '',\n data_presentation_url: data.presentation_url || '',\n link_normalizado_presentacion: linkPresentacionNormalizado.presentacion_ejecucion_url || linkPresentacionNormalizado.presentation_url || '',\n copia_presentacion_webViewLink: copiaPresentacion.webViewLink || ''\n }\n }\n }\n }\n];" }, "type": "n8n-nodes-base.code", "typeVersion": 2, @@ -18153,7 +18153,7 @@ }, { "parameters": { - "amount": 10 + "amount": 4 }, "type": "n8n-nodes-base.wait", "typeVersion": 1.1, @@ -18305,7 +18305,7 @@ }, { "parameters": { - "amount": 8 + "amount": 4 }, "type": "n8n-nodes-base.wait", "typeVersion": 1.1, @@ -18451,12 +18451,12 @@ "parameters": { "content": "## 📥 ENTRADA WHATSAPP Y GRUPO OFICIAL\n\nEste bloque recibe eventos desde Evolution API y normaliza el mensaje recibido.\n\nResponsabilidades:\n- Detectar si viene de grupo o chat individual.\n- Identificar sender real en grupo.\n- Resolver whatsapp_to para responder al grupo correcto.\n- Detectar texto, audio, imagen, video o documento.\n- Normalizar comandos oficiales.\n- Validar que el mensaje venga de un grupo permitido.\n\nRegla principal:\nsolo se procesa si el grupo está autorizado.", "height": 528, - "width": 832 + "width": 848 }, "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ - -9552, + -9568, -1344 ], "id": "d86e235c-45ef-48c5-a816-6c32f33fd9ae", @@ -20189,15 +20189,15 @@ } }, "authors": "Isaac Aracena", - "name": "Version a72fa72c", + "name": "Version 135d4b16", "description": "", "autosaved": true, "workflowPublishHistory": [ { - "createdAt": "2026-06-25T19:31:03.336Z", - "id": 1885, + "createdAt": "2026-07-04T22:34:01.172Z", + "id": 2460, "workflowId": "CdosDIWBgnDMnPdr", - "versionId": "a72fa72c-6f91-4fca-9982-2b2438e339eb", + "versionId": "135d4b16-b87e-4b14-8234-57a60538586e", "event": "activated", "userId": "0a88c0b1-928e-4412-896e-c5d1c99b2029" }