/** * 项目管理与事实黑板 */ 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 `
`; } function formatSeverityBadge(severity) { const s = (severity || 'info').toLowerCase(); const cls = 'projects-severity--' + (['critical', 'high', 'medium', 'low', 'info'].includes(s) ? s : 'info'); return `${escapeHtml(severity || '—')}`; } 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 = '
暂无项目
'; updateProjectsDetailVisibility(); return; } if (!list.length) { el.innerHTML = '
无匹配项目
'; 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 ? '置顶' : '', p.status === 'archived' ? '归档' : '', ].join(''); return `
${escapeHtml(p.name)}${badges}
${formatProjectTime(p.updated_at)}
`; }).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 = '加载中…'; const res = await apiFetch(`/api/projects/${currentProjectId}/facts?limit=200`); if (!res.ok) { tbody.innerHTML = '加载失败'; return; } const facts = await res.json(); if (!facts.length) { tbody.innerHTML = '暂无事实,点击「添加事实」或由 Agent 自动写入'; refreshProjectHeaderStats(); return; } tbody.innerHTML = facts.map((f) => { const keyEsc = escapeHtml(f.fact_key); const idEsc = escapeHtml(f.id); return ` ${keyEsc} ${escapeHtml(f.category)} ${escapeHtml(f.summary)} ${formatConfidenceBadge(f.confidence)} ${formatProjectTime(f.updated_at, f.created_at)} ${renderProjectFactActions(keyEsc, idEsc)} `; }).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 = '加载中…'; const res = await apiFetch(`/api/vulnerabilities?project_id=${encodeURIComponent(currentProjectId)}&limit=100`); if (!res.ok) { tbody.innerHTML = '加载失败'; return; } const data = await res.json(); const items = data.Vulnerabilities || data.vulnerabilities || data.items || []; if (!items.length) { tbody.innerHTML = '本项目暂无漏洞记录'; refreshProjectHeaderStats(); return; } tbody.innerHTML = items.map((v) => { const idEsc = escapeHtml(v.id); return ` ${escapeHtml(v.title)} ${formatSeverityBadge(v.severity)} ${escapeHtml(v.status)}
`; }).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, '"'); } 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 = '
暂无项目,可在「项目管理」中创建
'; 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 = `
${isNone ? '—' : '📁'}
${escapeHtml(p.name || '未命名')}
${escapeHtml(desc)}
${isSelected ? '
' : ''} `; list.appendChild(btn); }); } async function renderChatProjectPanel() { const list = document.getElementById('chat-project-list'); if (!list) return; list.innerHTML = '
加载中…
'; try { await ensureProjectsLoaded(); } catch (e) { console.warn(e); list.innerHTML = '
加载失败,请稍后重试
'; 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;