From c2fe8caddf7ea7cf2321e2c72e4910fb24871526 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Thu, 28 May 2026 20:46:35 +0800 Subject: [PATCH] docs: refresh v1.8.1 release docs --- .env.example | 7 ++ CHANGELOG.md | 13 ++ README.md | 43 +++++-- README_ZH.md | 45 +++++-- VERSION | 2 +- docs/AIRPORT_REALTIME_SOURCES.md | 37 +++++- docs/API_ZH.md | 50 +++++++- docs/CITY_DATA_SOURCES.md | 36 +++++- docs/COMMERCIALIZATION.md | 10 +- docs/CONFIGURATION_ZH.md | 14 +++ docs/FRONTEND_DEPLOYMENT_ZH.md | 9 ++ docs/MODEL_STACK_AND_DEB_ZH.md | 38 +++++- docs/MONITORING_ZH.md | 18 ++- docs/SERVICES_ZH.md | 84 +++++++------ docs/SUPABASE_SETUP_ZH.md | 2 +- docs/TECH_DEBT_ZH.md | 2 +- docs/data-architecture-review.md | 24 ++-- docs/deep-research-report.md | 18 ++- docs/images/demo_ankara.png | Bin 267698 -> 0 bytes docs/images/demo_map.png | Bin 969291 -> 0 bytes docs/payments/POLYGONSCAN_VERIFY.md | 2 +- docs/product-review-jun-2026.md | 9 +- ...edis-stream-realtime-event-architecture.md | 28 +++-- ...ream-realtime-event-architecture-design.md | 3 +- frontend/README.md | 11 +- .../landing/InstitutionalLandingPage.tsx | 115 +++--------------- frontend/package-lock.json | 4 +- frontend/package.json | 2 +- frontend/public/static/tel.png | Bin 0 -> 360450 bytes frontend/public/static/web.png | Bin 0 -> 242690 bytes 30 files changed, 397 insertions(+), 229 deletions(-) delete mode 100644 docs/images/demo_ankara.png delete mode 100644 docs/images/demo_map.png create mode 100644 frontend/public/static/tel.png create mode 100644 frontend/public/static/web.png diff --git a/.env.example b/.env.example index 3ac5b2b3..2541c9bd 100644 --- a/.env.example +++ b/.env.example @@ -19,6 +19,13 @@ UVICORN_WORKERS=1 UID=1000 GID=1000 POLYWEATHER_STATE_STORAGE_MODE=sqlite +# Realtime chart event store. Production should use Redis Stream; local/single-process +# development can set POLYWEATHER_EVENT_STORE=sqlite. +POLYWEATHER_EVENT_STORE=redis +POLYWEATHER_REDIS_URL=redis://polyweather_redis:6379/0 +POLYWEATHER_REDIS_STREAM_KEY=stream:city_observation +POLYWEATHER_REDIS_STREAM_MAXLEN=50000 +POLYWEATHER_REDIS_REQUIRED=true # Backend CORS allowlist. Add your Vercel production/preview domains when # NEXT_PUBLIC_POLYWEATHER_API_BASE_URL points browsers directly at this backend. WEB_CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000,https://polyweather.top,https://www.polyweather.top,https://api.polyweather.top diff --git a/CHANGELOG.md b/CHANGELOG.md index 67743409..e2a15a32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 1.8.1 - 2026-05-28 + +### 文档与发布 +- README / README_ZH 改用 `frontend/public/static/web.png` 与 `frontend/public/static/tel.png` 作为产品截图,并移除旧 `docs/images` README 截图引用。 +- 同步版本源到 `1.8.1`,刷新 API、Supabase、技术债、PolygonScan 验证等文档标题版本。 +- 更新前端、实时事件、数据源、模型栈与服务文档,补齐 Redis Stream + SSE Patch、DEB hourly consensus、城市当地时间图表、legacy 高斯图表叠加、跑道/CoWIN 曲线和中英文 Telegram 推送口径。 + +### 当前线上口径确认 +- 生产实时层为“HTTP snapshot + SSE patch + replayable event store”;前端只消费 `/api/events`,不直接连接 Redis。 +- `deb_hourly_consensus.v1` 是峰值窗口与 DEB 曲线展示的优先小时路径;DEB 不作为实测来源。 +- AMSC/AMOS 跑道曲线和香港 CoWIN 6087 参考站曲线按城市当地时间展示,结算跑道高亮,辅助跑道弱化。 + + ## 1.8.0 - 2026-05-27 ### 新增与重构 diff --git a/README.md b/README.md index 3e4c06d7..bb733fad 100644 --- a/README.md +++ b/README.md @@ -9,19 +9,19 @@ Public docs center: `/docs/intro` on the main site (bilingual product documentat ## Product Screenshots -### Global Dashboard +### Realtime Terminal -![PolyWeather global dashboard](docs/images/demo_map.png) +![PolyWeather realtime terminal](frontend/public/static/web.png) -### City Analysis (Ankara) +### Telegram Runway Alerts -![PolyWeather Ankara analysis](docs/images/demo_ankara.png) +![PolyWeather Telegram runway alerts](frontend/public/static/tel.png) ## Star History [![Star History Chart](https://api.star-history.com/svg?repos=yangyuan-zhen/PolyWeather&type=Date)](https://star-history.com/#yangyuan-zhen/PolyWeather&Date) -## Product Status (2026-05-23) +## Product Status (2026-05-28) - Subscription live: `Pro Monthly 10 USDC`. - Points system live: earn via group chat, welcome bonus (+20), first-message-of-day bonus (+2), weekly participation rewards. @@ -31,12 +31,21 @@ Public docs center: `/docs/intro` on the main site (bilingual product documentat - Auto-reconciliation live: event listener + periodic confirm loop. - Ops dashboard live: `/ops` for memberships, leaderboard, manual point grants, and payment incident triage. - Lightweight observability live: `/healthz`, `/api/system/status`, `/metrics`. +- Realtime terminal live: visible city charts subscribe through `/api/events?cities=...&since_revision=...`, receive `city_observation_patch.v1` SSE patches, and replay short gaps from Redis Stream in production or SQLite fallback in local/single-node mode. +- Chart refresh is observation-driven: live patches merge into the current chart without a loading overlay; only visible charts run a 60s no-patch fallback, and returning from a background browser tab triggers a foreground catch-up refresh. +- Temperature charts default to All Day, keep an optional Peak window derived from the DEB hourly path, and render all timestamps in the selected city's local time. +- The chart core has been split into focused logic/canvas/state modules; Recharts now receives explicit measured dimensions to avoid 0x0 rendering and disappearing curves. +- DEB hourly consensus (`deb_hourly_consensus.v1`) is now the preferred hourly forecast path for peak-window detection and chart overlays; DEB remains a forecast curve, never an observation source. +- Legacy Gaussian probability is rendered as horizontal probability bands and a `mu` reference line on the chart, rather than as a fake time-series curve. +- Settlement runway curves are visible by default for AMSC/AMOS cities; the configured settlement runway is highlighted and auxiliary runways are shown as secondary context. +- Hong Kong uses CoWIN station `6087` (Po Leung Kuk Choi Kai Yau School) as the 1-minute reference-station curve, with HKO 10-minute observations kept as the official meteorological layer. +- Telegram airport/runway pushes are bilingual by default and use settlement-endpoint runway temperatures for slope/current/summary copy. - Runtime state, cache, and core offline training/backfill flows now use SQLite as the primary path; legacy JSON/JSONL files remain only for migration, export, and explicit fallback input. - 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 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. +- Terminal city cards now combine settlement observations, DEB hourly consensus, model cluster context, calibrated probability, and market-bucket mapping without blocking the chart on AI text generation. +- Terminal data uses page memory cache, browser `localStorage`, backend short-TTL cache, SSE patch replay, and foreground refresh so returning from another tab restores the latest visible chart state quickly. - 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 (legacy Gaussian or EMOS), while model consensus remains a secondary reference. @@ -62,8 +71,9 @@ See: [AGPL-3.0 & Commercial Boundary](docs/OPEN_CORE_POLICY.md) - Aggregates observations and forecasts for 51 monitored cities. - Uses DEB (Dynamic Error Balancing) to blend multi-model highs. +- Builds a DEB-weighted hourly consensus path for peak-window logic and chart display. - Generates settlement-oriented calibrated probability buckets (`mu` + bucket distribution) via legacy Gaussian or EMOS/CRPS calibration. -- Adds city decision cards that combine AI airport reads, expected-high centers, full market-bucket mapping, and model-market difference in one view. +- Adds city decision cards that combine live observations, 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. @@ -88,7 +98,10 @@ flowchart LR WX --> AMOS["AMOS runway sensors (Korea)"] WX --> HKO["HKO / CWA / NOAA / Official settlement sources"] - API --> ANA["DEB + Trend + Probability + Market Scan"] + API --> ANA["DEB + Hourly Consensus + Probability + Market Scan"] + API --> SSE["SSE /api/events"] + WX --> SSE + SSE --> EVENT["Redis Stream / SQLite Event Log"] ANA --> PAY["Payment State (Intent + Event + Confirm Loop)"] ANA --> STATE["SQLite runtime state"] ``` @@ -125,6 +138,8 @@ npm run dev - `TAF` is used as an airport-side confirmation layer, not as the main temperature model. - Calibrated probability uses legacy Gaussian (default) or EMOS/CRPS when evaluated; model vote counts remain an explanatory consensus line, not the final probability. - Browser extension remains a lightweight monitoring + basic-bias product, while the site holds the full analysis experience. +- Realtime terminal charts use SSE patches plus replayable event storage; full HTTP detail remains the authoritative snapshot. +- Chart observations are shown in the city's local time, not the browser timezone. ## Runtime Data (Recommended on VPS) @@ -134,8 +149,14 @@ Use external runtime storage to avoid SQLite/git conflicts: POLYWEATHER_RUNTIME_DATA_DIR=/var/lib/polyweather POLYWEATHER_DB_PATH=/var/lib/polyweather/polyweather.db POLYWEATHER_STATE_STORAGE_MODE=sqlite +POLYWEATHER_EVENT_STORE=redis +POLYWEATHER_REDIS_URL=redis://polyweather_redis:6379/0 +POLYWEATHER_REDIS_STREAM_MAXLEN=50000 +POLYWEATHER_REDIS_REQUIRED=true ``` +For local development or a strict single-process fallback, keep `POLYWEATHER_EVENT_STORE=sqlite`. + ## EMOS Local Training Do not run full EMOS retraining on a small VPS. The VPS should collect data and load approved calibration files; training should run on a local/dev machine using a copied production SQLite database: @@ -214,5 +235,5 @@ curl http://127.0.0.1:8000/api/payments/runtime ## Version -- Version: `v1.8.0` -- Last Updated: `2026-05-23` +- Version: `v1.8.1` +- Last Updated: `2026-05-28` diff --git a/README_ZH.md b/README_ZH.md index decfb821..3cd2b69d 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -6,15 +6,15 @@ ## 产品截图 -### 全球看板 +### 实时终端 -![PolyWeather 全球地图看板](docs/images/demo_map.png) +![PolyWeather 实时终端](frontend/public/static/web.png) -### 城市分析(Ankara) +### Telegram 跑道推送 -![PolyWeather Ankara 分析页](docs/images/demo_ankara.png) +![PolyWeather Telegram 跑道推送](frontend/public/static/tel.png) -## 当前产品状态(2026-05-23) +## 当前产品状态(2026-05-28) - 已上线订阅制:`Pro 月付 10 USDC`。 - 已上线积分体系:群内发言赚分 + 首次发言欢迎奖励 (+20) + 每日首条消息奖励 (+2) + 每周全员参与奖。 @@ -26,6 +26,15 @@ - 已上线轻量运营后台:`/ops`(会员、周榜、补分、支付异常单)。 - 已上线轻量可观测性:`/healthz`、`/api/system/status`、`/metrics`。 - 已补最小外部监控栈:Prometheus + Alertmanager + Grafana + Telegram 告警 relay。 +- 实时终端已切换到可重放事件流:可见城市图表通过 `/api/events?cities=...&since_revision=...` 订阅 `city_observation_patch.v1`,生产环境使用 Redis Stream 做短窗口 replay,本地/单进程可回退 SQLite event log。 +- 图表刷新由实测事件驱动:SSE patch 直接合并到当前曲线,不弹 loading 遮罩;只有可见图表启用 60 秒无 patch 兜底,浏览器后台返回前台时会主动补齐最新 detail。 +- 城市图表默认展示“全天”,可选“高温”窗口由 DEB hourly path 推导;所有图表横轴都按城市当地时间展示,不按用户浏览器时区。 +- 核心图表组件已拆分为逻辑、状态与 canvas 渲染模块;Recharts 使用 `ResizeObserver` 后的明确宽高,规避 0x0 渲染和长时间挂页后曲线消失。 +- DEB hourly consensus(`deb_hourly_consensus.v1`)已作为峰值窗口和图表 DEB 曲线的优先小时路径;DEB 仍然是预测曲线,不作为实测来源。 +- legacy 高斯概率在图表上展示为概率温度带和 `mu` 参考线,不再伪装成一条时间序列曲线。 +- AMSC/AMOS 城市的结算跑道曲线默认展示并高亮,辅助跑道作为弱化曲线保留;釜山单跑道只展示 `SR/SL` 结算跑道,不再重复显示 AMOS 聚合线。 +- 香港默认展示 CoWIN `6087`(保良局陈守仁小学)1 分钟参考站曲线,HKO 10 分钟实测保留为官方气象层。 +- Telegram 机场/跑道推送默认中英文双语,并统一使用结算端点跑道温度计算当前值、15 分钟趋势和文案。 - 运行态状态、缓存与核心离线训练/回填链路已完成 SQLite 主路径收口;legacy JSON/JSONL 仅保留给迁移、导出与显式回退输入。 - EMOS/CRPS 校准链路已接通,但生产主概率保持 `legacy` 或 `emos_shadow`;`emos_primary` 只在本地离线评估通过并手动灰度后启用。 - 官方增强站网已统一接入: @@ -37,15 +46,15 @@ - `CWA`(台湾) - 东京现已接入羽田 `JMA AMeDAS` 10 分钟温度作为官方增强层。 - 已支持 Dashboard 定向预热 worker / cron 路径,运行态在 `/api/system/status` 与 `/ops` 可见。 -- `/ops` 现已展示缓存桶数量、summary cache hit/miss 与 prewarm heartbeat。 +- `/ops` 现已展示缓存桶数量、summary cache hit/miss 与运行态 heartbeat。 - 今日日内分析已改为“专业气象判断台”:顶部先给气象主判断、置信度、基准/上修/下修路径、下一观测点,再展示证据链、失效条件、确认条件和模型层。 - 日内分析弹窗在 full detail / market detail 同步完成前会锁住旧内容并显示刷新状态,避免用户短暂看到上一轮缓存数据后误判。 -- 城市决策卡已接入 AI 机场报文解读:先用 METAR、DEB、多模型集群和 AI 最高温中枢判断天气路径,再映射到温度桶。 -- AI 机场报文解读现在同时使用页面内存缓存、浏览器 `localStorage` 和后端短 TTL 缓存;从其他选项卡切回决策卡时会优先恢复已有流式内容或最终结果。 +- 终端城市卡已改为结构化实况 + DEB hourly consensus + 多模型集群 + 校准概率 + 市场温度桶,不再让图表等待 AI 文案生成。 +- 终端数据同时使用页面内存缓存、浏览器 `localStorage`、后端短 TTL 缓存、SSE patch replay 和前台恢复刷新;从其他选项卡切回时会优先恢复最新可见图表状态。 - 市场温度桶匹配已改为完整 `all_buckets` 映射,按 exact / range / or higher / or lower 方向严格匹配,避免把天气中枢错配到不合理尾部桶。 - 决策卡中的“模型-市场差”口径为 `模型概率 - 市场隐含概率`,正值表示天气概率高于市场报价,负值表示市场已经更充分计价。 - 概率区已改为”校准模型概率”;默认展示生产概率引擎输出(legacy 高斯或 EMOS),模型共识作为辅助参考。 -- 今日日内结构解读已支持可选 Groq 改写层,失败时自动回退规则文案。 +- 今日日内结构解读以规则与结构化信号为主,AI 文案只作为可降级辅助层,不替代实测、DEB、TAF 或结算逻辑。 - 前端设计系统全面重构:统一 CSS token 体系、消除 !important 滥用(134→49)、合并断点(18→10)、数百处硬编码颜色迁移至 CSS 变量、添加 ARIA 无障碍属性和键盘导航。完整审查记录见 `docs/frontend-ui-design-review.md`。 ## 许可证与商用边界(重要) @@ -62,8 +71,9 @@ - 聚合 51 个监控城市的实测与预报数据。 - DEB(Dynamic Error Balancing)融合多模型最高温。 +- 构建 DEB 加权小时共识曲线,用于峰值窗口判断和图表默认 DEB 展示。 - 输出结算导向校准概率分布(`mu` + 温度桶),通过 legacy 高斯或 EMOS/CRPS 校准引擎。 -- 地图城市决策卡把 AI 机场报文解读、最高温中枢、完整市场温度桶和模型-市场差放在同一张卡中展示。 +- 地图城市决策卡把结构化实况、最高温中枢、完整市场温度桶和模型-市场差放在同一张卡中展示。 - Web 仪表盘与 Telegram Bot 复用同一分析内核。 - 支付链路具备事件重放、SQLite 审计事件与 RPC 容灾能力。 - 官方增强层与跑道级传感器支持按国家 provider 统一接入(含韩国 AMOS 首尔/釜山跑道实测),不替代机场主站、METAR 或明确官方结算站。 @@ -85,7 +95,10 @@ flowchart LR WX --> OM["Open-Meteo"] WX --> HKO["HKO / CWA / NOAA 等官方结算源"] - API --> ANA["DEB + 趋势 + 概率 + 市场扫描"] + API --> ANA["DEB + 小时共识 + 概率 + 市场扫描"] + API --> SSE["SSE /api/events"] + WX --> SSE + SSE --> EVENT["Redis Stream / SQLite Event Log"] ANA --> PAY["支付状态(Intent + Event + Confirm Loop)"] ANA --> STATE["SQLite runtime state
legacy files only for migration/export fallback"] ``` @@ -121,8 +134,14 @@ npm run dev POLYWEATHER_RUNTIME_DATA_DIR=/var/lib/polyweather POLYWEATHER_DB_PATH=/var/lib/polyweather/polyweather.db POLYWEATHER_STATE_STORAGE_MODE=sqlite +POLYWEATHER_EVENT_STORE=redis +POLYWEATHER_REDIS_URL=redis://polyweather_redis:6379/0 +POLYWEATHER_REDIS_STREAM_MAXLEN=50000 +POLYWEATHER_REDIS_REQUIRED=true ``` +本地开发或严格单进程兜底可使用 `POLYWEATHER_EVENT_STORE=sqlite`。 + ## EMOS 本地训练流程 低配 VPS 只负责采集、服务和加载已通过评估的参数,不建议在 VPS 上跑 EMOS 全量训练。训练前先从 VPS 拉 SQLite 副本到本地: @@ -230,5 +249,5 @@ POLYWEATHER_OPS_ADMIN_EMAILS=yhrsc30@gmail.com ## 当前版本 -- 版本:`v1.8.0` -- 文档最后更新:`2026-05-23` +- 版本:`v1.8.1` +- 文档最后更新:`2026-05-28` diff --git a/VERSION b/VERSION index 27f9cd32..a8fdfda1 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.8.0 +1.8.1 diff --git a/docs/AIRPORT_REALTIME_SOURCES.md b/docs/AIRPORT_REALTIME_SOURCES.md index bb9a8910..30eed830 100644 --- a/docs/AIRPORT_REALTIME_SOURCES.md +++ b/docs/AIRPORT_REALTIME_SOURCES.md @@ -1,11 +1,23 @@ # 机场高频实时数据源 +最后更新:`2026-05-28` + ## 已接入城市 | 城市 | 机场 | ICAO/站点 | 数据源 | 频率 | 类型 | 费用 | |------|------|-----------|--------|------|------|------| | 首尔 | 仁川国际 | RKSI | AMOS (`global.amo.go.kr`) | 1 分钟 | 跑道对温度(2对) | 免费 | | 釜山 | 金海国际 | RKPK | AMOS (`global.amo.go.kr`) | 1 分钟 | 跑道对温度(1对) | 免费 | +| 香港 | CoWIN 6087 | 6087 | CoWIN (`cowin.hku.hk`) | 1 分钟 | 参考站温度(保良局陈守仁小学) | 免费 | +| 香港 | HKO | HKO | HKO 官方 CSV (`data.weather.gov.hk`) | 10 分钟 | 官方气象站温度 | 免费 | +| 台北 | 松山/中央气象署 | 466920 | CWA 开放数据 | 10 分钟 | 官方站点温度 | 免费 | +| 北京 | 首都机场 | ZBAA | AMSC AWOS | 1 分钟 | 跑道端点气温 | 免费 | +| 上海 | 浦东机场 | ZSPD | AMSC AWOS | 1 分钟 | 跑道端点气温 | 免费 | +| 广州 | 白云机场 | ZGGG | AMSC AWOS | 1 分钟 | 跑道端点气温 | 免费 | +| 成都 | 双流机场 | ZUUU | AMSC AWOS | 1 分钟 | 跑道端点气温 | 免费 | +| 重庆 | 江北机场 | ZUCK | AMSC AWOS | 1 分钟 | 跑道端点气温 | 免费 | +| 武汉 | 天河机场 | ZHHH | AMSC AWOS | 1 分钟 | 跑道端点气温 | 免费 | +| 青岛 | 胶东机场 | ZSQD | AMSC AWOS | 1 分钟 | 跑道端点气温 | 免费 | | 东京 | 羽田 | RJTT | JMA AMeDAS (`jma.go.jp`) | 10 分钟 | 机场站点实时温度 | 免费 | | 安卡拉 | Esenboğa | 17128 | MGM (`servis.mgm.gov.tr`) | 5-15 分钟 | 机场站点实时温度 | 免费 | | 伊斯坦布尔 | 伊斯坦布尔机场 | 17058 | MGM (`servis.mgm.gov.tr`) | 5-15 分钟 | 机场站点实时温度 | 免费 | @@ -36,6 +48,14 @@ > 后端通过 `weather_sources.py` 拉取并注入 `airport_primary`,前端市场监控通过 > `resolveMonitorTemperature` 优先读取 `airport_primary.temp` 获得小数精度温度。 +> **CoWIN 6087**: 香港图表默认参考站为 HKU CoWIN `6087`(保良局陈守仁小学)。 +> 该源提供约 1 分钟温度序列,作为 PM 最高温市场的高频参考曲线;HKO 10 分钟数据 +> 仍作为官方气象层保留。后端通过 `cowin_sources.py` 拉取并写入 `cowin_obs`。 + +> **AMSC AWOS**: 中国内地跑道城市读取 AMSC `getWindPlate` 中的 `TDZ_TEMP` / +> `MID_TEMP` / `END_TEMP`。这些字段是跑道观测位置气温,不是道面温度。 +> 结算跑道展示使用配置的结算端点;辅助跑道只作为背景曲线。 + ## 推送机制 - 每城按原生频率独立推送,不捆绑 @@ -44,14 +64,16 @@ - 仅当当前温度距 DEB 预测最高 ≤3°C 时推送 - 确认过峰值后自动停止 -## 前端实时同步与 SSE Patch 机制 +## 前端实时同步与 SSE Patch / Redis Stream 机制 -为了向用户提供秒级实况响应并降低服务器负载,系统已从定时轮询架构全面迁移至 **Server-Sent Events (SSE) 增量更新(SSE Patch)** 架构。 +为了向用户提供接近行情盘的实况响应并降低服务器负载,系统使用 **HTTP snapshot + Server-Sent Events (SSE) Patch + 可重放事件日志** 架构。生产环境推荐 Redis Stream;本地或单进程可回退 SQLite event log。 ### 1. 数据推送链路 (Data Pipeline) 1. **Collector 采集端触发**:在 `weather_sources.py` 中,当高频实况源(如 AMOS, CoWIN, MADIS 等)采集到温度更新或观测时间变更时,会调用 `_emit_temperature_patch_if_changed` 过滤重复值,并异步向 `/api/internal/collector-patch` 发送 POST 报文。 -2. **FastAPI SSE 广播**:FastAPI 后端的 `sse_router.py` 接收到 Patch 后,将其推入 `sse_manager` 进行全局广播,事件被包装为 `city_patch` 增量包,包含自增的全局 `revision` 和最新的 `changes`。 -3. **BFF 代理流**:浏览器前端通过 BFF (Next.js rewrites) 建立与 `/api/events` 的持久连接,从而无需定时轮询。 +2. **标准化事件**:`realtime_patch_schema.py` 将旧 `city_patch` 或新 payload 统一成 `city_observation_patch.v1`。 +3. **事件存储**:生产环境写入 Redis Stream(`stream:city_observation`)并生成全局递增 `revision`;SQLite `observation_patch_events` 保留为本地/兜底 replay。 +4. **FastAPI SSE 广播**:FastAPI 后端的 `sse_router.py` 根据城市订阅集合向匹配连接推送 patch;断线重连时按 `since_revision` replay。 +5. **BFF 代理流**:浏览器前端通过 BFF 建立与 `/api/events` 的持久连接,从而无需固定整图轮询。 ### 2. 前端消费与刷新规则 (Frontend Freshness Rules) - **扫描列表免轮询更新**:`use-scan-terminal-query.ts` 通过 `useSsePatchVersion` 钩子订阅全局 SSE 版本。当有任何城市产生更新时,列表将触发按需重绘,之前固定的 5 分钟 `setInterval` 定时轮询已被彻底禁用。 @@ -62,6 +84,8 @@ 1. 调用轻量级的 `/api/city/{city}/summary` 快速拉取最新实况温度。 2. 调用 `fetchHourlyForecastForCity(city, { ignoreCache: true })` 强刷完整的城市详情数据,确保数据一致性。 - **按需加载与 Stagger 优化**:在加载城市详情时,前端会优先加载 Active 状态的图表,而处于 Background/非活动状态的图表则通过 staggered timer (按槽位索引延迟 300ms~1500ms) 异步获取,以分流请求峰值。 +- **前台恢复补齐**:浏览器标签页长时间在后台时,回来后会主动强刷可见图表 full detail,避免 SSE 被浏览器挂起后曲线落后。 +- **当地时间**:patch 中保留 `city_timezone` / `observed_at_utc`,前端按城市当地时间绘制横轴。 ## 消息模板 @@ -83,6 +107,11 @@ Seoul / Incheon 16:03 | `TELEGRAM_AIRPORT_PUSH_INTERVAL_SEC` | 循环轮询间隔 | `60` | | `TELEGRAM_AIRPORT_PUSH_LANGUAGE` | 机场推送语言覆盖,可选 `both`/`en`/`zh` | `both` | | `KNMI_API_KEY` | KNMI API 密钥(阿姆斯特丹必填) | — | +| `POLYWEATHER_EVENT_STORE` | 实时事件存储,可选 `redis`/`sqlite` | `sqlite` | +| `POLYWEATHER_REDIS_URL` | Redis Stream 连接地址 | `redis://127.0.0.1:6379/0` | +| `POLYWEATHER_REDIS_STREAM_KEY` | Redis Stream key | `stream:city_observation` | +| `POLYWEATHER_REDIS_STREAM_MAXLEN` | Redis Stream 保留长度 | `50000` | +| `POLYWEATHER_REDIS_REQUIRED` | Redis 不可用时是否启动失败 | `true` | ## 未接入城市 diff --git a/docs/API_ZH.md b/docs/API_ZH.md index 30aefc1d..7ed07ff8 100644 --- a/docs/API_ZH.md +++ b/docs/API_ZH.md @@ -1,6 +1,6 @@ -# PolyWeather API 文档(v1.8.0) +# PolyWeather API 文档(v1.8.1) -最后更新:`2026-04-27` +最后更新:`2026-05-28` 本文档描述当前对外可用 API 口径(`web/app.py` + `web/routes.py` + `frontend/app/api/*`)。 @@ -17,7 +17,9 @@ flowchart LR FE["Browser / Dashboard"] --> BFF["Next.js Route Handlers (/api/*)"] BFF --> API["FastAPI (/web/app.py + /web/routes.py)"] API --> WX["Weather Collector"] - API --> ANA["DEB + Trend + Probability + Market Scan"] + API --> ANA["DEB + Hourly Consensus + Probability + Market Scan"] + API --> SSE["Realtime SSE (/api/events)"] + SSE --> EVENT["Redis Stream / SQLite Event Log"] API --> PAY["Payment Intent + Event + Confirm Loops"] API --> OBS["healthz / system status / metrics"] ``` @@ -31,6 +33,27 @@ flowchart LR | `/api/city/{name}/summary` | GET | 轻量摘要 | | `/api/city/{name}/detail` | GET | 聚合详情(含 market_scan) | | `/api/history/{name}` | GET | 历史对账 | +| `/api/events` | GET | SSE 实时观测事件流 | +| `/api/internal/collector-patch` | POST | 采集器内部写入实时观测 patch | + +### `GET /api/events` + +浏览器实时图表入口,使用 `text/event-stream`。 + +参数: + +- `cities=shanghai,hong kong`:可选,逗号分隔城市列表;为空表示订阅全部城市。 +- `since_revision=`:可选,断线重连后从指定 revision 之后 replay。 +- `replay_limit=`:可选,默认 `500`,后端会做上限保护。 + +事件: + +- `connected`:连接建立,包含当前 `latest_revision`。 +- `city_observation_patch.v1`:标准实时观测 patch,包含城市、序列、温度、观测 UTC 时间、城市时区、revision。 +- `resync_required`:replay 窗口不足或事件存储不可用,前端应回到 HTTP snapshot 重建画面。 +- `heartbeat`:保活。 + +生产环境推荐 `POLYWEATHER_EVENT_STORE=redis`,以 Redis Stream 保存短窗口事件并支持多 worker fanout;本地或单进程可使用 SQLite event log。 ### `GET /api/city/{name}/detail` @@ -51,6 +74,7 @@ flowchart LR - `probabilities.engine / calibration_mode / calibration_version` - `probabilities.raw_mu / raw_sigma / calibrated_mu / calibrated_sigma` - `probabilities.shadow_distribution` +- `deb.hourly_consensus / deb.hourly_path.base_source` - `intraday_meteorology.headline / confidence` - `intraday_meteorology.base_case_bucket / upside_bucket / downside_bucket` - `intraday_meteorology.next_observation_time` @@ -90,7 +114,7 @@ flowchart LR #### 2. `probabilities` -概率层基于 legacy 高斯分桶,以 DEB 融合预测 μ 和 ensemble spread σ 生成 1°C 粒度概率分布。 +概率层基于 legacy 高斯分桶,以 DEB 融合预测 `mu` 和 ensemble spread `sigma` 生成 1°C 粒度概率分布。前端图表将 legacy 高斯展示为水平概率温度带和 `mu` 参考线,不把概率分布渲染成时间序列曲线。 概率字段: @@ -99,6 +123,22 @@ flowchart LR - `distribution`:当天合约桶概率分布 - `distribution_all`:包含外围桶的完整分布 +#### 2.1 `deb.hourly_consensus` + +DEB hourly consensus 是当前图表与峰值窗口的优先小时路径。 + +重点字段: + +- `version`:当前为 `deb_hourly_consensus.v1` +- `base_source`:`multi_model_hourly_deb_weights` +- `times` / `temps`:城市当地日的小时路径 +- `model_weights`:DEB 权重折叠后的模型权重 + +说明: + +- 该路径是预测曲线,不是实测来源。 +- 图表默认展示全天;“高温”视图仅根据该路径推导 peak-centric 窗口。 + #### 3. `detail_depth` `detail_depth` 用于区分轻量 detail 与完整 detail。前端如果发现: @@ -255,6 +295,8 @@ flowchart LR - `summary?force_refresh=true`:`Cache-Control: no-store` - 详情接口与支付接口:`no-store` - `METAR` / `TAF` / settlement current 由后端各自维护短 TTL 缓存 +- 实时事件层:`POLYWEATHER_EVENT_STORE=redis` 时使用 Redis Stream;未启用 Redis 时使用 SQLite `observation_patch_events` 作为 replay fallback +- 前端终端图表:HTTP detail 仍是完整 snapshot,SSE patch 只做增量观测追加;长连接断开或后台恢复时前端会按 `since_revision` replay 或强刷 detail - 前端打开今日日内分析时,如果 full detail 或 market scan 正在同步,会先显示刷新锁,不展示可交互的旧内容 - 城市决策卡 AI 解读前端缓存键为 `city + local_date + locale + METAR signature`;signature 优先使用原始 METAR,缺失时回退到报文时间、观测时间和温度 - 城市决策卡 AI 解读使用两层前端缓存:页面内存缓存保存 loading / 流式进度 / 最终 payload,`localStorage` 保存最终成功 payload,默认 TTL 1 小时 diff --git a/docs/CITY_DATA_SOURCES.md b/docs/CITY_DATA_SOURCES.md index 7677300d..2e424491 100644 --- a/docs/CITY_DATA_SOURCES.md +++ b/docs/CITY_DATA_SOURCES.md @@ -1,6 +1,6 @@ # 城市实时数据源总览 -> 最后更新: 2026-05-26 | 51 城市 +> 最后更新: 2026-05-28 | 51 城市 ## 数据源分级 @@ -111,11 +111,30 @@ ankara, istanbul, helsinki, amsterdam, paris - **Tier 4 城市**(仅 METAR):修正效果有限,不建议依赖 +## 实时事件与图表刷新逻辑 + +当前终端图表不是固定整图轮询,而是: + +1. 首屏 / 切换城市时拉取 `/api/city/{city}/detail` 作为完整 snapshot。 +2. 可见图表连接 `/api/events?cities=...&since_revision=...&replay_limit=500`。 +3. 采集器产出 `city_observation_patch.v1` 后写入 Redis Stream(生产)或 SQLite event log(本地/兜底),再通过 SSE 推给浏览器。 +4. 前端把 patch 追加到已有实测序列,不显示 loading 遮罩;只有可见图表 2 分钟无 patch 时才启动 60 秒兜底刷新。 +5. 浏览器从后台切回前台时,前端会立即补一次 full detail,防止长时间挂页后图表落后。 + +频率取决于源头: + +- AMSC / AMOS / CoWIN / MSS:源头约 1 分钟,图表按 1 分钟粒度追加。 +- MADIS:源头约 5 分钟。 +- HKO / CWA / JMA / FMI / KNMI:源头约 10 分钟。 +- METAR-only 城市:按 METAR 可用频率和缓存 TTL,不伪装成 1 分钟实测。 + +所有图表横轴和 tooltip 时间均按城市当地时间展示,不按用户浏览器时区。 + ## 关于网站终端图表的数据曲线展示逻辑 ### 1. 实测数据(默认全开,突出核心) -- **跑道全量展示**:北京、上海、广州、成都、重庆、武汉、首尔等城市的跑道实测数据,默认全量开启,无需手动勾选。 +- **跑道全量展示**:北京、上海、广州、成都、重庆、武汉、青岛、首尔、釜山等城市的跑道实测数据,默认全量开启,无需手动勾选。 - **结算跑道高亮**:系统内置了各大机场的官方结算跑道映射。命中的跑道将被**重点强调**(加粗的青色实线 #009688,线宽 2.8),并标记为“[跑道号] 结算跑道”。具体的跑道映射如下: - 北京:19/01 - 上海:17L/35R @@ -125,15 +144,24 @@ ankara, istanbul, helsinki, amsterdam, paris - 武汉:04/22 - 青岛:16/34 - 首尔:15R/33L + - 釜山:SR/SL - **辅助跑道弱化**:同一机场下的其他非结算跑道,也会同时展示,但采用较细的虚线(线宽 1.2)以作陪衬区分。 -- **其他实测展示**:所有城市的 METAR 报文曲线、官方气象站实测(如 Hong Kong / Lau Fau Shan 的香港天文台曲线)均默认展示。 +- **单跑道机场去重**:釜山只有 `SR/SL` 跑道曲线时,不再额外展示 AMOS 聚合线,避免两条线语义重复。 +- **香港参考曲线**:Hong Kong 默认展示 CoWIN `6087`(保良局陈守仁小学)1 分钟参考站曲线;HKO 10 分钟实测作为官方气象层保留。 +- **其他实测展示**:所有城市的 METAR 报文曲线、官方气象站实测(如 Shenzhen / Lau Fau Shan 的 HKO 自动站、Taipei 的 CWA)均默认展示。 ### 2. 核心预测数据(默认展示) -- **DEB 模型融合**:作为平台核心的高精度智能融合预测曲线,默认始终展示给用户。 +- **DEB 模型融合**:作为平台核心的智能融合预测曲线,默认始终展示给用户。DEB 是预测,不参与“实测接近峰值”的视觉预警计算。 +- **DEB hourly consensus**:图表优先使用 `deb_hourly_consensus.v1` 的小时路径展示 DEB 曲线和推导“高温”窗口;如果缺失才回退旧的 hourly + DEB offset 路径。 ### 3. 多模型原始数据(默认隐藏,按需自选) - **保持整洁**:为了防止图表线缆过于杂乱,各大原始模型(ECMWF, GFS, ICON, GEM 等)的数据曲线在初次加载时**默认隐藏**。 - **特例**:仅针对巴黎(Paris),由于其 AROME HD 是高精度的 15 分钟级临近预报,极具参考价值,因此默认开启。 - **自由交互**:用户可通过图表底部的图例交互按钮,随时自由勾选、叠加或隐藏任意所需的数据曲线。 + +### 4. 高斯概率图层 + +- legacy 高斯概率不会作为时间序列曲线展示。 +- 图表上只渲染概率温度带和 `mu` 参考线,帮助用户判断当前实测距概率中心和高概率区域的关系。 diff --git a/docs/COMMERCIALIZATION.md b/docs/COMMERCIALIZATION.md index 99d89743..bf6e92c6 100644 --- a/docs/COMMERCIALIZATION.md +++ b/docs/COMMERCIALIZATION.md @@ -1,6 +1,6 @@ # 商业化说明(Production) -最后更新:`2026-04-18` +最后更新:`2026-05-28` ## 1. 定位 @@ -8,8 +8,8 @@ PolyWeather 是面向温度结算场景的气象决策层,不是通用天气 核心价值: -- 观测优先(METAR / 机场主站 / 明确官方站点;MGM、NMC、JMA、AMOS 等作为增强层) -- 结算导向(DEB + 校准概率桶) +- 观测优先(METAR / 机场主站 / 明确官方站点;MGM、JMA、AMOS、AMSC、CoWIN、CWA 等作为增强层) +- 结算导向(DEB hourly consensus + 校准概率桶) - 气象判断优先(证据链、失效条件、下一观测点) - 市场映射(行情对照 + 错价雷达),但不把交易建议放在第一层产品承诺 @@ -24,6 +24,8 @@ PolyWeather 是面向温度结算场景的气象决策层,不是通用天气 | 支付自动确认 | 已上线 | Event Loop + Confirm Loop | | 钱包绑定 | 已上线 | 浏览器钱包 + WalletConnect | | 私有频道推送 | 已上线 | 可拆分业务频道 | +| 实时终端 | 已上线 | SSE Patch + Redis Stream replay | +| Telegram 双语推送 | 已上线 | 中英文一起推送,结算跑道端点口径 | ## 3. 权限模型(当前) @@ -33,6 +35,7 @@ PolyWeather 是面向温度结算场景的气象决策层,不是通用天气 - 今日日内深度分析(含高温时段) - 专业气象结论条、证据链、失效条件、确认条件 - 概率分布层(基于 DEB 融合 + 高斯分桶) + - 实时终端多城市图表(跑道/官方站点/DEB/概率带) - 历史对账 + 未来日期分析 - 全平台智能气象推送 @@ -62,6 +65,7 @@ PolyWeather 是面向温度结算场景的气象决策层,不是通用天气 5. 安全边界:敏感配置不进仓库。 6. 数据口径:机场市场按 METAR / 机场主站解释;Wunderground 只可描述为历史页面或参考入口,不描述成“站”。 7. 加载口径:日内分析和右侧详情在 full detail 未补齐前必须显示同步状态,不能把旧缓存伪装成完整付费内容。 +8. 实时口径:终端图表应优先保留已有曲线并无痛合并 patch,不用 loading 遮罩覆盖已可用数据。 ## 7. 后续路线 diff --git a/docs/CONFIGURATION_ZH.md b/docs/CONFIGURATION_ZH.md index c327cd47..f88375f2 100644 --- a/docs/CONFIGURATION_ZH.md +++ b/docs/CONFIGURATION_ZH.md @@ -1,5 +1,7 @@ # 配置与密钥管理(中文) +最后更新:`2026-05-28` + ## 1. 目标 PolyWeather 的环境变量很多,但不是所有变量都属于同一层级。 @@ -79,6 +81,7 @@ PolyWeather 的环境变量很多,但不是所有变量都属于同一层级 - `POLYWEATHER_RUNTIME_DATA_DIR` - `POLYWEATHER_DB_PATH` - `POLYWEATHER_STATE_STORAGE_MODE` +- `POLYWEATHER_EVENT_STORE` 前端: @@ -104,6 +107,8 @@ PolyWeather 的环境变量很多,但不是所有变量都属于同一层级 - `POLYWEATHER_AUTH_REQUIRE_SUBSCRIPTION` - `POLYWEATHER_OPS_ADMIN_EMAILS` - `POLYWEATHER_STATE_STORAGE_MODE` +- `POLYWEATHER_EVENT_STORE` +- `POLYWEATHER_REDIS_REQUIRED` - `POLYWEATHER_PAYMENT_ENABLED` - `POLYGON_WALLET_WATCH_ENABLED` - `TELEGRAM_ALERT_PUSH_ENABLED` @@ -127,6 +132,9 @@ PolyWeather 的环境变量很多,但不是所有变量都属于同一层级 - `TELEGRAM_MARKET_FOCUS_DIGEST_TOP_N` - `POLYWEATHER_PAYMENT_RPC_URLS` - `TAF_CACHE_TTL_SEC` +- `POLYWEATHER_REDIS_URL` +- `POLYWEATHER_REDIS_STREAM_KEY` +- `POLYWEATHER_REDIS_STREAM_MAXLEN` 策略: @@ -215,6 +223,10 @@ TELEGRAM_CHAT_ID=... POLYWEATHER_RUNTIME_DATA_DIR=/var/lib/polyweather POLYWEATHER_DB_PATH=/var/lib/polyweather/polyweather.db POLYWEATHER_STATE_STORAGE_MODE=sqlite +POLYWEATHER_EVENT_STORE=redis +POLYWEATHER_REDIS_URL=redis://polyweather_redis:6379/0 +POLYWEATHER_REDIS_STREAM_MAXLEN=50000 +POLYWEATHER_REDIS_REQUIRED=true UID=1000 GID=1000 POLYWEATHER_AUTH_ENABLED=true @@ -245,6 +257,8 @@ POLYWEATHER_BACKEND_URL=http://polyweather_web:8000 - `POLYWEATHER_RUNTIME_DATA_DIR` 建议放在仓库外,例如 `/var/lib/polyweather`。 - `docker-compose.yml` 会把这个目录同时挂载到容器内的 `/var/lib/polyweather` 和 `/app/data`,兼容现有缓存与 SQLite 路径。 - `POLYWEATHER_STATE_STORAGE_MODE` 当前线上推荐直接使用 `sqlite`。 +- `POLYWEATHER_EVENT_STORE=redis` 表示实时观测 patch 使用 Redis Stream 做短窗口 replay 和多 worker fanout;本地或单进程可改为 `sqlite`。 +- `POLYWEATHER_REDIS_REQUIRED=true` 表示 Redis 不可用时后端启动失败,避免生产环境广播不可 replay 的实时事件;开发环境可设为 `false` 允许回退 SQLite。 - `POLYWEATHER_PAYMENT_RPC_URLS` 支持逗号分隔多个 RPC;如果暂时只用单 RPC,也可以继续只配 `POLYWEATHER_PAYMENT_RPC_URL`。 - 机器人市场监控包含 `关键提醒` 与 `关注清单`:关键提醒逐城判断并受冷却控制,关注清单每轮先扫描完整城市列表,再按全局 Top N 推送;同一轮已经触发关键提醒的城市不会重复出现在关注清单里。 - `TELEGRAM_MARKET_FOCUS_DIGEST_INTERVAL_SEC` 表示主动推送间隔,默认 `1800` 秒(30 分钟)。 diff --git a/docs/FRONTEND_DEPLOYMENT_ZH.md b/docs/FRONTEND_DEPLOYMENT_ZH.md index 8ec96d1c..677631d0 100644 --- a/docs/FRONTEND_DEPLOYMENT_ZH.md +++ b/docs/FRONTEND_DEPLOYMENT_ZH.md @@ -1,5 +1,7 @@ # 前端部署配置(Vercel) +最后更新:`2026-05-28` + 本文只覆盖 `frontend` 目录对应的 Next.js 前端部署。 ## 一、部署目标 @@ -16,6 +18,12 @@ 2. Next `/api/*` -> `POLYWEATHER_API_BASE_URL` 3. FastAPI 后端 -> 分析 / 支付 / 鉴权服务 +实时图表同样走 Next Route Handler / rewrite: + +1. 浏览器 `EventSource` -> `/api/events?cities=...&since_revision=...` +2. Next 转发到 FastAPI `/api/events` +3. FastAPI 从 Redis Stream / SQLite event log replay 后进入 live SSE + ## 二、Vercel 项目设置 在 Vercel 导入 GitHub 仓库后,使用下面的设置: @@ -193,6 +201,7 @@ Vercel 部署前至少确认: 4. 如果启用鉴权,Supabase redirect URL 已包含前端域名 5. `GET /api/payments/config` 返回的是当前最新地址,而不是旧收款合约 6. 如果启用了 `/ops`,确认 `POLYWEATHER_OPS_ADMIN_EMAILS` 已在 Vercel 与后端同时配置 +7. 确认 `/api/events` 没有被 CDN 缓存或压缩成普通 JSON;它必须保持 `text/event-stream` ## 九、常见问题 diff --git a/docs/MODEL_STACK_AND_DEB_ZH.md b/docs/MODEL_STACK_AND_DEB_ZH.md index 0b747239..95e14ac9 100644 --- a/docs/MODEL_STACK_AND_DEB_ZH.md +++ b/docs/MODEL_STACK_AND_DEB_ZH.md @@ -2,7 +2,7 @@ 本文档记录 PolyWeather 当前开放模型接入、区域覆盖差异,以及 DEB 在新增模型后的计权规则。 -最后更新:`2026-05-27` +最后更新:`2026-05-28` ## 1. 接入方式 @@ -172,7 +172,33 @@ raw current_forecasts 当 `weights_info` 出现 `家族去重`,表示当前输入模型数量多于 DEB 实际入模数量,系统已先折叠同家族模型。 -### 5.1 版本化预测与偏差校正 +### 5.1 DEB hourly consensus + +当前图表和峰值窗口优先使用 `deb_hourly_consensus.v1`。 + +入口: + +- `src/analysis/deb_hourly_consensus.py` +- `web/analysis_service.py` +- `src/analysis/trend_engine.py` + +处理逻辑: + +```text +multi_model hourly curves + -> 按 DEB 可用模型和家族去重口径选择候选 + -> 使用 DEB 权重生成逐小时 consensus + -> 输出 deb.hourly_consensus + -> 图表 DEB Forecast 与 peak-window 逻辑优先读取该路径 +``` + +关键口径: + +- `deb_hourly_consensus.v1` 是预测曲线,不是实测曲线。 +- 它只用于日内形状、峰值窗口和 DEB Forecast 展示;视觉预警和“接近峰值”状态必须优先读取实测/跑道/官方站点当前值与今日实测高点。 +- 如果多模型小时路径缺失,系统才回退旧的 `hourly_plus_deb_offset`。 + +### 5.2 版本化预测与偏差校正 DEB 原始融合逻辑不推倒重写,`calculate_dynamic_weights(...)` 仍作为 raw baseline。线上生产入口使用 `calculate_deb_prediction(...)` 包装 raw baseline,并在有足够历史样本时追加城市级 recent signed-bias correction。 @@ -213,6 +239,7 @@ python scripts/backtest_deb_versions.py \ - `multi_model` - `multi_model_daily` - `source_forecasts.open_meteo_multi_model.model_metadata` +- `deb.hourly_consensus` 显示分组: @@ -233,6 +260,13 @@ python scripts/backtest_deb_versions.py \ 区域模型不覆盖时不显示空模型。 +终端图表默认: + +- 展示“全天”视图,按城市当地日 00:00-23:59 和真实观测时间绘制。 +- 可选“高温”视图,窗口由 `deb_hourly_consensus.v1` 的峰值区域推导。 +- DEB Forecast 使用橙色预测曲线;实测/跑道/官方站点曲线独立展示,不把 DEB 当成实测。 +- legacy 高斯概率显示为水平温度带和 `mu` 参考线,不参与时间序列曲线。 + ### 6.1 “来源 Open-Meteo”是什么意思 前端中的 `来源 - Open-Meteo` 表示本次多模型数据通过 Open-Meteo model API 归一化接入。 diff --git a/docs/MONITORING_ZH.md b/docs/MONITORING_ZH.md index a94c3990..166f7fc7 100644 --- a/docs/MONITORING_ZH.md +++ b/docs/MONITORING_ZH.md @@ -1,6 +1,6 @@ # 外部监控与告警说明 -最后更新:`2026-04-18` +最后更新:`2026-05-28` ## 1. 目标 @@ -11,6 +11,7 @@ - Relay 把告警推到运营频道 - Grafana 展示趋势面板 - 巡检脚本补健康检查 +- 关注 `/api/events` 长连接与 realtime event store 是否正常 replay ## 2. 组件 @@ -89,6 +90,14 @@ POLYWEATHER_MONITORING_ALERT_CHAT_IDS= - Source Requests by Outcome - Source Error Rate (15m) +实时事件层建议额外观察: + +- Redis Stream latest revision +- Redis 连接状态 +- SQLite fallback 是否被启用 +- SSE active connection count +- `resync_required` 出现频率 + ## 7. 巡检脚本 手动巡检: @@ -102,6 +111,7 @@ python scripts/check_ops_health.py --base-url http://127.0.0.1:8000 - `/healthz` - `/api/system/status` - `/metrics` +- `/api/events`(手动验证时查看 `connected` / `heartbeat` / replay 事件) 任何一项失败都会非零退出,适合挂到 crontab 或 systemd timer。 @@ -130,6 +140,11 @@ python scripts/check_ops_health.py --base-url http://127.0.0.1:8000 - 多日预报是否只返回当天单卡 - 日内分析 full detail / market scan 是否仍在同步 - 右侧详情面板是否正在用同步占位卡提示用户 +- 实时事件状态: + - event store 类型(Redis / SQLite) + - latest revision + - Redis 是否连通 + - 是否处于 `degraded_from=redis` fallback 这意味着: @@ -180,3 +195,4 @@ npm run build - 更细粒度支付指标 - 按城市/来源拆分的业务 SLA - 按城市拆分的前端补齐耗时与 stale-detail 告警 +- Redis Stream 长度、内存与 replay gap 告警 diff --git a/docs/SERVICES_ZH.md b/docs/SERVICES_ZH.md index 77216818..46dd82fb 100644 --- a/docs/SERVICES_ZH.md +++ b/docs/SERVICES_ZH.md @@ -1,53 +1,63 @@ # 外部服务依赖总览 -最后更新:`2026-05-23` +最后更新:`2026-05-28` -项目调用了 20 个外部服务,按状态分为三类。 +项目调用外部天气、鉴权、支付和实时事件服务。原则是:核心链路必须有明确健康检查;可选数据源不可拖垮已可用城市;实时事件层可从 Redis Stream 降级到 SQLite event log。 ## 核心(必须有,挂了服务不可用) -| 服务 | 用途 | 状态 | -| ---------------------- | --------------------- | ---- | -| Open-Meteo | 52 城天气预报 | ✅ | -| AviationWeather (NOAA) | METAR/TAF 航空观测 | ✅ | -| MADIS (NOAA) | 美国 5 分钟高频观测 | ✅ | -| Supabase | 用户认证 + 订阅 | ✅ | -| Telegram Bot API | Bot 消息 + 群成员检查 | ✅ | -| KNMI | Amsterdam 10 分钟观测 | ✅ |## 国家气象源(特定城市必须) +| 服务 | 用途 | 状态 | +| --- | --- | --- | +| Open-Meteo | 51 城天气预报、多模型小时曲线、DEB hourly consensus 输入 | ✅ | +| AviationWeather (NOAA) | METAR / TAF 航空观测 | ✅ | +| MADIS (NOAA) | 美国机场 5 分钟高频观测 | ✅ | +| Supabase | 用户认证、订阅状态、会员恢复 | ✅ | +| Telegram Bot API | Bot 消息、群成员检查、双语跑道推送 | ✅ | +| Redis | `city_observation_patch.v1` Stream、SSE replay、多 worker fanout | ✅ | +| SQLite | 运行态数据库、支付审计、实时事件 fallback | ✅ | -| 服务 | 城市 | 状态 | -| -------------------- | --------------------- | ----------- | -| JMA (日本) | Tokyo | ✅ | -| KMA + AMOS (韩国) | Seoul, Busan | ✅ | -| AMSC AWOS (中国) | 北京/上海/广州等 6 城 | ✅ | -| MGM (土耳其) | Ankara, Istanbul | ✅ | -| FMI (芬兰) | Helsinki | ✅ | -| HKO (香港) | Hong Kong | ✅ | -| CWA (台湾) | Taipei | ✅ | -| NMC (中国) | 国内城市 fallback | ✅ | -| Singapore MSS | Singapore | ✅ | -| IMGW (波兰) | Warsaw | ⚠️ 未配 key | -| Russia pogodaiklimat | Moscow | ❌ 已移除 | +## 国家气象源(特定城市必须) + +| 服务 | 城市 | 状态 | +| --- | --- | --- | +| JMA AMeDAS | Tokyo | ✅ | +| AMOS (韩国) | Seoul, Busan 跑道传感器 | ✅ | +| AMSC AWOS (中国) | 北京、上海、广州、成都、重庆、武汉、青岛跑道端点气温 | ✅ | +| MGM (土耳其) | Ankara, Istanbul | ✅ | +| FMI (芬兰) | Helsinki | ✅ | +| KNMI (荷兰) | Amsterdam | ✅(需 key) | +| CoWIN 6087 (香港) | Hong Kong 1 分钟参考站 | ✅ | +| HKO (香港) | Hong Kong / Shenzhen / Lau Fau Shan 10 分钟官方气象层 | ✅ | +| CWA (台湾) | Taipei | ✅ | +| Singapore MSS | Singapore | ✅ | +| IMS Lod (以色列) | Tel Aviv | ✅ | +| AEROWEB / AROME HD | Paris | ✅ | +| NMC (中国) | 国内城市 fallback | ✅ | +| IMGW (波兰) | Warsaw | ⚠️ 未配 key | ## 可选 / 已禁用 -| 服务 | 用途 | 状态 | -| -------------- | ------------- | ----------- | +| 服务 | 用途 | 状态 | +| --- | --- | --- | | OpenWeatherMap | 天气 fallback | ⚠️ 未配 key | -| VisualCrossing | 历史天气 | ⚠️ 未配 key | -| Meteoblue | 天气预报 | ❌ 已移除 | -| SynopticData | 美国站点观测 | ⚠️ 未配 key | +| VisualCrossing | 历史天气 | ⚠️ 未配 key | +| SynopticData | 美国站点观测 | ⚠️ 未配 key | +| Meteoblue | 天气预报 | ❌ 已移除 | +| Russia pogodaiklimat | Moscow 历史源 | ❌ 已移除 | +| Groq | AI commentary | ❌ 已移除 | -## AI / 其他 +## AI / 支付 / 前端 -| 服务 | 用途 | 状态 | -| ----------------- | ---------------- | ----------- | +| 服务 | 用途 | 状态 | +| --- | --- | --- | | MiMo (xiaomimimo) | 城市分析 AI 评论 | ✅ 当前使用 | -| DeepSeek | AI fallback | - 备用 | -| Groq | AI commentary | ❌ 已移除 | -| Polygon RPC | 链上支付 | ✅ | -| WalletConnect | 前端钱包连接 | ⚠️ 未配 key | +| DeepSeek | AI fallback | 备用 | +| Polygon RPC | 链上支付、自动确认 | ✅ | +| WalletConnect | 前端钱包连接 | ⚠️ 未配 key 时钱包入口降级 | -## 合计 +## 运维口径 -15 个在用,3 个可选/未配置,3 个已移除。 +- 生产实时事件推荐:`POLYWEATHER_EVENT_STORE=redis` + `POLYWEATHER_REDIS_URL=redis://polyweather_redis:6379/0`。 +- 本地或单进程兜底:`POLYWEATHER_EVENT_STORE=sqlite`。 +- Redis 只负责短窗口 replay 与多 worker fanout,不是长期天气历史库。 +- DEB hourly consensus 依赖 Open-Meteo 多模型小时曲线;若上游限流,图表应保留已有 snapshot 和实测 patch,不把缺失模型误报为实测缺失。 diff --git a/docs/SUPABASE_SETUP_ZH.md b/docs/SUPABASE_SETUP_ZH.md index c90db4f2..f96d6d9a 100644 --- a/docs/SUPABASE_SETUP_ZH.md +++ b/docs/SUPABASE_SETUP_ZH.md @@ -1,4 +1,4 @@ -# Supabase + 登录 + 支付接入说明(v1.8.0) +# Supabase + 登录 + 支付接入说明(v1.8.1) 最后更新:`2026-03-14` diff --git a/docs/TECH_DEBT_ZH.md b/docs/TECH_DEBT_ZH.md index 257973b6..e3f6354b 100644 --- a/docs/TECH_DEBT_ZH.md +++ b/docs/TECH_DEBT_ZH.md @@ -1,4 +1,4 @@ -# 技术债与工程待办(v1.8.0) +# 技术债与工程待办(v1.8.1) 最后更新:`2026-05-10` diff --git a/docs/data-architecture-review.md b/docs/data-architecture-review.md index 595271c1..e4c1f349 100644 --- a/docs/data-architecture-review.md +++ b/docs/data-architecture-review.md @@ -2,7 +2,7 @@ > 审查日期:2026-06 | 视角:系统架构师 | 范围:完整数据采集→分析→API→前端状态 > -> **修复状态:8/8 已完成** +> **修复状态:9/9 已完成;最后校准:2026-05-28** ## 一、数据架构总览 @@ -13,9 +13,8 @@ Open-Meteo (预报+多模型) ─┐ METAR/TAF (航空气象) ─┤ NWS (美国) / MGM (土耳其) ─┤ -JMA/AMOS/NMC/HKO/CWA ─┤ -Wunderground / NOAA ─┤ -Polymarket Gamma/CLOB ─┤ +JMA/AMOS/AMSC/HKO/CWA ─┤ +CoWIN / MADIS / NOAA ─┤ ├─ WeatherDataCollector ├─ dashboard-client.ts │ (内存缓存 + SQLite磁盘缓存) │ (ETag浏览器缓存 + SWR) │ │ @@ -27,8 +26,11 @@ Polymarket Gamma/CLOB ─┤ │ ├─ ThreadPoolExecutor(4) │ (120s TTL) │ └─ AI 增强层 (DeepSeek) │ │ │ - └─ FastAPI routes └─ API代理 (Next.js rewrites) - (36个端点 + ETag 304) + ├─ FastAPI routes └─ API代理 (Next.js rewrites) + │ (HTTP snapshot + ETag 304) + │ + └─ SSE /api/events + (Redis Stream / SQLite replay) ``` ## 二、数据采集层 @@ -43,10 +45,8 @@ Polymarket Gamma/CLOB ─┤ | NWS | 国家预报 | 美国 | 按请求 | | MGM | 国家官方 | 土耳其 | 300s | | ECMWF/GFS/ICON/GEM/JMA | 多模型 NWP | 全球 | 300s | -| HKO/CWA/NOAA/AMOS/NMC | 结算观测 | 特定国家 | 60s (AMOS) / 300s | -| Wunderground | 个人气象站 | 全球备用 | 按请求 | -| Polymarket Gamma | 市场发现 | 所有温度市场 | 60s | -| Polymarket CLOB | 订单簿 | 匹配市场 | 30s | +| HKO/CWA/NOAA/AMOS/AMSC/CoWIN | 结算/参考观测 | 特定国家 | 60s-600s | +| Redis Stream | 实时事件 replay / 多 worker fanout | 后端内部 | event-driven | ### 待改进 @@ -78,7 +78,7 @@ Polymarket Gamma/CLOB ─┤ **已修复:TTL 匹配** — `SCAN_TERMINAL_PAYLOAD_TTL_SEC` 30s → 120s,匹配 ThreadPoolExecutor(4)×60 城的实际重算耗时。 -**已实现:SSE 增量推送(SSE Patch)与按需刷新** — 引入 FastAPI SSE 广播通道 (`/api/events`)。数据采集端更新时自动向 `/api/internal/collector-patch` 推送最新温度;前端扫描终端订阅该流,不再执行固定的 5 分钟定时轮询,而是根据 Patch 变化即时更新列表;当前选中图表基于 `useLatestPatch` 实现 1 分钟级温度的增量合并与实时曲线绘制。 +**已实现:SSE 增量推送(SSE Patch)与可重放事件日志** — 引入 FastAPI SSE 广播通道 (`/api/events`)。数据采集端更新时自动向 `/api/internal/collector-patch` 推送最新温度;后端标准化为 `city_observation_patch.v1`,写入 Redis Stream(生产)或 SQLite event log(兜底)后再广播。前端扫描终端订阅该流,不再执行固定的 5 分钟定时轮询,而是根据 Patch 变化即时更新列表;当前选中图表基于 `useLatestPatch` 实现 1 分钟级温度的增量合并与实时曲线绘制。 | # | 问题 | 优先级 | |---|------|------| @@ -94,7 +94,7 @@ Polymarket Gamma/CLOB ─┤ **已修复:扫描数据复用** — `preloadCityFromRow()` 从扫描终端行预填充城市详情缓存,选城市后详情面板立即显示。 -**已实现:SSE 订阅与 2 分钟无 Patch 兜底机制** — 引入 `useLatestPatch` 与 `useSsePatchVersion` 管理实况数据的准实时合并。若长连接中断或 2 分钟内未收到任何增量 Patch,可见图表自动触发 60s 降级轮询(从 `/api/city/{city}/summary` 获取最新实况,并以 ignoreCache 强刷 full detail)。 +**已实现:SSE 订阅、replay 与 2 分钟无 Patch 兜底机制** — 引入 `useLatestPatch` 与 `useSsePatchVersion` 管理实况数据的准实时合并。若长连接中断,前端使用 `since_revision` replay 缺失事件;若 2 分钟内未收到任何增量 Patch,可见图表自动触发 60s 降级轮询(从 `/api/city/{city}/summary` 获取最新实况,并以 ignoreCache 强刷 full detail)。浏览器后台恢复时会主动刷新可见图表 full detail。 ## 六、待办 diff --git a/docs/deep-research-report.md b/docs/deep-research-report.md index 4d495c98..8385a5ec 100644 --- a/docs/deep-research-report.md +++ b/docs/deep-research-report.md @@ -4,22 +4,22 @@ PolyWeather(仓库:`yangyuan-zhen/PolyWeather`)定位为**面向温度类结算预测市场(如 Polymarket 的温度结算合约)**的”生产级气象情报系统”,核心在于把多源天气观测/预报转化为**结算导向的概率桶(μ + bucket distribution)**;同时提供 Web 仪表盘与 Telegram Bot 两套交互入口,并包含 Polygon 链上 USDC/USDC.e 支付、自动补单与订阅/积分体系。项目 README 现明确仓库代码采用 `AGPL-3.0-only`,同时将品牌、商标、生产私有数据与运营阈值保留在代码许可证之外。 -> **2026-05-23 更新(v1.7.0)**:Polymarket 价格拉取与 UI 层(MarketDecisionLine)已删除,`market_scan` 当前返回空;LGBM 已完全移除,概率引擎仅保留 legacy 高斯 + EMOS/CRPS;Groq、Meteoblue、NMC、pogodaiklimat 数据源和 prewarm 预热系统已移除。 +> **2026-05-28 更新(v1.8.1)**:终端图表已切换为 HTTP snapshot + SSE Patch + Redis Stream/SQLite replay 架构;DEB hourly consensus (`deb_hourly_consensus.v1`) 成为峰值窗口和 DEB 曲线展示的优先小时路径;README 产品截图迁移到 `frontend/public/static`。 ## 项目概览 PolyWeather 的目标与范围在 README/README_ZH 中定义得较清楚:为温度结算市场提供气象情报(多源采集→融合→概率→对照市场报价),并提供“官方看板(Vercel 前端)+ VPS 后端 + Telegram Bot”。 项目主功能可归纳为五层: -**天气层(数据源/采集)**:聚合 51 个城市的实测与预报;支持 AviationWeather METAR(机场观测)、韩国 AMOS 跑道级观测(首尔/釜山)、土耳其 MGM 站网、Open-Meteo(含多模型与集合预报)、美国 NWS(仅美国城市)、以及部分城市使用明确官方站点或历史页面入口(香港 HKO、台湾/深圳相关历史页面等)等。机场类市场仍以 METAR / 机场主站为结算锚点,Wunderground 不描述为物理观测站。 +**天气层(数据源/采集)**:聚合 51 个城市的实测与预报;支持 AviationWeather METAR/TAF、韩国 AMOS 跑道级观测(首尔/釜山)、中国 AMSC AWOS 跑道端点气温、香港 CoWIN 6087 / HKO、土耳其 MGM、Open-Meteo 多模型与集合预报、美国 MADIS HFMETAR 等。机场类市场仍以 METAR / 机场主站或明确官方结算源为锚点,Wunderground 不描述为物理观测站。 **分析层(DEB/趋势/概率/结算口径)**: DEB(Dynamic Error Balancing)基于过去 N 天模型误差(MAE)倒数加权,输出融合预报;运行态仍维护近 14 天 `daily_records` 缓存做当前对账,但长期监督真值与训练特征已经迁到 SQLite 永久表中,并支持基于 WU(Weather Underground 口径)四舍五入的结算命中评估。 趋势/概率引擎在 `trend_engine.py` 中实现:综合“集合预报区间→σ/μ→高温窗口→死盘判定→温度桶概率分布→边界提示”等,用于 bot 展示与 web 结构化数据输出。 -**城市决策层(Scan Terminal / 结构化实况层)**:地图点击城市后加入城市决策卡,前端拉取 full detail、多模型区间、最新 METAR,并通过 `/api/city/{name}/detail` 生成城市级 结构化解读。该解读由 `structured_signal`、`structured_signal`、`reasoning`、`structured_signal`、`risks` 与原始 METAR 证据组成;最高温中枢优先使用 DEB,再回退到 DEB、多模型中心、日内 pace 或当前实测。 +**城市决策层(Scan Terminal / 结构化实况层)**:地图点击城市后加入城市决策卡,前端拉取 full detail、多模型区间、最新 METAR/官方站点/跑道观测,并通过 `/api/city/{name}/detail` 生成城市级结构化数据。终端图表使用 HTTP snapshot + SSE patch + Redis/SQLite replay,最高温中枢优先使用 DEB hourly consensus,再结合多模型中心、日内 pace 和当前实测。 **市场层(Polymarket 行情对照)**:*[v1.7.0 已移除]* 原先从 Gamma API 发现市场、从 CLOB 读取价格/盘口并计算”模型-市场差”,已于 2026-05-23 随 Polymarket 价格拉取层一并删除。当前 `market_scan` 返回空。 **商业化与支付**:订阅(`Pro Monthly 10 USDC`)、积分抵扣、Polygon 链上收款合约(USDC/USDC.e),并提供“事件监听 + 周期确认”的自动补单机制。 **支持的数据集/数据源**:项目不是传统“训练数据集+模型训练”的机器学习仓库;其“数据集”本质是外部实时/预报 API 与站点观测数据。对外部数据的使用需要遵守来源方的访问与速率限制,例如 AviationWeather Data API 明确限制请求频率(含每分钟请求上限/建议降低频率与使用缓存文件)。 **许可证**:仓库根目录 `LICENSE` 当前为 `AGPL-3.0-only`。同时 README 与策略文档明确:品牌、商标、生产私有数据与运营策略不随代码许可证一并授权。 -(插图:项目 README 中包含产品截图,可用于快速理解信息架构与 UI 形态) -![PolyWeather demo map](https://raw.githubusercontent.com/yangyuan-zhen/PolyWeather/main/docs/images/demo_map.png) +(插图:项目 README 中包含产品截图,可用于快速理解实时终端与 Telegram 推送形态) +![PolyWeather realtime terminal](https://raw.githubusercontent.com/yangyuan-zhen/PolyWeather/main/frontend/public/static/web.png) ## 架构与代码库分析 @@ -45,7 +45,7 @@ DEB(Dynamic Error Balancing)基于过去 N 天模型误差(MAE)倒数加 flowchart TB subgraph Clients WEB[Next.js Frontend
Vercel] - CITYCARD[Scan Terminal City Cards
AI airport read + bucket mapping] + CITYCARD[Scan Terminal City Cards
structured observations + probability mapping] TG[Telegram Bot
TeleBot + Handlers] end @@ -127,7 +127,7 @@ Scan Terminal 的城市决策卡现在承担“从天气分析到市场动作解 **Python 依赖**:`requirements.txt` 包含 `requests`、`loguru`、`pyTelegramBotAPI`、`python-dotenv`、`numpy`、`web3`、`fastapi`、`uvicorn` 等,符合“采集+bot+api+链上交互”的需求。 **容器环境**:`Dockerfile` 基于 `python:3.11-slim`,默认启动 bot;`docker-compose.yml` 通过不同 command 分别启动 bot 与 web(`python bot_listener.py` / `python web/app.py`),并挂载运行态数据目录。 -**前端依赖**:前端 README 描述 Next.js、Leaflet、Chart.js、Supabase Auth、WalletConnect 等;`frontend/package.json` 是前端依赖来源。 +**前端依赖**:前端 README 描述 Next.js、Leaflet、Recharts、Supabase Auth、WalletConnect 等;`frontend/package.json` 是前端依赖来源。 ### 数据预处理、模型与“训练/推理”管线 本项目的“模型”主要是统计融合与规则/启发式引擎,而非深度网络训练: @@ -181,7 +181,7 @@ Web/Telegram 请求 → FastAPI 调用采集器抓取/复用缓存 → 分析引 **对标结论**:PolyWeather 与这类“全球神经天气模型”不在同一层级:PolyWeather 是“面向结算市场的产品化情报系统”,其价值核心是**将预测转成可交易/可结算的决策信息**。短中期内更高 ROI 的方向不是“自训大模型”,而是把现有“采集+后处理+市场映射”的链路做成**可复现、可观测、可评测、可扩展**的工程平台;在许可合规前提下,再评估引入外部模型推理作为额外信号源。 ## 优先级改进建议 -下表按截至 `2026-05-23` 的真实状态重排优先级。已完成项不再继续列为”待做”,只保留当前仍需推进的事项。 +下表按截至 `2026-05-28` 的真实状态重排优先级。已完成项不再继续列为”待做”,只保留当前仍需推进的事项。 | 优先级 | 改进项 | 预估工作量 | 主要收益 | 主要风险 | 可执行步骤(建议顺序) | | ------ | --------------------------------------------------------------------------------------------------------------------------------- | -------------------: | ------------------------------------------------------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 中 | **把最小外部监控继续补深**:从“可告警”提升到“可运营” | 3–7 天 | 不再只知道服务坏没坏,还能看资源趋势、来源 SLA 和支付波动 | 指标过多会带来维护噪音 | 1) 增加节点 CPU/内存/磁盘 → 2) 增加 SQLite/支付体积与事件趋势 → 3) 把 HTTP/来源指标细分到城市/来源维度 → 4) 增加日报或异常摘要 | @@ -270,5 +270,3 @@ PolyWeather 的评测应围绕“结算场景”而非传统数值天气预报 - Pangu-Weather(权重 BY-NC-SA,禁商用):https://github.com/198808xc/Pangu-Weather - Polymarket 官方 Python CLOB SDK(MIT):https://github.com/Polymarket/py-clob-client - aiopolymarket(async、类型安全):https://github.com/the-odds-company/aiopolymarket - - diff --git a/docs/images/demo_ankara.png b/docs/images/demo_ankara.png deleted file mode 100644 index 7e12554e985f45ea0cb6939911207793f588bfb5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 267698 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