UNPKG

@bashcat/cwa-mcp-weather

Version:

MCP 伺服器整合中央氣象署 (CWA) 開放資料 API - 完整支援所有15個天氣工具

1,122 lines 62.7 kB
import { COUNTY_API_MAPPING } from './types.js'; import { readFileSync } from 'fs'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const stationMapping = JSON.parse(readFileSync(join(__dirname, 'station-mapping.json'), 'utf-8')); const tidalStationTypes = JSON.parse(readFileSync(join(__dirname, 'tidal-station-types.json'), 'utf-8')); export class CWAAPIClient { config; constructor(apiKey) { this.config = { baseUrl: 'https://opendata.cwa.gov.tw/api', apiKey }; } /** * 呼叫 CWA API */ async callAPI(endpoint, params = {}) { // 移除 endpoint 前導斜線,確保正確的 URL 構建 const cleanEndpoint = endpoint.startsWith('/') ? endpoint.slice(1) : endpoint; const url = new URL(cleanEndpoint, this.config.baseUrl + '/'); // 添加必要參數 url.searchParams.append('Authorization', this.config.apiKey); url.searchParams.append('format', 'JSON'); // 添加其他參數 Object.entries(params).forEach(([key, value]) => { if (value !== undefined && value !== null) { if (Array.isArray(value)) { value.forEach(v => url.searchParams.append(key, v)); } else { url.searchParams.append(key, value.toString()); } } }); try { const response = await fetch(url.toString()); if (!response.ok) { if (response.status === 401) { throw new Error(`CWA API 授權失敗 (${response.status}): 請檢查API密鑰是否正確`); } else if (response.status === 404) { throw new Error(`CWA API 端點不存在 (${response.status}): ${cleanEndpoint}`); } else { throw new Error(`CWA API 請求失敗: ${response.status} ${response.statusText}`); } } const data = await response.json(); if (data.success !== 'true') { throw new Error('CWA API 回傳錯誤'); } // 將請求參數附加到回應資料中,以便格式化時使用 data._requestParams = params; return data; } catch (error) { throw new Error(`CWA API 呼叫失敗: ${error instanceof Error ? error.message : '未知錯誤'}`); } } /** * 載入站點映射資料 */ loadStationMapping() { // 使用頂部已載入的資料,保持向後相容性 return stationMapping; } // === 預報類 API === /** * 取得縣市天氣預報(今明36小時) */ async getCountyWeather(params = {}) { return this.callAPI('/v1/rest/datastore/F-C0032-001', params); } /** * 取得鄉鎮天氣預報 */ async getTownshipWeather(params) { const apiMapping = COUNTY_API_MAPPING[params.county]; if (!apiMapping) { throw new Error(`不支援的縣市: ${params.county}`); } const apiId = params.period === '3days' ? apiMapping.threeDays : apiMapping.weekly; const endpoint = `/v1/rest/datastore/${apiId}`; // 移除 county 和 period 參數,因為它們不是 API 參數 const { county, period, ...apiParams } = params; return this.callAPI(endpoint, apiParams); } /** * 取得潮汐預報 */ async getTidalForecast(params = {}) { return this.callAPI('/v1/rest/datastore/F-A0021-001', params); } /** * 取得健康氣象預報 */ async getHealthWeatherForecast(params) { const { apiId, ...apiParams } = params; return this.callAPI(`/v1/rest/datastore/${apiId}`, apiParams); } // === 觀測類 API === /** * 取得自動氣象站觀測資料 */ async getWeatherStationData(params = {}) { const data = await this.callAPI('/v1/rest/datastore/O-A0001-001', params); // 如果有指定站名,進行精確搜尋篩選 if (params.stationName && params.stationName.length > 0 && data.records && data.records.Station) { const filteredStations = data.records.Station.filter((station) => { return params.stationName.some(searchName => { // 精確搜尋: 優先縣市名稱完全匹配 if (station.GeoInfo?.CountyName === searchName) return true; if (station.GeoInfo?.TownName === searchName) return true; if (station.StationName === searchName) return true; // 模糊搜尋: 包含搜尋詞 const countyMatch = station.GeoInfo?.CountyName?.includes(searchName); const townMatch = station.GeoInfo?.TownName?.includes(searchName); const stationMatch = station.StationName?.includes(searchName); return countyMatch || townMatch || stationMatch; }); }); data.records.Station = filteredStations; } return data; } /** * 取得自動雨量站觀測資料 */ async getRainfallStationData(params = {}) { const data = await this.callAPI('/v1/rest/datastore/O-A0002-001', params); // 如果有指定站名,進行精確搜尋篩選 if (params.stationName && params.stationName.length > 0 && data.records && data.records.Station) { const filteredStations = data.records.Station.filter((station) => { return params.stationName.some(searchName => { // 精確搜尋: 優先縣市名稱完全匹配 if (station.GeoInfo?.CountyName === searchName) return true; if (station.GeoInfo?.TownName === searchName) return true; if (station.StationName === searchName) return true; // 模糊搜尋: 包含搜尋詞 const countyMatch = station.GeoInfo?.CountyName?.includes(searchName); const townMatch = station.GeoInfo?.TownName?.includes(searchName); const stationMatch = station.StationName?.includes(searchName); return countyMatch || townMatch || stationMatch; }); }); data.records.Station = filteredStations; } return data; } /** * 取得現在天氣觀測報告 */ async getCurrentWeatherReport(params = {}) { const data = await this.callAPI('/v1/rest/datastore/O-A0003-001', params); // 如果有指定地點名稱,進行精確搜尋篩選 if (params.locationName && params.locationName.length > 0 && data.records && data.records.Station) { const filteredStations = data.records.Station.filter((station) => { return params.locationName.some(searchName => { // 精確搜尋: 優先縣市名稱完全匹配 if (station.GeoInfo?.CountyName === searchName) return true; if (station.GeoInfo?.TownName === searchName) return true; if (station.StationName === searchName) return true; // 模糊搜尋: 包含搜尋詞 const countyMatch = station.GeoInfo?.CountyName?.includes(searchName); const townMatch = station.GeoInfo?.TownName?.includes(searchName); const stationMatch = station.StationName?.includes(searchName); return countyMatch || townMatch || stationMatch; }); }); data.records.Station = filteredStations; } return data; } /** * 取得紫外線指數 */ async getUVIndex(params = {}) { const data = await this.callAPI('/v1/rest/datastore/O-A0005-001', params); // 如果有指定地點名稱,根據 StationID 對應來篩選 if (params.locationName && params.locationName.length > 0 && data.records?.weatherElement?.location) { // 載入站點映射資料 const stationMapping = this.loadStationMapping(); const filteredLocations = data.records.weatherElement.location.filter((location) => { const stationInfo = stationMapping[location.StationID]; if (!stationInfo) return false; return params.locationName.some(searchName => { // 精確搜尋: 縣市名稱、站點名稱 const countyMatch = stationInfo.CountyName.includes(searchName); const stationMatch = stationInfo.StationName.includes(searchName); const combinedMatch = `${stationInfo.CountyName}${stationInfo.StationName}`.includes(searchName); return countyMatch || stationMatch || combinedMatch; }); }); data.records.weatherElement.location = filteredLocations; } return data; } /** * 取得臭氧總量觀測資料 */ async getOzoneData(params = {}) { return this.callAPI('/v1/rest/datastore/O-A0006-001', params); } /** * 取得海象監測資料 */ async getMarineData(params) { const apiId = params.period === '48hours' ? 'O-B0074-001' : 'O-B0075-003'; const { period, ...apiParams } = params; return this.callAPI(`/v1/rest/datastore/${apiId}`, apiParams); } // === 地震海嘯類 API === /** * 取得海嘯資訊 */ async getTsunamiData(params = {}) { return this.callAPI('/v1/rest/datastore/E-A0014-001', params); } /** * 取得地震報告 */ async getEarthquakeReport(params) { let apiId; if (params.type === 'significant') { apiId = params.language === 'zh' ? 'E-A0015-001' : 'E-A0015-002'; } else { apiId = params.language === 'zh' ? 'E-A0016-001' : 'E-A0016-002'; } const { type, language, ...apiParams } = params; return this.callAPI(`/v1/rest/datastore/${apiId}`, apiParams); } // === 氣候類 API === /** * 取得每日雨量資料 */ async getDailyRainfall(params = {}) { return this.callAPI('/v1/rest/datastore/C-B0024-001', params); } /** * 取得月平均資料 */ async getMonthlyAverage(params = {}) { return this.callAPI('/v1/rest/datastore/C-B0025-001', params); } /** * 取得氣象測站基本資料 */ async getStationInfo(params) { const apiId = params.type === 'manned' ? 'C-B0074-001' : 'C-B0074-002'; const { type, ...apiParams } = params; return this.callAPI(`/v1/rest/datastore/${apiId}`, apiParams); } // === 天氣警特報類 API === /** * 取得天氣特報 */ async getWeatherWarning(params) { const apiId = params.type === 'county' ? 'W-C0033-001' : 'W-C0033-002'; const { type, ...apiParams } = params; return this.callAPI(`/v1/rest/datastore/${apiId}`, apiParams); } /** * 取得颱風資訊 */ async getTyphoonData(params = {}) { return this.callAPI('/v1/rest/datastore/W-C0034-005', params); } // === 數值預報類 API === /** * 取得數值預報資料 */ async getNumericalForecast(params = {}) { return this.callAPI('/v1/rest/datastore/M-A0064-001', params); } // === 天文類 API === /** * 取得日出日沒時刻 */ async getSunriseSunset(params = {}) { return this.callAPI('/v1/rest/datastore/A-B0062-001', params); } /** * 取得月出月沒時刻 */ async getMoonriseMoonset(params = {}) { return this.callAPI('/v1/rest/datastore/A-B0063-001', params); } // === 工具方法 === /** * 取得所有支援的縣市列表 */ getSupportedCounties() { return Object.keys(COUNTY_API_MAPPING); } /** * 檢查 API 是否可用(測試連線) */ async testConnection() { try { await this.getCountyWeather({ locationName: ['臺北市'] }); return true; } catch (error) { console.error('CWA API 連線測試失敗:', error); return false; } } /** * 格式化天氣資料為可讀的文字(限制長度避免 MCP 協議問題) */ formatWeatherData(data) { let result = `## ${data.records.datasetDescription}\n\n`; let addedLocations = 0; const maxLocations = 3; // 建議單次查詢1-3個地點 for (const location of data.records.location) { if (addedLocations >= maxLocations) { result += `\n**提示:僅顯示前${maxLocations}個地點。建議指定特定縣市以獲得完整資訊。**\n`; break; } let locationSection = `### ${location.locationName}\n`; if (location.geocode) { locationSection += `地區代碼: ${location.geocode}\n`; } if (location.lat && location.lon) { locationSection += `座標: ${location.lat}, ${location.lon}\n`; } location.weatherElement.forEach((element) => { locationSection += `\n**${element.elementName}**\n`; // 顯示所有時間點,不限制 element.time.forEach((timeData) => { const startTime = new Date(timeData.startTime).toLocaleString('zh-TW'); const endTime = new Date(timeData.endTime).toLocaleString('zh-TW'); locationSection += `- ${startTime} ~ ${endTime}: ${timeData.parameter.parameterName}`; if (timeData.parameter.parameterValue) { locationSection += ` (${timeData.parameter.parameterValue}`; if (timeData.parameter.parameterUnit) { locationSection += ` ${timeData.parameter.parameterUnit}`; } locationSection += ')'; } locationSection += '\n'; }); }); locationSection += '\n---\n\n'; result += locationSection; addedLocations++; } return result; } /** * 格式化觀測資料為可讀的文字 */ formatObservationData(data) { let result = `## 觀測資料\n\n`; // 檢查紫外線指數資料結構 if (data.records && data.records.weatherElement) { const weatherElement = data.records.weatherElement; if (weatherElement.elementName === '氣象站每日紫外線指數最大值' && weatherElement.location) { result = `## 每日紫外線指數最大值\n\n`; if (weatherElement.Date) { result += `**觀測日期:** ${weatherElement.Date}\n\n`; } let addedStations = 0; const maxStations = 10; const stationMapping = this.loadStationMapping(); // 使用已經篩選過的站點資料 const stationsToShow = weatherElement.location; if (stationsToShow.length === 0) { return `## 每日紫外線指數最大值\n\n**無符合條件的測站資料**`; } for (const station of stationsToShow) { if (addedStations >= maxStations) { result += `\n**提示:僅顯示前${maxStations}個測站的紫外線指數。**\n`; break; } // 從 station-mapping 中取得站點資訊 const stationInfo = stationMapping[station.StationID]; if (stationInfo) { result += `### ${stationInfo.StationName} (${station.StationID})\n`; result += `位置: ${stationInfo.CountyName} ${stationInfo.Location}\n`; result += `紫外線指數: ${station.UVIndex}\n\n---\n\n`; } else { result += `### 測站 ${station.StationID}\n`; result += `紫外線指數: ${station.UVIndex}\n\n---\n\n`; } addedStations++; } return result; } } // 檢查數據結構 - 觀測資料的結構是 records.Station if (!data.records || !data.records.Station) { return `## 觀測資料\n\n**無可用的觀測資料**`; } // 確保 Station 是數組 const stations = Array.isArray(data.records.Station) ? data.records.Station : [data.records.Station]; // 如果原始請求有 locationName 或 stationName 參數,先進行篩選 let filteredStations = stations; if (data._requestParams?.locationName) { filteredStations = this.filterStationsByLocation(stations, data._requestParams.locationName); } else if (data._requestParams?.stationName) { filteredStations = this.filterStationsByLocation(stations, data._requestParams.stationName); } if (filteredStations.length === 0) { return `## 觀測資料\n\n**無符合條件的觀測站資料**`; } let addedStations = 0; const maxStations = 10; // 限制顯示前10個測站 for (const station of filteredStations) { if (addedStations >= maxStations) { result += `\n**提示:僅顯示前${maxStations}個測站。建議指定特定地點以獲得詳細資訊。**\n`; break; } let stationSection = `### ${station.StationName} (${station.StationId})\n`; if (station.ObsTime?.DateTime) { const obsTime = new Date(station.ObsTime.DateTime).toLocaleString('zh-TW'); stationSection += `觀測時間: ${obsTime}\n`; } if (station.GeoInfo) { stationSection += `位置: ${station.GeoInfo.CountyName} ${station.GeoInfo.TownName}\n`; stationSection += `海拔: ${station.GeoInfo.StationAltitude}m\n`; } stationSection += '\n**觀測數據:**\n'; // 處理觀測要素 if (station.WeatherElement) { const weather = station.WeatherElement; if (weather.Weather && weather.Weather !== '-99') { stationSection += `- 天氣狀況: ${weather.Weather}\n`; } if (weather.AirTemperature && weather.AirTemperature !== '-99') { stationSection += `- 氣溫: ${weather.AirTemperature}°C\n`; } if (weather.RelativeHumidity && weather.RelativeHumidity !== '-99') { stationSection += `- 相對濕度: ${weather.RelativeHumidity}%\n`; } if (weather.WindDirection && weather.WindDirection !== '-99') { stationSection += `- 風向: ${weather.WindDirection}°\n`; } if (weather.WindSpeed && weather.WindSpeed !== '-99') { stationSection += `- 風速: ${weather.WindSpeed} m/s\n`; } if (weather.AirPressure && weather.AirPressure !== '-99') { stationSection += `- 氣壓: ${weather.AirPressure} hPa\n`; } if (weather.Now?.Precipitation && weather.Now.Precipitation !== '-99') { stationSection += `- 降雨量: ${weather.Now.Precipitation} mm\n`; } } stationSection += '\n---\n\n'; result += stationSection; addedStations++; } return result; } /** * 格式化地震資料為可讀的文字(限制長度避免 MCP 協議問題) */ formatEarthquakeData(data) { let result = `## 地震報告\n\n`; // 檢查數據結構 - 地震資料的結構是 records.Earthquake if (!data.records || !data.records.Earthquake) { return `## 地震報告\n\n**無可用的地震資料**`; } const earthquakes = Array.isArray(data.records.Earthquake) ? data.records.Earthquake : [data.records.Earthquake]; let addedEarthquakes = 0; const maxEarthquakes = 3; // 限制顯示最近3筆地震 for (const earthquake of earthquakes) { if (addedEarthquakes >= maxEarthquakes) { result += `\n**提示:僅顯示最近${maxEarthquakes}筆地震報告。**\n`; break; } let earthquakeSection = `### 地震編號: ${earthquake.EarthquakeNo}\n`; if (earthquake.EarthquakeInfo) { const info = earthquake.EarthquakeInfo; if (info.OriginTime) { const time = new Date(info.OriginTime).toLocaleString('zh-TW'); earthquakeSection += `**發生時間:** ${time}\n`; } if (info.Epicenter) { earthquakeSection += `**震央位置:** ${info.Epicenter.Location}\n`; earthquakeSection += `**震央座標:** ${info.Epicenter.EpicenterLatitude}°N, ${info.Epicenter.EpicenterLongitude}°E\n`; } if (info.FocalDepth) { earthquakeSection += `**震源深度:** ${info.FocalDepth} 公里\n`; } if (info.EarthquakeMagnitude) { earthquakeSection += `**規模:** ${info.EarthquakeMagnitude.MagnitudeType} ${info.EarthquakeMagnitude.MagnitudeValue}\n`; } if (info.Source) { earthquakeSection += `**資料來源:** ${info.Source}\n`; } } if (earthquake.ReportType) { earthquakeSection += `**報告類型:** ${earthquake.ReportType}\n`; } if (earthquake.ReportColor) { earthquakeSection += `**警報等級:** ${earthquake.ReportColor}\n`; } if (earthquake.ReportContent) { earthquakeSection += `**報告內容:** ${earthquake.ReportContent}\n`; } // 震度資訊(限制顯示前3個區域) if (earthquake.Intensity && earthquake.Intensity.ShakingArea) { earthquakeSection += `\n**震度分布:**\n`; const areas = earthquake.Intensity.ShakingArea.slice(0, 3); areas.forEach((area) => { if (area.AreaDesc && area.AreaIntensity) { earthquakeSection += `- ${area.AreaDesc}: 震度${area.AreaIntensity}級\n`; if (area.CountyName) { earthquakeSection += ` 影響地區: ${area.CountyName}\n`; } } }); } if (earthquake.Web) { earthquakeSection += `\n**詳細資訊:** ${earthquake.Web}\n`; } if (earthquake.ReportImageURI) { earthquakeSection += `**地震報告圖:** ${earthquake.ReportImageURI}\n`; } earthquakeSection += '\n---\n\n'; result += earthquakeSection; addedEarthquakes++; } return result; } /** * 格式化海嘯資料為可讀的文字 */ formatTsunamiData(data) { let result = `## 🌊 海嘯資訊\n\n`; // 檢查數據結構 - 海嘯資料的結構是 records.Tsunami if (!data.records || !data.records.Tsunami) { result += `**目前無海嘯警報或資訊**\n\n`; result += `📋 **說明:** 海嘯資訊由中央氣象署即時監控,當無海嘯威脅時不會發布警報。\n\n`; result += `🔍 **查詢範圍:** 西北太平洋及台灣周邊海域\n`; result += `⏰ **查詢時間:** ${new Date().toLocaleString('zh-TW')}\n\n`; result += `💡 **注意事項:**\n`; result += `- 海嘯警報僅在有實際威脅時發布\n`; result += `- 中央氣象署24小時監控海嘯活動\n`; result += `- 如有海嘯威脅將立即發布警報\n`; return result; } const tsunamis = Array.isArray(data.records.Tsunami) ? data.records.Tsunami : [data.records.Tsunami]; if (tsunamis.length === 0) { result += `**目前無海嘯警報或資訊**\n\n`; result += `📋 **說明:** 海嘯資訊由中央氣象署即時監控,當無海嘯威脅時不會發布警報。\n\n`; result += `⏰ **查詢時間:** ${new Date().toLocaleString('zh-TW')}\n`; return result; } let addedTsunamis = 0; const maxTsunamis = 5; // 限制顯示最近5筆海嘯資訊 for (const tsunami of tsunamis) { if (addedTsunamis >= maxTsunamis) { result += `\n**提示:僅顯示最近${maxTsunamis}筆海嘯資訊。**\n`; break; } let tsunamiSection = `### 海嘯資訊 ${addedTsunamis + 1}\n`; if (tsunami.TsunamiNo) { tsunamiSection += `**海嘯編號:** ${tsunami.TsunamiNo}\n`; } if (tsunami.ReportType) { tsunamiSection += `**報告類型:** ${tsunami.ReportType}\n`; } if (tsunami.ReportColor) { let colorEmoji = ''; switch (tsunami.ReportColor) { case '紅色': colorEmoji = '🔴'; break; case '橙色': colorEmoji = '🟠'; break; case '黃色': colorEmoji = '🟡'; break; case '綠色': colorEmoji = '🟢'; break; default: colorEmoji = '⚠️'; } tsunamiSection += `**警報等級:** ${colorEmoji} ${tsunami.ReportColor}\n`; } if (tsunami.TsunamiInfo) { const info = tsunami.TsunamiInfo; if (info.OriginTime) { const time = new Date(info.OriginTime).toLocaleString('zh-TW'); tsunamiSection += `**發生時間:** ${time}\n`; } if (info.Epicenter) { tsunamiSection += `**震央位置:** ${info.Epicenter.Location}\n`; if (info.Epicenter.EpicenterLatitude && info.Epicenter.EpicenterLongitude) { tsunamiSection += `**震央座標:** ${info.Epicenter.EpicenterLatitude}°N, ${info.Epicenter.EpicenterLongitude}°E\n`; } } if (info.EarthquakeMagnitude) { tsunamiSection += `**地震規模:** ${info.EarthquakeMagnitude.MagnitudeType} ${info.EarthquakeMagnitude.MagnitudeValue}\n`; } if (info.FocalDepth) { tsunamiSection += `**震源深度:** ${info.FocalDepth} 公里\n`; } } if (tsunami.ReportContent) { tsunamiSection += `**海嘯內容:** ${tsunami.ReportContent}\n`; } // 影響區域資訊 if (tsunami.TsunamiArea && Array.isArray(tsunami.TsunamiArea)) { tsunamiSection += `\n**影響區域:**\n`; tsunami.TsunamiArea.forEach((area, index) => { if (index < 5) { // 限制顯示前5個區域 tsunamiSection += `- ${area.AreaName}: ${area.TsunamiHeight || '待確認'}\n`; if (area.EstimatedArrivalTime) { const arrivalTime = new Date(area.EstimatedArrivalTime).toLocaleString('zh-TW'); tsunamiSection += ` 預估到達時間: ${arrivalTime}\n`; } } }); } if (tsunami.Web) { tsunamiSection += `\n**詳細資訊:** ${tsunami.Web}\n`; } if (tsunami.ReportImageURI) { tsunamiSection += `**海嘯報告圖:** ${tsunami.ReportImageURI}\n`; } tsunamiSection += '\n---\n\n'; result += tsunamiSection; addedTsunamis++; } if (addedTsunamis === 0) { result += `**目前無海嘯警報或資訊**\n\n⏰ **查詢時間:** ${new Date().toLocaleString('zh-TW')}\n`; } return result; } /** * 格式化警特報資料為可讀的文字 */ formatWarningData(data) { let result = `## 天氣警特報\n\n`; data.records.record.forEach(record => { result += `### ${record.datasetDescription}\n`; if (record.locationName) { result += `**影響地區:** ${record.locationName}\n`; } if (record.validTime) { const startTime = new Date(record.validTime.startTime).toLocaleString('zh-TW'); const endTime = new Date(record.validTime.endTime).toLocaleString('zh-TW'); result += `**有效時間:** ${startTime} ~ ${endTime}\n`; } if (record.hazardConditions?.hazards) { result += `**災害類型:**\n`; record.hazardConditions.hazards.forEach(hazard => { result += `- ${hazard.phenomena} (${hazard.significance}): ${hazard.info}\n`; }); } if (record.contents?.contentText) { result += `**內容:** ${record.contents.contentText}\n`; } result += '\n---\n\n'; }); return result; } /** * 格式化日出日沒資料為可讀的文字 */ formatAstronomyData(data) { let result = `## 日出日沒時刻\n\n`; if (!data.records || !data.records.locations || !data.records.locations.location) { return `## 日出日沒時刻\n\n**無可用的天文資料**`; } const locations = Array.isArray(data.records.locations.location) ? data.records.locations.location : [data.records.locations.location]; let addedLocations = 0; const maxLocations = 1; // 建議單地點查詢 for (const location of locations) { if (addedLocations >= maxLocations) { result += `\n**提示:僅顯示1個地點資料。建議指定特定縣市查詢。**\n`; break; } // 從API回應的note或其他地方取得地點資訊,或使用索引 const locationName = data.records.note ? data.records.note.includes('臺北') ? '臺北市' : data.records.note.includes('高雄') ? '高雄市' : `地點 ${addedLocations + 1}` : `地點 ${addedLocations + 1}`; result += `### ${locationName}\n\n`; if (location.time && Array.isArray(location.time)) { const recentDays = location.time.slice(0, 7); // 顯示最近7天 recentDays.forEach((timeData) => { if (timeData.Date) { result += `**${timeData.Date}**\n`; if (timeData.BeginCivilTwilightTime) { result += `- 民用曙光開始: ${timeData.BeginCivilTwilightTime}\n`; } if (timeData.SunRiseTime) { result += `- 日出時間: ${timeData.SunRiseTime}`; if (timeData.SunRiseAZ) { result += ` (方位角: ${timeData.SunRiseAZ}°)`; } result += `\n`; } if (timeData.SunTransitTime) { result += `- 正午時間: ${timeData.SunTransitTime}`; if (timeData.SunTransitAlt) { result += ` (太陽仰角: ${timeData.SunTransitAlt})`; } result += `\n`; } if (timeData.SunSetTime) { result += `- 日沒時間: ${timeData.SunSetTime}`; if (timeData.SunSetAZ) { result += ` (方位角: ${timeData.SunSetAZ}°)`; } result += `\n`; } if (timeData.EndCivilTwilightTime) { result += `- 民用暮光結束: ${timeData.EndCivilTwilightTime}\n`; } result += `\n`; } }); } result += '---\n\n'; addedLocations++; } return result; } /** * 格式化月出月沒資料為可讀的文字 */ formatMoonData(data) { let result = `## 月出月沒時刻\n\n`; if (!data.records || !data.records.locations || !data.records.locations.location) { return `## 月出月沒時刻\n\n**無可用的月相資料**`; } const locations = Array.isArray(data.records.locations.location) ? data.records.locations.location : [data.records.locations.location]; let addedLocations = 0; const maxLocations = 1; // 建議單地點查詢 for (const location of locations) { if (addedLocations >= maxLocations) { result += `\n**提示:僅顯示1個地點資料。建議指定特定縣市查詢。**\n`; break; } if (location.locationName) { result += `### ${location.locationName}\n\n`; } if (location.time && Array.isArray(location.time)) { const recentDays = location.time.slice(0, 7); // 顯示最近7天 recentDays.forEach((timeData) => { if (timeData.Date) { result += `**${timeData.Date}**\n`; if (timeData.MoonRiseTime) { result += `- 月出時間: ${timeData.MoonRiseTime}\n`; } else { result += `- 月出時間: 當日不出\n`; } if (timeData.MoonTransitTime) { result += `- 月中天時間: ${timeData.MoonTransitTime}\n`; } if (timeData.MoonSetTime) { result += `- 月沒時間: ${timeData.MoonSetTime}\n`; } else { result += `- 月沒時間: 當日不沒\n`; } if (timeData.MoonPhase) { result += `- 月相: ${timeData.MoonPhase}\n`; } result += `\n`; } }); } result += '---\n\n'; addedLocations++; } return result; } /** * 格式化鄉鎮天氣預報資料 */ formatTownshipWeatherData(data) { let result = `## 鄉鎮天氣預報\n\n`; let addedLocations = 0; const maxLocations = 3; // 建議單次查詢1-3個地點 if (!data.records.Locations || !Array.isArray(data.records.Locations)) { return result + '無可用的鄉鎮天氣預報資料\n'; } for (const locationsGroup of data.records.Locations) { if (!locationsGroup.Location || !Array.isArray(locationsGroup.Location)) { continue; } result += `### ${locationsGroup.LocationsName}\n\n`; for (const location of locationsGroup.Location) { if (addedLocations >= maxLocations) { result += `**提示:僅顯示前${maxLocations}個鄉鎮。建議指定特定鄉鎮以獲得詳細資訊。**\n`; break; } result += `#### ${location.LocationName}\n`; if (location.Geocode) { result += `地區代碼: ${location.Geocode}\n`; } if (location.Latitude && location.Longitude) { result += `座標: ${location.Latitude}, ${location.Longitude}\n`; } result += '\n'; // 處理天氣要素 if (location.WeatherElement && Array.isArray(location.WeatherElement)) { // 只顯示主要的天氣要素 const mainElements = ['天氣現象', '溫度', '降雨機率', '相對濕度', '風向', '風速']; location.WeatherElement.forEach((element) => { if (mainElements.includes(element.ElementName)) { result += `**${element.ElementName}**\n`; // 只顯示前6個時間點(今天剩餘時間) if (element.Time && Array.isArray(element.Time)) { const timeSlots = element.Time.slice(0, 6); timeSlots.forEach((timeData) => { if (timeData.DataTime) { const time = new Date(timeData.DataTime).toLocaleString('zh-TW'); let value = ''; // 提取數值 if (timeData.ElementValue && Array.isArray(timeData.ElementValue) && timeData.ElementValue.length > 0) { if (element.ElementName === '天氣現象') { value = timeData.ElementValue[0].Weather || timeData.ElementValue[0].WeatherDescription || ''; } else if (element.ElementName === '溫度') { value = timeData.ElementValue[0].Temperature ? `${timeData.ElementValue[0].Temperature}°C` : ''; } else if (element.ElementName === '降雨機率') { value = timeData.ElementValue[0].ProbabilityOfPrecipitation ? `${timeData.ElementValue[0].ProbabilityOfPrecipitation}%` : ''; } else if (element.ElementName === '相對濕度') { value = timeData.ElementValue[0].RelativeHumidity ? `${timeData.ElementValue[0].RelativeHumidity}%` : ''; } else if (element.ElementName === '風向') { value = timeData.ElementValue[0].WindDirection || ''; } else if (element.ElementName === '風速') { value = timeData.ElementValue[0].WindSpeed ? `${timeData.ElementValue[0].WindSpeed} m/s` : ''; } else { // 通用處理 const firstValue = timeData.ElementValue[0]; value = Object.values(firstValue)[0] || ''; } } if (value) { result += `- ${time}: ${value}\n`; } } }); } result += '\n'; } }); } result += '---\n\n'; addedLocations++; } } if (addedLocations === 0) { result += '無可用的天氣預報資料\n'; } else { result += `**提示:僅顯示主要天氣要素和今天剩餘時間。**\n`; } return result; } /** * 取得潮汐站點的類型資訊 */ getTidalStationInfo(locationName, locationId) { // 確保 locationName 存在 if (!locationName) { return { name: "未知地點", type: "行政區域", icon: "🌊" }; } // 首先嘗試用 LocationId 查找 if (locationId && tidalStationTypes[locationId]) { return tidalStationTypes[locationId]; } // 如果沒有 LocationId,嘗試透過名稱匹配 for (const [id, info] of Object.entries(tidalStationTypes)) { const stationInfo = info; if (stationInfo.name === locationName || (stationInfo.name && locationName.includes(stationInfo.name)) || (locationName && stationInfo.name.includes(locationName))) { return stationInfo; } } // 如果找不到,返回預設的行政區域類型 return { name: locationName, type: "行政區域", icon: "🌊" }; } /** * 格式化潮汐資料為可讀的文字 */ formatTidalData(data) { let result = `## 🌊 潮汐預報\n\n`; // 檢查新的潮汐數據結構 (dataset.content.Locations) if (data.dataset && data.dataset.content && data.dataset.content.Locations) { const locations = data.dataset.content.Locations; for (const location of locations) { const locationName = location.Location.LocationName; const locationId = location.Location.LocationId; const stationInfo = this.getTidalStationInfo(locationName, locationId); result += `### ${stationInfo.icon} ${stationInfo.name}(${stationInfo.type})\n\n`; if (location.Location.TimePeriods && location.Location.TimePeriods.Daily) { const dailyData = location.Location.TimePeriods.Daily[0]; // 取第一天的數據 if (dailyData && dailyData.Time) { result += `**${dailyData.Date} (農曆 ${dailyData.LunarDate}) - ${dailyData.TideRange}潮**\n\n`; // 分析高潮低潮 const tideData = dailyData.Time.map((timeData) => ({ time: new Date(timeData.DateTime).toLocaleTimeString('zh-TW', { hour: '2-digit', minute: '2-digit', hour12: false }), type: timeData.Tide, height: parseFloat(timeData.TideHeights.AboveTWVD) / 100 // 轉換為公尺 })); // 排序並輸出 tideData.sort((a, b) => a.time.localeCompare(b.time)); const maxHeight = Math.max(...tideData.map((t) => t.height)); const minHeight = Math.min(...tideData.map((t) => t.height)); for (const tide of tideData) { let icon = tide.type === '滿潮' ? '🔸' : '🔹'; let typeText = tide.type === '滿潮' ? '**滿潮**' : '**乾潮**'; result += `- ${tide.time}:${tide.height.toFixed(2)}m ${icon} ${typeText}\n`; } result += `\n📊 **潮位範圍**:${minHeight.toFixed(2)}m - ${maxHeight.toFixed(2)}m\n`; } } result += `\n`; } return result; } // 檢查舊的潮汐數據結構 (records.location) if (!data.records || !data.records.location) { return `${result}**無可用的潮汐預報資料**`; } const locations = Array.isArray(data.records.location) ? data.records.location : [data.records.location]; // 獲取當天日期用於篩選 const today = new Date(); const todayString = today.toISOString().split('T')[0]; // YYYY-MM-DD 格式 for (const location of locations) { const locationName = location.locationName; const stationInfo = this.getTidalStationInfo(locationName); result += `### ${stationInfo.icon} ${stationInfo.name}(${stationInfo.type})\n\n`; if (location.weatherElement) { const timeElement = location.weatherElement.find((el) => el.elementName === 'time'); const heightElement = location.weatherElement.find((el) => el.elementName === 'height'); if (timeElement && heightElement && timeElement.time && heightElement.time) { // 篩選當天的潮汐資料 const todayTides = timeElement.time.filter((timeData) => { const tideDate = timeData.dataTime?.split('T')[0]; return tideDate === todayString; }); if (todayTides.length === 0) { result += `**${todayString} 無潮汐資料**\n\n`; continue; } // 收集潮汐時間和高度數據 const tideData = []; for (const timeData of todayTides) { const correspondingHeight = heightElement.time.find((heightData) => heightData.dataTime === timeData.dataTime); const time = new Date(timeData.dataTime).toLocaleTimeString('zh-TW', { hour: '2-digit', minute: '2-digit', hour12: false }); const height = correspondingHeight?.elementValue?.[0]?.value || 'N/A'; if (height !== 'N/A') { tideData.push({ time, height: parseFloat(height) }); } } // 排序並分析高潮低潮 tideData.sort((a, b) => a.time.localeCompare(b.time)); if (tideData.length > 0) { result += `**${today.toLocaleDateString('zh-TW')} 潮汐時間**\n\n`; // 找出最高和最低潮位 const maxHeight = Math.max(...tideData.map((t) => t.height)); const minHeight = Math.min(...tideData.map((t) => t.height)); for (const tide of tideData) { let tideType = ''; if (tide.height === maxHeight) { tideType = ' 🔸 **滿潮**'; } else if (tide.height === minHeight) { tideType = ' 🔹 **乾潮**'; } result += `- ${tide.time}:${tide.height.toFixed(2)}m${tideType}\n`; } // 添加簡要摘要 result += `\n📊 **今日潮位範圍**:${minHeight.toFixed(2)}m - ${maxHeight.toFixed(2)}m\n`; } } } result += `\n`; } return result; } /** * 格式化預報資料為可讀的文字 */ formatForecastData(data) { let result = `## 🌦️ 天氣預報\n\n`; if (!data.records || !data.records.location) { return `${result}**無可用的天氣預報資料**`; } const locations = Array.isArray(data.records.location) ? data.records.location : [data.records.location]; for (const location of locations) { result += `### ${location.locationName}\n\n`; if (location.weatherElement) { location.weatherElement.forEach((element) => { result += `**${element.elementName}**\n`; // 只顯示前3個時間點 const timeSlots = element.time ? element.time.slice(0, 3) : []; timeSlots.forEach((timeData) => { if (timeData.startTime && timeData.endTime) { const startTime = new Date(timeData.startTime).toLocaleString('zh-TW'); const endTime = new Date(timeData.endTime).toLocaleString('zh-TW'); result += `- ${startTime} ~ ${endTime}: ${timeData.parameter.parameterName}`; if (timeData.parameter.parameterValue) { result += ` (${timeData.parameter.parameterValue}`; if (timeData.parameter.parameterUnit) { result += ` ${timeData.parameter.parameterUnit}`; } result += ')'; } result += '\n'; } }); }); } result += '\n---\n\n'; } return result; } /** * 通用格式化方法 */ formatData(data, type) { // 縣市天氣預報 (records.location) if (type === 'weather' && 'records' in data && 'location' in data.records) { return this.formatWeatherData(data); } // 鄉鎮天氣預報 (records.Locations) else if (type === 'weather' && 'records' in data && 'Locations' in data.records) { return this.formatTownshipWeatherData(data); } // 潮汐預報處理 - 檢查 records.TideForecasts 結構 (暫時註解以檢查資料結構) // else if (type === 'tidal' && 'records' in data && 'TideForecasts' in data.records) { // return this.formatTidalForecastData(data); // } // 潮汐預報處理 - 檢查新的數據結構 else if (type === 'tidal' && 'dataset' in data && 'content' in data.dataset && 'Locations' in data.dataset.content) { return this.formatTidalData(data); } else if (type === 'tidal' && 'records' in data && 'location' in data.records) { return this.formatTidalData(data); } else if (type === 'observation' && 'records' in data) { return this.formatObservationData(data); } else if (type === 'earthquake' && 'records' in data && 'Earthquake' in data.records) { return this.formatEarthquakeData(data); } else if (type === 'tsunami' && 'records' in data && 'Tsunami' in data.records) { return this.formatTsunamiData(data); } else if (type === 'warning' && 'records' in data && 'record' in data.records) { return this.formatWarningData(data); } else if (type === 'warning' && 'records' in data && 'location' in data.records) { // 特殊處理:縣市警特報 (location 格式) let result = `## 天氣警特報\n\n`; if (!data.records || !data.records.location) { return result + `**目前無天氣警特報發布**\n`; } const locations = Array.isArray(data.records.location) ? data.records.location : [data.records.location]; // 過濾出有警特報的縣市 const alertedLocations = locations.filter((location) => location.hazardConditions && location.hazardConditions.hazards && location.hazardConditions.hazards.length > 0); if (alertedLocations.length === 0) { result += `**目前全台各縣市均無天氣警特報**\n\n`; result += `📍 **查詢範圍:** ${locations.length} 個縣市\n`; result += `⏰ **查詢時間:** ${new Date().toLocaleString('zh-TW')}\n`; return result; } result += `**⚠️ 目前有天氣警特報的縣市:**\n\n`; alertedLocations.forEach((location, index) => { result += `### ${index + 1}. ${location.locationName}\n`; if (location.geocode) { result += `**地區代碼:** ${location.geocode}\n`; } if (location.hazardConditions?.hazards) { result += `**警特報內容:**\n`; location.hazardConditions.hazards.forEach((hazard) => { let hazardText = `- **${hazard.phenomena}** (${hazard.significance})`; if (hazard.info && hazard.info.trim()) { hazardText += `: ${hazard.info}`; } if (hazard.startTime && hazard.endTime) { const startTime