import { NextRequest, NextResponse } from "next/server"; import { execFileSync } from "node:child_process"; import { createCipheriv, createHash } from "node:crypto"; import { z } from "zod"; import { auth } from "@/app/(auth)/auth"; import { getSystemSetting } from "@/lib/db/queries"; import { AGENT_WORKFLOW_MATERIAL_API_CONFIG_KEY, buildAgentWorkflowMaterialApiUrl, isAgentWorkflowMaterialResourceEnabled, normalizeAgentWorkflowMaterialApiConfig, } from "@/lib/agents/workflow-material-config"; import { AGENT_WORKFLOW_GOODS_INFO_API_CONFIG_KEY, normalizeAgentWorkflowGoodsInfoApiConfig, } from "@/lib/agents/workflow-goods-info-config"; import type { AgentDetailReplacementReferenceItem, AgentModeReferenceCandidate, AgentWorkflowMaterialQueryType, AgentWorkflowProductDetailChannel, AgentWorkflowReferenceQueryType, } from "@/types/agent"; export const maxDuration = 180; const productDetailChannelSchema = z.preprocess((value) => { const normalized = typeof value === "string" ? value.trim() : ""; return normalized || undefined; }, z.enum(["taobao", "1688", "douyin", "jd"]).optional()); const querySchema = z.object({ keyword: z.string().trim().min(1).max(64), queryType: z.enum(["sku", "spu", "productId"]).default("spu"), target: z.enum(["main", "detail", "sku"]), brandName: z.string().trim().max(64).optional(), channel: productDetailChannelSchema, strictQueryType: z.enum(["true", "false", "1", "0"]).optional(), }); function jsonNoStore( body: Parameters[0], init?: ResponseInit ) { return NextResponse.json(body, { ...init, headers: { "Cache-Control": "no-store, max-age=0", ...(init?.headers || {}), }, }); } function normalizeProductDetailChannel( value: unknown, fallback: AgentWorkflowProductDetailChannel = "taobao" ): AgentWorkflowProductDetailChannel { const normalized = String(value || "").trim(); if ( normalized === "taobao" || normalized === "1688" || normalized === "douyin" || normalized === "jd" ) { return normalized; } return fallback; } function getProductDetailRequestLogUrl(value: string) { try { const url = new URL(value); return `${url.origin}${url.pathname}?${url.searchParams.toString()}`; } catch { return value.slice(0, 500); } } type ExternalTaskDetailCandidate = { materialId?: string; taskNo?: string; productModel?: string; brandName?: string; brand?: string; productBrand?: string; brand_name?: string; brand_name_cn?: string; brandTitle?: string; brand_title?: string; categoryName?: string; shopName?: string; sceneName?: string; uploadedAt?: string; isFullSet?: boolean; downloadUrls?: { main?: string[]; sku?: string[]; detail?: string[]; all?: string[]; }; }; type ExternalTaskDetailResponse = { success?: boolean; data?: { resourceType?: string; spu?: string | null; sku?: string | null; brandName?: string | null; brand?: string | null; productBrand?: string | null; brand_name?: string | null; brand_name_cn?: string | null; brandTitle?: string | null; brand_title?: string | null; categoryName?: string | null; shopName?: string | null; candidateCount?: number; candidates?: ExternalTaskDetailCandidate[]; }; 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 sanitizeText(value: unknown, maxLength: number) { return String(value || "").trim().slice(0, maxLength); } function normalizeComparableName(value: unknown) { return String(value || "") .trim() .toLowerCase() .replace(/\s+/g, ""); } function normalizeUrlList(value: unknown) { if (!Array.isArray(value)) return [] as string[]; return Array.from( new Set( value .map((item) => String(item || "").trim()) .filter((item) => /^https?:\/\//i.test(item)) ) ).slice(0, 64); } function getExternalBrandName(value: { brandName?: string | null; brand?: string | null; productBrand?: string | null; brand_name?: string | null; brand_name_cn?: string | null; brandTitle?: string | null; brand_title?: string | null; }) { return ( String( value.brandName || value.brand || value.productBrand || value.brand_name || value.brand_name_cn || value.brandTitle || value.brand_title || "" ).trim() || null ); } 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 Reference 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 getGoodsInfoBrandName(data: Record) { return ( sanitizeText(data["品牌"], 64) || sanitizeText(data["品牌名称"], 64) || sanitizeText(data["商品品牌"], 64) || sanitizeText(data["品牌名"], 64) || sanitizeText(data["所属品牌"], 64) || sanitizeText(data["brandName"], 64) || sanitizeText(data["brand"], 64) || sanitizeText(data["productBrand"], 64) || null ); } function getGoodsInfoMatchedSpu(data: Record) { return ( sanitizeText(data["spu"], 64) || sanitizeText(data["SPU"], 64) || sanitizeText(data["productSpu"], 64) || sanitizeText(data["ProductSpu"], 64) || sanitizeText(data["商品SPU"], 64) || sanitizeText(data["商品spu"], 64) || sanitizeText(data["款号"], 64) || sanitizeText(data["商品款号"], 64) || null ); } function getGoodsInfoMatchedSku(data: Record) { return ( sanitizeText(data["sku"], 64) || sanitizeText(data["SKU"], 64) || sanitizeText(data["skuId"], 64) || sanitizeText(data["SkuId"], 64) || sanitizeText(data["skuoid"], 64) || sanitizeText(data["SKUOID"], 64) || sanitizeText(data["商品SKU"], 64) || sanitizeText(data["商品sku"], 64) || null ); } async function fetchGoodsLookupByKeyword(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) { const data = result.data; const brandName = getGoodsInfoBrandName(data); const matchedSpu = getGoodsInfoMatchedSpu(data); const matchedSku = getGoodsInfoMatchedSku(data); if (brandName || matchedSpu || matchedSku) { return { brandName, matchedSpu, matchedSku, }; } } } catch (error) { console.warn("[Agent Workflow Reference Search] Goods info lookup failed:", { requestPath, error, }); } } return null; } function mapUpstreamErrorStatus(code: string) { if ( code === "SKU_NOT_FOUND" || code === "SPU_NOT_FOUND" || code === "TASK_DETAIL_NOT_FOUND" || code === "FULL_SET_NOT_FOUND" ) { return 404; } if (code === "INVALID_API_KEY") { return 500; } if (code === "SKU_NOT_UNIQUE") { return 409; } return 502; } function normalizeCandidate( value: ExternalTaskDetailCandidate, index: number, fallbackBrandName?: string | null ): AgentModeReferenceCandidate { const uploadedAt = String(value.uploadedAt || "").trim() || null; const taskNo = String(value.taskNo || "").trim() || null; const materialId = String(value.materialId || "").trim() || null; const sceneName = String(value.sceneName || "").trim() || null; const id = taskNo || materialId || `candidate-${index + 1}`; const brandName = getExternalBrandName(value) || fallbackBrandName || null; return { id, materialId, taskNo, productModel: String(value.productModel || "").trim() || null, brandName, categoryName: String(value.categoryName || "").trim() || null, shopName: String(value.shopName || "").trim() || null, sceneName, uploadedAt, isFullSet: value.isFullSet !== false, mainImageUrls: normalizeUrlList(value.downloadUrls?.main), skuImageUrls: normalizeUrlList(value.downloadUrls?.sku), detailImageUrls: normalizeUrlList(value.downloadUrls?.detail), allImageUrls: normalizeUrlList(value.downloadUrls?.all), }; } function getCandidateTargetUrls( candidate: AgentModeReferenceCandidate, target: "main" | "detail" | "sku" ) { if (target === "main") return candidate.mainImageUrls; return target === "detail" ? candidate.detailImageUrls : candidate.skuImageUrls; } function getRenderableCandidateTargetUrls( candidate: AgentModeReferenceCandidate, target: "main" | "detail" | "sku" ) { return getCandidateTargetUrls(candidate, target); } function buildReferenceItems(params: { candidate: AgentModeReferenceCandidate; target: "main" | "detail" | "sku"; }): AgentDetailReplacementReferenceItem[] { const urls = getRenderableCandidateTargetUrls(params.candidate, params.target); const targetLabel = params.target === "main" ? "主图参考图" : params.target === "detail" ? "详情参考图" : "SKU参考图"; const prefix = params.candidate.sceneName || params.candidate.taskNo || params.candidate.productModel || "候选整套"; return urls.map((url, index) => ({ id: `${params.candidate.id}-${params.target}-${index + 1}`, label: `${prefix} ${targetLabel} ${String(index + 1).padStart(2, "0")}`, imageUrl: url, previewUrl: url, candidateId: params.candidate.id, sortOrder: index, passthrough: false, })); } function sortCandidatesByUploadedAtDesc(candidates: AgentModeReferenceCandidate[]) { return candidates.slice().sort((left, right) => { const leftTime = left.uploadedAt ? new Date(left.uploadedAt).getTime() : 0; const rightTime = right.uploadedAt ? new Date(right.uploadedAt).getTime() : 0; return rightTime - leftTime; }); } function isCandidateMatchedBrand( candidate: AgentModeReferenceCandidate, brandName?: string ) { return ( isBrandNameMatched(candidate.brandName, brandName) || isBrandNameMatched(candidate.shopName, brandName) ); } function isBrandNameMatched( candidateBrandName?: string | null, brandName?: string ) { const normalizedRequestedBrand = normalizeComparableName(brandName); if (!normalizedRequestedBrand) return true; const normalizedBrandName = normalizeComparableName(candidateBrandName); if (!normalizedBrandName) return false; if (normalizedBrandName === normalizedRequestedBrand) return true; // 只允许“候选品牌包含当前品牌”的弱匹配,避免短品牌名反向包含导致串品牌。 return ( normalizedRequestedBrand.length >= 2 && normalizedBrandName.includes(normalizedRequestedBrand) ); } function filterCandidatesByBrand( candidates: AgentModeReferenceCandidate[], brandName?: string ) { const normalizedRequestedBrand = normalizeComparableName(brandName); if (!normalizedRequestedBrand) return candidates; return candidates.filter((candidate) => isCandidateMatchedBrand(candidate, normalizedRequestedBrand) ); } async function requestTaskDetailAttempt(params: { keyword: string; queryType: AgentWorkflowMaterialQueryType; target: "main" | "detail" | "sku"; requestedBrandName: string; verifiedGoodsBrandName?: string | null; config: ReturnType; }) { const upstreamUrl = buildAgentWorkflowMaterialApiUrl(params.config, { resourceType: "task-detail", queryType: params.queryType, keyword: params.keyword, }); const upstreamResponse = await fetch(upstreamUrl, { method: "GET", headers: { "X-API-Key": params.config.apiKey, }, cache: "no-store", signal: AbortSignal.timeout(params.config.requestTimeoutMs || 30_000), }); const upstreamResult = (await upstreamResponse.json().catch(() => ({}))) as ExternalTaskDetailResponse; const upstreamBrandName = getExternalBrandName(upstreamResult.data || {}); const rawCandidates = upstreamResult.data?.candidates || []; const candidateBrandSamples = Array.from( new Set( rawCandidates .flatMap((candidate) => [ getExternalBrandName(candidate), String(candidate.shopName || "").trim() || null, ]) .filter((brandName): brandName is string => Boolean(brandName)) ) ).slice(0, 8); const hasVerifiedRequestedBrand = Boolean(params.verifiedGoodsBrandName) && isBrandNameMatched(params.verifiedGoodsBrandName, params.requestedBrandName); const hasUpstreamRequestedBrand = Boolean(upstreamBrandName) && isBrandNameMatched(upstreamBrandName, params.requestedBrandName); let fallbackBrandName: string | null = null; if (params.requestedBrandName) { if (hasVerifiedRequestedBrand) { fallbackBrandName = params.verifiedGoodsBrandName || null; } else if (hasUpstreamRequestedBrand) { fallbackBrandName = upstreamBrandName || null; } } else { fallbackBrandName = upstreamBrandName; } const normalizedCandidates = upstreamResponse.ok && upstreamResult.success === true && upstreamResult.data ? filterCandidatesByBrand( sortCandidatesByUploadedAtDesc( rawCandidates.map((item, index) => normalizeCandidate(item, index, fallbackBrandName) ) ), params.requestedBrandName, ).filter( (candidate) => getRenderableCandidateTargetUrls(candidate, params.target).length > 0 ) : []; return { queryType: params.queryType, upstreamResponse, upstreamResult, upstreamBrandName, rawCandidates, candidateBrandSamples, fallbackBrandName, normalizedCandidates, }; } function buildProductDetailRequestUrl(params: { productId: string; channel: AgentWorkflowProductDetailChannel; method: "GET" | "POST"; config: ReturnType; }) { const baseUrl = normalizeBaseUrl(params.config.productDetailBaseUrl || ""); const channel = normalizeProductDetailChannel( params.channel, normalizeProductDetailChannel(params.config.productDetailDefaultChannel) ); const rawPath = String(params.config.productDetailPath || "").trim() || (params.method === "POST" ? "/open-api/detail-images/query" : "/open-api/detail-images"); if (!baseUrl && !/^https?:\/\//i.test(rawPath)) { throw new Error("商品ID详情图接口基地址未配置"); } let requestUrl = /^https?:\/\//i.test(rawPath) ? rawPath : `${baseUrl}/${rawPath.replace(/^\/+/, "")}`; const encodedProductId = encodeURIComponent(params.productId); const encodedChannel = encodeURIComponent(channel); const beforeReplace = requestUrl; requestUrl = requestUrl .replace(/\{\{\s*item_id\s*\}\}/gi, encodedProductId) .replace(/\{\s*item_id\s*\}/gi, encodedProductId) .replace(/\{\{\s*productId\s*\}\}/gi, encodedProductId) .replace(/\{\s*productId\s*\}/gi, encodedProductId) .replace(/\{\{\s*channel\s*\}\}/gi, encodedChannel) .replace(/\{\s*channel\s*\}/gi, encodedChannel); if (params.method === "GET") { const url = new URL(requestUrl); if (!String(url.searchParams.get("item_id") || "").trim()) { url.searchParams.set("item_id", params.productId); } url.searchParams.set( "channel", normalizeProductDetailChannel(url.searchParams.get("channel"), channel) ); if (!url.searchParams.has("refresh")) { url.searchParams.set("refresh", "false"); } if (!url.searchParams.has("sync_analysis")) { url.searchParams.set( "sync_analysis", params.config.productDetailSyncAnalysis === false ? "0" : "1" ); } if (!url.searchParams.has("force_reanalyze")) { url.searchParams.set( "force_reanalyze", params.config.productDetailForceReanalyze === true ? "1" : "0" ); } requestUrl = url.toString(); } else if (requestUrl === beforeReplace) { return requestUrl; } return requestUrl; } function getProductDetailResponseData(value: unknown) { const raw = value && typeof value === "object" && !Array.isArray(value) ? (value as Record) : {}; return raw.data && typeof raw.data === "object" && !Array.isArray(raw.data) ? (raw.data as Record) : {}; } function extractProductDetailImageUrls(value: unknown) { const matchedUrls = new Set(); const keyPattern = /(detail|desc|description|product).*?(image|img|pic|url|urls)|(image|img|pic|url|urls).*?(detail|desc|description|product)|^(images?|imageUrls?|detailImages?|detailImageUrls?|pics?|urls?)$/i; const visit = (input: unknown, keyHint = "", forced = false, depth = 0) => { if (depth > 8 || matchedUrls.size >= 64) return; if (typeof input === "string") { const normalized = input.trim(); if ((forced || keyPattern.test(keyHint)) && /^https?:\/\//i.test(normalized)) { matchedUrls.add(normalized); } return; } if (Array.isArray(input)) { const nextForced = forced || keyPattern.test(keyHint); input.forEach((item) => visit(item, keyHint, nextForced, depth + 1)); return; } if (!input || typeof input !== "object") { return; } Object.entries(input as Record).forEach(([key, child]) => { visit(child, key, forced || keyPattern.test(key), depth + 1); }); }; visit(value, Array.isArray(value) ? "images" : "", Array.isArray(value), 0); return Array.from(matchedUrls); } async function requestProductDetailReferences(params: { productId: string; channel: AgentWorkflowProductDetailChannel; target: "main" | "detail"; config: ReturnType; }) { const channel = normalizeProductDetailChannel( params.channel, normalizeProductDetailChannel(params.config.productDetailDefaultChannel) ); if (!params.config.productDetailEnabled) { return jsonNoStore( { success: false, error: { code: "PRODUCT_DETAIL_API_DISABLED", message: "商品ID详情图接口未启用,请先在系统设置中配置", }, }, { status: 400 } ); } if (!params.config.productDetailBaseUrl && !params.config.productDetailPath) { return jsonNoStore( { success: false, error: { code: "PRODUCT_DETAIL_API_NOT_CONFIGURED", message: "商品ID详情图接口地址未配置,请先在系统设置中填写", }, }, { status: 400 } ); } const apiKey = String(params.config.productDetailApiKey || params.config.apiKey || "").trim(); if (!apiKey) { return jsonNoStore( { success: false, error: { code: "PRODUCT_DETAIL_API_KEY_MISSING", message: "商品ID详情图接口 API Key 未配置,请先在系统设置中填写", }, }, { status: 500 } ); } const method = params.config.productDetailMethod === "POST" ? "POST" : "GET"; const requestUrl = buildProductDetailRequestUrl({ ...params, channel, method, }); const requestBody = { item_id: params.productId, channel, refresh: false, sync_analysis: params.config.productDetailSyncAnalysis !== false, force_reanalyze: params.config.productDetailForceReanalyze === true, }; const startedAt = Date.now(); console.info("[Agent Workflow Reference Search] Product detail request:", { method, productId: params.productId, channel, url: getProductDetailRequestLogUrl(requestUrl), }); let response: Response; try { response = await fetch(requestUrl, { method, headers: { Accept: "application/json", Connection: "close", "User-Agent": "curl/8.0", "X-API-KEY": apiKey, ...(method === "POST" ? { "Content-Type": "application/json" } : {}), }, body: method === "POST" ? JSON.stringify(requestBody) : undefined, cache: "no-store", }); } catch (error) { console.warn("[Agent Workflow Reference Search] Product detail request failed:", { method, productId: params.productId, channel, elapsedMs: Date.now() - startedAt, error: error instanceof Error ? error.message : String(error), }); return jsonNoStore( { success: false, error: { code: "PRODUCT_DETAIL_API_FETCH_FAILED", message: "商品ID详情图接口访问失败,请稍后重试或检查接口网络", }, }, { status: 502 } ); } const result = await response.json().catch(() => ({})); const responseRecord = result && typeof result === "object" && !Array.isArray(result) ? (result as Record) : {}; if (responseRecord.success === false) { const errorCode = sanitizeText(responseRecord.error, 128) || "PRODUCT_DETAIL_API_ERROR"; const errorMessage = sanitizeText(responseRecord.message, 255) || sanitizeText(responseRecord.error, 255) || "商品ID详情图查询失败"; return jsonNoStore( { success: false, error: { code: errorCode, message: errorMessage, }, }, { status: response.ok ? 502 : response.status } ); } if (!response.ok) { return jsonNoStore( { success: false, error: { code: "PRODUCT_DETAIL_API_ERROR", message: `商品ID详情图查询失败(${response.status})`, }, }, { status: 502 } ); } const productData = getProductDetailResponseData(result); const detailImageUrls = normalizeUrlList(productData.detailImages); const fallbackDetailImageUrls = detailImageUrls.length > 0 ? detailImageUrls : extractProductDetailImageUrls(result); const mainImageUrls = normalizeUrlList(productData.mainImages); const resolvedTarget = params.target === "main" ? "main" : "detail"; if (resolvedTarget === "detail" && fallbackDetailImageUrls.length === 0) { return jsonNoStore( { success: false, error: { code: "PRODUCT_DETAIL_IMAGES_NOT_FOUND", message: "商品ID接口未返回可用详情图片", }, }, { status: 404 } ); } if (resolvedTarget === "main" && mainImageUrls.length === 0) { return jsonNoStore( { success: false, error: { code: "PRODUCT_MAIN_IMAGES_NOT_FOUND", message: "商品ID接口未返回可用主图", }, }, { status: 404 } ); } const resolvedItemId = sanitizeText(productData.itemId, 128) || sanitizeText(productData.sourceItemId, 128) || params.productId; const title = sanitizeText(productData.title, 255); const shopName = sanitizeText(productData.shopName, 128) || null; const channelLabel = sanitizeText(productData.channelLabel, 32) || (channel === "taobao" ? "淘宝" : channel === "douyin" ? "抖音" : channel === "jd" ? "京东" : "1688"); const candidate: AgentModeReferenceCandidate = { id: `product-id-${channel}-${resolvedItemId}`, materialId: resolvedItemId, taskNo: sanitizeText(responseRecord.request_id, 128) || null, productModel: title || resolvedItemId, brandName: null, categoryName: null, shopName, sceneName: title || `${channelLabel}商品 ${resolvedItemId}`, uploadedAt: sanitizeText(productData.fetchedAt, 64) || new Date().toISOString(), isFullSet: true, mainImageUrls, skuImageUrls: [], detailImageUrls: fallbackDetailImageUrls, allImageUrls: Array.from(new Set([...mainImageUrls, ...fallbackDetailImageUrls])), }; const references = buildReferenceItems({ candidate, target: resolvedTarget, }); return jsonNoStore({ success: true, data: { query: resolvedItemId, queryType: "productId" as const, target: resolvedTarget, matchedSpu: null, matchedSku: resolvedItemId, brandName: null, categoryName: null, shopName, channel, candidateCount: 1, candidates: [candidate], selectedCandidateId: candidate.id, references, }, }); } export async function GET(request: NextRequest) { try { const session = await auth(); if (!session?.user?.id) { return jsonNoStore({ error: "未登录" }, { status: 401 }); } const parsed = querySchema.parse({ keyword: request.nextUrl.searchParams.get("keyword"), queryType: request.nextUrl.searchParams.get("queryType") || "spu", target: request.nextUrl.searchParams.get("target"), brandName: request.nextUrl.searchParams.get("brandName") || undefined, channel: request.nextUrl.searchParams.get("channel") || undefined, strictQueryType: request.nextUrl.searchParams.get("strictQueryType") || undefined, }); 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 ); const queryType = parsed.queryType as AgentWorkflowReferenceQueryType; const normalizedKeyword = queryType === "productId" ? parsed.keyword.trim() : parsed.keyword.trim().toUpperCase(); if (queryType === "productId") { if (parsed.target !== "detail" && parsed.target !== "main") { return jsonNoStore( { success: false, error: { code: "UNSUPPORTED_PRODUCT_ID_TARGET", message: "商品ID查询当前仅支持主图或详情图", }, }, { status: 400 } ); } const productDetailChannel = normalizeProductDetailChannel( parsed.channel, normalizeProductDetailChannel(config.productDetailDefaultChannel) ); return requestProductDetailReferences({ productId: normalizedKeyword, channel: productDetailChannel, target: parsed.target, config, }); } if (!config.enabled) { return jsonNoStore( { error: "素材搜索功能未开启,请先在系统设置中启用" }, { status: 400 } ); } if (!isAgentWorkflowMaterialResourceEnabled(config, "task-detail")) { return jsonNoStore( { error: "任务-详情整套素材接口未开启,请先在系统设置中启用" }, { status: 400 } ); } if (!config.apiKey) { return jsonNoStore( { error: "素材搜索 API Key 未配置,请先在系统设置中保存" }, { status: 500 } ); } const materialQueryType = queryType as AgentWorkflowMaterialQueryType; const requestedBrandName = String(parsed.brandName || "").trim(); const goodsLookup = await fetchGoodsLookupByKeyword({ keyword: normalizedKeyword, config: goodsInfoConfig, }).catch((error) => { console.warn("[Agent Workflow Reference Search] Goods info lookup failed:", error); return null; }); const verifiedGoodsBrandName = goodsLookup?.brandName || null; const resolvedMaterialKeyword = materialQueryType === "spu" && goodsLookup?.matchedSpu ? goodsLookup.matchedSpu.trim().toUpperCase() : normalizedKeyword; if (materialQueryType === "spu" && resolvedMaterialKeyword !== normalizedKeyword) { console.info("[Agent Workflow Reference Search] Resolved SPU query keyword:", { keyword: normalizedKeyword, resolvedSpu: resolvedMaterialKeyword, matchedSku: goodsLookup?.matchedSku || null, }); } if ( requestedBrandName && verifiedGoodsBrandName && !isBrandNameMatched(verifiedGoodsBrandName, requestedBrandName) ) { console.warn("[Agent Workflow Reference Search] Goods brand hint mismatched selected brand, keep searching with upstream data:", { keyword: normalizedKeyword, requestedBrandName, verifiedGoodsBrandName, }); } const strictQueryType = parsed.strictQueryType === "true" || parsed.strictQueryType === "1"; const queryTypesToTry = strictQueryType ? [materialQueryType] : Array.from( new Set([ materialQueryType, materialQueryType === "spu" ? "sku" : "spu", ]) ); let selectedAttempt: | Awaited> | null = null; let firstUpstreamFailure: | Awaited> | null = null; let firstEmptyAttempt: | Awaited> | null = null; for (const currentQueryType of queryTypesToTry) { const attempt = await requestTaskDetailAttempt({ keyword: resolvedMaterialKeyword, queryType: currentQueryType, target: parsed.target, requestedBrandName, verifiedGoodsBrandName, config, }); if (requestedBrandName) { console.info("[Agent Workflow Reference Search] Brand gate:", { keyword: normalizedKeyword, resolvedKeyword: resolvedMaterialKeyword, queryType: currentQueryType, target: parsed.target, requestedBrandName, verifiedGoodsBrandName, upstreamBrandName: attempt.upstreamBrandName, candidateCount: attempt.rawCandidates.length, candidateBrandSamples: attempt.candidateBrandSamples, }); } if ( attempt.upstreamResponse.ok && attempt.upstreamResult.success === true && attempt.upstreamResult.data ) { if (!firstEmptyAttempt) { firstEmptyAttempt = attempt; } if (attempt.normalizedCandidates.length > 0) { selectedAttempt = attempt; break; } continue; } if (!firstUpstreamFailure) { firstUpstreamFailure = attempt; } } if (!selectedAttempt) { if (firstEmptyAttempt) { return jsonNoStore( { success: false, error: { code: requestedBrandName ? "BRAND_REFERENCE_NOT_FOUND" : parsed.target === "main" ? "MAIN_REFERENCE_NOT_FOUND" : parsed.target === "detail" ? "DETAIL_REFERENCE_NOT_FOUND" : "SKU_REFERENCE_NOT_FOUND", message: requestedBrandName ? `当前品牌「${requestedBrandName}」下没有可用的${ parsed.target === "main" ? "主图参考图" : parsed.target === "detail" ? "详情参考图" : "SKU参考图" }` : parsed.target === "main" ? "当前整套素材中没有可用的主图参考图" : parsed.target === "detail" ? "当前整套素材中没有可用的详情参考图" : "当前整套素材中没有可用的 SKU 参考图", }, }, { status: 404 } ); } const errorCode = String( firstUpstreamFailure?.upstreamResult.error?.code || "UPSTREAM_ERROR" ).trim(); const errorMessage = String(firstUpstreamFailure?.upstreamResult.error?.message || "").trim() || `参考图搜索失败(${firstUpstreamFailure?.upstreamResponse.status || 500})`; return jsonNoStore( { success: false, error: { code: errorCode, message: errorMessage, }, }, { status: mapUpstreamErrorStatus(errorCode) } ); } if (selectedAttempt.queryType !== materialQueryType) { console.info("[Agent Workflow Reference Search] Query type fallback succeeded:", { keyword: normalizedKeyword, resolvedKeyword: resolvedMaterialKeyword, requestedQueryType: materialQueryType, resolvedQueryType: selectedAttempt.queryType, target: parsed.target, }); } const normalizedCandidates = selectedAttempt.normalizedCandidates; const selectedCandidate = normalizedCandidates[0]; const references = buildReferenceItems({ candidate: selectedCandidate, target: parsed.target, }); return jsonNoStore({ success: true, data: { query: normalizedKeyword, resolvedQuery: resolvedMaterialKeyword, queryType: selectedAttempt.queryType, target: parsed.target, matchedSpu: String(selectedAttempt.upstreamResult.data?.spu || "").trim() || null, matchedSku: String(selectedAttempt.upstreamResult.data?.sku || "").trim() || null, brandName: selectedCandidate.brandName || selectedAttempt.fallbackBrandName || selectedCandidate.shopName || null, categoryName: selectedCandidate.categoryName || String(selectedAttempt.upstreamResult.data?.categoryName || "").trim() || null, shopName: selectedCandidate.shopName || String(selectedAttempt.upstreamResult.data?.shopName || "").trim() || null, candidateCount: normalizedCandidates.length, candidates: normalizedCandidates, selectedCandidateId: selectedCandidate.id, references, }, }); } catch (error) { console.error("[Agent Workflow Reference Search] Error:", error); if (error instanceof z.ZodError) { return jsonNoStore( { error: "搜索参数无效", details: error.errors }, { status: 400 } ); } return jsonNoStore( { error: "参考图搜索失败,请稍后重试" }, { status: 500 } ); } }