自动化航班价格比较
高级
这是一个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)可能需要您自行付费。
相关工作流推荐
学生录取与入学自动化
使用Excel、验证和邮件通知自动化学生录取流程
If
Code
Email Send
+4
10 节点Oneclick AI Squad
杂项
AI驱动的演讲者与场次推荐引擎
使用Claude AI和Google Sheets生成活动演讲者推荐
If
Code
Webhook
+5
20 节点Oneclick AI Squad
杂项
通过iOS自动化的地理围栏学生活动跟踪器
使用iOS自动化、Google Sheets和邮件提醒跟踪学生到达情况
If
Set
Webhook
+4
15 节点Oneclick AI Squad
杂项
面向财务与会计的 AI 驱动发票提醒与付款追踪器
基于AI的发票提醒与付款追踪器,专为财务与会计设计
If
Set
Code
+8
35 节点Oneclick AI Squad
发票处理
AI候选人筛选与面试安排系统
基于AI的候选人筛选与面试安排(OpenAI GPT与Google套件)
If
Code
Webhook
+6
18 节点Oneclick AI Squad
人力资源
通过多平台爬取和邮件报告实现酒店价格自动比对
通过多平台爬取和邮件报告自动化酒店价格比较
If
Set
Code
+6
19 节点Oneclick AI Squad
市场调研
工作流信息
难度等级
高级
节点数量19
分类1
节点类型7
作者
Oneclick AI Squad
@oneclick-aiThe 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 查看 →
分享此工作流