diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index 47f92cb48..1a13ccfc0 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -115,6 +115,43 @@ import { getAgentTargetResolutionLabel, type ImageGenerationStatusStreamPayload, } from "@/components/canvas/chat-panel/canvas-agent-stream"; +import { + type DetailArchive, + type DetailBriefForm, + DETAIL_BUNDLE_SLOT_INDEXES, + DETAIL_FIELD_EXAMPLES, + DETAIL_GENERATION_MODE_OPTIONS, + type DetailGenerationMode, + DETAIL_MODEL_SLOT_INDEX, + type DetailPlanResponse, + type DetailPlanScreenSpec, + DETAIL_PRODUCT_SLOT_INDEXES, + DETAIL_REVIEW_COUNT, + DETAIL_REVIEW_IMAGE_COUNT, + DETAIL_REVIEW_IMAGE_HEIGHT, + DETAIL_REVIEW_IMAGE_WIDTH, + DETAIL_REVIEW_REGEN_COUNT, + type DetailReviewGroup, + type DetailReviewPlan, + type DetailScreenReviewResult, + type DetailSectionKey, + DETAIL_SCREEN_REVIEW_MAX_ATTEMPTS, + DETAIL_SLOT_CONFIGS, + DETAIL_STYLE_BOARD_HEIGHT, + DETAIL_STYLE_BOARD_SPACING, + DETAIL_STYLE_BOARD_WIDTH, + DETAIL_TARGET_HEIGHT, + DETAIL_TARGET_WIDTH, + DETAIL_TYPOGRAPHY_LOCK, + type DetailUiReviewSnapshot, + type DetailUiValidationSnapshot, + extractDetailFrameCount, + extractFirstNumber, + extractFontWeight, + normalizeDetailGenerationMode, + sanitizeDetailImageModelPrompt, + splitDetailKeywords, +} from "@/components/canvas/chat-panel/canvas-detail-config"; import { clampChatRequestText, deriveSessionTitleFromMessages, @@ -822,339 +859,6 @@ interface CanvasChatSession { updatedAt: number; } -type DetailBriefForm = { - productSpu: string; - demandType: string; - productName: string; - targetAudience: string; - mainUser: string; - usageScenario: string; - triggerMoment: string; - coreNeed: string; - riskConcern: string; - expectedResult: string; - coreSellingPoint: string; - positioningStatement: string; - userVoice: string; - competitorReference: string; - priceBand: string; - brandTone: string; - bannedWords: string; -}; - -type DetailSlotConfig = { - key: string; - label: string; - role: "product" | "model" | "bundle"; -}; - -type DetailSectionKey = "required" | "strategy" | "assets" | "archives"; -type DetailGenerationMode = "classic" | "agent_layout"; - -type DetailArchive = { - id: string; - title: string; - prompt: string; - generationMode: DetailGenerationMode; - brief: DetailBriefForm; - slots: Array; - createdAt: number; - updatedAt?: number; - summary?: string; - imageCount?: number; - coverUrl?: string; - uiSurfaceSnapshot?: UiSurfaceSnapshot | null; - uiSurfaceLatestReview?: { - screenIndex?: number; - action?: "pass" | "protocol_patch" | "regenerate_image"; - passed?: boolean; - issues?: string[]; - } | null; -}; - -type DetailPlanResponse = { - generationMode?: DetailGenerationMode; - summary?: string; - prompts?: string[]; - screenSize?: { - width: number; - height: number; - }; - globalStyle?: { - designLanguage?: string; - typography?: { - fontFamily?: string; - title?: string; - subtitle?: string; - body?: string; - }; - layout?: { - grid?: string; - safeArea?: string; - rhythm?: string; - }; - palette?: string; - }; - screens?: Array<{ - index: number; - screenType?: "A" | "B" | "C"; - objective?: string; - title?: string; - subtitle?: string; - copy?: string; - layoutNote?: string; - useModel?: boolean; - modelReason?: string; - bundleShowcase?: boolean; - bundleReason?: string; - visualPrompt?: string; - fabe?: { - b?: string; - f?: string; - a?: string; - e?: string; - }; - }>; - skillFlow?: { - name?: string; - version?: string; - steps?: string[]; - }; - designSkill?: { - version?: string; - name?: string; - source?: string; - visualDirection?: string[]; - reviewChecklist?: string[]; - }; - qualityChecks?: { - passed?: boolean; - issues?: string[]; - }; - uiValidation?: { - passed?: boolean; - issues?: string[]; - }; - preflight?: { - passed?: boolean; - designDirectorScore?: number; - marketingDirectorScore?: number; - issues?: string[]; - rewriteApplied?: boolean; - }; - model?: { - id: string; - displayName: string; - providerType: string; - }; - uiProtocol?: DetailUiProtocol; - uiSurface?: UiSurfaceSnapshot; - uiDslV2Screens?: DetailUiDslV2Document[]; - brandStyleBoard?: UiScreenDocument; -}; -type DetailPlanScreenSpec = NonNullable[number]; - -const DETAIL_TARGET_WIDTH = 790; -const DETAIL_TARGET_HEIGHT = 1300; -const DETAIL_STYLE_BOARD_WIDTH = 790; -const DETAIL_STYLE_BOARD_HEIGHT = 1080; -const DETAIL_STYLE_BOARD_SPACING = 36; -const DETAIL_REVIEW_COUNT = 3; -const DETAIL_REVIEW_REGEN_COUNT = 2; -const DETAIL_REVIEW_IMAGE_COUNT = 3; -const DETAIL_REVIEW_IMAGE_WIDTH = 420; -const DETAIL_REVIEW_IMAGE_HEIGHT = 420; -const DETAIL_SCREEN_REVIEW_MAX_ATTEMPTS = 1; -const DETAIL_TYPOGRAPHY_LOCK = { - title: "主标题 64px / 700 / 行高1.15,建议单行最多2行,不得出现“第X屏”", - subtitle: "副标题 34px / 600 / 行高1.25,位于主标题下方16px", - body: "正文 24px / 500 / 行高1.45,段间距10px,每块最多4行", - safeArea: "文字安全区固定 y=64~360,产品主体避免遮挡标题区", - rhythm: "左右边距56px,模块间距24px,全套页面留白节奏一致", -} as const; -const DETAIL_SLOT_CONFIGS: DetailSlotConfig[] = [ - { key: "product-1", label: "产品图1", role: "product" }, - { key: "product-2", label: "产品图2", role: "product" }, - { key: "product-3", label: "产品图3", role: "product" }, - { key: "product-4", label: "产品图4", role: "product" }, - { key: "product-5", label: "产品图5", role: "product" }, - { key: "model-1", label: "模特图", role: "model" }, - { key: "bundle-1", label: "搭配图1", role: "bundle" }, - { key: "bundle-2", label: "搭配图2", role: "bundle" }, - { key: "bundle-3", label: "搭配图3", role: "bundle" }, -]; -const DETAIL_PRODUCT_SLOT_INDEXES = DETAIL_SLOT_CONFIGS.map((slot, index) => - slot.role === "product" ? index : -1 -).filter((index) => index >= 0); -const DETAIL_MODEL_SLOT_INDEX = DETAIL_SLOT_CONFIGS.findIndex((slot) => slot.role === "model"); -const DETAIL_BUNDLE_SLOT_INDEXES = DETAIL_SLOT_CONFIGS.map((slot, index) => - slot.role === "bundle" ? index : -1 -).filter((index) => index >= 0); -const DETAIL_FIELD_EXAMPLES: Record = { - productSpu: "示例:SPU-889901", - demandType: "示例:清洁除尘 / 户外通勤 / 礼赠", - productName: "示例:Buyi-AI 无线手持吸尘器", - targetAudience: "示例:25-40岁养宠家庭、宝妈", - mainUser: "示例:家庭主理人、租房上班族", - usageScenario: "示例:下班回家10分钟快速清洁", - triggerMoment: "示例:宠物掉毛高峰、饭后碎屑清理", - coreNeed: "示例:省时省力清除毛发与灰尘", - riskConcern: "示例:担心吸力不足、续航短、噪音大", - expectedResult: "示例:3分钟清洁桌面,地面无明显毛发", - coreSellingPoint: "示例:18000Pa吸力、0.9kg轻量、一键倒尘", - positioningStatement: "示例:让日常清洁更快更轻松", - userVoice: "示例:家里有猫掉毛太多,希望安静一点不吵宝宝", - competitorReference: "示例:对标戴森入门款,突出轻便高性价比", - priceBand: "示例:199-299元", - brandTone: "示例:专业、可靠、亲和", - bannedWords: "示例:最强、第一、100%治愈", -}; -const DETAIL_GENERATION_MODE_OPTIONS: Array<{ - value: DetailGenerationMode; - label: string; - description: string; -}> = [ - { - value: "classic", - label: "传统生图", - description: "整页直接生图,速度更快", - }, - { - value: "agent_layout", - label: "AGENT排版", - description: "生图只出画面,文字层可编辑", - }, -]; - -function extractDetailFrameCount(text: string): number { - const normalized = text.replace(/\s+/g, ""); - const matched = normalized.match(/(\d{1,2})(?:张|屏|页)/); - if (!matched) return 15; - const value = Number(matched[1]); - if (!Number.isFinite(value)) return 15; - return Math.min(20, Math.max(5, Math.round(value))); -} - -type DetailReviewPlan = { - index: number; - title: string; - reviewText: string; - imagePrompt: string; -}; - -type DetailReviewGroup = { - reviewText: string; - images: Array<{ originalUrl: string; thumbnailUrl: string }>; -}; - -type DetailScreenReviewResult = { - pass: boolean; - reviewAction?: "pass" | "protocol_patch" | "regenerate_image"; - score?: number; - issues?: string[]; - layoutFixes?: string[]; - revisedPrompt?: string; - uiPatch?: DetailUiScreenReviewPatch; - uiSurfacePatch?: UiSurfacePatch; - uiValidation?: { - passed?: boolean; - issues?: string[]; - }; - model?: { - id?: string; - displayName?: string; - providerType?: string; - } | null; -}; - -type DetailUiValidationSnapshot = { - passed?: boolean; - issues?: string[]; -}; - -type DetailUiReviewSnapshot = { - screenIndex: number; - action?: "pass" | "protocol_patch" | "regenerate_image"; - passed?: boolean; - issues?: string[]; -}; - -function normalizeDetailGenerationMode(value: unknown): DetailGenerationMode { - return value === "agent_layout" ? "agent_layout" : "classic"; -} - -function extractFirstNumber(value: string, fallback: number): number { - const matched = String(value || "").match(/(\d{1,3})/); - if (!matched) return fallback; - const parsed = Number(matched[1]); - if (!Number.isFinite(parsed)) return fallback; - return parsed; -} - -function extractFontWeight(value: string, fallback = "600"): string { - const matched = String(value || "").match(/(?:\/|weight[::]?\s*)(\d{3})/i); - if (!matched) return fallback; - return matched[1] || fallback; -} - -function sanitizeDetailImageModelPrompt(prompt: string): string { - const filteredLines = String(prompt || "") - .split(/\n+/) - .map((line) => line.trim()) - .filter(Boolean) - .map((line) => { - if ( - /^(字体规范|标题规范|副标题规范|正文规范|版式规范|安全区|留白节奏|Typography|Layout|Palette|固定字体锁定|固定副标题锁定|固定正文锁定|固定排版安全区|固定留白节奏)/i.test( - line - ) - ) { - return ""; - } - return line; - }) - .filter(Boolean); - - 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, "输出为竖版电商详情图。"], - [/y\s*=\s*\d+\s*[-~~]\s*\d+/gi, "顶部标题安全区"], - [/左右边距\s*\d+\s*px/gi, "左右留白充足"], - [/模块间距\s*\d+\s*px/gi, "模块间距均匀"], - [/标题和副标题间距\s*\d+\s*px/gi, "标题与副标题间距清晰"], - [/主标题\s*\d+\s*px[^,。;\n]*/gi, "主标题醒目突出"], - [/副标题\s*\d+\s*px[^,。;\n]*/gi, "副标题层级清晰"], - [/正文\s*\d+\s*px[^,。;\n]*/gi, "正文精简易读"], - [/商业摄影质感,留出清晰文案排版区,不要水印。?/gi, "商业摄影质感,留出清晰文字区域,不要水印。"], - ]; - - for (const [pattern, replacement] of literalReplacements) { - result = result.replace(pattern, replacement); - } - - result = result - .replace(/\b\d+\s*px\b/gi, "") - .replace(/[((]\s*顶部标题安全区\s*[))]/g, "顶部标题安全区") - .replace(/;{2,}/g, ";") - .replace(/。{2,}/g, "。") - .replace(/\s{2,}/g, " ") - .replace(/\n{3,}/g, "\n\n") - .trim(); - - return result; -} - -function splitDetailKeywords(value: string, limit = 3): string[] { - const raw = String(value || "") - .split(/[\n,,、;;|]/) - .map((item) => item.replace(/\s+/g, " ").trim()) - .filter(Boolean); - if (raw.length > 0) return raw.slice(0, limit); - return []; -} - function buildDetailReviewPlans(params: { brief: DetailBriefForm; summary?: string; diff --git a/components/canvas/chat-panel/canvas-detail-config.ts b/components/canvas/chat-panel/canvas-detail-config.ts new file mode 100644 index 000000000..a2a372ac1 --- /dev/null +++ b/components/canvas/chat-panel/canvas-detail-config.ts @@ -0,0 +1,348 @@ +import type { DetailUiProtocol, DetailUiScreenReviewPatch } from "@/lib/detail-ui-protocol"; +import type { DetailUiDslV2Document } from "@/lib/detail-ui-dsl-v2"; +import type { UiScreenDocument } from "@/lib/ui-screen/schema"; +import type { UiSurfacePatch, UiSurfaceSnapshot } from "@/lib/ui-surface-protocol"; +import type { PickedObjectAttachment } from "@/components/canvas/chat-panel/canvas-chat-session"; + +export type DetailBriefForm = { + productSpu: string; + demandType: string; + productName: string; + targetAudience: string; + mainUser: string; + usageScenario: string; + triggerMoment: string; + coreNeed: string; + riskConcern: string; + expectedResult: string; + coreSellingPoint: string; + positioningStatement: string; + userVoice: string; + competitorReference: string; + priceBand: string; + brandTone: string; + bannedWords: string; +}; + +export type DetailSlotConfig = { + key: string; + label: string; + role: "product" | "model" | "bundle"; +}; + +export type DetailSectionKey = "required" | "strategy" | "assets" | "archives"; +export type DetailGenerationMode = "classic" | "agent_layout"; + +export type DetailArchive = { + id: string; + title: string; + prompt: string; + generationMode: DetailGenerationMode; + brief: DetailBriefForm; + slots: Array; + createdAt: number; + updatedAt?: number; + summary?: string; + imageCount?: number; + coverUrl?: string; + uiSurfaceSnapshot?: UiSurfaceSnapshot | null; + uiSurfaceLatestReview?: { + screenIndex?: number; + action?: "pass" | "protocol_patch" | "regenerate_image"; + passed?: boolean; + issues?: string[]; + } | null; +}; + +export type DetailPlanResponse = { + generationMode?: DetailGenerationMode; + summary?: string; + prompts?: string[]; + screenSize?: { + width: number; + height: number; + }; + globalStyle?: { + designLanguage?: string; + typography?: { + fontFamily?: string; + title?: string; + subtitle?: string; + body?: string; + }; + layout?: { + grid?: string; + safeArea?: string; + rhythm?: string; + }; + palette?: string; + }; + screens?: Array<{ + index: number; + screenType?: "A" | "B" | "C"; + objective?: string; + title?: string; + subtitle?: string; + copy?: string; + layoutNote?: string; + useModel?: boolean; + modelReason?: string; + bundleShowcase?: boolean; + bundleReason?: string; + visualPrompt?: string; + fabe?: { + b?: string; + f?: string; + a?: string; + e?: string; + }; + }>; + skillFlow?: { + name?: string; + version?: string; + steps?: string[]; + }; + designSkill?: { + version?: string; + name?: string; + source?: string; + visualDirection?: string[]; + reviewChecklist?: string[]; + }; + qualityChecks?: { + passed?: boolean; + issues?: string[]; + }; + uiValidation?: { + passed?: boolean; + issues?: string[]; + }; + preflight?: { + passed?: boolean; + designDirectorScore?: number; + marketingDirectorScore?: number; + issues?: string[]; + rewriteApplied?: boolean; + }; + model?: { + id: string; + displayName: string; + providerType: string; + }; + uiProtocol?: DetailUiProtocol; + uiSurface?: UiSurfaceSnapshot; + uiDslV2Screens?: DetailUiDslV2Document[]; + brandStyleBoard?: UiScreenDocument; +}; + +export type DetailPlanScreenSpec = NonNullable[number]; + +export type DetailReviewPlan = { + index: number; + title: string; + reviewText: string; + imagePrompt: string; +}; + +export type DetailReviewGroup = { + reviewText: string; + images: Array<{ originalUrl: string; thumbnailUrl: string }>; +}; + +export type DetailScreenReviewResult = { + pass: boolean; + reviewAction?: "pass" | "protocol_patch" | "regenerate_image"; + score?: number; + issues?: string[]; + layoutFixes?: string[]; + revisedPrompt?: string; + uiPatch?: DetailUiScreenReviewPatch; + uiSurfacePatch?: UiSurfacePatch; + uiValidation?: { + passed?: boolean; + issues?: string[]; + }; + model?: { + id?: string; + displayName?: string; + providerType?: string; + } | null; +}; + +export type DetailUiValidationSnapshot = { + passed?: boolean; + issues?: string[]; +}; + +export type DetailUiReviewSnapshot = { + screenIndex: number; + action?: "pass" | "protocol_patch" | "regenerate_image"; + passed?: boolean; + issues?: string[]; +}; + +export const DETAIL_TARGET_WIDTH = 790; +export const DETAIL_TARGET_HEIGHT = 1300; +export const DETAIL_STYLE_BOARD_WIDTH = 790; +export const DETAIL_STYLE_BOARD_HEIGHT = 1080; +export const DETAIL_STYLE_BOARD_SPACING = 36; +export const DETAIL_REVIEW_COUNT = 3; +export const DETAIL_REVIEW_REGEN_COUNT = 2; +export const DETAIL_REVIEW_IMAGE_COUNT = 3; +export const DETAIL_REVIEW_IMAGE_WIDTH = 420; +export const DETAIL_REVIEW_IMAGE_HEIGHT = 420; +export const DETAIL_SCREEN_REVIEW_MAX_ATTEMPTS = 1; + +export const DETAIL_TYPOGRAPHY_LOCK = { + title: "主标题 64px / 700 / 行高1.15,建议单行最多2行,不得出现“第X屏”", + subtitle: "副标题 34px / 600 / 行高1.25,位于主标题下方16px", + body: "正文 24px / 500 / 行高1.45,段间距10px,每块最多4行", + safeArea: "文字安全区固定 y=64~360,产品主体避免遮挡标题区", + rhythm: "左右边距56px,模块间距24px,全套页面留白节奏一致", +} as const; + +export const DETAIL_SLOT_CONFIGS: DetailSlotConfig[] = [ + { key: "product-1", label: "产品图1", role: "product" }, + { key: "product-2", label: "产品图2", role: "product" }, + { key: "product-3", label: "产品图3", role: "product" }, + { key: "product-4", label: "产品图4", role: "product" }, + { key: "product-5", label: "产品图5", role: "product" }, + { key: "model-1", label: "模特图", role: "model" }, + { key: "bundle-1", label: "搭配图1", role: "bundle" }, + { key: "bundle-2", label: "搭配图2", role: "bundle" }, + { key: "bundle-3", label: "搭配图3", role: "bundle" }, +]; + +export const DETAIL_PRODUCT_SLOT_INDEXES = DETAIL_SLOT_CONFIGS.map((slot, index) => + slot.role === "product" ? index : -1 +).filter((index) => index >= 0); + +export const DETAIL_MODEL_SLOT_INDEX = DETAIL_SLOT_CONFIGS.findIndex( + (slot) => slot.role === "model" +); + +export const DETAIL_BUNDLE_SLOT_INDEXES = DETAIL_SLOT_CONFIGS.map((slot, index) => + slot.role === "bundle" ? index : -1 +).filter((index) => index >= 0); + +export const DETAIL_FIELD_EXAMPLES: Record = { + productSpu: "示例:SPU-889901", + demandType: "示例:清洁除尘 / 户外通勤 / 礼赠", + productName: "示例:Buyi-AI 无线手持吸尘器", + targetAudience: "示例:25-40岁养宠家庭、宝妈", + mainUser: "示例:家庭主理人、租房上班族", + usageScenario: "示例:下班回家10分钟快速清洁", + triggerMoment: "示例:宠物掉毛高峰、饭后碎屑清理", + coreNeed: "示例:省时省力清除毛发与灰尘", + riskConcern: "示例:担心吸力不足、续航短、噪音大", + expectedResult: "示例:3分钟清洁桌面,地面无明显毛发", + coreSellingPoint: "示例:18000Pa吸力、0.9kg轻量、一键倒尘", + positioningStatement: "示例:让日常清洁更快更轻松", + userVoice: "示例:家里有猫掉毛太多,希望安静一点不吵宝宝", + competitorReference: "示例:对标戴森入门款,突出轻便高性价比", + priceBand: "示例:199-299元", + brandTone: "示例:专业、可靠、亲和", + bannedWords: "示例:最强、第一、100%治愈", +}; + +export const DETAIL_GENERATION_MODE_OPTIONS: Array<{ + value: DetailGenerationMode; + label: string; + description: string; +}> = [ + { + value: "classic", + label: "传统生图", + description: "整页直接生图,速度更快", + }, + { + value: "agent_layout", + label: "AGENT排版", + description: "生图只出画面,文字层可编辑", + }, +]; + +export function extractDetailFrameCount(text: string): number { + const normalized = text.replace(/\s+/g, ""); + const matched = normalized.match(/(\d{1,2})(?:张|屏|页)/); + if (!matched) return 15; + const value = Number(matched[1]); + if (!Number.isFinite(value)) return 15; + return Math.min(20, Math.max(5, Math.round(value))); +} + +export function normalizeDetailGenerationMode(value: unknown): DetailGenerationMode { + return value === "agent_layout" ? "agent_layout" : "classic"; +} + +export function extractFirstNumber(value: string, fallback: number): number { + const matched = String(value || "").match(/(\d{1,3})/); + if (!matched) return fallback; + const parsed = Number(matched[1]); + if (!Number.isFinite(parsed)) return fallback; + return parsed; +} + +export function extractFontWeight(value: string, fallback = "600"): string { + const matched = String(value || "").match(/(?:\/|weight[::]?\s*)(\d{3})/i); + if (!matched) return fallback; + return matched[1] || fallback; +} + +export function sanitizeDetailImageModelPrompt(prompt: string): string { + const filteredLines = String(prompt || "") + .split(/\n+/) + .map((line) => line.trim()) + .filter(Boolean) + .map((line) => { + if ( + /^(字体规范|标题规范|副标题规范|正文规范|版式规范|安全区|留白节奏|Typography|Layout|Palette|固定字体锁定|固定副标题锁定|固定正文锁定|固定排版安全区|固定留白节奏)/i.test( + line + ) + ) { + return ""; + } + return line; + }) + .filter(Boolean); + + 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, "输出为竖版电商详情图。"], + [/y\s*=\s*\d+\s*[-~~]\s*\d+/gi, "顶部标题安全区"], + [/左右边距\s*\d+\s*px/gi, "左右留白充足"], + [/模块间距\s*\d+\s*px/gi, "模块间距均匀"], + [/标题和副标题间距\s*\d+\s*px/gi, "标题与副标题间距清晰"], + [/主标题\s*\d+\s*px[^,。;\n]*/gi, "主标题醒目突出"], + [/副标题\s*\d+\s*px[^,。;\n]*/gi, "副标题层级清晰"], + [/正文\s*\d+\s*px[^,。;\n]*/gi, "正文精简易读"], + [/商业摄影质感,留出清晰文案排版区,不要水印。?/gi, "商业摄影质感,留出清晰文字区域,不要水印。"], + ]; + + for (const [pattern, replacement] of literalReplacements) { + result = result.replace(pattern, replacement); + } + + result = result + .replace(/\b\d+\s*px\b/gi, "") + .replace(/[((]\s*顶部标题安全区\s*[))]/g, "顶部标题安全区") + .replace(/;{2,}/g, ";") + .replace(/。{2,}/g, "。") + .replace(/\s{2,}/g, " ") + .replace(/\n{3,}/g, "\n\n") + .trim(); + + return result; +} + +export function splitDetailKeywords(value: string, limit = 3): string[] { + const raw = String(value || "") + .split(/[\n,,、;;|]/) + .map((item) => item.replace(/\s+/g, " ").trim()) + .filter(Boolean); + if (raw.length > 0) return raw.slice(0, limit); + return []; +}