This version of Antigravity is no longer supported. Please upgrade to receive the latest features.

This commit is contained in:
2569718930@qq.com
2026-04-29 18:52:55 +08:00
parent 1ccc275a23
commit 5b32e71f5d
8 changed files with 133 additions and 60 deletions
+12 -5
View File
@@ -136,12 +136,19 @@ POLYWEATHER_GROQ_COMMENTARY_MODEL=openai/gpt-oss-20b
POLYWEATHER_GROQ_COMMENTARY_TIMEOUT_SEC=8
POLYWEATHER_GROQ_COMMENTARY_CACHE_TTL_SEC=1800
# Optional DeepSeek V4-Pro market scan review for Pro users
# Optional OpenAI-compatible market scan review for Pro users
# Temporary default provider: MiMo via https://token-plan-cn.xiaomimimo.com/v1.
POLYWEATHER_SCAN_AI_ENABLED=false
POLYWEATHER_DEEPSEEK_API_KEY=
POLYWEATHER_DEEPSEEK_BASE_URL=https://api.deepseek.com
POLYWEATHER_SCAN_AI_MODEL=deepseek-v4-flash
POLYWEATHER_SCAN_CITY_AI_MODEL=deepseek-v4-flash
POLYWEATHER_SCAN_AI_API_KEY=
POLYWEATHER_SCAN_AI_PROVIDER=mimo
POLYWEATHER_SCAN_AI_PROVIDER_LABEL=MiMo
POLYWEATHER_SCAN_AI_BASE_URL=https://token-plan-cn.xiaomimimo.com/v1
POLYWEATHER_SCAN_AI_MODEL=MiMo-V2.5-Pro
POLYWEATHER_SCAN_CITY_AI_MODEL=MiMo-V2.5-Pro
# Backward-compatible legacy DeepSeek variables are still read if the generic
# POLYWEATHER_SCAN_AI_* variables are unset.
# POLYWEATHER_DEEPSEEK_API_KEY=
# POLYWEATHER_DEEPSEEK_BASE_URL=https://api.deepseek.com
POLYWEATHER_SCAN_AI_TIMEOUT_SEC=18
POLYWEATHER_SCAN_CITY_AI_TIMEOUT_SEC=30
POLYWEATHER_SCAN_CITY_AI_RETRY_ON_STREAM_PARSE_ERROR=false
+8
View File
@@ -270,6 +270,13 @@ POLYWEATHER_GROQ_COMMENTARY_ENABLED=false
POLYWEATHER_GROQ_COMMENTARY_MODEL=openai/gpt-oss-20b
POLYWEATHER_GROQ_COMMENTARY_TIMEOUT_SEC=8
POLYWEATHER_GROQ_COMMENTARY_CACHE_TTL_SEC=1800
POLYWEATHER_SCAN_AI_ENABLED=false
POLYWEATHER_SCAN_AI_API_KEY=...
POLYWEATHER_SCAN_AI_PROVIDER=mimo
POLYWEATHER_SCAN_AI_PROVIDER_LABEL=MiMo
POLYWEATHER_SCAN_AI_BASE_URL=https://token-plan-cn.xiaomimimo.com/v1
POLYWEATHER_SCAN_AI_MODEL=MiMo-V2.5-Pro
POLYWEATHER_SCAN_CITY_AI_MODEL=MiMo-V2.5-Pro
```
说明:
@@ -281,6 +288,7 @@ POLYWEATHER_GROQ_COMMENTARY_CACHE_TTL_SEC=1800
- `POLYWEATHER_STATE_STORAGE_MODE` 当前线上推荐直接使用 `sqlite`
- `POLYWEATHER_PAYMENT_RPC_URLS` 支持逗号分隔多个 RPC;如果暂时只用单 RPC,也可以继续只配 `POLYWEATHER_PAYMENT_RPC_URL`
- 机器人市场监控当前以 `关注清单` 为主,按固定间隔主动推送。
- `POLYWEATHER_SCAN_AI_*` 走 OpenAI-compatible `/chat/completions`;当前临时默认 MiMo,可用 `POLYWEATHER_SCAN_AI_BASE_URL``POLYWEATHER_SCAN_AI_MODEL` 随时切回其他兼容 provider。
- `TELEGRAM_MARKET_FOCUS_DIGEST_INTERVAL_SEC` 表示主动推送间隔,默认 `1800` 秒(30 分钟)。
- `POLYMARKET_WALLET_ACTIVITY_ENABLED` 已退役,保留为 `false` 即可,不建议再启用钱包异动监听。
- `POLYWEATHER_DASHBOARD_PREWARM_ENABLED=true` 时,建议同时启用独立 worker 或 bot 内嵌预热线程。
+4 -4
View File
@@ -54,7 +54,7 @@ flowchart TB
subgraph API
FAST[FastAPI<br/>web/app.py]
LLM[City AI stream<br/>DeepSeek-compatible provider]
LLM[City AI stream<br/>OpenAI-compatible provider]
end
subgraph Data
@@ -124,7 +124,7 @@ Scan Terminal 的城市决策卡现在承担“从天气分析到市场动作解
1. **地图点击与 pinned city**:免费/付费入口都会先把城市加入决策卡;未付费用户若权限不足,仍应保留卡片承载升级/限制提示,而不是点击后无反馈。
2. **full detail hydration**:卡片请求城市 full detail,拿到 DEB、当前/历史实测、多模型区间与最新 METAR。现阶段 detail hydration 仍偏保守串行,优先保障后端数据源稳定;真正消耗 LLM 的 AI 解读另行限流。
3. **AI 机场报文解读**:前端最多同时保留 2 条城市 AI stream,第三个及以后城市会进入队列并展示排队提示,避免多个 DeepSeek stream 同时竞争导致第三城/第四城解析失败。后端城市 AI 配置建议为 `POLYWEATHER_SCAN_CITY_AI_MODEL=deepseek-v4-flash``POLYWEATHER_SCAN_CITY_AI_TIMEOUT_SEC=30``POLYWEATHER_SCAN_CITY_AI_MAX_TOKENS=900``POLYWEATHER_SCAN_CITY_AI_RETRY_ON_STREAM_PARSE_ERROR=true`
3. **AI 机场报文解读**:前端最多同时保留 2 条城市 AI stream,第三个及以后城市会进入队列并展示排队提示,避免多个 provider stream 同时竞争导致第三城/第四城解析失败。当前临时使用 MiMo`POLYWEATHER_SCAN_AI_BASE_URL=https://token-plan-cn.xiaomimimo.com/v1``POLYWEATHER_SCAN_CITY_AI_MODEL=MiMo-V2.5-Pro`;其他后端城市 AI 配置建议为 `POLYWEATHER_SCAN_CITY_AI_TIMEOUT_SEC=30``POLYWEATHER_SCAN_CITY_AI_MAX_TOKENS=900``POLYWEATHER_SCAN_CITY_AI_RETRY_ON_STREAM_PARSE_ERROR=true`
4. **缓存策略**:页面内存缓存保留 loading/stream/final 状态,切换选项卡返回时不应空白重拉;localStorage 持久化最终成功、非 degraded 的 payload;后端 city AI cache key 已移除当前 `local_time` 干扰,主要按城市、日期与 METAR signature 失效。
5. **市场桶匹配**:城市市场扫描必须使用 full `all_buckets`,按温度 exact/range/“or higher”/“or lower” 方向严格匹配;不再用宽松 ±8°C fallback,以避免拿到 16°C 之类错误桶。前端展示统一使用“模型-市场差”,即 `model_probability - market_implied_probability`,并修复温度单位重复渲染(如 `31°°C`)。
@@ -166,7 +166,7 @@ Web/Telegram 请求 → FastAPI 调用采集器抓取/复用缓存 → 分析引
**运行态状态/缓存与核心离线链路的 SQLite 收口已完成**`daily_records``telegram_alert_state``probability_training_snapshots``open_meteo` 缓存已经支持并在生产中主读 SQLite,迁移/校验脚本可用;进一步地,在临时移除 `data/*.json` / `data/*.jsonl` 后,训练集导出、概率拟合、评估报告、shadow report 和关键 backfill 脚本已验证仍可运行。当前 legacy 文件路径主要是显式回退入口,而不再是默认主输入。
**历史真值治理已从设计缺陷修复到可追溯运行**`daily_records` 继续作为近 14 天运行态缓存,但已经不再承担长期监督真值职责;项目新增了永久真值表、真值 revision 审计表和长期训练特征表,并为 `Taipei` / `Shenzhen` 补上了历史页面回填链路。当前风险已不再是“监督真值会不会继续被 14 天裁剪吞掉”,而是“历史长期特征能否持续积累到足够支撑 EMOS/LGBM 重新评估”。
**第三方服务合规与稳定性风险**
项目强依赖外部 APIOpen-Meteo、AviationWeather、NWS、HKO、CWA、Polymarket、Supabase)以及城市 AI providerDeepSeek-compatible stream)。其中 AviationWeather Data API 有明确速率限制;Polymarket 官方说明 Gamma/Data/CLOB 三套 API 分属不同域,CLOB 交易端点需鉴权且策略可能变化;Supabase 明确强调 `service_role`/secret keys 绝不可暴露。若缺乏集中治理(重试/退避/熔断/降级/配额监控/密钥轮换),稳定性与合规不可控。城市 AI 解读已经通过前端 2 并发队列、30s timeout、stream parse retry 与缓存 key 稳定化降低第三/第四城市失败概率,但仍需持续记录 stream duration、cache hit、retry、degraded 与 queue depth。
项目强依赖外部 APIOpen-Meteo、AviationWeather、NWS、HKO、CWA、Polymarket、Supabase)以及城市 AI providerOpenAI-compatible stream,当前临时 MiMo)。其中 AviationWeather Data API 有明确速率限制;Polymarket 官方说明 Gamma/Data/CLOB 三套 API 分属不同域,CLOB 交易端点需鉴权且策略可能变化;Supabase 明确强调 `service_role`/secret keys 绝不可暴露。若缺乏集中治理(重试/退避/熔断/降级/配额监控/密钥轮换),稳定性与合规不可控。城市 AI 解读已经通过前端 2 并发队列、30s timeout、stream parse retry 与缓存 key 稳定化降低第三/第四城市失败概率,但仍需持续记录 stream duration、cache hit、retry、degraded 与 queue depth。
**可观测性最小闭环已完成,但监控深度仍待加强**:项目现在已有 `/healthz``/api/system/status``/metrics`,并已补齐 Prometheus 抓取、Alertmanager 规则、Grafana 面板、Telegram relay 与巡检脚本。与此同时,`/ops` 已经逐步演进为后台管理台而不只是状态页:除支付、会员、用户与 EMOS 门禁外,还新增了训练数据治理卡片、城市覆盖矩阵,以及 `/ops/truth-history` 这种可直接查询 `actual_high / settlement_source / station_code / truth_version / updated_by / updated_at` 的真值表浏览页。当前缺口不再是“有没有外部监控”,而是节点级资源、数据库体积趋势、更细粒度支付指标、按城市/来源拆分的业务 SLA,以及是否需要进一步补 `truth revision` 明细页、趋势图和运营日报。
**EMOS 已完成工程接入,但未完成生产发布**EMOS/CRPS 校准、shadow 观测、rollout report、上线门禁都已实现;当前真实门禁结果为 `hold`,阻塞原因是 shadow bucket brier 明显退化。因此概率引擎标准化并非未做,而是“工程完成、发布未通过”。
**许可证/商业使用的潜在冲突点**:仓库自身现为 `AGPL-3.0-only`,但如果未来尝试引入外部神经天气模型,仍需单独核验第三方代码与权重的商用条件:GraphCast 仓库代码 Apache-2.0,但权重使用 CC BY-NC-SA 4.0(非商业),Pangu-Weather 权重同样 BY-NC-SA 且明确禁止商业用途;不加区分地把这些模型用于付费产品会留下法律风险。
@@ -263,7 +263,7 @@ PolyWeather 的评测应围绕“结算场景”而非传统数值天气预报
### 主要风险与缓解策略
**外部 API / AI provider 速率限制与格式变更**AviationWeather 明确 rate limit 与建议使用 cache 文件;Open-Meteo 也可能在不同端点策略上变化;DeepSeek-compatible city AI stream 可能出现 timeout、stream JSON 截断或并发竞争。缓解:统一“请求预算”与退避/熔断;关键响应做 schema 校验与回放测试;对高频数据优先拉取官方 cache/批量接口(若可用);城市 AI 保持小并发队列、30s timeout、stream parse retry、页面内存缓存与 degraded fallback。
**外部 API / AI provider 速率限制与格式变更**AviationWeather 明确 rate limit 与建议使用 cache 文件;Open-Meteo 也可能在不同端点策略上变化;OpenAI-compatible city AI stream 可能出现 timeout、stream JSON 截断或并发竞争。缓解:统一“请求预算”与退避/熔断;关键响应做 schema 校验与回放测试;对高频数据优先拉取官方 cache/批量接口(若可用);城市 AI 保持小并发队列、30s timeout、stream parse retry、页面内存缓存与 degraded fallback。
**密钥泄露与权限滥用**Supabase 明确强调 `service_role` 属高权限密钥,绝不可出现在前端或公开环境。缓解:密钥分级、CI secret scan、运行时最小权限、日志脱敏。
**支付链路最终一致性与链上不确定性**:链上事件索引延迟、RPC 不稳定、交易确认数不足都会导致误判。当前项目已经补齐“事件监听 + 确认补单”双路径、事件重放脚本、SQLite 审计事件与多 RPC fallback;现阶段的主要剩余风险不再是“没有防护”,而是链上合约仍为最小实现,owner 为单地址管理,且没有 pause 开关与 SafeERC20。
**引入外部神经天气模型的商业合规风险**GraphCast/Pangu-Weather 的权重许可均带非商业限制(CC BY-NC-SA/BY-NC-SA);若 PolyWeather 是付费产品,必须先做法务与授权评审。缓解:只在研究环境评估;商用优先选择可商用权重/购买授权/自研。
+6 -2
View File
@@ -181,11 +181,12 @@ def test_merge_scan_ai_result_applies_city_forecast_and_metadata():
merged = merge_scan_ai_result(
payload,
ai_raw,
model="deepseek-v4-flash",
model="MiMo-V2.5-Pro",
max_rows=40,
timeout_sec=40,
cache_ttl_sec=1800,
base_url="https://api.deepseek.com",
base_url="https://token-plan-cn.xiaomimimo.com/v1",
provider="mimo",
duration_ms=123,
input_rows=1,
)
@@ -197,3 +198,6 @@ def test_merge_scan_ai_result_applies_city_forecast_and_metadata():
assert merged["ai_scan"]["sent_cities"] == 1
assert merged["ai_scan"]["sent_rows"] == 1
assert merged["ai_scan"]["duration_ms"] == 123
assert merged["ai_scan"]["model"] == "MiMo-V2.5-Pro"
assert merged["ai_scan"]["provider"] == "mimo"
assert merged["ai_scan"]["base_url"] == "https://token-plan-cn.xiaomimimo.com/v1"
+1 -1
View File
@@ -41,7 +41,7 @@ def build_city_ai_request_json(
observation_label_zh = context["read_label_zh"]
observation_instruction = context["instruction_zh"]
system_prompt = (
"你是 PolyWeather 的 DeepSeek 城市最高温预测员。你必须直接阅读用户给出的城市 JSON,"
"你是 PolyWeather 的城市最高温 AI 预测员。你必须直接阅读用户给出的城市 JSON,"
"判断该城市今日最高温路径。不要写套利、交易、BUY YES/NO、价格、edge 或 Kelly。"
f"你的核心输出是:最终最高温点估计、置信区间、置信度、最终判断、{observation_label_zh}、判断依据和风险。"
"必须综合 DEB 最终融合值、全部天气模型预测、实测序列、最新观测、峰值窗口、当地时间、季节背景。"
+1 -1
View File
@@ -35,7 +35,7 @@ def _call_deepseek_city_ai(
timeout_sec: int,
) -> Dict[str, Any]:
if not api_key:
raise RuntimeError("POLYWEATHER_DEEPSEEK_API_KEY is not configured")
raise RuntimeError("scan AI API key is not configured")
normalized_locale = _normalize_locale(locale)
timeout = httpx.Timeout(
timeout=float(timeout_sec),
+2 -1
View File
@@ -84,6 +84,7 @@ def merge_scan_ai_result(
cache_ttl_sec: int,
base_url: str,
cached: bool = False,
provider: str = "openai-compatible",
duration_ms: Optional[int] = None,
input_rows: Optional[int] = None,
) -> Dict[str, Any]:
@@ -229,7 +230,7 @@ def merge_scan_ai_result(
"duration_ms": duration_ms,
"timeout_sec": timeout_sec,
"cache_ttl_sec": cache_ttl_sec,
"provider": "deepseek",
"provider": provider,
"base_url": base_url,
"summary_zh": ai_raw.get("summary_zh"),
"summary_en": ai_raw.get("summary_en"),
+99 -46
View File
@@ -95,17 +95,68 @@ SCAN_TERMINAL_BUILD_TIMEOUT_SEC = max(
8,
int(os.getenv("POLYWEATHER_SCAN_TERMINAL_BUILD_TIMEOUT_SEC", "22")),
)
SCAN_AI_MODEL = str(
os.getenv("POLYWEATHER_SCAN_AI_MODEL") or "deepseek-v4-flash"
).strip()
SCAN_CITY_AI_MODEL = str(
os.getenv("POLYWEATHER_SCAN_CITY_AI_MODEL")
or os.getenv("POLYWEATHER_SCAN_AI_MODEL")
or "deepseek-v4-flash"
).strip()
SCAN_AI_BASE_URL = str(
os.getenv("POLYWEATHER_DEEPSEEK_BASE_URL") or "https://api.deepseek.com"
).strip().rstrip("/")
DEFAULT_SCAN_AI_MODEL = "MiMo-V2.5-Pro"
DEFAULT_SCAN_AI_BASE_URL = "https://token-plan-cn.xiaomimimo.com/v1"
SCAN_AI_API_KEY_ENV_HINT = (
"POLYWEATHER_SCAN_AI_API_KEY "
"(or POLYWEATHER_MIMO_API_KEY / POLYWEATHER_DEEPSEEK_API_KEY)"
)
def _env_str(*names: str, default: str = "") -> str:
for name in names:
value = str(os.getenv(name) or "").strip()
if value:
return value
return str(default).strip()
def _scan_ai_api_key() -> str:
return _env_str(
"POLYWEATHER_SCAN_AI_API_KEY",
"POLYWEATHER_MIMO_API_KEY",
"POLYWEATHER_DEEPSEEK_API_KEY",
)
def _infer_scan_ai_provider(base_url: str, model: str) -> str:
text = f"{base_url} {model}".lower()
if "xiaomimimo" in text or "mimo" in text:
return "mimo"
if "deepseek" in text:
return "deepseek"
return "openai-compatible"
def _scan_ai_provider_label(provider: str) -> str:
normalized = provider.strip().lower()
if normalized == "mimo":
return "MiMo"
if normalized == "deepseek":
return "DeepSeek"
return "AI provider"
SCAN_AI_MODEL = _env_str("POLYWEATHER_SCAN_AI_MODEL", default=DEFAULT_SCAN_AI_MODEL)
SCAN_CITY_AI_MODEL = _env_str(
"POLYWEATHER_SCAN_CITY_AI_MODEL",
"POLYWEATHER_SCAN_AI_MODEL",
default=SCAN_AI_MODEL or DEFAULT_SCAN_AI_MODEL,
)
SCAN_AI_BASE_URL = _env_str(
"POLYWEATHER_SCAN_AI_BASE_URL",
"POLYWEATHER_MIMO_BASE_URL",
"POLYWEATHER_DEEPSEEK_BASE_URL",
default=DEFAULT_SCAN_AI_BASE_URL,
).rstrip("/")
SCAN_AI_PROVIDER = _env_str(
"POLYWEATHER_SCAN_AI_PROVIDER",
default=_infer_scan_ai_provider(SCAN_AI_BASE_URL, SCAN_CITY_AI_MODEL),
)
SCAN_AI_PROVIDER_LABEL = _env_str(
"POLYWEATHER_SCAN_AI_PROVIDER_LABEL",
default=_scan_ai_provider_label(SCAN_AI_PROVIDER),
)
SCAN_AI_ENABLED = str(
os.getenv("POLYWEATHER_SCAN_AI_ENABLED") or "false"
).strip().lower() in {"1", "true", "yes", "on"}
@@ -182,9 +233,9 @@ def _start_scan_terminal_background_refresh(filters: Dict[str, Any]) -> bool:
def _call_deepseek_scan_ai(ai_input: Dict[str, Any]) -> Dict[str, Any]:
api_key = str(os.getenv("POLYWEATHER_DEEPSEEK_API_KEY") or "").strip()
api_key = _scan_ai_api_key()
if not api_key:
raise RuntimeError("POLYWEATHER_DEEPSEEK_API_KEY is not configured")
raise RuntimeError(f"{SCAN_AI_API_KEY_ENV_HINT} is not configured")
system_prompt = (
"你是 PolyWeather 的付费 V4-Pro 城市最高温预测员。你只能基于用户提供的 JSON 快照做判断,"
@@ -357,7 +408,7 @@ def _call_deepseek_city_ai(ai_input: Dict[str, Any], *, locale: str = "zh-CN") -
return _call_deepseek_city_ai_provider(
ai_input,
locale=locale,
api_key=str(os.getenv("POLYWEATHER_DEEPSEEK_API_KEY") or "").strip(),
api_key=_scan_ai_api_key(),
base_url=SCAN_AI_BASE_URL,
model=SCAN_CITY_AI_MODEL,
max_tokens=SCAN_CITY_AI_MAX_TOKENS,
@@ -470,7 +521,7 @@ def _build_city_ai_result_payload(
"status": "ready",
"cached": cached,
"model": SCAN_CITY_AI_MODEL,
"provider": "deepseek",
"provider": SCAN_AI_PROVIDER,
"city": data.get("name"),
"city_display_name": data.get("display_name"),
"generated_at": generated_at,
@@ -508,7 +559,7 @@ def stream_scan_city_ai_forecast_payload(
{
"status": "failed",
"model": SCAN_CITY_AI_MODEL,
"provider": "deepseek",
"provider": SCAN_AI_PROVIDER,
"city": city_name,
"city_display_name": str(city or "").strip() or city_name,
"reason": reason_en if normalized_locale == "en-US" else reason_zh,
@@ -544,7 +595,7 @@ def stream_scan_city_ai_forecast_payload(
"status": "ready",
"cached": True,
"model": SCAN_CITY_AI_MODEL,
"provider": "deepseek",
"provider": SCAN_AI_PROVIDER,
"city": cached.get("city") or city_name,
"city_display_name": cached.get("city_display_name") or city_name,
"generated_at": cached.get("generated_at"),
@@ -561,14 +612,14 @@ def stream_scan_city_ai_forecast_payload(
observation_anchor = ai_input.get("observation_anchor") if isinstance(ai_input.get("observation_anchor"), dict) else {}
is_airport_metar = observation_anchor.get("is_airport_metar") is not False
calling_message_zh = (
"DeepSeek 正在快速增强机场报文解读…"
f"{SCAN_AI_PROVIDER_LABEL} 正在快速增强机场报文解读…"
if is_airport_metar
else "DeepSeek 正在快速增强香港天文台观测解读…"
else f"{SCAN_AI_PROVIDER_LABEL} 正在快速增强香港天文台观测解读…"
)
calling_message_en = (
"DeepSeek is adding a fast airport-bulletin enhancement…"
f"{SCAN_AI_PROVIDER_LABEL} is adding a fast airport-bulletin enhancement…"
if is_airport_metar
else "DeepSeek is adding a fast HKO observation enhancement…"
else f"{SCAN_AI_PROVIDER_LABEL} is adding a fast HKO observation enhancement…"
)
yield _sse_event(
"preview",
@@ -600,23 +651,23 @@ def stream_scan_city_ai_forecast_payload(
{
"status": "disabled",
"model": SCAN_CITY_AI_MODEL,
"provider": "deepseek",
"provider": SCAN_AI_PROVIDER,
"city": data.get("name") or city_name,
"city_display_name": data.get("display_name") or city_name,
"reason": "POLYWEATHER_SCAN_AI_ENABLED is not enabled",
},
)
return
if not str(os.getenv("POLYWEATHER_DEEPSEEK_API_KEY") or "").strip():
if not _scan_ai_api_key():
yield _sse_event(
"final",
{
"status": "missing_key",
"model": SCAN_CITY_AI_MODEL,
"provider": "deepseek",
"provider": SCAN_AI_PROVIDER,
"city": data.get("name") or city_name,
"city_display_name": data.get("display_name") or city_name,
"reason": "POLYWEATHER_DEEPSEEK_API_KEY is not configured",
"reason": f"{SCAN_AI_API_KEY_ENV_HINT} is not configured",
},
)
return
@@ -630,7 +681,7 @@ def stream_scan_city_ai_forecast_payload(
pool=5.0,
)
headers = {
"Authorization": f"Bearer {os.getenv('POLYWEATHER_DEEPSEEK_API_KEY')}",
"Authorization": f"Bearer {_scan_ai_api_key()}",
"Content-Type": "application/json",
}
accumulated = ""
@@ -758,8 +809,8 @@ def stream_scan_city_ai_forecast_payload(
)
except httpx.TimeoutException as exc:
duration_ms = int((time.time() - started_at) * 1000)
reason_en = f"DeepSeek city AI timed out after {SCAN_CITY_AI_TIMEOUT_SEC}s"
reason_zh = f"DeepSeek 城市 AI 在 {SCAN_CITY_AI_TIMEOUT_SEC} 秒内未返回"
reason_en = f"{SCAN_AI_PROVIDER_LABEL} city AI timed out after {SCAN_CITY_AI_TIMEOUT_SEC}s"
reason_zh = f"{SCAN_AI_PROVIDER_LABEL} 城市 AI 在 {SCAN_CITY_AI_TIMEOUT_SEC} 秒内未返回"
logger.warning(
"scan city AI stream timeout fallback city={} duration_ms={} model={} error={}",
data.get("name") or city_name,
@@ -800,7 +851,7 @@ def stream_scan_city_ai_forecast_payload(
{
"status": "failed",
"model": SCAN_CITY_AI_MODEL,
"provider": "deepseek",
"provider": SCAN_AI_PROVIDER,
"city": data.get("name") or city_name,
"city_display_name": data.get("display_name") or city_name,
"duration_ms": int((time.time() - started_at) * 1000),
@@ -827,7 +878,7 @@ def build_scan_city_ai_forecast_payload(
return {
"status": "failed",
"model": SCAN_CITY_AI_MODEL,
"provider": "deepseek",
"provider": SCAN_AI_PROVIDER,
"city": city_name,
"city_display_name": str(city or "").strip() or city_name,
"reason": (
@@ -866,7 +917,7 @@ def build_scan_city_ai_forecast_payload(
"status": "ready",
"cached": True,
"model": SCAN_CITY_AI_MODEL,
"provider": "deepseek",
"provider": SCAN_AI_PROVIDER,
"city": cached.get("city") or city_name,
"city_display_name": cached.get("city_display_name") or city_name,
"generated_at": cached.get("generated_at"),
@@ -883,24 +934,24 @@ def build_scan_city_ai_forecast_payload(
return {
"status": "disabled",
"model": SCAN_CITY_AI_MODEL,
"provider": "deepseek",
"provider": SCAN_AI_PROVIDER,
"city": data.get("name") or city_name,
"city_display_name": data.get("display_name") or city_name,
"reason": "POLYWEATHER_SCAN_AI_ENABLED is not enabled",
}
if not str(os.getenv("POLYWEATHER_DEEPSEEK_API_KEY") or "").strip():
if not _scan_ai_api_key():
logger.warning(
"scan city AI forecast missing DeepSeek key city={} model={}",
"scan city AI forecast missing provider key city={} model={}",
data.get("name") or city_name,
SCAN_CITY_AI_MODEL,
)
return {
"status": "missing_key",
"model": SCAN_CITY_AI_MODEL,
"provider": "deepseek",
"provider": SCAN_AI_PROVIDER,
"city": data.get("name") or city_name,
"city_display_name": data.get("display_name") or city_name,
"reason": "POLYWEATHER_DEEPSEEK_API_KEY is not configured",
"reason": f"{SCAN_AI_API_KEY_ENV_HINT} is not configured",
}
try:
@@ -918,8 +969,8 @@ def build_scan_city_ai_forecast_payload(
ai_raw = _call_deepseek_city_ai(ai_input, locale=normalized_locale)
except httpx.TimeoutException as exc:
duration_ms = int((time.time() - started_at) * 1000)
reason_en = f"DeepSeek city AI timed out after {SCAN_CITY_AI_TIMEOUT_SEC}s"
reason_zh = f"DeepSeek 城市 AI 在 {SCAN_CITY_AI_TIMEOUT_SEC} 秒内未返回"
reason_en = f"{SCAN_AI_PROVIDER_LABEL} city AI timed out after {SCAN_CITY_AI_TIMEOUT_SEC}s"
reason_zh = f"{SCAN_AI_PROVIDER_LABEL} 城市 AI 在 {SCAN_CITY_AI_TIMEOUT_SEC} 秒内未返回"
ai_raw = _build_city_ai_fallback(
ai_input,
locale=normalized_locale,
@@ -938,7 +989,7 @@ def build_scan_city_ai_forecast_payload(
"degraded": True,
"cached": False,
"model": SCAN_CITY_AI_MODEL,
"provider": "deepseek",
"provider": SCAN_AI_PROVIDER,
"city": data.get("name") or city_name,
"city_display_name": data.get("display_name") or city_name,
"generated_at": generated_at,
@@ -953,12 +1004,12 @@ def build_scan_city_ai_forecast_payload(
raw_reason = str(exc)
empty_ai_content = raw_reason.strip().lower() == "empty ai content"
reason_en = (
"DeepSeek city AI returned no usable text. Retry the city analysis."
f"{SCAN_AI_PROVIDER_LABEL} city AI returned no usable text. Retry the city analysis."
if empty_ai_content
else raw_reason
)
reason_zh = (
"DeepSeek 城市 AI 没有返回有效正文,请刷新重试。"
f"{SCAN_AI_PROVIDER_LABEL} 城市 AI 没有返回有效正文,请刷新重试。"
if empty_ai_content
else raw_reason
)
@@ -980,7 +1031,7 @@ def build_scan_city_ai_forecast_payload(
"degraded": True,
"cached": False,
"model": SCAN_CITY_AI_MODEL,
"provider": "deepseek",
"provider": SCAN_AI_PROVIDER,
"city": data.get("name") or city_name,
"city_display_name": data.get("display_name") or city_name,
"generated_at": generated_at,
@@ -1009,7 +1060,7 @@ def build_scan_city_ai_forecast_payload(
"status": "ready",
"cached": False,
"model": SCAN_CITY_AI_MODEL,
"provider": "deepseek",
"provider": SCAN_AI_PROVIDER,
"city": data.get("name") or city_name,
"city_display_name": data.get("display_name") or city_name,
"generated_at": generated_at,
@@ -1039,7 +1090,7 @@ def _build_scan_ai_unavailable_payload(
"duration_ms": duration_ms,
"timeout_sec": SCAN_AI_TIMEOUT_SEC,
"cache_ttl_sec": SCAN_AI_CACHE_TTL_SEC,
"provider": "deepseek",
"provider": SCAN_AI_PROVIDER,
"base_url": SCAN_AI_BASE_URL,
"reason": reason,
},
@@ -1250,11 +1301,11 @@ def build_scan_terminal_ai_payload(
status="disabled",
reason="POLYWEATHER_SCAN_AI_ENABLED is not enabled",
)
if not str(os.getenv("POLYWEATHER_DEEPSEEK_API_KEY") or "").strip():
if not _scan_ai_api_key():
return _build_scan_ai_unavailable_payload(
payload,
status="missing_key",
reason="POLYWEATHER_DEEPSEEK_API_KEY is not configured",
reason=f"{SCAN_AI_API_KEY_ENV_HINT} is not configured",
)
cached = get_cached_scan_ai_result(
@@ -1279,6 +1330,7 @@ def build_scan_terminal_ai_payload(
cache_ttl_sec=SCAN_AI_CACHE_TTL_SEC,
base_url=SCAN_AI_BASE_URL,
cached=True,
provider=SCAN_AI_PROVIDER,
duration_ms=0,
input_rows=len(payload.get("rows") or []),
)
@@ -1323,6 +1375,7 @@ def build_scan_terminal_ai_payload(
cache_ttl_sec=SCAN_AI_CACHE_TTL_SEC,
base_url=SCAN_AI_BASE_URL,
cached=False,
provider=SCAN_AI_PROVIDER,
duration_ms=duration_ms,
input_rows=len(payload.get("rows") or []),
)