import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; import { auth } from "@/app/(auth)/auth"; import { getSystemSetting, upsertMediaAsset } from "@/lib/db/queries"; import { uploadFile } from "@/lib/storage/service"; import { AGENT_WORKFLOW_MATERIAL_API_CONFIG_KEY, normalizeAgentWorkflowMaterialApiConfig, } from "@/lib/agents/workflow-material-config"; export const maxDuration = 180; const querySchema = z.object({ keyword: z.string().trim().min(1).max(64), source: z.enum(["primary", "secondary"]).optional(), queryType: z.enum(["sku", "spu"]).optional(), }); type ExternalImageRecord = { key?: string; label?: string; fileName?: string; fileSize?: number; downloadUrl?: string; }; type ExternalSearchResponse = { success?: boolean; data?: { sku?: string; spu?: string; materialId?: string; productModel?: string; skuCount?: number; skuList?: Array<{ skuName?: string; skuId?: string; hasWhiteBackground?: boolean; whiteBackgroundCount?: number; previewImage?: { downloadUrl?: string; fileName?: string; } | null; }>; whiteBackground?: { fixed?: ExternalImageRecord[]; other?: ExternalImageRecord[]; }; downloadUrls?: { zip?: string; }; }; error?: { code?: string; message?: string; }; }; function normalizeBaseUrl(value: string) { return String(value || "").trim().replace(/\/+$/, ""); } function sanitizeFilenamePart(value: string) { return String(value || "") .trim() .replace(/[\\/:*?"<>|]+/g, "-") .replace(/\s+/g, "-") .slice(0, 80); } function getExtensionFromMimeType(mimeType: string) { const normalized = String(mimeType || "").toLowerCase(); if (normalized.includes("png")) return "png"; if (normalized.includes("webp")) return "webp"; if (normalized.includes("jpeg") || normalized.includes("jpg")) return "jpg"; if (normalized.includes("gif")) return "gif"; return "png"; } function ensureFilenameWithExtension(filename: string, mimeType: string) { const trimmed = sanitizeFilenamePart(filename) || `workflow-material-${Date.now()}`; if (/\.[a-z0-9]+$/i.test(trimmed)) { return trimmed; } return `${trimmed}.${getExtensionFromMimeType(mimeType)}`; } async function importExternalImage(params: { userId: string; keyword: string; source: "primary" | "secondary"; matchedSku: string; productModel?: string | null; group: "fixed" | "other"; sortOrder: number; image: ExternalImageRecord; }) { const downloadUrl = String(params.image.downloadUrl || "").trim(); if (!downloadUrl) return null; const imageResponse = await fetch(downloadUrl, { cache: "no-store", redirect: "follow", }); if (!imageResponse.ok) { throw new Error(`下载图片失败(${imageResponse.status})`); } const imageBuffer = await imageResponse.arrayBuffer(); const mimeType = String(imageResponse.headers.get("content-type") || "").split(";")[0].trim() || "image/png"; const filename = ensureFilenameWithExtension( params.image.fileName || params.image.label || `${params.matchedSku || params.keyword}-${params.group}-${params.sortOrder + 1}`, mimeType ); const uploaded = await uploadFile(filename, imageBuffer); let assetId: string | null = null; try { const asset = await upsertMediaAsset({ userId: params.userId, mediaType: "image", sourceType: "agent_workflow_material_search", status: "ready", originalUrl: uploaded.url, previewUrl: uploaded.url, thumbnailUrl: uploaded.url, mimeType: uploaded.contentType || mimeType, displayName: filename, sourceTable: "agent_workflow_material_search", metadata: { keyword: params.keyword, source: params.source, matchedSku: params.matchedSku, productModel: params.productModel || null, group: params.group, externalKey: String(params.image.key || "").trim() || null, fileSize: Number(params.image.fileSize || 0) || null, pathname: uploaded.pathname, }, }); assetId = asset?.id ?? null; } catch (error) { console.warn("[Agent Workflow Material Search] Failed to register media asset:", error); } return { id: assetId || `${params.group}-${params.sortOrder + 1}`, key: String(params.image.key || "").trim() || `${params.group}-${params.sortOrder + 1}`, label: String(params.image.label || "").trim() || `图片${params.sortOrder + 1}`, fileName: filename, imageUrl: uploaded.url, assetId, mimeType: uploaded.contentType || mimeType, group: params.group, sortOrder: params.sortOrder, }; } function mapUpstreamErrorStatus(code: string) { if ( code === "SKU_NOT_FOUND" || code === "SPU_NOT_FOUND" || code === "NO_SKUS" || code === "WHITE_BACKGROUND_NOT_FOUND" ) { return 404; } if (code === "INVALID_API_KEY") { return 500; } if (code === "SKU_NOT_UNIQUE") { return 409; } return 502; } export async function GET(request: NextRequest) { try { const session = await auth(); if (!session?.user?.id) { return NextResponse.json({ error: "未登录" }, { status: 401 }); } const parsed = querySchema.parse({ keyword: request.nextUrl.searchParams.get("keyword"), source: request.nextUrl.searchParams.get("source"), queryType: request.nextUrl.searchParams.get("queryType"), }); const setting = await getSystemSetting({ key: AGENT_WORKFLOW_MATERIAL_API_CONFIG_KEY }); const config = normalizeAgentWorkflowMaterialApiConfig(setting?.value); if (!config.enabled) { return NextResponse.json( { error: "素材搜索功能未开启,请先在系统设置中启用" }, { status: 400 } ); } if (!config.apiKey) { return NextResponse.json( { error: "素材搜索 API Key 未配置,请先在系统设置中保存" }, { status: 500 } ); } const normalizedKeyword = parsed.keyword.trim().toUpperCase(); const queryType = parsed.queryType === "spu" ? "spu" : "sku"; const upstreamUrl = `${normalizeBaseUrl( config.baseUrl )}/standard-model-white-background?resourceType=white-background&${queryType}=${encodeURIComponent(normalizedKeyword)}`; const upstreamResponse = await fetch(upstreamUrl, { method: "GET", headers: { "X-API-Key": config.apiKey, }, cache: "no-store", }); const upstreamResult = (await upstreamResponse.json().catch(() => ({}))) as ExternalSearchResponse; if (!upstreamResponse.ok || upstreamResult.success !== true || !upstreamResult.data) { const errorCode = String(upstreamResult.error?.code || "UPSTREAM_ERROR").trim(); const errorMessage = String(upstreamResult.error?.message || "").trim() || `素材搜索失败(${upstreamResponse.status})`; return NextResponse.json( { success: false, error: { code: errorCode, message: errorMessage, }, }, { status: mapUpstreamErrorStatus(errorCode) } ); } if (queryType === "spu") { const skuOptions = Array.isArray(upstreamResult.data.skuList) ? upstreamResult.data.skuList .map((item) => { const skuName = String(item?.skuName || "").trim(); if (!skuName) return null; return { skuName, skuId: String(item?.skuId || "").trim() || null, hasWhiteBackground: item?.hasWhiteBackground === true, whiteBackgroundCount: Number.isFinite(Number(item?.whiteBackgroundCount)) ? Math.max(0, Math.min(999, Math.round(Number(item?.whiteBackgroundCount)))) : 0, previewImageUrl: String(item?.previewImage?.downloadUrl || "").trim() || null, previewFileName: String(item?.previewImage?.fileName || "").trim() || null, }; }) .filter(Boolean) : []; return NextResponse.json({ success: true, data: { query: normalizedKeyword, queryType: "spu", matchedSku: "", matchedSpu: String(upstreamResult.data.spu || normalizedKeyword).trim() || normalizedKeyword, materialId: String(upstreamResult.data.materialId || "").trim() || null, productModel: String(upstreamResult.data.spu || normalizedKeyword).trim() || null, zipUrl: null, fixedCount: 0, otherCount: 0, totalCount: skuOptions.length, images: [], skuOptions, }, }); } const matchedSku = String(upstreamResult.data.sku || normalizedKeyword).trim() || normalizedKeyword; const productModel = String(upstreamResult.data.productModel || "").trim() || null; const fixedImages = Array.isArray(upstreamResult.data.whiteBackground?.fixed) ? upstreamResult.data.whiteBackground?.fixed || [] : []; const otherImages = Array.isArray(upstreamResult.data.whiteBackground?.other) ? upstreamResult.data.whiteBackground?.other || [] : []; const importedImages = ( await Promise.all([ ...fixedImages.map((image, index) => importExternalImage({ userId: session.user.id, keyword: normalizedKeyword, source: parsed.source || "primary", matchedSku, productModel, group: "fixed", sortOrder: index, image, }).catch((error) => { console.warn("[Agent Workflow Material Search] Fixed image import failed:", error); return null; }) ), ...otherImages.map((image, index) => importExternalImage({ userId: session.user.id, keyword: normalizedKeyword, source: parsed.source || "primary", matchedSku, productModel, group: "other", sortOrder: fixedImages.length + index, image, }).catch((error) => { console.warn("[Agent Workflow Material Search] Other image import failed:", error); return null; }) ), ]) ).filter( ( item ): item is { id: string; key: string; label: string; fileName: string; imageUrl: string; assetId: string | null; mimeType: string; group: "fixed" | "other"; sortOrder: number; } => Boolean(item) ); if (importedImages.length === 0) { return NextResponse.json( { success: false, error: { code: "IMAGE_IMPORT_FAILED", message: "已获取到素材记录,但图片导入失败,请稍后重试", }, }, { status: 502 } ); } return NextResponse.json({ success: true, data: { query: normalizedKeyword, queryType: "sku", matchedSku, matchedSpu: productModel, materialId: String(upstreamResult.data.materialId || "").trim() || null, productModel, zipUrl: String(upstreamResult.data.downloadUrls?.zip || "").trim() || null, fixedCount: importedImages.filter((item) => item.group === "fixed").length, otherCount: importedImages.filter((item) => item.group === "other").length, totalCount: importedImages.length, images: importedImages, skuOptions: [], }, }); } catch (error) { console.error("[Agent Workflow Material Search] Error:", error); if (error instanceof z.ZodError) { return NextResponse.json( { error: "搜索参数无效", details: error.errors }, { status: 400 } ); } return NextResponse.json( { error: "素材搜索失败,请稍后重试" }, { status: 500 } ); } }