From a66b8fc82160d9f181dcd634b00428b023350e36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Fri, 3 Jul 2026 19:33:10 +0800 Subject: [PATCH] Add files via upload --- web/static/i18n/en-US.json | 11 +++++ web/static/i18n/zh-CN.json | 11 +++++ web/static/js/chat.js | 4 ++ web/static/js/monitor.js | 89 ++++++++++++++++++++++++++++++++++++++ web/static/js/workflows.js | 66 +++++++++++++++++++++++----- 5 files changed, 170 insertions(+), 11 deletions(-) diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index 6ffeacc6..c9a577f4 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -2975,10 +2975,13 @@ "selectTool": "Select a tool", "toolDisabled": " (disabled)", "argumentsTemplate": "Arguments template", + "argumentsStatic": "Tool arguments (JSON)", "timeoutSeconds": "Timeout (seconds)", "optional": "Optional", "agentMode": "Agent mode", "inputSource": "Input source", + "inputBinding": "Input field binding", + "inputBindingHint": "from = data source, field = field name (e.g. output, message)", "nodeInstruction": "Node instruction", "instructionPlaceholder": "Describe what this node should accomplish", "outputKey": "Output variable name", @@ -2991,7 +2994,15 @@ "hitlPrompt": "Approval prompt", "hitlPromptPlaceholder": "Approve to continue", "hitlReviewer": "Reviewer", + "hitlInteractiveHint": "The run pauses at this node; approve or reject via API or the monitor panel to continue.", + "promptBinding": "Prompt field binding", + "promptBindingHint": "When prompt text is empty, read approval text from the bound field", "outputSource": "Variable source", + "sourceBinding": "Output field binding", + "sourceBindingHint": "Write the bound field value to the output variable; static value below overrides when set", + "staticValue": "Static output value", + "resultBinding": "End summary binding", + "resultBindingHint": "Field shown in the end node summary", "endTemplate": "End summary template" }, "defaultHitlPrompt": "Please approve whether this step should continue", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index e1c5658a..871e8c61 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -2963,10 +2963,13 @@ "selectTool": "请选择工具", "toolDisabled": "(未启用)", "argumentsTemplate": "参数模板", + "argumentsStatic": "工具参数(JSON)", "timeoutSeconds": "超时秒数", "optional": "可选", "agentMode": "Agent 模式", "inputSource": "输入来源", + "inputBinding": "输入字段绑定", + "inputBindingHint": "from 选数据来源,field 为字段名(如 output、message)", "nodeInstruction": "节点指令", "instructionPlaceholder": "描述该节点要完成的任务", "outputKey": "输出变量名", @@ -2979,7 +2982,15 @@ "hitlPrompt": "审批提示", "hitlPromptPlaceholder": "请审批是否继续", "hitlReviewer": "审批方", + "hitlInteractiveHint": "运行时在审批节点暂停,需通过 API 或监控面板人工通过/拒绝后继续。", + "promptBinding": "提示字段绑定", + "promptBindingHint": "留空提示文案时,从绑定字段读取审批说明", "outputSource": "变量来源", + "sourceBinding": "输出字段绑定", + "sourceBindingHint": "将绑定字段的值写入输出变量;也可填写下方固定值覆盖", + "staticValue": "固定输出值", + "resultBinding": "结束摘要绑定", + "resultBindingHint": "结束节点展示的摘要字段", "endTemplate": "结束摘要模板" }, "defaultHitlPrompt": "请审批该步骤是否继续执行", diff --git a/web/static/js/chat.js b/web/static/js/chat.js index 59a8efc9..6b5dea10 100644 --- a/web/static/js/chat.js +++ b/web/static/js/chat.js @@ -2572,6 +2572,10 @@ function renderProcessDetails(messageId, processDetails, options) { itemTitle = '🤖 Agent 输出' + (label ? (' · ' + label) : ''); } else if (eventType === 'workflow_hitl_checkpoint') { itemTitle = '🧑‍⚖️ 人工确认检查点'; + } else if (eventType === 'workflow_hitl_waiting') { + itemTitle = '🧑‍⚖️ 工作流等待审批'; + } else if (eventType === 'workflow_paused') { + itemTitle = '⏸️ 工作流已暂停'; } else if (eventType === 'iteration') { const n = data.iteration || 1; if (data.orchestration === 'plan_execute' && data.einoScope === 'main') { diff --git a/web/static/js/monitor.js b/web/static/js/monitor.js index 61017639..425a1263 100644 --- a/web/static/js/monitor.js +++ b/web/static/js/monitor.js @@ -1910,6 +1910,26 @@ function handleStreamEvent(event, progressElement, progressId, break; } + case 'workflow_hitl_waiting': { + const d = event.data || {}; + const hitlItemId = addTimelineItem(timeline, 'workflow_hitl_waiting', { + title: '🧑‍⚖️ 工作流等待审批', + message: event.message || '', + data: d + }); + renderInlineWorkflowHitlApproval(hitlItemId, d); + break; + } + + case 'workflow_paused': { + addTimelineItem(timeline, 'workflow_paused', { + title: '⏸️ 工作流已暂停', + message: event.message || '', + data: event.data || {} + }); + break; + } + case 'eino_trace_run': case 'eino_trace_start': case 'eino_trace_end': @@ -2834,6 +2854,75 @@ function renderInlineHitlApproval(itemId, data) { rejectBtn.onclick = function () { submit('reject'); }; } +function renderInlineWorkflowHitlApproval(itemId, data) { + const item = document.getElementById(itemId); + if (!item || !data) return; + const runId = data.workflowRunId || data.workflow_run_id; + if (!runId) return; + let contentEl = item.querySelector('.timeline-item-content'); + if (!contentEl) { + contentEl = document.createElement('div'); + contentEl.className = 'timeline-item-content'; + item.appendChild(contentEl); + } + const existingPanel = contentEl.querySelector('.workflow-hitl-inline-approval'); + if (existingPanel) existingPanel.remove(); + + const label = data.label || data.nodeId || runId; + const prompt = data.prompt || ''; + const panel = document.createElement('div'); + panel.className = 'workflow-hitl-inline-approval hitl-inline-approval'; + panel.innerHTML = ` +
${escapeHtml(label)} 等待人工审批。
+ ${prompt ? `
${escapeHtml(prompt)}
` : ''} +
备注(可选)
+ +
+ + +
+
+ `; + contentEl.appendChild(panel); + + const approveBtn = panel.querySelector('.workflow-hitl-inline-approve'); + const rejectBtn = panel.querySelector('.workflow-hitl-inline-reject'); + const commentInput = panel.querySelector('.workflow-hitl-inline-comment'); + const statusEl = panel.querySelector('.workflow-hitl-inline-status'); + + const setBusy = function (busy) { + approveBtn.disabled = busy; + rejectBtn.disabled = busy; + }; + + const submit = async function (approved) { + setBusy(true); + const comment = String(commentInput.value || '').trim(); + try { + const fetchFn = typeof apiFetch === 'function' ? apiFetch : fetch; + const response = await fetchFn(`/api/workflows/runs/${encodeURIComponent(runId)}/resume`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ approved: approved, comment: comment }) + }); + const body = response && typeof response.json === 'function' ? await response.json() : null; + if (!response || !response.ok) { + statusEl.textContent = (body && body.error) ? body.error : '提交失败,请重试'; + setBusy(false); + return; + } + statusEl.textContent = approved ? '已通过,工作流继续执行' : '已拒绝'; + panel.classList.add('hitl-inline-done'); + } catch (e) { + statusEl.textContent = '提交失败:' + (e && e.message ? e.message : 'unknown error'); + setBusy(false); + } + }; + + approveBtn.onclick = function () { submit(true); }; + rejectBtn.onclick = function () { submit(false); }; +} + function hitlEscapeAttrSelector(val) { const s = String(val); if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') { diff --git a/web/static/js/workflows.js b/web/static/js/workflows.js index 45e4d3cb..767dd011 100644 --- a/web/static/js/workflows.js +++ b/web/static/js/workflows.js @@ -55,6 +55,45 @@ .replace(/'/g, '''); } + const BINDING_FROM_OPTIONS = ['previous', 'inputs', 'outputs']; + + function bindingFromConfig(cfg, key, fallbackFrom, fallbackField) { + const b = cfg && cfg[key]; + if (b && typeof b === 'object') { + return { + from: b.from || fallbackFrom, + field: b.field || fallbackField + }; + } + return { from: fallbackFrom, field: fallbackField }; + } + + function bindingFieldHtml(prefix, labelKey, binding, hintKey) { + const from = binding.from || 'previous'; + const field = binding.field || 'output'; + const label = _t(labelKey); + const hint = hintKey ? _t(hintKey) : ''; + const options = BINDING_FROM_OPTIONS.map(v => + `` + ).join(''); + return ` +
+ +
+ + +
+ ${hint ? '

' + hint + '

' : ''} +
`; + } + + function readBinding(prefix) { + return { + from: (document.getElementById(prefix + '-from') || {}).value || 'previous', + field: (document.getElementById(prefix + '-field') || {}).value || 'output' + }; + } + function defaultGraph() { return { nodes: [], edges: [], config: {} }; } @@ -66,15 +105,15 @@ case 'tool': return { tool_name: '', arguments: '{}', timeout_seconds: '' }; case 'agent': - return { agent_mode: 'eino_single', input_source: '{{previous.output}}', instruction: '', output_key: 'agent_result' }; + return { agent_mode: 'eino_single', input_binding: { from: 'previous', field: 'output' }, instruction: '', output_key: 'agent_result' }; case 'condition': return { expression: '{{previous.output}} != ""' }; case 'hitl': - return { prompt: _t('workflows.defaultHitlPrompt'), reviewer: 'human' }; + return { prompt: _t('workflows.defaultHitlPrompt'), prompt_binding: { from: 'previous', field: 'output' }, reviewer: 'human' }; case 'output': - return { output_key: 'result', source: '{{previous.output}}' }; + return { output_key: 'result', source_binding: { from: 'previous', field: 'output' } }; case 'end': - return { result_template: '{{outputs.result}}' }; + return { result_binding: { from: 'outputs', field: 'result' } }; default: return {}; } @@ -453,7 +492,7 @@ ${workflowToolOptions.map(tool => ``).join('')} - ${typedTextarea('workflow-tool-arguments', _t('workflows.config.argumentsTemplate'), cfg.arguments, '{"target":"{{inputs.target}}"}')} + ${typedTextarea('workflow-tool-arguments', _t('workflows.config.argumentsStatic'), cfg.arguments, '{"target":"example.com"}')} ${typedField('workflow-tool-timeout', _t('workflows.config.timeoutSeconds'), cfg.timeout_seconds, _t('workflows.config.optional'))} `; if (!workflowToolsLoaded) { @@ -470,7 +509,7 @@ ${AGENT_MODES.map(mode => ``).join('')} - ${typedField('workflow-agent-input-source', _t('workflows.config.inputSource'), cfg.input_source, '{{previous.output}}')} + ${bindingFieldHtml('workflow-agent-input', 'workflows.config.inputBinding', bindingFromConfig(cfg, 'input_binding', 'previous', 'output'), 'workflows.config.inputBindingHint')} ${typedTextarea('workflow-agent-instruction', _t('workflows.config.nodeInstruction'), cfg.instruction, _t('workflows.config.instructionPlaceholder'))} ${typedField('workflow-agent-output-key', _t('workflows.config.outputKey'), cfg.output_key, 'agent_result')} `; @@ -484,6 +523,8 @@ case 'hitl': wrap.innerHTML = ` ${typedTextarea('workflow-hitl-prompt', _t('workflows.config.hitlPrompt'), cfg.prompt, _t('workflows.config.hitlPromptPlaceholder'))} + ${bindingFieldHtml('workflow-hitl-prompt-binding', 'workflows.config.promptBinding', bindingFromConfig(cfg, 'prompt_binding', 'previous', 'output'), 'workflows.config.promptBindingHint')} +

${_t('workflows.config.hitlInteractiveHint')}