Add files via upload

This commit is contained in:
公明
2026-06-09 17:34:36 +08:00
committed by GitHub
parent 99a41d8188
commit 6bfa7b8959
9 changed files with 521 additions and 212 deletions
-107
View File
@@ -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; }
+249
View File
@@ -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;
+8 -9
View File
@@ -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",
+7 -9
View File
@@ -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",
-26
View File
@@ -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;
};
// ============================================================================
// 模态框
// ============================================================================
+220 -2
View File
@@ -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 = (
'<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="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/><line x1="12" y1="6" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>' +
'</span>' +
'<div class="dashboard-empty-title">' + esc(dt('dashboard.noFactsYet', null, '暂无近期事实')) + '</div>' +
'<div class="dashboard-empty-desc">' + esc(desc) + '</div>' +
'<button type="button" class="dashboard-empty-action" data-action="' + esc(ctaAction) + '">' +
esc(ctaLabel) + ' →</button>'
);
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 = '<span class="dashboard-recent-fact-pin' + (f.pinned ? ' is-pinned' : '') + '"' +
(f.pinned ? (' title="' + esc(dt('projects.pinned', null, '置顶')) + '"') : '') +
' aria-hidden="true">' + (f.pinned ? '📌' : '') + '</span>';
var categoryBadge = '<span class="dashboard-recent-fact-cat cat-' + esc(category.toLowerCase().replace(/[^a-z0-9_-]/g, '')) + '">' + esc(category) + '</span>';
var confBadge = '<span class="dashboard-recent-fact-conf conf-' + esc(confidence) + '">' + esc(factConfidenceShortLabel(confidence)) + '</span>';
var summary = '<span class="dashboard-recent-fact-summary" title="' + esc(f.summary || '') + '">' + esc(f.summary || dt('common.untitled', null, '无标题')) + '</span>';
// 勿用 i18n 插值拼接 fact_keyi18next 会把 / 转成 &#x2F; 导致乱码
var projectLabel = (f.project_name || '').trim() || dt('projects.defaultProjectName', null, '项目');
var factKeyLabel = (f.fact_key || '').trim() || '—';
var metaText = projectLabel + ' · ' + factKeyLabel;
var metaLine = '<span class="dashboard-recent-fact-meta" title="' + esc(metaText) + '">' + esc(metaText) + '</span>';
var time = '<span class="dashboard-recent-fact-time">' + esc(timeAgoStr(f.updated_at)) + '</span>';
item.innerHTML = pinMark + categoryBadge + confBadge + summary + metaLine + time;
wrap.appendChild(item);
});
}
// 漏洞状态映射:把 status 字符串规整到 4 类(避免脏数据)
function statusKey(s) {
s = String(s || '').toLowerCase();
+9 -1
View File
@@ -574,8 +574,11 @@ async function loadProjectFacts() {
const vulnLink = f.related_vulnerability_id
? `<span class="projects-fact-vuln-link" title="${escapeHtml(tp('projects.relatedVulnIdTitle'))}">${escapeHtml(f.related_vulnerability_id.slice(0, 8))}…</span>`
: '';
const pinBadge = f.pinned
? `<span class="projects-list-item-badge" title="${escapeHtml(tp('projects.pinned'))}">${escapeHtml(tp('projects.pinned'))}</span>`
: '';
return `<tr>
<td class="cell-fact-key"><code class="projects-fact-key-chip" title="${keyEsc}">${keyEsc}</code>${vulnLink}</td>
<td class="cell-fact-key"><code class="projects-fact-key-chip" title="${keyEsc}">${keyEsc}</code>${pinBadge}${vulnLink}</td>
<td class="cell-fact-category">${formatCategoryBadge(f.category)}</td>
<td class="cell-summary" title="${escapeHtml(f.summary)}">${escapeHtml(f.summary)}</td>
<td>${formatFactBodyBadge(f)}</td>
@@ -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;
+6 -5
View File
@@ -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);
+22 -53
View File
@@ -221,7 +221,6 @@
</svg>
</div>
<div class="nav-submenu" id="submenu-c2">
<div class="nav-submenu-item" data-page="c2" onclick="switchPage('c2')" data-i18n="nav.c2Manage">C2 管理</div>
<div class="nav-submenu-item" data-page="c2-listeners" onclick="switchPage('c2-listeners')" data-i18n="nav.c2Listeners">监听器</div>
<div class="nav-submenu-item" data-page="c2-sessions" onclick="switchPage('c2-sessions')" data-i18n="nav.c2Sessions">会话</div>
<div class="nav-submenu-item" data-page="c2-tasks" onclick="switchPage('c2-tasks')" data-i18n="nav.c2Tasks">任务</div>
@@ -574,20 +573,30 @@
</div>
</div>
</section>
<section class="dashboard-section dashboard-section-recent-vulns">
<div class="dashboard-section-header">
<h3 class="dashboard-section-title" data-i18n="dashboard.recentVulns">最近漏洞</h3>
<a class="dashboard-section-link" onclick="switchPage('vulnerabilities')" data-i18n="dashboard.viewAll">查看全部 →</a>
<section class="dashboard-section dashboard-section-recent-feed">
<div class="dashboard-section-header dashboard-section-header--tabs">
<nav class="dashboard-feed-tabs" role="tablist" aria-label="最近漏洞与近期事实">
<button type="button" class="dashboard-feed-tab is-active" role="tab" id="dashboard-feed-tab-vulns" aria-selected="true" aria-controls="dashboard-feed-panel-vulns" onclick="switchDashboardFeedTab('vulns')" data-i18n="dashboard.recentVulns">最近漏洞</button>
<button type="button" class="dashboard-feed-tab" role="tab" id="dashboard-feed-tab-facts" aria-selected="false" aria-controls="dashboard-feed-panel-facts" onclick="switchDashboardFeedTab('facts')"><span data-i18n="dashboard.recentFacts">近期事实</span><span class="dashboard-feed-tab-badge" id="dashboard-feed-tab-facts-badge" hidden></span></button>
</nav>
<a class="dashboard-section-link" id="dashboard-feed-view-all" 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-feed-panel" id="dashboard-feed-panel-vulns" role="tabpanel" aria-labelledby="dashboard-feed-tab-vulns">
<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>
</div>
<div class="dashboard-feed-panel" id="dashboard-feed-panel-facts" role="tabpanel" aria-labelledby="dashboard-feed-tab-facts" hidden>
<div class="dashboard-recent-facts" id="dashboard-recent-facts">
<div class="dashboard-recent-facts-empty" id="dashboard-recent-facts-empty" data-i18n="dashboard.noFactsYet">暂无近期事实</div>
</div>
</div>
</section>
<!-- C2 概览:介于「最近漏洞」与「批量任务队列」之间 -->
<section class="dashboard-section dashboard-section-c2" id="dashboard-section-c2" hidden>
<div class="dashboard-section-header">
<h3 class="dashboard-section-title" data-i18n="dashboard.c2OverviewTitle">C2 概览</h3>
<a class="dashboard-section-link" onclick="switchPage('c2')" data-i18n="dashboard.c2GoManage">进入 C2 →</a>
<a class="dashboard-section-link" onclick="switchPage('c2-listeners')" data-i18n="dashboard.c2GoManage">进入 C2 →</a>
</div>
<div class="dashboard-c2-strip">
<div class="dashboard-c2-stat" role="button" tabindex="0" onclick="switchPage('c2-listeners')" onkeydown="if(event.key==='Enter'||event.key===' ') { event.preventDefault(); switchPage('c2-listeners'); }" data-i18n="dashboard.c2ClickListeners" data-i18n-attr="title" title="查看监听器">
@@ -2005,51 +2014,6 @@
</div>
</div>
<!-- C2 管理页面容器(各子页面通过 JS 动态渲染) -->
<div id="page-c2" class="page">
<div class="page-header">
<h2 data-i18n="c2.title">C2 管理</h2>
</div>
<div class="page-content" id="c2-content">
<div class="c2-layout">
<div id="c2-main" class="c2-main">
<div class="c2-welcome">
<div class="c2-welcome-icon">
<svg width="72" height="72" viewBox="0 0 24 24" fill="none" stroke="url(#c2-grad)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<defs><linearGradient id="c2-grad" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#00d4ff"/><stop offset="100%" stop-color="#a855f7"/></linearGradient></defs>
<path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9"></path>
<path d="M7.8 16.2c-2.3-2.3-2.3-6.1 0-8.5"></path>
<circle cx="12" cy="12" r="2"></circle>
<path d="M16.2 7.8c2.3 2.3 2.3 6.1 0 8.5"></path>
<path d="M19.1 4.9C23 8.8 23 15.2 19.1 19"></path>
</svg>
</div>
<h3 data-i18n="c2.welcomeTitle">AI-Native C2 框架</h3>
<p data-i18n="c2.welcomeDesc">以 MCP 工具为一等公民,让 LLM 可以像调用 nmap 一样调用 C2 完成"上线 → 控制 → 任务 → 横向 → 清场"全流程</p>
<div class="c2-stats" id="c2-dashboard-stats">
<div class="c2-stat-item">
<span class="c2-stat-value" id="c2-stat-listeners">-</span>
<span class="c2-stat-label" data-i18n="c2.statListeners">运行中监听器</span>
</div>
<div class="c2-stat-item">
<span class="c2-stat-value" id="c2-stat-sessions">-</span>
<span class="c2-stat-label" data-i18n="c2.statSessions">在线会话</span>
</div>
<div class="c2-stat-item">
<span class="c2-stat-value" id="c2-stat-pending">-</span>
<span class="c2-stat-label" data-i18n="c2.statPending">待审任务</span>
</div>
</div>
<div class="c2-actions">
<button class="btn-primary" onclick="switchPage('c2-listeners')" data-i18n="c2.goListeners">管理监听器</button>
<button class="btn-secondary" onclick="switchPage('c2-sessions')" data-i18n="c2.goSessions">查看会话</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- C2 监听器管理页面 -->
<div id="page-c2-listeners" class="page">
<div class="page-header">
@@ -4260,6 +4224,11 @@
<textarea id="fact-modal-body" class="form-input fact-modal-body-input" rows="14" placeholder="攻击链步骤、HTTP/命令 POC、响应现象、证据…" oninput="updateFactFormHints()"></textarea>
<p id="fact-modal-body-hint" class="projects-field-hint" role="status"></p>
</div>
<div class="projects-form-field">
<label class="projects-filter-check projects-pin-toggle">
<input type="checkbox" id="fact-modal-pinned"> <span data-i18n="projects.pinFact">置顶事实(列表与黑板索引优先)</span>
</label>
</div>
<div class="projects-form-field">
<label for="fact-modal-related-vuln" data-i18n="projects.relatedVulnIdLabel">关联漏洞 ID</label>
<input type="text" id="fact-modal-related-vuln" class="form-input" placeholder="可选" data-i18n="projects.optional" data-i18n-attr="placeholder">