Add files via upload

This commit is contained in:
公明
2026-07-03 19:33:10 +08:00
committed by GitHub
parent 58be62fa24
commit a66b8fc821
5 changed files with 170 additions and 11 deletions
+11
View File
@@ -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",
+11
View File
@@ -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": "请审批该步骤是否继续执行",
+4
View File
@@ -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') {
+89
View File
@@ -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 = `
<div class="hitl-input-help"><strong>${escapeHtml(label)}</strong> </div>
${prompt ? `<div class="hitl-input-help">${escapeHtml(prompt)}</div>` : ''}
<div class="hitl-input-help">备注可选</div>
<input class="hitl-config-input workflow-hitl-inline-comment" type="text" placeholder="审批意见">
<div class="hitl-pending-actions">
<button class="btn-secondary workflow-hitl-inline-reject">拒绝</button>
<button class="btn-primary workflow-hitl-inline-approve">通过</button>
</div>
<div class="hitl-input-help workflow-hitl-inline-status"></div>
`;
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') {
+55 -11
View File
@@ -55,6 +55,45 @@
.replace(/'/g, '&#39;');
}
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 =>
`<option value="${esc(v)}" ${v === from ? 'selected' : ''}>${esc(v)}</option>`
).join('');
return `
<div class="form-group">
<label>${esc(label)}</label>
<div class="workflow-binding-row" style="display:flex;gap:8px;">
<select id="${prefix}-from" onchange="updateWorkflowTypedConfig()" style="flex:1;">${options}</select>
<input type="text" id="${prefix}-field" value="${esc(field)}" placeholder="output" oninput="updateWorkflowTypedConfig()" style="flex:1;">
</div>
${hint ? '<p class="workflow-config-hint">' + hint + '</p>' : ''}
</div>`;
}
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 => `<option value="${esc(tool.key)}" ${tool.key === cfg.tool_name ? 'selected' : ''}>${esc(tool.key)}${tool.enabled ? '' : esc(_t('workflows.config.toolDisabled'))}</option>`).join('')}
</select>
</div>
${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 => `<option value="${mode}" ${mode === cfg.agent_mode ? 'selected' : ''}>${mode}</option>`).join('')}
</select>
</div>
${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')}
<p class="workflow-config-hint">${_t('workflows.config.hitlInteractiveHint')}</p>
<div class="form-group">
<label for="workflow-hitl-reviewer">${esc(_t('workflows.config.hitlReviewer'))}</label>
<select id="workflow-hitl-reviewer" onchange="updateWorkflowTypedConfig()">
@@ -496,11 +537,12 @@
case 'output':
wrap.innerHTML = `
${typedField('workflow-output-key', _t('workflows.config.outputKey'), cfg.output_key, 'result')}
${typedField('workflow-output-source', _t('workflows.config.outputSource'), cfg.source, '{{previous.output}}')}
${bindingFieldHtml('workflow-output-source', 'workflows.config.sourceBinding', bindingFromConfig(cfg, 'source_binding', 'previous', 'output'), 'workflows.config.sourceBindingHint')}
${typedField('workflow-output-static', _t('workflows.config.staticValue'), cfg.static_value || '', _t('workflows.config.optional'))}
`;
break;
case 'end':
wrap.innerHTML = typedTextarea('workflow-end-template', _t('workflows.config.endTemplate'), cfg.result_template, '{{outputs.result}}');
wrap.innerHTML = bindingFieldHtml('workflow-end-result', 'workflows.config.resultBinding', bindingFromConfig(cfg, 'result_binding', 'outputs', 'result'), 'workflows.config.resultBindingHint');
break;
default:
wrap.innerHTML = '';
@@ -552,7 +594,7 @@
case 'agent':
return {
agent_mode: (document.getElementById('workflow-agent-mode') || {}).value || 'eino_single',
input_source: (document.getElementById('workflow-agent-input-source') || {}).value || '{{previous.output}}',
input_binding: readBinding('workflow-agent-input'),
instruction: (document.getElementById('workflow-agent-instruction') || {}).value || '',
output_key: (document.getElementById('workflow-agent-output-key') || {}).value || 'agent_result'
};
@@ -561,15 +603,17 @@
case 'hitl':
return {
prompt: (document.getElementById('workflow-hitl-prompt') || {}).value || '',
prompt_binding: readBinding('workflow-hitl-prompt-binding'),
reviewer: (document.getElementById('workflow-hitl-reviewer') || {}).value || 'human'
};
case 'output':
return {
output_key: (document.getElementById('workflow-output-key') || {}).value || 'result',
source: (document.getElementById('workflow-output-source') || {}).value || ''
source_binding: readBinding('workflow-output-source'),
static_value: (document.getElementById('workflow-output-static') || {}).value || ''
};
case 'end':
return { result_template: (document.getElementById('workflow-end-template') || {}).value || '' };
return { result_binding: readBinding('workflow-end-result') };
default:
return {};
}