feat: add special workflow module exclusions
This commit is contained in:
parent
aaba40b2d6
commit
885708b2ec
@ -250,8 +250,8 @@ function scheduleWorkflowIdleTask(callback: () => void, timeoutMs = 2000) {
|
||||
return () => window.cancelIdleCallback(idleId);
|
||||
}
|
||||
|
||||
const timer = window.setTimeout(callback, timeoutMs);
|
||||
return () => window.clearTimeout(timer);
|
||||
const timer = globalThis.setTimeout(callback, timeoutMs);
|
||||
return () => globalThis.clearTimeout(timer);
|
||||
}
|
||||
|
||||
function BufferedInput({
|
||||
@ -488,6 +488,9 @@ function createWorkflowOptionDraft(
|
||||
sortOrder: optionIndex,
|
||||
promptFragment: `请按选项${optionIndex + 1}执行。`,
|
||||
blockedOptionKeys: [],
|
||||
specialModuleEnabled: false,
|
||||
specialModuleExclusionEnabled: false,
|
||||
specialModuleAllowedOptionKeys: [],
|
||||
children: [],
|
||||
};
|
||||
}
|
||||
@ -610,7 +613,10 @@ function analyzeWorkflowOptionContent(
|
||||
withPreview: summary.withPreview + (hasWorkflowOptionPreview(option) ? 1 : 0),
|
||||
disabled: summary.disabled + (option.enabled === false ? 1 : 0),
|
||||
withMutualExclusion:
|
||||
summary.withMutualExclusion + ((option.blockedOptionKeys || []).length > 0 ? 1 : 0),
|
||||
summary.withMutualExclusion +
|
||||
((option.blockedOptionKeys || []).length > 0 || option.specialModuleExclusionEnabled
|
||||
? 1
|
||||
: 0),
|
||||
}),
|
||||
createEmptyWorkflowOptionContentSummary()
|
||||
);
|
||||
@ -747,6 +753,8 @@ function collectWorkflowOptionRelations(
|
||||
hasContent: boolean;
|
||||
disabled: boolean;
|
||||
blockedCount: number;
|
||||
specialModuleEnabled: boolean;
|
||||
specialModuleExclusionEnabled: boolean;
|
||||
}> {
|
||||
return options.flatMap((option) => {
|
||||
const currentPath = [...ancestors, option.buttonName || option.key];
|
||||
@ -762,6 +770,8 @@ function collectWorkflowOptionRelations(
|
||||
hasContent: hasWorkflowOptionConfiguredContent(option),
|
||||
disabled: option.enabled === false,
|
||||
blockedCount: (option.blockedOptionKeys || []).length,
|
||||
specialModuleEnabled: Boolean(option.specialModuleEnabled),
|
||||
specialModuleExclusionEnabled: Boolean(option.specialModuleExclusionEnabled),
|
||||
},
|
||||
...collectWorkflowOptionRelations(option.children || [], currentPath),
|
||||
];
|
||||
@ -774,6 +784,15 @@ function collectWorkflowOptionRelationsFromSteps(
|
||||
key: string;
|
||||
label: string;
|
||||
depth: number;
|
||||
childCount: number;
|
||||
hasPrompt: boolean;
|
||||
hasDescription: boolean;
|
||||
hasPreview: boolean;
|
||||
hasContent: boolean;
|
||||
disabled: boolean;
|
||||
blockedCount: number;
|
||||
specialModuleEnabled: boolean;
|
||||
specialModuleExclusionEnabled: boolean;
|
||||
stepKey: string;
|
||||
stepTitle: string;
|
||||
}> {
|
||||
@ -1017,6 +1036,7 @@ function cloneWorkflowOptionSnapshot(
|
||||
return {
|
||||
...option,
|
||||
blockedOptionKeys: [...(option.blockedOptionKeys || [])],
|
||||
specialModuleAllowedOptionKeys: [...(option.specialModuleAllowedOptionKeys || [])],
|
||||
children: (option.children || []).map(cloneWorkflowOptionSnapshot),
|
||||
};
|
||||
}
|
||||
@ -1040,6 +1060,9 @@ function remapWorkflowOptionBlockedKeys(
|
||||
blockedOptionKeys: (option.blockedOptionKeys || []).map((blockedKey) =>
|
||||
keyMap.get(blockedKey) || blockedKey
|
||||
),
|
||||
specialModuleAllowedOptionKeys: (option.specialModuleAllowedOptionKeys || []).map(
|
||||
(allowedKey) => keyMap.get(allowedKey) || allowedKey
|
||||
),
|
||||
children: remapWorkflowOptionBlockedKeys(option.children || [], keyMap),
|
||||
}));
|
||||
}
|
||||
@ -2090,6 +2113,21 @@ export function AgentWorkflowSettingsClient({
|
||||
() => new Map(workflowStepAnalyses.map((item) => [item.key, item] as const)),
|
||||
[workflowStepAnalyses]
|
||||
);
|
||||
const workflowSpecialModuleOptions = useMemo(
|
||||
() =>
|
||||
flattenAgentWorkflowSteps(deferredWorkflowSteps || []).flatMap(({ step }) =>
|
||||
[...(step.options || [])]
|
||||
.sort((left, right) => left.sortOrder - right.sortOrder)
|
||||
.filter((option) => option.specialModuleEnabled)
|
||||
.map((option) => ({
|
||||
option,
|
||||
stepKey: step.key,
|
||||
stepTitle: step.title || step.key,
|
||||
path: `${step.title || step.key} / ${option.buttonName || option.key}`,
|
||||
}))
|
||||
),
|
||||
[deferredWorkflowSteps]
|
||||
);
|
||||
const workflowOptionGapDetails = useMemo(
|
||||
() =>
|
||||
buildWorkflowOptionGapDetails(deferredWorkflowSteps || []).sort(
|
||||
@ -3924,6 +3962,10 @@ export function AgentWorkflowSettingsClient({
|
||||
const selectedBlockedItems = (option.blockedOptionKeys || [])
|
||||
.map((blockedKey) => workflowOptionRelationLookup.get(blockedKey))
|
||||
.filter((item): item is WorkflowOptionRelation => Boolean(item));
|
||||
const availableSpecialModuleOptions = workflowSpecialModuleOptions.filter(
|
||||
(item) => item.option.key !== option.key && (item.option.children || []).length > 0
|
||||
);
|
||||
const specialAllowedKeys = new Set(option.specialModuleAllowedOptionKeys || []);
|
||||
const isExpanded = isWorkflowOptionExpanded(option.key, depth);
|
||||
const blockedSearchValue = blockedOptionSearches[refKey] || "";
|
||||
const normalizedBlockedSearchValue = blockedSearchValue.trim().toLowerCase();
|
||||
@ -3947,6 +3989,87 @@ export function AgentWorkflowSettingsClient({
|
||||
: [];
|
||||
const canMoveUp = depth === 1 && indexPath[0] > 1;
|
||||
const canMoveDown = depth === 1 && indexPath[0] < (step.options || []).length;
|
||||
const toggleSpecialModuleAllowedOption = (allowedOptionKey: string) => {
|
||||
updateOption(step.key, option.key, (currentOption) => {
|
||||
const nextAllowedKeys = new Set(currentOption.specialModuleAllowedOptionKeys || []);
|
||||
if (nextAllowedKeys.has(allowedOptionKey)) {
|
||||
nextAllowedKeys.delete(allowedOptionKey);
|
||||
} else {
|
||||
nextAllowedKeys.add(allowedOptionKey);
|
||||
}
|
||||
|
||||
return {
|
||||
...currentOption,
|
||||
specialModuleAllowedOptionKeys: Array.from(nextAllowedKeys),
|
||||
};
|
||||
});
|
||||
};
|
||||
const setSpecialModuleAllowedBranch = (
|
||||
candidateOption: AgentWorkflowOptionUiConfig,
|
||||
allowed: boolean
|
||||
) => {
|
||||
updateOption(step.key, option.key, (currentOption) => {
|
||||
const nextAllowedKeys = new Set(currentOption.specialModuleAllowedOptionKeys || []);
|
||||
const branchKeys = flattenAgentWorkflowOptionTree([candidateOption]).map(
|
||||
({ option: branchOption }) => branchOption.key
|
||||
);
|
||||
|
||||
branchKeys.forEach((branchKey) => nextAllowedKeys.delete(branchKey));
|
||||
if (allowed) {
|
||||
nextAllowedKeys.add(candidateOption.key);
|
||||
}
|
||||
|
||||
return {
|
||||
...currentOption,
|
||||
specialModuleAllowedOptionKeys: Array.from(nextAllowedKeys),
|
||||
};
|
||||
});
|
||||
};
|
||||
const renderSpecialModuleAllowedOption = (
|
||||
candidateOption: AgentWorkflowOptionUiConfig,
|
||||
candidateDepth: number,
|
||||
inheritedAllowed = false
|
||||
): ReactNode => {
|
||||
const directlyAllowed = specialAllowedKeys.has(candidateOption.key);
|
||||
const allowed = inheritedAllowed || directlyAllowed;
|
||||
const sortedCandidateChildren = [...(candidateOption.children || [])].sort(
|
||||
(left, right) => left.sortOrder - right.sortOrder
|
||||
);
|
||||
|
||||
return (
|
||||
<div key={candidateOption.key} className={cn("space-y-1", candidateDepth > 1 && "pl-3")}>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between gap-3 rounded-md border px-3 py-2 text-left text-xs transition-colors",
|
||||
allowed
|
||||
? "border-emerald-500/50 bg-emerald-500/10 text-foreground"
|
||||
: "border-border/70 bg-background hover:border-foreground/20 hover:bg-muted/60"
|
||||
)}
|
||||
onClick={() => toggleSpecialModuleAllowedOption(candidateOption.key)}
|
||||
>
|
||||
<span className="min-w-0">
|
||||
<span className="block break-words font-medium leading-5">
|
||||
{candidateOption.buttonName || candidateOption.key}
|
||||
</span>
|
||||
<span className="block text-[10px] text-muted-foreground">
|
||||
{getWorkflowLevelLabel(candidateDepth, "卡片")} · {candidateOption.key}
|
||||
</span>
|
||||
</span>
|
||||
<Badge variant={allowed ? "default" : "outline"} className="shrink-0">
|
||||
{directlyAllowed ? "已放行" : inheritedAllowed ? "随父级放行" : "默认排斥"}
|
||||
</Badge>
|
||||
</button>
|
||||
{sortedCandidateChildren.length > 0 ? (
|
||||
<div className="space-y-1 border-l border-dashed pl-2">
|
||||
{sortedCandidateChildren.map((childOption) =>
|
||||
renderSpecialModuleAllowedOption(childOption, candidateDepth + 1, allowed)
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div key={option.key} className={cn("space-y-4", depth > 1 && "pl-4 sm:pl-6")}>
|
||||
@ -3982,6 +4105,10 @@ export function AgentWorkflowSettingsClient({
|
||||
</span>
|
||||
<Badge variant="outline">{levelLabel}</Badge>
|
||||
<Badge variant="secondary">{sortedChildren.length} 子卡片</Badge>
|
||||
{option.specialModuleEnabled ? <Badge variant="default">特殊模块</Badge> : null}
|
||||
{option.specialModuleExclusionEnabled ? (
|
||||
<Badge variant="secondary">特殊模块快捷排斥</Badge>
|
||||
) : null}
|
||||
{!option.enabled ? <Badge variant="secondary">前台显示开发中</Badge> : null}
|
||||
<span className="max-w-[240px] truncate text-sm font-medium">
|
||||
{option.buttonName || "未命名卡片"}
|
||||
@ -4165,6 +4292,26 @@ export function AgentWorkflowSettingsClient({
|
||||
className="mt-1 scale-90"
|
||||
/>
|
||||
</div>
|
||||
{depth === 1 ? (
|
||||
<div className="flex items-start justify-between gap-4 rounded-lg border border-emerald-500/20 bg-emerald-500/5 p-3">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">特殊模块</Label>
|
||||
<p className="text-[11px] leading-5 text-muted-foreground">
|
||||
开启后,这张一级卡片会出现在互斥配置的“特殊模块快捷排斥”里。其它卡片可只勾选少量放行子卡片,未放行的子卡片会自动排斥。
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={Boolean(option.specialModuleEnabled)}
|
||||
onCheckedChange={(checked) =>
|
||||
updateOption(step.key, option.key, (currentOption) => ({
|
||||
...currentOption,
|
||||
specialModuleEnabled: checked,
|
||||
}))
|
||||
}
|
||||
className="mt-1 scale-90"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<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>
|
||||
@ -4210,131 +4357,212 @@ export function AgentWorkflowSettingsClient({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2 rounded-md border bg-background/70 p-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-xs font-medium text-muted-foreground">
|
||||
候选卡片列表
|
||||
{blockedCandidatePanelExpanded
|
||||
? ` · 已展示 ${filteredBlockedCandidates.length}/${blockedCandidates.length}`
|
||||
: ""}
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 px-2 text-xs"
|
||||
onClick={() =>
|
||||
setExpandedBlockedOptionPanels((prev) => ({
|
||||
...prev,
|
||||
[refKey]: !blockedCandidatePanelExpanded,
|
||||
}))
|
||||
}
|
||||
>
|
||||
{blockedCandidatePanelExpanded ? "收起候选" : "展开候选"}
|
||||
</Button>
|
||||
</div>
|
||||
{blockedCandidatePanelExpanded ? (
|
||||
<>
|
||||
<div className="relative">
|
||||
<Search className="pointer-events-none absolute left-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
value={blockedSearchValue}
|
||||
onChange={(e) =>
|
||||
setBlockedOptionSearches((prev) => ({
|
||||
...prev,
|
||||
[refKey]: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder={`${workflowConstraintSettings.crossLevelMutualExclusionEnabled ? "搜索所有层级卡片" : "搜索同级卡片"}(${blockedCandidates.length} 个候选)`}
|
||||
className="h-8 pl-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="max-h-72 space-y-2 overflow-y-auto">
|
||||
{filteredBlockedCandidates.length > 0 ? (
|
||||
<>
|
||||
{filteredBlockedCandidates.map((candidate) => {
|
||||
const active = (option.blockedOptionKeys || []).includes(candidate.key);
|
||||
const candidateStepAnalysis = workflowStepAnalysisLookup.get(
|
||||
candidate.stepKey
|
||||
);
|
||||
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 flex-1 space-y-1 pr-3">
|
||||
<span className="block whitespace-normal break-words font-medium leading-5">
|
||||
{candidate.label}
|
||||
</span>
|
||||
<span className="block whitespace-normal break-words text-[10px] leading-4 text-muted-foreground">
|
||||
所属模块:
|
||||
{candidateStepAnalysis?.path || candidate.stepTitle} ·{" "}
|
||||
{getWorkflowLevelLabel(candidate.depth, "卡片")} ·{" "}
|
||||
{candidate.key}
|
||||
</span>
|
||||
</span>
|
||||
<span className="flex shrink-0 flex-wrap justify-end gap-1">
|
||||
<Badge variant={active ? "default" : "outline"}>
|
||||
{active ? "已限制" : "可限制"}
|
||||
</Badge>
|
||||
<Badge
|
||||
variant={
|
||||
candidateStepAnalysis?.hasOwnContent
|
||||
? "secondary"
|
||||
: "outline"
|
||||
}
|
||||
>
|
||||
{candidateStepAnalysis?.hasOwnContent
|
||||
? "模块有内容"
|
||||
: "模块空内容"}
|
||||
</Badge>
|
||||
<Badge variant={candidate.hasContent ? "secondary" : "outline"}>
|
||||
{candidate.hasContent ? "卡片有内容" : "卡片空内容"}
|
||||
</Badge>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
<div className="px-2 py-3 text-center text-xs text-muted-foreground">
|
||||
{blockedCandidates.length > 0
|
||||
? "没有命中搜索结果,换个关键词试试"
|
||||
: workflowConstraintSettings.crossLevelMutualExclusionEnabled
|
||||
? "当前工作流下没有其他可限制的卡片"
|
||||
: "当前工作流下没有其他可限制的同级卡片"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="rounded-md border border-dashed bg-muted/30 px-3 py-2 text-[11px] text-muted-foreground">
|
||||
候选列表已折叠,需要配置互斥时再展开,避免大量卡片同时渲染导致编辑卡顿。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{availableSpecialModuleOptions.length > 0 ? (
|
||||
<div className="space-y-3 rounded-md border border-emerald-500/20 bg-emerald-500/5 p-3">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">特殊模块快捷排斥</Label>
|
||||
<p className="text-[11px] leading-5 text-muted-foreground">
|
||||
开启后,下面特殊模块中未勾选放行的子卡片会自动进入排斥;勾选父级会放行整支子树。
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={Boolean(option.specialModuleExclusionEnabled)}
|
||||
onCheckedChange={(checked) =>
|
||||
updateOption(step.key, option.key, (currentOption) => ({
|
||||
...currentOption,
|
||||
specialModuleExclusionEnabled: checked,
|
||||
}))
|
||||
}
|
||||
className="mt-1 scale-90"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{option.specialModuleExclusionEnabled ? (
|
||||
<div className="max-h-72 space-y-3 overflow-y-auto rounded-md border bg-background/70 p-2">
|
||||
{availableSpecialModuleOptions.map((specialModule) => {
|
||||
const rootAllowed = specialAllowedKeys.has(specialModule.option.key);
|
||||
|
||||
return (
|
||||
<div key={specialModule.option.key} className="space-y-2 rounded-lg border p-2">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<div className="break-words text-xs font-medium leading-5">
|
||||
{specialModule.path}
|
||||
</div>
|
||||
<div className="text-[10px] text-muted-foreground">
|
||||
特殊模块 · {specialModule.option.key}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 flex-wrap items-center justify-end gap-1">
|
||||
<Badge variant={rootAllowed ? "default" : "outline"}>
|
||||
{rootAllowed
|
||||
? "整组已放行"
|
||||
: `${(specialModule.option.children || []).length} 个子卡片`}
|
||||
</Badge>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-7 px-2 text-[11px]"
|
||||
disabled={rootAllowed}
|
||||
onClick={() => setSpecialModuleAllowedBranch(specialModule.option, true)}
|
||||
>
|
||||
整组放行
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 px-2 text-[11px]"
|
||||
onClick={() => setSpecialModuleAllowedBranch(specialModule.option, false)}
|
||||
>
|
||||
恢复默认排斥
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{(specialModule.option.children || []).map((childOption) =>
|
||||
renderSpecialModuleAllowedOption(childOption, 2, rootAllowed)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-md border border-dashed bg-background/70 px-3 py-2 text-[11px] text-muted-foreground">
|
||||
当前未启用特殊模块快捷排斥;启用后只需要勾选“不排斥”的少量子卡片。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="space-y-2 rounded-md border bg-background/70 p-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-xs font-medium text-muted-foreground">
|
||||
候选卡片列表
|
||||
{blockedCandidatePanelExpanded
|
||||
? ` · 已展示 ${filteredBlockedCandidates.length}/${blockedCandidates.length}`
|
||||
: ""}
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 px-2 text-xs"
|
||||
onClick={() =>
|
||||
setExpandedBlockedOptionPanels((prev) => ({
|
||||
...prev,
|
||||
[refKey]: !blockedCandidatePanelExpanded,
|
||||
}))
|
||||
}
|
||||
>
|
||||
{blockedCandidatePanelExpanded ? "收起候选" : "展开候选"}
|
||||
</Button>
|
||||
</div>
|
||||
{blockedCandidatePanelExpanded ? (
|
||||
<>
|
||||
<div className="relative">
|
||||
<Search className="pointer-events-none absolute left-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
value={blockedSearchValue}
|
||||
onChange={(e) =>
|
||||
setBlockedOptionSearches((prev) => ({
|
||||
...prev,
|
||||
[refKey]: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder={`${workflowConstraintSettings.crossLevelMutualExclusionEnabled ? "搜索所有层级卡片" : "搜索同级卡片"}(${blockedCandidates.length} 个候选)`}
|
||||
className="h-8 pl-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="max-h-72 space-y-2 overflow-y-auto">
|
||||
{filteredBlockedCandidates.length > 0 ? (
|
||||
<>
|
||||
{filteredBlockedCandidates.map((candidate) => {
|
||||
const active = (option.blockedOptionKeys || []).includes(candidate.key);
|
||||
const candidateStepAnalysis = workflowStepAnalysisLookup.get(
|
||||
candidate.stepKey
|
||||
);
|
||||
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 flex-1 space-y-1 pr-3">
|
||||
<span className="block whitespace-normal break-words font-medium leading-5">
|
||||
{candidate.label}
|
||||
</span>
|
||||
<span className="block whitespace-normal break-words text-[10px] leading-4 text-muted-foreground">
|
||||
所属模块:
|
||||
{candidateStepAnalysis?.path || candidate.stepTitle} ·{" "}
|
||||
{getWorkflowLevelLabel(candidate.depth, "卡片")} ·{" "}
|
||||
{candidate.key}
|
||||
</span>
|
||||
</span>
|
||||
<span className="flex shrink-0 flex-wrap justify-end gap-1">
|
||||
<Badge variant={active ? "default" : "outline"}>
|
||||
{active ? "已限制" : "可限制"}
|
||||
</Badge>
|
||||
<Badge
|
||||
variant={
|
||||
candidateStepAnalysis?.hasOwnContent
|
||||
? "secondary"
|
||||
: "outline"
|
||||
}
|
||||
>
|
||||
{candidateStepAnalysis?.hasOwnContent
|
||||
? "模块有内容"
|
||||
: "模块空内容"}
|
||||
</Badge>
|
||||
<Badge variant={candidate.hasContent ? "secondary" : "outline"}>
|
||||
{candidate.hasContent ? "卡片有内容" : "卡片空内容"}
|
||||
</Badge>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
<div className="px-2 py-3 text-center text-xs text-muted-foreground">
|
||||
{blockedCandidates.length > 0
|
||||
? "没有命中搜索结果,换个关键词试试"
|
||||
: workflowConstraintSettings.crossLevelMutualExclusionEnabled
|
||||
? "当前工作流下没有其他可限制的卡片"
|
||||
: "当前工作流下没有其他可限制的同级卡片"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="rounded-md border border-dashed bg-muted/30 px-3 py-2 text-[11px] text-muted-foreground">
|
||||
候选列表已折叠,需要配置互斥时再展开,避免大量卡片同时渲染导致编辑卡顿。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -906,6 +906,12 @@ const agentWorkflowOptionSchema: z.ZodTypeAny = z.lazy(() =>
|
||||
.nullable()
|
||||
.optional(),
|
||||
blockedOptionKeys: z.array(z.string().trim().min(1).max(64)).max(100).optional(),
|
||||
specialModuleEnabled: z.boolean().optional(),
|
||||
specialModuleExclusionEnabled: z.boolean().optional(),
|
||||
specialModuleAllowedOptionKeys: z
|
||||
.array(z.string().trim().min(1).max(64))
|
||||
.max(5000)
|
||||
.optional(),
|
||||
children: z.array(agentWorkflowOptionSchema).max(100).optional(),
|
||||
})
|
||||
);
|
||||
|
||||
@ -16,6 +16,7 @@ export const AGENT_WORKFLOW_UI_CONFIG_KEY = "agent_workflow_ui_config";
|
||||
export const AGENT_WORKFLOW_MAX_TREE_DEPTH = 8;
|
||||
export const AGENT_WORKFLOW_MAX_OPTION_DEPTH = 100;
|
||||
export const AGENT_WORKFLOW_PROMPT_FRAGMENT_MAX_LENGTH = 20_000;
|
||||
const AGENT_WORKFLOW_SPECIAL_MODULE_ALLOWED_KEYS_MAX = 5000;
|
||||
export const defaultAgentWorkflowCategoryLabels: AgentWorkflowCategoryLabels = {
|
||||
firstCategory: "品牌",
|
||||
secondCategory: "店铺",
|
||||
@ -57,6 +58,11 @@ function cloneOption(
|
||||
0,
|
||||
100
|
||||
),
|
||||
specialModuleEnabled: Boolean(option.specialModuleEnabled),
|
||||
specialModuleExclusionEnabled: Boolean(option.specialModuleExclusionEnabled),
|
||||
specialModuleAllowedOptionKeys: Array.from(
|
||||
new Set((option.specialModuleAllowedOptionKeys || []).filter(Boolean))
|
||||
).slice(0, AGENT_WORKFLOW_SPECIAL_MODULE_ALLOWED_KEYS_MAX),
|
||||
children: (option.children || []).map(cloneOption),
|
||||
};
|
||||
}
|
||||
@ -102,6 +108,9 @@ function createOption(
|
||||
sortOrder,
|
||||
promptFragment,
|
||||
blockedOptionKeys: [],
|
||||
specialModuleEnabled: false,
|
||||
specialModuleExclusionEnabled: false,
|
||||
specialModuleAllowedOptionKeys: [],
|
||||
children: [],
|
||||
};
|
||||
}
|
||||
@ -896,6 +905,9 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
|
||||
defaultOption?.promptFragment ||
|
||||
buttonName;
|
||||
const hasRawBlockedOptionKeys = Array.isArray(optionRecord.blockedOptionKeys);
|
||||
const hasRawSpecialModuleAllowedOptionKeys = Array.isArray(
|
||||
optionRecord.specialModuleAllowedOptionKeys
|
||||
);
|
||||
const hasRawChildren = Array.isArray(optionRecord.children);
|
||||
const rawChildren = hasRawChildren ? (optionRecord.children as unknown[]) : [];
|
||||
const defaultChildren =
|
||||
@ -922,6 +934,23 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
|
||||
blockedOptionKeys: hasRawBlockedOptionKeys
|
||||
? normalizeKeyList(optionRecord.blockedOptionKeys, 100)
|
||||
: normalizeKeyList(defaultOption?.blockedOptionKeys, 100),
|
||||
specialModuleEnabled: normalizeBoolean(
|
||||
optionRecord.specialModuleEnabled,
|
||||
defaultOption?.specialModuleEnabled ?? false
|
||||
),
|
||||
specialModuleExclusionEnabled: normalizeBoolean(
|
||||
optionRecord.specialModuleExclusionEnabled,
|
||||
defaultOption?.specialModuleExclusionEnabled ?? false
|
||||
),
|
||||
specialModuleAllowedOptionKeys: hasRawSpecialModuleAllowedOptionKeys
|
||||
? normalizeKeyList(
|
||||
optionRecord.specialModuleAllowedOptionKeys,
|
||||
AGENT_WORKFLOW_SPECIAL_MODULE_ALLOWED_KEYS_MAX
|
||||
)
|
||||
: normalizeKeyList(
|
||||
defaultOption?.specialModuleAllowedOptionKeys,
|
||||
AGENT_WORKFLOW_SPECIAL_MODULE_ALLOWED_KEYS_MAX
|
||||
),
|
||||
children:
|
||||
depth < AGENT_WORKFLOW_MAX_OPTION_DEPTH
|
||||
? childSource
|
||||
@ -1378,8 +1407,10 @@ export function getAgentWorkflowBlockedOptionKeys(params: {
|
||||
selectionValue?: string | null;
|
||||
allowCrossLevel?: boolean;
|
||||
optionDepthLookup?: Map<string, number>;
|
||||
specialModuleRootOptions?: AgentWorkflowOptionUiConfig[];
|
||||
}) {
|
||||
const selectedOptions = getAgentWorkflowSelectedOptionPath(params);
|
||||
const selectedOptionKeys = new Set(selectedOptions.map((option) => option.key));
|
||||
const optionDepthLookup =
|
||||
params.optionDepthLookup ||
|
||||
new Map(
|
||||
@ -1392,7 +1423,7 @@ export function getAgentWorkflowBlockedOptionKeys(params: {
|
||||
new Set(
|
||||
selectedOptions.flatMap((option) => {
|
||||
const currentDepth = optionDepthLookup.get(option.key);
|
||||
return (option.blockedOptionKeys || []).filter((blockedKey) => {
|
||||
const explicitBlockedKeys = (option.blockedOptionKeys || []).filter((blockedKey) => {
|
||||
if (!blockedKey) {
|
||||
return false;
|
||||
}
|
||||
@ -1404,11 +1435,79 @@ export function getAgentWorkflowBlockedOptionKeys(params: {
|
||||
}
|
||||
return optionDepthLookup.get(blockedKey) === currentDepth;
|
||||
});
|
||||
const specialModuleBlockedKeys = option.specialModuleExclusionEnabled
|
||||
? getSpecialModuleBlockedOptionKeys({
|
||||
allowedOptionKeys: option.specialModuleAllowedOptionKeys || [],
|
||||
selectedOptionKeys,
|
||||
sourceOptionKey: option.key,
|
||||
specialModuleRootOptions: params.specialModuleRootOptions || [],
|
||||
})
|
||||
: [];
|
||||
|
||||
return [...explicitBlockedKeys, ...specialModuleBlockedKeys];
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function collectAllowedSpecialModulePathKeys(
|
||||
option: AgentWorkflowOptionUiConfig,
|
||||
allowedOptionKeys: Set<string>,
|
||||
ancestors: string[] = []
|
||||
): string[] {
|
||||
const currentAncestors = [...ancestors, option.key];
|
||||
const descendantKeys = (option.children || []).flatMap((child) =>
|
||||
collectAllowedSpecialModulePathKeys(child, allowedOptionKeys, currentAncestors)
|
||||
);
|
||||
|
||||
if (allowedOptionKeys.has(option.key)) {
|
||||
return [
|
||||
...currentAncestors,
|
||||
...flattenAgentWorkflowOptionTree(option.children || []).map(
|
||||
({ option: childOption }) => childOption.key
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
return descendantKeys;
|
||||
}
|
||||
|
||||
function getSpecialModuleBlockedOptionKeys(params: {
|
||||
allowedOptionKeys: string[];
|
||||
selectedOptionKeys: Set<string>;
|
||||
sourceOptionKey: string;
|
||||
specialModuleRootOptions: AgentWorkflowOptionUiConfig[];
|
||||
}) {
|
||||
const allowedOptionKeys = new Set(params.allowedOptionKeys.filter(Boolean));
|
||||
const allowedPathKeys = new Set<string>();
|
||||
|
||||
for (const rootOption of params.specialModuleRootOptions) {
|
||||
if (rootOption.key === params.sourceOptionKey) {
|
||||
continue;
|
||||
}
|
||||
|
||||
collectAllowedSpecialModulePathKeys(rootOption, allowedOptionKeys).forEach((key) =>
|
||||
allowedPathKeys.add(key)
|
||||
);
|
||||
}
|
||||
|
||||
return params.specialModuleRootOptions.flatMap((rootOption) => {
|
||||
if (rootOption.key === params.sourceOptionKey) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return flattenAgentWorkflowOptionTree(rootOption.children || [])
|
||||
.map(({ option }) => option.key)
|
||||
.filter(
|
||||
(key) =>
|
||||
key &&
|
||||
!allowedOptionKeys.has(key) &&
|
||||
!allowedPathKeys.has(key) &&
|
||||
!params.selectedOptionKeys.has(key)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function collectAgentWorkflowOptionBranchKeys(
|
||||
options: AgentWorkflowOptionUiConfig[],
|
||||
targetKey: string
|
||||
@ -1445,6 +1544,9 @@ export function getAgentWorkflowBlockedOptionKeysForSelections(params: {
|
||||
] as const)
|
||||
)
|
||||
);
|
||||
const specialModuleRootOptions = visibleSteps.flatMap((step) =>
|
||||
(step.options || []).filter((option) => option.specialModuleEnabled)
|
||||
);
|
||||
|
||||
const directlyBlockedKeys = Array.from(
|
||||
new Set(
|
||||
@ -1454,6 +1556,7 @@ export function getAgentWorkflowBlockedOptionKeysForSelections(params: {
|
||||
selectionValue: params.selections[step.key],
|
||||
allowCrossLevel: constraintSettings.crossLevelMutualExclusionEnabled,
|
||||
optionDepthLookup: visibleOptionDepthLookup,
|
||||
specialModuleRootOptions,
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
@ -88,6 +88,9 @@ export interface AgentWorkflowOptionUiConfig {
|
||||
sortOrder: number;
|
||||
promptFragment?: string | null;
|
||||
blockedOptionKeys?: string[];
|
||||
specialModuleEnabled?: boolean;
|
||||
specialModuleExclusionEnabled?: boolean;
|
||||
specialModuleAllowedOptionKeys?: string[];
|
||||
children?: AgentWorkflowOptionUiConfig[];
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user