fix: support global workflow 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-26 12:15:53 +08:00
parent 1768413eca
commit 04428bf6b2
3 changed files with 84 additions and 32 deletions

View File

@ -234,6 +234,27 @@ function collectWorkflowOptionRelations(
});
}
function collectWorkflowOptionRelationsFromSteps(
steps: AgentWorkflowStepUiConfig[]
): Array<{
key: string;
label: string;
depth: number;
stepKey: string;
stepTitle: string;
}> {
return flattenAgentWorkflowSteps(steps).flatMap(({ step }) =>
collectWorkflowOptionRelations(step.options || []).map((item) => ({
...item,
label: `${step.title} / ${item.label}`,
stepKey: step.key,
stepTitle: step.title,
}))
);
}
type WorkflowOptionRelation = ReturnType<typeof collectWorkflowOptionRelationsFromSteps>[number];
function updateWorkflowOptionTree(
options: AgentWorkflowOptionUiConfig[],
optionKey: string,
@ -1234,20 +1255,12 @@ export function AgentWorkflowSettingsClient({
(left, right) => left.sortOrder - right.sortOrder
);
const canAddChild = depth < AGENT_WORKFLOW_MAX_OPTION_DEPTH;
const optionRelations = collectWorkflowOptionRelations(step.options || []);
const optionRelations = collectWorkflowOptionRelationsFromSteps(config.steps || []);
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)
);
.filter((item): item is WorkflowOptionRelation => Boolean(item));
return (
<div key={option.key} className={cn("space-y-4", depth > 1 && "pl-4 sm:pl-6")}>
@ -1367,7 +1380,7 @@ export function AgentWorkflowSettingsClient({
<div className="space-y-1">
<Label className="text-xs">互斥卡片限制</Label>
<p className="text-[11px] leading-5 text-muted-foreground">
选中当前卡片后,下方指定的其它卡片会在前台变灰并不可选。
选中当前卡片后,下方指定的其它卡片会在前台变灰并不可选。可跨所有流程节点,并覆盖各节点下的所有层级子卡片。
</p>
</div>
@ -1453,7 +1466,7 @@ export function AgentWorkflowSettingsClient({
})
) : (
<div className="px-2 py-3 text-center text-xs text-muted-foreground">
当前步骤下没有其他可限制卡片
当前工作流下没有其他可限制卡片
</div>
)}
</div>

View File

@ -12,7 +12,7 @@ import {
defaultAgentWorkflowUiConfig,
flattenAgentWorkflowSteps,
flattenVisibleAgentWorkflowSteps,
getAgentWorkflowBlockedOptionKeys,
getAgentWorkflowBlockedOptionKeysForSelections,
getAgentWorkflowBrandStoreEntries,
getAgentWorkflowPriceBandOptions,
getAgentWorkflowSelectedOptionPath,
@ -405,18 +405,16 @@ 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,
const blockedOptionKeys = useMemo(
() =>
new Set(
getAgentWorkflowBlockedOptionKeys({
step,
selectionValue: selections[step.key],
getAgentWorkflowBlockedOptionKeysForSelections({
config: resolvedConfig,
selections,
})
),
] as const);
return new Map(entries);
}, [resolvedConfig.steps, selections]);
[resolvedConfig, selections]
);
const [autoAdvanceNotice, setAutoAdvanceNotice] = useState<{
completedStepTitle: string;
completedSummary: string;
@ -571,7 +569,6 @@ 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 || []
);
@ -593,7 +590,7 @@ export function AgentWorkflowPanelDarkV2({
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
{options.map((option, optionIndex) => {
const isDisabled =
currentSelectedKey !== option.key && blockedKeys.has(option.key);
currentSelectedKey !== option.key && blockedOptionKeys.has(option.key);
return (
<StepOptionCard
@ -857,11 +854,9 @@ 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);
currentSelectedKey !== option.key &&
blockedOptionKeys.has(option.key);
const nextChildren = getEnabledAgentWorkflowOptionTree(
option.children || []
);

View File

@ -1,6 +1,7 @@
import type {
AgentWorkflowBrandStoreEntry,
AgentWorkflowOptionUiConfig,
AgentWorkflowStepKey,
AgentWorkflowStepUiConfig,
AgentWorkflowUiConfig,
} from "@/types/agent";
@ -290,6 +291,16 @@ export function countAgentWorkflowOptionTree(
return flattenAgentWorkflowOptionTree(options).length;
}
function collectWorkflowOptionKeySetFromSteps(
steps: AgentWorkflowStepUiConfig[]
): Set<string> {
return new Set(
flattenAgentWorkflowSteps(steps).flatMap(({ step }) =>
flattenAgentWorkflowOptionTree(step.options || []).map(({ option }) => option.key)
)
);
}
export function normalizeAgentWorkflowOptionOrdering(
options: AgentWorkflowOptionUiConfig[]
): AgentWorkflowOptionUiConfig[] {
@ -385,6 +396,17 @@ function sanitizeBlockedOptionTree(
}));
}
function sanitizeBlockedWorkflowStepTree(
steps: AgentWorkflowStepUiConfig[],
validKeys: Set<string>
): AgentWorkflowStepUiConfig[] {
return steps.map((step) => ({
...step,
options: sanitizeBlockedOptionTree(step.options || [], validKeys),
children: sanitizeBlockedWorkflowStepTree(step.children || [], validKeys),
}));
}
export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiConfig {
const raw =
value && typeof value === "object" && !Array.isArray(value)
@ -569,8 +591,6 @@ 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[]) : [];
const defaultChildren =
@ -588,7 +608,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: sanitizedOptions,
options,
children:
depth < AGENT_WORKFLOW_MAX_TREE_DEPTH
? childSource
@ -601,6 +621,8 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
const normalizedSteps = sourceSteps
.map((item, stepIndex) => normalizeStep(item, stepIndex, 1))
.sort((left, right) => left.sortOrder - right.sortOrder);
const validOptionKeys = collectWorkflowOptionKeySetFromSteps(normalizedSteps);
const sanitizedSteps = sanitizeBlockedWorkflowStepTree(normalizedSteps, validOptionKeys);
return {
inputOptions: {
@ -617,7 +639,7 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
priceBands: normalizedPriceBands,
brandStoreEntries: fallbackBrandStoreEntries,
},
steps: normalizedSteps,
steps: sanitizedSteps,
};
}
@ -691,6 +713,28 @@ export function getAgentWorkflowBlockedOptionKeys(params: {
);
}
export function getAgentWorkflowBlockedOptionKeysForSelections(params: {
config?: AgentWorkflowUiConfig | null;
selections: Partial<Record<AgentWorkflowStepKey, string>>;
}) {
const config = params.config || defaultAgentWorkflowUiConfig;
const visibleSteps = flattenVisibleAgentWorkflowSteps(
getEnabledAgentWorkflowStepTree(Array.isArray(config.steps) ? config.steps : []),
params.selections
).map(({ step }) => step);
return Array.from(
new Set(
visibleSteps.flatMap((step) =>
getAgentWorkflowBlockedOptionKeys({
step,
selectionValue: params.selections[step.key],
})
)
)
);
}
export function isAgentWorkflowStepSelectionComplete(params: {
step: AgentWorkflowStepUiConfig;
selectionValue?: string | null;