541 lines
16 KiB
TypeScript
541 lines
16 KiB
TypeScript
export type AgentPresetMessageImagePart = {
|
||
type: "file";
|
||
url: string;
|
||
name: string;
|
||
mediaType: string;
|
||
assetId?: string;
|
||
};
|
||
|
||
export function extractCurrentMessageImageParts(
|
||
parts: Array<Record<string, any>>,
|
||
): 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<string, string[]>();
|
||
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<string, string>([
|
||
["角色", "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<string>();
|
||
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<string>,
|
||
) {
|
||
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<string>();
|
||
const workflowStructuralValues = new Map<string, string>();
|
||
const workflowSemanticLabels = new Set<string>();
|
||
|
||
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();
|
||
}
|