diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index 3298aa96b..28cc082be 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -2863,7 +2863,108 @@ function normalizeAgentSupplementPrompt(text: string, presetName?: string | null } function normalizeAgentWorkflowPrompt(text: string) { - return String(text || "").trim(); + return String(text || "") + .split(/\n+/) + .map((line) => line.replace(/\s+/g, " ").trim()) + .filter(Boolean) + .join("\n") + .trim(); +} + +const AGENT_PROMPT_STRUCTURAL_LABELS = new Set([ + "角色", + "技能", + "目标", + "约束条件", + "流程", + "输出", +]); + +function normalizePromptLineForCompare(line: string) { + return String(line || "") + .replace(/\s+/g, " ") + .replace(/[::]/g, ":") + .replace(/[。;;,,、!!??]+$/g, "") + .trim() + .toLowerCase(); +} + +function parsePromptLineLabel(line: string) { + const normalized = String(line || "").replace(/\s+/g, " ").trim(); + if (!normalized) return { label: "", 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 }; +} + +function dedupeAgentSlotPromptAgainstWorkflow(params: { + workflowPrompt?: string | null; + slotPrompt?: string | null; +}) { + const dedupedSlotLines: string[] = []; + const seenExactLines = new Set(); + const workflowStructuralValues = new Map(); + + 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 && 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); + normalizedSlotLines.forEach((line) => { + const normalizedLine = normalizePromptLineForCompare(line); + if (!normalizedLine || seenExactLines.has(normalizedLine)) { + return; + } + const parsed = parsePromptLineLabel(line); + 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(); } function buildAgentSlotPrompt(params: { @@ -2881,9 +2982,12 @@ function buildAgentSlotPrompt(params: { if (slotTitle) { lines.push(`【槽位】${slotTitle}`); } - const slotPrompt = String(params.slotPrompt || "").trim(); - if (slotPrompt) { - lines.push(slotPrompt); + const dedupedSlotPrompt = dedupeAgentSlotPromptAgainstWorkflow({ + workflowPrompt, + slotPrompt: params.slotPrompt, + }); + if (dedupedSlotPrompt) { + lines.push(dedupedSlotPrompt); } const supplement = String(params.userSupplementPrompt || "").trim(); if (supplement) { diff --git a/lib/agents/workflow-composer.ts b/lib/agents/workflow-composer.ts index 2d42ebd2e..7c72ef114 100644 --- a/lib/agents/workflow-composer.ts +++ b/lib/agents/workflow-composer.ts @@ -7,6 +7,7 @@ import { } from "@/lib/agents/workflow-ui-config"; import type { AgentWorkflowComposerContext, + AgentWorkflowComposerSelection, AgentWorkflowMaterialSelections, AgentWorkflowReferenceImage, AgentWorkflowSelectedBrandModel, @@ -22,6 +23,57 @@ export interface AgentWorkflowInputsValue { priceBand: string; } +function normalizeWorkflowText(value: string) { + return String(value || "") + .replace(/\s+/g, " ") + .trim(); +} + +function buildGroupedSelectionLines(params: { + selections: AgentWorkflowComposerSelection[]; + valueKey: "optionName" | "promptFragment"; + separator: string; +}) { + const grouped = new Map< + string, + { + stepTitle: string; + values: string[]; + valueSet: Set; + } + >(); + + params.selections.forEach((item) => { + const rawValue = + params.valueKey === "promptFragment" + ? item.promptFragment || item.optionName + : item.optionName; + const normalizedValue = normalizeWorkflowText(rawValue || ""); + if (!normalizedValue) { + return; + } + + const groupKey = item.stepKey; + const currentGroup = grouped.get(groupKey) || { + stepTitle: item.stepTitle, + values: [], + valueSet: new Set(), + }; + + const dedupeKey = normalizedValue.toLowerCase(); + if (!currentGroup.valueSet.has(dedupeKey)) { + currentGroup.valueSet.add(dedupeKey); + currentGroup.values.push(normalizedValue); + } + + grouped.set(groupKey, currentGroup); + }); + + return Array.from(grouped.values()) + .filter((group) => group.values.length > 0) + .map((group) => `${group.stepTitle}:${group.values.join(params.separator)}`); +} + export function buildAgentWorkflowComposerContext(params: { config?: AgentWorkflowUiConfig | null; inputs: AgentWorkflowInputsValue; @@ -89,9 +141,13 @@ export function buildAgentWorkflowComposerContext(params: { `品牌模特图:${referenceImages.filter((item) => item.source === "brandModel").length}张` ); } - selections.forEach((item) => { - summaryParts.push(`${item.stepTitle}:${item.optionName}`); - }); + summaryParts.push( + ...buildGroupedSelectionLines({ + selections, + valueKey: "optionName", + separator: " / ", + }) + ); if (summaryParts.length === 0) { return null; @@ -149,9 +205,13 @@ export function buildAgentWorkflowComposerContext(params: { }` ); } - selections.forEach((item) => { - promptLines.push(`${item.stepTitle}:${item.promptFragment || item.optionName}`); - }); + promptLines.push( + ...buildGroupedSelectionLines({ + selections, + valueKey: "promptFragment", + separator: ";", + }) + ); return { primarySku: params.inputs.primarySku.trim() || undefined,