177 lines
5.7 KiB
TypeScript
177 lines
5.7 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
import { NextRequest, NextResponse } from "next/server";
|
|
import { z } from "zod";
|
|
import { auth } from "@/app/(auth)/auth";
|
|
import { executeRmbgJob, RmbgServiceError } from "@/lib/ai/rmbg/service";
|
|
import { upsertMediaAsset } from "@/lib/db/queries";
|
|
|
|
export const runtime = "nodejs";
|
|
export const maxDuration = 180;
|
|
|
|
const payloadSchema = z.object({
|
|
source: z.enum(["primary", "secondary"]).default("primary"),
|
|
query: z.string().trim().max(64).optional(),
|
|
queryType: z.enum(["sku", "spu"]).optional(),
|
|
matchedSku: z.string().trim().max(64).optional(),
|
|
matchedSpu: z.string().trim().max(64).optional(),
|
|
productModel: z.string().trim().max(128).optional(),
|
|
});
|
|
|
|
function getOptionalFormString(value: FormDataEntryValue | null) {
|
|
if (typeof value !== "string") {
|
|
return undefined;
|
|
}
|
|
|
|
const normalizedValue = value.trim();
|
|
return normalizedValue.length > 0 ? normalizedValue : undefined;
|
|
}
|
|
|
|
function isUploadLikeFile(value: FormDataEntryValue | null) {
|
|
return (
|
|
Boolean(value) &&
|
|
typeof value !== "string" &&
|
|
typeof (value as Blob).arrayBuffer === "function"
|
|
);
|
|
}
|
|
|
|
function sanitizeFilenameStem(value: string) {
|
|
return String(value || "")
|
|
.trim()
|
|
.replace(/\.[^.]+$/, "")
|
|
.replace(/[\\/:*?"<>|]+/g, "-")
|
|
.replace(/\s+/g, "-")
|
|
.replace(/-+/g, "-")
|
|
.replace(/^-|-$/g, "")
|
|
.slice(0, 80);
|
|
}
|
|
|
|
function normalizeUploadedImageMimeType(file: Blob & { name?: string; type?: string }) {
|
|
const type = String(file.type || "").trim().toLowerCase();
|
|
if (type.startsWith("image/")) {
|
|
return type;
|
|
}
|
|
|
|
const fileName = String(file.name || "").trim().toLowerCase();
|
|
if (/\.(png)$/i.test(fileName)) return "image/png";
|
|
if (/\.(jpe?g)$/i.test(fileName)) return "image/jpeg";
|
|
if (/\.(webp)$/i.test(fileName)) return "image/webp";
|
|
if (/\.(gif)$/i.test(fileName)) return "image/gif";
|
|
if (/\.(bmp)$/i.test(fileName)) return "image/bmp";
|
|
if (/\.(heic)$/i.test(fileName)) return "image/heic";
|
|
if (/\.(heif)$/i.test(fileName)) return "image/heif";
|
|
return "";
|
|
}
|
|
|
|
export async function POST(request: NextRequest) {
|
|
try {
|
|
const session = await auth();
|
|
if (!session?.user?.id) {
|
|
return NextResponse.json({ error: "未登录" }, { status: 401 });
|
|
}
|
|
|
|
const formData = await request.formData();
|
|
const fileEntry = formData.get("file");
|
|
if (!isUploadLikeFile(fileEntry)) {
|
|
return NextResponse.json({ error: "缺少图片文件" }, { status: 400 });
|
|
}
|
|
const file = fileEntry as Blob & { name?: string; type?: string };
|
|
|
|
const mimeType = normalizeUploadedImageMimeType(file);
|
|
if (!mimeType) {
|
|
return NextResponse.json({ error: "请上传图片文件" }, { status: 400 });
|
|
}
|
|
|
|
const payload = payloadSchema.parse({
|
|
source: getOptionalFormString(formData.get("source")),
|
|
query: getOptionalFormString(formData.get("query")),
|
|
queryType: getOptionalFormString(formData.get("queryType")),
|
|
matchedSku: getOptionalFormString(formData.get("matchedSku")),
|
|
matchedSpu: getOptionalFormString(formData.get("matchedSpu")),
|
|
productModel: getOptionalFormString(formData.get("productModel")),
|
|
});
|
|
|
|
const arrayBuffer = await file.arrayBuffer();
|
|
const imageBuffer = Buffer.from(arrayBuffer);
|
|
if (imageBuffer.byteLength <= 0) {
|
|
return NextResponse.json({ error: "图片文件为空,请重新上传" }, { status: 400 });
|
|
}
|
|
|
|
const promptPrefix = payload.source === "secondary" ? "搭配SKU" : "主SPU";
|
|
const prompt = `${promptPrefix}素材抠图`;
|
|
|
|
const result = await executeRmbgJob({
|
|
userId: session.user.id,
|
|
imageBuffer,
|
|
prompt,
|
|
skipGallerySave: true,
|
|
});
|
|
|
|
const originalFileName = String(file.name || "").trim() || `workflow-upload-${Date.now()}.png`;
|
|
const originalStem = sanitizeFilenameStem(originalFileName) || `workflow-upload-${Date.now()}`;
|
|
const displayName = `${originalStem}-rmbg.png`;
|
|
const asset = await upsertMediaAsset({
|
|
userId: session.user.id,
|
|
mediaType: "image",
|
|
sourceType: "agent_workflow_material_upload",
|
|
status: "ready",
|
|
originalUrl: result.original_url,
|
|
previewUrl: result.original_url,
|
|
thumbnailUrl: result.thumbnail_url,
|
|
mimeType: "image/png",
|
|
displayName,
|
|
prompt,
|
|
modelId: result.provider,
|
|
sourceTable: "agent_workflow_material_upload",
|
|
metadata: {
|
|
source: payload.source,
|
|
query: payload.query || null,
|
|
queryType: payload.queryType || "sku",
|
|
matchedSku: payload.matchedSku || null,
|
|
matchedSpu: payload.matchedSpu || null,
|
|
productModel: payload.productModel || null,
|
|
originalFileName,
|
|
originalMimeType: mimeType,
|
|
provider: result.provider,
|
|
},
|
|
});
|
|
|
|
const imageId = asset?.id || `upload-${randomUUID()}`;
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
data: {
|
|
id: imageId,
|
|
key: imageId,
|
|
label: "用户上传",
|
|
fileName: displayName,
|
|
imageUrl: result.original_url,
|
|
assetId: asset?.id ?? null,
|
|
mimeType: "image/png",
|
|
sourceType: "upload",
|
|
group: "other",
|
|
sortOrder: 0,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
return NextResponse.json(
|
|
{ error: error.issues[0]?.message || "请求参数不合法" },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
if (error instanceof RmbgServiceError) {
|
|
return NextResponse.json(
|
|
{ error: error.message, detail: error.detail || error.message },
|
|
{ status: error.status }
|
|
);
|
|
}
|
|
|
|
console.error("[Agent Workflow Material Upload] Failed to process upload:", error);
|
|
return NextResponse.json(
|
|
{ error: error instanceof Error ? error.message : "上传失败" },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|