cwadayi commited on
Commit
750a4d3
·
verified ·
1 Parent(s): 13d91c0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +137 -21
app.py CHANGED
@@ -1,5 +1,5 @@
1
  import os
2
- # ✅ 避免 Matplotlib 在唯讀路徑寫入設定
3
  os.environ["MPLCONFIGDIR"] = "/tmp/matplotlib"
4
 
5
  import uuid
@@ -68,7 +68,30 @@ app = Flask(__name__)
68
  configuration = Configuration(access_token=CHANNEL_ACCESS_TOKEN)
69
  handler = WebhookHandler(CHANNEL_SECRET)
70
 
71
- # --- Routes ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  @app.route("/", methods=["GET"])
73
  def home():
74
  return """
@@ -101,12 +124,9 @@ def healthz():
101
  def serve_static(filename):
102
  return send_from_directory(STATIC_DIR, filename)
103
 
104
- # --- Earthquake Data Functions ---
105
  USGS_API_BASE_URL = "https://earthquake.usgs.gov/fdsnws/event/1/query"
106
 
107
- def _iso(dt: datetime) -> str:
108
- return dt.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S")
109
-
110
  def fetch_global_last24h_text(min_mag=5.0, limit=10) -> str:
111
  """顯示近 24 小時全球 M≥min_mag 地震,含日期。"""
112
  now_utc = datetime.now(timezone.utc)
@@ -130,7 +150,6 @@ def fetch_global_last24h_text(min_mag=5.0, limit=10) -> str:
130
  for f in features:
131
  p = f["properties"]
132
  t_utc = datetime.fromtimestamp(p["time"] / 1000, tz=timezone.utc)
133
- # ✅ 改用含日期格式
134
  lines.append(
135
  f"震級: {p['mag']:.1f} | 日期時間: {t_utc.strftime('%Y-%m-%d %H:%M')} (UTC)\n地點: {p.get('place','')}"
136
  )
@@ -177,7 +196,101 @@ def fetch_taiwan_df_this_year(min_mag=5.0) -> pd.DataFrame | str:
177
  except Exception as e:
178
  return f"❌ 查詢失敗: {e}"
179
 
180
- # --- Map Creation ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
  def create_and_save_map(df: pd.DataFrame) -> str:
182
  fig, ax = plt.subplots(figsize=(9, 6), dpi=150)
183
  lon_min, lon_max = 118.5, 123.5
@@ -215,13 +328,7 @@ def create_and_save_map(df: pd.DataFrame) -> str:
215
  plt.close(fig)
216
  return filename
217
 
218
- def _base_url_for_images() -> str:
219
- # 盡量用公開的 HF Space URL,確保 LINE 能抓到圖
220
- if HF_SPACE_URL:
221
- return HF_SPACE_URL.rstrip("/")
222
- return request.url_root.rstrip("/")
223
-
224
- # --- LINE Webhook ---
225
  @app.route("/callback", methods=["POST"])
226
  def callback():
227
  signature = request.headers.get("X-Line-Signature")
@@ -232,7 +339,7 @@ def callback():
232
  abort(400)
233
  return "OK"
234
 
235
- # --- Message Handler ---
236
  @handler.add(MessageEvent, message=TextMessageContent)
237
  def handle_message(event):
238
  user_message = (event.message.text or "").strip().lower()
@@ -240,7 +347,15 @@ def handle_message(event):
240
  with ApiClient(configuration) as api_client:
241
  line_bot_api = MessagingApi(api_client)
242
 
243
- # 繪圖
 
 
 
 
 
 
 
 
244
  if ("臺灣地震畫圖" in user_message) or ("台灣地震畫圖" in user_message):
245
  result = fetch_taiwan_df_this_year()
246
  if isinstance(result, pd.DataFrame):
@@ -264,7 +379,7 @@ def handle_message(event):
264
  line_bot_api.reply_message_with_http_info(reply)
265
  return
266
 
267
- # 說明
268
  if user_message == "/help":
269
  text = (
270
  "📖 地震預警 dayichen 指令說明\n\n"
@@ -272,6 +387,7 @@ def handle_message(event):
272
  "➡️ 地震 / quake\n 說明:查詢全球最近 24 小時內,M≥5.0 的顯著地震(含日期時間)。\n\n"
273
  "➡️ 臺灣地震 / 台灣地震\n 說明:查詢今年以來台灣區域 (21–26°N, 119–123°E) M≥5.0 地震(含日期時間)。\n\n"
274
  "➡️ 臺灣地震畫圖 / 台灣地震畫圖\n 說明:繪製今年台灣區域 M≥5.0 地震分佈圖並回傳圖片。\n\n"
 
275
  "➡️ 你好\n 說明:顯示歡迎訊息。"
276
  )
277
  line_bot_api.reply_message_with_http_info(
@@ -279,7 +395,7 @@ def handle_message(event):
279
  )
280
  return
281
 
282
- # 台灣清單(已含日期)
283
  if ("臺灣地震" in user_message) or ("台灣地震" in user_message):
284
  result = fetch_taiwan_df_this_year()
285
  if isinstance(result, pd.DataFrame):
@@ -298,7 +414,7 @@ def handle_message(event):
298
  )
299
  return
300
 
301
- # 全球 24 小時(已含日期)
302
  if ("地震" in user_message) or ("quake" in user_message):
303
  reply_text = fetch_global_last24h_text()
304
  line_bot_api.reply_message_with_http_info(
@@ -306,7 +422,7 @@ def handle_message(event):
306
  )
307
  return
308
 
309
- # 打招呼
310
  if ("你好" in user_message) or ("hi" in user_message):
311
  line_bot_api.reply_message_with_http_info(
312
  ReplyMessageRequest(
 
1
  import os
2
+ # ✅ Matplotlib config/cache 放入可寫路徑
3
  os.environ["MPLCONFIGDIR"] = "/tmp/matplotlib"
4
 
5
  import uuid
 
68
  configuration = Configuration(access_token=CHANNEL_ACCESS_TOKEN)
69
  handler = WebhookHandler(CHANNEL_SECRET)
70
 
71
+ # -------------------- 共用小工具 --------------------
72
+ def _iso(dt: datetime) -> str:
73
+ return dt.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S")
74
+
75
+ def _base_url_for_images() -> str:
76
+ if HF_SPACE_URL:
77
+ return HF_SPACE_URL.rstrip("/")
78
+ return request.url_root.rstrip("/")
79
+
80
+ def _fmt_dt_utc(ts_ms: int | float | None) -> str:
81
+ """將毫秒或秒級 timestamp 轉成 YYYY-MM-DD HH:MM (UTC),未知回傳 '未知'"""
82
+ if ts_ms is None:
83
+ return "未知"
84
+ try:
85
+ # 多數 API 為毫秒;若是秒也能容錯
86
+ if ts_ms > 1e12: # 毫秒
87
+ dt = datetime.fromtimestamp(ts_ms / 1000, tz=timezone.utc)
88
+ else:
89
+ dt = datetime.fromtimestamp(ts_ms, tz=timezone.utc)
90
+ return dt.strftime("%Y-%m-%d %H:%M")
91
+ except Exception:
92
+ return "未知"
93
+
94
+ # -------------------- 路由 --------------------
95
  @app.route("/", methods=["GET"])
96
  def home():
97
  return """
 
