feat: split PSD parsing into fast parse and async backfill
This commit is contained in:
parent
8c8b1794e4
commit
e84bfd94ec
@ -182,16 +182,25 @@ function countEditableTextLayers(template: AgentPsdTemplate | null) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasTemplateParsedCanvas(template: AgentPsdTemplate | null) {
|
||||||
|
return Boolean(
|
||||||
|
template &&
|
||||||
|
Number(template.width || 0) > 1 &&
|
||||||
|
Number(template.height || 0) > 1 &&
|
||||||
|
String(template.previewUrl || "").trim()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function getTemplateParseLabel(template: AgentPsdTemplate | null) {
|
function getTemplateParseLabel(template: AgentPsdTemplate | null) {
|
||||||
if (!template) return "未解析";
|
if (!template) return "未解析";
|
||||||
|
|
||||||
switch (template.parseStatus) {
|
switch (template.parseStatus) {
|
||||||
case "processing":
|
case "processing":
|
||||||
return "解析中";
|
return hasTemplateParsedCanvas(template) ? "补全中" : "解析中";
|
||||||
case "completed":
|
case "completed":
|
||||||
return "已解析";
|
return "已解析";
|
||||||
case "failed":
|
case "failed":
|
||||||
return "解析失败";
|
return hasTemplateParsedCanvas(template) ? "补全失败" : "解析失败";
|
||||||
default:
|
default:
|
||||||
return "待解析";
|
return "待解析";
|
||||||
}
|
}
|
||||||
@ -219,6 +228,9 @@ function getTemplateParseProgress(template: AgentPsdTemplate | null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getTemplateParseActionLabel(template: AgentPsdTemplate | null) {
|
function getTemplateParseActionLabel(template: AgentPsdTemplate | null) {
|
||||||
|
if (template?.parseStatus === "processing") {
|
||||||
|
return "补全中";
|
||||||
|
}
|
||||||
return template?.parseStatus === "completed" ? "重新解析" : "解析 PSD";
|
return template?.parseStatus === "completed" ? "重新解析" : "解析 PSD";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -775,8 +787,12 @@ export function PsdTemplatesSettingsClient({
|
|||||||
onStart: () => {
|
onStart: () => {
|
||||||
setTemplateUploadStatus("正在解析 PSD,请稍候...");
|
setTemplateUploadStatus("正在解析 PSD,请稍候...");
|
||||||
},
|
},
|
||||||
onDone: () => {
|
onDone: (template) => {
|
||||||
setTemplateUploadStatus("解析完成");
|
setTemplateUploadStatus(
|
||||||
|
template.parseStatus === "completed"
|
||||||
|
? "解析完成"
|
||||||
|
: "快速解析完成,后台继续补全资产..."
|
||||||
|
);
|
||||||
},
|
},
|
||||||
onError: (message) => {
|
onError: (message) => {
|
||||||
setTemplateUploadStatus(`解析失败:${message}`);
|
setTemplateUploadStatus(`解析失败:${message}`);
|
||||||
@ -1035,7 +1051,7 @@ export function PsdTemplatesSettingsClient({
|
|||||||
options?: {
|
options?: {
|
||||||
skipCompletedConfirm?: boolean;
|
skipCompletedConfirm?: boolean;
|
||||||
onStart?: () => void;
|
onStart?: () => void;
|
||||||
onDone?: () => void;
|
onDone?: (template: AgentPsdTemplate) => void;
|
||||||
onError?: (message: string) => void;
|
onError?: (message: string) => void;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
@ -1099,11 +1115,15 @@ export function PsdTemplatesSettingsClient({
|
|||||||
throw new Error(payload.error || "PSD 解析失败");
|
throw new Error(payload.error || "PSD 解析失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.success(`模板“${payload.template.name}”解析完成`);
|
toast.success(
|
||||||
|
payload.template.parseStatus === "completed"
|
||||||
|
? `模板“${payload.template.name}”解析完成`
|
||||||
|
: `模板“${payload.template.name}”已完成快速解析,后台继续补全资产`
|
||||||
|
);
|
||||||
setDraftTemplate(cloneTemplate(payload.template));
|
setDraftTemplate(cloneTemplate(payload.template));
|
||||||
await mutate();
|
await mutate();
|
||||||
setSelectedTemplateId(payload.template.id);
|
setSelectedTemplateId(payload.template.id);
|
||||||
options?.onDone?.();
|
options?.onDone?.(payload.template);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await mutate();
|
await mutate();
|
||||||
const message = error instanceof Error ? error.message : "PSD 解析失败";
|
const message = error instanceof Error ? error.message : "PSD 解析失败";
|
||||||
@ -1184,7 +1204,7 @@ export function PsdTemplatesSettingsClient({
|
|||||||
{getTemplateParseLabel(draftTemplate)}
|
{getTemplateParseLabel(draftTemplate)}
|
||||||
</Badge>
|
</Badge>
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{draftTemplate.parseStatus === "completed"
|
{hasTemplateParsedCanvas(draftTemplate)
|
||||||
? `${draftTemplate.width} x ${draftTemplate.height}`
|
? `${draftTemplate.width} x ${draftTemplate.height}`
|
||||||
: "等待解析后显示尺寸"}
|
: "等待解析后显示尺寸"}
|
||||||
</span>
|
</span>
|
||||||
@ -1851,7 +1871,7 @@ export function PsdTemplatesSettingsClient({
|
|||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="truncate text-sm font-medium">{template.name}</div>
|
<div className="truncate text-sm font-medium">{template.name}</div>
|
||||||
<div className="mt-1 text-xs text-muted-foreground">
|
<div className="mt-1 text-xs text-muted-foreground">
|
||||||
{template.parseStatus === "completed"
|
{hasTemplateParsedCanvas(template)
|
||||||
? `${template.width} x ${template.height} · ${template.layerCount} 图层`
|
? `${template.width} x ${template.height} · ${template.layerCount} 图层`
|
||||||
: "待解析完成后生成尺寸和图层"}
|
: "待解析完成后生成尺寸和图层"}
|
||||||
</div>
|
</div>
|
||||||
@ -2031,12 +2051,12 @@ export function PsdTemplatesSettingsClient({
|
|||||||
className="flex w-full items-center justify-center bg-black/5"
|
className="flex w-full items-center justify-center bg-black/5"
|
||||||
style={{
|
style={{
|
||||||
aspectRatio:
|
aspectRatio:
|
||||||
draftTemplate.parseStatus === "completed"
|
hasTemplateParsedCanvas(draftTemplate)
|
||||||
? `${Math.max(1, draftTemplate.width)} / ${Math.max(1, draftTemplate.height)}`
|
? `${Math.max(1, draftTemplate.width)} / ${Math.max(1, draftTemplate.height)}`
|
||||||
: "1 / 1",
|
: "1 / 1",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{draftTemplate.parseStatus === "completed" ? (
|
{hasTemplateParsedCanvas(draftTemplate) ? (
|
||||||
<img
|
<img
|
||||||
src={draftTemplate.previewUrl}
|
src={draftTemplate.previewUrl}
|
||||||
alt={draftTemplate.name}
|
alt={draftTemplate.name}
|
||||||
@ -2082,7 +2102,9 @@ export function PsdTemplatesSettingsClient({
|
|||||||
{draftTemplate.parseStage ||
|
{draftTemplate.parseStage ||
|
||||||
(draftTemplate.parseStatus === "completed"
|
(draftTemplate.parseStatus === "completed"
|
||||||
? "解析完成,可开始配置槽位"
|
? "解析完成,可开始配置槽位"
|
||||||
: "上传完成后需要手动解析,适合批量上传多个 PSD 再逐个处理")}
|
: hasTemplateParsedCanvas(draftTemplate)
|
||||||
|
? "快速解析已完成,后台正在补全图层缩略图、源图和模板底图"
|
||||||
|
: "上传完成后需要手动解析,适合批量上传多个 PSD 再逐个处理")}
|
||||||
</div>
|
</div>
|
||||||
{draftTemplate.parseStatus === "processing" ? (
|
{draftTemplate.parseStatus === "processing" ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@ -2103,7 +2125,7 @@ export function PsdTemplatesSettingsClient({
|
|||||||
<div className="rounded-xl border p-3">
|
<div className="rounded-xl border p-3">
|
||||||
<div className="text-xs text-muted-foreground">画布尺寸</div>
|
<div className="text-xs text-muted-foreground">画布尺寸</div>
|
||||||
<div className="mt-1 text-sm font-medium">
|
<div className="mt-1 text-sm font-medium">
|
||||||
{draftTemplate.parseStatus === "completed"
|
{hasTemplateParsedCanvas(draftTemplate)
|
||||||
? `${draftTemplate.width} x ${draftTemplate.height}`
|
? `${draftTemplate.width} x ${draftTemplate.height}`
|
||||||
: "待解析"}
|
: "待解析"}
|
||||||
</div>
|
</div>
|
||||||
@ -2111,7 +2133,7 @@ export function PsdTemplatesSettingsClient({
|
|||||||
<div className="rounded-xl border p-3">
|
<div className="rounded-xl border p-3">
|
||||||
<div className="text-xs text-muted-foreground">当前槽位</div>
|
<div className="text-xs text-muted-foreground">当前槽位</div>
|
||||||
<div className="mt-1 text-sm font-medium">
|
<div className="mt-1 text-sm font-medium">
|
||||||
{draftTemplate.parseStatus === "completed"
|
{hasTemplateParsedCanvas(draftTemplate)
|
||||||
? `${countEditableLayers(draftTemplate)} / ${draftTemplate.layers.length}`
|
? `${countEditableLayers(draftTemplate)} / ${draftTemplate.layers.length}`
|
||||||
: "待解析"}
|
: "待解析"}
|
||||||
</div>
|
</div>
|
||||||
@ -2388,7 +2410,7 @@ export function PsdTemplatesSettingsClient({
|
|||||||
</Badge>
|
</Badge>
|
||||||
<Badge variant="secondary">
|
<Badge variant="secondary">
|
||||||
当前图层数:
|
当前图层数:
|
||||||
{draftTemplate.parseStatus === "completed"
|
{hasTemplateParsedCanvas(draftTemplate)
|
||||||
? draftTemplate.layers.length
|
? draftTemplate.layers.length
|
||||||
: "待解析"}
|
: "待解析"}
|
||||||
</Badge>
|
</Badge>
|
||||||
@ -2438,7 +2460,9 @@ export function PsdTemplatesSettingsClient({
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-4 text-base font-medium">先解析 PSD,再配置槽位</div>
|
<div className="mt-4 text-base font-medium">先解析 PSD,再配置槽位</div>
|
||||||
<div className="mt-2 text-sm text-muted-foreground">
|
<div className="mt-2 text-sm text-muted-foreground">
|
||||||
当前模板还没有生成图层结构、缩略图和字体识别结果,完成解析后这里会自动显示可编辑配置项。
|
{hasTemplateParsedCanvas(draftTemplate)
|
||||||
|
? "快速解析已经返回图层结构,后台正在补全缩略图、源图和底图,完成后这里会自动开放完整配置。"
|
||||||
|
: "当前模板还没有生成图层结构、缩略图和字体识别结果,完成解析后这里会自动显示可编辑配置项。"}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 flex justify-center">
|
<div className="mt-4 flex justify-center">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import { findBestMatchingPsdFont } from "@/lib/agents/psd-font-matching";
|
|||||||
import { readAgentPsdFonts } from "@/lib/agents/psd-font-settings";
|
import { readAgentPsdFonts } from "@/lib/agents/psd-font-settings";
|
||||||
import {
|
import {
|
||||||
AGENT_PSD_TEMPLATES_KEY,
|
AGENT_PSD_TEMPLATES_KEY,
|
||||||
AGENT_PSD_TEMPLATE_ASSET_VERSION,
|
|
||||||
type AgentPsdTemplate,
|
type AgentPsdTemplate,
|
||||||
type AgentPsdTemplateLayerSlot,
|
type AgentPsdTemplateLayerSlot,
|
||||||
type AgentPsdTemplateMutationResponse,
|
type AgentPsdTemplateMutationResponse,
|
||||||
@ -13,14 +12,13 @@ import {
|
|||||||
import {
|
import {
|
||||||
type ParsedPsdTemplateLayer,
|
type ParsedPsdTemplateLayer,
|
||||||
parsePsdTemplateFile,
|
parsePsdTemplateFile,
|
||||||
preparePsdTemplateRenderAssets,
|
|
||||||
} from "@/lib/agents/psd-template-parser";
|
} from "@/lib/agents/psd-template-parser";
|
||||||
import { pickPrimaryPsdImageLayerId } from "@/lib/agents/psd-template-image-slot-selection";
|
import { pickPrimaryPsdImageLayerId } from "@/lib/agents/psd-template-image-slot-selection";
|
||||||
|
import { schedulePsdTemplateAssetBackfill } from "@/lib/agents/psd-template-asset-backfill";
|
||||||
import {
|
import {
|
||||||
cacheAgentPsdSourceLocally,
|
cacheAgentPsdSourceLocally,
|
||||||
hasAgentPsdLocalCache,
|
hasAgentPsdLocalCache,
|
||||||
readAgentPsdSourceFromLocalCache,
|
readAgentPsdSourceFromLocalCache,
|
||||||
resolveAgentPsdLocalCachePath,
|
|
||||||
} from "@/lib/agents/psd-local-cache";
|
} from "@/lib/agents/psd-local-cache";
|
||||||
import { readAgentPsdTemplates } from "@/lib/agents/psd-template-settings";
|
import { readAgentPsdTemplates } from "@/lib/agents/psd-template-settings";
|
||||||
import { uploadFile } from "@/lib/storage/service";
|
import { uploadFile } from "@/lib/storage/service";
|
||||||
@ -229,11 +227,6 @@ export async function POST(
|
|||||||
error: undefined,
|
error: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const localFilePath = resolveAgentPsdLocalCachePath({
|
|
||||||
templateId: id,
|
|
||||||
fileName: currentTemplate.fileName,
|
|
||||||
});
|
|
||||||
|
|
||||||
let fileBuffer: ArrayBuffer;
|
let fileBuffer: ArrayBuffer;
|
||||||
if (
|
if (
|
||||||
await hasAgentPsdLocalCache({
|
await hasAgentPsdLocalCache({
|
||||||
@ -322,57 +315,17 @@ export async function POST(
|
|||||||
canvasHeight: parsedTemplate.height,
|
canvasHeight: parsedTemplate.height,
|
||||||
});
|
});
|
||||||
|
|
||||||
await updateParseProgress({
|
|
||||||
templateId: id,
|
|
||||||
updatedBy: session.user.id,
|
|
||||||
progress: 78,
|
|
||||||
stage: "正在生成图层预览和遮罩",
|
|
||||||
error: undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
const preparedAssets = await preparePsdTemplateRenderAssets({
|
|
||||||
templateId: id,
|
|
||||||
fileUrl: currentTemplate.fileUrl,
|
|
||||||
filePath: localFilePath,
|
|
||||||
fileBuffer,
|
|
||||||
layers: initialLayers,
|
|
||||||
});
|
|
||||||
|
|
||||||
await updateParseProgress({
|
|
||||||
templateId: id,
|
|
||||||
updatedBy: session.user.id,
|
|
||||||
progress: 92,
|
|
||||||
stage: "正在生成模板底图",
|
|
||||||
error: undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
const uploadedRenderBase = await uploadFile(
|
|
||||||
`agent-psd-base-${id}-${Date.now()}.png`,
|
|
||||||
toArrayBuffer(preparedAssets.renderBaseBuffer)
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasEditableLayers = preparedAssets.layers.some((layer) => layer.isEditable);
|
|
||||||
const nextStatus =
|
|
||||||
currentTemplate.status === "active" ||
|
|
||||||
(currentTemplate.layers.length === 0 && hasEditableLayers)
|
|
||||||
? "active"
|
|
||||||
: currentTemplate.status;
|
|
||||||
|
|
||||||
const updatedTemplate = await updateTemplate(id, session.user.id, (template) => ({
|
const updatedTemplate = await updateTemplate(id, session.user.id, (template) => ({
|
||||||
...template,
|
...template,
|
||||||
previewUrl: uploadedPreview.url,
|
previewUrl: uploadedPreview.url,
|
||||||
previewPath: uploadedPreview.pathname,
|
previewPath: uploadedPreview.pathname,
|
||||||
renderBaseUrl: uploadedRenderBase.url,
|
|
||||||
renderBasePath: uploadedRenderBase.pathname,
|
|
||||||
width: parsedTemplate.width,
|
width: parsedTemplate.width,
|
||||||
height: parsedTemplate.height,
|
height: parsedTemplate.height,
|
||||||
layerCount: parsedTemplate.layers.length,
|
layerCount: parsedTemplate.layers.length,
|
||||||
layers: preparedAssets.layers,
|
layers: initialLayers,
|
||||||
assetVersion: AGENT_PSD_TEMPLATE_ASSET_VERSION,
|
parseStatus: "processing",
|
||||||
status: nextStatus,
|
parseProgress: 48,
|
||||||
parseStatus: "completed",
|
parseStage: "快速解析完成,正在后台补全图层资源和模板底图",
|
||||||
parseProgress: 100,
|
|
||||||
parseStage: "解析完成",
|
|
||||||
parseError: undefined,
|
parseError: undefined,
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
}));
|
}));
|
||||||
@ -381,6 +334,11 @@ export async function POST(
|
|||||||
return jsonError(404, "未找到 PSD 模板");
|
return jsonError(404, "未找到 PSD 模板");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
schedulePsdTemplateAssetBackfill({
|
||||||
|
templateId: id,
|
||||||
|
updatedBy: session.user.id,
|
||||||
|
});
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
template: updatedTemplate,
|
template: updatedTemplate,
|
||||||
|
|||||||
267
lib/agents/psd-template-asset-backfill.ts
Normal file
267
lib/agents/psd-template-asset-backfill.ts
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
import "server-only";
|
||||||
|
|
||||||
|
import { upsertSystemSetting } from "@/lib/db/queries";
|
||||||
|
import {
|
||||||
|
AGENT_PSD_TEMPLATES_KEY,
|
||||||
|
AGENT_PSD_TEMPLATE_ASSET_VERSION,
|
||||||
|
isAgentPsdImageLayerSlot,
|
||||||
|
isAgentPsdTextLayerSlot,
|
||||||
|
type AgentPsdTemplate,
|
||||||
|
type AgentPsdTemplateLayerSlot,
|
||||||
|
} from "@/lib/agents/psd-template-config";
|
||||||
|
import { resolveAgentPsdLocalCachePath } from "@/lib/agents/psd-local-cache";
|
||||||
|
import { preparePsdTemplateRenderAssets } from "@/lib/agents/psd-template-parser";
|
||||||
|
import { readAgentPsdTemplates } from "@/lib/agents/psd-template-settings";
|
||||||
|
import { uploadFile } from "@/lib/storage/service";
|
||||||
|
|
||||||
|
type PsdTemplateAssetBackfillJobState = {
|
||||||
|
running: boolean;
|
||||||
|
rerun: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
// eslint-disable-next-line no-var
|
||||||
|
var __agentPsdTemplateAssetBackfillJobs:
|
||||||
|
| Map<string, PsdTemplateAssetBackfillJobState>
|
||||||
|
| undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getJobMap() {
|
||||||
|
if (!globalThis.__agentPsdTemplateAssetBackfillJobs) {
|
||||||
|
globalThis.__agentPsdTemplateAssetBackfillJobs = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
return globalThis.__agentPsdTemplateAssetBackfillJobs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toArrayBuffer(buffer: Buffer) {
|
||||||
|
return buffer.buffer.slice(
|
||||||
|
buffer.byteOffset,
|
||||||
|
buffer.byteOffset + buffer.byteLength
|
||||||
|
) as ArrayBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getErrorMessage(error: unknown) {
|
||||||
|
if (error instanceof Error && error.message.trim()) {
|
||||||
|
return error.message.trim();
|
||||||
|
}
|
||||||
|
return "PSD 后台资产补全失败";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateTemplateById(params: {
|
||||||
|
templateId: string;
|
||||||
|
updatedBy: string;
|
||||||
|
updater: (template: AgentPsdTemplate) => AgentPsdTemplate;
|
||||||
|
}) {
|
||||||
|
const templates = await readAgentPsdTemplates();
|
||||||
|
const currentTemplate = templates.find((entry) => entry.id === params.templateId);
|
||||||
|
|
||||||
|
if (!currentTemplate) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextTemplate = params.updater(currentTemplate);
|
||||||
|
await upsertSystemSetting({
|
||||||
|
key: AGENT_PSD_TEMPLATES_KEY,
|
||||||
|
value: templates.map((entry) => (entry.id === params.templateId ? nextTemplate : entry)),
|
||||||
|
updatedBy: params.updatedBy,
|
||||||
|
});
|
||||||
|
|
||||||
|
return nextTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergePreparedLayerAssets(params: {
|
||||||
|
currentLayers: AgentPsdTemplateLayerSlot[];
|
||||||
|
preparedLayers: AgentPsdTemplateLayerSlot[];
|
||||||
|
}) {
|
||||||
|
const preparedLayerMap = new Map(
|
||||||
|
params.preparedLayers.map((layer) => [layer.id, layer] as const)
|
||||||
|
);
|
||||||
|
|
||||||
|
return params.currentLayers.map((layer) => {
|
||||||
|
const preparedLayer = preparedLayerMap.get(layer.id);
|
||||||
|
if (!preparedLayer) {
|
||||||
|
return layer;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAgentPsdImageLayerSlot(layer) && isAgentPsdImageLayerSlot(preparedLayer)) {
|
||||||
|
return {
|
||||||
|
...layer,
|
||||||
|
previewUrl: preparedLayer.previewUrl || layer.previewUrl,
|
||||||
|
previewPath: preparedLayer.previewPath || layer.previewPath,
|
||||||
|
sourceUrl: preparedLayer.sourceUrl || layer.sourceUrl,
|
||||||
|
sourcePath: preparedLayer.sourcePath || layer.sourcePath,
|
||||||
|
contentBounds: preparedLayer.contentBounds || layer.contentBounds,
|
||||||
|
maskUrl: preparedLayer.maskUrl,
|
||||||
|
maskPath: preparedLayer.maskPath,
|
||||||
|
} satisfies AgentPsdTemplateLayerSlot;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAgentPsdTextLayerSlot(layer) && isAgentPsdTextLayerSlot(preparedLayer)) {
|
||||||
|
let nextLayer: AgentPsdTemplateLayerSlot = {
|
||||||
|
...layer,
|
||||||
|
previewUrl: preparedLayer.previewUrl || layer.previewUrl,
|
||||||
|
previewPath: preparedLayer.previewPath || layer.previewPath,
|
||||||
|
sourceUrl: preparedLayer.sourceUrl || layer.sourceUrl,
|
||||||
|
sourcePath: preparedLayer.sourcePath || layer.sourcePath,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (layer.styleSource !== "custom") {
|
||||||
|
nextLayer = {
|
||||||
|
...nextLayer,
|
||||||
|
textColor:
|
||||||
|
layer.textColor === "#111111" && preparedLayer.textColor
|
||||||
|
? preparedLayer.textColor
|
||||||
|
: layer.textColor,
|
||||||
|
textAlign:
|
||||||
|
layer.textAlign === "left" && preparedLayer.textAlign !== "left"
|
||||||
|
? preparedLayer.textAlign
|
||||||
|
: layer.textAlign,
|
||||||
|
paddingX: layer.paddingX > 0 ? layer.paddingX : preparedLayer.paddingX,
|
||||||
|
paddingY: layer.paddingY > 0 ? layer.paddingY : preparedLayer.paddingY,
|
||||||
|
fontId: layer.fontId || preparedLayer.fontId,
|
||||||
|
fontFamily: layer.fontFamily || preparedLayer.fontFamily,
|
||||||
|
fontLabel: layer.fontLabel || preparedLayer.fontLabel,
|
||||||
|
fontFileUrl: layer.fontFileUrl || preparedLayer.fontFileUrl,
|
||||||
|
fontContentType: layer.fontContentType || preparedLayer.fontContentType,
|
||||||
|
} satisfies AgentPsdTemplateLayerSlot;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextLayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
return layer;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function backfillTemplateAssetsOnce(params: {
|
||||||
|
templateId: string;
|
||||||
|
updatedBy: string;
|
||||||
|
}) {
|
||||||
|
const templates = await readAgentPsdTemplates();
|
||||||
|
const template = templates.find((entry) => entry.id === params.templateId);
|
||||||
|
|
||||||
|
if (!template || template.parseStatus !== "processing") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (template.layers.length === 0 || template.width <= 1 || template.height <= 1) {
|
||||||
|
throw new Error("PSD 模板尚未完成快速解析");
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateTemplateById({
|
||||||
|
templateId: template.id,
|
||||||
|
updatedBy: params.updatedBy,
|
||||||
|
updater: (currentTemplate) => ({
|
||||||
|
...currentTemplate,
|
||||||
|
parseStatus: "processing",
|
||||||
|
parseProgress: Math.max(62, Number(currentTemplate.parseProgress || 0)),
|
||||||
|
parseStage: "快速解析完成,正在后台补全图层资源",
|
||||||
|
parseError: undefined,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const preparedAssets = await preparePsdTemplateRenderAssets({
|
||||||
|
templateId: template.id,
|
||||||
|
fileUrl: template.fileUrl,
|
||||||
|
filePath: resolveAgentPsdLocalCachePath({
|
||||||
|
templateId: template.id,
|
||||||
|
fileName: template.fileName,
|
||||||
|
}),
|
||||||
|
layers: template.layers,
|
||||||
|
});
|
||||||
|
|
||||||
|
await updateTemplateById({
|
||||||
|
templateId: template.id,
|
||||||
|
updatedBy: params.updatedBy,
|
||||||
|
updater: (currentTemplate) => ({
|
||||||
|
...currentTemplate,
|
||||||
|
parseStatus: "processing",
|
||||||
|
parseProgress: Math.max(88, Number(currentTemplate.parseProgress || 0)),
|
||||||
|
parseStage: "正在后台生成模板底图",
|
||||||
|
parseError: undefined,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const uploadedRenderBase = await uploadFile(
|
||||||
|
`agent-psd-base-${template.id}-${Date.now()}.png`,
|
||||||
|
toArrayBuffer(preparedAssets.renderBaseBuffer)
|
||||||
|
);
|
||||||
|
|
||||||
|
await updateTemplateById({
|
||||||
|
templateId: template.id,
|
||||||
|
updatedBy: params.updatedBy,
|
||||||
|
updater: (currentTemplate) => {
|
||||||
|
const mergedLayers = mergePreparedLayerAssets({
|
||||||
|
currentLayers: currentTemplate.layers,
|
||||||
|
preparedLayers: preparedAssets.layers,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...currentTemplate,
|
||||||
|
renderBaseUrl: uploadedRenderBase.url,
|
||||||
|
renderBasePath: uploadedRenderBase.pathname,
|
||||||
|
layers: mergedLayers,
|
||||||
|
layerCount: mergedLayers.length,
|
||||||
|
assetVersion: AGENT_PSD_TEMPLATE_ASSET_VERSION,
|
||||||
|
parseStatus: "completed",
|
||||||
|
parseProgress: 100,
|
||||||
|
parseStage: "快速解析和后台资产补全已完成",
|
||||||
|
parseError: undefined,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function schedulePsdTemplateAssetBackfill(params: {
|
||||||
|
templateId: string;
|
||||||
|
updatedBy: string;
|
||||||
|
}) {
|
||||||
|
const jobs = getJobMap();
|
||||||
|
const existingJob = jobs.get(params.templateId);
|
||||||
|
if (existingJob?.running) {
|
||||||
|
existingJob.rerun = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const jobState: PsdTemplateAssetBackfillJobState = {
|
||||||
|
running: true,
|
||||||
|
rerun: false,
|
||||||
|
};
|
||||||
|
jobs.set(params.templateId, jobState);
|
||||||
|
|
||||||
|
queueMicrotask(() => {
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
do {
|
||||||
|
jobState.rerun = false;
|
||||||
|
await backfillTemplateAssetsOnce(params);
|
||||||
|
} while (jobState.rerun);
|
||||||
|
} catch (error) {
|
||||||
|
const message = getErrorMessage(error);
|
||||||
|
await updateTemplateById({
|
||||||
|
templateId: params.templateId,
|
||||||
|
updatedBy: params.updatedBy,
|
||||||
|
updater: (currentTemplate) => ({
|
||||||
|
...currentTemplate,
|
||||||
|
parseStatus: "failed",
|
||||||
|
parseProgress: Math.min(95, Math.max(45, Number(currentTemplate.parseProgress || 0))),
|
||||||
|
parseStage: "快速解析已完成,但后台补全资产失败",
|
||||||
|
parseError: message,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
}),
|
||||||
|
}).catch(() => undefined);
|
||||||
|
|
||||||
|
console.error("[PSD Template] background asset backfill failed:", {
|
||||||
|
templateId: params.templateId,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
jobs.delete(params.templateId);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user