diff --git a/app/api/agent-workflow/reference-search/route.ts b/app/api/agent-workflow/reference-search/route.ts index c6d487823..96e280320 100644 --- a/app/api/agent-workflow/reference-search/route.ts +++ b/app/api/agent-workflow/reference-search/route.ts @@ -17,7 +17,7 @@ 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(["detail", "sku"]), + target: z.enum(["main", "detail", "sku"]), }); type ExternalTaskDetailCandidate = { @@ -116,17 +116,23 @@ function normalizeCandidate( function getCandidateTargetUrls( candidate: AgentModeReferenceCandidate, - target: "detail" | "sku" + target: "main" | "detail" | "sku" ) { + if (target === "main") return candidate.mainImageUrls; return target === "detail" ? candidate.detailImageUrls : candidate.skuImageUrls; } function buildReferenceItems(params: { candidate: AgentModeReferenceCandidate; - target: "detail" | "sku"; + target: "main" | "detail" | "sku"; }): AgentDetailReplacementReferenceItem[] { const urls = getCandidateTargetUrls(params.candidate, params.target); - const targetLabel = params.target === "detail" ? "详情参考图" : "SKU参考图"; + const targetLabel = + params.target === "main" + ? "主图参考图" + : params.target === "detail" + ? "详情参考图" + : "SKU参考图"; const prefix = params.candidate.sceneName || params.candidate.taskNo || @@ -222,11 +228,15 @@ export async function GET(request: NextRequest) { success: false, error: { code: - parsed.target === "detail" + parsed.target === "main" + ? "MAIN_REFERENCE_NOT_FOUND" + : parsed.target === "detail" ? "DETAIL_REFERENCE_NOT_FOUND" : "SKU_REFERENCE_NOT_FOUND", message: - parsed.target === "detail" + parsed.target === "main" + ? "当前整套素材中没有可用的主图参考图" + : parsed.target === "detail" ? "当前整套素材中没有可用的详情参考图" : "当前整套素材中没有可用的 SKU 参考图", }, diff --git a/components/agents/AgentGenerationWorkspaceDarkV2.tsx b/components/agents/AgentGenerationWorkspaceDarkV2.tsx index 1acf39abd..6822919be 100644 --- a/components/agents/AgentGenerationWorkspaceDarkV2.tsx +++ b/components/agents/AgentGenerationWorkspaceDarkV2.tsx @@ -54,6 +54,7 @@ import type { AgentModeReferenceCandidate, AgentModeWorkspaceState, AgentPreset, + AgentTaskDetailReferenceSource, AgentWorkflowShowcaseConfig, AgentWorkflowComposerContext, AgentWorkflowMaterialQueryType, @@ -107,7 +108,7 @@ interface AgentModeReferenceSearchResponse { data?: { query: string; queryType: AgentWorkflowMaterialQueryType; - target: "detail" | "sku"; + target: AgentTaskDetailReferenceSource; matchedSpu?: string | null; matchedSku?: string | null; categoryName?: string | null; @@ -123,6 +124,10 @@ interface AgentModeReferenceSearchResponse { }; } +function getTaskDetailReferenceSourceLabel(source: AgentTaskDetailReferenceSource) { + return source === "main" ? "主图" : source === "sku" ? "SKU图" : "详情图"; +} + function areImageGenerationParamsEqual( left: ImageGenerationParams, right: ImageGenerationParams @@ -237,15 +242,20 @@ function getImageDimensions(url: string): Promise<{ function buildReferenceCardsFromCandidate(params: { candidate: AgentModeReferenceCandidate | null | undefined; - target: "detail" | "sku"; + target: AgentTaskDetailReferenceSource; productCount?: number; + assignProductIndex?: boolean; }) { const candidate = params.candidate; if (!candidate) return [] as AgentModeWorkspaceState["detailReplacement"]["references"]; const urls = - params.target === "detail" ? candidate.detailImageUrls || [] : candidate.skuImageUrls || []; - const targetLabel = params.target === "detail" ? "详情参考图" : "SKU参考图"; + params.target === "main" + ? candidate.mainImageUrls || [] + : params.target === "detail" + ? candidate.detailImageUrls || [] + : candidate.skuImageUrls || []; + const targetLabel = `${getTaskDetailReferenceSourceLabel(params.target)}参考图`; const prefix = candidate.sceneName || candidate.taskNo || candidate.productModel || "候选整套"; @@ -256,17 +266,19 @@ function buildReferenceCardsFromCandidate(params: { candidateId: candidate.id, sortOrder: index, assignedProductIndex: - params.target === "sku" && (params.productCount || 0) > 0 + params.assignProductIndex === true && (params.productCount || 0) > 0 ? Math.min(index, Math.max((params.productCount || 1) - 1, 0)) : null, })); } -function formatCandidateMeta(candidate: AgentModeReferenceCandidate, target: "detail" | "sku") { +function formatCandidateMeta(candidate: AgentModeReferenceCandidate, target: AgentTaskDetailReferenceSource) { const imageCount = - target === "detail" - ? candidate.detailImageUrls.length - : candidate.skuImageUrls.length; + target === "main" + ? candidate.mainImageUrls.length + : target === "detail" + ? candidate.detailImageUrls.length + : candidate.skuImageUrls.length; const uploadedAt = candidate.uploadedAt ? new Date(candidate.uploadedAt) : null; const timeText = uploadedAt && !Number.isNaN(uploadedAt.getTime()) @@ -289,10 +301,14 @@ function formatCandidateMeta(candidate: AgentModeReferenceCandidate, target: "de function getCandidatePreviewUrl( candidate: AgentModeReferenceCandidate, - target: "detail" | "sku" + target: AgentTaskDetailReferenceSource ) { const targetUrls = - target === "detail" ? candidate.detailImageUrls || [] : candidate.skuImageUrls || []; + target === "main" + ? candidate.mainImageUrls || [] + : target === "detail" + ? candidate.detailImageUrls || [] + : candidate.skuImageUrls || []; return ( targetUrls[0] || candidate.mainImageUrls[0] || @@ -301,6 +317,17 @@ function getCandidatePreviewUrl( ); } +function getCandidateTargetCount( + candidate: AgentModeReferenceCandidate, + target: AgentTaskDetailReferenceSource +) { + return target === "main" + ? candidate.mainImageUrls.length + : target === "detail" + ? candidate.detailImageUrls.length + : candidate.skuImageUrls.length; +} + type ReplacementCardStatus = "waiting" | "queued" | "generating" | "completed" | "missing"; function getReplacementCardStatus(params: { @@ -535,6 +562,10 @@ export function AgentGenerationWorkspaceDarkV2({ fallback: modelSlotEnabled ? 3 : productSlotCount, }) : 0; + const detailReferenceTarget = presetModeConfig.detail.referenceImageSource; + const skuReferenceTarget = presetModeConfig.sku.referenceImageSource; + const detailReferenceSourceLabel = getTaskDetailReferenceSourceLabel(detailReferenceTarget); + const skuReferenceSourceLabel = getTaskDetailReferenceSourceLabel(skuReferenceTarget); const shouldAllowModelOverride = !isSceneGenerationPreset || presetModeConfig.scene.allowModelOverride; const selectedProductAttachments = attachments.filter((item, index) => modelSlotIndex >= 0 ? index !== modelSlotIndex && Boolean(item) : Boolean(item) @@ -1274,11 +1305,11 @@ export function AgentGenerationWorkspaceDarkV2({ return; } if (isDetailReplacementPreset && detailReplacementReferenceCards.length === 0) { - toast.error("请先查询并选择详情参考图"); + toast.error(`请先查询并选择${detailReferenceSourceLabel}参考图`); return; } if (isSkuReplacementPreset && skuReplacementReferenceCards.length === 0) { - toast.error("请先查询并选择 SKU 参考图"); + toast.error(`请先查询并选择${skuReferenceSourceLabel}参考图`); return; } const uploadedAttachmentCount = attachments.filter(Boolean).length; @@ -1431,7 +1462,7 @@ export function AgentGenerationWorkspaceDarkV2({ const handleDetailReplacementSearch = useCallback(async () => { const normalized = detailReplacementQueryInput.trim().toUpperCase(); if (!normalized) { - toast.error("请输入 SPU 后再查询详情图"); + toast.error(`请输入 SPU 后再查询${detailReferenceSourceLabel}`); return; } @@ -1444,13 +1475,13 @@ export function AgentGenerationWorkspaceDarkV2({ const response = await fetch( `/api/agent-workflow/reference-search?keyword=${encodeURIComponent( normalized - )}&queryType=spu&target=detail`, + )}&queryType=spu&target=${detailReferenceTarget}`, { cache: "no-store" } ); const result = (await response.json().catch(() => ({}))) as AgentModeReferenceSearchResponse; if (!response.ok || !result?.success || !result?.data) { - throw new Error(result?.error?.message || "详情参考图查询失败"); + throw new Error(result?.error?.message || `${detailReferenceSourceLabel}参考图查询失败`); } updateDetailReplacementState({ @@ -1465,15 +1496,15 @@ export function AgentGenerationWorkspaceDarkV2({ isLoading: false, lastQuery: normalized, }); - toast.success(`已载入 ${result.data.references?.length || 0} 张详情参考图`); + toast.success(`已载入 ${result.data.references?.length || 0} 张${detailReferenceSourceLabel}参考图`); } catch (error) { setDetailReplacementSearchState({ isLoading: false, lastQuery: normalized, }); - toast.error(error instanceof Error ? error.message : "详情参考图查询失败"); + toast.error(error instanceof Error ? error.message : `${detailReferenceSourceLabel}参考图查询失败`); } - }, [detailReplacementQueryInput, updateDetailReplacementState]); + }, [detailReferenceSourceLabel, detailReferenceTarget, detailReplacementQueryInput, updateDetailReplacementState]); const handleSelectDetailReplacementCandidate = useCallback((candidateId: string) => { updateDetailReplacementState((current) => { const nextCandidate = @@ -1483,12 +1514,12 @@ export function AgentGenerationWorkspaceDarkV2({ selectedCandidateId: candidateId, references: buildReferenceCardsFromCandidate({ candidate: nextCandidate, - target: "detail", + target: detailReferenceTarget, productCount: selectedProductAttachments.length, }), }; }); - }, [selectedProductAttachments.length, updateDetailReplacementState]); + }, [detailReferenceTarget, selectedProductAttachments.length, updateDetailReplacementState]); const handleRemoveDetailReferenceCard = useCallback((cardId: string) => { updateDetailReplacementState((current) => ({ ...current, @@ -1506,7 +1537,7 @@ export function AgentGenerationWorkspaceDarkV2({ const handleSkuReplacementSearch = useCallback(async () => { const normalized = skuReplacementQueryInput.trim().toUpperCase(); if (!normalized) { - toast.error(`请输入${skuReplacementQueryType === "spu" ? "SPU" : "SKU"}后再查询 SKU 参考图`); + toast.error(`请输入${skuReplacementQueryType === "spu" ? "SPU" : "SKU"}后再查询${skuReferenceSourceLabel}参考图`); return; } @@ -1519,13 +1550,13 @@ export function AgentGenerationWorkspaceDarkV2({ const response = await fetch( `/api/agent-workflow/reference-search?keyword=${encodeURIComponent( normalized - )}&queryType=${skuReplacementQueryType}&target=sku`, + )}&queryType=${skuReplacementQueryType}&target=${skuReferenceTarget}`, { cache: "no-store" } ); const result = (await response.json().catch(() => ({}))) as AgentModeReferenceSearchResponse; if (!response.ok || !result?.success || !result?.data) { - throw new Error(result?.error?.message || "SKU 参考图查询失败"); + throw new Error(result?.error?.message || `${skuReferenceSourceLabel}参考图查询失败`); } updateSkuReplacementState({ @@ -1547,16 +1578,18 @@ export function AgentGenerationWorkspaceDarkV2({ isLoading: false, lastQuery: normalized, }); - toast.success(`已载入 ${result.data.references?.length || 0} 张 SKU 参考图`); + toast.success(`已载入 ${result.data.references?.length || 0} 张${skuReferenceSourceLabel}参考图`); } catch (error) { setSkuReplacementSearchState({ isLoading: false, lastQuery: normalized, }); - toast.error(error instanceof Error ? error.message : "SKU 参考图查询失败"); + toast.error(error instanceof Error ? error.message : `${skuReferenceSourceLabel}参考图查询失败`); } }, [ selectedProductAttachments.length, + skuReferenceSourceLabel, + skuReferenceTarget, skuReplacementQueryInput, skuReplacementQueryType, updateSkuReplacementState, @@ -1570,12 +1603,13 @@ export function AgentGenerationWorkspaceDarkV2({ selectedCandidateId: candidateId, references: buildReferenceCardsFromCandidate({ candidate: nextCandidate, - target: "sku", + target: skuReferenceTarget, productCount: selectedProductAttachments.length, + assignProductIndex: true, }), }; }); - }, [selectedProductAttachments.length, updateSkuReplacementState]); + }, [selectedProductAttachments.length, skuReferenceTarget, updateSkuReplacementState]); const handleAssignSkuReferenceProduct = useCallback((cardId: string, productIndex: number) => { updateSkuReplacementState((current) => ({ ...current, @@ -2191,9 +2225,9 @@ export function AgentGenerationWorkspaceDarkV2({