From f02c0d175b5cd86cfcb00eb3019b561f4c279710 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 20:23:46 +0800 Subject: [PATCH] Add files via upload --- web/static/js/chat.js | 9 +- web/static/js/hitl.js | 123 +++++++++++++++++++++++++--- web/static/js/monitor.js | 172 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 293 insertions(+), 11 deletions(-) diff --git a/web/static/js/chat.js b/web/static/js/chat.js index 6b5dea10..9f7b1c40 100644 --- a/web/static/js/chat.js +++ b/web/static/js/chat.js @@ -2707,16 +2707,23 @@ function finishProcessDetailsRender(messageElement, processDetails, isLazyNotLoa } const hasPendingHitlInDetails = processDetails.some(d => d && d.eventType === 'hitl_interrupt'); + const hasPendingWorkflowHitl = processDetails.some(d => d && d.eventType === 'workflow_hitl_waiting'); const hasErrorOrCancelled = processDetails.some(d => d.eventType === 'error' || d.eventType === 'cancelled' ); - if (hasErrorOrCancelled && !hasPendingHitlInDetails) { + if (hasErrorOrCancelled && !hasPendingHitlInDetails && !hasPendingWorkflowHitl) { timeline.classList.remove('expanded'); const processDetailBtn = messageElement.querySelector('.process-detail-btn'); if (processDetailBtn) { processDetailBtn.innerHTML = '' + (typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情') + ''; } } + if (hasPendingWorkflowHitl && messageElement && messageElement.id) { + const convId = typeof window.currentConversationId === 'string' ? window.currentConversationId : ''; + if (convId && typeof window.restoreWorkflowHitlInlineForConversation === 'function') { + window.restoreWorkflowHitlInlineForConversation(convId); + } + } } /** 懒加载折叠态:后台拉摘要,提示迭代规模而不加载全量详情 */ diff --git a/web/static/js/hitl.js b/web/static/js/hitl.js index b52ee233..c61b3d4b 100644 --- a/web/static/js/hitl.js +++ b/web/static/js/hitl.js @@ -678,14 +678,9 @@ async function followAgentRunAfterHitlDecision(conversationId) { } function renderHitlPendingList(items) { - const container = document.getElementById('hitl-pending-list'); - if (!container) return; const list = Array.isArray(items) ? items : []; - if (!list.length) { - container.innerHTML = '
' + escapeHtml(hitlT('emptyState', 'No pending approvals')) + '
'; - return; - } - container.innerHTML = list.map(function (item) { + if (!list.length) return ''; + return list.map(function (item) { const payloadObj = hitlParsePayloadObject(item.payload || ''); const payload = String(item.payload || ''); const contextHtml = hitlRenderContextBlocks(payloadObj); @@ -722,6 +717,86 @@ function renderHitlPendingList(items) { }).join(''); } +function hitlWorkflowPendingLabel(run) { + const pending = hitlParsePayloadObject(run.pending_hitl_json || run.pendingHitlJson || ''); + const pendingHitl = pending.pendingHitl && typeof pending.pendingHitl === 'object' ? pending.pendingHitl : pending; + return pendingHitl.label || pendingHitl.nodeId || run.pending_hitl_node_id || run.pendingHitlNodeId || run.workflow_id || run.workflowId || run.id || '-'; +} + +function renderWorkflowHitlPendingList(runs) { + const list = Array.isArray(runs) ? runs : []; + if (!list.length) return ''; + return list.map(function (run) { + const runId = String(run.id || '').trim(); + const pending = hitlParsePayloadObject(run.pending_hitl_json || run.pendingHitlJson || ''); + const pendingHitl = pending.pendingHitl && typeof pending.pendingHitl === 'object' ? pending.pendingHitl : pending; + const label = hitlWorkflowPendingLabel(run); + const prompt = String(pendingHitl.prompt || '').trim(); + const convId = String(run.conversation_id || run.conversationId || '').trim(); + const qRun = JSON.stringify(runId).replace(/"/g, '"'); + const qConv = JSON.stringify(convId).replace(/"/g, '"'); + const workflowLabel = hitlT('workflowPendingTitle', 'Workflow approval'); + const openChatLabel = hitlT('openConversation', 'Open conversation'); + return ( + '
' + + '
' + + '
' + + '' + escapeHtml(workflowLabel) + '' + + '' + escapeHtml(label) + '' + + '
' + + '
' + + '
' + escapeHtml(hitlT('conversationLabel', 'Conversation:')) + ' ' + escapeHtml(convId || '-') + '
' + + (prompt ? ('
' + escapeHtml(prompt) + '
') : '') + + '
' + escapeHtml(hitlT('commentHelp', 'Comment (optional): briefly note the approval reason.')) + '
' + + '' + + '
' + + (convId ? ('') : '') + + '' + + '' + + '
' + + '
' + ); + }).join(''); +} + +async function submitWorkflowHitlDecisionFromPage(runId, approved, conversationId) { + const rid = String(runId || '').trim(); + if (!rid) return; + const commentEl = document.getElementById('workflow-hitl-comment-' + rid); + const comment = commentEl ? String(commentEl.value || '').trim() : ''; + try { + if (typeof window.submitWorkflowHitlDecision === 'function') { + await window.submitWorkflowHitlDecision(rid, approved, comment); + } else { + const resp = await hitlApiFetch('/api/workflows/runs/' + encodeURIComponent(rid) + '/resume', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'same-origin', + body: JSON.stringify({ approved: !!approved, comment: comment }) + }); + const body = await resp.json().catch(function () { return {}; }); + if (!resp.ok) throw new Error((body && body.error) ? body.error : 'submit failed'); + } + if (conversationId && typeof followAgentRunAfterHitlDecision === 'function') { + await followAgentRunAfterHitlDecision(conversationId); + } + await refreshHitlPending(); + } catch (e) { + alert((e && e.message) ? e.message : hitlT('submitFailed', 'Submit failed')); + } +} + +function openHitlConversation(conversationId) { + const cid = String(conversationId || '').trim(); + if (!cid) return; + if (typeof switchPage === 'function') { + switchPage('chat'); + } + if (typeof loadConversation === 'function') { + loadConversation(cid); + } +} + async function refreshHitlPending() { const container = document.getElementById('hitl-pending-list'); if (!container) return; @@ -739,7 +814,27 @@ async function refreshHitlPending() { } const data = await resp.json(); const items = Array.isArray(data.items) ? data.items : []; - hitlPendingTotal = typeof data.total === 'number' ? data.total : items.length; + let workflowRuns = []; + try { + const wfResp = await hitlApiFetch('/api/workflows/runs/pending', { credentials: 'same-origin' }); + if (wfResp.ok) { + const wfData = await wfResp.json().catch(function () { return {}; }); + workflowRuns = Array.isArray(wfData.runs) ? wfData.runs : []; + } + } catch (wfErr) { + console.warn('fetch workflow pending runs failed', wfErr); + } + const searchQ = q && q.value.trim() ? q.value.trim().toLowerCase() : ''; + if (searchQ) { + workflowRuns = workflowRuns.filter(function (run) { + const conv = String(run.conversation_id || run.conversationId || '').toLowerCase(); + const wfId = String(run.workflow_id || run.workflowId || '').toLowerCase(); + const runId = String(run.id || '').toLowerCase(); + const label = hitlWorkflowPendingLabel(run).toLowerCase(); + return conv.indexOf(searchQ) >= 0 || wfId.indexOf(searchQ) >= 0 || runId.indexOf(searchQ) >= 0 || label.indexOf(searchQ) >= 0; + }); + } + hitlPendingTotal = (typeof data.total === 'number' ? data.total : items.length) + workflowRuns.length; const maxPage = Math.max(1, Math.ceil(hitlPendingTotal / hitlPendingPageSize)); if (hitlPendingPage > maxPage) { hitlPendingPage = maxPage; @@ -753,7 +848,13 @@ async function refreshHitlPending() { } hitlPendingCache = items; hitlPendingLoaded = true; - renderHitlPendingList(items); + const workflowHtml = renderWorkflowHitlPendingList(workflowRuns); + const toolHtml = items.length ? renderHitlPendingList(items) : ''; + if (!workflowHtml && !toolHtml) { + container.innerHTML = '
' + escapeHtml(hitlT('emptyState', 'No pending approvals')) + '
'; + } else { + container.innerHTML = workflowHtml + (workflowHtml && toolHtml ? '
' : '') + (toolHtml || ''); + } renderHitlPendingPagination(); } catch (e) { hitlPendingLoaded = false; @@ -1325,7 +1426,7 @@ function refreshHitlLogsI18n() { function refreshHitlPendingI18n() { if (!document.getElementById('hitl-pending-list') || !hitlPendingLoaded) return; - renderHitlPendingList(hitlPendingCache); + refreshHitlPending(); } function refreshHitlI18n() { @@ -1501,6 +1602,8 @@ window.onHitlLogsPageSizeChange = onHitlLogsPageSizeChange; window.onHitlPendingPageSizeChange = onHitlPendingPageSizeChange; window.submitHitlDecision = submitHitlDecision; window.submitHitlDecisionWithPayload = submitHitlDecisionWithPayload; +window.submitWorkflowHitlDecisionFromPage = submitWorkflowHitlDecisionFromPage; +window.openHitlConversation = openHitlConversation; window.dismissHitlItem = dismissHitlItem; window.followAgentRunAfterHitlDecision = followAgentRunAfterHitlDecision; diff --git a/web/static/js/monitor.js b/web/static/js/monitor.js index 425a1263..56b84360 100644 --- a/web/static/js/monitor.js +++ b/web/static/js/monitor.js @@ -1921,6 +1921,24 @@ function handleStreamEvent(event, progressElement, progressId, break; } + case 'workflow_hitl_resumed': { + addTimelineItem(timeline, 'workflow_hitl_resumed', { + title: '✅ 审批已通过', + message: event.message || '人工审批已通过,继续执行', + data: event.data || {} + }); + break; + } + + case 'workflow_hitl_rejected': { + addTimelineItem(timeline, 'workflow_hitl_rejected', { + title: '❌ 审批已拒绝', + message: event.message || '', + data: event.data || {} + }); + break; + } + case 'workflow_paused': { addTimelineItem(timeline, 'workflow_paused', { title: '⏸️ 工作流已暂停', @@ -2687,6 +2705,16 @@ function handleStreamEvent(event, progressElement, progressId, break; case 'done': + if (event.data && event.data.workflowStatus === 'awaiting_hitl') { + const waitingTitle = document.querySelector(`#${progressId} .progress-title`); + if (waitingTitle) { + waitingTitle.textContent = '⏸️ ' + (typeof window.t === 'function' ? window.t('chat.workflowAwaitingApproval') : '工作流等待审批'); + } + if (progressTaskState.has(progressId)) { + finalizeProgressTask(progressId, typeof window.t === 'function' ? window.t('chat.workflowAwaitingApproval') : '等待审批'); + } + break; + } // 清理流式输出状态 responseStreamStateByProgressId.delete(progressId); mainIterationStateByProgressId.delete(String(progressId)); @@ -2911,6 +2939,11 @@ function renderInlineWorkflowHitlApproval(itemId, data) { setBusy(false); return; } + if (body && body.streamResuming) { + statusEl.textContent = approved ? '已通过,工作流继续执行中…' : '已拒绝'; + panel.classList.add('hitl-inline-done'); + return; + } statusEl.textContent = approved ? '已通过,工作流继续执行' : '已拒绝'; panel.classList.add('hitl-inline-done'); } catch (e) { @@ -2923,6 +2956,136 @@ function renderInlineWorkflowHitlApproval(itemId, data) { rejectBtn.onclick = function () { submit(false); }; } +function parseWorkflowHitlPendingJSON(raw) { + if (!raw) return {}; + if (typeof raw === 'object') return raw; + try { + const o = JSON.parse(String(raw)); + return o && typeof o === 'object' ? o : {}; + } catch (e) { + return {}; + } +} + +function workflowHitlDataFromRun(run) { + if (!run) return null; + const runId = run.id || run.workflowRunId || run.workflow_run_id; + if (!runId) return null; + const pending = parseWorkflowHitlPendingJSON(run.pending_hitl_json || run.pendingHitlJson || run.pendingHitlJSON); + const pendingHitl = pending.pendingHitl && typeof pending.pendingHitl === 'object' ? pending.pendingHitl : pending; + return { + workflowRunId: String(runId), + nodeId: pendingHitl.nodeId || run.pending_hitl_node_id || run.pendingHitlNodeId || '', + label: pendingHitl.label || pendingHitl.nodeId || run.pending_hitl_node_id || run.pendingHitlNodeId || runId, + prompt: pendingHitl.prompt || '', + conversationId: run.conversation_id || run.conversationId || '' + }; +} + +function findWorkflowHitlTimelineItem(detailsContainer, runId) { + if (!detailsContainer || !runId) return null; + const rid = String(runId).trim(); + const byRun = detailsContainer.querySelector('[data-workflow-run-id="' + hitlEscapeAttrSelector(rid) + '"]'); + if (byRun) return byRun; + const items = detailsContainer.querySelectorAll('.timeline-item-workflow_hitl_waiting'); + for (let i = items.length - 1; i >= 0; i--) { + const el = items[i]; + if (!el.querySelector('.workflow-hitl-inline-approval.hitl-inline-done')) { + return el; + } + } + return items.length ? items[items.length - 1] : null; +} + +/** + * 刷新或切换会话后:根据 workflow_runs(awaiting_hitl) 恢复工作流内联审批入口。 + */ +async function restoreWorkflowHitlInlineForConversation(conversationId) { + if (!conversationId || typeof apiFetch !== 'function') return; + if (typeof window.currentConversationId === 'string' && window.currentConversationId !== conversationId) { + return; + } + try { + const resp = await apiFetch('/api/workflows/runs/pending?conversationId=' + encodeURIComponent(conversationId)); + if (!resp.ok) return; + const data = await resp.json().catch(function () { return {}; }); + const runs = Array.isArray(data.runs) ? data.runs : []; + if (!runs.length) return; + + let msgEl = document.querySelector('#chat-messages [data-backend-message-id]'); + const nodes = document.querySelectorAll('#chat-messages .message.assistant'); + for (let i = nodes.length - 1; i >= 0; i--) { + if (nodes[i] && nodes[i].dataset && nodes[i].dataset.backendMessageId) { + msgEl = nodes[i]; + break; + } + } + if (!msgEl || !msgEl.id) return; + const clientMsgId = msgEl.id; + const backendMsgId = msgEl.dataset.backendMessageId; + const detailsContainer = document.getElementById('process-details-' + clientMsgId); + if (!detailsContainer) return; + + if (detailsContainer.dataset.lazyNotLoaded === '1' && detailsContainer.dataset.loaded !== '1') { + try { + detailsContainer.dataset.loading = '1'; + if (typeof loadProcessDetailsPaginated === 'function') { + await loadProcessDetailsPaginated(clientMsgId, backendMsgId); + } else if (typeof apiFetch === 'function' && backendMsgId) { + const res = await apiFetch('/api/messages/' + encodeURIComponent(backendMsgId) + '/process-details'); + const j = await res.json().catch(function () { return {}; }); + if (res.ok && typeof renderProcessDetails === 'function') { + renderProcessDetails(clientMsgId, (j && Array.isArray(j.processDetails)) ? j.processDetails : []); + } + } + } catch (e) { + console.error('加载过程详情失败(工作流 HITL 恢复):', e); + } finally { + detailsContainer.dataset.loading = '0'; + } + } + + expandProcessDetailsTimeline(clientMsgId); + + for (let i = 0; i < runs.length; i++) { + const hitlData = workflowHitlDataFromRun(runs[i]); + if (!hitlData) continue; + let hitlItemEl = findWorkflowHitlTimelineItem(detailsContainer, hitlData.workflowRunId); + if (!hitlItemEl) { + const timeline = detailsContainer.querySelector('.progress-timeline'); + if (timeline && typeof addTimelineItem === 'function') { + const itemId = addTimelineItem(timeline, 'workflow_hitl_waiting', { + title: '🧑‍⚖️ 工作流等待审批', + message: hitlData.label || '', + data: hitlData + }); + hitlItemEl = document.getElementById(itemId); + } + } + if (hitlItemEl && hitlItemEl.id) { + renderInlineWorkflowHitlApproval(hitlItemEl.id, hitlData); + } + } + } catch (e) { + console.error('restoreWorkflowHitlInlineForConversation failed', e); + } +} + +window.restoreWorkflowHitlInlineForConversation = restoreWorkflowHitlInlineForConversation; +window.submitWorkflowHitlDecision = async function submitWorkflowHitlDecision(runId, approved, comment) { + const fetchFn = typeof apiFetch === 'function' ? apiFetch : fetch; + const response = await fetchFn('/api/workflows/runs/' + encodeURIComponent(String(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) { + throw new Error((body && body.error) ? body.error : '提交失败'); + } + return body; +}; + function hitlEscapeAttrSelector(val) { const s = String(val); if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') { @@ -3046,6 +3209,9 @@ async function restoreHitlInlineForConversation(conversationId) { if (!hitlItemEl) continue; renderInlineHitlApproval(hitlItemEl.id, hitlData); } + if (typeof restoreWorkflowHitlInlineForConversation === 'function') { + await restoreWorkflowHitlInlineForConversation(conversationId); + } } catch (e) { console.error('restoreHitlInlineForConversation failed', e); } @@ -3542,6 +3708,12 @@ function addTimelineItem(timeline, type, options) { if (type === 'hitl_interrupt' && options.data && options.data.interruptId != null && String(options.data.interruptId).trim() !== '') { item.dataset.hitlInterruptId = String(options.data.interruptId).trim(); } + if (type === 'workflow_hitl_waiting' && options.data) { + const runId = options.data.workflowRunId || options.data.workflow_run_id; + if (runId != null && String(runId).trim() !== '') { + item.dataset.workflowRunId = String(runId).trim(); + } + } if (type === 'tool_result' && options.data) { const d = options.data; item.dataset.toolName = (d.toolName != null && d.toolName !== '') ? String(d.toolName) : '';