Feat/workflow retry (#11885)

This commit is contained in:
zxhlyh 2024-12-20 15:44:37 +08:00 committed by GitHub
parent dacd457478
commit 0c0120ef27
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 690 additions and 51 deletions

View File

@ -64,6 +64,12 @@ const WorkflowProcessItem = ({
setShowMessageLogModal(true) setShowMessageLogModal(true)
}, [item, setCurrentLogItem, setCurrentLogModalActiveTab, setShowMessageLogModal]) }, [item, setCurrentLogItem, setCurrentLogModalActiveTab, setShowMessageLogModal])
const showRetryDetail = useCallback(() => {
setCurrentLogItem(item)
setCurrentLogModalActiveTab('TRACING')
setShowMessageLogModal(true)
}, [item, setCurrentLogItem, setCurrentLogModalActiveTab, setShowMessageLogModal])
return ( return (
<div <div
className={cn( className={cn(
@ -105,6 +111,7 @@ const WorkflowProcessItem = ({
<TracingPanel <TracingPanel
list={data.tracing} list={data.tracing}
onShowIterationDetail={showIterationDetail} onShowIterationDetail={showIterationDetail}
onShowRetryDetail={showRetryDetail}
hideNodeInfo={hideInfo} hideNodeInfo={hideInfo}
hideNodeProcessDetail={hideProcessDetail} hideNodeProcessDetail={hideProcessDetail}
/> />

View File

@ -28,6 +28,7 @@ export type InputProps = {
destructive?: boolean destructive?: boolean
wrapperClassName?: string wrapperClassName?: string
styleCss?: CSSProperties styleCss?: CSSProperties
unit?: string
} & React.InputHTMLAttributes<HTMLInputElement> & VariantProps<typeof inputVariants> } & React.InputHTMLAttributes<HTMLInputElement> & VariantProps<typeof inputVariants>
const Input = ({ const Input = ({
@ -43,6 +44,7 @@ const Input = ({
value, value,
placeholder, placeholder,
onChange, onChange,
unit,
...props ...props
}: InputProps) => { }: InputProps) => {
const { t } = useTranslation() const { t } = useTranslation()
@ -80,6 +82,13 @@ const Input = ({
{destructive && ( {destructive && (
<RiErrorWarningLine className='absolute right-2 top-1/2 -translate-y-1/2 w-4 h-4 text-text-destructive-secondary' /> <RiErrorWarningLine className='absolute right-2 top-1/2 -translate-y-1/2 w-4 h-4 text-text-destructive-secondary' />
)} )}
{
unit && (
<div className='absolute right-2 top-1/2 -translate-y-1/2 system-sm-regular text-text-tertiary'>
{unit}
</div>
)
}
</div> </div>
) )
} }

View File

@ -506,3 +506,5 @@ export const WORKFLOW_DATA_UPDATE = 'WORKFLOW_DATA_UPDATE'
export const CUSTOM_NODE = 'custom' export const CUSTOM_NODE = 'custom'
export const CUSTOM_EDGE = 'custom' export const CUSTOM_EDGE = 'custom'
export const DSL_EXPORT_CHECK = 'DSL_EXPORT_CHECK' export const DSL_EXPORT_CHECK = 'DSL_EXPORT_CHECK'
export const DEFAULT_RETRY_MAX = 3
export const DEFAULT_RETRY_INTERVAL = 100

View File

@ -28,6 +28,7 @@ import {
getFilesInLogs, getFilesInLogs,
} from '@/app/components/base/file-uploader/utils' } from '@/app/components/base/file-uploader/utils'
import { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types' import { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types'
import type { NodeTracing } from '@/types/workflow'
export const useWorkflowRun = () => { export const useWorkflowRun = () => {
const store = useStoreApi() const store = useStoreApi()
@ -114,6 +115,7 @@ export const useWorkflowRun = () => {
onIterationStart, onIterationStart,
onIterationNext, onIterationNext,
onIterationFinish, onIterationFinish,
onNodeRetry,
onError, onError,
...restCallback ...restCallback
} = callback || {} } = callback || {}
@ -440,10 +442,13 @@ export const useWorkflowRun = () => {
}) })
if (currentIndex > -1 && draft.tracing) { if (currentIndex > -1 && draft.tracing) {
draft.tracing[currentIndex] = { draft.tracing[currentIndex] = {
...data,
...(draft.tracing[currentIndex].extras ...(draft.tracing[currentIndex].extras
? { extras: draft.tracing[currentIndex].extras } ? { extras: draft.tracing[currentIndex].extras }
: {}), : {}),
...data, ...(draft.tracing[currentIndex].retryDetail
? { retryDetail: draft.tracing[currentIndex].retryDetail }
: {}),
} as any } as any
} }
})) }))
@ -616,6 +621,41 @@ export const useWorkflowRun = () => {
if (onIterationFinish) if (onIterationFinish)
onIterationFinish(params) onIterationFinish(params)
}, },
onNodeRetry: (params) => {
const { data } = params
const {
workflowRunningData,
setWorkflowRunningData,
} = workflowStore.getState()
const {
getNodes,
setNodes,
} = store.getState()
const nodes = getNodes()
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
const tracing = draft.tracing!
const currentRetryNodeIndex = tracing.findIndex(trace => trace.node_id === data.node_id)
if (currentRetryNodeIndex > -1) {
const currentRetryNode = tracing[currentRetryNodeIndex]
if (currentRetryNode.retryDetail)
draft.tracing![currentRetryNodeIndex].retryDetail!.push(data as NodeTracing)
else
draft.tracing![currentRetryNodeIndex].retryDetail = [data as NodeTracing]
}
}))
const newNodes = produce(nodes, (draft) => {
const currentNode = draft.find(node => node.id === data.node_id)!
currentNode.data._retryIndex = data.retry_index
})
setNodes(newNodes)
if (onNodeRetry)
onNodeRetry(params)
},
onParallelBranchStarted: (params) => { onParallelBranchStarted: (params) => {
// console.log(params, 'parallel start') // console.log(params, 'parallel start')
}, },

View File

