diff --git a/app/api/agent-workflow/material-upload/route.ts b/app/api/agent-workflow/material-upload/route.ts index c2b65740b..5de23516e 100644 --- a/app/api/agent-workflow/material-upload/route.ts +++ b/app/api/agent-workflow/material-upload/route.ts @@ -17,6 +17,15 @@ const payloadSchema = z.object({ productModel: z.string().trim().max(128).optional(), }); +function getOptionalFormString(value: FormDataEntryValue | null) { + if (typeof value !== "string") { + return undefined; + } + + const normalizedValue = value.trim(); + return normalizedValue.length > 0 ? normalizedValue : undefined; +} + function isUploadLikeFile(value: FormDataEntryValue | null) { return ( Boolean(value) && @@ -73,12 +82,12 @@ export async function POST(request: NextRequest) { } const payload = payloadSchema.parse({ - source: formData.get("source"), - query: formData.get("query"), - queryType: formData.get("queryType"), - matchedSku: formData.get("matchedSku"), - matchedSpu: formData.get("matchedSpu"), - productModel: formData.get("productModel"), + source: getOptionalFormString(formData.get("source")), + query: getOptionalFormString(formData.get("query")), + queryType: getOptionalFormString(formData.get("queryType")), + matchedSku: getOptionalFormString(formData.get("matchedSku")), + matchedSpu: getOptionalFormString(formData.get("matchedSpu")), + productModel: getOptionalFormString(formData.get("productModel")), }); const arrayBuffer = await file.arrayBuffer(); diff --git a/app/canvas/[id]/page.tsx b/app/canvas/[id]/page.tsx index 0bbac3957..6b23f8de4 100644 --- a/app/canvas/[id]/page.tsx +++ b/app/canvas/[id]/page.tsx @@ -1801,6 +1801,39 @@ export default function CanvasEditorPage({ params }: PageProps) { } return 2; }, [selectedAgentPresetData?.type, selectedAgentPresetModeConfig]); + const materialModelImageRequirement = useMemo(() => { + if (selectedAgentPresetData?.type !== "image") { + return 0; + } + if (selectedAgentPresetModeConfig.modeType === "scene_generation") { + return selectedAgentPresetModeConfig.scene.includeModelSlot ? 1 : 0; + } + if (selectedAgentPresetModeConfig.modeType === "detail_replacement") { + return selectedAgentPresetModeConfig.detail.includeModelSlot ? 1 : 0; + } + if (selectedAgentPresetModeConfig.modeType === "sku_replacement") { + return selectedAgentPresetModeConfig.sku.includeModelSlot ? 1 : 0; + } + return 1; + }, [selectedAgentPresetData?.type, selectedAgentPresetModeConfig]); + const materialTotalUploadRequirement = useMemo(() => { + if (selectedAgentPresetData?.type !== "image") { + return null; + } + const explicitCount = Number(selectedAgentPresetData.requiredUploadCount); + if (Number.isFinite(explicitCount) && explicitCount > 0) { + return Math.max( + materialProductSelectionLimit || 0, + Math.max(1, Math.round(explicitCount)) + ); + } + return (materialProductSelectionLimit || 0) + materialModelImageRequirement; + }, [ + materialModelImageRequirement, + materialProductSelectionLimit, + selectedAgentPresetData?.requiredUploadCount, + selectedAgentPresetData?.type, + ]); const workflowProgress = useMemo( () => getAgentWorkflowProgress({ @@ -2091,6 +2124,8 @@ export default function CanvasEditorPage({ params }: PageProps) { materials={workflowMaterials} onMaterialsChange={handleWorkflowMaterialsChange} productSelectionLimit={materialProductSelectionLimit} + totalUploadRequirement={materialTotalUploadRequirement} + modelImageRequirement={materialModelImageRequirement} disabled={!selectedAgentPresetId} theme="dark" /> diff --git a/components/agents/AgentWorkflowMaterialPicker.tsx b/components/agents/AgentWorkflowMaterialPicker.tsx index 0887a290d..a67be75fd 100644 --- a/components/agents/AgentWorkflowMaterialPicker.tsx +++ b/components/agents/AgentWorkflowMaterialPicker.tsx @@ -426,7 +426,7 @@ export function AgentWorkflowMaterialPicker({ 直接上传商品图
- 搜索不到合适白底图时,可以直接上传原图。系统会自动抠图后再加入当前候选素材。 + 搜索不到合适白底图时,可以直接上传原图。这里只统计商品图,模特图会在后续单独处理;系统会自动抠图后再加入当前候选素材。
@@ -437,7 +437,7 @@ export function AgentWorkflowMaterialPicker({ isDark ? "bg-white/8 text-white/82" : "bg-muted text-muted-foreground" )} > - 当前模式需 {resolvedSelectionLimit} 张产品图 + 本步骤需 {resolvedSelectionLimit} 张商品图 ) : null} void; productSelectionLimit?: number | null; + totalUploadRequirement?: number | null; + modelImageRequirement?: number | null; title?: string; description?: string; disabled?: boolean; @@ -136,6 +138,8 @@ export function AgentWorkflowMaterialSelectionPanel({ materials, onMaterialsChange, productSelectionLimit = null, + totalUploadRequirement = null, + modelImageRequirement = 0, title, description, disabled = false, @@ -205,15 +209,49 @@ export function AgentWorkflowMaterialSelectionPanel({ const [isSecondaryMaterialVisible, setIsSecondaryMaterialVisible] = useState( hasSecondaryMaterialContent ); + const normalizedRawProductSelectionLimit = + typeof productSelectionLimit === "number" && Number.isFinite(productSelectionLimit) && productSelectionLimit > 0 + ? Math.max(1, Math.round(productSelectionLimit)) + : null; + const normalizedTotalUploadRequirement = + typeof totalUploadRequirement === "number" && + Number.isFinite(totalUploadRequirement) && + totalUploadRequirement > 0 + ? Math.max(1, Math.round(totalUploadRequirement)) + : null; + const normalizedModelImageRequirement = + typeof modelImageRequirement === "number" && Number.isFinite(modelImageRequirement) && modelImageRequirement > 0 + ? Math.max(0, Math.round(modelImageRequirement)) + : 0; + const normalizedDerivedProductSelectionLimit = + normalizedTotalUploadRequirement !== null + ? Math.max(normalizedTotalUploadRequirement - normalizedModelImageRequirement, 0) + : null; + const normalizedProductSelectionLimit = + normalizedRawProductSelectionLimit !== null + ? normalizedDerivedProductSelectionLimit !== null && + normalizedDerivedProductSelectionLimit > 0 && + normalizedDerivedProductSelectionLimit < normalizedRawProductSelectionLimit + ? normalizedDerivedProductSelectionLimit + : normalizedRawProductSelectionLimit + : normalizedDerivedProductSelectionLimit; + const hasAdjustedProductSelectionLimit = + normalizedRawProductSelectionLimit !== null && + normalizedProductSelectionLimit !== null && + normalizedProductSelectionLimit !== normalizedRawProductSelectionLimit; const primarySelectedCount = materials.primary.selectedImageIds.length; const secondarySelectedCount = materials.secondary.selectedImageIds.length; const primarySelectionLimit = - typeof productSelectionLimit === "number" && Number.isFinite(productSelectionLimit) && productSelectionLimit > 0 - ? Math.max(productSelectionLimit - secondarySelectedCount, primarySelectedCount, 0) + typeof normalizedProductSelectionLimit === "number" && + Number.isFinite(normalizedProductSelectionLimit) && + normalizedProductSelectionLimit > 0 + ? Math.max(normalizedProductSelectionLimit - secondarySelectedCount, primarySelectedCount, 0) : null; const secondarySelectionLimit = - typeof productSelectionLimit === "number" && Number.isFinite(productSelectionLimit) && productSelectionLimit > 0 - ? Math.max(productSelectionLimit - primarySelectedCount, secondarySelectedCount, 0) + typeof normalizedProductSelectionLimit === "number" && + Number.isFinite(normalizedProductSelectionLimit) && + normalizedProductSelectionLimit > 0 + ? Math.max(normalizedProductSelectionLimit - primarySelectedCount, secondarySelectedCount, 0) : null; useEffect(() => { @@ -480,6 +518,12 @@ export function AgentWorkflowMaterialSelectionPanel({ }); }; const isDark = theme === "dark"; + const selectionLimitLabel = + normalizedProductSelectionLimit !== null + ? normalizedModelImageRequirement > 0 + ? `当前步骤商品图上限 ${normalizedProductSelectionLimit} 张(模特图 ${normalizedModelImageRequirement} 张另算)` + : `当前步骤商品图上限 ${normalizedProductSelectionLimit} 张` + : undefined; return (
) : null} + {!disabled && + (normalizedTotalUploadRequirement !== null || + normalizedProductSelectionLimit !== null || + normalizedModelImageRequirement > 0) ? ( +
+ {[ + normalizedTotalUploadRequirement !== null + ? `当前模式共需 ${normalizedTotalUploadRequirement} 张素材` + : "", + normalizedProductSelectionLimit !== null + ? `其中本步骤商品图最多 ${normalizedProductSelectionLimit} 张` + : "", + normalizedModelImageRequirement > 0 + ? `模特图 ${normalizedModelImageRequirement} 张在后续单独带入,不占这里的商品图上传位` + : "", + hasAdjustedProductSelectionLimit + ? "已按总素材数扣除模特图后的商品图数量限制上传槽位" + : "", + ] + .filter(Boolean) + .join(",")} + 。 +
+ ) : null} + 0 - ? `当前模式产品图总上限 ${productSelectionLimit} 张` - : undefined - } + selectionLimitLabel={selectionLimitLabel} onValueChange={(value) => handleMaterialInputChange("primary", value)} onQueryModeChange={(nextMode) => setMaterialSearchMode((prev) => ({ @@ -642,11 +712,7 @@ export function AgentWorkflowMaterialSelectionPanel({ result={materials.secondary.result} selectedImageIds={materials.secondary.selectedImageIds} selectionLimit={secondarySelectionLimit} - selectionLimitLabel={ - typeof productSelectionLimit === "number" && Number.isFinite(productSelectionLimit) && productSelectionLimit > 0 - ? `当前模式产品图总上限 ${productSelectionLimit} 张` - : undefined - } + selectionLimitLabel={selectionLimitLabel} onValueChange={(value) => handleMaterialInputChange("secondary", value)} onQueryModeChange={(nextMode) => setMaterialSearchMode((prev) => ({