feat: add weekly reward loop, i18n support, and scan terminal dashboard components
This commit is contained in:
@@ -22,6 +22,7 @@ except Exception:
|
||||
|
||||
from src.data_collection.city_registry import CITY_REGISTRY
|
||||
from src.data_collection.weather_sources import WeatherDataCollector
|
||||
from src.utils.telegram_i18n import copy_text, normalize_push_language, telegram_push_language
|
||||
|
||||
TARGET_CITIES: List[str] = [
|
||||
"beijing",
|
||||
@@ -73,6 +74,14 @@ def _env_int(name: str, default: int, min_val: int = 0) -> int:
|
||||
return default
|
||||
|
||||
|
||||
def _daily_report_language() -> str:
|
||||
return telegram_push_language(
|
||||
"DAILY_WEATHER_REPORT_LANGUAGE",
|
||||
"TELEGRAM_PUSH_LANGUAGE",
|
||||
"POLYWEATHER_TELEGRAM_PUSH_LANGUAGE",
|
||||
)
|
||||
|
||||
|
||||
def _fetch_cma_forecast(city_key: str) -> Optional[Dict[str, Any]]:
|
||||
"""Scrape today's forecast from weather.com.cn (CMA)."""
|
||||
code = CMA_CITY_CODES.get(city_key)
|
||||
@@ -158,13 +167,14 @@ def _fetch_city_data(
|
||||
collector: WeatherDataCollector, city_key: str
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
name = CITY_NAME_ZH.get(city_key, city_key)
|
||||
info = CITY_REGISTRY.get(city_key)
|
||||
name_en = str((info or {}).get("name") or city_key.title())
|
||||
|
||||
# 1. Try CMA first for weather description
|
||||
cma = _fetch_cma_forecast(city_key)
|
||||
|
||||
# 2. Get Open-Meteo data for temperature fallback
|
||||
om_high = None
|
||||
info = CITY_REGISTRY.get(city_key)
|
||||
if info:
|
||||
try:
|
||||
results = collector.fetch_all_sources(
|
||||
@@ -209,6 +219,7 @@ def _fetch_city_data(
|
||||
return {
|
||||
"city": city_key,
|
||||
"name": name,
|
||||
"name_en": name_en,
|
||||
"weather": weather or "?",
|
||||
"forecast_high": forecast_high,
|
||||
}
|
||||
@@ -235,12 +246,40 @@ def _wmo_to_weather(code: Any) -> str:
|
||||
return "阴"
|
||||
|
||||
|
||||
def _build_ai_prompt(cities_data: List[Dict[str, Any]], report_date: str) -> str:
|
||||
lines = [f"今天是 {report_date}。请用自然亲切的中文写一段天气日报。\n"]
|
||||
lines.append("城市天气数据:")
|
||||
def _build_ai_prompt(
|
||||
cities_data: List[Dict[str, Any]],
|
||||
report_date: str,
|
||||
language: Optional[str] = None,
|
||||
) -> str:
|
||||
language = normalize_push_language(language or _daily_report_language())
|
||||
if language == "zh":
|
||||
lines = [f"今天是 {report_date}。请用自然亲切的中文写一段天气日报。\n"]
|
||||
lines.append("城市天气数据:")
|
||||
for c in cities_data:
|
||||
lines.append(f"{c['name']}:{c['weather']},最高{c['forecast_high']}度")
|
||||
lines.append("\n要求:每城一行播报,城市名<b>加粗</b>,开头问候,禁止结尾废话。")
|
||||
return "\n".join(lines)
|
||||
|
||||
if language == "en":
|
||||
lines = [f"Today is {report_date}. Write a concise Telegram weather briefing in English.\n"]
|
||||
lines.append("City weather data:")
|
||||
for c in cities_data:
|
||||
lines.append(
|
||||
f"{c.get('name_en') or c['city'].title()}: weather={c['weather']}, high={c['forecast_high']}°C"
|
||||
)
|
||||
lines.append("\nRequirements: one line per city, bold city names with <b>, start with a short greeting, no generic closing.")
|
||||
return "\n".join(lines)
|
||||
|
||||
lines = [f"Today is {report_date}. Write a bilingual Telegram weather briefing.\n"]
|
||||
lines.append("City weather data:")
|
||||
for c in cities_data:
|
||||
lines.append(f"{c['name']}:{c['weather']},最高{c['forecast_high']}度")
|
||||
lines.append("\n要求:每城一行播报,城市名<b>加粗</b>,开头问候,禁止结尾废话。")
|
||||
lines.append(
|
||||
f"{c.get('name_en') or c['city'].title()} / {c['name']}: weather={c['weather']}, high={c['forecast_high']}°C"
|
||||
)
|
||||
lines.append(
|
||||
"\nRequirements: one line per city, English first then Chinese in the same line, "
|
||||
"bold city names with <b>, start with a short bilingual greeting, no generic closing."
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
@@ -352,8 +391,9 @@ def _runner(bot: Any, config: Dict[str, Any]) -> None:
|
||||
time.sleep(60)
|
||||
continue
|
||||
|
||||
language = _daily_report_language()
|
||||
report_date = now.strftime("%m月%d日")
|
||||
prompt = _build_ai_prompt(cities_data, report_date)
|
||||
prompt = _build_ai_prompt(cities_data, report_date, language=language)
|
||||
report_text = _call_ai(prompt)
|
||||
|
||||
if not report_text:
|
||||
@@ -363,7 +403,11 @@ def _runner(bot: Any, config: Dict[str, Any]) -> None:
|
||||
continue
|
||||
|
||||
try:
|
||||
report_text += "\n\n⚠️ 以上为粗略预测,仅供参考。"
|
||||
report_text += "\n\n" + copy_text(
|
||||
language,
|
||||
"⚠️ Rough forecast only. For reference.",
|
||||
"⚠️ 以上为粗略预测,仅供参考。",
|
||||
)
|
||||
bot.send_message(
|
||||
FORUM_CHAT_ID,
|
||||
report_text,
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def normalize_push_language(raw: Optional[str]) -> str:
|
||||
value = str(raw or "").strip().lower().replace("_", "-")
|
||||
if value in {"zh", "zh-cn", "cn", "chinese"}:
|
||||
return "zh"
|
||||
if value in {"en", "en-us", "english"}:
|
||||
return "en"
|
||||
if value in {"both", "bilingual", "dual", "en-zh", "zh-en"}:
|
||||
return "both"
|
||||
return "both"
|
||||
|
||||
|
||||
def telegram_push_language(*env_names: str, default: str = "both") -> str:
|
||||
names = env_names or (
|
||||
"TELEGRAM_PUSH_LANGUAGE",
|
||||
"POLYWEATHER_TELEGRAM_PUSH_LANGUAGE",
|
||||
)
|
||||
for name in names:
|
||||
raw = os.getenv(name)
|
||||
if raw:
|
||||
return normalize_push_language(raw)
|
||||
return normalize_push_language(default)
|
||||
|
||||
|
||||
def is_zh(language: Optional[str]) -> bool:
|
||||
return normalize_push_language(language) == "zh"
|
||||
|
||||
|
||||
def is_bilingual(language: Optional[str]) -> bool:
|
||||
return normalize_push_language(language) == "both"
|
||||
|
||||
|
||||
def copy_text(language: Optional[str], en: str, zh: str) -> str:
|
||||
if is_zh(language):
|
||||
return zh
|
||||
if is_bilingual(language):
|
||||
return f"{en} / {zh}"
|
||||
return en
|
||||
+108
-48
@@ -19,6 +19,13 @@ from src.database.runtime_state import (
|
||||
)
|
||||
from src.data_collection.city_registry import CITY_REGISTRY
|
||||
from src.utils.telegram_chat_ids import get_telegram_chat_ids_from_env
|
||||
from src.utils.telegram_i18n import (
|
||||
copy_text as _copy,
|
||||
is_bilingual as _is_bilingual,
|
||||
is_zh as _is_zh,
|
||||
normalize_push_language as _normalize_push_language,
|
||||
telegram_push_language as _resolve_telegram_push_language,
|
||||
)
|
||||
|
||||
# Forum topic routing: maps city_key -> message_thread_id for the push forum group.
|
||||
# Created by scripts/create_forum_topics.py, stored in the runtime data dir.
|
||||
@@ -144,6 +151,14 @@ def _env_float(name: str, default: float) -> float:
|
||||
return default
|
||||
|
||||
|
||||
def _telegram_push_language() -> str:
|
||||
return _resolve_telegram_push_language(
|
||||
"TELEGRAM_AIRPORT_PUSH_LANGUAGE",
|
||||
"TELEGRAM_PUSH_LANGUAGE",
|
||||
"POLYWEATHER_TELEGRAM_PUSH_LANGUAGE",
|
||||
)
|
||||
|
||||
|
||||
def _norm_prob(v: Any) -> Optional[float]:
|
||||
if v is None:
|
||||
return None
|
||||
@@ -631,11 +646,16 @@ WIND_REGIME: Dict[str, Dict[str, Tuple[int, int]]] = {
|
||||
# Legacy alias for backward compat with existing _select_focus_runway_obs / _focus_runway_pairs_for_city
|
||||
FOCUS_RUNWAY_PAIRS = SETTLEMENT_RUNWAY_PAIRS
|
||||
|
||||
_FUNCTION_HASHTAGS = {
|
||||
_FUNCTION_HASHTAGS_ZH = {
|
||||
"runway": "#跑道观测",
|
||||
"airport": "#机场观测",
|
||||
"trade": "#交易机会",
|
||||
}
|
||||
_FUNCTION_HASHTAGS_EN = {
|
||||
"runway": "#RunwayObs",
|
||||
"airport": "#AirportObs",
|
||||
"trade": "#TradeAlert",
|
||||
}
|
||||
|
||||
|
||||
def _city_hashtag(city: Optional[str]) -> Optional[str]:
|
||||
@@ -659,15 +679,27 @@ def _build_telegram_hashtag_line(
|
||||
city: Optional[str] = None,
|
||||
station: Optional[str] = None,
|
||||
extra: Optional[List[str]] = None,
|
||||
language: Optional[str] = None,
|
||||
) -> str:
|
||||
tags: List[str] = []
|
||||
primary = _FUNCTION_HASHTAGS.get(kind)
|
||||
if primary:
|
||||
tags.append(primary)
|
||||
tag_maps = (
|
||||
(_FUNCTION_HASHTAGS_EN, _FUNCTION_HASHTAGS_ZH)
|
||||
if _is_bilingual(language)
|
||||
else ((_FUNCTION_HASHTAGS_ZH,) if _is_zh(language) else (_FUNCTION_HASHTAGS_EN,))
|
||||
)
|
||||
for tag_map in tag_maps:
|
||||
primary = tag_map.get(kind)
|
||||
if primary and primary not in tags:
|
||||
tags.append(primary)
|
||||
for item in extra or []:
|
||||
tag = _FUNCTION_HASHTAGS.get(item, item)
|
||||
if tag and tag not in tags:
|
||||
tags.append(tag)
|
||||
added = False
|
||||
for tag_map in tag_maps:
|
||||
tag = tag_map.get(item)
|
||||
if tag and tag not in tags:
|
||||
tags.append(tag)
|
||||
added = True
|
||||
if not added and item and item not in tags:
|
||||
tags.append(item)
|
||||
city_tag = _city_hashtag(city)
|
||||
if city_tag and city_tag not in tags:
|
||||
tags.append(city_tag)
|
||||
@@ -752,7 +784,7 @@ def _is_settlement_runway(city: str, r1: str, r2: str) -> bool:
|
||||
return _runway_pair_key(r1, r2) in pair_set
|
||||
|
||||
|
||||
def _wind_regime_label(city: str, wind_dir: Optional[int]) -> Optional[str]:
|
||||
def _wind_regime_label(city: str, wind_dir: Optional[int], language: Optional[str] = None) -> Optional[str]:
|
||||
"""Classify wind direction into a thermal regime label."""
|
||||
if wind_dir is None:
|
||||
return None
|
||||
@@ -760,9 +792,9 @@ def _wind_regime_label(city: str, wind_dir: Optional[int]) -> Optional[str]:
|
||||
sea = regimes.get("sea_breeze")
|
||||
warm = regimes.get("warm_advection")
|
||||
if sea and sea[0] != sea[1] and sea[0] <= wind_dir <= sea[1]:
|
||||
return "海风降温"
|
||||
return _copy(language, "sea-breeze cooling", "海风降温")
|
||||
if warm and warm[0] != warm[1] and warm[0] <= wind_dir <= warm[1]:
|
||||
return "暖平流增强"
|
||||
return _copy(language, "warm advection", "暖平流增强")
|
||||
return None
|
||||
|
||||
|
||||
@@ -789,18 +821,21 @@ def _runway_heat_signal(
|
||||
slope_15m: Optional[float],
|
||||
wind_dir: Optional[int],
|
||||
city: str,
|
||||
language: Optional[str] = None,
|
||||
) -> str:
|
||||
"""Compute a simple runway heat signal label."""
|
||||
if slope_15m is None:
|
||||
return ""
|
||||
regime = _wind_regime_label(city, wind_dir)
|
||||
regime = _wind_regime_label(city, wind_dir, language)
|
||||
warm_regime = _copy(language, "warm advection", "暖平流增强")
|
||||
sea_regime = _copy(language, "sea-breeze cooling", "海风降温")
|
||||
if slope_15m >= 1.0:
|
||||
return "🚀 冲顶增强" if regime == "暖平流增强" else "🔥 升温中"
|
||||
return _copy(language, "🚀 Peak push strengthening", "🚀 冲顶增强") if regime == warm_regime else _copy(language, "🔥 Warming", "🔥 升温中")
|
||||
if slope_15m >= 0.5:
|
||||
return "🔥 升温中"
|
||||
return _copy(language, "🔥 Warming", "🔥 升温中")
|
||||
if slope_15m >= -0.2:
|
||||
return "⚠️ 高位观察" if regime == "海风降温" else "⏸️ 高位横盘"
|
||||
return "🧊 过峰风险"
|
||||
return _copy(language, "⚠️ Watch sea-breeze cooling", "⚠️ 高位观察") if regime == sea_regime else _copy(language, "⏸️ Holding near high", "⏸️ 高位横盘")
|
||||
return _copy(language, "🧊 Peak-risk cooling", "🧊 过峰风险")
|
||||
|
||||
|
||||
def _focused_runway_max(city: str, city_weather: Dict[str, Any]) -> Optional[float]:
|
||||
@@ -953,7 +988,9 @@ def _build_airport_status_message(
|
||||
source_label: str = "",
|
||||
arome_temp: Optional[float] = None,
|
||||
aeroweb_available: bool = False,
|
||||
language: Optional[str] = None,
|
||||
) -> str:
|
||||
language = _normalize_push_language(language or _telegram_push_language())
|
||||
_AIRPORT_EN = {"seoul": "Incheon", "singapore": "Changi", "busan": "Gimhae", "tokyo": "Haneda",
|
||||
"ankara": "Esenboğa", "helsinki": "Vantaa", "amsterdam": "Schiphol",
|
||||
"istanbul": "Airport", "paris": "Le Bourget",
|
||||
@@ -1018,8 +1055,8 @@ def _build_airport_status_message(
|
||||
# ── Heat model ──
|
||||
wind_dir = amos.get("wind_dir") if is_amsc else None
|
||||
slope_15m = _compute_slope_15m(amos_icao, display_temp) if is_amsc and display_temp is not None else None
|
||||
heat_signal = _runway_heat_signal(display_temp or 0, slope_15m, wind_dir, city) if is_amsc else ""
|
||||
wind_label = _wind_regime_label(city, wind_dir) if is_amsc and wind_dir is not None else None
|
||||
heat_signal = _runway_heat_signal(display_temp or 0, slope_15m, wind_dir, city, language) if is_amsc else ""
|
||||
wind_label = _wind_regime_label(city, wind_dir, language) if is_amsc and wind_dir is not None else None
|
||||
|
||||
max_so_far, max_temp_time = _get_airport_daily_high(city_weather)
|
||||
# ── Build message ──
|
||||
@@ -1029,6 +1066,7 @@ def _build_airport_status_message(
|
||||
hashtag_line = _build_telegram_hashtag_line(
|
||||
"runway" if has_runway else "airport",
|
||||
city=city,
|
||||
language=language,
|
||||
)
|
||||
icao_display = f"{amos_icao} · " if amos_icao else ""
|
||||
settlement_str = f" · ★{settlement_pair[0]}/{settlement_pair[1]}" if settlement_pair else ""
|
||||
@@ -1055,7 +1093,7 @@ def _build_airport_status_message(
|
||||
mid = pts.get("mid_temp")
|
||||
end = pts.get("end_temp")
|
||||
is_settlement = _is_settlement_runway(city, r1, r2)
|
||||
marker = " ★结算" if is_settlement else ""
|
||||
marker = _copy(language, " ★Settlement", " ★结算") if is_settlement else ""
|
||||
tmax = pts.get("target_runway_max")
|
||||
if tdz is not None or mid is not None or end is not None:
|
||||
line = f"{r1}/{r2}{marker} TDZ:{_fmt(tdz)} MID:{_fmt(mid)} END:{_fmt(end)}"
|
||||
@@ -1063,7 +1101,8 @@ def _build_airport_status_message(
|
||||
line += f" max:{tmax:.1f}"
|
||||
lines.append(line)
|
||||
else:
|
||||
lines.append(f"{r1}/{r2}{marker} {t:.1f}°C")
|
||||
temp_symbol = str(city_weather.get("temp_symbol") or "°C").strip()
|
||||
lines.append(f"{r1}/{r2}{marker} {t:.1f}{temp_symbol}")
|
||||
|
||||
# Summary stats
|
||||
lines.append("")
|
||||
@@ -1072,36 +1111,43 @@ def _build_airport_status_message(
|
||||
|
||||
if city == "paris":
|
||||
if aeroweb_available and display_temp is not None:
|
||||
lines.append(f"当前实况:{cur_str}")
|
||||
lines.append(f"{_copy(language, 'Current observation', '当前实况')}: {cur_str}")
|
||||
else:
|
||||
lines.append("当前实况:暂无")
|
||||
lines.append(_copy(language, "Current observation: unavailable", "当前实况:暂无"))
|
||||
else:
|
||||
if has_runway:
|
||||
lines.append(f"结算跑道当前:{cur_str}")
|
||||
current_label = (
|
||||
_copy(language, "Settlement runway now", "结算跑道当前")
|
||||
if settlement_pair
|
||||
else _copy(language, "Runway now", "跑道当前")
|
||||
)
|
||||
lines.append(f"{current_label}: {cur_str}")
|
||||
else:
|
||||
lines.append(f"当前:{cur_str}")
|
||||
lines.append(f"{_copy(language, 'Current', '当前')}: {cur_str}")
|
||||
|
||||
if city == "paris":
|
||||
if aeroweb_available and max_so_far is not None:
|
||||
time_str = f"({max_temp_time})" if max_temp_time else ""
|
||||
lines.append(f"日高实况:{max_so_far:.1f}{temp_symbol}{time_str}")
|
||||
time_str = f" ({max_temp_time})" if max_temp_time and not _is_zh(language) else (f"({max_temp_time})" if max_temp_time else "")
|
||||
lines.append(f"{_copy(language, 'Observed high', '日高实况')}: {max_so_far:.1f}{temp_symbol}{time_str}")
|
||||
elif not aeroweb_available:
|
||||
last_temp = _LAST_AEROWEB.get("temp")
|
||||
if last_temp is not None:
|
||||
last_time = _LAST_AEROWEB.get("max_time", "")
|
||||
time_str = f"({last_time})" if last_time else ""
|
||||
lines.append(f"最近实况:{last_temp:.1f}{temp_symbol}{time_str}")
|
||||
time_str = f" ({last_time})" if last_time and not _is_zh(language) else (f"({last_time})" if last_time else "")
|
||||
lines.append(f"{_copy(language, 'Latest observation', '最近实况')}: {last_temp:.1f}{temp_symbol}{time_str}")
|
||||
elif max_so_far is not None:
|
||||
time_str = f"({max_temp_time})" if max_temp_time else ""
|
||||
time_str = f" ({max_temp_time})" if max_temp_time and not _is_zh(language) else (f"({max_temp_time})" if max_temp_time else "")
|
||||
if has_runway:
|
||||
lines.append(f"今日跑道高点:{max_so_far:.1f}{temp_symbol}{time_str}")
|
||||
high_label = _copy(language, "Today's runway high", "今日跑道高点")
|
||||
lines.append(f"{high_label}: {max_so_far:.1f}{temp_symbol}{time_str}")
|
||||
else:
|
||||
lines.append(f"日高:{max_so_far:.1f}{temp_symbol}{time_str}")
|
||||
high_label = _copy(language, "Today's high", "日高")
|
||||
lines.append(f"{high_label}: {max_so_far:.1f}{temp_symbol}{time_str}")
|
||||
if slope_15m is not None:
|
||||
sign = "+" if slope_15m >= 0 else ""
|
||||
lines.append(f"15分钟趋势:{sign}{slope_15m:.1f}°C")
|
||||
lines.append(f"{_copy(language, '15m trend', '15分钟趋势')}: {sign}{slope_15m:.1f}°")
|
||||
if wind_dir is not None:
|
||||
wind_str = f"风向:{wind_dir}°"
|
||||
wind_str = f"{_copy(language, 'Wind dir', '风向')}: {wind_dir}°"
|
||||
if wind_label:
|
||||
wind_str += f" {wind_label}"
|
||||
lines.append(wind_str)
|
||||
@@ -1127,7 +1173,7 @@ def _build_airport_status_message(
|
||||
bj_h = hh + 8
|
||||
if bj_h >= 24:
|
||||
bj_h -= 24
|
||||
metar_time = f"北京时 {bj_h:02d}:{mm}"
|
||||
metar_time = f"{_copy(language, 'Beijing time', '北京时')} {bj_h:02d}:{mm}"
|
||||
break
|
||||
if metar_temp or metar_time:
|
||||
bits = []
|
||||
@@ -1135,46 +1181,54 @@ def _build_airport_status_message(
|
||||
bits.append(f"{metar_temp}{temp_symbol}")
|
||||
if metar_time:
|
||||
bits.append(metar_time)
|
||||
lines.append(f"报文: {' '.join(bits)}")
|
||||
lines.append(f"{_copy(language, 'METAR', '报文')}: {' '.join(bits)}")
|
||||
if deb_pred is not None:
|
||||
if city == "paris":
|
||||
if aeroweb_available and display_temp is not None:
|
||||
diff = display_temp - deb_pred
|
||||
sign = "+" if diff >= 0 else ""
|
||||
lines.append(f"DEB:{deb_pred:.1f}{temp_symbol}(距实况 {sign}{diff:.1f}°)")
|
||||
suffix = _copy(language, f" (vs obs {sign}{diff:.1f}°)", f"(距实况 {sign}{diff:.1f}°)")
|
||||
lines.append(f"DEB: {deb_pred:.1f}{temp_symbol}{suffix}" if not _is_zh(language) else f"DEB:{deb_pred:.1f}{temp_symbol}{suffix}")
|
||||
elif not aeroweb_available:
|
||||
last_temp = _LAST_AEROWEB.get("temp")
|
||||
if last_temp is not None:
|
||||
diff = last_temp - deb_pred
|
||||
sign = "+" if diff >= 0 else ""
|
||||
lines.append(f"DEB:{deb_pred:.1f}{temp_symbol}(距最近实况 {sign}{diff:.1f}°)")
|
||||
suffix = _copy(language, f" (vs latest obs {sign}{diff:.1f}°)", f"(距最近实况 {sign}{diff:.1f}°)")
|
||||
lines.append(f"DEB: {deb_pred:.1f}{temp_symbol}{suffix}" if not _is_zh(language) else f"DEB:{deb_pred:.1f}{temp_symbol}{suffix}")
|
||||
else:
|
||||
lines.append(f"DEB:{deb_pred:.1f}{temp_symbol}")
|
||||
lines.append(f"DEB: {deb_pred:.1f}{temp_symbol}" if not _is_zh(language) else f"DEB:{deb_pred:.1f}{temp_symbol}")
|
||||
else:
|
||||
lines.append(f"DEB:{deb_pred:.1f}{temp_symbol}")
|
||||
lines.append(f"DEB: {deb_pred:.1f}{temp_symbol}" if not _is_zh(language) else f"DEB:{deb_pred:.1f}{temp_symbol}")
|
||||
elif display_temp is not None and display_temp > deb_pred:
|
||||
lines.append(f"DEB:{deb_pred:.1f}{temp_symbol}(已突破 +{display_temp - deb_pred:.1f}°)")
|
||||
suffix = _copy(language, f" (already above +{display_temp - deb_pred:.1f}°)", f"(已突破 +{display_temp - deb_pred:.1f}°)")
|
||||
lines.append(f"DEB: {deb_pred:.1f}{temp_symbol}{suffix}" if not _is_zh(language) else f"DEB:{deb_pred:.1f}{temp_symbol}{suffix}")
|
||||
else:
|
||||
lines.append(f"DEB:{deb_pred:.1f}{temp_symbol}")
|
||||
lines.append(f"DEB: {deb_pred:.1f}{temp_symbol}" if not _is_zh(language) else f"DEB:{deb_pred:.1f}{temp_symbol}")
|
||||
|
||||
if city == "paris":
|
||||
lines.append("")
|
||||
if aeroweb_available and display_temp is not None:
|
||||
lines.append(f"📡 AEROWEB 机场实况:{display_temp:.1f}{temp_symbol} · Météo-France")
|
||||
label = _copy(language, "AEROWEB airport obs", "AEROWEB 机场实况")
|
||||
lines.append(f"📡 {label}: {display_temp:.1f}{temp_symbol} · Météo-France")
|
||||
else:
|
||||
lines.append("📡 AEROWEB 机场实况:暂不可用")
|
||||
lines.append(_copy(language, "📡 AEROWEB airport obs: unavailable", "📡 AEROWEB 机场实况:暂不可用"))
|
||||
if arome_temp is not None:
|
||||
if aeroweb_available and display_temp is not None:
|
||||
d = arome_temp - display_temp
|
||||
sign = "+" if d >= 0 else ""
|
||||
lines.append(f"🕐 AROME HD 15分钟临近预报:{arome_temp:.1f}{temp_symbol}(较实况 {sign}{d:.1f}°)")
|
||||
label = _copy(language, "AROME HD 15m nowcast", "AROME HD 15分钟临近预报")
|
||||
suffix = _copy(language, f" (vs obs {sign}{d:.1f}°)", f"(较实况 {sign}{d:.1f}°)")
|
||||
lines.append(f"🕐 {label}: {arome_temp:.1f}{temp_symbol}{suffix}")
|
||||
else:
|
||||
lines.append(f"🕐 AROME HD 15分钟临近预报:{arome_temp:.1f}{temp_symbol}")
|
||||
label = _copy(language, "AROME HD 15m nowcast", "AROME HD 15分钟临近预报")
|
||||
lines.append(f"🕐 {label}: {arome_temp:.1f}{temp_symbol}")
|
||||
else:
|
||||
lines.append("🕐 AROME HD 15分钟临近预报:--")
|
||||
label = _copy(language, "AROME HD 15m nowcast", "AROME HD 15分钟临近预报")
|
||||
lines.append(f"🕐 {label}: --")
|
||||
if not aeroweb_available:
|
||||
lines.append("")
|
||||
lines.append("提示:当前仅显示模型参考,不更新实况日高。")
|
||||
lines.append(_copy(language, "Note: showing model reference only; observed high is not updating.", "提示:当前仅显示模型参考,不更新实况日高。"))
|
||||
elif source_label:
|
||||
lines.append("")
|
||||
lines.append(f"📡 {source_label}")
|
||||
@@ -1186,8 +1240,14 @@ def _build_airport_status_message(
|
||||
if len(vals) >= 2:
|
||||
lo, hi = vals[0][0], vals[-1][0]
|
||||
spread = hi - lo
|
||||
spread_label = "低分歧" if spread <= 2.0 else ("中等分歧" if spread <= 4.0 else "高分歧")
|
||||
lines.append(f"模型区间:{lo:.1f}~{hi:.1f}{temp_symbol} {spread_label}")
|
||||
if spread <= 2.0:
|
||||
spread_label = _copy(language, "low dispersion", "低分歧")
|
||||
elif spread <= 4.0:
|
||||
spread_label = _copy(language, "moderate dispersion", "中等分歧")
|
||||
else:
|
||||
spread_label = _copy(language, "high dispersion", "高分歧")
|
||||
range_label = _copy(language, "Model range", "模型区间")
|
||||
lines.append(f"{range_label}: {lo:.1f}~{hi:.1f}{temp_symbol} {spread_label}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user