mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-16 13:19:17 +02:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d9fcfd87e | |||
| 91cb650234 | |||
| 44e7d3b340 | |||
| 531b05299a | |||
| 0de69a6345 |
+1
-1
@@ -10,7 +10,7 @@
|
||||
# ============================================
|
||||
|
||||
# 前端显示的版本号(可选,不填则显示默认版本)
|
||||
version: "v1.5.15"
|
||||
version: "v1.5.16"
|
||||
# 服务器配置
|
||||
server:
|
||||
host: 0.0.0.0 # 监听地址,0.0.0.0 表示监听所有网络接口
|
||||
|
||||
@@ -9289,6 +9289,7 @@ header {
|
||||
margin-bottom: 0;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -13231,6 +13232,9 @@ header {
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
justify-self: end;
|
||||
text-align: right;
|
||||
min-width: 3.5rem;
|
||||
}
|
||||
|
||||
/* External MCP 健康度:能力总览中专门一行的 N/N + 状态徽章 */
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
"lastUpdated": "Last updated",
|
||||
"viewAll": "View all →",
|
||||
"recentVulns": "Recent vulnerabilities",
|
||||
"noVulnYet": "No vulnerabilities yet — start your first scan",
|
||||
"noVulnYet": "No recent vulnerabilities",
|
||||
"capabilities": "Capabilities",
|
||||
"mcpTools": "MCP tools",
|
||||
"rolesLabel": "Roles",
|
||||
@@ -184,7 +184,7 @@
|
||||
"recoCheckMonitorDesc": "View failed request details in MCP monitor",
|
||||
"recoSetupMcp": "Configure your first MCP tool",
|
||||
"recoSetupMcpDesc": "Install MCP server before Agent can invoke specific capabilities",
|
||||
"recoStartScan": "Start your first scan",
|
||||
"recoStartScan": "Start a scan from chat",
|
||||
"recoStartScanDesc": "Describe your target in chat, AI will help execute",
|
||||
"recentEvents": "Recent Events",
|
||||
"eventUntitled": "Event",
|
||||
@@ -193,7 +193,7 @@
|
||||
"mcpPartialDown_one": "{{count}} stopped",
|
||||
"mcpPartialDown_other": "{{count}} stopped",
|
||||
"mcpAllDown": "All stopped",
|
||||
"noVulnDesc": "System looks safe — start a scan to discover potential issues",
|
||||
"noVulnDesc": "This list shows recent records; new results appear here when detection completes in chat",
|
||||
"startScanBtn": "Go to chat to scan"
|
||||
},
|
||||
"chat": {
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
"lastUpdated": "上次更新",
|
||||
"viewAll": "查看全部 →",
|
||||
"recentVulns": "最近漏洞",
|
||||
"noVulnYet": "暂无漏洞,开始你的第一次扫描吧",
|
||||
"noVulnYet": "暂无最近漏洞",
|
||||
"capabilities": "能力总览",
|
||||
"mcpTools": "MCP 工具",
|
||||
"rolesLabel": "角色",
|
||||
@@ -174,7 +174,7 @@
|
||||
"recoCheckMonitorDesc": "在 MCP 监控中查看失败的请求详情",
|
||||
"recoSetupMcp": "配置首个 MCP 工具",
|
||||
"recoSetupMcpDesc": "安装 MCP 服务后 Agent 才能调用具体能力",
|
||||
"recoStartScan": "开始第一次扫描",
|
||||
"recoStartScan": "在对话中发起扫描",
|
||||
"recoStartScanDesc": "在对话中描述目标,让 AI 协助执行",
|
||||
"recentEvents": "最近事件",
|
||||
"eventUntitled": "事件",
|
||||
@@ -182,7 +182,7 @@
|
||||
"mcpAllRunning": "全部运行",
|
||||
"mcpPartialDown": "{{count}} 个未运行",
|
||||
"mcpAllDown": "全部未运行",
|
||||
"noVulnDesc": "系统目前安全,开始一次扫描可以发现潜在问题",
|
||||
"noVulnDesc": "此处展示近期漏洞记录;在对话中完成检测后,新结果会出现在这里",
|
||||
"startScanBtn": "前往对话发起扫描"
|
||||
},
|
||||
"chat": {
|
||||
|
||||
+56
-16
@@ -505,10 +505,34 @@ function setKpiRateBadge(id, rate, failedCount) {
|
||||
}
|
||||
}
|
||||
|
||||
// sessionStorage:告警条「×」忽略记录 + 最近一次**实际展示过**的 reason 片段(不含 level),
|
||||
// 用于在「问题从多变少」(如审完 HITL 后只剩严重漏洞)时,避免误用更早对「仅子集」的忽略。
|
||||
var DASH_SESSION_ALERT_DISMISSED = 'dashboard.dismissedAlert';
|
||||
var DASH_SESSION_ALERT_LAST_REASONS = 'dashboard.alertLastReasons';
|
||||
|
||||
function dashboardAlertReasonKeySetFromJoined(s) {
|
||||
if (!s || typeof s !== 'string') return new Set();
|
||||
return new Set(s.split(',').map(function (x) { return x.trim(); }).filter(Boolean));
|
||||
}
|
||||
|
||||
/** 当前 reason 片段相对上次展示的片段是否为真子集(用于清除过时的忽略) */
|
||||
function dashboardAlertCurrentIsStrictSubsetOfLastShown(currentReasonJoined, lastReasonJoined) {
|
||||
var cur = dashboardAlertReasonKeySetFromJoined(currentReasonJoined);
|
||||
var last = dashboardAlertReasonKeySetFromJoined(lastReasonJoined);
|
||||
if (cur.size === 0 || last.size === 0) return false;
|
||||
if (cur.size >= last.size) return false;
|
||||
var ok = true;
|
||||
cur.forEach(function (k) {
|
||||
if (!last.has(k)) ok = false;
|
||||
});
|
||||
return ok;
|
||||
}
|
||||
|
||||
// 关键提醒条:根据严重情况渲染或隐藏。
|
||||
// - level: danger(红) > warning(橙) > info(蓝),按 reasons 自动取最高级
|
||||
// - 用户点 × 后,把当前 reasons 指纹存入 sessionStorage,本会话内再出现完全相同的内容会自动跳过
|
||||
// - 当 reasons 集合发生变化(如又新增一类问题),指纹失效,banner 重新弹出,避免「忽略后永远不再提醒」
|
||||
// - 若曾展示过「更多类问题」的组合,之后仅部分问题消失,即使指纹与早年忽略相同,也会清除忽略并继续提醒(见 dashboard.alertLastReasons)
|
||||
function renderDashboardAlertBanner(stats) {
|
||||
const banner = document.getElementById('dashboard-alert-banner');
|
||||
const titleEl = document.getElementById('dashboard-alert-title');
|
||||
@@ -552,15 +576,28 @@ function renderDashboardAlertBanner(stats) {
|
||||
banner.hidden = true;
|
||||
banner.classList.remove('is-warning', 'is-danger', 'is-info');
|
||||
dashboardState.dismissedAlertKey = null;
|
||||
try { sessionStorage.removeItem(DASH_SESSION_ALERT_LAST_REASONS); } catch (_) {}
|
||||
return;
|
||||
}
|
||||
|
||||
var fingerprint = level + '|' + reasonKeys.join(',');
|
||||
var reasonPartJoined = reasonKeys.join(',');
|
||||
|
||||
// 检查是否被本会话忽略过同样的内容;若当前仅为「上次曾展示组合」的真子集,则清除忽略(最佳实践:部分处置后仍提醒剩余项)
|
||||
var dismissed = null;
|
||||
try { dismissed = sessionStorage.getItem(DASH_SESSION_ALERT_DISMISSED); } catch (_) {}
|
||||
var lastShownReasons = '';
|
||||
try { lastShownReasons = sessionStorage.getItem(DASH_SESSION_ALERT_LAST_REASONS) || ''; } catch (_) {}
|
||||
|
||||
if (dismissed === fingerprint && dashboardAlertCurrentIsStrictSubsetOfLastShown(reasonPartJoined, lastShownReasons)) {
|
||||
try {
|
||||
sessionStorage.removeItem(DASH_SESSION_ALERT_DISMISSED);
|
||||
dismissed = null;
|
||||
} catch (_) { /* ignore */ }
|
||||
}
|
||||
|
||||
dashboardState.dismissedAlertKey = fingerprint;
|
||||
|
||||
// 检查是否被本会话忽略过同样的内容
|
||||
var dismissed = null;
|
||||
try { dismissed = sessionStorage.getItem('dashboard.dismissedAlert'); } catch (_) {}
|
||||
if (dismissed === fingerprint) {
|
||||
banner.hidden = true;
|
||||
return;
|
||||
@@ -609,6 +646,8 @@ function renderDashboardAlertBanner(stats) {
|
||||
btn.onclick = function () { try { switchPage('mcp-management'); } catch (e) {} };
|
||||
actsEl.appendChild(btn);
|
||||
}
|
||||
|
||||
try { sessionStorage.setItem(DASH_SESSION_ALERT_LAST_REASONS, reasonPartJoined); } catch (_) {}
|
||||
}
|
||||
|
||||
// External MCP 健康度:从 /api/external-mcp/stats 解析出 running / total / down,
|
||||
@@ -662,8 +701,8 @@ function getHitlPendingCount(res) {
|
||||
|
||||
// 「最近事件」内联展示:取通知摘要里最重要的前 N 条
|
||||
// 设计原则:
|
||||
// - 不重复 alert banner / KPI 已经表达过的信息(漏洞、HITL 等会被过滤掉避免冗余)
|
||||
// - 只显示 p0/p1 优先级,p2 作为兜底(当 p0/p1 不够时)
|
||||
// - 不重复 alert banner / KPI 已表达的「新漏洞」通知(vulnerability_created 仍过滤)
|
||||
// - HITL 待审批在推荐操作等处也会提示,但仍在此展示时间线,便于与任务完成等并列查看
|
||||
// - 整个 section 在没有可显示内容时整个隐藏,避免空模块占地方
|
||||
function renderRecentEvents(notifRes) {
|
||||
var section = document.getElementById('dashboard-section-events');
|
||||
@@ -671,8 +710,8 @@ function renderRecentEvents(notifRes) {
|
||||
if (!section || !listEl) return;
|
||||
|
||||
var items = (notifRes && Array.isArray(notifRes.items)) ? notifRes.items : [];
|
||||
// 过滤:只看有意义的事件,去掉 actionable 已处理的、以及类型已经在仪表盘其他位置覆盖的
|
||||
var coveredTypes = { 'vulnerability_created': true, 'hitl_pending': true };
|
||||
// 过滤:去掉新漏洞类型(与「最近漏洞」等板块避免重复);HITL 不再过滤
|
||||
var coveredTypes = { 'vulnerability_created': true };
|
||||
var filtered = items.filter(function (it) {
|
||||
if (!it || !it.type) return false;
|
||||
if (coveredTypes[it.type]) return false;
|
||||
@@ -685,8 +724,8 @@ function renderRecentEvents(notifRes) {
|
||||
var la = levelOrder[a.level] != null ? levelOrder[a.level] : 9;
|
||||
var lb = levelOrder[b.level] != null ? levelOrder[b.level] : 9;
|
||||
if (la !== lb) return la - lb;
|
||||
var ta = a.createdAt || a.created_at || 0;
|
||||
var tb = b.createdAt || b.created_at || 0;
|
||||
var ta = a.ts || a.createdAt || a.created_at || 0;
|
||||
var tb = b.ts || b.createdAt || b.created_at || 0;
|
||||
return new Date(tb).getTime() - new Date(ta).getTime();
|
||||
});
|
||||
|
||||
@@ -701,8 +740,9 @@ function renderRecentEvents(notifRes) {
|
||||
listEl.innerHTML = top.map(function (it) {
|
||||
var level = it.level || 'p2';
|
||||
var title = esc(it.title || it.message || dt('dashboard.eventUntitled', null, '事件'));
|
||||
var msg = esc(it.message || it.summary || '');
|
||||
var when = esc(timeAgoStr(it.createdAt || it.created_at));
|
||||
var msg = esc(it.message || it.summary || it.desc || '');
|
||||
var whenRaw = timeAgoStr(it.ts || it.createdAt || it.created_at);
|
||||
var when = esc(whenRaw || '—');
|
||||
return (
|
||||
'<div class="dashboard-event-item lvl-' + esc(level) + '">' +
|
||||
'<span class="dashboard-event-dot" aria-hidden="true"></span>' +
|
||||
@@ -730,7 +770,7 @@ function renderRecommendedActions(state) {
|
||||
if (state.openCriticalCount > 0) {
|
||||
actions.push({
|
||||
level: 'urgent',
|
||||
icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
|
||||
icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><circle cx="12" cy="17" r="1" fill="currentColor" stroke="none"/></svg>',
|
||||
title: dt('dashboard.recoFixCritical', { count: state.openCriticalCount },
|
||||
'修复 ' + state.openCriticalCount + ' 个待处理严重漏洞'),
|
||||
desc: dt('dashboard.recoFixCriticalDesc', null, '严重等级的漏洞应优先处置'),
|
||||
@@ -784,7 +824,7 @@ function renderRecommendedActions(state) {
|
||||
actions.push({
|
||||
level: 'setup',
|
||||
icon: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>',
|
||||
title: dt('dashboard.recoStartScan', null, '开始第一次扫描'),
|
||||
title: dt('dashboard.recoStartScan', null, '在对话中发起扫描'),
|
||||
desc: dt('dashboard.recoStartScanDesc', null, '在对话中描述目标,让 AI 协助执行'),
|
||||
page: 'chat'
|
||||
});
|
||||
@@ -972,8 +1012,8 @@ function renderRecentVulns(res) {
|
||||
'<span class="dashboard-empty-icon" aria-hidden="true">' +
|
||||
'<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="M9 12l2 2 4-4"/></svg>' +
|
||||
'</span>' +
|
||||
'<div class="dashboard-empty-title">' + esc(dt('dashboard.noVulnYet', null, '暂无漏洞')) + '</div>' +
|
||||
'<div class="dashboard-empty-desc">' + esc(dt('dashboard.noVulnDesc', null, '系统目前安全,开始一次扫描可以发现潜在问题')) + '</div>' +
|
||||
'<div class="dashboard-empty-title">' + esc(dt('dashboard.noVulnYet', null, '暂无最近漏洞')) + '</div>' +
|
||||
'<div class="dashboard-empty-desc">' + esc(dt('dashboard.noVulnDesc', null, '此处展示近期漏洞记录;在对话中完成检测后,新结果会出现在这里')) + '</div>' +
|
||||
'<button type="button" class="dashboard-empty-action" data-action="scan">' +
|
||||
esc(dt('dashboard.startScanBtn', null, '前往对话发起扫描')) + ' →</button>'
|
||||
);
|
||||
@@ -1377,7 +1417,7 @@ document.addEventListener('click', function (ev) {
|
||||
if (!btn) return;
|
||||
ev.preventDefault();
|
||||
var key = dashboardState.dismissedAlertKey || '';
|
||||
try { sessionStorage.setItem('dashboard.dismissedAlert', key); } catch (_) {}
|
||||
try { sessionStorage.setItem(DASH_SESSION_ALERT_DISMISSED, key); } catch (_) {}
|
||||
var banner = document.getElementById('dashboard-alert-banner');
|
||||
if (banner) banner.hidden = true;
|
||||
});
|
||||
|
||||
@@ -287,10 +287,18 @@
|
||||
closeDropdown();
|
||||
return;
|
||||
}
|
||||
dropdown.style.display = 'block';
|
||||
bellBtn.classList.add('active');
|
||||
state.dropdownOpen = true;
|
||||
await refreshNotifications();
|
||||
// 从仪表盘「查看全部」等容器外入口打开时,同一 click 会冒泡到 document,
|
||||
// handleDocumentClick 会误判为「点在外面」并立刻关掉。推迟到宏任务再展开即可。
|
||||
const runOpen = async function () {
|
||||
if (dropdown.style.display !== 'none') return;
|
||||
dropdown.style.display = 'block';
|
||||
bellBtn.classList.add('active');
|
||||
state.dropdownOpen = true;
|
||||
await refreshNotifications();
|
||||
};
|
||||
window.setTimeout(function () {
|
||||
void runOpen();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
async function markAllSeen() {
|
||||
|
||||
+35
-5
@@ -1,6 +1,28 @@
|
||||
// 角色管理相关功能
|
||||
function _t(key, opts) {
|
||||
return typeof window.t === 'function' ? window.t(key, opts) : key;
|
||||
if (typeof window.t === 'function') {
|
||||
try {
|
||||
var translated = window.t(key, opts);
|
||||
if (typeof translated === 'string' && translated && translated !== key) {
|
||||
return translated;
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
// i18n 未就绪或词条缺失时避免把 key 暴露给用户(与 zh-CN 默认一致)
|
||||
if (key === 'roles.noDescription') return '暂无描述';
|
||||
if (key === 'roles.noDescriptionShort') return '无描述';
|
||||
if (key === 'roles.defaultRoleDescription') {
|
||||
return '默认角色,不额外携带用户提示词,使用默认MCP';
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
/** 角色配置中的描述:trim,并把误存为 i18n key 的字面量视为空 */
|
||||
function rolePlainDescription(role) {
|
||||
const raw = typeof role.description === 'string' ? role.description.trim() : '';
|
||||
if (!raw) return '';
|
||||
if (raw === 'roles.noDescription' || raw === 'roles.noDescriptionShort') return '';
|
||||
return raw;
|
||||
}
|
||||
let currentRole = localStorage.getItem('currentRole') || '';
|
||||
let roles = [];
|
||||
@@ -56,6 +78,11 @@ function sortRoles(rolesArray) {
|
||||
|
||||
// 加载所有角色
|
||||
async function loadRoles() {
|
||||
if (window.i18nReady && typeof window.i18nReady.then === 'function') {
|
||||
try {
|
||||
await window.i18nReady;
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
try {
|
||||
const response = await apiFetch('/api/roles');
|
||||
if (!response.ok) {
|
||||
@@ -189,8 +216,9 @@ function renderRoleSelectionSidebar() {
|
||||
const icon = getRoleIcon(role);
|
||||
|
||||
// 处理默认角色的描述
|
||||
let description = role.description || _t('roles.noDescription');
|
||||
if (isDefaultRole && !role.description) {
|
||||
const plainDesc = rolePlainDescription(role);
|
||||
let description = plainDesc || _t('roles.noDescription');
|
||||
if (isDefaultRole && !plainDesc) {
|
||||
description = _t('roles.defaultRoleDescription');
|
||||
}
|
||||
|
||||
@@ -316,6 +344,7 @@ function renderRolesList() {
|
||||
const sortedRoles = sortRoles(filteredRoles);
|
||||
|
||||
rolesList.innerHTML = sortedRoles.map(role => {
|
||||
const plainDesc = rolePlainDescription(role);
|
||||
// 获取角色图标,如果是Unicode转义格式则转换为emoji
|
||||
let roleIcon = role.icon || '👤';
|
||||
if (roleIcon && typeof roleIcon === 'string') {
|
||||
@@ -369,7 +398,7 @@ function renderRolesList() {
|
||||
${role.enabled !== false ? _t('roles.enabled') : _t('roles.disabled')}
|
||||
</span>
|
||||
</div>
|
||||
<div class="role-card-description">${escapeHtml(role.description || _t('roles.noDescriptionShort'))}</div>
|
||||
<div class="role-card-description">${escapeHtml(plainDesc || _t('roles.noDescriptionShort'))}</div>
|
||||
<div class="role-card-tools">
|
||||
<span class="role-card-tools-label">${_t('roleModal.toolsLabel')}</span>
|
||||
<span class="role-card-tools-value">${toolsDisplay}</span>
|
||||
@@ -1575,9 +1604,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
updateRoleSelectorDisplay();
|
||||
});
|
||||
|
||||
// 语言切换后刷新角色选择器显示(默认/自定义角色名)
|
||||
// 语言切换后刷新角色选择器与「选择角色」列表文案
|
||||
document.addEventListener('languagechange', () => {
|
||||
updateRoleSelectorDisplay();
|
||||
renderRoleSelectionSidebar();
|
||||
});
|
||||
|
||||
// 获取当前选中的角色(供chat.js使用)
|
||||
|
||||
@@ -405,10 +405,13 @@ async function loadToolsList(page = 1, searchKeyword = '') {
|
||||
}
|
||||
}
|
||||
|
||||
// 每行有两类复选框:行首「启用工具」与名称旁「常驻」;统计/全选只应针对行首启用复选框
|
||||
const TOOL_ENABLE_CHECKBOX_SELECTOR = '#tools-list .tool-item > input[type="checkbox"]';
|
||||
|
||||
// 保存当前页的工具状态到全局映射
|
||||
function saveCurrentPageToolStates() {
|
||||
document.querySelectorAll('#tools-list .tool-item').forEach(item => {
|
||||
const checkbox = item.querySelector('input[type="checkbox"]');
|
||||
const checkbox = item.querySelector(':scope > input[type="checkbox"]');
|
||||
const toolKey = item.dataset.toolKey; // 使用唯一标识符
|
||||
const toolName = item.dataset.toolName;
|
||||
const isExternal = item.dataset.isExternal === 'true';
|
||||
@@ -745,7 +748,7 @@ function handleToolAlwaysVisibleChange(toolName, alwaysVisible) {
|
||||
|
||||
// 全选工具
|
||||
function selectAllTools() {
|
||||
document.querySelectorAll('#tools-list input[type="checkbox"]').forEach(checkbox => {
|
||||
document.querySelectorAll(TOOL_ENABLE_CHECKBOX_SELECTOR).forEach(checkbox => {
|
||||
checkbox.checked = true;
|
||||
// 更新全局状态映射
|
||||
const toolItem = checkbox.closest('.tool-item');
|
||||
@@ -769,7 +772,7 @@ function selectAllTools() {
|
||||
|
||||
// 全不选工具
|
||||
function deselectAllTools() {
|
||||
document.querySelectorAll('#tools-list input[type="checkbox"]').forEach(checkbox => {
|
||||
document.querySelectorAll(TOOL_ENABLE_CHECKBOX_SELECTOR).forEach(checkbox => {
|
||||
checkbox.checked = false;
|
||||
// 更新全局状态映射
|
||||
const toolItem = checkbox.closest('.tool-item');
|
||||
@@ -826,9 +829,9 @@ async function updateToolsStats() {
|
||||
// 先保存当前页的状态到全局映射
|
||||
saveCurrentPageToolStates();
|
||||
|
||||
// 计算当前页的启用工具数
|
||||
const currentPageEnabled = Array.from(document.querySelectorAll('#tools-list input[type="checkbox"]:checked')).length;
|
||||
const currentPageTotal = document.querySelectorAll('#tools-list input[type="checkbox"]').length;
|
||||
// 计算当前页的启用工具数(仅行首「启用」复选框,不含「常驻」)
|
||||
const currentPageEnabled = Array.from(document.querySelectorAll(`${TOOL_ENABLE_CHECKBOX_SELECTOR}:checked`)).length;
|
||||
const currentPageTotal = document.querySelectorAll(TOOL_ENABLE_CHECKBOX_SELECTOR).length;
|
||||
|
||||
// 计算所有工具的启用数
|
||||
let totalEnabled = 0;
|
||||
|
||||
@@ -498,7 +498,7 @@
|
||||
<a class="dashboard-section-link" onclick="switchPage('vulnerabilities')" data-i18n="dashboard.viewAll">查看全部 →</a>
|
||||
</div>
|
||||
<div class="dashboard-recent-vulns" id="dashboard-recent-vulns">
|
||||
<div class="dashboard-recent-vulns-empty" id="dashboard-recent-vulns-empty" data-i18n="dashboard.noVulnYet">暂无漏洞,开始你的第一次扫描吧</div>
|
||||
<div class="dashboard-recent-vulns-empty" id="dashboard-recent-vulns-empty" data-i18n="dashboard.noVulnYet">暂无最近漏洞</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="dashboard-section dashboard-section-overview">
|
||||
|
||||
Reference in New Issue
Block a user