From 26b6f3ae9be14542a9c8587a2f2c6a97ea4a9adf Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Mon, 25 May 2026 23:14:48 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=A0=E9=99=A4=20AI=20=E6=9C=BA=E5=9C=BA?= =?UTF-8?q?=E8=A7=A3=E8=AF=BB=2012=20=E4=B8=AA=E5=A4=B1=E6=95=88=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LiveTemperatureThresholdChart.tsx | 2 +- .../__tests__/refreshCadencePolicy.test.ts | 7 +- tests/test_refresh_policy.py | 4 + tests/test_web_observability.py | 469 ------------------ 4 files changed, 10 insertions(+), 472 deletions(-) diff --git a/frontend/components/dashboard/scan-terminal/LiveTemperatureThresholdChart.tsx b/frontend/components/dashboard/scan-terminal/LiveTemperatureThresholdChart.tsx index 6e1b348f..e3ce2cb3 100644 --- a/frontend/components/dashboard/scan-terminal/LiveTemperatureThresholdChart.tsx +++ b/frontend/components/dashboard/scan-terminal/LiveTemperatureThresholdChart.tsx @@ -177,7 +177,7 @@ type RunwayHistorySeries = { }; const MAX_OBS_POINTS = 1440; -const HOURLY_CACHE_TTL_MS = DASHBOARD_REFRESH_POLICY_MS.observation; +const HOURLY_CACHE_TTL_MS = DASHBOARD_REFRESH_POLICY_MS.model; const FULL_DAY_SLOT_MINUTES = 30; const FULL_DAY_SLOTS = 48; const SLOT_INTERVAL_MS = FULL_DAY_SLOT_MINUTES * 60 * 1000; diff --git a/frontend/components/dashboard/scan-terminal/__tests__/refreshCadencePolicy.test.ts b/frontend/components/dashboard/scan-terminal/__tests__/refreshCadencePolicy.test.ts index 88a985a9..6e3ac2e4 100644 --- a/frontend/components/dashboard/scan-terminal/__tests__/refreshCadencePolicy.test.ts +++ b/frontend/components/dashboard/scan-terminal/__tests__/refreshCadencePolicy.test.ts @@ -13,6 +13,7 @@ function assert(condition: unknown, message: string) { export function runTests() { assert(DASHBOARD_REFRESH_POLICY_MS.observation === 60_000, "observation layer should refresh every 60 seconds"); assert(DASHBOARD_REFRESH_POLICY_MS.scanRows === 5 * 60_000, "region/city rows should refresh every 5 minutes"); + assert(DASHBOARD_REFRESH_POLICY_MS.marketOverview === 10 * 60_000, "market overview should refresh every 10 minutes"); assert(DASHBOARD_REFRESH_POLICY_MS.model === 30 * 60_000, "DEB and multi-model data should refresh every 30 minutes"); assert(DASHBOARD_REFRESH_POLICY_SEC.metar === 5 * 60, "METAR polling should be 5 minutes"); assert(scanTerminalQueryPolicy.autoRefreshMs === DASHBOARD_REFRESH_POLICY_MS.scanRows, "scan terminal auto refresh should use the shared row cadence"); @@ -36,8 +37,10 @@ export function runTests() { "scan list local cache should use the shared 5-minute row cadence", ); assert( - chartSource.includes("DASHBOARD_REFRESH_POLICY_MS.observation"), - "selected city chart detail cache should use the shared 60-second observation cadence", + chartSource.includes("DASHBOARD_REFRESH_POLICY_MS.model") && + !chartSource.includes("setInterval(") && + !chartSource.includes("window.setInterval"), + "selected city detail chart should be on-demand and use model-layer cache instead of 60-second polling", ); assert( !overviewApiSource.includes("/chat/completions") && diff --git a/tests/test_refresh_policy.py b/tests/test_refresh_policy.py index dfb015de..8d481581 100644 --- a/tests/test_refresh_policy.py +++ b/tests/test_refresh_policy.py @@ -1,4 +1,5 @@ from src.utils.refresh_policy import ( + MARKET_OVERVIEW_TTL_SEC, METAR_POLL_TTL_SEC, MODEL_CACHE_TTL_SEC, OBSERVATION_REFRESH_SEC, @@ -10,18 +11,21 @@ def test_refresh_policy_cadences_are_layered(): assert OBSERVATION_REFRESH_SEC == 60 assert METAR_POLL_TTL_SEC == 300 assert SCAN_ROWS_REFRESH_SEC == 300 + assert MARKET_OVERVIEW_TTL_SEC == 600 assert MODEL_CACHE_TTL_SEC == 1800 def test_backend_defaults_use_refresh_policy(): import src.data_collection.weather_sources as weather_sources import web.services.city_runtime as city_runtime + import web.services.market_overview_api as market_overview_api import web.services.scan_ai_config as scan_ai_config assert scan_ai_config.SCAN_TERMINAL_PAYLOAD_TTL_SEC == SCAN_ROWS_REFRESH_SEC assert city_runtime.CITY_FULL_CACHE_TTL_SEC == OBSERVATION_REFRESH_SEC assert city_runtime.CITY_PANEL_CACHE_TTL_SEC == SCAN_ROWS_REFRESH_SEC assert city_runtime.CITY_MARKET_CACHE_TTL_SEC == SCAN_ROWS_REFRESH_SEC + assert market_overview_api.OVERVIEW_CACHE_TTL_SEC == MARKET_OVERVIEW_TTL_SEC source = weather_sources.WeatherDataCollector({}) assert source.metar_cache_ttl_sec == METAR_POLL_TTL_SEC diff --git a/tests/test_web_observability.py b/tests/test_web_observability.py index ccdbff19..910f6a0c 100644 --- a/tests/test_web_observability.py +++ b/tests/test_web_observability.py @@ -50,475 +50,6 @@ def test_metrics_endpoint_returns_prometheus_payload(): assert 'polyweather_http_requests_total' in response.text -def test_city_ai_fallback_reasoning_identifies_fast_evidence_mode(): - payload = scan_terminal_service._build_city_ai_fallback( - { - "city_display_name": "Tokyo", - "temp_symbol": "°C", - "deb": {"prediction": 17.8}, - "model_cluster": { - "sources": [ - {"value": 17.0}, - {"value": 17.8}, - {"value": 20.6}, - ] - }, - "observation_anchor": { - "is_airport_metar": True, - "station_code": "RJTT", - }, - "airport_current": { - "station_code": "RJTT", - "temp": 16.0, - "report_time": "21:30Z", - "raw_metar": "RJTT 262130Z AUTO 00000KT 9999 FEW030 16/10 Q1015", - }, - }, - locale="zh-CN", - reason="preview", - ) - - assert "当前为快速证据模式" in payload["reasoning_zh"] - assert "完整 AI 机场报文解读返回后再合并" in payload["reasoning_zh"] - assert "AI 机场报文解读正常" not in payload["reasoning_zh"] - assert "后补" not in payload["reasoning_zh"] - assert "AI 增强可作为后续补充" not in payload["reasoning_zh"] - - -def test_city_ai_fallback_revises_up_when_latest_metar_breaks_above_models(): - payload = scan_terminal_service._build_city_ai_fallback( - { - "city_display_name": "Manila", - "temp_symbol": "°C", - "deb": {"prediction": 34.0}, - "model_cluster": { - "sources": [ - {"value": 32.5}, - {"value": 33.8}, - {"value": 34.0}, - {"value": 34.7}, - ] - }, - "observation_anchor": { - "is_airport_metar": True, - "station_code": "RPLL", - }, - "airport_current": { - "station_code": "RPLL", - "temp": 35.0, - "report_time": "03:00Z / 当地 11:00", - "raw_metar": "RPLL 270300Z 34004KT CAVOK 35/24 Q1009", - }, - }, - locale="zh-CN", - reason="stream preview", - ) - - assert payload["predicted_max"] == 35.0 - assert payload["range_high"] == 35.0 - assert "高于原先 34.0°C 中枢" in payload["final_judgment_zh"] - assert "上修到至少 35.0°C" in payload["final_judgment_zh"] - assert "共同支撑本轮最高温中枢" not in payload["reasoning_zh"] - assert "超过模型上沿 34.7°C" in payload["reasoning_zh"] - assert "继续上修最高温中枢" in payload["risks_zh"][0] - - -def test_city_ai_fallback_revises_down_after_peak_when_observed_high_lags(): - payload = scan_terminal_service._build_city_ai_fallback( - { - "city_display_name": "London", - "temp_symbol": "°C", - "deb": {"prediction": 30.0}, - "model_cluster": { - "sources": [ - {"value": 29.2}, - {"value": 30.0}, - {"value": 31.1}, - ] - }, - "window_phase": "post_peak", - "peak_window_label": "14:00-16:59", - "observation_anchor": { - "is_airport_metar": True, - "station_code": "EGLL", - }, - "airport_current": { - "station_code": "EGLL", - "temp": 27.0, - "max_so_far": 27.5, - "report_time": "16:30Z", - "raw_metar": "EGLL 271630Z 22008KT 9999 SCT030 27/15 Q1012", - }, - }, - locale="zh-CN", - reason="stream preview", - ) - - assert payload["predicted_max"] == 27.5 - assert "峰值窗口(14:00-16:59)已过或接近结束" in payload["final_judgment_zh"] - assert "最高温中枢需先下修到 27.5°C" in payload["final_judgment_zh"] - assert "共同支撑本轮最高温中枢" not in payload["reasoning_zh"] - assert "下修压力" in payload["reasoning_zh"] - assert "继续下修最高温中枢" in payload["risks_zh"][0] - - -def test_city_ai_fallback_does_not_downrevise_before_peak_window(): - payload = scan_terminal_service._build_city_ai_fallback( - { - "city_display_name": "Dubai", - "temp_symbol": "°C", - "deb": {"prediction": 41.0}, - "model_cluster": { - "sources": [ - {"value": 40.5}, - {"value": 41.0}, - {"value": 41.6}, - ] - }, - "window_phase": "early_today", - "minutes_until_peak_start": 240, - "peak_window_label": "14:00-16:59", - "observation_anchor": { - "is_airport_metar": True, - "station_code": "OMDB", - }, - "airport_current": { - "station_code": "OMDB", - "temp": 35.0, - "report_time": "08:00Z", - "raw_metar": "OMDB 270800Z 29007KT CAVOK 35/20 Q1008", - }, - }, - locale="zh-CN", - reason="stream preview", - ) - - assert payload["predicted_max"] == 41.0 - assert "暂不直接下修" in payload["reasoning_zh"] - assert "峰值窗口尚未到来" in payload["reasoning_zh"] - assert "若峰值窗口前继续偏低,需要下修最高温中枢" in payload["risks_zh"][0] - - -def test_city_ai_fallback_marks_peak_window_passed_without_waiting_for_warming(): - payload = scan_terminal_service._build_city_ai_fallback( - { - "city_display_name": "Paris", - "temp_symbol": "°C", - "deb": {"prediction": 28.0}, - "model_cluster": { - "sources": [ - {"value": 27.6}, - {"value": 28.0}, - {"value": 28.5}, - ] - }, - "window_phase": "post_peak", - "peak_window_label": "13:00-15:59", - "observation_anchor": { - "is_airport_metar": True, - "station_code": "LFPG", - }, - "airport_current": { - "station_code": "LFPG", - "temp": 27.0, - "max_so_far": 27.2, - "report_time": "17:00Z", - "raw_metar": "LFPG 271700Z 25006KT 9999 FEW035 27/13 Q1014", - }, - }, - locale="zh-CN", - reason="stream preview", - ) - - assert "峰值窗口(13:00-15:59)已过" in payload["final_judgment_zh"] - assert "不是继续按待升温路径解读" in payload["reasoning_zh"] - assert "避免继续上调最高温中枢" in payload["risks_zh"][0] - - -def test_city_ai_fallback_treats_stale_metar_as_background_not_anchor(): - payload = scan_terminal_service._build_city_ai_fallback( - { - "city_display_name": "Manila", - "temp_symbol": "°C", - "deb": {"prediction": 34.0}, - "model_cluster": { - "sources": [ - {"value": 33.5}, - {"value": 34.0}, - {"value": 34.4}, - ] - }, - "metar_context": { - "stale_for_today": True, - "last_observation_time": "00:00Z", - }, - "observation_anchor": { - "is_airport_metar": True, - "station_code": "RPLL", - }, - "airport_current": { - "station_code": "RPLL", - "temp": 36.0, - "report_time": "00:00Z", - "raw_metar": "RPLL 270000Z 34004KT CAVOK 36/24 Q1009", - }, - }, - locale="zh-CN", - reason="stream preview", - ) - - assert payload["predicted_max"] == 34.0 - assert "过旧" in payload["metar_read_zh"] - assert "不能作为强实况锚点" in payload["metar_read_zh"] - assert "先以 DEB 和多模型路径为主" in payload["final_judgment_zh"] - assert "不能作为强实况锚点" in payload["reasoning_zh"] - assert "上修到至少 36.0°C" not in payload["final_judgment_zh"] - - -def test_city_ai_cache_key_changes_when_observation_fingerprint_changes(): - """METAR 原文不变 → 缓存 key 不变(命中);METAR 原文变了 → key 变化(miss)。""" - base_input = { - "city": "Manila", - "local_date": "2026-04-28", - "observation_anchor": { - "source": "METAR", - "is_airport_metar": True, - "station_code": "RPLL", - }, - "airport_current": { - "obs_time": "03:00Z", - "raw_metar": "RPLL 280300Z 34004KT CAVOK 34/24 Q1009", - }, - "metar_context": { - "stale_for_today": False, - "last_observation_time": "03:00Z", - }, - } - changed_input = { - **base_input, - "airport_current": { - **base_input["airport_current"], - "raw_metar": "RPLL 280330Z 36006KT 9999 FEW020 33/25 Q1010", - "obs_time": "03:30Z", - }, - } - - assert scan_terminal_service._scan_city_ai_cache_key(base_input) != scan_terminal_service._scan_city_ai_cache_key(changed_input) - - -def test_city_ai_stream_request_only_asks_provider_for_observation_read(): - request_payload = scan_terminal_service._build_city_ai_stream_request( - { - "city": "Tokyo", - "city_display_name": "Tokyo", - "temp_symbol": "°C", - "deb": {"prediction": 17.8}, - "model_cluster": {"sources": [{"value": 17.0}, {"value": 17.8}]}, - "observation_anchor": { - "is_airport_metar": True, - "read_label_zh": "机场报文解读", - }, - "airport_current": { - "station_code": "RJTT", - "temp": 16.0, - "report_time": "21:30Z", - "raw_metar": "RJTT 262130Z AUTO 00000KT 9999 FEW030 16/10 Q1015", - }, - }, - locale="zh-CN", - ) - - user_payload = request_payload["messages"][1]["content"] - assert request_payload["stream"] is True - assert request_payload["max_tokens"] <= 1200 - assert request_payload["max_tokens"] < scan_terminal_service.SCAN_AI_MAX_TOKENS - assert "taf_read_zh" in user_payload - assert "probability_read_zh" in user_payload - assert "predicted_max" in user_payload - assert "final_judgment" in user_payload - - -def test_city_ai_partial_json_trims_dangling_taf_clause(): - payload = scan_terminal_service._build_city_ai_fallback( - { - "city_display_name": "London", - "temp_symbol": "°C", - "deb": {"prediction": 24.3}, - "model_cluster": { - "sources": [ - {"value": 22.3}, - {"value": 23.1}, - {"value": 24.3}, - {"value": 26.3}, - ] - }, - "observation_anchor": { - "is_airport_metar": True, - "station_code": "EGLL", - }, - "airport_current": { - "station_code": "EGLL", - "temp": 21.0, - "report_time": "09:00Z", - "raw_metar": "EGLL 270900Z 34004KT CAVOK 21/09 Q1016", - }, - }, - locale="zh-CN", - reason="AI content is not a JSON object", - raw_content=( - '{"metar_read_zh":"最新METAR报文09:00观测温度21°C,西北风4节(340°),' - 'CAVOK(能见度良好,无重要云)。当前西北风弱,趋向增温但影响有限;' - 'TAF预示10-11点转南风(18012KT),南风可能带来凉爽海风抑制升温。",' - '"reasoning_zh":"DEB预测24.3°C,多数模型集中在23-26°C,' - '当前09时实测21°C处于快速升温路径,但TAF显示' - ), - ) - - assert "但TAF显示" not in payload["reasoning_zh"] - assert payload["reasoning_zh"].endswith("。") - assert "当前09时实测21°C处于快速升温路径" in payload["reasoning_zh"] - - -def test_city_ai_schema_completion_trims_dangling_taf_clause(): - payload = scan_terminal_service._complete_city_ai_payload( - { - "predicted_max": 24.3, - "range_low": 22.3, - "range_high": 26.3, - "unit": "°C", - "confidence": "medium", - "final_judgment_zh": "London 最高温中枢暂看24°C附近。", - "final_judgment_en": "London high is centered near 24°C.", - "metar_read_zh": "最新METAR报文09:00观测温度21°C,西北风4节,CAVOK。", - "metar_read_en": "The latest METAR shows 21°C at 09:00 with northwesterly wind and CAVOK.", - "reasoning_zh": "当前09时实测21°C处于快速升温路径,但TAF显示", - "reasoning_en": "The 09:00 observation is on a fast warming path, but TAF shows", - "risks_zh": ["后续METAR若升温放缓,需要下修。"], - "risks_en": ["If later METAR warming slows, revise lower."], - "model_cluster_note_zh": "4/4 个模型落在 DEB ±2°C 内。", - "model_cluster_note_en": "4/4 models sit within 2°C of DEB.", - }, - { - "city_display_name": "London", - "temp_symbol": "°C", - "deb": {"prediction": 24.3}, - "model_cluster": {"sources": [{"value": 22.3}, {"value": 26.3}]}, - "observation_anchor": {"is_airport_metar": True, "station_code": "EGLL"}, - "airport_current": { - "station_code": "EGLL", - "temp": 21.0, - "report_time": "09:00Z", - "raw_metar": "EGLL 270900Z 34004KT CAVOK 21/09 Q1016", - }, - }, - locale="zh-CN", - ) - - assert payload["reasoning_zh"] == "当前09时实测21°C处于快速升温路径。" - assert payload["reasoning_en"] == "The 09:00 observation is on a fast warming path." - assert payload["_polyweather_meta"]["trimmed_incomplete_fields"] == [ - "reasoning_en", - "reasoning_zh", - ] - - -def test_city_ai_schema_completion_guards_stale_observation_text(): - payload = scan_terminal_service._complete_city_ai_payload( - { - "predicted_max": 36.0, - "range_low": 35.0, - "range_high": 37.0, - "unit": "°C", - "confidence": "medium", - "final_judgment_zh": "Manila 最新 METAR 已经支撑 36°C 高温中枢。", - "final_judgment_en": "Manila latest METAR supports a 36°C high center.", - "metar_read_zh": "RPLL 最新 METAR 显示 36°C,当前作为强实况锚点。", - "metar_read_en": "RPLL latest METAR shows 36°C and is a strong live anchor.", - "reasoning_zh": "最新 METAR 与模型共同支撑上修。", - "reasoning_en": "Latest METAR and models jointly support an upward revision.", - "risks_zh": ["若继续升温,需要上修。"], - "risks_en": ["If it keeps warming, revise upward."], - "model_cluster_note_zh": "3/3 个模型集中。", - "model_cluster_note_en": "3/3 models are clustered.", - }, - { - "city_display_name": "Manila", - "temp_symbol": "°C", - "deb": {"prediction": 34.0}, - "model_cluster": { - "sources": [{"value": 33.5}, {"value": 34.0}, {"value": 34.4}] - }, - "metar_context": { - "stale_for_today": True, - "last_observation_time": "00:00Z", - }, - "observation_anchor": {"is_airport_metar": True, "station_code": "RPLL"}, - "airport_current": { - "station_code": "RPLL", - "temp": 36.0, - "report_time": "00:00Z", - "raw_metar": "RPLL 270000Z 34004KT CAVOK 36/24 Q1009", - }, - }, - locale="zh-CN", - ) - - assert payload["predicted_max"] == 34.0 - assert "过旧" in payload["metar_read_zh"] - assert "不能作为强实况锚点" in payload["metar_read_zh"] - assert "先以 DEB 和多模型路径为主" in payload["final_judgment_zh"] - assert "共同支撑上修" not in payload["reasoning_zh"] - assert "deterministic_guard_fields" in payload["_polyweather_meta"] - - -def test_city_ai_schema_completion_guards_observed_high_break_numbers(): - payload = scan_terminal_service._complete_city_ai_payload( - { - "predicted_max": 34.0, - "range_low": 32.5, - "range_high": 34.7, - "unit": "°C", - "confidence": "medium", - "final_judgment_zh": "Manila 最高温仍以 34.0°C 为中枢。", - "final_judgment_en": "Manila high remains centered near 34.0°C.", - "metar_read_zh": "RPLL 最新 METAR 显示 35°C,CAVOK。", - "metar_read_en": "RPLL latest METAR shows 35°C and CAVOK.", - "reasoning_zh": "模型区间仍覆盖当前路径,无需上修。", - "reasoning_en": "The model range still covers the path, so no upward revision is needed.", - "risks_zh": ["后续报文偏离再修正。"], - "risks_en": ["Revise if later reports diverge."], - "model_cluster_note_zh": "4/4 个模型集中。", - "model_cluster_note_en": "4/4 models are clustered.", - }, - { - "city_display_name": "Manila", - "temp_symbol": "°C", - "deb": {"prediction": 34.0}, - "model_cluster": { - "sources": [ - {"value": 32.5}, - {"value": 33.8}, - {"value": 34.0}, - {"value": 34.7}, - ] - }, - "observation_anchor": {"is_airport_metar": True, "station_code": "RPLL"}, - "airport_current": { - "station_code": "RPLL", - "temp": 35.0, - "report_time": "03:00Z / 当地 11:00", - "raw_metar": "RPLL 270300Z 34004KT CAVOK 35/24 Q1009", - }, - }, - locale="zh-CN", - ) - - assert payload["predicted_max"] == 35.0 - assert payload["range_high"] == 35.0 - assert "上修到至少 35.0°C" in payload["final_judgment_zh"] - assert "无需上修" not in payload["reasoning_zh"] - assert "deterministic_guard_fields" in payload["_polyweather_meta"] - def test_cities_endpoint_uses_denver_display_name_for_aurora_market(): response = client.get("/api/cities")