124
  def serve_static(filename):
125
  return send_from_directory(STATIC_DIR, filename)
126
 
127
+ # -------------------- 資料存取:USGS --------------------
128
  USGS_API_BASE_URL = "https://earthquake.usgs.gov/fdsnws/event/1/query"
129
 
 
 
 
130
  def fetch_global_last24h_text(min_mag=5.0, limit=10) -> str:
131
  """顯示近 24 小時全球 M≥min_mag 地震,含日期。"""
132
  now_utc = datetime.now(timezone.utc)
 
150
  for f in features:
151
  p = f["properties"]
152
  t_utc = datetime.fromtimestamp(p["time"] / 1000, tz=timezone.utc)
 
153
  lines.append(
154
  f"震級: {p['mag']:.1f} | 日期時間: {t_utc.strftime('%Y-%m-%d %H:%M')} (UTC)\n地點: {p.get('place','')}"
155
  )
 
196
  except Exception as e:
197
  return f"❌ 查詢失敗: {e}"
198
 
199
+ # -------------------- 資料存取:CWA 地震預警 --------------------
200
+ CWA_ALARM_API = "https://app-2.cwa.gov.tw/api/v1/earthquake/alarm/list"
201
+
202
+ def fetch_cwa_alarm_list(limit: int = 5) -> str:
203
+ """
204
+ 取得交通部中央氣象署「地震預警」清單並格式化為文字。
205
+ 盡量容錯不同欄位名稱;最多輸出 limit 筆。
206
+ """
207
+ try:
208
+ r = requests.get(CWA_ALARM_API, timeout=10)
209
+ r.raise_for_status()
210
+ data = r.json()
211
+ except Exception as e:
212
+ return f"❌ 地震預警查詢失敗:{e}"
213
+
214
+ # 嘗試從常見包裝鍵取出陣列
215
+ items = None
216
+ for key in ("data", "records", "result", "list", "items"):
217
+ if isinstance(data, dict) and isinstance(data.get(key), list):
218
+ items = data.get(key)
219
+ break
220
+ if items is None:
221
+ # 也可能根本就是陣列
222
+ if isinstance(data, list):
223
+ items = data
224
+ else:
225
+ return "⚠️ 無法解析地震預警資料(未知格式)。"
226
+
227
+ if not items:
228
+ return "✅ 目前沒有地震預警。"
229
+
230
+ lines = ["🚨 地震預警(最新):", "-" * 20]
231
+ count = 0
232
+ for it in items:
233
+ if count >= limit:
234
+ break
235
+ # 嘗試各種常見欄位名稱
236
+ event_id = it.get("eventId") or it.get("eventid") or it.get("id") or "—"
237
+ # 可能是 ISO 字串、毫秒或秒
238
+ origin = it.get("originTime") or it.get("time") or it.get("origin_time") or it.get("earthquake_time")
239
+ dt_str = "未知"
240
+ if isinstance(origin, (int, float)):
241
+ dt_str = _fmt_dt_utc(origin)
242
+ elif isinstance(origin, str):
243
+ # 直接顯示(若是 ISO 字串)
244
+ try:
245
+ # 嘗試 parse 並以 UTC 格式化
246
+ dt = datetime.fromisoformat(origin.replace("Z", "+00:00"))
247
+ dt_str = dt.astimezone(timezone.utc).strftime("%Y-%m-%d %H:%M")
248
+ except Exception:
249
+ dt_str = origin # 無法解析就原樣輸出
250
+
251
+ mag = it.get("magnitude") or it.get("mag") or it.get("ml") or it.get("mw") or "—"
252
+ depth = it.get("depth") or "—"
253
+ lat = it.get("latitude") or it.get("lat") or "—"
254
+ lon = it.get("longitude") or it.get("lon") or "—"
255
+ place = it.get("location") or it.get("place") or it.get("region") or "—"
256
+
257
+ # 預警區域/縣市(可能是陣列或字串)
258
+ areas = it.get("areas") or it.get("alertAreas") or it.get("counties") or it.get("cities")
259
+ if isinstance(areas, list):
260
+ # 可能是字串陣列或物件陣列
261
+ try:
262
+ area_names = []
263
+ for a in areas:
264
+ if isinstance(a, str):
265
+ area_names.append(a)
266
+ elif isinstance(a, dict):
267
+ name = a.get("name") or a.get("area") or a.get("city") or a.get("county")
268
+ if name:
269
+ area_names.append(str(name))
270
+ areas_txt = "、".join(area_names) if area_names else "—"
271
+ except Exception:
272
+ areas_txt = "—"
273
+ elif isinstance(areas, str):
274
+ areas_txt = areas
275
+ else:
276
+ areas_txt = "—"
277
+
278
+ lines.append(
279
+ f"事件: {event_id}\n"
280
+ f"日期時間: {dt_str} (UTC)\n"
281
+ f"震級/深度: M{mag} / {depth} km\n"
282
+ f"位置: {place}(lat {lat}, lon {lon})\n"
283
+ f"預警區域: {areas_txt}"
284
+ )
285
+ lines.append("") # 空行
286
+ count += 1
287
+
288
+ if len(items) > limit:
289
+ lines.append(f"... 另有 {len(items) - limit} 筆。")
290
+
291
+ return "\n".join(lines).strip()
292
+
293
+ # -------------------- 繪圖 --------------------
294
  def create_and_save_map(df: pd.DataFrame) -> str:
