feat: implement AI city forecast and market scan integration for terminal dashboard

This commit is contained in:
2569718930@qq.com
2026-04-26 11:08:58 +08:00
parent e374ad4bf0
commit 9b77a3a0e2
7 changed files with 117 additions and 26 deletions
+45
View File
@@ -0,0 +1,45 @@
"use client";
import {
getSupabaseBrowserClient,
hasSupabasePublicEnv,
} from "@/lib/supabase/client";
const PUBLIC_BACKEND_API_BASE_URL =
process.env.NEXT_PUBLIC_POLYWEATHER_API_BASE_URL?.trim() || "";
export function hasDirectBackendApiBaseUrl() {
return Boolean(PUBLIC_BACKEND_API_BASE_URL);
}
export function resolveBackendApiUrl(path: string) {
if (!PUBLIC_BACKEND_API_BASE_URL || /^https?:\/\//i.test(path)) {
return path;
}
const base = PUBLIC_BACKEND_API_BASE_URL.replace(/\/+$/, "");
const suffix = path.startsWith("/") ? path : `/${path}`;
return `${base}${suffix}`;
}
export async function buildBrowserBackendHeaders(init?: HeadersInit) {
const headers = new Headers(init);
if (!headers.has("Accept")) {
headers.set("Accept", "application/json");
}
if (!headers.has("Authorization") && hasSupabasePublicEnv()) {
try {
const {
data: { session },
} = await getSupabaseBrowserClient().auth.getSession();
const accessToken = session?.access_token || "";
if (accessToken) {
headers.set("Authorization", `Bearer ${accessToken}`);
}
} catch {
// Direct backend mode must also work for public/optional-auth dashboards.
}
}
return headers;
}
+16 -8
View File
@@ -9,6 +9,10 @@ import {
ScanTerminalFilters,
ScanTerminalResponse,
} from "@/lib/dashboard-types";
import {
buildBrowserBackendHeaders,
resolveBackendApiUrl,
} from "@/lib/backend-api";
export type AssistantOpportunityContext = {
city_name: string;
@@ -108,11 +112,15 @@ async function fetchJson<T>(url: string, options?: { timeoutMs?: number }): Prom
const timeoutId = controller
? window.setTimeout(() => controller.abort(), timeoutMs)
: null;
const requestUrl = resolveBackendApiUrl(url);
const headers = await buildBrowserBackendHeaders({
Accept: "application/json",
});
let response: Response;
try {
response = await fetch(url, {
headers: { Accept: "application/json" },
response = await fetch(requestUrl, {
headers,
cache: "no-store",
signal: controller?.signal,
});
@@ -457,18 +465,18 @@ export const dashboardClient = {
if (existing) {
return existing;
}
const request = fetch("/api/scan/terminal/ai", {
const request = buildBrowserBackendHeaders({
Accept: "application/json",
"Content-Type": "application/json",
}).then((headers) => fetch(resolveBackendApiUrl("/api/scan/terminal/ai"), {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
headers,
cache: "no-store",
body: JSON.stringify({
filters: payload.filters,
snapshot_id: snapshotId || null,
}),
})
}))
.then(async (response) => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);