// 漏洞管理相关功能 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(vulnT('vulnerabilityPage.loading'))}
`; try { // 检查apiFetch是否可用 if (typeof apiFetch === 'undefined') { console.error('apiFetch未定义,请确保auth.js已加载'); throw new Error('apiFetch未定义'); } // 如果指定了页码,使用页码;否则使用当前页码 if (page !== null) { vulnerabilityPagination.currentPage = page; } const params = new URLSearchParams(); params.append('page', vulnerabilityPagination.currentPage.toString()); params.append('limit', vulnerabilityPagination.pageSize.toString()); if (vulnerabilityFilters.id) { params.append('id', vulnerabilityFilters.id); } if (vulnerabilityFilters.conversation_id) { params.append('conversation_id', vulnerabilityFilters.conversation_id); } if (vulnerabilityFilters.task_id) { params.append('task_id', vulnerabilityFilters.task_id); } if (vulnerabilityFilters.conversation_tag) { params.append('conversation_tag', vulnerabilityFilters.conversation_tag); } if (vulnerabilityFilters.task_tag) { params.append('task_tag', vulnerabilityFilters.task_tag); } if (vulnerabilityFilters.severity) { params.append('severity', vulnerabilityFilters.severity); } if (vulnerabilityFilters.status) { params.append('status', vulnerabilityFilters.status); } const response = await apiFetch(`/api/vulnerabilities?${params.toString()}`); if (!response.ok) { const errorText = await response.text(); console.error('获取漏洞列表失败:', response.status, errorText); throw new Error(`获取漏洞列表失败: ${response.status}`); } const data = await response.json(); // 判断响应格式:新格式(有total字段)还是旧格式(直接是数组) let vulnerabilities; if (Array.isArray(data)) { // 旧格式:直接是数组 vulnerabilities = data; // 使用数组长度作为总数(可能不准确,但至少能显示分页控件) vulnerabilityPagination.total = data.length; vulnerabilityPagination.totalPages = Math.max(1, Math.ceil(data.length / vulnerabilityPagination.pageSize)); console.warn('后端返回的是旧格式(数组),建议更新后端API以支持分页'); } else if ('vulnerabilities' in data) { // 新格式:包含分页信息的对象(vulnerabilities可能为null或数组) vulnerabilities = Array.isArray(data.vulnerabilities) ? data.vulnerabilities : []; vulnerabilityPagination.total = data.total || 0; vulnerabilityPagination.currentPage = data.page || vulnerabilityPagination.currentPage; vulnerabilityPagination.pageSize = data.page_size || vulnerabilityPagination.pageSize; vulnerabilityPagination.totalPages = data.total_pages || 1; } else { // 未知格式,尝试作为数组处理 vulnerabilities = []; console.error('未知的响应格式:', data); } renderVulnerabilities(vulnerabilities); renderVulnerabilityPagination(); } catch (error) { console.error('加载漏洞列表失败:', error); listContainer.innerHTML = `
${escapeHtml(vulnT('vulnerabilityPage.loadListFailed'))}: ${escapeHtml(error.message)}
`; } } // 渲染漏洞列表 function renderVulnerabilities(vulnerabilities) { const listContainer = document.getElementById('vulnerabilities-list'); // 处理空值情况(使用 data-i18n 以便语言切换时自动更新) if (!vulnerabilities || !Array.isArray(vulnerabilities)) { listContainer.innerHTML = '
暂无漏洞记录
'; if (typeof window.applyTranslations === 'function') { window.applyTranslations(listContainer); } return; } if (vulnerabilities.length === 0) { listContainer.innerHTML = '
暂无漏洞记录
'; if (typeof window.applyTranslations === 'function') { window.applyTranslations(listContainer); } // 清空分页信息 const paginationContainer = document.getElementById('vulnerability-pagination'); if (paginationContainer) { paginationContainer.innerHTML = ''; } return; } const html = vulnerabilities.map(vuln => { const severityClass = `severity-${vuln.severity}`; const severityText = vulnSeverityLabel(vuln.severity); const statusText = vulnStatusLabel(vuln.status); const createdDate = new Date(vuln.created_at).toLocaleString(vulnDateLocale()); const dlTitle = escapeHtml(vulnT('vulnerabilityPage.downloadMarkdownTitle')); const editTitle = escapeHtml(vulnT('common.edit')); const deleteTitle = escapeHtml(vulnT('common.delete')); return `

