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 `
+ `;
+ }
+
+ 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')}