From 7ac5bdf44f16bbde19cbc786ba69a1ff188850fd Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 3 Jun 2026 16:47:50 +0800 Subject: [PATCH] fix: harden detail replacement page routing --- .../api/chat/_lib/detail-replacement.ts | 525 ++++++++++++++++-- app/(chat)/api/chat/route.ts | 6 + 2 files changed, 492 insertions(+), 39 deletions(-) diff --git a/app/(chat)/api/chat/_lib/detail-replacement.ts b/app/(chat)/api/chat/_lib/detail-replacement.ts index a1f19af30..9f82da663 100644 --- a/app/(chat)/api/chat/_lib/detail-replacement.ts +++ b/app/(chat)/api/chat/_lib/detail-replacement.ts @@ -279,9 +279,6 @@ export async function normalizeReplacementResultImage(params: { generatedMetadata.width && generatedMetadata.height ? generatedMetadata.width / generatedMetadata.height : null; - const shouldCropTallComposite = - Boolean(targetAspectRatio && generatedAspectRatio) && - Number(generatedAspectRatio) < Number(targetAspectRatio) * 0.78; const shouldFillSceneCanvas = params.scenePlan?.promptPackPolicy !== "forbid" && (params.scenePlan?.generationRoute === "prompt_pack_scene" || @@ -292,6 +289,10 @@ export async function normalizeReplacementResultImage(params: { params.scenePlan?.textPolicy === "lock" || params.scenePlan?.generationRoute === "copy_locked_regenerate" || params.scenePlan?.generationRoute === "layout_preserve_replace"); + const shouldCropTallComposite = + !shouldPreserveFullGeneratedCanvas && + Boolean(targetAspectRatio && generatedAspectRatio) && + Number(generatedAspectRatio) < Number(targetAspectRatio) * 0.78; const resizeFit = shouldCropTallComposite ? "cover" : shouldPreserveFullGeneratedCanvas @@ -343,6 +344,16 @@ export type DetailReplacementPeoplePolicy = | "none" | "preserve_relation" | "require_user_model"; +export type DetailReplacementPageKind = + | "prompt_pack_scene" + | "white_scene" + | "white_layout" + | "copy_locked" + | "people_scene" + | "action_poster" + | "dimension_spec" + | "material_detail" + | "unknown"; export type NormalizedAgentModeSourceImage = { url: string; @@ -369,6 +380,7 @@ export type ReplacementReferenceDecision = { }; export type ReplacementScenePlan = { + pageKind: DetailReplacementPageKind; generationRoute: DetailReplacementGenerationRoute; promptPackPolicy: DetailReplacementPromptPackPolicy; textPolicy: DetailReplacementTextPolicy; @@ -401,10 +413,14 @@ type ReplacementReferenceDecisionCore = Omit< const DETAIL_HARD_LAYOUT_HINT_PATTERN = /产品尺寸|尺寸|规格|参数|单位|厘米|\bcm\b|产品说明|说明图|信息图|参数图|规格图|材质标注|工艺标注|结构标注|功能图标|图标说明|尺寸标注|承重参数|安装说明|标签图|卖点卡|白底产品|白底参数|白底规格/i; +const DETAIL_DIMENSION_LAYOUT_HINT_PATTERN = + /产品尺寸|尺寸图|尺寸说明|尺寸标注|尺寸线|标尺|长宽高|宽度|高度|深度|单位[::]?\s*(cm|厘米)|\bcm\b|160\s*\/\s*180\s*\/\s*210/i; const DETAIL_STRONG_COPY_HINT_PATTERN = /白底|浅色底|产品说明|参数|规格|尺寸|卖点|标签|说明图|信息图|海报|版式|排版|文字|标题|编号|标注|材质标注|工艺标注|代名词|典雅|高贵/i; +const DETAIL_REFERENCE_POSTER_ACTION_HINT_PATTERN = + /动作海报|测试海报|测试图|来张自拍|自拍|合照|多人围坐|围坐|聚餐|多人聚餐|横向受力|受力测试|单侧承重|承重测试|重心|依旧平稳|依然稳固|试坐|坐在桌|坐桌|坐上桌/i; const DETAIL_PEOPLE_ACTION_HINT_PATTERN = - /自拍|合照|拍照|拿手机|手机自拍|聚会|好友|朋友|闺蜜|夫妻|情侣|家庭|亲子|陪伴|举杯|互动|模特|人物|人像|多人|人群|男士|女士|男人|女人|男生|女生|小孩|儿童|宝宝|顾客|吃饭的人|用餐的人|正在吃饭|坐在|坐着|站在|走动|行走|手持|试坐|倚靠|承重|受力|测试|稳定|稳固|使用产品|产品使用|体验/i; + /自拍|合照|拍照|拿手机|手机自拍|聚会|聚餐|围坐|好友|朋友|闺蜜|夫妻|情侣|家庭|亲子|陪伴|举杯|互动|模特|人物|人像|多人|人群|男士|女士|男人|女人|男生|女生|小孩|儿童|宝宝|顾客|吃饭的人|用餐的人|正在吃饭|坐在|坐着|站在|走动|行走|手持|试坐|倚靠|承重|受力|受力测试|重心|测试|稳定|稳固|使用产品|产品使用|体验/i; export type ReplacementScenePlanningRuntime = | { @@ -490,6 +506,20 @@ function ensureDetailReferenceGenerates 0) { const productRangeLabel = @@ -2046,6 +2486,7 @@ export function buildAgentReplacementPrompt(params: { const scenePlanLines = params.scenePlan ? [ "【本屏场景预分析】", + `页面类型:${params.scenePlan.pageKind}`, `自动路由:${params.scenePlan.generationRoute}`, `词包策略:${params.scenePlan.promptPackPolicy}`, `文案策略:${params.scenePlan.textPolicy}`, @@ -2147,6 +2588,12 @@ export function buildAgentReplacementPrompt(params: { ...marketplaceBrandRemovalLines, "你现在做的是“详情参考图背景/场景图替换”,不是重新设计整张详情图。", "【版式锁层规则】把最后一张参考图当成 PSD 模板:文字、标题、编号、卖点标签、按钮、图标、白底/浅色底、留白、分割线、圆角图片框、图片框裁切边界都属于锁定版式层,不允许移动、缩放、重排或重新设计。", + isReferencePosterActionPlan + ? "【参考动作海报硬约束】本屏不使用前置词包;必须按照最后一张参考图做同版式类似图。若标题/标签写多人围坐、聚餐、横向受力测试、单侧承重、重心平稳等,人物数量、相对位置、围坐/试坐/承重动作、产品受力关系、产品外轮廓、桌面长宽比例、圆角、腿部结构和透视角度都必须跟参考图一致。" + : "", + isDimensionDetailPlan + ? "【产品尺寸图硬约束】本屏不使用前置词包;必须是白底/米白底产品抠图尺寸说明页。保留标题“产品尺寸”、单位 cm、尺寸线/箭头/数字标注和原版式位置;产品主体外轮廓、长宽比例、圆角、腿部结构和透视必须跟参考图一致;禁止生成室内场景、椅子、柜体、灯具、花瓶、人物或软装背景。" + : "", "如果参考图是“上方文字/标签白底区 + 下方图片/场景圆角框”的结构,必须原样保留标题区高度、文字换行、文字字号、文字左上角位置、标签位置、图片框起始高度、圆角、左右边距和上下留白;禁止把新场景铺到标题白底区背后。", "如果参考图里存在嵌入式图片框、产品照片框、人物展示框或圆角大图区域,只能在该原始框内重建背景/产品/人物画面;新画面必须被裁切在原图片框边界内,不能扩大图片框,不能覆盖文字区、标签区或外层白底。", "版式层优先级高于词包:前置词包只负责图片框内部的空间风格、灯光、软装、背景和产品场景,不授权修改页面排版、文字位置、标签样式、留白比例或视觉层级。", @@ -2175,7 +2622,7 @@ export function buildAgentReplacementPrompt(params: { params.scenePlan?.peoplePolicy === "require_user_model" ? "本屏包含人物但没有提供用户模特图:禁止把参考图中的人物身份当作用户模特迁移;只能保留人物数量、关系、动作和构图语义,不要换成不相关强人物主体。" : params.scenePlan?.peoplePolicy === "preserve_relation" - ? "本屏包含人物关系:必须保留人物数量、相对位置、互动关系、动作方向和出镜比例;如果参考图文字写了自拍/合照/聚会/用餐/拿手机等动作,必须让人物执行该动作,不要把多人图改成单人图、无人图或无关模特图。" + ? "本屏包含人物关系:必须保留人物数量、相对位置、互动关系、动作方向和出镜比例;如果参考图文字写了自拍/合照/多人围坐/聚餐/用餐/单侧承重/横向受力测试/拿手机等动作,必须让人物执行该动作,不要把多人图改成单人图、无人图或无关模特图。" : "如果参考图没有明确人物展示需求,不要凭空新增人物主体。", params.scenePlan?.whiteBackgroundLikely && shouldApplyPromptPack ? "本屏是白/浅色真实场景且词包已启用:必须让图片框内旧背景、产品、软装、灯光和空间发生清晰可见的重建;不能只微调原图或原样复刻,但也不能破坏外层白底/浅色版式和文字区。" diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index 278a9eab4..dac7594b0 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -3398,6 +3398,7 @@ export async function POST(request: Request) { reason: replacementExecutionPlan.reason, detectionSource: replacementExecutionPlan.detectionSource, scenePlanSource: replacementExecutionPlan.scenePlan.analysisSource, + pageKind: replacementExecutionPlan.scenePlan.pageKind, generationRoute: replacementExecutionPlan.scenePlan.generationRoute, promptPackPolicy: replacementExecutionPlan.scenePlan.promptPackPolicy, packPolicy: replacementExecutionPlan.scenePlan.promptPackPolicy, @@ -3453,6 +3454,7 @@ export async function POST(request: Request) { referenceIndex: referenceIndex + 1, totalReferences: replacementReferences.length, referenceLabel: reference.label, + pageKind: replacementExecutionPlan.scenePlan.pageKind, generationRoute: replacementExecutionPlan.scenePlan.generationRoute, packPolicy: replacementExecutionPlan.scenePlan.promptPackPolicy, selectedProductImages: selectedProductContextImageUrls.length, @@ -4160,6 +4162,7 @@ export async function POST(request: Request) { replacementExecutionPlan.scenePlan.productSelectionReason, useModelImage: replacementExecutionPlan.scenePlan.useModelImage, modelImageReason: replacementExecutionPlan.scenePlan.modelImageReason, + pageKind: replacementExecutionPlan.scenePlan.pageKind, generationRoute: replacementExecutionPlan.scenePlan.generationRoute, promptPackPolicy: replacementExecutionPlan.scenePlan.promptPackPolicy, routeReason: replacementExecutionPlan.scenePlan.routeReason, @@ -4273,6 +4276,9 @@ export async function POST(request: Request) { referenceLabel: reference.label, pairedProductIndex: typeof pairedProductIndex === "number" ? pairedProductIndex + 1 : null, + pageKind: replacementExecutionPlan.scenePlan.pageKind, + generationRoute: replacementExecutionPlan.scenePlan.generationRoute, + promptPackPolicy: replacementExecutionPlan.scenePlan.promptPackPolicy, sourceImageCount: sourceImageUrls.length, generationAspectRatio, imageOrder: {