import { NextRequest, NextResponse } from "next/server"; import { z } from "zod"; import { auth } from "@/app/(auth)/auth"; import { getSystemSetting } from "@/lib/db/queries"; import { AGENT_WORKFLOW_MATERIAL_API_CONFIG_KEY, normalizeAgentWorkflowMaterialApiConfig, } from "@/lib/agents/workflow-material-config"; import type { AgentDetailReplacementReferenceItem, AgentModeReferenceCandidate, AgentWorkflowMaterialQueryType, } from "@/types/agent"; export const maxDuration = 180; const querySchema = z.object({ keyword: z.string().trim().min(1).max(64), queryType: z.enum(["sku", "spu"]).default("spu"), target: z.enum(["main", "detail", "sku"]), brandName: z.string().trim().max(64).optional(), }); function jsonNoStore( body: Parameters[0], init?: ResponseInit ) { return NextResponse.json(body, { ...init, headers: { "Cache-Control": "no-store, max-age=0", ...(init?.headers || {}), }, }); } type ExternalTaskDetailCandidate = { materialId?: string; taskNo?: string; productModel?: string; brandName?: string; brand?: string; productBrand?: string; brand_name?: 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; categoryName?: string | null; shopName?: string | null; candidateCount?: number; candidates?: ExternalTaskDetailCandidate[]; }; error?: { code?: string; message?: string; }; }; function normalizeBaseUrl(value: string) { return String(value || "").trim().replace(/\/+$/, ""); } 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 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 ): 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 = String( value.brandName || value.brand || value.productBrand || value.brand_name || "" ).trim() || 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, candidateId: params.candidate.id, sortOrder: index, })); } 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 ) { const normalizedRequestedBrand = normalizeComparableName(brandName); if (!normalizedRequestedBrand) return true; const normalizedBrandName = normalizeComparableName(candidate.brandName); if (!normalizedBrandName) return false; return ( normalizedBrandName === normalizedRequestedBrand || normalizedBrandName.includes(normalizedRequestedBrand) || normalizedRequestedBrand.includes(normalizedBrandName) ); } function filterCandidatesByBrand( candidates: AgentModeReferenceCandidate[], brandName?: string ) { const normalizedRequestedBrand = normalizeComparableName(brandName); if (!normalizedRequestedBrand) return candidates; const hasBrandMetadata = candidates.some((candidate) => Boolean(normalizeComparableName(candidate.brandName)) ); if (!hasBrandMetadata) { return candidates; } return candidates.filter((candidate) => isCandidateMatchedBrand(candidate, normalizedRequestedBrand) ); } 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, }); const setting = await getSystemSetting({ key: AGENT_WORKFLOW_MATERIAL_API_CONFIG_KEY }); const config = normalizeAgentWorkflowMaterialApiConfig(setting?.value); if (!config.enabled) { return jsonNoStore( { error: "素材搜索功能未开启,请先在系统设置中启用" }, { status: 400 } ); } if (!config.apiKey) { return jsonNoStore( { error: "素材搜索 API Key 未配置,请先在系统设置中保存" }, { status: 500 } ); } const normalizedKeyword = parsed.keyword.trim().toUpperCase(); const queryType = parsed.queryType as AgentWorkflowMaterialQueryType; const upstreamUrl = `${normalizeBaseUrl( config.baseUrl )}/standard-model-white-background?resourceType=task-detail&${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 ExternalTaskDetailResponse; 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 jsonNoStore( { success: false, error: { code: errorCode, message: errorMessage, }, }, { status: mapUpstreamErrorStatus(errorCode) } ); } const requestedBrandName = String(parsed.brandName || "").trim(); const sortedCandidates = sortCandidatesByUploadedAtDesc( (upstreamResult.data.candidates || []).map((item, index) => normalizeCandidate(item, index)) ); const normalizedCandidates = filterCandidatesByBrand(sortedCandidates, requestedBrandName) .filter((candidate) => getRenderableCandidateTargetUrls(candidate, parsed.target).length > 0); if (normalizedCandidates.length === 0) { 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 selectedCandidate = normalizedCandidates[0]; const references = buildReferenceItems({ candidate: selectedCandidate, target: parsed.target, }); return jsonNoStore({ success: true, data: { query: normalizedKeyword, queryType, target: parsed.target, matchedSpu: String(upstreamResult.data.spu || "").trim() || null, matchedSku: String(upstreamResult.data.sku || "").trim() || null, brandName: selectedCandidate.brandName || String( upstreamResult.data.brandName || upstreamResult.data.brand || upstreamResult.data.productBrand || "" ).trim() || requestedBrandName || null, categoryName: selectedCandidate.categoryName || String(upstreamResult.data.categoryName || "").trim() || null, shopName: selectedCandidate.shopName || String(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 } ); } }