feat: implement AI city forecast streaming service and UI components for scan terminal

This commit is contained in:
2569718930@qq.com
2026-04-26 13:43:55 +08:00
parent 5344779e32
commit d42cdf6b51
5 changed files with 77 additions and 48 deletions
+1
View File
@@ -141,6 +141,7 @@ POLYWEATHER_SCAN_AI_ENABLED=false
POLYWEATHER_DEEPSEEK_API_KEY= POLYWEATHER_DEEPSEEK_API_KEY=
POLYWEATHER_DEEPSEEK_BASE_URL=https://api.deepseek.com POLYWEATHER_DEEPSEEK_BASE_URL=https://api.deepseek.com
POLYWEATHER_SCAN_AI_MODEL=deepseek-v4-pro POLYWEATHER_SCAN_AI_MODEL=deepseek-v4-pro
POLYWEATHER_SCAN_CITY_AI_MODEL=deepseek-v4-flash
POLYWEATHER_SCAN_AI_TIMEOUT_SEC=40 POLYWEATHER_SCAN_AI_TIMEOUT_SEC=40
POLYWEATHER_SCAN_CITY_AI_TIMEOUT_SEC=18 POLYWEATHER_SCAN_CITY_AI_TIMEOUT_SEC=18
POLYWEATHER_SCAN_CITY_AI_RETRY_ON_STREAM_PARSE_ERROR=false POLYWEATHER_SCAN_CITY_AI_RETRY_ON_STREAM_PARSE_ERROR=false
@@ -425,8 +425,8 @@ function AiPinnedCityCard({
<p className={aiForecast.streamText ? "scan-ai-weather-summary" : undefined}> <p className={aiForecast.streamText ? "scan-ai-weather-summary" : undefined}>
{aiForecast.streamText || {aiForecast.streamText ||
(isEn (isEn
? "Deepseek V4 pro is reading the latest airport bulletin..." ? "DeepSeek is reading the latest airport bulletin..."
: "Deepseek V4 pro 正在解读最新机场报文...")} : "DeepSeek 正在解读最新机场报文...")}
</p> </p>
{!aiForecast.streamText ? ( {!aiForecast.streamText ? (
<p className="scan-ai-city-muted"> <p className="scan-ai-city-muted">
@@ -460,8 +460,8 @@ function AiPinnedCityCard({
<p> <p>
{aiForecast.payload?.status === "timeout" {aiForecast.payload?.status === "timeout"
? isEn ? isEn
? "Deepseek V4-Pro timed out. You can retry; city data and the right briefing were not refreshed." ? "DeepSeek enhancement timed out. You can retry; city data and the right briefing were not refreshed."
: "Deepseek V4-Pro 本次解读超时,可稍后重试;城市数据和右侧简报不会被刷新。" : "DeepSeek 增强本次超时,可稍后重试;城市数据和右侧简报不会被刷新。"
: fallbackAiReason || : fallbackAiReason ||
(isEn (isEn
? "AI read is unavailable for this city right now." ? "AI read is unavailable for this city right now."
@@ -9,6 +9,8 @@ export type AiCityForecastPayload = {
reason_zh?: string | null; reason_zh?: string | null;
reason_en?: string | null; reason_en?: string | null;
raw_reason?: string | null; raw_reason?: string | null;
degraded?: boolean | null;
cached?: boolean | null;
model?: string | null; model?: string | null;
provider?: string | null; provider?: string | null;
city_forecast?: { city_forecast?: {
@@ -11,6 +11,7 @@ import {
fetchBackendApi, fetchBackendApi,
} from "@/lib/backend-api"; } from "@/lib/backend-api";
import type { CityDetail, MarketScan } from "@/lib/dashboard-types"; import type { CityDetail, MarketScan } from "@/lib/dashboard-types";
import { extractStreamingAirportRead } from "./ai-city-stream";
import { normalizeCityKey } from "./decision-utils"; import { normalizeCityKey } from "./decision-utils";
const AI_CITY_FORECAST_CACHE_PREFIX = "polyWeather_aiCityForecast_v2"; const AI_CITY_FORECAST_CACHE_PREFIX = "polyWeather_aiCityForecast_v2";
@@ -106,6 +107,7 @@ function parseAiCityStreamBlock(block: string): AiCityStreamEvent | null {
async function readAiCityForecastStream( async function readAiCityForecastStream(
response: Response, response: Response,
locale: string,
onProgress?: (progress: AiCityStreamProgress) => void, onProgress?: (progress: AiCityStreamProgress) => void,
) { ) {
if (!response.body) { if (!response.body) {
@@ -115,6 +117,7 @@ async function readAiCityForecastStream(
const reader = response.body.getReader(); const reader = response.body.getReader();
const decoder = new TextDecoder(); const decoder = new TextDecoder();
let buffer = ""; let buffer = "";
let accumulatedRaw = "";
let finalPayload: AiCityForecastPayload | null = null; let finalPayload: AiCityForecastPayload | null = null;
const consumeBlock = (block: string) => { const consumeBlock = (block: string) => {
@@ -130,10 +133,26 @@ async function readAiCityForecastStream(
return; return;
} }
if (event === "delta") { if (event === "delta") {
const content = String(data.content || "");
const rawLength = Number(data.raw_length); const rawLength = Number(data.raw_length);
onProgress?.({ if (content) {
accumulatedRaw += content;
}
const streamingAirportRead = extractStreamingAirportRead(
accumulatedRaw,
locale,
);
const progress: AiCityStreamProgress = {
raw_length: Number.isFinite(rawLength) ? rawLength : null, raw_length: Number.isFinite(rawLength) ? rawLength : null,
}); };
if (streamingAirportRead) {
if (locale === "en-US") {
progress.metar_read_en = streamingAirportRead;
} else {
progress.metar_read_zh = streamingAirportRead;
}
}
onProgress?.(progress);
} }
}; };
@@ -214,7 +233,7 @@ function requestAiCityForecast({
: `HTTP ${response.status}`, : `HTTP ${response.status}`,
); );
} }
return readAiCityForecastStream(response, onProgress); return readAiCityForecastStream(response, locale, onProgress);
}) })
.finally(() => { .finally(() => {
pendingAiCityForecastRequests.delete(requestKey); pendingAiCityForecastRequests.delete(requestKey);
@@ -235,8 +254,8 @@ function getAiCityStreamProgressText(progress: AiCityStreamProgress, isEn: boole
const rawLength = Number(progress.raw_length); const rawLength = Number(progress.raw_length);
if (Number.isFinite(rawLength) && rawLength > 0) { if (Number.isFinite(rawLength) && rawLength > 0) {
return isEn return isEn
? `DeepSeek V4-Pro is streaming the airport bulletin read... ${Math.round(rawLength)} chars received.` ? `DeepSeek is streaming the airport-bulletin enhancement... ${Math.round(rawLength)} chars received.`
: `DeepSeek V4-Pro 正在流式解读机场报文... 已收到 ${Math.round(rawLength)} 字符。`; : `DeepSeek 正在流式增强机场报文解读... 已收到 ${Math.round(rawLength)} 字符。`;
} }
return ""; return "";
} }
@@ -364,7 +383,7 @@ export function useAiCityForecast({
: initialFallback.city_forecast?.metar_read_zh) || : initialFallback.city_forecast?.metar_read_zh) ||
(isEn (isEn
? "Reading the latest airport bulletin with model/METAR fallback ready..." ? "Reading the latest airport bulletin with model/METAR fallback ready..."
: "已先用最新 METAR 给出兜底解读,正在等待 DeepSeek V4-Pro 补充…"), : "已先用最新 METAR 给出兜底解读,正在等待 DeepSeek 补充…"),
}); });
void requestAiCityForecast({ void requestAiCityForecast({
city: detailCityName, city: detailCityName,
@@ -397,7 +416,9 @@ export function useAiCityForecast({
isEn, isEn,
report, report,
}); });
writeCachedPayload(cacheKey, usablePayload); if (usablePayload.status === "ready" && !usablePayload.degraded) {
writeCachedPayload(cacheKey, usablePayload);
}
setAiForecast({ payload: usablePayload, status: "ready" }); setAiForecast({ payload: usablePayload, status: "ready" });
} }
}) })
@@ -409,7 +430,6 @@ export function useAiCityForecast({
isEn, isEn,
report, report,
}); });
writeCachedPayload(cacheKey, fallbackPayload);
setAiForecast({ payload: fallbackPayload, status: "ready" }); setAiForecast({ payload: fallbackPayload, status: "ready" });
} }
}); });
+42 -36
View File
@@ -55,6 +55,11 @@ SCAN_TERMINAL_BUILD_TIMEOUT_SEC = max(
SCAN_AI_MODEL = str( SCAN_AI_MODEL = str(
os.getenv("POLYWEATHER_SCAN_AI_MODEL") or "deepseek-v4-pro" os.getenv("POLYWEATHER_SCAN_AI_MODEL") or "deepseek-v4-pro"
).strip() ).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( SCAN_AI_BASE_URL = str(
os.getenv("POLYWEATHER_DEEPSEEK_BASE_URL") or "https://api.deepseek.com" os.getenv("POLYWEATHER_DEEPSEEK_BASE_URL") or "https://api.deepseek.com"
).strip().rstrip("/") ).strip().rstrip("/")
@@ -1217,7 +1222,7 @@ def _call_deepseek_scan_ai(ai_input: Dict[str, Any]) -> Dict[str, Any]:
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
json={ json={
"model": SCAN_AI_MODEL, "model": SCAN_CITY_AI_MODEL,
"temperature": 0.1, "temperature": 0.1,
"max_tokens": SCAN_AI_MAX_TOKENS, "max_tokens": SCAN_AI_MAX_TOKENS,
"response_format": {"type": "json_object"}, "response_format": {"type": "json_object"},
@@ -1342,7 +1347,7 @@ def _call_deepseek_city_ai(ai_input: Dict[str, Any], *, locale: str = "zh-CN") -
normalized_locale = _normalize_locale(locale) normalized_locale = _normalize_locale(locale)
system_prompt = ( system_prompt = (
"你是 PolyWeather 的 Deepseek V4-Pro 城市最高温预测员。你必须直接阅读用户给出的城市 JSON," "你是 PolyWeather 的 DeepSeek 城市最高温预测员。你必须直接阅读用户给出的城市 JSON,"
"判断该城市今日最高温路径。不要写套利、交易、BUY YES/NO、价格、edge 或 Kelly。" "判断该城市今日最高温路径。不要写套利、交易、BUY YES/NO、价格、edge 或 Kelly。"
"你的核心输出是:最终最高温点估计、置信区间、置信度、最终判断、机场报文/METAR 解读、判断依据和风险。" "你的核心输出是:最终最高温点估计、置信区间、置信度、最终判断、机场报文/METAR 解读、判断依据和风险。"
"必须综合 DEB 最终融合值、全部天气模型预测、METAR 实测序列、最新机场报文、峰值窗口、当地时间、季节背景。" "必须综合 DEB 最终融合值、全部天气模型预测、METAR 实测序列、最新机场报文、峰值窗口、当地时间、季节背景。"
@@ -1389,7 +1394,7 @@ def _call_deepseek_city_ai(ai_input: Dict[str, Any], *, locale: str = "zh-CN") -
pool=5.0, pool=5.0,
) )
request_json = { request_json = {
"model": SCAN_AI_MODEL, "model": SCAN_CITY_AI_MODEL,
"temperature": 0.2, "temperature": 0.2,
"max_tokens": SCAN_CITY_AI_MAX_TOKENS, "max_tokens": SCAN_CITY_AI_MAX_TOKENS,
"response_format": {"type": "json_object"}, "response_format": {"type": "json_object"},
@@ -1476,7 +1481,7 @@ def _call_deepseek_city_ai(ai_input: Dict[str, Any], *, locale: str = "zh-CN") -
preview, preview,
) )
repair_payload = { repair_payload = {
"model": SCAN_AI_MODEL, "model": SCAN_CITY_AI_MODEL,
"temperature": 0.0, "temperature": 0.0,
"max_tokens": min(max(SCAN_CITY_AI_MAX_TOKENS, 1200), 64000), "max_tokens": min(max(SCAN_CITY_AI_MAX_TOKENS, 1200), 64000),
"response_format": {"type": "json_object"}, "response_format": {"type": "json_object"},
@@ -1580,6 +1585,7 @@ def _scan_city_ai_cache_key(ai_input: Dict[str, Any]) -> str:
key_payload = { key_payload = {
"prompt_version": SCAN_CITY_AI_PROMPT_VERSION, "prompt_version": SCAN_CITY_AI_PROMPT_VERSION,
"schema_version": ai_input.get("schema_version"), "schema_version": ai_input.get("schema_version"),
"model": SCAN_CITY_AI_MODEL,
"city": ai_input.get("city"), "city": ai_input.get("city"),
"local_date": ai_input.get("local_date"), "local_date": ai_input.get("local_date"),
"local_time": ai_input.get("local_time"), "local_time": ai_input.get("local_time"),
@@ -1620,7 +1626,7 @@ def _build_city_ai_stream_request(
"不要写交易建议、BUY/SELL、Kelly 或套利。" "不要写交易建议、BUY/SELL、Kelly 或套利。"
) )
return { return {
"model": SCAN_AI_MODEL, "model": SCAN_CITY_AI_MODEL,
"temperature": 0.2, "temperature": 0.2,
"max_tokens": SCAN_CITY_AI_MAX_TOKENS, "max_tokens": SCAN_CITY_AI_MAX_TOKENS,
"response_format": {"type": "json_object"}, "response_format": {"type": "json_object"},
@@ -1691,7 +1697,7 @@ def _build_city_ai_result_payload(
payload: Dict[str, Any] = { payload: Dict[str, Any] = {
"status": "ready", "status": "ready",
"cached": cached, "cached": cached,
"model": SCAN_AI_MODEL, "model": SCAN_CITY_AI_MODEL,
"provider": "deepseek", "provider": "deepseek",
"city": data.get("name"), "city": data.get("name"),
"city_display_name": data.get("display_name"), "city_display_name": data.get("display_name"),
@@ -1729,7 +1735,7 @@ def stream_scan_city_ai_forecast_payload(
"final", "final",
{ {
"status": "failed", "status": "failed",
"model": SCAN_AI_MODEL, "model": SCAN_CITY_AI_MODEL,
"provider": "deepseek", "provider": "deepseek",
"city": city_name, "city": city_name,
"city_display_name": str(city or "").strip() or city_name, "city_display_name": str(city or "").strip() or city_name,
@@ -1765,7 +1771,7 @@ def stream_scan_city_ai_forecast_payload(
{ {
"status": "ready", "status": "ready",
"cached": True, "cached": True,
"model": SCAN_AI_MODEL, "model": SCAN_CITY_AI_MODEL,
"provider": "deepseek", "provider": "deepseek",
"city": cached.get("city") or city_name, "city": cached.get("city") or city_name,
"city_display_name": cached.get("city_display_name") or city_name, "city_display_name": cached.get("city_display_name") or city_name,
@@ -1799,8 +1805,8 @@ def stream_scan_city_ai_forecast_payload(
"stage": "calling_ai", "stage": "calling_ai",
"city": data.get("name") or city_name, "city": data.get("name") or city_name,
"city_display_name": data.get("display_name") or city_name, "city_display_name": data.get("display_name") or city_name,
"message_zh": "DeepSeek V4-Pro 开始流式解读机场报文…", "message_zh": "DeepSeek 正在快速增强机场报文解读",
"message_en": "DeepSeek V4-Pro is streaming the airport bulletin read", "message_en": "DeepSeek is adding a fast airport-bulletin enhancement",
}, },
) )
@@ -1809,7 +1815,7 @@ def stream_scan_city_ai_forecast_payload(
"final", "final",
{ {
"status": "disabled", "status": "disabled",
"model": SCAN_AI_MODEL, "model": SCAN_CITY_AI_MODEL,
"provider": "deepseek", "provider": "deepseek",
"city": data.get("name") or city_name, "city": data.get("name") or city_name,
"city_display_name": data.get("display_name") or city_name, "city_display_name": data.get("display_name") or city_name,
@@ -1822,7 +1828,7 @@ def stream_scan_city_ai_forecast_payload(
"final", "final",
{ {
"status": "missing_key", "status": "missing_key",
"model": SCAN_AI_MODEL, "model": SCAN_CITY_AI_MODEL,
"provider": "deepseek", "provider": "deepseek",
"city": data.get("name") or city_name, "city": data.get("name") or city_name,
"city_display_name": data.get("display_name") or city_name, "city_display_name": data.get("display_name") or city_name,
@@ -1967,13 +1973,13 @@ def stream_scan_city_ai_forecast_payload(
) )
except httpx.TimeoutException as exc: except httpx.TimeoutException as exc:
duration_ms = int((time.time() - started_at) * 1000) duration_ms = int((time.time() - started_at) * 1000)
reason_en = f"DeepSeek V4-Pro timed out after {SCAN_CITY_AI_TIMEOUT_SEC}s" reason_en = f"DeepSeek city AI timed out after {SCAN_CITY_AI_TIMEOUT_SEC}s"
reason_zh = f"DeepSeek V4-Pro{SCAN_CITY_AI_TIMEOUT_SEC} 秒内未返回" reason_zh = f"DeepSeek 城市 AI{SCAN_CITY_AI_TIMEOUT_SEC} 秒内未返回"
logger.warning( logger.warning(
"scan city AI stream timeout fallback city={} duration_ms={} model={} error={}", "scan city AI stream timeout fallback city={} duration_ms={} model={} error={}",
data.get("name") or city_name, data.get("name") or city_name,
duration_ms, duration_ms,
SCAN_AI_MODEL, SCAN_CITY_AI_MODEL,
exc, exc,
) )
ai_raw = _build_city_ai_fallback( ai_raw = _build_city_ai_fallback(
@@ -2001,14 +2007,14 @@ def stream_scan_city_ai_forecast_payload(
logger.warning( logger.warning(
"scan city AI stream failed city={} model={} error={}", "scan city AI stream failed city={} model={} error={}",
data.get("name") or city_name, data.get("name") or city_name,
SCAN_AI_MODEL, SCAN_CITY_AI_MODEL,
reason, reason,
) )
yield _sse_event( yield _sse_event(
"final", "final",
{ {
"status": "failed", "status": "failed",
"model": SCAN_AI_MODEL, "model": SCAN_CITY_AI_MODEL,
"provider": "deepseek", "provider": "deepseek",
"city": data.get("name") or city_name, "city": data.get("name") or city_name,
"city_display_name": data.get("display_name") or city_name, "city_display_name": data.get("display_name") or city_name,
@@ -2035,7 +2041,7 @@ def build_scan_city_ai_forecast_payload(
if city_name not in CITIES: if city_name not in CITIES:
return { return {
"status": "failed", "status": "failed",
"model": SCAN_AI_MODEL, "model": SCAN_CITY_AI_MODEL,
"provider": "deepseek", "provider": "deepseek",
"city": city_name, "city": city_name,
"city_display_name": str(city or "").strip() or city_name, "city_display_name": str(city or "").strip() or city_name,
@@ -2052,7 +2058,7 @@ def build_scan_city_ai_forecast_payload(
city_name, city_name,
force_refresh, force_refresh,
normalized_locale, normalized_locale,
SCAN_AI_MODEL, SCAN_CITY_AI_MODEL,
) )
data = _analyze( data = _analyze(
city_name, city_name,
@@ -2069,12 +2075,12 @@ def build_scan_city_ai_forecast_payload(
logger.info( logger.info(
"scan city AI forecast cache hit city={} model={}", "scan city AI forecast cache hit city={} model={}",
cached.get("city") or city_name, cached.get("city") or city_name,
SCAN_AI_MODEL, SCAN_CITY_AI_MODEL,
) )
return { return {
"status": "ready", "status": "ready",
"cached": True, "cached": True,
"model": SCAN_AI_MODEL, "model": SCAN_CITY_AI_MODEL,
"provider": "deepseek", "provider": "deepseek",
"city": cached.get("city") or city_name, "city": cached.get("city") or city_name,
"city_display_name": cached.get("city_display_name") or city_name, "city_display_name": cached.get("city_display_name") or city_name,
@@ -2087,11 +2093,11 @@ def build_scan_city_ai_forecast_payload(
logger.warning( logger.warning(
"scan city AI forecast disabled city={} model={}", "scan city AI forecast disabled city={} model={}",
data.get("name") or city_name, data.get("name") or city_name,
SCAN_AI_MODEL, SCAN_CITY_AI_MODEL,
) )
return { return {
"status": "disabled", "status": "disabled",
"model": SCAN_AI_MODEL, "model": SCAN_CITY_AI_MODEL,
"provider": "deepseek", "provider": "deepseek",
"city": data.get("name") or city_name, "city": data.get("name") or city_name,
"city_display_name": data.get("display_name") or city_name, "city_display_name": data.get("display_name") or city_name,
@@ -2101,11 +2107,11 @@ def build_scan_city_ai_forecast_payload(
logger.warning( logger.warning(
"scan city AI forecast missing DeepSeek key city={} model={}", "scan city AI forecast missing DeepSeek key city={} model={}",
data.get("name") or city_name, data.get("name") or city_name,
SCAN_AI_MODEL, SCAN_CITY_AI_MODEL,
) )
return { return {
"status": "missing_key", "status": "missing_key",
"model": SCAN_AI_MODEL, "model": SCAN_CITY_AI_MODEL,
"provider": "deepseek", "provider": "deepseek",
"city": data.get("name") or city_name, "city": data.get("name") or city_name,
"city_display_name": data.get("display_name") or city_name, "city_display_name": data.get("display_name") or city_name,
@@ -2117,7 +2123,7 @@ def build_scan_city_ai_forecast_payload(
"scan city AI forecast calling provider city={} station={} model={} raw_metar_present={}", "scan city AI forecast calling provider city={} station={} model={} raw_metar_present={}",
data.get("name") or city_name, data.get("name") or city_name,
((ai_input.get("airport") or {}).get("icao") if isinstance(ai_input.get("airport"), dict) else None), ((ai_input.get("airport") or {}).get("icao") if isinstance(ai_input.get("airport"), dict) else None),
SCAN_AI_MODEL, SCAN_CITY_AI_MODEL,
bool( bool(
(ai_input.get("airport_current") or {}).get("raw_metar") (ai_input.get("airport_current") or {}).get("raw_metar")
if isinstance(ai_input.get("airport_current"), dict) if isinstance(ai_input.get("airport_current"), dict)
@@ -2127,8 +2133,8 @@ def build_scan_city_ai_forecast_payload(
ai_raw = _call_deepseek_city_ai(ai_input, locale=normalized_locale) ai_raw = _call_deepseek_city_ai(ai_input, locale=normalized_locale)
except httpx.TimeoutException as exc: except httpx.TimeoutException as exc:
duration_ms = int((time.time() - started_at) * 1000) duration_ms = int((time.time() - started_at) * 1000)
reason_en = f"DeepSeek V4-Pro timed out after {SCAN_CITY_AI_TIMEOUT_SEC}s" reason_en = f"DeepSeek city AI timed out after {SCAN_CITY_AI_TIMEOUT_SEC}s"
reason_zh = f"DeepSeek V4-Pro{SCAN_CITY_AI_TIMEOUT_SEC} 秒内未返回" reason_zh = f"DeepSeek 城市 AI{SCAN_CITY_AI_TIMEOUT_SEC} 秒内未返回"
ai_raw = _build_city_ai_fallback( ai_raw = _build_city_ai_fallback(
ai_input, ai_input,
locale=normalized_locale, locale=normalized_locale,
@@ -2139,14 +2145,14 @@ def build_scan_city_ai_forecast_payload(
"scan city AI forecast timeout fallback city={} duration_ms={} model={} error={}", "scan city AI forecast timeout fallback city={} duration_ms={} model={} error={}",
data.get("name") or city_name, data.get("name") or city_name,
duration_ms, duration_ms,
SCAN_AI_MODEL, SCAN_CITY_AI_MODEL,
exc, exc,
) )
return { return {
"status": "ready", "status": "ready",
"degraded": True, "degraded": True,
"cached": False, "cached": False,
"model": SCAN_AI_MODEL, "model": SCAN_CITY_AI_MODEL,
"provider": "deepseek", "provider": "deepseek",
"city": data.get("name") or city_name, "city": data.get("name") or city_name,
"city_display_name": data.get("display_name") or city_name, "city_display_name": data.get("display_name") or city_name,
@@ -2162,12 +2168,12 @@ def build_scan_city_ai_forecast_payload(
raw_reason = str(exc) raw_reason = str(exc)
empty_ai_content = raw_reason.strip().lower() == "empty ai content" empty_ai_content = raw_reason.strip().lower() == "empty ai content"
reason_en = ( reason_en = (
"DeepSeek V4-Pro returned no usable text. Retry the city analysis." "DeepSeek city AI returned no usable text. Retry the city analysis."
if empty_ai_content if empty_ai_content
else raw_reason else raw_reason
) )
reason_zh = ( reason_zh = (
"DeepSeek V4-Pro 没有返回有效正文,请刷新重试。" "DeepSeek 城市 AI 没有返回有效正文,请刷新重试。"
if empty_ai_content if empty_ai_content
else raw_reason else raw_reason
) )
@@ -2175,7 +2181,7 @@ def build_scan_city_ai_forecast_payload(
"scan city AI forecast failed city={} duration_ms={} model={} error={}", "scan city AI forecast failed city={} duration_ms={} model={} error={}",
data.get("name") or city_name, data.get("name") or city_name,
duration_ms, duration_ms,
SCAN_AI_MODEL, SCAN_CITY_AI_MODEL,
raw_reason, raw_reason,
) )
ai_raw = _build_city_ai_fallback( ai_raw = _build_city_ai_fallback(
@@ -2188,7 +2194,7 @@ def build_scan_city_ai_forecast_payload(
"status": "ready", "status": "ready",
"degraded": True, "degraded": True,
"cached": False, "cached": False,
"model": SCAN_AI_MODEL, "model": SCAN_CITY_AI_MODEL,
"provider": "deepseek", "provider": "deepseek",
"city": data.get("name") or city_name, "city": data.get("name") or city_name,
"city_display_name": data.get("display_name") or city_name, "city_display_name": data.get("display_name") or city_name,
@@ -2211,13 +2217,13 @@ def build_scan_city_ai_forecast_payload(
"scan city AI forecast complete city={} duration_ms={} model={} confidence={}", "scan city AI forecast complete city={} duration_ms={} model={} confidence={}",
data.get("name") or city_name, data.get("name") or city_name,
int((time.time() - started_at) * 1000), int((time.time() - started_at) * 1000),
SCAN_AI_MODEL, SCAN_CITY_AI_MODEL,
ai_raw.get("confidence") if isinstance(ai_raw, dict) else None, ai_raw.get("confidence") if isinstance(ai_raw, dict) else None,
) )
return { return {
"status": "ready", "status": "ready",
"cached": False, "cached": False,
"model": SCAN_AI_MODEL, "model": SCAN_CITY_AI_MODEL,
"provider": "deepseek", "provider": "deepseek",
"city": data.get("name") or city_name, "city": data.get("name") or city_name,
"city_display_name": data.get("display_name") or city_name, "city_display_name": data.get("display_name") or city_name,