diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index 115329adf..c75c986d8 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -3116,12 +3116,17 @@ function buildAgentSlotPrompt(params: { function buildAgentSceneWorkflowPrompt(params: { workflowPrompt?: string | null; userSupplementPrompt?: string | null; + imageRolePrompt?: string | null; }) { const lines: string[] = []; const workflowPrompt = String(params.workflowPrompt || "").trim(); if (workflowPrompt) { lines.push(workflowPrompt); } + const imageRolePrompt = String(params.imageRolePrompt || "").trim(); + if (imageRolePrompt) { + lines.push(imageRolePrompt); + } const supplement = String(params.userSupplementPrompt || "").trim(); if (supplement) { lines.push(`【补充要求】${supplement}`); @@ -3129,6 +3134,56 @@ function buildAgentSceneWorkflowPrompt(params: { return lines.join("\n\n").trim(); } +function buildAgentSceneImageRolePrompt(params: { + uploadedImageCount: number; + productImageCount: number; + includeModelImage: boolean; +}) { + if (params.uploadedImageCount <= 0) { + return ""; + } + + const lines = ["【输入图片角色说明】"]; + const resolvedProductCount = Math.max( + 1, + Math.min(params.productImageCount, params.uploadedImageCount) + ); + const hasModelImage = + params.includeModelImage && params.uploadedImageCount > resolvedProductCount; + + for (let index = 0; index < params.uploadedImageCount; index += 1) { + if (index < resolvedProductCount) { + lines.push( + `图片${index + 1}:产品白底参考图,用于锁定产品的外观、比例、材质、颜色、结构和细节。` + ); + continue; + } + + if (hasModelImage && index === resolvedProductCount) { + lines.push( + `图片${index + 1}:固定模特图。若画面需要人物,必须复用这张图中的同一模特(脸型、发型、体型、肤色、穿搭风格保持一致),不得替换成其他人物。` + ); + continue; + } + + lines.push( + `图片${index + 1}:补充参考图,只用于补充产品角度、细节或辅助信息,不可替代主产品外观。` + ); + } + + lines.push( + `必须以图片1${resolvedProductCount > 1 ? `到图片${resolvedProductCount}` : ""}中的产品信息为最高优先级,严格保持同一产品。` + ); + lines.push("不要把多张输入图拼贴成多个主体,也不要把所有输入图元素强行同时融入结果。"); + lines.push( + hasModelImage + ? `如果当前场景有人物展示需求,只允许使用图片${resolvedProductCount + 1}里的模特;如果不需要人物,则不要强行加入人物。` + : "当前没有固定模特图,不要凭空生成强人物主体。" + ); + + return lines.join("\n"); +} + type AgentReplacementModePayload = NonNullable; type ReplacementReferenceDecision = { @@ -6554,6 +6609,12 @@ export async function POST(request: Request) { const isSkuReplacementPreset = selectedAgentPreset.type === "image" && presetModeConfig.modeType === "sku_replacement"; + const sceneProductSlotCount = isSceneGenerationPreset + ? Math.max(1, presetModeConfig.scene.productImageSlotCount || 1) + : 0; + const sceneModelSlotEnabled = isSceneGenerationPreset + ? presetModeConfig.scene.includeModelSlot + : false; const replacementProductSlotCount = isDetailReplacementPreset ? presetModeConfig.detail.productImageSlotCount : isSkuReplacementPreset @@ -7221,9 +7282,15 @@ export async function POST(request: Request) { [...sharedUploadedAssetIds, ...(sceneSlot?.referenceAssetIds || [])].filter(Boolean), ), ); + const sceneImageRolePrompt = buildAgentSceneImageRolePrompt({ + uploadedImageCount: sharedUploadedImageUrls.length, + productImageCount: sceneProductSlotCount, + includeModelImage: sceneModelSlotEnabled, + }); const scenePrompt = buildAgentSceneWorkflowPrompt({ workflowPrompt: hiddenPromptText || null, + imageRolePrompt: sceneImageRolePrompt, userSupplementPrompt: sanitizedUserSupplementPrompt || null, }) || buildAgentSlotPrompt({ diff --git a/app/canvas/[id]/page.tsx b/app/canvas/[id]/page.tsx index e8a26e3be..02658ed1f 100644 --- a/app/canvas/[id]/page.tsx +++ b/app/canvas/[id]/page.tsx @@ -327,6 +327,10 @@ export default function CanvasEditorPage({ params }: PageProps) { >({}); const [selectedBrandModel, setSelectedBrandModel] = useState(null); + const workflowInputsRef = useRef(workflowInputs); + const workflowMaterialsRef = useRef(workflowMaterials); + const workflowSelectionsRef = useRef(workflowSelections); + const selectedBrandModelRef = useRef(selectedBrandModel); const [selectedAgentPresetId, setSelectedAgentPresetId] = useState(null); const [currentAgentStage, setCurrentAgentStage] = useState<"workflow" | "mode">("workflow"); const [agentModeState, setAgentModeState] = useState( @@ -1707,6 +1711,49 @@ export default function CanvasEditorPage({ params }: PageProps) { workflowMaterials, workflowSelections, ]); + useEffect(() => { + workflowInputsRef.current = workflowInputs; + }, [workflowInputs]); + useEffect(() => { + workflowMaterialsRef.current = workflowMaterials; + }, [workflowMaterials]); + useEffect(() => { + workflowSelectionsRef.current = workflowSelections; + }, [workflowSelections]); + useEffect(() => { + selectedBrandModelRef.current = selectedBrandModel; + }, [selectedBrandModel]); + const handleWorkflowInputsChange = useCallback((next: AgentWorkflowInputs) => { + workflowInputsRef.current = next; + setWorkflowInputs(next); + }, []); + const handleWorkflowMaterialsChange = useCallback((next: AgentWorkflowMaterialSelections) => { + workflowMaterialsRef.current = next; + setWorkflowMaterials(next); + }, []); + const handleWorkflowSelectionsChange = useCallback( + (next: Partial>) => { + workflowSelectionsRef.current = next; + setWorkflowSelections(next); + }, + [] + ); + const handleSelectedBrandModelChange = useCallback( + (next: AgentWorkflowSelectedBrandModel | null) => { + selectedBrandModelRef.current = next; + setSelectedBrandModel(next); + }, + [] + ); + const getLatestWorkflowContext = useCallback(() => { + return buildAgentWorkflowComposerContext({ + config: resolvedWorkflowUiConfig || defaultAgentWorkflowUiConfig, + inputs: workflowInputsRef.current, + materials: workflowMaterialsRef.current, + selectedBrandModel: selectedBrandModelRef.current, + selections: workflowSelectionsRef.current, + }); + }, [resolvedWorkflowUiConfig]); const { data: selectedAgentPresetResponse } = useSWR<{ success?: boolean; data?: AgentPreset; @@ -1896,6 +1943,7 @@ export default function CanvasEditorPage({ params }: PageProps) { selectedAgentPresetId={isAgentWorkspaceEnabled ? selectedAgentPresetId : null} selectedBrandModel={selectedBrandModel} workflowContext={workflowContext} + getLatestWorkflowContext={getLatestWorkflowContext} agentModeState={agentModeState} onAgentModeStateChange={setAgentModeState} workspaceState={agentWorkspaceState} @@ -1920,7 +1968,7 @@ export default function CanvasEditorPage({ params }: PageProps) { } @@ -1929,11 +1977,11 @@ export default function CanvasEditorPage({ params }: PageProps) { embedded config={resolvedWorkflowUiConfig} inputs={workflowInputs} - onInputsChange={setWorkflowInputs} + onInputsChange={handleWorkflowInputsChange} materials={workflowMaterials} - onMaterialsChange={setWorkflowMaterials} + onMaterialsChange={handleWorkflowMaterialsChange} selections={workflowSelections} - onSelectionsChange={setWorkflowSelections} + onSelectionsChange={handleWorkflowSelectionsChange} showBrandStoreSection={false} /> } @@ -1950,16 +1998,16 @@ export default function CanvasEditorPage({ params }: PageProps) { } materialSelectionPanel={ AgentWorkflowComposerContext | null; onBack?: () => void; presetPanel?: ReactNode; brandStorePanel?: ReactNode; @@ -850,6 +851,7 @@ export function AgentGenerationWorkspaceDarkV2({ selectedAgentPresetId, selectedBrandModel = null, workflowContext = null, + getLatestWorkflowContext, onBack, presetPanel, brandStorePanel, @@ -2457,9 +2459,10 @@ export function AgentGenerationWorkspaceDarkV2({ if (!attachment) return; parts.push({ type: "file", mediaType: attachment.contentType || "image/jpeg", name: attachment.name || "参考图", url: attachment.url, assetId: attachment.assetId }); }); + const resolvedWorkflowContext = getLatestWorkflowContext?.() ?? workflowContext; let textContent = input.trim(); - if (!textContent && selectedAgentPreset?.type === "text" && workflowContext?.summaryText) { - textContent = `请基于当前工作流设定生成方案:${workflowContext.summaryText}`; + if (!textContent && selectedAgentPreset?.type === "text" && resolvedWorkflowContext?.summaryText) { + textContent = `请基于当前工作流设定生成方案:${resolvedWorkflowContext.summaryText}`; } if (!textContent && selectedAgentPreset?.type === "text" && selectedAgentPreset) { textContent = `请按「${selectedAgentPreset.name}」输出内容`; @@ -2471,8 +2474,8 @@ export function AgentGenerationWorkspaceDarkV2({ toast.error("请至少补充要求或上传图片"); return; } - const nextHiddenPrompt = workflowContext?.promptText - ? `【工作流设定】\n${workflowContext.promptText}\n\n请严格遵循以上工作流设定输出内容,并保持整体结果一致。` + const nextHiddenPrompt = resolvedWorkflowContext?.promptText + ? `【工作流设定】\n${resolvedWorkflowContext.promptText}\n\n请严格遵循以上工作流设定输出内容,并保持整体结果一致。` : null; hiddenPromptRef.current = nextHiddenPrompt; generationRequestSnapshotRef.current = { @@ -2522,8 +2525,8 @@ export function AgentGenerationWorkspaceDarkV2({ setPendingGeneration, skuReplacementReferenceCards.length, userMessageCount, - workflowContext?.promptText, - workflowContext?.summaryText, + getLatestWorkflowContext, + workflowContext, ]); const handleStopGeneration = useCallback(() => { detailGenerationCancelRef.current = true; diff --git a/components/agents/AgentWorkflowMaterialPicker.tsx b/components/agents/AgentWorkflowMaterialPicker.tsx index d658f0c3f..a4d77d2bf 100644 --- a/components/agents/AgentWorkflowMaterialPicker.tsx +++ b/components/agents/AgentWorkflowMaterialPicker.tsx @@ -110,6 +110,12 @@ export function AgentWorkflowMaterialPicker({ value: result?.goodsInfo?.boardType || null, }, ].filter((item) => Boolean(item.value)); + const goodsInfoMissingHint = + result?.queryType === "sku" && + Boolean(String(result?.matchedSku || result?.query || "").trim()) && + goodsInfoItems.length === 0 + ? `当前 SKU「${result?.matchedSku || result?.query}」暂未从商品信息接口匹配到商品名称、尺寸或材质信息。` + : ""; const remainingSelectableCount = resolvedSelectionLimit === null ? null : Math.max(resolvedSelectionLimit - selectedCount, 0); const totalImageCount = result?.images?.length || 0; @@ -125,6 +131,16 @@ export function AgentWorkflowMaterialPicker({ const title = result?.goodsInfo?.productName || result?.matchedSku || result?.query || "当前SKU"; return `${title} · 系统已自动选中 ${selectedCount} 张图`; }, [result?.goodsInfo?.productName, result?.matchedSku, result?.query, selectedCount]); + const collapsedGoodsInfoSummary = useMemo( + () => + [ + result?.goodsInfo?.sizeInfo ? `尺寸:${result.goodsInfo.sizeInfo}` : "", + result?.goodsInfo?.materialInfo ? `材质:${result.goodsInfo.materialInfo}` : "", + ] + .filter(Boolean) + .join(" · "), + [result?.goodsInfo?.materialInfo, result?.goodsInfo?.sizeInfo] + ); const visibleImages = useMemo(() => { if (!result?.images?.length) { return []; @@ -323,6 +339,11 @@ export function AgentWorkflowMaterialPicker({
默认先替你收起结果区,需要检查商品信息、已选图片或手动微调时再展开查看。
+ {collapsedGoodsInfoSummary ? ( +
+ {collapsedGoodsInfoSummary} +
+ ) : null}