feat: implement AI city forecast streaming service and documentation

This commit is contained in:
2569718930@qq.com
2026-04-27 01:38:37 +08:00
parent 8fe25d06a5
commit e2ad51ee65
8 changed files with 239 additions and 56 deletions
+8 -3
View File
@@ -1,5 +1,13 @@
# Changelog
## Unreleased
- 城市决策卡新增 AI 机场报文解读缓存说明:页面内存缓存保留 loading / 流式片段 / 最终结果,`localStorage` 保存最终成功 payload,后端 AI 缓存不再因 `local_time` 变化失效
- 城市决策卡市场层改用完整 `all_buckets` 并严格识别 exact / range / or higher / or lower 温度桶方向,避免最高温中枢错配到不合理尾部桶
- 温度桶标签统一规范化 `C/F/°C/°F`,修复 `31°°C` 这类重复单位展示
- 决策卡展示文案将“概率差”收口为“模型-市场差”,明确口径为 `模型概率 - 市场隐含概率`
- README、前端 README、API 文档和网页 `/docs` 文档同步补充城市决策卡、AI 机场报文解读组成、缓存策略和市场层解释
## 1.5.4 - 2026-04-18
- 今日日内分析升级为专业气象判断台:主判断、置信度、基准/上修/下修路径、下一观测点、证据链、失效条件和确认条件前置展示
@@ -58,9 +66,6 @@
- 日内结构总摘要补充“TAF 未新增压温不等于继续升温”的解释,避免误读
- 浏览器插件多日预报改为 `DEB` 优先,基础判断卡补充方向、置信度与原因,并统一引流到主站首页
## Unreleased
## 1.5.0 - 2026-03-21
- 运行态状态与缓存支持 SQLite 渐进迁移,新增 `POLYWEATHER_STATE_STORAGE_MODE=file|dual|sqlite`
+6 -1
View File
@@ -17,7 +17,7 @@ Public docs center: `/docs/intro` on the main site (bilingual product documentat
![PolyWeather Ankara analysis](docs/images/demo_ankara.png)
## Product Status (2026-04-19)
## Product Status (2026-04-27)
- Subscription live: `Pro Monthly 5 USDC`.
- Points redemption live: `500 points = 1 USDC`, max `3 USDC` off.
@@ -29,6 +29,10 @@ Public docs center: `/docs/intro` on the main site (bilingual product documentat
- EMOS/CRPS calibration is wired and trainable, but production should stay on `legacy` or `emos_shadow`; `emos_primary` is only for candidates that pass local offline evaluation and manual rollout.
- Intraday analysis is now positioned as a professional meteorology read: headline, confidence, base/upside/downside paths, next observation point, evidence chain, failure modes, and confirmation rules.
- Intraday modal now blocks stale cached detail during refresh, so users do not briefly trade off old city/date data before full detail arrives.
- City decision cards now include the AI airport read: METAR, DEB, model cluster, and the AI expected-high center are resolved before mapping the result to Polymarket temperature buckets.
- AI airport reads now use in-page memory cache, browser `localStorage`, and backend short-TTL cache; returning from another dashboard tab restores existing stream text or final results before any new request is needed.
- Market bucket matching now uses the full `all_buckets` surface and strict exact / range / or-higher / or-lower direction checks, reducing bad matches to unreasonable tail buckets.
- The card label “model-market difference” means `model probability - market-implied probability`; positive values indicate weather probability above market pricing, while negative values indicate the YES is already priced more fully.
- Calibrated model probability is now the primary probability panel. It shows the active production probability engine; EMOS/LGBM are surfaced only when evaluated or shadowed, while model consensus and market prices remain secondary references.
- Non-Hong Kong airport cities now ingest `TAF` and parse `FM / TEMPO / BECMG / PROB30/40`.
- Temperature chart now overlays `TAF Timing` markers near the expected peak window.
@@ -57,6 +61,7 @@ See: [AGPL-3.0 & Commercial Boundary](docs/OPEN_CORE_POLICY.md)
- Uses DEB (Dynamic Error Balancing) to blend multi-model highs.
- Generates settlement-oriented calibrated probability buckets (`mu` + bucket distribution), with `LGBM` metadata surfaced when the calibrated engine is active.
- Maps weather view to Polymarket quotes for mispricing scan.
- Adds city decision cards that combine AI airport reads, expected-high centers, full market-bucket mapping, and model-market difference in one view.
- Reuses one analysis core across web dashboard and Telegram bot.
- Adds payment audit trails, replay tooling, and incident visibility in ops.
- Adds peak-window-oriented intraday analysis with meteorology headline, path buckets, evidence chain, invalidation rules, and confirmation rules.
+6 -1
View File
@@ -14,7 +14,7 @@
![PolyWeather Ankara 分析页](docs/images/demo_ankara.png)
## 当前产品状态(2026-04-19
## 当前产品状态(2026-04-27
- 已上线订阅制:`Pro 月付 5 USDC`
- 已上线积分抵扣:`500 积分 = 1 USDC`,最多抵扣 `3 USDC`
@@ -38,6 +38,10 @@
- `/ops` 现已展示缓存桶数量、summary cache hit/miss 与 prewarm heartbeat。
- 今日日内分析已改为“专业气象判断台”:顶部先给气象主判断、置信度、基准/上修/下修路径、下一观测点,再展示证据链、失效条件、确认条件和模型层。
- 日内分析弹窗在 full detail / market detail 同步完成前会锁住旧内容并显示刷新状态,避免用户短暂看到上一轮缓存数据后误判。
- 城市决策卡已接入 AI 机场报文解读:先用 METAR、DEB、多模型集群和 AI 最高温中枢判断天气路径,再映射到 Polymarket 温度桶。
- AI 机场报文解读现在同时使用页面内存缓存、浏览器 `localStorage` 和后端短 TTL 缓存;从其他选项卡切回决策卡时会优先恢复已有流式内容或最终结果。
- 市场温度桶匹配已改为完整 `all_buckets` 映射,按 exact / range / or higher / or lower 方向严格匹配,避免把天气中枢错配到不合理尾部桶。
- 决策卡中的“模型-市场差”口径为 `模型概率 - 市场隐含概率`,正值表示天气概率高于市场报价,负值表示市场已经更充分计价。
- 概率区已改为“校准模型概率”;默认展示生产概率引擎输出,EMOS/LGBM 只在通过评估或作为 shadow 时进入解释层。
- 今日日内结构解读已支持可选 `Groq` 改写层,失败时自动回退规则文案。
- 前端部署文档已补充 Vercel 节流建议,包括 analytics 关闭、eager fetch 开关与扫描流量防火墙规则。
@@ -58,6 +62,7 @@
- DEBDynamic Error Balancing)融合多模型最高温。
- 输出结算导向校准概率分布(`mu` + 温度桶),并在 LGBM 生效时展示校准引擎元数据。
- 将模型观点映射到 Polymarket 行情,做错价扫描。
- 地图城市决策卡把 AI 机场报文解读、最高温中枢、完整市场温度桶和模型-市场差放在同一张卡中展示。
- Web 仪表盘与 Telegram Bot 复用同一分析内核。
- 支付链路具备事件重放、SQLite 审计事件与 RPC 容灾能力。
- 官方增强层支持按国家 provider 统一接入,但不替代机场主站、METAR 或明确官方结算站。
+48 -1
View File
@@ -1,6 +1,6 @@
# PolyWeather API 文档(v1.5.4
最后更新:`2026-04-18`
最后更新:`2026-04-27`
本文档描述当前对外可用 API 口径(`web/app.py` + `web/routes.py` + `frontend/app/api/*`)。
@@ -31,6 +31,8 @@ flowchart LR
| `/api/city/{name}/summary` | GET | 轻量摘要 |
| `/api/city/{name}/detail` | GET | 聚合详情(含 market_scan |
| `/api/history/{name}` | GET | 历史对账 |
| `/api/scan/terminal/ai-city` | POST | 城市决策卡 AI 解读(非流式 JSON) |
| `/api/scan/terminal/ai-city/stream` | POST | 城市决策卡 AI 解读(SSE 流式) |
### `GET /api/city/{name}/detail`
@@ -59,6 +61,47 @@ flowchart LR
- `vertical_profile_signal.heating_setup / suppression_risk / trigger_risk / mixing_strength`
- `taf.signal.peak_window / suppression_level / disruption_level / markers`
### `POST /api/scan/terminal/ai-city/stream`
城市决策卡使用该接口生成“AI 机场报文解读”。前端默认请求 SSE 流,流式展示机场报文解读片段,最终以 `final` 事件返回完整 payload。
请求体:
```json
{
"city": "Buenos Aires",
"force_refresh": false,
"locale": "zh-CN"
}
```
SSE 事件:
- `progress`:阶段性状态,例如开始调用 AI、切换非流式重试。
- `preview`:可显示的预览文本。
- `delta`:模型流式文本增量,前端会从中提取机场报文解读片段。
- `final`:完整 `AiCityForecastPayload`
重点字段:
- `status`:通常为 `ready`;超时或降级时会带 `reason / reason_zh / reason_en`
- `cached`:是否命中后端 AI 缓存。
- `degraded`:是否为降级结果。前端不会把 degraded 结果写入长期 localStorage,但会保留页面内存态,避免切换选项卡后空白。
- `city_forecast.predicted_max`AI 给出的预计最高温中枢候选。
- `city_forecast.range_low / range_high`AI 给出的天气区间。
- `city_forecast.final_judgment_zh / final_judgment_en`:最终判断。
- `city_forecast.metar_read_zh / metar_read_en`:机场报文解读。
- `city_forecast.reasoning_zh / reasoning_en`:把 METAR、DEB、多模型集群与日内风险合并后的推理。
- `city_forecast.model_cluster_note_zh / model_cluster_note_en`:模型集群说明。
- `city_forecast.risks_zh / risks_en`:后续上修或下修触发条件。
### 城市决策卡市场层口径
- 前端会请求完整 `market_scan` / `all_buckets`,而不是只取 lite 结果。
- 温度桶匹配按今日预计最高温中枢映射,并区分 exact、range、or higher、or lower,避免把 30°C 附近的天气中枢误配到不合理尾部桶。
- `模型-市场差 = 模型概率 - 市场隐含概率`。正值表示天气概率高于市场报价;负值表示市场已经更充分计价。
- 温度桶标签会统一规范化 `C/F/°C/°F`,避免前端重复显示单位。
### `detail` 新增结构信号说明
`/api/city/{name}/detail` 现在会返回一组更偏交易场景的结构字段:
@@ -253,6 +296,10 @@ flowchart LR
- 详情接口与支付接口:`no-store`
- `METAR` / `TAF` / settlement current 由后端各自维护短 TTL 缓存
- 前端打开今日日内分析时,如果 full detail 或 market scan 正在同步,会先显示刷新锁,不展示可交互的旧内容
- 城市决策卡 AI 解读前端缓存键为 `city + local_date + locale + METAR signature`signature 优先使用原始 METAR,缺失时回退到报文时间、观测时间和温度
- 城市决策卡 AI 解读使用两层前端缓存:页面内存缓存保存 loading / 流式进度 / 最终 payload`localStorage` 保存最终成功 payload,默认 TTL 1 小时
- 后端城市 AI 缓存不使用 `local_time` 作为 key,避免同一观测因当前时钟变化反复失效
- 城市市场扫描完整桶缓存按 `city + local_date + full` 存储,默认 TTL 10 分钟
## 9. 调试示例
+4
View File
@@ -39,6 +39,10 @@ PolyWeather Pro 的生产前端工程。
- 城市详情会自动识别“单模型 / 单日”的稀疏缓存并主动刷新,避免误把残缺 detail 当作完整结果
- 右侧详情面板在多日预报仍未补齐时会显示同步占位卡,不再把“只有今天一张卡”的中间态伪装成完整数据
- 日内分析弹窗在 full detail / market scan 同步时会锁住旧内容并显示刷新状态,避免用户短暂看到旧城市或旧日期的数据
- 城市决策卡支持从地图点击城市进入;机会榜和日历仍按 Pro 权限控制,地图探索和城市简报可作为轻量入口
- 城市决策卡的 AI 机场报文解读包括最终判断、METAR 解读、推理说明、模型集群备注、风险提示和原始 METAR
- AI 机场报文解读按 `city + local_date + locale + METAR signature` 做页面内存缓存和 `localStorage` 最终结果缓存;切换选项卡返回时会优先恢复已有内容
- 市场价格层使用完整 `all_buckets` 匹配温度桶,并把 `模型-市场差` 解释为 `模型概率 - 市场隐含概率`
- 概率区展示当前生产概率引擎输出;EMOS / LGBM 只在评估通过或 shadow 对照时进入解释层,模型共识和市场价格只作为辅助说明
- `/ops` 现已展示 prewarm worker 运行态、缓存桶状态与 summary cache hit/miss
@@ -16,16 +16,19 @@ import { normalizeCityKey } from "./decision-utils";
const AI_CITY_FORECAST_CACHE_PREFIX = "polyWeather_aiCityForecast_v3";
const AI_CITY_FORECAST_CACHE_TTL_MS = 60 * 60 * 1000;
const AI_CITY_FORECAST_MAX_CONCURRENT_STREAMS = 2;
const CITY_MARKET_SCAN_CACHE_PREFIX = "polyWeather_cityMarketScan_v3";
const CITY_MARKET_SCAN_CACHE_TTL_MS = 10 * 60 * 1000;
const pendingAiCityForecastRequests = new Map<
string,
Promise<AiCityForecastPayload>
>();
const queuedAiCityForecastTasks: Array<() => void> = [];
const aiCityForecastStateCache = new Map<
string,
{ state: AiCityForecastState; updatedAt: number }
>();
let activeAiCityForecastStreams = 0;
type AiCityStreamProgress = {
stage?: string | null;
@@ -114,6 +117,33 @@ function writeCachedAiForecastState(key: string, state: AiCityForecastState) {
});
}
function runQueuedAiCityForecastTask<T>(
task: () => Promise<T>,
onQueued?: () => void,
) {
return new Promise<T>((resolve, reject) => {
const run = () => {
activeAiCityForecastStreams += 1;
task()
.then(resolve, reject)
.finally(() => {
activeAiCityForecastStreams = Math.max(
0,
activeAiCityForecastStreams - 1,
);
const next = queuedAiCityForecastTasks.shift();
if (next) next();
});
};
if (activeAiCityForecastStreams < AI_CITY_FORECAST_MAX_CONCURRENT_STREAMS) {
run();
} else {
onQueued?.();
queuedAiCityForecastTasks.push(run);
}
});
}
function parseAiCityStreamBlock(block: string): AiCityStreamEvent | null {
const eventLines = block
.split(/\r?\n/)
@@ -221,52 +251,57 @@ function requestAiCityForecast({
const pending = pendingAiCityForecastRequests.get(requestKey);
if (pending) return pending;
const request = buildBrowserBackendHeaders({
Accept: "text/event-stream",
"Content-Type": "application/json",
})
.then((headers) =>
fetchBackendApi("/api/scan/terminal/ai-city/stream", {
method: "POST",
headers,
cache: "no-store",
body: JSON.stringify({
city,
force_refresh: forceRefresh,
locale,
}),
const request = runQueuedAiCityForecastTask(async () => {
const headers = await buildBrowserBackendHeaders({
Accept: "text/event-stream",
"Content-Type": "application/json",
});
const response = await fetchBackendApi("/api/scan/terminal/ai-city/stream", {
method: "POST",
headers,
cache: "no-store",
body: JSON.stringify({
city,
force_refresh: forceRefresh,
locale,
}),
)
.then(async (response) => {
if (!response.ok) {
let detailMessage = "";
try {
const raw = await response.text();
const errorPayload = JSON.parse(raw);
const message = String(errorPayload?.error || "").trim();
const rawDetail = String(errorPayload?.detail || "").trim();
const elapsed = Number(errorPayload?.elapsed_ms);
const timeout = Number(errorPayload?.timeout_ms);
detailMessage = [
message,
rawDetail,
Number.isFinite(elapsed) && Number.isFinite(timeout)
? `elapsed ${Math.round(elapsed / 1000)}s / timeout ${Math.round(timeout / 1000)}s`
: "",
]
.filter(Boolean)
.join(" · ");
} catch {
detailMessage = "";
}
throw new Error(
detailMessage
? `HTTP ${response.status} · ${detailMessage}`
: `HTTP ${response.status}`,
);
});
if (!response.ok) {
let detailMessage = "";
try {
const raw = await response.text();
const errorPayload = JSON.parse(raw);
const message = String(errorPayload?.error || "").trim();
const rawDetail = String(errorPayload?.detail || "").trim();
const elapsed = Number(errorPayload?.elapsed_ms);
const timeout = Number(errorPayload?.timeout_ms);
detailMessage = [
message,
rawDetail,
Number.isFinite(elapsed) && Number.isFinite(timeout)
? `elapsed ${Math.round(elapsed / 1000)}s / timeout ${Math.round(timeout / 1000)}s`
: "",
]
.filter(Boolean)
.join(" · ");
} catch {
detailMessage = "";
}
return readAiCityForecastStream(response, locale, onProgress);
})
throw new Error(
detailMessage
? `HTTP ${response.status} · ${detailMessage}`
: `HTTP ${response.status}`,
);
}
return readAiCityForecastStream(response, locale, onProgress);
}, () => {
onProgress?.({
stage: "queued",
message_en:
"AI airport-bulletin read is queued behind the cities already streaming...",
message_zh: "AI 机场报文解读已排队,正在等待前面的城市完成流式生成…",
});
})
.finally(() => {
pendingAiCityForecastRequests.delete(requestKey);
});
+86 -4
View File
@@ -55,14 +55,14 @@ export const DOCS_PAGES: DocsPage[] = [
id: "core-modules",
title: "你会在页面上看到什么",
blocks: [
{ type: "bullets", items: ["锚点状态:先确认当前机场主站实测、日内已见高点和结算时钟。", "当前节奏:把“此刻应到温度”和“机场实测”放在一张卡里,判断今天跑得快还是慢。", "专业气象结论条:先给今日主判断、置信度、基准/上修/下修路径和下一观测点。", "校准模型概率 / 模型区间与分歧:概率层看当前生产概率引擎输出;EMOS / LGBM 只有在评估通过或 shadow 对照时进入解释层,模型区间用于解释分歧。", "气象证据链 / 失效条件 / 确认条件:解释为什么这么判断,以及什么情况会让判断降级。", "历史对账:查看已结算样本、DEB MAE、单模型表现和新增模型参考。"] },
{ type: "bullets", items: ["锚点状态:先确认当前机场主站实测、日内已见高点和结算时钟。", "当前节奏:把“此刻应到温度”和“机场实测”放在一张卡里,判断今天跑得快还是慢。", "专业气象结论条:先给今日主判断、置信度、基准/上修/下修路径和下一观测点。", "城市决策卡:从地图进入城市简报,读取 AI 机场报文解读、最高温中枢、市场温度桶和模型-市场差。", "校准模型概率 / 模型区间与分歧:概率层看当前生产概率引擎输出;EMOS / LGBM 只有在评估通过或 shadow 对照时进入解释层,模型区间用于解释分歧。", "气象证据链 / 失效条件 / 确认条件:解释为什么这么判断,以及什么情况会让判断降级。", "历史对账:查看已结算样本、DEB MAE、单模型表现和新增模型参考。"] },
],
},
{
id: "how-to-read",
title: "如何快速读懂主站",
blocks: [
{ type: "steps", items: ["先看专业气象结论条,确认今日主判断、基准路径和下一观测点。", "再看锚点状态和今日气温预测图,确认机场实测、DEB、峰值窗口和关键档位线。", "接着看气象证据链、失效条件和确认条件,判断这个路径有没有被新观测破坏。", "最后看校准模型概率、模型区间和市场参考,判断概率是否已经被市场充分计价。"] },
{ type: "steps", items: ["先看专业气象结论条或城市决策卡,确认今日主判断、最高温中枢和下一观测点。", "再看锚点状态和今日气温预测图,确认机场实测、DEB、峰值窗口和关键档位线。", "接着看 AI 机场报文解读、气象证据链、失效条件和确认条件,判断这个路径有没有被新观测破坏。", "最后看校准模型概率、模型区间、市场温度桶和模型-市场差,判断概率是否已经被市场充分计价。"] },
],
},
],
@@ -83,14 +83,14 @@ export const DOCS_PAGES: DocsPage[] = [
id: "core-modules",
title: "What you see on the site",
blocks: [
{ type: "bullets", items: ["Anchor status: current airport-primary observation, day-high-so-far, and the settlement clock.", "Current pace: compares where the airport should be by now versus the actual observation.", "Professional meteorology read: headline, confidence, base/upside/downside path, and next observation point.", "Calibrated model probability / model spread: probability comes from the calibrated engine; spread explains model disagreement.", "Evidence chain / failure modes / confirmation: why the read is valid and what would downgrade it.", "History reconciliation: settled-sample MAE, single-model performance, and the new model reference stack."] },
{ type: "bullets", items: ["Anchor status: current airport-primary observation, day-high-so-far, and the settlement clock.", "Current pace: compares where the airport should be by now versus the actual observation.", "Professional meteorology read: headline, confidence, base/upside/downside path, and next observation point.", "City decision cards: map-launched city briefs with the AI airport read, expected-high center, market bucket, and model-market difference.", "Calibrated model probability / model spread: probability comes from the calibrated engine; spread explains model disagreement.", "Evidence chain / failure modes / confirmation: why the read is valid and what would downgrade it.", "History reconciliation: settled-sample MAE, single-model performance, and the new model reference stack."] },
],
},
{
id: "how-to-read",
title: "How to read the dashboard quickly",
blocks: [
{ type: "steps", items: ["Start with the professional meteorology read: headline, base path, and next observation point.", "Use anchor status and the intraday chart to check current observations, DEB, peak window, and key bucket lines.", "Read the evidence chain, failure modes, and confirmation rules to see whether the path is still valid.", "Then compare the calibrated probability, model spread, and market reference."] },
{ type: "steps", items: ["Start with the professional meteorology read or city decision card: headline, expected-high center, base path, and next observation point.", "Use anchor status and the intraday chart to check current observations, DEB, peak window, and key bucket lines.", "Read the AI airport read, evidence chain, failure modes, and confirmation rules to see whether the path is still valid.", "Then compare calibrated probability, model spread, market bucket, and model-market difference."] },
],
},
],
@@ -177,6 +177,88 @@ export const DOCS_PAGES: DocsPage[] = [
},
},
},
{
slug: "city-decision-cards",
group: "analysis",
content: {
"zh-CN": {
title: "城市决策卡",
description: "这页解释地图城市决策卡如何把 AI 机场报文解读、最高温中枢、市场温度桶和模型-市场差组合成可验证判断。",
sections: [
{
id: "entry-and-permission",
title: "从地图进入决策卡",
blocks: [
{ type: "paragraph", text: "用户可以从地图点击城市进入城市决策卡。机会榜和日历属于 Pro 能力;地图探索和城市简报仍可作为轻量入口使用。" },
{ type: "callout", tone: "info", title: "先天气、后市场", text: "决策卡顶部的天气判断层不读取市场价格,先用 METAR、DEB、多模型集合和 AI 解读确定最高温中枢,再把该中枢映射到市场温度桶。" },
],
},
{
id: "ai-airport-read",
title: "AI 机场报文解读包括什么",
blocks: [
{ type: "bullets", items: ["最终判断:预计最高温中枢、上修/下修空间和当前操作口径。", "METAR 解读:报文时间、实测温度、露点/湿度、风向风速、能见度、云量、气压和 NOSIG / TAF 等机场侧信号。", "推理说明:把最新实测、DEB、多模型集群和午后对流/云雨/风向风险合并成日内节奏判断。", "模型集群备注:展示模型数量、模型区间,以及是否集中在 DEB ±2°C 内。", "风险提示:后续 METAR 或路径明显偏离时,说明应如何上调或下修。", "原始 METAR:保留原始报文,便于人工复核。"] },
],
},
{
id: "market-layer",
title: "市场价格层怎么读",
blocks: [
{ type: "paragraph", text: "温度桶标签来自完整市场桶列表,并会按 label / slug / question 识别 exact、or higher、or lower、range 等方向,避免把 30.5°C 错配到不合理的 16°C 或反向尾部桶。" },
{ type: "callout", tone: "info", title: "模型-市场差", text: "模型-市场差 = 模型概率 − 市场隐含概率。正数表示天气概率高于市场报价;负数表示市场已经把该 YES 计价得更充分。它不是温度变化,也不是收益率。" },
{ type: "paragraph", text: "YES 买入价以可执行报价为主;没有可靠模型概率或 YES 价格时,决策卡会显示报价已匹配但暂不计算模型-市场差。" },
],
},
{
id: "cache-behavior",
title: "为什么切换选项卡后不应重新空白加载",
blocks: [
{ type: "paragraph", text: "城市决策卡会用 city + local_date + locale + METAR signature 作为 AI 解读缓存键。METAR signature 优先使用原始报文,缺失时回退到报文时间、观测时间和温度。" },
{ type: "bullets", items: ["页面内存缓存:保存 loading 状态、流式进度、机场报文解读片段和最终结果;从其他选项卡切回时优先恢复旧内容。", "浏览器 localStorage:保存最终成功的 AI payload,默认 TTL 为 1 小时。", "后端 AI 缓存:不再把 local_time 放入缓存键,避免同一报文因当前时间变化反复失效。", "市场扫描缓存:完整 all_buckets 结果按城市和日期缓存,默认 TTL 为 10 分钟。"] },
],
},
],
},
"en-US": {
title: "City Decision Cards",
description: "How the city card combines the AI airport read, expected-high center, market bucket mapping, and model-market difference into a verifiable decision.",
sections: [
{
id: "entry-and-permission",
title: "Opening a card from the map",
blocks: [
{ type: "paragraph", text: "Users can click a city on the map to open its city decision card. The opportunity board and calendar are Pro surfaces; map exploration and city briefs remain the lightweight entry point." },
{ type: "callout", tone: "info", title: "Weather first, market second", text: "The weather decision layer does not use market price input. It first sets the expected-high center from METAR, DEB, the model cluster, and AI reasoning, then maps that center to the relevant market bucket." },
],
},
{
id: "ai-airport-read",
title: "What the AI airport read contains",
blocks: [
{ type: "bullets", items: ["Final judgment: expected-high center, upside/downside room, and the working decision.", "METAR read: report time, observed temperature, dew point / humidity, wind, visibility, clouds, pressure, and NOSIG / TAF airport-side signals.", "Reasoning: combines live observations, DEB, the model cluster, and convective / cloud / wind risks into an intraday pace read.", "Model-cluster note: model count, model range, and whether the cluster sits within DEB ±2°C.", "Risk notes: how later METAR/path breaks should raise or lower the high center.", "Raw METAR: preserved so the read can be manually audited."] },
],
},
{
id: "market-layer",
title: "How to read the market layer",
blocks: [
{ type: "paragraph", text: "Bucket labels come from the full market bucket list. The card reads label / slug / question text to distinguish exact, or-higher, or-lower, and range buckets, so a 30.5°C weather center is not matched to an unreasonable 16°C or reverse-tail bucket." },
{ type: "callout", tone: "info", title: "Model-market difference", text: "Model-market difference = model probability minus market-implied probability. A positive value means the weather probability is above market pricing; a negative value means the YES is already priced more fully by the market. It is not a temperature delta or return." },
{ type: "paragraph", text: "YES buy uses executable quote data when available. If either model probability or YES price is incomplete, the card shows the quote match but withholds the model-market difference." },
],
},
{
id: "cache-behavior",
title: "Why tab switching should not blank the AI read",
blocks: [
{ type: "paragraph", text: "The card keys AI reads by city + local_date + locale + METAR signature. The signature prefers the raw report and falls back to report time, observation time, and temperature." },
{ type: "bullets", items: ["In-page memory cache: stores loading state, stream progress, airport-read snippets, and final results so returning from another tab restores prior content first.", "Browser localStorage: stores final successful AI payloads for one hour by default.", "Backend AI cache: excludes local_time from the key so the same report does not expire merely because the current clock changed.", "Market-scan cache: stores full all_buckets results by city and date for 10 minutes by default."] },
],
},
],
},
},
},
{
slug: "model-stack-deb",
group: "analysis",
+2 -2
View File
@@ -74,12 +74,12 @@ SCAN_AI_TIMEOUT_SEC = _env_int(
)
SCAN_CITY_AI_TIMEOUT_SEC = _env_int(
"POLYWEATHER_SCAN_CITY_AI_TIMEOUT_SEC",
18,
30,
min_value=10,
max_value=120,
)
SCAN_CITY_AI_RETRY_ON_STREAM_PARSE_ERROR = str(
os.getenv("POLYWEATHER_SCAN_CITY_AI_RETRY_ON_STREAM_PARSE_ERROR") or "false"
os.getenv("POLYWEATHER_SCAN_CITY_AI_RETRY_ON_STREAM_PARSE_ERROR") or "true"
).strip().lower() in {"1", "true", "yes", "on"}
SCAN_AI_CACHE_TTL_SEC = max(
30,