Add files via upload

This commit is contained in:
公明
2026-07-16 19:30:29 +08:00
committed by GitHub
parent 2147623543
commit 8e42e72e42
13 changed files with 2283 additions and 67 deletions
File diff suppressed because it is too large Load Diff
+3
View File
@@ -360,6 +360,9 @@ const PAGE_PERMISSION_MAP = {
chat: 'chat:read',
hitl: 'hitl:read',
'info-collect': 'fofa:execute',
assets: 'asset:read',
'asset-overview': 'asset:read',
'asset-library': 'asset:read',
tasks: 'tasks:read',
workflows: 'workflow:read',
projects: 'project:read',
+45 -13
View File
@@ -3079,6 +3079,29 @@ function getCachedToolExecutionSummaries(messageElement) {
}
}
/**
* 过程摘要中的早期/快速工具结果可能没有 executionId但消息本身会按调用顺序保存 ID
* 合并两份数据避免渲染摘要时丢失可用的弹窗详情入口
*/
function mergeToolExecutionSummariesWithIds(summaries, executionIds) {
const normalizedSummaries = Array.isArray(summaries)
? summaries.map(normalizeToolExecutionSummaryForButton)
: [];
const normalizedIds = normalizeMcpExecutionIds(executionIds);
const claimedIds = new Set(
normalizedSummaries.map((item) => item.executionId).filter(Boolean)
);
const fallbackIds = normalizedIds.filter((id) => !claimedIds.has(id));
let fallbackIndex = 0;
return normalizedSummaries.map((item) => {
if (item.executionId || fallbackIndex >= fallbackIds.length) return item;
return {
...item,
executionId: fallbackIds[fallbackIndex++]
};
});
}
function setPendingToolExecutionSummaries(messageElement, summaries) {
if (!messageElement || !messageElement.dataset || !Array.isArray(summaries)) return;
const normalized = cacheToolExecutionSummaries(messageElement, summaries);
@@ -3337,9 +3360,28 @@ window.setMcpExecutionSummaryCount = setMcpExecutionSummaryCount;
window.setPendingMcpExecutionIds = setPendingMcpExecutionIds;
window.setPendingToolExecutionSummaries = setPendingToolExecutionSummaries;
async function openTaskToolExecutionDetail(messageElement, item, index) {
let detailItem = item;
if (!detailItem.executionId) {
const refreshedItem = await resolveToolExecutionSummaryForFocus(messageElement, '', index);
const mergedItems = mergeToolExecutionSummariesWithIds(
getCachedToolExecutionSummaries(messageElement),
getCachedMcpExecutionIds(messageElement)
);
detailItem = mergedItems[index] || refreshedItem || detailItem;
}
if (detailItem.executionId) {
await showMCPDetail(detailItem.executionId);
return;
}
alert(typeof window.t === 'function'
? window.t('chat.toolExecutionDetailPending')
: '工具执行详情尚未同步,请稍后重试。');
}
/**
* 声明式渲染工具调用列表
* 过程摘要是展示与定位的唯一模型executionIds 仅在摘要尚未到达时提供占位
* 过程摘要是展示详情入口的唯一模型executionIds 仅在摘要尚未到达时提供占位
* 每次更新整体替换列表避免增量追加产生双重状态
*/
function renderMcpCallButtons(messageElement) {
@@ -3350,7 +3392,7 @@ function renderMcpCallButtons(messageElement) {
const executionIds = getCachedMcpExecutionIds(messageElement);
const summaries = getCachedToolExecutionSummaries(messageElement);
const items = summaries.length > 0
? summaries
? mergeToolExecutionSummariesWithIds(summaries, executionIds)
: executionIds.map((executionId) => normalizeToolExecutionSummaryForButton({ executionId }));
const renderVersion = String((parseInt(toolList.dataset.renderVersion, 10) || 0) + 1);
@@ -3367,17 +3409,7 @@ function renderMcpCallButtons(messageElement) {
if (item.toolCallId) {
btn.dataset.toolCallId = item.toolCallId;
}
btn.onclick = async () => {
let focusItem = item;
if (!focusItem.processDetailId && !focusItem.toolCallId && focusItem.executionId) {
focusItem = await resolveToolExecutionSummaryForFocus(
messageElement,
focusItem.executionId,
index
) || focusItem;
}
await focusToolExecutionInProcessDetails(messageElement, focusItem, index);
};
btn.onclick = () => openTaskToolExecutionDetail(messageElement, item, index);
if (item.toolName) {
renderToolExecutionButtonContent(btn, item.toolName, String(index + 1), item.status);
} else {
+4 -3
View File
@@ -66,7 +66,9 @@
const skipText = el.getAttribute('data-i18n-skip-text') === 'true';
const isFormControl = (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA');
const attrList = el.getAttribute('data-i18n-attr');
const text = i18next.t(key);
const translated = i18next.t(key);
// 缺键时保留模板中的后备文案,避免页面直接显示 assets.project 一类内部键名。
const text = translated && translated !== key ? translated : '';
// 仅当元素无子元素(仅文本或空)时才替换文本,避免覆盖卡片内的数字、子节点等;input/textarea 永不设置 textContent
const hasNoElementChildren = !el.querySelector('*');
if (!skipText && !isFormControl && hasNoElementChildren && text && typeof text === 'string') {
@@ -80,7 +82,7 @@
var val = text;
if (attr === 'title' && titleKey) {
var titleText = i18next.t(titleKey);
if (titleText && typeof titleText === 'string') val = titleText;
if (titleText && titleText !== titleKey && typeof titleText === 'string') val = titleText;
}
if (val && typeof val === 'string') {
el.setAttribute(attr, val);
@@ -233,4 +235,3 @@
});
});
})();
+4 -1
View File
@@ -556,6 +556,9 @@ function renderFofaResults(payload) {
<path d="M16 8h3a4 4 0 0 1 0 8h-3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</button>
<button class="btn-icon" data-require-permission="asset:write" onclick="importFofaRowAsset(${idx}); event.stopPropagation();" title="${escapeHtml(_t('assets.importOne'))}">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M12 3v12m0 0 4-4m-4 4-4-4M4 19h16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
</div>
`;
@@ -566,6 +569,7 @@ function renderFofaResults(payload) {
// 更新全选框状态
syncSelectAllCheckbox();
if (typeof applyRBACToUI === 'function') applyRBACToUI(els.tbody);
}
function inferTargetFromRow(row, fields) {
@@ -1147,4 +1151,3 @@ if (document.readyState === 'loading') {
} else {
updateSelectedMeta();
}
+49 -1
View File
@@ -7,6 +7,7 @@ const PROJECTS_LIST_PAGE_SIZE_KEY = 'cyberstrike.projects_list_page_size';
let currentProjectId = null;
let currentProjectUpdatedAt = null;
let currentProjectTab = 'facts';
let currentProjectAssets = [];
const projectNameById = {};
let _projectsListReady = false;
let _projectsFetchPromise = null;
@@ -1016,8 +1017,9 @@ async function selectProject(id) {
}
function switchProjectTab(tab) {
if (tab === 'assets' && typeof hasPermission === 'function' && !hasPermission('asset:read')) tab = 'facts';
currentProjectTab = tab;
['facts', 'graph', 'conversations', 'vulns', 'settings'].forEach((t) => {
['facts', 'graph', 'assets', 'conversations', 'vulns', 'settings'].forEach((t) => {
const btn = document.getElementById(`project-tab-${t}`);
const panel = document.getElementById(`project-panel-${t}`);
if (btn) btn.classList.toggle('is-active', t === tab);
@@ -1025,10 +1027,54 @@ function switchProjectTab(tab) {
});
if (tab === 'facts') loadProjectFacts();
if (tab === 'graph') loadProjectFactGraph();
if (tab === 'assets') loadProjectAssets();
if (tab === 'conversations') loadProjectConversations();
if (tab === 'vulns') loadProjectVulnerabilities();
}
async function loadProjectAssets() {
const tbody = document.getElementById('project-assets-tbody');
const countEl = document.getElementById('project-assets-count');
if (!tbody || !currentProjectId) return;
tbody.innerHTML = `<tr class="is-empty-row"><td colspan="7">${escapeHtml(tpFmt('common.loading', '加载中...'))}</td></tr>`;
const qs = new URLSearchParams({ project_id: currentProjectId, page: '1', page_size: '100' });
const res = await apiFetch(`/api/assets?${qs.toString()}`);
if (!res.ok) {
currentProjectAssets = [];
if (countEl) countEl.textContent = '0';
tbody.innerHTML = `<tr class="is-empty-row"><td colspan="7">${escapeHtml(tpFmt('common.loadFailed', '加载失败'))}</td></tr>`;
return;
}
const data = await res.json();
currentProjectAssets = data.assets || [];
if (countEl) countEl.textContent = tpFmt('projects.assetCount', `${data.total || 0} 个资产`, { count: data.total || 0 });
if (!currentProjectAssets.length) {
tbody.innerHTML = `<tr class="is-empty-row"><td colspan="7">${escapeHtml(tpFmt('projects.noBoundAssets', '暂无绑定到此项目的资产'))}</td></tr>`;
return;
}
tbody.innerHTML = currentProjectAssets.map((asset, index) => {
const target = asset.host || asset.domain || asset.ip || '-';
const service = [asset.protocol, asset.port ? ':' + asset.port : ''].join('') || '-';
const fingerprint = [asset.title, asset.server].filter(Boolean).join(' · ') || '-';
const updated = asset.last_seen_at ? new Date(asset.last_seen_at).toLocaleString() : '-';
const status = asset.status === 'inactive' ? tpFmt('assets.statusInactive', '停用') : tpFmt('assets.statusActive', '活跃');
return `<tr>
<td class="cell-summary"><button type="button" class="projects-asset-target" onclick="openProjectAssetDetail(${index})" title="${escapeHtml(target)}">${escapeHtml(target)}</button></td>
<td><code>${escapeHtml(service)}</code></td>
<td class="cell-summary" title="${escapeHtml(fingerprint)}">${escapeHtml(fingerprint)}</td>
<td>${escapeHtml(asset.source || '-')}</td>
<td>${escapeHtml(updated)}</td>
<td><span class="asset-status asset-status--${escapeHtml(asset.status || 'active')}">${escapeHtml(status)}</span></td>
<td class="col-actions"><div class="projects-table-actions"><button type="button" class="projects-action-btn projects-action-btn--view" onclick="openProjectAssetDetail(${index})">${escapeHtml(tpFmt('common.view', '查看'))}</button></div></td>
</tr>`;
}).join('');
}
function openProjectAssetDetail(index) {
const asset = currentProjectAssets[Number(index)];
if (asset && typeof window.openAssetDetailRecord === 'function') window.openAssetDetailRecord(asset);
}
let _selectedGraphFactKey = null;
let _selectedGraphEdgeId = null;
let _currentGraphData = null;
@@ -2759,6 +2805,8 @@ window.viewFactsForVulnerability = viewFactsForVulnerability;
window.openProjectConversation = openProjectConversation;
window.unbindConversationFromProject = unbindConversationFromProject;
window.loadProjectConversations = loadProjectConversations;
window.loadProjectAssets = loadProjectAssets;
window.openProjectAssetDetail = openProjectAssetDetail;
window.loadProjectFactGraph = loadProjectFactGraph;
window.filterProjectFactGraph = filterProjectFactGraph;
window.centerProjectFactGraph = centerProjectFactGraph;
+4
View File
@@ -113,6 +113,10 @@
scanFofaRow: 'fofa:execute',
batchScanSelectedFofaRows: 'fofa:execute',
exportFofaResults: 'fofa:execute',
importSelectedFofaAssets: 'asset:write',
importFofaRowAsset: 'asset:write',
saveAsset: 'asset:write',
deleteAsset: 'asset:delete',
// 任务队列
showBatchImportModal: 'tasks:write',
+1 -1
View File
@@ -47,7 +47,7 @@ const rbacScopeMeta = {
const rbacResourceLabels = {
user: '平台用户',
project: '项目', conversation: '对话', vulnerability: '漏洞', webshell: 'WebShell 连接',
batch_task: '批量任务', c2_listener: 'C2 监听器',
batch_task: '批量任务', c2_listener: 'C2 监听器', asset: '资产',
};
function rbacText(value, fallback = '') {
+17 -3
View File
@@ -81,7 +81,7 @@ function initRouter() {
const hashParts = hash.split('?');
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', 'platform-rbac', 'workflows', 'skills-monitor', 'skills-management', 'agents-management', 'settings', 'tasks', 'c2-listeners', 'c2-sessions', 'c2-tasks', 'c2-payloads', 'c2-events', 'c2-profiles'].includes(pageId)) {
if (pageId && ['dashboard', 'chat', 'hitl', 'asset-overview', 'asset-library', 'info-collect', 'projects', 'vulnerabilities', 'webshell', 'chat-files', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'platform-rbac', 'workflows', '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);
@@ -141,7 +141,15 @@ function updateNavState(pageId) {
});
// 设置活动状态
if (pageId === 'mcp-monitor' || pageId === 'mcp-management') {
if (pageId === 'asset-overview' || pageId === 'asset-library' || pageId === 'info-collect') {
const assetItem = document.querySelector('.nav-item[data-page="assets"]');
if (assetItem) {
assetItem.classList.add('active');
assetItem.classList.add('expanded');
}
const submenuItem = document.querySelector(`.nav-submenu-item[data-page="${pageId}"]`);
if (submenuItem) submenuItem.classList.add('active');
} else if (pageId === 'mcp-monitor' || pageId === 'mcp-management') {
// MCP子菜单项
const mcpItem = document.querySelector('.nav-item[data-page="mcp"]');
if (mcpItem) {
@@ -374,6 +382,12 @@ async function initPage(pageId) {
initInfoCollectPage();
}
break;
case 'asset-overview':
if (typeof loadAssetOverview === 'function') loadAssetOverview();
break;
case 'asset-library':
if (typeof loadAssets === 'function') loadAssets();
break;
case 'tasks':
// 初始化任务管理页面
if (typeof initTasksPage === 'function') {
@@ -549,7 +563,7 @@ document.addEventListener('DOMContentLoaded', function() {
let pageId = hashParts[0];
if (pageId === 'c2') pageId = 'c2-listeners';
if (pageId && ['dashboard', 'chat', 'hitl', 'info-collect', 'projects', 'tasks', 'workflows', 'vulnerabilities', 'webshell', 'chat-files', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'platform-rbac', 'skills-monitor', 'skills-management', 'agents-management', 'settings', 'c2-listeners', 'c2-sessions', 'c2-tasks', 'c2-payloads', 'c2-events', 'c2-profiles'].includes(pageId)) {
if (pageId && ['dashboard', 'chat', 'hitl', 'asset-overview', 'asset-library', 'info-collect', 'projects', 'tasks', 'workflows', 'vulnerabilities', 'webshell', 'chat-files', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'platform-rbac', '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);