From e23a90a961b76b35d61aaabe82d501c1cc2699dc Mon Sep 17 00:00:00 2001
From: "2569718930@qq.com" <2569718930@qq.com>
Date: Sun, 10 May 2026 18:39:37 +0800
Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=87=9C=E5=B1=B1=20AMOS=20?=
=?UTF-8?q?=E6=95=B0=E6=8D=AE=E8=8E=B7=E5=8F=96=20+=20=E8=B7=91=E9=81=93?=
=?UTF-8?q?=E9=9D=A2=E6=9D=BF=E5=AE=B9=E9=94=99?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
AMOS 页面用 JS 切换机场,GET 参数无效。新增策略:
- 先 GET 建立 session,再 POST form data 切换机场
- 尝试多种 form data 组合(icao/stn/airport/code)
- 回退到 GET 参数尝试
跑道面板容错:
- 无温度数据时仍展示跑道风/能见度/RVR
- 温度缺失显示 "--" 而非隐藏整行
- analysis_service 输出条件放宽:有 temp_c 或 runway_obs 即可
Tested: python -m ruff check ., npx tsc --noEmit
---
.../scan-terminal/AmosRunwayPanel.tsx | 29 +++---
src/data_collection/amos_station_sources.py | 89 +++++++++++++------
web/analysis_service.py | 2 +-
3 files changed, 78 insertions(+), 42 deletions(-)
diff --git a/frontend/components/dashboard/scan-terminal/AmosRunwayPanel.tsx b/frontend/components/dashboard/scan-terminal/AmosRunwayPanel.tsx
index 837f8ec4..7720fdd3 100644
--- a/frontend/components/dashboard/scan-terminal/AmosRunwayPanel.tsx
+++ b/frontend/components/dashboard/scan-terminal/AmosRunwayPanel.tsx
@@ -24,9 +24,14 @@ export function AmosRunwayPanel({
const runwayVis = amos.runway_obs?.visibility_mor;
const runwayRvr = amos.runway_obs?.rvr;
- if (!runwayPairs || !runwayTemps || runwayPairs.length === 0) return null;
+ if (!runwayPairs || runwayPairs.length === 0) return null;
- const pairs = runwayPairs.slice(0, runwayTemps.length);
+ const maxItems = Math.max(
+ runwayPairs.length,
+ runwayTemps?.length || 0,
+ runwayWinds?.length || 0,
+ );
+ const pairs = runwayPairs.slice(0, maxItems);
return (
@@ -49,17 +54,15 @@ export function AmosRunwayPanel({
{rwyA}/{rwyB}
- {temps ? (
-
- {temps[0].toFixed(1)}{tempSymbol}
- {temps[1] != null ? (
-
- {isEn ? " Dew " : " 露点 "}
- {temps[1].toFixed(1)}{tempSymbol}
-
- ) : null}
-
- ) : null}
+
+ {temps?.[0] != null ? `${temps[0].toFixed(1)}${tempSymbol}` : "--"}
+ {temps?.[1] != null ? (
+
+ {isEn ? " Dew " : " 露点 "}
+ {temps[1].toFixed(1)}{tempSymbol}
+
+ ) : null}
+
{vis != null && vis > 0 ? (
{isEn ? "Vis " : "能见度 "}
diff --git a/src/data_collection/amos_station_sources.py b/src/data_collection/amos_station_sources.py
index 6c760220..6cad1f76 100644
--- a/src/data_collection/amos_station_sources.py
+++ b/src/data_collection/amos_station_sources.py
@@ -136,43 +136,76 @@ class AmosStationSourceMixin:
amos_cache_ttl_sec: int = 300 # 5 minutes
def _amos_get_page(self, icao: str) -> Optional[str]:
- """Fetch the AMOS page for a given ICAO code."""
+ """Fetch the AMOS page for a given ICAO code.
+
+ The AMOS site uses JavaScript airport switching. We try multiple
+ approaches: GET with parameters, POST with form data, and cookie replay.
+ """
started = time.perf_counter()
+ station_id = AMOS_STATION_IDS.get(icao, "")
+ airport_label_ko = {
+ "RKSI": "인천",
+ "RKPK": "김해",
+ }.get(icao, "")
+
+ fetch_impl = None
+ if hasattr(self, "session") and hasattr(self.session, "get"):
+ def _get(url: str) -> str:
+ resp = self.session.get(url, timeout=float(getattr(self, "timeout", 4.0)))
+ resp.raise_for_status()
+ return resp.text
+
+ def _post(url: str, data: dict) -> str:
+ resp = self.session.post(url, data=data, timeout=float(getattr(self, "timeout", 4.0)))
+ resp.raise_for_status()
+ return resp.text
+ fetch_impl = (_get, _post)
+ else:
+ getter = getattr(self, "_http_get_text", None)
+ if not callable(getter):
+ return None
+ def _get(url: str) -> str:
+ return str(getter(url))
+ def _post(url: str, data: dict) -> str:
+ return str(getter(url))
+ fetch_impl = (_get, _post)
+
+ http_get, http_post = fetch_impl
+
try:
- # Try multiple URL patterns
- urls = [
- f"{AMOS_BASE_URL}?icao={icao}",
- f"{AMOS_BASE_URL}?stn={AMOS_STATION_IDS.get(icao, '')}",
- AMOS_BASE_URL, # default page (usually RKSI)
- ]
- for url in urls:
+ # Strategy 1: GET first to establish session, then POST to switch
+ _ = http_get(AMOS_BASE_URL)
+ for form_data in [
+ {"icao": icao, "stn": station_id, "airport": airport_label_ko},
+ {"stnId": station_id},
+ {"code": icao},
+ {"selectAirport": icao},
+ ]:
try:
- getter = getattr(self, "_http_get_text", None)
- if callable(getter):
- text = str(getter(url))
- else:
- if not hasattr(self, "session"):
- break
- response = self.session.get(
- url, timeout=float(getattr(self, "timeout", 4.0))
- )
- response.raise_for_status()
- text = response.text
- if text and icao in text.upper():
- record_source_call(
- "amos", "page", "success",
- (time.perf_counter() - started) * 1000.0,
- )
+ text = http_post(AMOS_BASE_URL, form_data)
+ if text and (icao in text.upper() or airport_label_ko in text):
+ record_source_call("amos", "page", "success", (time.perf_counter() - started) * 1000.0)
return text
except Exception:
continue
+
+ # Strategy 2: GET with query parameters
+ for url in [
+ f"{AMOS_BASE_URL}?icao={icao}",
+ f"{AMOS_BASE_URL}?stn={station_id}",
+ ]:
+ try:
+ text = http_get(url)
+ if text and (icao in text.upper() or airport_label_ko in text):
+ record_source_call("amos", "page", "success", (time.perf_counter() - started) * 1000.0)
+ return text
+ except Exception:
+ continue
+
return None
except Exception as exc:
logger.debug("AMOS page fetch failed icao={}: {}", icao, exc)
- record_source_call(
- "amos", "page", "error",
- (time.perf_counter() - started) * 1000.0,
- )
+ record_source_call("amos", "page", "error", (time.perf_counter() - started) * 1000.0)
return None
def fetch_amos_official_current(
diff --git a/web/analysis_service.py b/web/analysis_service.py
index 0769d9a3..22135048 100644
--- a/web/analysis_service.py
+++ b/web/analysis_service.py
@@ -2487,7 +2487,7 @@ def _analyze(
"mgm": mgm_data,
"mgm_nearby": raw.get("mgm_nearby", []),
"nearby_source": raw.get("nearby_source") or ("mgm" if city.lower() in TURKISH_MGM_CITIES else "metar_cluster"),
- "amos": amos_data if amos_data.get("temp_c") is not None else None,
+ "amos": amos_data if amos_data and (amos_data.get("temp_c") is not None or amos_data.get("runway_obs") is not None) else None,
"forecast": {
"today_high": om_today,
"daily": forecast_daily,