fix: align workflow material upload limits
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-04-02 00:24:16 +08:00
parent 01748fe501
commit e7cc0c9555
4 changed files with 132 additions and 22 deletions

View File

@ -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();

View File

@ -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"
/>

View File

@ -426,7 +426,7 @@ export function AgentWorkflowMaterialPicker({
直接上传商品图
</div>
<div className={cn("text-xs leading-5", isDark ? "text-white/58" : "text-muted-foreground")}>
搜索不到合适白底图时,可以直接上传原图。系统会自动抠图后再加入当前候选素材。
搜索不到合适白底图时,可以直接上传原图。这里只统计商品图,模特图会在后续单独处理;系统会自动抠图后再加入当前候选素材。
</div>
</div>
<div className="flex flex-wrap gap-2 text-xs">
@ -437,7 +437,7 @@ export function AgentWorkflowMaterialPicker({
isDark ? "bg-white/8 text-white/82" : "bg-muted text-muted-foreground"
)}
>
当前模式需 {resolvedSelectionLimit} 张产品图
本步骤需 {resolvedSelectionLimit} 张商品图
</span>
) : null}
<span

View File

@ -19,6 +19,8 @@ interface AgentWorkflowMaterialSelectionPanelProps {
materials: AgentWorkflowMaterialSelections;
onMaterialsChange: (next: AgentWorkflowMaterialSelections) => 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 (
<div
@ -514,6 +558,36 @@ export function AgentWorkflowMaterialSelectionPanel({
</div>
) : null}
{!disabled &&
(normalizedTotalUploadRequirement !== null ||
normalizedProductSelectionLimit !== null ||
normalizedModelImageRequirement > 0) ? (
<div
className={cn(
"rounded-[22px] border px-4 py-3 text-xs leading-6",
isDark ? "border-emerald-500/18 bg-emerald-500/[0.08] text-emerald-100" : "border-emerald-200 bg-emerald-50 text-emerald-800"
)}
>
{[
normalizedTotalUploadRequirement !== null
? `当前模式共需 ${normalizedTotalUploadRequirement} 张素材`
: "",
normalizedProductSelectionLimit !== null
? `其中本步骤商品图最多 ${normalizedProductSelectionLimit} 张`
: "",
normalizedModelImageRequirement > 0
? `模特图 ${normalizedModelImageRequirement} 张在后续单独带入,不占这里的商品图上传位`
: "",
hasAdjustedProductSelectionLimit
? "已按总素材数扣除模特图后的商品图数量限制上传槽位"
: "",
]
.filter(Boolean)
.join(",")}
。
</div>
) : null}
<AgentWorkflowMaterialPicker
value={inputs.primarySku}
queryMode={materialSearchMode.primary}
@ -531,11 +605,7 @@ export function AgentWorkflowMaterialSelectionPanel({
result={materials.primary.result}
selectedImageIds={materials.primary.selectedImageIds}
selectionLimit={primarySelectionLimit}
selectionLimitLabel={
typeof productSelectionLimit === "number" && Number.isFinite(productSelectionLimit) && productSelectionLimit > 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) => ({