export type AgentPresetMessageImagePart = { type: "file"; url: string; name: string; mediaType: string; assetId?: string; }; export function extractCurrentMessageImageParts( parts: Array>, ): AgentPresetMessageImagePart[] { return parts .filter( (part): part is AgentPresetMessageImagePart => part?.type === "file" && typeof part?.url === "string" && /^https?:\/\//i.test(part.url) && typeof part?.mediaType === "string" && part.mediaType.startsWith("image/") ) .map((part) => ({ type: "file", url: String(part.url).trim(), name: String(part.name || "图片").trim() || "图片", mediaType: String(part.mediaType || "image/jpeg").trim() || "image/jpeg", assetId: typeof part.assetId === "string" && part.assetId.trim() ? part.assetId.trim() : undefined, })); } export function getSceneUploadedImageBreakdown(params: { uploadedImageParts: AgentPresetMessageImagePart[]; fallbackPrimaryCount: number; fallbackIncludeModelImage: boolean; }) { const normalizedNames = params.uploadedImageParts.map((part) => String(part.name || "").trim()); const primaryCountFromNames = normalizedNames.filter((name) => name.startsWith("主SKU参考图")).length; const secondaryCountFromNames = normalizedNames.filter((name) => name.startsWith("搭配SKU参考图")).length; const hasModelFromNames = normalizedNames.some((name) => name.startsWith("品牌模特图")); const hasNamedBreakdown = primaryCountFromNames > 0 || secondaryCountFromNames > 0 || hasModelFromNames; if (hasNamedBreakdown) { return { primaryProductImageCount: primaryCountFromNames, bundleProductImageCount: secondaryCountFromNames, includeModelImage: hasModelFromNames, }; } const uploadedImageCount = params.uploadedImageParts.length; const includeModelImage = params.fallbackIncludeModelImage && uploadedImageCount > params.fallbackPrimaryCount; const bundleProductImageCount = Math.max( 0, uploadedImageCount - params.fallbackPrimaryCount - (includeModelImage ? 1 : 0) ); const primaryProductImageCount = Math.max( 0, Math.min( params.fallbackPrimaryCount, uploadedImageCount - bundleProductImageCount - (includeModelImage ? 1 : 0) ) ); return { primaryProductImageCount, bundleProductImageCount, includeModelImage, }; } export function resolveScenePromptOptimizationImageUrls(params: { uploadedImageParts: AgentPresetMessageImagePart[]; primaryProductImageCount: number; bundleProductImageCount: number; includeModelImage: boolean; }) { const namedPrimaryParts = params.uploadedImageParts.filter((part) => String(part.name || "").trim().startsWith("主SKU参考图") ); const namedModelPart = params.uploadedImageParts.find((part) => String(part.name || "").trim().startsWith("品牌模特图") ); if (namedPrimaryParts.length > 0 || namedModelPart) { return { primaryImageUrl: namedPrimaryParts[0]?.url || "", modelImageUrl: namedModelPart?.url || null, }; } const primaryImageUrl = params.uploadedImageParts.slice(0, Math.max(0, params.primaryProductImageCount))[0]?.url || ""; const modelIndex = params.primaryProductImageCount + params.bundleProductImageCount; return { primaryImageUrl, modelImageUrl: params.includeModelImage && modelIndex < params.uploadedImageParts.length ? params.uploadedImageParts[modelIndex]?.url || null : null, }; } export function normalizeAgentSupplementPrompt(text: string, presetName?: string | null) { const normalized = String(text || "").trim(); if (!normalized) return ""; const escapedPresetName = String(presetName || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const genericPattern = escapedPresetName ? new RegExp(`^请按「${escapedPresetName}」风格化配置顺序处理这\\d+张图片。?$`) : /^请按「.+」风格化配置顺序处理这\d+张图片。?$/; if (genericPattern.test(normalized)) { return ""; } return normalized; } export function normalizeAgentWorkflowPrompt(text: string) { return String(text || "") .split(/\n+/) .map((line) => line.replace(/\s+/g, " ").trim()) .filter(Boolean) .join("\n") .trim(); } export function parseAgentWorkflowPromptSections(text: string) { const sectionMap = new Map(); let activeSection = "__root__"; const normalizedPrompt = normalizeAgentWorkflowPrompt(text); if (!normalizedPrompt) { return sectionMap; } normalizedPrompt.split("\n").forEach((line) => { const trimmedLine = line.trim(); if (!trimmedLine) { return; } const headerMatch = trimmedLine.match(/^【([^】]{1,30})】$/); if (headerMatch) { activeSection = headerMatch[1].trim(); if (!sectionMap.has(activeSection)) { sectionMap.set(activeSection, []); } return; } const currentLines = sectionMap.get(activeSection) || []; currentLines.push(trimmedLine); sectionMap.set(activeSection, currentLines); }); return sectionMap; } export function buildAgentPromptSectionText( title: string, lines: string[], options?: { bullet?: boolean } ) { const normalizedLines = lines .map((line) => String(line || "").trim()) .filter(Boolean); if (normalizedLines.length === 0) { return ""; } const sectionBody = options?.bullet ? normalizedLines.map((line) => (line.startsWith("- ") ? line : `- ${line}`)).join("\n") : normalizedLines.join("\n"); return `【${title}】\n${sectionBody}`; } export function buildOptimizedAgentSceneWorkflowPrompt(params: { workflowPrompt: string; optimizationResult?: { sceneGoal: string; environmentSummary: string; propSummary: string; cameraLightingSummary: string; characterSummary: string; negativeConstraints: string[]; conflicts: string[]; } | null; }) { const normalizedWorkflowPrompt = normalizeAgentWorkflowPrompt(params.workflowPrompt); if (!normalizedWorkflowPrompt) { return ""; } const optimizationResult = params.optimizationResult || null; if (!optimizationResult) { return normalizedWorkflowPrompt; } const sectionMap = parseAgentWorkflowPromptSections(normalizedWorkflowPrompt); const sceneGoalLines = [optimizationResult.sceneGoal, optimizationResult.environmentSummary].filter( Boolean ); const propLines = [optimizationResult.propSummary].filter(Boolean); const cameraLightingLines = [optimizationResult.cameraLightingSummary].filter(Boolean); const characterLines = [optimizationResult.characterSummary].filter(Boolean); const negativeLines = Array.from( new Set([ ...optimizationResult.negativeConstraints, ...optimizationResult.conflicts.map((item) => `冲突校正:${item}`), ]) ); const optimizedSections = [ buildAgentPromptSectionText("项目背景", sectionMap.get("项目背景") || sectionMap.get("__root__") || []), buildAgentPromptSectionText("主SKU锁定", sectionMap.get("主SKU锁定") || []), buildAgentPromptSectionText("搭配SKU锁定", sectionMap.get("搭配SKU锁定") || []), buildAgentPromptSectionText("参考图", sectionMap.get("参考图") || []), buildAgentPromptSectionText("场景创作总纲", sceneGoalLines), buildAgentPromptSectionText("陈列与道具", propLines), buildAgentPromptSectionText("机位与光线", cameraLightingLines), buildAgentPromptSectionText("人物规则", characterLines), buildAgentPromptSectionText("负面约束", negativeLines, { bullet: true }), ].filter(Boolean); return optimizedSections.join("\n\n").trim(); } const AGENT_PROMPT_STRUCTURAL_LABELS = new Set([ "角色", "技能", "目标", "约束条件", "流程", "输出", ]); const AGENT_PROMPT_LABEL_SEMANTIC_ALIASES = new Map([ ["角色", "role"], ["技能", "skills"], ["目标", "goal"], ["约束条件", "constraints"], ["流程", "process"], ["输出", "output"], ["人群选择", "audience"], ["人物", "audience"], ["主题风格", "style"], ["整体风格", "style"], ["空间选择", "space"], ["空间布局", "space"], ["主空间", "space"], ["子空间选择", "subspace"], ["子空间", "subspace"], ["硬装方案", "hardDecoration"], ["硬装", "hardDecoration"], ["软装方案", "softDecoration"], ["软装", "softDecoration"], ["光氛选择", "lighting"], ["光源布置", "lighting"], ["灯具", "lighting"], ["机位选择", "camera"], ["拍摄角度", "camera"], ["配饰陈列", "accessories"], ["放在产品内的小件配饰", "accessories"], ["前景", "sceneLayout"], ["中景", "sceneLayout"], ["背景", "sceneLayout"], ]); const AGENT_SCENE_WORKFLOW_OWNED_SEMANTICS = new Set([ "role", "skills", "goal", "constraints", "process", "output", "audience", "style", "space", "subspace", "hardDecoration", "softDecoration", "lighting", "camera", "accessories", "sceneLayout", ]); export function normalizePromptLineForCompare(line: string) { return String(line || "") .replace(/\s+/g, " ") .replace(/[::]/g, ":") .replace(/[。;;,,、!!??]+$/g, "") .trim() .toLowerCase(); } export function parsePromptLineLabel(line: string) { const normalized = String(line || "").replace(/\s+/g, " ").trim(); if (!normalized) return { label: "", value: "" }; const headerOnlyMatch = normalized.match(/^([^::]{1,20})[::]\s*$/); if (headerOnlyMatch) { return { label: headerOnlyMatch[1].trim(), value: "", }; } const colonMatch = normalized.match(/^([^::]{1,20})[::]\s*(.+)$/); if (colonMatch) { return { label: colonMatch[1].trim(), value: colonMatch[2].trim(), }; } const whitespaceMatch = normalized.match(/^([^\s]{1,20})\s+(.+)$/); if (whitespaceMatch) { return { label: whitespaceMatch[1].trim(), value: whitespaceMatch[2].trim(), }; } return { label: "", value: normalized }; } export function normalizePromptLabel(label: string) { return String(label || "") .replace(/\s+/g, "") .replace(/[::]/g, "") .trim(); } export function getPromptSemanticLabel(label: string) { const normalized = normalizePromptLabel(label); return AGENT_PROMPT_LABEL_SEMANTIC_ALIASES.get(normalized) || normalized; } export function collectPromptSemanticLabelsFromText(text: string) { const semanticLabels = new Set(); const normalizedText = String(text || "").replace(/\s+/g, " ").trim(); if (!normalizedText) { return semanticLabels; } for (const [label, semantic] of AGENT_PROMPT_LABEL_SEMANTIC_ALIASES.entries()) { if (!label) { continue; } if (normalizedText.includes(`${label}:`) || normalizedText.includes(`${label}:`)) { semanticLabels.add(semantic); continue; } if (normalizedText.includes(`${label} `) || normalizedText.includes(label)) { semanticLabels.add(semantic); } } return semanticLabels; } export function shouldDropSceneSlotSemanticLine( slotSemanticLabel: string, workflowSemanticLabels: Set, ) { if (!slotSemanticLabel || !AGENT_SCENE_WORKFLOW_OWNED_SEMANTICS.has(slotSemanticLabel)) { return false; } if (slotSemanticLabel === "sceneLayout") { return [ "style", "space", "subspace", "hardDecoration", "softDecoration", "accessories", "camera", "lighting", ].some((semantic) => workflowSemanticLabels.has(semantic)); } return workflowSemanticLabels.has(slotSemanticLabel); } export function dedupeAgentSlotPromptAgainstWorkflow(params: { workflowPrompt?: string | null; slotPrompt?: string | null; modeType?: "scene_generation" | "detail_replacement" | "sku_replacement" | "generic"; }) { const dedupedSlotLines: string[] = []; const seenExactLines = new Set(); const workflowStructuralValues = new Map(); const workflowSemanticLabels = new Set(); const rememberLine = (line: string) => { const normalizedLine = normalizePromptLineForCompare(line); if (!normalizedLine) { return; } seenExactLines.add(normalizedLine); }; const normalizedWorkflowLines = normalizeAgentWorkflowPrompt(params.workflowPrompt || "") .split("\n") .map((line) => line.trim()) .filter(Boolean); normalizedWorkflowLines.forEach((line) => { const parsed = parsePromptLineLabel(line); if (parsed.label) { workflowSemanticLabels.add(getPromptSemanticLabel(parsed.label)); } collectPromptSemanticLabelsFromText(line).forEach((semantic) => { workflowSemanticLabels.add(semantic); }); collectPromptSemanticLabelsFromText(parsed.value || "").forEach((semantic) => { workflowSemanticLabels.add(semantic); }); if (parsed.label && AGENT_PROMPT_STRUCTURAL_LABELS.has(parsed.label)) { workflowStructuralValues.set( parsed.label, normalizePromptLineForCompare(parsed.value || "") ); } rememberLine(line); }); const normalizedSlotLines = String(params.slotPrompt || "") .split(/\n+/) .map((line) => line.replace(/\s+/g, " ").trim()) .filter(Boolean); let activeSlotSemanticLabel = ""; normalizedSlotLines.forEach((line) => { const normalizedLine = normalizePromptLineForCompare(line); if (!normalizedLine || seenExactLines.has(normalizedLine)) { return; } const parsed = parsePromptLineLabel(line); const slotSemanticLabel = parsed.label ? getPromptSemanticLabel(parsed.label) : ""; if (slotSemanticLabel) { activeSlotSemanticLabel = slotSemanticLabel; } if ( params.modeType === "scene_generation" && shouldDropSceneSlotSemanticLine( slotSemanticLabel || activeSlotSemanticLabel, workflowSemanticLabels, ) ) { return; } if (parsed.label && AGENT_PROMPT_STRUCTURAL_LABELS.has(parsed.label)) { const normalizedValue = normalizePromptLineForCompare(parsed.value || ""); const workflowValue = workflowStructuralValues.get(parsed.label); if (workflowValue && workflowValue === normalizedValue) { return; } } seenExactLines.add(normalizedLine); dedupedSlotLines.push(line); }); return dedupedSlotLines.join("\n").trim(); } export function buildAgentSlotPrompt(params: { workflowPrompt?: string | null; slotPrompt: string; slotTitle?: string | null; userSupplementPrompt?: string | null; includeSlotTitle?: boolean; modeType?: "scene_generation" | "detail_replacement" | "sku_replacement" | "generic"; }) { const lines: string[] = []; const workflowPrompt = normalizeAgentWorkflowPrompt(params.workflowPrompt || ""); if (workflowPrompt) { lines.push(workflowPrompt); } const slotTitle = String(params.slotTitle || "").trim(); if (params.includeSlotTitle !== false && slotTitle) { lines.push(`【槽位】${slotTitle}`); } const dedupedSlotPrompt = dedupeAgentSlotPromptAgainstWorkflow({ workflowPrompt, slotPrompt: params.slotPrompt, modeType: params.modeType || "generic", }); if (dedupedSlotPrompt) { lines.push(dedupedSlotPrompt); } const supplement = String(params.userSupplementPrompt || "").trim(); if (supplement) { lines.push(`【补充要求】${supplement}`); } return lines.join("\n\n").trim(); } export function buildAgentSceneWorkflowPrompt(params: { workflowPrompt?: string | null; slotPrompt?: string | null; userSupplementPrompt?: string | null; imageRolePrompt?: string | null; }) { const lines: string[] = []; const workflowPrompt = normalizeAgentWorkflowPrompt(params.workflowPrompt || ""); if (workflowPrompt) { lines.push(workflowPrompt); } const slotPrompt = String(params.slotPrompt || "") .split(/\n+/) .map((line) => line.replace(/\s+/g, " ").trim()) .filter(Boolean) .join("\n") .trim(); if (slotPrompt) { lines.push(slotPrompt); } let imageRolePrompt = String(params.imageRolePrompt || "").trim(); if (workflowPrompt.includes("品牌模特图:已选") || workflowPrompt.includes("固定模特图")) { imageRolePrompt = imageRolePrompt .replace( /^如当前请求未附带固定模特图,仅在工作流明确要求人物时再安排辅助人物,且人物不能喧宾夺主。$/m, "" ) .replace(/^当前没有固定模特图,不要凭空生成强人物主体。$/m, "") .split(/\n+/) .map((line) => line.trim()) .filter(Boolean) .join("\n"); } if (imageRolePrompt) { lines.push(imageRolePrompt); } const supplement = String(params.userSupplementPrompt || "").trim(); if (supplement) { lines.push(`【补充要求】${supplement}`); } return lines.join("\n\n").trim(); }