// 漏洞管理相关功能
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 = {
q: '',
id: '',
project_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;
let vulnerabilityMoreFiltersPopoverOpen = false;
let vulnerabilityFilterDebounceTimer = null;
const VULNERABILITY_FILTER_DEBOUNCE_MS = 400;
const VULNERABILITY_DATALIST_MAX = 8;
const VULNERABILITY_DATALIST_MIN_QUERY = 2;
const VULN_FILTER_CHIP_FIELDS = [
{ key: 'q', labelKey: 'vulnerabilityPage.searchKeywordShort' },
{ key: 'id', labelKey: 'vulnerabilityPage.vulnId' },
{ key: 'status', labelKey: null, format: 'status' },
{ key: 'severity', labelKey: null, format: 'severity' },
{ key: 'project_id', labelKey: null },
{ 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();
const pid = (params.get('project_id') || '').trim();
const q = (params.get('q') || params.get('search') || '').trim();
if (!vid && !cid && !tid && !sev && !st && !convTag && !taskTag && !q && !pid) {
return;
}
vulnerabilityFilters.q = '';
vulnerabilityFilters.id = '';
vulnerabilityFilters.project_id = '';
vulnerabilityFilters.conversation_id = '';
vulnerabilityFilters.task_id = '';
vulnerabilityFilters.conversation_tag = '';
vulnerabilityFilters.task_tag = '';
vulnerabilityFilters.severity = '';
vulnerabilityFilters.status = '';
const searchEl = document.getElementById('vulnerability-search-filter');
const exactIdEl = document.getElementById('vulnerability-exact-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 projEl = document.getElementById('vulnerability-project-filter');
const sevEl = document.getElementById('vulnerability-severity-filter');
const stEl = document.getElementById('vulnerability-status-filter');
if (searchEl) searchEl.value = '';
if (exactIdEl) exactIdEl.value = '';
if (convEl) convEl.value = '';
if (taskEl) taskEl.value = '';
if (convTagEl) convTagEl.value = '';
if (taskTagEl) taskTagEl.value = '';
if (projEl) projEl.value = '';
if (sevEl) sevEl.value = '';
if (stEl) stEl.value = '';
if (q) {
vulnerabilityFilters.q = q;
if (searchEl) searchEl.value = q;
}
if (pid) {
vulnerabilityFilters.project_id = pid;
if (projEl) projEl.value = pid;
}
if (vid) {
vulnerabilityFilters.id = vid;
if (exactIdEl) exactIdEl.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;
syncVulnerabilityStatCardActiveState();
updateVulnerabilityFilterPanelState();
renderVulnerabilityFilterChips();
}
// 初始化漏洞管理页面
async function initVulnerabilityPage() {
// 从localStorage加载每页条数设置
vulnerabilityPagination.pageSize = getVulnerabilityPageSize();
initVulnerabilityStatCards();
initVulnerabilityFilterPanel();
syncVulnerabilityFiltersFromLocationHash();
await refreshVulnerabilityProjectFilter();
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.q = (document.getElementById('vulnerability-search-filter')?.value || '').trim();
vulnerabilityFilters.id = (document.getElementById('vulnerability-exact-id-filter')?.value || '').trim();
vulnerabilityFilters.project_id = (document.getElementById('vulnerability-project-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.id || f.conversation_id || f.task_id || f.conversation_tag || f.task_tag);
}
function hasAnyVulnerabilityFilterActive() {
const f = vulnerabilityFilters;
return Boolean(
f.q || f.id || f.project_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 = [
['q', f.q],
['id', f.id],
['project_id', f.project_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 scheduleVulnerabilityFilterApply(immediate) {
if (vulnerabilityFilterDebounceTimer) {
clearTimeout(vulnerabilityFilterDebounceTimer);
vulnerabilityFilterDebounceTimer = null;
}
if (immediate) {
applyVulnerabilityFilters();
return;
}
vulnerabilityFilterDebounceTimer = setTimeout(function () {
vulnerabilityFilterDebounceTimer = null;
applyVulnerabilityFilters();
}, VULNERABILITY_FILTER_DEBOUNCE_MS);
}
function syncVulnerabilityAdvancedFieldsFromFilters() {
const map = {
id: 'vulnerability-exact-id-filter',
conversation_id: 'vulnerability-conversation-filter',
task_id: 'vulnerability-task-filter',
conversation_tag: 'vulnerability-conversation-tag-filter',
task_tag: 'vulnerability-task-tag-filter'
};
Object.keys(map).forEach(function (key) {
const el = document.getElementById(map[key]);
if (el) el.value = vulnerabilityFilters[key] || '';
});
}
function setVulnerabilityMoreFiltersPopoverOpen(open) {
const btn = document.getElementById('vulnerability-more-filters-btn');
const popover = document.getElementById('vulnerability-more-filters-popover');
if (!btn || !popover) return;
vulnerabilityMoreFiltersPopoverOpen = open;
btn.setAttribute('aria-expanded', open ? 'true' : 'false');
btn.classList.toggle('is-active', open);
popover.hidden = !open;
}
function toggleVulnerabilityMoreFiltersPopover(ev) {
if (ev) {
ev.preventDefault();
ev.stopPropagation();
}
const opening = !vulnerabilityMoreFiltersPopoverOpen;
if (opening) {
readVulnerabilityFiltersFromForm();
syncVulnerabilityAdvancedFieldsFromFilters();
}
setVulnerabilityMoreFiltersPopoverOpen(opening);
}
function closeVulnerabilityMoreFiltersPopover(revertDraft) {
if (revertDraft) syncVulnerabilityAdvancedFieldsFromFilters();
setVulnerabilityMoreFiltersPopoverOpen(false);
}
function clearVulnerabilityAdvancedFilterFields() {
['vulnerability-exact-id-filter', 'vulnerability-conversation-filter', 'vulnerability-task-filter',
'vulnerability-conversation-tag-filter', 'vulnerability-task-tag-filter'].forEach(function (id) {
const el = document.getElementById(id);
if (el) el.value = '';
});
}
function applyVulnerabilityMoreFiltersFromPopover() {
closeVulnerabilityMoreFiltersPopover(false);
scheduleVulnerabilityFilterApply(true);
}
function onVulnerabilityFilterDocumentPointerDown(ev) {
if (!vulnerabilityMoreFiltersPopoverOpen) return;
const anchor = document.querySelector('.vulnerability-more-filters-anchor');
if (anchor && anchor.contains(ev.target)) return;
closeVulnerabilityMoreFiltersPopover(true);
}
function initVulnerabilityFilterPanel() {
const panel = document.getElementById('vulnerability-filter-panel');
if (!panel) return;
if (vulnerabilityFilterPanelBound) {
updateVulnerabilityFilterPanelState();
return;
}
vulnerabilityFilterPanelBound = true;
const stEl = document.getElementById('vulnerability-status-filter');
if (stEl) stEl.addEventListener('change', function () { scheduleVulnerabilityFilterApply(true); });
const searchEl = document.getElementById('vulnerability-search-filter');
if (searchEl) {
searchEl.addEventListener('input', function () { scheduleVulnerabilityFilterApply(false); });
searchEl.addEventListener('keydown', function (ev) {
if (ev.key === 'Enter') {
ev.preventDefault();
scheduleVulnerabilityFilterApply(true);
}
});
searchEl.addEventListener('search', function () {
if (searchEl.value === '') scheduleVulnerabilityFilterApply(true);
});
}
const moreBtn = document.getElementById('vulnerability-more-filters-btn');
if (moreBtn) moreBtn.addEventListener('click', toggleVulnerabilityMoreFiltersPopover);
const applyBtn = document.getElementById('vulnerability-more-filters-apply');
if (applyBtn) applyBtn.addEventListener('click', applyVulnerabilityMoreFiltersFromPopover);
const resetBtn = document.getElementById('vulnerability-more-filters-reset');
if (resetBtn) {
resetBtn.addEventListener('click', function () {
clearVulnerabilityAdvancedFilterFields();
applyVulnerabilityMoreFiltersFromPopover();
});
}
const advancedTextIds = [
'vulnerability-exact-id-filter',
'vulnerability-conversation-filter',
'vulnerability-task-filter',
'vulnerability-conversation-tag-filter',
'vulnerability-task-tag-filter'
];
advancedTextIds.forEach(function (id) {
const el = document.getElementById(id);
if (!el) return;
el.addEventListener('keydown', function (ev) {
if (ev.key === 'Enter') {
ev.preventDefault();
applyVulnerabilityMoreFiltersFromPopover();
}
});
});
document.addEventListener('mousedown', onVulnerabilityFilterDocumentPointerDown);
document.addEventListener('keydown', function (ev) {
if (ev.key === 'Escape' && vulnerabilityMoreFiltersPopoverOpen) {
closeVulnerabilityMoreFiltersPopover(true);
}
});
bindVulnerabilityFilterTypeaheads();
}
function countVulnerabilityAdvancedFiltersActive() {
const f = vulnerabilityFilters;
let n = 0;
if (f.id) n++;
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();
const clearBtn = document.getElementById('vulnerability-filter-clear-btn');
if (clearBtn) clearBtn.hidden = !hasAnyVulnerabilityFilterActive();
}
function formatVulnerabilityFilterChipValue(key, value) {
if (key === 'severity') return vulnSeverityLabel(value);
if (key === 'status') return vulnStatusLabel(value);
if (key === 'project_id') {
const name = typeof getProjectName === 'function' ? getProjectName(value) : '';
return name && name !== value ? name : 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) : (field.key === 'project_id' ? '项目' : '');
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 = {
q: 'vulnerability-search-filter',
id: 'vulnerability-exact-id-filter',
conversation_id: 'vulnerability-conversation-filter',
task_id: 'vulnerability-task-filter',
conversation_tag: 'vulnerability-conversation-tag-filter',
task_tag: 'vulnerability-task-tag-filter',
project_id: 'vulnerability-project-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 = buildVulnerabilityFilterParams();
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 = buildVulnerabilityFilterParams();
params.append('page', vulnerabilityPagination.currentPage.toString());
params.append('limit', vulnerabilityPagination.pageSize.toString());
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 projectLabel = vuln.project_id
? escapeHtml(typeof getProjectName === 'function' ? getProjectName(vuln.project_id) : vuln.project_id)
: escapeHtml(vulnT('vulnerabilityPage.projectUnbound'));
const projectBadge = vuln.project_id
? `${escapeHtml(vulnT('vulnerabilityPage.detailProject'))}: ${projectLabel}`
: `${escapeHtml(vulnT('vulnerabilityPage.projectUnbound'))}`;
const dlTitle = escapeHtml(vulnT('vulnerabilityPage.downloadMarkdownTitle'));
const editTitle = escapeHtml(vulnT('common.edit'));
const deleteTitle = escapeHtml(vulnT('common.delete'));
return `
${vuln.description ? `
${escapeHtml(vuln.description)}
` : ''}
${vulnDetailField(vulnT('vulnerabilityPage.detailVulnId'), vuln.id, true)}
${vulnDetailProjectField(vuln)}
${vuln.type ? vulnDetailField(vulnT('vulnerabilityPage.detailType'), vuln.type, false) : ''}
${vuln.target ? vulnDetailField(vulnT('vulnerabilityPage.detailTarget'), vuln.target, false) : ''}
${vulnDetailField(vulnT('vulnerabilityPage.detailConversationId'), vuln.conversation_id, true)}
${vuln.task_id ? vulnDetailField(vulnT('vulnerabilityPage.detailTaskId'), vuln.task_id, true) : ''}
${vuln.task_queue_id ? vulnDetailField(vulnT('vulnerabilityPage.detailTaskQueueId'), vuln.task_queue_id, true) : ''}
${vuln.conversation_tag ? vulnDetailField(vulnT('vulnerabilityPage.detailConversationTag'), vuln.conversation_tag, false) : ''}
${vuln.task_tag ? vulnDetailField(vulnT('vulnerabilityPage.detailTaskTag'), vuln.task_tag, false) : ''}
${vuln.proof ? `
${escapeHtml(vulnT('vulnerabilityPage.detailProof'))}:${escapeHtml(vuln.proof)} ` : ''}
${vuln.impact ? `
${escapeHtml(vulnT('vulnerabilityPage.detailImpact'))}: ${escapeHtml(vuln.impact)}
` : ''}
${vuln.recommendation ? `
${escapeHtml(vulnT('vulnerabilityPage.detailRecommendation'))}: ${escapeHtml(vuln.recommendation)}
` : ''}
`;
}).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 buildVulnerabilityProjectOptionsHtml(selectedId) {
const sel = (selectedId || '').trim();
let html = ``;
const entries = typeof projectNameById !== 'undefined' ? Object.entries(projectNameById) : [];
entries.sort((a, b) => (a[1] || '').localeCompare(b[1] || '', undefined, { sensitivity: 'base' }));
entries.forEach(([id, name]) => {
if (!id) return;
const selected = id === sel ? ' selected' : '';
html += ``;
});
if (sel && !entries.some(([id]) => id === sel)) {
html += ``;
}
return html;
}
async function populateVulnerabilityModalProjectSelect(selectedId) {
const sel = document.getElementById('vulnerability-project-id');
if (!sel) return;
try {
const res = await apiFetch('/api/projects?limit=200');
if (res.ok) {
const list = await res.json();
if (typeof rebuildProjectNameMap === 'function') {
rebuildProjectNameMap(list);
} else if (typeof projectNameById !== 'undefined') {
(list || []).forEach((p) => { if (p.id) projectNameById[p.id] = p.name || p.id; });
}
}
} catch (e) {
console.warn('加载项目列表失败', e);
}
sel.innerHTML = buildVulnerabilityProjectOptionsHtml(selectedId || '');
sel.value = selectedId || '';
}
// 显示添加漏洞模态框
async function showAddVulnerabilityModal() {
currentVulnerabilityId = null;
document.getElementById('vulnerability-modal-title').textContent = vulnT('vulnerability.addVuln');
const defaultProject = vulnerabilityFilters.project_id || '';
await populateVulnerabilityModalProjectSelect(defaultProject);
// 清空表单
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 || '';
await populateVulnerabilityModalProjectSelect(vuln.project_id || '');
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 projectId = (document.getElementById('vulnerability-project-id')?.value || '').trim();
const data = {
conversation_id: conversationId,
project_id: projectId,
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';
let body = data;
if (currentVulnerabilityId) {
body = {
project_id: projectId,
conversation_tag: data.conversation_tag,
task_tag: data.task_tag,
title: data.title,
description: data.description,
severity: data.severity,
status: data.status,
type: data.type,
target: data.target,
proof: data.proof,
impact: data.impact,
recommendation: data.recommendation,
};
}
const response = await apiFetch(url, {
method: method,
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
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() {
closeVulnerabilityMoreFiltersPopover(false);
if (vulnerabilityFilterDebounceTimer) {
clearTimeout(vulnerabilityFilterDebounceTimer);
vulnerabilityFilterDebounceTimer = null;
}
const fields = [
'vulnerability-search-filter',
'vulnerability-exact-id-filter',
'vulnerability-conversation-filter',
'vulnerability-task-filter',
'vulnerability-conversation-tag-filter',
'vulnerability-task-tag-filter',
'vulnerability-project-filter',
'vulnerability-severity-filter',
'vulnerability-status-filter'
];
fields.forEach(function (id) {
const el = document.getElementById(id);
if (el) el.value = '';
});
vulnerabilityFilters = {
q: '',
id: '',
project_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 vulnDetailProjectField(vuln) {
const label = vulnT('vulnerabilityPage.detailProject');
const hint = escapeHtml(vulnT('vulnerabilityPage.projectBindHint'));
return `
${escapeHtml(label)}
`;
}
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();
if (vulnerabilityFilters.q) {
params.append('q', vulnerabilityFilters.q);
}
const keys = ['id', 'project_id', 'conversation_id', 'task_id', 'conversation_tag', 'task_tag', 'severity', 'status'];
keys.forEach(function (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();
}
});
async function bindVulnerabilityProject(vulnId, projectId, silent) {
if (!vulnId) return;
try {
const response = await apiFetch(`/api/vulnerabilities/${encodeURIComponent(vulnId)}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ project_id: projectId || '' }),
});
if (!response.ok) {
const err = await response.json().catch(() => ({}));
throw new Error(err.error || vulnT('vulnerabilityPage.projectBindFailed'));
}
if (!silent) {
alert(vulnT('vulnerabilityPage.projectBindOk'));
}
loadVulnerabilityStats();
loadVulnerabilities();
} catch (error) {
console.error('绑定项目失败:', error);
alert(vulnT('vulnerabilityPage.projectBindFailed') + ': ' + error.message);
loadVulnerabilities();
}
}
async function refreshVulnerabilityProjectFilter() {
const sel = document.getElementById('vulnerability-project-filter');
if (!sel) return;
try {
const res = await apiFetch('/api/projects?limit=200');
if (!res.ok) return;
const list = await res.json();
if (typeof rebuildProjectNameMap === 'function') {
rebuildProjectNameMap(list);
} else if (typeof projectNameById !== 'undefined') {
list.forEach((p) => { if (p.id) projectNameById[p.id] = p.name || p.id; });
}
const cur = vulnerabilityFilters.project_id || sel.value || '';
let html = '';
(list || []).forEach((p) => {
if (!p.id) return;
const selected = p.id === cur ? ' selected' : '';
const arch = p.status === 'archived' ? ' [归档]' : '';
html += ``;
});
sel.innerHTML = html;
if (cur) sel.value = cur;
const modalSel = document.getElementById('vulnerability-project-id');
if (modalSel && document.getElementById('vulnerability-modal')?.style.display === 'block') {
const modalCur = modalSel.value || '';
modalSel.innerHTML = buildVulnerabilityProjectOptionsHtml(modalCur);
modalSel.value = modalCur;
}
} catch (e) {
console.warn('加载项目筛选列表失败', e);
}
}
function setVulnerabilityProjectFilter(projectId) {
vulnerabilityFilters.project_id = projectId || '';
const sel = document.getElementById('vulnerability-project-filter');
if (sel) sel.value = projectId || '';
applyVulnerabilityFilters();
}
function setVulnerabilityIdFilter(vulnId) {
vulnerabilityFilters.id = vulnId || '';
const el = document.getElementById('vulnerability-exact-id-filter');
if (el) el.value = vulnId || '';
applyVulnerabilityFilters();
}
window.refreshVulnerabilityProjectFilter = refreshVulnerabilityProjectFilter;
window.setVulnerabilityProjectFilter = setVulnerabilityProjectFilter;
window.setVulnerabilityIdFilter = setVulnerabilityIdFilter;
window.bindVulnerabilityProject = bindVulnerabilityProject;
window.buildVulnerabilityProjectOptionsHtml = buildVulnerabilityProjectOptionsHtml;