// 仪表盘页面:拉取运行中任务、漏洞统计、批量任务、工具与 Skills 统计并渲染 async function refreshDashboard() { const runningEl = document.getElementById('dashboard-running-tasks'); const vulnTotalEl = document.getElementById('dashboard-vuln-total'); const severityIds = ['critical', 'high', 'medium', 'low', 'info']; if (runningEl) runningEl.textContent = '…'; if (vulnTotalEl) vulnTotalEl.textContent = '…'; severityIds.forEach(s => { const el = document.getElementById('dashboard-severity-' + s); if (el) el.textContent = '0'; const barEl = document.getElementById('dashboard-bar-' + s); if (barEl) barEl.style.width = '0%'; }); setDashboardOverviewPlaceholder('…'); setEl('dashboard-kpi-tools-calls', '…'); setEl('dashboard-kpi-success-rate', '…'); var chartPlaceholder = document.getElementById('dashboard-tools-pie-placeholder'); if (chartPlaceholder) { chartPlaceholder.style.removeProperty('display'); chartPlaceholder.textContent = (typeof window.t === 'function' ? window.t('common.loading') : '加载中…'); } var barChartEl = document.getElementById('dashboard-tools-bar-chart'); if (barChartEl) { barChartEl.style.display = 'none'; barChartEl.innerHTML = ''; } if (typeof apiFetch === 'undefined') { if (runningEl) runningEl.textContent = '-'; if (vulnTotalEl) vulnTotalEl.textContent = '-'; setDashboardOverviewPlaceholder('-'); return; } try { const [tasksRes, vulnRes, batchRes, monitorRes, knowledgeRes, skillsRes] = await Promise.all([ apiFetch('/api/agent-loop/tasks').then(r => r.ok ? r.json() : null).catch(() => null), apiFetch('/api/vulnerabilities/stats').then(r => r.ok ? r.json() : null).catch(() => null), apiFetch('/api/batch-tasks?limit=500&page=1').then(r => r.ok ? r.json() : null).catch(() => null), apiFetch('/api/monitor/stats').then(r => r.ok ? r.json() : null).catch(() => null), apiFetch('/api/knowledge/stats').then(r => r.ok ? r.json() : null).catch(() => null), apiFetch('/api/skills/stats').then(r => r.ok ? r.json() : null).catch(() => null) ]); // 运行中任务:Agent 循环任务 + 批量队列「执行中」数量统一统计,避免顶部 KPI 与运行概览不一致 let agentRunningCount = null; if (tasksRes && Array.isArray(tasksRes.tasks)) { agentRunningCount = tasksRes.tasks.length; } let batchRunningCount = 0; if (batchRes && Array.isArray(batchRes.queues)) { batchRes.queues.forEach(q => { if ((q.status || '').toLowerCase() === 'running') batchRunningCount++; }); } if (runningEl) { if (agentRunningCount !== null) { runningEl.textContent = String(agentRunningCount + batchRunningCount); } else if (batchRes && Array.isArray(batchRes.queues)) { runningEl.textContent = String(batchRunningCount); } else { runningEl.textContent = '-'; } } if (vulnRes && typeof vulnRes.total === 'number') { if (vulnTotalEl) vulnTotalEl.textContent = String(vulnRes.total); const bySeverity = vulnRes.by_severity || {}; const total = vulnRes.total || 0; severityIds.forEach(sev => { const count = bySeverity[sev] || 0; const el = document.getElementById('dashboard-severity-' + sev); if (el) el.textContent = String(count); const barEl = document.getElementById('dashboard-bar-' + sev); if (barEl) barEl.style.width = total > 0 ? (count / total * 100) + '%' : '0%'; }); } else { if (vulnTotalEl) vulnTotalEl.textContent = '-'; severityIds.forEach(sev => { const barEl = document.getElementById('dashboard-bar-' + sev); if (barEl) barEl.style.width = '0%'; }); } // 批量任务队列:按状态统计(优化版;running 与上方 batchRunningCount 一致) if (batchRes && Array.isArray(batchRes.queues)) { const queues = batchRes.queues; let pending = 0, running = batchRunningCount, done = 0; queues.forEach(q => { const s = (q.status || '').toLowerCase(); if (s === 'pending' || s === 'paused') pending++; else if (s === 'running') { /* already counted into batchRunningCount */ } else if (s === 'completed' || s === 'cancelled') done++; }); const total = pending + running + done; setEl('dashboard-batch-pending', String(pending)); setEl('dashboard-batch-running', String(running)); setEl('dashboard-batch-done', String(done)); setEl('dashboard-batch-total', total > 0 ? (typeof window.t === 'function' ? window.t('dashboard.totalCount', { count: total }) : `共 ${total} 个`) : (typeof window.t === 'function' ? window.t('dashboard.noTasks') : '暂无任务')); // 更新进度条 if (total > 0) { const pendingPct = (pending / total * 100).toFixed(1); const runningPct = (running / total * 100).toFixed(1); const donePct = (done / total * 100).toFixed(1); updateProgressBar('dashboard-batch-progress-pending', pendingPct); updateProgressBar('dashboard-batch-progress-running', runningPct); updateProgressBar('dashboard-batch-progress-done', donePct); } else { updateProgressBar('dashboard-batch-progress-pending', '0'); updateProgressBar('dashboard-batch-progress-running', '0'); updateProgressBar('dashboard-batch-progress-done', '0'); } } else { setEl('dashboard-batch-pending', '-'); setEl('dashboard-batch-running', '-'); setEl('dashboard-batch-done', '-'); setEl('dashboard-batch-total', '-'); updateProgressBar('dashboard-batch-progress-pending', '0'); updateProgressBar('dashboard-batch-progress-running', '0'); updateProgressBar('dashboard-batch-progress-done', '0'); } // 工具调用:monitor/stats 为 { toolName: { totalCalls, successCalls, failedCalls, ... } }(优化版) if (monitorRes && typeof monitorRes === 'object') { const names = Object.keys(monitorRes); let totalCalls = 0, totalSuccess = 0, totalFailed = 0; names.forEach(k => { const v = monitorRes[k]; const n = v && (v.totalCalls ?? v.TotalCalls); if (typeof n === 'number') totalCalls += n; const s = v && (v.successCalls ?? v.SuccessCalls); if (typeof s === 'number') totalSuccess += s; const f = v && (v.failedCalls ?? v.FailedCalls); if (typeof f === 'number') totalFailed += f; }); setEl('dashboard-tools-count', String(names.length)); setEl('dashboard-tools-calls', formatNumber(totalCalls)); setEl('dashboard-kpi-tools-calls', String(totalCalls)); var rateStr = totalCalls > 0 ? ((totalSuccess / totalCalls) * 100).toFixed(1) + '%' : '-'; setEl('dashboard-kpi-success-rate', rateStr); setEl('dashboard-tools-success-rate', rateStr !== '-' ? `成功率 ${rateStr}` : '-'); renderDashboardToolsBar(monitorRes); } else { setEl('dashboard-tools-count', '-'); setEl('dashboard-tools-calls', '-'); setEl('dashboard-kpi-tools-calls', '-'); setEl('dashboard-kpi-success-rate', '-'); setEl('dashboard-tools-success-rate', '-'); renderDashboardToolsBar(null); } // 知识:{ enabled, total_categories, total_items, ... }(优化版) const knowledgeItemsEl = document.getElementById('dashboard-knowledge-items'); const knowledgeCategoriesEl = document.getElementById('dashboard-knowledge-categories'); const knowledgeStatusEl = document.getElementById('dashboard-knowledge-status'); if (knowledgeRes && typeof knowledgeRes === 'object') { if (knowledgeRes.enabled === false) { // 功能未启用:用状态标签展示,数值保持为 "-" if (knowledgeStatusEl) knowledgeStatusEl.textContent = (typeof window.t === 'function' ? window.t('dashboard.notEnabled') : '未启用'); if (knowledgeItemsEl) knowledgeItemsEl.textContent = '-'; if (knowledgeCategoriesEl) knowledgeCategoriesEl.textContent = '-'; } else { const categories = knowledgeRes.total_categories ?? 0; const items = knowledgeRes.total_items ?? 0; if (knowledgeItemsEl) knowledgeItemsEl.textContent = formatNumber(items); if (knowledgeCategoriesEl) knowledgeCategoriesEl.textContent = formatNumber(categories); // 根据数据量给个轻量状态文案 if (knowledgeStatusEl) { if (items > 0 || categories > 0) { knowledgeStatusEl.textContent = (typeof window.t === 'function' ? window.t('dashboard.enabled') : '已启用'); } else { knowledgeStatusEl.textContent = (typeof window.t === 'function' ? window.t('dashboard.toConfigure') : '待配置'); } } } } else { if (knowledgeItemsEl) knowledgeItemsEl.textContent = '-'; if (knowledgeCategoriesEl) knowledgeCategoriesEl.textContent = '-'; if (knowledgeStatusEl) knowledgeStatusEl.textContent = '-'; } // Skills:{ total_skills, total_calls, ... }(优化版) if (skillsRes && typeof skillsRes === 'object') { const totalSkills = skillsRes.total_skills ?? 0; const totalCalls = skillsRes.total_calls ?? 0; setEl('dashboard-skills-count', formatNumber(totalSkills)); setEl('dashboard-skills-calls', formatNumber(totalCalls)); // 设置状态标签 const statusEl = document.getElementById('dashboard-skills-status'); if (statusEl) { if (totalCalls === 0) { statusEl.textContent = (typeof window.t === 'function' ? window.t('dashboard.toUse') : '待使用'); statusEl.style.background = 'rgba(0, 0, 0, 0.05)'; statusEl.style.color = 'var(--text-secondary)'; } else if (totalCalls < 10) { statusEl.textContent = (typeof window.t === 'function' ? window.t('dashboard.active') : '活跃'); statusEl.style.background = 'rgba(16, 185, 129, 0.1)'; statusEl.style.color = '#10b981'; } else { statusEl.textContent = (typeof window.t === 'function' ? window.t('dashboard.highFreq') : '高频'); statusEl.style.background = 'rgba(59, 130, 246, 0.1)'; statusEl.style.color = '#3b82f6'; } } } else { setEl('dashboard-skills-count', '-'); setEl('dashboard-skills-calls', '-'); const statusEl = document.getElementById('dashboard-skills-status'); if (statusEl) statusEl.textContent = '-'; } } catch (e) { console.warn('仪表盘拉取统计失败', e); if (runningEl) runningEl.textContent = '-'; if (vulnTotalEl) vulnTotalEl.textContent = '-'; setDashboardOverviewPlaceholder('-'); setEl('dashboard-kpi-success-rate', '-'); setEl('dashboard-kpi-tools-calls', '-'); renderDashboardToolsBar(null); var ph = document.getElementById('dashboard-tools-pie-placeholder'); if (ph) { ph.style.removeProperty('display'); ph.textContent = (typeof window.t === 'function' ? window.t('dashboard.noCallData') : '暂无调用数据'); } } } function setEl(id, text) { const el = document.getElementById(id); if (el) el.textContent = text; } function setDashboardOverviewPlaceholder(t) { ['dashboard-batch-pending', 'dashboard-batch-running', 'dashboard-batch-done', 'dashboard-batch-total', 'dashboard-tools-count', 'dashboard-tools-calls', 'dashboard-tools-success-rate', 'dashboard-skills-count', 'dashboard-skills-calls', 'dashboard-skills-status', 'dashboard-knowledge-items', 'dashboard-knowledge-categories', 'dashboard-knowledge-status'].forEach(id => setEl(id, t)); updateProgressBar('dashboard-batch-progress-pending', '0'); updateProgressBar('dashboard-batch-progress-running', '0'); updateProgressBar('dashboard-batch-progress-done', '0'); } // 格式化数字,添加千位分隔符 function formatNumber(num) { if (typeof num !== 'number' || isNaN(num)) return '-'; if (num === 0) return '0'; return num.toLocaleString('zh-CN'); } // 更新进度条宽度 function updateProgressBar(id, percentage) { const el = document.getElementById(id); if (el) { const pct = parseFloat(percentage) || 0; el.style.width = Math.max(0, Math.min(100, pct)) + '%'; } } // Top 30 工具执行次数柱状图颜色(30 色不重复,柔和、易区分) var DASHBOARD_BAR_COLORS = [ '#93c5fd', '#a78bfa', '#6ee7b7', '#fde047', '#fda4af', '#7dd3fc', '#a5b4fc', '#5eead4', '#fdba74', '#e9d5ff', '#67e8f9', '#c4b5fd', '#86efac', '#fcd34d', '#f9a8d4', '#bae6fd', '#c7d2fe', '#99f6e4', '#fed7aa', '#ddd6fe', '#22d3ee', '#8b5cf6', '#4ade80', '#fbbf24', '#fb7185', '#38bdf8', '#818cf8', '#2dd4bf', '#fb923c', '#e0e7ff' ]; function esc(s) { if (typeof s !== 'string') return ''; return s.replace(/&/g, '&').replace(/ 0; }) .sort(function (a, b) { return b.totalCalls - a.totalCalls; }) .slice(0, 30); if (entries.length === 0) { placeholder.style.removeProperty('display'); placeholder.textContent = (typeof window.t === 'function' ? window.t('dashboard.noCallData') : '暂无调用数据'); barChartEl.style.display = 'none'; barChartEl.innerHTML = ''; return; } placeholder.style.display = 'none'; barChartEl.style.display = 'block'; const maxCalls = Math.max.apply(null, entries.map(function (e) { return e.totalCalls; })); var html = ''; entries.forEach(function (e, i) { var pct = maxCalls > 0 ? (e.totalCalls / maxCalls) * 100 : 0; var label = e.name.length > 12 ? e.name.slice(0, 10) + '…' : e.name; var color = DASHBOARD_BAR_COLORS[i % DASHBOARD_BAR_COLORS.length]; var fullName = esc(e.name); html += '
'; }); barChartEl.innerHTML = html; attachDashboardBarTooltips(barChartEl); } var dashboardBarTooltipEl = null; var dashboardBarTooltipTimer = null; function attachDashboardBarTooltips(barChartEl) { if (!barChartEl) return; if (!dashboardBarTooltipEl) { dashboardBarTooltipEl = document.createElement('div'); dashboardBarTooltipEl.className = 'dashboard-tools-bar-tooltip'; dashboardBarTooltipEl.setAttribute('role', 'tooltip'); document.body.appendChild(dashboardBarTooltipEl); } barChartEl.removeEventListener('mouseover', dashboardBarTooltipOnOver); barChartEl.removeEventListener('mouseout', dashboardBarTooltipOnOut); barChartEl.addEventListener('mouseover', dashboardBarTooltipOnOver); barChartEl.addEventListener('mouseout', dashboardBarTooltipOnOut); } function dashboardBarTooltipOnOver(ev) { var item = ev.target && ev.target.closest && ev.target.closest('.dashboard-tools-bar-item'); if (!item || !dashboardBarTooltipEl) return; var text = item.getAttribute('data-tooltip'); if (!text) return; clearTimeout(dashboardBarTooltipTimer); dashboardBarTooltipTimer = setTimeout(function () { dashboardBarTooltipEl.textContent = text; dashboardBarTooltipEl.style.display = 'block'; requestAnimationFrame(function () { var rect = item.getBoundingClientRect(); var ttRect = dashboardBarTooltipEl.getBoundingClientRect(); var x = rect.left + (rect.width / 2) - (ttRect.width / 2); var y = rect.top - ttRect.height - 6; if (y < 8) y = rect.bottom + 6; var pad = 8; if (x < pad) x = pad; if (x + ttRect.width > window.innerWidth - pad) x = window.innerWidth - ttRect.width - pad; dashboardBarTooltipEl.style.left = x + 'px'; dashboardBarTooltipEl.style.top = y + 'px'; }); }, 180); } function dashboardBarTooltipOnOut(ev) { var item = ev.target && ev.target.closest && ev.target.closest('.dashboard-tools-bar-item'); var related = ev.relatedTarget && ev.relatedTarget.closest && ev.relatedTarget.closest('.dashboard-tools-bar-item'); if (item && item === related) return; clearTimeout(dashboardBarTooltipTimer); dashboardBarTooltipTimer = null; if (dashboardBarTooltipEl) dashboardBarTooltipEl.style.display = 'none'; }