function hitlModeNormalize(m) { let v = String(m || '').trim().toLowerCase().replace(/-/g, '_'); if (v === 'feedback' || v === 'followup') { v = 'approval'; } const allowed = ['off', 'approval', 'review_edit']; return allowed.indexOf(v) >= 0 ? v : 'off'; } function hitlEffectiveEnabled(cfg) { if (!cfg) return false; if (cfg.enabled === true) return true; return hitlModeNormalize(cfg.mode) !== 'off'; } function readHitlLocalStorageConv(conversationId) { if (!conversationId) return null; try { const key = 'cyberstrike-chat-hitl:' + String(conversationId).trim(); const raw = localStorage.getItem(key); if (!raw) return null; const parsed = JSON.parse(raw); if (!parsed || typeof parsed !== 'object') return null; return parsed; } catch (e) { return null; } } function hitlSensitiveToolsToArray(config) { if (Array.isArray(config && config.sensitiveTools)) return config.sensitiveTools; const s = config && config.sensitiveTools; if (typeof s === 'string') { return s.split(/[,\n\r]+/).map(function (x) { return x.trim(); }).filter(Boolean); } return []; } function getCurrentConversationIdForHitl() { if (typeof window.currentConversationId === 'string' && window.currentConversationId) { return window.currentConversationId; } const active = document.querySelector('.conversation-item.active'); if (active && active.dataset && active.dataset.conversationId) { return active.dataset.conversationId; } return ''; } async function fetchHitlConversationConfig(conversationId) { if (!conversationId) return null; const resp = await hitlApiFetch('/api/hitl/config/' + encodeURIComponent(conversationId), { credentials: 'same-origin' }); if (!resp.ok) return null; const data = await resp.json(); if (!data || !data.hitl) return null; return { hitl: data.hitl, hitlGlobalToolWhitelist: Array.isArray(data.hitlGlobalToolWhitelist) ? data.hitlGlobalToolWhitelist : [] }; } /** 无会话时:将免审批工具合并进服务端 config.yaml,返回更新后的全局白名单数组 */ async function mergeHitlGlobalToolWhitelist(sensitiveTools) { const list = Array.isArray(sensitiveTools) ? sensitiveTools : []; const resp = await hitlApiFetch('/api/hitl/tool-whitelist', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sensitiveTools: list }) }); if (!resp.ok) { const msg = await readHitlApiError(resp); throw new Error(msg || ('HTTP ' + resp.status)); } const data = await resp.json(); if (data && Array.isArray(data.hitlGlobalToolWhitelist)) { return data.hitlGlobalToolWhitelist; } return []; } async function saveHitlConversationConfig(conversationId, config) { if (!conversationId || !config) return false; const mode = hitlModeNormalize(config.mode || 'off'); const enabled = typeof config.enabled === 'boolean' ? config.enabled : (mode !== 'off'); const sensitiveTools = hitlSensitiveToolsToArray(config); const resp = await hitlApiFetch('/api/hitl/config', { method: 'PUT', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ conversationId: conversationId, enabled: enabled, mode: mode, sensitiveTools: sensitiveTools, timeoutSeconds: config.timeoutSeconds || 300 }) }); if (!resp.ok) { const msg = await readHitlApiError(resp); throw new Error(msg || ('HTTP ' + resp.status)); } return true; } async function syncHitlConfigFromServer(conversationId) { const pack = await fetchHitlConversationConfig(conversationId); if (!pack || !pack.hitl) return; const cfg = pack.hitl; const globalWL = pack.hitlGlobalToolWhitelist || []; if (typeof window !== 'undefined') { window.csaiHitlGlobalToolWhitelist = globalWL; } const strip = typeof window.hitlStripGlobalToolsFromFormString === 'function' ? window.hitlStripGlobalToolsFromFormString : function (_g, s) { return typeof s === 'string' ? s.trim() : ''; }; let merged = cfg; if (!hitlEffectiveEnabled(cfg)) { const local = readHitlLocalStorageConv(conversationId); const localMode = local && local.mode ? hitlModeNormalize(local.mode) : 'off'; if (localMode !== 'off') { let localToolsStr = typeof local.sensitiveTools === 'string' ? local.sensitiveTools : ''; localToolsStr = strip(globalWL, localToolsStr); merged = { enabled: true, mode: localMode, sensitiveTools: localToolsStr.split(/[,\n\r]+/).map(function (s) { return s.trim(); }).filter(Boolean), timeoutSeconds: cfg.timeoutSeconds || 300 }; saveHitlConversationConfig(conversationId, { mode: localMode, sensitiveTools: localToolsStr, enabled: true, timeoutSeconds: merged.timeoutSeconds }).catch(function (err) { console.warn('HITL 会话配置同步到服务器失败(将仅保留本地 UI):', err); }); } else { const gl = typeof window.getHitlLastGlobalConfig === 'function' ? window.getHitlLastGlobalConfig() : null; const glMode = gl && gl.mode ? hitlModeNormalize(gl.mode) : 'off'; if (glMode !== 'off') { let glToolsStr = typeof gl.sensitiveTools === 'string' ? gl.sensitiveTools : ''; glToolsStr = strip(globalWL, glToolsStr); merged = { enabled: true, mode: glMode, sensitiveTools: glToolsStr.split(/[,\n\r]+/).map(function (s) { return s.trim(); }).filter(Boolean), timeoutSeconds: cfg.timeoutSeconds || 300 }; saveHitlConversationConfig(conversationId, { mode: glMode, sensitiveTools: glToolsStr, enabled: true, timeoutSeconds: merged.timeoutSeconds }).catch(function (err) { console.warn('HITL 会话配置同步到服务器失败(将仅保留本地 UI):', err); }); } } } const uiMode = hitlEffectiveEnabled(merged) ? hitlModeNormalize(merged.mode) : 'off'; const rawArr = Array.isArray(merged.sensitiveTools) ? merged.sensitiveTools : hitlSensitiveToolsToArray({ sensitiveTools: merged.sensitiveTools }); const sessionOnlyStr = strip(globalWL, rawArr.join(', ')); const normalizedCfg = Object.assign({}, merged, { mode: uiMode, sensitiveTools: sessionOnlyStr }); if (typeof window.saveHitlConfigForConversation === 'function') { window.saveHitlConfigForConversation(conversationId, normalizedCfg); } else { try { localStorage.setItem('chat_hitl_config_' + conversationId, JSON.stringify(normalizedCfg)); } catch (e) {} } if (typeof window.applyHitlConfigToUI === 'function') { window.applyHitlConfigToUI(normalizedCfg); } reconcileHitlUiState(); } async function syncHitlConfigToServerByCurrentConversation() { const conversationId = getCurrentConversationIdForHitl(); if (!conversationId) return; if (typeof window.readHitlConfigFromForm !== 'function') return; const cfg = window.readHitlConfigFromForm(); await saveHitlConversationConfig(conversationId, cfg); } function reconcileHitlUiState() { if (typeof window.readHitlConfigFromForm === 'function' && typeof window.updateHitlStatusUI === 'function') { try { const cfg = window.readHitlConfigFromForm(); window.updateHitlStatusUI(cfg); } catch (e) {} } } let hitlFollowRunSeq = 0; /** * 审批提交后原 SSE 已断开:轮询任务列表,运行中则拉取过程详情;任务结束后再整页加载会话以对齐终态。 */ async function followAgentRunAfterHitlDecision(conversationId) { if (!conversationId || typeof apiFetch !== 'function') return; if (typeof window.attachRunningTaskEventStream === 'function') { try { const attached = await window.attachRunningTaskEventStream(conversationId); if (attached) return; } catch (e) { console.warn('attachRunningTaskEventStream', e); } } var mySeq = ++hitlFollowRunSeq; var intervalMs = 2000; var firstDelayMs = 500; var maxMs = 30 * 60 * 1000; var deadline = Date.now() + maxMs; function taskStillActive(cid) { return apiFetch('/api/agent-loop/tasks').then(function (r) { if (!r.ok) return false; return r.json().then(function (j) { var tasks = (j && j.tasks) ? j.tasks : []; return tasks.some(function (t) { return t && t.conversationId === cid && (t.status === 'running' || t.status === 'cancelling'); }); }); }).catch(function () { return false; }); } await new Promise(function (r) { setTimeout(r, firstDelayMs); }); while (mySeq === hitlFollowRunSeq) { if (Date.now() > deadline) { if (typeof window.loadConversation === 'function' && window.currentConversationId === conversationId) { await window.loadConversation(conversationId); } if (typeof loadActiveTasks === 'function') loadActiveTasks(); return; } try { var active = await taskStillActive(conversationId); var onThisConv = (typeof window.currentConversationId === 'string' && window.currentConversationId === conversationId); if (onThisConv && typeof window.refreshLastAssistantProcessDetails === 'function') { await window.refreshLastAssistantProcessDetails(conversationId); } if (!active) { await new Promise(function (r) { setTimeout(r, 450); }); if (typeof window.loadConversation === 'function' && window.currentConversationId === conversationId) { await window.loadConversation(conversationId); } if (typeof loadActiveTasks === 'function') loadActiveTasks(); return; } } catch (e) { console.warn('followAgentRunAfterHitlDecision', e); } await new Promise(function (r) { setTimeout(r, intervalMs); }); } } async function refreshHitlPending() { const container = document.getElementById('hitl-pending-list'); if (!container) return; container.innerHTML = '
' + escapeHtml(preview) + '' + (allowEdit ? ('