/** * 项目管理与事实黑板 */ 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 `${escapeHtml(label)}`; } function renderProjectFactActions(keyEsc, idEsc) { return `
${keyEsc}