修复釜山 AMOS 数据获取 + 跑道面板容错
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
This commit is contained in:
@@ -24,9 +24,14 @@ export function AmosRunwayPanel({
|
|||||||
const runwayVis = amos.runway_obs?.visibility_mor;
|
const runwayVis = amos.runway_obs?.visibility_mor;
|
||||||
const runwayRvr = amos.runway_obs?.rvr;
|
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 (
|
return (
|
||||||
<div className="scan-amos-runway-panel">
|
<div className="scan-amos-runway-panel">
|
||||||
@@ -49,17 +54,15 @@ export function AmosRunwayPanel({
|
|||||||
<div className="scan-amos-runway-label">
|
<div className="scan-amos-runway-label">
|
||||||
{rwyA}/{rwyB}
|
{rwyA}/{rwyB}
|
||||||
</div>
|
</div>
|
||||||
{temps ? (
|
<div className={`scan-amos-runway-temp ${runwayTempClass(temps?.[0])}`}>
|
||||||
<div className={`scan-amos-runway-temp ${runwayTempClass(temps[0])}`}>
|
{temps?.[0] != null ? `${temps[0].toFixed(1)}${tempSymbol}` : "--"}
|
||||||
{temps[0].toFixed(1)}{tempSymbol}
|
{temps?.[1] != null ? (
|
||||||
{temps[1] != null ? (
|
|
||||||
<small>
|
<small>
|
||||||
{isEn ? " Dew " : " 露点 "}
|
{isEn ? " Dew " : " 露点 "}
|
||||||
{temps[1].toFixed(1)}{tempSymbol}
|
{temps[1].toFixed(1)}{tempSymbol}
|
||||||
</small>
|
</small>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
|
||||||
{vis != null && vis > 0 ? (
|
{vis != null && vis > 0 ? (
|
||||||
<div className="scan-amos-runway-detail">
|
<div className="scan-amos-runway-detail">
|
||||||
{isEn ? "Vis " : "能见度 "}
|
{isEn ? "Vis " : "能见度 "}
|
||||||
|
|||||||
@@ -136,43 +136,76 @@ class AmosStationSourceMixin:
|
|||||||
amos_cache_ttl_sec: int = 300 # 5 minutes
|
amos_cache_ttl_sec: int = 300 # 5 minutes
|
||||||
|
|
||||||
def _amos_get_page(self, icao: str) -> Optional[str]:
|
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()
|
started = time.perf_counter()
|
||||||
try:
|
station_id = AMOS_STATION_IDS.get(icao, "")
|
||||||
# Try multiple URL patterns
|
airport_label_ko = {
|
||||||
urls = [
|
"RKSI": "인천",
|
||||||
f"{AMOS_BASE_URL}?icao={icao}",
|
"RKPK": "김해",
|
||||||
f"{AMOS_BASE_URL}?stn={AMOS_STATION_IDS.get(icao, '')}",
|
}.get(icao, "")
|
||||||
AMOS_BASE_URL, # default page (usually RKSI)
|
|
||||||
]
|
fetch_impl = None
|
||||||
for url in urls:
|
if hasattr(self, "session") and hasattr(self.session, "get"):
|
||||||
try:
|
def _get(url: str) -> str:
|
||||||
getter = getattr(self, "_http_get_text", None)
|
resp = self.session.get(url, timeout=float(getattr(self, "timeout", 4.0)))
|
||||||
if callable(getter):
|
resp.raise_for_status()
|
||||||
text = str(getter(url))
|
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:
|
else:
|
||||||
if not hasattr(self, "session"):
|
getter = getattr(self, "_http_get_text", None)
|
||||||
break
|
if not callable(getter):
|
||||||
response = self.session.get(
|
return None
|
||||||
url, timeout=float(getattr(self, "timeout", 4.0))
|
def _get(url: str) -> str:
|
||||||
)
|
return str(getter(url))
|
||||||
response.raise_for_status()
|
def _post(url: str, data: dict) -> str:
|
||||||
text = response.text
|
return str(getter(url))
|
||||||
if text and icao in text.upper():
|
fetch_impl = (_get, _post)
|
||||||
record_source_call(
|
|
||||||
"amos", "page", "success",
|
http_get, http_post = fetch_impl
|
||||||
(time.perf_counter() - started) * 1000.0,
|
|
||||||
)
|
try:
|
||||||
|
# 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:
|
||||||
|
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
|
return text
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
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
|
return None
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.debug("AMOS page fetch failed icao={}: {}", icao, exc)
|
logger.debug("AMOS page fetch failed icao={}: {}", icao, exc)
|
||||||
record_source_call(
|
record_source_call("amos", "page", "error", (time.perf_counter() - started) * 1000.0)
|
||||||
"amos", "page", "error",
|
|
||||||
(time.perf_counter() - started) * 1000.0,
|
|
||||||
)
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def fetch_amos_official_current(
|
def fetch_amos_official_current(
|
||||||
|
|||||||
@@ -2487,7 +2487,7 @@ def _analyze(
|
|||||||
"mgm": mgm_data,
|
"mgm": mgm_data,
|
||||||
"mgm_nearby": raw.get("mgm_nearby", []),
|
"mgm_nearby": raw.get("mgm_nearby", []),
|
||||||
"nearby_source": raw.get("nearby_source") or ("mgm" if city.lower() in TURKISH_MGM_CITIES else "metar_cluster"),
|
"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": {
|
"forecast": {
|
||||||
"today_high": om_today,
|
"today_high": om_today,
|
||||||
"daily": forecast_daily,
|
"daily": forecast_daily,
|
||||||
|
|||||||
Reference in New Issue
Block a user