From 963b2098c07cc35d9db781846f91855aa74055e6 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Sun, 26 Apr 2026 04:32:46 +0800 Subject: [PATCH] feat: implement AI city scan proxy route and add premium dark theme dashboard styling --- .../app/api/scan/terminal/ai-city/route.ts | 22 +++++++++++++++---- .../components/dashboard/Dashboard.module.css | 6 ++--- .../dashboard/ScanTerminalDashboard.tsx | 21 +++++++++++++++++- 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/frontend/app/api/scan/terminal/ai-city/route.ts b/frontend/app/api/scan/terminal/ai-city/route.ts index eaf07cf6..64fd5ad0 100644 --- a/frontend/app/api/scan/terminal/ai-city/route.ts +++ b/frontend/app/api/scan/terminal/ai-city/route.ts @@ -6,12 +6,12 @@ import { const API_BASE = process.env.POLYWEATHER_API_BASE_URL; const SCAN_AI_PROXY_TIMEOUT_MS = Math.max( - 35_000, - Number(process.env.POLYWEATHER_SCAN_AI_PROXY_TIMEOUT_MS || "55000") || 55_000, + 85_000, + Number(process.env.POLYWEATHER_SCAN_AI_PROXY_TIMEOUT_MS || "85000") || 85_000, ); export const dynamic = "force-dynamic"; -export const maxDuration = 60; +export const maxDuration = 90; export async function POST(req: NextRequest) { if (!API_BASE) { @@ -30,18 +30,20 @@ export async function POST(req: NextRequest) { let auth: Awaited> | null = null; const controller = new AbortController(); + const startedAt = Date.now(); const timeoutId = setTimeout(() => controller.abort(), SCAN_AI_PROXY_TIMEOUT_MS); + const requestBody = body && typeof body === "object" ? body as Record : {}; try { auth = await buildBackendRequestHeaders(req); const headers = new Headers(auth.headers); headers.set("Content-Type", "application/json"); headers.set("Accept", "application/json"); - const requestBody = body && typeof body === "object" ? body as Record : {}; console.info("[scan-ai-city] proxy request", { city: requestBody.city, force_refresh: requestBody.force_refresh === true, locale: requestBody.locale, + timeout_ms: SCAN_AI_PROXY_TIMEOUT_MS, }); const res = await fetch(`${API_BASE}/api/scan/terminal/ai-city`, { method: "POST", @@ -68,6 +70,7 @@ export async function POST(req: NextRequest) { city: data?.city, model: data?.model, cached: data?.cached === true, + elapsed_ms: Date.now() - startedAt, }); const response = NextResponse.json(data, { headers: { @@ -77,12 +80,23 @@ export async function POST(req: NextRequest) { return applyAuthResponseCookies(response, auth.response); } catch (error) { const timedOut = controller.signal.aborted; + const elapsedMs = Date.now() - startedAt; + console.warn("[scan-ai-city] proxy failed", { + city: requestBody.city, + timed_out: timedOut, + elapsed_ms: elapsedMs, + timeout_ms: SCAN_AI_PROXY_TIMEOUT_MS, + error: String(error), + }); const response = NextResponse.json( { error: timedOut ? "City AI request timed out" : "Failed to fetch city AI data", detail: String(error), + elapsed_ms: elapsedMs, + timeout_ms: SCAN_AI_PROXY_TIMEOUT_MS, + city: requestBody.city, }, { status: timedOut ? 504 : 500 }, ); diff --git a/frontend/components/dashboard/Dashboard.module.css b/frontend/components/dashboard/Dashboard.module.css index 561642b6..d9096e4c 100644 --- a/frontend/components/dashboard/Dashboard.module.css +++ b/frontend/components/dashboard/Dashboard.module.css @@ -11149,9 +11149,9 @@ } .root :global(.scan-ai-city-body) { - padding: 18px; - max-height: clamp(420px, 58vh, 760px); - overflow-y: auto; + padding: 18px 18px 36px; + max-height: none; + overflow: visible; overscroll-behavior: contain; } diff --git a/frontend/components/dashboard/ScanTerminalDashboard.tsx b/frontend/components/dashboard/ScanTerminalDashboard.tsx index 642193bb..cba7f13e 100644 --- a/frontend/components/dashboard/ScanTerminalDashboard.tsx +++ b/frontend/components/dashboard/ScanTerminalDashboard.tsx @@ -652,7 +652,26 @@ function AiPinnedCityCard({ }) .then(async (response) => { if (!response.ok) { - throw new Error(`HTTP ${response.status}`); + let detail = ""; + try { + const errorPayload = await response.json(); + const message = String(errorPayload?.error || "").trim(); + const rawDetail = String(errorPayload?.detail || "").trim(); + const elapsed = Number(errorPayload?.elapsed_ms); + const timeout = Number(errorPayload?.timeout_ms); + detail = [ + message, + rawDetail, + Number.isFinite(elapsed) && Number.isFinite(timeout) + ? `elapsed ${Math.round(elapsed / 1000)}s / timeout ${Math.round(timeout / 1000)}s` + : "", + ] + .filter(Boolean) + .join(" · "); + } catch { + detail = ""; + } + throw new Error(detail ? `HTTP ${response.status} · ${detail}` : `HTTP ${response.status}`); } return response.json() as Promise; })