diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index fef9f9697..1ffd3f424 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -6135,6 +6135,19 @@ export async function POST(request: Request) { const replacementSourceType = isDetailReplacementPreset ? "detail_replacement" : "sku_replacement"; + console.info("[Chat API] Replacement reference decision:", { + mode: replacementSourceType, + referenceIndex: referenceIndex + 1, + totalReferences: replacementReferences.length, + referenceLabel: reference.label, + referenceImageUrl: reference.imageUrl, + pairedProductIndex: + typeof pairedProductIndex === "number" ? pairedProductIndex + 1 : null, + shouldReplace: referenceDecision.shouldReplace, + reason: referenceDecision.reason, + detectionModelName: referenceDecision.detectionModelName, + detectionSource: referenceDecision.detectionSource, + }); const referenceSourceImageUrls = Array.from( new Set([ ...sharedUploadedImageUrls, @@ -6144,6 +6157,13 @@ export async function POST(request: Request) { ); if (!referenceDecision.shouldReplace) { + console.info("[Chat API] Replacement reference skipped and reused:", { + mode: replacementSourceType, + referenceIndex: referenceIndex + 1, + totalReferences: replacementReferences.length, + referenceLabel: reference.label, + reason: referenceDecision.reason, + }); await emitReplacementResultMessage({ reference, bodyText: referenceDecision.reason, @@ -6185,6 +6205,15 @@ export async function POST(request: Request) { hasModelImage: Boolean(modelInputImageUrl), pairedProductIndex, }); + console.info("[Chat API] Replacement image generation started:", { + mode: replacementSourceType, + referenceIndex: referenceIndex + 1, + totalReferences: replacementReferences.length, + referenceLabel: reference.label, + pairedProductIndex: + typeof pairedProductIndex === "number" ? pairedProductIndex + 1 : null, + sourceImageCount: sourceImageUrls.length, + }); try { const result = await generateCanvasImage({ @@ -6204,6 +6233,14 @@ export async function POST(request: Request) { if (!firstImage) { throw new Error("未返回结果图片"); } + console.info("[Chat API] Replacement image generation completed:", { + mode: replacementSourceType, + referenceIndex: referenceIndex + 1, + totalReferences: replacementReferences.length, + referenceLabel: reference.label, + modelDisplayName: resolvedModelDisplayName, + resultImageUrl: firstImage.originalUrl, + }); await emitReplacementResultMessage({ reference, image: firstImage, diff --git a/app/api/agent-workflow/reference-search/route.ts b/app/api/agent-workflow/reference-search/route.ts index 5dfd10070..c0f42abc0 100644 --- a/app/api/agent-workflow/reference-search/route.ts +++ b/app/api/agent-workflow/reference-search/route.ts @@ -130,11 +130,22 @@ function getCandidateTargetUrls( return target === "detail" ? candidate.detailImageUrls : candidate.skuImageUrls; } +function getRenderableCandidateTargetUrls( + candidate: AgentModeReferenceCandidate, + target: "main" | "detail" | "sku" +) { + const urls = getCandidateTargetUrls(candidate, target); + if (target === "detail" && urls.length > 1) { + return urls.slice(1); + } + return urls; +} + function buildReferenceItems(params: { candidate: AgentModeReferenceCandidate; target: "main" | "detail" | "sku"; }): AgentDetailReplacementReferenceItem[] { - const urls = getCandidateTargetUrls(params.candidate, params.target); + const urls = getRenderableCandidateTargetUrls(params.candidate, params.target); const targetLabel = params.target === "main" ? "主图参考图" @@ -252,7 +263,7 @@ export async function GET(request: NextRequest) { (upstreamResult.data.candidates || []).map((item, index) => normalizeCandidate(item, index)) ) .filter((candidate) => isCandidateMatchedStore(candidate, requestedStoreName)) - .filter((candidate) => getCandidateTargetUrls(candidate, parsed.target).length > 0); + .filter((candidate) => getRenderableCandidateTargetUrls(candidate, parsed.target).length > 0); if (normalizedCandidates.length === 0) { return NextResponse.json( diff --git a/components/agents/AgentGenerationWorkspaceDarkV2.tsx b/components/agents/AgentGenerationWorkspaceDarkV2.tsx index ad57e5137..c059bfa9a 100644 --- a/components/agents/AgentGenerationWorkspaceDarkV2.tsx +++ b/components/agents/AgentGenerationWorkspaceDarkV2.tsx @@ -249,12 +249,14 @@ function buildReferenceCardsFromCandidate(params: { const candidate = params.candidate; if (!candidate) return [] as AgentModeWorkspaceState["detailReplacement"]["references"]; - const urls = + const rawUrls = params.target === "main" ? candidate.mainImageUrls || [] : params.target === "detail" ? candidate.detailImageUrls || [] : candidate.skuImageUrls || []; + const urls = + params.target === "detail" && rawUrls.length > 1 ? rawUrls.slice(1) : rawUrls; const targetLabel = `${getTaskDetailReferenceSourceLabel(params.target)}参考图`; const prefix = candidate.sceneName || candidate.taskNo || candidate.productModel || "候选整套"; @@ -273,12 +275,13 @@ function buildReferenceCardsFromCandidate(params: { } function formatCandidateMeta(candidate: AgentModeReferenceCandidate, target: AgentTaskDetailReferenceSource) { - const imageCount = + const rawImageCount = target === "main" ? candidate.mainImageUrls.length : target === "detail" ? candidate.detailImageUrls.length : candidate.skuImageUrls.length; + const imageCount = target === "detail" && rawImageCount > 1 ? rawImageCount - 1 : rawImageCount; const uploadedAt = candidate.uploadedAt ? new Date(candidate.uploadedAt) : null; const timeText = uploadedAt && !Number.isNaN(uploadedAt.getTime()) @@ -303,12 +306,14 @@ function getCandidatePreviewUrl( candidate: AgentModeReferenceCandidate, target: AgentTaskDetailReferenceSource ) { - const targetUrls = + const rawTargetUrls = target === "main" ? candidate.mainImageUrls || [] : target === "detail" ? candidate.detailImageUrls || [] : candidate.skuImageUrls || []; + const targetUrls = + target === "detail" && rawTargetUrls.length > 1 ? rawTargetUrls.slice(1) : rawTargetUrls; return ( targetUrls[0] || candidate.mainImageUrls[0] || @@ -321,23 +326,56 @@ function getCandidateTargetCount( candidate: AgentModeReferenceCandidate, target: AgentTaskDetailReferenceSource ) { - return target === "main" - ? candidate.mainImageUrls.length - : target === "detail" - ? candidate.detailImageUrls.length - : candidate.skuImageUrls.length; + const rawCount = + target === "main" + ? candidate.mainImageUrls.length + : target === "detail" + ? candidate.detailImageUrls.length + : candidate.skuImageUrls.length; + return target === "detail" && rawCount > 1 ? rawCount - 1 : rawCount; } -type ReplacementCardStatus = "waiting" | "queued" | "generating" | "completed" | "missing"; +type ReplacementCardStatus = + | "waiting" + | "queued" + | "generating" + | "completed" + | "passthrough" + | "fallback" + | "missing"; + +function inferReplacementCardStatusFromText(text: string | null | undefined): ReplacementCardStatus | null { + const normalized = normalizeAssistantText(String(text || "")) + .replace(/\s+/g, " ") + .trim(); + if (!normalized) { + return null; + } + + if (normalized.includes("生成失败,当前先沿用参考图")) { + return "fallback"; + } + + if ( + normalized.includes("沿用参考图") || + normalized.includes("未检测到可替换的家具主体") || + normalized.includes("视觉判断失败") + ) { + return "passthrough"; + } + + return "completed"; +} function getReplacementCardStatus(params: { index: number; resultUrl: string | null; + resultText?: string | null; generatedCount: number; isGenerating: boolean; }) : ReplacementCardStatus { if (params.resultUrl) { - return "completed"; + return inferReplacementCardStatusFromText(params.resultText) || "completed"; } if (params.isGenerating) { return params.index === params.generatedCount ? "generating" : "queued"; @@ -350,6 +388,16 @@ function getReplacementCardStatus(params: { function getReplacementCardStatusMeta(status: ReplacementCardStatus) { switch (status) { + case "passthrough": + return { + label: "沿用参考图", + className: "border-cyan-400/30 bg-cyan-500/[0.10] text-cyan-200", + }; + case "fallback": + return { + label: "失败已沿用", + className: "border-amber-400/30 bg-amber-500/[0.10] text-amber-200", + }; case "completed": return { label: "已生成", @@ -619,7 +667,7 @@ export function AgentGenerationWorkspaceDarkV2({ const hasMaterialStep = Boolean(materialSelectionPanel); const hasWorkflowStyleStep = Boolean(workflowStylePanel) || hasBrandModelStep; - const { messages, setMessages, sendMessage, status } = useChat({ + const { messages, setMessages, sendMessage, status, stop } = useChat({ id: chatId, messages: historyMessages, experimental_throttle: 100, @@ -871,48 +919,78 @@ export function AgentGenerationWorkspaceDarkV2({ } return generatedGroups[generatedGroups.length - 1] || null; }, [generatedGroups, messages]); + const latestReplacementResults = useMemo( + () => + replacementGeneratedGroupsAfterLastUser.map((group) => ({ + url: group.imageUrls[0] || null, + text: group.text || "", + })), + [replacementGeneratedGroupsAfterLastUser] + ); const latestReplacementResultUrls = useMemo( () => - replacementGeneratedGroupsAfterLastUser.flatMap((group) => group.imageUrls), - [replacementGeneratedGroupsAfterLastUser] + latestReplacementResults + .map((item) => item.url) + .filter((url): url is string => Boolean(url)), + [latestReplacementResults] ); const displayedResultUrl = selectedResultUrl || latestGeneratedGroup?.imageUrls[0] || null; const isGenerating = status === "streaming" || status === "submitted"; const detailResultCards = useMemo( () => detailReplacementReferenceCards.map((card, index) => { - const resultUrl = latestReplacementResultUrls[index] || null; + const resultMeta = latestReplacementResults[index]; + const resultUrl = resultMeta?.url || null; return { ...card, index, resultUrl, + resultText: resultMeta?.text || "", status: getReplacementCardStatus({ index, resultUrl, - generatedCount: latestReplacementResultUrls.length, + resultText: resultMeta?.text || "", + generatedCount: latestReplacementResults.length, isGenerating, }), }; }), - [detailReplacementReferenceCards, isGenerating, latestReplacementResultUrls] + [detailReplacementReferenceCards, isGenerating, latestReplacementResults] ); const skuResultCards = useMemo( () => skuReplacementReferenceCards.map((card, index) => { - const resultUrl = latestReplacementResultUrls[index] || null; + const resultMeta = latestReplacementResults[index]; + const resultUrl = resultMeta?.url || null; return { ...card, index, resultUrl, + resultText: resultMeta?.text || "", status: getReplacementCardStatus({ index, resultUrl, - generatedCount: latestReplacementResultUrls.length, + resultText: resultMeta?.text || "", + generatedCount: latestReplacementResults.length, isGenerating, }), }; }), - [isGenerating, latestReplacementResultUrls, skuReplacementReferenceCards] + [isGenerating, latestReplacementResults, skuReplacementReferenceCards] + ); + const detailProcessedCount = useMemo( + () => + detailResultCards.filter((card) => + ["completed", "passthrough", "fallback"].includes(card.status) + ).length, + [detailResultCards] + ); + const skuProcessedCount = useMemo( + () => + skuResultCards.filter((card) => + ["completed", "passthrough", "fallback"].includes(card.status) + ).length, + [skuResultCards] ); const sceneResultCardCount = Math.max( allGeneratedImages.length, @@ -1376,6 +1454,11 @@ export function AgentGenerationWorkspaceDarkV2({ workflowContext?.promptText, workflowContext?.summaryText, ]); + const handleStopGeneration = useCallback(() => { + stop(); + setMessages((current) => current); + toast.info("已停止当前生成任务"); + }, [setMessages, stop]); const handleDownloadOriginalImage = useCallback(async (imageUrl: string) => { const raw = String(imageUrl || "").trim(); @@ -2088,34 +2171,47 @@ export function AgentGenerationWorkspaceDarkV2({ imageGenConfig={currentModel?.imageGenConfig || undefined} /> - {isGenerating ? ( - <> - - {isReplacementPreset ? "批量替换中..." : "生成中..."} - - ) : ( - <> - - {isReplacementPreset ? "开始批量替换" : "开始生成"} - - )} - + + ) : null} + {slotRequirementMessage ? (
{slotRequirementMessage} @@ -2213,7 +2309,7 @@ export function AgentGenerationWorkspaceDarkV2({
{isDetailReplacementPreset ? ( -
+
@@ -2422,7 +2518,7 @@ export function AgentGenerationWorkspaceDarkV2({
) : null} -
+
{detailReplacementReferenceCards.length > 0 ? ( detailReplacementReferenceCards.map((card, index) => ( (() => { @@ -2496,11 +2592,11 @@ export function AgentGenerationWorkspaceDarkV2({
- 已接通 + 已处理 {detailProcessedCount}/{detailResultCards.length || 0}
-
+
{detailResultCards.length > 0 ? ( detailResultCards.map((card) => ( (() => { @@ -2577,7 +2673,11 @@ export function AgentGenerationWorkspaceDarkV2({ )}
{card.resultUrl - ? "已生成,可继续逐屏调整文字或重新执行。" + ? card.status === "passthrough" + ? "当前参考图未检测到可替换产品,本屏直接沿用参考图展示。" + : card.status === "fallback" + ? "当前参考图替换失败,本屏先沿用参考图展示。" + : "已生成,可继续逐屏调整文字或重新执行。" : card.status === "generating" ? "当前正在生成这一屏,请稍候。" : card.status === "queued" @@ -2948,7 +3048,7 @@ export function AgentGenerationWorkspaceDarkV2({
- 已接通 + 已处理 {skuProcessedCount}/{skuResultCards.length || 0}
@@ -3022,7 +3122,11 @@ export function AgentGenerationWorkspaceDarkV2({ )}
{card.resultUrl - ? "已生成,可继续逐张检查并后续批量下载。" + ? card.status === "passthrough" + ? "当前参考图未检测到可替换产品,这一张直接沿用参考图展示。" + : card.status === "fallback" + ? "当前参考图替换失败,这一张先沿用参考图展示。" + : "已生成,可继续逐张检查并后续批量下载。" : card.status === "generating" ? "当前正在生成这一张,请稍候。" : card.status === "queued"