fix(workflow): prevent redundant sandbox download refetch after reset

Problem:\n- In variable inspect artifacts view, clicking Reset All invalidates sandbox download query keys.\n- If a previously selected file has been removed, the download-url query may still refetch with stale path and return 400.\n- Default query retry amplifies this into repeated failed requests in this scenario.\n\nSolution:\n- Extend sandbox file invalidation with an option to skip download query refetch.\n- Use that option in Reset All flow so download-url queries are marked stale without immediate refetch.\n- Derive selected file path from latest sandbox flat data and disable download-url query when file no longer exists.\n- Disable retry only for artifacts-tab download-url query to avoid repeated 400 retries in this path.\n- Align tree selectedPath with derived selectedFilePath and add hook tests for invalidation behavior.\n\nValidation:\n- pnpm vitest --run service/use-sandbox-file.spec.tsx
This commit is contained in:
yyh
2026-02-07 22:43:13 +08:00
parent a761ab5cee
commit 2b848d7e93
4 changed files with 93 additions and 8 deletions

View File

@ -224,7 +224,7 @@ export const useInspectVarsCrudCommon = ({
await Promise.all([
invalidateConversationVarValues(),
invalidateSysVarValues(),
invalidateSandboxFiles(),
invalidateSandboxFiles({ refetchDownloadFile: false }),
])
deleteAllInspectVars()
handleEdgeCancelRunningStatus()

View File

@ -4,7 +4,7 @@ import {
RiCloseLine,
RiMenuLine,
} from '@remixicon/react'
import { useCallback, useState } from 'react'
import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import SearchLinesSparkle from '@/app/components/base/icons/src/vender/knowledge/SearchLinesSparkle'
@ -62,15 +62,27 @@ const ArtifactsTab = (headerProps: InspectHeaderProps) => {
)
const isResponding = useStore(s => s.isResponding)
const { data: treeData, hasFiles, isLoading } = useSandboxFilesTree(appId, {
const { data: treeData, flatData, hasFiles, isLoading } = useSandboxFilesTree(appId, {
enabled: !!appId,
refetchInterval: (isWorkflowRunning || isResponding) ? 5000 : false,
})
const { mutateAsync: fetchDownloadUrl, isPending: isDownloading } = useDownloadSandboxFile(appId)
const [selectedFile, setSelectedFile] = useState<SandboxFileTreeNode | null>(null)
const selectedFilePath = useMemo(() => {
if (!selectedFile)
return undefined
const selectedExists = flatData?.some(
node => !node.is_dir && node.path === selectedFile.path,
) ?? false
return selectedExists ? selectedFile.path : undefined
}, [flatData, selectedFile])
const { data: downloadUrlData, isLoading: isDownloadUrlLoading } = useSandboxFileDownloadUrl(
appId,
selectedFile?.path,
selectedFilePath,
{ retry: false },
)
const handleFileSelect = useCallback((node: SandboxFileTreeNode) => {
@ -113,7 +125,7 @@ const ArtifactsTab = (headerProps: InspectHeaderProps) => {
)
}
const file = selectedFile
const file = selectedFilePath ? selectedFile : null
const parts = file?.path.split('/') ?? []
let cumPath = ''
const pathSegments = parts.map((part, i) => {
@ -130,7 +142,7 @@ const ArtifactsTab = (headerProps: InspectHeaderProps) => {
data={treeData}
onDownload={handleTreeDownload}
onSelect={handleFileSelect}
selectedPath={selectedFile?.path}
selectedPath={selectedFilePath}
isDownloading={isDownloading}
/>
</div>

View File

@ -0,0 +1,60 @@
import type { ReactNode } from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { act, renderHook } from '@testing-library/react'
import { consoleQuery } from './client'
import { useInvalidateSandboxFiles } from './use-sandbox-file'
const createQueryClient = () => new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
const createWrapper = (queryClient: QueryClient) => {
return ({ children }: { children: ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
}
describe('useInvalidateSandboxFiles', () => {
it('should keep download query refetch enabled by default', async () => {
const queryClient = createQueryClient()
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries').mockResolvedValue(undefined)
const { result } = renderHook(() => useInvalidateSandboxFiles(), {
wrapper: createWrapper(queryClient),
})
await act(async () => {
await result.current()
})
expect(invalidateSpy).toHaveBeenNthCalledWith(1, {
queryKey: consoleQuery.sandboxFile.listFiles.key(),
})
expect(invalidateSpy).toHaveBeenNthCalledWith(2, {
queryKey: consoleQuery.sandboxFile.downloadFile.key(),
})
})
it('should disable download query refetch when requested', async () => {
const queryClient = createQueryClient()
const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries').mockResolvedValue(undefined)
const { result } = renderHook(() => useInvalidateSandboxFiles(), {
wrapper: createWrapper(queryClient),
})
await act(async () => {
await result.current({ refetchDownloadFile: false })
})
expect(invalidateSpy).toHaveBeenNthCalledWith(1, {
queryKey: consoleQuery.sandboxFile.listFiles.key(),
})
expect(invalidateSpy).toHaveBeenNthCalledWith(2, {
queryKey: consoleQuery.sandboxFile.downloadFile.key(),
refetchType: 'none',
})
})
})

View File

@ -14,6 +14,15 @@ type UseGetSandboxFilesOptions = {
refetchInterval?: number | false
}
type UseSandboxFileDownloadUrlOptions = {
enabled?: boolean
retry?: boolean | number
}
type InvalidateSandboxFilesOptions = {
refetchDownloadFile?: boolean
}
export function useGetSandboxFiles(
appId: string | undefined,
options?: UseGetSandboxFilesOptions,
@ -39,6 +48,7 @@ export function useGetSandboxFiles(
export function useSandboxFileDownloadUrl(
appId: string | undefined,
path: string | undefined,
options?: UseSandboxFileDownloadUrlOptions,
) {
return useQuery({
queryKey: consoleQuery.sandboxFile.downloadFile.queryKey({
@ -48,19 +58,22 @@ export function useSandboxFileDownloadUrl(
params: { appId: appId! },
body: { path: path! },
}),
enabled: !!appId && !!path,
enabled: !!appId && !!path && (options?.enabled ?? true),
retry: options?.retry,
})
}
export function useInvalidateSandboxFiles() {
const queryClient = useQueryClient()
return useCallback(() => {
return useCallback((options?: InvalidateSandboxFilesOptions) => {
const shouldRefetchDownloadFile = options?.refetchDownloadFile ?? true
return Promise.all([
queryClient.invalidateQueries({
queryKey: consoleQuery.sandboxFile.listFiles.key(),
}),
queryClient.invalidateQueries({
queryKey: consoleQuery.sandboxFile.downloadFile.key(),
...(shouldRefetchDownloadFile ? {} : { refetchType: 'none' as const }),
}),
])
}, [queryClient])