8
n8n 中文网amn8n.com

自动化航班价格比较

高级

这是一个Miscellaneous领域的自动化工作流,包含 19 个节点。主要使用 If, Ssh, Code, Webhook, EmailSend 等节点。 跨多个预订平台比较航班价格,并生成邮件报告

前置要求
  • HTTP Webhook 端点(n8n 会自动生成)

分类

工作流预览
可视化展示节点连接关系,支持缩放和平移
导出工作流
复制以下 JSON 配置到 n8n 导入,即可使用此工作流
{
  "id": "IuvYgHrzvtEck50d",
  "meta": {
    "instanceId": "dd69efaf8212c74ad206700d104739d3329588a6f3f8381a46a481f34c9cc281",
    "templateCredsSetupCompleted": true
  },
  "name": "自动化航班价格比较",
  "tags": [],
  "nodes": [
    {
      "id": "d4a5235a-57c0-42f6-9841-8f45d04dfe1c",
      "name": "Webhook - 接收航班请求",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -1088,
        432
      ],
      "webhookId": "flight-price-webhook",
      "parameters": {
        "path": "flight-price-compare",
        "options": {
          "allowedOrigins": "*"
        },
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2.1
    },
    {
      "id": "81bb3dac-0cd3-4826-8436-573bc393f2c4",
      "name": "解析和验证航班请求",
      "type": "n8n-nodes-base.code",
      "position": [
        -864,
        432
      ],
      "parameters": {
        "jsCode": "// Enhanced Flight Request Parser with NLP\nconst body = $input.first().json.body || {};\nconst query = body.message || body.query || '';\n\n// Extract user details\nconst userEmail = body.email || body.user_email || '';\nconst userName = body.name || body.user_name || 'Traveler';\nconst notifyPriceDrop = body.notify_price_drop || false;\n\n// Greeting handler\nconst greetings = ['hi', 'hello', 'hey', 'start', 'help'];\nif (greetings.some(g => query.toLowerCase().includes(g)) && query.split(' ').length < 5) {\n  return [{\n    json: {\n      status: 'greeting',\n      response: `Hi ${userName}! ✈️ I'm your Flight Price Comparison Assistant.\\n\\nTell me:\\n✅ From city/airport\\n✅ To city/airport\\n✅ Departure date\\n✅ Trip type (one-way/round-trip)\\n\\nExample: \"Flight from New York to London on 25th March, one-way\"`,\n      userEmail,\n      userName\n    }\n  }];\n}\n\n// Airport/City codes mapping\nconst airportCodes = {\n  'new york': 'JFK', 'nyc': 'JFK', 'london': 'LHR', 'paris': 'CDG',\n  'dubai': 'DXB', 'singapore': 'SIN', 'tokyo': 'NRT', 'mumbai': 'BOM',\n  'delhi': 'DEL', 'bangalore': 'BLR', 'los angeles': 'LAX', 'chicago': 'ORD',\n  'san francisco': 'SFO', 'boston': 'BOS', 'miami': 'MIA', 'sydney': 'SYD',\n  'melbourne': 'MEL', 'hong kong': 'HKG', 'bangkok': 'BKK', 'amsterdam': 'AMS',\n  'frankfurt': 'FRA', 'toronto': 'YYZ', 'vancouver': 'YVR', 'seattle': 'SEA'\n};\n\nfunction getAirportCode(text) {\n  const lower = text.toLowerCase().trim();\n  if (/^[A-Z]{3}$/i.test(text)) return text.toUpperCase();\n  return airportCodes[lower] || text.toUpperCase();\n}\n\n// Parse dates\nfunction parseDate(text) {\n  const monthMap = {\n    jan: 0, january: 0, feb: 1, february: 1, mar: 2, march: 2,\n    apr: 3, april: 3, may: 4, jun: 5, june: 5,\n    jul: 6, july: 6, aug: 7, august: 7, sep: 8, september: 8,\n    oct: 9, october: 9, nov: 10, november: 10, dec: 11, december: 11\n  };\n\n  const dateRegex = /(\\d{1,2})(st|nd|rd|th)?\\s+(jan|january|feb|february|mar|march|apr|april|may|jun|june|jul|july|aug|august|sep|september|oct|october|nov|november|dec|december)|\\d{4}-\\d{2}-\\d{2}/gi;\n  const matches = [...text.matchAll(dateRegex)];\n  const dates = [];\n  const currentYear = new Date().getFullYear();\n  \n  matches.forEach(match => {\n    if (match[0].includes('-')) {\n      dates.push(new Date(match[0]));\n    } else {\n      const day = parseInt(match[1]);\n      const monthStr = match[3].toLowerCase();\n      const month = monthMap[monthStr];\n      if (day && month !== undefined) {\n        dates.push(new Date(currentYear, month, day));\n      }\n    }\n  });\n  \n  return dates.sort((a, b) => a - b);\n}\n\n// Extract origin and destination\nlet origin = '';\nlet destination = '';\n\nconst routePattern = /(?:from|leaving)?\\s*([a-z\\s]{3,25})\\s+to\\s+([a-z\\s]{3,25})/i;\nconst routeMatch = query.match(routePattern);\n\nif (routeMatch) {\n  origin = routeMatch[1].trim();\n  destination = routeMatch[2].trim();\n} else {\n  origin = body.from || body.origin || body.departure_airport || '';\n  destination = body.to || body.destination || body.arrival_airport || '';\n}\n\nconst originCode = getAirportCode(origin);\nconst destinationCode = getAirportCode(destination);\n\nconst dates = parseDate(query + ' ' + (body.departure_date || ''));\nlet departureDate = dates[0] || null;\nlet returnDate = dates[1] || null;\n\nlet tripType = 'one-way';\nif (query.match(/round[\\s-]?trip|return/i) || returnDate) {\n  tripType = 'round-trip';\n}\nif (body.trip_type) {\n  tripType = body.trip_type.toLowerCase();\n}\n\nconst passengers = body.passengers || 1;\nconst cabinClass = body.class || body.cabin_class || 'economy';\n\nconst errors = [];\nif (!originCode || originCode.length < 3) errors.push('departure city/airport');\nif (!destinationCode || destinationCode.length < 3) errors.push('arrival city/airport');\nif (!departureDate) errors.push('departure date');\nif (tripType === 'round-trip' && !returnDate) errors.push('return date');\n\nif (errors.length > 0) {\n  return [{\n    json: {\n      status: 'missing_info',\n      response: `I need more information: ${errors.join(', ')}.\\n\\nExample: \"Flight from Mumbai to Dubai on 15th March, round-trip returning 20th March\"`,\n      userEmail,\n      userName\n    }\n  }];\n}\n\nconst formatDate = (date) => {\n  if (!date) return null;\n  const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];\n  return `${date.getDate()} ${months[date.getMonth()]} ${date.getFullYear()}`;\n};\n\nconst formatDateISO = (date) => {\n  if (!date) return null;\n  return date.toISOString().split('T')[0];\n};\n\nreturn [{\n  json: {\n    status: 'ready',\n    origin: originCode,\n    destination: destinationCode,\n    originCity: origin,\n    destinationCity: destination,\n    departureDate: formatDate(departureDate),\n    returnDate: returnDate ? formatDate(returnDate) : null,\n    departureDateISO: formatDateISO(departureDate),\n    returnDateISO: formatDateISO(returnDate),\n    tripType,\n    passengers,\n    cabinClass,\n    userEmail,\n    userName,\n    notifyPriceDrop,\n    originalQuery: query\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "3f5bb362-ca6a-4c1d-a959-9f194fcc7e99",
      "name": "检查请求是否有效",
      "type": "n8n-nodes-base.if",
      "position": [
        -640,
        432
      ],
      "parameters": {
        "conditions": {
          "string": [
            {
              "value1": "={{$json.status}}",
              "value2": "ready"
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "07df7d09-a38d-4540-b8f8-0e81405e086b",
      "name": "抓取 Kayak",
      "type": "n8n-nodes-base.ssh",
      "onError": "continueErrorOutput",
      "position": [
        -416,
        48
      ],
      "parameters": {
        "cwd": "/home/oneclick-server2/",
        "command": "=python3 /home/oneclick-server2/flight_scraper.py {{ $json.origin }} {{ $json.destination }} {{ $json.departureDateISO }} {{ $json.returnDateISO || '' }} {{ $json.tripType }} {{ $json.passengers }} {{ $json.cabinClass }} kayak",
        "authentication": "privateKey"
      },
      "credentials": {
        "sshPrivateKey": {
          "id": "ilPh8oO4GfSlc0Qy",
          "name": "SSH Password account - test "
        }
      },
      "typeVersion": 1
    },
    {
      "id": "2acab75d-4d75-49d7-8c37-dc36740e7636",
      "name": "抓取 Skyscanner",
      "type": "n8n-nodes-base.ssh",
      "onError": "continueErrorOutput",
      "position": [
        -416,
        240
      ],
      "parameters": {
        "cwd": "/home/oneclick-server2/",
        "command": "=python3 /home/oneclick-server2/flight_scraper.py {{ $json.origin }} {{ $json.destination }} {{ $json.departureDateISO }} {{ $json.returnDateISO || '' }} {{ $json.tripType }} {{ $json.passengers }} {{ $json.cabinClass }} skyscanner",
        "authentication": "privateKey"
      },
      "credentials": {
        "sshPrivateKey": {
          "id": "ilPh8oO4GfSlc0Qy",
          "name": "SSH Password account - test "
        }
      },
      "typeVersion": 1
    },
    {
      "id": "b74690a3-61b9-41df-967b-bf395fb6aade",
      "name": "抓取 Expedia",
      "type": "n8n-nodes-base.ssh",
      "onError": "continueErrorOutput",
      "position": [
        -416,
        432
      ],
      "parameters": {
        "cwd": "/home/oneclick-server2/",
        "command": "=python3 /home/oneclick-server2/flight_scraper.py {{ $json.origin }} {{ $json.destination }} {{ $json.departureDateISO }} {{ $json.returnDateISO || '' }} {{ $json.tripType }} {{ $json.passengers }} {{ $json.cabinClass }} expedia",
        "authentication": "privateKey"
      },
      "credentials": {
        "sshPrivateKey": {
          "id": "ilPh8oO4GfSlc0Qy",
          "name": "SSH Password account - test "
        }
      },
      "typeVersion": 1
    },
    {
      "id": "76e69f7a-9683-47ce-bdf0-b673c504a4b2",
      "name": "抓取 Google Flights",
      "type": "n8n-nodes-base.ssh",
      "onError": "continueErrorOutput",
      "position": [
        -416,
        816
      ],
      "parameters": {
        "cwd": "/home/oneclick-server2/",
        "command": "=python3 /home/oneclick-server2/flight_scraper.py {{ $json.origin }} {{ $json.destination }} {{ $json.departureDateISO }} {{ $json.returnDateISO || '' }} {{ $json.tripType }} {{ $json.passengers }} {{ $json.cabinClass }} googleflights",
        "authentication": "privateKey"
      },
      "credentials": {
        "sshPrivateKey": {
          "id": "ilPh8oO4GfSlc0Qy",
          "name": "SSH Password account - test "
        }
      },
      "typeVersion": 1
    },
    {
      "id": "020178b1-0de8-4cbc-812d-bec95ad92d8f",
      "name": "汇总和分析价格",
      "type": "n8n-nodes-base.code",
      "position": [
        -64,
        336
      ],
      "parameters": {
        "jsCode": "// Aggregate flight prices from all platforms\nconst items = $input.all();\nconst searchData = items[0].json;\n\nconst flights = [];\nconst errors = [];\n\nconst platforms = ['Kayak', 'Skyscanner', 'Expedia', 'Google Flights'];\n\nitems.slice(1).forEach((item, index) => {\n  const platform = platforms[index];\n  const output = item.json.stdout || '';\n  \n  if (item.json.stderr || !output) {\n    errors.push(`${platform}: Unable to fetch`);\n    return;\n  }\n  \n  const lines = output.trim().split('\\n');\n  \n  lines.forEach(line => {\n    if (line.includes('|')) {\n      const parts = line.split('|');\n      if (parts.length >= 6) {\n        const price = parseFloat(parts[1].replace(/[^0-9.]/g, ''));\n        \n        if (price && price > 0) {\n          flights.push({\n            platform,\n            airline: parts[0].trim(),\n            price,\n            currency: parts[1].match(/[A-Z₹$€£]/)?.[0] || '$',\n            duration: parts[2].trim(),\n            stops: parts[3].trim(),\n            departureTime: parts[4].trim(),\n            arrivalTime: parts[5].trim(),\n            bookingUrl: parts[6]?.trim() || '#',\n            cabinClass: searchData.cabinClass\n          });\n        }\n      }\n    }\n  });\n});\n\nif (flights.length === 0) {\n  return [{\n    json: {\n      status: 'no_results',\n      message: 'No flights found for your search criteria.',\n      errors,\n      ...searchData\n    }\n  }];\n}\n\nflights.sort((a, b) => a.price - b.price);\n\nconst bestDeal = flights[0];\nconst avgPrice = flights.reduce((sum, f) => sum + f.price, 0) / flights.length;\nconst maxPrice = flights[flights.length - 1].price;\nconst savings = maxPrice - bestDeal.price;\n\nconst directFlights = flights.filter(f => f.stops === '0' || f.stops.toLowerCase().includes('non'));\nconst bestDirectFlight = directFlights.length > 0 ? directFlights[0] : null;\n\nreturn [{\n  json: {\n    status: 'success',\n    ...searchData,\n    results: flights,\n    bestDeal,\n    bestDirectFlight,\n    avgPrice: Math.round(avgPrice),\n    maxPrice: Math.round(maxPrice),\n    savings: Math.round(savings),\n    totalResults: flights.length,\n    directFlightsCount: directFlights.length,\n    errors,\n    searchTimestamp: new Date().toISOString()\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "b195bb98-d48b-442d-8ceb-7809a2f65d15",
      "name": "格式化邮件报告",
      "type": "n8n-nodes-base.code",
      "position": [
        256,
        336
      ],
      "parameters": {
        "jsCode": "// Format email report\nconst data = $input.first().json;\n\nif (data.status === 'no_results') {\n  return [{\n    json: {\n      subject: `❌ No Flights - ${data.origin} to ${data.destination}`,\n      text: `No flights found for ${data.origin} to ${data.destination} on ${data.departureDate}`,\n      ...data\n    }\n  }];\n}\n\nconst { origin, destination, departureDate, returnDate, tripType, passengers, results, bestDeal, avgPrice, savings } = data;\n\nconst topResults = results.slice(0, 10);\nconst resultsText = topResults.map((f, i) => \n  `${i + 1}. ${f.airline} - ${f.currency}${f.price} (${f.stops === '0' ? 'Non-stop' : f.stops + ' stop(s)'}) - ${f.platform}`\n).join('\\n');\n\nconst textReport = `\nFLIGHT PRICE COMPARISON\n${'='.repeat(50)}\n\nRoute: ${origin} → ${destination}\nDeparture: ${departureDate}\n${returnDate ? `Return: ${returnDate}\\n` : ''}Trip Type: ${tripType}\nPassengers: ${passengers}\n\n🏆 BEST DEAL\n${'-'.repeat(50)}\n${bestDeal.airline}\nPrice: ${bestDeal.currency}${bestDeal.price}\nDuration: ${bestDeal.duration}\nStops: ${bestDeal.stops === '0' ? 'Non-stop' : bestDeal.stops + ' stop(s)'}\nPlatform: ${bestDeal.platform}\n${savings > 0 ? `\\n💰 Save ${bestDeal.currency}${savings} vs highest price!` : ''}\n\n📊 ALL RESULTS (Top 10)\n${'-'.repeat(50)}\n${resultsText}\n\nAverage Price: ${bestDeal.currency}${avgPrice}\nTotal Results: ${results.length}\n\nPrices subject to availability.\nHappy travels! ✈️\n`;\n\nreturn [{\n  json: {\n    subject: `✈️ ${origin} → ${destination} - Best: ${bestDeal.currency}${bestDeal.price}`,\n    text: textReport,\n    ...data\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "994f5fa6-2e45-42f7-af12-f98026250c04",
      "name": "发送邮件报告",
      "type": "n8n-nodes-base.emailSend",
      "position": [
        480,
        336
      ],
      "webhookId": "7e60915f-7994-42f1-b850-dcb254a4d83c",
      "parameters": {
        "text": "={{$json.text}}",
        "options": {},
        "subject": "={{$json.subject}}",
        "toEmail": "={{$json.userEmail}}",
        "fromEmail": "flights@pricecomparison.com",
        "emailFormat": "text"
      },
      "credentials": {
        "smtp": {
          "id": "G1kyF8cSWTZ4vouN",
          "name": "SMTP -test"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "8b3fbdb5-879f-4a02-8927-8632f944bd53",
      "name": "Webhook 响应(成功)",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        784,
        336
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "={{ {\n  \"success\": true,\n  \"message\": \"Flight comparison sent to \" + $json.userEmail,\n  \"route\": $json.origin + \" → \" + $json.destination,\n  \"bestPrice\": $json.bestDeal.price,\n  \"airline\": $json.bestDeal.airline,\n  \"totalResults\": $json.totalResults\n} }}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "d1d130de-f7f2-42ed-9f2b-591446329e6c",
      "name": "Webhook 响应(错误)",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        -416,
        624
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "={{ {\n  \"success\": false,\n  \"message\": $json.response || \"Request failed\",\n  \"status\": $json.status\n} }}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "35dd7ec1-cb14-4b34-9138-0d8e8af2380a",
      "name": "便签",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1184,
        -624
      ],
      "parameters": {
        "color": 4,
        "width": 484,
        "height": 476,
        "content": "## 🎯 工作流目的"
      },
      "typeVersion": 1
    },
    {
      "id": "c312076a-7442-41fe-ac65-692581acde7d",
      "name": "便签1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1408,
        96
      ],
      "parameters": {
        "color": 5,
        "width": 452,
        "height": 516,
        "content": "## 📥 输入阶段"
      },
      "typeVersion": 1
    },
    {
      "id": "f8aa3a61-314d-4b4e-83bb-477f50a7562a",
      "name": "便签2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -928,
        48
      ],
      "parameters": {
        "color": 5,
        "width": 260,
        "height": 612,
        "content": "## 🧠 解析阶段"
      },
      "typeVersion": 1
    },
    {
      "id": "32ea52fc-41ef-4f5c-bf12-c0b13b9a2866",
      "name": "便签3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -508,
        -280
      ],
      "parameters": {
        "color": 5,
        "width": 280,
        "height": 1224,
        "content": "## 🔍 抓取阶段"
      },
      "typeVersion": 1
    },
    {
      "id": "d503d1f7-06ca-4d75-96fc-b0a6d3fccb1a",
      "name": "便签4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -112,
        -32
      ],
      "parameters": {
        "color": 5,
        "width": 260,
        "height": 660,
        "content": "## 📊 分析阶段"
      },
      "typeVersion": 1
    },
    {
      "id": "b194c549-bdad-4f31-9ea3-5990caea9b84",
      "name": "便签5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        224,
        0
      ],
      "parameters": {
        "color": 5,
        "width": 388,
        "height": 628,
        "content": "## 📧 报告阶段"
      },
      "typeVersion": 1
    },
    {
      "id": "0e963069-8538-431f-977b-9832b8905af4",
      "name": "### 需要帮助?",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        688,
        0
      ],
      "parameters": {
        "color": 5,
        "width": 260,
        "height": 612,
        "content": "## ✅ 响应阶段"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "8562102b-7099-407b-ae1f-69da37f8da99",
  "connections": {
    "Scrape Kayak": {
      "main": [
        [
          {
            "node": "Aggregate & Analyze Prices",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scrape Expedia": {
      "main": [
        [
          {
            "node": "Aggregate & Analyze Prices",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scrape Skyscanner": {
      "main": [
        [
          {
            "node": "Aggregate & Analyze Prices",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Email Report": {
      "main": [
        [
          {
            "node": "Webhook Response (Success)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Email Report": {
      "main": [
        [
          {
            "node": "Send Email Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scrape Google Flights": {
      "main": [
        [
          {
            "node": "Aggregate & Analyze Prices",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check If Request Valid": {
      "main": [
        [
          {
            "node": "Scrape Kayak",
            "type": "main",
            "index": 0
          },
          {
            "node": "Scrape Skyscanner",
            "type": "main",
            "index": 0
          },
          {
            "node": "Scrape Expedia",
            "type": "main",
            "index": 0
          },
          {
            "node": "Scrape Google Flights",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Webhook Response (Error)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate & Analyze Prices": {
      "main": [
        [
          {
            "node": "Format Email Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse & Validate Flight Request": {
      "main": [
        [
          {
            "node": "Check If Request Valid",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook - Receive Flight Request": {
      "main": [
        [
          {
            "node": "Parse & Validate Flight Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
常见问题

如何使用这个工作流?

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

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

高级 - 杂项

需要付费吗?

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

工作流信息
难度等级
高级
节点数量19
分类1
节点类型7
难度说明

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

作者
Oneclick AI Squad

Oneclick AI Squad

@oneclick-ai

The AI Squad Initiative is a pioneering effort to build, automate and scale AI-powered workflows using n8n.io. Our mission is to help individuals and businesses integrate AI agents seamlessly into their daily operations from automating tasks and enhancing productivity to creating innovative, intelligent solutions. We design modular, reusable AI workflow templates that empower creators, developers and teams to supercharge their automation with minimal effort and maximum impact.

外部链接
在 n8n.io 查看

分享此工作流