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 { analyzeWorkflowMaterialAppearanceFromImage } from "@/lib/agents/workflow-material-appearance"; 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(), appearanceRecognitionEnabled: z.enum(["true", "false", "1", "0"]).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")), appearanceRecognitionEnabled: getOptionalFormString( formData.get("appearanceRecognitionEnabled") ), }); const appearanceRecognitionEnabled = payload.appearanceRecognitionEnabled !== "false" && payload.appearanceRecognitionEnabled !== "0"; 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 appearanceSummary = appearanceRecognitionEnabled ? await analyzeWorkflowMaterialAppearanceFromImage(result.original_url).catch((error) => { console.warn("[Agent Workflow Material Upload] Appearance analysis failed:", error); return null; }) : null; 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", appearanceSummary, 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 } ); } }