From a197a213370529c854ba4d0d860d7904aba69ffd Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 3 Apr 2026 13:30:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E5=B7=A5=E4=BD=9C?= =?UTF-8?q?=E6=B5=81=E8=B7=A8=E7=BA=A7=E4=BA=92=E6=96=A5=E4=B8=8E=E5=BC=80?= =?UTF-8?q?=E5=8F=91=E4=B8=AD=E5=8D=A1=E7=89=87=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../agent-workflow-settings-client.tsx | 89 +++++++++++---- app/api/site-settings/route.ts | 5 + app/canvas/[id]/page.tsx | 5 +- .../agents/AgentWorkflowPanelDarkV2.tsx | 72 +++++++++++-- lib/agents/workflow-ui-config.ts | 102 +++++++++++++++++- types/agent.ts | 5 + 6 files changed, 244 insertions(+), 34 deletions(-) diff --git a/app/(admin)/settings/agent-workflow/agent-workflow-settings-client.tsx b/app/(admin)/settings/agent-workflow/agent-workflow-settings-client.tsx index eb4652f3f..2ddde1833 100644 --- a/app/(admin)/settings/agent-workflow/agent-workflow-settings-client.tsx +++ b/app/(admin)/settings/agent-workflow/agent-workflow-settings-client.tsx @@ -28,6 +28,7 @@ import { Video, } from "lucide-react"; import { + defaultAgentWorkflowConstraintSettings, AGENT_WORKFLOW_MAX_OPTION_DEPTH, AGENT_WORKFLOW_MAX_TREE_DEPTH, countAgentWorkflowOptions, @@ -40,6 +41,7 @@ import { flattenAgentWorkflowOptionTree, getEnabledAgentWorkflowOptionTree, getAgentWorkflowCategoryLabels, + getAgentWorkflowConstraintSettings, normalizeAgentWorkflowOptionOrdering, flattenAgentWorkflowSteps, normalizeAgentWorkflowStepOrdering, @@ -913,6 +915,10 @@ export function AgentWorkflowSettingsClient({ () => getAgentWorkflowCategoryLabels(config), [config] ); + const workflowConstraintSettings = useMemo( + () => getAgentWorkflowConstraintSettings(config), + [config] + ); const firstCategoryLabel = categoryLabels.firstCategory; const secondCategoryLabel = categoryLabels.secondCategory; const thirdCategoryLabel = categoryLabels.thirdCategory; @@ -2339,7 +2345,9 @@ export function AgentWorkflowSettingsClient({ ); const canAddChild = depth < AGENT_WORKFLOW_MAX_OPTION_DEPTH; const blockedCandidates = workflowOptionRelations.filter( - (item) => item.key !== option.key && item.depth === depth + (item) => + item.key !== option.key && + (workflowConstraintSettings.crossLevelMutualExclusionEnabled || item.depth === depth) ); const selectedBlockedItems = (option.blockedOptionKeys || []) .map((blockedKey) => workflowOptionRelationLookup.get(blockedKey)) @@ -2393,6 +2401,7 @@ export function AgentWorkflowSettingsClient({ {levelLabel} {sortedChildren.length} 子卡片 + {!option.enabled ? 前台显示开发中 : null} {option.buttonName || "未命名卡片"} @@ -2454,16 +2463,6 @@ export function AgentWorkflowSettingsClient({ > - - updateOption(step.key, option.key, (currentOption) => ({ - ...currentOption, - enabled: checked, - })) - } - className="scale-75" - /> @@ -2560,11 +2559,31 @@ export function AgentWorkflowSettingsClient({ className="resize-none text-xs" /> +
+
+ +

+ 关闭后前端依然会展示这张卡片,但会标记为“开发中”且不可点击。 +

+
+ + updateOption(step.key, option.key, (currentOption) => ({ + ...currentOption, + enabled: checked, + })) + } + className="mt-1 scale-90" + /> +

- 当前只展示同级卡片供你配置互斥。一级只与所有一级卡片互斥,二级只与所有二级卡片互斥;生效时会自动覆盖目标卡片下的全部子级。 + {workflowConstraintSettings.crossLevelMutualExclusionEnabled + ? "当前已开启跨级互斥,可直接限制任意层级卡片;生效时会自动覆盖目标卡片下的全部子级。" + : "当前仅展示同级卡片供你配置互斥。一级只与所有一级卡片互斥,二级只与所有二级卡片互斥;生效时会自动覆盖目标卡片下的全部子级。"}

@@ -2614,7 +2633,7 @@ export function AgentWorkflowSettingsClient({ [refKey]: e.target.value, })) } - placeholder={`搜索同级卡片(${blockedCandidates.length} 个候选)`} + placeholder={`${workflowConstraintSettings.crossLevelMutualExclusionEnabled ? "搜索所有层级卡片" : "搜索同级卡片"}(${blockedCandidates.length} 个候选)`} className="h-8 pl-8 text-xs" />
@@ -2667,7 +2686,9 @@ export function AgentWorkflowSettingsClient({
{blockedCandidates.length > 0 ? "没有命中搜索结果,换个关键词试试" - : "当前工作流下没有其他可限制的同级卡片"} + : workflowConstraintSettings.crossLevelMutualExclusionEnabled + ? "当前工作流下没有其他可限制的卡片" + : "当前工作流下没有其他可限制的同级卡片"}
)} @@ -4421,19 +4442,47 @@ export function AgentWorkflowSettingsClient({ variants={containerVariants} className="space-y-4" > - -
+ +
{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[]; }