MiMo AI 能力扩展:TAF解读、概率分布解读、异常检测、市场概览

AI 解读字段扩展:
    - 新增 taf_read_zh/en:解读机场预报中影响今日峰值窗口的变化
    - 新增 probability_read_zh/en:描述概率分布形态(最高桶、偏左/偏右)
    - stream max_tokens 900→1200 容纳新输出字段
    - 缓存 key 简化为 METAR原文+观测时间,大幅提升命中率
    - 兜底函数补全 TAF 和概率字段的确定性生成

    异常检测:
    - 纯数学计算,零 AI 延迟:实测温度 vs 全部模型预测上下限
    - 三级告警:breakout_above / breakout_below / deviation

    市场概览:
    - 新增 POST /api/scan/terminal/overview(MiMo 批量解读,缓存10分钟)
    - 前端 MarketOverviewBanner 可折叠横幅(顶栏与标签栏之间)
    - 移动端适配 640px/768px 断点,暗色/亮色双主题

    Scope-risk: MEDIUM — 170 测试通过,TypeScript 零错误,ruff 零告警
    Tested: python -m pytest -q (170 passed), npx tsc --noEmit (0 errors), ruff check .
This commit is contained in:
2569718930@qq.com
2026-05-14 22:41:31 +08:00
parent 6c08a68413
commit 2ee00f8016
13 changed files with 823 additions and 55 deletions
+32
View File
@@ -333,6 +333,34 @@ def _build_city_ai_fallback(
"range_low": range_low,
"range_high": range_high,
}
taf_data = ai_input.get("taf") if isinstance(ai_input.get("taf"), dict) else {}
if partial_ai.get("taf_read_zh") or partial_ai.get("taf_read_en"):
taf_zh = str(partial_ai.get("taf_read_zh") or partial_ai.get("taf_read_en") or "").strip()
taf_en = str(partial_ai.get("taf_read_en") or partial_ai.get("taf_read_zh") or "").strip()
elif taf_data.get("raw_taf"):
taf_zh = f"TAF 可用,需人工判读:{str(taf_data.get('raw_taf', ''))[:120]}"
taf_en = f"TAF available, manual read: {str(taf_data.get('raw_taf', ''))[:120]}"
else:
taf_zh = "无可用 TAF"
taf_en = "No TAF available"
prob_data = ai_input.get("probability") if isinstance(ai_input.get("probability"), dict) else {}
if partial_ai.get("probability_read_zh") or partial_ai.get("probability_read_en"):
prob_zh = str(partial_ai.get("probability_read_zh") or partial_ai.get("probability_read_en") or "").strip()
prob_en = str(partial_ai.get("probability_read_en") or partial_ai.get("probability_read_zh") or "").strip()
elif prob_data.get("top_buckets"):
top = prob_data["top_buckets"][0] if isinstance(prob_data["top_buckets"], list) else {}
if isinstance(top, dict) and top.get("label"):
skew_text = f",分布{'偏右' if prob_data.get('skew') == 'right' else '偏左' if prob_data.get('skew') == 'left' else '对称'}" if prob_data.get("skew") else ""
prob_zh = f"最高概率桶 {top.get('label', '')}{top.get('prob', '?')}%{skew_text}"
prob_en = f"Peak bucket {top.get('label', '')} ({top.get('prob', '?')}%){skew_text.replace('偏右', ', right-skewed').replace('偏左', ', left-skewed').replace('对称', ', symmetric')}"
else:
prob_zh = "概率分布数据可用但格式异常"
prob_en = "Probability data available but malformed"
else:
prob_zh = "概率分布暂未生成"
prob_en = "Probability distribution not yet generated"
return {
"predicted_max": partial_ai.get("predicted_max", predicted),
"range_low": partial_ai.get("range_low", range_low),
@@ -343,6 +371,10 @@ def _build_city_ai_fallback(
"final_judgment_en": final_en,
"metar_read_zh": metar_zh,
"metar_read_en": metar_en,
"taf_read_zh": taf_zh,
"taf_read_en": taf_en,
"probability_read_zh": prob_zh,
"probability_read_en": prob_en,
"reasoning_zh": reasoning_zh,
"reasoning_en": reasoning_en,
"risks_zh": risks_zh,