mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-26 17:27:54 +02:00
Add files via upload
This commit is contained in:
@@ -113,6 +113,7 @@ CyberStrikeAI is an **AI-native security testing platform** built in Go. It inte
|
||||
- 🔒 Password-protected web UI, audit logs, and SQLite persistence
|
||||
- 📚 Knowledge base (RAG) with embedding-based vector retrieval (cosine similarity), optional **Eino Compose** indexing pipeline, and configurable post-retrieval budgets / reranking hooks
|
||||
- 📁 Conversation grouping with pinning, rename, and batch management
|
||||
- 📂 **Project management**: group conversations and vulnerabilities by project; **shared facts** (project blackboard) persist cross-session context (targets, env, auth notes) with auto-injection for agents and MCP tools (`upsert_project_fact`, `get_project_fact`, …)
|
||||
- 🛡️ Vulnerability management with CRUD operations, severity tracking, status workflow, and statistics
|
||||
- 📋 Batch task management: create task queues, add multiple tasks, and execute them sequentially
|
||||
- 🎭 Role-based testing: predefined security testing roles (Penetration Testing, CTF, Web App Scanning, etc.) with custom prompts and tool restrictions
|
||||
|
||||
@@ -112,6 +112,7 @@ CyberStrikeAI 是一款 **AI 原生安全测试平台**,基于 Go 构建,集
|
||||
- 🔒 Web 登录保护、审计日志、SQLite 持久化
|
||||
- 📚 知识库(RAG):向量嵌入与余弦相似度检索(与 Eino `retriever.Retriever` 语义一致),可选 **Eino Compose** 索引流水线及检索后处理(预算、重排等配置项)
|
||||
- 📁 对话分组管理:支持分组创建、置顶、重命名、删除等操作
|
||||
- 📂 **项目管理**:按项目归类对话与漏洞;**共享事实**(项目黑板)在多会话间沉淀目标/环境/认证等认知,自动注入 Agent 上下文,支持 MCP 工具读写(`upsert_project_fact`、`get_project_fact` 等)
|
||||
- 🛡️ 漏洞管理功能:完整的漏洞 CRUD 操作,支持严重程度分级、状态流转、按对话/严重程度/状态过滤,以及统计看板
|
||||
- 📋 批量任务管理:创建任务队列,批量添加任务,依次顺序执行,支持任务编辑与状态跟踪
|
||||
- 🎭 角色化测试:预设安全测试角色(渗透测试、CTF、Web 应用扫描等),支持自定义提示词和工具限制
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1588,6 +1588,11 @@
|
||||
"detailVulnId": "Vuln ID",
|
||||
"detailType": "Type",
|
||||
"detailTarget": "Target",
|
||||
"detailProject": "Project",
|
||||
"projectUnbound": "No project",
|
||||
"projectBindHint": "Once bound, agents can list this finding under the project scope.",
|
||||
"projectBindFailed": "Failed to update project binding",
|
||||
"projectBindOk": "Project binding updated",
|
||||
"detailConversationId": "Conversation ID",
|
||||
"detailTaskId": "Task ID",
|
||||
"detailTaskQueueId": "Task queue ID",
|
||||
@@ -2161,6 +2166,9 @@
|
||||
"add": "Add"
|
||||
},
|
||||
"vulnerabilityModal": {
|
||||
"project": "Project",
|
||||
"projectNone": "(Unbound)",
|
||||
"projectHint": "Bound findings appear in list_vulnerabilities for that project; leave empty to infer from the conversation when possible.",
|
||||
"conversationId": "Conversation ID",
|
||||
"conversationIdPlaceholder": "Enter conversation ID",
|
||||
"conversationTag": "Conversation tag",
|
||||
|
||||
@@ -1577,6 +1577,11 @@
|
||||
"detailVulnId": "漏洞ID",
|
||||
"detailType": "类型",
|
||||
"detailTarget": "目标",
|
||||
"detailProject": "所属项目",
|
||||
"projectUnbound": "未绑定项目",
|
||||
"projectBindHint": "绑定后 Agent 可在项目范围内查询到该漏洞",
|
||||
"projectBindFailed": "绑定项目失败",
|
||||
"projectBindOk": "已更新项目绑定",
|
||||
"detailConversationId": "会话ID",
|
||||
"detailTaskId": "任务ID",
|
||||
"detailTaskQueueId": "任务队列ID",
|
||||
@@ -2150,6 +2155,9 @@
|
||||
"add": "添加"
|
||||
},
|
||||
"vulnerabilityModal": {
|
||||
"project": "所属项目",
|
||||
"projectNone": "(未绑定)",
|
||||
"projectHint": "绑定后 Agent 在项目范围内可通过 list_vulnerabilities 看到本条记录;留空则尝试从会话自动关联。",
|
||||
"conversationId": "会话ID",
|
||||
"conversationIdPlaceholder": "输入会话ID",
|
||||
"conversationTag": "对话标签",
|
||||
|
||||
@@ -646,6 +646,9 @@ function toggleAgentModePanel() {
|
||||
if (typeof closeRoleSelectionPanel === 'function') {
|
||||
closeRoleSelectionPanel();
|
||||
}
|
||||
if (typeof closeChatProjectPanel === 'function') {
|
||||
closeChatProjectPanel();
|
||||
}
|
||||
panel.style.display = 'flex';
|
||||
btn.classList.add('active');
|
||||
btn.setAttribute('aria-expanded', 'true');
|
||||
@@ -897,6 +900,10 @@ async function sendMessage() {
|
||||
conversationId: currentConversationId,
|
||||
role: typeof getCurrentRole === 'function' ? getCurrentRole() : ''
|
||||
};
|
||||
if (!currentConversationId && typeof getActiveProjectId === 'function') {
|
||||
const pid = getActiveProjectId();
|
||||
if (pid) body.projectId = pid;
|
||||
}
|
||||
const hitlCfg = readHitlConfigFromForm();
|
||||
if (normalizeHitlMode(hitlCfg.mode) !== HITL_MODE_OFF) {
|
||||
const sensitiveTools = hitlToolsSplitToArray(hitlCfg.sensitiveTools || '');
|
||||
@@ -2900,10 +2907,14 @@ async function startNewConversation() {
|
||||
}
|
||||
|
||||
currentConversationId = null;
|
||||
window._loadedConversationProjectId = '';
|
||||
try {
|
||||
window.currentConversationId = '';
|
||||
} catch (e) { /* ignore */ }
|
||||
currentConversationGroupId = null; // 新对话不属于任何分组
|
||||
if (typeof refreshChatProjectSelector === 'function') {
|
||||
refreshChatProjectSelector();
|
||||
}
|
||||
document.getElementById('chat-messages').innerHTML = '';
|
||||
const readyMsgNew = typeof window.t === 'function' ? window.t('chat.systemReadyMessage') : '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。';
|
||||
addMessage('assistant', readyMsgNew, null, null, null, { systemReadyMessage: true });
|
||||
@@ -3158,9 +3169,13 @@ async function loadConversation(conversationId) {
|
||||
|
||||
// 更新当前对话ID
|
||||
currentConversationId = conversationId;
|
||||
window._loadedConversationProjectId = conversation.projectId || conversation.project_id || '';
|
||||
try {
|
||||
window.currentConversationId = conversationId;
|
||||
} catch (e) { /* ignore */ }
|
||||
if (typeof refreshChatProjectSelector === 'function') {
|
||||
refreshChatProjectSelector();
|
||||
}
|
||||
if (typeof window.syncHitlConfigFromServer === 'function') {
|
||||
await window.syncHitlConfigFromServer(conversationId);
|
||||
} else {
|
||||
|
||||
@@ -1028,7 +1028,12 @@ async function batchScanSelectedFofaRows() {
|
||||
const resp = await apiFetch('/api/batch-tasks', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ title, tasks, role })
|
||||
body: JSON.stringify({
|
||||
title,
|
||||
tasks,
|
||||
role,
|
||||
projectId: typeof getActiveProjectId === 'function' ? getActiveProjectId() || '' : '',
|
||||
})
|
||||
});
|
||||
const result = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok) {
|
||||
|
||||
@@ -0,0 +1,907 @@
|
||||
/**
|
||||
* 项目管理与事实黑板
|
||||
*/
|
||||
let projectsCache = [];
|
||||
let projectsCacheAll = [];
|
||||
let currentProjectId = null;
|
||||
let currentProjectTab = 'facts';
|
||||
const projectNameById = {};
|
||||
let _projectsListReady = false;
|
||||
let _projectsFetchPromise = null;
|
||||
|
||||
const PROJECT_ACTIVE_KEY = 'cyberstrike.activeProjectId';
|
||||
|
||||
function getActiveProjectId() {
|
||||
try {
|
||||
return localStorage.getItem(PROJECT_ACTIVE_KEY) || '';
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function setActiveProjectId(id) {
|
||||
try {
|
||||
if (id) localStorage.setItem(PROJECT_ACTIVE_KEY, id);
|
||||
else localStorage.removeItem(PROJECT_ACTIVE_KEY);
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
function rebuildProjectNameMap(list) {
|
||||
Object.keys(projectNameById).forEach((k) => delete projectNameById[k]);
|
||||
(list || []).forEach((p) => {
|
||||
if (p && p.id) projectNameById[p.id] = p.name || p.id;
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchProjectsList(includeArchived) {
|
||||
const showArchived = includeArchived || document.getElementById('projects-show-archived')?.checked;
|
||||
const url = showArchived ? '/api/projects?limit=200' : '/api/projects?status=active&limit=200';
|
||||
const res = await apiFetch(url);
|
||||
if (!res.ok) throw new Error('加载项目失败');
|
||||
const data = await res.json();
|
||||
projectsCache = Array.isArray(data) ? data : [];
|
||||
rebuildProjectNameMap(projectsCache);
|
||||
_projectsListReady = true;
|
||||
return projectsCache;
|
||||
}
|
||||
|
||||
/** 对话页等项目选择器:确保列表已拉取(去重并发请求) */
|
||||
async function ensureProjectsLoaded(force) {
|
||||
if (!force && _projectsListReady) return projectsCache;
|
||||
if (!force && _projectsFetchPromise) return _projectsFetchPromise;
|
||||
_projectsFetchPromise = fetchProjectsList(false)
|
||||
.catch((e) => {
|
||||
_projectsListReady = false;
|
||||
throw e;
|
||||
})
|
||||
.finally(() => {
|
||||
_projectsFetchPromise = null;
|
||||
});
|
||||
return _projectsFetchPromise;
|
||||
}
|
||||
|
||||
function prefetchProjectsForChat() {
|
||||
ensureProjectsLoaded().catch(() => {});
|
||||
}
|
||||
|
||||
function getProjectName(id) {
|
||||
return projectNameById[id] || id || '';
|
||||
}
|
||||
|
||||
function initProjectsModalEscape() {
|
||||
if (window._projectsModalEscapeBound) return;
|
||||
window._projectsModalEscapeBound = true;
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key !== 'Escape') return;
|
||||
if (document.getElementById('project-modal')?.style.display === 'flex') closeProjectModal();
|
||||
else if (document.getElementById('fact-modal')?.style.display === 'flex') closeFactModal();
|
||||
else if (document.getElementById('fact-detail-modal')?.style.display === 'flex') closeFactDetailModal();
|
||||
});
|
||||
}
|
||||
|
||||
async function initProjectsPage() {
|
||||
const page = document.getElementById('page-projects');
|
||||
if (!page || page.style.display === 'none') return;
|
||||
initProjectsModalEscape();
|
||||
updateProjectsDetailVisibility();
|
||||
await loadProjectsList();
|
||||
if (!currentProjectId && projectsCache.length) {
|
||||
const fromHash = new URLSearchParams(window.location.hash.split('?')[1] || '').get('id');
|
||||
currentProjectId = fromHash || projectsCache[0].id;
|
||||
}
|
||||
renderProjectsSidebar();
|
||||
if (currentProjectId) {
|
||||
await selectProject(currentProjectId);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadProjectsList() {
|
||||
await fetchProjectsList();
|
||||
renderProjectsSidebar();
|
||||
if (typeof refreshChatProjectSelector === 'function') {
|
||||
refreshChatProjectSelector();
|
||||
}
|
||||
if (typeof refreshVulnerabilityProjectFilter === 'function') {
|
||||
refreshVulnerabilityProjectFilter();
|
||||
}
|
||||
}
|
||||
|
||||
function projectInitial(name) {
|
||||
const s = (name || 'P').trim();
|
||||
return s ? s.charAt(0).toUpperCase() : 'P';
|
||||
}
|
||||
|
||||
function updateProjectsDetailVisibility() {
|
||||
const main = document.getElementById('projects-detail-main');
|
||||
const placeholder = document.getElementById('projects-detail-placeholder');
|
||||
const inner = document.getElementById('projects-detail-inner');
|
||||
const show = !!currentProjectId;
|
||||
if (main) main.classList.toggle('has-project', show);
|
||||
if (placeholder) placeholder.hidden = show;
|
||||
if (inner) inner.hidden = !show;
|
||||
}
|
||||
|
||||
function updateProjectsListCount() {
|
||||
const el = document.getElementById('projects-list-count');
|
||||
if (el) el.textContent = String(projectsCache.length);
|
||||
}
|
||||
|
||||
function formatConfidenceBadge(confidence) {
|
||||
const c = (confidence || '').toLowerCase();
|
||||
let cls = 'projects-confidence--tentative';
|
||||
let label = c || '—';
|
||||
if (c === 'confirmed') {
|
||||
cls = 'projects-confidence--confirmed';
|
||||
label = '已确认';
|
||||
} else if (c === 'deprecated') {
|
||||
cls = 'projects-confidence--deprecated';
|
||||
label = '已废弃';
|
||||
} else if (c === 'tentative') {
|
||||
label = '待确认';
|
||||
}
|
||||
return `<span class="projects-confidence ${cls}">${escapeHtml(label)}</span>`;
|
||||
}
|
||||
|
||||
function renderProjectFactActions(keyEsc, idEsc) {
|
||||
return `<div class="projects-table-actions">
|
||||
<button type="button" class="projects-action-btn projects-action-btn--edit" data-fact-key="${keyEsc}" onclick="showEditFactModal(this.dataset.factKey)" title="编辑各字段">编辑</button>
|
||||
<button type="button" class="projects-action-btn projects-action-btn--view" data-fact-key="${keyEsc}" onclick="viewProjectFactBody(this.dataset.factKey)" title="查看完整 body">详情</button>
|
||||
<button type="button" class="projects-action-btn projects-action-btn--mute" data-fact-key="${keyEsc}" onclick="deprecateProjectFactByKey(this.dataset.factKey)" title="标记为已废弃">废弃</button>
|
||||
<button type="button" class="projects-action-btn projects-action-btn--danger" data-fact-id="${idEsc}" onclick="deleteProjectFact(this.dataset.factId)" title="永久删除">删除</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function formatSeverityBadge(severity) {
|
||||
const s = (severity || 'info').toLowerCase();
|
||||
const cls = 'projects-severity--' + (['critical', 'high', 'medium', 'low', 'info'].includes(s) ? s : 'info');
|
||||
return `<span class="projects-severity ${cls}">${escapeHtml(severity || '—')}</span>`;
|
||||
}
|
||||
|
||||
function getProjectsListFilter() {
|
||||
return (document.getElementById('projects-list-search')?.value || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
function filterProjectsList() {
|
||||
renderProjectsSidebar();
|
||||
}
|
||||
|
||||
function renderProjectsSidebar() {
|
||||
const el = document.getElementById('projects-list');
|
||||
if (!el) return;
|
||||
updateProjectsListCount();
|
||||
const q = getProjectsListFilter();
|
||||
const list = q
|
||||
? projectsCache.filter((p) => (p.name || '').toLowerCase().includes(q) || (p.description || '').toLowerCase().includes(q))
|
||||
: projectsCache;
|
||||
if (!projectsCache.length) {
|
||||
el.innerHTML =
|
||||
'<div class="projects-empty">暂无项目<br><button type="button" class="btn-primary btn-small projects-empty-btn" onclick="showNewProjectModal()">新建项目</button></div>';
|
||||
updateProjectsDetailVisibility();
|
||||
return;
|
||||
}
|
||||
if (!list.length) {
|
||||
el.innerHTML = '<div class="projects-empty">无匹配项目</div>';
|
||||
updateProjectsDetailVisibility();
|
||||
return;
|
||||
}
|
||||
el.innerHTML = list.map((p) => {
|
||||
const active = p.id === currentProjectId ? ' is-active' : '';
|
||||
const archived = p.status === 'archived' ? ' is-archived' : '';
|
||||
const badges = [
|
||||
p.pinned ? '<span class="projects-list-item-badge">置顶</span>' : '',
|
||||
p.status === 'archived' ? '<span class="projects-list-item-badge">归档</span>' : '',
|
||||
].join('');
|
||||
return `<div class="projects-list-item${active}${archived}" data-id="${escapeHtml(p.id)}" onclick="selectProject('${escapeHtml(p.id)}')">
|
||||
<div class="projects-list-item-body">
|
||||
<div class="projects-list-item-name">${escapeHtml(p.name)}${badges}</div>
|
||||
<div class="projects-list-item-meta">${formatProjectTime(p.updated_at)}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
updateProjectsDetailVisibility();
|
||||
}
|
||||
|
||||
function updateProjectStatusPill(status) {
|
||||
const el = document.getElementById('projects-detail-status');
|
||||
if (!el) return;
|
||||
const archived = status === 'archived';
|
||||
el.textContent = archived ? '已归档' : '进行中';
|
||||
el.className = 'projects-status-pill ' + (archived ? 'projects-status-pill--archived' : 'projects-status-pill--active');
|
||||
}
|
||||
|
||||
function updateProjectStats(factCount, vulnCount) {
|
||||
const f = document.getElementById('project-stat-facts');
|
||||
const v = document.getElementById('project-stat-vulns');
|
||||
if (f) f.textContent = `${factCount ?? 0} 条事实`;
|
||||
if (v) v.textContent = `${vulnCount ?? 0} 个漏洞`;
|
||||
}
|
||||
|
||||
async function selectProject(id) {
|
||||
currentProjectId = id;
|
||||
renderProjectsSidebar();
|
||||
updateProjectsDetailVisibility();
|
||||
try {
|
||||
const res = await apiFetch(`/api/projects/${id}`);
|
||||
if (!res.ok) throw new Error('项目不存在');
|
||||
const p = await res.json();
|
||||
const titleEl = document.getElementById('projects-detail-title');
|
||||
if (titleEl) titleEl.textContent = p.name || '项目';
|
||||
document.getElementById('project-edit-name').value = p.name || '';
|
||||
document.getElementById('project-edit-description').value = p.description || '';
|
||||
document.getElementById('project-edit-scope').value = p.scope_json || '';
|
||||
const statusEl = document.getElementById('project-edit-status');
|
||||
if (statusEl) statusEl.value = p.status || 'active';
|
||||
updateProjectStatusPill(p.status || 'active');
|
||||
const metaEl = document.getElementById('projects-detail-meta');
|
||||
if (metaEl) metaEl.textContent = `更新于 ${formatProjectTime(p.updated_at)}`;
|
||||
const descEl = document.getElementById('projects-detail-desc');
|
||||
if (descEl) {
|
||||
const desc = (p.description || '').trim();
|
||||
if (desc) {
|
||||
descEl.textContent = desc;
|
||||
descEl.hidden = false;
|
||||
} else {
|
||||
descEl.textContent = '';
|
||||
descEl.hidden = true;
|
||||
}
|
||||
}
|
||||
projectNameById[p.id] = p.name || p.id;
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
refreshProjectHeaderStats();
|
||||
switchProjectTab(currentProjectTab);
|
||||
}
|
||||
|
||||
function switchProjectTab(tab) {
|
||||
currentProjectTab = tab;
|
||||
['facts', '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);
|
||||
if (panel) panel.hidden = t !== tab;
|
||||
});
|
||||
if (tab === 'facts') loadProjectFacts();
|
||||
if (tab === 'vulns') loadProjectVulnerabilities();
|
||||
}
|
||||
|
||||
async function loadProjectFacts() {
|
||||
const tbody = document.getElementById('project-facts-tbody');
|
||||
if (!tbody || !currentProjectId) return;
|
||||
tbody.innerHTML = '<tr class="is-empty-row"><td colspan="6">加载中…</td></tr>';
|
||||
const res = await apiFetch(`/api/projects/${currentProjectId}/facts?limit=200`);
|
||||
if (!res.ok) {
|
||||
tbody.innerHTML = '<tr class="is-empty-row"><td colspan="6">加载失败</td></tr>';
|
||||
return;
|
||||
}
|
||||
const facts = await res.json();
|
||||
if (!facts.length) {
|
||||
tbody.innerHTML = '<tr class="is-empty-row"><td colspan="6">暂无事实,点击「添加事实」或由 Agent 自动写入</td></tr>';
|
||||
refreshProjectHeaderStats();
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = facts.map((f) => {
|
||||
const keyEsc = escapeHtml(f.fact_key);
|
||||
const idEsc = escapeHtml(f.id);
|
||||
return `<tr>
|
||||
<td><code>${keyEsc}</code></td>
|
||||
<td>${escapeHtml(f.category)}</td>
|
||||
<td class="cell-summary" title="${escapeHtml(f.summary)}">${escapeHtml(f.summary)}</td>
|
||||
<td>${formatConfidenceBadge(f.confidence)}</td>
|
||||
<td>${formatProjectTime(f.updated_at, f.created_at)}</td>
|
||||
<td class="col-actions">${renderProjectFactActions(keyEsc, idEsc)}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
refreshProjectHeaderStats();
|
||||
}
|
||||
|
||||
async function refreshProjectHeaderStats() {
|
||||
if (!currentProjectId) return;
|
||||
try {
|
||||
const [factsRes, vulnRes] = await Promise.all([
|
||||
apiFetch(`/api/projects/${currentProjectId}/facts?limit=500`),
|
||||
apiFetch(`/api/vulnerabilities?project_id=${encodeURIComponent(currentProjectId)}&limit=100`),
|
||||
]);
|
||||
let fc = 0;
|
||||
let vc = 0;
|
||||
if (factsRes.ok) {
|
||||
const f = await factsRes.json();
|
||||
fc = Array.isArray(f) ? f.length : 0;
|
||||
}
|
||||
if (vulnRes.ok) {
|
||||
const d = await vulnRes.json();
|
||||
const items = d.Vulnerabilities || d.vulnerabilities || d.items || [];
|
||||
vc = items.length;
|
||||
}
|
||||
updateProjectStats(fc, vc);
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
}
|
||||
|
||||
let _factDetailKey = null;
|
||||
|
||||
async function viewProjectFactBody(factKey) {
|
||||
const res = await apiFetch(`/api/projects/${currentProjectId}/facts?fact_key=${encodeURIComponent(factKey)}`);
|
||||
if (!res.ok) return alert('加载失败');
|
||||
const f = await res.json();
|
||||
_factDetailKey = f.fact_key;
|
||||
document.getElementById('fact-detail-title').textContent = `[${f.fact_key}]`;
|
||||
document.getElementById('fact-detail-meta').textContent =
|
||||
`分类: ${f.category} · 置信度: ${f.confidence} · 更新: ${formatProjectTime(f.updated_at, f.created_at)}` +
|
||||
(f.related_vulnerability_id ? ` · 关联漏洞: ${f.related_vulnerability_id}` : '');
|
||||
document.getElementById('fact-detail-body').textContent = f.body || '(无 body)';
|
||||
openProjectsOverlay('fact-detail-modal');
|
||||
}
|
||||
|
||||
function editFactFromDetail() {
|
||||
const key = _factDetailKey;
|
||||
closeFactDetailModal();
|
||||
if (key) showEditFactModal(key);
|
||||
}
|
||||
|
||||
function closeFactDetailModal() {
|
||||
closeProjectsOverlay('fact-detail-modal');
|
||||
_factDetailKey = null;
|
||||
}
|
||||
|
||||
async function deprecateProjectFactByKey(factKey) {
|
||||
if (!confirm(`将事实 ${factKey} 标记为 deprecated?`)) return;
|
||||
const res = await apiFetch(`/api/projects/${currentProjectId}/facts/deprecate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ fact_key: factKey }),
|
||||
});
|
||||
if (!res.ok) return alert('操作失败');
|
||||
loadProjectFacts();
|
||||
}
|
||||
|
||||
function openVulnerabilitiesForProject(projectId) {
|
||||
const pid = projectId || currentProjectId;
|
||||
if (!pid) return;
|
||||
if (typeof switchPage === 'function') {
|
||||
switchPage('vulnerabilities');
|
||||
}
|
||||
if (typeof window.setVulnerabilityProjectFilter === 'function') {
|
||||
window.setVulnerabilityProjectFilter(pid);
|
||||
} else {
|
||||
window.location.hash = `vulnerabilities?project_id=${encodeURIComponent(pid)}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadProjectVulnerabilities() {
|
||||
const tbody = document.getElementById('project-vulns-tbody');
|
||||
if (!tbody || !currentProjectId) return;
|
||||
tbody.innerHTML = '<tr class="is-empty-row"><td colspan="4">加载中…</td></tr>';
|
||||
const res = await apiFetch(`/api/vulnerabilities?project_id=${encodeURIComponent(currentProjectId)}&limit=100`);
|
||||
if (!res.ok) {
|
||||
tbody.innerHTML = '<tr class="is-empty-row"><td colspan="4">加载失败</td></tr>';
|
||||
return;
|
||||
}
|
||||
const data = await res.json();
|
||||
const items = data.Vulnerabilities || data.vulnerabilities || data.items || [];
|
||||
if (!items.length) {
|
||||
tbody.innerHTML = '<tr class="is-empty-row"><td colspan="4">本项目暂无漏洞记录</td></tr>';
|
||||
refreshProjectHeaderStats();
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = items.map((v) => {
|
||||
const idEsc = escapeHtml(v.id);
|
||||
return `<tr>
|
||||
<td class="cell-summary" title="${escapeHtml(v.title)}">${escapeHtml(v.title)}</td>
|
||||
<td>${formatSeverityBadge(v.severity)}</td>
|
||||
<td>${escapeHtml(v.status)}</td>
|
||||
<td class="col-actions">
|
||||
<div class="projects-table-actions">
|
||||
<button type="button" class="projects-action-btn projects-action-btn--view" data-vuln-id="${idEsc}" onclick="openVulnerabilityDetail(this.dataset.vulnId)">查看</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
refreshProjectHeaderStats();
|
||||
}
|
||||
|
||||
function openVulnerabilityDetail(vulnId) {
|
||||
openVulnerabilitiesForProject(currentProjectId);
|
||||
if (typeof window.setVulnerabilityIdFilter === 'function') {
|
||||
setTimeout(() => window.setVulnerabilityIdFilter(vulnId), 300);
|
||||
}
|
||||
}
|
||||
|
||||
function openProjectsOverlay(id) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
el.style.display = 'flex';
|
||||
document.body.classList.add('projects-modal-open');
|
||||
const focusTarget = el.querySelector('input.form-input, textarea.form-input, select.form-input');
|
||||
if (focusTarget) {
|
||||
setTimeout(() => focusTarget.focus(), 80);
|
||||
}
|
||||
}
|
||||
|
||||
function closeProjectsOverlay(id) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.style.display = 'none';
|
||||
const anyOpen = document.querySelector('.projects-modal-overlay[style*="flex"]');
|
||||
if (!anyOpen) document.body.classList.remove('projects-modal-open');
|
||||
}
|
||||
|
||||
function showNewProjectModal() {
|
||||
document.getElementById('project-modal-title').textContent = '新建项目';
|
||||
const sub = document.getElementById('project-modal-subtitle');
|
||||
if (sub) sub.textContent = '创建后可绑定对话,跨会话共享事实黑板';
|
||||
const submitBtn = document.getElementById('project-modal-submit-btn');
|
||||
if (submitBtn) submitBtn.textContent = '创建项目';
|
||||
document.getElementById('project-modal-name').value = '';
|
||||
document.getElementById('project-modal-description').value = '';
|
||||
window._projectModalEditId = null;
|
||||
openProjectsOverlay('project-modal');
|
||||
}
|
||||
|
||||
async function saveProjectModal() {
|
||||
const name = document.getElementById('project-modal-name').value.trim();
|
||||
if (!name) return alert('请输入项目名称');
|
||||
const body = {
|
||||
name,
|
||||
description: document.getElementById('project-modal-description').value.trim(),
|
||||
};
|
||||
const editId = window._projectModalEditId;
|
||||
const res = editId
|
||||
? await apiFetch(`/api/projects/${editId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
|
||||
: await apiFetch('/api/projects', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
alert(err.error || '保存失败');
|
||||
return;
|
||||
}
|
||||
closeProjectModal();
|
||||
const saved = await res.json();
|
||||
await loadProjectsList();
|
||||
if (saved.id) await selectProject(saved.id);
|
||||
}
|
||||
|
||||
function closeProjectModal() {
|
||||
closeProjectsOverlay('project-modal');
|
||||
}
|
||||
|
||||
function formatProjectScopeJson() {
|
||||
const el = document.getElementById('project-edit-scope');
|
||||
if (!el) return;
|
||||
const raw = el.value.trim();
|
||||
if (!raw) return;
|
||||
try {
|
||||
el.value = JSON.stringify(JSON.parse(raw), null, 2);
|
||||
} catch (e) {
|
||||
alert('JSON 格式无效:' + (e.message || String(e)));
|
||||
}
|
||||
}
|
||||
|
||||
function insertProjectScopeExample() {
|
||||
const el = document.getElementById('project-edit-scope');
|
||||
if (!el) return;
|
||||
const example = {
|
||||
targets: ['https://example.com'],
|
||||
exclude: ['*.cdn.example.com'],
|
||||
notes: '仅授权 Web 应用层测试',
|
||||
};
|
||||
el.value = JSON.stringify(example, null, 2);
|
||||
el.focus();
|
||||
}
|
||||
|
||||
async function saveProjectSettings() {
|
||||
if (!currentProjectId) return;
|
||||
const scopeRaw = document.getElementById('project-edit-scope').value.trim();
|
||||
if (scopeRaw) {
|
||||
try {
|
||||
JSON.parse(scopeRaw);
|
||||
} catch (e) {
|
||||
alert('测试范围 JSON 无效,请先修正或点击「格式化」:' + (e.message || String(e)));
|
||||
return;
|
||||
}
|
||||
}
|
||||
const body = {
|
||||
name: document.getElementById('project-edit-name').value.trim(),
|
||||
description: document.getElementById('project-edit-description').value.trim(),
|
||||
scope_json: scopeRaw,
|
||||
status: document.getElementById('project-edit-status')?.value || 'active',
|
||||
};
|
||||
const res = await apiFetch(`/api/projects/${currentProjectId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!res.ok) return alert('保存失败');
|
||||
await loadProjectsList();
|
||||
await selectProject(currentProjectId);
|
||||
alert('已保存');
|
||||
}
|
||||
|
||||
async function archiveCurrentProject() {
|
||||
if (!currentProjectId) return;
|
||||
const statusEl = document.getElementById('project-edit-status');
|
||||
const cur = statusEl?.value || 'active';
|
||||
const next = cur === 'archived' ? 'active' : 'archived';
|
||||
if (!confirm(next === 'archived' ? '归档后默认不再出现在活跃列表,是否继续?' : '恢复为 active?')) return;
|
||||
const res = await apiFetch(`/api/projects/${currentProjectId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ status: next }),
|
||||
});
|
||||
if (!res.ok) return alert('操作失败');
|
||||
await loadProjectsList();
|
||||
await selectProject(currentProjectId);
|
||||
}
|
||||
|
||||
async function deleteCurrentProject() {
|
||||
if (!currentProjectId || !confirm('确定删除该项目?事实将一并删除,对话将解除绑定。')) return;
|
||||
const deletedId = currentProjectId;
|
||||
const deletedIndex = projectsCache.findIndex((p) => p.id === deletedId);
|
||||
const res = await apiFetch(`/api/projects/${deletedId}`, { method: 'DELETE' });
|
||||
if (!res.ok) return alert('删除失败');
|
||||
if (getActiveProjectId() === deletedId) setActiveProjectId('');
|
||||
currentProjectId = null;
|
||||
await loadProjectsList();
|
||||
if (projectsCache.length) {
|
||||
const nextIndex = Math.min(deletedIndex >= 0 ? deletedIndex : 0, projectsCache.length - 1);
|
||||
await selectProject(projectsCache[nextIndex].id);
|
||||
} else {
|
||||
updateProjectsDetailVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
function resetFactModalForm() {
|
||||
window._factModalEditId = null;
|
||||
const keyEl = document.getElementById('fact-modal-key');
|
||||
if (keyEl) keyEl.disabled = false;
|
||||
document.getElementById('fact-modal-title').textContent = '添加事实';
|
||||
document.getElementById('fact-modal-submit-btn').textContent = '保存事实';
|
||||
document.getElementById('fact-modal-key').value = '';
|
||||
document.getElementById('fact-modal-category').value = 'note';
|
||||
document.getElementById('fact-modal-summary').value = '';
|
||||
document.getElementById('fact-modal-body').value = '';
|
||||
document.getElementById('fact-modal-confidence').value = 'tentative';
|
||||
const rel = document.getElementById('fact-modal-related-vuln');
|
||||
if (rel) rel.value = '';
|
||||
}
|
||||
|
||||
function fillFactModalForm(f) {
|
||||
window._factModalEditId = f.id;
|
||||
document.getElementById('fact-modal-title').textContent = '编辑事实';
|
||||
document.getElementById('fact-modal-submit-btn').textContent = '保存修改';
|
||||
document.getElementById('fact-modal-key').value = f.fact_key || '';
|
||||
document.getElementById('fact-modal-category').value = f.category || 'note';
|
||||
document.getElementById('fact-modal-summary').value = f.summary || '';
|
||||
document.getElementById('fact-modal-body').value = f.body || '';
|
||||
const conf = (f.confidence || 'tentative').toLowerCase();
|
||||
const confEl = document.getElementById('fact-modal-confidence');
|
||||
if (confEl) {
|
||||
const allowed = ['tentative', 'confirmed', 'deprecated'];
|
||||
confEl.value = allowed.includes(conf) ? conf : 'tentative';
|
||||
}
|
||||
const rel = document.getElementById('fact-modal-related-vuln');
|
||||
if (rel) rel.value = f.related_vulnerability_id || '';
|
||||
}
|
||||
|
||||
function showAddFactModal() {
|
||||
if (!currentProjectId) return alert('请先选择项目');
|
||||
resetFactModalForm();
|
||||
openProjectsOverlay('fact-modal');
|
||||
}
|
||||
|
||||
async function showEditFactModal(factKey) {
|
||||
if (!currentProjectId) return alert('请先选择项目');
|
||||
const res = await apiFetch(
|
||||
`/api/projects/${currentProjectId}/facts?fact_key=${encodeURIComponent(factKey)}`,
|
||||
);
|
||||
if (!res.ok) return alert('加载事实失败');
|
||||
const f = await res.json();
|
||||
resetFactModalForm();
|
||||
fillFactModalForm(f);
|
||||
openProjectsOverlay('fact-modal');
|
||||
}
|
||||
|
||||
function closeFactModal() {
|
||||
closeProjectsOverlay('fact-modal');
|
||||
resetFactModalForm();
|
||||
}
|
||||
|
||||
async function saveFactModal() {
|
||||
const fact_key = document.getElementById('fact-modal-key').value.trim();
|
||||
const summary = document.getElementById('fact-modal-summary').value.trim();
|
||||
if (!fact_key || !summary) return alert('fact_key 与 summary 必填');
|
||||
const payload = {
|
||||
fact_key,
|
||||
category: document.getElementById('fact-modal-category').value.trim() || 'note',
|
||||
summary,
|
||||
body: document.getElementById('fact-modal-body').value,
|
||||
confidence: document.getElementById('fact-modal-confidence').value,
|
||||
related_vulnerability_id: document.getElementById('fact-modal-related-vuln')?.value?.trim() || '',
|
||||
};
|
||||
const editId = window._factModalEditId;
|
||||
const res = editId
|
||||
? await apiFetch(`/api/projects/${currentProjectId}/facts/${editId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
: await apiFetch(`/api/projects/${currentProjectId}/facts`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
return alert(err.error || '保存失败');
|
||||
}
|
||||
closeFactModal();
|
||||
loadProjectFacts();
|
||||
}
|
||||
|
||||
async function deleteProjectFact(id) {
|
||||
if (!confirm('删除该事实?')) return;
|
||||
await apiFetch(`/api/projects/${currentProjectId}/facts/${id}`, { method: 'DELETE' });
|
||||
loadProjectFacts();
|
||||
}
|
||||
|
||||
function parseProjectDate(t) {
|
||||
if (t == null || t === '') return null;
|
||||
if (typeof t === 'number' && Number.isFinite(t)) {
|
||||
const d = new Date(t);
|
||||
return isNaN(d.getTime()) || d.getFullYear() < 2000 ? null : d;
|
||||
}
|
||||
let s = String(t).trim();
|
||||
if (!s || s.startsWith('0001-01-01')) return null;
|
||||
let d = new Date(s);
|
||||
if (!isNaN(d.getTime()) && d.getFullYear() >= 2000) return d;
|
||||
const m = s.match(
|
||||
/^(\d{4})-(\d{2})-(\d{2})[T\s](\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?(?:([Zz]|([+-])(\d{2}):?(\d{2}))?)?$/,
|
||||
);
|
||||
if (m) {
|
||||
const ms = m[7] ? parseInt(String(m[7]).slice(0, 3).padEnd(3, '0'), 10) : 0;
|
||||
let offMin = 0;
|
||||
if (m[8] && m[9] && m[10]) {
|
||||
offMin = parseInt(m[10], 10) * 60 + parseInt(m[11] || '0', 10);
|
||||
if (m[9] === '-') offMin = -offMin;
|
||||
}
|
||||
d = new Date(
|
||||
Date.UTC(
|
||||
parseInt(m[1], 10),
|
||||
parseInt(m[2], 10) - 1,
|
||||
parseInt(m[3], 10),
|
||||
parseInt(m[4], 10),
|
||||
parseInt(m[5], 10),
|
||||
parseInt(m[6], 10),
|
||||
ms,
|
||||
) - offMin * 60 * 1000,
|
||||
);
|
||||
if (!isNaN(d.getTime()) && d.getFullYear() >= 2000) return d;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function formatProjectTime(t, fallback) {
|
||||
const d = parseProjectDate(t) || (fallback != null ? parseProjectDate(fallback) : null);
|
||||
if (!d) return '尚未更新';
|
||||
const now = Date.now();
|
||||
const diff = now - d.getTime();
|
||||
if (diff < 60000) return '刚刚';
|
||||
if (diff < 3600000) return `${Math.floor(diff / 60000)} 分钟前`;
|
||||
if (diff < 86400000) return `${Math.floor(diff / 3600000)} 小时前`;
|
||||
if (diff < 604800000) return `${Math.floor(diff / 86400000)} 天前`;
|
||||
return d.toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
if (s == null) return '';
|
||||
return String(s)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function getChatProjectSelection() {
|
||||
const convId = window.currentConversationId;
|
||||
if (convId) {
|
||||
return window._loadedConversationProjectId || '';
|
||||
}
|
||||
return getActiveProjectId();
|
||||
}
|
||||
|
||||
function updateChatProjectButtonLabel() {
|
||||
const textEl = document.getElementById('chat-project-text');
|
||||
if (!textEl) return;
|
||||
const id = getChatProjectSelection();
|
||||
textEl.textContent = id ? getProjectName(id) || id : '无项目';
|
||||
}
|
||||
|
||||
function renderChatProjectPanelList() {
|
||||
const list = document.getElementById('chat-project-list');
|
||||
if (!list) return;
|
||||
const selected = getChatProjectSelection();
|
||||
const activeProjects = projectsCache.filter((p) => p.status !== 'archived');
|
||||
const items = [{ id: '', name: '无项目', description: '不绑定项目黑板' }, ...activeProjects];
|
||||
if (!items.length) {
|
||||
list.innerHTML = '<div class="chat-project-panel-empty">暂无项目,可在「项目管理」中创建</div>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = '';
|
||||
items.forEach((p) => {
|
||||
const isNone = !p.id;
|
||||
const isSelected = isNone ? !selected : selected === p.id;
|
||||
const desc = isNone
|
||||
? (p.description || '')
|
||||
: (p.description || '').trim().slice(0, 80) || '共享事实黑板';
|
||||
const projectId = p.id || '';
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'role-selection-item-main' + (isSelected ? ' selected' : '');
|
||||
btn.setAttribute('role', 'option');
|
||||
btn.onclick = () => {
|
||||
selectChatProject(projectId);
|
||||
};
|
||||
btn.innerHTML = `
|
||||
<div class="role-selection-item-icon-main">${isNone ? '—' : '📁'}</div>
|
||||
<div class="role-selection-item-content-main">
|
||||
<div class="role-selection-item-name-main">${escapeHtml(p.name || '未命名')}</div>
|
||||
<div class="role-selection-item-description-main">${escapeHtml(desc)}</div>
|
||||
</div>
|
||||
${isSelected ? '<div class="role-selection-checkmark-main">✓</div>' : ''}
|
||||
`;
|
||||
list.appendChild(btn);
|
||||
});
|
||||
}
|
||||
|
||||
async function renderChatProjectPanel() {
|
||||
const list = document.getElementById('chat-project-list');
|
||||
if (!list) return;
|
||||
list.innerHTML = '<div class="chat-project-panel-loading">加载中…</div>';
|
||||
try {
|
||||
await ensureProjectsLoaded();
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
list.innerHTML = '<div class="chat-project-panel-empty">加载失败,请稍后重试</div>';
|
||||
return;
|
||||
}
|
||||
renderChatProjectPanelList();
|
||||
}
|
||||
|
||||
function closeChatProjectPanel() {
|
||||
const panel = document.getElementById('chat-project-panel');
|
||||
const btn = document.getElementById('chat-project-btn');
|
||||
if (panel) panel.style.display = 'none';
|
||||
if (btn) {
|
||||
btn.classList.remove('active');
|
||||
btn.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleChatProjectPanel() {
|
||||
const panel = document.getElementById('chat-project-panel');
|
||||
const btn = document.getElementById('chat-project-btn');
|
||||
if (!panel) return;
|
||||
const isHidden = panel.style.display === 'none' || !panel.style.display;
|
||||
if (!isHidden) {
|
||||
closeChatProjectPanel();
|
||||
return;
|
||||
}
|
||||
if (typeof closeRoleSelectionPanel === 'function') closeRoleSelectionPanel();
|
||||
if (typeof closeAgentModePanel === 'function') closeAgentModePanel();
|
||||
if (typeof closeChatReasoningPanel === 'function') closeChatReasoningPanel();
|
||||
panel.style.display = 'flex';
|
||||
if (btn) {
|
||||
btn.classList.add('active');
|
||||
btn.setAttribute('aria-expanded', 'true');
|
||||
}
|
||||
await renderChatProjectPanel();
|
||||
}
|
||||
|
||||
async function selectChatProject(projectId) {
|
||||
closeChatProjectPanel();
|
||||
await applyChatProjectSelection(projectId || '');
|
||||
}
|
||||
|
||||
async function applyChatProjectSelection(projectId) {
|
||||
const prev = getChatProjectSelection();
|
||||
if (projectId === prev) {
|
||||
updateChatProjectButtonLabel();
|
||||
return;
|
||||
}
|
||||
if (window.currentConversationId) {
|
||||
try {
|
||||
const res = await apiFetch(`/api/conversations/${encodeURIComponent(window.currentConversationId)}/project`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ projectId }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
throw new Error(err.error || res.statusText);
|
||||
}
|
||||
window._loadedConversationProjectId = projectId;
|
||||
if (typeof showNotification === 'function') {
|
||||
showNotification(projectId ? '已绑定项目' : '已解除项目绑定', 'success');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert('更新项目绑定失败: ' + (e.message || e));
|
||||
updateChatProjectButtonLabel();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
setActiveProjectId(projectId);
|
||||
}
|
||||
updateChatProjectButtonLabel();
|
||||
}
|
||||
|
||||
/** 对话页项目选择器:同步按钮文案;若浮层已打开则刷新列表 */
|
||||
async function refreshChatProjectSelector() {
|
||||
if (!document.getElementById('chat-project-btn')) return;
|
||||
try {
|
||||
await ensureProjectsLoaded();
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
updateChatProjectButtonLabel();
|
||||
const panel = document.getElementById('chat-project-panel');
|
||||
if (panel && panel.style.display === 'flex') {
|
||||
renderChatProjectPanelList();
|
||||
}
|
||||
}
|
||||
|
||||
async function onChatProjectChange() {
|
||||
/* 兼容旧调用;新 UI 使用 selectChatProject */
|
||||
await applyChatProjectSelection(getChatProjectSelection());
|
||||
}
|
||||
|
||||
function initChatProjectSelector() {
|
||||
if (window._chatProjectSelectorInited) return;
|
||||
window._chatProjectSelectorInited = true;
|
||||
prefetchProjectsForChat();
|
||||
updateChatProjectButtonLabel();
|
||||
document.addEventListener('click', (e) => {
|
||||
const panel = document.getElementById('chat-project-panel');
|
||||
const wrapper = document.querySelector('.project-selector-wrapper');
|
||||
if (!panel || panel.style.display === 'none' || !panel.style.display) return;
|
||||
if (!wrapper?.contains(e.target)) {
|
||||
closeChatProjectPanel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initChatProjectSelector);
|
||||
} else {
|
||||
initChatProjectSelector();
|
||||
}
|
||||
|
||||
window.initProjectsPage = initProjectsPage;
|
||||
window.showNewProjectModal = showNewProjectModal;
|
||||
window.saveProjectModal = saveProjectModal;
|
||||
window.closeProjectModal = closeProjectModal;
|
||||
window.selectProject = selectProject;
|
||||
window.switchProjectTab = switchProjectTab;
|
||||
window.showAddFactModal = showAddFactModal;
|
||||
window.showEditFactModal = showEditFactModal;
|
||||
window.editFactFromDetail = editFactFromDetail;
|
||||
window.saveFactModal = saveFactModal;
|
||||
window.closeFactModal = closeFactModal;
|
||||
window.closeFactDetailModal = closeFactDetailModal;
|
||||
window.saveProjectSettings = saveProjectSettings;
|
||||
window.archiveCurrentProject = archiveCurrentProject;
|
||||
window.deleteCurrentProject = deleteCurrentProject;
|
||||
window.refreshChatProjectSelector = refreshChatProjectSelector;
|
||||
window.onChatProjectChange = onChatProjectChange;
|
||||
window.toggleChatProjectPanel = toggleChatProjectPanel;
|
||||
window.closeChatProjectPanel = closeChatProjectPanel;
|
||||
window.selectChatProject = selectChatProject;
|
||||
window.prefetchProjectsForChat = prefetchProjectsForChat;
|
||||
window.getActiveProjectId = getActiveProjectId;
|
||||
window.getProjectName = getProjectName;
|
||||
window.viewProjectFactBody = viewProjectFactBody;
|
||||
window.deprecateProjectFactByKey = deprecateProjectFactByKey;
|
||||
window.openVulnerabilitiesForProject = openVulnerabilitiesForProject;
|
||||
window.openVulnerabilityDetail = openVulnerabilityDetail;
|
||||
window.filterProjectsList = filterProjectsList;
|
||||
window.rebuildProjectNameMap = rebuildProjectNameMap;
|
||||
window.projectNameById = projectNameById;
|
||||
+26
-15
@@ -244,30 +244,46 @@ function selectRole(roleName) {
|
||||
renderRoleSelectionSidebar(); // 重新渲染以更新选中状态
|
||||
}
|
||||
|
||||
function getChatRoleSelectorWrapper() {
|
||||
return document.getElementById('role-selector-wrapper')
|
||||
|| document.getElementById('role-selector-btn')?.closest('.role-selector-wrapper:not(.project-selector-wrapper)');
|
||||
}
|
||||
|
||||
function isRoleSelectionPanelOpen() {
|
||||
const panel = document.getElementById('role-selection-panel');
|
||||
if (!panel) return false;
|
||||
return panel.style.display !== 'none' && panel.style.display !== '';
|
||||
}
|
||||
|
||||
// 切换角色选择面板显示/隐藏
|
||||
function toggleRoleSelectionPanel() {
|
||||
const panel = document.getElementById('role-selection-panel');
|
||||
const roleSelectorBtn = document.getElementById('role-selector-btn');
|
||||
if (!panel) return;
|
||||
|
||||
const isHidden = panel.style.display === 'none' || !panel.style.display;
|
||||
const isHidden = !isRoleSelectionPanelOpen();
|
||||
|
||||
if (isHidden) {
|
||||
if (typeof closeAgentModePanel === 'function') {
|
||||
closeAgentModePanel();
|
||||
}
|
||||
if (typeof closeChatProjectPanel === 'function') {
|
||||
closeChatProjectPanel();
|
||||
}
|
||||
if (typeof closeChatReasoningPanel === 'function') {
|
||||
closeChatReasoningPanel();
|
||||
}
|
||||
renderRoleSelectionSidebar();
|
||||
panel.style.display = 'flex'; // 使用flex布局
|
||||
// 添加打开状态的视觉反馈
|
||||
if (roleSelectorBtn) {
|
||||
roleSelectorBtn.classList.add('active');
|
||||
roleSelectorBtn.setAttribute('aria-expanded', 'true');
|
||||
}
|
||||
|
||||
// 确保面板渲染后再检查位置
|
||||
setTimeout(() => {
|
||||
const wrapper = document.querySelector('.role-selector-wrapper');
|
||||
const wrapper = getChatRoleSelectorWrapper();
|
||||
if (wrapper) {
|
||||
const rect = wrapper.getBoundingClientRect();
|
||||
const panelHeight = panel.offsetHeight || 400;
|
||||
@@ -281,11 +297,7 @@ function toggleRoleSelectionPanel() {
|
||||
}
|
||||
}, 10);
|
||||
} else {
|
||||
panel.style.display = 'none';
|
||||
// 移除打开状态的视觉反馈
|
||||
if (roleSelectorBtn) {
|
||||
roleSelectorBtn.classList.remove('active');
|
||||
}
|
||||
closeRoleSelectionPanel();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,6 +310,7 @@ function closeRoleSelectionPanel() {
|
||||
}
|
||||
if (roleSelectorBtn) {
|
||||
roleSelectorBtn.classList.remove('active');
|
||||
roleSelectorBtn.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1568,9 +1581,9 @@ async function deleteRole(roleName) {
|
||||
}
|
||||
|
||||
// 在页面切换时初始化角色列表
|
||||
if (typeof switchPage === 'function') {
|
||||
const originalSwitchPage = switchPage;
|
||||
switchPage = function(page) {
|
||||
if (typeof window.switchPage === 'function') {
|
||||
const originalSwitchPage = window.switchPage;
|
||||
window.switchPage = function(page) {
|
||||
originalSwitchPage(page);
|
||||
if (page === 'roles-management') {
|
||||
loadRoles().then(() => renderRolesList());
|
||||
@@ -1590,11 +1603,9 @@ document.addEventListener('click', (e) => {
|
||||
closeRoleModal();
|
||||
}
|
||||
|
||||
// 点击角色选择面板外部关闭面板(但不包括角色选择按钮和面板本身)
|
||||
const roleSelectionPanel = document.getElementById('role-selection-panel');
|
||||
const roleSelectorWrapper = document.querySelector('.role-selector-wrapper');
|
||||
if (roleSelectionPanel && roleSelectionPanel.style.display !== 'none' && roleSelectionPanel.style.display) {
|
||||
// 检查点击是否在面板或包装器上
|
||||
// 点击角色选择面板外部关闭(须用 #role-selector-wrapper,勿用 .role-selector-wrapper:项目选择器也带该类)
|
||||
if (isRoleSelectionPanelOpen()) {
|
||||
const roleSelectorWrapper = getChatRoleSelectorWrapper();
|
||||
if (!roleSelectorWrapper?.contains(e.target)) {
|
||||
closeRoleSelectionPanel();
|
||||
}
|
||||
|
||||
+70
-7
@@ -25,6 +25,13 @@ function scheduleChatConversationFromHash(delayMs) {
|
||||
}
|
||||
const params = new URLSearchParams(hashParts.slice(1).join('?'));
|
||||
const conversationId = params.get('conversation');
|
||||
const projectId = params.get('project');
|
||||
if (projectId && typeof setActiveProjectId === 'function') {
|
||||
setActiveProjectId(projectId);
|
||||
if (typeof refreshChatProjectSelector === 'function') {
|
||||
refreshChatProjectSelector();
|
||||
}
|
||||
}
|
||||
if (!conversationId) {
|
||||
return;
|
||||
}
|
||||
@@ -50,7 +57,7 @@ function initRouter() {
|
||||
if (hash) {
|
||||
const hashParts = hash.split('?');
|
||||
const pageId = hashParts[0];
|
||||
if (pageId && ['dashboard', 'chat', 'hitl', 'info-collect', '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)) {
|
||||
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)) {
|
||||
switchPage(pageId);
|
||||
if (pageId === 'chat') {
|
||||
scheduleChatConversationFromHash(500);
|
||||
@@ -187,6 +194,24 @@ function updateNavState(pageId) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 读取侧栏子菜单项(仅 .nav-submenu 内,避免误匹配) */
|
||||
function getNavSubmenuItems(navItem) {
|
||||
if (!navItem) return [];
|
||||
const submenu = navItem.querySelector('.nav-submenu');
|
||||
if (!submenu) return [];
|
||||
return Array.from(submenu.querySelectorAll('.nav-submenu-item'));
|
||||
}
|
||||
|
||||
/** 仅一个子页时直接进入,避免展开后菜单在侧栏底部不可见 */
|
||||
function navigateSingleSubmenuPage(navItem) {
|
||||
const items = getNavSubmenuItems(navItem);
|
||||
if (items.length !== 1) return false;
|
||||
const pageId = items[0].getAttribute('data-page');
|
||||
if (!pageId) return false;
|
||||
switchPage(pageId);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 切换子菜单
|
||||
function toggleSubmenu(menuId) {
|
||||
const sidebar = document.getElementById('main-sidebar');
|
||||
@@ -194,24 +219,50 @@ function toggleSubmenu(menuId) {
|
||||
|
||||
if (!navItem) return;
|
||||
|
||||
const collapsed = sidebar && sidebar.classList.contains('collapsed');
|
||||
|
||||
// 检查侧边栏是否折叠
|
||||
if (sidebar && sidebar.classList.contains('collapsed')) {
|
||||
if (collapsed) {
|
||||
// 折叠状态下显示弹出菜单
|
||||
showSubmenuPopup(navItem, menuId);
|
||||
} else {
|
||||
// 展开状态下正常切换子菜单
|
||||
navItem.classList.toggle('expanded');
|
||||
return;
|
||||
}
|
||||
|
||||
// 展开侧栏且仅一个子项(角色、Agents 等):单击直接进入,无需再点二级菜单
|
||||
if (navigateSingleSubmenuPage(navItem)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 展开状态下切换子菜单,并滚入视口以便看到子项
|
||||
const willExpand = !navItem.classList.contains('expanded');
|
||||
navItem.classList.toggle('expanded');
|
||||
if (willExpand) {
|
||||
requestAnimationFrame(() => {
|
||||
navItem.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
||||
const items = getNavSubmenuItems(navItem);
|
||||
const last = items[items.length - 1];
|
||||
if (last) {
|
||||
last.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
window.toggleSubmenu = toggleSubmenu;
|
||||
|
||||
// 显示子菜单弹出框
|
||||
function showSubmenuPopup(navItem, menuId) {
|
||||
// 移除其他已打开的弹出菜单
|
||||
const existingPopup = document.querySelector('.submenu-popup');
|
||||
if (existingPopup) {
|
||||
const sameMenu = existingPopup.dataset.menuId === menuId;
|
||||
existingPopup.remove();
|
||||
return; // 如果已经打开,点击时关闭
|
||||
// 再次点击同一项:仅关闭;点击另一项:继续打开新菜单
|
||||
if (sameMenu) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (navigateSingleSubmenuPage(navItem)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const navItemContent = navItem.querySelector('.nav-item-content');
|
||||
@@ -225,6 +276,7 @@ function showSubmenuPopup(navItem, menuId) {
|
||||
// 创建弹出菜单
|
||||
const popup = document.createElement('div');
|
||||
popup.className = 'submenu-popup';
|
||||
popup.dataset.menuId = menuId;
|
||||
popup.style.position = 'fixed';
|
||||
popup.style.left = (rect.right + 8) + 'px';
|
||||
popup.style.top = rect.top + 'px';
|
||||
@@ -289,6 +341,12 @@ async function initPage(pageId) {
|
||||
case 'chat':
|
||||
// 恢复对话列表折叠状态(从其他页返回时保持用户选择)
|
||||
initConversationSidebarState();
|
||||
if (typeof prefetchProjectsForChat === 'function') {
|
||||
prefetchProjectsForChat();
|
||||
}
|
||||
if (typeof refreshChatProjectSelector === 'function') {
|
||||
refreshChatProjectSelector();
|
||||
}
|
||||
break;
|
||||
case 'hitl':
|
||||
if (typeof refreshHitlPending === 'function') {
|
||||
@@ -348,6 +406,11 @@ async function initPage(pageId) {
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'projects':
|
||||
if (typeof initProjectsPage === 'function') {
|
||||
initProjectsPage();
|
||||
}
|
||||
break;
|
||||
case 'vulnerabilities':
|
||||
// 初始化漏洞管理页面
|
||||
if (typeof initVulnerabilityPage === 'function') {
|
||||
|
||||
+10
-1
@@ -979,7 +979,16 @@ async function createBatchQueue() {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ title, tasks, role, agentMode, scheduleMode, cronExpr, executeNow }),
|
||||
body: JSON.stringify({
|
||||
title,
|
||||
tasks,
|
||||
role,
|
||||
agentMode,
|
||||
scheduleMode,
|
||||
cronExpr,
|
||||
executeNow,
|
||||
projectId: typeof getActiveProjectId === 'function' ? getActiveProjectId() || '' : '',
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -48,6 +48,7 @@ let currentVulnerabilityId = null;
|
||||
let vulnerabilityFilters = {
|
||||
q: '',
|
||||
id: '',
|
||||
project_id: '',
|
||||
conversation_id: '',
|
||||
task_id: '',
|
||||
conversation_tag: '',
|
||||
@@ -77,6 +78,7 @@ const VULN_FILTER_CHIP_FIELDS = [
|
||||
{ key: 'id', labelKey: 'vulnerabilityPage.vulnId' },
|
||||
{ key: 'status', labelKey: null, format: 'status' },
|
||||
{ key: 'severity', labelKey: null, format: 'severity' },
|
||||
{ key: 'project_id', labelKey: null },
|
||||
{ key: 'conversation_id', labelKey: 'vulnerabilityPage.conversationId' },
|
||||
{ key: 'task_id', labelKey: 'vulnerabilityPage.taskOrQueueId' },
|
||||
{ key: 'conversation_tag', labelKey: 'vulnerabilityPage.conversationTag' },
|
||||
@@ -98,13 +100,15 @@ function syncVulnerabilityFiltersFromLocationHash() {
|
||||
const st = (params.get('status') || '').trim();
|
||||
const convTag = (params.get('conversation_tag') || '').trim();
|
||||
const taskTag = (params.get('task_tag') || '').trim();
|
||||
const pid = (params.get('project_id') || '').trim();
|
||||
const q = (params.get('q') || params.get('search') || '').trim();
|
||||
if (!vid && !cid && !tid && !sev && !st && !convTag && !taskTag && !q) {
|
||||
if (!vid && !cid && !tid && !sev && !st && !convTag && !taskTag && !q && !pid) {
|
||||
return;
|
||||
}
|
||||
|
||||
vulnerabilityFilters.q = '';
|
||||
vulnerabilityFilters.id = '';
|
||||
vulnerabilityFilters.project_id = '';
|
||||
vulnerabilityFilters.conversation_id = '';
|
||||
vulnerabilityFilters.task_id = '';
|
||||
vulnerabilityFilters.conversation_tag = '';
|
||||
@@ -117,6 +121,7 @@ function syncVulnerabilityFiltersFromLocationHash() {
|
||||
const taskEl = document.getElementById('vulnerability-task-filter');
|
||||
const convTagEl = document.getElementById('vulnerability-conversation-tag-filter');
|
||||
const taskTagEl = document.getElementById('vulnerability-task-tag-filter');
|
||||
const projEl = document.getElementById('vulnerability-project-filter');
|
||||
const sevEl = document.getElementById('vulnerability-severity-filter');
|
||||
const stEl = document.getElementById('vulnerability-status-filter');
|
||||
if (searchEl) searchEl.value = '';
|
||||
@@ -125,6 +130,7 @@ function syncVulnerabilityFiltersFromLocationHash() {
|
||||
if (taskEl) taskEl.value = '';
|
||||
if (convTagEl) convTagEl.value = '';
|
||||
if (taskTagEl) taskTagEl.value = '';
|
||||
if (projEl) projEl.value = '';
|
||||
if (sevEl) sevEl.value = '';
|
||||
if (stEl) stEl.value = '';
|
||||
|
||||
@@ -132,6 +138,10 @@ function syncVulnerabilityFiltersFromLocationHash() {
|
||||
vulnerabilityFilters.q = q;
|
||||
if (searchEl) searchEl.value = q;
|
||||
}
|
||||
if (pid) {
|
||||
vulnerabilityFilters.project_id = pid;
|
||||
if (projEl) projEl.value = pid;
|
||||
}
|
||||
if (vid) {
|
||||
vulnerabilityFilters.id = vid;
|
||||
if (exactIdEl) exactIdEl.value = vid;
|
||||
@@ -167,12 +177,13 @@ function syncVulnerabilityFiltersFromLocationHash() {
|
||||
}
|
||||
|
||||
// 初始化漏洞管理页面
|
||||
function initVulnerabilityPage() {
|
||||
async function initVulnerabilityPage() {
|
||||
// 从localStorage加载每页条数设置
|
||||
vulnerabilityPagination.pageSize = getVulnerabilityPageSize();
|
||||
initVulnerabilityStatCards();
|
||||
initVulnerabilityFilterPanel();
|
||||
syncVulnerabilityFiltersFromLocationHash();
|
||||
await refreshVulnerabilityProjectFilter();
|
||||
updateVulnerabilityFilterPanelState();
|
||||
renderVulnerabilityFilterChips();
|
||||
loadVulnerabilityFilterOptions();
|
||||
@@ -224,6 +235,7 @@ function applyVulnerabilitySeverityFilter(severity) {
|
||||
function readVulnerabilityFiltersFromForm() {
|
||||
vulnerabilityFilters.q = (document.getElementById('vulnerability-search-filter')?.value || '').trim();
|
||||
vulnerabilityFilters.id = (document.getElementById('vulnerability-exact-id-filter')?.value || '').trim();
|
||||
vulnerabilityFilters.project_id = (document.getElementById('vulnerability-project-filter')?.value || '').trim();
|
||||
vulnerabilityFilters.conversation_id = (document.getElementById('vulnerability-conversation-filter')?.value || '').trim();
|
||||
vulnerabilityFilters.task_id = (document.getElementById('vulnerability-task-filter')?.value || '').trim();
|
||||
vulnerabilityFilters.conversation_tag = (document.getElementById('vulnerability-conversation-tag-filter')?.value || '').trim();
|
||||
@@ -241,7 +253,7 @@ function hasVulnerabilityAdvancedFiltersActive() {
|
||||
function hasAnyVulnerabilityFilterActive() {
|
||||
const f = vulnerabilityFilters;
|
||||
return Boolean(
|
||||
f.q || f.id || f.conversation_id || f.task_id || f.conversation_tag || f.task_tag || f.severity || f.status
|
||||
f.q || f.id || f.project_id || f.conversation_id || f.task_id || f.conversation_tag || f.task_tag || f.severity || f.status
|
||||
);
|
||||
}
|
||||
|
||||
@@ -265,6 +277,7 @@ function updateVulnerabilityLocationHashFromFilters() {
|
||||
const pairs = [
|
||||
['q', f.q],
|
||||
['id', f.id],
|
||||
['project_id', f.project_id],
|
||||
['conversation_id', f.conversation_id],
|
||||
['task_id', f.task_id],
|
||||
['conversation_tag', f.conversation_tag],
|
||||
@@ -476,6 +489,10 @@ function updateVulnerabilityFilterPanelState() {
|
||||
function formatVulnerabilityFilterChipValue(key, value) {
|
||||
if (key === 'severity') return vulnSeverityLabel(value);
|
||||
if (key === 'status') return vulnStatusLabel(value);
|
||||
if (key === 'project_id') {
|
||||
const name = typeof getProjectName === 'function' ? getProjectName(value) : '';
|
||||
return name && name !== value ? name : value;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -489,7 +506,7 @@ function renderVulnerabilityFilterChips() {
|
||||
VULN_FILTER_CHIP_FIELDS.forEach(function (field) {
|
||||
const val = vulnerabilityFilters[field.key];
|
||||
if (!val) return;
|
||||
const label = field.labelKey ? vulnT(field.labelKey) : '';
|
||||
const label = field.labelKey ? vulnT(field.labelKey) : (field.key === 'project_id' ? '项目' : '');
|
||||
const displayVal = formatVulnerabilityFilterChipValue(field.key, val);
|
||||
const text = label ? label + ': ' + displayVal : displayVal;
|
||||
chips.push({ key: field.key, text: text });
|
||||
@@ -529,6 +546,7 @@ function removeVulnerabilityFilterByKey(key) {
|
||||
task_id: 'vulnerability-task-filter',
|
||||
conversation_tag: 'vulnerability-conversation-tag-filter',
|
||||
task_tag: 'vulnerability-task-tag-filter',
|
||||
project_id: 'vulnerability-project-filter',
|
||||
severity: 'vulnerability-severity-filter',
|
||||
status: 'vulnerability-status-filter'
|
||||
};
|
||||
@@ -850,6 +868,12 @@ function renderVulnerabilities(vulnerabilities) {
|
||||
const severityText = vulnSeverityLabel(vuln.severity);
|
||||
const statusText = vulnStatusLabel(vuln.status);
|
||||
const createdDate = new Date(vuln.created_at).toLocaleString(vulnDateLocale());
|
||||
const projectLabel = vuln.project_id
|
||||
? escapeHtml(typeof getProjectName === 'function' ? getProjectName(vuln.project_id) : vuln.project_id)
|
||||
: escapeHtml(vulnT('vulnerabilityPage.projectUnbound'));
|
||||
const projectBadge = vuln.project_id
|
||||
? `<span class="vulnerability-project-badge" title="${escapeHtml(vuln.project_id)}">${escapeHtml(vulnT('vulnerabilityPage.detailProject'))}: ${projectLabel}</span>`
|
||||
: `<span class="vulnerability-project-badge vulnerability-project-badge--unbound">${escapeHtml(vulnT('vulnerabilityPage.projectUnbound'))}</span>`;
|
||||
const dlTitle = escapeHtml(vulnT('vulnerabilityPage.downloadMarkdownTitle'));
|
||||
const editTitle = escapeHtml(vulnT('common.edit'));
|
||||
const deleteTitle = escapeHtml(vulnT('common.delete'));
|
||||
@@ -867,6 +891,7 @@ function renderVulnerabilities(vulnerabilities) {
|
||||
<div class="vulnerability-meta">
|
||||
<span class="severity-badge ${severityClass}">${severityText}</span>
|
||||
<span class="status-badge status-${vuln.status}">${statusText}</span>
|
||||
${projectBadge}
|
||||
<span class="vulnerability-date">${createdDate}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -895,6 +920,7 @@ function renderVulnerabilities(vulnerabilities) {
|
||||
${vuln.description ? `<div class="vulnerability-description">${escapeHtml(vuln.description)}</div>` : ''}
|
||||
<div class="vulnerability-details">
|
||||
${vulnDetailField(vulnT('vulnerabilityPage.detailVulnId'), vuln.id, true)}
|
||||
${vulnDetailProjectField(vuln)}
|
||||
${vuln.type ? vulnDetailField(vulnT('vulnerabilityPage.detailType'), vuln.type, false) : ''}
|
||||
${vuln.target ? vulnDetailField(vulnT('vulnerabilityPage.detailTarget'), vuln.target, false) : ''}
|
||||
${vulnDetailField(vulnT('vulnerabilityPage.detailConversationId'), vuln.conversation_id, true)}
|
||||
@@ -1005,11 +1031,50 @@ async function changeVulnerabilityPageSize() {
|
||||
await loadVulnerabilities();
|
||||
}
|
||||
|
||||
function buildVulnerabilityProjectOptionsHtml(selectedId) {
|
||||
const sel = (selectedId || '').trim();
|
||||
let html = `<option value="">${escapeHtml(vulnT('vulnerabilityModal.projectNone'))}</option>`;
|
||||
const entries = typeof projectNameById !== 'undefined' ? Object.entries(projectNameById) : [];
|
||||
entries.sort((a, b) => (a[1] || '').localeCompare(b[1] || '', undefined, { sensitivity: 'base' }));
|
||||
entries.forEach(([id, name]) => {
|
||||
if (!id) return;
|
||||
const selected = id === sel ? ' selected' : '';
|
||||
html += `<option value="${escapeHtml(id)}"${selected}>${escapeHtml(name || id)}</option>`;
|
||||
});
|
||||
if (sel && !entries.some(([id]) => id === sel)) {
|
||||
html += `<option value="${escapeHtml(sel)}" selected>${escapeHtml(sel)}</option>`;
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
async function populateVulnerabilityModalProjectSelect(selectedId) {
|
||||
const sel = document.getElementById('vulnerability-project-id');
|
||||
if (!sel) return;
|
||||
try {
|
||||
const res = await apiFetch('/api/projects?limit=200');
|
||||
if (res.ok) {
|
||||
const list = await res.json();
|
||||
if (typeof rebuildProjectNameMap === 'function') {
|
||||
rebuildProjectNameMap(list);
|
||||
} else if (typeof projectNameById !== 'undefined') {
|
||||
(list || []).forEach((p) => { if (p.id) projectNameById[p.id] = p.name || p.id; });
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('加载项目列表失败', e);
|
||||
}
|
||||
sel.innerHTML = buildVulnerabilityProjectOptionsHtml(selectedId || '');
|
||||
sel.value = selectedId || '';
|
||||
}
|
||||
|
||||
// 显示添加漏洞模态框
|
||||
function showAddVulnerabilityModal() {
|
||||
async function showAddVulnerabilityModal() {
|
||||
currentVulnerabilityId = null;
|
||||
document.getElementById('vulnerability-modal-title').textContent = vulnT('vulnerability.addVuln');
|
||||
|
||||
const defaultProject = vulnerabilityFilters.project_id || '';
|
||||
await populateVulnerabilityModalProjectSelect(defaultProject);
|
||||
|
||||
// 清空表单
|
||||
document.getElementById('vulnerability-conversation-id').value = '';
|
||||
document.getElementById('vulnerability-conversation-tag').value = '';
|
||||
@@ -1051,6 +1116,8 @@ async function editVulnerability(id) {
|
||||
document.getElementById('vulnerability-impact').value = vuln.impact || '';
|
||||
document.getElementById('vulnerability-recommendation').value = vuln.recommendation || '';
|
||||
|
||||
await populateVulnerabilityModalProjectSelect(vuln.project_id || '');
|
||||
|
||||
document.getElementById('vulnerability-modal').style.display = 'block';
|
||||
} catch (error) {
|
||||
console.error('加载漏洞失败:', error);
|
||||
@@ -1069,8 +1136,11 @@ async function saveVulnerability() {
|
||||
return;
|
||||
}
|
||||
|
||||
const projectId = (document.getElementById('vulnerability-project-id')?.value || '').trim();
|
||||
|
||||
const data = {
|
||||
conversation_id: conversationId,
|
||||
project_id: projectId,
|
||||
conversation_tag: document.getElementById('vulnerability-conversation-tag').value.trim(),
|
||||
task_tag: document.getElementById('vulnerability-task-tag').value.trim(),
|
||||
title: title,
|
||||
@@ -1090,12 +1160,30 @@ async function saveVulnerability() {
|
||||
: '/api/vulnerabilities';
|
||||
const method = currentVulnerabilityId ? 'PUT' : 'POST';
|
||||
|
||||
let body = data;
|
||||
if (currentVulnerabilityId) {
|
||||
body = {
|
||||
project_id: projectId,
|
||||
conversation_tag: data.conversation_tag,
|
||||
task_tag: data.task_tag,
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
severity: data.severity,
|
||||
status: data.status,
|
||||
type: data.type,
|
||||
target: data.target,
|
||||
proof: data.proof,
|
||||
impact: data.impact,
|
||||
recommendation: data.recommendation,
|
||||
};
|
||||
}
|
||||
|
||||
const response = await apiFetch(url, {
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -1167,6 +1255,7 @@ function clearVulnerabilityFilters() {
|
||||
'vulnerability-task-filter',
|
||||
'vulnerability-conversation-tag-filter',
|
||||
'vulnerability-task-tag-filter',
|
||||
'vulnerability-project-filter',
|
||||
'vulnerability-severity-filter',
|
||||
'vulnerability-status-filter'
|
||||
];
|
||||
@@ -1178,6 +1267,7 @@ function clearVulnerabilityFilters() {
|
||||
vulnerabilityFilters = {
|
||||
q: '',
|
||||
id: '',
|
||||
project_id: '',
|
||||
conversation_id: '',
|
||||
task_id: '',
|
||||
conversation_tag: '',
|
||||
@@ -1272,6 +1362,21 @@ function vulnerabilityCopyEncoded(evt, encoded) {
|
||||
}
|
||||
}
|
||||
|
||||
function vulnDetailProjectField(vuln) {
|
||||
const label = vulnT('vulnerabilityPage.detailProject');
|
||||
const hint = escapeHtml(vulnT('vulnerabilityPage.projectBindHint'));
|
||||
return `<div class="vuln-detail-field">
|
||||
<div class="vuln-detail-field__label">${escapeHtml(label)}</div>
|
||||
<div class="vuln-detail-field__row">
|
||||
<select class="vuln-detail-field-select vulnerability-project-bind-select" data-vuln-id="${escapeHtml(vuln.id)}"
|
||||
onchange="bindVulnerabilityProject(this.dataset.vulnId, this.value, true)" onclick="event.stopPropagation();"
|
||||
title="${hint}" aria-label="${escapeHtml(label)}">
|
||||
${buildVulnerabilityProjectOptionsHtml(vuln.project_id || '')}
|
||||
</select>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function vulnDetailField(label, value, asCode) {
|
||||
if (value === undefined || value === null || String(value) === '') {
|
||||
return '';
|
||||
@@ -1352,7 +1457,7 @@ function buildVulnerabilityFilterParams() {
|
||||
if (vulnerabilityFilters.q) {
|
||||
params.append('q', vulnerabilityFilters.q);
|
||||
}
|
||||
const keys = ['id', 'conversation_id', 'task_id', 'conversation_tag', 'task_tag', 'severity', 'status'];
|
||||
const keys = ['id', 'project_id', 'conversation_id', 'task_id', 'conversation_tag', 'task_tag', 'severity', 'status'];
|
||||
keys.forEach(function (k) {
|
||||
if (vulnerabilityFilters[k]) {
|
||||
params.append(k, vulnerabilityFilters[k]);
|
||||
@@ -1470,3 +1575,80 @@ document.addEventListener('languagechange', function () {
|
||||
}
|
||||
});
|
||||
|
||||
async function bindVulnerabilityProject(vulnId, projectId, silent) {
|
||||
if (!vulnId) return;
|
||||
try {
|
||||
const response = await apiFetch(`/api/vulnerabilities/${encodeURIComponent(vulnId)}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ project_id: projectId || '' }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const err = await response.json().catch(() => ({}));
|
||||
throw new Error(err.error || vulnT('vulnerabilityPage.projectBindFailed'));
|
||||
}
|
||||
if (!silent) {
|
||||
alert(vulnT('vulnerabilityPage.projectBindOk'));
|
||||
}
|
||||
loadVulnerabilityStats();
|
||||
loadVulnerabilities();
|
||||
} catch (error) {
|
||||
console.error('绑定项目失败:', error);
|
||||
alert(vulnT('vulnerabilityPage.projectBindFailed') + ': ' + error.message);
|
||||
loadVulnerabilities();
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshVulnerabilityProjectFilter() {
|
||||
const sel = document.getElementById('vulnerability-project-filter');
|
||||
if (!sel) return;
|
||||
try {
|
||||
const res = await apiFetch('/api/projects?limit=200');
|
||||
if (!res.ok) return;
|
||||
const list = await res.json();
|
||||
if (typeof rebuildProjectNameMap === 'function') {
|
||||
rebuildProjectNameMap(list);
|
||||
} else if (typeof projectNameById !== 'undefined') {
|
||||
list.forEach((p) => { if (p.id) projectNameById[p.id] = p.name || p.id; });
|
||||
}
|
||||
const cur = vulnerabilityFilters.project_id || sel.value || '';
|
||||
let html = '<option value="">全部项目</option>';
|
||||
(list || []).forEach((p) => {
|
||||
if (!p.id) return;
|
||||
const selected = p.id === cur ? ' selected' : '';
|
||||
const arch = p.status === 'archived' ? ' [归档]' : '';
|
||||
html += `<option value="${escapeHtml(p.id)}"${selected}>${escapeHtml(p.name || p.id)}${arch}</option>`;
|
||||
});
|
||||
sel.innerHTML = html;
|
||||
if (cur) sel.value = cur;
|
||||
const modalSel = document.getElementById('vulnerability-project-id');
|
||||
if (modalSel && document.getElementById('vulnerability-modal')?.style.display === 'block') {
|
||||
const modalCur = modalSel.value || '';
|
||||
modalSel.innerHTML = buildVulnerabilityProjectOptionsHtml(modalCur);
|
||||
modalSel.value = modalCur;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('加载项目筛选列表失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
function setVulnerabilityProjectFilter(projectId) {
|
||||
vulnerabilityFilters.project_id = projectId || '';
|
||||
const sel = document.getElementById('vulnerability-project-filter');
|
||||
if (sel) sel.value = projectId || '';
|
||||
applyVulnerabilityFilters();
|
||||
}
|
||||
|
||||
function setVulnerabilityIdFilter(vulnId) {
|
||||
vulnerabilityFilters.id = vulnId || '';
|
||||
const el = document.getElementById('vulnerability-exact-id-filter');
|
||||
if (el) el.value = vulnId || '';
|
||||
applyVulnerabilityFilters();
|
||||
}
|
||||
|
||||
window.refreshVulnerabilityProjectFilter = refreshVulnerabilityProjectFilter;
|
||||
window.setVulnerabilityProjectFilter = setVulnerabilityProjectFilter;
|
||||
window.setVulnerabilityIdFilter = setVulnerabilityIdFilter;
|
||||
window.bindVulnerabilityProject = bindVulnerabilityProject;
|
||||
window.buildVulnerabilityProjectOptionsHtml = buildVulnerabilityProjectOptionsHtml;
|
||||
|
||||
|
||||
+315
-2
@@ -161,6 +161,16 @@
|
||||
<span data-i18n="nav.tasks">任务管理</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-item" data-page="projects">
|
||||
<div class="nav-item-content" data-title="项目管理" onclick="switchPage('projects')">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polygon points="12 2 2 7 12 12 22 7 12 2"></polygon>
|
||||
<polyline points="2 17 12 22 22 17"></polyline>
|
||||
<polyline points="2 12 12 17 22 12"></polyline>
|
||||
</svg>
|
||||
<span>项目管理</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-item" data-page="vulnerabilities">
|
||||
<div class="nav-item-content" data-title="漏洞管理" onclick="switchPage('vulnerabilities')" data-i18n="nav.vulnerabilities" data-i18n-attr="data-title" data-i18n-skip-text="true">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
@@ -944,8 +954,28 @@
|
||||
<div id="chat-input-container" class="chat-input-container">
|
||||
<div class="chat-input-primary-row">
|
||||
<div class="chat-input-leading">
|
||||
<div class="role-selector-wrapper">
|
||||
<button id="role-selector-btn" class="role-selector-btn" onclick="toggleRoleSelectionPanel()" data-i18n="chat.selectRole" data-i18n-attr="title" title="选择角色">
|
||||
<div class="role-selector-wrapper project-selector-wrapper">
|
||||
<button type="button" id="chat-project-btn" class="role-selector-btn" onclick="toggleChatProjectPanel()" aria-label="选择项目" aria-haspopup="listbox" aria-expanded="false" title="绑定项目后共享事实黑板(跨对话)">
|
||||
<span class="role-selector-icon" aria-hidden="true">📁</span>
|
||||
<span id="chat-project-text" class="role-selector-text">无项目</span>
|
||||
<svg class="role-selector-arrow" width="10" height="10" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div id="chat-project-panel" class="role-selection-panel chat-project-panel" style="display: none;" role="listbox" aria-labelledby="chat-project-panel-title">
|
||||
<div class="role-selection-panel-header">
|
||||
<h3 id="chat-project-panel-title" class="role-selection-panel-title">选择项目</h3>
|
||||
<button type="button" class="role-selection-panel-close" onclick="closeChatProjectPanel()" title="关闭" aria-label="关闭">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="chat-project-list" class="role-selection-list-main"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="role-selector-wrapper" class="role-selector-wrapper">
|
||||
<button type="button" id="role-selector-btn" class="role-selector-btn" onclick="toggleRoleSelectionPanel()" data-i18n="chat.selectRole" data-i18n-attr="title" title="选择角色">
|
||||
<span id="role-selector-icon" class="role-selector-icon">🔵</span>
|
||||
<span id="role-selector-text" class="role-selector-text" data-i18n="chat.defaultRole">默认</span>
|
||||
<svg class="role-selector-arrow" width="10" height="10" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -1384,6 +1414,177 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 项目管理页面 -->
|
||||
<div id="page-projects" class="page projects-page">
|
||||
<div class="page-header">
|
||||
<h2>项目管理</h2>
|
||||
<div class="page-header-actions">
|
||||
<label class="projects-show-archived-label"><input type="checkbox" id="projects-show-archived" onchange="loadProjectsList()"> 显示已归档</label>
|
||||
<button class="btn-secondary" type="button" onclick="loadProjectsList()">刷新</button>
|
||||
<button class="btn-primary" type="button" onclick="showNewProjectModal()">+ 新建项目</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-content projects-page-layout">
|
||||
<aside class="projects-sidebar-card">
|
||||
<div class="projects-sidebar-head">
|
||||
<span class="projects-sidebar-title">项目列表</span>
|
||||
<span class="projects-sidebar-count" id="projects-list-count">0</span>
|
||||
</div>
|
||||
<div class="projects-sidebar-search">
|
||||
<input type="search" id="projects-list-search" class="form-input" placeholder="搜索项目…" oninput="filterProjectsList()" autocomplete="off">
|
||||
</div>
|
||||
<div id="projects-list" class="projects-list"></div>
|
||||
</aside>
|
||||
<main class="projects-detail" id="projects-detail-main">
|
||||
<div class="projects-detail-placeholder" id="projects-detail-placeholder">
|
||||
<h3>选择或创建项目</h3>
|
||||
<p>项目用于跨对话共享「事实黑板」:目标、环境、认证等信息会在绑定项目的对话中自动注入。</p>
|
||||
<button class="btn-primary" type="button" onclick="showNewProjectModal()">创建第一个项目</button>
|
||||
</div>
|
||||
<div class="projects-detail-inner" id="projects-detail-inner" hidden>
|
||||
<header class="projects-detail-header">
|
||||
<div class="projects-detail-header-main">
|
||||
<div class="projects-detail-title-row">
|
||||
<h3 id="projects-detail-title" class="projects-detail-title">项目</h3>
|
||||
<span id="projects-detail-status" class="projects-status-pill projects-status-pill--active">进行中</span>
|
||||
</div>
|
||||
<p id="projects-detail-meta" class="projects-detail-meta"></p>
|
||||
<p id="projects-detail-desc" class="projects-detail-desc"></p>
|
||||
<div class="projects-detail-stats" id="projects-detail-stats">
|
||||
<span class="projects-stat-chip" id="project-stat-facts">0 条事实</span>
|
||||
<span class="projects-stat-chip" id="project-stat-vulns">0 个漏洞</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="projects-detail-header-actions">
|
||||
<button type="button" class="btn-secondary btn-small" onclick="openVulnerabilitiesForProject()">漏洞管理</button>
|
||||
<button type="button" class="btn-primary btn-small" onclick="showAddFactModal()">+ 添加事实</button>
|
||||
</div>
|
||||
</header>
|
||||
<nav class="projects-tabs" role="tablist">
|
||||
<button type="button" id="project-tab-facts" class="projects-tab is-active" role="tab" onclick="switchProjectTab('facts')">事实黑板</button>
|
||||
<button type="button" id="project-tab-vulns" class="projects-tab" role="tab" onclick="switchProjectTab('vulns')">关联漏洞</button>
|
||||
<button type="button" id="project-tab-settings" class="projects-tab" role="tab" onclick="switchProjectTab('settings')">设置</button>
|
||||
</nav>
|
||||
<div id="project-panel-facts" class="projects-panel" role="tabpanel">
|
||||
<div class="projects-panel-toolbar">
|
||||
<span class="projects-panel-hint">Agent 每轮可见 key + 摘要;完整内容通过 get_project_fact 获取</span>
|
||||
<button class="btn-primary btn-small" type="button" onclick="showAddFactModal()">+ 添加事实</button>
|
||||
</div>
|
||||
<div class="projects-table-wrap">
|
||||
<table class="data-table data-table--projects">
|
||||
<thead><tr><th>Key</th><th>分类</th><th>摘要</th><th>置信度</th><th>更新</th><th class="col-actions">操作</th></tr></thead>
|
||||
<tbody id="project-facts-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div id="project-panel-vulns" class="projects-panel" role="tabpanel" hidden>
|
||||
<div class="projects-panel-toolbar">
|
||||
<span class="projects-panel-hint">本项目下记录的漏洞汇总</span>
|
||||
<button type="button" class="btn-secondary btn-small" onclick="openVulnerabilitiesForProject()">在漏洞管理中查看</button>
|
||||
</div>
|
||||
<div class="projects-table-wrap">
|
||||
<table class="data-table data-table--projects">
|
||||
<thead><tr><th>标题</th><th>严重度</th><th>状态</th><th class="col-actions">操作</th></tr></thead>
|
||||
<tbody id="project-vulns-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div id="project-panel-settings" class="projects-panel projects-panel--settings" role="tabpanel" hidden>
|
||||
<div class="projects-settings-layout">
|
||||
<header class="projects-settings-intro">
|
||||
<div class="projects-settings-intro-text">
|
||||
<h4 class="projects-settings-intro-title">项目设置</h4>
|
||||
<p class="projects-settings-intro-hint">配置项目元数据与 Agent 授权边界,保存后即时生效于绑定对话。</p>
|
||||
</div>
|
||||
</header>
|
||||
<div class="projects-settings-grid">
|
||||
<section class="projects-settings-card projects-settings-card--basic">
|
||||
<div class="projects-settings-card-head">
|
||||
<div class="projects-settings-card-head-left">
|
||||
<span class="projects-settings-icon projects-settings-icon--blue" aria-hidden="true">
|
||||
<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="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
||||
</span>
|
||||
<div>
|
||||
<h4 class="projects-settings-card-title">基本信息</h4>
|
||||
<p class="projects-settings-card-hint">名称与描述会显示在项目详情中</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="projects-settings-card-body">
|
||||
<div class="projects-form-row projects-form-row--2">
|
||||
<div class="projects-form-field">
|
||||
<label for="project-edit-name">项目名称</label>
|
||||
<input type="text" id="project-edit-name" class="form-input" placeholder="例如:某客户 Web 渗透">
|
||||
</div>
|
||||
<div class="projects-form-field">
|
||||
<label for="project-edit-status">状态</label>
|
||||
<div class="projects-status-select-wrap">
|
||||
<select id="project-edit-status" class="form-input projects-status-select">
|
||||
<option value="active">进行中</option>
|
||||
<option value="archived">已归档</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="projects-form-field">
|
||||
<label for="project-edit-description">描述</label>
|
||||
<textarea id="project-edit-description" class="form-input" rows="4" placeholder="测试目标、授权范围、联系人、注意事项…"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="projects-settings-card projects-settings-card--scope">
|
||||
<div class="projects-settings-card-head">
|
||||
<div class="projects-settings-card-head-left">
|
||||
<span class="projects-settings-icon projects-settings-icon--violet" aria-hidden="true">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
|
||||
</span>
|
||||
<div>
|
||||
<h4 class="projects-settings-card-title">测试范围</h4>
|
||||
<p class="projects-settings-card-hint">JSON 格式,供 Agent 理解授权边界与目标资产</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="projects-scope-toolbar">
|
||||
<button type="button" class="btn-ghost btn-small" onclick="formatProjectScopeJson()" title="格式化 JSON">格式化</button>
|
||||
<button type="button" class="btn-ghost btn-small" onclick="insertProjectScopeExample()" title="插入示例">示例</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="projects-settings-card-body projects-settings-card-body--fill">
|
||||
<div class="projects-scope-editor">
|
||||
<label for="project-edit-scope" class="sr-only">范围 JSON</label>
|
||||
<textarea id="project-edit-scope" class="form-input form-input--mono projects-scope-textarea" spellcheck="false" placeholder='{"targets":["https://example.com"],"exclude":["*.cdn.example.com"]}'></textarea>
|
||||
</div>
|
||||
<p class="projects-scope-footnote">支持 <code>targets</code>、<code>exclude</code>、<code>notes</code> 等字段,留空表示不限制范围。</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<section class="projects-settings-card projects-settings-card--danger">
|
||||
<div class="projects-settings-danger-main">
|
||||
<span class="projects-settings-icon projects-settings-icon--red" aria-hidden="true">
|
||||
<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"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
|
||||
</span>
|
||||
<div>
|
||||
<h4 class="projects-settings-card-title">危险操作</h4>
|
||||
<p class="projects-settings-card-hint">归档后需在列表勾选「显示已归档」才能查看;删除将清除全部事实且不可恢复。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="projects-settings-danger-actions">
|
||||
<button class="btn-secondary btn-small" type="button" onclick="archiveCurrentProject()">归档 / 恢复</button>
|
||||
<button class="btn-secondary btn-small btn-danger-outline" type="button" onclick="deleteCurrentProject()">删除项目</button>
|
||||
</div>
|
||||
</section>
|
||||
<footer class="projects-settings-footer">
|
||||
<span class="projects-settings-footer-hint">修改后请点击保存以同步到服务器</span>
|
||||
<button class="btn-primary" type="button" onclick="saveProjectSettings()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
|
||||
保存更改
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 漏洞管理页面 -->
|
||||
<div id="page-vulnerabilities" class="page">
|
||||
<div class="page-header">
|
||||
@@ -1456,6 +1657,12 @@
|
||||
<input type="search" id="vulnerability-search-filter" autocomplete="off"
|
||||
data-i18n="vulnerabilityPage.searchKeyword" data-i18n-attr="placeholder" placeholder="搜索标题、描述、类型、目标…" />
|
||||
</label>
|
||||
<label class="vulnerability-filter-field vulnerability-filter-field--project">
|
||||
<span class="sr-only">项目</span>
|
||||
<select id="vulnerability-project-filter" title="按项目筛选" onchange="scheduleVulnerabilityFilterApply(true)">
|
||||
<option value="">全部项目</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="vulnerability-filter-field vulnerability-filter-field--status">
|
||||
<span class="sr-only" data-i18n="vulnerabilityPage.status">状态</span>
|
||||
<select id="vulnerability-status-filter" data-i18n-attr="title" title="状态">
|
||||
@@ -3492,6 +3699,13 @@
|
||||
<span class="modal-close" onclick="closeVulnerabilityModal()">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="vulnerability-project-id" data-i18n="vulnerabilityModal.project">所属项目</label>
|
||||
<select id="vulnerability-project-id" class="form-input">
|
||||
<option value="" data-i18n="vulnerabilityModal.projectNone">(未绑定)</option>
|
||||
</select>
|
||||
<p class="form-hint" data-i18n="vulnerabilityModal.projectHint">绑定后 Agent 在项目范围内可通过 list_vulnerabilities 看到本条记录;留空则尝试从会话自动关联。</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="vulnerability-conversation-id"><span data-i18n="vulnerabilityModal.conversationId">会话ID</span> <span style="color: red;">*</span></label>
|
||||
<input type="text" id="vulnerability-conversation-id" data-i18n="vulnerabilityModal.conversationIdPlaceholder" data-i18n-attr="placeholder" placeholder="输入会话ID" required />
|
||||
@@ -3724,6 +3938,104 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 项目管理弹窗(挂 body 下,避免被 .page overflow 裁剪) -->
|
||||
<div id="project-modal" class="modal-overlay projects-modal-overlay" style="display:none;" role="dialog" aria-modal="true" aria-labelledby="project-modal-title" onclick="if(event.target===this)closeProjectModal()">
|
||||
<div class="projects-modal-dialog" onclick="event.stopPropagation()">
|
||||
<div class="projects-modal-header">
|
||||
<div class="projects-modal-header-text">
|
||||
<div>
|
||||
<h3 id="project-modal-title">新建项目</h3>
|
||||
<p id="project-modal-subtitle" class="projects-modal-subtitle">创建后可绑定对话,跨会话共享事实黑板</p>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="projects-modal-close" onclick="closeProjectModal()" aria-label="关闭">×</button>
|
||||
</div>
|
||||
<div class="projects-modal-body">
|
||||
<div class="projects-form-field">
|
||||
<label for="project-modal-name">项目名称 <span class="required">*</span></label>
|
||||
<input type="text" id="project-modal-name" class="form-input" placeholder="例如:某客户 Web 渗透" autocomplete="off">
|
||||
</div>
|
||||
<div class="projects-form-field">
|
||||
<label for="project-modal-description">项目描述</label>
|
||||
<textarea id="project-modal-description" class="form-input" rows="4" placeholder="测试范围、授权边界、注意事项…"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="projects-modal-footer">
|
||||
<button class="btn-secondary" type="button" onclick="closeProjectModal()">取消</button>
|
||||
<button class="btn-primary" type="button" id="project-modal-submit-btn" onclick="saveProjectModal()">创建项目</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="fact-modal" class="modal-overlay projects-modal-overlay" style="display:none;" role="dialog" aria-modal="true" onclick="if(event.target===this)closeFactModal()">
|
||||
<div class="projects-modal-dialog projects-modal-dialog--wide" onclick="event.stopPropagation()">
|
||||
<div class="projects-modal-header">
|
||||
<div class="projects-modal-header-text">
|
||||
<div>
|
||||
<h3 id="fact-modal-title">添加事实</h3>
|
||||
<p class="projects-modal-subtitle">摘要会注入 Agent;完整内容通过 get_project_fact 获取</p>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="projects-modal-close" onclick="closeFactModal()" aria-label="关闭">×</button>
|
||||
</div>
|
||||
<div class="projects-modal-body">
|
||||
<div class="projects-form-field">
|
||||
<label for="fact-modal-key">fact_key</label>
|
||||
<input type="text" id="fact-modal-key" class="form-input" placeholder="target/primary_domain">
|
||||
</div>
|
||||
<div class="projects-form-row">
|
||||
<div class="projects-form-field">
|
||||
<label for="fact-modal-category">分类</label>
|
||||
<input type="text" id="fact-modal-category" class="form-input" value="note">
|
||||
</div>
|
||||
<div class="projects-form-field">
|
||||
<label for="fact-modal-confidence">置信度</label>
|
||||
<select id="fact-modal-confidence" class="form-input">
|
||||
<option value="tentative">待确认</option>
|
||||
<option value="confirmed">已确认</option>
|
||||
<option value="deprecated">已废弃</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="projects-form-field">
|
||||
<label for="fact-modal-summary">摘要</label>
|
||||
<input type="text" id="fact-modal-summary" class="form-input" placeholder="一行概述,会注入到 Agent 上下文">
|
||||
</div>
|
||||
<div class="projects-form-field">
|
||||
<label for="fact-modal-body">body(完整详情)</label>
|
||||
<textarea id="fact-modal-body" class="form-input" rows="5" placeholder="POC、长文本、原始输出等"></textarea>
|
||||
</div>
|
||||
<div class="projects-form-field">
|
||||
<label for="fact-modal-related-vuln">关联漏洞 ID</label>
|
||||
<input type="text" id="fact-modal-related-vuln" class="form-input" placeholder="可选">
|
||||
</div>
|
||||
</div>
|
||||
<div class="projects-modal-footer">
|
||||
<button class="btn-secondary" type="button" onclick="closeFactModal()">取消</button>
|
||||
<button class="btn-primary" type="button" id="fact-modal-submit-btn" onclick="saveFactModal()">保存事实</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="fact-detail-modal" class="modal-overlay projects-modal-overlay" style="display:none;" role="dialog" aria-modal="true" onclick="if(event.target===this)closeFactDetailModal()">
|
||||
<div class="projects-modal-dialog projects-modal-dialog--wide" onclick="event.stopPropagation()">
|
||||
<div class="projects-modal-header">
|
||||
<div class="projects-modal-header-text">
|
||||
<div>
|
||||
<h3 id="fact-detail-title">事实详情</h3>
|
||||
<p id="fact-detail-meta" class="projects-modal-subtitle"></p>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="projects-modal-close" onclick="closeFactDetailModal()" aria-label="关闭">×</button>
|
||||
</div>
|
||||
<div class="projects-modal-body">
|
||||
<pre id="fact-detail-body" class="fact-detail-body"></pre>
|
||||
</div>
|
||||
<div class="projects-modal-footer">
|
||||
<button class="btn-secondary" type="button" onclick="closeFactDetailModal()">关闭</button>
|
||||
<button class="btn-primary" type="button" id="fact-detail-edit-btn" onclick="editFactFromDetail()">编辑</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/i18next@23.11.5/i18next.min.js"></script>
|
||||
<script src="/static/js/i18n.js"></script>
|
||||
<script src="/static/js/builtin-tools.js"></script>
|
||||
@@ -3744,6 +4056,7 @@
|
||||
<script src="/static/js/terminal.js"></script>
|
||||
<script src="/static/js/knowledge.js"></script>
|
||||
<script src="/static/js/skills.js"></script>
|
||||
<script src="/static/js/projects.js"></script>
|
||||
<script src="/static/js/vulnerability.js?v=12"></script>
|
||||
<script src="/static/js/webshell.js"></script>
|
||||
<script src="/static/js/chat-files.js"></script>
|
||||
|
||||
Reference in New Issue
Block a user