295
  fig, ax = plt.subplots(figsize=(9, 6), dpi=150)
296
  lon_min, lon_max = 118.5, 123.5
 
328
  plt.close(fig)
329
  return filename
330
 
331
+ # -------------------- LINE Webhook --------------------
 
 
 
 
 
 
332
  @app.route("/callback", methods=["POST"])
333
  def callback():
334
  signature = request.headers.get("X-Line-Signature")
 
339
  abort(400)
340
  return "OK"
341
 
342
+ # -------------------- Message Handler --------------------
343
  @handler.add(MessageEvent, message=TextMessageContent)
344
  def handle_message(event):
345
  user_message = (event.message.text or "").strip().lower()
 
347
  with ApiClient(configuration) as api_client:
348
  line_bot_api = MessagingApi(api_client)
349
 
350
+ # ➊ 地震預警(新指令)
351
+ if "地震預警" in user_message:
352
+ reply_text = fetch_cwa_alarm_list(limit=5)
353
+ line_bot_api.reply_message_with_http_info(
354
+ ReplyMessageRequest(reply_token=event.reply_token, messages=[TextMessage(text=reply_text)])
355
+ )
356
+ return
357
+
358
+ # ➋ 臺灣地震畫圖
359
  if ("臺灣地震畫圖" in user_message) or ("台灣地震畫圖" in user_message):
