// 漏洞管理相关功能 function vulnT(key, opts) { if (typeof window.t === 'function') { return window.t(key, opts); } return key; } function vulnDateLocale() { try { const lang = (window.__locale || '').toLowerCase(); if (lang.indexOf('zh') === 0) { return 'zh-CN'; } } catch (e) { /* ignore */ } return 'en-US'; } function vulnSeverityLabel(code) { const m = { critical: 'dashboard.severityCritical', high: 'dashboard.severityHigh', medium: 'dashboard.severityMedium', low: 'dashboard.severityLow', info: 'dashboard.severityInfo' }; return m[code] ? vulnT(m[code]) : code; } function vulnStatusLabel(code) { const m = { open: 'vulnerabilityPage.statusOpen', confirmed: 'vulnerabilityPage.statusConfirmed', fixed: 'vulnerabilityPage.statusFixed', false_positive: 'vulnerabilityPage.statusFalsePositive' }; return m[code] ? vulnT(m[code]) : code; } // 从localStorage读取每页显示数量,默认为20 const getVulnerabilityPageSize = () => { const saved = localStorage.getItem('vulnerabilityPageSize'); return saved ? parseInt(saved, 10) : 20; }; let currentVulnerabilityId = null; let vulnerabilityFilters = { id: '', conversation_id: '', task_id: '', conversation_tag: '', task_tag: '', severity: '', status: '' }; let vulnerabilityPagination = { currentPage: 1, pageSize: getVulnerabilityPageSize(), total: 0, totalPages: 1 }; const VULN_STAT_SEVERITIES = ['critical', 'high', 'medium', 'low', 'info']; let vulnerabilityStatCardsBound = false; let vulnerabilityFilterPanelBound = false; let vulnerabilityFilterOptionsCache = null; const VULNERABILITY_ADVANCED_OPEN_KEY = 'vulnerabilityAdvancedFiltersOpen'; const VULNERABILITY_DATALIST_MAX = 8; const VULNERABILITY_DATALIST_MIN_QUERY = 2; const VULN_FILTER_CHIP_FIELDS = [ { key: 'id', labelKey: 'vulnerabilityPage.vulnId' }, { key: 'status', labelKey: null, format: 'status' }, { key: 'severity', labelKey: null, format: 'severity' }, { key: 'conversation_id', labelKey: 'vulnerabilityPage.conversationId' }, { key: 'task_id', labelKey: 'vulnerabilityPage.taskOrQueueId' }, { key: 'conversation_tag', labelKey: 'vulnerabilityPage.conversationTag' }, { key: 'task_tag', labelKey: 'vulnerabilityPage.taskTag' } ]; // 从地址栏 #vulnerabilities?conversation_id= / ?task_id= / ?id= 同步筛选(通知/对话菜单/任务管理联动) function syncVulnerabilityFiltersFromLocationHash() { const hash = window.location.hash.slice(1); const hashParts = hash.split('?'); if (hashParts[0] !== 'vulnerabilities' || hashParts.length < 2) { return; } const params = new URLSearchParams(hashParts.slice(1).join('?')); const vid = (params.get('id') || '').trim(); const cid = (params.get('conversation_id') || '').trim(); const tid = (params.get('task_id') || '').trim(); const sev = (params.get('severity') || '').trim(); const st = (params.get('status') || '').trim(); const convTag = (params.get('conversation_tag') || '').trim(); const taskTag = (params.get('task_tag') || '').trim(); if (!vid && !cid && !tid && !sev && !st && !convTag && !taskTag) { return; } vulnerabilityFilters.id = ''; vulnerabilityFilters.conversation_id = ''; vulnerabilityFilters.task_id = ''; vulnerabilityFilters.conversation_tag = ''; vulnerabilityFilters.task_tag = ''; vulnerabilityFilters.severity = ''; vulnerabilityFilters.status = ''; const idEl = document.getElementById('vulnerability-id-filter'); const convEl = document.getElementById('vulnerability-conversation-filter'); const taskEl = document.getElementById('vulnerability-task-filter'); const convTagEl = document.getElementById('vulnerability-conversation-tag-filter'); const taskTagEl = document.getElementById('vulnerability-task-tag-filter'); const sevEl = document.getElementById('vulnerability-severity-filter'); const stEl = document.getElementById('vulnerability-status-filter'); if (idEl) idEl.value = ''; if (convEl) convEl.value = ''; if (taskEl) taskEl.value = ''; if (convTagEl) convTagEl.value = ''; if (taskTagEl) taskTagEl.value = ''; if (sevEl) sevEl.value = ''; if (stEl) stEl.value = ''; if (vid) { vulnerabilityFilters.id = vid; if (idEl) idEl.value = vid; } if (cid) { vulnerabilityFilters.conversation_id = cid; if (convEl) convEl.value = cid; } if (tid) { vulnerabilityFilters.task_id = tid; if (taskEl) taskEl.value = tid; } if (convTag) { vulnerabilityFilters.conversation_tag = convTag; if (convTagEl) convTagEl.value = convTag; } if (taskTag) { vulnerabilityFilters.task_tag = taskTag; if (taskTagEl) taskTagEl.value = taskTag; } if (sev) { vulnerabilityFilters.severity = sev; if (sevEl) sevEl.value = sev; } if (st) { vulnerabilityFilters.status = st; if (stEl) stEl.value = st; } vulnerabilityPagination.currentPage = 1; if (hasVulnerabilityAdvancedFiltersActive()) { setVulnerabilityAdvancedFiltersOpen(true, false); } syncVulnerabilityStatCardActiveState(); updateVulnerabilityFilterPanelState(); renderVulnerabilityFilterChips(); } // 初始化漏洞管理页面 function initVulnerabilityPage() { // 从localStorage加载每页条数设置 vulnerabilityPagination.pageSize = getVulnerabilityPageSize(); initVulnerabilityStatCards(); initVulnerabilityFilterPanel(); syncVulnerabilityFiltersFromLocationHash(); updateVulnerabilityFilterPanelState(); renderVulnerabilityFilterChips(); loadVulnerabilityFilterOptions(); loadVulnerabilityStats(); loadVulnerabilities(); } function initVulnerabilityStatCards() { if (vulnerabilityStatCardsBound) { syncVulnerabilityStatCardActiveState(); return; } const root = document.getElementById('vulnerability-stat-cards'); if (!root) return; vulnerabilityStatCardsBound = true; root.addEventListener('click', onVulnerabilityStatCardClick); root.addEventListener('keydown', onVulnerabilityStatCardKeydown); } function onVulnerabilityStatCardClick(ev) { const totalCard = ev.target.closest('.stat-card.stat-card-total'); if (totalCard) { applyVulnerabilitySeverityFilter(''); return; } const card = ev.target.closest('.stat-card.is-clickable[data-severity]'); if (!card) return; const sev = card.getAttribute('data-severity'); if (!sev) return; const sevEl = document.getElementById('vulnerability-severity-filter'); const current = sevEl ? sevEl.value : vulnerabilityFilters.severity; applyVulnerabilitySeverityFilter(current === sev ? '' : sev); } function onVulnerabilityStatCardKeydown(ev) { if (ev.key !== 'Enter' && ev.key !== ' ') return; const card = ev.target.closest('.stat-card.is-clickable'); if (!card || !card.contains(ev.target)) return; ev.preventDefault(); card.click(); } function applyVulnerabilitySeverityFilter(severity) { const sevEl = document.getElementById('vulnerability-severity-filter'); if (sevEl) sevEl.value = severity || ''; applyVulnerabilityFilters(); } function readVulnerabilityFiltersFromForm() { vulnerabilityFilters.id = (document.getElementById('vulnerability-id-filter')?.value || '').trim(); vulnerabilityFilters.conversation_id = (document.getElementById('vulnerability-conversation-filter')?.value || '').trim(); vulnerabilityFilters.task_id = (document.getElementById('vulnerability-task-filter')?.value || '').trim(); vulnerabilityFilters.conversation_tag = (document.getElementById('vulnerability-conversation-tag-filter')?.value || '').trim(); vulnerabilityFilters.task_tag = (document.getElementById('vulnerability-task-tag-filter')?.value || '').trim(); vulnerabilityFilters.severity = document.getElementById('vulnerability-severity-filter')?.value || ''; vulnerabilityFilters.status = document.getElementById('vulnerability-status-filter')?.value || ''; return vulnerabilityFilters; } function hasVulnerabilityAdvancedFiltersActive() { const f = vulnerabilityFilters; return Boolean(f.conversation_id || f.task_id || f.conversation_tag || f.task_tag); } function hasAnyVulnerabilityFilterActive() { const f = vulnerabilityFilters; return Boolean( f.id || f.conversation_id || f.task_id || f.conversation_tag || f.task_tag || f.severity || f.status ); } function applyVulnerabilityFilters() { readVulnerabilityFiltersFromForm(); vulnerabilityPagination.currentPage = 1; syncVulnerabilityStatCardActiveState(); updateVulnerabilityLocationHashFromFilters(); updateVulnerabilityFilterPanelState(); renderVulnerabilityFilterChips(); loadVulnerabilityStats(); loadVulnerabilities(); } function updateVulnerabilityLocationHashFromFilters() { const hash = window.location.hash.slice(1); const hashParts = hash.split('?'); if (hashParts[0] !== 'vulnerabilities') return; const params = new URLSearchParams(hashParts.length >= 2 ? hashParts.slice(1).join('?') : ''); const f = vulnerabilityFilters; const pairs = [ ['id', f.id], ['conversation_id', f.conversation_id], ['task_id', f.task_id], ['conversation_tag', f.conversation_tag], ['task_tag', f.task_tag], ['severity', f.severity], ['status', f.status] ]; pairs.forEach(function (pair) { if (pair[1]) { params.set(pair[0], pair[1]); } else { params.delete(pair[0]); } }); const qs = params.toString(); const newHash = qs ? 'vulnerabilities?' + qs : 'vulnerabilities'; if (window.location.hash.slice(1) === newHash) return; const newFull = '#' + newHash; if (typeof history.replaceState === 'function') { history.replaceState(null, '', window.location.pathname + window.location.search + newFull); } else { window.location.hash = newHash; } } function toggleVulnerabilityAdvancedFilters(ev) { if (ev) { ev.preventDefault(); ev.stopPropagation(); } const toggleBtn = document.getElementById('vulnerability-advanced-toggle'); if (!toggleBtn) return; const expanded = toggleBtn.getAttribute('aria-expanded') === 'true'; setVulnerabilityAdvancedFiltersOpen(!expanded, true); } window.toggleVulnerabilityAdvancedFilters = toggleVulnerabilityAdvancedFilters; function initVulnerabilityFilterPanel() { const panel = document.getElementById('vulnerability-filter-panel'); if (!panel) return; if (vulnerabilityFilterPanelBound) { updateVulnerabilityFilterPanelState(); return; } vulnerabilityFilterPanelBound = true; let savedOpen = false; try { savedOpen = localStorage.getItem(VULNERABILITY_ADVANCED_OPEN_KEY) === 'true'; } catch (e) { /* ignore */ } setVulnerabilityAdvancedFiltersOpen(savedOpen, false); const stEl = document.getElementById('vulnerability-status-filter'); if (stEl) stEl.addEventListener('change', applyVulnerabilityFilters); const textIds = [ 'vulnerability-id-filter', 'vulnerability-conversation-filter', 'vulnerability-task-filter', 'vulnerability-conversation-tag-filter', 'vulnerability-task-tag-filter' ]; textIds.forEach(function (id) { const el = document.getElementById(id); if (!el) return; el.addEventListener('keydown', function (ev) { if (ev.key === 'Enter') { ev.preventDefault(); applyVulnerabilityFilters(); } }); }); bindVulnerabilityFilterTypeaheads(); } function setVulnerabilityAdvancedFiltersOpen(open, persist) { const toggleBtn = document.getElementById('vulnerability-advanced-toggle'); const advanced = document.getElementById('vulnerability-advanced-filters'); const wrap = document.querySelector('#vulnerability-filter-panel .vulnerability-filter-advanced-wrap'); if (!toggleBtn || !advanced) return; toggleBtn.setAttribute('aria-expanded', open ? 'true' : 'false'); advanced.hidden = !open; advanced.classList.toggle('is-open', open); if (wrap) wrap.classList.toggle('is-expanded', open); if (persist) { try { localStorage.setItem(VULNERABILITY_ADVANCED_OPEN_KEY, open ? 'true' : 'false'); } catch (e) { /* ignore */ } } } function countVulnerabilityAdvancedFiltersActive() { const f = vulnerabilityFilters; let n = 0; if (f.conversation_id) n++; if (f.task_id) n++; if (f.conversation_tag) n++; if (f.task_tag) n++; return n; } function updateVulnerabilityAdvancedBadge() { const badge = document.getElementById('vulnerability-advanced-badge'); if (!badge) return; readVulnerabilityFiltersFromForm(); const n = countVulnerabilityAdvancedFiltersActive(); if (n > 0) { badge.hidden = false; badge.textContent = '(' + n + ')'; badge.setAttribute('aria-label', String(n)); } else { badge.hidden = true; badge.textContent = ''; badge.removeAttribute('aria-label'); } } function updateVulnerabilityFilterPanelState() { const panel = document.getElementById('vulnerability-filter-panel'); if (!panel) return; readVulnerabilityFiltersFromForm(); panel.classList.toggle('is-filtered', hasAnyVulnerabilityFilterActive()); updateVulnerabilityAdvancedBadge(); } function formatVulnerabilityFilterChipValue(key, value) { if (key === 'severity') return vulnSeverityLabel(value); if (key === 'status') return vulnStatusLabel(value); return value; } function renderVulnerabilityFilterChips() { const wrap = document.getElementById('vulnerability-filter-chips'); const list = document.getElementById('vulnerability-filter-chips-list'); if (!wrap || !list) return; readVulnerabilityFiltersFromForm(); const chips = []; VULN_FILTER_CHIP_FIELDS.forEach(function (field) { const val = vulnerabilityFilters[field.key]; if (!val) return; const label = field.labelKey ? vulnT(field.labelKey) : ''; const displayVal = formatVulnerabilityFilterChipValue(field.key, val); const text = label ? label + ': ' + displayVal : displayVal; chips.push({ key: field.key, text: text }); }); if (!chips.length) { wrap.hidden = true; list.innerHTML = ''; return; } wrap.hidden = false; const removeLabel = vulnT('vulnerabilityPage.chipRemove'); list.innerHTML = chips.map(function (chip) { return ( '' ); }).join(''); list.querySelectorAll('.vulnerability-filter-chip').forEach(function (btn) { btn.addEventListener('click', function () { const key = btn.getAttribute('data-filter-key'); if (key) removeVulnerabilityFilterByKey(key); }); }); } function removeVulnerabilityFilterByKey(key) { const map = { id: 'vulnerability-id-filter', conversation_id: 'vulnerability-conversation-filter', task_id: 'vulnerability-task-filter', conversation_tag: 'vulnerability-conversation-tag-filter', task_tag: 'vulnerability-task-tag-filter', severity: 'vulnerability-severity-filter', status: 'vulnerability-status-filter' }; const elId = map[key]; if (elId) { const el = document.getElementById(elId); if (el) el.value = ''; } if (Object.prototype.hasOwnProperty.call(vulnerabilityFilters, key)) { vulnerabilityFilters[key] = ''; } applyVulnerabilityFilters(); } async function loadVulnerabilityFilterOptions() { if (typeof apiFetch === 'undefined') return; try { const response = await apiFetch('/api/vulnerabilities/filter-options'); if (!response.ok) return; vulnerabilityFilterOptionsCache = await response.json(); populateVulnerabilityDatalist( 'vulnerability-conversation-tag-suggestions', vulnerabilityFilterOptionsCache.conversation_tags, { max: 20 } ); populateVulnerabilityDatalist( 'vulnerability-task-tag-suggestions', vulnerabilityFilterOptionsCache.task_tags, { max: 20 } ); clearVulnerabilityDatalist('vulnerability-conversation-suggestions'); clearVulnerabilityDatalist('vulnerability-task-suggestions'); } catch (e) { console.warn('加载漏洞筛选建议失败', e); } } function clearVulnerabilityDatalist(listId) { const list = document.getElementById(listId); if (list) list.innerHTML = ''; } function populateVulnerabilityDatalist(listId, values, opts) { const list = document.getElementById(listId); if (!list || !Array.isArray(values)) return; const max = (opts && opts.max) || VULNERABILITY_DATALIST_MAX; const seen = new Set(); const unique = []; values.forEach(function (v) { const s = String(v || '').trim(); if (!s || seen.has(s)) return; seen.add(s); unique.push(s); if (unique.length >= max) return; }); list.innerHTML = unique.slice(0, max).map(function (v) { return ''; }).join(''); } function filterVulnerabilitySuggestionPool(pool, query) { if (!Array.isArray(pool) || !query) return []; const q = query.toLowerCase(); const out = []; for (let i = 0; i < pool.length && out.length < VULNERABILITY_DATALIST_MAX; i++) { const s = String(pool[i] || '').trim(); if (s && s.toLowerCase().indexOf(q) !== -1) out.push(s); } return out; } function updateVulnerabilityTypeaheadDatalist(inputId, listId, poolKey) { const el = document.getElementById(inputId); if (!el || !vulnerabilityFilterOptionsCache) return; const q = el.value.trim(); if (q.length < VULNERABILITY_DATALIST_MIN_QUERY) { clearVulnerabilityDatalist(listId); return; } let pool = vulnerabilityFilterOptionsCache[poolKey] || []; if (poolKey === 'task_ids') { pool = (vulnerabilityFilterOptionsCache.task_ids || []).concat(vulnerabilityFilterOptionsCache.queue_ids || []); } populateVulnerabilityDatalist(listId, filterVulnerabilitySuggestionPool(pool, q)); } function bindVulnerabilityFilterTypeaheads() { const pairs = [ { inputId: 'vulnerability-conversation-filter', listId: 'vulnerability-conversation-suggestions', poolKey: 'conversation_ids' }, { inputId: 'vulnerability-task-filter', listId: 'vulnerability-task-suggestions', poolKey: 'task_ids' } ]; pairs.forEach(function (pair) { const el = document.getElementById(pair.inputId); if (!el) return; el.addEventListener('input', function () { updateVulnerabilityTypeaheadDatalist(pair.inputId, pair.listId, pair.poolKey); }); el.addEventListener('blur', function () { setTimeout(function () { clearVulnerabilityDatalist(pair.listId); }, 150); }); }); ['vulnerability-conversation-tag-filter', 'vulnerability-task-tag-filter'].forEach(function (inputId) { const el = document.getElementById(inputId); if (!el) return; el.addEventListener('focus', function () { if (!vulnerabilityFilterOptionsCache) return; const listId = inputId === 'vulnerability-conversation-tag-filter' ? 'vulnerability-conversation-tag-suggestions' : 'vulnerability-task-tag-suggestions'; const key = inputId === 'vulnerability-conversation-tag-filter' ? 'conversation_tags' : 'task_tags'; const q = el.value.trim(); if (q.length >= VULNERABILITY_DATALIST_MIN_QUERY) { populateVulnerabilityDatalist(listId, filterVulnerabilitySuggestionPool(vulnerabilityFilterOptionsCache[key], q), { max: 20 }); } }); }); } function syncVulnerabilityStatCardActiveState() { const sevEl = document.getElementById('vulnerability-severity-filter'); const sev = (sevEl && sevEl.value) || vulnerabilityFilters.severity || ''; const root = document.getElementById('vulnerability-stat-cards'); if (!root) return; root.querySelectorAll('.stat-card.is-clickable').forEach(function (card) { if (card.classList.contains('stat-card-total')) { card.classList.toggle('is-active', !sev); card.setAttribute('aria-pressed', sev ? 'false' : 'true'); } else { const cardSev = card.getAttribute('data-severity'); const active = Boolean(sev && cardSev === sev); card.classList.toggle('is-active', active); card.setAttribute('aria-pressed', active ? 'true' : 'false'); } }); } function updateVulnerabilityStatStackedBar(bySeverity, total) { const bar = document.getElementById('stat-stacked-bar'); if (!bar) return; const segs = bar.querySelectorAll('.stat-stacked-seg'); if (!total) { bar.classList.add('is-empty'); segs.forEach(function (seg) { seg.style.flex = '0 0 0'; seg.style.display = 'none'; }); return; } bar.classList.remove('is-empty'); segs.forEach(function (seg) { const sev = seg.getAttribute('data-sev'); const count = bySeverity[sev] || 0; if (count <= 0) { seg.style.display = 'none'; seg.style.flex = '0 0 0'; return; } seg.style.display = ''; const pct = Math.max((count / total) * 100, 0); seg.style.flex = '1 1 ' + pct + '%'; }); } // 加载漏洞统计 async function loadVulnerabilityStats() { try { // 检查apiFetch是否可用 if (typeof apiFetch === 'undefined') { console.error('apiFetch未定义,请确保auth.js已加载'); throw new Error('apiFetch未定义'); } const params = new URLSearchParams(); if (vulnerabilityFilters.conversation_id) { params.append('conversation_id', vulnerabilityFilters.conversation_id); } if (vulnerabilityFilters.task_id) { params.append('task_id', vulnerabilityFilters.task_id); } const response = await apiFetch(`/api/vulnerabilities/stats?${params.toString()}`); if (!response.ok) { const errorText = await response.text(); console.error('获取统计失败:', response.status, errorText); throw new Error(`获取统计失败: ${response.status}`); } const stats = await response.json(); updateVulnerabilityStats(stats); } catch (error) { console.error('加载漏洞统计失败:', error); // 统计失败不影响列表显示,只重置统计为0 updateVulnerabilityStats(null); } } // 更新漏洞统计显示 function updateVulnerabilityStats(stats) { // 处理空值情况 if (!stats) { stats = { total: 0, by_severity: {}, by_status: {} }; } const total = stats.total || 0; const bySeverity = stats.by_severity || {}; const totalEl = document.getElementById('stat-total'); if (totalEl) { totalEl.textContent = String(total); totalEl.classList.toggle('is-zero', total === 0); } VULN_STAT_SEVERITIES.forEach(function (sev) { const count = bySeverity[sev] || 0; const valEl = document.getElementById('stat-' + sev); const pctEl = document.getElementById('stat-' + sev + '-pct'); if (valEl) { valEl.textContent = String(count); valEl.classList.toggle('is-zero', count === 0); } if (pctEl) { const pct = total > 0 ? Math.round((count / total) * 100) : 0; pctEl.textContent = pct + '%'; pctEl.setAttribute('aria-hidden', total === 0 ? 'true' : 'false'); } }); updateVulnerabilityStatStackedBar(bySeverity, total); syncVulnerabilityStatCardActiveState(); } // 加载漏洞列表 async function loadVulnerabilities(page = null) { const listContainer = document.getElementById('vulnerabilities-list'); listContainer.innerHTML = `
${escapeHtml(s)}`
: `${escapeHtml(s)}`;
const copyBtn = ``;
return `