dify/cli/src/commands/run/app/hitl-render.ts
Yunlu Wen c0ee821d45
refactor: use absolute path for inter dir importing (#36822)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-01 01:32:16 +00:00

116 lines
4.0 KiB
TypeScript

import type { HitlPausePayload } from './sse-collector'
import { colorEnabled, colorScheme } from '@/sys/io/color'
export type HitlExitObject = {
status: 'paused'
app_id: string
task_id: string
workflow_run_id: string
form_id: string
node_id: string
node_title: string
form_token: string | null
form_content: string
inputs: unknown[]
actions: unknown[]
display_in_ui: boolean
resolved_default_values: Record<string, string>
expiration_time: number
}
export function buildHitlExitObject(appId: string, payload: HitlPausePayload): HitlExitObject {
const d = payload.data
return {
status: 'paused',
app_id: appId,
task_id: payload.task_id,
workflow_run_id: payload.workflow_run_id,
form_id: d.form_id,
node_id: d.node_id,
node_title: d.node_title,
form_token: d.form_token,
form_content: d.form_content,
inputs: d.inputs,
actions: d.actions,
display_in_ui: d.display_in_ui,
resolved_default_values: d.resolved_default_values,
expiration_time: d.expiration_time,
}
}
export function renderHitlExit(obj: HitlExitObject): string {
return JSON.stringify(obj, null, 2)
}
type ActionRecord = { id: string, title?: string, button_style?: string }
type InputRecord = { output_variable_name?: string, label?: string, type?: string, required?: boolean }
export function renderHitlBlock(_appId: string, payload: HitlPausePayload, isTTY: boolean): string {
const d = payload.data
const cs = colorScheme(colorEnabled(isTTY))
const lines: string[] = []
lines.push(`${cs.warningIcon()} ${cs.bold('Workflow paused')} ${cs.dim('— input required')}`)
lines.push(` ${cs.dim('Node:')} ${d.node_title}`)
const msgLines = d.form_content.split('\n')
if (msgLines.length === 1) {
lines.push(` ${cs.dim('Message:')} ${d.form_content}`)
}
else {
lines.push(` ${cs.dim('Message:')}`)
for (const ml of msgLines)
lines.push(` ${ml}`)
}
const actions = (Array.isArray(d.actions) ? d.actions : []) as ActionRecord[]
if (actions.length > 0) {
const inline = actions.map((a) => {
const title = a.title ?? ''
return `${cs.cyan(`[${a.id}]`)} ${title}`
}).join(' ')
lines.push(` ${cs.dim('Actions:')} ${inline}`)
}
const inputs = (Array.isArray(d.inputs) ? d.inputs : []) as InputRecord[]
if (inputs.length > 0) {
const inline = inputs.map((inp) => {
const name = inp.output_variable_name ?? '?'
const label = typeof inp.label === 'string' && inp.label !== '' ? ` ${cs.dim(`${inp.label}`)}` : ''
const req = inp.required === true ? ` ${cs.yellow('*')}` : ''
return `- ${cs.cyan(name)}${req}${label}`
}).join(' ')
lines.push(` ${cs.dim('Inputs:')} ${inline}`)
}
lines.push('')
return `${lines.join('\n')}\n`
}
export function renderHitlOutput(appId: string, payload: HitlPausePayload, isText: boolean, isOutTTY: boolean): string {
if (isText)
return renderHitlBlock(appId, payload, isOutTTY)
const obj = buildHitlExitObject(appId, payload)
return `${renderHitlExit(obj)}\n`
}
const EXTERNAL_CHANNEL_NOTE = 'form delivered via email/external channel — resume only from that channel'
export function renderHitlHint(appId: string, payload: HitlPausePayload, isErrTTY: boolean): string {
const d = payload.data
const cs = colorScheme(colorEnabled(isErrTTY))
if (d.form_token === null) {
if (!isErrTTY)
return `hint: workflow paused — ${EXTERNAL_CHANNEL_NOTE}\n`
return `${cs.warningIcon()} ${cs.bold('workflow paused')}${cs.dim(EXTERNAL_CHANNEL_NOTE)}\n`
}
const actions = (d.actions ?? []) as { id: string }[]
let cmd = `difyctl resume app ${appId} ${d.form_token} --workflow-run-id ${payload.workflow_run_id}`
if (actions.length > 1) {
const firstAction = actions[0]?.id
if (firstAction !== undefined)
cmd += ` --action ${firstAction}`
}
if (!isErrTTY)
return `hint: workflow paused — resume with: ${cmd}\n`
return `${cs.warningIcon()} ${cs.bold('workflow paused')} — resume with:\n ${cs.cyan(cmd)}\n`
}