feat: add configurable home animation module
This commit is contained in:
parent
900a1c9e38
commit
5568325a0d
@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { useState, useEffect, useMemo, type ChangeEvent } from "react";
|
||||
import useSWR from "swr";
|
||||
import { toast } from "sonner";
|
||||
import { formatBeijingTime } from "@/lib/utils";
|
||||
@ -35,6 +35,7 @@ import {
|
||||
} from "@/lib/sqlserver/config";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { HomeAnimationEmbed } from "@/components/home/home-animation-embed";
|
||||
import {
|
||||
Trash2,
|
||||
Save,
|
||||
@ -127,6 +128,8 @@ import type { AgentWorkflowUiConfig } from "@/types/agent";
|
||||
const fetcher = (url: string) => fetch(url).then((res) => res.json());
|
||||
const DEFAULT_BFL_REALTIME_MODEL = "flux-2-klein-4b";
|
||||
const DEFAULT_BFL_BASE_URL = "https://api.bfl.ai";
|
||||
const HOME_CHAT_ANIMATION_MAX_BYTES = 1024 * 1024;
|
||||
const HOME_CHAT_ANIMATION_CONFIG_KEY = "home_chat_animation_config";
|
||||
|
||||
function createEmptyCanvasSkillTag(): CanvasSkillTag {
|
||||
return {
|
||||
@ -321,6 +324,13 @@ type OssImageMigrationConfig = {
|
||||
batchSize: number;
|
||||
};
|
||||
|
||||
type HomeAnimationConfig = {
|
||||
enabled: boolean;
|
||||
fileName: string;
|
||||
htmlContent: string;
|
||||
updatedAt: string | null;
|
||||
};
|
||||
|
||||
const defaultLovartPromptTagVisibility: LovartPromptTagVisibility = {
|
||||
nanoBananaPro: true,
|
||||
design: true,
|
||||
@ -624,6 +634,11 @@ export function AdminSettingsClient() {
|
||||
`/api/admin/settings?key=${DETAIL_AGENT_LOCAL_MOONSHOT_SETTING_KEY}`,
|
||||
fetcher,
|
||||
);
|
||||
const { data: homeAnimationSettingData, mutate: mutateHomeAnimationSetting } =
|
||||
useSWR<{ setting?: { value?: unknown } }>(
|
||||
`/api/admin/settings?key=${HOME_CHAT_ANIMATION_CONFIG_KEY}`,
|
||||
fetcher,
|
||||
);
|
||||
|
||||
// 🆕 生图记录数据
|
||||
const imageQueryParams = new URLSearchParams({
|
||||
@ -724,6 +739,12 @@ export function AdminSettingsClient() {
|
||||
membershipConfig: defaultMembershipConfig,
|
||||
membershipUserOverrides: defaultMembershipUserOverrides,
|
||||
});
|
||||
const [homeAnimationConfig, setHomeAnimationConfig] = useState<HomeAnimationConfig>({
|
||||
enabled: false,
|
||||
fileName: "",
|
||||
htmlContent: "",
|
||||
updatedAt: null,
|
||||
});
|
||||
const [newOverrideUserId, setNewOverrideUserId] = useState("");
|
||||
const [newOverrideEmail, setNewOverrideEmail] = useState("");
|
||||
const [newOverridePlanId, setNewOverridePlanId] = useState("");
|
||||
@ -890,6 +911,25 @@ export function AdminSettingsClient() {
|
||||
}
|
||||
}, [siteSettingsData]);
|
||||
|
||||
useEffect(() => {
|
||||
const raw =
|
||||
homeAnimationSettingData?.setting?.value &&
|
||||
typeof homeAnimationSettingData.setting.value === "object" &&
|
||||
!Array.isArray(homeAnimationSettingData.setting.value)
|
||||
? (homeAnimationSettingData.setting.value as Record<string, unknown>)
|
||||
: {};
|
||||
|
||||
setHomeAnimationConfig({
|
||||
enabled: raw.enabled === true,
|
||||
fileName: String(raw.fileName || "").trim(),
|
||||
htmlContent:
|
||||
typeof raw.htmlContent === "string"
|
||||
? raw.htmlContent.replace(/^\uFEFF/, "").replace(/\u0000/g, "")
|
||||
: "",
|
||||
updatedAt: String(raw.updatedAt || "").trim() || null,
|
||||
});
|
||||
}, [homeAnimationSettingData]);
|
||||
|
||||
useEffect(() => {
|
||||
const normalized = normalizeDetailAgentLocalMoonshotConfig(
|
||||
moonshotDetailAgentSettingData?.setting?.value,
|
||||
@ -1865,6 +1905,21 @@ export function AdminSettingsClient() {
|
||||
// --- 网站设置逻辑 ---
|
||||
const handleSaveSiteSettings = async () => {
|
||||
try {
|
||||
const normalizedHomeAnimationHtml = homeAnimationConfig.htmlContent
|
||||
.replace(/^\uFEFF/, "")
|
||||
.replace(/\u0000/g, "")
|
||||
.replace(/\u2028|\u2029/g, "\n");
|
||||
const homeAnimationBytes = new TextEncoder().encode(normalizedHomeAnimationHtml).length;
|
||||
|
||||
if (homeAnimationBytes > HOME_CHAT_ANIMATION_MAX_BYTES) {
|
||||
toast.error(
|
||||
`首页动画 HTML 过大,当前 ${(homeAnimationBytes / 1024).toFixed(1)}KB,最大 ${(
|
||||
HOME_CHAT_ANIMATION_MAX_BYTES / 1024
|
||||
).toFixed(0)}KB`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
siteSettings.storageFailoverConfig.enabled &&
|
||||
!siteSettings.storageFailoverConfig.backupStorageConfigId
|
||||
@ -1925,6 +1980,12 @@ export function AdminSettingsClient() {
|
||||
|
||||
const payload = {
|
||||
...siteSettings,
|
||||
homeAnimationConfig: {
|
||||
enabled: homeAnimationConfig.enabled,
|
||||
fileName: homeAnimationConfig.fileName.trim(),
|
||||
htmlContent: normalizedHomeAnimationHtml,
|
||||
updatedAt: homeAnimationConfig.updatedAt,
|
||||
},
|
||||
membershipConfig: parsedMembershipConfig,
|
||||
membershipUserOverrides: parsedMembershipOverrides,
|
||||
pointsPackages: parsedPointsPackages,
|
||||
@ -1935,12 +1996,68 @@ export function AdminSettingsClient() {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!response.ok) throw new Error("保存失败");
|
||||
setSiteSettings(payload);
|
||||
const responseData = await response.json().catch(() => null);
|
||||
if (!response.ok) throw new Error(responseData?.error || "保存失败");
|
||||
const { homeAnimationConfig: _ignoredHomeAnimationConfig, ...nextSiteSettings } = payload;
|
||||
setSiteSettings(nextSiteSettings);
|
||||
setHomeAnimationConfig((prev) => ({
|
||||
...prev,
|
||||
htmlContent: normalizedHomeAnimationHtml,
|
||||
updatedAt: new Date().toISOString(),
|
||||
}));
|
||||
toast.success("网站设置保存成功");
|
||||
mutateSiteSettings();
|
||||
mutateHomeAnimationSetting();
|
||||
} catch (error) {
|
||||
toast.error("保存网站设置失败");
|
||||
toast.error(error instanceof Error ? error.message : "保存网站设置失败");
|
||||
}
|
||||
};
|
||||
|
||||
const handleImportHomeAnimationFile = async (
|
||||
event: ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
const file = event.target.files?.[0];
|
||||
event.target.value = "";
|
||||
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedFileName = String(file.name || "").trim();
|
||||
const isHtmlFile =
|
||||
file.type === "text/html" ||
|
||||
/\.html?$/i.test(normalizedFileName);
|
||||
|
||||
if (!isHtmlFile) {
|
||||
toast.error("请上传 .html 文件");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const htmlContent = (await file.text())
|
||||
.replace(/^\uFEFF/, "")
|
||||
.replace(/\u0000/g, "")
|
||||
.replace(/\u2028|\u2029/g, "\n");
|
||||
const bytes = new TextEncoder().encode(htmlContent).length;
|
||||
|
||||
if (bytes > HOME_CHAT_ANIMATION_MAX_BYTES) {
|
||||
toast.error(
|
||||
`HTML 文件过大,当前 ${(bytes / 1024).toFixed(1)}KB,最大 ${(
|
||||
HOME_CHAT_ANIMATION_MAX_BYTES / 1024
|
||||
).toFixed(0)}KB`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setHomeAnimationConfig((prev) => ({
|
||||
...prev,
|
||||
fileName: normalizedFileName,
|
||||
htmlContent,
|
||||
updatedAt: null,
|
||||
}));
|
||||
toast.success("HTML 动效文件已导入,记得点击保存设置");
|
||||
} catch (error) {
|
||||
toast.error("读取 HTML 文件失败");
|
||||
}
|
||||
};
|
||||
|
||||
@ -5815,6 +5932,90 @@ export function AdminSettingsClient() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="border-t pt-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="homeAnimationEnabled">首页对话框下互动动画模块</Label>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
在首页对话框下方展示可交互的 HTML 动效模块,支持后续导入新 HTML 进行替换。
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
id="homeAnimationEnabled"
|
||||
checked={homeAnimationConfig.enabled}
|
||||
onCheckedChange={(checked) =>
|
||||
setHomeAnimationConfig((prev) => ({ ...prev, enabled: checked }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border bg-muted/10 p-4">
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div className="space-y-1">
|
||||
<div className="text-sm font-medium text-black">HTML 动效文件</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
支持导入 `.html` 文件。未上传自定义文件时,系统会尝试读取项目根目录的
|
||||
`动画效果.html` 作为默认兜底。
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
当前文件:
|
||||
{homeAnimationConfig.fileName
|
||||
? ` ${homeAnimationConfig.fileName}`
|
||||
: " 未上传自定义 HTML"}
|
||||
{homeAnimationConfig.updatedAt
|
||||
? ` · 上次更新 ${formatBeijingTime(homeAnimationConfig.updatedAt)}`
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<label className="inline-flex cursor-pointer">
|
||||
<input
|
||||
type="file"
|
||||
accept=".html,text/html"
|
||||
className="hidden"
|
||||
onChange={handleImportHomeAnimationFile}
|
||||
/>
|
||||
<Button type="button" variant="outline" className="pointer-events-none">
|
||||
{homeAnimationConfig.fileName ? "替换 HTML" : "导入 HTML"}
|
||||
</Button>
|
||||
</label>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
setHomeAnimationConfig((prev) => ({
|
||||
...prev,
|
||||
fileName: "",
|
||||
htmlContent: "",
|
||||
updatedAt: null,
|
||||
}))
|
||||
}
|
||||
disabled={!homeAnimationConfig.fileName && !homeAnimationConfig.htmlContent}
|
||||
>
|
||||
清空自定义 HTML
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{homeAnimationConfig.htmlContent ? (
|
||||
<div className="mt-4">
|
||||
<HomeAnimationEmbed
|
||||
htmlContent={homeAnimationConfig.htmlContent}
|
||||
title="当前导入动效预览"
|
||||
description="导入后点击页面底部的保存设置即可正式生效。"
|
||||
frameClassName="h-[240px] md:h-[300px]"
|
||||
className="shadow-none"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-4 rounded-xl border border-dashed border-black/10 bg-white/60 px-4 py-5 text-sm text-muted-foreground">
|
||||
当前未上传自定义 HTML。启用后会优先使用数据库中的自定义动效;如果为空,则尝试读取项目根目录的
|
||||
`动画效果.html`。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 无限画布设置 */}
|
||||
<div className="border-t pt-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
|
||||
39
app/api/home-animation/route.ts
Normal file
39
app/api/home-animation/route.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getSystemSetting } from "@/lib/db/queries";
|
||||
import {
|
||||
HOME_CHAT_ANIMATION_CONFIG_KEY,
|
||||
resolvePublicHomeChatAnimationConfig,
|
||||
} from "@/lib/home/home-animation-config";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
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",
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -67,6 +67,12 @@ import {
|
||||
normalizeOssImageMigrationConfig,
|
||||
OSS_MIGRATION_CONFIG_KEY,
|
||||
} from "@/lib/storage/migration-config";
|
||||
import {
|
||||
getHomeChatAnimationHtmlBytes,
|
||||
HOME_CHAT_ANIMATION_CONFIG_KEY,
|
||||
HOME_CHAT_ANIMATION_MAX_BYTES,
|
||||
normalizeHomeChatAnimationConfig,
|
||||
} from "@/lib/home/home-animation-config";
|
||||
import type { AgentCategoryTreeConfig } from "@/types/agent";
|
||||
|
||||
const HOME_VARIANT_KEY = "home_variant";
|
||||
@ -830,6 +836,14 @@ const updateSchema = z.object({
|
||||
batchSize: z.number().min(1).max(100).optional(),
|
||||
})
|
||||
.optional(),
|
||||
homeAnimationConfig: z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
fileName: z.string().trim().max(255).optional(),
|
||||
htmlContent: z.string().max(2_000_000).optional(),
|
||||
updatedAt: z.string().nullable().optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
// PUT /api/site-settings - 更新网站设置(管理员 / 智能体管理员)
|
||||
@ -888,8 +902,49 @@ export async function PUT(request: NextRequest) {
|
||||
storageFailoverConfig,
|
||||
ossImageMigrationConfig,
|
||||
canvasAgentModeEnabled,
|
||||
homeAnimationConfig,
|
||||
...siteSettingsPayload
|
||||
} = data;
|
||||
|
||||
const persistHomeAnimationConfig = async () => {
|
||||
if (homeAnimationConfig === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalized = normalizeHomeChatAnimationConfig(homeAnimationConfig);
|
||||
const htmlBytes = getHomeChatAnimationHtmlBytes(normalized.htmlContent);
|
||||
|
||||
if (htmlBytes > HOME_CHAT_ANIMATION_MAX_BYTES) {
|
||||
throw new Error(
|
||||
`首页动画 HTML 超过限制,当前 ${(htmlBytes / 1024).toFixed(1)}KB,最大 ${(
|
||||
HOME_CHAT_ANIMATION_MAX_BYTES / 1024
|
||||
).toFixed(0)}KB`,
|
||||
);
|
||||
}
|
||||
|
||||
await db
|
||||
.insert(systemSettings)
|
||||
.values({
|
||||
key: HOME_CHAT_ANIMATION_CONFIG_KEY,
|
||||
value: {
|
||||
...normalized,
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
updatedAt: new Date(),
|
||||
updatedBy: session.user.id,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: systemSettings.key,
|
||||
set: {
|
||||
value: {
|
||||
...normalized,
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
updatedAt: new Date(),
|
||||
updatedBy: session.user.id,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 查找现有设置
|
||||
const [existing] = await db.select().from(siteSettings).limit(1);
|
||||
@ -1371,6 +1426,8 @@ export async function PUT(request: NextRequest) {
|
||||
});
|
||||
}
|
||||
|
||||
await persistHomeAnimationConfig();
|
||||
|
||||
return NextResponse.json({
|
||||
...updated,
|
||||
homeVariant: homeVariant ?? normalizeHomeVariant(undefined),
|
||||
@ -1541,6 +1598,8 @@ export async function PUT(request: NextRequest) {
|
||||
});
|
||||
}
|
||||
|
||||
await persistHomeAnimationConfig();
|
||||
|
||||
if (aiDetailGenerationEnabled !== undefined) {
|
||||
await db
|
||||
.insert(systemSettings)
|
||||
@ -2043,6 +2102,13 @@ export async function PUT(request: NextRequest) {
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return NextResponse.json(
|
||||
{ error: error.message || "Failed to update site settings" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to update site settings" },
|
||||
|
||||
53
components/home/home-animation-embed.tsx
Normal file
53
components/home/home-animation-embed.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type HomeAnimationEmbedProps = {
|
||||
htmlContent: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
className?: string;
|
||||
frameClassName?: string;
|
||||
};
|
||||
|
||||
export function HomeAnimationEmbed({
|
||||
htmlContent,
|
||||
title,
|
||||
description,
|
||||
className,
|
||||
frameClassName,
|
||||
}: HomeAnimationEmbedProps) {
|
||||
if (!htmlContent.trim()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"overflow-hidden rounded-[24px] border border-black/10 bg-white shadow-[0_10px_24px_rgba(0,0,0,0.05)]",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{title || description ? (
|
||||
<div className="flex items-start justify-between gap-4 border-b border-black/8 px-4 py-3">
|
||||
<div className="min-w-0">
|
||||
{title ? (
|
||||
<div className="text-[14px] font-semibold text-black">{title}</div>
|
||||
) : null}
|
||||
{description ? (
|
||||
<div className="mt-1 text-[12px] text-black/45">{description}</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className={cn("h-[360px] w-full bg-[#f6f6f6] md:h-[420px]", frameClassName)}>
|
||||
<iframe
|
||||
title={title || "首页互动动画"}
|
||||
srcDoc={htmlContent}
|
||||
sandbox="allow-scripts allow-forms allow-pointer-lock"
|
||||
loading="lazy"
|
||||
className="h-full w-full border-0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -52,6 +52,7 @@ import {
|
||||
} from "@/lib/canvas-launch";
|
||||
import type { MembershipConfig } from "@/lib/membership/config";
|
||||
import { ImageAnnotator } from "@/components/image-annotator";
|
||||
import { HomeAnimationEmbed } from "@/components/home/home-animation-embed";
|
||||
import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry";
|
||||
|
||||
type HomeUser = {
|
||||
@ -463,6 +464,11 @@ export function LovartHome({
|
||||
"/api/auth-settings",
|
||||
fetcher
|
||||
);
|
||||
const { data: homeAnimationData } = useSWR<{
|
||||
enabled?: boolean;
|
||||
htmlContent?: string;
|
||||
fileName?: string;
|
||||
}>("/api/home-animation", fetcher);
|
||||
|
||||
const membershipConfig = membershipData?.config;
|
||||
const membershipPlans = membershipConfig?.plans || [];
|
||||
@ -1745,6 +1751,16 @@ export function LovartHome({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{homeAnimationData?.enabled && homeAnimationData?.htmlContent ? (
|
||||
<div className="mx-auto mt-6 w-full max-w-[780px]">
|
||||
<HomeAnimationEmbed
|
||||
htmlContent={homeAnimationData.htmlContent}
|
||||
title="互动演示"
|
||||
description="支持拖拽、滚轮与实时交互,模块内容可在系统设置中替换 HTML 更新。"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="mt-4 flex flex-wrap items-center justify-center gap-1.5">
|
||||
{visiblePromptTags.map((tag) => (
|
||||
<button
|
||||
|
||||
101
lib/home/home-animation-config.ts
Normal file
101
lib/home/home-animation-config.ts
Normal file
@ -0,0 +1,101 @@
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
|
||||
export const HOME_CHAT_ANIMATION_CONFIG_KEY = "home_chat_animation_config";
|
||||
export const HOME_CHAT_ANIMATION_MAX_BYTES = 1024 * 1024;
|
||||
|
||||
export type HomeChatAnimationConfig = {
|
||||
enabled: boolean;
|
||||
fileName: string;
|
||||
htmlContent: string;
|
||||
updatedAt: string | null;
|
||||
};
|
||||
|
||||
export function normalizeHomeChatAnimationHtml(value: unknown): string {
|
||||
if (typeof value !== "string") {
|
||||
return "";
|
||||
}
|
||||
|
||||
return value
|
||||
.replace(/^\uFEFF/, "")
|
||||
.replace(/\u0000/g, "")
|
||||
.replace(/\u2028|\u2029/g, "\n");
|
||||
}
|
||||
|
||||
export function getHomeChatAnimationHtmlBytes(htmlContent: string): number {
|
||||
return Buffer.byteLength(htmlContent, "utf8");
|
||||
}
|
||||
|
||||
export function normalizeHomeChatAnimationConfig(
|
||||
value: unknown,
|
||||
): HomeChatAnimationConfig {
|
||||
const raw =
|
||||
value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
|
||||
const enabled = raw.enabled === true;
|
||||
const fileName = String(raw.fileName || "").trim().slice(0, 255);
|
||||
const htmlContent = normalizeHomeChatAnimationHtml(raw.htmlContent);
|
||||
const updatedAt = String(raw.updatedAt || "").trim() || null;
|
||||
|
||||
return {
|
||||
enabled,
|
||||
fileName,
|
||||
htmlContent,
|
||||
updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
let cachedDefaultHomeAnimationHtml: string | null | undefined;
|
||||
|
||||
export function getDefaultHomeChatAnimationHtml(): string {
|
||||
if (cachedDefaultHomeAnimationHtml !== undefined) {
|
||||
return cachedDefaultHomeAnimationHtml || "";
|
||||
}
|
||||
|
||||
const candidatePaths = [join(process.cwd(), "动画效果.html")];
|
||||
|
||||
for (const candidatePath of candidatePaths) {
|
||||
if (!existsSync(candidatePath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cachedDefaultHomeAnimationHtml = normalizeHomeChatAnimationHtml(
|
||||
readFileSync(candidatePath, "utf8"),
|
||||
);
|
||||
return cachedDefaultHomeAnimationHtml;
|
||||
}
|
||||
|
||||
cachedDefaultHomeAnimationHtml = "";
|
||||
return "";
|
||||
}
|
||||
|
||||
export function resolvePublicHomeChatAnimationConfig(
|
||||
value: unknown,
|
||||
): HomeChatAnimationConfig & { usesDefaultFallback: boolean } {
|
||||
const normalized = normalizeHomeChatAnimationConfig(value);
|
||||
const fallbackHtml = getDefaultHomeChatAnimationHtml();
|
||||
const htmlContent = normalized.htmlContent || fallbackHtml;
|
||||
const usesDefaultFallback =
|
||||
!normalized.htmlContent && Boolean(fallbackHtml);
|
||||
|
||||
if (!normalized.enabled || !htmlContent) {
|
||||
return {
|
||||
enabled: false,
|
||||
fileName: normalized.fileName,
|
||||
htmlContent: "",
|
||||
updatedAt: normalized.updatedAt,
|
||||
usesDefaultFallback,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
enabled: true,
|
||||
fileName:
|
||||
normalized.fileName || (usesDefaultFallback ? "动画效果.html" : ""),
|
||||
htmlContent,
|
||||
updatedAt: normalized.updatedAt,
|
||||
usesDefaultFallback,
|
||||
};
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user