From a5b3e35df9229b20f2ef13dda200e3468afb6e88 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 19 May 2026 14:15:10 +0800 Subject: [PATCH] fix: refine local edit controls and workflow validation --- .../agent-workflow-settings-client.tsx | 22 ++- app/api/site-settings/route.ts | 36 ++++ components/canvas/nodes/ImageNode.tsx | 175 +++++++++--------- lib/agents/workflow-ui-config.ts | 77 ++++++++ 4 files changed, 222 insertions(+), 88 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 b81874c6d..f7dec5c43 100644 --- a/app/(admin)/settings/agent-workflow/agent-workflow-settings-client.tsx +++ b/app/(admin)/settings/agent-workflow/agent-workflow-settings-client.tsx @@ -64,6 +64,7 @@ import { getAgentWorkflowOfficialCategoryKeyLabels, getAgentWorkflowOfficialSecondCategoryKey, getAgentWorkflowOfficialThirdCategoryKey, + getAgentWorkflowMissingPreviewOptions, getAgentWorkflowPageFeatureSettings, normalizeAgentWorkflowBusinessModeSettings, normalizeAgentWorkflowOptionOrdering, @@ -678,7 +679,10 @@ function hasWorkflowOptionDescription(option: AgentWorkflowOptionUiConfig) { } function hasWorkflowOptionPreview(option: AgentWorkflowOptionUiConfig) { - return Boolean(String(option.previewImageUrl || "").trim()); + return Boolean( + String(option.previewImageUrl || "").trim() || + String(option.previewImageSourceUrl || "").trim() + ); } function hasWorkflowOptionConfiguredContent(option: AgentWorkflowOptionUiConfig) { @@ -3871,6 +3875,22 @@ export function AgentWorkflowSettingsClient({ return; } + if (changedEntries.some(({ field }) => field === "agentWorkflowUiConfig")) { + const missingPreviewOptions = getAgentWorkflowMissingPreviewOptions( + config.steps || [], + { enabledOnly: true } + ); + if (missingPreviewOptions.length > 0) { + const firstItems = missingPreviewOptions + .slice(0, 3) + .map((item) => `${item.stepPath} / ${item.path}`) + .join(";"); + throw new Error( + `当前有 ${missingPreviewOptions.length} 个已启用卡片缺少自己的预览图,请先补图或关闭对应卡片后再保存。${firstItems}` + ); + } + } + const conflictedFields = changedEntries .filter(({ field }) => latestServerSignatures[field] !== baseSignatures[field]) .map(({ field }) => AGENT_WORKFLOW_SETTINGS_FIELD_LABELS[field]); diff --git a/app/api/site-settings/route.ts b/app/api/site-settings/route.ts index 0e4adec2f..60d4bb46a 100644 --- a/app/api/site-settings/route.ts +++ b/app/api/site-settings/route.ts @@ -35,6 +35,7 @@ import { AGENT_WORKFLOW_UI_CONFIG_KEY, AGENT_WORKFLOW_PROMPT_FRAGMENT_MAX_LENGTH, defaultAgentWorkflowUiConfig, + getAgentWorkflowMissingPreviewOptions, normalizeAgentWorkflowStepOrdering, normalizeAgentWorkflowUiConfig, } from "@/lib/agents/workflow-ui-config"; @@ -176,6 +177,29 @@ const defaultRealtimeCanvasModeIconConfig: RealtimeCanvasModeIconConfig = { enhanceLabel: DEFAULT_ENHANCE_CANVAS_MODE_LABEL, }; +function createAgentWorkflowMissingPreviewResponse( + steps: ReturnType["steps"] +) { + const missingPreviewOptions = getAgentWorkflowMissingPreviewOptions(steps || [], { + enabledOnly: true, + }); + + if (missingPreviewOptions.length === 0) { + return null; + } + + return NextResponse.json( + { + error: `当前有 ${missingPreviewOptions.length} 个已启用卡片缺少自己的预览图,请先补图或关闭对应卡片后再保存。`, + details: missingPreviewOptions.slice(0, 30).map((item) => ({ + path: ["agentWorkflowUiConfig", item.stepPath, item.path], + message: `缺少预览图:${item.stepPath} / ${item.path}`, + })), + }, + { status: 400 } + ); +} + async function readCanvasFeatureAvailability(valueByKey: Map) { const [enabledTripoProviders, enabledWorldlabsProviders] = await Promise.all([ db @@ -2379,6 +2403,12 @@ export async function PUT(request: NextRequest) { if (agentWorkflowUiConfig !== undefined) { const normalized = normalizeAgentWorkflowUiConfig(agentWorkflowUiConfig); + const missingPreviewResponse = createAgentWorkflowMissingPreviewResponse( + normalized.steps + ); + if (missingPreviewResponse) { + return missingPreviewResponse; + } await persistSystemSettingWithHistory( AGENT_WORKFLOW_UI_CONFIG_KEY, normalized, @@ -3032,6 +3062,12 @@ export async function PUT(request: NextRequest) { if (agentWorkflowUiConfig !== undefined) { const normalized = normalizeAgentWorkflowUiConfig(agentWorkflowUiConfig); + const missingPreviewResponse = createAgentWorkflowMissingPreviewResponse( + normalized.steps + ); + if (missingPreviewResponse) { + return missingPreviewResponse; + } await persistSystemSettingWithHistory( AGENT_WORKFLOW_UI_CONFIG_KEY, normalized, diff --git a/components/canvas/nodes/ImageNode.tsx b/components/canvas/nodes/ImageNode.tsx index 2bccd7091..d9d0a7003 100644 --- a/components/canvas/nodes/ImageNode.tsx +++ b/components/canvas/nodes/ImageNode.tsx @@ -2884,7 +2884,7 @@ export function ImageNode({ !isSelected || isMultiSelected || !data.src || - !boxCutoutMode + (!boxCutoutMode && !localEditMode) ) { return; } @@ -2925,7 +2925,7 @@ export function ImageNode({ resizeObserver?.disconnect(); if (rafId !== null) window.cancelAnimationFrame(rafId); }; - }, [boxCutoutMode, data.src, isMultiSelected, isSelected]); + }, [boxCutoutMode, data.src, isMultiSelected, isSelected, localEditMode]); // 🆕 处理图片点击标注 const handlePickerClick = useCallback( @@ -7467,88 +7467,6 @@ export function ImageNode({ onPointerMove={handleLocalEditPointerMove} onPointerUp={handleLocalEditPointerUp} /> -
event.stopPropagation()} - onMouseDown={(event) => event.stopPropagation()} - onPointerDown={(event) => event.stopPropagation()} - > -
-
-
局部编辑
-
- 框选区域后填写每个区域的修改要求 -
-
- - {localEditRegions.length}/{LOCAL_EDIT_MAX_REGIONS} - -
-
- {localEditRegions.length > 0 ? ( - localEditRegions.map((region) => ( - - )) - ) : ( - - 暂无区域,直接在图片上拖拽框选。 - - )} -
-
- - - -
-
{boxCutoutMetrics ? (
{ if (region.prompt.trim()) { setEditingLocalEditRegionId(null); @@ -7632,7 +7550,7 @@ export function ImageNode({ /> ) : ( )} + + + + +
, + document.body + )} + {isSelected && !isMultiSelected && data.src && diff --git a/lib/agents/workflow-ui-config.ts b/lib/agents/workflow-ui-config.ts index d6405121c..c0e41ebd2 100644 --- a/lib/agents/workflow-ui-config.ts +++ b/lib/agents/workflow-ui-config.ts @@ -142,6 +142,16 @@ export interface FlattenedAgentWorkflowStep { parentKeys: string[]; } +export interface AgentWorkflowMissingPreviewOption { + key: string; + label: string; + path: string; + stepKey: string; + stepPath: string; + disabled: boolean; + childCount: number; +} + function cloneOption( option: AgentWorkflowOptionUiConfig ): AgentWorkflowOptionUiConfig { @@ -787,6 +797,73 @@ export function countAgentWorkflowOptionTree( return flattenAgentWorkflowOptionTree(options).length; } +export function hasAgentWorkflowOptionOwnPreview( + option: AgentWorkflowOptionUiConfig +): boolean { + return Boolean( + String(option.previewImageUrl || "").trim() || + String(option.previewImageSourceUrl || "").trim() + ); +} + +export function getAgentWorkflowMissingPreviewOptions( + steps: AgentWorkflowStepUiConfig[], + options: { enabledOnly?: boolean } = {} +): AgentWorkflowMissingPreviewOption[] { + const enabledOnly = options.enabledOnly !== false; + const missingOptions: AgentWorkflowMissingPreviewOption[] = []; + + const walkOptions = ( + optionItems: AgentWorkflowOptionUiConfig[], + stepKey: string, + stepPath: string, + parentPath: string[] = [] + ) => { + for (const option of [...(optionItems || [])].sort( + (left, right) => left.sortOrder - right.sortOrder + )) { + const disabled = option.enabled === false; + const label = option.buttonName || option.key; + const pathParts = [...parentPath, label]; + + if ((!enabledOnly || !disabled) && !hasAgentWorkflowOptionOwnPreview(option)) { + missingOptions.push({ + key: option.key, + label, + path: pathParts.join(" / "), + stepKey, + stepPath, + disabled, + childCount: (option.children || []).length, + }); + } + + if (!enabledOnly || !disabled) { + walkOptions(option.children || [], stepKey, stepPath, pathParts); + } + } + }; + + const walkSteps = (stepItems: AgentWorkflowStepUiConfig[], parentPath: string[] = []) => { + for (const step of [...(stepItems || [])].sort( + (left, right) => left.sortOrder - right.sortOrder + )) { + if (enabledOnly && step.enabled === false) { + continue; + } + + const stepTitle = step.title || step.key; + const stepPathParts = [...parentPath, stepTitle]; + const stepPath = stepPathParts.join(" / "); + walkOptions(step.options || [], step.key, stepPath); + walkSteps(step.children || [], stepPathParts); + } + }; + + walkSteps(steps || []); + return missingOptions; +} + function collectWorkflowOptionKeySetFromSteps( steps: AgentWorkflowStepUiConfig[] ): Set {