@ -17,17 +17,25 @@ import ResultPanel from '@/app/components/workflow/run/result-panel'
import Toast from '@/app/components/base/toast' import Toast from '@/app/components/base/toast'
import { TransferMethod } from '@/types/app' import { TransferMethod } from '@/types/app'
import { getProcessedFiles } from '@/app/components/base/file-uploader/utils' import { getProcessedFiles } from '@/app/components/base/file-uploader/utils'
import type { NodeTracing } from '@/types/workflow'
import RetryResultPanel from '@/app/components/workflow/run/retry-result-panel'
import type { BlockEnum } from '@/app/components/workflow/types'
import type { Emoji } from '@/app/components/tools/types'
const i18nPrefix = 'workflow.singleRun' const i18nPrefix = 'workflow.singleRun'
type BeforeRunFormProps = { type BeforeRunFormProps = {
nodeName: string nodeName: string
nodeType?: BlockEnum
toolIcon?: string | Emoji
onHide: () => void onHide: () => void
onRun: (submitData: Record<string, any>) => void onRun: (submitData: Record<string, any>) => void
onStop: () => void onStop: () => void
runningStatus: NodeRunningStatus runningStatus: NodeRunningStatus
result?: JSX.Element result?: JSX.Element
forms: FormProps[] forms: FormProps[]
retryDetails?: NodeTracing[]
onRetryDetailBack?: any
} }
function formatValue(value: string | any, type: InputVarType) { function formatValue(value: string | any, type: InputVarType) {
@ -50,12 +58,16 @@ function formatValue(value: string | any, type: InputVarType) {
} }
const BeforeRunForm: FC<BeforeRunFormProps> = ({ const BeforeRunForm: FC<BeforeRunFormProps> = ({
nodeName, nodeName,
nodeType,
toolIcon,
onHide, onHide,
onRun, onRun,
onStop, onStop,
runningStatus, runningStatus,
result, result,
forms, forms,
retryDetails,
onRetryDetailBack = () => { },
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
@ -122,11 +134,31 @@ const BeforeRunForm: FC<BeforeRunFormProps> = ({
<div className='text-base font-semibold text-gray-900 truncate'> <div className='text-base font-semibold text-gray-900 truncate'>
{t(`${i18nPrefix}.testRun`)} {nodeName} {t(`${i18nPrefix}.testRun`)} {nodeName}
</div> </div>
<div className='ml-2 shrink-0 p-1 cursor-pointer' onClick={onHide}> <div className='ml-2 shrink-0 p-1 cursor-pointer' onClick={() => {
onHide()
}}>
<RiCloseLine className='w-4 h-4 text-gray-500 ' /> <RiCloseLine className='w-4 h-4 text-gray-500 ' />
</div> </div>
</div> </div>
{
retryDetails?.length && (
<div className='h-0 grow overflow-y-auto pb-4'>
<RetryResultPanel
list={retryDetails.map((item, index) => ({
...item,
title: `${t('workflow.nodes.common.retry.retry')} ${index + 1}`,
node_type: nodeType!,
extras: {
icon: toolIcon!,
},
}))}
onBack={onRetryDetailBack}
/>
</div>
)
}
{
!retryDetails?.length && (
<div className='h-0 grow overflow-y-auto pb-4'> <div className='h-0 grow overflow-y-auto pb-4'>
<div className='mt-3 px-4 space-y-4'> <div className='mt-3 px-4 space-y-4'>
{forms.map((form, index) => ( {forms.map((form, index) => (
@ -140,7 +172,6 @@ const BeforeRunForm: FC<BeforeRunFormProps> = ({
</div> </div>
))} ))}
</div> </div>
<div className='mt-4 flex justify-between space-x-2 px-4' > <div className='mt-4 flex justify-between space-x-2 px-4' >
{isRunning && ( {isRunning && (
<div <div
@ -164,6 +195,8 @@ const BeforeRunForm: FC<BeforeRunFormProps> = ({
</> </>
)} )}
</div> </div>
)
}
</div> </div>
</div> </div>
) )

View File

@ -14,7 +14,6 @@ import type {
CommonNodeType, CommonNodeType,
Node, Node,
} from '@/app/components/workflow/types' } from '@/app/components/workflow/types'
import Split from '@/app/components/workflow/nodes/_base/components/split'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
type ErrorHandleProps = Pick<Node, 'id' | 'data'> type ErrorHandleProps = Pick<Node, 'id' | 'data'>
@ -45,7 +44,6 @@ const ErrorHandle = ({
return ( return (
<> <>
<Split />
<div className='py-4'> <div className='py-4'>
<Collapse <Collapse
disabled={!error_strategy} disabled={!error_strategy}

View File

@ -0,0 +1,41 @@
import {
useCallback,
useState,
} from 'react'
import type { WorkflowRetryConfig } from './types'
import {
useNodeDataUpdate,
} from '@/app/components/workflow/hooks'
import type { NodeTracing } from '@/types/workflow'
export const useRetryConfig = (
id: string,
) => {
const { handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate()
const handleRetryConfigChange = useCallback((value?: WorkflowRetryConfig) => {
handleNodeDataUpdateWithSyncDraft({
id,
data: {
retry_config: value,
},
})
}, [id, handleNodeDataUpdateWithSyncDraft])
return {
handleRetryConfigChange,
}
}
export const useRetryDetailShowInSingleRun = () => {
const [retryDetails, setRetryDetails] = useState<NodeTracing[] | undefined>()
const handleRetryDetailsChange = useCallback((details: NodeTracing[] | undefined) => {
setRetryDetails(details)
}, [])
return {
retryDetails,
handleRetryDetailsChange,
}
}

View File

@ -0,0 +1,88 @@
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiAlertFill,
RiCheckboxCircleFill,
RiLoader2Line,
} from '@remixicon/react'
import type { Node } from '@/app/components/workflow/types'
import { NodeRunningStatus } from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
type RetryOnNodeProps = Pick<Node, 'id' | 'data'>
const RetryOnNode = ({
data,
}: RetryOnNodeProps) => {
const { t } = useTranslation()
const { retry_config } = data
const showSelectedBorder = data.selected || data._isBundled || data._isEntering
const {
isRunning,
isSuccessful,
isException,
isFailed,
} = useMemo(() => {
return {
isRunning: data._runningStatus === NodeRunningStatus.Running && !showSelectedBorder,
isSuccessful: data._runningStatus === NodeRunningStatus.Succeeded && !showSelectedBorder,
isFailed: data._runningStatus === NodeRunningStatus.Failed && !showSelectedBorder,
isException: data._runningStatus === NodeRunningStatus.Exception && !showSelectedBorder,
}
}, [data._runningStatus, showSelectedBorder])
const showDefault = !isRunning && !isSuccessful && !isException && !isFailed
if (!retry_config)
return null
return (
<div className='px-3'>
<div className={cn(
'flex items-center justify-between px-[5px] py-1 bg-workflow-block-parma-bg border-[0.5px] border-transparent rounded-md system-xs-medium-uppercase text-text-tertiary',
isRunning && 'bg-state-accent-hover border-state-accent-active text-text-accent',
isSuccessful && 'bg-state-success-hover border-state-success-active text-text-success',
(isException || isFailed) && 'bg-state-warning-hover border-state-warning-active text-text-warning',
)}>
<div className='flex items-center'>
{
showDefault && (
t('workflow.nodes.common.retry.retryTimes', { times: retry_config.max_retries })
)
}
{
isRunning && (
<>
<RiLoader2Line className='animate-spin mr-1 w-3.5 h-3.5' />
{t('workflow.nodes.common.retry.retrying')}
</>
)
}
{
isSuccessful && (
<>
<RiCheckboxCircleFill className='mr-1 w-3.5 h-3.5' />
{t('workflow.nodes.common.retry.retrySuccessful')}
</>
)
}
{
(isFailed || isException) && (
<>
<RiAlertFill className='mr-1 w-3.5 h-3.5' />
{t('workflow.nodes.common.retry.retryFailed')}
</>
)
}
</div>
{
!showDefault && (
<div>
{data._retryIndex}/{data.retry_config?.max_retries}
</div>
)
}
</div>
</div>
)
}
export default RetryOnNode

View File

@ -0,0 +1,117 @@
import { useTranslation } from 'react-i18next'
import { useRetryConfig } from './hooks'
import s from './style.module.css'
import Switch from '@/app/components/base/switch'
import Slider from '@/app/components/base/slider'
import Input from '@/app/components/base/input'
import type {
Node,
} from '@/app/components/workflow/types'
import Split from '@/app/components/workflow/nodes/_base/components/split'
type RetryOnPanelProps = Pick<Node, 'id' | 'data'>
const RetryOnPanel = ({
id,
data,
}: RetryOnPanelProps) => {
const { t } = useTranslation()
const { handleRetryConfigChange } = useRetryConfig(id)
const { retry_config } = data
const handleRetryEnabledChange = (value: boolean) => {
handleRetryConfigChange({
retry_enabled: value,
max_retries: retry_config?.max_retries || 3,
retry_interval: retry_config?.retry_interval || 1000,
})
}
const handleMaxRetriesChange = (value: number) => {
if (value > 10)
value = 10
else if (value < 1)
value = 1
handleRetryConfigChange({
retry_enabled: true,
max_retries: value,
retry_interval: retry_config?.retry_interval || 1000,
})
}
const handleRetryIntervalChange = (value: number) => {
if (value > 5000)
value = 5000
else if (value < 100)
value = 100
handleRetryConfigChange({
retry_enabled: true,
max_retries: retry_config?.max_retries || 3,
retry_interval: value,
})
}
return (
<>
<div className='pt-2'>
<div className='flex items-center justify-between px-4 py-2 h-10'>
<div className='flex items-center'>
<div className='mr-0.5 system-sm-semibold-uppercase text-text-secondary'>{t('workflow.nodes.common.retry.retryOnFailure')}</div>
</div>
<Switch
defaultValue={retry_config?.retry_enabled}
onChange={v => handleRetryEnabledChange(v)}
/>
</div>
{
retry_config?.retry_enabled && (
<div className='px-4 pb-2'>
<div className='flex items-center mb-1 w-full'>
<div className='grow mr-2 system-xs-medium-uppercase'>{t('workflow.nodes.common.retry.maxRetries')}</div>
<Slider
className='mr-3 w-[108px]'
value={retry_config?.max_retries || 3}
onChange={handleMaxRetriesChange}
min={1}
max={10}
/>
<Input
type='number'
wrapperClassName='w-[80px]'
value={retry_config?.max_retries || 3}
onChange={e => handleMaxRetriesChange(e.target.value as any)}
min={1}
max={10}
unit={t('workflow.nodes.common.retry.times') || ''}
className={s.input}
/>
</div>
<div className='flex items-center'>
<div className='grow mr-2 system-xs-medium-uppercase'>{t('workflow.nodes.common.retry.retryInterval')}</div>
<Slider
className='mr-3 w-[108px]'
value={retry_config?.retry_interval || 1000}
onChange={handleRetryIntervalChange}
min={100}
max={5000}
/>
<Input
type='number'
wrapperClassName='w-[80px]'
value={retry_config?.retry_interval || 1000}
onChange={e => handleRetryIntervalChange(e.target.value as any)}
min={100}
max={5000}
unit={t('workflow.nodes.common.retry.ms') || ''}
className={s.input}
/>
</div>
</div>
)
}
</div>
<Split className='mx-4 mt-2' />
</>
)
}
export default RetryOnPanel

View File

@ -0,0 +1,5 @@
.input::-webkit-inner-spin-button,
.input::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}

View File

@ -0,0 +1,5 @@
export type WorkflowRetryConfig = {
max_retries: number
retry_interval: number
retry_enabled: boolean
}

View File

@ -25,7 +25,10 @@ import {
useNodesReadOnly, useNodesReadOnly,
useToolIcon, useToolIcon,
} from '../../hooks' } from '../../hooks'
import { hasErrorHandleNode } from '../../utils' import {
hasErrorHandleNode,
hasRetryNode,
} from '../../utils'
import { useNodeIterationInteractions } from '../iteration/use-interactions' import { useNodeIterationInteractions } from '../iteration/use-interactions'
import type { IterationNodeType } from '../iteration/types' import type { IterationNodeType } from '../iteration/types'
import { import {
@ -35,6 +38,7 @@ import {
import NodeResizer from './components/node-resizer' import NodeResizer from './components/node-resizer'
import NodeControl from './components/node-control' import NodeControl from './components/node-control'
import ErrorHandleOnNode from './components/error-handle/error-handle-on-node' import ErrorHandleOnNode from './components/error-handle/error-handle-on-node'
import RetryOnNode from './components/retry/retry-on-node'
import AddVariablePopupWithPosition from './components/add-variable-popup-with-position' import AddVariablePopupWithPosition from './components/add-variable-popup-with-position'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import BlockIcon from '@/app/components/workflow/block-icon' import BlockIcon from '@/app/components/workflow/block-icon'
@ -237,6 +241,14 @@ const BaseNode: FC<BaseNodeProps> = ({
</div> </div>
) )
} }
{
hasRetryNode(data.type) && (
<RetryOnNode
id={id}
data={data}
/>
)
}
{ {
hasErrorHandleNode(data.type) && ( hasErrorHandleNode(data.type) && (
<ErrorHandleOnNode <ErrorHandleOnNode

View File

@ -21,9 +21,11 @@ import {
TitleInput, TitleInput,
} from './components/title-description-input' } from './components/title-description-input'
import ErrorHandleOnPanel from './components/error-handle/error-handle-on-panel' import ErrorHandleOnPanel from './components/error-handle/error-handle-on-panel'
import RetryOnPanel from './components/retry/retry-on-panel'
import { useResizePanel } from './hooks/use-resize-panel' import { useResizePanel } from './hooks/use-resize-panel'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import BlockIcon from '@/app/components/workflow/block-icon' import BlockIcon from '@/app/components/workflow/block-icon'
import Split from '@/app/components/workflow/nodes/_base/components/split'
import { import {
WorkflowHistoryEvent, WorkflowHistoryEvent,
useAvailableBlocks, useAvailableBlocks,
@ -38,6 +40,7 @@ import {
import { import {
canRunBySingle, canRunBySingle,
hasErrorHandleNode, hasErrorHandleNode,
hasRetryNode,
} from '@/app/components/workflow/utils' } from '@/app/components/workflow/utils'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import type { Node } from '@/app/components/workflow/types' import type { Node } from '@/app/components/workflow/types'
@ -168,6 +171,15 @@ const BasePanel: FC<BasePanelProps> = ({
<div> <div>
{cloneElement(children, { id, data })} {cloneElement(children, { id, data })}
</div> </div>
<Split />
{
hasRetryNode(data.type) && (
<RetryOnPanel
id={id}
data={data}
/>
)
}
{ {
hasErrorHandleNode(data.type) && ( hasErrorHandleNode(data.type) && (
<ErrorHandleOnPanel <ErrorHandleOnPanel

View File

@ -2,7 +2,10 @@ import { BlockEnum } from '../../types'
import type { NodeDefault } from '../../types' import type { NodeDefault } from '../../types'
import { AuthorizationType, BodyType, Method } from './types' import { AuthorizationType, BodyType, Method } from './types'
import type { BodyPayload, HttpNodeType } from './types' import type { BodyPayload, HttpNodeType } from './types'
import { ALL_CHAT_AVAILABLE_BLOCKS, ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/constants' import {
ALL_CHAT_AVAILABLE_BLOCKS,
ALL_COMPLETION_AVAILABLE_BLOCKS,
} from '@/app/components/workflow/constants'
const nodeDefault: NodeDefault<HttpNodeType> = { const nodeDefault: NodeDefault<HttpNodeType> = {
defaultValue: { defaultValue: {
@ -24,6 +27,11 @@ const nodeDefault: NodeDefault<HttpNodeType> = {
max_read_timeout: 0, max_read_timeout: 0,
max_write_timeout: 0, max_write_timeout: 0,
}, },
retry_config: {
retry_enabled: true,
max_retries: 3,
retry_interval: 100,
},
}, },
getAvailablePrevNodes(isChatMode: boolean) { getAvailablePrevNodes(isChatMode: boolean) {
const nodes = isChatMode const nodes = isChatMode

View File

@ -1,5 +1,5 @@
import type { FC } from 'react' import type { FC } from 'react'
import React from 'react' import { memo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import useConfig from './use-config' import useConfig from './use-config'
import ApiInput from './components/api-input' import ApiInput from './components/api-input'
@ -18,6 +18,7 @@ import { FileArrow01 } from '@/app/components/base/icons/src/vender/line/files'
import type { NodePanelProps } from '@/app/components/workflow/types' import type { NodePanelProps } from '@/app/components/workflow/types'
import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form' import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form'
import ResultPanel from '@/app/components/workflow/run/result-panel' import ResultPanel from '@/app/components/workflow/run/result-panel'
import { useRetryDetailShowInSingleRun } from '@/app/components/workflow/nodes/_base/components/retry/hooks'
const i18nPrefix = 'workflow.nodes.http' const i18nPrefix = 'workflow.nodes.http'
@ -60,6 +61,10 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
hideCurlPanel, hideCurlPanel,
handleCurlImport, handleCurlImport,
} = useConfig(id, data) } = useConfig(id, data)
const {
retryDetails,
handleRetryDetailsChange,
} = useRetryDetailShowInSingleRun()
// To prevent prompt editor in body not update data. // To prevent prompt editor in body not update data.
if (!isDataReady) if (!isDataReady)
return null return null
@ -181,6 +186,7 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
{isShowSingleRun && ( {isShowSingleRun && (
<BeforeRunForm <BeforeRunForm
nodeName={inputs.title} nodeName={inputs.title}
nodeType={inputs.type}
onHide={hideSingleRun} onHide={hideSingleRun}
forms={[ forms={[
{ {
@ -192,7 +198,9 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
runningStatus={runningStatus} runningStatus={runningStatus}
onRun={handleRun} onRun={handleRun}
onStop={handleStop} onStop={handleStop}
result={<ResultPanel {...runResult} showSteps={false} />} retryDetails={retryDetails}
onRetryDetailBack={handleRetryDetailsChange}
result={<ResultPanel {...runResult} showSteps={false} onShowRetryDetail={handleRetryDetailsChange} />}
/> />
)} )}
{(isShowCurlPanel && !readOnly) && ( {(isShowCurlPanel && !readOnly) && (
@ -207,4 +215,4 @@ const Panel: FC<NodePanelProps<HttpNodeType>> = ({
) )
} }
export default React.memo(Panel) export default memo(Panel)

View File

@ -19,6 +19,7 @@ import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/c
import ResultPanel from '@/app/components/workflow/run/result-panel' import ResultPanel from '@/app/components/workflow/run/result-panel'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import Editor from '@/app/components/workflow/nodes/_base/components/prompt/editor' import Editor from '@/app/components/workflow/nodes/_base/components/prompt/editor'
import { useRetryDetailShowInSingleRun } from '@/app/components/workflow/nodes/_base/components/retry/hooks'
const i18nPrefix = 'workflow.nodes.llm' const i18nPrefix = 'workflow.nodes.llm'
@ -69,6 +70,10 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
runResult, runResult,
filterJinjia2InputVar, filterJinjia2InputVar,
} = useConfig(id, data) } = useConfig(id, data)
const {
retryDetails,
handleRetryDetailsChange,
} = useRetryDetailShowInSingleRun()
const model = inputs.model const model = inputs.model
@ -282,12 +287,15 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
{isShowSingleRun && ( {isShowSingleRun && (
<BeforeRunForm <BeforeRunForm
nodeName={inputs.title} nodeName={inputs.title}
nodeType={inputs.type}
onHide={hideSingleRun} onHide={hideSingleRun}
forms={singleRunForms} forms={singleRunForms}
runningStatus={runningStatus} runningStatus={runningStatus}
onRun={handleRun} onRun={handleRun}
onStop={handleStop} onStop={handleStop}
result={<ResultPanel {...runResult} showSteps={false} />} retryDetails={retryDetails}
onRetryDetailBack={handleRetryDetailsChange}
result={<ResultPanel {...runResult} showSteps={false} onShowRetryDetail={handleRetryDetailsChange} />}
/> />
)} )}
</div> </div>

View File

@ -14,6 +14,8 @@ import Loading from '@/app/components/base/loading'
import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form' import BeforeRunForm from '@/app/components/workflow/nodes/_base/components/before-run-form'
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars' import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
import ResultPanel from '@/app/components/workflow/run/result-panel' import ResultPanel from '@/app/components/workflow/run/result-panel'
import { useRetryDetailShowInSingleRun } from '@/app/components/workflow/nodes/_base/components/retry/hooks'
import { useToolIcon } from '@/app/components/workflow/hooks'
const i18nPrefix = 'workflow.nodes.tool' const i18nPrefix = 'workflow.nodes.tool'
@ -48,6 +50,11 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
handleStop, handleStop,
runResult, runResult,
} = useConfig(id, data) } = useConfig(id, data)
const toolIcon = useToolIcon(data)
const {
retryDetails,
handleRetryDetailsChange,
} = useRetryDetailShowInSingleRun()
if (isLoading) { if (isLoading) {
return <div className='flex h-[200px] items-center justify-center'> return <div className='flex h-[200px] items-center justify-center'>
@ -143,12 +150,16 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
{isShowSingleRun && ( {isShowSingleRun && (
<BeforeRunForm <BeforeRunForm
nodeName={inputs.title} nodeName={inputs.title}
nodeType={inputs.type}
toolIcon={toolIcon}
onHide={hideSingleRun} onHide={hideSingleRun}
forms={singleRunForms} forms={singleRunForms}
runningStatus={runningStatus} runningStatus={runningStatus}
onRun={handleRun} onRun={handleRun}
onStop={handleStop} onStop={handleStop}
result={<ResultPanel {...runResult} showSteps={false} />} retryDetails={retryDetails}
onRetryDetailBack={handleRetryDetailsChange}
result={<ResultPanel {...runResult} showSteps={false} onShowRetryDetail={handleRetryDetailsChange} />}
/> />
)} )}
</div> </div>

View File

@ -27,6 +27,7 @@ import {
getProcessedFilesFromResponse, getProcessedFilesFromResponse,
} from '@/app/components/base/file-uploader/utils' } from '@/app/components/base/file-uploader/utils'
import type { FileEntity } from '@/app/components/base/file-uploader/types' import type { FileEntity } from '@/app/components/base/file-uploader/types'
import type { NodeTracing } from '@/types/workflow'
type GetAbortController = (abortController: AbortController) => void type GetAbortController = (abortController: AbortController) => void
type SendCallback = { type SendCallback = {
@ -381,6 +382,28 @@ export const useChat = (
} }
})) }))
}, },
onNodeRetry: ({ data }) => {
if (data.iteration_id)
return
const currentIndex = responseItem.workflowProcess!.tracing!.findIndex((item) => {
if (!item.execution_metadata?.parallel_id)
return item.node_id === data.node_id
return item.node_id === data.node_id && (item.execution_metadata?.parallel_id === data.execution_metadata?.parallel_id || item.parallel_id === data.execution_metadata?.parallel_id)
})
if (responseItem.workflowProcess!.tracing[currentIndex].retryDetail)
responseItem.workflowProcess!.tracing[currentIndex].retryDetail?.push(data as NodeTracing)
else
responseItem.workflowProcess!.tracing[currentIndex].retryDetail = [data as NodeTracing]
handleUpdateChatList(produce(chatListRef.current, (draft) => {
const currentIndex = draft.findIndex(item => item.id === responseItem.id)
draft[currentIndex] = {
...draft[currentIndex],
...responseItem,
}
}))
},
onNodeFinished: ({ data }) => { onNodeFinished: ({ data }) => {
if (data.iteration_id) if (data.iteration_id)
return return
@ -394,6 +417,9 @@ export const useChat = (
...(responseItem.workflowProcess!.tracing[currentIndex]?.extras ...(responseItem.workflowProcess!.tracing[currentIndex]?.extras
? { extras: responseItem.workflowProcess!.tracing[currentIndex].extras } ? { extras: responseItem.workflowProcess!.tracing[currentIndex].extras }
: {}), : {}),
...(responseItem.workflowProcess!.tracing[currentIndex]?.retryDetail
? { retryDetail: responseItem.workflowProcess!.tracing[currentIndex].retryDetail }
: {}),
...data, ...data,
} as any } as any
handleUpdateChatList(produce(chatListRef.current, (draft) => { handleUpdateChatList(produce(chatListRef.current, (draft) => {

View File

@ -25,6 +25,7 @@ import {
import { SimpleBtn } from '../../app/text-generate/item' import { SimpleBtn } from '../../app/text-generate/item'
import Toast from '../../base/toast' import Toast from '../../base/toast'
import IterationResultPanel from '../run/iteration-result-panel' import IterationResultPanel from '../run/iteration-result-panel'
import RetryResultPanel from '../run/retry-result-panel'
import InputsPanel from './inputs-panel' import InputsPanel from './inputs-panel'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
@ -53,11 +54,16 @@ const WorkflowPreview = () => {
}, [workflowRunningData]) }, [workflowRunningData])
const [iterationRunResult, setIterationRunResult] = useState<NodeTracing[][]>([]) const [iterationRunResult, setIterationRunResult] = useState<NodeTracing[][]>([])
const [retryRunResult, setRetryRunResult] = useState<NodeTracing[]>([])
const [iterDurationMap, setIterDurationMap] = useState<IterationDurationMap>({}) const [iterDurationMap, setIterDurationMap] = useState<IterationDurationMap>({})
const [isShowIterationDetail, { const [isShowIterationDetail, {
setTrue: doShowIterationDetail, setTrue: doShowIterationDetail,
setFalse: doHideIterationDetail, setFalse: doHideIterationDetail,
}] = useBoolean(false) }] = useBoolean(false)
const [isShowRetryDetail, {
setTrue: doShowRetryDetail,
setFalse: doHideRetryDetail,
}] = useBoolean(false)
const handleShowIterationDetail = useCallback((detail: NodeTracing[][], iterationDurationMap: IterationDurationMap) => { const handleShowIterationDetail = useCallback((detail: NodeTracing[][], iterationDurationMap: IterationDurationMap) => {
setIterDurationMap(iterationDurationMap) setIterDurationMap(iterationDurationMap)
@ -65,6 +71,11 @@ const WorkflowPreview = () => {
doShowIterationDetail() doShowIterationDetail()
}, [doShowIterationDetail]) }, [doShowIterationDetail])
const handleRetryDetail = useCallback((detail: NodeTracing[]) => {
setRetryRunResult(detail)
doShowRetryDetail()
}, [doShowRetryDetail])
if (isShowIterationDetail) { if (isShowIterationDetail) {
return ( return (
<div className={` <div className={`
@ -201,11 +212,12 @@ const WorkflowPreview = () => {
<Loading /> <Loading />
</div> </div>
)} )}
{currentTab === 'TRACING' && ( {currentTab === 'TRACING' && !isShowRetryDetail && (
<TracingPanel <TracingPanel
className='bg-background-section-burn' className='bg-background-section-burn'
list={workflowRunningData?.tracing || []} list={workflowRunningData?.tracing || []}
onShowIterationDetail={handleShowIterationDetail} onShowIterationDetail={handleShowIterationDetail}
onShowRetryDetail={handleRetryDetail}
/> />
)} )}
{currentTab === 'TRACING' && !workflowRunningData?.tracing?.length && ( {currentTab === 'TRACING' && !workflowRunningData?.tracing?.length && (
@ -213,7 +225,14 @@ const WorkflowPreview = () => {
<Loading /> <Loading />
</div> </div>
)} )}
{
currentTab === 'TRACING' && isShowRetryDetail && (
<RetryResultPanel
list={retryRunResult}
onBack={doHideRetryDetail}
/>
)
}
</div> </div>
</> </>
)} )}

View File

@ -9,6 +9,7 @@ import OutputPanel from './output-panel'
import ResultPanel from './result-panel' import ResultPanel from './result-panel'
import TracingPanel from './tracing-panel' import TracingPanel from './tracing-panel'
import IterationResultPanel from './iteration-result-panel' import IterationResultPanel from './iteration-result-panel'
import RetryResultPanel from './retry-result-panel'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { ToastContext } from '@/app/components/base/toast' import { ToastContext } from '@/app/components/base/toast'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
@ -107,6 +108,18 @@ const RunPanel: FC<RunProps> = ({ hideResult, activeTab = 'RESULT', runID, getRe
const processNonIterationNode = (item: NodeTracing) => { const processNonIterationNode = (item: NodeTracing) => {
const { execution_metadata } = item const { execution_metadata } = item
if (!execution_metadata?.iteration_id) { if (!execution_metadata?.iteration_id) {
if (item.status === 'retry') {
const retryNode = result.find(node => node.node_id === item.node_id)
if (retryNode) {
if (retryNode?.retryDetail)
retryNode.retryDetail.push(item)
else
retryNode.retryDetail = [item]
}
return
}
result.push(item) result.push(item)
return return
} }
@ -181,10 +194,15 @@ const RunPanel: FC<RunProps> = ({ hideResult, activeTab = 'RESULT', runID, getRe
const [iterationRunResult, setIterationRunResult] = useState<NodeTracing[][]>([]) const [iterationRunResult, setIterationRunResult] = useState<NodeTracing[][]>([])
const [iterDurationMap, setIterDurationMap] = useState<IterationDurationMap>({}) const [iterDurationMap, setIterDurationMap] = useState<IterationDurationMap>({})
const [retryRunResult, setRetryRunResult] = useState<NodeTracing[]>([])
const [isShowIterationDetail, { const [isShowIterationDetail, {
setTrue: doShowIterationDetail, setTrue: doShowIterationDetail,
setFalse: doHideIterationDetail, setFalse: doHideIterationDetail,
}] = useBoolean(false) }] = useBoolean(false)
const [isShowRetryDetail, {
setTrue: doShowRetryDetail,
setFalse: doHideRetryDetail,
}] = useBoolean(false)
const handleShowIterationDetail = useCallback((detail: NodeTracing[][], iterDurationMap: IterationDurationMap) => { const handleShowIterationDetail = useCallback((detail: NodeTracing[][], iterDurationMap: IterationDurationMap) => {
setIterationRunResult(detail) setIterationRunResult(detail)
@ -192,6 +210,11 @@ const RunPanel: FC<RunProps> = ({ hideResult, activeTab = 'RESULT', runID, getRe
setIterDurationMap(iterDurationMap) setIterDurationMap(iterDurationMap)
}, [doShowIterationDetail, setIterationRunResult, setIterDurationMap]) }, [doShowIterationDetail, setIterationRunResult, setIterDurationMap])
const handleShowRetryDetail = useCallback((detail: NodeTracing[]) => {
setRetryRunResult(detail)
doShowRetryDetail()
}, [doShowRetryDetail, setRetryRunResult])
if (isShowIterationDetail) { if (isShowIterationDetail) {
return ( return (
<div className='grow relative flex flex-col'> <div className='grow relative flex flex-col'>
@ -261,13 +284,22 @@ const RunPanel: FC<RunProps> = ({ hideResult, activeTab = 'RESULT', runID, getRe
exceptionCounts={runDetail.exceptions_count} exceptionCounts={runDetail.exceptions_count}
/> />
)} )}
{!loading && currentTab === 'TRACING' && ( {!loading && currentTab === 'TRACING' && !isShowRetryDetail && (
<TracingPanel <TracingPanel
className='bg-background-section-burn' className='bg-background-section-burn'
list={list} list={list}
onShowIterationDetail={handleShowIterationDetail} onShowIterationDetail={handleShowIterationDetail}
onShowRetryDetail={handleShowRetryDetail}
/> />
)} )}
{
!loading && currentTab === 'TRACING' && isShowRetryDetail && (
<RetryResultPanel
list={retryRunResult}
onBack={doHideRetryDetail}
/>
)
}
</div> </div>
</div> </div>
) )

View File

@ -8,6 +8,7 @@ import {
RiCheckboxCircleFill, RiCheckboxCircleFill,
RiErrorWarningLine, RiErrorWarningLine,
RiLoader2Line, RiLoader2Line,
RiRestartFill,
} from '@remixicon/react' } from '@remixicon/react'
import BlockIcon from '../block-icon' import BlockIcon from '../block-icon'
import { BlockEnum } from '../types' import { BlockEnum } from '../types'
@ -20,6 +21,7 @@ import Button from '@/app/components/base/button'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import type { IterationDurationMap, NodeTracing } from '@/types/workflow' import type { IterationDurationMap, NodeTracing } from '@/types/workflow'
import ErrorHandleTip from '@/app/components/workflow/nodes/_base/components/error-handle/error-handle-tip' import ErrorHandleTip from '@/app/components/workflow/nodes/_base/components/error-handle/error-handle-tip'
import { hasRetryNode } from '@/app/components/workflow/utils'
type Props = { type Props = {
className?: string className?: string
@ -28,8 +30,10 @@ type Props = {
hideInfo?: boolean hideInfo?: boolean
hideProcessDetail?: boolean hideProcessDetail?: boolean
onShowIterationDetail?: (detail: NodeTracing[][], iterDurationMap: IterationDurationMap) => void onShowIterationDetail?: (detail: NodeTracing[][], iterDurationMap: IterationDurationMap) => void
onShowRetryDetail?: (detail: NodeTracing[]) => void
notShowIterationNav?: boolean notShowIterationNav?: boolean
justShowIterationNavArrow?: boolean justShowIterationNavArrow?: boolean
justShowRetryNavArrow?: boolean
} }
const NodePanel: FC<Props> = ({ const NodePanel: FC<Props> = ({
@ -39,6 +43,7 @@ const NodePanel: FC<Props> = ({
hideInfo = false, hideInfo = false,
hideProcessDetail, hideProcessDetail,
onShowIterationDetail, onShowIterationDetail,
onShowRetryDetail,
notShowIterationNav, notShowIterationNav,
justShowIterationNavArrow, justShowIterationNavArrow,
}) => { }) => {
@ -88,11 +93,17 @@ const NodePanel: FC<Props> = ({
}, [nodeInfo.expand, setCollapseState]) }, [nodeInfo.expand, setCollapseState])
const isIterationNode = nodeInfo.node_type === BlockEnum.Iteration const isIterationNode = nodeInfo.node_type === BlockEnum.Iteration
const isRetryNode = hasRetryNode(nodeInfo.node_type) && nodeInfo.retryDetail
const handleOnShowIterationDetail = (e: React.MouseEvent<HTMLButtonElement>) => { const handleOnShowIterationDetail = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation() e.stopPropagation()
e.nativeEvent.stopImmediatePropagation() e.nativeEvent.stopImmediatePropagation()
onShowIterationDetail?.(nodeInfo.details || [], nodeInfo?.iterDurationMap || nodeInfo.execution_metadata?.iteration_duration_map || {}) onShowIterationDetail?.(nodeInfo.details || [], nodeInfo?.iterDurationMap || nodeInfo.execution_metadata?.iteration_duration_map || {})
} }
const handleOnShowRetryDetail = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
e.nativeEvent.stopImmediatePropagation()
onShowRetryDetail?.(nodeInfo.retryDetail || [])
}
return ( return (
<div className={cn('px-2 py-1', className)}> <div className={cn('px-2 py-1', className)}>
<div className='group transition-all bg-background-default border border-components-panel-border rounded-[10px] shadow-xs hover:shadow-md'> <div className='group transition-all bg-background-default border border-components-panel-border rounded-[10px] shadow-xs hover:shadow-md'>
@ -169,6 +180,19 @@ const NodePanel: FC<Props> = ({
<Split className='mt-2' /> <Split className='mt-2' />
</div> </div>
)} )}
{isRetryNode && (
<Button
className='flex items-center justify-between mb-1 w-full'
variant='tertiary'
onClick={handleOnShowRetryDetail}
>
<div className='flex items-center'>
<RiRestartFill className='mr-0.5 w-4 h-4 text-components-button-tertiary-text flex-shrink-0' />
{t('workflow.nodes.common.retry.retries', { num: nodeInfo.retryDetail?.length })}
</div>
<RiArrowRightSLine className='w-4 h-4 text-components-button-tertiary-text flex-shrink-0' />
</Button>
)}
<div className={cn('mb-1', hideInfo && '!px-2 !py-0.5')}> <div className={cn('mb-1', hideInfo && '!px-2 !py-0.5')}>
{(nodeInfo.status === 'stopped') && ( {(nodeInfo.status === 'stopped') && (
<StatusContainer status='stopped'> <StatusContainer status='stopped'>

View File

@ -1,11 +1,17 @@
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import {
RiArrowRightSLine,
RiRestartFill,
} from '@remixicon/react'
import StatusPanel from './status' import StatusPanel from './status'
import MetaData from './meta' import MetaData from './meta'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor' import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import ErrorHandleTip from '@/app/components/workflow/nodes/_base/components/error-handle/error-handle-tip' import ErrorHandleTip from '@/app/components/workflow/nodes/_base/components/error-handle/error-handle-tip'
import type { NodeTracing } from '@/types/workflow'
import Button from '@/app/components/base/button'
type ResultPanelProps = { type ResultPanelProps = {
inputs?: string inputs?: string
@ -22,6 +28,8 @@ type ResultPanelProps = {
showSteps?: boolean showSteps?: boolean
exceptionCounts?: number exceptionCounts?: number
execution_metadata?: any execution_metadata?: any
retry_events?: NodeTracing[]
onShowRetryDetail?: (retries: NodeTracing[]) => void
} }
const ResultPanel: FC<ResultPanelProps> = ({ const ResultPanel: FC<ResultPanelProps> = ({
@ -38,8 +46,11 @@ const ResultPanel: FC<ResultPanelProps> = ({
showSteps, showSteps,
exceptionCounts, exceptionCounts,
execution_metadata, execution_metadata,
retry_events,
onShowRetryDetail,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<div className='bg-components-panel-bg py-2'> <div className='bg-components-panel-bg py-2'>
<div className='px-4 py-2'> <div className='px-4 py-2'>
@ -51,6 +62,23 @@ const ResultPanel: FC<ResultPanelProps> = ({
exceptionCounts={exceptionCounts} exceptionCounts={exceptionCounts}
/> />
</div> </div>
{
retry_events?.length && onShowRetryDetail && (
<div className='px-4'>
<Button
className='flex items-center justify-between w-full'
variant='tertiary'
onClick={() => onShowRetryDetail(retry_events)}
>
<div className='flex items-center'>
<RiRestartFill className='mr-0.5 w-4 h-4 text-components-button-tertiary-text flex-shrink-0' />
{t('workflow.nodes.common.retry.retries', { num: retry_events?.length })}
</div>
<RiArrowRightSLine className='w-4 h-4 text-components-button-tertiary-text flex-shrink-0' />
</Button>
</div>
)
}
<div className='px-4 py-2 flex flex-col gap-2'> <div className='px-4 py-2 flex flex-col gap-2'>
<CodeEditor <CodeEditor
readOnly readOnly

View File

@ -0,0 +1,46 @@
'use client'
import type { FC } from 'react'
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiArrowLeftLine,
} from '@remixicon/react'
import TracingPanel from './tracing-panel'
import type { NodeTracing } from '@/types/workflow'
type Props = {
list: NodeTracing[]
onBack: () => void
}
const RetryResultPanel: FC<Props> = ({
list,
onBack,
}) => {
const { t } = useTranslation()
return (
<div>
<div
className='flex items-center px-4 h-8 text-text-accent-secondary bg-components-panel-bg system-sm-medium cursor-pointer'
onClick={(e) => {
e.stopPropagation()
e.nativeEvent.stopImmediatePropagation()
onBack()
}}
>
<RiArrowLeftLine className='mr-1 w-4 h-4' />
{t('workflow.singleRun.back')}
</div>
<TracingPanel
list={list.map((item, index) => ({
...item,
title: `${t('workflow.nodes.common.retry.retry')} ${index + 1}`,
}))}
className='bg-background-section-burn'
/>
</div >
)
}
export default memo(RetryResultPanel)

View File

@ -21,6 +21,7 @@ import type { IterationDurationMap, NodeTracing } from '@/types/workflow'
type TracingPanelProps = { type TracingPanelProps = {
list: NodeTracing[] list: NodeTracing[]
onShowIterationDetail?: (detail: NodeTracing[][], iterDurationMap: IterationDurationMap) => void onShowIterationDetail?: (detail: NodeTracing[][], iterDurationMap: IterationDurationMap) => void
onShowRetryDetail?: (detail: NodeTracing[]) => void
className?: string className?: string
hideNodeInfo?: boolean hideNodeInfo?: boolean
hideNodeProcessDetail?: boolean hideNodeProcessDetail?: boolean
@ -160,6 +161,7 @@ function buildLogTree(nodes: NodeTracing[], t: (key: string) => string): Tracing
const TracingPanel: FC<TracingPanelProps> = ({ const TracingPanel: FC<TracingPanelProps> = ({
list, list,
onShowIterationDetail, onShowIterationDetail,
onShowRetryDetail,
className, className,
hideNodeInfo = false, hideNodeInfo = false,
hideNodeProcessDetail = false, hideNodeProcessDetail = false,
@ -251,7 +253,9 @@ const TracingPanel: FC<TracingPanelProps> = ({
<NodePanel <NodePanel
nodeInfo={node.data!} nodeInfo={node.data!}
onShowIterationDetail={onShowIterationDetail} onShowIterationDetail={onShowIterationDetail}
onShowRetryDetail={onShowRetryDetail}
justShowIterationNavArrow={true} justShowIterationNavArrow={true}
justShowRetryNavArrow={true}
hideInfo={hideNodeInfo} hideInfo={hideNodeInfo}
hideProcessDetail={hideNodeProcessDetail} hideProcessDetail={hideNodeProcessDetail}
/> />

View File

@ -13,6 +13,7 @@ import type {
DefaultValueForm, DefaultValueForm,
ErrorHandleTypeEnum, ErrorHandleTypeEnum,
} from '@/app/components/workflow/nodes/_base/components/error-handle/types' } from '@/app/components/workflow/nodes/_base/components/error-handle/types'
import type { WorkflowRetryConfig } from '@/app/components/workflow/nodes/_base/components/retry/types'
export enum BlockEnum { export enum BlockEnum {
Start = 'start', Start = 'start',
@ -68,6 +69,7 @@ export type CommonNodeType<T = {}> = {
_iterationIndex?: number _iterationIndex?: number
_inParallelHovering?: boolean _inParallelHovering?: boolean
_waitingRun?: boolean _waitingRun?: boolean
_retryIndex?: number
isInIteration?: boolean isInIteration?: boolean
iteration_id?: string iteration_id?: string
selected?: boolean selected?: boolean
@ -77,6 +79,7 @@ export type CommonNodeType<T = {}> = {
width?: number width?: number
height?: number height?: number
error_strategy?: ErrorHandleTypeEnum error_strategy?: ErrorHandleTypeEnum
retry_config?: WorkflowRetryConfig
default_value?: DefaultValueForm[] default_value?: DefaultValueForm[]
} & T & Partial<Pick<ToolDefaultValue, 'provider_id' | 'provider_type' | 'provider_name' | 'tool_name'>> } & T & Partial<Pick<ToolDefaultValue, 'provider_id' | 'provider_type' | 'provider_name' | 'tool_name'>>
@ -293,6 +296,7 @@ export enum NodeRunningStatus {
Succeeded = 'succeeded', Succeeded = 'succeeded',
Failed = 'failed', Failed = 'failed',
Exception = 'exception', Exception = 'exception',
Retry = 'retry',
} }
export type OnNodeAdd = ( export type OnNodeAdd = (

View File

@ -26,6 +26,8 @@ import {
} from './types' } from './types'
import { import {
CUSTOM_NODE, CUSTOM_NODE,
DEFAULT_RETRY_INTERVAL,
DEFAULT_RETRY_MAX,
ITERATION_CHILDREN_Z_INDEX, ITERATION_CHILDREN_Z_INDEX,
ITERATION_NODE_Z_INDEX, ITERATION_NODE_Z_INDEX,
NODE_WIDTH_X_OFFSET, NODE_WIDTH_X_OFFSET,
@ -279,6 +281,14 @@ export const initialNodes = (originNodes: Node[], originEdges: Edge[]) => {
iterationNodeData.error_handle_mode = iterationNodeData.error_handle_mode || ErrorHandleMode.Terminated iterationNodeData.error_handle_mode = iterationNodeData.error_handle_mode || ErrorHandleMode.Terminated
} }
if (node.data.type === BlockEnum.HttpRequest && !node.data.retry_config) {
node.data.retry_config = {
retry_enabled: true,
max_retries: DEFAULT_RETRY_MAX,
retry_interval: DEFAULT_RETRY_INTERVAL,
}
}
return node return node
}) })
} }
@ -797,3 +807,7 @@ export const isExceptionVariable = (variable: string, nodeType?: BlockEnum) => {
return false return false
} }
export const hasRetryNode = (nodeType?: BlockEnum) => {
return nodeType === BlockEnum.LLM || nodeType === BlockEnum.Tool || nodeType === BlockEnum.HttpRequest || nodeType === BlockEnum.Code
}

View File

@ -329,6 +329,20 @@ const translation = {
tip: 'There are {{num}} nodes in the process running abnormally, please go to tracing to check the logs.', tip: 'There are {{num}} nodes in the process running abnormally, please go to tracing to check the logs.',
}, },
}, },
retry: {
retry: 'Retry',
retryOnFailure: 'retry on failure',
maxRetries: 'max retries',
retryInterval: 'retry interval',
retryTimes: 'Retry {{times}} times on failure',
retrying: 'Retrying...',
retrySuccessful: 'Retry successful',
retryFailed: 'Retry failed',
retryFailedTimes: '{{times}} retries failed',
times: 'times',
ms: 'ms',
retries: '{{num}} Retries',
},
}, },
start: { start: {
required: 'required', required: 'required',

View File

@ -329,6 +329,20 @@ const translation = {
tip: '流程中有 {{num}} 个节点运行异常,请前往追踪查看日志。', tip: '流程中有 {{num}} 个节点运行异常,请前往追踪查看日志。',
}, },
}, },
retry: {
retry: '重试',
retryOnFailure: '失败时重试',
maxRetries: '最大重试次数',
retryInterval: '重试间隔',
retryTimes: '失败时重试 {{times}} 次',
retrying: '重试中...',
retrySuccessful: '重试成功',
retryFailed: '重试失败',
retryFailedTimes: '{{times}} 次重试失败',
times: '次',
ms: '毫秒',
retries: '{{num}} 重试次数',
},
}, },
start: { start: {
required: '必填', required: '必填',

View File

@ -62,6 +62,7 @@ export type IOnNodeStarted = (nodeStarted: NodeStartedResponse) => void
export type IOnNodeFinished = (nodeFinished: NodeFinishedResponse) => void export type IOnNodeFinished = (nodeFinished: NodeFinishedResponse) => void
export type IOnIterationStarted = (workflowStarted: IterationStartedResponse) => void export type IOnIterationStarted = (workflowStarted: IterationStartedResponse) => void
export type IOnIterationNext = (workflowStarted: IterationNextResponse) => void export type IOnIterationNext = (workflowStarted: IterationNextResponse) => void
export type IOnNodeRetry = (nodeFinished: NodeFinishedResponse) => void
export type IOnIterationFinished = (workflowFinished: IterationFinishedResponse) => void export type IOnIterationFinished = (workflowFinished: IterationFinishedResponse) => void
export type IOnParallelBranchStarted = (parallelBranchStarted: ParallelBranchStartedResponse) => void export type IOnParallelBranchStarted = (parallelBranchStarted: ParallelBranchStartedResponse) => void
export type IOnParallelBranchFinished = (parallelBranchFinished: ParallelBranchFinishedResponse) => void export type IOnParallelBranchFinished = (parallelBranchFinished: ParallelBranchFinishedResponse) => void
@ -92,6 +93,7 @@ export type IOtherOptions = {
onIterationStart?: IOnIterationStarted onIterationStart?: IOnIterationStarted
onIterationNext?: IOnIterationNext onIterationNext?: IOnIterationNext
onIterationFinish?: IOnIterationFinished onIterationFinish?: IOnIterationFinished
onNodeRetry?: IOnNodeRetry
onParallelBranchStarted?: IOnParallelBranchStarted onParallelBranchStarted?: IOnParallelBranchStarted
onParallelBranchFinished?: IOnParallelBranchFinished onParallelBranchFinished?: IOnParallelBranchFinished
onTextChunk?: IOnTextChunk onTextChunk?: IOnTextChunk
@ -165,6 +167,7 @@ const handleStream = (
onIterationStart?: IOnIterationStarted, onIterationStart?: IOnIterationStarted,
onIterationNext?: IOnIterationNext, onIterationNext?: IOnIterationNext,
onIterationFinish?: IOnIterationFinished, onIterationFinish?: IOnIterationFinished,
onNodeRetry?: IOnNodeRetry,
onParallelBranchStarted?: IOnParallelBranchStarted, onParallelBranchStarted?: IOnParallelBranchStarted,
onParallelBranchFinished?: IOnParallelBranchFinished, onParallelBranchFinished?: IOnParallelBranchFinished,
onTextChunk?: IOnTextChunk, onTextChunk?: IOnTextChunk,
@ -256,6 +259,9 @@ const handleStream = (
else if (bufferObj.event === 'iteration_completed') { else if (bufferObj.event === 'iteration_completed') {
onIterationFinish?.(bufferObj as IterationFinishedResponse) onIterationFinish?.(bufferObj as IterationFinishedResponse)
} }
else if (bufferObj.event === 'node_retry') {
onNodeRetry?.(bufferObj as NodeFinishedResponse)
}
else if (bufferObj.event === 'parallel_branch_started') { else if (bufferObj.event === 'parallel_branch_started') {
onParallelBranchStarted?.(bufferObj as ParallelBranchStartedResponse) onParallelBranchStarted?.(bufferObj as ParallelBranchStartedResponse)
} }
@ -462,6 +468,7 @@ export const ssePost = (
onIterationStart, onIterationStart,
onIterationNext, onIterationNext,
onIterationFinish, onIterationFinish,
onNodeRetry,
onParallelBranchStarted, onParallelBranchStarted,
onParallelBranchFinished, onParallelBranchFinished,
onTextChunk, onTextChunk,
@ -533,7 +540,7 @@ export const ssePost = (
return return
} }
onData?.(str, isFirstMessage, moreInfo) onData?.(str, isFirstMessage, moreInfo)
}, onCompleted, onThought, onMessageEnd, onMessageReplace, onFile, onWorkflowStarted, onWorkflowFinished, onNodeStarted, onNodeFinished, onIterationStart, onIterationNext, onIterationFinish, onParallelBranchStarted, onParallelBranchFinished, onTextChunk, onTTSChunk, onTTSEnd, onTextReplace) }, onCompleted, onThought, onMessageEnd, onMessageReplace, onFile, onWorkflowStarted, onWorkflowFinished, onNodeStarted, onNodeFinished, onIterationStart, onIterationNext, onIterationFinish, onNodeRetry, onParallelBranchStarted, onParallelBranchFinished, onTextChunk, onTTSChunk, onTTSEnd, onTextReplace)
}).catch((e) => { }).catch((e) => {
if (e.toString() !== 'AbortError: The user aborted a request.' && !e.toString().errorMessage.includes('TypeError: Cannot assign to read only property')) if (e.toString() !== 'AbortError: The user aborted a request.' && !e.toString().errorMessage.includes('TypeError: Cannot assign to read only property'))
Toast.notify({ type: 'error', message: e }) Toast.notify({ type: 'error', message: e })

View File

@ -52,10 +52,12 @@ export type NodeTracing = {
extras?: any extras?: any
expand?: boolean // for UI expand?: boolean // for UI
details?: NodeTracing[][] // iteration detail details?: NodeTracing[][] // iteration detail
retryDetail?: NodeTracing[] // retry detail
parallel_id?: string parallel_id?: string
parallel_start_node_id?: string parallel_start_node_id?: string
parent_parallel_id?: string parent_parallel_id?: string
parent_parallel_start_node_id?: string parent_parallel_start_node_id?: string
retry_index?: number
} }
export type FetchWorkflowDraftResponse = { export type FetchWorkflowDraftResponse = {
@ -178,6 +180,7 @@ export type NodeFinishedResponse = {
} }
created_at: number created_at: number
files?: FileResponse[] files?: FileResponse[]
retry_index?: number
} }
} }