fix: stabilize psd template parsing progress
This commit is contained in:
parent
37e2b66725
commit
800eb10ba4
@ -1,11 +1,8 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { upsertSystemSetting } from "@/lib/db/queries";
|
|
||||||
import { requireAgentWorkflowAdmin } from "@/lib/middleware/admin";
|
import { requireAgentWorkflowAdmin } from "@/lib/middleware/admin";
|
||||||
import { findBestMatchingPsdFont } from "@/lib/agents/psd-font-matching";
|
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,
|
|
||||||
type AgentPsdTemplate,
|
|
||||||
type AgentPsdTemplateLayerSlot,
|
type AgentPsdTemplateLayerSlot,
|
||||||
type AgentPsdTemplateMutationResponse,
|
type AgentPsdTemplateMutationResponse,
|
||||||
} from "@/lib/agents/psd-template-config";
|
} from "@/lib/agents/psd-template-config";
|
||||||
@ -15,6 +12,11 @@ import {
|
|||||||
} 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 { schedulePsdTemplateAssetBackfill } from "@/lib/agents/psd-template-asset-backfill";
|
||||||
|
import {
|
||||||
|
createAgentPsdTemplateParseHeartbeat,
|
||||||
|
updateAgentPsdTemplateById,
|
||||||
|
updateAgentPsdTemplateParseProgress,
|
||||||
|
} from "@/lib/agents/psd-template-progress";
|
||||||
import {
|
import {
|
||||||
cacheAgentPsdSourceLocally,
|
cacheAgentPsdSourceLocally,
|
||||||
hasAgentPsdLocalCache,
|
hasAgentPsdLocalCache,
|
||||||
@ -158,44 +160,15 @@ function buildInitialTemplateLayers(params: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateTemplate(
|
|
||||||
templateId: string,
|
|
||||||
updatedBy: string,
|
|
||||||
updater: (template: AgentPsdTemplate) => AgentPsdTemplate
|
|
||||||
) {
|
|
||||||
const templates = await readAgentPsdTemplates();
|
|
||||||
const currentTemplate = templates.find((entry) => entry.id === templateId);
|
|
||||||
|
|
||||||
if (!currentTemplate) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextTemplate = updater(currentTemplate);
|
|
||||||
await upsertSystemSetting({
|
|
||||||
key: AGENT_PSD_TEMPLATES_KEY,
|
|
||||||
value: templates.map((entry) => (entry.id === templateId ? nextTemplate : entry)),
|
|
||||||
updatedBy,
|
|
||||||
});
|
|
||||||
|
|
||||||
return nextTemplate;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateParseProgress(params: {
|
async function updateParseProgress(params: {
|
||||||
templateId: string;
|
templateId: string;
|
||||||
updatedBy: string;
|
updatedBy: string;
|
||||||
progress: number;
|
progress: number;
|
||||||
stage: string;
|
stage: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
restart?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { templateId, updatedBy, progress, stage, error } = params;
|
return updateAgentPsdTemplateParseProgress(params);
|
||||||
return updateTemplate(templateId, updatedBy, (template) => ({
|
|
||||||
...template,
|
|
||||||
parseStatus: "processing",
|
|
||||||
parseProgress: progress,
|
|
||||||
parseStage: stage,
|
|
||||||
parseError: error,
|
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(
|
export async function POST(
|
||||||
@ -231,137 +204,150 @@ export async function POST(
|
|||||||
return jsonError(409, "该模板正在解析中,请稍后刷新");
|
return jsonError(409, "该模板正在解析中,请稍后刷新");
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateParseProgress({
|
let latestProgress = {
|
||||||
templateId: id,
|
|
||||||
updatedBy: session.user.id,
|
|
||||||
progress: 5,
|
progress: 5,
|
||||||
stage: "准备解析 PSD",
|
stage: "准备解析 PSD",
|
||||||
error: undefined,
|
error: undefined as string | undefined,
|
||||||
});
|
};
|
||||||
|
const setParseProgress = async (progress: number, stage: string, error?: string) => {
|
||||||
let fileBuffer: ArrayBuffer;
|
latestProgress = {
|
||||||
if (
|
progress,
|
||||||
await hasAgentPsdLocalCache({
|
stage,
|
||||||
templateId: id,
|
error,
|
||||||
fileName: currentTemplate.fileName,
|
};
|
||||||
})
|
return updateParseProgress({
|
||||||
) {
|
|
||||||
await updateParseProgress({
|
|
||||||
templateId: id,
|
templateId: id,
|
||||||
updatedBy: session.user.id,
|
updatedBy: session.user.id,
|
||||||
progress: 18,
|
progress,
|
||||||
stage: "正在读取服务器本地 PSD 缓存",
|
stage,
|
||||||
error: undefined,
|
error,
|
||||||
});
|
|
||||||
const localBuffer = await readAgentPsdSourceFromLocalCache({
|
|
||||||
templateId: id,
|
|
||||||
fileName: currentTemplate.fileName,
|
|
||||||
});
|
|
||||||
fileBuffer = localBuffer.buffer.slice(
|
|
||||||
localBuffer.byteOffset,
|
|
||||||
localBuffer.byteOffset + localBuffer.byteLength
|
|
||||||
) as ArrayBuffer;
|
|
||||||
} else {
|
|
||||||
const response = await fetch(currentTemplate.fileUrl, {
|
|
||||||
cache: "no-store",
|
|
||||||
signal: AbortSignal.timeout(60_000),
|
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
if (!response.ok) {
|
await updateParseProgress({
|
||||||
throw new Error(`PSD 文件下载失败 (${response.status})`);
|
templateId: id,
|
||||||
|
updatedBy: session.user.id,
|
||||||
|
...latestProgress,
|
||||||
|
error: undefined,
|
||||||
|
restart: true,
|
||||||
|
});
|
||||||
|
const heartbeat = createAgentPsdTemplateParseHeartbeat({
|
||||||
|
templateId: id,
|
||||||
|
updatedBy: session.user.id,
|
||||||
|
getSnapshot: () => latestProgress,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
let fileBuffer: ArrayBuffer;
|
||||||
|
if (
|
||||||
|
await hasAgentPsdLocalCache({
|
||||||
|
templateId: id,
|
||||||
|
fileName: currentTemplate.fileName,
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
await setParseProgress(18, "正在读取服务器本地 PSD 缓存");
|
||||||
|
const localBuffer = await readAgentPsdSourceFromLocalCache({
|
||||||
|
templateId: id,
|
||||||
|
fileName: currentTemplate.fileName,
|
||||||
|
});
|
||||||
|
fileBuffer = localBuffer.buffer.slice(
|
||||||
|
localBuffer.byteOffset,
|
||||||
|
localBuffer.byteOffset + localBuffer.byteLength
|
||||||
|
) as ArrayBuffer;
|
||||||
|
} else {
|
||||||
|
const response = await fetch(currentTemplate.fileUrl, {
|
||||||
|
cache: "no-store",
|
||||||
|
signal: AbortSignal.timeout(60_000),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`PSD 文件下载失败 (${response.status})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await setParseProgress(18, "正在下载并写入服务器本地 PSD 缓存");
|
||||||
|
|
||||||
|
fileBuffer = await response.arrayBuffer();
|
||||||
|
await cacheAgentPsdSourceLocally({
|
||||||
|
templateId: id,
|
||||||
|
fileName: currentTemplate.fileName,
|
||||||
|
buffer: fileBuffer,
|
||||||
|
}).catch((error) => {
|
||||||
|
console.warn("[PSD Templates][Admin] failed to cache PSD source locally:", {
|
||||||
|
templateId: id,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateParseProgress({
|
await setParseProgress(24, "正在解析 PSD 图层结构与预览");
|
||||||
|
const parsedTemplate = await parsePsdTemplateFile(fileBuffer, {
|
||||||
|
onProgress: async (event) => {
|
||||||
|
await setParseProgress(event.progress, event.stage);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (parsedTemplate.layers.length === 0) {
|
||||||
|
throw new Error("PSD 中未识别到可配置图层");
|
||||||
|
}
|
||||||
|
|
||||||
|
await setParseProgress(42, "正在上传模板预览");
|
||||||
|
|
||||||
|
const uploadedPreview = await uploadFile(
|
||||||
|
`agent-psd-preview-${id}-${Date.now()}.png`,
|
||||||
|
toArrayBuffer(parsedTemplate.previewBuffer)
|
||||||
|
);
|
||||||
|
|
||||||
|
await setParseProgress(58, "正在匹配字体与初始化图层");
|
||||||
|
|
||||||
|
const fonts = await readAgentPsdFonts().catch(() => []);
|
||||||
|
const initialLayers = buildInitialTemplateLayers({
|
||||||
|
parsedLayers: parsedTemplate.layers,
|
||||||
|
fonts,
|
||||||
|
canvasWidth: parsedTemplate.width,
|
||||||
|
canvasHeight: parsedTemplate.height,
|
||||||
|
});
|
||||||
|
|
||||||
|
heartbeat.stop();
|
||||||
|
const updatedTemplate = await updateAgentPsdTemplateById({
|
||||||
templateId: id,
|
templateId: id,
|
||||||
updatedBy: session.user.id,
|
updatedBy: session.user.id,
|
||||||
progress: 18,
|
updater: (template) => {
|
||||||
stage: "正在下载并写入服务器本地 PSD 缓存",
|
const now = new Date().toISOString();
|
||||||
error: undefined,
|
return {
|
||||||
|
...template,
|
||||||
|
previewUrl: uploadedPreview.url,
|
||||||
|
previewPath: uploadedPreview.pathname,
|
||||||
|
width: parsedTemplate.width,
|
||||||
|
height: parsedTemplate.height,
|
||||||
|
layerCount: parsedTemplate.layers.length,
|
||||||
|
layers: initialLayers,
|
||||||
|
parseStatus: "processing",
|
||||||
|
parseProgress: 48,
|
||||||
|
parseStage: "快速解析完成,正在后台补全图层资源和模板底图",
|
||||||
|
parseError: undefined,
|
||||||
|
parseHeartbeatAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
fileBuffer = await response.arrayBuffer();
|
if (!updatedTemplate) {
|
||||||
await cacheAgentPsdSourceLocally({
|
return jsonError(404, "未找到 PSD 模板");
|
||||||
|
}
|
||||||
|
|
||||||
|
schedulePsdTemplateAssetBackfill({
|
||||||
templateId: id,
|
templateId: id,
|
||||||
fileName: currentTemplate.fileName,
|
updatedBy: session.user.id,
|
||||||
buffer: fileBuffer,
|
|
||||||
}).catch((error) => {
|
|
||||||
console.warn("[PSD Templates][Admin] failed to cache PSD source locally:", {
|
|
||||||
templateId: id,
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
template: updatedTemplate,
|
||||||
|
} satisfies AgentPsdTemplateMutationResponse);
|
||||||
|
} catch (error) {
|
||||||
|
heartbeat.stop();
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateParseProgress({
|
|
||||||
templateId: id,
|
|
||||||
updatedBy: session.user.id,
|
|
||||||
progress: 24,
|
|
||||||
stage: "正在解析 PSD 图层结构与预览",
|
|
||||||
error: undefined,
|
|
||||||
});
|
|
||||||
const parsedTemplate = await parsePsdTemplateFile(fileBuffer);
|
|
||||||
|
|
||||||
if (parsedTemplate.layers.length === 0) {
|
|
||||||
throw new Error("PSD 中未识别到可配置图层");
|
|
||||||
}
|
|
||||||
|
|
||||||
await updateParseProgress({
|
|
||||||
templateId: id,
|
|
||||||
updatedBy: session.user.id,
|
|
||||||
progress: 42,
|
|
||||||
stage: "正在上传模板预览",
|
|
||||||
error: undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
const uploadedPreview = await uploadFile(
|
|
||||||
`agent-psd-preview-${id}-${Date.now()}.png`,
|
|
||||||
toArrayBuffer(parsedTemplate.previewBuffer)
|
|
||||||
);
|
|
||||||
|
|
||||||
await updateParseProgress({
|
|
||||||
templateId: id,
|
|
||||||
updatedBy: session.user.id,
|
|
||||||
progress: 58,
|
|
||||||
stage: "正在匹配字体与初始化图层",
|
|
||||||
error: undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
const fonts = await readAgentPsdFonts().catch(() => []);
|
|
||||||
const initialLayers = buildInitialTemplateLayers({
|
|
||||||
parsedLayers: parsedTemplate.layers,
|
|
||||||
fonts,
|
|
||||||
canvasWidth: parsedTemplate.width,
|
|
||||||
canvasHeight: parsedTemplate.height,
|
|
||||||
});
|
|
||||||
|
|
||||||
const updatedTemplate = await updateTemplate(id, session.user.id, (template) => ({
|
|
||||||
...template,
|
|
||||||
previewUrl: uploadedPreview.url,
|
|
||||||
previewPath: uploadedPreview.pathname,
|
|
||||||
width: parsedTemplate.width,
|
|
||||||
height: parsedTemplate.height,
|
|
||||||
layerCount: parsedTemplate.layers.length,
|
|
||||||
layers: initialLayers,
|
|
||||||
parseStatus: "processing",
|
|
||||||
parseProgress: 48,
|
|
||||||
parseStage: "快速解析完成,正在后台补全图层资源和模板底图",
|
|
||||||
parseError: undefined,
|
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (!updatedTemplate) {
|
|
||||||
return jsonError(404, "未找到 PSD 模板");
|
|
||||||
}
|
|
||||||
|
|
||||||
schedulePsdTemplateAssetBackfill({
|
|
||||||
templateId: id,
|
|
||||||
updatedBy: session.user.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
template: updatedTemplate,
|
|
||||||
} satisfies AgentPsdTemplateMutationResponse);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = getErrorMessage(error);
|
const message = getErrorMessage(error);
|
||||||
const existingTemplate = (await readAgentPsdTemplates()).find((entry) => entry.id === id);
|
const existingTemplate = (await readAgentPsdTemplates()).find((entry) => entry.id === id);
|
||||||
@ -372,18 +358,22 @@ export async function POST(
|
|||||||
? "completed"
|
? "completed"
|
||||||
: "failed";
|
: "failed";
|
||||||
|
|
||||||
await updateTemplate(id, session.user.id, (template) => ({
|
await updateAgentPsdTemplateById({
|
||||||
...template,
|
templateId: id,
|
||||||
parseStatus: fallbackStatus,
|
updatedBy: session.user.id,
|
||||||
parseProgress: fallbackStatus === "completed" ? 100 : 0,
|
updater: (template) => ({
|
||||||
parseStage:
|
...template,
|
||||||
fallbackStatus === "completed" ? "沿用上次成功解析结果" : undefined,
|
parseStatus: fallbackStatus,
|
||||||
parseError:
|
parseProgress: fallbackStatus === "completed" ? 100 : 0,
|
||||||
fallbackStatus === "completed"
|
parseStage:
|
||||||
? `重新解析失败:${message}`
|
fallbackStatus === "completed" ? "沿用上次成功解析结果" : undefined,
|
||||||
: message,
|
parseError:
|
||||||
updatedAt: new Date().toISOString(),
|
fallbackStatus === "completed"
|
||||||
})).catch(() => undefined);
|
? `重新解析失败:${message}`
|
||||||
|
: message,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
}),
|
||||||
|
}).catch(() => undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error("[PSD Templates][Admin] parse failed:", error);
|
console.error("[PSD Templates][Admin] parse failed:", error);
|
||||||
|
|||||||
@ -1,14 +1,16 @@
|
|||||||
import "server-only";
|
import "server-only";
|
||||||
|
|
||||||
import { upsertSystemSetting } from "@/lib/db/queries";
|
|
||||||
import {
|
import {
|
||||||
AGENT_PSD_TEMPLATES_KEY,
|
|
||||||
AGENT_PSD_TEMPLATE_ASSET_VERSION,
|
AGENT_PSD_TEMPLATE_ASSET_VERSION,
|
||||||
isAgentPsdImageLayerSlot,
|
isAgentPsdImageLayerSlot,
|
||||||
isAgentPsdTextLayerSlot,
|
isAgentPsdTextLayerSlot,
|
||||||
type AgentPsdTemplate,
|
|
||||||
type AgentPsdTemplateLayerSlot,
|
type AgentPsdTemplateLayerSlot,
|
||||||
} from "@/lib/agents/psd-template-config";
|
} from "@/lib/agents/psd-template-config";
|
||||||
|
import {
|
||||||
|
createAgentPsdTemplateParseHeartbeat,
|
||||||
|
updateAgentPsdTemplateById,
|
||||||
|
updateAgentPsdTemplateParseProgress,
|
||||||
|
} from "@/lib/agents/psd-template-progress";
|
||||||
import { resolveAgentPsdLocalCachePath } from "@/lib/agents/psd-local-cache";
|
import { resolveAgentPsdLocalCachePath } from "@/lib/agents/psd-local-cache";
|
||||||
import {
|
import {
|
||||||
preparePsdTemplateRenderAssets,
|
preparePsdTemplateRenderAssets,
|
||||||
@ -51,28 +53,6 @@ function getErrorMessage(error: unknown) {
|
|||||||
return "PSD 后台资产补全失败";
|
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: {
|
function mergePreparedLayerAssets(params: {
|
||||||
currentLayers: AgentPsdTemplateLayerSlot[];
|
currentLayers: AgentPsdTemplateLayerSlot[];
|
||||||
preparedLayers: AgentPsdTemplateLayerSlot[];
|
preparedLayers: AgentPsdTemplateLayerSlot[];
|
||||||
@ -152,79 +132,98 @@ async function backfillTemplateAssetsOnce(params: {
|
|||||||
throw new Error("PSD 模板尚未完成快速解析");
|
throw new Error("PSD 模板尚未完成快速解析");
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateTemplateById({
|
let latestProgress = {
|
||||||
templateId: template.id,
|
progress: Math.max(62, Number(template.parseProgress || 0)),
|
||||||
updatedBy: params.updatedBy,
|
stage: "快速解析完成,正在后台补全图层资源",
|
||||||
updater: (currentTemplate) => ({
|
error: undefined as string | undefined,
|
||||||
...currentTemplate,
|
};
|
||||||
parseStatus: "processing",
|
const setBackfillProgress = async (
|
||||||
parseProgress: Math.max(62, Number(currentTemplate.parseProgress || 0)),
|
progress: number,
|
||||||
parseStage: "快速解析完成,正在后台补全图层资源",
|
stage: string,
|
||||||
parseError: undefined,
|
error?: string
|
||||||
updatedAt: new Date().toISOString(),
|
) => {
|
||||||
}),
|
latestProgress = {
|
||||||
});
|
progress,
|
||||||
|
stage,
|
||||||
const uploadAsset = await createStorageUploadSession();
|
error,
|
||||||
const preparedAssets = await preparePsdTemplateRenderAssets({
|
};
|
||||||
templateId: template.id,
|
await updateAgentPsdTemplateParseProgress({
|
||||||
fileUrl: template.fileUrl,
|
|
||||||
filePath: resolveAgentPsdLocalCachePath({
|
|
||||||
templateId: template.id,
|
templateId: template.id,
|
||||||
fileName: template.fileName,
|
updatedBy: params.updatedBy,
|
||||||
}),
|
progress,
|
||||||
layers: template.layers,
|
stage,
|
||||||
uploadAsset,
|
error,
|
||||||
});
|
});
|
||||||
|
};
|
||||||
await updateTemplateById({
|
const heartbeat = createAgentPsdTemplateParseHeartbeat({
|
||||||
templateId: template.id,
|
templateId: template.id,
|
||||||
updatedBy: params.updatedBy,
|
updatedBy: params.updatedBy,
|
||||||
updater: (currentTemplate) => ({
|
getSnapshot: () => latestProgress,
|
||||||
...currentTemplate,
|
|
||||||
parseStatus: "processing",
|
|
||||||
parseProgress: Math.max(88, Number(currentTemplate.parseProgress || 0)),
|
|
||||||
parseStage: "正在后台生成模板底图和快速导出分层",
|
|
||||||
parseError: undefined,
|
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const uploadedRenderBase = await uploadAsset(
|
await setBackfillProgress(latestProgress.progress, latestProgress.stage);
|
||||||
`agent-psd-base-${template.id}-${Date.now()}.png`,
|
|
||||||
toArrayBuffer(preparedAssets.renderBaseBuffer)
|
|
||||||
);
|
|
||||||
const uploadedRenderSegments = await uploadPreparedPsdTemplateRenderSegments({
|
|
||||||
templateId: template.id,
|
|
||||||
segments: preparedAssets.renderSegments,
|
|
||||||
uploadAsset,
|
|
||||||
});
|
|
||||||
|
|
||||||
await updateTemplateById({
|
try {
|
||||||
templateId: template.id,
|
const uploadAsset = await createStorageUploadSession();
|
||||||
updatedBy: params.updatedBy,
|
const preparedAssets = await preparePsdTemplateRenderAssets({
|
||||||
updater: (currentTemplate) => {
|
templateId: template.id,
|
||||||
const mergedLayers = mergePreparedLayerAssets({
|
fileUrl: template.fileUrl,
|
||||||
currentLayers: currentTemplate.layers,
|
filePath: resolveAgentPsdLocalCachePath({
|
||||||
preparedLayers: preparedAssets.layers,
|
templateId: template.id,
|
||||||
});
|
fileName: template.fileName,
|
||||||
|
}),
|
||||||
|
layers: template.layers,
|
||||||
|
uploadAsset,
|
||||||
|
onProgress: async (event) => {
|
||||||
|
await setBackfillProgress(event.progress, event.stage);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
await setBackfillProgress(88, "正在后台生成模板底图和快速导出分层");
|
||||||
...currentTemplate,
|
|
||||||
renderBaseUrl: uploadedRenderBase.url,
|
const uploadedRenderBase = await uploadAsset(
|
||||||
renderBasePath: uploadedRenderBase.pathname,
|
`agent-psd-base-${template.id}-${Date.now()}.png`,
|
||||||
renderSegments: uploadedRenderSegments,
|
toArrayBuffer(preparedAssets.renderBaseBuffer)
|
||||||
layers: mergedLayers,
|
);
|
||||||
layerCount: mergedLayers.length,
|
await setBackfillProgress(96, "正在上传快速导出分层");
|
||||||
assetVersion: AGENT_PSD_TEMPLATE_ASSET_VERSION,
|
const uploadedRenderSegments = await uploadPreparedPsdTemplateRenderSegments({
|
||||||
parseStatus: "completed",
|
templateId: template.id,
|
||||||
parseProgress: 100,
|
segments: preparedAssets.renderSegments,
|
||||||
parseStage: "快速解析和可用资产补全已完成",
|
uploadAsset,
|
||||||
parseError: undefined,
|
});
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
};
|
heartbeat.stop();
|
||||||
},
|
await updateAgentPsdTemplateById({
|
||||||
});
|
templateId: template.id,
|
||||||
|
updatedBy: params.updatedBy,
|
||||||
|
updater: (currentTemplate) => {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const mergedLayers = mergePreparedLayerAssets({
|
||||||
|
currentLayers: currentTemplate.layers,
|
||||||
|
preparedLayers: preparedAssets.layers,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...currentTemplate,
|
||||||
|
renderBaseUrl: uploadedRenderBase.url,
|
||||||
|
renderBasePath: uploadedRenderBase.pathname,
|
||||||
|
renderSegments: uploadedRenderSegments,
|
||||||
|
layers: mergedLayers,
|
||||||
|
layerCount: mergedLayers.length,
|
||||||
|
assetVersion: AGENT_PSD_TEMPLATE_ASSET_VERSION,
|
||||||
|
parseStatus: "completed",
|
||||||
|
parseProgress: 100,
|
||||||
|
parseStage: "快速解析和可用资产补全已完成",
|
||||||
|
parseError: undefined,
|
||||||
|
parseHeartbeatAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
heartbeat.stop();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function schedulePsdTemplateAssetBackfill(params: {
|
export function schedulePsdTemplateAssetBackfill(params: {
|
||||||
@ -253,7 +252,7 @@ export function schedulePsdTemplateAssetBackfill(params: {
|
|||||||
} while (jobState.rerun);
|
} while (jobState.rerun);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = getErrorMessage(error);
|
const message = getErrorMessage(error);
|
||||||
await updateTemplateById({
|
await updateAgentPsdTemplateById({
|
||||||
templateId: params.templateId,
|
templateId: params.templateId,
|
||||||
updatedBy: params.updatedBy,
|
updatedBy: params.updatedBy,
|
||||||
updater: (currentTemplate) => ({
|
updater: (currentTemplate) => ({
|
||||||
|
|||||||
@ -105,6 +105,8 @@ export interface AgentPsdTemplate {
|
|||||||
parseProgress?: number;
|
parseProgress?: number;
|
||||||
parseStage?: string;
|
parseStage?: string;
|
||||||
parseError?: string;
|
parseError?: string;
|
||||||
|
parseStartedAt?: string;
|
||||||
|
parseHeartbeatAt?: string;
|
||||||
fileName: string;
|
fileName: string;
|
||||||
fileUrl: string;
|
fileUrl: string;
|
||||||
filePath?: string;
|
filePath?: string;
|
||||||
@ -528,6 +530,8 @@ export function parseAgentPsdTemplates(value: unknown) {
|
|||||||
|
|
||||||
const parseStatus = normalizeAgentPsdTemplateParseStatus(record.parseStatus);
|
const parseStatus = normalizeAgentPsdTemplateParseStatus(record.parseStatus);
|
||||||
const rawParseProgress = normalizePositiveInteger(record.parseProgress);
|
const rawParseProgress = normalizePositiveInteger(record.parseProgress);
|
||||||
|
const parseStartedAt = toIsoDateString(record.parseStartedAt) || undefined;
|
||||||
|
const parseHeartbeatAt = toIsoDateString(record.parseHeartbeatAt) || undefined;
|
||||||
|
|
||||||
const categoryTags = normalizeAgentPsdTemplateCategoryTags(
|
const categoryTags = normalizeAgentPsdTemplateCategoryTags(
|
||||||
record.categoryTags ?? record.productCategoryTags
|
record.categoryTags ?? record.productCategoryTags
|
||||||
@ -549,6 +553,8 @@ export function parseAgentPsdTemplates(value: unknown) {
|
|||||||
: Math.min(100, Math.max(0, rawParseProgress)),
|
: Math.min(100, Math.max(0, rawParseProgress)),
|
||||||
parseStage: normalizeText(record.parseStage) || undefined,
|
parseStage: normalizeText(record.parseStage) || undefined,
|
||||||
parseError: normalizeText(record.parseError) || undefined,
|
parseError: normalizeText(record.parseError) || undefined,
|
||||||
|
parseStartedAt,
|
||||||
|
parseHeartbeatAt,
|
||||||
fileName,
|
fileName,
|
||||||
fileUrl,
|
fileUrl,
|
||||||
filePath: normalizeText(record.filePath) || undefined,
|
filePath: normalizeText(record.filePath) || undefined,
|
||||||
|
|||||||
@ -126,6 +126,26 @@ export interface PreparedPsdTemplateRenderSegment {
|
|||||||
buffer: Buffer;
|
buffer: Buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PsdTemplateParseProgressEvent {
|
||||||
|
progress: number;
|
||||||
|
stage: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type PsdTemplateParseProgressReporter = (
|
||||||
|
event: PsdTemplateParseProgressEvent
|
||||||
|
) => void | Promise<void>;
|
||||||
|
|
||||||
|
async function reportPsdTemplateParseProgress(
|
||||||
|
reporter: PsdTemplateParseProgressReporter | undefined,
|
||||||
|
event: PsdTemplateParseProgressEvent
|
||||||
|
) {
|
||||||
|
if (!reporter) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await reporter(event);
|
||||||
|
}
|
||||||
|
|
||||||
function resolveVisibleContentBounds(params: {
|
function resolveVisibleContentBounds(params: {
|
||||||
rgba: Uint8ClampedArray;
|
rgba: Uint8ClampedArray;
|
||||||
width: number;
|
width: number;
|
||||||
@ -509,17 +529,44 @@ async function resolvePsdSourceBuffer(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function parsePsdTemplateFile(
|
export async function parsePsdTemplateFile(
|
||||||
buffer: ArrayBuffer
|
buffer: ArrayBuffer,
|
||||||
|
options?: {
|
||||||
|
onProgress?: PsdTemplateParseProgressReporter;
|
||||||
|
}
|
||||||
): Promise<ParsedPsdTemplatePayload> {
|
): Promise<ParsedPsdTemplatePayload> {
|
||||||
|
await reportPsdTemplateParseProgress(options?.onProgress, {
|
||||||
|
progress: 26,
|
||||||
|
stage: "正在加载 PSD 解析引擎",
|
||||||
|
});
|
||||||
const psd = await parsePsdBuffer(buffer);
|
const psd = await parsePsdBuffer(buffer);
|
||||||
const width = Math.max(1, Math.round(Number(psd.width || 0)));
|
const width = Math.max(1, Math.round(Number(psd.width || 0)));
|
||||||
const height = Math.max(1, Math.round(Number(psd.height || 0)));
|
const height = Math.max(1, Math.round(Number(psd.height || 0)));
|
||||||
|
|
||||||
|
await reportPsdTemplateParseProgress(options?.onProgress, {
|
||||||
|
progress: 30,
|
||||||
|
stage: "正在合成 PSD 整体预览",
|
||||||
|
});
|
||||||
const previewRgba = await psd.composite();
|
const previewRgba = await psd.composite();
|
||||||
|
|
||||||
|
await reportPsdTemplateParseProgress(options?.onProgress, {
|
||||||
|
progress: 35,
|
||||||
|
stage: "正在生成 PSD 预览图",
|
||||||
|
});
|
||||||
const previewBuffer = await rgbaToPngBuffer(previewRgba, width, height);
|
const previewBuffer = await rgbaToPngBuffer(previewRgba, width, height);
|
||||||
|
|
||||||
|
await reportPsdTemplateParseProgress(options?.onProgress, {
|
||||||
|
progress: 39,
|
||||||
|
stage: "正在扫描可配置图层",
|
||||||
|
});
|
||||||
const layers = collectPsdLayerEntries(Array.isArray(psd.children) ? psd.children : []).map(
|
const layers = collectPsdLayerEntries(Array.isArray(psd.children) ? psd.children : []).map(
|
||||||
(entry) => entry.layer
|
(entry) => entry.layer
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await reportPsdTemplateParseProgress(options?.onProgress, {
|
||||||
|
progress: 41,
|
||||||
|
stage: `已识别 ${layers.length} 个可配置图层`,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
@ -705,11 +752,17 @@ export async function uploadPreparedPsdTemplateRenderSegments(params: {
|
|||||||
async function mapWithConcurrency<T, R>(
|
async function mapWithConcurrency<T, R>(
|
||||||
items: T[],
|
items: T[],
|
||||||
concurrency: number,
|
concurrency: number,
|
||||||
mapper: (item: T, index: number) => Promise<R>
|
mapper: (item: T, index: number) => Promise<R>,
|
||||||
|
onItemComplete?: (params: {
|
||||||
|
completed: number;
|
||||||
|
total: number;
|
||||||
|
index: number;
|
||||||
|
}) => void | Promise<void>
|
||||||
) {
|
) {
|
||||||
const results = new Array<R>(items.length);
|
const results = new Array<R>(items.length);
|
||||||
const workerCount = Math.max(1, Math.min(Math.round(concurrency), items.length || 1));
|
const workerCount = Math.max(1, Math.min(Math.round(concurrency), items.length || 1));
|
||||||
let nextIndex = 0;
|
let nextIndex = 0;
|
||||||
|
let completed = 0;
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
Array.from({ length: workerCount }, async () => {
|
Array.from({ length: workerCount }, async () => {
|
||||||
@ -717,6 +770,12 @@ async function mapWithConcurrency<T, R>(
|
|||||||
const currentIndex = nextIndex;
|
const currentIndex = nextIndex;
|
||||||
nextIndex += 1;
|
nextIndex += 1;
|
||||||
results[currentIndex] = await mapper(items[currentIndex], currentIndex);
|
results[currentIndex] = await mapper(items[currentIndex], currentIndex);
|
||||||
|
completed += 1;
|
||||||
|
await onItemComplete?.({
|
||||||
|
completed,
|
||||||
|
total: items.length,
|
||||||
|
index: currentIndex,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -752,16 +811,27 @@ export async function preparePsdTemplateRenderAssets(params: {
|
|||||||
assetProfile?: PsdTemplateLayerAssetProfile;
|
assetProfile?: PsdTemplateLayerAssetProfile;
|
||||||
assetConcurrency?: number;
|
assetConcurrency?: number;
|
||||||
uploadAsset?: StorageUploadFunction;
|
uploadAsset?: StorageUploadFunction;
|
||||||
|
onProgress?: PsdTemplateParseProgressReporter;
|
||||||
}) {
|
}) {
|
||||||
const assetProfile = params.assetProfile || "fast-runtime";
|
const assetProfile = params.assetProfile || "fast-runtime";
|
||||||
const assetConcurrency =
|
const assetConcurrency =
|
||||||
params.assetConcurrency || DEFAULT_PSD_TEMPLATE_ASSET_CONCURRENCY;
|
params.assetConcurrency || DEFAULT_PSD_TEMPLATE_ASSET_CONCURRENCY;
|
||||||
const uploadAsset = params.uploadAsset || uploadFile;
|
const uploadAsset = params.uploadAsset || uploadFile;
|
||||||
|
|
||||||
|
await reportPsdTemplateParseProgress(params.onProgress, {
|
||||||
|
progress: 63,
|
||||||
|
stage: "正在读取 PSD 源文件并准备补全资产",
|
||||||
|
});
|
||||||
const psdBuffer = await resolvePsdSourceBuffer({
|
const psdBuffer = await resolvePsdSourceBuffer({
|
||||||
fileUrl: params.fileUrl,
|
fileUrl: params.fileUrl,
|
||||||
filePath: params.filePath,
|
filePath: params.filePath,
|
||||||
fileBuffer: params.fileBuffer,
|
fileBuffer: params.fileBuffer,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await reportPsdTemplateParseProgress(params.onProgress, {
|
||||||
|
progress: 65,
|
||||||
|
stage: "正在解析 PSD 图层资源",
|
||||||
|
});
|
||||||
const psd = await parsePsdBuffer(psdBuffer);
|
const psd = await parsePsdBuffer(psdBuffer);
|
||||||
const canvasWidth = Math.max(1, Math.round(Number(psd.width || 0)));
|
const canvasWidth = Math.max(1, Math.round(Number(psd.width || 0)));
|
||||||
const canvasHeight = Math.max(1, Math.round(Number(psd.height || 0)));
|
const canvasHeight = Math.max(1, Math.round(Number(psd.height || 0)));
|
||||||
@ -778,6 +848,11 @@ export async function preparePsdTemplateRenderAssets(params: {
|
|||||||
|
|
||||||
const nextLayers = [...params.layers];
|
const nextLayers = [...params.layers];
|
||||||
|
|
||||||
|
await reportPsdTemplateParseProgress(params.onProgress, {
|
||||||
|
progress: 66,
|
||||||
|
stage: `正在补全图层缩略图和源图 0/${nextLayers.length}`,
|
||||||
|
});
|
||||||
|
|
||||||
const preparedLayerResults = await mapWithConcurrency(
|
const preparedLayerResults = await mapWithConcurrency(
|
||||||
nextLayers,
|
nextLayers,
|
||||||
assetConcurrency,
|
assetConcurrency,
|
||||||
@ -934,6 +1009,17 @@ export async function preparePsdTemplateRenderAssets(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return preparedLayer;
|
return preparedLayer;
|
||||||
|
},
|
||||||
|
async ({ completed, total }) => {
|
||||||
|
if (completed !== total && completed % 4 !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const progress = 66 + Math.round((completed / Math.max(1, total)) * 12);
|
||||||
|
await reportPsdTemplateParseProgress(params.onProgress, {
|
||||||
|
progress,
|
||||||
|
stage: `正在补全图层缩略图和源图 ${completed}/${total}`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -945,6 +1031,11 @@ export async function preparePsdTemplateRenderAssets(params: {
|
|||||||
.reverse()
|
.reverse()
|
||||||
.filter((entry) => entry.node && typeof entry.node.composite === "function");
|
.filter((entry) => entry.node && typeof entry.node.composite === "function");
|
||||||
|
|
||||||
|
await reportPsdTemplateParseProgress(params.onProgress, {
|
||||||
|
progress: 79,
|
||||||
|
stage: `正在生成模板底图图层 0/${drawableEntries.length}`,
|
||||||
|
});
|
||||||
|
|
||||||
const layerCompositeResults = await mapWithConcurrency(
|
const layerCompositeResults = await mapWithConcurrency(
|
||||||
drawableEntries,
|
drawableEntries,
|
||||||
assetConcurrency,
|
assetConcurrency,
|
||||||
@ -996,6 +1087,17 @@ export async function preparePsdTemplateRenderAssets(params: {
|
|||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
async ({ completed, total }) => {
|
||||||
|
if (completed !== total && completed % 4 !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const progress = 79 + Math.round((completed / Math.max(1, total)) * 9);
|
||||||
|
await reportPsdTemplateParseProgress(params.onProgress, {
|
||||||
|
progress,
|
||||||
|
stage: `正在生成模板底图图层 ${completed}/${total}`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -1025,6 +1127,11 @@ export async function preparePsdTemplateRenderAssets(params: {
|
|||||||
.map((result) => result.input);
|
.map((result) => result.input);
|
||||||
|
|
||||||
const sharp = await getSharp();
|
const sharp = await getSharp();
|
||||||
|
|
||||||
|
await reportPsdTemplateParseProgress(params.onProgress, {
|
||||||
|
progress: 89,
|
||||||
|
stage: "正在合成模板底图",
|
||||||
|
});
|
||||||
const transparentBaseBuffer = await createTransparentCanvasBuffer(
|
const transparentBaseBuffer = await createTransparentCanvasBuffer(
|
||||||
canvasWidth,
|
canvasWidth,
|
||||||
canvasHeight
|
canvasHeight
|
||||||
@ -1042,6 +1149,10 @@ export async function preparePsdTemplateRenderAssets(params: {
|
|||||||
for (const result of sortedLayerCompositeResults) {
|
for (const result of sortedLayerCompositeResults) {
|
||||||
if (result.editableBoundary) {
|
if (result.editableBoundary) {
|
||||||
if (pendingSegmentInputs.length > 0) {
|
if (pendingSegmentInputs.length > 0) {
|
||||||
|
await reportPsdTemplateParseProgress(params.onProgress, {
|
||||||
|
progress: 92,
|
||||||
|
stage: "正在生成快速导出分层",
|
||||||
|
});
|
||||||
renderSegments.push({
|
renderSegments.push({
|
||||||
id: `before-${sanitizeFileSegment(
|
id: `before-${sanitizeFileSegment(
|
||||||
result.beforeLayerId,
|
result.beforeLayerId,
|
||||||
@ -1061,6 +1172,10 @@ export async function preparePsdTemplateRenderAssets(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (pendingSegmentInputs.length > 0) {
|
if (pendingSegmentInputs.length > 0) {
|
||||||
|
await reportPsdTemplateParseProgress(params.onProgress, {
|
||||||
|
progress: 94,
|
||||||
|
stage: "正在生成顶部快速导出分层",
|
||||||
|
});
|
||||||
renderSegments.push({
|
renderSegments.push({
|
||||||
id: "above-editable-layers",
|
id: "above-editable-layers",
|
||||||
buffer: await composeSegmentBuffer(pendingSegmentInputs),
|
buffer: await composeSegmentBuffer(pendingSegmentInputs),
|
||||||
|
|||||||
127
lib/agents/psd-template-progress.ts
Normal file
127
lib/agents/psd-template-progress.ts
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import "server-only";
|
||||||
|
|
||||||
|
import { upsertSystemSetting } from "@/lib/db/queries";
|
||||||
|
import {
|
||||||
|
AGENT_PSD_TEMPLATES_KEY,
|
||||||
|
type AgentPsdTemplate,
|
||||||
|
} from "@/lib/agents/psd-template-config";
|
||||||
|
import { readAgentPsdTemplates } from "@/lib/agents/psd-template-settings";
|
||||||
|
|
||||||
|
const DEFAULT_PARSE_HEARTBEAT_INTERVAL_MS = 25_000;
|
||||||
|
|
||||||
|
function clampProgress(value: number) {
|
||||||
|
return Math.max(0, Math.min(99, Math.round(Number(value || 0))));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateAgentPsdTemplateById(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateAgentPsdTemplateParseProgress(params: {
|
||||||
|
templateId: string;
|
||||||
|
updatedBy: string;
|
||||||
|
progress: number;
|
||||||
|
stage: string;
|
||||||
|
error?: string;
|
||||||
|
restart?: boolean;
|
||||||
|
}) {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
return updateAgentPsdTemplateById({
|
||||||
|
templateId: params.templateId,
|
||||||
|
updatedBy: params.updatedBy,
|
||||||
|
updater: (template) => {
|
||||||
|
if (!params.restart && template.parseStatus !== "processing") {
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...template,
|
||||||
|
parseStatus: "processing",
|
||||||
|
parseProgress: clampProgress(params.progress),
|
||||||
|
parseStage: params.stage,
|
||||||
|
parseError: params.error,
|
||||||
|
parseStartedAt:
|
||||||
|
params.restart || !template.parseStartedAt
|
||||||
|
? now
|
||||||
|
: template.parseStartedAt,
|
||||||
|
parseHeartbeatAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createAgentPsdTemplateParseHeartbeat(params: {
|
||||||
|
templateId: string;
|
||||||
|
updatedBy: string;
|
||||||
|
getSnapshot: () => {
|
||||||
|
progress: number;
|
||||||
|
stage: string;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
intervalMs?: number;
|
||||||
|
}) {
|
||||||
|
let stopped = false;
|
||||||
|
let inFlight = false;
|
||||||
|
let failedBeats = 0;
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (stopped || inFlight) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
inFlight = true;
|
||||||
|
const snapshot = params.getSnapshot();
|
||||||
|
void updateAgentPsdTemplateParseProgress({
|
||||||
|
templateId: params.templateId,
|
||||||
|
updatedBy: params.updatedBy,
|
||||||
|
progress: snapshot.progress,
|
||||||
|
stage: snapshot.stage,
|
||||||
|
error: snapshot.error,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
failedBeats = 0;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
failedBeats += 1;
|
||||||
|
if (failedBeats <= 3) {
|
||||||
|
console.warn("[PSD Template] parse heartbeat update failed:", {
|
||||||
|
templateId: params.templateId,
|
||||||
|
failedBeats,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
inFlight = false;
|
||||||
|
});
|
||||||
|
}, params.intervalMs || DEFAULT_PARSE_HEARTBEAT_INTERVAL_MS);
|
||||||
|
|
||||||
|
return {
|
||||||
|
stop() {
|
||||||
|
stopped = true;
|
||||||
|
clearInterval(interval);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -20,15 +20,42 @@ export const AGENT_PSD_TEMPLATE_ASSET_BACKFILL_TIMEOUT_MS = getEnvPositiveInt(
|
|||||||
"AGENT_PSD_TEMPLATE_ASSET_BACKFILL_TIMEOUT_MS",
|
"AGENT_PSD_TEMPLATE_ASSET_BACKFILL_TIMEOUT_MS",
|
||||||
20 * 60 * 1000
|
20 * 60 * 1000
|
||||||
);
|
);
|
||||||
|
export const AGENT_PSD_TEMPLATE_PARSE_MAX_DURATION_MS = getEnvPositiveInt(
|
||||||
|
"AGENT_PSD_TEMPLATE_PARSE_MAX_DURATION_MS",
|
||||||
|
45 * 60 * 1000
|
||||||
|
);
|
||||||
|
|
||||||
function getTemplateUpdatedAtTime(template: AgentPsdTemplate) {
|
function getIsoTime(value?: string) {
|
||||||
const updatedAt = new Date(template.updatedAt).getTime();
|
const time = new Date(value || "").getTime();
|
||||||
if (Number.isFinite(updatedAt)) {
|
return Number.isFinite(time) ? time : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTemplateHeartbeatTime(template: AgentPsdTemplate) {
|
||||||
|
const heartbeatAt = getIsoTime(template.parseHeartbeatAt);
|
||||||
|
if (heartbeatAt) {
|
||||||
|
return heartbeatAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedAt = getIsoTime(template.updatedAt);
|
||||||
|
if (updatedAt) {
|
||||||
return updatedAt;
|
return updatedAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
const createdAt = new Date(template.createdAt).getTime();
|
return getIsoTime(template.createdAt);
|
||||||
return Number.isFinite(createdAt) ? createdAt : null;
|
}
|
||||||
|
|
||||||
|
function getTemplateParseStartedAtTime(template: AgentPsdTemplate) {
|
||||||
|
const startedAt = getIsoTime(template.parseStartedAt);
|
||||||
|
if (startedAt) {
|
||||||
|
return startedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedAt = getIsoTime(template.updatedAt);
|
||||||
|
if (updatedAt) {
|
||||||
|
return updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getIsoTime(template.createdAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasPassedFastParsePhase(template: AgentPsdTemplate) {
|
function hasPassedFastParsePhase(template: AgentPsdTemplate) {
|
||||||
@ -49,12 +76,24 @@ export function getAgentPsdTemplateParseAgeMs(
|
|||||||
template: AgentPsdTemplate,
|
template: AgentPsdTemplate,
|
||||||
now = Date.now()
|
now = Date.now()
|
||||||
) {
|
) {
|
||||||
const updatedAt = getTemplateUpdatedAtTime(template);
|
const heartbeatAt = getTemplateHeartbeatTime(template);
|
||||||
if (!updatedAt) {
|
if (!heartbeatAt) {
|
||||||
return Number.POSITIVE_INFINITY;
|
return Number.POSITIVE_INFINITY;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Math.max(0, now - updatedAt);
|
return Math.max(0, now - heartbeatAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAgentPsdTemplateParseDurationMs(
|
||||||
|
template: AgentPsdTemplate,
|
||||||
|
now = Date.now()
|
||||||
|
) {
|
||||||
|
const startedAt = getTemplateParseStartedAtTime(template);
|
||||||
|
if (!startedAt) {
|
||||||
|
return Number.POSITIVE_INFINITY;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.max(0, now - startedAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isAgentPsdTemplateParseTimedOut(
|
export function isAgentPsdTemplateParseTimedOut(
|
||||||
@ -67,20 +106,32 @@ export function isAgentPsdTemplateParseTimedOut(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
getAgentPsdTemplateParseAgeMs(template, now) >
|
getAgentPsdTemplateParseAgeMs(template, now) >
|
||||||
getAgentPsdTemplateParseTimeoutMs(template)
|
getAgentPsdTemplateParseTimeoutMs(template) ||
|
||||||
|
getAgentPsdTemplateParseDurationMs(template, now) >
|
||||||
|
AGENT_PSD_TEMPLATE_PARSE_MAX_DURATION_MS
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildTimeoutMessage(template: AgentPsdTemplate) {
|
function buildTimeoutMessage(template: AgentPsdTemplate, now = Date.now()) {
|
||||||
const timeoutMinutes = Math.round(
|
|
||||||
getAgentPsdTemplateParseTimeoutMs(template) / 60_000
|
|
||||||
);
|
|
||||||
const phase = hasPassedFastParsePhase(template)
|
const phase = hasPassedFastParsePhase(template)
|
||||||
? "后台图层资产补全"
|
? "后台图层资产补全"
|
||||||
: "PSD 快速解析";
|
: "PSD 快速解析";
|
||||||
const stage = template.parseStage ? `,最后阶段:${template.parseStage}` : "";
|
const stage = template.parseStage ? `,最后阶段:${template.parseStage}` : "";
|
||||||
|
|
||||||
return `${phase}超过 ${timeoutMinutes} 分钟未更新进度,系统已自动结束本次任务${stage}。可检查 PSD 文件后重新解析。`;
|
if (
|
||||||
|
getAgentPsdTemplateParseDurationMs(template, now) >
|
||||||
|
AGENT_PSD_TEMPLATE_PARSE_MAX_DURATION_MS
|
||||||
|
) {
|
||||||
|
const timeoutMinutes = Math.round(
|
||||||
|
AGENT_PSD_TEMPLATE_PARSE_MAX_DURATION_MS / 60_000
|
||||||
|
);
|
||||||
|
return `${phase}总耗时超过 ${timeoutMinutes} 分钟,系统已自动结束本次任务${stage}。请检查 PSD 是否过大、图层是否异常,或重新上传后再解析。`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeoutMinutes = Math.round(
|
||||||
|
getAgentPsdTemplateParseTimeoutMs(template) / 60_000
|
||||||
|
);
|
||||||
|
return `${phase}超过 ${timeoutMinutes} 分钟没有收到解析心跳,系统已自动结束本次任务${stage}。可检查 PSD 文件后重新解析。`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function failTimedOutAgentPsdTemplatesInMemory(params: {
|
export function failTimedOutAgentPsdTemplatesInMemory(params: {
|
||||||
@ -107,7 +158,8 @@ export function failTimedOutAgentPsdTemplatesInMemory(params: {
|
|||||||
parseStage: hasPassedFastParsePhase(template)
|
parseStage: hasPassedFastParsePhase(template)
|
||||||
? "后台图层资产补全超时"
|
? "后台图层资产补全超时"
|
||||||
: "PSD 快速解析超时",
|
: "PSD 快速解析超时",
|
||||||
parseError: buildTimeoutMessage(template),
|
parseError: buildTimeoutMessage(template, now.getTime()),
|
||||||
|
parseHeartbeatAt: now.toISOString(),
|
||||||
updatedAt: now.toISOString(),
|
updatedAt: now.toISOString(),
|
||||||
};
|
};
|
||||||
timedOutTemplates.push(nextTemplate);
|
timedOutTemplates.push(nextTemplate);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user