import { NextRequest, NextResponse } from "next/server"; import { createLogger } from "@/lib/observability/logger"; import { PRODUCT_PLANNING_ADVANCED_DISABLED, PRODUCT_PLANNING_ADVANCED_DISABLED_MESSAGE, } from "@/lib/product-planning/advanced-disabled"; import { getResolvedProductPlanningApiConfig } from "@/lib/product-planning/server-config"; import { isProductPlanningEnabled, PRODUCT_PLANNING_DISABLED_MESSAGE, } from "@/lib/product-planning/server-feature"; import { getProductPlanningToken } from "@/lib/product-planning/server-auth"; import type { ProductPlanningKeywordInsightTag, ProductPlanningKeywordInsightGroup, ProductPlanningKeywordInsightResponse, ProductPlanningKeywordInsightSuggestion, } from "@/lib/product-planning/types"; export const runtime = "nodejs"; const logger = createLogger("product-planning-keyword-insight-api"); function jsonError(status: number, keyword: string, error: string) { const payload: ProductPlanningKeywordInsightResponse = { success: false, keyword, suggestions: [], groups: [], error, }; return NextResponse.json(payload, { status }); } function normalizeSuggestions(items: unknown) { if (!Array.isArray(items)) return [] as ProductPlanningKeywordInsightSuggestion[]; return items .map((entry, index) => { if (!Array.isArray(entry) || entry.length < 2) return null; const keyword = String(entry[0] || "").trim(); const weight = Number.parseFloat(String(entry[1] || "0")); if (!keyword) return null; return { keyword, weight: Number.isFinite(weight) ? weight : 0, rank: index + 1, } satisfies ProductPlanningKeywordInsightSuggestion; }) .filter((item): item is ProductPlanningKeywordInsightSuggestion => Boolean(item)); } function normalizeGroups(items: unknown) { if (!Array.isArray(items)) return [] as ProductPlanningKeywordInsightGroup[]; return items .map((group) => { if (!group || typeof group !== "object") return null; const record = group as Record; const tagBuckets = Array.isArray(record.data) ? record.data : []; const tags = tagBuckets.flatMap((bucket) => Array.isArray(bucket) ? bucket .map((tag) => { if (!tag || typeof tag !== "object") return null; const tagRecord = tag as Record; const title = String(tagRecord.title || "").trim(); if (!title) return null; return { title, type: String(tagRecord.type || "").toLowerCase() === "hot" ? "hot" : "normal", } as const; }) .filter( (tag): tag is ProductPlanningKeywordInsightTag => Boolean(tag) ) : [] ); return { index: Number.parseInt(String(record.index || "0"), 10) || 0, type: String(record.type || "tag_group").trim() || "tag_group", tags, } satisfies ProductPlanningKeywordInsightGroup; }) .filter((item): item is ProductPlanningKeywordInsightGroup => Boolean(item)); } export async function GET(request: NextRequest) { if (PRODUCT_PLANNING_ADVANCED_DISABLED) { return jsonError(503, "", PRODUCT_PLANNING_ADVANCED_DISABLED_MESSAGE); } const token = await getProductPlanningToken(request); if (!token?.sub) { return jsonError(401, "", "未登录"); } if (!(await isProductPlanningEnabled())) { return jsonError(403, "", PRODUCT_PLANNING_DISABLED_MESSAGE); } const keyword = String(request.nextUrl.searchParams.get("q") || "").trim(); if (!keyword) { return jsonError(400, "", "缺少关键词参数"); } const config = await getResolvedProductPlanningApiConfig(); const apiKey = config.taobaoApiKey; const apiSecret = config.taobaoApiSecret; if (!apiKey || !apiSecret) { return jsonError(500, keyword, "缺少商品策划工具淘宝 API Key / Secret,请前往系统设置配置"); } const apiUrl = `${config.oneboundBaseUrl}/taobao/item_search_suggest/?key=${encodeURIComponent(apiKey)}` + `&secret=${encodeURIComponent(apiSecret)}&q=${encodeURIComponent(keyword)}&cache=yes`; try { const response = await fetch(apiUrl, { signal: AbortSignal.timeout(15000), headers: { Accept: "application/json" }, cache: "no-store", }); if (!response.ok) { return jsonError(502, keyword, `关键词洞察接口请求失败:${response.status}`); } const data = await response.json(); if (data?.error_code && data.error_code !== "0000") { return jsonError(502, keyword, data.error || data.reason || "关键词洞察接口返回错误"); } return NextResponse.json({ success: true, keyword, suggestions: normalizeSuggestions(data?.items?.result), groups: normalizeGroups(data?.items?.magic), } satisfies ProductPlanningKeywordInsightResponse); } catch (error) { logger.error("request_failed", error); return jsonError(502, keyword, "关键词洞察接口调用失败"); } }