秒开 AI 预测最高温:preview 事件提前下发确定性预测值
之前 predicted_max 只在 AI 流式 final 事件返回后才显示(10-25 秒), 但后端 fallback 早在 1ms 内就算好了 cluster median / DEB 中枢。 现在 preview 事件同步下发确定性 predicted_max + range_low/high, 前端也在 fallback payload 中自动从 multi_model + DEB 计算预测值, 用户点击城市后 ~100ms 即可看到 AI 预测最高温。
This commit is contained in:
@@ -146,6 +146,40 @@ function getAiCityStreamProgressText(
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function computeFallbackPredictedMax(detail: CityDetail | null): number | null {
|
||||||
|
if (!detail) return null;
|
||||||
|
const multiModel = detail.multi_model ?? {};
|
||||||
|
const entries = Object.entries(multiModel).filter(
|
||||||
|
([, v]) => v != null && Number.isFinite(v),
|
||||||
|
) as [string, number][];
|
||||||
|
const values = entries.map(([, v]) => v);
|
||||||
|
const debValue = detail.deb?.prediction ?? null;
|
||||||
|
const nonDebValues = entries
|
||||||
|
.filter(([name]) => !name.toLowerCase().includes("deb"))
|
||||||
|
.map(([, v]) => v);
|
||||||
|
const sorted = [...nonDebValues].sort((a, b) => a - b);
|
||||||
|
const clusterMedian =
|
||||||
|
sorted.length > 0 ? sorted[Math.floor(sorted.length / 2)] : null;
|
||||||
|
let predicted = clusterMedian;
|
||||||
|
if (predicted == null && debValue != null && Number.isFinite(debValue))
|
||||||
|
predicted = debValue;
|
||||||
|
if (predicted == null && values.length > 0)
|
||||||
|
predicted = values.reduce((a, b) => a + b, 0) / values.length;
|
||||||
|
if (predicted == null) {
|
||||||
|
const isHkoObservation = isHkoObservationCity(detail);
|
||||||
|
const currentTemp =
|
||||||
|
(isHkoObservation
|
||||||
|
? detail?.current?.temp
|
||||||
|
: detail?.airport_current?.temp ??
|
||||||
|
detail?.airport_primary?.temp ??
|
||||||
|
detail?.current?.temp) ?? null;
|
||||||
|
predicted = currentTemp != null && Number.isFinite(currentTemp)
|
||||||
|
? currentTemp
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
return predicted;
|
||||||
|
}
|
||||||
|
|
||||||
export function buildAiCityFallbackPayload({
|
export function buildAiCityFallbackPayload({
|
||||||
detail,
|
detail,
|
||||||
error,
|
error,
|
||||||
@@ -195,6 +229,12 @@ export function buildAiCityFallbackPayload({
|
|||||||
const reasonZh = `DEB、多模型集合和最新${sourceZh}已足够给出当前方向判断;页面会在 DeepSeek 返回后合并完整机场报文解读。`;
|
const reasonZh = `DEB、多模型集合和最新${sourceZh}已足够给出当前方向判断;页面会在 DeepSeek 返回后合并完整机场报文解读。`;
|
||||||
const reasonEn = `DEB, the model cluster and latest ${sourceEn} are enough for the current directional read; the page will merge the full airport-bulletin read when DeepSeek returns.`;
|
const reasonEn = `DEB, the model cluster and latest ${sourceEn} are enough for the current directional read; the page will merge the full airport-bulletin read when DeepSeek returns.`;
|
||||||
|
|
||||||
|
const fallbackPredictedMax = computeFallbackPredictedMax(detail);
|
||||||
|
const multiModel = detail?.multi_model ?? {};
|
||||||
|
const modelValues = Object.values(multiModel).filter(
|
||||||
|
(v): v is number => v != null && Number.isFinite(v),
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
city_forecast: {
|
city_forecast: {
|
||||||
confidence: "low",
|
confidence: "low",
|
||||||
@@ -204,9 +244,9 @@ export function buildAiCityFallbackPayload({
|
|||||||
metar_read_zh: metarZh,
|
metar_read_zh: metarZh,
|
||||||
model_cluster_note_en: "",
|
model_cluster_note_en: "",
|
||||||
model_cluster_note_zh: "",
|
model_cluster_note_zh: "",
|
||||||
predicted_max: null,
|
predicted_max: fallbackPredictedMax,
|
||||||
range_high: null,
|
range_high: modelValues.length > 0 ? Math.max(...modelValues) : null,
|
||||||
range_low: null,
|
range_low: modelValues.length > 0 ? Math.min(...modelValues) : null,
|
||||||
reasoning_en: reasonEn,
|
reasoning_en: reasonEn,
|
||||||
reasoning_zh: reasonZh,
|
reasoning_zh: reasonZh,
|
||||||
risks_en: [],
|
risks_en: [],
|
||||||
@@ -251,6 +291,60 @@ export function buildAiCityLoadingForecastState({
|
|||||||
return loadingState;
|
return loadingState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasPreviewQuantFields(progress: AiCityStreamProgress) {
|
||||||
|
return (
|
||||||
|
progress.predicted_max != null ||
|
||||||
|
progress.range_low != null ||
|
||||||
|
progress.range_high != null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergePreviewQuantPayload(
|
||||||
|
current: AiCityForecastState,
|
||||||
|
progress: AiCityStreamProgress,
|
||||||
|
): AiCityForecastPayload {
|
||||||
|
const prev = current.payload ?? {};
|
||||||
|
const prevCf = prev.city_forecast ?? {};
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
status: prev.status ?? "ready",
|
||||||
|
city_forecast: {
|
||||||
|
...prevCf,
|
||||||
|
...(progress.predicted_max != null
|
||||||
|
? { predicted_max: progress.predicted_max }
|
||||||
|
: {}),
|
||||||
|
...(progress.range_low != null
|
||||||
|
? { range_low: progress.range_low }
|
||||||
|
: {}),
|
||||||
|
...(progress.range_high != null
|
||||||
|
? { range_high: progress.range_high }
|
||||||
|
: {}),
|
||||||
|
...(progress.confidence != null
|
||||||
|
? { confidence: progress.confidence }
|
||||||
|
: {}),
|
||||||
|
...(progress.unit != null ? { unit: progress.unit } : {}),
|
||||||
|
...(progress.metar_read_zh != null
|
||||||
|
? { metar_read_zh: progress.metar_read_zh }
|
||||||
|
: {}),
|
||||||
|
...(progress.metar_read_en != null
|
||||||
|
? { metar_read_en: progress.metar_read_en }
|
||||||
|
: {}),
|
||||||
|
...(progress.final_judgment_zh != null
|
||||||
|
? { final_judgment_zh: progress.final_judgment_zh }
|
||||||
|
: {}),
|
||||||
|
...(progress.final_judgment_en != null
|
||||||
|
? { final_judgment_en: progress.final_judgment_en }
|
||||||
|
: {}),
|
||||||
|
...(progress.model_cluster_note_zh != null
|
||||||
|
? { model_cluster_note_zh: progress.model_cluster_note_zh }
|
||||||
|
: {}),
|
||||||
|
...(progress.model_cluster_note_en != null
|
||||||
|
? { model_cluster_note_en: progress.model_cluster_note_en }
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function buildAiCityProgressForecastState({
|
export function buildAiCityProgressForecastState({
|
||||||
cacheKey,
|
cacheKey,
|
||||||
current,
|
current,
|
||||||
@@ -263,25 +357,36 @@ export function buildAiCityProgressForecastState({
|
|||||||
progress: AiCityStreamProgress;
|
progress: AiCityStreamProgress;
|
||||||
}) {
|
}) {
|
||||||
const progressText = getAiCityStreamProgressText(progress, isEn);
|
const progressText = getAiCityStreamProgressText(progress, isEn);
|
||||||
if (!progressText) return null;
|
const hasQuant = hasPreviewQuantFields(progress);
|
||||||
|
if (!progressText && !hasQuant) return null;
|
||||||
const cachedProgressState = readCachedAiForecastState(cacheKey);
|
const cachedProgressState = readCachedAiForecastState(cacheKey);
|
||||||
const nextStreamText =
|
const nextStreamText =
|
||||||
progress.stage === "calling_ai" && cachedProgressState?.streamText
|
progress.stage === "calling_ai" && cachedProgressState?.streamText
|
||||||
? cachedProgressState.streamText
|
? cachedProgressState.streamText
|
||||||
: progressText;
|
: progressText || cachedProgressState?.streamText || "";
|
||||||
|
const nextPayload = hasQuant
|
||||||
|
? mergePreviewQuantPayload(
|
||||||
|
cachedProgressState ?? current,
|
||||||
|
progress,
|
||||||
|
)
|
||||||
|
: cachedProgressState?.payload ?? current.payload;
|
||||||
const cachedNextState: AiCityForecastState = {
|
const cachedNextState: AiCityForecastState = {
|
||||||
...cachedProgressState,
|
...cachedProgressState,
|
||||||
status: "loading",
|
status: "loading",
|
||||||
streamText: nextStreamText,
|
streamText: nextStreamText,
|
||||||
|
payload: nextPayload,
|
||||||
};
|
};
|
||||||
writeCachedAiForecastState(cacheKey, cachedNextState);
|
writeCachedAiForecastState(cacheKey, cachedNextState);
|
||||||
return {
|
return {
|
||||||
...current,
|
...current,
|
||||||
status: "loading",
|
status: "loading" as const,
|
||||||
streamText:
|
streamText:
|
||||||
progress.stage === "calling_ai" && current.streamText
|
progress.stage === "calling_ai" && current.streamText
|
||||||
? current.streamText
|
? current.streamText
|
||||||
: progressText,
|
: progressText || current.streamText || "",
|
||||||
|
payload: hasQuant
|
||||||
|
? mergePreviewQuantPayload(current, progress)
|
||||||
|
: current.payload,
|
||||||
} satisfies AiCityForecastState;
|
} satisfies AiCityForecastState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,14 @@ export type AiCityStreamProgress = {
|
|||||||
final_judgment_zh?: string | null;
|
final_judgment_zh?: string | null;
|
||||||
metar_read_en?: string | null;
|
metar_read_en?: string | null;
|
||||||
metar_read_zh?: string | null;
|
metar_read_zh?: string | null;
|
||||||
|
model_cluster_note_en?: string | null;
|
||||||
|
model_cluster_note_zh?: string | null;
|
||||||
raw_length?: number | null;
|
raw_length?: number | null;
|
||||||
|
predicted_max?: number | null;
|
||||||
|
range_low?: number | null;
|
||||||
|
range_high?: number | null;
|
||||||
|
confidence?: string | null;
|
||||||
|
unit?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const scanTerminalQueryPolicy = {
|
export const scanTerminalQueryPolicy = {
|
||||||
|
|||||||
@@ -635,6 +635,11 @@ def stream_scan_city_ai_forecast_payload(
|
|||||||
"final_judgment_en": preview_raw.get("final_judgment_en"),
|
"final_judgment_en": preview_raw.get("final_judgment_en"),
|
||||||
"model_cluster_note_zh": preview_raw.get("model_cluster_note_zh"),
|
"model_cluster_note_zh": preview_raw.get("model_cluster_note_zh"),
|
||||||
"model_cluster_note_en": preview_raw.get("model_cluster_note_en"),
|
"model_cluster_note_en": preview_raw.get("model_cluster_note_en"),
|
||||||
|
"predicted_max": preview_raw.get("predicted_max"),
|
||||||
|
"range_low": preview_raw.get("range_low"),
|
||||||
|
"range_high": preview_raw.get("range_high"),
|
||||||
|
"confidence": preview_raw.get("confidence"),
|
||||||
|
"unit": preview_raw.get("unit"),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
yield _sse_event(
|
yield _sse_event(
|
||||||
|
|||||||
Reference in New Issue
Block a user