${escapeHtml(vuln.title)}

${severityText} ${statusText} ${createdDate}
`; }).join(''); listContainer.innerHTML = html; if (typeof window.applyTranslations === 'function') { window.applyTranslations(listContainer); } // 如果通过漏洞ID筛选且只返回一条记录,自动展开详情(提升“点击查看”的用户体验) if (vulnerabilities.length === 1 && vulnerabilityFilters.id && vulnerabilityFilters.id === vulnerabilities[0].id) { setTimeout(() => { toggleVulnerabilityDetails(vulnerabilities[0].id); }, 300); } } // 渲染分页控件 function renderVulnerabilityPagination() { const paginationContainer = document.getElementById('vulnerability-pagination'); if (!paginationContainer) { return; } const { currentPage, totalPages, total, pageSize } = vulnerabilityPagination; // 如果没有数据,不显示分页控件 if (total === 0) { paginationContainer.innerHTML = ''; return; } // 计算显示范围 const start = total === 0 ? 0 : (currentPage - 1) * pageSize + 1; const end = total === 0 ? 0 : Math.min(currentPage * pageSize, total); let paginationHTML = ''; paginationContainer.innerHTML = paginationHTML; if (typeof window.applyTranslations === 'function') { window.applyTranslations(paginationContainer); } } // 改变每页显示数量 async function changeVulnerabilityPageSize() { const pageSizeSelect = document.getElementById('vulnerability-page-size-pagination'); if (!pageSizeSelect) return; const newPageSize = parseInt(pageSizeSelect.value, 10); if (isNaN(newPageSize) || newPageSize < 1) { return; } // 保存到localStorage localStorage.setItem('vulnerabilityPageSize', newPageSize.toString()); // 更新分页配置 vulnerabilityPagination.pageSize = newPageSize; // 重新计算当前页(保持显示的数据范围尽可能接近) const currentStartItem = (vulnerabilityPagination.currentPage - 1) * vulnerabilityPagination.pageSize + 1; const newPage = Math.max(1, Math.floor((currentStartItem - 1) / newPageSize) + 1); vulnerabilityPagination.currentPage = newPage; // 重新加载数据 await loadVulnerabilities(); } // 显示添加漏洞模态框 function showAddVulnerabilityModal() { currentVulnerabilityId = null; document.getElementById('vulnerability-modal-title').textContent = vulnT('vulnerability.addVuln'); // 清空表单 document.getElementById('vulnerability-conversation-id').value = ''; document.getElementById('vulnerability-conversation-tag').value = ''; document.getElementById('vulnerability-task-tag').value = ''; document.getElementById('vulnerability-title').value = ''; document.getElementById('vulnerability-description').value = ''; document.getElementById('vulnerability-severity').value = ''; document.getElementById('vulnerability-status').value = 'open'; document.getElementById('vulnerability-type').value = ''; document.getElementById('vulnerability-target').value = ''; document.getElementById('vulnerability-proof').value = ''; document.getElementById('vulnerability-impact').value = ''; document.getElementById('vulnerability-recommendation').value = ''; document.getElementById('vulnerability-modal').style.display = 'block'; } // 编辑漏洞 async function editVulnerability(id) { try { const response = await apiFetch(`/api/vulnerabilities/${id}`); if (!response.ok) throw new Error(vulnT('vulnerabilityPage.fetchFailed')); const vuln = await response.json(); currentVulnerabilityId = id; document.getElementById('vulnerability-modal-title').textContent = vulnT('vulnerability.editVuln'); // 填充表单 document.getElementById('vulnerability-conversation-id').value = vuln.conversation_id || ''; document.getElementById('vulnerability-conversation-tag').value = vuln.conversation_tag || ''; document.getElementById('vulnerability-task-tag').value = vuln.task_tag || ''; document.getElementById('vulnerability-title').value = vuln.title || ''; document.getElementById('vulnerability-description').value = vuln.description || ''; document.getElementById('vulnerability-severity').value = vuln.severity || ''; document.getElementById('vulnerability-status').value = vuln.status || 'open'; document.getElementById('vulnerability-type').value = vuln.type || ''; document.getElementById('vulnerability-target').value = vuln.target || ''; document.getElementById('vulnerability-proof').value = vuln.proof || ''; document.getElementById('vulnerability-impact').value = vuln.impact || ''; document.getElementById('vulnerability-recommendation').value = vuln.recommendation || ''; document.getElementById('vulnerability-modal').style.display = 'block'; } catch (error) { console.error('加载漏洞失败:', error); alert(vulnT('vulnerability.loadFailed') + ': ' + error.message); } } // 保存漏洞 async function saveVulnerability() { const conversationId = document.getElementById('vulnerability-conversation-id').value.trim(); const title = document.getElementById('vulnerability-title').value.trim(); const severity = document.getElementById('vulnerability-severity').value; if (!conversationId || !title || !severity) { alert(vulnT('vulnerabilityPage.saveRequiredFields')); return; } const data = { conversation_id: conversationId, conversation_tag: document.getElementById('vulnerability-conversation-tag').value.trim(), task_tag: document.getElementById('vulnerability-task-tag').value.trim(), title: title, description: document.getElementById('vulnerability-description').value.trim(), severity: severity, status: document.getElementById('vulnerability-status').value, type: document.getElementById('vulnerability-type').value.trim(), target: document.getElementById('vulnerability-target').value.trim(), proof: document.getElementById('vulnerability-proof').value.trim(), impact: document.getElementById('vulnerability-impact').value.trim(), recommendation: document.getElementById('vulnerability-recommendation').value.trim() }; try { const url = currentVulnerabilityId ? `/api/vulnerabilities/${currentVulnerabilityId}` : '/api/vulnerabilities'; const method = currentVulnerabilityId ? 'PUT' : 'POST'; const response = await apiFetch(url, { method: method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || vulnT('vulnerabilityPage.saveFailed')); } closeVulnerabilityModal(); loadVulnerabilityStats(); // 保存/更新后,重置到第一页 vulnerabilityPagination.currentPage = 1; loadVulnerabilities(); } catch (error) { console.error('保存漏洞失败:', error); alert(vulnT('vulnerabilityPage.saveFailed') + ': ' + error.message); } } // 删除漏洞 async function deleteVulnerability(id) { if (!confirm(vulnT('vulnerability.deleteConfirm'))) { return; } try { const response = await apiFetch(`/api/vulnerabilities/${id}`, { method: 'DELETE' }); if (!response.ok) throw new Error(vulnT('vulnerabilityPage.deleteFailed')); loadVulnerabilityStats(); // 删除后,如果当前页没有数据了,回到上一页 if (vulnerabilityPagination.currentPage > 1 && vulnerabilityPagination.total > 0) { const itemsOnCurrentPage = vulnerabilityPagination.total - (vulnerabilityPagination.currentPage - 1) * vulnerabilityPagination.pageSize; if (itemsOnCurrentPage <= 1) { vulnerabilityPagination.currentPage--; } } loadVulnerabilities(); } catch (error) { console.error('删除漏洞失败:', error); alert(vulnT('vulnerabilityPage.deleteFailed') + ': ' + error.message); } } // 关闭漏洞模态框 function closeVulnerabilityModal() { document.getElementById('vulnerability-modal').style.display = 'none'; currentVulnerabilityId = null; } // 筛选漏洞(应用当前表单条件) function filterVulnerabilities() { applyVulnerabilityFilters(); } // 清除筛选 function clearVulnerabilityFilters() { const fields = [ 'vulnerability-id-filter', 'vulnerability-conversation-filter', 'vulnerability-task-filter', 'vulnerability-conversation-tag-filter', 'vulnerability-task-tag-filter', 'vulnerability-severity-filter', 'vulnerability-status-filter' ]; fields.forEach(function (id) { const el = document.getElementById(id); if (el) el.value = ''; }); vulnerabilityFilters = { id: '', conversation_id: '', task_id: '', conversation_tag: '', task_tag: '', severity: '', status: '' }; applyVulnerabilityFilters(); } // 刷新漏洞 function refreshVulnerabilities() { loadVulnerabilityStats(); loadVulnerabilities(); } // 切换漏洞详情展开/折叠 function toggleVulnerabilityDetails(id) { const content = document.getElementById(`content-${id}`); const icon = document.getElementById(`expand-icon-${id}`); if (!content || !icon) return; if (content.style.display === 'none') { content.style.display = 'block'; icon.style.transform = 'rotate(90deg)'; } else { content.style.display = 'none'; icon.style.transform = 'rotate(0deg)'; } } // HTML转义 function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } /** 复制详情字段(编码由 encodeURIComponent 传入,避免引号截断) */ function vulnerabilityCopyEncoded(evt, encoded) { if (evt && evt.stopPropagation) { evt.stopPropagation(); } let text = ''; try { text = decodeURIComponent(encoded); } catch (e) { return; } const done = () => { if (evt && evt.target && evt.target.closest) { const btn = evt.target.closest('.vuln-detail-field__copy'); if (btn) { const t0 = btn.getAttribute('title') || ''; btn.setAttribute('title', vulnT('common.copied')); setTimeout(() => btn.setAttribute('title', t0), 1600); } } }; if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') { navigator.clipboard.writeText(text).then(done).catch(() => { try { const ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.left = '-9999px'; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); done(); } catch (err) { console.error('copy failed', err); } }); } else { try { const ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.left = '-9999px'; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); done(); } catch (err) { console.error('copy failed', err); } } } function vulnDetailField(label, value, asCode) { if (value === undefined || value === null || String(value) === '') { return ''; } const s = String(value); const enc = encodeURIComponent(s); const copyTitle = escapeHtml(vulnT('common.copy')); const valueEl = asCode ? `${escapeHtml(s)}` : `${escapeHtml(s)}`; const copyBtn = ``; return `
${escapeHtml(label)}
${valueEl}${copyBtn}
`; } // 将漏洞格式化为Markdown(章节标题随界面语言) function formatVulnerabilityAsMarkdown(vuln) { const severityText = vulnSeverityLabel(vuln.severity); const statusText = vulnStatusLabel(vuln.status); const loc = vulnDateLocale(); const createdDate = new Date(vuln.created_at).toLocaleString(loc); const updatedDate = new Date(vuln.updated_at).toLocaleString(loc); const L = (k) => vulnT('vulnerabilityMd.' + k); let markdown = `# ${vuln.title}\n\n`; markdown += `## ${L('headingBasic')}\n\n`; markdown += `- **${L('labelId')}**: \`${vuln.id}\`\n`; markdown += `- **${L('labelSeverity')}**: ${severityText}\n`; markdown += `- **${L('labelStatus')}**: ${statusText}\n`; if (vuln.type) { markdown += `- **${L('labelType')}**: ${vuln.type}\n`; } if (vuln.target) { markdown += `- **${L('labelTarget')}**: ${vuln.target}\n`; } markdown += `- **${L('labelConversationId')}**: \`${vuln.conversation_id}\`\n`; if (vuln.task_id) { markdown += `- **${L('labelTaskId')}**: \`${vuln.task_id}\`\n`; } if (vuln.task_queue_id) { markdown += `- **${L('labelTaskQueueId')}**: \`${vuln.task_queue_id}\`\n`; } if (vuln.conversation_tag) { markdown += `- **${L('labelConversationTag')}**: ${vuln.conversation_tag}\n`; } if (vuln.task_tag) { markdown += `- **${L('labelTaskTag')}**: ${vuln.task_tag}\n`; } markdown += `- **${L('labelCreated')}**: ${createdDate}\n`; markdown += `- **${L('labelUpdated')}**: ${updatedDate}\n\n`; if (vuln.description) { markdown += `## ${L('headingDescription')}\n\n${vuln.description}\n\n`; } if (vuln.proof) { markdown += `## ${L('headingProof')}\n\n\`\`\`\n${vuln.proof}\n\`\`\`\n\n`; } if (vuln.impact) { markdown += `## ${L('headingImpact')}\n\n${vuln.impact}\n\n`; } if (vuln.recommendation) { markdown += `## ${L('headingRecommendation')}\n\n${vuln.recommendation}\n\n`; } return markdown; } function buildVulnerabilityFilterParams() { const params = new URLSearchParams(); const keys = ['id', 'conversation_id', 'task_id', 'conversation_tag', 'task_tag', 'severity', 'status']; keys.forEach((k) => { if (vulnerabilityFilters[k]) { params.append(k, vulnerabilityFilters[k]); } }); return params; } function triggerTextDownload(fileName, content) { const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = fileName; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); } async function exportVulnerabilityReports() { try { const params = buildVulnerabilityFilterParams(); params.set('mode', 'summary'); params.set('group_by', 'conversation'); const response = await apiFetch(`/api/vulnerabilities/export?${params.toString()}`); if (!response.ok) { const error = await response.json().catch(() => ({ error: vulnT('vulnerabilityPage.exportFailedMessage') })); throw new Error(error.error || vulnT('vulnerabilityPage.exportFailedMessage')); } const data = await response.json(); const files = Array.isArray(data.files) ? data.files : []; if (!files.length) { alert(vulnT('vulnerabilityPage.exportNoResults')); return; } files.forEach((file, idx) => { setTimeout(() => triggerTextDownload(file.filename || `vulnerability-export-${idx + 1}.md`, file.content || ''), idx * 120); }); if (files.length > 1) { alert(vulnT('vulnerabilityPage.exportStarted', { count: files.length })); } } catch (error) { console.error('导出漏洞报告失败:', error); alert(vulnT('vulnerabilityPage.exportFailed') + ': ' + error.message); } } // 下载漏洞为Markdown格式 async function downloadVulnerabilityAsMarkdown(id, event) { try { const response = await apiFetch(`/api/vulnerabilities/${id}`); if (!response.ok) { throw new Error(vulnT('vulnerabilityPage.fetchFailed')); } const vuln = await response.json(); const markdown = formatVulnerabilityAsMarkdown(vuln); // 创建Blob对象 const blob = new Blob([markdown], { type: 'text/markdown;charset=utf-8' }); // 创建下载链接 const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; // 生成文件名(使用漏洞标题,清理特殊字符,保留中文) const cleanTitle = vuln.title .replace(/[<>:"/\\|?*]/g, '') // 移除Windows不允许的字符 .replace(/\s+/g, '_') // 空格替换为下划线 .substring(0, 50); // 限制长度 const fileName = `${cleanTitle}_${vuln.id.substring(0, 8)}.md`; link.download = fileName; // 触发下载 document.body.appendChild(link); link.click(); // 清理 document.body.removeChild(link); URL.revokeObjectURL(url); // 显示成功提示 if (event && event.target) { const button = event.target.closest('button'); if (button) { const originalTitle = button.title || vulnT('vulnerabilityPage.downloadMarkdownTitle'); button.title = vulnT('vulnerabilityPage.downloadOkTitle'); setTimeout(() => { button.title = originalTitle; }, 2000); } } } catch (error) { console.error('下载失败:', error); alert(vulnT('vulnerabilityPage.downloadFailed') + ': ' + error.message); } } // 点击模态框外部关闭 window.onclick = function(event) { const modal = document.getElementById('vulnerability-modal'); if (event.target === modal) { closeVulnerabilityModal(); } }; document.addEventListener('languagechange', function () { const page = document.getElementById('page-vulnerabilities'); if (page && page.classList.contains('active')) { renderVulnerabilityFilterChips(); loadVulnerabilities(); } });