feat: implement market scan data utilities and service for city weather card decisioning

This commit is contained in:
2569718930@qq.com
2026-04-27 00:50:14 +08:00
parent 6f9d75c68f
commit 97719a7ff0
3 changed files with 62 additions and 22 deletions
@@ -142,6 +142,24 @@ function getBucketAnchor(bucket: MarketTopBucket) {
return toFiniteMarketNumber(bucket.temp ?? bucket.value ?? bucket.lower);
}
function marketBucketLabelText(bucket?: MarketTopBucket | null) {
return String(`${bucket?.label || ""} ${bucket?.slug || ""} ${bucket?.question || ""}`)
.trim()
.toLowerCase();
}
function isMarketBucketAbove(bucket?: MarketTopBucket | null) {
return /\b(or[-\s]?higher|or[-\s]?above|above|higher|at least|greater than)\b/i.test(
marketBucketLabelText(bucket),
);
}
function isMarketBucketBelow(bucket?: MarketTopBucket | null) {
return /\b(or[-\s]?lower|or[-\s]?below|below|lower|at most|less than)\b/i.test(
marketBucketLabelText(bucket),
);
}
function getRoundedWeatherBucketValue(
expectedHigh: number | null,
tempSymbol: string,
@@ -212,9 +230,16 @@ export function pickMarketBucketForWeatherCenter(
const comparable = normalizeMarketComparableTemp(expectedHigh, tempSymbol, bucket);
const anchor = getBucketAnchor(bucket);
if (comparable == null || anchor == null) return false;
const unit = String(bucket.unit || "").toUpperCase();
const maxReasonableDelta = unit === "F" ? 16 : 8;
return Math.abs(anchor - comparable) <= maxReasonableDelta;
const roundedTarget = Math.round(comparable);
const roundedAnchor = Math.round(anchor);
const lower = bucket.lower != null ? Number(bucket.lower) : anchor;
const upper = bucket.upper != null ? Number(bucket.upper) : null;
if (upper != null && Number.isFinite(lower) && Number.isFinite(upper)) {
return roundedTarget >= lower - 0.01 && roundedTarget <= upper + 0.01;
}
if (isMarketBucketAbove(bucket)) return roundedTarget >= roundedAnchor;
if (isMarketBucketBelow(bucket)) return roundedTarget <= roundedAnchor;
return roundedAnchor === roundedTarget;
};
if (!buckets.length || expectedHigh == null || !Number.isFinite(expectedHigh)) {
return isReasonableFallback(selectedBucket) ? selectedBucket : null;
@@ -14,7 +14,7 @@ import type { CityDetail, MarketScan } from "@/lib/dashboard-types";
import { extractStreamingAirportRead } from "./ai-city-stream";
import { normalizeCityKey } from "./decision-utils";
const AI_CITY_FORECAST_CACHE_PREFIX = "polyWeather_aiCityForecast_v2";
const AI_CITY_FORECAST_CACHE_PREFIX = "polyWeather_aiCityForecast_v3";
const AI_CITY_FORECAST_CACHE_TTL_MS = 60 * 60 * 1000;
const CITY_MARKET_SCAN_CACHE_PREFIX = "polyWeather_cityMarketScan_v2";
const CITY_MARKET_SCAN_CACHE_TTL_MS = 10 * 60 * 1000;
@@ -357,10 +357,26 @@ export function useAiCityForecast({
});
const [aiRefreshToken, setAiRefreshToken] = useState(0);
const aiForecastKey = useMemo(
() =>
detail
? `${normalizeCityKey(detailCityName)}:${detail.local_date || ""}:${locale}:${report || ""}`
: "",
() => {
if (!detail) return "";
const airportCurrent = detail.airport_current || detail.current || {};
const metarSignature =
String(report || "").trim() ||
[
airportCurrent.report_time,
airportCurrent.obs_time_epoch,
airportCurrent.obs_time,
airportCurrent.temp,
]
.filter((part) => part != null && part !== "")
.join("|");
return [
normalizeCityKey(detailCityName),
detail.local_date || "",
locale,
metarSignature,
].join(":");
},
[detail, detailCityName, locale, report],
);
useEffect(() => {
@@ -423,19 +439,19 @@ export function useAiCityForecast({
})
.then((payload) => {
if (!payload) return;
const usablePayload =
payload?.city_forecast
? payload
: buildAiCityFallbackPayload({
detail,
error: payload?.reason || payload?.raw_reason || payload?.status,
isEn,
report,
});
if (usablePayload.status === "ready" && !usablePayload.degraded) {
writeCachedPayload(cacheKey, usablePayload);
}
if (!cancelled) {
const usablePayload =
payload?.city_forecast
? payload
: buildAiCityFallbackPayload({
detail,
error: payload?.reason || payload?.raw_reason || payload?.status,
isEn,
report,
});
if (usablePayload.status === "ready" && !usablePayload.degraded) {
writeCachedPayload(cacheKey, usablePayload);
}
setAiForecast({ payload: usablePayload, status: "ready" });
}
})
@@ -529,7 +545,7 @@ export function useCityMarketScan({
setMarketStatus("loading");
}
void ensureCityMarketScan(detailCityName, false, {
lite: true,
lite: false,
targetDate: detail.local_date || null,
})
.then((payload) => {
-1
View File
@@ -1691,7 +1691,6 @@ def _scan_city_ai_cache_key(ai_input: Dict[str, Any]) -> str:
"model": SCAN_CITY_AI_MODEL,
"city": ai_input.get("city"),
"local_date": ai_input.get("local_date"),
"local_time": ai_input.get("local_time"),
"deb": (ai_input.get("deb") or {}).get("prediction") if isinstance(ai_input.get("deb"), dict) else None,
"metar": (ai_input.get("airport_current") or {}).get("raw_metar") if isinstance(ai_input.get("airport_current"), dict) else None,
"obs": ai_input.get("metar_today_obs") or ai_input.get("metar_recent_obs") or [],