diff --git a/app/api/auth-settings/route.ts b/app/api/auth-settings/route.ts index 3c0cea6bc..45428ab96 100644 --- a/app/api/auth-settings/route.ts +++ b/app/api/auth-settings/route.ts @@ -4,6 +4,21 @@ import { db } from "@/lib/db"; import { authSettings } from "@/lib/db/schema"; import { eq } from "drizzle-orm"; import { auth } from "@/app/(auth)/auth"; +import { publicJson, withTimeout } from "@/lib/public-api/performance"; + +const PUBLIC_AUTH_SETTINGS_FALLBACK = { + dingTalkAppKey: "", + isDingTalkEnabled: false, + isGoogleEnabled: false, + authUrl: process.env.AUTH_URL || "", + isGoogleConfigured: false, + isRegistrationEnabled: true, + isEmailVerificationEnabled: false, + emailVerificationExpiresMinutes: 10, + isEmailServiceConfigured: false, + dingTalkOrganizations: [], + organizations: [], +}; function jsonNoStore(data: unknown, init?: ResponseInit) { const response = NextResponse.json(data, init); @@ -16,11 +31,16 @@ function jsonNoStore(data: unknown, init?: ResponseInit) { // GET /api/auth-settings - 获取认证配置 export async function GET() { + const startedAt = Date.now(); try { const session = await auth(); const isAdmin = session?.user?.type === "admin"; - const [settings] = await db.select().from(authSettings).limit(1); + const [settings] = await withTimeout( + db.select().from(authSettings).limit(1), + 1600, + "auth-settings" + ); const resolvedGoogleClientId = settings?.googleClientId || process.env.GOOGLE_CLIENT_ID || ""; const resolvedGoogleClientSecret = @@ -100,10 +120,12 @@ export async function GET() { }); } catch (error) { console.error("[Auth Settings API] Get error:", error); - return jsonNoStore( - { error: "Failed to fetch auth settings" }, - { status: 500 } - ); + return publicJson(PUBLIC_AUTH_SETTINGS_FALLBACK, { + cacheStatus: "FALLBACK", + cacheControl: "no-store, no-cache, must-revalidate, proxy-revalidate", + fallback: true, + startedAt, + }); } } diff --git a/app/api/featured/route.ts b/app/api/featured/route.ts index e74521706..6117ec908 100644 --- a/app/api/featured/route.ts +++ b/app/api/featured/route.ts @@ -1,78 +1,101 @@ -import { NextRequest, NextResponse } from "next/server"; +import { NextRequest } from "next/server"; import { db } from "@/lib/db"; import { generatedImage, user, aiModel } from "@/lib/db/schema"; import { eq, desc, sql } from "drizzle-orm"; +import { cachedPublicJson } from "@/lib/public-api/performance"; + +const FEATURED_TIMEOUT_MS = 1800; +const FEATURED_DEFAULT_PAGE_SIZE = 24; +const FEATURED_MAX_PAGE_SIZE = 48; + +function clampPositiveInt(value: string | null, fallback: number, max: number) { + const parsed = Number.parseInt(value || "", 10); + if (!Number.isFinite(parsed) || parsed < 1) { + return fallback; + } + return Math.min(parsed, max); +} // GET: 获取精选图片列表 export async function GET(request: NextRequest) { - try { - const searchParams = request.nextUrl.searchParams; - const page = Number.parseInt(searchParams.get("page") || "1"); - const pageSize = Number.parseInt(searchParams.get("pageSize") || "24"); - const offset = (page - 1) * pageSize; + const searchParams = request.nextUrl.searchParams; + const page = clampPositiveInt(searchParams.get("page"), 1, 500); + const pageSize = clampPositiveInt( + searchParams.get("pageSize"), + FEATURED_DEFAULT_PAGE_SIZE, + FEATURED_MAX_PAGE_SIZE + ); + const offset = (page - 1) * pageSize; + const fallbackData = { + images: [], + pagination: { + page, + pageSize, + total: 0, + totalPages: 0, + }, + }; - // 查询精选图片,关联用户和模型信息 - const featuredImages = await db - .select({ - id: generatedImage.id, - thumbnailUrl: generatedImage.thumbnailUrl, - originalUrl: generatedImage.originalUrl, - imageSize: generatedImage.imageSize, - prompt: generatedImage.prompt, - modelId: generatedImage.modelId, - featuredCategory: generatedImage.featuredCategory, - createdAt: generatedImage.createdAt, - featuredAt: generatedImage.featuredAt, - userId: generatedImage.userId, - userName: user.name, - userEmail: user.email, - userDepartment: user.department, - modelName: aiModel.displayName, - }) - .from(generatedImage) - .leftJoin(user, eq(generatedImage.userId, user.id)) - .leftJoin(aiModel, eq(generatedImage.modelId, aiModel.modelId)) - .where(eq(generatedImage.isFeatured, true)) - .orderBy(desc(generatedImage.featuredAt)) - .limit(pageSize) - .offset(offset); + return cachedPublicJson({ + cacheKey: `featured:${page}:${pageSize}`, + fallbackData, + timeoutMs: FEATURED_TIMEOUT_MS, + load: async () => { + // 查询精选图片,关联用户和模型信息 + const featuredImages = await db + .select({ + id: generatedImage.id, + thumbnailUrl: generatedImage.thumbnailUrl, + originalUrl: generatedImage.originalUrl, + imageSize: generatedImage.imageSize, + prompt: generatedImage.prompt, + modelId: generatedImage.modelId, + featuredCategory: generatedImage.featuredCategory, + createdAt: generatedImage.createdAt, + featuredAt: generatedImage.featuredAt, + userId: generatedImage.userId, + userName: user.name, + userEmail: user.email, + userDepartment: user.department, + modelName: aiModel.displayName, + totalCount: sql`count(*) over()::int`, + }) + .from(generatedImage) + .leftJoin(user, eq(generatedImage.userId, user.id)) + .leftJoin(aiModel, eq(generatedImage.modelId, aiModel.modelId)) + .where(eq(generatedImage.isFeatured, true)) + .orderBy(desc(generatedImage.featuredAt)) + .limit(pageSize) + .offset(offset); - // 获取总数 - const totalResult = await db - .select({ count: sql`count(*)::int` }) - .from(generatedImage) - .where(eq(generatedImage.isFeatured, true)); + const total = featuredImages[0]?.totalCount || 0; + const totalPages = Math.ceil(total / pageSize); - const total = totalResult[0]?.count || 0; - const totalPages = Math.ceil(total / pageSize); - - return NextResponse.json({ - images: featuredImages.map(img => ({ - id: img.id, - thumbnailUrl: img.thumbnailUrl, - originalUrl: img.originalUrl, - imageSize: img.imageSize, - prompt: img.prompt, - modelId: img.modelId, - modelName: img.modelName || img.modelId || "未知模型", - featuredCategory: img.featuredCategory, - createdAt: img.createdAt, - featuredAt: img.featuredAt, - user: { - id: img.userId, - name: img.userName || img.userEmail?.split("@")[0] || "未知用户", - department: img.userDepartment || "未设置部门", + return { + images: featuredImages.map((img) => ({ + id: img.id, + thumbnailUrl: img.thumbnailUrl, + originalUrl: img.originalUrl, + imageSize: img.imageSize, + prompt: img.prompt, + modelId: img.modelId, + modelName: img.modelName || img.modelId || "未知模型", + featuredCategory: img.featuredCategory, + createdAt: img.createdAt, + featuredAt: img.featuredAt, + user: { + id: img.userId, + name: img.userName || img.userEmail?.split("@")[0] || "未知用户", + department: img.userDepartment || "未设置部门", + }, + })), + pagination: { + page, + pageSize, + total, + totalPages, }, - })), - pagination: { - page, - pageSize, - total, - totalPages, - }, - }); - } catch (error) { - console.error("[Featured API] Error:", error); - return NextResponse.json({ error: "Failed to fetch featured images" }, { status: 500 }); - } + }; + }, + }); } diff --git a/app/api/home-animation/route.ts b/app/api/home-animation/route.ts index de1a5028e..8c9f4aaaa 100644 --- a/app/api/home-animation/route.ts +++ b/app/api/home-animation/route.ts @@ -1,39 +1,31 @@ -import { NextResponse } from "next/server"; import { getSystemSetting } from "@/lib/db/queries"; import { HOME_CHAT_ANIMATION_CONFIG_KEY, resolvePublicHomeChatAnimationConfig, } from "@/lib/home/home-animation-config"; +import { cachedPublicJson } from "@/lib/public-api/performance"; export const dynamic = "force-dynamic"; +const FALLBACK_HOME_ANIMATION = { + enabled: false, + fileName: "", + htmlContent: "", + updatedAt: null, + usesDefaultFallback: false, +}; + export async function GET() { - try { - const setting = await getSystemSetting({ - key: HOME_CHAT_ANIMATION_CONFIG_KEY, - }).catch(() => null); + return cachedPublicJson({ + cacheKey: "home-animation", + fallbackData: FALLBACK_HOME_ANIMATION, + timeoutMs: 1600, + load: async () => { + const setting = await getSystemSetting({ + key: HOME_CHAT_ANIMATION_CONFIG_KEY, + }).catch(() => null); - const payload = resolvePublicHomeChatAnimationConfig(setting?.value); - - return NextResponse.json(payload, { - headers: { - "Cache-Control": "no-store, max-age=0", - }, - }); - } catch { - return NextResponse.json( - { - enabled: false, - fileName: "", - htmlContent: "", - updatedAt: null, - usesDefaultFallback: false, - }, - { - headers: { - "Cache-Control": "no-store, max-age=0", - }, - }, - ); - } + return resolvePublicHomeChatAnimationConfig(setting?.value); + }, + }); } diff --git a/app/api/nav-links/route.ts b/app/api/nav-links/route.ts index 3e3891606..04bb55503 100644 --- a/app/api/nav-links/route.ts +++ b/app/api/nav-links/route.ts @@ -4,24 +4,24 @@ import { db } from "@/lib/db"; import { navLinks } from "@/lib/db/schema"; import { eq } from "drizzle-orm"; import { auth } from "@/app/(auth)/auth"; +import { cachedPublicJson } from "@/lib/public-api/performance"; // GET /api/nav-links - 获取所有导航链接 (公开) export async function GET() { - try { - const links = await db - .select() - .from(navLinks) - .where(eq(navLinks.isEnabled, true)) - .orderBy(navLinks.order); - - return NextResponse.json({ links }); - } catch (error) { - console.error("[Nav Links API] Get error:", error); - return NextResponse.json( - { error: "Failed to fetch nav links" }, - { status: 500 } - ); - } + return cachedPublicJson({ + cacheKey: "nav-links", + fallbackData: { links: [] }, + timeoutMs: 1600, + load: async () => { + const links = await db + .select() + .from(navLinks) + .where(eq(navLinks.isEnabled, true)) + .orderBy(navLinks.order); + + return { links }; + }, + }); } const createSchema = z.object({ diff --git a/app/api/site-settings/route.ts b/app/api/site-settings/route.ts index befa3d7f1..46136fefc 100644 --- a/app/api/site-settings/route.ts +++ b/app/api/site-settings/route.ts @@ -116,6 +116,7 @@ import { normalizeEnhanceProKreaConfig, } from "@/lib/enhance-pro/config"; import { STYLE_PRO_CONFIG_KEY, normalizeStyleProConfig } from "@/lib/style-pro/config"; +import { withTimeout } from "@/lib/public-api/performance"; import type { AgentCategoryTreeConfig } from "@/types/agent"; // Historically stored as "home_surprise_categories" but used for the featured gallery categories. @@ -504,13 +505,44 @@ const PUBLIC_SITE_SETTINGS_SCOPES = new Set([ "home", ]); +const HOME_SITE_SETTINGS_FALLBACK: Record = { + siteName: "AI Chatbot", + siteLogo: null, + siteLogoDark: null, + favicon: null, + lovartPromoText: "现在升级,立享 Kling 3.0 与 Nano Banana Pro,最高 5 折优惠", + lovartPromoActionText: "立即升级", + lovartHeroTitle: "由壹点AI 让设计更简单", + lovartHeroSubtitle: "懂你的设计代理,帮你搞定一切", + surpriseCategories: ["全部", "精选"], + canvasDefaultModelId: null, + lovartComposerPrefix: "让Buyi-ai", + lovartComposerPreviewTexts: [], + lovartPromptTagVisibility: {}, + contactConfig: normalizeContactConfig(undefined), + membershipConfig: normalizeMembershipConfig(undefined), + toolbox1Name: "工具箱", + toolbox1Enabled: true, + toolbox2Name: "工具箱2", + toolbox2Enabled: false, + productPlanningEnabled: true, + agentEnabled: true, + videoCanvasEnabled: true, + tutorialEnabled: false, + tutorialVideoUrl: null, + pointsPackages: normalizePointsPackages(undefined), + dulupayConfig: normalizeDulupayForPublic(undefined), + lovartHomeSidebarConfig: normalizeLovartHomeSidebarConfig(undefined), +}; + function createScopedSiteSettingsResponse( scope: PublicSiteSettingsScope, data: Record, - cacheStatus: "HIT" | "MISS" = "MISS", - cacheKey: string = scope + cacheStatus: "HIT" | "MISS" | "STALE" | "FALLBACK" = "MISS", + cacheKey: string = scope, + options: { fallback?: boolean; startedAt?: number } = {} ) { - if (cacheStatus !== "HIT") { + if (cacheStatus === "MISS") { setScopedSiteSettingsCache( cacheKey, data, @@ -527,6 +559,14 @@ function createScopedSiteSettingsResponse( ); response.headers.set("X-Site-Settings-Scope", scope); response.headers.set("X-Site-Settings-Cache", cacheStatus); + response.headers.set("X-Cache", cacheStatus); + response.headers.set("X-Fallback", options.fallback ? "1" : "0"); + if (options.startedAt) { + response.headers.set( + "Server-Timing", + `app;dur=${Math.max(0, Date.now() - options.startedAt)}` + ); + } return response; } @@ -883,6 +923,7 @@ async function getAgentWorkflowAdminSettingsResponse() { // GET /api/site-settings - 获取网站设置 (公开) export async function GET(request: NextRequest) { try { + const startedAt = Date.now(); const scope = request.nextUrl.searchParams.get("scope") || ""; if (PUBLIC_SITE_SETTINGS_SCOPES.has(scope)) { const scopedScope = scope as PublicSiteSettingsScope; @@ -902,14 +943,33 @@ export async function GET(request: NextRequest) { cacheKey ); } - if (cached) { + if (cached && scopedScope !== "home") { deleteScopedSiteSettingsCache(cacheKey); } - return getScopedPublicSiteSettings(scopedScope, { - brandName, - cacheKey, - }); + try { + const responsePromise = getScopedPublicSiteSettings(scopedScope, { + brandName, + cacheKey, + }); + return scopedScope === "home" + ? await withTimeout(responsePromise, 1600, "site-settings:home") + : await responsePromise; + } catch (error) { + if (scopedScope !== "home") { + throw error; + } + + const message = error instanceof Error ? error.message : String(error); + console.warn(`[Site Settings API] Home fallback: ${message}`); + return createScopedSiteSettingsResponse( + scopedScope, + cached?.data ?? HOME_SITE_SETTINGS_FALLBACK, + cached ? "STALE" : "FALLBACK", + cacheKey, + { fallback: true, startedAt } + ); + } } const session = await auth().catch(() => null); diff --git a/app/api/tutorial-videos/route.ts b/app/api/tutorial-videos/route.ts index 4e10ddecd..441a4662b 100644 --- a/app/api/tutorial-videos/route.ts +++ b/app/api/tutorial-videos/route.ts @@ -4,24 +4,24 @@ import { db } from "@/lib/db"; import { tutorialVideos } from "@/lib/db/schema"; import { eq, asc } from "drizzle-orm"; import { auth } from "@/app/(auth)/auth"; +import { cachedPublicJson } from "@/lib/public-api/performance"; // GET /api/tutorial-videos - 获取所有视频教程 (公开) export async function GET() { - try { - const videos = await db - .select() - .from(tutorialVideos) - .where(eq(tutorialVideos.isEnabled, true)) - .orderBy(asc(tutorialVideos.order)); - - return NextResponse.json({ videos }); - } catch (error) { - console.error("[Tutorial Videos API] Get error:", error); - return NextResponse.json( - { error: "Failed to fetch tutorial videos" }, - { status: 500 } - ); - } + return cachedPublicJson({ + cacheKey: "tutorial-videos", + fallbackData: { videos: [] }, + timeoutMs: 1600, + load: async () => { + const videos = await db + .select() + .from(tutorialVideos) + .where(eq(tutorialVideos.isEnabled, true)) + .orderBy(asc(tutorialVideos.order)); + + return { videos }; + }, + }); } const createSchema = z.object({ diff --git a/components/home/lovart-home.tsx b/components/home/lovart-home.tsx index 45672ae54..a335a77df 100644 --- a/components/home/lovart-home.tsx +++ b/components/home/lovart-home.tsx @@ -478,7 +478,9 @@ export function LovartHome({ const [typedPreviewText, setTypedPreviewText] = useState(""); const [previewCursorVisible, setPreviewCursorVisible] = useState(true); const [isSecondaryDataReady, setIsSecondaryDataReady] = useState(false); + const [isFeaturedDataReady, setIsFeaturedDataReady] = useState(false); const projectSectionRef = useRef(null); + const featuredSectionRef = useRef(null); const featuredLoadMoreRef = useRef(null); const filesRef = useRef([]); @@ -506,6 +508,35 @@ export function LovartHome({ return () => window.clearTimeout(timer); }, []); + useEffect(() => { + if (typeof window === "undefined" || isFeaturedDataReady) { + return; + } + + const loadFeaturedData = () => setIsFeaturedDataReady(true); + const fallbackTimer = window.setTimeout(loadFeaturedData, 2500); + + if (!featuredSectionRef.current || !("IntersectionObserver" in window)) { + return () => window.clearTimeout(fallbackTimer); + } + + const observer = new IntersectionObserver( + (entries) => { + if (entries.some((entry) => entry.isIntersecting)) { + loadFeaturedData(); + } + }, + { rootMargin: "900px 0px 900px 0px" } + ); + + observer.observe(featuredSectionRef.current); + + return () => { + window.clearTimeout(fallbackTimer); + observer.disconnect(); + }; + }, [isFeaturedDataReady]); + const { data: canvasData, mutate: mutateCanvas } = useSWR<{ success: boolean; data: CanvasProject[]; @@ -514,7 +545,7 @@ export function LovartHome({ const { data: featuredPages, size: featuredPageCount, setSize: setFeaturedPageCount, isValidating: isFeaturedValidating } = useSWRInfinite( (pageIndex, previousPageData) => { - if (!isSecondaryDataReady) { + if (!isSecondaryDataReady || !isFeaturedDataReady) { return null; } if ( @@ -635,7 +666,7 @@ export function LovartHome({ ); const { data: authSettingsData } = useSWR( - isSecondaryDataReady ? "/api/auth-settings" : null, + loginDialogOpen ? "/api/auth-settings" : null, fetcher ); const { data: homeAnimationData } = useSWR<{ @@ -2315,7 +2346,7 @@ export function LovartHome({ {!isProjectsOnlyView && ( -
+

灵感发现

diff --git a/lib/db/index.ts b/lib/db/index.ts index 9233938e8..58e16bfb5 100644 --- a/lib/db/index.ts +++ b/lib/db/index.ts @@ -1,7 +1,12 @@ import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import * as schema from "./schema"; +import { getEnvInt } from "@/lib/public-api/performance"; -const client = postgres(process.env.POSTGRES_URL!); +const client = postgres(process.env.POSTGRES_URL!, { + connect_timeout: getEnvInt("POSTGRES_CONNECT_TIMEOUT_SECONDS", 5, 1, 60), + idle_timeout: getEnvInt("POSTGRES_IDLE_TIMEOUT_SECONDS", 20, 1, 300), + max: getEnvInt("POSTGRES_MAX_CONNECTIONS", 10, 1, 50), + prepare: false, +}); export const db = drizzle(client, { schema }); - diff --git a/lib/db/migrations/0083_optimize_featured_home_query.sql b/lib/db/migrations/0083_optimize_featured_home_query.sql new file mode 100644 index 000000000..0026fd77e --- /dev/null +++ b/lib/db/migrations/0083_optimize_featured_home_query.sql @@ -0,0 +1,3 @@ +CREATE INDEX IF NOT EXISTS "GeneratedImage_featured_featuredAt_idx" +ON "GeneratedImage" ("isFeatured", "featuredAt" DESC) +WHERE "isFeatured" = true; diff --git a/lib/db/migrations/meta/_journal.json b/lib/db/migrations/meta/_journal.json index 0ffa0cf27..a7861865e 100644 --- a/lib/db/migrations/meta/_journal.json +++ b/lib/db/migrations/meta/_journal.json @@ -484,6 +484,13 @@ "when": 1779267679691, "tag": "0082_add_client_performance_events", "breakpoints": true + }, + { + "idx": 69, + "version": "7", + "when": 1779797137842, + "tag": "0083_optimize_featured_home_query", + "breakpoints": true } ] } diff --git a/lib/public-api/performance.ts b/lib/public-api/performance.ts new file mode 100644 index 000000000..d46bae8db --- /dev/null +++ b/lib/public-api/performance.ts @@ -0,0 +1,143 @@ +import { NextResponse } from "next/server"; + +type CacheStatus = "HIT" | "MISS" | "STALE" | "FALLBACK"; + +type PublicCacheEntry = { + data: T; + expiresAt: number; + staleUntil: number; +}; + +const PUBLIC_API_CACHE_KEY = Symbol.for("ai-chatbot.public-api-cache"); + +function getStore() { + const globalCache = globalThis as typeof globalThis & { + [PUBLIC_API_CACHE_KEY]?: Map>; + }; + + if (!globalCache[PUBLIC_API_CACHE_KEY]) { + globalCache[PUBLIC_API_CACHE_KEY] = new Map>(); + } + + return globalCache[PUBLIC_API_CACHE_KEY]!; +} + +export function getEnvInt( + name: string, + fallback: number, + min: number, + max: number +) { + const value = Number.parseInt(process.env[name] || "", 10); + if (!Number.isFinite(value)) { + return fallback; + } + return Math.min(max, Math.max(min, value)); +} + +export function withTimeout( + promise: Promise, + timeoutMs: number, + label = "operation" +) { + let timer: ReturnType | undefined; + const timeout = new Promise((_, reject) => { + timer = setTimeout(() => { + reject(new Error(`${label} timed out after ${timeoutMs}ms`)); + }, timeoutMs); + }); + + return Promise.race([promise, timeout]).finally(() => { + if (timer) { + clearTimeout(timer); + } + }); +} + +export function publicJson( + data: unknown, + { + cacheStatus, + cacheControl = "private, max-age=30, stale-while-revalidate=120", + fallback = false, + startedAt, + status, + }: { + cacheStatus: CacheStatus; + cacheControl?: string; + fallback?: boolean; + startedAt: number; + status?: number; + } +) { + const durationMs = Math.max(0, Date.now() - startedAt); + const response = NextResponse.json(data, status ? { status } : undefined); + response.headers.set("Cache-Control", cacheControl); + response.headers.set("X-Cache", cacheStatus); + response.headers.set("X-Fallback", fallback ? "1" : "0"); + response.headers.set("Server-Timing", `app;dur=${durationMs}`); + return response; +} + +export async function cachedPublicJson>({ + cacheKey, + fallbackData, + load, + timeoutMs = 1800, + ttlMs = 30_000, + staleMs = 5 * 60_000, + cacheControl, +}: { + cacheKey: string; + fallbackData: T; + load: () => Promise; + timeoutMs?: number; + ttlMs?: number; + staleMs?: number; + cacheControl?: string; +}) { + const startedAt = Date.now(); + const store = getStore(); + const cached = store.get(cacheKey); + + if (cached && cached.expiresAt > startedAt) { + return publicJson(cached.data, { + cacheStatus: "HIT", + cacheControl, + startedAt, + }); + } + + try { + const data = await withTimeout(load(), timeoutMs, cacheKey); + store.set(cacheKey, { + data, + expiresAt: Date.now() + ttlMs, + staleUntil: Date.now() + ttlMs + staleMs, + }); + return publicJson(data, { + cacheStatus: "MISS", + cacheControl, + startedAt, + }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.warn(`[Public API] ${cacheKey} fallback: ${message}`); + + if (cached && cached.staleUntil > Date.now()) { + return publicJson(cached.data, { + cacheStatus: "STALE", + cacheControl, + fallback: true, + startedAt, + }); + } + + return publicJson(fallbackData, { + cacheStatus: "FALLBACK", + cacheControl, + fallback: true, + startedAt, + }); + } +} diff --git a/proxy.ts b/proxy.ts index ed9570a59..b2d053ab0 100644 --- a/proxy.ts +++ b/proxy.ts @@ -252,6 +252,14 @@ export async function proxy(request: NextRequest) { return NextResponse.next(); } + // 允许匿名访问新版首页的轻量渐进配置接口 + if ( + request.method === "GET" && + ["/api/nav-links", "/api/tutorial-videos", "/api/home-animation"].includes(pathname) + ) { + return NextResponse.next(); + } + // 允许访问钉钉回调页面(public 文件夹中的文件) if (pathname === "/dingtalk-callback.html") { return NextResponse.next();