From c16862bd90504407234d7c494034f4b60bca8809 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 26 Mar 2026 19:00:37 +0800 Subject: [PATCH] fix agent workflow prompt state sync --- app/canvas/[id]/page.tsx | 214 ++++++++++++------ .../agents/AgentGenerationWorkspaceDarkV2.tsx | 163 +++---------- lib/agents/workflow-composer.ts | 19 +- lib/agents/workflow-ui-config.ts | 36 +++ 4 files changed, 223 insertions(+), 209 deletions(-) diff --git a/app/canvas/[id]/page.tsx b/app/canvas/[id]/page.tsx index 911ee17fb..0bbac3957 100644 --- a/app/canvas/[id]/page.tsx +++ b/app/canvas/[id]/page.tsx @@ -74,6 +74,7 @@ import { AgentWorkflowBrandStorePanel, AgentWorkflowPanelDarkV2, } from "@/components/agents/AgentWorkflowPanelDarkV2"; +import { AgentCanvasTopNavDark } from "@/components/agents/AgentCanvasTopNavDark"; import { AgentGenerationWorkspaceDarkV2 } from "@/components/agents/AgentGenerationWorkspaceDarkV2"; import { AgentProjectListSheet } from "@/components/agents/AgentProjectListSheet"; import { CanvasWorkspace } from "@/components/canvas"; @@ -86,7 +87,9 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { cn, fetcher } from "@/lib/utils"; import { defaultAgentWorkflowUiConfig, + flattenVisibleAgentWorkflowSteps, normalizeAgentWorkflowUiConfig, + sanitizeAgentWorkflowSelections, } from "@/lib/agents/workflow-ui-config"; import { createEmptyAgentWorkflowMaterialSelections, @@ -411,6 +414,18 @@ export default function CanvasEditorPage({ params }: PageProps) { isAgentWorkspaceEnabled && requestedAgentMode; const hasLoadedCurrentAgentProject = !requestedAgentMode || !agentProjectId || loadedAgentProjectId === agentProjectId; + const resolvedWorkflowUiConfig = useMemo( + () => normalizeAgentWorkflowUiConfig(siteSettingsData?.agentWorkflowUiConfig), + [siteSettingsData?.agentWorkflowUiConfig] + ); + const sanitizedWorkflowSelections = useMemo( + () => + sanitizeAgentWorkflowSelections({ + config: resolvedWorkflowUiConfig || defaultAgentWorkflowUiConfig, + selections: workflowSelections, + }), + [resolvedWorkflowUiConfig, workflowSelections] + ); const resolvedBrandModelConfig = useMemo( () => normalizeAgentWorkflowBrandModelConfig(siteSettingsData?.agentWorkflowBrandModelConfig), [siteSettingsData?.agentWorkflowBrandModelConfig] @@ -474,7 +489,7 @@ export default function CanvasEditorPage({ params }: PageProps) { priceBand: workflowInputs.priceBand.trim(), }, materials: workflowMaterials, - selections: workflowSelections, + selections: sanitizedWorkflowSelections, selectedBrandModel, selectedAgentPresetId: selectedAgentPresetId || null, currentStage: currentAgentStage, @@ -487,7 +502,7 @@ export default function CanvasEditorPage({ params }: PageProps) { selectedBrandModel, workflowInputs, workflowMaterials, - workflowSelections, + sanitizedWorkflowSelections, ]); const currentWorkflowPersistedStateRef = useRef( buildCurrentWorkflowPersistedState() @@ -658,7 +673,7 @@ export default function CanvasEditorPage({ params }: PageProps) { priceBand: workflowInputs.priceBand.trim(), }, materials: workflowMaterials, - selections: workflowSelections, + selections: sanitizedWorkflowSelections, selectedBrandModel, selectedAgentPresetId: selectedAgentPresetId || null, currentStage: currentAgentStage, @@ -715,7 +730,7 @@ export default function CanvasEditorPage({ params }: PageProps) { selectedBrandModel, workflowInputs, workflowMaterials, - workflowSelections, + sanitizedWorkflowSelections, ]); const persistedAgentProjectStateRef = useRef(buildPersistedAgentProjectState()); @@ -781,7 +796,7 @@ export default function CanvasEditorPage({ params }: PageProps) { JSON.stringify({ inputs: workflowInputs, materials: workflowMaterials, - selections: workflowSelections, + selections: sanitizedWorkflowSelections, selectedBrandModel, selectedAgentPresetId, currentAgentStage, @@ -796,7 +811,7 @@ export default function CanvasEditorPage({ params }: PageProps) { workflowHydrationSource, workflowInputs, workflowMaterials, - workflowSelections, + sanitizedWorkflowSelections, workflowStorageKey, selectedBrandModel, selectedAgentPresetId, @@ -841,48 +856,6 @@ export default function CanvasEditorPage({ params }: PageProps) { ] ); - useEffect(() => { - if (!requestedAgentMode || !isAgentWorkspaceEnabled || agentProjectId) return; - - let cancelled = false; - const createAgentProject = async () => { - setIsAgentProjectLoading(true); - try { - const result = await agentProjectService.createProject({ - title: buildDefaultAgentProjectTitle(), - projectState: createEmptyAgentGenerationProjectPersistedState(), - }); - if (!result.success || !result.data) { - throw new Error(result.error || "创建生图项目失败"); - } - if (cancelled) return; - toast.success("已为你创建新项目,可直接开始生图"); - updateCanvasSearchParams({ - mode: "agent", - agentProjectId: result.data.id, - }); - } catch (error) { - if (!cancelled) { - toast.error(error instanceof Error ? error.message : "创建生图项目失败"); - } - } finally { - if (!cancelled) { - setIsAgentProjectLoading(false); - } - } - }; - - void createAgentProject(); - return () => { - cancelled = true; - }; - }, [ - agentProjectId, - isAgentWorkspaceEnabled, - requestedAgentMode, - updateCanvasSearchParams, - ]); - useEffect(() => { if (!requestedAgentMode || !agentProjectId) return; @@ -1132,16 +1105,11 @@ export default function CanvasEditorPage({ params }: PageProps) { agentProjectId: nextProject.id, }); } else { - const createResult = await agentProjectService.createProject({ - title: buildDefaultAgentProjectTitle(), - projectState: createEmptyAgentGenerationProjectPersistedState(), + updateCanvasSearchParams({ + mode: "agent", + agentProjectId: null, }); - if (createResult.success && createResult.data) { - updateCanvasSearchParams({ - mode: "agent", - agentProjectId: createResult.data.id, - }); - } + setIsAgentProjectSheetOpen(true); } } @@ -1705,10 +1673,16 @@ export default function CanvasEditorPage({ params }: PageProps) { }; }, []); - const resolvedWorkflowUiConfig = useMemo( - () => normalizeAgentWorkflowUiConfig(siteSettingsData?.agentWorkflowUiConfig), - [siteSettingsData?.agentWorkflowUiConfig] - ); + useEffect(() => { + const currentSerialized = JSON.stringify(workflowSelections); + const sanitizedSerialized = JSON.stringify(sanitizedWorkflowSelections); + if (currentSerialized === sanitizedSerialized) { + return; + } + + workflowSelectionsRef.current = sanitizedWorkflowSelections; + setWorkflowSelections(sanitizedWorkflowSelections); + }, [sanitizedWorkflowSelections, workflowSelections]); const workflowContext = useMemo(() => { return buildAgentWorkflowComposerContext({ @@ -1716,14 +1690,14 @@ export default function CanvasEditorPage({ params }: PageProps) { inputs: workflowInputs, materials: workflowMaterials, selectedBrandModel, - selections: workflowSelections, + selections: sanitizedWorkflowSelections, }); }, [ resolvedWorkflowUiConfig, selectedBrandModel, + sanitizedWorkflowSelections, workflowInputs, workflowMaterials, - workflowSelections, ]); useEffect(() => { workflowInputsRef.current = workflowInputs; @@ -1732,8 +1706,8 @@ export default function CanvasEditorPage({ params }: PageProps) { workflowMaterialsRef.current = workflowMaterials; }, [workflowMaterials]); useEffect(() => { - workflowSelectionsRef.current = workflowSelections; - }, [workflowSelections]); + workflowSelectionsRef.current = sanitizedWorkflowSelections; + }, [sanitizedWorkflowSelections]); useEffect(() => { selectedBrandModelRef.current = selectedBrandModel; }, [selectedBrandModel]); @@ -1747,10 +1721,39 @@ export default function CanvasEditorPage({ params }: PageProps) { }, []); const handleWorkflowSelectionsChange = useCallback( (next: Partial>) => { - workflowSelectionsRef.current = next; - setWorkflowSelections(next); + const normalizedCurrentSelections = sanitizeAgentWorkflowSelections({ + config: resolvedWorkflowUiConfig || defaultAgentWorkflowUiConfig, + selections: workflowSelectionsRef.current, + }); + const normalizedNextSelections = sanitizeAgentWorkflowSelections({ + config: resolvedWorkflowUiConfig || defaultAgentWorkflowUiConfig, + selections: next, + }); + + const orderedStepKeys = flattenVisibleAgentWorkflowSteps( + (resolvedWorkflowUiConfig || defaultAgentWorkflowUiConfig).steps, + normalizedCurrentSelections + ).map(({ step }) => step.key); + const earliestChangedIndex = orderedStepKeys.findIndex((stepKey) => { + return String(normalizedCurrentSelections[stepKey] || "") !== String(normalizedNextSelections[stepKey] || ""); + }); + + let clearedSelections = { ...normalizedNextSelections }; + if (earliestChangedIndex >= 0) { + orderedStepKeys.slice(earliestChangedIndex + 1).forEach((stepKey) => { + delete clearedSelections[stepKey]; + }); + } + + clearedSelections = sanitizeAgentWorkflowSelections({ + config: resolvedWorkflowUiConfig || defaultAgentWorkflowUiConfig, + selections: clearedSelections, + }); + + workflowSelectionsRef.current = clearedSelections; + setWorkflowSelections(clearedSelections); }, - [] + [resolvedWorkflowUiConfig] ); const handleSelectedBrandModelChange = useCallback( (next: AgentWorkflowSelectedBrandModel | null) => { @@ -1802,12 +1805,14 @@ export default function CanvasEditorPage({ params }: PageProps) { () => getAgentWorkflowProgress({ config: resolvedWorkflowUiConfig || defaultAgentWorkflowUiConfig, - selections: workflowSelections, + selections: sanitizedWorkflowSelections, }), - [resolvedWorkflowUiConfig, workflowSelections] + [resolvedWorkflowUiConfig, sanitizedWorkflowSelections] ); const resolvedAgentProjectTitle = - String(agentProjectTitle || "").trim() || "未命名生图项目"; + hasActiveAgentProject + ? String(agentProjectTitle || "").trim() || "未命名生图项目" + : "未选择项目"; if (isLoading) { return ( @@ -1949,11 +1954,74 @@ export default function CanvasEditorPage({ params }: PageProps) { ) : null; + const agentProjectSelectionScreen = isAgentMode && !hasActiveAgentProject ? ( +
+ +
+
+
+
+ +
+

+ 先选择一个生图项目 +

+

+ 打开历史项目可以继续沿用之前的生图结果、模式和工作流。只有在你明确点击新建项目时,系统才会创建一个全新的智能体项目。 +

+
+ +
+ + + +
+
+
+
+ ) : null; + if (isAgentMode) { + if (!hasActiveAgentProject) { + return agentProjectSelectionScreen; + } + return ( diff --git a/components/agents/AgentGenerationWorkspaceDarkV2.tsx b/components/agents/AgentGenerationWorkspaceDarkV2.tsx index 58a89efb4..3c8480d8a 100644 --- a/components/agents/AgentGenerationWorkspaceDarkV2.tsx +++ b/components/agents/AgentGenerationWorkspaceDarkV2.tsx @@ -22,6 +22,7 @@ import { Download, ImagePlus, Loader2, + Maximize2, Sparkles, Upload, UserRound, @@ -144,11 +145,6 @@ interface GeneratedImageGroup { text: string; } -interface SceneResultToolbarTarget { - url: string; - index: number; -} - interface AgentModeReferenceSearchResponse { success?: boolean; data?: { @@ -730,6 +726,7 @@ interface LocalGenerationPendingState { const AGENT_PENDING_GENERATION_STORAGE_KEY = "ai-chatbot-agent-pending-generations"; const AGENT_PENDING_GENERATION_EXPIRY_MS = 30 * 60 * 1000; +const AGENT_SCENE_PENDING_FAILURE_GRACE_MS = 3 * 60 * 1000; function getStoredAgentPendingGenerations(): LocalGenerationPendingState[] { if (typeof window === "undefined") { @@ -942,8 +939,6 @@ export function AgentGenerationWorkspaceDarkV2({ const [activeResultModeType, setActiveResultModeType] = useState("scene_generation"); const [fullscreenPreview, setFullscreenPreview] = useState(null); - const [sceneResultToolbarTarget, setSceneResultToolbarTarget] = - useState(null); const [sceneResizeEditorTargetUrl, setSceneResizeEditorTargetUrl] = useState(null); const [batchDownloadState, setBatchDownloadState] = useState(null); const [detailReplacementSearchState, setDetailReplacementSearchState] = useState<{ @@ -2194,6 +2189,14 @@ export function AgentGenerationWorkspaceDarkV2({ ? detailGenerationRunResults.length > 0 : latestReplacementResults.length > 0; + if ( + pendingGeneration.modeType === "scene_generation" && + !hasProgress && + Date.now() - pendingGeneration.timestamp < AGENT_SCENE_PENDING_FAILURE_GRACE_MS + ) { + return; + } + if (!hasProgress) { setPendingGeneration(null); } @@ -2226,17 +2229,6 @@ export function AgentGenerationWorkspaceDarkV2({ } }, [activeResultModeUrls, selectedResultUrl]); - useEffect(() => { - if ( - sceneResultToolbarTarget?.url && - !normalizeResultCollectionUrls(mergedResultCollections.scene_generation).includes( - sceneResultToolbarTarget.url - ) - ) { - setSceneResultToolbarTarget(null); - } - }, [mergedResultCollections.scene_generation, sceneResultToolbarTarget]); - useEffect(() => { if (!selectedAgentPresetId || !hasAgentUploadSlots) { setIsAttachmentSlotExpanded(false); @@ -2642,7 +2634,6 @@ export function AgentGenerationWorkspaceDarkV2({ setActiveResultModeType("scene_generation"); setSelectedResultUrl(finalUrl); setSceneResizeEditorTargetUrl(null); - setSceneResultToolbarTarget(null); toast.success("尺寸调整结果已加入场景结果区"); }, [mergedResultCollections.scene_generation, onResultPreviewUrlsChange, uploadBase64ResultToStorage] @@ -2660,9 +2651,6 @@ export function AgentGenerationWorkspaceDarkV2({ if (fullscreenPreview === normalizedUrl) { setFullscreenPreview(null); } - if (sceneResultToolbarTarget?.url === normalizedUrl) { - setSceneResultToolbarTarget(null); - } if (sceneResizeEditorTargetUrl === normalizedUrl) { setSceneResizeEditorTargetUrl(null); } @@ -2670,13 +2658,7 @@ export function AgentGenerationWorkspaceDarkV2({ onDeleteResultImage(modeType, normalizedUrl); toast.success("结果图已删除"); }, - [ - fullscreenPreview, - onDeleteResultImage, - sceneResizeEditorTargetUrl, - sceneResultToolbarTarget, - selectedResultUrl, - ] + [fullscreenPreview, onDeleteResultImage, sceneResizeEditorTargetUrl, selectedResultUrl] ); const handleToolStepChange = useCallback( @@ -5481,12 +5463,6 @@ export function AgentGenerationWorkspaceDarkV2({ onClick={() => { if (!card.url) return; setSelectedResultUrl(card.url); - if (isSceneResultCard) { - setSceneResultToolbarTarget({ - url: card.url, - index: card.index, - }); - } }} onDoubleClick={() => { if (!card.url) return; @@ -5498,12 +5474,6 @@ export function AgentGenerationWorkspaceDarkV2({ if (event.key === "Enter" || event.key === " ") { event.preventDefault(); setSelectedResultUrl(card.url); - if (isSceneResultCard) { - setSceneResultToolbarTarget({ - url: card.url, - index: card.index, - }); - } } }} className={cn( @@ -5524,9 +5494,23 @@ export function AgentGenerationWorkspaceDarkV2({ loading="lazy" /> {isSceneResultCard ? ( -
- 单击打开工具栏,双击查看大图 -
+ <> + +
+ 双击查看大图 +
+ ) : null} - - -
- {`场景结果图 - handlePreviewImageLoadError(event, sceneResultToolbarTarget.url) - } - /> -
- -
- - - -
- - - ) : null} - - {sceneResizeEditorTargetUrl ? ( ; } >(); + const globalValueSet = new Set(); - params.selections.forEach((item) => { + const sourceSelections = params.leafOnly + ? params.selections.filter((item, index, items) => { + const nextItem = items[index + 1]; + return !nextItem || nextItem.stepKey !== item.stepKey; + }) + : params.selections; + + sourceSelections.forEach((item) => { const rawValue = params.valueKey === "promptFragment" ? item.promptFragment || item.optionName @@ -121,8 +131,14 @@ function buildGroupedSelectionLines(params: { }; const dedupeKey = normalizedValue.toLowerCase(); + if (params.dedupeAcrossSteps && globalValueSet.has(dedupeKey)) { + return; + } if (!currentGroup.valueSet.has(dedupeKey)) { currentGroup.valueSet.add(dedupeKey); + if (params.dedupeAcrossSteps) { + globalValueSet.add(dedupeKey); + } currentGroup.values.push(normalizedValue); } @@ -312,6 +328,7 @@ export function buildAgentWorkflowComposerContext(params: { selections, valueKey: "promptFragment", separator: ";", + dedupeAcrossSteps: true, }) ); diff --git a/lib/agents/workflow-ui-config.ts b/lib/agents/workflow-ui-config.ts index 77d86dfd8..bbfe01f84 100644 --- a/lib/agents/workflow-ui-config.ts +++ b/lib/agents/workflow-ui-config.ts @@ -701,6 +701,42 @@ export function getAgentWorkflowSelectedOptionPath(params: { return selectedOptions; } +export function sanitizeAgentWorkflowSelections(params: { + config?: AgentWorkflowUiConfig | null; + selections: Partial>; +}) { + const config = params.config || defaultAgentWorkflowUiConfig; + const enabledSteps = flattenAgentWorkflowSteps( + getEnabledAgentWorkflowStepTree(Array.isArray(config.steps) ? config.steps : []) + ).map(({ step }) => step); + + const sanitized: Partial> = {}; + + enabledSteps.forEach((step) => { + const rawSelectionValue = String(params.selections[step.key] || "").trim(); + if (!rawSelectionValue) { + return; + } + + const selectedOptions = getAgentWorkflowSelectedOptionPath({ + step, + selectionValue: rawSelectionValue, + }); + if (selectedOptions.length === 0) { + return; + } + + const serialized = serializeAgentWorkflowSelectionPath( + selectedOptions.map((option) => option.key) + ); + if (serialized) { + sanitized[step.key] = serialized; + } + }); + + return sanitized; +} + export function getAgentWorkflowBlockedOptionKeys(params: { step: AgentWorkflowStepUiConfig; selectionValue?: string | null;