This version of Antigravity is no longer supported. Please upgrade to receive the latest features.
This commit is contained in:
+12
-5
@@ -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
|
||||
|
||||
@@ -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 内嵌预热线程。
|
||||
|
||||
@@ -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 重新评估”。
|
||||
**第三方服务合规与稳定性风险**:
|
||||
项目强依赖外部 API(Open-Meteo、AviationWeather、NWS、HKO、CWA、Polymarket、Supabase)以及城市 AI provider(DeepSeek-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。
|
||||
项目强依赖外部 API(Open-Meteo、AviationWeather、NWS、HKO、CWA、Polymarket、Supabase)以及城市 AI provider(OpenAI-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 是付费产品,必须先做法务与授权评审。缓解:只在研究环境评估;商用优先选择可商用权重/购买授权/自研。
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 最终融合值、全部天气模型预测、实测序列、最新观测、峰值窗口、当地时间、季节背景。"
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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 []),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user