From d73836c365d60ada13984be0bd200355cd548b82 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 9 May 2026 14:37:34 +0800 Subject: [PATCH] fix: ensure product detail channel is forwarded --- .../agent-workflow/reference-search/route.ts | 116 ++++++++++++++---- 1 file changed, 94 insertions(+), 22 deletions(-) diff --git a/app/api/agent-workflow/reference-search/route.ts b/app/api/agent-workflow/reference-search/route.ts index 7b93055d5..72a4587f8 100644 --- a/app/api/agent-workflow/reference-search/route.ts +++ b/app/api/agent-workflow/reference-search/route.ts @@ -22,12 +22,17 @@ import type { 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: z.enum(["taobao", "1688", "douyin", "jd"]).optional(), + channel: productDetailChannelSchema, }); function jsonNoStore( @@ -43,6 +48,31 @@ function jsonNoStore( }); } +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; @@ -482,6 +512,10 @@ function buildProductDetailRequestUrl(params: { 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" @@ -495,7 +529,7 @@ function buildProductDetailRequestUrl(params: { ? rawPath : `${baseUrl}/${rawPath.replace(/^\/+/, "")}`; const encodedProductId = encodeURIComponent(params.productId); - const encodedChannel = encodeURIComponent(params.channel); + const encodedChannel = encodeURIComponent(channel); const beforeReplace = requestUrl; requestUrl = requestUrl .replace(/\{\{\s*item_id\s*\}\}/gi, encodedProductId) @@ -507,12 +541,13 @@ function buildProductDetailRequestUrl(params: { if (params.method === "GET") { const url = new URL(requestUrl); - if (!url.searchParams.has("item_id")) { + if (!String(url.searchParams.get("item_id") || "").trim()) { url.searchParams.set("item_id", params.productId); } - if (!url.searchParams.has("channel")) { - url.searchParams.set("channel", params.channel); - } + url.searchParams.set( + "channel", + normalizeProductDetailChannel(url.searchParams.get("channel"), channel) + ); if (!url.searchParams.has("refresh")) { url.searchParams.set("refresh", "false"); } @@ -585,6 +620,11 @@ async function requestProductDetailReferences(params: { channel: AgentWorkflowProductDetailChannel; config: ReturnType; }) { + const channel = normalizeProductDetailChannel( + params.channel, + normalizeProductDetailChannel(params.config.productDetailDefaultChannel) + ); + if (!params.config.productDetailEnabled) { return jsonNoStore( { @@ -626,25 +666,55 @@ async function requestProductDetailReferences(params: { const method = params.config.productDetailMethod === "POST" ? "POST" : "GET"; const requestUrl = buildProductDetailRequestUrl({ ...params, + channel, method, }); const requestBody = { item_id: params.productId, - channel: params.channel, + channel, refresh: false, sync_analysis: params.config.productDetailSyncAnalysis !== false, force_reanalyze: params.config.productDetailForceReanalyze === true, }; - const response = await fetch(requestUrl, { + const startedAt = Date.now(); + console.info("[Agent Workflow Reference Search] Product detail request:", { method, - headers: { - Accept: "application/json", - "X-API-KEY": apiKey, - ...(method === "POST" ? { "Content-Type": "application/json" } : {}), - }, - body: method === "POST" ? JSON.stringify(requestBody) : undefined, - cache: "no-store", + productId: params.productId, + channel, + url: getProductDetailRequestLogUrl(requestUrl), }); + + let response: Response; + try { + response = await fetch(requestUrl, { + method, + headers: { + Accept: "application/json", + "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) @@ -705,16 +775,16 @@ async function requestProductDetailReferences(params: { const shopName = sanitizeText(productData.shopName, 128) || null; const channelLabel = sanitizeText(productData.channelLabel, 32) || - (params.channel === "taobao" + (channel === "taobao" ? "淘宝" - : params.channel === "douyin" + : channel === "douyin" ? "抖音" - : params.channel === "jd" + : channel === "jd" ? "京东" : "1688"); const candidate: AgentModeReferenceCandidate = { - id: `product-id-${params.channel}-${resolvedItemId}`, + id: `product-id-${channel}-${resolvedItemId}`, materialId: resolvedItemId, taskNo: sanitizeText(responseRecord.request_id, 128) || null, productModel: title || resolvedItemId, @@ -745,7 +815,7 @@ async function requestProductDetailReferences(params: { brandName: null, categoryName: null, shopName, - channel: params.channel, + channel, candidateCount: 1, candidates: [candidate], selectedCandidateId: candidate.id, @@ -793,8 +863,10 @@ export async function GET(request: NextRequest) { { status: 400 } ); } - const productDetailChannel = - parsed.channel || config.productDetailDefaultChannel || "taobao"; + const productDetailChannel = normalizeProductDetailChannel( + parsed.channel, + normalizeProductDetailChannel(config.productDetailDefaultChannel) + ); return requestProductDetailReferences({ productId: normalizedKeyword, channel: productDetailChannel,