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) => ({