8
n8n 中文网amn8n.com

营养追踪与饮食记录(Telegram、Gemini AI和Google Sheets)

高级

这是一个AI Chatbot, Multimodal AI领域的自动化工作流,包含 55 个节点。主要使用 If, Set, Code, Merge, Switch 等节点。 营养追踪与饮食记录(Telegram、Gemini AI和Google Sheets)

前置要求
  • Telegram Bot Token
  • Google Sheets API 凭证
  • Google Gemini API Key
工作流预览
可视化展示节点连接关系,支持缩放和平移
导出工作流
复制以下 JSON 配置到 n8n 导入,即可使用此工作流
{
  "meta": {
    "instanceId": "50be75eaab016244f302e16f06394e6613d664bfc61e8cd41452474a0de6a3ee",
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "c142f5dd-fbeb-4b25-a0fe-f684546e6eff",
      "name": "下载语音消息",
      "type": "n8n-nodes-base.telegram",
      "position": [
        -1232,
        1248
      ],
      "webhookId": "d3e9d36d-6402-45d6-9f56-5886e616f9fa",
      "parameters": {
        "fileId": "={{ $('Telegram Trigger').item.json.message.voice.file_id }}",
        "resource": "file",
        "additionalFields": {}
      },
      "credentials": {
        "telegramApi": {
          "id": "rzhkYoexl5hHvqnv",
          "name": "Telegram account"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "e15bb617-61da-4620-936d-15b0b5cf9c59",
      "name": "输入消息路由器1",
      "type": "n8n-nodes-base.switch",
      "position": [
        -1600,
        1312
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "outputKey": "Text",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "fcb767ee-565e-4b56-a54e-6f97f739fc24",
                    "operator": {
                      "type": "string",
                      "operation": "exists",
                      "singleValue": true
                    },
                    "leftValue": "={{ $('Telegram Trigger').item.json.message.text }}",
                    "rightValue": ""
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "Voice Message",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "c1016c40-f8f2-4e08-8ec8-5cdb88f5c87a",
                    "operator": {
                      "type": "object",
                      "operation": "exists",
                      "singleValue": true
                    },
                    "leftValue": "={{ $('Telegram Trigger').item.json.message.voice }}",
                    "rightValue": ""
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "Image",
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "f8150ac7-eea4-4658-8da9-f7a1c88a471d",
                    "operator": {
                      "type": "string",
                      "operation": "exists",
                      "singleValue": true
                    },
                    "leftValue": "={{ $('Telegram Trigger').item.json.message.photo[0].file_id }}",
                    "rightValue": ""
                  }
                ]
              },
              "renameOutput": true
            }
          ]
        },
        "options": {
          "ignoreCase": false,
          "fallbackOutput": "extra",
          "allMatchingOutputs": true
        }
      },
      "typeVersion": 3.2
    },
    {
      "id": "68f15a08-cbe2-4ad9-9cfe-8b7a7c60787c",
      "name": "获取消息(文本)",
      "type": "n8n-nodes-base.set",
      "position": [
        -1232,
        1088
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "801ec600-22ad-4a94-a2b4-ae72eb271df0",
              "name": "message",
              "type": "string",
              "value": "={{ $('Telegram Trigger').item.json.message.text }}"
            },
            {
              "id": "263071fb-bcdf-42b0-bb46-71b75fa0bf2a",
              "name": "chat_id",
              "type": "string",
              "value": "={{ $('Telegram Trigger').item.json.message.chat.id }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "6561a9a0-2d00-424d-ba86-8852dcb7e935",
      "name": "下载图片",
      "type": "n8n-nodes-base.telegram",
      "position": [
        -1232,
        1424
      ],
      "webhookId": "b4a21f4c-e6a6-4c83-9e8e-af73cb7cc96a",
      "parameters": {
        "fileId": "={{ $('Telegram Trigger').item.json.message.photo[3]?.file_id || $('Telegram Trigger').item.json.message.photo[2]?.file_id || $('Telegram Trigger').item.json.message.photo[1]?.file_id }}",
        "resource": "file",
        "additionalFields": {}
      },
      "credentials": {
        "telegramApi": {
          "id": "rzhkYoexl5hHvqnv",
          "name": "Telegram account"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "b975dcce-68d8-4e3d-b50e-330f3f63812d",
      "name": "获取所有 img_url",
      "type": "@n8n/n8n-nodes-langchain.googleGemini",
      "position": [
        -896,
        1424
      ],
      "parameters": {
        "text": "=You are a Nutrition Vision Assistant. Think like a food scientist and registered dietitian. Reason silently and do not reveal your steps. From a single food photo, identify the meal components, estimate portion weight in grams per component using geometric/visual cues, then compute total calories, protein, carbs, and fat.\n\nEstimation method (internal only; do not output these steps)\n\nIdentify components: list the main foods (e.g., chicken breast, white rice, mixed salad, sauce).\n\nChoose references: map each component to a standard reference food.\n\nEstimate volume/size: use visible objects for scale (plate ≈ 27 cm diameter, fork tines ≈ 3.5 cm, spoon bowl ≈ 5–6 cm). Approximate shapes (cuboid, cylinder, dome) to get volume in ml (≈ cm³).\n\nConvert to grams (densities, g/ml): meats 1.05; cooked rice 0.66; cooked pasta 0.60; potato/solid starchy veg 0.80; leafy salad 0.15; sauces creamy 1.00; oils 0.91. If the image clearly suggests deep-fried or glossy/oily coating, account for added oil.\n\nMacros & energy per 100 g (reference values):\n\nWhite rice, cooked: 130 kcal, P 2.7, C 28, F 0.3\n\nPasta, cooked: 131 kcal, P 5.0, C 25, F 1.1\n\nChicken breast, cooked skinless: 165 kcal, P 31, C 0, F 3.6\n\nSalmon, cooked: 208 kcal, P 20, C 0, F 13\n\nLean ground beef (≈10% fat), cooked: 217 kcal, P 26, C 0, F 12\n\nBlack beans, cooked: 132 kcal, P 8.9, C 23.7, F 0.5\n\nPotato, baked: 93 kcal, P 2.5, C 21, F 0.1\n\nLettuce/leafy salad: 15 kcal, P 1.4, C 2.9, F 0.2\n\nAvocado: 160 kcal, P 2, C 9, F 15\n\nBread (white): 265 kcal, P 9, C 49, F 3.2\n\nEgg, cooked: 155 kcal, P 13, C 1.1, F 11\n\nCheddar cheese: 403 kcal, P 25, C 1.3, F 33\n\nOlive oil: 884 kcal, P 0, C 0, F 100\n(If a food is not listed, pick the closest standard equivalent.)\n\nHidden oil & sauces: if pan-fried or visibly glossy, add ~1 tablespoon oil = 13.5 g = 120 kcal = 13.5 g fat per clearly coated serving; adjust by visual coverage.\n\nSum totals: compute grams per component × (per-100 g macros/energy) and add all components.\n\nValidation: enforce Calories ≈ 4×Protein + 4×Carbs + 9×Fat. If off by >8%, adjust fat first (oil/sauce most variable), then carbs (starches), keeping protein consistent with visible lean mass.\n\nRounding: round all final totals to integers. Never output ranges or decimals.\n\nOutput rules (must follow exactly)\n\nPlain text only.\n\nUse this exact structure and field order.\n\nValues are numbers only (no units, no “g” or “kcal”), no extra text, no JSON, no notes.\n\nMeal Description: [short description]\nCalories: [number]\nProteins: [number]\nCarbs: [number]\nFat: [number]",
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "models/gemini-2.5-pro",
          "cachedResultName": "models/gemini-2.5-pro"
        },
        "options": {},
        "resource": "image",
        "inputType": "binary",
        "operation": "analyze"
      },
      "credentials": {
        "googlePalmApi": {
          "id": "to92mdfNe3L6sBae",
          "name": "Google Gemini(PaLM) Api account"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "04b43855-1600-424d-a2ae-458fb72a0525",
      "name": "分析语音消息",
      "type": "@n8n/n8n-nodes-langchain.googleGemini",
      "position": [
        -896,
        1248
      ],
      "parameters": {
        "text": "What's in this audio message from telegram user?",
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "models/gemini-2.5-pro",
          "cachedResultName": "models/gemini-2.5-pro"
        },
        "options": {},
        "resource": "audio",
        "inputType": "binary",
        "operation": "analyze"
      },
      "credentials": {
        "googlePalmApi": {
          "id": "to92mdfNe3L6sBae",
          "name": "Google Gemini(PaLM) Api account"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "3d5d43cc-4639-4d46-a095-b6de79d3aede",
      "name": "获取消息(音频/视频消息)",
      "type": "n8n-nodes-base.set",
      "position": [
        -720,
        1248
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "d8935452-fe20-469d-a68d-1aad056cb8dd",
              "name": "message",
              "type": "string",
              "value": "=Voice message description:{{ $json.candidates?.[0]?.content?.parts?.[0]?.text || $json.content?.parts?.[0]?.text }}"
            },
            {
              "id": "93f1bba1-1180-404a-93ca-c34cf1d1b7ac",
              "name": "chat_id",
              "type": "string",
              "value": "={{ $('Telegram Trigger').item.json.message.chat.id }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "60e92d69-3fd6-4311-ba91-ea445adf9727",
      "name": "获取消息(媒体消息)",
      "type": "n8n-nodes-base.set",
      "position": [
        -720,
        1424
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "d8935452-fe20-469d-a68d-1aad056cb8dd",
              "name": "message",
              "type": "string",
              "value": "={{ $json.content.parts[0].text }}"
            },
            {
              "id": "53e34499-7dad-4f94-aa7d-f778321f13f4",
              "name": "chat_id",
              "type": "string",
              "value": "={{ $('Telegram Trigger').item.json.message.chat.id }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "563fd16d-25d2-4496-94eb-d6a8be600e92",
      "name": "正在输入…",
      "type": "n8n-nodes-base.telegram",
      "position": [
        -1984,
        1200
      ],
      "webhookId": "412793ca-7cad-4a84-acea-98debbbfa2ac",
      "parameters": {
        "chatId": "={{ $json.message.chat.id }}",
        "operation": "sendChatAction"
      },
      "credentials": {
        "telegramApi": {
          "id": "rzhkYoexl5hHvqnv",
          "name": "Telegram account"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "a07eaf7d-8e67-41c8-b5bd-367fbc5917f1",
      "name": "修复MIME类型",
      "type": "n8n-nodes-base.code",
      "position": [
        -1056,
        1248
      ],
      "parameters": {
        "jsCode": "// --- Mapa Extendido de Tipos MIME ---\n// Una lista completa para cubrir la mayoría de los formatos de archivo comunes.\nconst mimeMap = {\n  // --- Formatos de Documentos ---\n  'pdf': 'application/pdf',\n  'txt': 'text/plain',\n  'rtf': 'application/rtf',\n  'csv': 'text/csv',\n  'html': 'text/html',\n  'htm': 'text/html',\n  'json': 'application/json',\n  'xml': 'application/xml', // 'text/xml' también es válido pero 'application/xml' es más común\n  'yaml': 'application/x-yaml',\n  'yml': 'application/x-yaml',\n\n  // --- Formatos de Microsoft Office ---\n  'doc': 'application/msword',\n  'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n  'xls': 'application/vnd.ms-excel',\n  'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n  'ppt': 'application/vnd.ms-powerpoint',\n  'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n  'pub': 'application/vnd.ms-publisher',\n\n  // --- Formatos de OpenOffice / LibreOffice ---\n  'odt': 'application/vnd.oasis.opendocument.text',\n  'ods': 'application/vnd.oasis.opendocument.spreadsheet',\n  'odp': 'application/vnd.oasis.opendocument.presentation',\n  'odg': 'application/vnd.oasis.opendocument.graphics',\n\n  // --- Formatos de Apple iWork ---\n  'pages': 'application/vnd.apple.pages',\n  'numbers': 'application/vnd.apple.numbers',\n  'key': 'application/vnd.apple.keynote',\n\n  // --- Formatos de Imagen ---\n  'png': 'image/png',\n  'jpg': 'image/jpeg',\n  'jpeg': 'image/jpeg',\n  'gif': 'image/gif',\n  'webp': 'image/webp',\n  'svg': 'image/svg+xml',\n  'bmp': 'image/bmp',\n  'ico': 'image/vnd.microsoft.icon',\n  'tif': 'image/tiff',\n  'tiff': 'image/tiff',\n  'heic': 'image/heic',\n  'heif': 'image/heif',\n\n  // --- Formatos de Audio ---\n  'mp3': 'audio/mpeg',\n  'wav': 'audio/wav',\n  'oga': 'audio/ogg',\n  'ogg': 'audio/ogg',\n  'flac': 'audio/flac',\n  'm4a': 'audio/mp4',\n  'aac': 'audio/aac',\n  'opus': 'audio/opus',\n  'wma': 'audio/x-ms-wma',\n  'mid': 'audio/midi',\n  'midi': 'audio/midi',\n\n  // --- Formatos de Video ---\n  'mp4': 'video/mp4',\n  'mov': 'video/quicktime',\n  'webm': 'video/webm',\n  'mpeg': 'video/mpeg',\n  'mpg': 'video/mpeg',\n  'avi': 'video/x-msvideo',\n  'wmv': 'video/x-ms-wmv',\n  'flv': 'video/x-flv',\n  'mkv': 'video/x-matroska',\n\n  // --- Formatos de Archivos y Compresión ---\n  'zip': 'application/zip',\n  'rar': 'application/vnd.rar',\n  '7z': 'application/x-7z-compressed',\n  'tar': 'application/x-tar',\n  'gz': 'application/gzip',\n  'bz2': 'application/x-bzip2',\n\n  // --- Otros Formatos ---\n  'epub': 'application/epub+zip',\n  'ics': 'text/calendar',\n  'vcf': 'text/vcard',\n  'js': 'text/javascript',\n  'css': 'text/css',\n  'sh': 'application/x-sh',\n  'py': 'text/x-python',\n};\n\n// --- Lógica de Procesamiento (sin cambios) ---\n\n// Obtenemos todos los items que llegan al nodo\nconst items = $input.all();\n\n// Iteramos sobre cada item para procesarlo\nfor (const item of items) {\n  // Verificamos que el item tenga datos binarios para procesar\n  if (item.binary && item.binary['data']) {\n    // Obtenemos el nombre del archivo de forma segura\n    const fileName = item.binary['data'].fileName || '';\n    if (!fileName) {\n      continue; // Si no hay nombre de archivo, pasamos al siguiente item\n    }\n\n    // Extraemos la extensión del archivo de forma robusta\n    const extension = fileName.slice((fileName.lastIndexOf(\".\") - 1 >>> 0) + 2).toLowerCase();\n\n    // Buscamos la extensión en nuestro mapa\n    const newMimeType = mimeMap[extension];\n\n    // Si encontramos una coincidencia en el mapa, actualizamos el mimeType\n    if (newMimeType) {\n      if(item.binary['data'].mimeType !== newMimeType) {\n        console.log(`Cambiando mimeType para '${fileName}' de '${item.binary['data'].mimeType}' a '${newMimeType}'.`);\n        item.binary['data'].mimeType = newMimeType;\n      }\n    }\n  }\n}\n\n// Devolvemos todos los items, modificados o no\nreturn items;"
      },
      "typeVersion": 2
    },
    {
      "id": "96ff38b5-261b-4359-bfec-8f48db43c005",
      "name": "修复MIME类型5",
      "type": "n8n-nodes-base.code",
      "position": [
        -1056,
        1424
      ],
      "parameters": {
        "jsCode": "// --- Mapa Extendido de Tipos MIME ---\n// Una lista completa para cubrir la mayoría de los formatos de archivo comunes.\nconst mimeMap = {\n  // --- Formatos de Documentos ---\n  'pdf': 'application/pdf',\n  'txt': 'text/plain',\n  'rtf': 'application/rtf',\n  'csv': 'text/csv',\n  'html': 'text/html',\n  'htm': 'text/html',\n  'json': 'application/json',\n  'xml': 'application/xml', // 'text/xml' también es válido pero 'application/xml' es más común\n  'yaml': 'application/x-yaml',\n  'yml': 'application/x-yaml',\n\n  // --- Formatos de Microsoft Office ---\n  'doc': 'application/msword',\n  'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n  'xls': 'application/vnd.ms-excel',\n  'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n  'ppt': 'application/vnd.ms-powerpoint',\n  'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n  'pub': 'application/vnd.ms-publisher',\n\n  // --- Formatos de OpenOffice / LibreOffice ---\n  'odt': 'application/vnd.oasis.opendocument.text',\n  'ods': 'application/vnd.oasis.opendocument.spreadsheet',\n  'odp': 'application/vnd.oasis.opendocument.presentation',\n  'odg': 'application/vnd.oasis.opendocument.graphics',\n\n  // --- Formatos de Apple iWork ---\n  'pages': 'application/vnd.apple.pages',\n  'numbers': 'application/vnd.apple.numbers',\n  'key': 'application/vnd.apple.keynote',\n\n  // --- Formatos de Imagen ---\n  'png': 'image/png',\n  'jpg': 'image/jpeg',\n  'jpeg': 'image/jpeg',\n  'gif': 'image/gif',\n  'webp': 'image/webp',\n  'svg': 'image/svg+xml',\n  'bmp': 'image/bmp',\n  'ico': 'image/vnd.microsoft.icon',\n  'tif': 'image/tiff',\n  'tiff': 'image/tiff',\n  'heic': 'image/heic',\n  'heif': 'image/heif',\n\n  // --- Formatos de Audio ---\n  'mp3': 'audio/mpeg',\n  'wav': 'audio/wav',\n  'oga': 'audio/ogg',\n  'ogg': 'audio/ogg',\n  'flac': 'audio/flac',\n  'm4a': 'audio/mp4',\n  'aac': 'audio/aac',\n  'opus': 'audio/opus',\n  'wma': 'audio/x-ms-wma',\n  'mid': 'audio/midi',\n  'midi': 'audio/midi',\n\n  // --- Formatos de Video ---\n  'mp4': 'video/mp4',\n  'mov': 'video/quicktime',\n  'webm': 'video/webm',\n  'mpeg': 'video/mpeg',\n  'mpg': 'video/mpeg',\n  'avi': 'video/x-msvideo',\n  'wmv': 'video/x-ms-wmv',\n  'flv': 'video/x-flv',\n  'mkv': 'video/x-matroska',\n\n  // --- Formatos de Archivos y Compresión ---\n  'zip': 'application/zip',\n  'rar': 'application/vnd.rar',\n  '7z': 'application/x-7z-compressed',\n  'tar': 'application/x-tar',\n  'gz': 'application/gzip',\n  'bz2': 'application/x-bzip2',\n\n  // --- Otros Formatos ---\n  'epub': 'application/epub+zip',\n  'ics': 'text/calendar',\n  'vcf': 'text/vcard',\n  'js': 'text/javascript',\n  'css': 'text/css',\n  'sh': 'application/x-sh',\n  'py': 'text/x-python',\n};\n\n// --- Lógica de Procesamiento (sin cambios) ---\n\n// Obtenemos todos los items que llegan al nodo\nconst items = $input.all();\n\n// Iteramos sobre cada item para procesarlo\nfor (const item of items) {\n  // Verificamos que el item tenga datos binarios para procesar\n  if (item.binary && item.binary['data']) {\n    // Obtenemos el nombre del archivo de forma segura\n    const fileName = item.binary['data'].fileName || '';\n    if (!fileName) {\n      continue; // Si no hay nombre de archivo, pasamos al siguiente item\n    }\n\n    // Extraemos la extensión del archivo de forma robusta\n    const extension = fileName.slice((fileName.lastIndexOf(\".\") - 1 >>> 0) + 2).toLowerCase();\n\n    // Buscamos la extensión en nuestro mapa\n    const newMimeType = mimeMap[extension];\n\n    // Si encontramos una coincidencia en el mapa, actualizamos el mimeType\n    if (newMimeType) {\n      if(item.binary['data'].mimeType !== newMimeType) {\n        console.log(`Cambiando mimeType para '${fileName}' de '${item.binary['data'].mimeType}' a '${newMimeType}'.`);\n        item.binary['data'].mimeType = newMimeType;\n      }\n    }\n  }\n}\n\n// Devolvemos todos los items, modificados o no\nreturn items;"
      },
      "typeVersion": 2
    },
    {
      "id": "29051108-72c1-49da-8b51-25350affe0de",
      "name": "获取错误消息1",
      "type": "n8n-nodes-base.set",
      "position": [
        -1232,
        1584
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "d8935452-fe20-469d-a68d-1aad056cb8dd",
              "name": "message",
              "type": "string",
              "value": "=It was not possible to process the file.File type not supported."
            },
            {
              "id": "38ba2498-2141-4a04-a22a-64563fe2ee6f",
              "name": "chat_id",
              "type": "string",
              "value": "={{ $('Telegram Trigger').item.json.message.chat.id }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "91c2f800-3c6f-411b-9950-cc8f907dbb00",
      "name": "Telegram 触发器",
      "type": "n8n-nodes-base.telegramTrigger",
      "position": [
        -2208,
        1360
      ],
      "webhookId": "9a941b53-cb32-4fe4-8bc0-4de80ad92c39",
      "parameters": {
        "updates": [
          "message"
        ],
        "additionalFields": {}
      },
      "credentials": {
        "telegramApi": {
          "id": "rzhkYoexl5hHvqnv",
          "name": "Telegram account"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "41cf82ed-b4f4-4224-93b2-e2c96f4c0f4c",
      "name": "发送短信",
      "type": "n8n-nodes-base.telegram",
      "position": [
        304,
        1312
      ],
      "webhookId": "a23f92f8-c4e9-44a5-8c17-e7623f18a851",
      "parameters": {
        "text": "={{ $json.message }}",
        "chatId": "={{ $('Telegram Trigger').item.json.message.chat.id }}",
        "additionalFields": {
          "parse_mode": "MarkdownV2",
          "appendAttribution": false
        }
      },
      "credentials": {
        "telegramApi": {
          "id": "rzhkYoexl5hHvqnv",
          "name": "Telegram account"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "938dbd1b-a566-4bcf-9c9c-7abf90174f8b",
      "name": "Google Gemini聊天模型",
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "position": [
        -304,
        1488
      ],
      "parameters": {
        "options": {}
      },
      "credentials": {
        "googlePalmApi": {
          "id": "to92mdfNe3L6sBae",
          "name": "Google Gemini(PaLM) Api account"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "ad29c874-9686-41aa-af26-5db22169f1dd",
      "name": "简单记忆",
      "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
      "position": [
        -176,
        1504
      ],
      "parameters": {
        "sessionKey": "={{ $json.chat_id }}",
        "sessionIdType": "customKey"
      },
      "typeVersion": 1.3
    },
    {
      "id": "dcbd0b50-3cb5-4e3e-be26-026199676bb7",
      "name": "MarkdownV2",
      "type": "n8n-nodes-base.code",
      "position": [
        144,
        1312
      ],
      "parameters": {
        "jsCode": "/**\n * MarkdownV2-safe formatter + auto-chunker for Telegram (n8n Code node)\n * --------------------------------------------------------------------\n * - Allows: *bold*, _italic_, ||spoiler||, [label](url)\n * - Escapes everything else for Telegram MarkdownV2\n * - Validates/normalizes URLs\n * - Converts \"# Heading\" lines to bold titles\n * - Splits long messages into <= 4096-char chunks (uses a 4000-char budget)\n * - Outputs one item per chunk so the Telegram node sends all parts\n *\n * Recommended: Run this node in \"Run Once for All Items\".\n */\n\nconst MAX_TELEGRAM = 4096;\nconst SAFE_BUDGET = 4000; // small margin to avoid edge overflows\n\n// ============ MarkdownV2 helpers ============\nfunction escapeMarkdownV2(text) {\n  if (!text) return '';\n  return String(text).replace(/([\\\\_*[\\]()~`>#+\\-=|{}.!])/g, '\\\\$1');\n}\n\nfunction escapeForUrl(url) {\n  return String(url).replace(/[)\\\\]/g, '\\\\$&');\n}\n\nfunction normalizeAndValidateUrl(url) {\n  let raw = String(url || '').trim();\n  try {\n    const u = new URL(raw);\n    return u.toString();\n  } catch {}\n  // Try https:// for bare domains\n  const domainLike = /^[a-z0-9.-]+\\.[a-z]{2,}([/:?#].*)?$/i.test(raw);\n  if (domainLike) {\n    try {\n      const u2 = new URL('https://' + raw);\n      return u2.toString();\n    } catch {}\n  }\n  return null;\n}\n\nfunction normalizeHeadings(text) {\n  // Turn \"# Title\" → \"*Title*\"\n  return text.replace(/^(#{1,6})\\s+(.*)$/gm, (m, hashes, title) => `*${title.trim()}*`);\n}\n\nfunction normalizeCommonMd(text) {\n  return String(text)\n    .replace(/\\*\\*([\\s\\S]*?)\\*\\*/g, '*$1*') // **bold** → *bold*\n    .replace(/__([\\s\\S]*?)__/g, '_$1_');    // __italic__ → _italic_\n}\n\n/**\n * Convert incoming text to Telegram-safe MarkdownV2.\n */\nfunction processMarkdownV2Safe(inputText) {\n  if (!inputText) return '';\n\n  let text = normalizeCommonMd(String(inputText));\n  text = normalizeHeadings(text);\n\n  const placeholders = { links: [], bolds: [], italics: [], spoilers: [] };\n\n  // Links: keep safe via placeholders during escaping\n  text = text.replace(/\\[([^\\]\\n]+)\\]\\(([^)]+)\\)/g, (m, label, url) => {\n    const normalizedUrl = normalizeAndValidateUrl(url);\n    if (!normalizedUrl) return escapeMarkdownV2(label);\n    const idx = placeholders.links.length;\n    const ph = `⟬L${idx}⟭`;\n    const safeLabel = escapeMarkdownV2(label);\n    const safeUrl = escapeForUrl(normalizedUrl);\n    placeholders.links.push(`[${safeLabel}](${safeUrl})`);\n    return ph;\n  });\n\n  // Bold\n  text = text.replace(/\\*([\\s\\S]+?)\\*/g, (m, inner) => {\n    const idx = placeholders.bolds.length;\n    const ph = `⟬B${idx}⟭`;\n    placeholders.bolds.push(`*${escapeMarkdownV2(inner)}*`);\n    return ph;\n  });\n\n  // Italic\n  text = text.replace(/_([\\s\\S]+?)_/g, (m, inner) => {\n    const idx = placeholders.italics.length;\n    const ph = `⟬I${idx}⟭`;\n    placeholders.italics.push(`_${escapeMarkdownV2(inner)}_`);\n    return ph;\n  });\n\n  // Spoilers\n  text = text.replace(/\\|\\|([\\s\\S]+?)\\|\\|/g, (m, inner) => {\n    const idx = placeholders.spoilers.length;\n    const ph = `⟬S${idx}⟭`;\n    placeholders.spoilers.push(`||${escapeMarkdownV2(inner)}||`);\n    return ph;\n  });\n\n  // Escape everything else\n  text = escapeMarkdownV2(text);\n\n  // Restore placeholders\n  placeholders.links.forEach((md, i) => { text = text.replace(`⟬L${i}⟭`, md); });\n  placeholders.bolds.forEach((md, i) => { text = text.replace(`⟬B${i}⟭`, md); });\n  placeholders.italics.forEach((md, i) => { text = text.replace(`⟬I${i}⟭`, md); });\n  placeholders.spoilers.forEach((md, i) => { text = text.replace(`⟬S${i}⟭`, md); });\n\n  return text;\n}\n\n// ============ Chunking helpers ============\n/**\n * Split text into Telegram-safe chunks <= maxLen.\n * Prefers paragraph boundaries, then sentence boundaries, then words.\n * Falls back to hard cuts only when unavoidable (e.g., extremely long URL).\n */\nfunction chunkForTelegram(text, maxLen = SAFE_BUDGET) {\n  if (!text || text.length <= maxLen) return [text || ''];\n\n  const parts = [];\n  let buffer = '';\n\n  const flush = () => {\n    if (buffer) {\n      parts.push(buffer);\n      buffer = '';\n    }\n  };\n\n  // 1) Paragraph-level packing\n  const paragraphs = text.split(/\\n{2,}/);\n  for (const pRaw of paragraphs) {\n    const p = pRaw; // keep paragraph as-is\n    const candidate = buffer ? buffer + '\\n\\n' + p : p;\n    if (candidate.length <= maxLen) {\n      buffer = candidate;\n      continue;\n    }\n    if (p.length <= maxLen) {\n      flush();\n      buffer = p;\n      continue;\n    }\n\n    // 2) Sentence-level packing (paragraph is still too big)\n    flush();\n    const sentences = p.split(/(?<=[.!?…])\\s+(?=[^\\s])/u);\n    let sBuf = '';\n    for (const s of sentences) {\n      const sCandidate = sBuf ? sBuf + ' ' + s : s;\n      if (sCandidate.length <= maxLen) {\n        sBuf = sCandidate;\n        continue;\n      }\n      if (s.length <= maxLen) {\n        if (sBuf) parts.push(sBuf);\n        sBuf = s;\n        continue;\n      }\n\n      // 3) Word-level packing (sentence is still too big)\n      if (sBuf) { parts.push(sBuf); sBuf = ''; }\n      let wBuf = '';\n      const words = s.split(/\\s+/);\n      for (const w of words) {\n        const wCandidate = wBuf ? wBuf + ' ' + w : w;\n        if (wCandidate.length <= maxLen) {\n          wBuf = wCandidate;\n          continue;\n        }\n        if (w.length <= maxLen) {\n          if (wBuf) parts.push(wBuf);\n          wBuf = w;\n          continue;\n        }\n        // 4) Hard split (extremely long token, e.g., massive URL)\n        if (wBuf) { parts.push(wBuf); wBuf = ''; }\n        const re = new RegExp(`.{1,${maxLen}}`, 'g');\n        const hardPieces = w.match(re) || [];\n        parts.push(...hardPieces);\n      }\n      if (wBuf) parts.push(wBuf);\n    }\n    if (sBuf) parts.push(sBuf);\n  }\n  if (buffer) parts.push(buffer);\n\n  // Final safety pass: trim chunks that might still exceed MAX_TELEGRAM\n  return parts.flatMap(part => {\n    if (part.length <= MAX_TELEGRAM) return [part];\n    const re = new RegExp(`.{1,${SAFE_BUDGET}}`, 'g');\n    return part.match(re) || [];\n  });\n}\n\n// ============ Main ============\nconst inputItems = $input.all();\nconst out = [];\n\nfor (const item of inputItems) {\n  const j = item.json || {};\n  const raw =\n    j.message ?? j.output ?? j.text ?? j.content ?? '';\n\n  const formatted = processMarkdownV2Safe(raw);\n  const chunks = chunkForTelegram(formatted, SAFE_BUDGET);\n\n  chunks.forEach((chunk, idx) => {\n    out.push({\n      json: {\n        ...j,\n        message: chunk,\n        message_part_index: idx + 1,\n        message_parts_total: chunks.length,\n      },\n      binary: item.binary,\n    });\n  });\n}\n\nreturn out;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "9e4623d1-a1d6-426b-bb97-186edb14f3f5",
      "name": "如果",
      "type": "n8n-nodes-base.if",
      "position": [
        -1808,
        1360
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "b9e63bbf-24e6-424b-ba4e-6acd3f17b57c",
              "operator": {
                "type": "number",
                "operation": "exists",
                "singleValue": true
              },
              "leftValue": "={{ $json.User_ID }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "1d9a7fa5-5b3c-4531-a268-99ec7befc78f",
      "name": "已注册?",
      "type": "n8n-nodes-base.googleSheets",
      "onError": "continueRegularOutput",
      "position": [
        -1984,
        1360
      ],
      "parameters": {
        "options": {},
        "filtersUI": {
          "values": [
            {
              "lookupValue": "={{ $('Telegram Trigger').item.json.message.chat.id }}",
              "lookupColumn": "User_ID"
            }
          ]
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1Dm_PA5AUHaFe4qatsKmBBfLpuxUPLw8xHlNd8Tv9Wpw/edit#gid=0",
          "cachedResultName": "Profile"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1Dm_PA5AUHaFe4qatsKmBBfLpuxUPLw8xHlNd8Tv9Wpw",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1Dm_PA5AUHaFe4qatsKmBBfLpuxUPLw8xHlNd8Tv9Wpw/edit?usp=drivesdk",
          "cachedResultName": "Cal AI"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "mZXXs3Zjc2MVpxXj",
          "name": "Google Sheets account"
        }
      },
      "typeVersion": 4.7,
      "alwaysOutputData": true
    },
    {
      "id": "2a6e36c8-12fc-48c2-a909-40ffbe07f6f9",
      "name": "发送文本消息1",
      "type": "n8n-nodes-base.telegram",
      "position": [
        -896,
        1952
      ],
      "webhookId": "a23f92f8-c4e9-44a5-8c17-e7623f18a851",
      "parameters": {
        "text": "={{ $json.message }}",
        "chatId": "={{ $('Telegram Trigger').item.json.message.chat.id }}",
        "additionalFields": {
          "parse_mode": "MarkdownV2",
          "appendAttribution": false
        }
      },
      "credentials": {
        "telegramApi": {
          "id": "rzhkYoexl5hHvqnv",
          "name": "Telegram account"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "6d1ab7a7-b072-45d0-ad0d-984d09f8b371",
      "name": "Google Gemini聊天模型1",
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "position": [
        -1504,
        2144
      ],
      "parameters": {
        "options": {}
      },
      "credentials": {
        "googlePalmApi": {
          "id": "to92mdfNe3L6sBae",
          "name": "Google Gemini(PaLM) Api account"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "3c36bb0c-84d9-43b2-83c0-69e36226c098",
      "name": "简单内存1",
      "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
      "position": [
        -1344,
        2208
      ],
      "parameters": {
        "sessionKey": "={{ $json.chat_id }}",
        "sessionIdType": "customKey"
      },
      "typeVersion": 1.3
    },
    {
      "id": "f371ad0b-bf0b-4927-a169-9b56cc8fce63",
      "name": "MarkdownV",
      "type": "n8n-nodes-base.code",
      "position": [
        -1056,
        1952
      ],
      "parameters": {
        "jsCode": "/**\n * MarkdownV2-safe formatter + auto-chunker for Telegram (n8n Code node)\n * --------------------------------------------------------------------\n * - Allows: *bold*, _italic_, ||spoiler||, [label](url)\n * - Escapes everything else for Telegram MarkdownV2\n * - Validates/normalizes URLs\n * - Converts \"# Heading\" lines to bold titles\n * - Splits long messages into <= 4096-char chunks (uses a 4000-char budget)\n * - Outputs one item per chunk so the Telegram node sends all parts\n *\n * Recommended: Run this node in \"Run Once for All Items\".\n */\n\nconst MAX_TELEGRAM = 4096;\nconst SAFE_BUDGET = 4000; // small margin to avoid edge overflows\n\n// ============ MarkdownV2 helpers ============\nfunction escapeMarkdownV2(text) {\n  if (!text) return '';\n  return String(text).replace(/([\\\\_*[\\]()~`>#+\\-=|{}.!])/g, '\\\\$1');\n}\n\nfunction escapeForUrl(url) {\n  return String(url).replace(/[)\\\\]/g, '\\\\$&');\n}\n\nfunction normalizeAndValidateUrl(url) {\n  let raw = String(url || '').trim();\n  try {\n    const u = new URL(raw);\n    return u.toString();\n  } catch {}\n  // Try https:// for bare domains\n  const domainLike = /^[a-z0-9.-]+\\.[a-z]{2,}([/:?#].*)?$/i.test(raw);\n  if (domainLike) {\n    try {\n      const u2 = new URL('https://' + raw);\n      return u2.toString();\n    } catch {}\n  }\n  return null;\n}\n\nfunction normalizeHeadings(text) {\n  // Turn \"# Title\" → \"*Title*\"\n  return text.replace(/^(#{1,6})\\s+(.*)$/gm, (m, hashes, title) => `*${title.trim()}*`);\n}\n\nfunction normalizeCommonMd(text) {\n  return String(text)\n    .replace(/\\*\\*([\\s\\S]*?)\\*\\*/g, '*$1*') // **bold** → *bold*\n    .replace(/__([\\s\\S]*?)__/g, '_$1_');    // __italic__ → _italic_\n}\n\n/**\n * Convert incoming text to Telegram-safe MarkdownV2.\n */\nfunction processMarkdownV2Safe(inputText) {\n  if (!inputText) return '';\n\n  let text = normalizeCommonMd(String(inputText));\n  text = normalizeHeadings(text);\n\n  const placeholders = { links: [], bolds: [], italics: [], spoilers: [] };\n\n  // Links: keep safe via placeholders during escaping\n  text = text.replace(/\\[([^\\]\\n]+)\\]\\(([^)]+)\\)/g, (m, label, url) => {\n    const normalizedUrl = normalizeAndValidateUrl(url);\n    if (!normalizedUrl) return escapeMarkdownV2(label);\n    const idx = placeholders.links.length;\n    const ph = `⟬L${idx}⟭`;\n    const safeLabel = escapeMarkdownV2(label);\n    const safeUrl = escapeForUrl(normalizedUrl);\n    placeholders.links.push(`[${safeLabel}](${safeUrl})`);\n    return ph;\n  });\n\n  // Bold\n  text = text.replace(/\\*([\\s\\S]+?)\\*/g, (m, inner) => {\n    const idx = placeholders.bolds.length;\n    const ph = `⟬B${idx}⟭`;\n    placeholders.bolds.push(`*${escapeMarkdownV2(inner)}*`);\n    return ph;\n  });\n\n  // Italic\n  text = text.replace(/_([\\s\\S]+?)_/g, (m, inner) => {\n    const idx = placeholders.italics.length;\n    const ph = `⟬I${idx}⟭`;\n    placeholders.italics.push(`_${escapeMarkdownV2(inner)}_`);\n    return ph;\n  });\n\n  // Spoilers\n  text = text.replace(/\\|\\|([\\s\\S]+?)\\|\\|/g, (m, inner) => {\n    const idx = placeholders.spoilers.length;\n    const ph = `⟬S${idx}⟭`;\n    placeholders.spoilers.push(`||${escapeMarkdownV2(inner)}||`);\n    return ph;\n  });\n\n  // Escape everything else\n  text = escapeMarkdownV2(text);\n\n  // Restore placeholders\n  placeholders.links.forEach((md, i) => { text = text.replace(`⟬L${i}⟭`, md); });\n  placeholders.bolds.forEach((md, i) => { text = text.replace(`⟬B${i}⟭`, md); });\n  placeholders.italics.forEach((md, i) => { text = text.replace(`⟬I${i}⟭`, md); });\n  placeholders.spoilers.forEach((md, i) => { text = text.replace(`⟬S${i}⟭`, md); });\n\n  return text;\n}\n\n// ============ Chunking helpers ============\n/**\n * Split text into Telegram-safe chunks <= maxLen.\n * Prefers paragraph boundaries, then sentence boundaries, then words.\n * Falls back to hard cuts only when unavoidable (e.g., extremely long URL).\n */\nfunction chunkForTelegram(text, maxLen = SAFE_BUDGET) {\n  if (!text || text.length <= maxLen) return [text || ''];\n\n  const parts = [];\n  let buffer = '';\n\n  const flush = () => {\n    if (buffer) {\n      parts.push(buffer);\n      buffer = '';\n    }\n  };\n\n  // 1) Paragraph-level packing\n  const paragraphs = text.split(/\\n{2,}/);\n  for (const pRaw of paragraphs) {\n    const p = pRaw; // keep paragraph as-is\n    const candidate = buffer ? buffer + '\\n\\n' + p : p;\n    if (candidate.length <= maxLen) {\n      buffer = candidate;\n      continue;\n    }\n    if (p.length <= maxLen) {\n      flush();\n      buffer = p;\n      continue;\n    }\n\n    // 2) Sentence-level packing (paragraph is still too big)\n    flush();\n    const sentences = p.split(/(?<=[.!?…])\\s+(?=[^\\s])/u);\n    let sBuf = '';\n    for (const s of sentences) {\n      const sCandidate = sBuf ? sBuf + ' ' + s : s;\n      if (sCandidate.length <= maxLen) {\n        sBuf = sCandidate;\n        continue;\n      }\n      if (s.length <= maxLen) {\n        if (sBuf) parts.push(sBuf);\n        sBuf = s;\n        continue;\n      }\n\n      // 3) Word-level packing (sentence is still too big)\n      if (sBuf) { parts.push(sBuf); sBuf = ''; }\n      let wBuf = '';\n      const words = s.split(/\\s+/);\n      for (const w of words) {\n        const wCandidate = wBuf ? wBuf + ' ' + w : w;\n        if (wCandidate.length <= maxLen) {\n          wBuf = wCandidate;\n          continue;\n        }\n        if (w.length <= maxLen) {\n          if (wBuf) parts.push(wBuf);\n          wBuf = w;\n          continue;\n        }\n        // 4) Hard split (extremely long token, e.g., massive URL)\n        if (wBuf) { parts.push(wBuf); wBuf = ''; }\n        const re = new RegExp(`.{1,${maxLen}}`, 'g');\n        const hardPieces = w.match(re) || [];\n        parts.push(...hardPieces);\n      }\n      if (wBuf) parts.push(wBuf);\n    }\n    if (sBuf) parts.push(sBuf);\n  }\n  if (buffer) parts.push(buffer);\n\n  // Final safety pass: trim chunks that might still exceed MAX_TELEGRAM\n  return parts.flatMap(part => {\n    if (part.length <= MAX_TELEGRAM) return [part];\n    const re = new RegExp(`.{1,${SAFE_BUDGET}}`, 'g');\n    return part.match(re) || [];\n  });\n}\n\n// ============ Main ============\nconst inputItems = $input.all();\nconst out = [];\n\nfor (const item of inputItems) {\n  const j = item.json || {};\n  const raw =\n    j.message ?? j.output ?? j.text ?? j.content ?? '';\n\n  const formatted = processMarkdownV2Safe(raw);\n  const chunks = chunkForTelegram(formatted, SAFE_BUDGET);\n\n  chunks.forEach((chunk, idx) => {\n    out.push({\n      json: {\n        ...j,\n        message: chunk,\n        message_part_index: idx + 1,\n        message_parts_total: chunks.length,\n      },\n      binary: item.binary,\n    });\n  });\n}\n\nreturn out;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "0086d3f2-cc2f-45a8-9db7-a7b1a803700a",
      "name": "获取餐食信息",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        -320,
        1984
      ],
      "parameters": {
        "options": {},
        "filtersUI": {
          "values": [
            {
              "lookupValue": "={{ $json.Date }}",
              "lookupColumn": "Date"
            }
          ]
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": 403788598,
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1Dm_PA5AUHaFe4qatsKmBBfLpuxUPLw8xHlNd8Tv9Wpw/edit#gid=403788598",
          "cachedResultName": "Meals"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1Dm_PA5AUHaFe4qatsKmBBfLpuxUPLw8xHlNd8Tv9Wpw",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1Dm_PA5AUHaFe4qatsKmBBfLpuxUPLw8xHlNd8Tv9Wpw/edit?usp=drivesdk",
          "cachedResultName": "Cal AI"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "Ote8KiToTK9EYOXy",
          "name": "Google Sheets account 2"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "6e61ba86-d1ad-4140-b59d-715fdf7472fa",
      "name": "获取用户信息",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        -304,
        2240
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1Dm_PA5AUHaFe4qatsKmBBfLpuxUPLw8xHlNd8Tv9Wpw/edit#gid=0",
          "cachedResultName": "Profile"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1Dm_PA5AUHaFe4qatsKmBBfLpuxUPLw8xHlNd8Tv9Wpw",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1Dm_PA5AUHaFe4qatsKmBBfLpuxUPLw8xHlNd8Tv9Wpw/edit?usp=drivesdk",
          "cachedResultName": "Cal AI"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "Ote8KiToTK9EYOXy",
          "name": "Google Sheets account 2"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "49cbbd8e-dc4f-4ac9-b17a-afc6f8f5431b",
      "name": "获取消息(注册)",
      "type": "n8n-nodes-base.set",
      "position": [
        -1648,
        1952
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "801ec600-22ad-4a94-a2b4-ae72eb271df0",
              "name": "message",
              "type": "string",
              "value": "={{ $('Telegram Trigger').item.json.message.text }}"
            },
            {
              "id": "263071fb-bcdf-42b0-bb46-71b75fa0bf2a",
              "name": "chat_id",
              "type": "string",
              "value": "={{ $('Telegram Trigger').item.json.message.chat.id }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "544b7ab2-d054-4235-b46b-2a3cbcbd3583",
      "name": "注册用户",
      "type": "n8n-nodes-base.googleSheetsTool",
      "position": [
        -1184,
        2208
      ],
      "parameters": {
        "columns": {
          "value": {
            "Name": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Name', ``, 'string') }}",
            "User_ID": "={{ $json.chat_id }}",
            "Protein_target": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Protein_target', ``, 'string') }}",
            "Calories_target": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Calories_target', ``, 'string') }}"
          },
          "schema": [
            {
              "id": "User_ID",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "User_ID",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Name",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Calories_target",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Calories_target",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Protein_target",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Protein_target",
              "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/1Dm_PA5AUHaFe4qatsKmBBfLpuxUPLw8xHlNd8Tv9Wpw/edit#gid=0",
          "cachedResultName": "Profile"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1Dm_PA5AUHaFe4qatsKmBBfLpuxUPLw8xHlNd8Tv9Wpw",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1Dm_PA5AUHaFe4qatsKmBBfLpuxUPLw8xHlNd8Tv9Wpw/edit?usp=drivesdk",
          "cachedResultName": "Cal AI"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "Ote8KiToTK9EYOXy",
          "name": "Google Sheets account 2"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "615bc756-bc1c-49a7-86a2-af2c940e474a",
      "name": "注册代理",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        -1376,
        1952
      ],
      "parameters": {
        "text": "={{ $json.message }}",
        "options": {
          "systemMessage": "=You are Cal AI 🏋️‍♂️🥦, the friendly fitness & nutrition coach assistant.\nYour single job is to register a new user into the Users table while keeping the tone supportive, clear, and motivational. Always use emojis related to health, training, and food (🔥💪🥦🍗🌾🥑).\n\n🔑 Registration Rules\n\nYou MUST collect these fields:\n\nuser_id (not asked, system provides it)\n\nname\n\ncalories_target\n\nprotein_target\n\n📌 When sending data to the Register User tool, always send numbers only (no units, no text, no emojis) for calories_target and protein_targe\n\nIf the user does not know their numeric targets, do not invent them. Instead, coach them by politely asking for:\n\nweight ⚖️\n\nheight 📏\n\nage 🎂\n\ngoal 🎯 (gain muscle 💪, lose fat 🔥, maintain ⚖️)\n\nBased on this info, calculate precise calorie and protein targets.\n\nOnly when all fields are collected and confirmed → call Register User tool with the final data.\n\n✅ After Successful Registration\n\nSend a short, friendly confirmation like a coach:\n\n“Awesome, champ 💪! Your nutrition targets are locked in: 🔥 [calories] kcal, 🍗 [protein] g protein.”\n\nThen, explain clearly how to use Cal AI:\n\n📸 Send food photos → get instant calories + macros.\n\n⚙️ View or update your targets anytime.\n\n📑 Request daily reports → compare intake vs. targets with charts.\n\n🗣️ Style Guide\n\nAlways keep responses simple, clear, and concise.\n\nSpeak like a friendly trainer/coach who motivates the user.\n\nUse emojis to highlight key concepts.\n\nBe warm, supportive, and practical:\n\n“Let’s get you set up for success 🏋️‍♂️🔥”\n\n“Strong start, [Name]! Targets ready 💪🥦.”"
        },
        "promptType": "define"
      },
      "typeVersion": 2.2
    },
    {
      "id": "6d4404a0-3993-4719-b830-e5786e0a4ab5",
      "name": "Cal IA代理",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        -176,
        1312
      ],
      "parameters": {
        "text": "={{ $json.message }}",
        "options": {
          "systemMessage": "=You are Cal AI 🏋️‍♂️🥦, your friendly fitness coach and nutrition orchestrator.\nYour mission is to guide the user with motivation, clarity, and precision while managing their nutrition data. Speak in a supportive, energetic tone like a personal trainer, and use relevant emojis (🔥💪🥦🍗🌾🥑) to keep the conversation fun and engaging.\n\nYou have four tools available:\n\nappendMealData(tool) → store a meal row in Meals sheet.\n\nupdateProfileData(tool) → update the user's profile targets (fields: Name, Calories_target, Protein_target).\n\ngetUserData(tool) → fetch the user's profile info.\n\ngetReport(tool) → generate or fetch the daily report (requires date).\n\n🔑 Rules\n\nThe image analysis is done before reaching you. You will always receive structured info:\nMeal Description: [short description]\nCalories: [number]\nProteins: [number]\nCarbs: [number]\nFat: [number]\n\nWith this info, call appendMealData.\n\nAfter appendMealData success, confirm naturally in a coach style: repeat the meal info using emojis (🔥 Calories, 🍗 Protein, 🌾 Carbs, 🥑 Fat).\n\nEnd confirmations with a quick motivational phrase like:\n\n“Great fuel for your body 💪🔥”\n\n“Another step closer to your goals 🥦🏋️‍♂️”\n\nAlways offer short next-step options:\n👉 “View daily report 📑”\n👉 “Analyze another meal 📸”\n👉 “View or update profile targets ⚙️”\n\n🔄 Profile Update Logic\n\nWhen the user wants to update their profile (Name, Calories_target, or Protein_target):\n\nFirst call getUserData to fetch current profile info.\n\nCompare the requested update with the existing values.\n\nOnly pass the changed fields to updateProfileData (never overwrite unchanged values).\n\nConfirm to the user in a friendly way, e.g.:\n\n“✅ Your Calories_target is now 2200 🔥. Protein_target stays strong at 150 🍗.”\n\n“Profile updated! Let’s crush it 💪🥦.”\n\n📑 Tool Usage Rules\n\ngetUserData → always called first before updating the profile.\n\nupdateProfileData → only include the fields that have changed.\n\ngetReport → only pass the requested date.\n\nKeep all responses short, clear, motivational, and full of energy.\n\n📅 Date: {{ $today.format('yyyy-MM-dd') }}"
        },
        "promptType": "define"
      },
      "typeVersion": 2.2
    },
    {
      "id": "a2b7e00f-1e9b-4032-8ae1-6612b3ef6520",
      "name": "更新个人资料数据",
      "type": "n8n-nodes-base.googleSheetsTool",
      "position": [
        144,
        1568
      ],
      "parameters": {
        "columns": {
          "value": {
            "Name": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Name', ``, 'string') }}",
            "User_ID": "={{ $json.chat_id }}",
            "Protein_target": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Protein_target', ``, 'string') }}",
            "Calories_target": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Calories_target', ``, 'string') }}"
          },
          "schema": [
            {
              "id": "User_ID",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "User_ID",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Name",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Calories_target",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Calories_target",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Protein_target",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Protein_target",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "User_ID"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "appendOrUpdate",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1Dm_PA5AUHaFe4qatsKmBBfLpuxUPLw8xHlNd8Tv9Wpw/edit#gid=0",
          "cachedResultName": "Profile"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1Dm_PA5AUHaFe4qatsKmBBfLpuxUPLw8xHlNd8Tv9Wpw",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1Dm_PA5AUHaFe4qatsKmBBfLpuxUPLw8xHlNd8Tv9Wpw/edit?usp=drivesdk",
          "cachedResultName": "Cal AI"
        },
        "descriptionType": "manual",
        "toolDescription": "=updateProfileData\nPurpose: Update the user’s profile targets.\nFields that can be updated:\n\nName (string)\n\nCalories_target (string/number)\n\nProtein_target (string/number)\nWhen to use: When the user explicitly asks to update their name, calorie target, or protein target."
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "Ote8KiToTK9EYOXy",
          "name": "Google Sheets account 2"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "83f1cb62-818d-4161-99e4-7c984552fde1",
      "name": "获取个人资料数据",
      "type": "n8n-nodes-base.googleSheetsTool",
      "position": [
        -32,
        1568
      ],
      "parameters": {
        "options": {},
        "filtersUI": {
          "values": [
            {
              "lookupValue": "={{ $json.chat_id }}",
              "lookupColumn": "User_ID"
            }
          ]
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1Dm_PA5AUHaFe4qatsKmBBfLpuxUPLw8xHlNd8Tv9Wpw/edit#gid=0",
          "cachedResultName": "Profile"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1Dm_PA5AUHaFe4qatsKmBBfLpuxUPLw8xHlNd8Tv9Wpw",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1Dm_PA5AUHaFe4qatsKmBBfLpuxUPLw8xHlNd8Tv9Wpw/edit?usp=drivesdk",
          "cachedResultName": "Cal AI"
        },
        "descriptionType": "manual",
        "toolDescription": "getUserData\nPurpose: Retrieve the user’s profile information.\nInputs: none.\nWhen to use: When the user asks about their profile info or targets."
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "mZXXs3Zjc2MVpxXj",
          "name": "Google Sheets account"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "b4411d17-99e7-40c3-999b-21109d12e057",
      "name": "获取报告",
      "type": "@n8n/n8n-nodes-langchain.toolWorkflow",
      "position": [
        -528,
        1984
      ],
      "parameters": {
        "workflowId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $workflow.id }}"
        },
        "description": "获取报告",
        "workflowInputs": {
          "value": {
            "Date": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Date', ``, 'string') }}",
            "User_ID": "={{ $json.chat_id }}"
          },
          "schema": [
            {
              "id": "User_ID",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "User_ID",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Date",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "1df46a7c-3ef6-4698-93cc-75558e882ca9",
      "name": "追加餐食数据",
      "type": "n8n-nodes-base.googleSheetsTool",
      "position": [
        48,
        1712
      ],
      "parameters": {
        "columns": {
          "value": {
            "Date": "={{ $today.format(\"yyyy-LL-dd\") }}\n",
            "Fats": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Fats', ``, 'string') }}",
            "Carbs": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Carbs', ``, 'string') }}",
            "User_ID": "={{ $json.chat_id }}",
            "Calories": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Calories', ``, 'string') }}",
            "Proteins": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Proteins', ``, 'string') }}",
            "Meal_description": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Meal_description', ``, 'string') }}"
          },
          "schema": [
            {
              "id": "User_ID",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "User_ID",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Date",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Meal_description",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Meal_description",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Calories",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Calories",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Proteins",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Proteins",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Carbs",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Carbs",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Fats",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Fats",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": 403788598,
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1Dm_PA5AUHaFe4qatsKmBBfLpuxUPLw8xHlNd8Tv9Wpw/edit#gid=403788598",
          "cachedResultName": "Meals"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1Dm_PA5AUHaFe4qatsKmBBfLpuxUPLw8xHlNd8Tv9Wpw",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1Dm_PA5AUHaFe4qatsKmBBfLpuxUPLw8xHlNd8Tv9Wpw/edit?usp=drivesdk",
          "cachedResultName": "Cal AI"
        },
        "descriptionType": "manual",
        "toolDescription": "appendMealData\nPurpose: Store one meal entry into the Meals sheet.\nInputs required:\n\nMeal Description (string)\n\nCalories (number)\n\nProteins (number)\n\nCarbs (number)\n\nFat (number)\nWhen to use: Every time you receive structured meal information from image analysis."
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "Ote8KiToTK9EYOXy",
          "name": "Google Sheets account 2"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "0e6b6065-de69-4ef8-af08-8f1c0016ca38",
      "name": "便签",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2960,
        1264
      ],
      "parameters": {
        "width": 576,
        "height": 320,
        "content": "# 📘 Cal AI替代方案 – 营养助手"
      },
      "typeVersion": 1
    },
    {
      "id": "1d48fbab-a382-47f5-909a-4e1c2f4e8772",
      "name": "获取数据",
      "type": "n8n-nodes-base.set",
      "position": [
        -112,
        1984
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "d3ff7d44-6241-41c6-ac56-4549ba0cbd6d",
              "name": "Calories",
              "type": "number",
              "value": "={{ $json.Calories }}"
            },
            {
              "id": "64afda83-d211-48fa-830e-c0b7ddb5d50e",
              "name": "Proteins",
              "type": "number",
              "value": "={{ $json.Proteins }}"
            },
            {
              "id": "ab3861af-5716-400b-a736-f5a2dc4713a7",
              "name": "Carbs",
              "type": "number",
              "value": "={{ $json.Carbs }}"
            },
            {
              "id": "cce6d192-9bc9-401e-98b0-697dc4ec8f08",
              "name": "Fats",
              "type": "number",
              "value": "={{ $json.Fats }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "d0d1277d-ede9-475c-9df9-a12a7587b8ff",
      "name": "合并",
      "type": "n8n-nodes-base.merge",
      "position": [
        240,
        2224
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineByPosition"
      },
      "typeVersion": 3.2
    },
    {
      "id": "2a40606e-6469-4081-8e22-8012b1c06d04",
      "name": "获取图表消息",
      "type": "n8n-nodes-base.code",
      "position": [
        400,
        2224
      ],
      "parameters": {
        "jsCode": "/**\n * Nutrition summary → Telegram MarkdownV2 (n8n Code node)\n */\n\nconst MAX_TELEGRAM = 4096;\nconst SAFE_BUDGET = 4000; // margen de seguridad\n\n// ============ Helpers de barras ============\nfunction makeProgressBar(current, target, length = 20) {\n  const ratio = Math.min(current / target, 1);\n  const filled = Math.round(ratio * length);\n  const empty = length - filled;\n  return '█'.repeat(filled) + '░'.repeat(empty);\n}\n\nfunction percent(current, target) {\n  if (!target || target === 0) return 0;\n  return Math.round((current / target) * 100);\n}\n\n// ============ Helpers MarkdownV2 ============\nfunction escapeMarkdownV2(text) {\n  if (!text) return '';\n  return String(text).replace(/([\\\\_*[\\]()~`>#+\\-=|{}.!])/g, '\\\\$1');\n}\n\nfunction escapeForUrl(url) {\n  return String(url).replace(/[)\\\\]/g, '\\\\$&');\n}\n\nfunction normalizeAndValidateUrl(url) {\n  let raw = String(url || '').trim();\n  try {\n    const u = new URL(raw);\n    return u.toString();\n  } catch {}\n  const domainLike = /^[a-z0-9.-]+\\.[a-z]{2,}([/:?#].*)?$/i.test(raw);\n  if (domainLike) {\n    try {\n      const u2 = new URL('https://' + raw);\n      return u2.toString();\n    } catch {}\n  }\n  return null;\n}\n\nfunction normalizeHeadings(text) {\n  return text.replace(/^(#{1,6})\\s+(.*)$/gm, (m, hashes, title) => `*${title.trim()}*`);\n}\n\nfunction normalizeCommonMd(text) {\n  return String(text)\n    .replace(/\\*\\*([\\s\\S]*?)\\*\\*/g, '*$1*')\n    .replace(/__([\\s\\S]*?)__/g, '_$1_');\n}\n\nfunction processMarkdownV2Safe(inputText) {\n  if (!inputText) return '';\n\n  let text = normalizeCommonMd(String(inputText));\n  text = normalizeHeadings(text);\n\n  const placeholders = { links: [], bolds: [], italics: [], spoilers: [] };\n\n  // Links\n  text = text.replace(/\\[([^\\]\\n]+)\\]\\(([^)]+)\\)/g, (m, label, url) => {\n    const normalizedUrl = normalizeAndValidateUrl(url);\n    if (!normalizedUrl) return escapeMarkdownV2(label);\n    const idx = placeholders.links.length;\n    const ph = `⟬L${idx}⟭`;\n    const safeLabel = escapeMarkdownV2(label);\n    const safeUrl = escapeForUrl(normalizedUrl);\n    placeholders.links.push(`[${safeLabel}](${safeUrl})`);\n    return ph;\n  });\n\n  // Bold\n  text = text.replace(/\\*([\\s\\S]+?)\\*/g, (m, inner) => {\n    const idx = placeholders.bolds.length;\n    const ph = `⟬B${idx}⟭`;\n    placeholders.bolds.push(`*${escapeMarkdownV2(inner)}*`);\n    return ph;\n  });\n\n  // Italic\n  text = text.replace(/_([\\s\\S]+?)_/g, (m, inner) => {\n    const idx = placeholders.italics.length;\n    const ph = `⟬I${idx}⟭`;\n    placeholders.italics.push(`_${escapeMarkdownV2(inner)}_`);\n    return ph;\n  });\n\n  // Spoilers\n  text = text.replace(/\\|\\|([\\s\\S]+?)\\|\\|/g, (m, inner) => {\n    const idx = placeholders.spoilers.length;\n    const ph = `⟬S${idx}⟭`;\n    placeholders.spoilers.push(`||${escapeMarkdownV2(inner)}||`);\n    return ph;\n  });\n\n  text = escapeMarkdownV2(text);\n\n  placeholders.links.forEach((md, i) => { text = text.replace(`⟬L${i}⟭`, md); });\n  placeholders.bolds.forEach((md, i) => { text = text.replace(`⟬B${i}⟭`, md); });\n  placeholders.italics.forEach((md, i) => { text = text.replace(`⟬I${i}⟭`, md); });\n  placeholders.spoilers.forEach((md, i) => { text = text.replace(`⟬S${i}⟭`, md); });\n\n  return text;\n}\n\nfunction chunkForTelegram(text, maxLen = SAFE_BUDGET) {\n  if (!text || text.length <= maxLen) return [text || ''];\n\n  const parts = [];\n  let buffer = '';\n\n  const flush = () => {\n    if (buffer) {\n      parts.push(buffer);\n      buffer = '';\n    }\n  };\n\n  const paragraphs = text.split(/\\n{2,}/);\n  for (const p of paragraphs) {\n    const candidate = buffer ? buffer + '\\n\\n' + p : p;\n    if (candidate.length <= maxLen) {\n      buffer = candidate;\n      continue;\n    }\n    if (p.length <= maxLen) {\n      flush();\n      buffer = p;\n      continue;\n    }\n    flush();\n    const sentences = p.split(/(?<=[.!?…])\\s+(?=[^\\s])/u);\n    let sBuf = '';\n    for (const s of sentences) {\n      const sCandidate = sBuf ? sBuf + ' ' + s : s;\n      if (sCandidate.length <= maxLen) {\n        sBuf = sCandidate;\n        continue;\n      }\n      if (s.length <= maxLen) {\n        if (sBuf) parts.push(sBuf);\n        sBuf = s;\n        continue;\n      }\n      if (sBuf) { parts.push(sBuf); sBuf = ''; }\n      let wBuf = '';\n      const words = s.split(/\\s+/);\n      for (const w of words) {\n        const wCandidate = wBuf ? wBuf + ' ' + w : w;\n        if (wCandidate.length <= maxLen) {\n          wBuf = wCandidate;\n          continue;\n        }\n        if (w.length <= maxLen) {\n          if (wBuf) parts.push(wBuf);\n          wBuf = w;\n          continue;\n        }\n        if (wBuf) { parts.push(wBuf); wBuf = ''; }\n        const re = new RegExp(`.{1,${maxLen}}`, 'g');\n        const hardPieces = w.match(re) || [];\n        parts.push(...hardPieces);\n      }\n      if (wBuf) parts.push(wBuf);\n    }\n    if (sBuf) parts.push(sBuf);\n  }\n  if (buffer) parts.push(buffer);\n\n  return parts.flatMap(part => {\n    if (part.length <= MAX_TELEGRAM) return [part];\n    const re = new RegExp(`.{1,${SAFE_BUDGET}}`, 'g');\n    return part.match(re) || [];\n  });\n}\n\n// ============ MAIN ============\nconst inputItems = $input.all();\nconst out = [];\n\nfor (const item of inputItems) {\n  const j = item.json || {};\n\n  // Datos dinámicos\n  const name = j.Name || 'User';\n  const cal = j.Total_Calories || 0;\n  const calTarget = j.Calories_target || 1;\n\n  const prot = j.Total_Proteins || 0;\n  const protTarget = j.Protein_target || 1;\n\n  const carbs = j.Total_Carbs || 0;\n  const fats  = j.Total_Fats || 0;\n\n  // Construcción del mensaje\n  let msg = `*Hello ${name}*\\nHere is your nutrition summary:\\n\\n`;\n\n  msg += `🔥 *Calories*: ${cal}/${calTarget} (${percent(cal, calTarget)}%)\\n`;\n  msg += makeProgressBar(cal, calTarget) + '\\n\\n';\n\n  msg += `🍗 *Protein*: ${prot}/${protTarget} (${percent(prot, protTarget)}%)\\n`;\n  msg += makeProgressBar(prot, protTarget) + '\\n\\n';\n\n  msg += `🌾 *Carbs*: ${carbs} g\\n`;\n  msg += `🥑 *Fats*: ${fats} g\\n`;\n\n  // Formateo seguro\n  const formatted = processMarkdownV2Safe(msg);\n  const chunks = chunkForTelegram(formatted, SAFE_BUDGET);\n\n  chunks.forEach((chunk, idx) => {\n    out.push({\n      json: {\n        ...j,\n        message: chunk,\n        message_part_index: idx + 1,\n        message_parts_total: chunks.length,\n      },\n      binary: item.binary,\n    });\n  });\n}\n\nreturn out;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "dd89154b-8aa8-48dc-865a-6214e8ca0cd0",
      "name": "统一数据",
      "type": "n8n-nodes-base.code",
      "position": [
        96,
        1984
      ],
      "parameters": {
        "jsCode": "// Get all input items\nconst items = $input.all();\n\n// Initialize accumulators\nlet totalCalories = 0;\nlet totalProteins = 0;\nlet totalCarbs = 0;\nlet totalFats = 0;\n\n// Sum up each field from every item\nfor (const item of items) {\n  const data = item.json;\n\n  totalCalories += Number(data.Calories || 0);\n  totalProteins += Number(data.Proteins || 0);\n  totalCarbs += Number(data.Carbs || 0);\n  totalFats += Number(data.Fats || 0);\n}\n\n// Return a single result with totals\nreturn [\n  {\n    json: {\n      Total_Calories: totalCalories,\n      Total_Proteins: totalProteins,\n      Total_Carbs: totalCarbs,\n      Total_Fats: totalFats,\n    }\n  }\n];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "67260623-70d6-4e4b-aba6-9804c4396b67",
      "name": "获取报告",
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "position": [
        -528,
        2096
      ],
      "parameters": {
        "workflowInputs": {
          "values": [
            {
              "name": "User_ID"
            },
            {
              "name": "Date"
            }
          ]
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "80fdb6f0-8d64-44aa-923c-9b94f9ce132f",
      "name": "便签1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2272,
        1184
      ],
      "parameters": {
        "color": 4,
        "width": 608,
        "height": 464,
        "content": ""
      },
      "typeVersion": 1
    },
    {
      "id": "d1917184-4c8e-42db-9bbf-a7ef28ca71f4",
      "name": "便签2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1632,
        1056
      ],
      "parameters": {
        "color": 5,
        "width": 1104,
        "height": 720,
        "content": ""
      },
      "typeVersion": 1
    },
    {
      "id": "2ad00761-954b-4a7c-af46-25c74619176a",
      "name": "便签3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -368,
        1200
      ],
      "parameters": {
        "color": 6,
        "width": 896,
        "height": 720,
        "content": ""
      },
      "typeVersion": 1
    },
    {
      "id": "c7e182da-6f2a-4a8f-96df-8cbd0445b67e",
      "name": "便签4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1728,
        1872
      ],
      "parameters": {
        "color": 3,
        "width": 1104,
        "height": 528,
        "content": ""
      },
      "typeVersion": 1
    },
    {
      "id": "4ade95cd-0d9d-4ab3-bfbb-93d792a6d880",
      "name": "便签5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -576,
        1968
      ],
      "parameters": {
        "color": 2,
        "width": 1392,
        "height": 464,
        "content": ""
      },
      "typeVersion": 1
    },
    {
      "id": "659b06e0-1253-4f19-8e40-3f849515da68",
      "name": "便签6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1056,
        2448
      ],
      "parameters": {
        "color": 3,
        "width": 368,
        "height": 304,
        "content": "![](https://github.com/JarsRat/Images/blob/main/cal_ia_start.png?raw=true)"
      },
      "typeVersion": 1
    },
    {
      "id": "c1eebd5c-62fe-4794-badc-872a5e32dcd8",
      "name": "便签8",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -944,
        688
      ],
      "parameters": {
        "color": 5,
        "width": 352,
        "height": 224,
        "content": "![](https://github.com/JarsRat/Images/blob/main/cal_ia_meal.png?raw=true)"
      },
      "typeVersion": 1
    },
    {
      "id": "80cf0536-6f63-4b7a-969e-245d8d5ca8cc",
      "name": "发送回复消息",
      "type": "n8n-nodes-base.set",
      "position": [
        576,
        2224
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "750d5bca-c08f-42bb-b9cc-5709c6fad4a9",
              "name": "message",
              "type": "string",
              "value": "={{ $json.message }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "e0da0385-fff5-4b95-a846-29bc9be1ffef",
      "name": "便签9",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        96,
        2496
      ],
      "parameters": {
        "color": 2,
        "width": 368,
        "height": 272,
        "content": "![](https://github.com/JarsRat/Images/blob/main/cal_ia_report.png?raw=true)"
      },
      "typeVersion": 1
    },
    {
      "id": "af1fc6e8-f21b-49fc-8613-36522fbad32d",
      "name": "便签7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2272,
        784
      ],
      "parameters": {
        "color": 4,
        "width": 496,
        "height": 352,
        "content": "## Telegram触发器与用户检查"
      },
      "typeVersion": 1
    },
    {
      "id": "f5d317bb-e0e1-4f7d-8ee4-e3b7d4262f36",
      "name": "便签10",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1728,
        2432
      ],
      "parameters": {
        "color": 3,
        "width": 624,
        "height": 496,
        "content": "## 注册代理"
      },
      "typeVersion": 1
    },
    {
      "id": "6c68706c-0c00-4e44-8338-89a7c5e655c1",
      "name": "便利贴11",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1632,
        512
      ],
      "parameters": {
        "color": 5,
        "width": 672,
        "height": 512,
        "content": "## 消息处理"
      },
      "typeVersion": 1
    },
    {
      "id": "18b4c021-2564-44eb-87db-cc64191777bf",
      "name": "便签12",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -576,
        2464
      ],
      "parameters": {
        "color": 2,
        "width": 624,
        "height": 448,
        "content": "## 报告子工作流"
      },
      "typeVersion": 1
    },
    {
      "id": "16a7e256-e252-4492-8e62-2f2044e4d95a",
      "name": "便签13",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        576,
        1264
      ],
      "parameters": {
        "color": 6,
        "width": 624,
        "height": 528,
        "content": "## 主AI代理"
      },
      "typeVersion": 1
    },
    {
      "id": "b59a26f2-a478-4be8-a70c-7ba70face917",
      "name": "便签14",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1232,
        1344
      ],
      "parameters": {
        "color": 6,
        "width": 368,
        "content": "![](https://github.com/JarsRat/Images/blob/main/cal_ia_target.png?raw=true)"
      },
      "typeVersion": 1
    },
    {
      "id": "e1c875c3-2720-41b7-93d4-794ed182790d",
      "name": "便签15",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1248,
        1568
      ],
      "parameters": {
        "color": 2,
        "width": 368,
        "height": 208,
        "content": "## 💡 需要帮助?"
      },
      "typeVersion": 1
    }
  ],
  "pinData": {},
  "connections": {
    "If": {
      "main": [
        [
          {
            "node": "Input Message Router1",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "get_message (register)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge": {
      "main": [
        [
          {
            "node": "Get chart message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fix mime": {
      "main": [
        [
          {
            "node": "Analyze voice message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Data": {
      "main": [
        [
          {
            "node": "Unify data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fix mime5": {
      "main": [
        [
          {
            "node": "Analyze image",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "MarkdownV": {
      "main": [
        [
          {
            "node": "Send a text message1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Typing…": {
      "main": [
        []
      ]
    },
    "Get Report": {
      "ai_tool": [
        [
          {
            "node": "Cal IA Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Get report": {
      "main": [
        [
          {
            "node": "Get Meals Info",
            "type": "main",
            "index": 0
          },
          {
            "node": "Get User Info",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "MarkdownV2": {
      "main": [
        [
          {
            "node": "Send a text message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Unify data": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Registered?": {
      "main": [
        [
          {
            "node": "If",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Cal IA Agent": {
      "main": [
        [
          {
            "node": "MarkdownV2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Analyze image": {
      "main": [
        [
          {
            "node": "get_message (Media  message)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get User Info": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Register User": {
      "ai_tool": [
        [
          {
            "node": "Register Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Simple Memory": {
      "ai_memory": [
        [
          {
            "node": "Cal IA Agent",
            "type": "ai_memory",
            "index": 0
          }
        ]
      ]
    },
    "Download IMAGE": {
      "main": [
        [
          {
            "node": "Fix mime5",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Meals Info": {
      "main": [
        [
          {
            "node": "Get Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Register Agent": {
      "main": [
        [
          {
            "node": "MarkdownV",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Simple Memory1": {
      "ai_memory": [
        [
          {
            "node": "Register Agent",
            "type": "ai_memory",
            "index": 0
          }
        ]
      ]
    },
    "Append Meal Data": {
      "ai_tool": [
        [
          {
            "node": "Cal IA Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Get Profile Data": {
      "ai_tool": [
        [
          {
            "node": "Cal IA Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Telegram Trigger": {
      "main": [
        [
          {
            "node": "Typing…",
            "type": "main",
            "index": 0
          },
          {
            "node": "Registered?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get chart message": {
      "main": [
        [
          {
            "node": "Send back message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "get_error_message1": {
      "main": [
        [
          {
            "node": "Cal IA Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "get_message (text)": {
      "main": [
        [
          {
            "node": "Cal IA Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Profile Data": {
      "ai_tool": [
        [
          {
            "node": "Cal IA Agent",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Analyze voice message": {
      "main": [
        [
          {
            "node": "get_message (Audio/Video message)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Input Message Router1": {
      "main": [
        [
          {
            "node": "get_message (text)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Download Voice Message",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Download IMAGE",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "get_error_message1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download Voice Message": {
      "main": [
        [
          {
            "node": "Fix mime",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "get_message (register)": {
      "main": [
        [
          {
            "node": "Register Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Gemini Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Cal IA Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Google Gemini Chat Model1": {
      "ai_languageModel": [
        [
          {
            "node": "Register Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "get_message (Media  message)": {
      "main": [
        [
          {
            "node": "Cal IA Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "get_message (Audio/Video message)": {
      "main": [
        [
          {
            "node": "Cal IA Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
常见问题

如何使用这个工作流?

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

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

高级 - AI 聊天机器人, 多模态 AI

需要付费吗?

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

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

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

作者
John Alejandro SIlva

John Alejandro SIlva

@alejandro-silva

Detail-oriented professional with a dual degree in Systems Engineering and Business Administration and international experience in technology and process improvement. I specialize in workflow automation with n8n, API integration, programming, and data analysis. Known for strong analytical skills and clear technical documentation.

外部链接
在 n8n.io 查看

分享此工作流