fix: upload scene fusion inputs as urls
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-05-09 17:22:12 +08:00
parent 3a4aece308
commit 510e3dc323

View File

@ -40,6 +40,7 @@ import {
SelectTrigger,
} from "@/components/ui/select";
import { createCanvasPsdTemplateLaunchToken, saveCanvasPsdTemplateLaunchPayload } from "@/lib/canvas/psd-template-launch";
import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry";
import { requestLongTaskResult } from "@/lib/client/long-task";
import {
SCENE_FUSION_ASPECT_RATIO_OPTIONS,
@ -171,6 +172,23 @@ function createPersistableSnapshot(snapshot: SceneFusionPersistedState): SceneFu
};
}
function getUploadedImageUrl(payload: unknown) {
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
return "";
}
const raw = payload as Record<string, unknown>;
return String(
raw.url ||
raw.originalUrl ||
raw.original_url ||
raw.previewUrl ||
raw.preview_url ||
raw.thumbnailUrl ||
raw.thumbnail_url ||
""
).trim();
}
const randomBackgroundStyles = [
"现代简约客厅",
"北欧卧室",
@ -447,6 +465,52 @@ export function SceneFusionView({
updateProductPreview(null);
};
const uploadImageFile = async (file: File) => {
const formData = new FormData();
formData.append("file", file, file.name || `scene-fusion-${Date.now()}.png`);
const response = await fetchUploadWithRetry(formData);
const payload = await response.json().catch(() => ({}));
const imageUrl = getUploadedImageUrl(payload);
if (!response.ok || !imageUrl) {
throw new Error(
String(
payload?.error ||
payload?.details ||
payload?.message ||
`${file.name || "图片"}上传失败`
)
);
}
return imageUrl;
};
const validateImageFiles = (files: File[]) => {
const validFiles: File[] = [];
for (const file of files) {
if (!file.type.startsWith("image/")) {
toast.error(`${file.name || "文件"}不是图片文件`);
continue;
}
if (file.size > 50 * 1024 * 1024) {
toast.error(`图片 ${file.name} 大小不能超过 50MB`);
continue;
}
validFiles.push(file);
}
return validFiles;
};
const uploadImageFiles = async (files: File[]) => {
const uploadedItems = await Promise.all(
files.map(async (file, index) => ({
file,
index,
imageUrl: await uploadImageFile(file),
}))
);
return uploadedItems.sort((left, right) => left.index - right.index);
};
useEffect(() => {
if (initialProductImage) {
setProductImage(initialProductImage);
@ -479,21 +543,25 @@ export function SceneFusionView({
if (typeof parsed.imageSize === "string") {
setImageSize(parsed.imageSize);
}
if (typeof parsed.productImage === "string" && parsed.productImage.trim()) {
setProductImage(parsed.productImage);
updateProductPreview(parsed.productImage);
const restoredProductImage = toPersistableImageValue(parsed.productImage);
if (restoredProductImage) {
setProductImage(restoredProductImage);
updateProductPreview(restoredProductImage);
}
if (typeof parsed.productName === "string") {
setProductName(parsed.productName);
}
if (Array.isArray(parsed.sceneItems)) {
setSceneItems(parsed.sceneItems);
setSceneItems(toPersistableItems(parsed.sceneItems));
}
if (Array.isArray(parsed.multiProductItems)) {
setMultiProductItems(parsed.multiProductItems);
setMultiProductItems(toPersistableItems(parsed.multiProductItems));
}
if (typeof parsed.referenceSceneImage === "string") {
setReferenceSceneImage(parsed.referenceSceneImage || null);
const restoredReferenceSceneImage = toPersistableImageValue(
parsed.referenceSceneImage
);
if (restoredReferenceSceneImage) {
setReferenceSceneImage(restoredReferenceSceneImage);
}
if (typeof parsed.referenceTargetName === "string") {
setReferenceTargetName(parsed.referenceTargetName);
@ -502,7 +570,7 @@ export function SceneFusionView({
setReferenceCustomPrompt(parsed.referenceCustomPrompt);
}
if (Array.isArray(parsed.randomItems)) {
setRandomItems(parsed.randomItems);
setRandomItems(toPersistableItems(parsed.randomItems));
}
if (typeof parsed.randomCount === "number" && Number.isFinite(parsed.randomCount)) {
setRandomCount(Math.max(1, Math.min(MAX_BATCH_ITEMS, parsed.randomCount)));
@ -511,7 +579,7 @@ export function SceneFusionView({
setRandomPrompt(parsed.randomPrompt);
}
if (Array.isArray(parsed.batchItems)) {
setBatchItems(parsed.batchItems);
setBatchItems(toPersistableItems(parsed.batchItems));
}
if (Array.isArray(parsed.selectedResultIds)) {
setSelectedResultIds(parsed.selectedResultIds.map((value) => String(value || "").trim()).filter(Boolean));
@ -638,144 +706,149 @@ export function SceneFusionView({
selectedResultIds,
]);
const handleProductUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
const handleProductUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const input = e.currentTarget;
const file = input.files?.[0];
if (!file) return;
if (file.size > 50 * 1024 * 1024) {
toast.error("图片大小不能超过 50MB");
const [validFile] = validateImageFiles([file]);
if (!validFile) {
input.value = "";
return;
}
setProductImage(null);
const previewUrl = URL.createObjectURL(file);
const previewUrl = URL.createObjectURL(validFile);
updateProductPreview(previewUrl, true);
const reader = new FileReader();
reader.onload = (event) => {
const result = event.target?.result;
if (typeof result === "string") {
setProductImage(result);
} else {
toast.error("图片读取失败,请重试");
}
};
reader.onerror = () => toast.error("图片读取失败,请重试");
reader.readAsDataURL(file);
e.target.value = "";
try {
const imageUrl = await uploadImageFile(validFile);
setProductImage(imageUrl);
updateProductPreview(imageUrl);
toast.success("产品图上传成功");
} catch (error) {
updateProductPreview(null);
toast.error(error instanceof Error ? error.message : "产品图上传失败");
} finally {
input.value = "";
}
};
const handleSceneUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(e.target.files || []);
if (files.length === 0) return;
const handleSceneUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const input = e.currentTarget;
const files = validateImageFiles(Array.from(input.files || []));
if (files.length === 0) {
input.value = "";
return;
}
if (sceneItems.length + files.length > maxScenes) {
toast.error(`单次最多上传 ${maxScenes} 张参考图`);
input.value = "";
return;
}
files.forEach((file) => {
if (file.size > 50 * 1024 * 1024) {
toast.error(`图片 ${file.name} 大小不能超过 50MB`);
return;
}
const reader = new FileReader();
reader.onload = (event) => {
const image = event.target?.result as string;
setSceneItems((prev) => [
...prev,
{
id: Math.random().toString(36).slice(2, 10),
image,
targetName: "",
customPrompt: "",
status: "pending",
},
]);
};
reader.readAsDataURL(file);
});
e.target.value = "";
try {
const uploadedItems = await uploadImageFiles(files);
setSceneItems((prev) => [
...prev,
...uploadedItems.map((item) => ({
id: Math.random().toString(36).slice(2, 10),
image: item.imageUrl,
targetName: "",
customPrompt: "",
status: "pending" as ItemStatus,
})),
]);
toast.success(`已上传 ${uploadedItems.length} 张参考图`);
} catch (error) {
toast.error(error instanceof Error ? error.message : "参考图上传失败");
} finally {
input.value = "";
}
};
const handleMultiProductUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(e.target.files || []);
if (files.length === 0) return;
const handleMultiProductUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const input = e.currentTarget;
const files = validateImageFiles(Array.from(input.files || []));
if (files.length === 0) {
input.value = "";
return;
}
if (multiProductItems.length + files.length > MAX_BATCH_ITEMS) {
toast.error(`单次最多上传 ${MAX_BATCH_ITEMS} 张产品图`);
input.value = "";
return;
}
files.forEach((file) => {
if (file.size > 50 * 1024 * 1024) {
toast.error(`图片 ${file.name} 大小不能超过 50MB`);
return;
}
const reader = new FileReader();
reader.onload = (event) => {
const image = event.target?.result as string;
setMultiProductItems((prev) => [
...prev,
{
id: Math.random().toString(36).slice(2, 10),
image,
status: "pending",
},
]);
};
reader.readAsDataURL(file);
});
e.target.value = "";
try {
const uploadedItems = await uploadImageFiles(files);
setMultiProductItems((prev) => [
...prev,
...uploadedItems.map((item) => ({
id: Math.random().toString(36).slice(2, 10),
image: item.imageUrl,
status: "pending" as ItemStatus,
})),
]);
toast.success(`已上传 ${uploadedItems.length} 张产品图`);
} catch (error) {
toast.error(error instanceof Error ? error.message : "产品图上传失败");
} finally {
input.value = "";
}
};
const handleReferenceSceneUpload = (
const handleReferenceSceneUpload = async (
e: React.ChangeEvent<HTMLInputElement>
) => {
const file = e.target.files?.[0];
const input = e.currentTarget;
const file = input.files?.[0];
if (!file) return;
if (file.size > 50 * 1024 * 1024) {
toast.error("图片大小不能超过 50MB");
const [validFile] = validateImageFiles([file]);
if (!validFile) {
input.value = "";
return;
}
const reader = new FileReader();
reader.onload = (event) => {
const image = event.target?.result as string;
setReferenceSceneImage(image);
};
reader.readAsDataURL(file);
e.target.value = "";
try {
const imageUrl = await uploadImageFile(validFile);
setReferenceSceneImage(imageUrl);
toast.success("参考图上传成功");
} catch (error) {
toast.error(error instanceof Error ? error.message : "参考图上传失败");
} finally {
input.value = "";
}
};
const handleBatchUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(e.target.files || []);
if (files.length === 0) return;
const handleBatchUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const input = e.currentTarget;
const files = validateImageFiles(Array.from(input.files || []));
if (files.length === 0) {
input.value = "";
return;
}
if (batchItems.length + files.length > MAX_BATCH_ITEMS) {
toast.error(`单次最多上传 ${MAX_BATCH_ITEMS} 张图片`);
input.value = "";
return;
}
files.forEach((file) => {
if (file.size > 50 * 1024 * 1024) {
toast.error(`图片 ${file.name} 大小不能超过 50MB`);
return;
}
const reader = new FileReader();
reader.onload = (event) => {
const image = event.target?.result as string;
setBatchItems((prev) => [
...prev,
{
id: Math.random().toString(36).slice(2, 10),
image,
status: "pending",
},
]);
};
reader.readAsDataURL(file);
});
e.target.value = "";
try {
const uploadedItems = await uploadImageFiles(files);
setBatchItems((prev) => [
...prev,
...uploadedItems.map((item) => ({
id: Math.random().toString(36).slice(2, 10),
image: item.imageUrl,
status: "pending" as ItemStatus,
})),
]);
toast.success(`已上传 ${uploadedItems.length} 张图片`);
} catch (error) {
toast.error(error instanceof Error ? error.message : "图片上传失败");
} finally {
input.value = "";
}
};
const updateSceneItem = (id: string, patch: Partial<ProcessItem>) => {
@ -1468,6 +1541,52 @@ export function SceneFusionView({
}
};
const retryBatchImageItem = async (item: ProcessItem) => {
if (item.image.startsWith("data:")) {
toast.error("这张图仍是旧版本地数据,请重新上传后再处理");
return;
}
setIsProcessing(true);
setBatchItems((prev) =>
prev.map((batchItem) =>
batchItem.id === item.id
? { ...batchItem, status: "processing", error: undefined }
: batchItem
)
);
try {
const result = await processBatchImageItem(item);
setBatchItems((prev) =>
prev.map((batchItem) =>
batchItem.id === item.id
? {
...batchItem,
status: "completed",
result: result.result,
originalUrl: result.originalUrl,
error: undefined,
}
: batchItem
)
);
toast.success("已重新处理 1 张图片");
} catch (error) {
const message = error instanceof Error ? error.message : "处理失败";
setBatchItems((prev) =>
prev.map((batchItem) =>
batchItem.id === item.id
? { ...batchItem, status: "failed", error: message }
: batchItem
)
);
toast.error(message);
} finally {
setIsProcessing(false);
}
};
const startProcessing = async () => {
if (mode === "scene-fission") {
if (!productImage) {
@ -1570,14 +1689,7 @@ export function SceneFusionView({
return;
}
setBatchItems((prev) =>
prev.map((batchItem) =>
batchItem.id === item.id
? { ...batchItem, status: "pending", error: undefined }
: batchItem
)
);
await runBatchImageMode();
await retryBatchImageItem(item);
};
const downloadImage = async (url: string, filename: string) => {