ai-chatbot/app/layout.tsx
Your Name 54c6b4a74e
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
fix: prevent agent workspace loading deadlock
2026-05-18 18:49:12 +08:00

204 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { Metadata } from "next";
import { Toaster } from "sonner";
import { ThemeProvider } from "@/components/theme-provider";
import { FaviconUpdater } from "@/components/favicon-updater";
import { ImageDeliveryOptimizer } from "@/components/image-delivery-optimizer";
import "./globals.css";
import { SessionProvider } from "next-auth/react";
import { getSiteSettings } from "@/lib/db/queries";
export async function generateMetadata(): Promise<Metadata> {
const settings = await getSiteSettings();
const updatedAt =
"updatedAt" in settings && settings.updatedAt
? new Date(settings.updatedAt).getTime()
: "default";
const faviconUrl = `/api/favicon?v=${updatedAt}`;
const metadataBase = process.env.AUTH_URL
? new URL(process.env.AUTH_URL)
: undefined;
return {
metadataBase,
title: {
template: `%s | ${settings.siteName}`,
default: settings.siteName,
},
description: settings.siteName,
icons: {
icon: [
{ url: faviconUrl, sizes: "any" },
{ url: faviconUrl, type: "image/x-icon" },
],
apple: faviconUrl,
shortcut: faviconUrl,
},
};
}
export const viewport = {
maximumScale: 1, // Disable auto-zoom on mobile Safari
};
const LIGHT_THEME_COLOR = "hsl(0 0% 100%)";
const DARK_THEME_COLOR = "hsl(240deg 10% 3.92%)";
const THEME_COLOR_SCRIPT = `\
(function() {
var html = document.documentElement;
var meta = document.querySelector('meta[name="theme-color"]');
if (!meta) {
meta = document.createElement('meta');
meta.setAttribute('name', 'theme-color');
document.head.appendChild(meta);
}
function updateThemeColor() {
var isDark = html.classList.contains('dark');
meta.setAttribute('content', isDark ? '${DARK_THEME_COLOR}' : '${LIGHT_THEME_COLOR}');
}
var observer = new MutationObserver(updateThemeColor);
observer.observe(html, { attributes: true, attributeFilter: ['class'] });
updateThemeColor();
})();`;
const CHUNK_ERROR_RECOVERY_SCRIPT = `\
(function() {
// 安全地访问sessionStorage(某些浏览器设置可能禁止访问)
function safeGetSessionStorage(key) {
try {
return sessionStorage.getItem(key);
} catch (e) {
return null;
}
}
function safeSetSessionStorage(key, value) {
try {
sessionStorage.setItem(key, value);
return true;
} catch (e) {
return false;
}
}
function safeRemoveSessionStorage(key) {
try {
sessionStorage.removeItem(key);
return true;
} catch (e) {
return false;
}
}
var RECOVERY_KEY = 'chunk-reload-attempted-v2';
var lastReloadAt = Number(safeGetSessionStorage(RECOVERY_KEY) || '0') || 0;
function shouldAutoRecover(message) {
if (!message) return false;
var normalized = String(message).toLowerCase();
return (
normalized.indexOf('chunkloaderror') !== -1 ||
normalized.indexOf('failed to load chunk') !== -1 ||
normalized.indexOf('loading chunk') !== -1 ||
normalized.indexOf('failed to fetch dynamically imported module') !== -1 ||
normalized.indexOf('error loading dynamically imported module') !== -1 ||
normalized.indexOf('importing a module script failed') !== -1 ||
normalized.indexOf('module script load failed') !== -1 ||
normalized.indexOf('/_next/static/chunks/') !== -1 ||
normalized.indexOf('failed to find server action') !== -1 ||
normalized.indexOf('server components render') !== -1 ||
normalized.indexOf('application error: a client-side exception') !== -1
);
}
function triggerAutoRecover(reason) {
var now = Date.now();
if (lastReloadAt && now - lastReloadAt < 30000) return;
lastReloadAt = now;
console.warn('[RuntimeRecover] 检测到前端运行时异常,自动刷新页面...', reason || '');
safeSetSessionStorage(RECOVERY_KEY, String(now));
setTimeout(function() {
window.location.reload();
}, 800);
}
window.addEventListener('error', function(event) {
var message = '';
if (event && event.message) message += event.message;
if (event && event.filename) message += ' ' + event.filename;
if (event && event.error && event.error.message) {
message += ' ' + event.error.message;
}
if (shouldAutoRecover(message)) {
triggerAutoRecover(message);
}
}, true);
window.addEventListener('unhandledrejection', function(event) {
var reason = event && event.reason;
var reasonText = '';
if (typeof reason === 'string') reasonText = reason;
else if (reason && typeof reason.message === 'string') reasonText = reason.message;
if (shouldAutoRecover(reasonText)) {
triggerAutoRecover(reasonText);
}
});
// 页面稳定一段时间后清除标记,避免旧资源异常无限刷新。
window.addEventListener('load', function() {
setTimeout(function() {
safeRemoveSessionStorage(RECOVERY_KEY);
}, 120000);
});
})();`;
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
// 获取网站设置中的默认主题
const settings = await getSiteSettings();
const defaultTheme = settings.defaultTheme || "system";
return (
<html
// `next-themes` injects an extra classname to the body element to avoid
// visual flicker before hydration. Hence the `suppressHydrationWarning`
// prop is necessary to avoid the React hydration mismatch warning.
// https://github.com/pacocoursey/next-themes?tab=readme-ov-file#with-app
lang="zh-CN"
suppressHydrationWarning
>
<head>
<script
// biome-ignore lint/security/noDangerouslySetInnerHtml: "Required"
dangerouslySetInnerHTML={{
__html: THEME_COLOR_SCRIPT,
}}
/>
<script
// biome-ignore lint/security/noDangerouslySetInnerHtml: "Required"
dangerouslySetInnerHTML={{
__html: CHUNK_ERROR_RECOVERY_SCRIPT,
}}
/>
</head>
<body className="antialiased">
<FaviconUpdater />
<ImageDeliveryOptimizer />
<ThemeProvider
attribute="class"
defaultTheme={defaultTheme}
disableTransitionOnChange
enableSystem
>
<Toaster position="top-center" />
<SessionProvider>{children}</SessionProvider>
</ThemeProvider>
</body>
</html>
);
}