+
+
{countAgentWorkflowSteps(sortedSteps)} 模块
{countAgentWorkflowOptions(sortedSteps)} 选项
+
+ {workflowConstraintSettings.crossLevelMutualExclusionEnabled ? "跨级互斥已开启" : "跨级互斥已关闭"}
+
+
+
+
+
+
跨级互斥
+
+ 关闭时仅允许同级卡片互斥;开启后任意级别卡片都可以互相限制。
+
+
+
+ setConfig((prev) => ({
+ ...prev,
+ constraintSettings: {
+ ...(prev.constraintSettings || defaultAgentWorkflowConstraintSettings),
+ crossLevelMutualExclusionEnabled: checked,
+ },
+ }))
+ }
+ />
+
+
-
{sortedSteps.length > 0 ? (
diff --git a/app/api/site-settings/route.ts b/app/api/site-settings/route.ts
index 464b1fcbf..48ca2340f 100644
--- a/app/api/site-settings/route.ts
+++ b/app/api/site-settings/route.ts
@@ -817,6 +817,11 @@ const updateSchema = z.object({
.optional(),
})
.optional(),
+ constraintSettings: z
+ .object({
+ crossLevelMutualExclusionEnabled: z.boolean().optional(),
+ })
+ .optional(),
steps: z
.array(
z.object({
diff --git a/app/canvas/[id]/page.tsx b/app/canvas/[id]/page.tsx
index c6e95a8db..5002a54f2 100644
--- a/app/canvas/[id]/page.tsx
+++ b/app/canvas/[id]/page.tsx
@@ -88,6 +88,7 @@ import { cn, fetcher } from "@/lib/utils";
import {
defaultAgentWorkflowUiConfig,
flattenVisibleAgentWorkflowSteps,
+ getEnabledAgentWorkflowStepTree,
normalizeAgentWorkflowUiConfig,
sanitizeAgentWorkflowSelections,
} from "@/lib/agents/workflow-ui-config";
@@ -1755,7 +1756,9 @@ export default function CanvasEditorPage({ params }: PageProps) {
});
const orderedStepKeys = flattenVisibleAgentWorkflowSteps(
- (resolvedWorkflowUiConfig || defaultAgentWorkflowUiConfig).steps,
+ getEnabledAgentWorkflowStepTree(
+ (resolvedWorkflowUiConfig || defaultAgentWorkflowUiConfig).steps
+ ),
normalizedCurrentSelections
).map(({ step }) => step.key);
const earliestChangedIndex = orderedStepKeys.findIndex((stepKey) => {
diff --git a/components/agents/AgentWorkflowPanelDarkV2.tsx b/components/agents/AgentWorkflowPanelDarkV2.tsx
index 972cf7568..9164285a8 100644
--- a/components/agents/AgentWorkflowPanelDarkV2.tsx
+++ b/components/agents/AgentWorkflowPanelDarkV2.tsx
@@ -23,6 +23,8 @@ import {
getAgentWorkflowStoreOptionsByBrand,
getEnabledAgentWorkflowOptionTree,
getEnabledAgentWorkflowStepTree,
+ getRenderableAgentWorkflowOptionTree,
+ getRenderableAgentWorkflowStepTree,
isAgentWorkflowStepSelectionComplete,
serializeAgentWorkflowSelectionPath,
} from "@/lib/agents/workflow-ui-config";
@@ -135,7 +137,7 @@ function getWorkflowStepOptionLevels(params: {
});
let depth = 1;
- let currentOptions = getEnabledAgentWorkflowOptionTree(params.step.options || []);
+ let currentOptions = getRenderableAgentWorkflowOptionTree(params.step.options || []);
while (currentOptions.length > 0) {
const selectedKey = selectedPath[depth - 1]?.key || "";
@@ -149,7 +151,7 @@ function getWorkflowStepOptionLevels(params: {
selectedOption,
});
- const nextOptions = getEnabledAgentWorkflowOptionTree(
+ const nextOptions = getRenderableAgentWorkflowOptionTree(
selectedOption?.children || []
);
if (!selectedOption || nextOptions.length === 0) {
@@ -176,6 +178,7 @@ function StepOptionCard({
step,
selected,
disabled = false,
+ disabledReason = null,
onClick,
index,
}: {
@@ -183,6 +186,7 @@ function StepOptionCard({
step: { title: string };
selected: boolean;
disabled?: boolean;
+ disabledReason?: "blocked" | "developing" | null;
onClick: () => void;
index: number;
}) {
@@ -242,7 +246,7 @@ function StepOptionCard({
{disabled && !selected ? (
- 已限制
+ {disabledReason === "developing" ? "开发中" : "已限制"}
) : null}
@@ -595,15 +599,23 @@ export function AgentWorkflowPanelDarkV2({
const resolvedConfig = useMemo(() => {
const source = config || defaultAgentWorkflowUiConfig;
return {
+ ...source,
steps: getEnabledAgentWorkflowStepTree(source.steps || []),
};
}, [config]);
+ const displayConfig = useMemo(() => {
+ const source = config || defaultAgentWorkflowUiConfig;
+ return {
+ ...source,
+ steps: getRenderableAgentWorkflowStepTree(source.steps || []),
+ };
+ }, [config]);
const flattenedResolvedSteps = useMemo(
() =>
- flattenVisibleAgentWorkflowSteps(resolvedConfig.steps, selections).filter(
+ flattenVisibleAgentWorkflowSteps(displayConfig.steps, selections).filter(
({ step }) => Array.isArray(step.options) && step.options.length > 0
),
- [resolvedConfig.steps, selections]
+ [displayConfig.steps, selections]
);
const summary = useMemo(
@@ -675,6 +687,27 @@ export function AgentWorkflowPanelDarkV2({
activeEmbeddedOptionLevels[activeEmbeddedOptionLevels.length - 1] ||
activeEmbeddedOptionLevels[0] ||
null;
+ const activeEmbeddedDeepestSelectedOption = useMemo(
+ () =>
+ [...activeEmbeddedOptionLevels]
+ .reverse()
+ .find((level) => level.selectedOption)?.selectedOption || null,
+ [activeEmbeddedOptionLevels]
+ );
+ const activeEmbeddedSelectedOptionHasDevelopingChildren = useMemo(() => {
+ if (!activeEmbeddedDeepestSelectedOption) {
+ return false;
+ }
+
+ const renderableChildren = getRenderableAgentWorkflowOptionTree(
+ activeEmbeddedDeepestSelectedOption.children || []
+ );
+ if (renderableChildren.length === 0) {
+ return false;
+ }
+
+ return getEnabledAgentWorkflowOptionTree(activeEmbeddedDeepestSelectedOption.children || []).length === 0;
+ }, [activeEmbeddedDeepestSelectedOption]);
useEffect(() => {
if (!embedded) {
@@ -742,6 +775,11 @@ export function AgentWorkflowPanelDarkV2({
return;
}
+ if (activeEmbeddedSelectedOptionHasDevelopingChildren) {
+ setPendingAutoAdvanceStepKey(null);
+ return;
+ }
+
const nextStep = flattenedResolvedSteps
.slice(activeEmbeddedStepIndex + 1)
.find(
@@ -768,6 +806,7 @@ export function AgentWorkflowPanelDarkV2({
activeEmbeddedStepEntry,
activeEmbeddedStepIndex,
activeEmbeddedStepKey,
+ activeEmbeddedSelectedOptionHasDevelopingChildren,
embedded,
flattenedResolvedSteps,
pendingAutoAdvanceStepKey,
@@ -800,7 +839,7 @@ export function AgentWorkflowPanelDarkV2({
const currentSelectedKey = selectedPathKeys[depth - 1] || "";
const currentSelectedOption =
options.find((option) => option.key === currentSelectedKey) || null;
- const visibleChildren = getEnabledAgentWorkflowOptionTree(
+ const visibleChildren = getRenderableAgentWorkflowOptionTree(
currentSelectedOption?.children || []
);
const levelLabel = depth === 1 ? "主卡片" : depth === 2 ? "二级卡片" : "三级卡片";
@@ -820,8 +859,12 @@ export function AgentWorkflowPanelDarkV2({
{options.map((option, optionIndex) => {
- const isDisabled =
- currentSelectedKey !== option.key && blockedOptionKeys.has(option.key);
+ const isDeveloping = option.enabled === false;
+ const isBlocked =
+ !isDeveloping &&
+ currentSelectedKey !== option.key &&
+ blockedOptionKeys.has(option.key);
+ const isDisabled = isDeveloping || isBlocked;
return (
{
if (isDisabled) {
return;
@@ -1077,10 +1121,13 @@ export function AgentWorkflowPanelDarkV2({
{activeEmbeddedOptionLevel.options.map((option, optionIndex) => {
const currentSelectedKey = activeEmbeddedOptionLevel.selectedKey;
- const isDisabled =
+ const isDeveloping = option.enabled === false;
+ const isBlocked =
+ !isDeveloping &&
currentSelectedKey !== option.key &&
blockedOptionKeys.has(option.key);
- const nextChildren = getEnabledAgentWorkflowOptionTree(
+ const isDisabled = isDeveloping || isBlocked;
+ const nextChildren = getRenderableAgentWorkflowOptionTree(
option.children || []
);
@@ -1091,6 +1138,9 @@ export function AgentWorkflowPanelDarkV2({
step={activeEmbeddedStepEntry.step}
selected={currentSelectedKey === option.key}
disabled={isDisabled}
+ disabledReason={
+ isDeveloping ? "developing" : isBlocked ? "blocked" : null
+ }
onClick={() => {
if (isDisabled) {
return;
@@ -1163,7 +1213,7 @@ export function AgentWorkflowPanelDarkV2({
{step.description && (
{step.description}
)}
- {renderOptionLevels(step, getEnabledAgentWorkflowOptionTree(step.options || []))}
+ {renderOptionLevels(step, getRenderableAgentWorkflowOptionTree(step.options || []))}
))
)}
diff --git a/lib/agents/workflow-ui-config.ts b/lib/agents/workflow-ui-config.ts
index e77521087..9897a0130 100644
--- a/lib/agents/workflow-ui-config.ts
+++ b/lib/agents/workflow-ui-config.ts
@@ -1,6 +1,7 @@
import type {
AgentWorkflowBrandStoreEntry,
AgentWorkflowCategoryLabels,
+ AgentWorkflowConstraintSettings,
AgentWorkflowFourthCategoryEntry,
AgentWorkflowOptionUiConfig,
AgentWorkflowStoreEntry,
@@ -20,6 +21,9 @@ export const defaultAgentWorkflowCategoryLabels: AgentWorkflowCategoryLabels = {
fourthCategory: "四级分类",
fifthCategory: "五级分类",
};
+export const defaultAgentWorkflowConstraintSettings: AgentWorkflowConstraintSettings = {
+ crossLevelMutualExclusionEnabled: false,
+};
export interface AgentWorkflowBrandCategoryOption {
value: string;
@@ -163,6 +167,7 @@ export const defaultAgentWorkflowUiConfig: AgentWorkflowUiConfig = {
priceBands: [],
brandStoreEntries: [],
},
+ constraintSettings: defaultAgentWorkflowConstraintSettings,
steps: defaultSteps.map((step, index) =>
cloneStep({
...step,
@@ -208,6 +213,22 @@ export function normalizeAgentWorkflowCategoryLabels(
};
}
+export function normalizeAgentWorkflowConstraintSettings(
+ value: unknown
+): AgentWorkflowConstraintSettings {
+ const record =
+ value && typeof value === "object" && !Array.isArray(value)
+ ? (value as Record
)
+ : {};
+
+ return {
+ crossLevelMutualExclusionEnabled: normalizeBoolean(
+ record.crossLevelMutualExclusionEnabled ?? record.enableCrossLevelMutualExclusion,
+ defaultAgentWorkflowConstraintSettings.crossLevelMutualExclusionEnabled
+ ),
+ };
+}
+
export function flattenAgentWorkflowSteps(
steps: AgentWorkflowStepUiConfig[],
depth = 1,
@@ -318,6 +339,35 @@ export function getEnabledAgentWorkflowStepTree(
.filter((step) => step.options.length > 0 || (step.children || []).length > 0);
}
+export function getRenderableAgentWorkflowOptionTree(
+ options: AgentWorkflowOptionUiConfig[]
+): AgentWorkflowOptionUiConfig[] {
+ return [...options]
+ .sort((left, right) => left.sortOrder - right.sortOrder)
+ .map((option) => ({
+ ...option,
+ children: getRenderableAgentWorkflowOptionTree(option.children || []),
+ }));
+}
+
+export function getRenderableAgentWorkflowStepTree(
+ steps: AgentWorkflowStepUiConfig[]
+): AgentWorkflowStepUiConfig[] {
+ return [...steps]
+ .sort((left, right) => left.sortOrder - right.sortOrder)
+ .filter((step) => step.enabled !== false)
+ .map((step) => ({
+ ...step,
+ options: getRenderableAgentWorkflowOptionTree(step.options || []),
+ children: getRenderableAgentWorkflowStepTree(step.children || []),
+ }))
+ .filter(
+ (step) =>
+ getEnabledAgentWorkflowOptionTree(step.options || []).length > 0 ||
+ (step.children || []).length > 0
+ );
+}
+
export interface FlattenedAgentWorkflowOption {
option: AgentWorkflowOptionUiConfig;
depth: number;
@@ -980,6 +1030,11 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
priceBands: normalizedPriceBands,
brandStoreEntries: fallbackBrandStoreEntries,
},
+ constraintSettings: normalizeAgentWorkflowConstraintSettings(
+ raw.constraintSettings ?? {
+ crossLevelMutualExclusionEnabled: raw.crossLevelMutualExclusionEnabled,
+ }
+ ),
steps: sanitizedSteps,
};
}
@@ -994,6 +1049,12 @@ export function getAgentWorkflowCategoryLabels(
return normalizeAgentWorkflowCategoryLabels(config?.inputOptions?.categoryLabels);
}
+export function getAgentWorkflowConstraintSettings(
+ config?: AgentWorkflowUiConfig | null
+) {
+ return normalizeAgentWorkflowConstraintSettings(config?.constraintSettings);
+}
+
export function getAgentWorkflowCategoryOptionsByBrand(params: {
config?: AgentWorkflowUiConfig | null;
brand?: string | null;
@@ -1214,7 +1275,9 @@ export function getAgentWorkflowSelectedOptionPath(params: {
let currentOptions = params.step.options || [];
for (const key of keys) {
- const matchedOption = currentOptions.find((option) => option.key === key);
+ const matchedOption = currentOptions.find(
+ (option) => option.key === key && option.enabled !== false
+ );
if (!matchedOption) break;
selectedOptions.push(matchedOption);
currentOptions = matchedOption.children || [];
@@ -1262,11 +1325,35 @@ export function sanitizeAgentWorkflowSelections(params: {
export function getAgentWorkflowBlockedOptionKeys(params: {
step: AgentWorkflowStepUiConfig;
selectionValue?: string | null;
+ allowCrossLevel?: boolean;
+ optionDepthLookup?: Map;
}) {
const selectedOptions = getAgentWorkflowSelectedOptionPath(params);
+ const optionDepthLookup =
+ params.optionDepthLookup ||
+ new Map(
+ flattenAgentWorkflowOptionTree(params.step.options || []).map(({ option, depth }) => [
+ option.key,
+ depth,
+ ] as const)
+ );
return Array.from(
new Set(
- selectedOptions.flatMap((option) => (option.blockedOptionKeys || []).filter(Boolean))
+ selectedOptions.flatMap((option) => {
+ const currentDepth = optionDepthLookup.get(option.key);
+ return (option.blockedOptionKeys || []).filter((blockedKey) => {
+ if (!blockedKey) {
+ return false;
+ }
+ if (params.allowCrossLevel) {
+ return true;
+ }
+ if (typeof currentDepth !== "number") {
+ return false;
+ }
+ return optionDepthLookup.get(blockedKey) === currentDepth;
+ });
+ })
)
);
}
@@ -1294,10 +1381,19 @@ export function getAgentWorkflowBlockedOptionKeysForSelections(params: {
selections: Partial>;
}) {
const config = params.config || defaultAgentWorkflowUiConfig;
+ const constraintSettings = getAgentWorkflowConstraintSettings(config);
const visibleSteps = flattenVisibleAgentWorkflowSteps(
getEnabledAgentWorkflowStepTree(Array.isArray(config.steps) ? config.steps : []),
params.selections
).map(({ step }) => step);
+ const visibleOptionDepthLookup = new Map(
+ visibleSteps.flatMap((step) =>
+ flattenAgentWorkflowOptionTree(step.options || []).map(({ option, depth }) => [
+ option.key,
+ depth,
+ ] as const)
+ )
+ );
const directlyBlockedKeys = Array.from(
new Set(
@@ -1305,6 +1401,8 @@ export function getAgentWorkflowBlockedOptionKeysForSelections(params: {
getAgentWorkflowBlockedOptionKeys({
step,
selectionValue: params.selections[step.key],
+ allowCrossLevel: constraintSettings.crossLevelMutualExclusionEnabled,
+ optionDepthLookup: visibleOptionDepthLookup,
})
)
)
diff --git a/types/agent.ts b/types/agent.ts
index 958e00486..bdf99e230 100644
--- a/types/agent.ts
+++ b/types/agent.ts
@@ -101,6 +101,10 @@ export interface AgentWorkflowStepUiConfig {
children?: AgentWorkflowStepUiConfig[];
}
+export interface AgentWorkflowConstraintSettings {
+ crossLevelMutualExclusionEnabled: boolean;
+}
+
export interface AgentWorkflowUiConfig {
inputOptions?: {
brands: string[];
@@ -112,6 +116,7 @@ export interface AgentWorkflowUiConfig {
priceBands?: string[];
brandStoreEntries?: AgentWorkflowBrandStoreEntry[];
};
+ constraintSettings?: AgentWorkflowConstraintSettings;
steps: AgentWorkflowStepUiConfig[];
}