diff --git a/web/static/css/c2.css b/web/static/css/c2.css index 0aedb3e2..6d885d5f 100644 --- a/web/static/css/c2.css +++ b/web/static/css/c2.css @@ -37,7 +37,6 @@ Form Controls (scoped to C2 pages) ============================================================================ */ -#page-c2 .form-control, #page-c2-listeners .form-control, #page-c2-sessions .form-control, #page-c2-tasks .form-control, @@ -61,7 +60,6 @@ appearance: none; } -#page-c2 .form-control:focus, #page-c2-listeners .form-control:focus, #page-c2-sessions .form-control:focus, #page-c2-tasks .form-control:focus, @@ -73,7 +71,6 @@ box-shadow: 0 0 0 3px var(--c2-accent-dim); } -#page-c2 select.form-control, #page-c2-payloads select.form-control, .c2-modal select.form-control { background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%2364748b' d='M2.5 4.5L6 8l3.5-3.5'/%3E%3C/svg%3E"); @@ -85,7 +82,6 @@ } /* 原生下拉:避免 appearance:none 在部分浏览器中导致 select 无法正常展开 */ -#page-c2 select.form-control.c2-native-select, #page-c2-payloads select.form-control.c2-native-select, .c2-modal select.form-control.c2-native-select { appearance: auto; @@ -94,7 +90,6 @@ padding-right: 14px; } -#page-c2 textarea.form-control, #page-c2-payloads textarea.form-control, .c2-modal textarea.form-control { resize: vertical; @@ -104,7 +99,6 @@ line-height: 1.6; } -#page-c2 .form-control::placeholder, #page-c2-payloads .form-control::placeholder, .c2-modal .form-control::placeholder { color: var(--c2-text-muted); @@ -140,9 +134,6 @@ Layout ============================================================================ */ -.c2-layout { display: flex; flex-direction: column; height: 100%; } -.c2-main { flex: 1; overflow-y: auto; } - .c2-empty { display: flex; flex-direction: column; @@ -171,103 +162,6 @@ margin: 12px; } -/* ============================================================================ - Dashboard / Welcome - ============================================================================ */ - -.c2-welcome { - text-align: center; - padding: 100px 24px 80px; - max-width: 860px; - margin: 0 auto; -} - -.c2-welcome-icon { - margin-bottom: 16px; - animation: c2-float 4s ease-in-out infinite; -} - -@keyframes c2-float { - 0%, 100% { transform: translateY(0); } - 50% { transform: translateY(-8px); } -} - -.c2-welcome h3 { - font-size: 28px; - margin-bottom: 12px; - color: var(--c2-text); - font-weight: 800; - letter-spacing: -0.5px; -} - -.c2-welcome p { - color: var(--c2-text-dim); - font-size: 15px; - line-height: 1.7; - margin-bottom: 48px; - max-width: 520px; - margin-left: auto; - margin-right: auto; -} - -.c2-stats { - display: flex; - justify-content: center; - gap: 16px; - margin-bottom: 48px; - flex-wrap: wrap; -} - -.c2-stat-item { - display: flex; - flex-direction: column; - align-items: center; - padding: 28px 40px; - background: var(--c2-surface); - border-radius: var(--c2-radius); - border: 1.5px solid var(--c2-border); - min-width: 160px; - transition: all 0.3s ease; -} - -.c2-stat-item:hover { - transform: translateY(-4px); - box-shadow: var(--c2-shadow-md); - border-color: var(--c2-accent); -} - -.c2-stat-item:nth-child(1) .c2-stat-value { color: var(--c2-accent); } -.c2-stat-item:nth-child(2) .c2-stat-value { color: var(--c2-green); } -.c2-stat-item:nth-child(3) .c2-stat-value { color: var(--c2-amber); } - -.c2-stat-value { - font-size: 36px; - font-weight: 800; - line-height: 1; - letter-spacing: -1px; -} - -.c2-stat-label { - font-size: 12px; - color: var(--c2-text-dim); - margin-top: 12px; - font-weight: 600; - letter-spacing: 0.3px; -} - -.c2-actions { - display: flex; - gap: 12px; - justify-content: center; - flex-wrap: wrap; - max-width: 420px; - margin-inline: auto; -} - -.c2-actions > button { - flex: 1; - min-width: min(100%, 160px); -} /* ============================================================================ Listener Cards ============================================================================ */ @@ -1590,7 +1484,6 @@ border-right: none; border-bottom: 1px solid var(--c2-border); } - .c2-stats { flex-direction: column; gap: 12px; } .c2-payload-grid { grid-template-columns: 1fr; } .c2-listener-grid { grid-template-columns: 1fr; padding: 16px; } .c2-task-detail-grid { grid-template-columns: 1fr; } diff --git a/web/static/css/style.css b/web/static/css/style.css index 2434d052..e7e96f48 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -15971,6 +15971,255 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible { transform: translateX(2px); } +/* 最近漏洞 / 近期事实 Tab */ +.dashboard-section-header--tabs { + align-items: center; + gap: 12px; +} + +.dashboard-feed-tabs { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 3px; + background: #f1f5f9; + border-radius: 10px; + border: 1px solid rgba(15, 23, 42, 0.06); +} + +.dashboard-feed-tab { + padding: 7px 14px; + border: none; + background: transparent; + color: #64748b; + border-radius: 8px; + cursor: pointer; + font-size: 0.8125rem; + font-weight: 600; + line-height: 1.2; + transition: background 0.15s, color 0.15s, box-shadow 0.15s; +} + +.dashboard-feed-tab:hover { + color: #0f172a; +} + +.dashboard-feed-tab.is-active { + color: #0066ff; + background: #fff; + box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08); +} + +.dashboard-feed-tab-badge { + margin-left: 4px; + font-size: 0.75rem; + font-weight: 600; + color: #64748b; + font-variant-numeric: tabular-nums; +} + +.dashboard-feed-tab.is-active .dashboard-feed-tab-badge { + color: #0066ff; +} + +.dashboard-feed-tab:focus-visible { + outline: 2px solid rgba(0, 102, 255, 0.45); + outline-offset: 2px; +} + +.dashboard-feed-panel[hidden] { + display: none !important; +} + +.dashboard-recent-facts { + display: flex; + flex-direction: column; + gap: 4px; + min-height: 60px; + width: 100%; + min-width: 0; + align-items: stretch; +} + +.dashboard-recent-facts-empty { + text-align: center; + color: var(--text-secondary); + padding: 28px 12px; + font-size: 0.875rem; + background: #fafbfc; + border-radius: 10px; + border: 1px dashed rgba(0, 0, 0, 0.08); +} + +.dashboard-recent-facts-empty.is-rich { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 28px 16px; + text-align: center; + gap: 8px; +} + +.dashboard-recent-facts-meta { + font-size: 0.75rem; + color: var(--text-secondary, #6b7280); + padding: 2px 4px 8px; + font-variant-numeric: tabular-nums; +} + +.dashboard-recent-fact-item { + display: grid; + /* 置顶 / 分类 / 置信度 固定列宽,保证各行对齐 */ + grid-template-columns: 20px 64px 56px minmax(0, 1.4fr) minmax(0, 1fr) 9.5rem; + align-items: center; + column-gap: 10px; + padding: 12px 10px; + border-radius: 8px; + cursor: pointer; + transition: background 0.15s; + text-decoration: none; + color: inherit; + border-bottom: 1px solid #f3f4f6; + width: 100%; + max-width: 100%; + min-width: 0; + box-sizing: border-box; +} + +.dashboard-recent-fact-item:last-child { + border-bottom: none; +} + +.dashboard-recent-fact-item:hover { + background: rgba(0, 102, 255, 0.04); +} + +.dashboard-recent-fact-item:focus-visible { + outline: 2px solid rgba(0, 102, 255, 0.5); + outline-offset: 2px; +} + +.dashboard-recent-fact-pin { + width: 20px; + flex-shrink: 0; + font-size: 0.75rem; + line-height: 1; + text-align: center; + justify-self: center; +} + +.dashboard-recent-fact-cat, +.dashboard-recent-fact-conf { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 3px 6px; + border-radius: 6px; + font-size: 0.6875rem; + font-weight: 700; + line-height: 1.2; + white-space: nowrap; + justify-self: start; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; +} + +.dashboard-recent-fact-cat { + width: 64px; + box-sizing: border-box; +} + +.dashboard-recent-fact-conf { + width: 56px; + box-sizing: border-box; +} + +.dashboard-recent-fact-cat { + background: rgba(99, 102, 241, 0.1); + color: #4338ca; +} + +.dashboard-recent-fact-cat.cat-finding, +.dashboard-recent-fact-cat.cat-vuln, +.dashboard-recent-fact-cat.cat-exploit, +.dashboard-recent-fact-cat.cat-poc, +.dashboard-recent-fact-cat.cat-chain { + background: rgba(239, 68, 68, 0.1); + color: #b91c1c; +} + +.dashboard-recent-fact-cat.cat-target, +.dashboard-recent-fact-cat.cat-env, +.dashboard-recent-fact-cat.cat-auth { + background: rgba(14, 165, 233, 0.12); + color: #0369a1; +} + +.dashboard-recent-fact-conf.conf-confirmed { + background: rgba(34, 197, 94, 0.12); + color: #15803d; +} + +.dashboard-recent-fact-conf.conf-tentative { + background: rgba(245, 158, 11, 0.12); + color: #b45309; +} + +.dashboard-recent-fact-summary { + font-weight: 600; + color: var(--text-primary); + font-size: 0.875rem; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.dashboard-recent-fact-meta { + color: var(--text-secondary); + font-size: 0.8125rem; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace; +} + +.dashboard-recent-fact-time { + color: var(--text-secondary); + font-size: 0.75rem; + text-align: left; + white-space: nowrap; + font-variant-numeric: tabular-nums; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; +} + +@media (max-width: 900px) { + .dashboard-recent-fact-item { + grid-template-columns: 20px 64px minmax(0, 1fr) auto 8.25rem; + } + .dashboard-recent-fact-conf { display: none; } +} + +@media (max-width: 720px) { + .dashboard-recent-fact-item { + grid-template-columns: 20px 64px minmax(0, 1fr) auto; + } + .dashboard-recent-fact-meta { display: none; } +} + +@media (max-width: 480px) { + .dashboard-recent-fact-item { + grid-template-columns: 20px minmax(0, 1fr) auto; + } + .dashboard-recent-fact-cat { display: none; } + .dashboard-recent-fact-time { display: none; } +} + /* 最近漏洞列表 */ .dashboard-recent-vulns { display: flex; diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index 7241012a..118f5497 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -79,7 +79,6 @@ "settings": "System settings", "hitl": "Human-in-the-loop", "c2": "C2", - "c2Manage": "C2 management", "c2Listeners": "Listeners", "c2Sessions": "Sessions", "c2Tasks": "Tasks", @@ -153,7 +152,14 @@ "lastUpdated": "Last updated", "viewAll": "View all →", "recentVulns": "Recent vulnerabilities", + "recentFacts": "Recent facts", "noVulnYet": "No recent vulnerabilities", + "noFactsYet": "No recent facts", + "noFactsDesc": "In project-bound chats, the agent records targets, findings, and attack chains; new facts appear here", + "createFirstProjectBtn": "Create first project", + "factProjectMeta": "{{project}} · {{key}}", + "factsAcrossProjects_one": "{{count}} active project · {{facts}} facts", + "factsAcrossProjects_other": "{{count}} active projects · {{facts}} facts", "capabilities": "Capabilities", "mcpTools": "MCP tools", "rolesLabel": "Roles", @@ -377,6 +383,7 @@ "settingsIntroTitle": "Project settings", "settingsIntroHint": "Configure project metadata and Agent authorization boundary; takes effect immediately for bound conversations after saving.", "pinProject": "Pin project (show first in list)", + "pinFact": "Pin fact (prioritize in list and blackboard index)", "editDescriptionPlaceholder": "Targets, authorization scope, contacts, notes…", "scopeTitle": "Test scope", "scopeHint": "JSON format for Agent authorization boundary and target assets", @@ -2529,14 +2536,6 @@ "checkboxLinkTitle": "Check to link this tool to this role" }, "c2": { - "title": "C2 Management", - "welcomeTitle": "AI-Native C2 Framework", - "welcomeDesc": "MCP-native design: let LLM call C2 like calling nmap to complete the full chain: initial access → control → tasks → lateral movement → cleanup", - "statListeners": "Running Listeners", - "statSessions": "Online Sessions", - "statPending": "Pending Tasks", - "goListeners": "Manage Listeners", - "goSessions": "View Sessions", "clipboardCopied": "Copied to clipboard", "fmt": { "durationMs": "{{n}}ms", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index d7ee6d96..7dbf2652 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -79,7 +79,6 @@ "settings": "系统设置", "hitl": "人机协同", "c2": "C2", - "c2Manage": "C2 管理", "c2Listeners": "监听器", "c2Sessions": "会话", "c2Tasks": "任务", @@ -153,7 +152,13 @@ "lastUpdated": "上次更新", "viewAll": "查看全部 →", "recentVulns": "最近漏洞", + "recentFacts": "近期事实", "noVulnYet": "暂无最近漏洞", + "noFactsYet": "暂无近期事实", + "noFactsDesc": "在绑定项目的对话中,Agent 会自动记录目标、漏洞、攻击链等事实;新事实会出现在这里", + "createFirstProjectBtn": "创建第一个项目", + "factProjectMeta": "{{project}} · {{key}}", + "factsAcrossProjects": "{{count}} 个活跃项目 · {{facts}} 条事实", "capabilities": "能力总览", "mcpTools": "MCP 工具", "rolesLabel": "角色", @@ -366,6 +371,7 @@ "settingsIntroTitle": "项目设置", "settingsIntroHint": "配置项目元数据与 Agent 授权边界,保存后即时生效于绑定对话。", "pinProject": "置顶项目(列表优先显示)", + "pinFact": "置顶事实(列表与黑板索引优先)", "editDescriptionPlaceholder": "测试目标、授权范围、联系人、注意事项…", "scopeTitle": "测试范围", "scopeHint": "JSON 格式,供 Agent 理解授权边界与目标资产", @@ -2518,14 +2524,6 @@ "checkboxLinkTitle": "勾选表示本角色关联使用该工具" }, "c2": { - "title": "C2 管理", - "welcomeTitle": "AI-Native C2 框架", - "welcomeDesc": "以 MCP 工具为一等公民,让 LLM 可以像调用 nmap 一样调用 C2 完成「上线 → 控制 → 任务 → 横向 → 清场」全流程", - "statListeners": "运行中监听器", - "statSessions": "在线会话", - "statPending": "待审任务", - "goListeners": "管理监听器", - "goSessions": "查看会话", "clipboardCopied": "已复制到剪贴板", "fmt": { "durationMs": "{{n}}ms", diff --git a/web/static/js/c2.js b/web/static/js/c2.js index a263a3d4..ce2d1389 100644 --- a/web/static/js/c2.js +++ b/web/static/js/c2.js @@ -321,7 +321,6 @@ } switch(pageId) { - case 'c2': case 'c2-listeners': C2.loadListeners(); break; @@ -370,7 +369,6 @@ C2.profiles = pdata.profiles; } C2.renderListeners(); - C2.updateDashboardStats(); }); }; @@ -736,7 +734,6 @@ return apiRequest('GET', `${API_BASE}/sessions`).then(data => { C2.sessions = data.sessions || []; C2.renderSessions(); - C2.updateDashboardStats(); }); }; @@ -2037,7 +2034,6 @@ C2.renderTasks(); C2.renderTasksPagination(); C2.syncTasksToolbar(); - C2.updateDashboardStats(); }).catch(err => { showToast(err.message || String(err), 'error'); }); @@ -2163,7 +2159,6 @@ const tasks = data.tasks || []; if (typeof data.pending_queued_count === 'number') { C2.tasksPendingQueuedCount = data.pending_queued_count; - C2.updateDashboardStats(); } if (!container) return; @@ -2819,7 +2814,6 @@ showToast(`[${event.category}] ${event.message}`, event.level === 'critical' ? 'error' : 'info'); } - C2.updateDashboardStats(); }; // ============================================================================ @@ -2953,26 +2947,6 @@ }); }; - // ============================================================================ - // 仪表盘 - // ============================================================================ - - C2.updateDashboardStats = function() { - const runningListeners = C2.listeners.filter(l => l.status === 'running').length; - const activeSessions = C2.sessions.filter(s => s.status === 'active').length; - const pendingTasks = typeof C2.tasksPendingQueuedCount === 'number' - ? C2.tasksPendingQueuedCount - : C2.tasks.filter(t => t.status === 'queued' || t.status === 'pending').length; - - const elListeners = document.getElementById('c2-stat-listeners'); - const elSessions = document.getElementById('c2-stat-sessions'); - const elPending = document.getElementById('c2-stat-pending'); - - if (elListeners) elListeners.textContent = runningListeners; - if (elSessions) elSessions.textContent = activeSessions; - if (elPending) elPending.textContent = pendingTasks; - }; - // ============================================================================ // 模态框 // ============================================================================ diff --git a/web/static/js/dashboard.js b/web/static/js/dashboard.js index 8ac5eb19..96c6008d 100644 --- a/web/static/js/dashboard.js +++ b/web/static/js/dashboard.js @@ -21,6 +21,8 @@ var dashboardState = { lastUpdatedAt: 0, // 上次成功刷新的时间戳(ms) dismissedAlertKey: null, // 当前会话中被用户「×」掉的告警内容指纹(同样的 reasons 不再弹) lastResources: null, // 上一轮关键资源快照,用于判断是否首次有数据 / 智能 CTA + recentFeedTab: 'vulns', // 最近漏洞 / 近期事实 Tab + lastProjectSummary: null, // 最近一次项目仪表盘摘要(供 Tab 切换时重绘) }; async function refreshDashboard() { @@ -57,6 +59,7 @@ async function refreshDashboard() { hideEl('dashboard-kpi-vuln-critical-badge'); hideEl('dashboard-alert-banner'); setRecentVulnsLoading(); + setRecentFactsLoading(); ['tools', 'skills', 'knowledge', 'roles', 'agents', 'webshell'].forEach(function (k) { setEl('dashboard-resource-' + k, '…'); }); @@ -104,7 +107,8 @@ async function refreshDashboard() { openCriticalRes, openHighRes, openMediumRes, openLowRes, toolsConfigRes, hitlPendingRes, notificationsRes, externalMcpStatsRes, webshellRes, - c2ListenersRes, c2SessionsRes, c2TasksRes + c2ListenersRes, c2SessionsRes, c2TasksRes, + projectSummaryRes ] = await Promise.all([ fetchJson('/api/agent-loop/tasks'), fetchJson('/api/vulnerabilities/stats'), @@ -134,7 +138,8 @@ async function refreshDashboard() { // C2 仪表盘条:监听器 / 会话 / 待处理任务(任务接口含 pending_queued_count) fetchJson('/api/c2/listeners'), fetchJson('/api/c2/sessions?limit=500'), - fetchJson('/api/c2/tasks?page=1&page_size=1') + fetchJson('/api/c2/tasks?page=1&page_size=1'), + fetchJson('/api/projects/dashboard-summary?fact_limit=5') ]); // 如果在 await 期间 controller 已被 abort,说明又有新刷新启动了,丢弃本次结果 @@ -387,6 +392,9 @@ async function refreshDashboard() { // 最近漏洞列表 renderRecentVulns(recentVulnsRes); + dashboardState.lastProjectSummary = projectSummaryRes; + renderRecentFacts(projectSummaryRes); + updateDashboardFeedTabBadge(projectSummaryRes); // External MCP 健康度(同时拿到 down 数喂给 alert banner / 推荐操作) var externalMcpDown = renderExternalMcpHealth(externalMcpStatsRes); @@ -454,6 +462,7 @@ async function refreshDashboard() { var c2secErr = document.getElementById('dashboard-section-c2'); if (c2secErr) c2secErr.hidden = true; setRecentVulnsError(); + setRecentFactsError(); renderDashboardToolsBar(null); var ph = document.getElementById('dashboard-tools-pie-placeholder'); if (ph) { ph.style.removeProperty('display'); ph.textContent = (typeof window.t === 'function' ? window.t('dashboard.noCallData') : '暂无调用数据'); } @@ -1130,6 +1139,215 @@ function renderRecentVulns(res) { }); } +// 最近漏洞 / 近期事实 Tab 切换(共用列表区域,查看全部链接随 Tab 变化) +function switchDashboardFeedTab(tab) { + tab = tab === 'facts' ? 'facts' : 'vulns'; + dashboardState.recentFeedTab = tab; + + var tabVulns = document.getElementById('dashboard-feed-tab-vulns'); + var tabFacts = document.getElementById('dashboard-feed-tab-facts'); + var panelVulns = document.getElementById('dashboard-feed-panel-vulns'); + var panelFacts = document.getElementById('dashboard-feed-panel-facts'); + if (tabVulns) { + tabVulns.classList.toggle('is-active', tab === 'vulns'); + tabVulns.setAttribute('aria-selected', tab === 'vulns' ? 'true' : 'false'); + } + if (tabFacts) { + tabFacts.classList.toggle('is-active', tab === 'facts'); + tabFacts.setAttribute('aria-selected', tab === 'facts' ? 'true' : 'false'); + } + if (panelVulns) panelVulns.hidden = tab !== 'vulns'; + if (panelFacts) panelFacts.hidden = tab !== 'facts'; + updateDashboardFeedViewAll(tab); +} + +function updateDashboardFeedViewAll(tab) { + var link = document.getElementById('dashboard-feed-view-all'); + if (!link) return; + if (tab === 'facts') { + link.onclick = function () { try { switchPage('projects'); } catch (_) {} }; + } else { + link.onclick = function () { try { switchPage('vulnerabilities'); } catch (_) {} }; + } +} + +function updateDashboardFeedTabBadge(summaryRes) { + var badge = document.getElementById('dashboard-feed-tab-facts-badge'); + if (!badge) return; + var facts = (summaryRes && Array.isArray(summaryRes.recent_facts)) ? summaryRes.recent_facts.length : 0; + if (facts > 0) { + badge.hidden = false; + badge.textContent = '(' + facts + ')'; + } else { + badge.hidden = true; + badge.textContent = ''; + } +} + +function setRecentFactsLoading() { + var wrap = document.getElementById('dashboard-recent-facts'); + var empty = document.getElementById('dashboard-recent-facts-empty'); + if (!wrap) return; + clearRecentFactsList(wrap); + if (empty) { + empty.hidden = false; + empty.classList.remove('is-rich'); + empty.textContent = dt('common.loading', null, '加载中…'); + } +} + +function clearRecentFactsList(wrap) { + if (!wrap) return; + Array.from(wrap.querySelectorAll('.dashboard-recent-fact-item, .dashboard-recent-facts-meta')).forEach(function (n) { n.remove(); }); +} + +function setRecentFactsError() { + var wrap = document.getElementById('dashboard-recent-facts'); + var empty = document.getElementById('dashboard-recent-facts-empty'); + if (!wrap) return; + clearRecentFactsList(wrap); + if (empty) { + empty.hidden = false; + empty.classList.remove('is-rich'); + empty.textContent = dt('common.loadFailed', null, '加载失败'); + } +} + +function factConfidenceShortLabel(confidence) { + var c = String(confidence || '').toLowerCase(); + if (c === 'confirmed') return dt('projects.confidenceConfirmed', null, '已确认'); + if (c === 'tentative') return dt('projects.confidenceTentative', null, '待确认'); + return c || '—'; +} + +function factCategoryShortLabel(category) { + var raw = String(category || '').trim(); + return raw || 'note'; +} + +function openProjectFactFromDashboard(projectId, factKey) { + if (!projectId) return; + if (typeof switchPage === 'function') { + switchPage('projects'); + } + setTimeout(async function () { + if (typeof window.initProjectsPage === 'function') { + await window.initProjectsPage(); + } + if (typeof window.selectProject === 'function') { + await window.selectProject(projectId); + } + if (typeof window.switchProjectTab === 'function') { + window.switchProjectTab('facts'); + } + if (factKey && typeof window.viewProjectFactBody === 'function') { + window.viewProjectFactBody(factKey); + } + }, 350); +} + +function renderRecentFacts(res) { + var wrap = document.getElementById('dashboard-recent-facts'); + var empty = document.getElementById('dashboard-recent-facts-empty'); + if (!wrap) return; + + clearRecentFactsList(wrap); + + var list = (res && Array.isArray(res.recent_facts)) ? res.recent_facts : []; + var totals = (res && res.totals) ? res.totals : {}; + var activeProjects = totals.active_projects || 0; + var totalFacts = totals.total_facts || 0; + + if (list.length === 0) { + if (empty) { + empty.hidden = false; + empty.classList.add('is-rich'); + var desc = activeProjects > 0 + ? dt('dashboard.noFactsDesc', null, '在绑定项目的对话中,Agent 会自动记录目标、漏洞、攻击链等事实;新事实会出现在这里') + : dt('projects.selectOrCreateHint', null, '项目用于跨对话共享「事实黑板」:目标、环境、认证等信息会在绑定项目的对话中自动注入。'); + var ctaLabel = activeProjects > 0 + ? dt('dashboard.goToChat', null, '前往对话') + : dt('dashboard.createFirstProjectBtn', null, '创建第一个项目'); + var ctaAction = activeProjects > 0 ? 'chat' : 'project'; + empty.innerHTML = ( + '' + + '
' + esc(dt('dashboard.noFactsYet', null, '暂无近期事实')) + '
' + + '
' + esc(desc) + '
' + + '' + ); + var btn = empty.querySelector('[data-action]'); + if (btn) { + btn.onclick = function () { + var action = btn.getAttribute('data-action'); + if (action === 'project') { + try { switchPage('projects'); } catch (_) {} + setTimeout(function () { + if (typeof window.showNewProjectModal === 'function') { + window.showNewProjectModal(); + } + }, 350); + } else { + try { switchPage('chat'); } catch (_) {} + } + }; + } + } + return; + } + + if (empty) { + empty.hidden = true; + empty.classList.remove('is-rich'); + } + + if (activeProjects > 0 || totalFacts > 0) { + var meta = document.createElement('div'); + meta.className = 'dashboard-recent-facts-meta'; + meta.textContent = dt('dashboard.factsAcrossProjects', { count: activeProjects, facts: totalFacts }, + activeProjects + ' 个活跃项目 · ' + totalFacts + ' 条事实'); + wrap.appendChild(meta); + } + + list.slice(0, 5).forEach(function (f) { + if (!f) return; + var category = factCategoryShortLabel(f.category); + var confidence = String(f.confidence || 'tentative').toLowerCase(); + var item = document.createElement('a'); + item.className = 'dashboard-recent-fact-item'; + item.setAttribute('role', 'button'); + item.tabIndex = 0; + var pid = f.project_id || ''; + var fkey = f.fact_key || ''; + item.onclick = function () { openProjectFactFromDashboard(pid, fkey); }; + item.onkeydown = function (e) { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + item.click(); + } + }; + + // 置顶列始终占位,避免有/无图钉时后续列错位 + var pinMark = ''; + var categoryBadge = '' + esc(category) + ''; + var confBadge = '' + esc(factConfidenceShortLabel(confidence)) + ''; + var summary = '' + esc(f.summary || dt('common.untitled', null, '无标题')) + ''; + // 勿用 i18n 插值拼接 fact_key:i18next 会把 / 转成 / 导致乱码 + var projectLabel = (f.project_name || '').trim() || dt('projects.defaultProjectName', null, '项目'); + var factKeyLabel = (f.fact_key || '').trim() || '—'; + var metaText = projectLabel + ' · ' + factKeyLabel; + var metaLine = '' + esc(metaText) + ''; + var time = '' + esc(timeAgoStr(f.updated_at)) + ''; + + item.innerHTML = pinMark + categoryBadge + confBadge + summary + metaLine + time; + wrap.appendChild(item); + }); +} + // 漏洞状态映射:把 status 字符串规整到 4 类(避免脏数据) function statusKey(s) { s = String(s || '').toLowerCase(); diff --git a/web/static/js/projects.js b/web/static/js/projects.js index c3a36fb0..83bb8b92 100644 --- a/web/static/js/projects.js +++ b/web/static/js/projects.js @@ -574,8 +574,11 @@ async function loadProjectFacts() { const vulnLink = f.related_vulnerability_id ? `${escapeHtml(f.related_vulnerability_id.slice(0, 8))}…` : ''; + const pinBadge = f.pinned + ? `${escapeHtml(tp('projects.pinned'))}` + : ''; return ` - ${keyEsc}${vulnLink} + ${keyEsc}${pinBadge}${vulnLink} ${formatCategoryBadge(f.category)} ${escapeHtml(f.summary)} ${formatFactBodyBadge(f)} @@ -1165,6 +1168,8 @@ function resetFactModalForm() { document.getElementById('fact-modal-summary').value = ''; document.getElementById('fact-modal-body').value = ''; document.getElementById('fact-modal-confidence').value = 'tentative'; + const pinEl = document.getElementById('fact-modal-pinned'); + if (pinEl) pinEl.checked = false; const rel = document.getElementById('fact-modal-related-vuln'); if (rel) rel.value = ''; updateFactFormHints(); @@ -1198,6 +1203,8 @@ function fillFactModalForm(f) { } const rel = document.getElementById('fact-modal-related-vuln'); if (rel) rel.value = f.related_vulnerability_id || ''; + const pinEl = document.getElementById('fact-modal-pinned'); + if (pinEl) pinEl.checked = !!f.pinned; updateFactFormHints(); } @@ -1242,6 +1249,7 @@ async function saveFactModal() { summary, body, confidence: document.getElementById('fact-modal-confidence').value, + pinned: !!document.getElementById('fact-modal-pinned')?.checked, related_vulnerability_id: document.getElementById('fact-modal-related-vuln')?.value?.trim() || '', }; const editId = window._factModalEditId; diff --git a/web/static/js/router.js b/web/static/js/router.js index 7aae8b5e..b9b5b7b5 100644 --- a/web/static/js/router.js +++ b/web/static/js/router.js @@ -56,8 +56,9 @@ function initRouter() { const hash = window.location.hash.slice(1); if (hash) { const hashParts = hash.split('?'); - const pageId = hashParts[0]; - if (pageId && ['dashboard', 'chat', 'hitl', 'info-collect', 'projects', 'vulnerabilities', 'webshell', 'chat-files', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'skills-monitor', 'skills-management', 'agents-management', 'settings', 'tasks', 'c2', 'c2-listeners', 'c2-sessions', 'c2-tasks', 'c2-payloads', 'c2-events', 'c2-profiles'].includes(pageId)) { + let pageId = hashParts[0]; + if (pageId === 'c2') pageId = 'c2-listeners'; + if (pageId && ['dashboard', 'chat', 'hitl', 'info-collect', 'projects', 'vulnerabilities', 'webshell', 'chat-files', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'skills-monitor', 'skills-management', 'agents-management', 'settings', 'tasks', 'c2-listeners', 'c2-sessions', 'c2-tasks', 'c2-payloads', 'c2-events', 'c2-profiles'].includes(pageId)) { switchPage(pageId); if (pageId === 'chat') { scheduleChatConversationFromHash(500); @@ -464,7 +465,6 @@ async function initPage(pageId) { loadMarkdownAgents(); } break; - case 'c2': case 'c2-listeners': case 'c2-sessions': case 'c2-tasks': @@ -494,9 +494,10 @@ document.addEventListener('DOMContentLoaded', function() { const hash = window.location.hash.slice(1); // 处理带参数的hash(如 chat?conversation=xxx) const hashParts = hash.split('?'); - const pageId = hashParts[0]; + let pageId = hashParts[0]; - if (pageId && ['dashboard', 'chat', 'hitl', 'info-collect', 'tasks', 'vulnerabilities', 'webshell', 'chat-files', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'skills-monitor', 'skills-management', 'agents-management', 'settings', 'c2', 'c2-listeners', 'c2-sessions', 'c2-tasks', 'c2-payloads', 'c2-events', 'c2-profiles'].includes(pageId)) { + if (pageId === 'c2') pageId = 'c2-listeners'; + if (pageId && ['dashboard', 'chat', 'hitl', 'info-collect', 'tasks', 'vulnerabilities', 'webshell', 'chat-files', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'skills-monitor', 'skills-management', 'agents-management', 'settings', 'c2-listeners', 'c2-sessions', 'c2-tasks', 'c2-payloads', 'c2-events', 'c2-profiles'].includes(pageId)) { switchPage(pageId); if (pageId === 'chat') { scheduleChatConversationFromHash(200); diff --git a/web/templates/index.html b/web/templates/index.html index 06fd8aff..6c9e1070 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -221,7 +221,6 @@ -
-
-

最近漏洞

- 查看全部 → +
+
+ + 查看全部 →
-
-
暂无最近漏洞
+
+
+
暂无最近漏洞
+
+
+