ai-chatbot/app/(chat)/api/chat/_lib/agent-scene-workflow.ts
2026-05-26 22:34:12 +08:00

541 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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();
}