perf: trim public site settings payloads
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-04-30 01:31:33 +08:00
parent 77a32a4d60
commit 3249b19f56
16 changed files with 397 additions and 21 deletions

View File

@ -217,7 +217,7 @@ export default function Page() {
console.log("=".repeat(50));
// 获取网站设置(添加时间戳防止缓存)
fetch(`/api/site-settings?t=${Date.now()}`)
fetch(`/api/site-settings?scope=auth&t=${Date.now()}`)
.then((res) => res.json())
.then((data) => {
console.log("[Login] Site settings data:", data);

View File

@ -41,7 +41,7 @@ export default function Page() {
.catch(console.error);
// 获取网站设置(添加时间戳防止缓存)
fetch(`/api/site-settings?t=${Date.now()}`)
fetch(`/api/site-settings?scope=branding&t=${Date.now()}`)
.then((res) => res.json())
.then((data) => {
if (data.siteName) {

View File

@ -440,9 +440,338 @@ function normalizeAgentCategoryTreeConfig(value: unknown): AgentCategoryTreeConf
};
}
type PublicSiteSettingsScope =
| "agent-brand-model"
| "agent-canvas"
| "agent-presets"
| "auth"
| "branding"
| "canvas"
| "canvas-chat"
| "chat"
| "home";
const PUBLIC_SITE_SETTINGS_SCOPES = new Set<string>([
"agent-brand-model",
"agent-canvas",
"agent-presets",
"auth",
"branding",
"canvas",
"canvas-chat",
"chat",
"home",
]);
function createScopedSiteSettingsResponse(data: Record<string, unknown>) {
const response = NextResponse.json(data);
// Lightweight public settings change rarely; keep them browser-local briefly
// so page opens do not repeatedly wait on the large full settings endpoint.
response.headers.set(
"Cache-Control",
"private, max-age=30, stale-while-revalidate=120"
);
return response;
}
async function readScopedSiteSettings(keys: string[]) {
const [settings, settingRows] = await Promise.all([
db
.select()
.from(siteSettings)
.limit(1)
.then((rows) => rows[0]),
keys.length > 0
? db
.select()
.from(systemSettings)
.where(inArray(systemSettings.key, keys))
: Promise.resolve([]),
]);
return {
settings,
valueByKey: new Map(settingRows.map((row) => [row.key, row.value])),
};
}
function createBrandingSettings(settings: Record<string, unknown> | undefined) {
return {
siteName: String(settings?.siteName || "AI Chatbot"),
siteLogo: settings?.siteLogo || null,
siteLogoDark: settings?.siteLogoDark || null,
favicon: settings?.favicon || null,
};
}
async function getScopedPublicSiteSettings(scope: PublicSiteSettingsScope) {
if (scope === "auth") {
const { settings, valueByKey } = await readScopedSiteSettings([
"login_notification",
]);
return createScopedSiteSettingsResponse({
...createBrandingSettings(settings as Record<string, unknown> | undefined),
notification: valueByKey.get("login_notification") || {
enabled: false,
title: "",
content: "",
},
});
}
if (scope === "agent-brand-model") {
const { valueByKey } = await readScopedSiteSettings([
AGENT_WORKFLOW_BRAND_MODEL_CONFIG_KEY,
]);
return createScopedSiteSettingsResponse({
agentWorkflowBrandModelConfig: normalizeAgentWorkflowBrandModelConfig(
valueByKey.get(AGENT_WORKFLOW_BRAND_MODEL_CONFIG_KEY)
),
});
}
if (scope === "agent-presets") {
const { valueByKey } = await readScopedSiteSettings([
AGENT_PRESET_LIBRARY_CONFIG_KEY,
AGENT_WORKFLOW_SHOWCASE_CONFIG_KEY,
]);
return createScopedSiteSettingsResponse({
agentPresetLibraryConfig: normalizeAgentPresetLibraryConfig(
valueByKey.get(AGENT_PRESET_LIBRARY_CONFIG_KEY)
),
agentWorkflowShowcaseConfig: normalizeAgentWorkflowShowcaseConfig(
valueByKey.get(AGENT_WORKFLOW_SHOWCASE_CONFIG_KEY)
),
});
}
if (scope === "agent-canvas") {
const [{ settings, valueByKey }, agentPsdTemplates] = await Promise.all([
readScopedSiteSettings([
REALTIME_CANVAS_CONFIG_KEY,
REALTIME_CANVAS_MODE_ICON_CONFIG_KEY,
CANVAS_TOOLBAR_CONFIG_SETTING_KEY,
LAYER_SPLIT_CONFIG_KEY,
CANVAS_AGENT_MODE_ENABLED_KEY,
VIDEO_CANVAS_ENABLED_KEY,
VIDEO_CANVAS_SIDEBAR_ENABLED_KEY,
AGENT_WORKFLOW_UI_CONFIG_KEY,
AGENT_WORKFLOW_BRAND_MODEL_CONFIG_KEY,
AGENT_WORKFLOW_SHOWCASE_CONFIG_KEY,
AGENT_PSD_TEMPLATE_CATEGORY_TAG_CONFIG_KEY,
]),
readAgentPsdTemplates().catch(() => []),
]);
const typedSettings = settings as Record<string, unknown> | undefined;
const videoCanvasSidebarEnabled = normalizeVideoCanvasSidebarEnabled(
valueByKey.get(VIDEO_CANVAS_SIDEBAR_ENABLED_KEY)
);
const agentPsdTemplateCategoryTagConfig =
normalizeAgentPsdTemplateCategoryTagConfig(
valueByKey.get(AGENT_PSD_TEMPLATE_CATEGORY_TAG_CONFIG_KEY)
);
return createScopedSiteSettingsResponse({
...createBrandingSettings(typedSettings),
agentEnabled: typedSettings?.agentEnabled ?? true,
canvasAgentModeEnabled: normalizeCanvasAgentModeEnabled(
valueByKey.get(CANVAS_AGENT_MODE_ENABLED_KEY)
),
videoCanvasEnabled: normalizeVideoCanvasEnabled(
valueByKey.get(VIDEO_CANVAS_ENABLED_KEY),
valueByKey.get(VIDEO_CANVAS_SIDEBAR_ENABLED_KEY)
),
videoCanvasSidebarEnabled,
realtimeCanvasEnabled: true,
realtimeCanvasServiceEnabled:
(valueByKey.get(REALTIME_CANVAS_CONFIG_KEY) as
| { enabled?: boolean }
| undefined)?.enabled !== false,
realtimeCanvasModeIconConfig: normalizeRealtimeCanvasModeIconConfig(
valueByKey.get(REALTIME_CANVAS_MODE_ICON_CONFIG_KEY)
),
canvasToolbarConfig: normalizeCanvasToolbarConfig(
valueByKey.get(CANVAS_TOOLBAR_CONFIG_SETTING_KEY)
),
layerSplitEnabled: normalizeLayerSplitEnabled(
valueByKey.get(LAYER_SPLIT_CONFIG_KEY)
),
agentWorkflowUiConfig: normalizeAgentWorkflowUiConfig(
valueByKey.get(AGENT_WORKFLOW_UI_CONFIG_KEY)
),
agentWorkflowBrandModelConfig: normalizeAgentWorkflowBrandModelConfig(
valueByKey.get(AGENT_WORKFLOW_BRAND_MODEL_CONFIG_KEY)
),
agentWorkflowShowcaseConfig: normalizeAgentWorkflowShowcaseConfig(
valueByKey.get(AGENT_WORKFLOW_SHOWCASE_CONFIG_KEY)
),
agentPsdTemplateCategoryTagIndex: buildAgentPsdTemplateCategoryTagIndex({
templates: agentPsdTemplates,
config: agentPsdTemplateCategoryTagConfig,
}),
});
}
if (scope === "branding") {
const { settings } = await readScopedSiteSettings([]);
return createScopedSiteSettingsResponse(
createBrandingSettings(settings as Record<string, unknown> | undefined)
);
}
if (scope === "chat") {
const { settings } = await readScopedSiteSettings([]);
const typedSettings = settings as Record<string, unknown> | undefined;
return createScopedSiteSettingsResponse({
...createBrandingSettings(typedSettings),
showAutoModel: typedSettings?.showAutoModel ?? true,
usageTimeEnabled: typedSettings?.usageTimeEnabled ?? false,
usageTimeStart: typedSettings?.usageTimeStart || "08:00",
usageTimeEnd: typedSettings?.usageTimeEnd || "20:00",
usageTimeMessage:
typedSettings?.usageTimeMessage || "由壹点累了也是需要休息的",
});
}
if (scope === "canvas") {
const { settings, valueByKey } = await readScopedSiteSettings([
REALTIME_CANVAS_CONFIG_KEY,
REALTIME_CANVAS_MODE_ICON_CONFIG_KEY,
CANVAS_TOOLBAR_CONFIG_SETTING_KEY,
LAYER_SPLIT_CONFIG_KEY,
CANVAS_AGENT_MODE_ENABLED_KEY,
VIDEO_CANVAS_ENABLED_KEY,
VIDEO_CANVAS_SIDEBAR_ENABLED_KEY,
]);
const typedSettings = settings as Record<string, unknown> | undefined;
const videoCanvasSidebarEnabled = normalizeVideoCanvasSidebarEnabled(
valueByKey.get(VIDEO_CANVAS_SIDEBAR_ENABLED_KEY)
);
return createScopedSiteSettingsResponse({
...createBrandingSettings(typedSettings),
agentEnabled: typedSettings?.agentEnabled ?? true,
canvasAgentModeEnabled: normalizeCanvasAgentModeEnabled(
valueByKey.get(CANVAS_AGENT_MODE_ENABLED_KEY)
),
videoCanvasEnabled: normalizeVideoCanvasEnabled(
valueByKey.get(VIDEO_CANVAS_ENABLED_KEY),
valueByKey.get(VIDEO_CANVAS_SIDEBAR_ENABLED_KEY)
),
videoCanvasSidebarEnabled,
realtimeCanvasEnabled: true,
realtimeCanvasServiceEnabled:
(valueByKey.get(REALTIME_CANVAS_CONFIG_KEY) as
| { enabled?: boolean }
| undefined)?.enabled !== false,
realtimeCanvasModeIconConfig: normalizeRealtimeCanvasModeIconConfig(
valueByKey.get(REALTIME_CANVAS_MODE_ICON_CONFIG_KEY)
),
canvasToolbarConfig: normalizeCanvasToolbarConfig(
valueByKey.get(CANVAS_TOOLBAR_CONFIG_SETTING_KEY)
),
layerSplitEnabled: normalizeLayerSplitEnabled(
valueByKey.get(LAYER_SPLIT_CONFIG_KEY)
),
});
}
if (scope === "canvas-chat") {
const { settings, valueByKey } = await readScopedSiteSettings([
AI_DETAIL_GENERATOR_ENABLED_KEY,
LOVART_COMPOSER_PREVIEW_TEXTS_KEY,
LOVART_COMPOSER_PREFIX_KEY,
CANVAS_TOOLBAR_CONFIG_SETTING_KEY,
]);
const typedSettings = settings as Record<string, unknown> | undefined;
return createScopedSiteSettingsResponse({
...createBrandingSettings(typedSettings),
bannerEnabled: typedSettings?.bannerEnabled ?? true,
bannerText:
typedSettings?.bannerText || "升级会员, Nano Banana Pro 免费365天!",
bannerLink: typedSettings?.bannerLink || null,
lovartComposerPrefix: normalizeLovartComposerPrefix(
valueByKey.get(LOVART_COMPOSER_PREFIX_KEY)
),
lovartComposerPreviewTexts: normalizeLovartComposerPreviewTexts(
valueByKey.get(LOVART_COMPOSER_PREVIEW_TEXTS_KEY)
),
canvasEnabled: typedSettings?.canvasEnabled ?? true,
aiDetailGenerationEnabled: Boolean(
valueByKey.get(AI_DETAIL_GENERATOR_ENABLED_KEY)
),
canvasDefaultModelId: typedSettings?.canvasDefaultModelId || null,
canvasAutoModeDefaultModelId:
typedSettings?.canvasAutoModeDefaultModelId || null,
canvasDetailDefaultModelId:
typedSettings?.canvasDetailDefaultModelId || null,
canvasToolbarConfig: normalizeCanvasToolbarConfig(
valueByKey.get(CANVAS_TOOLBAR_CONFIG_SETTING_KEY)
),
});
}
const { settings, valueByKey } = await readScopedSiteSettings([
LOVART_COMPOSER_PREVIEW_TEXTS_KEY,
LOVART_COMPOSER_PREFIX_KEY,
LOVART_PROMPT_TAG_VISIBILITY_KEY,
CONTACT_CONFIG_KEY,
MEMBERSHIP_CONFIG_KEY,
POINTS_PACKAGES_KEY,
DULUPAY_CONFIG_KEY,
LOVART_HOME_SIDEBAR_CONFIG_KEY,
VIDEO_CANVAS_ENABLED_KEY,
VIDEO_CANVAS_SIDEBAR_ENABLED_KEY,
]);
const typedSettings = settings as Record<string, unknown> | undefined;
return createScopedSiteSettingsResponse({
...createBrandingSettings(typedSettings),
canvasDefaultModelId: typedSettings?.canvasDefaultModelId || null,
lovartComposerPrefix: normalizeLovartComposerPrefix(
valueByKey.get(LOVART_COMPOSER_PREFIX_KEY)
),
lovartComposerPreviewTexts: normalizeLovartComposerPreviewTexts(
valueByKey.get(LOVART_COMPOSER_PREVIEW_TEXTS_KEY)
),
lovartPromptTagVisibility: normalizeLovartPromptTagVisibility(
valueByKey.get(LOVART_PROMPT_TAG_VISIBILITY_KEY)
),
contactConfig: normalizeContactConfig(valueByKey.get(CONTACT_CONFIG_KEY)),
membershipConfig: normalizeMembershipConfig(
valueByKey.get(MEMBERSHIP_CONFIG_KEY)
),
toolbox1Name: typedSettings?.toolbox1Name || "工具箱",
toolbox1Enabled: typedSettings?.toolbox1Enabled ?? true,
toolbox2Name: typedSettings?.toolbox2Name || "工具箱2",
toolbox2Enabled: typedSettings?.toolbox2Enabled ?? false,
productPlanningEnabled: typedSettings?.productPlanningEnabled ?? true,
agentEnabled: typedSettings?.agentEnabled ?? true,
videoCanvasEnabled: normalizeVideoCanvasEnabled(
valueByKey.get(VIDEO_CANVAS_ENABLED_KEY),
valueByKey.get(VIDEO_CANVAS_SIDEBAR_ENABLED_KEY)
),
tutorialEnabled: typedSettings?.tutorialEnabled ?? false,
tutorialVideoUrl: typedSettings?.tutorialVideoUrl || null,
pointsPackages: normalizePointsPackages(
valueByKey.get(POINTS_PACKAGES_KEY)
),
dulupayConfig: normalizeDulupayForPublic(valueByKey.get(DULUPAY_CONFIG_KEY)),
lovartHomeSidebarConfig: normalizeLovartHomeSidebarConfig(
valueByKey.get(LOVART_HOME_SIDEBAR_CONFIG_KEY)
),
});
}
// GET /api/site-settings - 获取网站设置 (公开)
export async function GET() {
export async function GET(request: NextRequest) {
try {
const scope = request.nextUrl.searchParams.get("scope") || "";
if (PUBLIC_SITE_SETTINGS_SCOPES.has(scope)) {
return getScopedPublicSiteSettings(scope as PublicSiteSettingsScope);
}
const session = await auth().catch(() => null);
const isAdmin = isSiteAdminType(session?.user?.type);
const canManageAgentWorkflow = isAgentWorkflowAdminType(session?.user?.type);

View File

@ -1790,7 +1790,12 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
}>;
}>;
};
}>("/api/site-settings", fetcher);
}>(
requestedAgentMode
? "/api/site-settings?scope=agent-canvas"
: "/api/site-settings?scope=canvas",
fetcher
);
const { data: sceneFusionConfigData } = useSWR<{
enabled?: boolean;
}>("/api/scene-fusion/config", fetcher);

View File

@ -18,13 +18,22 @@ const LiblibVideoCanvasReplica = dynamic(
}
);
export function CanvasPageClientRouter({ initialId }: { initialId: string }) {
export function CanvasPageClientRouter({
initialId,
initialMode = "",
initialWorkspaceType = "",
}: {
initialId: string;
initialMode?: string;
initialWorkspaceType?: string;
}) {
const routeParams = useParams();
const searchParams = useSearchParams();
const routeId = routeParams?.id;
const id = Array.isArray(routeId) ? routeId[0] || initialId : typeof routeId === "string" ? routeId : initialId;
const mode = searchParams.get("mode");
const requestedWorkspaceType = searchParams.get("workspaceType");
const mode = searchParams.get("mode") || initialMode;
const requestedWorkspaceType =
searchParams.get("workspaceType") || initialWorkspaceType;
const requestedAgentMode = mode === "agent";
const isVideoMode = !requestedAgentMode && (mode === "video" || requestedWorkspaceType === "video");
const legacyParams = useMemo(() => Promise.resolve({ id }), [id]);

View File

@ -784,7 +784,10 @@ export default function CanvasModel3DPage() {
const batchImageSlotsRef = useRef<MultiViewSlot[]>([]);
// Fetch site settings for Logo
const { data: siteSettingsData } = useSWR("/api/site-settings", fetcher);
const { data: siteSettingsData } = useSWR(
"/api/site-settings?scope=branding",
fetcher
);
const { data: modelAliasConfigResponse } = useSWR<{
success?: boolean;
data?: PublicThreeDModelAliasConfig;

View File

@ -2,10 +2,34 @@ import { Loader2 } from "lucide-react";
import { Suspense } from "react";
import { CanvasPageClientRouter } from "./CanvasPageClientRouter";
export default function CanvasEditorPage() {
type CanvasEditorPageProps = {
params: Promise<{ id: string }>;
searchParams?: Promise<{
mode?: string | string[];
workspaceType?: string | string[];
}>;
};
function getFirstParamValue(value: string | string[] | undefined) {
return Array.isArray(value) ? value[0] || "" : value || "";
}
export default async function CanvasEditorPage({
params,
searchParams,
}: CanvasEditorPageProps) {
const { id } = await params;
const resolvedSearchParams = searchParams ? await searchParams : {};
return (
<Suspense fallback={<CanvasPageLoader />}>
<CanvasPageClientRouter initialId="" />
<CanvasPageClientRouter
initialId={id}
initialMode={getFirstParamValue(resolvedSearchParams.mode)}
initialWorkspaceType={getFirstParamValue(
resolvedSearchParams.workspaceType
)}
/>
</Suspense>
);
}

View File

@ -150,7 +150,7 @@ export function AgentPresetDialog({
}>("/api/models", fetcher);
const { data: siteSettingsData } = useSWR<{
agentPresetLibraryConfig?: AgentPresetLibraryConfig;
}>("/api/site-settings", fetcher);
}>("/api/site-settings?scope=agent-presets", fetcher);
const [name, setName] = useState("");
const [type, setType] = useState<"image" | "video" | "text">("image");

View File

@ -63,7 +63,7 @@ export function AgentPresetPanelDarkV2({
agentWorkflowShowcaseConfig?: unknown;
agentPresetLibraryConfig?: AgentPresetLibraryConfig;
}>(
"/api/site-settings",
"/api/site-settings?scope=agent-presets",
(url: string) => fetch(url, { cache: "no-store" }).then((res) => res.json())
);

View File

@ -25,7 +25,7 @@ export function AgentWorkflowBrandModelPicker({
theme = "light",
}: AgentWorkflowBrandModelPickerProps) {
const { data } = useSWR<{ agentWorkflowBrandModelConfig?: AgentWorkflowBrandModelConfig }>(
"/api/site-settings",
"/api/site-settings?scope=agent-brand-model",
fetcher
);

View File

@ -3011,7 +3011,7 @@ export function CanvasChatPanel({
canvasAutoModeDefaultModelId?: string | null;
canvasDetailDefaultModelId?: string | null;
canvasToolbarConfig?: CanvasToolbarConfig;
}>("/api/site-settings", fetcher);
}>("/api/site-settings?scope=canvas-chat", fetcher);
const { data: canvasSkillTagsData } = useSWR<{
enabled?: boolean;
title?: string;

View File

@ -2685,7 +2685,10 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib
const router = useRouter();
const canvasRef = useRef<HTMLDivElement | null>(null);
const dragRef = useRef<DragState | null>(null);
const { data: siteBrandSettings } = useSWR<SiteBrandSettings>("/api/site-settings", fetcher);
const { data: siteBrandSettings } = useSWR<SiteBrandSettings>(
"/api/site-settings?scope=branding",
fetcher
);
const { data: videoCanvasFunctionSettings } = useSWR<{ config?: VideoCanvasFunctionConfig }>(
"/api/video-canvas/config",
fetcher
@ -11146,7 +11149,10 @@ function MemberStoreModal({ onClose }: { onClose: () => void }) {
const [activeTab, setActiveTab] = useState<MemberStoreTab>("credits");
const [billingCycle, setBillingCycle] = useState<"monthly" | "annual">("monthly");
const [creatingPaymentPackageId, setCreatingPaymentPackageId] = useState<string | null>(null);
const { data: siteSettingsData } = useSWR<VideoCanvasBillingSettings>("/api/site-settings", fetcher);
const { data: siteSettingsData } = useSWR<VideoCanvasBillingSettings>(
"/api/site-settings?scope=home",
fetcher
);
const { data: membershipSummary } = useSWR<MembershipSummaryResponse>("/api/user/membership-summary", fetcher);
const { data: accountCenterData } = useSWR<AccountCenterResponse>("/api/user/account-center", fetcher);

View File

@ -1019,7 +1019,7 @@ export function RealtimeCanvasDialog({
useEffect(() => {
let cancelled = false;
fetch(`/api/site-settings?t=${Date.now()}`, { cache: "no-store" })
fetch(`/api/site-settings?scope=canvas&t=${Date.now()}`)
.then((res) => (res.ok ? res.json() : null))
.then((data) => {
if (!data || cancelled) return;

View File

@ -595,7 +595,7 @@ export function LovartHome({
defaultType?: string;
};
lovartHomeSidebarConfig?: unknown;
}>("/api/site-settings", fetcher);
}>("/api/site-settings?scope=home", fetcher);
const { data: tutorialVideosData } = useSWR<{ videos: TutorialVideo[] }>(
isSecondaryDataReady && siteSettingsData?.tutorialEnabled

View File

@ -419,7 +419,7 @@ export function ImageAnnotator({
setMounted(true);
// 获取网站设置
fetch(`/api/site-settings?t=${Date.now()}`)
fetch(`/api/site-settings?scope=branding&t=${Date.now()}`)
.then((res) => res.json())
.then((data) => {
if (data.siteName) setSiteName(data.siteName);

View File

@ -234,7 +234,7 @@ function PureMultimodalInput({
} | null>(null);
// 获取网站设置(用于时间限制检查)
const { data: siteSettings } = useSWR("/api/site-settings", fetcher);
const { data: siteSettings } = useSWR("/api/site-settings?scope=chat", fetcher);
const { data: modelCatalog } = useSWR<{
models: DynamicModel[];
}>("/api/models", fetcher);
@ -1565,7 +1565,7 @@ function PureModelSelectorCompact({
// 获取站点设置
const { data: siteSettings } = useSWR<{
showAutoModel: boolean;
}>("/api/site-settings", fetcher);
}>("/api/site-settings?scope=chat", fetcher);
const allModels = useMemo(
() =>