8
n8n 中文网amn8n.com

3个城市的天气预报

高级

这是一个Personal Productivity, Multimodal AI领域的自动化工作流,包含 20 个节点。主要使用 Set, Code, Gmail, Merge, FormTrigger 等节点。 使用AI增强格式将OpenWeatherMap多城市天气预报发送到Gmail

前置要求
  • Google 账号和 Gmail API 凭证
  • 可能需要目标 API 的认证凭证
  • OpenAI API Key
工作流预览
可视化展示节点连接关系,支持缩放和平移
导出工作流
复制以下 JSON 配置到 n8n 导入,即可使用此工作流
{
  "id": "MQmMLg34LuvMsRan",
  "meta": {
    "instanceId": "a24c3628d7fa97ff1f4002e54a6f44641e116c70a069e29f6d0359e4681ff811",
    "templateId": "806",
    "templateCredsSetupCompleted": true
  },
  "name": "3个城市的天气预报",
  "tags": [],
  "nodes": [
    {
      "id": "45a9c8c7-b596-4f48-baf8-09a46e4004df",
      "name": "JS - 验证来自LLM的JSON",
      "type": "n8n-nodes-base.code",
      "position": [
        1824,
        16
      ],
      "parameters": {
        "jsCode": "function extractRaw(o) {\n  if (typeof o === 'string') return o;\n  const m = o.message || o.messages || o;\n  if (typeof m?.content === 'string') return m.content;\n  if (Array.isArray(m?.content)) {\n    const first = m.content[0];\n    if (typeof first?.text?.value === 'string') return first.text.value;\n    if (typeof first?.text === 'string') return first.text;\n  }\n  if (Array.isArray(m)) {\n    const c = m[0]?.content;\n    if (typeof c === 'string') return c;\n    if (Array.isArray(c)) {\n      const first = c[0];\n      if (typeof first?.text?.value === 'string') return first.text.value;\n      if (typeof first?.text === 'string') return first.text;\n    }\n  }\n  return JSON.stringify(o);\n}\nfunction tryParseJSON(s) {\n  if (!s || typeof s !== 'string') return null;\n  try { return JSON.parse(s); } catch {}\n  const start = s.indexOf('{');\n  const end = s.lastIndexOf('}');\n  if (start >= 0 && end > start) {\n    const candidate = s.slice(start, end + 1);\n    try { return JSON.parse(candidate); } catch {}\n  }\n  return null;\n}\n\nconst raw = extractRaw($json);\nconst parsed = tryParseJSON(raw);\n\nif (parsed && parsed.subject && parsed.html) {\n  return [{ subject: parsed.subject, html: parsed.html, ok: true }];\n} else {\n  return [{ ok: false, reason: 'bad_json', raw }];\n}\n"
      },
      "typeVersion": 2
    },
    {
      "id": "f837e628-3bac-4d22-afef-32ba3df0c016",
      "name": "GMAIL - 发送天气简报",
      "type": "n8n-nodes-base.gmail",
      "position": [
        1712,
        352
      ],
      "webhookId": "9043b9cb-2579-49d5-8812-f39b84575dc7",
      "parameters": {
        "sendTo": "<<recipient@example.com>>",
        "message": "={{ $('LLM - AI Weather Briefing Composer').item.json.message.content.html }}",
        "options": {},
        "subject": "={{ $('LLM - AI Weather Briefing Composer').item.json.message.content.subject }}"
      },
      "credentials": {
        "gmailOAuth2": {
          "id": "<< REPLACE_WITH_CREDENTIAL_REF >>",
          "name": "Gmail account"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "b6c7d8b2-0a69-472d-b508-3453eadeea4b",
      "name": "JS - 为LLM打包城市数据",
      "type": "n8n-nodes-base.code",
      "position": [
        1328,
        96
      ],
      "parameters": {
        "jsCode": "// Pack all incoming items into one payload for the LLM.\nconst cities = items.map(i => ({\n  city: i.json.city,\n  country: i.json.country,\n  summaries: i.json.summaries\n}));\nreturn [{ cities }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "e4a3a918-3b48-44ba-a463-3d77786fdc9a",
      "name": "JS - 构建每日摘要(5天)",
      "type": "n8n-nodes-base.code",
      "position": [
        1216,
        288
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// We need to get the asked city from Pick Lat/Lon node\n// Since we can't use $items() reliably, we'll store it differently\n\n// The OpenWeatherMap response has city.name which is the API's name\n// But we need the original asked city name\n// Check if we have it in the input, otherwise fall back to API name\nconst apiCityName = $json.city?.name || 'City';\nconst apiCountry = $json.city?.country || '';\nconst tzOffset = $json.city?.timezone || 0; // seconds\nconst data = $json.list || [];\nconst units = 'metric';\n\n// Use the API city name for now - we'll fix this in the next step\nconst city = apiCityName;\nconst country = apiCountry;\n\nfunction toLocalDateStr(dt) {\n  // dt is unix seconds; shift by city timezone\n  const d = new Date((dt + tzOffset) * 1000);\n  return d.toISOString().slice(0,10); // YYYY-MM-DD\n}\n\n// Group by day\nconst days = {};\nfor (const p of data) {\n  const day = toLocalDateStr(p.dt);\n  if (!days[day]) days[day] = { points: [] };\n  days[day].points.push(p);\n}\n\n// Aggregate per day\nconst daySummaries = Object.entries(days).map(([date, obj]) => {\n  const pts = obj.points;\n\n  const temps = pts.map(p => p.main.temp);\n  const min = Math.min(...temps);\n  const max = Math.max(...temps);\n\n  // wind\n  const windMax = Math.max(...pts.map(p => (p.wind?.gust ?? p.wind?.speed ?? 0)));\n\n  // rain total (mm)\n  const rain = pts.reduce((acc, p) => acc + (p.rain?.['3h'] || 0), 0);\n\n  // pop average (precip probability 0..1)\n  const popAvg = pts.reduce((a,p)=>a+(p.pop||0),0) / (pts.length || 1);\n\n  // humidity average\n  const humAvg = pts.reduce((a,p)=>a+(p.main?.humidity||0),0) / (pts.length || 1);\n\n  // dominant condition by frequency\n  const counts = {};\n  for (const p of pts) {\n    const desc = (p.weather?.[0]?.main || 'Other');\n    counts[desc] = (counts[desc] || 0) + 1;\n  }\n  const dominant = Object.entries(counts).sort((a,b)=>b[1]-a[1])[0]?.[0] || 'Other';\n\n  return {\n    date,\n    min_temp: Number(min.toFixed(1)),\n    max_temp: Number(max.toFixed(1)),\n    dominant_condition: dominant,\n    rain_mm: Number(rain.toFixed(1)),\n    precip_prob_avg: Number((popAvg*100).toFixed(0)),\n    wind_max: Number(windMax.toFixed(1)),\n    humidity_avg: Number(humAvg.toFixed(0)),\n    unit_temp: units === 'metric' ? '°C' : '°F',\n    unit_wind: units === 'metric' ? 'm/s' : 'mph'\n  };\n}).sort((a,b)=>a.date.localeCompare(b.date));\n\n// keep only next 5 days\nconst next5 = daySummaries.slice(0,5);\n\nreturn {\n  city,\n  country,\n  timezone_offset_seconds: tzOffset,\n  summaries: next5\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "0a824d7f-d3d3-4c8e-b782-36d5c94191ae",
      "name": "OpenWeatherMap (OWM) - 5天预报(按城市)",
      "type": "n8n-nodes-base.openWeatherMap",
      "position": [
        1024,
        80
      ],
      "parameters": {
        "cityName": "={{ $json.asked_city }}",
        "operation": "5DayForecast"
      },
      "credentials": {
        "openWeatherMapApi": {
          "id": "KwBMaZHr4WNwZ47Y",
          "name": "OpenWeatherMap account 2"
        }
      },
      "executeOnce": false,
      "typeVersion": 1
    },
    {
      "id": "2ad3a2e8-3605-4203-a113-44fe99c34e66",
      "name": "JS - 选择经纬度",
      "type": "n8n-nodes-base.code",
      "position": [
        640,
        288
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const asked = $json.city || null;\nconst location = $json.body?.[0] || $json.body;\n\nif (!location || !location.lat || !location.lon) {\n  throw new Error('No valid geocoding results for: ' + asked);\n}\n\nreturn {\n  asked_city: asked,\n  city_from_api: location.name || asked,\n  country: location.country || null,\n  lat: location.lat,\n  lon: location.lon\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "e6202cc9-8516-4c12-beea-1c3dca68dc8a",
      "name": "SET - 将查询城市传递至预报",
      "type": "n8n-nodes-base.set",
      "position": [
        832,
        288
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "2c7d6017-02ca-4e76-ab6e-000707eea2ab",
              "name": "asked_city",
              "type": "string",
              "value": "={{ $json.asked_city }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "d2f43ed1-2f4a-476b-8e38-4277ce72e786",
      "name": "MERGE - 配对城市",
      "type": "n8n-nodes-base.merge",
      "position": [
        464,
        288
      ],
      "parameters": {
        "mode": "combine",
        "options": {
          "includeUnpaired": false
        },
        "combineBy": "combineByPosition"
      },
      "typeVersion": 3.2
    },
    {
      "id": "5dd95724-b5fe-4230-87b4-4554f1dc74ea",
      "name": "HTTP - OWM地理编码",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        224,
        32
      ],
      "parameters": {
        "url": "https://api.openweathermap.org/geo/1.0/direct\n",
        "options": {
          "response": {
            "response": {
              "fullResponse": true,
              "responseFormat": "json"
            }
          }
        },
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "q",
              "value": "=={{ encodeURIComponent($json.city) }}"
            },
            {
              "name": "limit",
              "value": "1"
            },
            {
              "name": "appid",
              "value": "<<YOUR_API_KEY>>"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "d574f191-aed1-472c-80e0-96f22ef14480",
      "name": "JS - 标准化城市输入",
      "type": "n8n-nodes-base.code",
      "position": [
        16,
        304
      ],
      "parameters": {
        "jsCode": "// Collect only city names from the form payload\n// Filter out metadata fields like submittedAt and formMode\nconst cities = [];\n\nfor (const [key, value] of Object.entries($json)) {\n  // Exclude known metadata fields\n  if (key === 'submittedAt' || key === 'formMode') {\n    continue;\n  }\n  \n  // Include all other string values that are not empty\n  if (typeof value === 'string' && value.trim()) {\n    cities.push(value.trim());\n  }\n}\n\n// Limit to 3 cities and create one item per city\nreturn cities.slice(0, 3).map(c => ({ city: c }));"
      },
      "typeVersion": 2
    },
    {
      "id": "11ec0415-f62f-4e61-8416-7468c235e2b7",
      "name": "TRIGGER - 输入城市名称",
      "type": "n8n-nodes-base.formTrigger",
      "position": [
        -160,
        304
      ],
      "webhookId": "37fec581-130f-4c8f-9aad-33f0832f2fd3",
      "parameters": {
        "options": {},
        "formTitle": "Weather predictor demo task",
        "formFields": {
          "values": [
            {
              "fieldLabel": "Enter City Name 1",
              "placeholder": "eg: Hyderabad",
              "requiredField": true
            },
            {
              "fieldLabel": "Enter City Name 2",
              "placeholder": "eg: Bangalore",
              "requiredField": true
            },
            {
              "fieldLabel": "Enter City Name 3",
              "placeholder": "Chennai",
              "requiredField": true
            }
          ]
        },
        "formDescription": "It provides the weather prediction of 3 cities and provide general tips and precuations"
      },
      "typeVersion": 2.3
    },
    {
      "id": "7b1dfae5-be7d-4d76-9f6c-3bf3bc0eb271",
      "name": "LLM - AI天气简报生成器",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        1520,
        16
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o-mini",
          "cachedResultName": "GPT-4O-MINI"
        },
        "options": {},
        "messages": {
          "values": [
            {
              "role": "system",
              "content": "=You are an expert meteorologist creating professional weather briefings. Return STRICT JSON with keys: subject, html. Use clear, actionable language. No conversational text outside the JSON.\n\n"
            },
            {
              "content": "=You are an expert meteorologist creating professional weather briefings. Return STRICT JSON with keys: subject, html. Use clear, actionable language. No conversational text outside the JSON.\n```\n\n## **Revised User Prompt:**\n```\nCreate a professional 5-day weather forecast email for EACH city in this JSON:\n{{ JSON.stringify($json.cities) }}\n\nREQUIREMENTS:\n\n📋 Structure:\n- Start with a friendly greeting and brief intro\n- For each city, use H2 with \"🌤️ City — Country\"\n- HTML table with these columns: Date | Temp Range | Conditions | Rain(mm) | Precip(%) | Wind(m/s) | General Tips | Precautions\n- Add a concluding paragraph with travel/planning tips across all cities\n\n🎨 Styling (use inline CSS):\n- Header row: background #4A90E2 (blue), white text, bold, font-size: 14px\n- Borders: 1px solid #DEE2E6 on all cells\n- Padding: 10px in cells\n- Font: Arial, sans-serif, font-size: 13px\n- Center-align all table content\n- Table width: 100%, border-collapse: collapse\n\n🌈 CONDITION-BASED ROW COLORS (apply to entire row based on dominant_condition):\n- \"Clear\": background-color: #FFFACD (light yellow)\n- \"Clouds\": background-color: #ADD8E6 (light blue)\n- \"Rain\": background-color: #FFB6C6 (light red/pink)\n- Extremely sunny (max_temp ≥ 35°C AND condition is \"Clear\"): background-color: #FFBF00 (amber)\n- Default (other conditions): background-color: #FFFFFF (white)\n\n📊 Content Guidelines:\n- Date: Format as \"YYYY-MM-DD (Day)\" - e.g., \"2025-10-19 (Sun)\"\n- Temp Range: Show as \"Min°C / Max°C\" (e.g., \"24° / 32°\")\n- Conditions: Add weather emoji + condition name:\n  - Clear: ☀️ Clear\n  - Clouds: ☁️ Clouds\n  - Rain: 🌧️ Rain\n  - Thunderstorm: ⛈️ Thunderstorm\n- Rain: Show mm value\n- Precip%: Show percentage value\n- Wind: Show m/s value\n- General Tips: Brief everyday advice (e.g., \"Stay hydrated\", \"Dress lightly\")\n- Precautions: Specific safety warnings (detailed below)\n\n⚠️ PRECAUTIONS Column Content (show relevant warnings only, or \"None\" if weather is safe):\n- Heavy rain (rain_mm ≥ 10 OR precip_prob_avg ≥ 70): \"⚠️ Heavy rain expected. Carry umbrella, avoid travel if possible. Expect waterlogging.\"\n- Moderate rain (rain_mm ≥ 5 AND < 10 OR precip_prob_avg ≥ 50 AND < 70): \"☔ Rain likely. Carry rain gear, drive carefully.\"\n- Extreme heat (max_temp ≥ 35°C): \"🌡️ HEAT ALERT: Stay indoors during peak hours (12-4 PM). Drink 3+ liters water. Risk of heat stroke.\"\n- High heat (max_temp ≥ 32°C AND < 35°C): \"☀️ Very hot. Limit outdoor activities. Use sunscreen SPF 50+.\"\n- High wind (wind_max ≥ 10 m/s): \"💨 Strong winds expected. Secure loose objects. Avoid outdoor hoardings.\"\n- Thunderstorms (if \"Thunderstorm\" in conditions): \"⛈️ STORM WARNING: Stay indoors. Unplug electronics. Avoid water bodies.\"\n- Multiple conditions: Combine warnings separated by \" | \"\n- Safe weather: \"None\" or \"✅ Safe weather conditions\"\n\n💡 GENERAL TIPS Column Content (positive, practical daily advice):\n- Clear & pleasant (max < 32°C): \"Great for outdoor activities\"\n- Clear & hot (max ≥ 32°C): \"Stay hydrated, use sun protection\"\n- Clouds: \"Comfortable weather, good for sightseeing\"\n- Light rain: \"Keep umbrella handy\"\n- Heavy rain: \"Plan indoor activities\"\n- Windy: \"Secure loose items\"\n\n📝 Email Subject:\nCreate an engaging subject that highlights the most significant weather event:\n- If any day has rain_mm ≥ 10: \"⚠️ Heavy Rain Alert: [City Names] | 5-Day Forecast\"\n- If any day has max_temp ≥ 35°C: \"🌡️ Heat Wave Alert: [City Names] | 5-Day Forecast\"\n- If all days are clear: \"☀️ Sunny Week Ahead: [City Names] | 5-Day Forecast\"\n- Otherwise: \"5-Day Weather Forecast: [City Names]\"\n\n🎯 Conclusion Paragraph:\nInclude:\n- Overall weather pattern across all cities\n- Best days for outdoor activities/travel\n- Most important safety advisory if any extreme weather\n- Friendly closing line\n\nCRITICAL: Apply the condition-based background color to the ENTIRE ROW (all cells in that row), not just the Conditions column.\n\nReturn ONLY valid JSON:\n{\"subject\":\"...\",\"html\":\"...\"}"
            }
          ]
        },
        "jsonOutput": true
      },
      "credentials": {
        "openAiApi": {
          "id": "<< REPLACE_WITH_OPENAI_CREDENTIAL_REF >>",
          "name": "OpenAi account"
        }
      },
      "typeVersion": 1.8
    },
    {
      "id": "07e5c472-3d02-4579-a1ef-77bd8ea44bf5",
      "name": "便签",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -816,
        -512
      ],
      "parameters": {
        "width": 608,
        "height": 1056,
        "content": "# 试用说明"
      },
      "typeVersion": 1
    },
    {
      "id": "39f8510b-c1a3-44b7-97bd-fe491bd72d15",
      "name": "便签1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        176,
        -160
      ],
      "parameters": {
        "color": 4,
        "width": 208,
        "height": 656,
        "content": "## 地理编码 (OWM)"
      },
      "typeVersion": 1
    },
    {
      "id": "746f1466-187a-421b-8160-b4a982dd02a8",
      "name": "便签2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        400,
        96
      ],
      "parameters": {
        "color": 6,
        "width": 560,
        "height": 400,
        "content": "## 城市与坐标配对"
      },
      "typeVersion": 1
    },
    {
      "id": "fd98f570-9beb-4cb7-9de3-eb1fb2599a33",
      "name": "便签3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        976,
        -144
      ],
      "parameters": {
        "color": 3,
        "width": 208,
        "height": 640,
        "content": "## 预报获取"
      },
      "typeVersion": 1
    },
    {
      "id": "7864bbf7-eeab-43a6-ab00-7d1786a5f414",
      "name": "便签4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1456,
        -144
      ],
      "parameters": {
        "color": 2,
        "width": 576,
        "height": 352,
        "content": "## LLM AI天气简报"
      },
      "typeVersion": 1
    },
    {
      "id": "8ac37160-d037-42a3-8900-c70f326c5901",
      "name": "便签5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1200,
        -144
      ],
      "parameters": {
        "color": 4,
        "height": 640,
        "content": "## 摘要与打包"
      },
      "typeVersion": 1
    },
    {
      "id": "fe07b361-deae-4f13-b3a7-d85804d5d7a7",
      "name": "### 需要帮助?",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1456,
        224
      ],
      "parameters": {
        "color": 6,
        "width": 576,
        "height": 288,
        "content": "## 交付"
      },
      "typeVersion": 1
    },
    {
      "id": "0d440118-543a-44ca-bdc1-03187c110850",
      "name": "## 试试看!",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -192,
        112
      ],
      "parameters": {
        "color": 5,
        "width": 352,
        "height": 384,
        "content": "## 输入与标准化"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "dd0565ff-6b0c-4003-a4a6-fd5579f32295",
  "connections": {
    "JS - Pick Lat/Lon": {
      "main": [
        [
          {
            "node": "SET - Carry Asked City to Forecast",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "MERGE - Pair Cities": {
      "main": [
        [
          {
            "node": "JS - Pick Lat/Lon",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP - OWM Geocoding": {
      "main": [
        [
          {
            "node": "MERGE - Pair Cities",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "JS - Bundle Cities for LLM": {
      "main": [
        [
          {
            "node": "LLM - AI Weather Briefing Composer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "JS - Normalize City Inputs": {
      "main": [
        [
          {
            "node": "HTTP - OWM Geocoding",
            "type": "main",
            "index": 0
          },
          {
            "node": "MERGE - Pair Cities",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "TRIGGER - Input City Names": {
      "main": [
        [
          {
            "node": "JS - Normalize City Inputs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "JS - Validate JSON from LLM": {
      "main": [
        [
          {
            "node": "GMAIL - Send Weather Briefing",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "LLM - AI Weather Briefing Composer": {
      "main": [
        [
          {
            "node": "JS - Validate JSON from LLM",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SET - Carry Asked City to Forecast": {
      "main": [
        [
          {
            "node": "OpenWeatherMap (OWM) - 5 Day Forecast (by City)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "JS - Build Daily Summaries (5 days)": {
      "main": [
        [
          {
            "node": "JS - Bundle Cities for LLM",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenWeatherMap (OWM) - 5 Day Forecast (by City)": {
      "main": [
        [
          {
            "node": "JS - Build Daily Summaries (5 days)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
常见问题

如何使用这个工作流?

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

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

高级 - 个人效率, 多模态 AI

需要付费吗?

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

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

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

作者
Sridevi Edupuganti

Sridevi Edupuganti

@edupuganti

I help customers experience 10x faster ROI through AI Automation. AI Generalist | Pursuing Generative AI & ML(IIT-G) | Google Certified Prompt Engineer | Ex-VP | Ex-Microsoft | ISB Certified CTO & AI Leader | Azure & AI Strategist | 5X Azure | n8n level2 | Wellness Advocate & Cult Ninja

外部链接
在 n8n.io 查看

分享此工作流