fix: ensure product detail channel is forwarded
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-05-09 14:37:34 +08:00
parent e6d4c31120
commit d73836c365

View File

@ -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<typeof normalizeAgentWorkflowMaterialApiConfig>;
}) {
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<typeof normalizeAgentWorkflowMaterialApiConfig>;
}) {
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,