8
n8n 中文网amn8n.com

ESPOLBot日历|常见问题

高级

这是一个Support Chatbot, AI Chatbot领域的自动化工作流,包含 91 个节点。主要使用 Set, Code, Html, Wait, Switch 等节点。 基于Telegram、MongoDB和Gemini AI的大学常见问题与日历助手

前置要求
  • MongoDB 连接字符串
  • Telegram Bot Token
  • 可能需要目标 API 的认证凭证
  • Google Sheets API 凭证
  • Google Gemini API Key
工作流预览
可视化展示节点连接关系,支持缩放和平移
导出工作流
复制以下 JSON 配置到 n8n 导入,即可使用此工作流
{
  "id": "qVwL4UVRZaWLVZtQ",
  "meta": {
    "instanceId": "8a5440a5ddd0e75b8a9a2e3575b14d94d566aa1e1140eba5b0cc9b9eb91aaea3",
    "templateCredsSetupCompleted": true
  },
  "name": "ESPOLBot 日历|常见问题",
  "tags": [],
  "nodes": [
    {
      "id": "6d06b9e5-4027-4626-8315-2b02a3087727",
      "name": "构建_Gemini提示1",
      "type": "n8n-nodes-base.code",
      "position": [
        -1008,
        2448
      ],
      "parameters": {
        "jsCode": "const preguntaUsuario = $json.pregunta_usuario;\nconst faqsRelevantes = $json.contexto_faqs || [];\n\nlet contextoFAQs = '';\nif (faqsRelevantes.length > 0) {\n  contextoFAQs = 'Aqui hay informacion relevante de la base de datos:\\n\\n';\n  contextoFAQs += faqsRelevantes.map((faq, index) => {\n    return `${index + 1}. P: ${faq.pregunta}\\n   R: ${faq.respuesta}${faq.enlaces ? `\\n   Link: ${faq.enlaces}` : ''}`;\n  }).join('\\n\\n');\n} else {\n  contextoFAQs = 'No encontre informacion especifica en la base de datos para esta pregunta.';\n}\n\nconst prompt = `Eres un asistente de ESPOL. Un estudiante te pregunto:\n\n\"${preguntaUsuario}\"\n\n${contextoFAQs}\n\nIMPORTANTE:\n- La pregunta del usuario ES VALIDA y CLARA\n- Si hay informacion arriba, usala para responder\n- Si NO hay informacion especifica, di algo como: \"No tengo informacion especifica sobre [tema], pero puedes contactar a ESPOL\"\n- NUNCA digas \"no has hecho ninguna pregunta\"\n\nContactos generales:\n- Email: admision@espol.edu.ec\n- Telefono: (04) 2269-269\n- Web: https://www.espol.edu.ec\n\nResponde de forma COMPLETA, clara y util. Incluye TODOS los detalles necesarios:`;\n\nreturn [{\n  json: {\n    prompt: prompt\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "95fcc718-5c12-423d-b062-b9935d3f8506",
      "name": "搜索_相关_常见问题",
      "type": "n8n-nodes-base.code",
      "position": [
        -1264,
        2448
      ],
      "parameters": {
        "jsCode": "function limpiarTexto(texto) {\n  if (!texto) return '';\n  return texto\n    .toLowerCase()\n    .normalize('NFD').replace(/[\\u0300-\\u036f]/g, '')\n    .replace(/[¿?¡!.,;:]/g, '')\n    .replace(/\\s+/g, ' ')\n    .trim();\n}\n\n// Leer la pregunta del Telegram Trigger\n// En runOnceForAllItems, usar .all() para acceder al primer (y normalmente único) mensaje\nconst telegramInput = $('Telegram Trigger - Inicio').all()[0]?.json || {};\nconst preguntaUsuario = telegramInput.message?.text || '';\nconst chatId = telegramInput.message?.chat?.id || '';\n\n// Las FAQs vienen del input (MongoDB)\nconst todasLasFAQs = $input.all();\n\nconst preguntaLimpia = limpiarTexto(preguntaUsuario);\nconst palabrasUsuario = preguntaLimpia.split(' ').filter(p => p.length > 2);\n\nconst faqsRelevantes = [];\n\nconsole.log(`========================================`);\nconsole.log(`Pregunta usuario: \"${preguntaUsuario}\"`);\nconsole.log(`Chat ID: ${chatId}`);\nconsole.log(`Palabras clave: ${palabrasUsuario.join(', ')}`);\nconsole.log(`Total FAQs en MongoDB: ${todasLasFAQs.length}`);\n\nfor (const faq of todasLasFAQs) {\n  const preguntaFAQ = faq.json.PREGUNTA || faq.json.pregunta || '';\n  if (!preguntaFAQ) continue;\n  \n  const preguntaFAQLimpia = limpiarTexto(preguntaFAQ);\n  \n  let coincidencias = 0;\n  for (const palabra of palabrasUsuario) {\n    if (preguntaFAQLimpia.includes(palabra)) {\n      coincidencias++;\n    }\n  }\n  \n  if (coincidencias >= 2) {\n    const score = coincidencias / palabrasUsuario.length;\n    \n    faqsRelevantes.push({\n      pregunta: preguntaFAQ,\n      respuesta: faq.json.RESPUESTA || faq.json.respuesta || '',\n      enlaces: faq.json.ENLACES || faq.json.enlaces || null,\n      similitud: Math.round(score * 100) / 100,\n      coincidencias: coincidencias\n    });\n  }\n}\n\nfaqsRelevantes.sort((a, b) => b.coincidencias - a.coincidencias);\nconst top10 = faqsRelevantes.slice(0, 10);\n\nconsole.log(`FAQs encontradas: ${top10.length}`);\nif (top10.length > 0) {\n  console.log(`Top 5 candidatas:`);\n  top10.slice(0, 5).forEach((faq, i) => {\n    console.log(`  ${i+1}. \"${faq.pregunta.substring(0, 70)}...\" (${faq.coincidencias} palabras)`);\n  });\n} else {\n  console.log(`⚠️ NO SE ENCONTRARON FAQs`);\n}\nconsole.log(`========================================`);\n\nreturn [{\n  json: {\n    pregunta_usuario: preguntaUsuario,\n    contexto_faqs: top10,\n    total_encontradas: top10.length,\n    chat_id: chatId\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "d5e736d4-9d75-44d5-b1c8-dfe94b289e68",
      "name": "用户回复1",
      "type": "n8n-nodes-base.switch",
      "position": [
        -1616,
        1184
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "3874c471-84a4-4058-8a35-0cc833b8617d",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.callback_query.data }}",
                    "rightValue": "feedback_yes"
                  }
                ]
              }
            },
            {
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "a53cc39a-2efc-4196-9425-1afba6fc8d7d",
                    "operator": {
                      "name": "filter.operator.equals",
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.callback_query.data }}",
                    "rightValue": "feedback_no"
                  }
                ]
              }
            }
          ]
        },
        "options": {}
      },
      "typeVersion": 3.3
    },
    {
      "id": "d9139b9c-e970-4c60-88b8-47bd105ab0aa",
      "name": "是 - 感谢1",
      "type": "n8n-nodes-base.telegram",
      "position": [
        -1264,
        1008
      ],
      "webhookId": "d64de90b-2449-4bcc-8815-f43c2e94c0b5",
      "parameters": {
        "text": "🎉 ¡Genial! Me alegra mucho saber que pude ayudarte 💪 Si necesitas algo más, estoy aquí para ti 🤖",
        "chatId": "={{ $json.result.id }}",
        "additionalFields": {
          "appendAttribution": false
        }
      },
      "credentials": {
        "telegramApi": {
          "id": "ovdN9afPb1hJiiET",
          "name": "ESPOLBot"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "9c41f1ef-12bf-4620-9719-821461334db6",
      "name": "否 - 反馈消息1",
      "type": "n8n-nodes-base.telegram",
      "position": [
        -1264,
        1152
      ],
      "webhookId": "606e94b8-5fcd-4977-852e-d0030aacf8a6",
      "parameters": {
        "text": "=😔 Lamento que la información no te haya sido útil.\n¿Podrías contarme qué necesitabas exactamente para mejorar mi ayuda? 🤖",
        "chatId": "={{ $json.callback_query.message.chat.id }}",
        "forceReply": {
          "force_reply": true
        },
        "replyMarkup": "forceReply",
        "additionalFields": {
          "appendAttribution": false
        }
      },
      "credentials": {
        "telegramApi": {
          "id": "ovdN9afPb1hJiiET",
          "name": "ESPOLBot"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "cc0258e3-303c-44e5-8c96-80287ba1ab88",
      "name": "命令1",
      "type": "n8n-nodes-base.switch",
      "position": [
        -1248,
        1664
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "outputKey": "/help",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "loose"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "62d19c71-6b1b-4d67-9a45-8aff710df07c",
                    "operator": {
                      "type": "boolean",
                      "operation": "true",
                      "singleValue": true
                    },
                    "leftValue": "={{ $json[\"message\"] && $json[\"message\"][\"text\"] === \"/help\" }}",
                    "rightValue": "/start"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "/faqs",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "loose"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "4a392957-3709-4c6e-a7ac-6f6004836860",
                    "operator": {
                      "type": "boolean",
                      "operation": "true",
                      "singleValue": true
                    },
                    "leftValue": "={{ $json[\"message\"] && $json[\"message\"][\"text\"] === \"/faqs\" }}",
                    "rightValue": ""
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "/contact",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "loose"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "32b9b535-3e0a-4d3f-8691-fdc1e955a9ce",
                    "operator": {
                      "type": "boolean",
                      "operation": "true",
                      "singleValue": true
                    },
                    "leftValue": "={{ $json[\"message\"] && $json[\"message\"][\"text\"] === \"/contact\" }}",
                    "rightValue": ""
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "/events",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "loose"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "9393b68b-7c44-48cf-b2d0-d23289673ff6",
                    "operator": {
                      "type": "boolean",
                      "operation": "true",
                      "singleValue": true
                    },
                    "leftValue": "={{ $json[\"message\"] && $json[\"message\"][\"text\"] === \"/events\" }}",
                    "rightValue": ""
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "/feedback",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "loose"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "df2037b1-36f0-4428-bdd4-b5118f687d8f",
                    "operator": {
                      "type": "boolean",
                      "operation": "true",
                      "singleValue": true
                    },
                    "leftValue": "={{ $json[\"message\"] && $json[\"message\"][\"text\"] === \"/feedback\" }}",
                    "rightValue": ""
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "/start",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "loose"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "58d0352b-c98a-4ac7-bf60-f6a6e6919744",
                    "operator": {
                      "type": "boolean",
                      "operation": "true",
                      "singleValue": true
                    },
                    "leftValue": "={{ $json[\"message\"] && $json[\"message\"][\"text\"] === \"/start\" }}",
                    "rightValue": ""
                  }
                ]
              },
              "renameOutput": true
            }
          ]
        },
        "options": {},
        "looseTypeValidation": true
      },
      "typeVersion": 3.3
    },
    {
      "id": "79a97506-c1e2-4049-a62a-e5820b681a0d",
      "name": "开始 - 反馈",
      "type": "n8n-nodes-base.telegram",
      "position": [
        -880,
        1312
      ],
      "webhookId": "c87b9628-09ed-468b-ab95-9b97e4c4f0ef",
      "parameters": {
        "text": "=💬 *Enviar Feedback*\n\n¿Tienes alguna sugerencia o comentario sobre el bot? \n\n📝 Escribe tu mensaje a continuación y lo revisaremos.\n\n🤖 *¡Tu opinión es importante!* \n\n💡 *Tip:* Los botones 👍 👎 aparecen después de cada respuesta para que califiques la información.",
        "chatId": "={{ $json.message.chat.id }}",
        "replyMarkup": "inlineKeyboard",
        "inlineKeyboard": {
          "rows": [
            {
              "row": {
                "buttons": [
                  {
                    "text": "👍 Sí, me ayudó",
                    "additionalFields": {
                      "callback_data": "feedback_yes"
                    }
                  },
                  {
                    "text": "👎 No, no me ayudó",
                    "additionalFields": {
                      "callback_data": "feedback_no"
                    }
                  }
                ]
              }
            }
          ]
        },
        "additionalFields": {
          "parse_mode": "Markdown",
          "appendAttribution": false
        }
      },
      "credentials": {
        "telegramApi": {
          "id": "ovdN9afPb1hJiiET",
          "name": "ESPOLBot"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "accfeb10-fcb6-4b1e-a1bb-a40ed82ebcdd",
      "name": "常见问题",
      "type": "n8n-nodes-base.telegram",
      "position": [
        -880,
        784
      ],
      "webhookId": "04237280-d7f2-4b1e-96cf-37343b850b8b",
      "parameters": {
        "text": "💬 Sección de Consultas – ChatBot ESPOL\n\nEstás en la sección de preguntas frecuentes.\nAquí puedes escribir cualquier duda o consulta sobre la universidad —por ejemplo, temas de matrícula, eventos, horarios o servicios estudiantiles— y te responderé enseguida.\n\n✍️ Escribe tu pregunta para continuar.\nPor ejemplo: “Necesito información sobre los eventos de esta semana” o “¿Dónde puedo comunicarme con admisiones?”",
        "chatId": "={{ $('Telegram Trigger - Inicio').item.json.message.chat.id }}",
        "additionalFields": {
          "parse_mode": "Markdown",
          "appendAttribution": false
        }
      },
      "credentials": {
        "telegramApi": {
          "id": "ovdN9afPb1hJiiET",
          "name": "ESPOLBot"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "ccd1f1ed-5aa2-402c-a40e-5650ee6066d7",
      "name": "从MongoDB读取常见问题",
      "type": "n8n-nodes-base.mongoDb",
      "position": [
        -1520,
        2448
      ],
      "parameters": {
        "options": {},
        "collection": "espol_faqs"
      },
      "credentials": {
        "mongoDb": {
          "id": "gYgPeYFM9Twdvd1B",
          "name": "MongoDB account 2"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "6529ea5d-0af3-4acc-b81b-087730a484b6",
      "name": "Telegram触发器 - 开始",
      "type": "n8n-nodes-base.telegramTrigger",
      "position": [
        -1872,
        1744
      ],
      "webhookId": "472c70d1-8362-4668-83dc-3e7c2985bc08",
      "parameters": {
        "updates": [
          "message",
          "callback_query",
          "*"
        ],
        "additionalFields": {}
      },
      "credentials": {
        "telegramApi": {
          "id": "ovdN9afPb1hJiiET",
          "name": "ESPOLBot"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "5bfddfd3-92ac-432f-aa64-3f19ab58f30a",
      "name": "联系人回复",
      "type": "n8n-nodes-base.telegram",
      "position": [
        -880,
        960
      ],
      "webhookId": "57a49a16-8222-4da4-8df5-06b6325c4083",
      "parameters": {
        "text": "=📞 *Información de Contacto ESPOL*\n\n🌐 Web: https://www.espol.edu.ec\n📧 Email: admision@espol.edu.ec\n📱 Teléfono: (04) 2269-269\n\n📍 Campus Gustavo Galindo Velasco\nKm. 30.5 Vía Perimetral\nGuayaquil, Ecuador\n\n📧 Otros contactos:\nComunicación: comunicacion@espol.edu.ec\nRelaciones Externas y Vinculación Corporativa:  relex@espol.edu.ec\nPostgrados: postgrad@espol.edu.ec\nTransparencia: transparencia@espol.edu.ec",
        "chatId": "={{ $('Telegram Trigger - Inicio').item.json.message.chat.id }}",
        "additionalFields": {
          "parse_mode": "Markdown",
          "appendAttribution": false
        }
      },
      "credentials": {
        "telegramApi": {
          "id": "ovdN9afPb1hJiiET",
          "name": "ESPOLBot"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "8aa3525d-815a-486a-92cb-9d9e5da991e5",
      "name": "事件回复",
      "type": "n8n-nodes-base.telegram",
      "position": [
        -880,
        1136
      ],
      "webhookId": "8688a44d-b766-45c4-b314-3ef4dc105e1d",
      "parameters": {
        "text": "=📅 *Próximos Eventos Académicos*\n\nPara ver el calendario completo de eventos, visita:\nhttps://www.espol.edu.ec/es/calendario-academico\n\nO pregúntame sobre fechas específicas, por ejemplo:\n\"¿Cuándo son las vacaciones?\"\n\"¿Cuándo es el examen de admisión?\"",
        "chatId": "={{ $json.message.chat.id }}",
        "additionalFields": {
          "parse_mode": "Markdown",
          "appendAttribution": false
        }
      },
      "credentials": {
        "telegramApi": {
          "id": "ovdN9afPb1hJiiET",
          "name": "ESPOLBot"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "767c3c89-dfb0-4505-8c87-a0208fe31aba",
      "name": "清理Gemini文本",
      "type": "n8n-nodes-base.code",
      "position": [
        -480,
        2448
      ],
      "parameters": {
        "jsCode": "const geminiResponse = $input.first().json;\nconst chatId = $('Buscar_FAQs_Relevantes').first().json.chat_id;\n\nlet texto = '';\n\ntry {\n  if (geminiResponse.candidates && geminiResponse.candidates[0] && geminiResponse.candidates[0].content && geminiResponse.candidates[0].content.parts) {\n    texto = geminiResponse.candidates[0].content.parts[0].text;\n  } else if (geminiResponse.content && geminiResponse.content.parts) {\n    texto = geminiResponse.content.parts[0].text;\n  } else if (geminiResponse.text) {\n    texto = geminiResponse.text;\n  } else {\n    texto = \"Lo siento, hubo un problema al procesar la respuesta.\";\n  }\n} catch (error) {\n  texto = \"Lo siento, ocurrio un error.\";\n}\n\nif (texto && texto.length > 0) {\n  texto = texto.replace(/\\*\\*/g, '').replace(/`/g, '').trim();\n}\n\nreturn [{\n  json: {\n    texto_limpio: texto,\n    chat_id: chatId\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "7cabbbc4-e002-4c04-a4d9-b5c3633e98af",
      "name": "获取聊天",
      "type": "n8n-nodes-base.telegram",
      "position": [
        -1488,
        1056
      ],
      "webhookId": "4540ece6-38eb-40ac-bb7c-862f2e198807",
      "parameters": {
        "chatId": "={{ $json.callback_query.message.chat.id }}",
        "resource": "chat"
      },
      "credentials": {
        "telegramApi": {
          "id": "ovdN9afPb1hJiiET",
          "name": "ESPOLBot"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "b5cdf1bf-6f19-4e62-a812-022e5fe6073c",
      "name": "切换_读取哪个数据库",
      "type": "n8n-nodes-base.switch",
      "position": [
        -1856,
        2304
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "outputKey": "=Consultar Calendario",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "9404fad9-6063-443e-9a4c-80c86a20a6a9",
                    "operator": {
                      "type": "boolean",
                      "operation": "true",
                      "singleValue": true
                    },
                    "leftValue": "={{ $json.es_consulta_calendario }}",
                    "rightValue": ""
                  }
                ]
              },
              "renameOutput": true
            }
          ]
        },
        "options": {
          "fallbackOutput": "extra"
        }
      },
      "typeVersion": 3.3
    },
    {
      "id": "09ddecdf-9223-4371-bd53-9d836315b7a3",
      "name": "日历预检测器",
      "type": "n8n-nodes-base.code",
      "position": [
        -2064,
        2304
      ],
      "parameters": {
        "jsCode": "// ===== DETECTOR PRE-LECTURA DE CALENDARIO =====\n\nfunction limpiarTexto(texto) {\n  if (!texto) return '';\n  return texto\n    .toLowerCase()\n    .normalize('NFD').replace(/[\\u0300-\\u036f]/g, '')\n    .replace(/[¿?¡!.,;:]/g, '')\n    .replace(/\\s+/g, ' ')\n    .trim();\n}\n\n// Procesar todos los items entrantes del Trigger\nconst inputItems = $('Telegram Trigger - Inicio').all();\nconst resultados = [];\n\nfor (const item of inputItems) {\n  const preguntaUsuario = item.json.message?.text || '';\n  const chatId = item.json.message?.chat?.id || '';\n  const preguntaLimpia = limpiarTexto(preguntaUsuario);\n\n  // ===== PALABRAS CLAVE DE CALENDARIO =====\n  const palabrasClaveCalendario = {\n    eventos: [\n      'matricula', 'matriculas', 'matriculacion',\n      'inicio de clases', 'fin de clases', 'clases',\n      'primer termino', 'segundo termino', 'tercer termino',\n      'examenes', 'examen', 'prueba', 'pruebas',\n      'evaluaciones', 'evaluacion',\n      'suspension', 'feriado', 'feriados',\n      'vacaciones', 'receso',\n      'cambio de carrera', 'cambio de materia',\n      'retiro', 'retiros',\n      'solicitudes', 'solicitud',\n      'inscripciones', 'inscripcion',\n      'graduacion', 'grado',\n      'titulacion',\n      'becas', 'beca',\n      'nivelacion'\n    ],\n    \n    tiempo: [\n      'cuando', 'fecha', 'fechas', 'dia', 'dias',\n      'plazo', 'plazos', 'periodo', 'periodos',\n      'termino', 'terminos', 'semestre', 'año',\n      'inicio', 'fin', 'cierre', 'apertura',\n      'hasta cuando', 'desde cuando',\n      'calendario', 'cronograma',\n      'horario', 'horarios',\n      'proxima', 'proximo', 'siguiente'\n    ],\n    \n    terminos: [\n      '2024-2025', '2025-2026',\n      'PAO', 'PAOI', 'PAOII', 'IPAO', 'IIPAO', 'PAO1', 'PAO2',\n      'primer', 'segundo', 'tercer',\n      'trimestre', 'bimestre', 'semestral'\n    ]\n  };\n\n  // Combinar todas las palabras clave\n  const todasLasPalabrasClave = [\n    ...palabrasClaveCalendario.eventos,\n    ...palabrasClaveCalendario.tiempo,\n    ...palabrasClaveCalendario.terminos\n  ];\n\n  // ===== DETECCIÓN =====\n  let palabrasEncontradas = [];\n  let puntuacion = 0;\n\n  for (const palabra of todasLasPalabrasClave) {\n    if (preguntaLimpia.includes(palabra)) {\n      palabrasEncontradas.push(palabra);\n      if (palabrasClaveCalendario.eventos.includes(palabra)) {\n        puntuacion += 2;\n      } else {\n        puntuacion += 1;\n      }\n    }\n  }\n\n  // Decisión: ¿Es consulta de calendario?\n  const esConsultaCalendario = puntuacion >= 2 || palabrasEncontradas.length >= 2;\n\n  // LOGS\n  console.log(`========================================`);\n  console.log(`🔍 DETECTOR PRE-LECTURA DE CALENDARIO`);\n  console.log(`Pregunta: \"${preguntaUsuario}\"`);\n  console.log(`Palabras encontradas: ${palabrasEncontradas.join(', ') || 'ninguna'}`);\n  console.log(`Puntuación: ${puntuacion}`);\n  console.log(`¿Es calendario? ${esConsultaCalendario ? '✅ SÍ' : '❌ NO'}`);\n  console.log(`${esConsultaCalendario ? '→ Irá a BD_Calendario' : '→ Irá a FAQs (flujo normal)'}`);\n  console.log(`========================================`);\n\n  resultados.push({\n    json: {\n      pregunta_usuario: preguntaUsuario,\n      chat_id: chatId,\n      es_consulta_calendario: esConsultaCalendario,\n      palabras_encontradas: palabrasEncontradas,\n      puntuacion: puntuacion\n    }\n  });\n}\n\nreturn resultados;"
      },
      "typeVersion": 2
    },
    {
      "id": "81ea9f6c-121c-4bc5-99e4-e947dae8bf7a",
      "name": "Gemini消息",
      "type": "@n8n/n8n-nodes-langchain.googleGemini",
      "position": [
        -800,
        2448
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "models/gemini-2.5-flash-lite-preview-06-17",
          "cachedResultName": "models/gemini-2.5-flash-lite-preview-06-17"
        },
        "options": {
          "temperature": 0.3,
          "maxOutputTokens": 2000
        },
        "messages": {
          "values": [
            {
              "content": "={{ $json.prompt }}"
            }
          ]
        },
        "simplify": false
      },
      "credentials": {
        "googlePalmApi": {
          "id": "5akz9ZmFGR6TDmkP",
          "name": "Google Gemini(PaLM) Api account"
        }
      },
      "typeVersion": 1,
      "alwaysOutputData": false
    },
    {
      "id": "8116c6c4-e4ca-4450-89ab-17df67b4b886",
      "name": "发送关于常见问题的回复",
      "type": "n8n-nodes-base.telegram",
      "position": [
        -224,
        2448
      ],
      "webhookId": "0096bd46-0086-4c23-87a8-a63bfebca2d8",
      "parameters": {
        "text": "={{ $json.texto_limpio }}\n\n💬 ¿Deseas revisar otra consulta o hacer una nueva pregunta?",
        "chatId": "={{ $json.chat_id }}",
        "replyMarkup": "inlineKeyboard",
        "inlineKeyboard": {
          "rows": [
            {
              "row": {
                "buttons": [
                  {
                    "text": "👍 Sí",
                    "additionalFields": {
                      "callback_data": "=feedback_yes"
                    }
                  },
                  {
                    "text": "👎 No",
                    "additionalFields": {
                      "callback_data": "=feedback_no"
                    }
                  }
                ]
              }
            }
          ]
        },
        "additionalFields": {
          "parse_mode": "HTML",
          "appendAttribution": false
        }
      },
      "credentials": {
        "telegramApi": {
          "id": "ovdN9afPb1hJiiET",
          "name": "ESPOLBot"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "5f4eed47-a777-4d27-a80a-bcc145808f42",
      "name": "保存反馈到CSV",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        -1264,
        1296
      ],
      "parameters": {
        "columns": {
          "value": {
            "id": "={{ $json.message.chat.id }}",
            "Nombre": "={{ $json.message.reply_to_message.chat.first_name }}",
            "Comentario": "={{ $json.message.text }}"
          },
          "schema": [
            {
              "id": "id",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "id",
              "defaultMatch": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "Nombre",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Nombre",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Comentario",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Comentario",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "id"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1dcEsIPMMnjBtQ1YXy5Zc50OWwBVOcWXpxkJUJWZmE4E/edit#gid=0",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1dcEsIPMMnjBtQ1YXy5Zc50OWwBVOcWXpxkJUJWZmE4E",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1dcEsIPMMnjBtQ1YXy5Zc50OWwBVOcWXpxkJUJWZmE4E/edit?usp=drivesdk",
          "cachedResultName": "Feedback"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "uBrRRX1SVbzPprTl",
          "name": "Google Sheets account 6"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "cdf6ad08-7f31-467e-9feb-9d3a8e1c9ba6",
      "name": "构建_日历提示1",
      "type": "n8n-nodes-base.code",
      "position": [
        -1008,
        2160
      ],
      "parameters": {
        "jsCode": "const preguntaUsuario = $json.consulta || $json.pregunta_usuario || $json.pregunta || '';\nconst chatId = $json.chat_id;\n\n// Preferir el nuevo esquema del buscador\nlet eventos = Array.isArray($json.resultados) ? $json.resultados : [];\n\n// Compatibilidad con el arreglo antiguo { eventos_calendario: [...] }\nif ((!eventos || eventos.length === 0) && Array.isArray($json.eventos_calendario)) {\n  eventos = $json.eventos_calendario.map((e) => ({\n    PERIODO_FECHAS: e.periodo || e.PERIODO_FECHAS || '',\n    ACTIVIDADES_GRADO: e.actividades_grado || e.ACTIVIDADES_GRADO || '',\n    PROCESOS_GRADO: e.procesos_grado || e.PROCESOS_GRADO || '',\n    ACTIVIDADES_FORMACION: e.actividades_formacion || e.ACTIVIDADES_FORMACION || '',\n    fecha_inicio_ts: e.fecha_inicio_ts ?? null,\n    score: e.score ?? null,\n  }));\n}\n\n// Señal de “demasiadas coincidencias” y facetas sugeridas (si vienen del buscador)\nconst demasiadas = Boolean($json.demasiadas_coincidencias);\nconst filtros = $json.filtros_sugeridos || null;\n\n// === Funciones auxiliares ===\nfunction safe(v) { return (v == null ? '' : String(v)); }\n\nfunction fmtFecha(ts) {\n  if (ts == null) return '';\n  try {\n    const d = new Date(Number(ts));\n    if (isNaN(d.getTime())) return '';\n    const dd = String(d.getDate()).padStart(2, '0');\n    const mm = String(d.getMonth() + 1).padStart(2, '0');\n    const yyyy = d.getFullYear();\n    return `${yyyy}-${mm}-${dd}`;\n  } catch { return ''; }\n}\n\n// === Determinar PAO actual según la fecha del sistema ===\nconst ahora = new Date();\nconst mesActual = ahora.getMonth() + 1;\nconst anioActual = ahora.getFullYear();\n\nlet paoActual = '';\nif (mesActual >= 5 && mesActual <= 9) paoActual = 'PAO I';\nelse if (mesActual >= 9 || mesActual <= 2) paoActual = 'PAO II';\nelse paoActual = 'Vacaciones o PAE';\n\nconst contextoTiempo = `📆 Fecha actual: ${anioActual}-${String(mesActual).padStart(2, '0')} (${paoActual} en curso)`;\n\n// === Ordenar eventos por relevancia y fecha ===\nconst eventosOrdenados = [...eventos].sort((a, b) => {\n  const sa = (a.score == null) ? -Infinity : Number(a.score);\n  const sb = (b.score == null) ? -Infinity : Number(b.score);\n  if (sb !== sa) return sb - sa;\n\n  const ta = (a.fecha_inicio_ts == null) ? Number.POSITIVE_INFINITY : Number(a.fecha_inicio_ts);\n  const tb = (b.fecha_inicio_ts == null) ? Number.POSITIVE_INFINITY : Number(b.fecha_inicio_ts);\n  return ta - tb;\n});\n\nlet contextoCalendario = '';\n\nif (eventosOrdenados.length > 0) {\n  contextoCalendario = '📅 He encontrado estas coincidencias (ordenadas por relevancia):\\n\\n';\n\n  // Mostrar solo el top 10\n  const toShow = eventosOrdenados.slice(0, 10);\n\n  contextoCalendario += toShow.map((ev, idx) => {\n    const periodo = safe(ev.PERIODO_FECHAS || ev.periodo);\n    const ag = safe(ev.ACTIVIDADES_GRADO || ev.actividades_grado);\n    const pg = safe(ev.PROCESOS_GRADO || ev.procesos_grado);\n    const af = safe(ev.ACTIVIDADES_FORMACION || ev.actividades_formacion);\n    const fechaISO = fmtFecha(ev.fecha_inicio_ts);\n    const score = (ev.score != null) ? Number(ev.score).toFixed(3) : '';\n\n    let info = `${idx + 1}. 📌 ${periodo || '(sin fecha)'}`;\n    if (fechaISO) info += `\\n   🗓️ Inicio (estimado): ${fechaISO}`;\n    if (ag.trim()) info += `\\n   🎓 Actividades de Grado: ${ag}`;\n    if (pg.trim()) info += `\\n   📋 Procesos: ${pg}`;\n    if (af.trim()) info += `\\n   🔧 Formación Técnica: ${af}`;\n    if (score) info += `\\n   🔎 score:${score}`;\n    return info;\n  }).join('\\n\\n');\n\n  // Sugerencias si hay demasiadas coincidencias\n  if (demasiadas) {\n    const tips = [];\n    if (filtros?.posibles_anios?.length) tips.push(`• especifica año: ${filtros.posibles_anios.slice(-3).join(' / ')}`);\n    if (filtros?.posibles_meses?.length) tips.push(`• añade mes: ${filtros.posibles_meses.slice(0, 4).join(' / ')}`);\n    if (filtros?.posibles_tipos_evaluacion?.length) tips.push(`• tipo de evaluación: ${filtros.posibles_tipos_evaluacion.join(' / ')}`);\n    if (filtros?.posibles_ciclos?.length) tips.push(`• ciclo: ${filtros.posibles_ciclos.join(' / ')}`);\n\n    if (tips.length) {\n      contextoCalendario += '\\n\\n⚠️ Hay muchas coincidencias. Sugerencias para refinar:\\n' + tips.join('\\n');\n    }\n  }\n\n} else {\n  contextoCalendario = 'No encontré información específica en el calendario académico para tu consulta.';\n}\n\n// === Reglas de interpretación PAO/PAE ===\nconst reglasPAO = `\nREGLAS DE INTERPRETACIÓN (PAO/PAE y feriados):\n• Si un evento dice “vacaciones” y la fecha cae ENTRE finales de febrero y los primeros días de mayo ⇒ corresponde a VACACIONES del PAO II.\n• Desde inicios de mayo hasta inicios/mediados de septiembre ⇒ corresponde a PAO I.\n• Si un evento cae entre finales de septiembre y la 1.ª o 2.ª semana de febrero ⇒ corresponde a PAO II (período lectivo).\n• En PAO II, si aparecen “vacaciones estudiantiles” del 28 de diciembre al 1 de enero ⇒ son feriado (no vacaciones entre PAOs).\n• PAE (Programa de Acompañamiento/Evaluación) ocurre normalmente desde inicios de marzo hasta finales de abril o inicios de mayo.\n• Si dentro de un PAO aparece “vacaciones”, interprétalas como feriados específicos, no como cambio de PAO.\n• Si el evento no indica explícitamente PAO I o II, infiérelo con estas reglas. Si hay ambigüedad o la fecha cae fuera de rango, indica que no hay información específica.\n`.trim();\n\n// === Construcción final del prompt ===\nconst prompt = `Eres un asistente de ESPOL especializado en calendario académico.\n\n${contextoTiempo}\n\nPregunta del estudiante:\n\"${preguntaUsuario}\"\n\n${contextoCalendario}\n\n${reglasPAO}\n\nINSTRUCCIONES IMPORTANTES (ESTRICTAS):\n1) Si existe la lista anterior, elige SOLO 1 evento que sea la MEJOR COINCIDENCIA para la pregunta.\n   • Considera intención (evaluación/vacaciones/inicio/fin), ordinal (primera/segunda/tercera),\n     y año/mes implícitos o explícitos.\n   • Si ningún evento es claramente pertinente, responde que NO hay información específica.\n2) No inventes datos. No cites eventos que no estén arriba.\n3) Si das un resultado, preséntalo claro y breve con emojis, destacando FECHA y ACTIVIDAD.\n4) Si NO hay información específica, sugiere contactar a:\n   📧 Email: admision@espol.edu.ec\n   📞 Teléfono: (04) 2269-269\n   🌐 Web: https://www.espol.edu.ec\n5) Si identificas el PAO (I o II) por las fechas, indícalo entre paréntesis o nota breve (“según fechas corresponde al PAO I/II”).\n6) Responde de forma clara, organizada y útil.`;\n\n// === Salida para el siguiente nodo (Gemini) ===\nreturn [{\n  json: {\n    prompt,\n    chat_id: chatId\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "728f012c-ceed-400d-a643-8f1353d9f8f1",
      "name": "向模型1发送消息",
      "type": "@n8n/n8n-nodes-langchain.googleGemini",
      "position": [
        -800,
        2160
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "models/gemini-2.5-flash-preview-05-20",
          "cachedResultName": "models/gemini-2.5-flash-preview-05-20"
        },
        "options": {
          "temperature": 0.3,
          "maxOutputTokens": 200000000
        },
        "messages": {
          "values": [
            {
              "content": "={{ $json.prompt }}"
            }
          ]
        },
        "simplify": false
      },
      "credentials": {
        "googlePalmApi": {
          "id": "5akz9ZmFGR6TDmkP",
          "name": "Google Gemini(PaLM) Api account"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "4af01328-9345-4714-a79d-ea81b2a16a38",
      "name": "清理_Gemini_文本1",
      "type": "n8n-nodes-base.code",
      "position": [
        -480,
        2160
      ],
      "parameters": {
        "jsCode": "// ---- 1) Extracción robusta del texto de Gemini ----\nfunction extraerTextoGemini(payload) {\n  // Formato más común: candidates[0].content.parts[*].text\n  try {\n    const parts = payload?.candidates?.[0]?.content?.parts;\n    if (Array.isArray(parts) && parts.length > 0) {\n      // Concatenar todos los parts .text\n      const textos = parts\n        .map(p => (typeof p?.text === 'string' ? p.text : ''))\n        .filter(Boolean);\n      if (textos.length) return textos.join('\\n').trim();\n    }\n  } catch {}\n\n  // Alternativos:\n  if (typeof payload?.text === 'string' && payload.text.trim()) {\n    return payload.text.trim();\n  }\n\n  if (Array.isArray(payload?.content?.parts) && payload.content.parts.length > 0) {\n    const textos2 = payload.content.parts\n      .map(p => (typeof p?.text === 'string' ? p.text : ''))\n      .filter(Boolean);\n    if (textos2.length) return textos2.join('\\n').trim();\n  }\n\n  // Último recurso: stringify recortado\n  try {\n    const s = JSON.stringify(payload);\n    if (s && s.length) return s.substring(0, 8000);\n  } catch {}\n\n  return 'Lo siento, no pude procesar tu consulta.';\n}\n\nconst respuestaGemini = extraerTextoGemini($json);\n\n// ---- 2) Recuperar chat_id desde varios nodos/ubicaciones ----\nlet chatId = $json.chat_id;\n\nif (!chatId) {\n  // Tu prompt actual\n  try { chatId = $('Construir_Prompt_Calendario1').first()?.json?.chat_id; } catch {}\n}\nif (!chatId) {\n  // Nombre anterior\n  try { chatId = $('Construir_Prompt_Calendario').first()?.json?.chat_id; } catch {}\n}\nif (!chatId) {\n  try { chatId = $('Buscar_Eventos_Calendario').first()?.json?.chat_id; } catch {}\n}\nif (!chatId) {\n  try { chatId = $('Detector_Calendario_Pre').first()?.json?.chat_id; } catch {}\n}\nif (!chatId) {\n  // Telegram Trigger\n  try { chatId = $('Telegram Trigger - Inicio').first()?.json?.message?.chat?.id; } catch {}\n}\n\n// ---- 3) Limpieza / saneamiento para enviar por Telegram con parse_mode HTML ----\nfunction escapeHTML(s) {\n  // Evita que Telegram interprete etiquetas si usas parse_mode: \"HTML\"\n  return s.replace(/&/g, '&amp;')\n          .replace(/</g, '&lt;')\n          .replace(/>/g, '&gt;');\n}\n\nfunction limpiarRespuesta(texto) {\n  if (!texto) return 'Lo siento, no obtuve una respuesta válida.';\n\n  // Eliminar bloques de código ```...``` (incluye variantes con lenguaje)\n  texto = texto.replace(/```[\\s\\S]*?```/g, '');\n\n  // Eliminar comillas invertidas inline `\n  texto = texto.replace(/`+/g, '');\n\n  // Convertir enlaces markdown [texto](url) → \"texto (url)\"\n  texto = texto.replace(/\\[([^\\]]+)\\]\\((https?:\\/\\/[^\\s)]+)\\)/g, '$1 ($2)');\n\n  // Quitar markdown simple de estilo: **negritas**, *itálicas*, __subrayado__ _\n  texto = texto.replace(/\\*\\*(.*?)\\*\\*/g, '$1')\n               .replace(/\\*(.*?)\\*/g, '$1')\n               .replace(/__(.*?)__/g, '$1')\n               .replace(/_(.*?)_/g, '$1');\n\n  // Quitar encabezados markdown (##, ###, etc.)\n  texto = texto.replace(/^\\s{0,3}#{1,6}\\s+/gm, '');\n\n  // Normalizar saltos y espacios\n  texto = texto.replace(/\\r/g, '')\n               .replace(/\\n{3,}/g, '\\n\\n')\n               .replace(/[ \\t]+\\n/g, '\\n')\n               .trim();\n\n  // Escapar HTML para Telegram (parse_mode: \"HTML\")\n  texto = escapeHTML(texto);\n\n  // Límite prudente para Telegram (4096 es el duro). Dejamos margen.\n  const LIMITE = 3500;\n  if (texto.length > LIMITE) {\n    texto = texto.substring(0, LIMITE - 20).trimEnd() + '… (mensaje acortado)';\n  }\n\n  return texto;\n}\n\nconst textoLimpio = limpiarRespuesta(respuestaGemini);\n\n// ---- 4) Logs útiles ----\nconsole.log(`========================================`);\nconsole.log(`🧹 LIMPIEZA DE TEXTO`);\nconsole.log(`Chat ID encontrado: ${chatId}`);\nconsole.log(`Texto original (100): ${String(respuestaGemini).substring(0, 100)}...`);\nconsole.log(`Texto limpio   (100): ${textoLimpio.substring(0, 100)}...`);\nconsole.log(`========================================`);\n\n// ---- 5) Salida ----\nreturn [{\n  json: {\n    texto_limpio: textoLimpio,\n    chat_id: chatId,\n    respuesta_original: respuestaGemini\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "84c7ffb2-d72d-432a-ac54-9892d31ac5b9",
      "name": "发送关于日历的回复1",
      "type": "n8n-nodes-base.telegram",
      "position": [
        -240,
        2160
      ],
      "webhookId": "c4b9e9ad-3d55-4653-b1eb-e4621c6425d8",
      "parameters": {
        "text": "={{ $json.texto_limpio }}\n\n💬 ¿Deseas revisar otra consulta o hacer una nueva pregunta?",
        "chatId": "={{ $json.chat_id }}",
        "replyMarkup": "inlineKeyboard",
        "inlineKeyboard": {
          "rows": [
            {
              "row": {
                "buttons": [
                  {
                    "text": "👍 Sí",
                    "additionalFields": {
                      "callback_data": "=feedback_yes"
                    }
                  },
                  {
                    "text": "👎 No",
                    "additionalFields": {
                      "callback_data": "=feedback_no"
                    }
                  }
                ]
              }
            }
          ]
        },
        "additionalFields": {
          "parse_mode": "HTML",
          "appendAttribution": false
        }
      },
      "credentials": {
        "telegramApi": {
          "id": "ovdN9afPb1hJiiET",
          "name": "ESPOLBot"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "144e290f-5f0a-400b-b82b-269d90d5f32a",
      "name": "学术日历",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        -1520,
        2160
      ],
      "parameters": {
        "options": {
          "outputFormatting": {
            "values": {
              "date": "FORMATTED_STRING",
              "general": "UNFORMATTED_VALUE"
            }
          },
          "returnFirstMatch": false
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": 1817620056,
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1WWE-eLP0g5M9Q2CeGpQZat56OhJrQyeYZJShjtETCgo/edit#gid=1817620056",
          "cachedResultName": "CALENDAR"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1WWE-eLP0g5M9Q2CeGpQZat56OhJrQyeYZJShjtETCgo",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1WWE-eLP0g5M9Q2CeGpQZat56OhJrQyeYZJShjtETCgo/edit?usp=drivesdk",
          "cachedResultName": "BASE-DATOS-FAQ_ESPOL"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "aXzbiC4KJQoHD5SM",
          "name": "Google Sheets account"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "21267be1-e42b-4362-aefc-42bf96705258",
      "name": "搜索_日历事件1",
      "type": "n8n-nodes-base.code",
      "position": [
        -1264,
        2160
      ],
      "parameters": {
        "jsCode": "const detectorItems = $('Detector_Calendario_Pre').all();\nconst detector =\n  Array.isArray(detectorItems) && detectorItems.length > 0\n    ? detectorItems[0].json\n    : {};\nconst preguntaUsuario = detector.pregunta_usuario || '';\nconst chatId = detector.chat_id ?? null;\nconst filas = $input.all().map(it => it.json || {});\n\nconst HOY = new Date();\nconst ANIO_ACTUAL = HOY.getFullYear();\nconst ANIO_SIG = ANIO_ACTUAL + 1;\n\n// ---------- helpers ----------\nfunction limpiarTexto(t) {\n  if (!t) return '';\n  return String(t)\n    .toLowerCase()\n    .normalize('NFD').replace(/[\\u0300-\\u036f]/g, '')\n    .replace(/[^a-z0-9ñ\\s]/gi, ' ')\n    .replace(/\\s+/g, ' ')\n    .trim();\n}\n\nconst STOP = new Set([\n  \"de\",\"la\",\"que\",\"el\",\"en\",\"y\",\"a\",\"los\",\"del\",\"se\",\"las\",\"por\",\"un\",\"para\",\"con\",\"no\",\"una\",\n  \"su\",\"al\",\"lo\",\"como\",\"mas\",\"más\",\"pero\",\"sus\",\"le\",\"ya\",\"o\",\"fue\",\"ha\",\"si\",\"sí\",\"porque\",\n  \"muy\",\"sin\",\"sobre\",\"tambien\",\"también\",\"entre\",\"cuando\",\"todo\",\"esta\",\"está\",\"ser\",\"son\",\n  \"dos\",\"han\",\"hay\",\"donde\",\"quien\",\"quién\",\"desde\",\"cada\",\"cual\",\"cuál\",\"cuales\",\"cuáles\"\n]);\nfunction tokenizar(t) { return limpiarTexto(t).split(' ').filter(x => x && x.length > 2 && !STOP.has(x)); }\n\nfunction baseEs(w) {\n  w = limpiarTexto(w);\n  if (!w) return '';\n  w = w.replace(/\\b1(ra|era)\\b/g, 'primera')\n       .replace(/\\b2da\\b/g, 'segunda')\n       .replace(/\\b3ra\\b/g, 'tercera');\n  if (w.endsWith('es')) w = w.slice(0, -2);\n  else if (w.endsWith('s')) w = w.slice(0, -1);\n  w = w.replace(/evaluacione?$/,'evaluacion');\n  return w;\n}\nfunction matchToken(userTok, rowTok) {\n  const u = baseEs(userTok), r = baseEs(rowTok);\n  if (!u || !r) return false;\n  if (u === r) return true;\n  if (u.length >= 5 && (r.includes(u) || u.includes(r))) return true;\n  return false;\n}\n\nfunction detectarIntencion(pregunta) {\n  const p = limpiarTexto(pregunta);\n  if (/(vacaciones|receso|feriado|descanso)/.test(p)) return \"vacaciones\";\n  if (/(evaluacion|evaluación|examen|parcial|prueba|evaluar|evaluaciones)/.test(p)) return \"evaluacion\";\n  if (/(inicio|empieza|matriculacion|matriculación|induccion|inducción|apertura|arranque)/.test(p)) return \"inicio\";\n  if (/(final|fin|cierre|termina|culmina|conclusion|conclusión)/.test(p)) return \"fin\";\n  if (/(eleccion|elección|votacion|votación)/.test(p)) return \"elecciones\";\n  return \"otro\";\n}\nfunction detectarOrdinal(p) {\n  const t = limpiarTexto(p);\n  if (/\\b(primera|1ra|1era)\\b/.test(t) || /\\bevaluacion\\s*i\\b/.test(t)) return 1;\n  if (/\\b(segunda|2da)\\b/.test(t)     || /\\bevaluacion\\s*ii\\b/.test(t)) return 2;\n  if (/\\b(tercera|3ra)\\b/.test(t)     || /\\bevaluacion\\s*iii\\b/.test(t)) return 3;\n  return 0;\n}\n\nconst MESES = { ene:0, enero:0, feb:1, febrero:1, mar:2, marzo:2, abr:3, abril:3, may:4, mayo:4, jun:5, junio:5, jul:6, julio:6, ago:7, agosto:7, sep:8, sept:8, septiembre:8, oct:9, octubre:9, nov:10, noviembre:10, dic:11, diciembre:11 };\n\nfunction normalizarPeriodo(p) {\n  if (!p) return '';\n  return String(p).replace(/([0-9])([a-zA-Z])/g, '$1 $2').replace(/\\s*-\\s*/g, ' - ').replace(/\\s+/g, ' ').trim();\n}\nfunction parseFechaInicio(periodo) {\n  const txt = normalizarPeriodo(periodo).toLowerCase();\n\n  // \"13 - 17 julio 2026\"\n  let m = /(\\d{1,2})\\s*-\\s*(\\d{1,2})\\s+([a-záéíóúñ]+)\\s+(\\d{4})/.exec(txt);\n  if (m) {\n    const d1 = parseInt(m[1], 10);\n    const mes = MESES[m[3].normalize('NFD').replace(/[\\u0300-\\u036f]/g, '')];\n    const year = parseInt(m[4], 10);\n    if (mes != null) return new Date(year, mes, d1, 0, 0, 0, 0).getTime();\n  }\n\n  // \"30 marzo - 03 abril 2026\"\n  m = /(\\d{1,2})\\s+([a-záéíóúñ]+)\\s*-\\s*(\\d{1,2})\\s+([a-záéíóúñ]+)\\s+(\\d{4})/.exec(txt);\n  if (m) {\n    const d1 = parseInt(m[1], 10);\n    const mes1 = MESES[m[2].normalize('NFD').replace(/[\\u0300-\\u036f]/g, '')];\n    const year = parseInt(m[5], 10);\n    if (mes1 != null) return new Date(year, mes1, d1, 0, 0, 0, 0).getTime();\n  }\n\n  // sin año → asume actual\n  m = /(\\d{1,2})\\s*-\\s*(\\d{1,2})\\s+([a-záéíóúñ]+)/.exec(txt);\n  if (m) {\n    const d1 = parseInt(m[1], 10);\n    const mes1 = MESES[m[3].normalize('NFD').replace(/[\\u0300-\\u036f]/g, '')];\n    if (mes1 != null) return new Date(ANIO_ACTUAL, mes1, d1, 0, 0, 0, 0).getTime();\n  }\n  m = /(\\d{1,2})\\s+([a-záéíóúñ]+)\\s*-\\s*(\\d{1,2})\\s+([a-záéíóúñ]+)/.exec(txt);\n  if (m) {\n    const d1 = parseInt(m[1], 10);\n    const mes1 = MESES[m[2].normalize('NFD').replace(/[\\u0300-\\u036f]/g, '')];\n    if (mes1 != null) return new Date(ANIO_ACTUAL, mes1, d1, 0, 0, 0, 0).getTime();\n  }\n  return null;\n}\nfunction extraerAniosTexto(texto) {\n  const t = String(texto || '');\n  const m = t.match(/\\b(20\\d{2})\\b/g);\n  if (!m) return new Set();\n  return new Set(m.map(x => parseInt(x, 10)));\n}\nfunction extraerAniosPregunta(p) { return extraerAniosTexto(p); }\nfunction extraerMesesPregunta(p) {\n  const t = limpiarTexto(p);\n  const meses = new Set();\n  Object.keys(MESES).forEach(k => { if (new RegExp(`\\\\b${k}\\\\b`).test(t)) meses.add(MESES[k]); });\n  return meses;\n}\nfunction yearFromTs(ts) { if (ts == null) return null; try { return new Date(ts).getFullYear(); } catch { return null; } }\n\nconst intencion = detectarIntencion(preguntaUsuario);\nconst ordinalBuscado = detectarOrdinal(preguntaUsuario);\nconst tokensUsuario = tokenizar(preguntaUsuario);\nconst setTokens = new Set(tokensUsuario);\nconst aniosPregunta = extraerAniosPregunta(preguntaUsuario);\nconst mesesPregunta = extraerMesesPregunta(preguntaUsuario);\nconst HARD_FILTER_BY_YEAR = aniosPregunta.size > 0;\n\n// ---------- scoring ----------\nconst PESOS = { PERIODO_FECHAS: 0.8, ACTIVIDADES_GRADO: 1.0, PROCESOS_GRADO: 0.6, ACTIVIDADES_FORMACION: 0.9 };\nconst BONUS_INTENCION = 2.2;\nconst BONUS_FRASE_EXACTA = 1.0;\nconst BONUS_TIENE_EVALUACION = 0.7;\nconst BONUS_ORDINAL_MATCH = 1.5;\nconst PENALIZA_ORD_DISTINTO = 0.5;\nconst PENALIZA_SIN_EVAL_CON_ORD = 0.35;\n\nconst BONUS_ANIO_ACTUAL_STRONG = 1.1;\nconst PENALIZA_ANIO_NO_ACTUAL   = 0.6;\nconst BONUS_MATCH_ANIO_PREGUNTA = 1.2;\nconst BONUS_MATCH_MES_ACTUAL    = 0.6;\nconst BONUS_ANIO_EXPLICITO      = 0.3;\n\nconst UMBRAL_SCORE_MIN = 0;\nconst UMBRAL_DEMASIADAS = 25; // ← si hay más que esto, sugerimos refinar\n\nif (!preguntaUsuario || !limpiarTexto(preguntaUsuario)) {\n  return [{ json: { ok:false, motivo:\"pregunta_vacia\", mensaje:\"No se recibió una pregunta (pregunta_usuario).\", chat_id:chatId } }];\n}\nif (!Array.isArray(filas) || filas.length === 0) {\n  return [{ json: { ok:false, motivo:\"sin_datos\", mensaje:\"No llegaron filas desde Google Sheets.\", chat_id:chatId } }];\n}\n\nfunction filaTokens(row) {\n  const F = (k) => { const v = row[k]; return (v == null) ? '' : String(v); };\n  const actTecKey = 'ACTIVIDADES DE FORMACIÓN TÉCNICA Y TECNOLÓGICA';\n  const campos = {\n    row_number: row.row_number ?? null,\n    PERIODO_FECHAS: F('PERIODO_FECHAS'),\n    ACTIVIDADES_GRADO: F('ACTIVIDADES_GRADO'),\n    PROCESOS_GRADO: F('PROCESOS_GRADO'),\n    ACTIVIDADES_FORMACION: F(actTecKey) || F('ACTIVIDADES_FORMACION') || ''\n  };\n  const textoTotal = [campos.PERIODO_FECHAS, campos.ACTIVIDADES_GRADO, campos.PROCESOS_GRADO, campos.ACTIVIDADES_FORMACION].filter(Boolean).join(' | ');\n  const normTotal = limpiarTexto(textoTotal);\n  const toks = {\n    PERIODO_FECHAS: tokenizar(campos.PERIODO_FECHAS),\n    ACTIVIDADES_GRADO: tokenizar(campos.ACTIVIDADES_GRADO),\n    PROCESOS_GRADO: tokenizar(campos.PROCESOS_GRADO),\n    ACTIVIDADES_FORMACION: tokenizar(campos.ACTIVIDADES_FORMACION),\n    TOTAL: tokenizar(textoTotal)\n  };\n  const aniosTexto = extraerAniosTexto(textoTotal);\n  const tsInicio = parseFechaInicio(campos.PERIODO_FECHAS);\n  const anioTs = yearFromTs(tsInicio);\n  const aniosFila = new Set(aniosTexto);\n  if (anioTs != null) aniosFila.add(anioTs);\n  let mesInicio = null;\n  if (tsInicio != null) mesInicio = new Date(tsInicio).getMonth();\n\n  return { campos, textoTotal, normTotal, toks, aniosFila, tsInicio, mesInicio };\n}\nfunction filaTieneOrdinalEvaluacion(norm) {\n  const hasEval = /\\bevaluacion\\b/.test(norm);\n  const primera = hasEval && (/\\bprimera\\b/.test(norm) || /\\bevaluacion\\s*i\\b/.test(norm) || /\\bi\\s*evaluacion\\b/.test(norm));\n  const segunda = hasEval && (/\\bsegunda\\b/.test(norm) || /\\bevaluacion\\s*ii\\b/.test(norm) || /\\bii\\s*evaluacion\\b/.test(norm));\n  const tercera = hasEval && (/\\btercera\\b/.test(norm) || /\\bevaluacion\\s*iii\\b/.test(norm) || /\\biii\\s*evaluacion\\b/.test(norm));\n  return { hasEval, primera, segunda, tercera };\n}\nfunction intersect(setA, setB) { for (const x of setA) if (setB.has(x)) return true; return false; }\n\nfunction puntuar(row) {\n  const { campos, textoTotal, normTotal, toks, aniosFila, tsInicio, mesInicio } = filaTokens(row);\n  if (!normTotal) return null;\n\n  if (HARD_FILTER_BY_YEAR) {\n    if (aniosFila.size > 0 && !intersect(aniosFila, aniosPregunta)) return null;\n    // si no hay año en la fila, la dejamos pasar con score bajo (puedes excluirla retornando null)\n  }\n\n  let score = 0, coincidencias = 0;\n  const matchPorCampo = { PERIODO_FECHAS: [], ACTIVIDADES_GRADO: [], PROCESOS_GRADO: [], ACTIVIDADES_FORMACION: [] };\n  const matchTokens = [];\n\n  for (const t of setTokens) {\n    let hit = false;\n    for (const campo of Object.keys(PESOS)) {\n      const arr = toks[campo];\n      if (arr && arr.some(rt => matchToken(t, rt))) {\n        score += PESOS[campo];\n        matchPorCampo[campo].push(t);\n        hit = true;\n      }\n    }\n    if (hit) { coincidencias++; matchTokens.push(t); }\n  }\n\n  const INTENT_TERMS = {\n    vacaciones: [\"vacaciones\",\"receso\",\"feriado\",\"descanso\",\"estudiantiles\"],\n    evaluacion: [\"evaluacion\",\"evaluaciones\",\"examen\",\"parcial\",\"prueba\",\"ciclo\",\"semana de evaluacion\"],\n    inicio: [\"inicio\",\"empieza\",\"apertura\",\"induccion\",\"matriculacion\",\"novatos\"],\n    fin: [\"final\",\"fin\",\"cierre\",\"termina\",\"culmina\",\"conclusion\",\"proceso final\"],\n    elecciones: [\"eleccion\",\"votacion\"]\n  };\n  if (intencion !== 'otro') {\n    const terms = INTENT_TERMS[intencion] || [];\n    if (terms.some(term => normTotal.includes(limpiarTexto(term)))) score += BONUS_INTENCION;\n  }\n\n  const ord = filaTieneOrdinalEvaluacion(normTotal);\n  if (intencion === 'evaluacion' && ord.hasEval) score += BONUS_TIENE_EVALUACION;\n  if (ordinalBuscado) {\n    const ok = (ordinalBuscado === 1 && ord.primera) || (ordinalBuscado === 2 && ord.segunda) || (ordinalBuscado === 3 && ord.tercera);\n    if (ok) score += BONUS_ORDINAL_MATCH;\n    else {\n      if (ord.primera || ord.segunda || ord.tercera) score -= PENALIZA_ORD_DISTINTO;\n      if (!ord.hasEval) score -= PENALIZA_SIN_EVAL_CON_ORD;\n    }\n  }\n\n  if (HARD_FILTER_BY_YEAR) {\n    if (aniosFila.size > 0 && intersect(aniosFila, aniosPregunta)) score += BONUS_MATCH_ANIO_PREGUNTA;\n  } else {\n    if (aniosFila.size > 0) {\n      if (aniosFila.has(ANIO_ACTUAL)) score += BONUS_ANIO_ACTUAL_STRONG;\n      else score -= PENALIZA_ANIO_NO_ACTUAL;\n    }\n  }\n\n  if (mesesPregunta.size > 0 && mesInicio != null) {\n    if (mesesPregunta.has(mesInicio)) {\n      if (!HARD_FILTER_BY_YEAR && (aniosFila.size === 0 || aniosFila.has(ANIO_ACTUAL))) score += BONUS_MATCH_MES_ACTUAL;\n      else score += BONUS_MATCH_MES_ACTUAL * 0.6;\n    }\n  }\n\n  if (/\\b20\\d{2}\\b/.test(normTotal)) score += BONUS_ANIO_EXPLICITO;\n\n  const frase = limpiarTexto(preguntaUsuario);\n  if (frase && normTotal.includes(frase)) score += BONUS_FRASE_EXACTA;\n\n  const len = normTotal.split(' ').length || 1;\n  score = score / Math.pow(len, 0.03);\n\n  return {\n    row_number: campos.row_number,\n    PERIODO_FECHAS: campos.PERIODO_FECHAS,\n    ACTIVIDADES_GRADO: campos.ACTIVIDADES_GRADO,\n    PROCESOS_GRADO: campos.PROCESOS_GRADO,\n    ACTIVIDADES_FORMACION: campos.ACTIVIDADES_FORMACION,\n    preview: textoTotal,\n    score: Number(score.toFixed(4)),\n    coincidencias,\n    match_tokens: matchTokens,\n    match_por_campo: matchPorCampo,\n    fecha_inicio_ts: tsInicio,\n    mes_inicio: mesInicio,\n    anios_fila: [...aniosFila]\n  };\n}\n\n// ---------- ejecutar y ordenar ----------\nconst evaluadas = [];\nfor (const row of filas) {\n  const r = puntuar(row);\n  if (!r) continue;\n  if (r.score > UMBRAL_SCORE_MIN || r.coincidencias > 0) evaluadas.push(r);\n}\n\nevaluadas.sort((a, b) => {\n  if (b.score !== a.score) return b.score - a.score;\n  if (b.coincidencias !== a.coincidencias) return b.coincidencias - a.coincidencias;\n  const ta = a.fecha_inicio_ts ?? Number.POSITIVE_INFINITY;\n  const tb = b.fecha_inicio_ts ?? Number.POSITIVE_INFINITY;\n  return ta - tb;\n});\n\n// chat_ids válidos (si existen en la data)\nconst chatIds = $input.all()\n  .map(it => it.json?.CHAT_ID)\n  .filter(id => id !== undefined && id !== null && String(id).trim() !== '')\n  .map(id => String(id).trim());\nconst chat_ids_validos = [...new Set(chatIds)];\n\n// ---------- sin resultados ----------\nif (evaluadas.length === 0) {\n  const sugerencias = { email: \"admision@espol.edu.ec\", telefono: \"(04) 2269-269\", web: \"https://www.espol.edu.ec\" };\n  const yearInfo = aniosPregunta.size > 0 ? ` para el/los año(s): ${[...aniosPregunta].join(', ')}` : ` para el año ${ANIO_ACTUAL}`;\n  const mensaje = [\n    `😕 No encontré información específica${yearInfo}.`,\n    \"Puedes comunicarte por:\",\n    `📧 ${sugerencias.email}`,\n    `📞 ${sugerencias.telefono}`,\n    `🌐 ${sugerencias.web}`\n  ].join('\\n');\n\n  return [{ json: {\n    ok: false,\n    consulta: preguntaUsuario,\n    intencion_detectada: intencion,\n    mensaje,\n    sugerencias_contacto: sugerencias,\n    chat_id: chatId,\n    chat_ids_validos\n  }}];\n}\n\n// ---------- construir sugerencias si hay demasiadas coincidencias ----------\nfunction nombreMes(idx) {\n  const nombres = ['enero','febrero','marzo','abril','mayo','junio','julio','agosto','septiembre','octubre','noviembre','diciembre'];\n  return (idx >=0 && idx <=11) ? nombres[idx] : null;\n}\nfunction recolectarFacetas(rows) {\n  const anios = new Set();\n  const meses = new Set();\n  const tiposEval = new Set(); // primera/segunda/tercera\n  const ciclos = new Set();    // Ciclo 1/2/3\n\n  for (const r of rows) {\n    (r.anios_fila || []).forEach(y => anios.add(y));\n    if (typeof r.mes_inicio === 'number') meses.add(r.mes_inicio);\n\n    const norm = limpiarTexto(`${r.PERIODO_FECHAS} ${r.ACTIVIDADES_GRADO} ${r.PROCESOS_GRADO} ${r.ACTIVIDADES_FORMACION}`);\n    if (/\\bprimera\\b/.test(norm) || /\\bevaluacion\\s*i\\b/.test(norm)) tiposEval.add('primera');\n    if (/\\bsegunda\\b/.test(norm) || /\\bevaluacion\\s*ii\\b/.test(norm)) tiposEval.add('segunda');\n    if (/\\btercera\\b/.test(norm) || /\\bevaluacion\\s*iii\\b/.test(norm)) tiposEval.add('tercera');\n\n    const m = norm.match(/\\bciclo\\s*(1|2|3)\\b/);\n    if (m) ciclos.add(`Ciclo ${m[1]}`);\n  }\n\n  return {\n    anios: [...anios].sort((a,b)=>a-b),\n    meses: [...meses].sort((a,b)=>a-b).map(nombreMes).filter(Boolean),\n    tiposEval: [...tiposEval],\n    ciclos: [...ciclos]\n  };\n}\n\nlet sugerencia_reformulacion = null;\nlet filtros_sugeridos = null;\nconst DEMASIADAS = evaluadas.length > UMBRAL_DEMASIADAS;\n\nif (DEMASIADAS) {\n  const facetas = recolectarFacetas(evaluadas);\n  filtros_sugeridos = {\n    posibles_anios: facetas.anios,\n    posibles_meses: facetas.meses,\n    posibles_tipos_evaluacion: facetas.tiposEval,\n    posibles_ciclos: facetas.ciclos\n  };\n\n  const tips = [];\n  if (facetas.anios.length > 0) tips.push(`• especifica año: **${facetas.anios.slice(-3).join(' / ')}**`);\n  if (facetas.meses.length > 0) tips.push(`• añade mes: **${facetas.meses.slice(0,4).join(' / ')}**`);\n  if (facetas.tiposEval.length > 0) tips.push(`• tipo de evaluación: **${facetas.tiposEval.join(' / ')}**`);\n  if (facetas.ciclos.length > 0) tips.push(`• ciclo: **${facetas.ciclos.join(' / ')}**`);\n\n  // ejemplo guiado\n  const ejemplo = [];\n  if (intencion === 'evaluacion') {\n    const ejA = facetas.anios[0] ?? ANIO_ACTUAL;\n    const ejMes = facetas.meses[0] ?? 'julio';\n    ejemplo.push(`Ej.: \"¿Cuándo es la **primera evaluación** de **${ejMes} ${ejA}** en PAE?\"`);\n  } else if (facetas.meses.length > 0 || facetas.anios.length > 0) {\n    const ejA = facetas.anios[0] ?? ANIO_ACTUAL;\n    const ejMes = facetas.meses[0] ?? 'abril';\n    ejemplo.push(`Ej.: \"Actividades formativas de **${ejMes} ${ejA}**\"`);\n  }\n\n  sugerencia_reformulacion =\n    `🔎 He encontrado **${evaluadas.length}** coincidencias. ` +\n    `Para llegar más rápido a lo que necesitas, prueba a refinar tu pregunta:\\n` +\n    (tips.length ? tips.join('\\n') + '\\n' : '') +\n    (ejemplo.length ? ejemplo.join('\\n') : '');\n}\n\n// ---------- mensaje formateado (todas las coincidencias) ----------\nconst encabezado = `📅 Encontré ${evaluadas.length} coincidencia(s) para:\\n“${preguntaUsuario}”`;\nconst cuerpo = evaluadas.map((e, i) => {\n  const partes = [];\n  partes.push(`${i+1}. 📌 ${e.PERIODO_FECHAS || '(sin fecha)'}`);\n  if (e.ACTIVIDADES_GRADO?.trim()) partes.push(`   🎓 Actividades de Grado: ${e.ACTIVIDADES_GRADO}`);\n  if (e.PROCESOS_GRADO?.trim())     partes.push(`   📋 Procesos: ${e.PROCESOS_GRADO}`);\n  if (e.ACTIVIDADES_FORMACION?.trim()) partes.push(`   🔧 Formación Técnica: ${e.ACTIVIDADES_FORMACION}`);\n  return partes.join('\\n');\n}).join('\\n\\n');\n\nconst mensaje_formateado = sugerencia_reformulacion\n  ? `${encabezado}\\n\\n${cuerpo}\\n\\n${sugerencia_reformulacion}`\n  : `${encabezado}\\n\\n${cuerpo}`;\n\nreturn [{\n  json: {\n    ok: true,\n    consulta: preguntaUsuario,\n    intencion_detectada: intencion,\n    ordinal_detectado: detectarOrdinal(preguntaUsuario) || null,\n    anios_detectados_en_pregunta: [...aniosPregunta],\n    meses_detectados_en_pregunta: [...mesesPregunta],\n    total_evaluadas: filas.length,\n    total_encontrados: evaluadas.length,\n    demasiadas_coincidencias: DEMASIADAS,\n    umbral_demasiadas: UMBRAL_DEMASIADAS,\n    filtros_sugeridos,                 // ← facetas para construir botones/quick-replies si quieres\n    resultados: evaluadas,             // ← TODAS las coincidencias ordenadas\n    mensaje_formateado,\n    chat_id: chatId,\n    chat_ids_validos: chat_ids_validos\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "2f04c332-95f8-4251-a7d8-c3183c3bf341",
      "name": "帮助消息",
      "type": "n8n-nodes-base.telegram",
      "position": [
        -880,
        608
      ],
      "webhookId": "cec2b109-dbe4-4246-a86e-ab226fd460a7",
      "parameters": {
        "text": "=🤖 Bienvenido al ChatBot ESPOL\n\n👋 ¡Hola! Soy ChatBot ESPOL, tu asistente virtual de la Escuela Superior Politécnica del Litoral.\nEstoy aquí para responder tus preguntas sobre la universidad: matrículas, eventos, horarios, docentes, servicios estudiantiles y mucho más.\n\n💬 Solo escribe tu duda y te responderé al instante.\nPor ejemplo:\n\n“¿Cuándo empiezan las matrículas?”\n“¿Qué eventos hay esta semana?”\n“¿Dónde puedo contactar a Bienestar Estudiantil?”\n\nTambién puedes usar estos comandos:\n\n/help – Ver los comandos disponibles\n\n/faqs – Acceder a la sección de consultas y preguntas frecuentes\n\n/contact – Mostrar contactos y canales oficiales\n\n/events – Ver próximos eventos\n\n/feedback – Enviar sugerencias o comentarios sobre el bot o los servicios\n\n🎓 Tu información, siempre a un mensaje de distancia.\n¿Sobre qué te gustaría consultar hoy?",
        "chatId": "={{ $json.message.chat.id }}",
        "additionalFields": {
          "parse_mode": "Markdown",
          "appendAttribution": false
        }
      },
      "credentials": {
        "telegramApi": {
          "id": "ovdN9afPb1hJiiET",
          "name": "ESPOLBot"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "58882226-f879-4ae5-b653-025ae55e833f",
      "name": "欢迎",
      "type": "n8n-nodes-base.telegram",
      "position": [
        -880,
        1504
      ],
      "webhookId": "67736a18-8268-4b0b-a904-77b356cf3059",
      "parameters": {
        "text": "=👋🏽 ¡Qué más, politécnico o futuro politécnico! \n\nBienvenido al bot oficial de ayuda para toda la comunidad de la ESPOL 🐢💛💙.  \n\nAquí puedes preguntarme lo que necesites sobre tu vida politécnica o lo que se viene en la U:  \n📅 Fechas importantes del calendario académico. \n🎓 Preguntas frecuentes de todas las facultades — tanto pregrado como postgrado. \n🎟️ Info sobre becas, clubes, cursos, eventos, transporte, servicios y más.  \n\nY pilas 👀, porque cada semana te voy a mandar un recordatorio con lo más relevante que se viene para que no se te pase nada — ni matrícula, ni retiro, ni feriado 😎.  Así que tranqui, compa, pregunta nomás.\n\n¡Vamos con todo, politécnico! 🚀💪",
        "chatId": "={{ $json.message.chat.id }}",
        "additionalFields": {
          "appendAttribution": false
        }
      },
      "credentials": {
        "telegramApi": {
          "id": "ovdN9afPb1hJiiET",
          "name": "ESPOLBot"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "5f1bf72a-e7f4-405f-a1fc-549008b73747",
      "name": "Google Gemini 聊天模型",
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "position": [
        -672,
        1680
      ],
      "parameters": {
        "options": {},
        "modelName": "models/gemini-2.5-flash-lite"
      },
      "credentials": {
        "googlePalmApi": {
          "id": "5akz9ZmFGR6TDmkP",
          "name": "Google Gemini(PaLM) Api account"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "bef601ae-58f3-4785-a49e-4a7225c34ed9",
      "name": "ESPOL数据库",
      "type": "n8n-nodes-base.googleSheetsTool",
      "position": [
        -528,
        1712
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "id",
          "value": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Sheet', ``, 'string') }}"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1WWE-eLP0g5M9Q2CeGpQZat56OhJrQyeYZJShjtETCgo",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1WWE-eLP0g5M9Q2CeGpQZat56OhJrQyeYZJShjtETCgo/edit?usp=drivesdk",
          "cachedResultName": "BASE-DATOS-FAQ_ESPOL"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "aXzbiC4KJQoHD5SM",
          "name": "Google Sheets account"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "c925a437-914c-4e86-8619-2a670ee53c40",
      "name": "指南中的随机问题",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        -672,
        1504
      ],
      "parameters": {
        "text": "=El usuario acaba de iniciar el bot con /start.\nNo le muestres un mensaje de bienvenida.\nSólo sugiérele 5 preguntas al azar (diferentes cada vez) que podría hacerle al bot, relacionadas con la información que está guardada en BD_ESPOL (preguntas frecuentes de facultades, calendario académico, becas, clubes, cursos, eventos, etc.).\n\nEjemplo de formato esperado:\n\n👋🏽 ¡Qué más, futuro politécnico! Bienvenido al bot de ayuda de la ESPOL 🐢💛💙\nAquí puedes preguntar lo que necesites sobre tu vida universitaria o lo que se viene en el semestre.\n\nPuedes empezar con algo como:\n• ¿Cuándo abren las matrículas?\n• ¿Qué clubes hay en la ESPOL?\n• ¿Dónde puedo ver las fechas de titulación?\n• ¿Quien es el coordinador de la FIMCP?\n• ¿Cuando empiezan las vacaciones del IIPAO 2025?\n¡Dale nomás, pregunta lo que necesites que el bot te ayuda al instante! 😎",
        "options": {
          "systemMessage": "=Eres un asistente politécnico amigable y juvenil, que habla con tono guayaco, relajado y positivo. Estás conectado a una base de datos llamada BD_ESPOL, que contiene información sobre preguntas frecuentes, calendario académico, becas, clubes, cursos, eventos, servicios, vida universitaria, transporte, titulación y reglamentos.\n\nTu objetivo es ayudar a los estudiantes y futuros politécnicos a orientarse en la ESPOL.\n\nCada vez que un usuario nuevo inicia conversación, no debes volver a saludar, ya que el bot principal ya lo hizo. Solo debes explicarle brevemente cómo puede usar el bot y mostrarle 5 preguntas aleatorias distintas cada vez que podría intentar para descubrir las funciones del bot.\n\nEl mensaje debe sonar natural, como si fueras un compañero universitario de Guayaquil, usando expresiones relajadas pero respetuosas.\nEvita usar asteriscos, puntos o cualquier formato Markdown. Usa guiones (-) para enumerar las preguntas.\n\nLas preguntas deben cubrir temas variados: calendario académico, becas, facultades, servicios, transporte, vida universitaria, titulación y eventos.\nCada vez que respondas, cambia el orden o tipo de preguntas para que no sean las mismas.\n\nEjemplo de estilo y estructura:\nAquí puedes preguntar sobre fechas, becas, clubes o eventos\nMira, puedes probar con algo como\n\n- ¿Cuándo empiezan las clases?\n\n- ¿Qué becas hay disponibles?\n\n- ¿Dónde veo los cursos extracurriculares?\n\n- ¿Qué eventos hay esta semana?\n\n- ¿Cómo puedo unirme a un club estudiantil?\n\nTermina siempre con una frase motivadora tipo\n\n¡Dale, pregunta nomás!\n\n¡Pruébalo y ve lo bacán que está esto!"
        },
        "promptType": "define"
      },
      "typeVersion": 3
    },
    {
      "id": "b74244ac-ed9a-4979-95ac-d15a620c7df4",
      "name": "等待",
      "type": "n8n-nodes-base.wait",
      "position": [
        -368,
        1504
      ],
      "webhookId": "d38e8f38-68a5-4b15-b8c3-4dce9f102f38",
      "parameters": {},
      "typeVersion": 1.1
    },
    {
      "id": "3104667b-6c32-4a83-9627-23609865000e",
      "name": "如何提问指南",
      "type": "n8n-nodes-base.telegram",
      "position": [
        -208,
        1504
      ],
      "webhookId": "4e6cea20-85ce-4b9a-b931-8fa22d93b87b",
      "parameters": {
        "text": "={{ $json.output }}",
        "chatId": "={{ $('Telegram Trigger - Inicio').item.json.message.chat.id }}",
        "additionalFields": {
          "parse_mode": "HTML",
          "appendAttribution": false
        }
      },
      "credentials": {
        "telegramApi": {
          "id": "ovdN9afPb1hJiiET",
          "name": "ESPOLBot"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "dff25489-2b4d-49da-9398-9efa73bbe8f5",
      "name": "Google Gemini 聊天模型1",
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "position": [
        -3168,
        1840
      ],
      "parameters": {
        "options": {}
      },
      "credentials": {
        "googlePalmApi": {
          "id": "5akz9ZmFGR6TDmkP",
          "name": "Google Gemini(PaLM) Api account"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "7e30adff-e90b-495b-af22-d82ab8a464a3",
      "name": "日历数据库1",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        -3872,
        1664
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": 1817620056,
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1WWE-eLP0g5M9Q2CeGpQZat56OhJrQyeYZJShjtETCgo/edit#gid=1817620056",
          "cachedResultName": "CALENDAR"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1WWE-eLP0g5M9Q2CeGpQZat56OhJrQyeYZJShjtETCgo",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1WWE-eLP0g5M9Q2CeGpQZat56OhJrQyeYZJShjtETCgo/edit?usp=drivesdk",
          "cachedResultName": "BASE-DATOS-FAQ_ESPOL"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "aXzbiC4KJQoHD5SM",
          "name": "Google Sheets account"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "6ee54764-d999-48bc-a498-d4ded389b2f9",
      "name": "公告生成器1",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        -3168,
        1664
      ],
      "parameters": {
        "text": "=Convierte el siguiente texto en un mensaje casual ecuatoriano:\n\nPeriodo: {{ $json[\"RESULTADOS[0].PERIODO_FECHAS\"] }}\nActividades: {{ $json[\"RESULTADOS[0].ACTIVIDADES_GRADO\"] }}\nProcesos: {{ $json[\"RESULTADOS[0].PROCESOS_GRADO\"] }}\nFormacion tecnica: {{ $json['RESULTADOS[0][\\'ACTIVIDADES DE FORMACIÓN TÉCNICA Y TECNOLÓGICA\\']'] }}\n",
        "options": {
          "systemMessage": "=Eres un redactor ecuatoriano experto en comunicación universitaria. Tu tarea es convertir los datos de tres semanas (anterior, actual y siguiente) en un solo mensaje casual, fresco y cercano, como si lo dijera un estudiante politécnico.  \n\nSigue estas reglas:\n\n1. **Prioriza la semana del medio (la actual)**. Es la principal. Describe sus actividades con entusiasmo, usando un tono motivador y natural.\n2. Luego menciona brevemente lo ocurrido **la semana pasada (primer item)**, con frases tipo:  \n   “Si te lo perdiste la semana pasada…” o “Por si no te enteraste…”.\n3. Finalmente, habla de **la semana siguiente (último item)**, pero nunca digas “la próxima semana” a secas.  \n   Siempre menciona explícitamente **la fecha del periodo**, por ejemplo:  \n   “Y para la semana del 17 al 21 de noviembre…”  \n   Usa frases como: “prepárate para”, “se viene”, “atentos que en la semana del…”.\n4. Usa expresiones naturales de Ecuador como *pilas, ñaño, pana, de una, qué bestia, bacán, full, a romperla*.\n5. Separa las secciones con saltos de línea y mantén un estilo cercano y fluido, tipo mensaje de Telegram o redes.\n6. No inventes información fuera del JSON, solo reformula y organiza lo recibido.\n7. El resultado final debe ser un **solo texto en español ecuatoriano**, sin etiquetas, ni formato JSON, ni comillas.\n\nEjemplo de estructura esperada:\n\n¡Qué más, mi gente politécnica! Esta semana del 10 al 14 de noviembre se viene con todo 💪.  \nSeguimos a full con las actividades formativas y los panas de Formación Técnica también están dándole duro. ¡Qué bestia la energía que se siente en el campus!  \n\nSi te lo perdiste la semana pasada, hubo evaluaciones y descansos para recargar pilas. ¡Espero que hayas aprovechado! 😎  \n\nY atentos para la semana del 17 al 21 de noviembre, que llega la PRIMERA EVALUACIÓN del grado. ¡A romperla con todo el ñeque, panas! 📚🔥\n"
        },
        "promptType": "define"
      },
      "typeVersion": 3
    },
    {
      "id": "91ec2456-dae0-4a1c-9e19-883df9771bd8",
      "name": "便签20",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -4208,
        1376
      ],
      "parameters": {
        "color": 5,
        "width": 2224,
        "height": 656,
        "content": "# 📢 **公告生成系统** 🧠"
      },
      "typeVersion": 1
    },
    {
      "id": "e145604d-3454-4370-847b-c88a46e7973f",
      "name": "便利贴21",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -4176,
        1488
      ],
      "parameters": {
        "color": 5,
        "width": 256,
        "height": 368,
        "content": "## 每周提醒"
      },
      "typeVersion": 1
    },
    {
      "id": "bb5ecbad-5350-4010-a658-bd868d7bdcad",
      "name": "便签22",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -3920,
        1488
      ],
      "parameters": {
        "color": 5,
        "width": 208,
        "height": 368,
        "content": "## 日历"
      },
      "typeVersion": 1
    },
    {
      "id": "1b0a4908-baec-4ce6-b482-ce5ac6707fbe",
      "name": "便签23",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -3712,
        1488
      ],
      "parameters": {
        "color": 5,
        "width": 496,
        "height": 368,
        "content": "## 自动化"
      },
      "typeVersion": 1
    },
    {
      "id": "ffcd4a54-58f6-4718-be90-678e020d12b8",
      "name": "便利贴24",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -3216,
        1488
      ],
      "parameters": {
        "color": 5,
        "width": 368,
        "height": 496,
        "content": "## 自动公告"
      },
      "typeVersion": 1
    },
    {
      "id": "65cf8025-fbd1-4c53-892c-3c39af419f6a",
      "name": "便签25",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2848,
        1456
      ],
      "parameters": {
        "color": 5,
        "width": 336,
        "height": 320,
        "content": "## 音频生成器"
      },
      "typeVersion": 1
    },
    {
      "id": "2010027d-9238-4bab-8025-0abcc28dd958",
      "name": "便签26",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2512,
        1408
      ],
      "parameters": {
        "color": 5,
        "width": 352,
        "height": 304,
        "content": "## 语音发送"
      },
      "typeVersion": 1
    },
    {
      "id": "8c041a63-8828-4f90-b670-85e018f73157",
      "name": "发送语音消息1",
      "type": "n8n-nodes-base.telegram",
      "position": [
        -2400,
        1536
      ],
      "webhookId": "ae4eba6e-2e6a-4a50-a4ec-6c527850b3c7",
      "parameters": {
        "file": "={{ $('GENERADOR DE MENSAJE DE VOZ').item.json.URL }}",
        "chatId": "={{ $json.chatId }}",
        "operation": "sendAudio",
        "additionalFields": {
          "caption": "={{ \n(() => {\n  const today = new Date();\n  const dayOfWeek = today.getDay(); // 0=domingo\n  const diffMonday = (dayOfWeek === 0 ? -6 : 1 - dayOfWeek) + 7; // +7 => próxima semana\n  const diffFriday = diffMonday + 4;\n\n  const monday = new Date(today);\n  monday.setDate(today.getDate() + diffMonday);\n\n  const friday = new Date(today);\n  friday.setDate(today.getDate() + diffFriday);\n\n  const fmt = d => `${d.getDate().toString().padStart(2, '0')}/${(d.getMonth() + 1).toString().padStart(2, '0')}`;\n\n  return `Anuncio Semanal - Semana del ${fmt(monday)} al ${fmt(friday)}`;\n})()\n}}\n"
        }
      },
      "credentials": {
        "telegramApi": {
          "id": "ovdN9afPb1hJiiET",
          "name": "ESPOLBot"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "9681b6de-a5db-443e-89a5-e6a927b92b89",
      "name": "发送文本消息1",
      "type": "n8n-nodes-base.telegram",
      "position": [
        -2384,
        1824
      ],
      "webhookId": "fc1657ce-66de-4cae-940f-86f0afae3ae5",
      "parameters": {
        "text": "={{ $json.text }}",
        "chatId": "={{ $json.chatId }}",
        "additionalFields": {
          "appendAttribution": false
        }
      },
      "credentials": {
        "telegramApi": {
          "id": "ovdN9afPb1hJiiET",
          "name": "ESPOLBot"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "245f9062-c877-46f0-9df3-d722308dca44",
      "name": "便签 27",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2512,
        1728
      ],
      "parameters": {
        "color": 5,
        "width": 352,
        "height": 272,
        "content": "## 文本发送"
      },
      "typeVersion": 1
    },
    {
      "id": "67e74e9f-13b3-4f5f-80f4-e35e75558704",
      "name": "优化器",
      "type": "n8n-nodes-base.code",
      "position": [
        -2800,
        1808
      ],
      "parameters": {
        "jsCode": "// Tu array de chat IDs\nconst chatIds = $('ACTIVIDADES DE LA SEMANA1').first().json.CHAT_IDS_VALIDOS; // reemplaza con tus IDs\n\n// Mensaje a enviar\nconst mensaje = $input.first().json.output;\n\n// Creamos un item por cada chat ID\nreturn chatIds.map(id => ({\n    json: {\n        chatId: id,\n        text: mensaje\n    }\n}));\n"
      },
      "typeVersion": 2
    },
    {
      "id": "23d73abe-5b2b-42c5-b50f-fea043f4cd1a",
      "name": "优化器1",
      "type": "n8n-nodes-base.code",
      "position": [
        -2640,
        1584
      ],
      "parameters": {
        "jsCode": "// Tu array de chat IDs\nconst chatIds = $('ACTIVIDADES DE LA SEMANA1').first().json.CHAT_IDS_VALIDOS; // reemplaza con tus IDs\n\n// Mensaje a enviar\nconst mensaje = $input.first().json.output;\n\n// Creamos un item por cada chat ID\nreturn chatIds.map(id => ({\n    json: {\n        chatId: id,\n        text: mensaje\n    }\n}));\n"
      },
      "typeVersion": 2
    },
    {
      "id": "5dfc42b7-8684-4e92-95ca-7e1154345402",
      "name": "语音消息生成器",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -2800,
        1584
      ],
      "parameters": {
        "url": "https://ttsmp3.com/makemp3_new.php",
        "method": "POST",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        },
        "sendBody": true,
        "contentType": "form-urlencoded",
        "bodyParameters": {
          "parameters": [
            {
              "name": "msg",
              "value": "={{ $json.output }}"
            },
            {
              "name": "lang",
              "value": "Mia"
            },
            {
              "name": "source",
              "value": "ttsmp3"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "7008a1aa-c9fb-4777-aac2-63f51cb88d49",
      "name": "获取_ID1",
      "type": "n8n-nodes-base.set",
      "position": [
        -880,
        1712
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "fc9b6408-8e37-4395-935f-33985719dae3",
              "name": "chat_id",
              "type": "number",
              "value": "={{ $json.message.chat.id }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "0310e05e-bc18-48de-952d-2c7e5163ab1f",
      "name": "添加_唯一_ID1",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        -624,
        1856
      ],
      "parameters": {
        "columns": {
          "value": {
            "CHAT_ID": "={{ $json.chat_id }}"
          },
          "schema": [
            {
              "id": "PERIODO_FECHAS",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "PERIODO_FECHAS",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "ACTIVIDADES_GRADO",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "ACTIVIDADES_GRADO",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "PROCESOS_GRADO",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "PROCESOS_GRADO",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "ACTIVIDADES DE FORMACIÓN TÉCNICA Y TECNOLÓGICA",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "ACTIVIDADES DE FORMACIÓN TÉCNICA Y TECNOLÓGICA",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "CHAT_ID",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "CHAT_ID",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "CHAT_ID"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": 1817620056,
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1WWE-eLP0g5M9Q2CeGpQZat56OhJrQyeYZJShjtETCgo/edit#gid=1817620056",
          "cachedResultName": "CALENDAR"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1WWE-eLP0g5M9Q2CeGpQZat56OhJrQyeYZJShjtETCgo",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1WWE-eLP0g5M9Q2CeGpQZat56OhJrQyeYZJShjtETCgo/edit?usp=drivesdk",
          "cachedResultName": "BASE-DATOS-FAQ_ESPOL"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "aXzbiC4KJQoHD5SM",
          "name": "Google Sheets account"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "c797bd9d-e22d-40af-b685-0ecff2c2e838",
      "name": "便签",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -976,
        320
      ],
      "parameters": {
        "color": 5,
        "width": 1040,
        "height": 1712,
        "content": "# 🏷️ **标签系统 - 快速参考** ✨"
      },
      "typeVersion": 1
    },
    {
      "id": "52455d30-745d-4373-a56e-770a643192b7",
      "name": "便签1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -416,
        1680
      ],
      "parameters": {
        "color": 5,
        "width": 432,
        "height": 304,
        "content": "## 👋 **欢迎用户** 🎉"
      },
      "typeVersion": 1
    },
    {
      "id": "97535bd5-a38d-4d93-8eb8-11ea593ec658",
      "name": "便签10",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -3328,
        416
      ],
      "parameters": {
        "color": 5,
        "width": 608,
        "height": 288,
        "content": "## 数据操作和组织"
      },
      "typeVersion": 1
    },
    {
      "id": "6f2891fd-2874-4273-aeeb-5092191df6ec",
      "name": "拆分输出",
      "type": "n8n-nodes-base.splitOut",
      "position": [
        -3056,
        560
      ],
      "parameters": {
        "options": {},
        "fieldToSplitOut": "PERIODOS-FECHAS, ACTIVIDADES_GRADO, PROCESOS_GRADO, ACTIVIDADES_TECNICAS-TECNOLÓGICAS"
      },
      "typeVersion": 1
    },
    {
      "id": "525e2001-f72e-4f12-b0b8-1773498e9298",
      "name": "HTML",
      "type": "n8n-nodes-base.html",
      "position": [
        -3248,
        560
      ],
      "parameters": {
        "options": {},
        "operation": "extractHtmlContent",
        "extractionValues": {
          "values": [
            {
              "key": "PERIODOS-FECHAS",
              "cssSelector": "table tbody tr td:first-child",
              "returnArray": true
            },
            {
              "key": "ACTIVIDADES_GRADO",
              "cssSelector": "table tbody tr td:nth-child(2)",
              "returnArray": true
            },
            {
              "key": "PROCESOS_GRADO",
              "cssSelector": "table tbody tr td:nth-child(3)",
              "returnArray": true
            },
            {
              "key": "ACTIVIDADES_TECNICAS-TECNOLÓGICAS",
              "cssSelector": "table tbody tr td:nth-child(4)",
              "returnArray": true
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "bc233dd1-fa9b-4d75-ab77-4542eb14b535",
      "name": "便签11",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2720,
        416
      ],
      "parameters": {
        "color": 5,
        "width": 256,
        "height": 288,
        "content": "## 数据库"
      },
      "typeVersion": 1
    },
    {
      "id": "f3e79e09-fe05-4b71-a2cd-5810a88e8252",
      "name": "### 替换 Airtable 连接",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -3552,
        416
      ],
      "parameters": {
        "color": 5,
        "width": 224,
        "height": 288,
        "content": "## 网络爬取"
      },
      "typeVersion": 1
    },
    {
      "id": "6a1fe95d-ab71-42a9-8906-dca393412815",
      "name": "便签8",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -3824,
        416
      ],
      "parameters": {
        "color": 5,
        "width": 272,
        "height": 288,
        "content": "## 7天定时器"
      },
      "typeVersion": 1
    },
    {
      "id": "4fc9dc13-c0a9-424e-8fab-873ae62f7c09",
      "name": "日历数据库",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        -2656,
        560
      ],
      "parameters": {
        "columns": {
          "value": {
            "PERIODO_FECHAS": "={{ $json[\"PERIODOS-FECHAS\"] }}",
            "PROCESOS_GRADO": "={{ $json.PROCESOS_GRADO }}",
            "ACTIVIDADES_GRADO": "={{ $json.ACTIVIDADES_GRADO }}",
            "ACTIVIDADES DE FORMACIÓN TÉCNICA Y TECNOLÓGICA": "={{ $json[\"ACTIVIDADES_TECNICAS-TECNOLÓGICAS\"] }}"
          },
          "schema": [
            {
              "id": "PERIODO_FECHAS",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "PERIODO_FECHAS",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "ACTIVIDADES_GRADO",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "ACTIVIDADES_GRADO",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "PROCESOS_GRADO",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "PROCESOS_GRADO",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "ACTIVIDADES DE FORMACIÓN TÉCNICA Y TECNOLÓGICA",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "ACTIVIDADES DE FORMACIÓN TÉCNICA Y TECNOLÓGICA",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "CHAT_ID",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "CHAT_ID",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "PERIODO_FECHAS"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "appendOrUpdate",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": 1817620056,
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1WWE-eLP0g5M9Q2CeGpQZat56OhJrQyeYZJShjtETCgo/edit#gid=1817620056",
          "cachedResultName": "CALENDAR"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1WWE-eLP0g5M9Q2CeGpQZat56OhJrQyeYZJShjtETCgo",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1WWE-eLP0g5M9Q2CeGpQZat56OhJrQyeYZJShjtETCgo/edit?usp=drivesdk",
          "cachedResultName": "BASE-DATOS-FAQ_ESPOL"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "aXzbiC4KJQoHD5SM",
          "name": "Google Sheets account"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "3c9d1e36-949b-41c0-82d3-14fe677e407e",
      "name": "便签12",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -4208,
        320
      ],
      "parameters": {
        "color": 5,
        "width": 2224,
        "height": 1056,
        "content": "# 📅 **日历(通过网络爬取获取)** 🔍"
      },
      "typeVersion": 1
    },
    {
      "id": "db4dfee5-023e-4736-8b12-4f62879a3907",
      "name": "网络爬取",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -3488,
        560
      ],
      "parameters": {
        "url": "https://www.espol.edu.ec/es/vida-politecnica/calendario-grado",
        "options": {
          "timeout": 60000
        },
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "User-Agent",
              "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "d23cb762-8460-41b9-b291-179baa9985d2",
      "name": "日期校正器",
      "type": "n8n-nodes-base.code",
      "position": [
        -2880,
        560
      ],
      "parameters": {
        "jsCode": "// Recibir items\nconst itemsIn = $input.all();\nlet currentYear = '';\nconst results = [];\n\nfunction detectMonth(text) {\n  // Detecta el mes en el texto (minúsculas para evitar errores)\n  const months = {\n    enero: 1, febrero: 2, marzo: 3, abril: 4, mayo: 5, junio: 6,\n    julio: 7, agosto: 8, septiembre: 9, octubre: 10, noviembre: 11, diciembre: 12\n  };\n  const lower = text.toLowerCase();\n  for (const [month, num] of Object.entries(months)) {\n    if (lower.includes(month)) return num;\n  }\n  return null;\n}\n\nfor (const item of itemsIn) {\n  const data = item.json;\n  let period = data[\"PERIODOS-FECHAS\"] ? String(data[\"PERIODOS-FECHAS\"]).trim() : \"\";\n\n  // Detectar si hay un año explícito\n  const yearMatch = period.match(/\\b(20\\d{2})\\b/);\n  if (yearMatch) {\n    currentYear = parseInt(yearMatch[1]);\n  }\n\n  // Si es encabezado, mantener igual\n  if (/^(VACACIONES|PAE|PAO)/i.test(period)) {\n    results.push({ json: { ...data } });\n    continue;\n  }\n\n  // Detectar meses del período\n  const firstMonth = detectMonth(period.split('-')[0] || '');\n  const secondMonth = detectMonth(period.split('-')[1] || '');\n\n  // Si tenemos dos meses y el segundo es menor que el primero → cambio de año\n  if (firstMonth && secondMonth && secondMonth < firstMonth) {\n    currentYear++;\n  }\n\n  // Si no hay año en el texto, agregar el actual\n  if (!/\\b20\\d{2}\\b/.test(period) && currentYear) {\n    period = `${period} ${currentYear}`;\n  }\n\n  results.push({\n    json: {\n      ...data,\n      \"PERIODOS-FECHAS\": period\n    }\n  });\n}\n\nreturn results;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "d3138f0c-9007-4d94-ab5f-308ff5fca401",
      "name": "便签 2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -4144,
        720
      ],
      "parameters": {
        "color": 5,
        "width": 1680,
        "height": 624,
        "content": "# 💾 **数据库** 🗂️"
      },
      "typeVersion": 1
    },
    {
      "id": "aae3673d-ab7d-421c-8955-2321e55717b1",
      "name": "便签 4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1968,
        320
      ],
      "parameters": {
        "color": 3,
        "width": 976,
        "height": 1136,
        "content": "# 💬 流程:用户回复与反馈"
      },
      "typeVersion": 1
    },
    {
      "id": "4bccb5c4-73cf-413b-a8e9-494e8af81b71",
      "name": "便签 3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -672,
        592
      ],
      "parameters": {
        "color": 5,
        "width": 688,
        "height": 128,
        "content": "## 🆘 **帮助 (/HELP)** 💡"
      },
      "typeVersion": 1
    },
    {
      "id": "33fba8d9-8d5a-4328-9f3f-9a847cd1eafc",
      "name": "便签 5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -672,
        768
      ],
      "parameters": {
        "color": 5,
        "width": 688,
        "height": 128,
        "content": "## ❓ **咨询 (/FAQS)** 💬"
      },
      "typeVersion": 1
    },
    {
      "id": "ffb51e14-36fb-4cbf-98e1-7f507786aa3f",
      "name": "便签6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -672,
        944
      ],
      "parameters": {
        "color": 5,
        "width": 688,
        "height": 128,
        "content": "## 📇 **联系人 (/CONTACT)** 📞"
      },
      "typeVersion": 1
    },
    {
      "id": "fced05c4-2d46-4083-98e7-d5e852cea1cd",
      "name": "便签7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -672,
        1120
      ],
      "parameters": {
        "color": 5,
        "width": 688,
        "height": 128,
        "content": "## 📅 **活动 (/EVENTS)** 🎉"
      },
      "typeVersion": 1
    },
    {
      "id": "85c34e8b-d150-493b-9e3c-6bdad3cdb2d9",
      "name": "## 试试看!",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -672,
        1296
      ],
      "parameters": {
        "color": 5,
        "width": 688,
        "height": 128,
        "content": "## 📝 **反馈 (/FEEDBACK)** ⭐"
      },
      "typeVersion": 1
    },
    {
      "id": "f58f8a5d-646c-4ebd-9c43-dfe65a0c6514",
      "name": "便签 14",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1968,
        1456
      ],
      "parameters": {
        "color": 3,
        "width": 976,
        "height": 576,
        "content": "# 🚀 **自动化启动** ⚙️"
      },
      "typeVersion": 1
    },
    {
      "id": "dea1510f-eae5-4f48-9b9d-cf61ba080faa",
      "name": "便签15",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1936,
        1600
      ],
      "parameters": {
        "color": 3,
        "width": 224,
        "height": 304,
        "content": "## Telegram触发器"
      },
      "typeVersion": 1
    },
    {
      "id": "506b453a-061b-4644-b318-237fc1b660b9",
      "name": "便签 16",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1712,
        1536
      ],
      "parameters": {
        "color": 3,
        "width": 384,
        "height": 400,
        "content": "## 消息分类器"
      },
      "typeVersion": 1
    },
    {
      "id": "797b1458-c900-40af-89fb-866cf15e0ee8",
      "name": "切换",
      "type": "n8n-nodes-base.switch",
      "position": [
        -1712,
        1712
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": false,
                  "typeValidation": "loose"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "0e1c1bb4-3315-4632-a9dc-5f917baf41da",
                    "operator": {
                      "type": "boolean",
                      "operation": "true",
                      "singleValue": true
                    },
                    "leftValue": "={{ $json.callback_query !== undefined }}",
                    "rightValue": "0"
                  }
                ]
              }
            },
            {
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": false,
                  "typeValidation": "loose"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "23c2e3a0-e7c8-46de-b504-2cabd445ed54",
                    "operator": {
                      "type": "string",
                      "operation": "contains"
                    },
                    "leftValue": "={{ ($json.message?.reply_to_message?.text || '') }}",
                    "rightValue": "Lamento que la información no te haya sido útil"
                  }
                ]
              }
            },
            {
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": false,
                  "typeValidation": "loose"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "318d7097-ff17-4636-b7bf-958de3da6788",
                    "operator": {
                      "type": "boolean",
                      "operation": "true",
                      "singleValue": true
                    },
                    "leftValue": "={{ $json.message?.text?.startsWith('/') }}",
                    "rightValue": ""
                  }
                ]
              }
            }
          ]
        },
        "options": {
          "ignoreCase": true,
          "fallbackOutput": "extra"
        },
        "looseTypeValidation": true
      },
      "typeVersion": 3.3
    },
    {
      "id": "d69dcc76-b210-4b80-893a-1649643e602d",
      "name": "近期交易",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1328,
        1600
      ],
      "parameters": {
        "color": 3,
        "width": 320,
        "height": 304,
        "content": ""
      },
      "typeVersion": 1
    },
    {
      "id": "76454d3f-9393-486c-a229-696c60ce324e",
      "name": "便利贴18",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -3120,
        720
      ],
      "parameters": {
        "color": 5,
        "width": 528,
        "height": 624,
        "content": "## - **招生** 📝"
      },
      "typeVersion": 1
    },
    {
      "id": "f75dd036-9cc5-4c9d-92ae-34bb89bbf4cd",
      "name": "便签 19",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2592,
        720
      ],
      "parameters": {
        "color": 5,
        "width": 560,
        "height": 624,
        "content": "## - **ESPOL历史** 🏛️"
      },
      "typeVersion": 1
    },
    {
      "id": "6a8a52db-c44d-4174-a92f-cd113b5a0d2a",
      "name": "便签 28",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -4208,
        -192
      ],
      "parameters": {
        "width": 2224,
        "height": 496,
        "content": "# 🤖 **第10组 – Telegram常见问题与学术日历聊天机器人** 📅💬"
      },
      "typeVersion": 1
    },
    {
      "id": "4602e289-d4d7-4dd0-9777-e148b1175a20",
      "name": "便签29",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -3952,
        112
      ],
      "parameters": {
        "width": 1760,
        "height": 80,
        "content": "# ✨ **专注于大学辅助的智能自动化项目 ✨**"
      },
      "typeVersion": 1
    },
    {
      "id": "0cde6cc8-dd35-4fb3-afc9-1ec73921b970",
      "name": "便签30",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2592,
        -16
      ],
      "parameters": {
        "width": 448,
        "height": 96,
        "content": "# 👩‍💻 **NICOLE GUEVARA** 🚀"
      },
      "typeVersion": 1
    },
    {
      "id": "08dc6029-171d-44d5-92b1-eaeb6cc57bc0",
      "name": "便签 31",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -3328,
        -16
      ],
      "parameters": {
        "width": 496,
        "height": 96,
        "content": "# 👩‍💻 **DOMÉNICA AMORES** 💡"
      },
      "typeVersion": 1
    },
    {
      "id": "a35f356d-1afd-4aa9-bfbf-d43ac88909b7",
      "name": "便签32",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -4048,
        -16
      ],
      "parameters": {
        "width": 464,
        "height": 96,
        "content": "# 👨‍💻 **ADRIÁN VILLAMAR** ⚙️"
      },
      "typeVersion": 1
    },
    {
      "id": "e4d2da34-4840-4228-8d25-54cb5325a6f2",
      "name": "便签33",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1936,
        1024
      ],
      "parameters": {
        "color": 2,
        "width": 272,
        "height": 384,
        "content": "## 💡 *此流程允许自动收集用户评论并存储,改善机器人满意度和改进的跟踪。*"
      },
      "typeVersion": 1
    },
    {
      "id": "107e8fe1-081d-4541-a73e-d7aac4e11c11",
      "name": "便签34",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -4208,
        2048
      ],
      "parameters": {
        "width": 4272,
        "height": 672,
        "content": ""
      },
      "typeVersion": 1
    },
    {
      "id": "ae2d015b-3691-459b-b4c8-2ad948c176ba",
      "name": "便签35",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2192,
        2064
      ],
      "parameters": {
        "width": 2224,
        "height": 608,
        "content": "#      🤖 **自动化和个性化消息生成系统** 💬"
      },
      "typeVersion": 1
    },
    {
      "id": "090a8f45-775a-45be-afe5-ada7e40f426c",
      "name": "便签36",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -4160,
        2064
      ],
      "parameters": {
        "width": 1904,
        "height": 608,
        "content": "# 🧠 **自动化和个性化消息生成的大脑**"
      },
      "typeVersion": 1
    },
    {
      "id": "876d72d2-2dbc-44a8-a3f3-98b6fdece7a4",
      "name": "便签37",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1968,
        -192
      ],
      "parameters": {
        "width": 2032,
        "height": 496,
        "content": "# 🔗 机器人和数据库链接"
      },
      "typeVersion": 1
    },
    {
      "id": "207caeb2-0dad-4a90-8cb7-f943663ecfc6",
      "name": "JavaScript1 中的代码",
      "type": "n8n-nodes-base.code",
      "position": [
        -3360,
        1664
      ],
      "parameters": {
        "jsCode": "const semanas = $input.all();\n\n// Inicializamos un objeto vacío\nconst combinado = {};\n\n// Recorremos todas las semanas y sus claves\nfor (const semana of semanas) {\n  const datos = semana.json;\n  for (const [clave, valor] of Object.entries(datos)) {\n    if (!combinado[clave]) combinado[clave] = [];\n    if (valor !== undefined && valor !== null && valor !== \"\")\n      combinado[clave].push(String(valor).trim());\n  }\n}\n\n// Unimos los valores repetidos en un solo string, separados por comas\nfor (const clave of Object.keys(combinado)) {\n  combinado[clave] = [...new Set(combinado[clave])].join(\", \");\n}\n\n// Devolvemos un solo item con todo unido\nreturn [{ json: combinado }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "2760029c-b039-4aab-95bd-005caeead0b6",
      "name": "分割输出2",
      "type": "n8n-nodes-base.splitOut",
      "position": [
        -3504,
        1664
      ],
      "parameters": {
        "options": {},
        "fieldToSplitOut": "RESULTADOS[0].PERIODO_FECHAS, RESULTADOS[0].ACTIVIDADES_GRADO, RESULTADOS[0].PROCESOS_GRADO, RESULTADOS[0]['ACTIVIDADES DE FORMACIÓN TÉCNICA Y TECNOLÓGICA']"
      },
      "typeVersion": 1
    },
    {
      "id": "9dff1b64-2d74-4de5-8dee-cfcebfcd3977",
      "name": "ACTIVIDADES DE LA SEMANA1",
      "type": "n8n-nodes-base.code",
      "position": [
        -3664,
        1664
      ],
      "parameters": {
        "jsCode": "// 🗓️ FECHA ACTUAL Y AÑO\nconst hoy = new Date();\nconst anioActual = hoy.getFullYear();\n\n// ✅ FUNCIÓN PARA CALCULAR EL LUNES DE UNA SEMANA RELATIVA\nfunction obtenerLunes(base, desplazamientoSemanas = 0) {\n  const dia = base.getDay(); // 0=domingo, 1=lunes...\n  const lunes = new Date(base);\n  const diff = (1 - dia + 7) % 7; // hasta lunes\n  lunes.setDate(base.getDate() + diff + desplazamientoSemanas * 7);\n  return lunes;\n}\n\n// 📆 CALCULAR LAS TRES SEMANAS CLAVE\nconst lunesSemanaAnterior = obtenerLunes(hoy, -1);\nconst lunesProximaSemana = obtenerLunes(hoy, 0);\nconst lunesSemanaPosterior = obtenerLunes(hoy, 1);\n\n// 🧠 FUNCIÓN PARA INTERPRETAR RANGOS TIPO “09 - 13 NOVIEMBRE” O “9-13nov”\nfunction parsearRango(rango) {\n  if (!rango || typeof rango !== \"string\") return null;\n\n  rango = rango\n    .replace(/([0-9])([a-zA-ZñÑ])/g, \"$1 $2\")\n    .replace(/\\s+/g, \" \")\n    .trim()\n    .toLowerCase();\n\n  const match = rango.match(/(\\d{1,2})\\s*-\\s*(\\d{1,2})\\s*([a-zñ]+)/);\n  if (!match) return null;\n\n  const [, inicioStr, finStr, mesStr] = match;\n  const inicio = parseInt(inicioStr);\n  const fin = parseInt(finStr);\n\n  const meses = {\n    ene: 0, enero: 0,\n    feb: 1, febrero: 1,\n    mar: 2, marzo: 2,\n    abr: 3, abril: 3,\n    may: 4, mayo: 4,\n    jun: 5, junio: 5,\n    jul: 6, julio: 6,\n    ago: 7, agosto: 7,\n    sep: 8, sept: 8, septiembre: 8,\n    oct: 9, octubre: 9,\n    nov: 10, noviembre: 10,\n    dic: 11, diciembre: 11\n  };\n\n  const mesNum = meses[mesStr];\n  if (mesNum === undefined) return null;\n\n  let anioRango = anioActual;\n  if (mesNum === 0 && hoy.getMonth() === 11) {\n    anioRango = anioActual + 1;\n  }\n\n  const inicioFecha = new Date(anioRango, mesNum, inicio);\n  const finFecha = new Date(anioRango, mesNum, fin);\n  return { inicio: inicioFecha, fin: finFecha };\n}\n\n// 🔍 FUNCIÓN PARA BUSCAR COINCIDENCIA DE SEMANA\nfunction filtrarPorSemana(items, lunesReferencia) {\n  const resultados = [];\n  for (const item of items) {\n    const rango = parsearRango(item.json.PERIODO_FECHAS);\n    if (!rango) continue;\n\n    const mismoInicio =\n      rango.inicio.getFullYear() === lunesReferencia.getFullYear() &&\n      rango.inicio.getDate() === lunesReferencia.getDate() &&\n      rango.inicio.getMonth() === lunesReferencia.getMonth();\n\n    if (mismoInicio) resultados.push(item);\n  }\n  return resultados;\n}\n\n// 📊 OBTENER RESULTADOS PARA CADA SEMANA\nconst resultadosAnterior = filtrarPorSemana(items, lunesSemanaAnterior);\nconst resultadosActual = filtrarPorSemana(items, lunesProximaSemana);\nconst resultadosPosterior = filtrarPorSemana(items, lunesSemanaPosterior);\n\n// 🧹 CHAT_IDS VÁLIDOS (ÚNICOS)\nconst chatIds = items\n  .map(item => item.json.CHAT_ID)\n  .filter(id => id !== undefined && id !== null && String(id).trim() !== \"\")\n  .map(id => String(id).trim());\n\nconst uniqueChatIds = [...new Set(chatIds)];\n\n// 🏁 SALIDA FINAL\nreturn [\n  {\n    json: {\n      SEMANA: \"ANTERIOR\",\n      LUNES_SEMANA: lunesSemanaAnterior.toISOString().split(\"T\")[0],\n      RESULTADOS: resultadosAnterior.map(i => i.json),\n      CHAT_IDS_VALIDOS: uniqueChatIds\n    }\n  },\n  {\n    json: {\n      SEMANA: \"ACTUAL / PRÓXIMA\",\n      LUNES_SEMANA: lunesProximaSemana.toISOString().split(\"T\")[0],\n      RESULTADOS: resultadosActual.map(i => i.json),\n      CHAT_IDS_VALIDOS: uniqueChatIds\n    }\n  },\n  {\n    json: {\n      SEMANA: \"POSTERIOR\",\n      LUNES_SEMANA: lunesSemanaPosterior.toISOString().split(\"T\")[0],\n      RESULTADOS: resultadosPosterior.map(i => i.json),\n      CHAT_IDS_VALIDOS: uniqueChatIds\n    }\n  }\n];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "03a100d6-1492-4834-a638-653ff0a235ca",
      "name": "ANUNCIO SEMANAL",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -4096,
        1680
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtHour": 19
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "af582ce7-df08-42dc-b4e6-e04bbc086c21",
      "name": "ACTUALIZAR CALENDARIO",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -3728,
        560
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "daysInterval": 7
            }
          ]
        }
      },
      "typeVersion": 1.2
    }
  ],
  "active": false,
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "1f231775-d723-4123-a5cf-9178e15e246d",
  "connections": {
    "HTML": {
      "main": [
        [
          {
            "node": "Split Out",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait": {
      "main": [
        [
          {
            "node": "GUIA DE COMO PREGUNTAR",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Switch": {
      "main": [
        [
          {
            "node": "Respuesta Usuario1",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Guardado en cvs Feedback",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Comandos1",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Detector_Calendario_Pre",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "BD_ESPOL": {
      "ai_tool": [
        [
          {
            "node": "PREGUNTAS AL AZAR DE GUIA",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Comandos1": {
      "main": [
        [
          {
            "node": "Mensaje Help",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "FAQs",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Respuesta Contact",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Respuesta Events",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Inicio - Feedback",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "BIENVENIDA",
            "type": "main",
            "index": 0
          },
          {
            "node": "OBTENER_ID1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Out": {
      "main": [
        [
          {
            "node": "CORRECTOR DE FECHAS",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "BIENVENIDA": {
      "main": [
        [
          {
            "node": "PREGUNTAS AL AZAR DE GUIA",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get a chat": {
      "main": [
        [
          {
            "node": "Sí - Agradecimiento1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SCRRAPPING": {
      "main": [
        [
          {
            "node": "HTML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Out2": {
      "main": [
        [
          {
            "node": "Code in JavaScript1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OBTENER_ID1": {
      "main": [
        [
          {
            "node": "AGREGA ID_UNICAS1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OPTIMIZADOR": {
      "main": [
        [
          {
            "node": "ENVIO DE MENSAJE DE TEXTO1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OPTIMIZADOR1": {
      "main": [
        [
          {
            "node": "ENVIO DE MENSAJE DE VOZ1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "BD_CALENDARIO1": {
      "main": [
        [
          {
            "node": "ACTIVIDADES DE LA SEMANA1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "ANUNCIO SEMANAL": {
      "main": [
        [
          {
            "node": "BD_CALENDARIO1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Message a model1": {
      "main": [
        [
          {
            "node": "Limpiar_Texto_Gemini1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Inicio - Feedback": {
      "main": [
        []
      ]
    },
    "Mensaje de Gemini": {
      "main": [
        [
          {
            "node": "Limpiar texto Gemini",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Respuesta Usuario1": {
      "main": [
        [
          {
            "node": "Get a chat",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "No - Mensaje Feedback1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Switch_Que_BD_Leer": {
      "main": [
        [
          {
            "node": "Calendario Académico",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Leer FAQs de MongoDB",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "CORRECTOR DE FECHAS": {
      "main": [
        [
          {
            "node": "BD-CALENDARIO",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code in JavaScript1": {
      "main": [
        [
          {
            "node": "GENERADOR DE ANUNCIOS1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Leer FAQs de MongoDB": {
      "main": [
        [
          {
            "node": "Buscar_FAQs_Relevantes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Limpiar texto Gemini": {
      "main": [
        [
          {
            "node": "Enviar Respuesta sobre faqs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "ACTUALIZAR CALENDARIO": {
      "main": [
        [
          {
            "node": "SCRRAPPING",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calendario Académico": {
      "main": [
        [
          {
            "node": "Buscar_Eventos_Calendario1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Limpiar_Texto_Gemini1": {
      "main": [
        [
          {
            "node": "Enviar Respuesta sobre calendario1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Buscar_FAQs_Relevantes": {
      "main": [
        [
          {
            "node": "Construir_Prompt_Gemini1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GENERADOR DE ANUNCIOS1": {
      "main": [
        [
          {
            "node": "OPTIMIZADOR",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "No - Mensaje Feedback1": {
      "main": [
        []
      ]
    },
    "Detector_Calendario_Pre": {
      "main": [
        [
          {
            "node": "Switch_Que_BD_Leer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Construir_Prompt_Gemini1": {
      "main": [
        [
          {
            "node": "Mensaje de Gemini",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Gemini Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "PREGUNTAS AL AZAR DE GUIA",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Guardado en cvs Feedback": {
      "main": [
        []
      ]
    },
    "ACTIVIDADES DE LA SEMANA1": {
      "main": [
        [
          {
            "node": "Split Out2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Gemini Chat Model1": {
      "ai_languageModel": [
        [
          {
            "node": "GENERADOR DE ANUNCIOS1",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "PREGUNTAS AL AZAR DE GUIA": {
      "main": [
        [
          {
            "node": "Wait",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Telegram Trigger - Inicio": {
      "main": [
        [
          {
            "node": "Switch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Buscar_Eventos_Calendario1": {
      "main": [
        [
          {
            "node": "Construir_Prompt_Calendario1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Enviar Respuesta sobre faqs": {
      "main": [
        []
      ]
    },
    "GENERADOR DE MENSAJE DE VOZ": {
      "main": [
        [
          {
            "node": "OPTIMIZADOR1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Construir_Prompt_Calendario1": {
      "main": [
        [
          {
            "node": "Message a model1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
常见问题

如何使用这个工作流?

复制上方的 JSON 配置代码,在您的 n8n 实例中创建新工作流并选择「从 JSON 导入」,粘贴配置后根据需要修改凭证设置即可。

这个工作流适合什么场景?

高级 - 客服机器人, AI 聊天机器人

需要付费吗?

本工作流完全免费,您可以直接导入使用。但请注意,工作流中使用的第三方服务(如 OpenAI API)可能需要您自行付费。

工作流信息
难度等级
高级
节点数量91
分类2
节点类型17
难度说明

适合高级用户,包含 16+ 个节点的复杂工作流

作者
Club de Inteligencia Artificial Politécnico CIAP

Club de Inteligencia Artificial Politécnico CIAP

@ciap
外部链接
在 n8n.io 查看

分享此工作流