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"]), storeName: z.string().trim().max(64).optional(), }); type ExternalTaskDetailCandidate = { materialId?: string; taskNo?: string; productModel?: 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; 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 normalizeShopName(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}`; return { id, materialId, taskNo, productModel: String(value.productModel || "").trim() || null, 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 isCandidateMatchedStore( candidate: AgentModeReferenceCandidate, storeName?: string ) { const normalizedRequestedStore = normalizeShopName(storeName); if (!normalizedRequestedStore) return true; const normalizedShopName = normalizeShopName(candidate.shopName); const normalizedCategoryName = normalizeShopName(candidate.categoryName); return ( normalizedShopName === normalizedRequestedStore || normalizedCategoryName === normalizedRequestedStore || (Boolean(normalizedShopName) && normalizedShopName.includes(normalizedRequestedStore)) || (Boolean(normalizedShopName) && normalizedRequestedStore.includes(normalizedShopName)) || (Boolean(normalizedCategoryName) && normalizedCategoryName.includes(normalizedRequestedStore)) || (Boolean(normalizedCategoryName) && normalizedRequestedStore.includes(normalizedCategoryName)) ); } export async function GET(request: NextRequest) { try { const session = await auth(); if (!session?.user?.id) { return NextResponse.json({ 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"), storeName: request.nextUrl.searchParams.get("storeName") || undefined, }); const setting = await getSystemSetting({ key: AGENT_WORKFLOW_MATERIAL_API_CONFIG_KEY }); const config = normalizeAgentWorkflowMaterialApiConfig(setting?.value); if (!config.enabled) { return NextResponse.json( { error: "素材搜索功能未开启,请先在系统设置中启用" }, { status: 400 } ); } if (!config.apiKey) { return NextResponse.json( { 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 NextResponse.json( { success: false, error: { code: errorCode, message: errorMessage, }, }, { status: mapUpstreamErrorStatus(errorCode) } ); } const requestedStoreName = String(parsed.storeName || "").trim(); const normalizedCandidates = sortCandidatesByUploadedAtDesc( (upstreamResult.data.candidates || []).map((item, index) => normalizeCandidate(item, index)) ) .filter((candidate) => isCandidateMatchedStore(candidate, requestedStoreName)) .filter((candidate) => getRenderableCandidateTargetUrls(candidate, parsed.target).length > 0); if (normalizedCandidates.length === 0) { return NextResponse.json( { success: false, error: { code: requestedStoreName ? "STORE_REFERENCE_NOT_FOUND" : parsed.target === "main" ? "MAIN_REFERENCE_NOT_FOUND" : parsed.target === "detail" ? "DETAIL_REFERENCE_NOT_FOUND" : "SKU_REFERENCE_NOT_FOUND", message: requestedStoreName ? `当前店铺「${requestedStoreName}」下没有可用的${ 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 NextResponse.json({ success: true, data: { query: normalizedKeyword, queryType, target: parsed.target, matchedSpu: String(upstreamResult.data.spu || "").trim() || null, matchedSku: String(upstreamResult.data.sku || "").trim() || 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 NextResponse.json( { error: "搜索参数无效", details: error.errors }, { status: 400 } ); } return NextResponse.json( { error: "参考图搜索失败,请稍后重试" }, { status: 500 } ); } }