fix agent workflow prompt cleanup and card blocking
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-03-25 22:21:09 +08:00
parent fb2f3461c2
commit 785177bed4
6 changed files with 416 additions and 28 deletions

View File

@ -170,6 +170,7 @@ function createWorkflowOptionDraft(
enabled: true,
sortOrder: optionIndex,
promptFragment: `请按选项${optionIndex + 1}执行。`,
blockedOptionKeys: [],
children: [],
};
}
@ -212,6 +213,27 @@ function collectWorkflowOptionKeys(options: AgentWorkflowOptionUiConfig[]): stri
return flattenAgentWorkflowOptionTree(options).map(({ option }) => option.key);
}
function collectWorkflowOptionRelations(
options: AgentWorkflowOptionUiConfig[],
ancestors: string[] = []
): Array<{
key: string;
label: string;
depth: number;
}> {
return options.flatMap((option) => {
const currentPath = [...ancestors, option.buttonName || option.key];
return [
{
key: option.key,
label: currentPath.join(" / "),
depth: currentPath.length,
},
...collectWorkflowOptionRelations(option.children || [], currentPath),
];
});
}
function updateWorkflowOptionTree(
options: AgentWorkflowOptionUiConfig[],
optionKey: string,
@ -1212,6 +1234,20 @@ export function AgentWorkflowSettingsClient({
(left, right) => left.sortOrder - right.sortOrder
);
const canAddChild = depth < AGENT_WORKFLOW_MAX_OPTION_DEPTH;
const optionRelations = collectWorkflowOptionRelations(step.options || []);
const optionLookup = new Map(optionRelations.map((item) => [item.key, item] as const));
const blockedCandidates = optionRelations.filter((item) => item.key !== option.key);
const selectedBlockedItems = (option.blockedOptionKeys || [])
.map((blockedKey) => optionLookup.get(blockedKey))
.filter(
(
item
): item is {
key: string;
label: string;
depth: number;
} => Boolean(item)
);
return (
<div key={option.key} className={cn("space-y-4", depth > 1 && "pl-4 sm:pl-6")}>
@ -1327,6 +1363,101 @@ export function AgentWorkflowSettingsClient({
className="resize-none text-xs"
/>
</div>
<div className="space-y-2 rounded-lg border border-dashed bg-muted/20 p-3">
<div className="space-y-1">
<Label className="text-xs">互斥卡片限制</Label>
<p className="text-[11px] leading-5 text-muted-foreground">
选中当前卡片后,下方指定的其它卡片会在前台变灰并不可选。
</p>
</div>
{selectedBlockedItems.length > 0 ? (
<div className="flex flex-wrap gap-2">
{selectedBlockedItems.map((blockedItem) => (
<span
key={blockedItem.key}
className="inline-flex items-center gap-2 rounded-full border bg-background px-3 py-1.5 text-xs"
>
<span className="text-muted-foreground">
{getWorkflowLevelLabel(blockedItem.depth, "卡片")}
</span>
<span className="max-w-[220px] truncate">{blockedItem.label}</span>
<button
type="button"
className="rounded-full p-0.5 text-muted-foreground transition-colors hover:bg-muted hover:text-destructive"
onClick={() =>
updateOption(step.key, option.key, (currentOption) => ({
...currentOption,
blockedOptionKeys: (currentOption.blockedOptionKeys || []).filter(
(item) => item !== blockedItem.key
),
}))
}
aria-label={`移除互斥卡片 ${blockedItem.label}`}
>
<Trash2 className="h-3 w-3" />
</button>
</span>
))}
</div>
) : (
<div className="rounded-md border border-dashed bg-background/70 px-3 py-2 text-xs text-muted-foreground">
当前还没有配置互斥卡片
</div>
)}
<div className="max-h-44 space-y-2 overflow-y-auto rounded-md border bg-background/70 p-2">
{blockedCandidates.length > 0 ? (
blockedCandidates.map((candidate) => {
const active = (option.blockedOptionKeys || []).includes(candidate.key);
return (
<button
key={candidate.key}
type="button"
className={cn(
"flex w-full items-center justify-between rounded-md border px-3 py-2 text-left text-xs transition-colors",
active
? "border-amber-500/50 bg-amber-500/10 text-foreground"
: "border-border/70 bg-background hover:border-foreground/20 hover:bg-muted/60"
)}
onClick={() =>
updateOption(step.key, option.key, (currentOption) => {
const currentBlocked = new Set(
currentOption.blockedOptionKeys || []
);
if (currentBlocked.has(candidate.key)) {
currentBlocked.delete(candidate.key);
} else {
currentBlocked.add(candidate.key);
}
return {
...currentOption,
blockedOptionKeys: Array.from(currentBlocked),
};
})
}
>
<span className="min-w-0">
<span className="block truncate font-medium">
{candidate.label}
</span>
<span className="block text-[10px] text-muted-foreground">
{getWorkflowLevelLabel(candidate.depth, "卡片")}
</span>
</span>
<Badge variant={active ? "default" : "outline"}>
{active ? "已限制" : "可限制"}
</Badge>
</button>
);
})
) : (
<div className="px-2 py-3 text-center text-xs text-muted-foreground">
当前步骤下没有其他可限制卡片
</div>
)}
</div>
</div>
</div>
</div>
</div>

View File

@ -2880,6 +2880,57 @@ 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",
]);
function normalizePromptLineForCompare(line: string) {
return String(line || "")
.replace(/\s+/g, " ")
@ -2893,6 +2944,14 @@ 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 {
@ -2912,13 +2971,51 @@ function parsePromptLineLabel(line: string) {
return { label: "", value: normalized };
}
function normalizePromptLabel(label: string) {
return String(label || "")
.replace(/\s+/g, "")
.replace(/[::]/g, "")
.trim();
}
function getPromptSemanticLabel(label: string) {
const normalized = normalizePromptLabel(label);
return AGENT_PROMPT_LABEL_SEMANTIC_ALIASES.get(normalized) || normalized;
}
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);
}
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);
@ -2934,6 +3031,9 @@ function dedupeAgentSlotPromptAgainstWorkflow(params: {
.filter(Boolean);
normalizedWorkflowLines.forEach((line) => {
const parsed = parsePromptLineLabel(line);
if (parsed.label) {
workflowSemanticLabels.add(getPromptSemanticLabel(parsed.label));
}
if (parsed.label && AGENT_PROMPT_STRUCTURAL_LABELS.has(parsed.label)) {
workflowStructuralValues.set(
parsed.label,
@ -2947,12 +3047,26 @@ function dedupeAgentSlotPromptAgainstWorkflow(params: {
.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);
@ -2972,6 +3086,8 @@ function buildAgentSlotPrompt(params: {
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 || "");
@ -2979,12 +3095,13 @@ function buildAgentSlotPrompt(params: {
lines.push(workflowPrompt);
}
const slotTitle = String(params.slotTitle || "").trim();
if (slotTitle) {
if (params.includeSlotTitle !== false && slotTitle) {
lines.push(`【槽位】${slotTitle}`);
}
const dedupedSlotPrompt = dedupeAgentSlotPromptAgainstWorkflow({
workflowPrompt,
slotPrompt: params.slotPrompt,
modeType: params.modeType || "generic",
});
if (dedupedSlotPrompt) {
lines.push(dedupedSlotPrompt);
@ -2996,6 +3113,22 @@ function buildAgentSlotPrompt(params: {
return lines.join("\n\n").trim();
}
function buildAgentSceneWorkflowPrompt(params: {
workflowPrompt?: string | null;
userSupplementPrompt?: string | null;
}) {
const lines: string[] = [];
const workflowPrompt = String(params.workflowPrompt || "").trim();
if (workflowPrompt) {
lines.push(workflowPrompt);
}
const supplement = String(params.userSupplementPrompt || "").trim();
if (supplement) {
lines.push(`【补充要求】${supplement}`);
}
return lines.join("\n\n").trim();
}
type AgentReplacementModePayload = NonNullable<PostRequestBody["agentModePayload"]>;
type ReplacementReferenceDecision = {
@ -6633,6 +6766,12 @@ export async function POST(request: Request) {
replacementSlot?.promptTemplate || selectedAgentPreset.promptTemplate,
slotTitle: selectedAgentPreset.name,
userSupplementPrompt: sanitizedUserSupplementPrompt || null,
includeSlotTitle: false,
modeType: isDetailReplacementPreset
? "detail_replacement"
: isSkuReplacementPreset
? "sku_replacement"
: "generic",
});
if (productInputImageUrls.length === 0) {
@ -7082,12 +7221,18 @@ export async function POST(request: Request) {
[...sharedUploadedAssetIds, ...(sceneSlot?.referenceAssetIds || [])].filter(Boolean),
),
);
const scenePrompt = buildAgentSlotPrompt({
const scenePrompt =
buildAgentSceneWorkflowPrompt({
workflowPrompt: hiddenPromptText || null,
userSupplementPrompt: sanitizedUserSupplementPrompt || null,
}) ||
buildAgentSlotPrompt({
workflowPrompt: hiddenPromptText || null,
slotPrompt: sceneSlot?.promptTemplate || selectedAgentPreset.promptTemplate,
slotTitle: selectedAgentPreset.name,
userSupplementPrompt: sanitizedUserSupplementPrompt || null,
});
includeSlotTitle: false,
modeType: "scene_generation",
});
const sceneGenerationStartedAt = Date.now();
try {
@ -7113,6 +7258,9 @@ export async function POST(request: Request) {
resolvedImageCount,
sourceImageCount: sourceImageUrls.length,
sourceAssetCount: sourceAssetIds.length,
promptStrategy: hiddenPromptText || sanitizedUserSupplementPrompt
? "workflow_only"
: "legacy_template_fallback",
promptLength: String(scenePrompt || `生成${selectedAgentPreset.name}`).length,
aspectRatio:
typeof mergedImageParams.aspectRatio === "string"
@ -7326,6 +7474,7 @@ export async function POST(request: Request) {
slotPrompt: slot.promptTemplate,
slotTitle: slot.title || slot.uploadLabel || null,
userSupplementPrompt: sanitizedUserSupplementPrompt || null,
modeType: "generic",
});
const assistantMessageId = generateUUID();

View File

@ -724,6 +724,10 @@ const updateSchema = z.object({
enabled: z.boolean().optional(),
sortOrder: z.number().min(0).max(999).optional(),
promptFragment: z.string().trim().max(1000).nullable().optional(),
blockedOptionKeys: z
.array(z.string().trim().min(1).max(64))
.max(100)
.optional(),
children: z.array(z.any()).max(100).optional(),
})
)

View File

@ -12,6 +12,7 @@ import {
defaultAgentWorkflowUiConfig,
flattenAgentWorkflowSteps,
flattenVisibleAgentWorkflowSteps,
getAgentWorkflowBlockedOptionKeys,
getAgentWorkflowBrandStoreEntries,
getAgentWorkflowPriceBandOptions,
getAgentWorkflowSelectedOptionPath,
@ -159,31 +160,37 @@ function StepOptionCard({
option,
step,
selected,
disabled = false,
onClick,
index,
}: {
option: { key: string; buttonName: string; description?: string | null; previewImageUrl?: string | null };
step: { title: string };
selected: boolean;
disabled?: boolean;
onClick: () => void;
index: number;
}) {
return (
<motion.button
type="button"
disabled={disabled}
onClick={onClick}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: index * 0.05 }}
whileHover={{ y: -4, scale: 1.02 }}
whileTap={{ scale: 0.98 }}
whileHover={disabled ? undefined : { y: -4, scale: 1.02 }}
whileTap={disabled ? undefined : { scale: 0.98 }}
className={cn(
"group relative overflow-hidden rounded-2xl text-left",
"aspect-[16/10] w-full",
"transition-all duration-500",
disabled && "cursor-not-allowed opacity-45 saturate-0",
selected
? "ring-2 ring-white/30 shadow-[0_0_30px_rgba(255,255,255,0.1)]"
: "ring-1 ring-white/12 hover:ring-white/24"
: disabled
? "ring-1 ring-white/8"
: "ring-1 ring-white/12 hover:ring-white/24"
)}
>
{/* 背景 */}
@ -193,7 +200,10 @@ function StepOptionCard({
<img
src={option.previewImageUrl}
alt={option.buttonName}
className="h-full w-full object-cover transition-transform duration-700 group-hover:scale-110"
className={cn(
"h-full w-full object-cover transition-transform duration-700",
!disabled && "group-hover:scale-110"
)}
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/40 to-transparent" />
</>
@ -215,6 +225,12 @@ function StepOptionCard({
</motion.div>
)}
{disabled && !selected ? (
<div className="absolute right-3 top-3 rounded-full bg-black/65 px-2 py-1 text-[10px] font-medium text-white/80 backdrop-blur-md">
已限制
</div>
) : null}
{/* 步骤标签 */}
<div className="absolute left-3 top-3">
<span className="rounded-full bg-black/60 px-2.5 py-1 text-[10px] font-medium text-white/80 backdrop-blur-md">
@ -389,6 +405,18 @@ export function AgentWorkflowPanelDarkV2({
]);
return Object.fromEntries(entries) as Record<string, string>;
}, [resolvedConfig.steps, selections]);
const blockedOptionKeyMap = useMemo(() => {
const entries = flattenAgentWorkflowSteps(resolvedConfig.steps).map(({ step }) => [
step.key,
new Set(
getAgentWorkflowBlockedOptionKeys({
step,
selectionValue: selections[step.key],
})
),
] as const);
return new Map(entries);
}, [resolvedConfig.steps, selections]);
const [activeEmbeddedStepKey, setActiveEmbeddedStepKey] = useState<string>("");
const [activeEmbeddedOptionDepth, setActiveEmbeddedOptionDepth] = useState(1);
const [pendingAutoAdvanceStepKey, setPendingAutoAdvanceStepKey] = useState<string | null>(null);
@ -519,6 +547,7 @@ export function AgentWorkflowPanelDarkV2({
const currentSelectedKey = selectedPathKeys[depth - 1] || "";
const currentSelectedOption =
options.find((option) => option.key === currentSelectedKey) || null;
const blockedKeys = blockedOptionKeyMap.get(step.key) || new Set<string>();
const visibleChildren = getEnabledAgentWorkflowOptionTree(
currentSelectedOption?.children || []
);
@ -538,25 +567,37 @@ export function AgentWorkflowPanelDarkV2({
</div>
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
{options.map((option, optionIndex) => (
<StepOptionCard
key={`${option.key}-${depth}`}
option={option}
step={step}
selected={currentSelectedKey === option.key}
onClick={() => {
const prefix = parentPath.slice(0, depth - 1);
const nextKeys =
currentSelectedKey === option.key ? prefix : [...prefix, option.key];
setPendingAutoAdvanceStepKey(step.key);
onSelectionsChange({
...selections,
[step.key]: nextKeys.length > 0 ? serializeAgentWorkflowSelectionPath(nextKeys) : undefined,
});
}}
index={optionIndex}
/>
))}
{options.map((option, optionIndex) => {
const isDisabled =
currentSelectedKey !== option.key && blockedKeys.has(option.key);
return (
<StepOptionCard
key={`${option.key}-${depth}`}
option={option}
step={step}
selected={currentSelectedKey === option.key}
disabled={isDisabled}
onClick={() => {
if (isDisabled) {
return;
}
const prefix = parentPath.slice(0, depth - 1);
const nextKeys =
currentSelectedKey === option.key ? prefix : [...prefix, option.key];
setPendingAutoAdvanceStepKey(step.key);
onSelectionsChange({
...selections,
[step.key]:
nextKeys.length > 0
? serializeAgentWorkflowSelectionPath(nextKeys)
: undefined,
});
}}
index={optionIndex}
/>
);
})}
</div>
{currentSelectedOption && visibleChildren.length > 0
@ -748,6 +789,11 @@ export function AgentWorkflowPanelDarkV2({
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
{activeEmbeddedOptionLevel.options.map((option, optionIndex) => {
const currentSelectedKey = activeEmbeddedOptionLevel.selectedKey;
const blockedKeys =
blockedOptionKeyMap.get(activeEmbeddedStepEntry.step.key) ||
new Set<string>();
const isDisabled =
currentSelectedKey !== option.key && blockedKeys.has(option.key);
const nextChildren = getEnabledAgentWorkflowOptionTree(
option.children || []
);
@ -758,7 +804,11 @@ export function AgentWorkflowPanelDarkV2({
option={option}
step={activeEmbeddedStepEntry.step}
selected={currentSelectedKey === option.key}
disabled={isDisabled}
onClick={() => {
if (isDisabled) {
return;
}
const selectedPathKeys = activeEmbeddedOptionLevels
.slice(0, activeEmbeddedOptionLevel.depth - 1)
.map((level) => level.selectedKey)

View File

@ -23,6 +23,10 @@ function cloneOption(
description: option.description || "",
previewImageUrl: option.previewImageUrl || "",
promptFragment: option.promptFragment || "",
blockedOptionKeys: Array.from(new Set((option.blockedOptionKeys || []).filter(Boolean))).slice(
0,
100
),
children: (option.children || []).map(cloneOption),
};
}
@ -67,6 +71,7 @@ function createOption(
enabled: true,
sortOrder,
promptFragment,
blockedOptionKeys: [],
children: [],
};
}
@ -349,6 +354,37 @@ function normalizeNumber(value: unknown, fallback: number, max = 999) {
return Math.max(0, Math.min(max, Math.round(parsed)));
}
function normalizeKeyList(value: unknown, max = 100) {
if (!Array.isArray(value)) return [] as string[];
return Array.from(
new Set(
value
.map((item, index) => normalizeKey(item, `blocked_option_${index + 1}`))
.filter(Boolean)
.slice(0, max)
)
);
}
function sanitizeBlockedOptionTree(
options: AgentWorkflowOptionUiConfig[],
validKeys = new Set(
flattenAgentWorkflowOptionTree(options).map(({ option }) => option.key)
)
): AgentWorkflowOptionUiConfig[] {
return options.map((option) => ({
...option,
blockedOptionKeys: Array.from(
new Set(
(option.blockedOptionKeys || []).filter(
(blockedKey) => blockedKey !== option.key && validKeys.has(blockedKey)
)
)
).slice(0, 100),
children: sanitizeBlockedOptionTree(option.children || [], validKeys),
}));
}
export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiConfig {
const raw =
value && typeof value === "object" && !Array.isArray(value)
@ -456,6 +492,7 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
normalizeText(optionRecord.promptFragment, 1000) ||
defaultOption?.promptFragment ||
buttonName;
const hasRawBlockedOptionKeys = Array.isArray(optionRecord.blockedOptionKeys);
const hasRawChildren = Array.isArray(optionRecord.children);
const rawChildren = hasRawChildren ? (optionRecord.children as unknown[]) : [];
const defaultChildren =
@ -479,6 +516,9 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
defaultOption?.sortOrder ?? optionIndex
),
promptFragment,
blockedOptionKeys: hasRawBlockedOptionKeys
? normalizeKeyList(optionRecord.blockedOptionKeys, 100)
: normalizeKeyList(defaultOption?.blockedOptionKeys, 100),
children:
depth < AGENT_WORKFLOW_MAX_OPTION_DEPTH
? childSource
@ -529,6 +569,7 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
normalizeOption(option, optionIndex, stepKey, usedOptionKeys, defaultOptions, 1)
)
.sort((left, right) => left.sortOrder - right.sortOrder);
const sanitizedOptions = sanitizeBlockedOptionTree(options);
const hasRawChildren = Array.isArray(stepRecord?.children);
const rawChildren = hasRawChildren ? (stepRecord?.children as unknown[]) : [];
@ -547,7 +588,7 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
description: normalizeText(stepRecord?.description, 300) || fallbackDescription,
enabled: normalizeBoolean(stepRecord?.enabled, defaultStep?.enabled ?? true),
sortOrder: normalizeNumber(stepRecord?.sortOrder, defaultStep?.sortOrder ?? stepIndex),
options,
options: sanitizedOptions,
children:
depth < AGENT_WORKFLOW_MAX_TREE_DEPTH
? childSource
@ -638,6 +679,18 @@ export function getAgentWorkflowSelectedOptionPath(params: {
return selectedOptions;
}
export function getAgentWorkflowBlockedOptionKeys(params: {
step: AgentWorkflowStepUiConfig;
selectionValue?: string | null;
}) {
const selectedOptions = getAgentWorkflowSelectedOptionPath(params);
return Array.from(
new Set(
selectedOptions.flatMap((option) => (option.blockedOptionKeys || []).filter(Boolean))
)
);
}
export function isAgentWorkflowStepSelectionComplete(params: {
step: AgentWorkflowStepUiConfig;
selectionValue?: string | null;

View File

@ -87,6 +87,7 @@ export interface AgentWorkflowOptionUiConfig {
enabled: boolean;
sortOrder: number;
promptFragment?: string | null;
blockedOptionKeys?: string[];
children?: AgentWorkflowOptionUiConfig[];
}