@bashcat/cwa-mcp-weather
Version:
MCP 伺服器整合中央氣象署 (CWA) 開放資料 API - 完整支援所有15個天氣工具
1,122 lines • 62.7 kB
JavaScript
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