import { generateText } from "ai"; import { execFileSync } from "node:child_process"; import { createCipheriv, createHash } from "node:crypto"; import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; import { auth } from "@/app/(auth)/auth"; import { getLanguageModelById } from "@/lib/ai/dynamic-provider"; import { getEnabledAiModelsWithProvider, 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"; import { analyzeWorkflowMaterialAppearanceFromImage } from "@/lib/agents/workflow-material-appearance"; import { AGENT_WORKFLOW_GOODS_INFO_API_CONFIG_KEY, normalizeAgentWorkflowGoodsInfoApiConfig, } from "@/lib/agents/workflow-goods-info-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(), requiredCount: z.coerce.number().int().min(1).max(4).optional(), appearanceRecognitionEnabled: z.enum(["true", "false", "1", "0"]).optional(), }); const QWEN_VL_MODEL_MATCHER = /(qwen|千问|通义).*(vl|vision)|qwen3-vl|qwen-vl/i; const FUNCTIONAL_IMAGE_HINT_REGEX = /开门|开柜|柜门|抽屉|打开|开合|功能|展示|收纳|储物|细节/i; 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; }; }; type ExternalGoodsInfoResponse = { success?: boolean; errMsg?: string | null; data?: Record | null; }; function normalizeBaseUrl(value: string) { return String(value || "").trim().replace(/\/+$/, ""); } function sanitizeGoodsInfoText(value: unknown, maxLength = 255) { return String(value || "").trim().slice(0, maxLength); } function createGoodsInfoSignature(timespan: string, secret: string) { const secretBuffer = Buffer.from(String(secret || "").trim(), "utf8"); if (secretBuffer.length !== 8) { throw new Error("商品信息接口 Secret 长度必须为 8 位"); } const plaintext = String(timespan || "").trim(); let encryptedHex = ""; try { const cipher = createCipheriv("des-cbc", secretBuffer, secretBuffer); cipher.setAutoPadding(true); const encrypted = Buffer.concat([ cipher.update(plaintext, "utf8"), cipher.final(), ]); encryptedHex = encrypted.toString("hex").toUpperCase(); } catch (error) { const opensslOutput = execFileSync( "openssl", [ "enc", "-des-cbc", "-provider", "legacy", "-K", secretBuffer.toString("hex"), "-iv", secretBuffer.toString("hex"), "-nosalt", ], { input: Buffer.from(plaintext, "utf8"), } ); encryptedHex = Buffer.from(opensslOutput).toString("hex").toUpperCase(); console.warn( "[Agent Workflow Material Search] Fallback to openssl legacy DES for goods info signature:", error instanceof Error ? error.message : error ); } return createHash("md5").update(encryptedHex, "utf8").digest("hex").toUpperCase(); } function buildGoodsInfoSummary(data: Record) { const productName = sanitizeGoodsInfoText(data["商品名称"], 255) || sanitizeGoodsInfoText(data["名称"], 255) || null; const sizeInfoParts = [ sanitizeGoodsInfoText(data["商品尺寸"], 128), [data["长"], data["宽"], data["高"]] .map((item) => sanitizeGoodsInfoText(item, 32)) .filter(Boolean) .join(" x "), ].filter(Boolean); const materialInfoParts = [ sanitizeGoodsInfoText(data["材质"], 128), sanitizeGoodsInfoText(data["材质类型"], 128), ].filter(Boolean); return { productName, sizeInfo: sizeInfoParts.join(" · ") || null, materialInfo: materialInfoParts.join(" · ") || null, designer: sanitizeGoodsInfoText(data["设计师"], 128) || null, buyer: sanitizeGoodsInfoText(data["采购负责人"], 128) || null, boardType: sanitizeGoodsInfoText(data["板材类型"], 128) || null, matchedSpu: sanitizeGoodsInfoText(data["spu"], 64) || sanitizeGoodsInfoText(data["SPU"], 64) || null, }; } async function fetchGoodsInfoByKeyword(params: { keyword: string; config: ReturnType; }) { const baseUrl = normalizeBaseUrl(params.config.baseUrl); const clientKey = String(params.config.clientKey || "").trim(); const secret = String(params.config.secret || "").trim(); if (!params.config.enabled || !baseUrl || !clientKey || !secret) { return null; } const timespan = String(Math.floor(Date.now() / 1000)); const sign = createGoodsInfoSignature(timespan, secret); const commonHeaders = { Key: clientKey, Timespan: timespan, Sign: sign, Accept: "application/json", }; const requestPaths = [ `/api/GoodsInfo/SearchByProcode?procode=${encodeURIComponent(params.keyword)}`, `/api/GoodsInfo/SearchBySkuoid?skuoid=${encodeURIComponent(params.keyword)}`, ]; for (const requestPath of requestPaths) { try { const response = await fetch(`${baseUrl}${requestPath}`, { method: "GET", headers: commonHeaders, cache: "no-store", }); const result = (await response.json().catch(() => ({}))) as ExternalGoodsInfoResponse; if (response.ok && result.success === true && result.data && typeof result.data === "object") { return buildGoodsInfoSummary(result.data); } } catch (error) { console.warn("[Agent Workflow Material Search] Goods info query failed:", { requestPath, error, }); } } return null; } function normalizeImageIdentityText(...values: Array) { return values .map((value) => String(value || "").trim().toLowerCase().replace(/\s+/g, "")) .filter(Boolean) .join(" "); } function normalizeBooleanLike(value: unknown): boolean | null { if (typeof value === "boolean") { return value; } const normalized = String(value || "").trim().toLowerCase(); if (!normalized) { return null; } if (["true", "yes", "y", "1", "是"].includes(normalized)) { return true; } if (["false", "no", "n", "0", "否"].includes(normalized)) { return false; } return null; } function extractJsonObjectText(text: string): string | null { const normalized = String(text || "").trim(); if (!normalized) { return null; } const fencedMatch = normalized.match(/```(?:json)?\s*([\s\S]*?)```/i); if (fencedMatch?.[1]) { const fencedText = fencedMatch[1].trim(); if (fencedText.startsWith("{") && fencedText.endsWith("}")) { return fencedText; } } if (normalized.startsWith("{") && normalized.endsWith("}")) { return normalized; } const firstBraceIndex = normalized.indexOf("{"); const lastBraceIndex = normalized.lastIndexOf("}"); if (firstBraceIndex >= 0 && lastBraceIndex > firstBraceIndex) { return normalized.slice(firstBraceIndex, lastBraceIndex + 1); } return null; } function parseFunctionalShowcaseDecision(text: string): { isFunctionalShowcase: boolean; score: number; } | null { const jsonText = extractJsonObjectText(text); if (jsonText) { try { const parsed = JSON.parse(jsonText) as Record; const isFunctionalShowcase = normalizeBooleanLike(parsed.isFunctionalShowcase) ?? normalizeBooleanLike(parsed.isFunctional) ?? normalizeBooleanLike(parsed.shouldUse); const score = Number(parsed.score); if (typeof isFunctionalShowcase === "boolean") { return { isFunctionalShowcase, score: Number.isFinite(score) ? Math.max(0, Math.min(100, Math.round(score))) : isFunctionalShowcase ? 80 : 0, }; } } catch (error) { console.warn("[Agent Workflow Material Search] Failed to parse functional showcase JSON:", error); } } const normalized = String(text || "").replace(/\s+/g, " ").trim(); if (!normalized) { return null; } const positive = /功能图|功能展示|打开图|开门图|开柜图|抽屉图|展示图|适合作为第4张|可作为d档|yes|true/i.test( normalized, ); const negative = /不是功能图|不适合|非功能展示|no|false/i.test(normalized); if (!positive && !negative) { return null; } return { isFunctionalShowcase: positive && !negative, score: positive && !negative ? 75 : 0, }; } function getFixedAnglePriority(image: { key: string; label: string; fileName: string; group: "fixed" | "other"; }) { if (image.group !== "fixed") { return null; } const normalized = normalizeImageIdentityText(image.key, image.label, image.fileName); if (normalized.includes("leftfront45") || /左前45/.test(normalized)) { return "A" as const; } if (normalized.includes("rightfront45") || /右前45/.test(normalized)) { return "B" as const; } if (normalized === "front" || normalized.includes(" front") || /正视|正面/.test(normalized)) { return "C" as const; } return null; } function getFallbackFixedAngleScore(image: { key: string; label: string; fileName: string; group: "fixed" | "other"; }) { if (image.group !== "fixed") { return -1; } const normalized = normalizeImageIdentityText(image.key, image.label, image.fileName); if (normalized.includes("left") && !normalized.includes("front") && !normalized.includes("back")) { return 60; } if (normalized.includes("right") && !normalized.includes("front") && !normalized.includes("back")) { return 58; } if (normalized.includes("leftback45") || /左后45/.test(normalized)) { return 52; } if (normalized.includes("rightback45") || /右后45/.test(normalized)) { return 50; } if (normalized.includes("back") || /背面/.test(normalized)) { return 46; } return 20; } function getFunctionalHintScore(image: { key: string; label: string; fileName: string; group: "fixed" | "other"; }) { const normalized = normalizeImageIdentityText(image.key, image.label, image.fileName); if (FUNCTIONAL_IMAGE_HINT_REGEX.test(normalized)) { return 85; } return image.group === "other" ? 10 : 0; } async function analyzeFunctionalShowcaseImage(params: { imageUrl: string; models?: Awaited>; }) { const prompt = [ "你是白底图角度与功能展示判定器。", "请判断这张白底产品图是否适合作为第4优先级的功能展示图。", "功能展示图指:柜门打开、抽屉拉开、功能结构展示、收纳展示、机构展示等。", "普通角度白底图、纯正面图、纯45度图、纯侧面图,都不是功能展示图。", '只输出 JSON:{"isFunctionalShowcase":true,"score":92,"reason":"不超过20字"}', ].join("\n"); const configuredModels = Array.isArray(params.models) ? params.models : await getEnabledAiModelsWithProvider().catch((error) => { console.warn( "[Agent Workflow Material Search] Failed to load configured Qwen-VL model list:", error, ); return []; }); const configuredQwenVisionModel = configuredModels.find( ({ model }) => model.supportsVision && !model.isSubModel && QWEN_VL_MODEL_MATCHER.test(`${model.displayName} ${model.modelId}`), ); if (configuredQwenVisionModel) { try { const languageModel = await getLanguageModelById(configuredQwenVisionModel.model.id); if (languageModel) { const { text } = await generateText({ model: languageModel, messages: [ { role: "user", content: [ { type: "text", text: prompt, }, { type: "image", image: params.imageUrl, }, ], }, ], temperature: 0, maxTokens: 120, abortSignal: AbortSignal.timeout(25_000), } as any); const parsed = parseFunctionalShowcaseDecision(text); if (parsed) { return parsed; } } } catch (error) { console.warn("[Agent Workflow Material Search] Configured Qwen-VL material analysis failed:", error); } } try { const { callQwen3Vl } = await import("@/lib/ai/qwen3-vl"); const result = await callQwen3Vl({ imageUrl: params.imageUrl, prompt, temperature: 0, maxTokens: 120, timeoutMs: 25_000, retries: 1, }); return ( parseFunctionalShowcaseDecision(result.text) || { isFunctionalShowcase: false, score: 0, } ); } catch (error) { console.warn("[Agent Workflow Material Search] Fallback Qwen-VL material analysis failed:", error); } return { isFunctionalShowcase: false, score: 0, }; } async function recommendMaterialImageIds(params: { images: Array<{ id: string; key: string; label: string; fileName: string; imageUrl: string; assetId: string | null; mimeType: string; group: "fixed" | "other"; sortOrder: number; }>; requiredCount: number; }) { const requiredCount = Math.max(1, Math.min(4, Math.round(params.requiredCount))); const selectedIds: string[] = []; const fixedImages = params.images.filter((image) => image.group === "fixed"); const otherImages = params.images.filter((image) => image.group === "other"); const pushImage = (image?: { id: string } | null) => { if (!image?.id) return; if (!selectedIds.includes(image.id)) { selectedIds.push(image.id); } }; pushImage(fixedImages.find((image) => getFixedAnglePriority(image) === "A")); if (selectedIds.length >= requiredCount) return selectedIds.slice(0, requiredCount); pushImage(fixedImages.find((image) => getFixedAnglePriority(image) === "B")); if (selectedIds.length >= requiredCount) return selectedIds.slice(0, requiredCount); pushImage(fixedImages.find((image) => getFixedAnglePriority(image) === "C")); if (selectedIds.length >= requiredCount) return selectedIds.slice(0, requiredCount); const remainingNeedCount = requiredCount - selectedIds.length; if (remainingNeedCount > 0 && otherImages.length > 0) { const heuristicFunctionalCandidates = otherImages .map((image) => ({ image, score: getFunctionalHintScore(image), })) .filter((item) => item.score >= 80) .sort((left, right) => right.score - left.score || left.image.sortOrder - right.image.sortOrder); heuristicFunctionalCandidates .slice(0, remainingNeedCount) .forEach((item) => pushImage(item.image)); } const functionalRemainingNeedCount = requiredCount - selectedIds.length; if (functionalRemainingNeedCount > 0 && otherImages.length > 0) { const analyzableImages = otherImages .filter((image) => !selectedIds.includes(image.id)) .slice(0, 6); if (analyzableImages.length > 0) { const configuredModels = await getEnabledAiModelsWithProvider().catch((error) => { console.warn("[Agent Workflow Material Search] Failed to load vision models:", error); return undefined; }); const analyzedCandidates = await Promise.all( analyzableImages.map(async (image) => { const analyzed = await analyzeFunctionalShowcaseImage({ imageUrl: image.imageUrl, models: configuredModels, }); return { image, score: analyzed.isFunctionalShowcase ? analyzed.score : 0, }; }), ); analyzedCandidates .filter((item) => item.score > 0) .sort((left, right) => right.score - left.score || left.image.sortOrder - right.image.sortOrder) .slice(0, functionalRemainingNeedCount) .forEach((item) => pushImage(item.image)); } } const stillNeedCount = requiredCount - selectedIds.length; if (stillNeedCount > 0) { const remainingFixedImages = fixedImages .filter((image) => !selectedIds.includes(image.id)) .sort( (left, right) => getFallbackFixedAngleScore(right) - getFallbackFixedAngleScore(left) || left.sortOrder - right.sortOrder, ); remainingFixedImages.slice(0, stillNeedCount).forEach((image) => pushImage(image)); } const finalNeedCount = requiredCount - selectedIds.length; if (finalNeedCount > 0) { params.images .filter((image) => !selectedIds.includes(image.id)) .sort((left, right) => left.sortOrder - right.sortOrder) .slice(0, finalNeedCount) .forEach((image) => pushImage(image)); } return selectedIds.slice(0, requiredCount); } 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"), requiredCount: request.nextUrl.searchParams.get("requiredCount") || undefined, appearanceRecognitionEnabled: request.nextUrl.searchParams.get("appearanceRecognitionEnabled") || undefined, }); const appearanceRecognitionEnabled = parsed.appearanceRecognitionEnabled !== "false" && parsed.appearanceRecognitionEnabled !== "0"; const setting = await getSystemSetting({ key: AGENT_WORKFLOW_MATERIAL_API_CONFIG_KEY }); const config = normalizeAgentWorkflowMaterialApiConfig(setting?.value); const goodsInfoSetting = await getSystemSetting({ key: AGENT_WORKFLOW_GOODS_INFO_API_CONFIG_KEY, }); const goodsInfoConfig = normalizeAgentWorkflowGoodsInfoApiConfig( goodsInfoSetting?.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, goodsInfo: null, }, }); } const matchedSku = String(upstreamResult.data.sku || normalizedKeyword).trim() || normalizedKeyword; const productModel = String(upstreamResult.data.productModel || "").trim() || null; const goodsInfo = queryType === "sku" ? await fetchGoodsInfoByKeyword({ keyword: normalizedKeyword, config: goodsInfoConfig, }).catch((error) => { console.warn("[Agent Workflow Material Search] Goods info fetch failed:", error); return null; }) : null; const requiredCount = Number.isFinite(Number(parsed.requiredCount)) ? Math.max(1, Math.min(4, Math.round(Number(parsed.requiredCount)))) : 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 } ); } const recommendedImageIds = requiredCount ? await recommendMaterialImageIds({ images: importedImages, requiredCount, }) : []; const firstImageAppearanceSummary = appearanceRecognitionEnabled ? await analyzeWorkflowMaterialAppearanceFromImage( importedImages[0]?.imageUrl || "" ).catch((error) => { console.warn("[Agent Workflow Material Search] Appearance analysis failed:", error); return null; }) : null; const annotatedImages = importedImages.map((image, index) => index === 0 && firstImageAppearanceSummary ? { ...image, appearanceSummary: firstImageAppearanceSummary, } : image ); return NextResponse.json({ success: true, data: { query: normalizedKeyword, queryType: "sku", matchedSku, matchedSpu: goodsInfo?.matchedSpu || productModel, materialId: String(upstreamResult.data.materialId || "").trim() || null, productModel, zipUrl: String(upstreamResult.data.downloadUrls?.zip || "").trim() || null, fixedCount: annotatedImages.filter((item) => item.group === "fixed").length, otherCount: annotatedImages.filter((item) => item.group === "other").length, totalCount: annotatedImages.length, images: annotatedImages, recommendedImageIds, skuOptions: [], goodsInfo: goodsInfo ? { productName: goodsInfo.productName, sizeInfo: goodsInfo.sizeInfo, materialInfo: goodsInfo.materialInfo, designer: goodsInfo.designer, buyer: goodsInfo.buyer, boardType: goodsInfo.boardType, appearanceSummary: firstImageAppearanceSummary, } : firstImageAppearanceSummary ? { appearanceSummary: firstImageAppearanceSummary, } : null, }, }); } 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 } ); } }