refactor: extract canvas detail configuration

This commit is contained in:
Your Name 2026-05-27 10:21:03 +08:00
parent ef837f9985
commit db4d0943bf
2 changed files with 385 additions and 333 deletions

View File

@ -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<PickedObjectAttachment | null>;
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<DetailPlanResponse["screens"]>[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<keyof DetailBriefForm, string> = {
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;

View File

@ -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<PickedObjectAttachment | null>;
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<DetailPlanResponse["screens"]>[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<keyof DetailBriefForm, string> = {
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 [];
}