feat: implement human input form timeout handling and enhance expiration time display

This commit is contained in:
twwu 2026-01-27 15:57:13 +08:00
parent 94671d42c1
commit 7cd4a7c1de
20 changed files with 156 additions and 43 deletions

View File

@ -215,7 +215,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
else
switchTab('DETAIL')
}, [workflowProcessData?.files?.length, workflowProcessData?.resultText, workflowProcessData?.humanInputFormDataList, workflowProcessData?.humanInputFilledFormDataList])
const handleSubmitHumanInputForm = useCallback(async (formToken: string, formData: any) => {
const handleSubmitHumanInputForm = useCallback(async (formToken: string, formData: { inputs: Record<string, string>, action: string }) => {
if (appSourceType === AppSourceType.installedApp)
await submitHumanInputFormService(formToken, formData)
else

View File

@ -1,6 +1,8 @@
'use client'
import { RiAlertFill, RiTimeLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useLocale } from '@/context/i18n'
import { cn } from '@/utils/classnames'
import { getRelativeTime, isRelativeTimeSameOrAfter } from './utils'
type ExpirationTimeProps = {
@ -16,10 +18,27 @@ const ExpirationTime = ({
const isSameOrAfter = isRelativeTimeSameOrAfter(expirationTime)
return (
<div className="system-xs-regular mt-1 text-text-tertiary">
{isSameOrAfter
? t('humanInput.expirationTimeNowOrFuture', { relativeTime, ns: 'share' })
: t('humanInput.expirationTimePast', { relativeTime, ns: 'share' })}
<div
className={cn(
'system-xs-regular mt-1 flex items-center gap-x-1 text-text-tertiary',
isSameOrAfter && 'text-text-warning',
)}
>
{
isSameOrAfter
? (
<>
<RiTimeLine className="size-3.5" />
<span>{t('humanInput.expirationTimeNowOrFuture', { relativeTime, ns: 'share' })}</span>
</>
)
: (
<>
<RiAlertFill className="size-3.5" />
<span>{t('humanInput.expiredTip', { ns: 'share' })}</span>
</>
)
}
</div>
)
}

View File

@ -18,14 +18,14 @@ const HumanInputForm = ({
const [inputs, setInputs] = useState(defaultInputs)
const [isSubmitting, setIsSubmitting] = useState(false)
const handleInputsChange = useCallback((name: string, value: any) => {
const handleInputsChange = useCallback((name: string, value: string) => {
setInputs(prev => ({
...prev,
[name]: value,
}))
}, [])
const submit = async (formToken: string, actionID: string, inputs: Record<string, any>) => {
const submit = async (formToken: string, actionID: string, inputs: Record<string, string>) => {
setIsSubmitting(true)
await onSubmit?.(formToken, { inputs, action: actionID })
setIsSubmitting(false)

View File

@ -11,9 +11,7 @@ export type UnsubmittedHumanInputContentProps = {
showEmailTip?: boolean
isEmailDebugMode?: boolean
showDebugModeTip?: boolean
showTimeout?: boolean
expirationTime?: number
onSubmit?: (formToken: string, data: any) => Promise<void>
onSubmit?: (formToken: string, data: { inputs: Record<string, string>, action: string }) => Promise<void>
}
export type SubmittedHumanInputContentProps = {
@ -22,12 +20,12 @@ export type SubmittedHumanInputContentProps = {
export type HumanInputFormProps = {
formData: HumanInputFormData
onSubmit?: (formToken: string, data: any) => Promise<void>
onSubmit?: (formToken: string, data: { inputs: Record<string, string>, action: string }) => Promise<void>
}
export type ContentItemProps = {
content: string
formInputFields: FormInputItem[]
inputs: Record<string, string>
onInputChange: (name: string, value: any) => void
onInputChange: (name: string, value: string) => void
}

View File

@ -8,10 +8,10 @@ export const UnsubmittedHumanInputContent = ({
showEmailTip = false,
isEmailDebugMode = false,
showDebugModeTip = false,
showTimeout = false,
expirationTime,
onSubmit,
}: UnsubmittedHumanInputContentProps) => {
const { expiration_time } = formData
return (
<>
{/* Form */}
@ -27,9 +27,9 @@ export const UnsubmittedHumanInputContent = ({
showDebugModeTip={showDebugModeTip}
/>
)}
{/* Timeout */}
{showTimeout && typeof expirationTime === 'number' && (
<ExpirationTime expirationTime={expirationTime} />
{/* Expiration Time */}
{typeof expiration_time === 'number' && (
<ExpirationTime expirationTime={expiration_time} />
)}
</>
)

View File

@ -1,4 +1,5 @@
import type { DeliveryMethod } from '@/app/components/workflow/nodes/human-input/types'
import type { DeliveryMethod, HumanInputNodeType } from '@/app/components/workflow/nodes/human-input/types'
import type { Node } from '@/app/components/workflow/types'
import type { HumanInputFormData } from '@/types/workflow'
import { useMemo } from 'react'
import { DeliveryMethodType } from '@/app/components/workflow/nodes/human-input/types'
@ -7,8 +8,8 @@ import { UnsubmittedHumanInputContent } from './human-input-content/unsubmitted'
type HumanInputFormListProps = {
humanInputFormDataList: HumanInputFormData[]
onHumanInputFormSubmit?: (formToken: string, formData: any) => Promise<void>
getHumanInputNodeData?: (nodeID: string) => any
onHumanInputFormSubmit?: (formToken: string, formData: { inputs: Record<string, string>, action: string }) => Promise<void>
getHumanInputNodeData?: (nodeID: string) => Node<HumanInputNodeType> | undefined
}
const HumanInputFormList = ({

View File

@ -527,6 +527,14 @@ export const useChat = (
}
})
},
onHumanInputFormTimeout: ({ data: humanInputFormTimeoutData }) => {
updateChatTreeNode(messageId, (responseItem) => {
if (responseItem.humanInputFormDataList?.length) {
const currentFormIndex = responseItem.humanInputFormDataList.findIndex(item => item.node_id === humanInputFormTimeoutData.node_id)
responseItem.humanInputFormDataList[currentFormIndex].expiration_time = humanInputFormTimeoutData.expiration_time
}
})
},
onWorkflowPaused: ({ data: workflowPausedData }) => {
const resumeUrl = `/workflow/${workflowPausedData.workflow_run_id}/events`
sseGet(
@ -1089,6 +1097,18 @@ export const useChat = (
parentId: data.parent_message_id,
})
},
onHumanInputFormTimeout: ({ data: humanInputFormTimeoutData }) => {
if (responseItem.humanInputFormDataList?.length) {
const currentFormIndex = responseItem.humanInputFormDataList!.findIndex(item => item.node_id === humanInputFormTimeoutData.node_id)
responseItem.humanInputFormDataList[currentFormIndex].expiration_time = humanInputFormTimeoutData.expiration_time
}
updateCurrentQAOnTree({
placeholderQuestionId,
questionItem,
responseItem,
parentId: data.parent_message_id,
})
},
onWorkflowPaused: ({ data: workflowPausedData }) => {
const url = `/workflow/${workflowPausedData.workflow_run_id}/events`
sseGet(

View File

@ -81,6 +81,7 @@ export const useWorkflowRun = () => {
handleWorkflowNodeFinished,
handleWorkflowNodeHumanInputRequired,
handleWorkflowNodeHumanInputFormFilled,
handleWorkflowNodeHumanInputFormTimeout,
handleWorkflowNodeIterationStarted,
handleWorkflowNodeIterationNext,
handleWorkflowNodeIterationFinished,
@ -182,6 +183,7 @@ export const useWorkflowRun = () => {
onWorkflowPaused,
onHumanInputRequired,
onHumanInputFormFilled,
onHumanInputFormTimeout,
onCompleted,
...restCallback
} = callback || {}
@ -513,6 +515,11 @@ export const useWorkflowRun = () => {
if (onHumanInputFormFilled)
onHumanInputFormFilled(params)
},
onHumanInputFormTimeout: (params) => {
handleWorkflowNodeHumanInputFormTimeout(params)
if (onHumanInputFormTimeout)
onHumanInputFormTimeout(params)
},
onError: wrappedOnError,
onCompleted: wrappedOnCompleted,
}
@ -627,6 +634,7 @@ export const useWorkflowRun = () => {
baseSseOptions.onAgentLog,
baseSseOptions.onHumanInputRequired,
baseSseOptions.onHumanInputFormFilled,
baseSseOptions.onHumanInputFormTimeout,
baseSseOptions.onWorkflowPaused,
baseSseOptions.onDataSourceNodeProcessing,
baseSseOptions.onDataSourceNodeCompleted,
@ -814,6 +822,11 @@ export const useWorkflowRun = () => {
if (onHumanInputFormFilled)
onHumanInputFormFilled(params)
},
onHumanInputFormTimeout: (params) => {
handleWorkflowNodeHumanInputFormTimeout(params)
if (onHumanInputFormTimeout)
onHumanInputFormTimeout(params)
},
...restCallback,
}
@ -824,7 +837,7 @@ export const useWorkflowRun = () => {
},
finalCallbacks,
)
}, [store, doSyncWorkflowDraft, workflowStore, pathname, handleWorkflowFailed, flowId, handleWorkflowStarted, handleWorkflowFinished, fetchInspectVars, invalidAllLastRun, handleWorkflowNodeStarted, handleWorkflowNodeFinished, handleWorkflowNodeIterationStarted, handleWorkflowNodeIterationNext, handleWorkflowNodeIterationFinished, handleWorkflowNodeLoopStarted, handleWorkflowNodeLoopNext, handleWorkflowNodeLoopFinished, handleWorkflowNodeRetry, handleWorkflowAgentLog, handleWorkflowTextChunk, handleWorkflowTextReplace, handleWorkflowPaused, handleWorkflowNodeHumanInputRequired, handleWorkflowNodeHumanInputFormFilled])
}, [store, doSyncWorkflowDraft, workflowStore, pathname, handleWorkflowFailed, flowId, handleWorkflowStarted, handleWorkflowFinished, fetchInspectVars, invalidAllLastRun, handleWorkflowNodeStarted, handleWorkflowNodeFinished, handleWorkflowNodeIterationStarted, handleWorkflowNodeIterationNext, handleWorkflowNodeIterationFinished, handleWorkflowNodeLoopStarted, handleWorkflowNodeLoopNext, handleWorkflowNodeLoopFinished, handleWorkflowNodeRetry, handleWorkflowAgentLog, handleWorkflowTextChunk, handleWorkflowTextReplace, handleWorkflowPaused, handleWorkflowNodeHumanInputRequired, handleWorkflowNodeHumanInputFormFilled, handleWorkflowNodeHumanInputFormTimeout])
const handleStopRun = useCallback((taskId: string) => {
const setStoppedState = () => {

View File

@ -3,6 +3,7 @@ export * from './use-workflow-failed'
export * from './use-workflow-finished'
export * from './use-workflow-node-finished'
export * from './use-workflow-node-human-input-form-filled'
export * from './use-workflow-node-human-input-form-timeout'
export * from './use-workflow-node-human-input-required'
export * from './use-workflow-node-iteration-finished'
export * from './use-workflow-node-iteration-next'

View File

@ -0,0 +1,28 @@
import type { HumanInputFormTimeoutResponse } from '@/types/workflow'
import { produce } from 'immer'
import { useCallback } from 'react'
import { useWorkflowStore } from '@/app/components/workflow/store'
export const useWorkflowNodeHumanInputFormTimeout = () => {
const workflowStore = useWorkflowStore()
const handleWorkflowNodeHumanInputFormTimeout = useCallback((params: HumanInputFormTimeoutResponse) => {
const { data } = params
const {
workflowRunningData,
setWorkflowRunningData,
} = workflowStore.getState()
const newWorkflowRunningData = produce(workflowRunningData!, (draft) => {
if (draft.humanInputFormDataList?.length) {
const currentFormIndex = draft.humanInputFormDataList.findIndex(item => item.node_id === data.node_id)
draft.humanInputFormDataList[currentFormIndex].expiration_time = data.expiration_time
}
})
setWorkflowRunningData(newWorkflowRunningData)
}, [workflowStore])
return {
handleWorkflowNodeHumanInputFormTimeout,
}
}

View File

@ -4,6 +4,7 @@ import {
useWorkflowFinished,
useWorkflowNodeFinished,
useWorkflowNodeHumanInputFormFilled,
useWorkflowNodeHumanInputFormTimeout,
useWorkflowNodeHumanInputRequired,
useWorkflowNodeIterationFinished,
useWorkflowNodeIterationNext,
@ -38,6 +39,7 @@ export const useWorkflowRunEvent = () => {
const { handleWorkflowPaused } = useWorkflowPaused()
const { handleWorkflowNodeHumanInputRequired } = useWorkflowNodeHumanInputRequired()
const { handleWorkflowNodeHumanInputFormFilled } = useWorkflowNodeHumanInputFormFilled()
const { handleWorkflowNodeHumanInputFormTimeout } = useWorkflowNodeHumanInputFormTimeout()
return {
handleWorkflowStarted,
@ -58,5 +60,6 @@ export const useWorkflowRunEvent = () => {
handleWorkflowPaused,
handleWorkflowNodeHumanInputFormFilled,
handleWorkflowNodeHumanInputRequired,
handleWorkflowNodeHumanInputFormTimeout,
}
}

View File

@ -613,6 +613,18 @@ export const useChat = (
parentId: params.parent_message_id,
})
},
onHumanInputFormTimeout: ({ data }) => {
if (responseItem.humanInputFormDataList?.length) {
const currentFormIndex = responseItem.humanInputFormDataList.findIndex(item => item.node_id === data.node_id)
responseItem.humanInputFormDataList[currentFormIndex].expiration_time = data.expiration_time
}
updateCurrentQAOnTree({
placeholderQuestionId,
questionItem,
responseItem,
parentId: params.parent_message_id,
})
},
onWorkflowPaused: ({ data: _data }) => {
responseItem.workflowProcess!.status = WorkflowRunningStatus.Paused
updateCurrentQAOnTree({
@ -864,6 +876,14 @@ export const useChat = (
}
})
},
onHumanInputFormTimeout: ({ data: humanInputFormTimeoutData }) => {
updateChatTreeNode(messageId, (responseItem) => {
if (responseItem.humanInputFormDataList?.length) {
const currentFormIndex = responseItem.humanInputFormDataList.findIndex(item => item.node_id === humanInputFormTimeoutData.node_id)
responseItem.humanInputFormDataList[currentFormIndex].expiration_time = humanInputFormTimeoutData.expiration_time
}
})
},
onWorkflowPaused: ({ data: workflowPausedData }) => {
const resumeUrl = `/workflow/${workflowPausedData.workflow_run_id}/events`
sseGet(

View File

@ -594,7 +594,7 @@
"count": 3
},
"ts/no-explicit-any": {
"count": 5
"count": 4
}
},
"app/components/app/text-generate/item/result-tab.tsx": {
@ -794,26 +794,11 @@
"count": 1
}
},
"app/components/base/chat/chat/answer/human-input-content/human-input-form.tsx": {
"ts/no-explicit-any": {
"count": 2
}
},
"app/components/base/chat/chat/answer/human-input-content/type.ts": {
"ts/no-explicit-any": {
"count": 3
}
},
"app/components/base/chat/chat/answer/human-input-content/utils.ts": {
"ts/no-explicit-any": {
"count": 1
}
},
"app/components/base/chat/chat/answer/human-input-form-list.tsx": {
"ts/no-explicit-any": {
"count": 2
}
},
"app/components/base/chat/chat/answer/index.tsx": {
"react-hooks-extra/no-direct-set-state-in-use-effect": {
"count": 3
@ -4439,7 +4424,7 @@
},
"service/workflow.ts": {
"ts/no-explicit-any": {
"count": 4
"count": 3
}
},
"testing/testing.md": {

View File

@ -60,8 +60,8 @@
"generation.title": "AI Completion",
"humanInput.completed": "Seems like this request was dealt with elsewhere.",
"humanInput.expirationTimeNowOrFuture": "This action will expire {{relativeTime}}.",
"humanInput.expirationTimePast": "This action has expired {{relativeTime}}.",
"humanInput.expired": "Seems like this request has expired.",
"humanInput.expiredTip": "This action has expired.",
"humanInput.formNotFound": "Form not found.",
"humanInput.recorded": "Your input has been recorded.",
"humanInput.sorry": "Sorry!",

View File

@ -60,8 +60,8 @@
"generation.title": "AI 智能书写",
"humanInput.completed": "此请求似乎在其他地方得到了处理。",
"humanInput.expirationTimeNowOrFuture": "此操作将在 {{relativeTime}}过期。",
"humanInput.expirationTimePast": "此操作已在 {{relativeTime}}过期。",
"humanInput.expired": "此请求似乎已过期。",
"humanInput.expiredTip": "此操作已过期。",
"humanInput.formNotFound": "表单不存在。",
"humanInput.recorded": "您的输入已被记录。",
"humanInput.sorry": "抱歉!",

View File

@ -9,6 +9,7 @@ import type {
import type {
AgentLogResponse,
HumanInputFormFilledResponse,
HumanInputFormTimeoutResponse,
HumanInputRequiredResponse,
IterationFinishedResponse,
IterationNextResponse,
@ -75,6 +76,7 @@ export type IOnAgentLog = (agentLog: AgentLogResponse) => void
export type IOHumanInputRequired = (humanInputRequired: HumanInputRequiredResponse) => void
export type IOnHumanInputFormFilled = (humanInputFormFilled: HumanInputFormFilledResponse) => void
export type IOnHumanInputFormTimeout = (humanInputFormTimeout: HumanInputFormTimeoutResponse) => void
export type IOWorkflowPaused = (workflowPaused: WorkflowPausedResponse) => void
export type IOnDataSourceNodeProcessing = (dataSourceNodeProcessing: DataSourceNodeProcessingResponse) => void
export type IOnDataSourceNodeCompleted = (dataSourceNodeCompleted: DataSourceNodeCompletedResponse) => void
@ -121,6 +123,7 @@ export type IOtherOptions = {
onAgentLog?: IOnAgentLog
onHumanInputRequired?: IOHumanInputRequired
onHumanInputFormFilled?: IOnHumanInputFormFilled
onHumanInputFormTimeout?: IOnHumanInputFormTimeout
onWorkflowPaused?: IOWorkflowPaused
// Pipeline data source node run
@ -206,6 +209,7 @@ export const handleStream = (
onAgentLog?: IOnAgentLog,
onHumanInputRequired?: IOHumanInputRequired,
onHumanInputFormFilled?: IOnHumanInputFormFilled,
onHumanInputFormTimeout?: IOnHumanInputFormTimeout,
onWorkflowPaused?: IOWorkflowPaused,
onDataSourceNodeProcessing?: IOnDataSourceNodeProcessing,
onDataSourceNodeCompleted?: IOnDataSourceNodeCompleted,
@ -345,6 +349,9 @@ export const handleStream = (
else if (bufferObj.event === 'human_input_form_filled') {
onHumanInputFormFilled?.(bufferObj as HumanInputFormFilledResponse)
}
else if (bufferObj.event === 'human_input_form_timeout') {
onHumanInputFormTimeout?.(bufferObj as HumanInputFormTimeoutResponse)
}
else if (bufferObj.event === 'workflow_paused') {
onWorkflowPaused?.(bufferObj as WorkflowPausedResponse)
}
@ -472,6 +479,7 @@ export const ssePost = async (
onLoopFinish,
onHumanInputRequired,
onHumanInputFormFilled,
onHumanInputFormTimeout,
onWorkflowPaused,
onDataSourceNodeProcessing,
onDataSourceNodeCompleted,
@ -576,6 +584,7 @@ export const ssePost = async (
onAgentLog,
onHumanInputRequired,
onHumanInputFormFilled,
onHumanInputFormTimeout,
onWorkflowPaused,
onDataSourceNodeProcessing,
onDataSourceNodeCompleted,
@ -624,6 +633,7 @@ export const sseGet = async (
onLoopFinish,
onHumanInputRequired,
onHumanInputFormFilled,
onHumanInputFormTimeout,
onWorkflowPaused,
onDataSourceNodeProcessing,
onDataSourceNodeCompleted,
@ -721,6 +731,7 @@ export const sseGet = async (
onAgentLog,
onHumanInputRequired,
onHumanInputFormFilled,
onHumanInputFormTimeout,
onWorkflowPaused,
onDataSourceNodeProcessing,
onDataSourceNodeCompleted,

View File

@ -279,7 +279,7 @@ export const getHumanInputForm = (token: string) => {
}
export const submitHumanInputForm = (token: string, data: {
inputs: Record<string, any>
inputs: Record<string, string>
action: string
}) => {
return post(`/form/human_input/${token}`, { body: data })

View File

@ -197,7 +197,7 @@ export const useGetHumanInputForm = (token: string, options: ShareQueryOptions =
export type SubmitHumanInputFormParams = {
token: string
data: {
inputs: Record<string, unknown>
inputs: Record<string, string>
action: string
}
}

View File

@ -99,7 +99,7 @@ export const fetchNodeInspectVars = async (flowType: FlowType, flowId: string, n
}
export const submitHumanInputForm = (token: string, data: {
inputs: Record<string, any>
inputs: Record<string, string>
action: string
}) => {
return post(`/form/human_input/${token}`, { body: data })

View File

@ -323,6 +323,7 @@ export type HumanInputFormData = {
form_token: string
resolved_default_values: Record<string, string>
display_in_ui: boolean
expiration_time: number
}
export type HumanInputRequiredResponse = {
@ -347,6 +348,19 @@ export type HumanInputFormFilledResponse = {
data: HumanInputFilledFormData
}
export type HumanInputFormTimeoutData = {
node_id: string
node_title: string
expiration_time: number
}
export type HumanInputFormTimeoutResponse = {
task_id: string
workflow_run_id: string
event: string
data: HumanInputFormTimeoutData
}
export type WorkflowRunHistory = {
id: string
version: string