mirror of
https://github.com/langgenius/dify.git
synced 2026-05-02 00:18:03 +08:00
feat: Human Input Node (#32060)
The frontend and backend implementation for the human input node. Co-authored-by: twwu <twwu@dify.ai> Co-authored-by: JzoNg <jzongcode@gmail.com> Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com> Co-authored-by: zhsama <torvalds@linux.do>
This commit is contained in:
@ -8,6 +8,9 @@ import type {
|
||||
} from '@/types/pipeline'
|
||||
import type {
|
||||
AgentLogResponse,
|
||||
HumanInputFormFilledResponse,
|
||||
HumanInputFormTimeoutResponse,
|
||||
HumanInputRequiredResponse,
|
||||
IterationFinishedResponse,
|
||||
IterationNextResponse,
|
||||
IterationStartedResponse,
|
||||
@ -21,6 +24,7 @@ import type {
|
||||
TextChunkResponse,
|
||||
TextReplaceResponse,
|
||||
WorkflowFinishedResponse,
|
||||
WorkflowPausedResponse,
|
||||
WorkflowStartedResponse,
|
||||
} from '@/types/workflow'
|
||||
import Cookies from 'js-cookie'
|
||||
@ -29,7 +33,7 @@ import { API_PREFIX, CSRF_COOKIE_NAME, CSRF_HEADER_NAME, IS_CE_EDITION, PASSPORT
|
||||
import { asyncRunSafe } from '@/utils'
|
||||
import { basePath } from '@/utils/var'
|
||||
import { base, ContentType, getBaseOptions } from './fetch'
|
||||
import { refreshAccessTokenOrRelogin } from './refresh-token'
|
||||
import { refreshAccessTokenOrReLogin } from './refresh-token'
|
||||
import { getWebAppPassport } from './webapp-auth'
|
||||
|
||||
const TIME_OUT = 100000
|
||||
@ -70,6 +74,10 @@ export type IOnLoopNext = (workflowStarted: LoopNextResponse) => void
|
||||
export type IOnLoopFinished = (workflowFinished: LoopFinishedResponse) => void
|
||||
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
|
||||
export type IOnDataSourceNodeError = (dataSourceNodeError: DataSourceNodeErrorResponse) => void
|
||||
@ -113,6 +121,10 @@ export type IOtherOptions = {
|
||||
onLoopNext?: IOnLoopNext
|
||||
onLoopFinish?: IOnLoopFinished
|
||||
onAgentLog?: IOnAgentLog
|
||||
onHumanInputRequired?: IOHumanInputRequired
|
||||
onHumanInputFormFilled?: IOnHumanInputFormFilled
|
||||
onHumanInputFormTimeout?: IOnHumanInputFormTimeout
|
||||
onWorkflowPaused?: IOWorkflowPaused
|
||||
|
||||
// Pipeline data source node run
|
||||
onDataSourceNodeProcessing?: IOnDataSourceNodeProcessing
|
||||
@ -153,6 +165,14 @@ function requiredWebSSOLogin(message?: string, code?: number) {
|
||||
globalThis.location.href = `${globalThis.location.origin}${basePath}${WBB_APP_LOGIN_PATH}?${params.toString()}`
|
||||
}
|
||||
|
||||
function formatURL(url: string, isPublicAPI: boolean) {
|
||||
const urlPrefix = isPublicAPI ? PUBLIC_API_PREFIX : API_PREFIX
|
||||
if (url.startsWith('http://') || url.startsWith('https://'))
|
||||
return url
|
||||
const urlWithoutProtocol = url.startsWith('/') ? url : `/${url}`
|
||||
return `${urlPrefix}${urlWithoutProtocol}`
|
||||
}
|
||||
|
||||
export function format(text: string) {
|
||||
let res = text.trim()
|
||||
if (res.startsWith('\n'))
|
||||
@ -187,6 +207,10 @@ export const handleStream = (
|
||||
onTTSEnd?: IOnTTSEnd,
|
||||
onTextReplace?: IOnTextReplace,
|
||||
onAgentLog?: IOnAgentLog,
|
||||
onHumanInputRequired?: IOHumanInputRequired,
|
||||
onHumanInputFormFilled?: IOnHumanInputFormFilled,
|
||||
onHumanInputFormTimeout?: IOnHumanInputFormTimeout,
|
||||
onWorkflowPaused?: IOWorkflowPaused,
|
||||
onDataSourceNodeProcessing?: IOnDataSourceNodeProcessing,
|
||||
onDataSourceNodeCompleted?: IOnDataSourceNodeCompleted,
|
||||
onDataSourceNodeError?: IOnDataSourceNodeError,
|
||||
@ -319,6 +343,18 @@ export const handleStream = (
|
||||
else if (bufferObj.event === 'tts_message_end') {
|
||||
onTTSEnd?.(bufferObj.message_id, bufferObj.audio)
|
||||
}
|
||||
else if (bufferObj.event === 'human_input_required') {
|
||||
onHumanInputRequired?.(bufferObj as HumanInputRequiredResponse)
|
||||
}
|
||||
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)
|
||||
}
|
||||
else if (bufferObj.event === 'datasource_processing') {
|
||||
onDataSourceNodeProcessing?.(bufferObj as DataSourceNodeProcessingResponse)
|
||||
}
|
||||
@ -441,6 +477,10 @@ export const ssePost = async (
|
||||
onLoopStart,
|
||||
onLoopNext,
|
||||
onLoopFinish,
|
||||
onHumanInputRequired,
|
||||
onHumanInputFormFilled,
|
||||
onHumanInputFormTimeout,
|
||||
onWorkflowPaused,
|
||||
onDataSourceNodeProcessing,
|
||||
onDataSourceNodeCompleted,
|
||||
onDataSourceNodeError,
|
||||
@ -467,10 +507,7 @@ export const ssePost = async (
|
||||
|
||||
getAbortController?.(abortController)
|
||||
|
||||
const urlPrefix = isPublicAPI ? PUBLIC_API_PREFIX : API_PREFIX
|
||||
const urlWithPrefix = (url.startsWith('http://') || url.startsWith('https://'))
|
||||
? url
|
||||
: `${urlPrefix}${url.startsWith('/') ? url : `/${url}`}`
|
||||
const urlWithPrefix = formatURL(url, isPublicAPI)
|
||||
|
||||
const { body } = options
|
||||
if (body)
|
||||
@ -495,7 +532,7 @@ export const ssePost = async (
|
||||
})
|
||||
}
|
||||
else {
|
||||
refreshAccessTokenOrRelogin(TIME_OUT).then(() => {
|
||||
refreshAccessTokenOrReLogin(TIME_OUT).then(() => {
|
||||
ssePost(url, fetchOptions, otherOptions)
|
||||
}).catch((err) => {
|
||||
console.error(err)
|
||||
@ -545,6 +582,157 @@ export const ssePost = async (
|
||||
onTTSEnd,
|
||||
onTextReplace,
|
||||
onAgentLog,
|
||||
onHumanInputRequired,
|
||||
onHumanInputFormFilled,
|
||||
onHumanInputFormTimeout,
|
||||
onWorkflowPaused,
|
||||
onDataSourceNodeProcessing,
|
||||
onDataSourceNodeCompleted,
|
||||
onDataSourceNodeError,
|
||||
)
|
||||
})
|
||||
.catch((e) => {
|
||||
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 })
|
||||
onError?.(e)
|
||||
})
|
||||
}
|
||||
|
||||
export const sseGet = async (
|
||||
url: string,
|
||||
fetchOptions: FetchOptionType,
|
||||
otherOptions: IOtherOptions,
|
||||
) => {
|
||||
const {
|
||||
isPublicAPI = false,
|
||||
onData,
|
||||
onCompleted,
|
||||
onThought,
|
||||
onFile,
|
||||
onMessageEnd,
|
||||
onMessageReplace,
|
||||
onWorkflowStarted,
|
||||
onWorkflowFinished,
|
||||
onNodeStarted,
|
||||
onNodeFinished,
|
||||
onIterationStart,
|
||||
onIterationNext,
|
||||
onIterationFinish,
|
||||
onNodeRetry,
|
||||
onParallelBranchStarted,
|
||||
onParallelBranchFinished,
|
||||
onTextChunk,
|
||||
onTTSChunk,
|
||||
onTTSEnd,
|
||||
onTextReplace,
|
||||
onAgentLog,
|
||||
onError,
|
||||
getAbortController,
|
||||
onLoopStart,
|
||||
onLoopNext,
|
||||
onLoopFinish,
|
||||
onHumanInputRequired,
|
||||
onHumanInputFormFilled,
|
||||
onHumanInputFormTimeout,
|
||||
onWorkflowPaused,
|
||||
onDataSourceNodeProcessing,
|
||||
onDataSourceNodeCompleted,
|
||||
onDataSourceNodeError,
|
||||
} = otherOptions
|
||||
const abortController = new AbortController()
|
||||
|
||||
const baseOptions = getBaseOptions()
|
||||
const shareCode = globalThis.location.pathname.split('/').slice(-1)[0]
|
||||
const options = Object.assign({}, baseOptions, {
|
||||
signal: abortController.signal,
|
||||
headers: new Headers({
|
||||
[CSRF_HEADER_NAME]: Cookies.get(CSRF_COOKIE_NAME()) || '',
|
||||
[WEB_APP_SHARE_CODE_HEADER_NAME]: shareCode,
|
||||
[PASSPORT_HEADER_NAME]: getWebAppPassport(shareCode),
|
||||
}),
|
||||
} as RequestInit, fetchOptions)
|
||||
|
||||
const contentType = (options.headers as Headers).get('Content-Type')
|
||||
if (!contentType)
|
||||
(options.headers as Headers).set('Content-Type', ContentType.json)
|
||||
|
||||
getAbortController?.(abortController)
|
||||
|
||||
const urlWithPrefix = formatURL(url, isPublicAPI)
|
||||
|
||||
globalThis.fetch(urlWithPrefix, options as RequestInit)
|
||||
.then((res) => {
|
||||
if (!/^[23]\d{2}$/.test(String(res.status))) {
|
||||
if (res.status === 401) {
|
||||
if (isPublicAPI) {
|
||||
res.json().then((data: { code?: string, message?: string }) => {
|
||||
if (isPublicAPI) {
|
||||
if (data.code === 'web_app_access_denied')
|
||||
requiredWebSSOLogin(data.message, 403)
|
||||
|
||||
if (data.code === 'web_sso_auth_required')
|
||||
requiredWebSSOLogin()
|
||||
|
||||
if (data.code === 'unauthorized')
|
||||
requiredWebSSOLogin()
|
||||
}
|
||||
})
|
||||
}
|
||||
else {
|
||||
refreshAccessTokenOrReLogin(TIME_OUT).then(() => {
|
||||
sseGet(url, fetchOptions, otherOptions)
|
||||
}).catch((err) => {
|
||||
console.error(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
else {
|
||||
res.json().then((data) => {
|
||||
Toast.notify({ type: 'error', message: data.message || 'Server Error' })
|
||||
})
|
||||
onError?.('Server Error')
|
||||
}
|
||||
return
|
||||
}
|
||||
return handleStream(
|
||||
res,
|
||||
(str: string, isFirstMessage: boolean, moreInfo: IOnDataMoreInfo) => {
|
||||
if (moreInfo.errorMessage) {
|
||||
onError?.(moreInfo.errorMessage, moreInfo.errorCode)
|
||||
// TypeError: Cannot assign to read only property ... will happen in page leave, so it should be ignored.
|
||||
if (moreInfo.errorMessage !== 'AbortError: The user aborted a request.' && !moreInfo.errorMessage.includes('TypeError: Cannot assign to read only property'))
|
||||
Toast.notify({ type: 'error', message: moreInfo.errorMessage })
|
||||
return
|
||||
}
|
||||
onData?.(str, isFirstMessage, moreInfo)
|
||||
},
|
||||
onCompleted,
|
||||
onThought,
|
||||
onMessageEnd,
|
||||
onMessageReplace,
|
||||
onFile,
|
||||
onWorkflowStarted,
|
||||
onWorkflowFinished,
|
||||
onNodeStarted,
|
||||
onNodeFinished,
|
||||
onIterationStart,
|
||||
onIterationNext,
|
||||
onIterationFinish,
|
||||
onLoopStart,
|
||||
onLoopNext,
|
||||
onLoopFinish,
|
||||
onNodeRetry,
|
||||
onParallelBranchStarted,
|
||||
onParallelBranchFinished,
|
||||
onTextChunk,
|
||||
onTTSChunk,
|
||||
onTTSEnd,
|
||||
onTextReplace,
|
||||
onAgentLog,
|
||||
onHumanInputRequired,
|
||||
onHumanInputFormFilled,
|
||||
onHumanInputFormTimeout,
|
||||
onWorkflowPaused,
|
||||
onDataSourceNodeProcessing,
|
||||
onDataSourceNodeCompleted,
|
||||
onDataSourceNodeError,
|
||||
@ -612,7 +800,7 @@ export const request = async<T>(url: string, options = {}, otherOptions?: IOther
|
||||
}
|
||||
|
||||
// refresh token
|
||||
const [refreshErr] = await asyncRunSafe(refreshAccessTokenOrRelogin(TIME_OUT))
|
||||
const [refreshErr] = await asyncRunSafe(refreshAccessTokenOrReLogin(TIME_OUT))
|
||||
if (refreshErr === null)
|
||||
return baseFetch<T>(url, options, otherOptionsForBaseFetch)
|
||||
if (location.pathname !== `${basePath}/signin` || !IS_CE_EDITION) {
|
||||
|
||||
Reference in New Issue
Block a user