fix(workflow): preserve shared App cache across editor teardown

This commit is contained in:
yyh
2026-10-15 23:10:53 +08:00
parent 7ac3d2624f
commit 8e9d4d8d80
7 changed files with 37 additions and 65 deletions
+1 -1
View File
@@ -49,7 +49,7 @@ const RagPipelineWrapper = () => {
if (!pipelineId) return <Conversion />
return (
<EditorResourceScope identity={{ kind: 'knowledge-pipeline', pipelineId }}>
<EditorResourceScope resource={{ kind: 'knowledge-pipeline', pipelineId }}>
<WorkflowContextProvider injectWorkflowStoreSliceFn={createRagPipelineSliceSlice}>
<RagPipeline />
</WorkflowContextProvider>
+1 -1
View File
@@ -80,7 +80,7 @@ const SnippetPage = () => {
const SnippetPageWrapper = ({ snippetId }: SnippetPageProps) => {
return (
<EditorResourceScope identity={{ kind: 'snippet', snippetId }}>
<EditorResourceScope resource={{ kind: 'snippet', snippetId }}>
<WorkflowContextProvider>
<SnippetPage />
</WorkflowContextProvider>
+1 -1
View File
@@ -150,7 +150,7 @@ const WorkflowAppWrapper = () => {
if (!isWorkflowAppMode(mode)) throw new Error('This app does not have a workflow editor')
return (
<EditorResourceScope identity={{ kind: 'app', appId }}>
<EditorResourceScope resource={{ kind: 'app', appId, mode }}>
<WorkflowContextProvider
injectWorkflowStoreSliceFn={createWorkflowSlice as InjectWorkflowStoreSliceFn}
>
@@ -1,28 +1,22 @@
'use client'
import type { ReactNode } from 'react'
import type { EditorIdentity } from './state'
import type { EditorResource } from './resource'
import { ScopeProvider } from 'jotai-scope'
import { editorIdentityStateAtom } from './state'
import { getEditorResourceId } from './resource'
import { editorResourceAtom } from './state'
export function EditorResourceScope({
identity,
resource,
children,
}: {
identity: EditorIdentity
resource: EditorResource
children: ReactNode
}) {
const id =
identity.kind === 'app'
? identity.appId
: identity.kind === 'knowledge-pipeline'
? identity.pipelineId
: identity.snippetId
return (
<ScopeProvider
key={`${identity.kind}:${id}`}
atoms={[[editorIdentityStateAtom, identity]]}
key={`${resource.kind}:${getEditorResourceId(resource)}`}
atoms={[[editorResourceAtom, resource]]}
name="WorkflowEditor"
>
{children}
@@ -1,68 +1,39 @@
import type { EditorResource } from './resource'
import { atom } from 'jotai'
import { atomWithLazy } from 'jotai/utils'
import { isWorkflowAppMode } from '@/app/components/apps/app-type'
import { consoleQuery } from '@/service/console'
import { atomWithResolvedSuspenseQuery } from '@/utils/query-atoms'
import { isChatflow } from './resource'
export type EditorIdentity =
| Readonly<{ kind: 'app'; appId: string }>
| Readonly<{ kind: 'knowledge-pipeline'; pipelineId: string }>
| Readonly<{ kind: 'snippet'; snippetId: string }>
export const editorIdentityStateAtom = atomWithLazy<EditorIdentity>(() => {
export const editorResourceAtom = atomWithLazy<EditorResource>(() => {
throw new Error('EditorResourceScope is required for the workflow editor')
})
export const editorResourceKindAtom = atom((get) => get(editorIdentityStateAtom).kind)
export const editorResourceKindAtom = atom((get) => get(editorResourceAtom).kind)
export const editorAppIdAtom = atom((get) => {
const identity = get(editorIdentityStateAtom)
return identity.kind === 'app' ? identity.appId : undefined
const resource = get(editorResourceAtom)
return resource.kind === 'app' ? resource.appId : undefined
})
export const editorPipelineIdAtom = atom((get) => {
const identity = get(editorIdentityStateAtom)
return identity.kind === 'knowledge-pipeline' ? identity.pipelineId : undefined
const resource = get(editorResourceAtom)
return resource.kind === 'knowledge-pipeline' ? resource.pipelineId : undefined
})
// The app entrypoint resolves this query before mounting its editor session.
const editorAppQueryAtom = atomWithResolvedSuspenseQuery((get) => {
const identity = get(editorIdentityStateAtom)
if (identity.kind !== 'app') throw new Error('App details require an app editor')
return consoleQuery.apps.byAppId.get.queryOptions({
input: { params: { app_id: identity.appId } },
})
})
const appModeAtom = atom((get) => get(editorAppQueryAtom).data.mode)
export const editorAppNameAtom = atom((get) =>
get(editorResourceKindAtom) === 'app' ? get(editorAppQueryAtom).data.name : undefined,
)
export const editorResourceAtom = atom<EditorResource>((get) => {
const identity = get(editorIdentityStateAtom)
if (identity.kind !== 'app') return identity
const mode = get(appModeAtom)
if (!isWorkflowAppMode(mode)) throw new Error('This app does not have a workflow editor')
return { ...identity, mode }
})
export const isChatflowAtom = atom((get) => isChatflow(get(editorResourceAtom)))
export const workflowAppIdentityAtom = atom((get) => {
const identity = get(editorIdentityStateAtom)
if (identity.kind !== 'app') throw new Error('This operation requires a workflow app editor')
return identity
const resource = get(editorResourceAtom)
if (resource.kind !== 'app') throw new Error('This operation requires a workflow app editor')
return resource
})
export const knowledgePipelineIdentityAtom = atom((get) => {
const identity = get(editorIdentityStateAtom)
if (identity.kind !== 'knowledge-pipeline')
const resource = get(editorResourceAtom)
if (resource.kind !== 'knowledge-pipeline')
throw new Error('This operation requires a knowledge pipeline editor')
return identity
return resource
})
export const snippetIdentityAtom = atom((get) => {
const identity = get(editorIdentityStateAtom)
if (identity.kind !== 'snippet') throw new Error('This operation requires a snippet editor')
return identity
const resource = get(editorResourceAtom)
if (resource.kind !== 'snippet') throw new Error('This operation requires a snippet editor')
return resource
})
+2 -3
View File
@@ -577,17 +577,16 @@ export const Workflow: FC<WorkflowProps> = memo(
const dataSourceList = useStore((s) => s.dataSourceList)
// buildInTools, customTools, workflowTools, mcpTools, dataSourceList
const resource = useAtomValue(editorResourceAtom)
const resourceId = getEditorResourceId(resource)
const [isLoadedVars, setIsLoadedVars] = useState(false)
const [vars, setVars] = useState<VarInInspect[]>([])
useEffect(() => {
;(async () => {
if (!resource.kind || !resourceId) return
if (!getEditorResourceId(resource)) return
const data = await fetchAllInspectVars(resource)
setVars(data)
setIsLoadedVars(true)
})()
}, [resource.kind, resourceId])
}, [resource])
useEffect(() => {
if (schemaTypeDefinitions && isLoadedVars) {
fetchInspectVars({
@@ -8,14 +8,16 @@ import {
DropdownMenuTrigger,
} from '@langgenius/dify-ui/dropdown-menu'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { skipToken, useQuery } from '@tanstack/react-query'
import { toJpeg, toPng, toSvg } from 'html-to-image'
import { useAtomValue } from 'jotai'
import { memo, useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { getNodesBounds, useReactFlow } from 'reactflow'
import ImagePreview from '@/app/components/base/image-uploader/image-preview'
import { editorAppNameAtom } from '@/app/components/workflow/editor-resource/state'
import { editorAppIdAtom } from '@/app/components/workflow/editor-resource/state'
import { useStore } from '@/app/components/workflow/store'
import { consoleQuery } from '@/service/console'
import { downloadUrl } from '@/utils/download'
import { useNodesReadOnly } from '../hooks/use-workflow'
import TipPopup from './tip-popup'
@@ -28,7 +30,13 @@ const ExportImage = memo(() => {
const [previewUrl, setPreviewUrl] = useState('')
const [previewTitle, setPreviewTitle] = useState('')
const knowledgeName = useStore((s) => s.knowledgeName)
const appName = useAtomValue(editorAppNameAtom)
const appId = useAtomValue(editorAppIdAtom)
const { data: appName } = useQuery(
consoleQuery.apps.byAppId.get.queryOptions({
input: appId ? { params: { app_id: appId } } : skipToken,
select: (app) => app.name,
}),
)
const isReadOnly = getNodesReadOnly()
const handleExportImage = useCallback(