fix: stabilize PSD render asset downloads
This commit is contained in:
parent
649825072b
commit
c9c78f20d4
@ -84,6 +84,7 @@ import {
|
||||
} from "@/lib/agents/psd-text-layout";
|
||||
import { requestLongTaskResult } from "@/lib/client/long-task";
|
||||
import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry";
|
||||
import { buildImageProxyUrl } from "@/lib/image-delivery";
|
||||
import { getFriendlyRateLimitMessage, getUserFacingRequestErrorMessage } from "@/lib/request-error";
|
||||
|
||||
function buildTemplateListRequestUrl(params?: {
|
||||
@ -242,6 +243,7 @@ const PREVIEW_STAGE_TARGET_HEIGHT = 760;
|
||||
const MIN_IMAGE_SCALE = 0.4;
|
||||
const MAX_IMAGE_SCALE = 3;
|
||||
const UNASSIGNED_BRAND_LABEL = "未分类";
|
||||
const IMAGE_EXTENSION_REGEX = /\.([a-z0-9]{2,5})$/i;
|
||||
|
||||
function buildTemplatePreviewThumbUrl(
|
||||
previewUrl: string,
|
||||
@ -265,6 +267,76 @@ function buildTemplatePreviewThumbUrl(
|
||||
return `/api/image-proxy?${searchParams.toString()}`;
|
||||
}
|
||||
|
||||
function isLikelyStableRenderImageUrl(url: string) {
|
||||
const normalized = String(url || "").trim();
|
||||
if (!normalized) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
normalized.startsWith("data:") ||
|
||||
normalized.startsWith("blob:") ||
|
||||
normalized.startsWith("/api/image-proxy")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof window === "undefined") {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = new URL(normalized, window.location.origin);
|
||||
if (parsed.origin === window.location.origin) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const hostname = parsed.hostname.toLowerCase();
|
||||
return (
|
||||
hostname.includes("aliyuncs.com") ||
|
||||
hostname.includes("oss-cn-") ||
|
||||
hostname.includes("cloudfront.net") ||
|
||||
hostname.includes("r2.dev")
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function inferImageExtension(url: string, contentType: string) {
|
||||
const normalizedContentType = String(contentType || "").toLowerCase();
|
||||
if (normalizedContentType.includes("png")) {
|
||||
return "png";
|
||||
}
|
||||
if (normalizedContentType.includes("webp")) {
|
||||
return "webp";
|
||||
}
|
||||
if (
|
||||
normalizedContentType.includes("jpeg") ||
|
||||
normalizedContentType.includes("jpg")
|
||||
) {
|
||||
return "jpg";
|
||||
}
|
||||
if (normalizedContentType.includes("gif")) {
|
||||
return "gif";
|
||||
}
|
||||
if (normalizedContentType.includes("avif")) {
|
||||
return "avif";
|
||||
}
|
||||
|
||||
try {
|
||||
const pathname = new URL(url, "https://placeholder.invalid").pathname;
|
||||
const matched = pathname.match(IMAGE_EXTENSION_REGEX);
|
||||
if (matched?.[1]) {
|
||||
return matched[1].toLowerCase();
|
||||
}
|
||||
} catch {
|
||||
// Ignore malformed URLs and fall back to png.
|
||||
}
|
||||
|
||||
return "png";
|
||||
}
|
||||
|
||||
function normalizeManagedBrandName(value?: string | null) {
|
||||
const normalized = String(value || "").trim();
|
||||
if (!normalized || normalized === "未分配品牌") {
|
||||
@ -1256,6 +1328,7 @@ export function AgentPsdTemplateEditorDialog({
|
||||
const imagePlacementsRef = useRef(imagePlacements);
|
||||
const textAssignmentsRef = useRef(textAssignments);
|
||||
const textColorAssignmentsRef = useRef(textColorAssignments);
|
||||
const archivedRenderImageUrlCacheRef = useRef<Record<string, string>>({});
|
||||
const selectedLayerIdRef = useRef(selectedLayerId);
|
||||
|
||||
imageAssignmentsRef.current = imageAssignments;
|
||||
@ -1264,6 +1337,12 @@ export function AgentPsdTemplateEditorDialog({
|
||||
textColorAssignmentsRef.current = textColorAssignments;
|
||||
selectedLayerIdRef.current = selectedLayerId;
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
archivedRenderImageUrlCacheRef.current = {};
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const selectedTemplateSummary = useMemo(
|
||||
() =>
|
||||
templates.find((template) => template.id === selectedTemplateId) || null,
|
||||
@ -2179,6 +2258,101 @@ export function AgentPsdTemplateEditorDialog({
|
||||
}));
|
||||
}
|
||||
|
||||
async function archiveRenderImageUrl(
|
||||
rawUrl: string,
|
||||
label: string
|
||||
): Promise<string> {
|
||||
const normalizedUrl = String(rawUrl || "").trim();
|
||||
if (!normalizedUrl || isLikelyStableRenderImageUrl(normalizedUrl)) {
|
||||
return normalizedUrl;
|
||||
}
|
||||
|
||||
const cachedUrl = archivedRenderImageUrlCacheRef.current[normalizedUrl];
|
||||
if (cachedUrl) {
|
||||
return cachedUrl;
|
||||
}
|
||||
|
||||
const fetchCandidates = Array.from(
|
||||
new Set([normalizedUrl, buildImageProxyUrl(normalizedUrl)].filter(Boolean))
|
||||
);
|
||||
|
||||
let lastError: unknown = null;
|
||||
for (const candidateUrl of fetchCandidates) {
|
||||
try {
|
||||
const response = await fetch(candidateUrl, {
|
||||
method: "GET",
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`${label}读取失败(${response.status})`);
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
if (!blob.size) {
|
||||
throw new Error(`${label}读取结果为空`);
|
||||
}
|
||||
|
||||
const extension = inferImageExtension(
|
||||
normalizedUrl,
|
||||
blob.type || response.headers.get("content-type") || ""
|
||||
);
|
||||
const formData = new FormData();
|
||||
formData.set(
|
||||
"file",
|
||||
new File([blob], `psd-render-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.${extension}`, {
|
||||
type: blob.type || "image/png",
|
||||
})
|
||||
);
|
||||
|
||||
const uploadResponse = await fetchUploadWithRetry(formData);
|
||||
const uploadPayload = await parseActionJsonResponse<{ url?: string }>(
|
||||
uploadResponse,
|
||||
`${label}转存失败`,
|
||||
getFriendlyRateLimitMessage("当前上传过于频繁,请稍后再试")
|
||||
);
|
||||
const uploadedUrl = String(uploadPayload?.url || "").trim();
|
||||
if (!uploadedUrl) {
|
||||
throw new Error(`${label}转存失败`);
|
||||
}
|
||||
|
||||
archivedRenderImageUrlCacheRef.current[normalizedUrl] = uploadedUrl;
|
||||
return uploadedUrl;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
getUserFacingRequestErrorMessage(
|
||||
lastError,
|
||||
`${label}无法转存到本站存储,请重新上传图片后再试`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async function normalizeRenderImageInputs() {
|
||||
const nextSourceImageUrl = await archiveRenderImageUrl(
|
||||
sourceImageUrl || "",
|
||||
"PSD 套版主图"
|
||||
);
|
||||
const nextImageAssignments = { ...effectiveImageAssignments };
|
||||
const layerNameById = new Map(
|
||||
editableImageLayers.map((layer) => [layer.id, layer.layerName])
|
||||
);
|
||||
|
||||
for (const [layerId, layerUrl] of Object.entries(nextImageAssignments)) {
|
||||
nextImageAssignments[layerId] = await archiveRenderImageUrl(
|
||||
layerUrl,
|
||||
`PSD 图层素材「${layerNameById.get(layerId) || layerId}」`
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
sourceImageUrl: nextSourceImageUrl,
|
||||
imageAssignments: nextImageAssignments,
|
||||
};
|
||||
}
|
||||
|
||||
function handleImageDragStart(
|
||||
layer: AgentPsdTemplateImageLayerSlot,
|
||||
event: ReactPointerEvent<HTMLDivElement>
|
||||
@ -2251,15 +2425,31 @@ export function AgentPsdTemplateEditorDialog({
|
||||
try {
|
||||
setIsRendering(true);
|
||||
setRenderedResultUrl("");
|
||||
const normalizedRenderInputs = await normalizeRenderImageInputs();
|
||||
if (normalizedRenderInputs.sourceImageUrl !== (sourceImageUrl || "")) {
|
||||
const defaultSourceLayer = editableImageLayers.find(
|
||||
(layer) => layer.isDefaultResultSource
|
||||
);
|
||||
if (defaultSourceLayer) {
|
||||
setImageAssignments((current) => ({
|
||||
...current,
|
||||
[defaultSourceLayer.id]: normalizedRenderInputs.sourceImageUrl,
|
||||
}));
|
||||
}
|
||||
}
|
||||
setImageAssignments((current) => ({
|
||||
...current,
|
||||
...normalizedRenderInputs.imageAssignments,
|
||||
}));
|
||||
const payload = (await requestLongTaskResult({
|
||||
endpoint: "/api/psd-templates/render",
|
||||
timeoutMs: 8 * 60 * 1000,
|
||||
maxPollFailures: 8,
|
||||
payload: {
|
||||
templateId: selectedTemplate.id,
|
||||
sourceImageUrl: sourceImageUrl || "",
|
||||
sourceImageUrl: normalizedRenderInputs.sourceImageUrl,
|
||||
hiddenLayerIds,
|
||||
imageAssignments: effectiveImageAssignments,
|
||||
imageAssignments: normalizedRenderInputs.imageAssignments,
|
||||
imagePlacements: effectiveImagePlacements,
|
||||
textAssignments: effectiveTextAssignments,
|
||||
textColorAssignments: effectiveTextColorAssignments,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user