diff --git a/components/canvas/chat-panel/canvas-detail-config.ts b/components/canvas/chat-panel/canvas-detail-config.ts index a2a372ac1..9b2071f06 100644 --- a/components/canvas/chat-panel/canvas-detail-config.ts +++ b/components/canvas/chat-panel/canvas-detail-config.ts @@ -284,7 +284,7 @@ export function extractFirstNumber(value: string, fallback: number): number { } export function extractFontWeight(value: string, fallback = "600"): string { - const matched = String(value || "").match(/(?:\/|weight[::]?\s*)(\d{3})/i); + const matched = String(value || "").match(/(?:\/\s*|weight[::]?\s*)(\d{3})/i); if (!matched) return fallback; return matched[1] || fallback; } @@ -296,7 +296,7 @@ export function sanitizeDetailImageModelPrompt(prompt: string): string { .filter(Boolean) .map((line) => { if ( - /^(字体规范|标题规范|副标题规范|正文规范|版式规范|安全区|留白节奏|Typography|Layout|Palette|固定字体锁定|固定副标题锁定|固定正文锁定|固定排版安全区|固定留白节奏)/i.test( + /^(字体规范|标题规范|副标题规范|正文规范|版式规范|留白节奏|Typography|Layout|Palette|固定字体锁定|固定副标题锁定|固定正文锁定|固定排版安全区|固定留白节奏)/i.test( line ) ) { @@ -309,9 +309,9 @@ export function sanitizeDetailImageModelPrompt(prompt: string): string { let result = filteredLines.join("\n"); const literalReplacements: Array<[RegExp, string]> = [ + [/输出为竖版电商详情图,尺寸固定\s*\d+\s*x\s*\d+\s*(?:像素|pixels?)。?/gi, "输出为竖版电商详情图。"], [/画布固定(?:竖版)?\s*\d+\s*x\s*\d+\s*(?:像素|pixels?)。?/gi, "画布为竖版详情页构图。"], [/尺寸固定\s*\d+\s*x\s*\d+\s*(?:像素|pixels?)。?/gi, "尺寸为竖版详情页比例。"], - [/输出为竖版电商详情图,尺寸固定\s*\d+\s*x\s*\d+\s*(?:像素|pixels?)。?/gi, "输出为竖版电商详情图。"], [/y\s*=\s*\d+\s*[-~~]\s*\d+/gi, "顶部标题安全区"], [/左右边距\s*\d+\s*px/gi, "左右留白充足"], [/模块间距\s*\d+\s*px/gi, "模块间距均匀"], diff --git a/tests/routes/canvas-chat-helpers.test.ts b/tests/routes/canvas-chat-helpers.test.ts index 7f1edd679..b58476089 100644 --- a/tests/routes/canvas-chat-helpers.test.ts +++ b/tests/routes/canvas-chat-helpers.test.ts @@ -11,6 +11,21 @@ import { buildDetailReviewAppendMarkdown, buildDetailReviewPlans, } from "@/components/canvas/chat-panel/canvas-detail-markdown"; +import { + DETAIL_BUNDLE_SLOT_INDEXES, + DETAIL_GENERATION_MODE_OPTIONS, + DETAIL_MODEL_SLOT_INDEX, + DETAIL_PRODUCT_SLOT_INDEXES, + DETAIL_SLOT_CONFIGS, + DETAIL_TARGET_HEIGHT, + DETAIL_TARGET_WIDTH, + extractDetailFrameCount, + extractFirstNumber, + extractFontWeight, + normalizeDetailGenerationMode, + sanitizeDetailImageModelPrompt, + splitDetailKeywords, +} from "@/components/canvas/chat-panel/canvas-detail-config"; import type { ChatMessage } from "@/lib/types"; test.describe("canvas chat pure helpers", () => { @@ -119,6 +134,67 @@ test.describe("canvas chat pure helpers", () => { expect(plans[0]?.imagePrompt).toContain("无线吸尘器"); }); + test("keeps detail slot indexes and generation mode options stable", () => { + expect(DETAIL_TARGET_WIDTH).toBe(790); + expect(DETAIL_TARGET_HEIGHT).toBe(1300); + expect(DETAIL_SLOT_CONFIGS.map((slot) => slot.role)).toEqual([ + "product", + "product", + "product", + "product", + "product", + "model", + "bundle", + "bundle", + "bundle", + ]); + expect(DETAIL_PRODUCT_SLOT_INDEXES).toEqual([0, 1, 2, 3, 4]); + expect(DETAIL_MODEL_SLOT_INDEX).toBe(5); + expect(DETAIL_BUNDLE_SLOT_INDEXES).toEqual([6, 7, 8]); + expect(DETAIL_GENERATION_MODE_OPTIONS.map((option) => option.value)).toEqual([ + "classic", + "agent_layout", + ]); + }); + + test("normalizes detail config parser helpers defensively", () => { + expect(normalizeDetailGenerationMode("agent_layout")).toBe("agent_layout"); + expect(normalizeDetailGenerationMode("unknown")).toBe("classic"); + expect(extractDetailFrameCount("需要 23 张详情页")).toBe(20); + expect(extractDetailFrameCount("需要 2 屏")).toBe(5); + expect(extractDetailFrameCount("没有明确数量")).toBe(15); + expect(extractFirstNumber("标题 64px / 700", 32)).toBe(64); + expect(extractFirstNumber("无数字", 32)).toBe(32); + expect(extractFontWeight("主标题 64px / 700 / 行高1.15", "600")).toBe("700"); + expect(extractFontWeight("weight: 500", "600")).toBe("500"); + expect(extractFontWeight("无权重", "600")).toBe("600"); + expect(splitDetailKeywords("强吸力,轻量; 低噪音 | 易清洁", 3)).toEqual([ + "强吸力", + "轻量", + "低噪音", + ]); + }); + + test("sanitizes detail image model prompts away from pixel-locked layout instructions", () => { + const sanitized = sanitizeDetailImageModelPrompt( + [ + "字体规范:主标题 64px / 700 / 行高1.15", + "输出为竖版电商详情图,尺寸固定 790x1300 像素。", + "安全区 y=64~360,左右边距56px,模块间距24px。", + "商业摄影质感,留出清晰文案排版区,不要水印。", + "突出产品真实材质与使用场景。", + ].join("\n") + ); + + expect(sanitized).not.toContain("790x1300"); + expect(sanitized).not.toContain("64px"); + expect(sanitized).not.toContain("56px"); + expect(sanitized).toContain("输出为竖版电商详情图。"); + expect(sanitized).toContain("顶部标题安全区"); + expect(sanitized).toContain("左右留白充足"); + expect(sanitized).toContain("突出产品真实材质与使用场景。"); + }); + test("builds stable detail result and appended review markdown", () => { const markdown = buildDetailResultMarkdown({ generationMode: "agent_layout",