360
  result = fetch_taiwan_df_this_year()
361
  if isinstance(result, pd.DataFrame):
 
379
  line_bot_api.reply_message_with_http_info(reply)
380
  return
381
 
382
+ # 說明
383
  if user_message == "/help":
384
  text = (
385
  "📖 地震預警 dayichen 指令說明\n\n"
 
387
  "➡️ 地震 / quake\n 說明:查詢全球最近 24 小時內,M≥5.0 的顯著地震(含日期時間)。\n\n"
388
  "➡️ 臺灣地震 / 台灣地震\n 說明:查詢今年以來台灣區域 (21–26°N, 119–123°E) M≥5.0 地震(含日期時間)。\n\n"
389
  "➡️ 臺灣地震畫圖 / 台灣地震畫圖\n 說明:繪製今年台灣區域 M≥5.0 地震分佈圖並回傳圖片。\n\n"
390
+ "➡️ 地震預警\n 說明:取得中央氣象署地震預警清單(最新 5 筆)。\n\n"
391
  "➡️ 你好\n 說明:顯示歡迎訊息。"
392
  )
393
  line_bot_api.reply_message_with_http_info(
 
395
  )
396
  return
397
 
398
+ # ➍ 台灣清單
399
  if ("臺灣地震" in user_message) or ("台灣地震" in user_message):
400
  result = fetch_taiwan_df_this_year()
401
  if isinstance(result, pd.DataFrame):
 
414
  )
415
  return
416
 
417
+ # 全球 24 小時
418
  if ("地震" in user_message) or ("quake" in user_message):
419
  reply_text = fetch_global_last24h_text()
420
  line_bot_api.reply_message_with_http_info(
 
422
  )
423
  return
424
 
425
+ # 打招呼
426
  if ("你好" in user_message) or ("hi" in user_message):
427
  line_bot_api.reply_message_with_http_info(
428
  ReplyMessageRequest(