diff --git a/web/static/css/style.css b/web/static/css/style.css index 94428ab8..3de68c95 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -14880,6 +14880,10 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible { position: relative; flex-shrink: 0; } +.ws-project-selector-wrapper { + position: relative; + flex-shrink: 0; +} .ws-agent-mode-wrapper { flex-shrink: 0; } diff --git a/web/static/js/projects.js b/web/static/js/projects.js index 49d6dfb6..64d24a28 100644 --- a/web/static/js/projects.js +++ b/web/static/js/projects.js @@ -1270,12 +1270,18 @@ async function saveProjectModal() { return; } const fromChat = !!window._projectModalFromChat; + const fromWebshellConnId = window._projectModalFromWebshellConnId || ''; window._projectModalFromChat = false; + window._projectModalFromWebshellConnId = ''; closeProjectModal(); const saved = await res.json(); await loadProjectsList(); if (saved.id) { - if (fromChat && !editId) { + if (fromWebshellConnId && !editId) { + if (typeof applyWebshellAiProjectSelection === 'function') { + await applyWebshellAiProjectSelection(saved.id); + } + } else if (fromChat && !editId) { await applyChatProjectSelection(saved.id); } else { await selectProject(saved.id); diff --git a/web/static/js/webshell.js b/web/static/js/webshell.js index c06f82a0..fee81d99 100644 --- a/web/static/js/webshell.js +++ b/web/static/js/webshell.js @@ -27,6 +27,9 @@ const WEBSHELL_HISTORY_MAX = 100; let webshellClearInProgress = false; // AI 助手:按连接 ID 保存对话 ID,便于多轮对话 let webshellAiConvMap = {}; +// AI 助手:项目绑定(已有对话按 convId,新对话按 connId 草稿) +let webshellAiProjectByConvId = {}; +let webshellAiDraftProjectByConn = {}; let webshellAiSending = false; let webshellAiAbortController = null; // AbortController for current AI stream let webshellAiStreamReader = null; // Current ReadableStreamDefaultReader @@ -266,6 +269,7 @@ function wsToggleRolePanel() { var isOpen = panel.style.display === 'flex'; if (isOpen) { wsCloseRolePanel(); return; } wsCloseAgentModePanel(); + wsCloseProjectPanel(); panel.style.display = 'flex'; } function wsCloseRolePanel() { @@ -340,6 +344,7 @@ function wsToggleAgentModePanel() { var isOpen = panel.style.display === 'flex'; if (isOpen) { wsCloseAgentModePanel(); return; } wsCloseRolePanel(); + wsCloseProjectPanel(); panel.style.display = 'flex'; } function wsCloseAgentModePanel() { @@ -347,10 +352,204 @@ function wsCloseAgentModePanel() { if (panel) panel.style.display = 'none'; } +// ─── WebShell AI 项目选择器(与主「对话」页对齐) ─── + +function wsProjectT(key, fallback) { + if (typeof window.t === 'function') { + var v = window.t(key); + if (v && v !== key) return v; + } + return fallback; +} + +function getWebshellAiConvId(conn) { + if (!conn || !conn.id) return ''; + return webshellAiConvMap[conn.id] || ''; +} + +function getWebshellAiProjectSelection(conn) { + if (!conn || !conn.id) return ''; + var convId = getWebshellAiConvId(conn); + if (convId) return webshellAiProjectByConvId[convId] || ''; + return webshellAiDraftProjectByConn[conn.id] || ''; +} + +function wsSetWebshellAiProject(conn, projectId) { + if (!conn || !conn.id) return; + var pid = projectId || ''; + var convId = getWebshellAiConvId(conn); + if (convId) { + if (pid) webshellAiProjectByConvId[convId] = pid; + else delete webshellAiProjectByConvId[convId]; + } else if (pid) { + webshellAiDraftProjectByConn[conn.id] = pid; + } else { + delete webshellAiDraftProjectByConn[conn.id]; + } + wsUpdateProjectButtonLabel(); +} + +function wsIsActiveProjectId(id) { + if (!id) return false; + var map = window.projectNameById || {}; + return !!map[id]; +} + +function wsResolveWebshellAiProjectSelection(conn) { + var raw = getWebshellAiProjectSelection(conn); + if (!raw) return ''; + return wsIsActiveProjectId(raw) ? raw : ''; +} + +function wsUpdateProjectButtonLabel() { + var textEl = document.getElementById('ws-project-text'); + if (!textEl || !webshellCurrentConn) return; + var id = wsResolveWebshellAiProjectSelection(webshellCurrentConn); + var nameMap = window.projectNameById || {}; + textEl.textContent = id && nameMap[id] ? nameMap[id] : wsProjectT('projects.noProject', '无项目'); +} + +async function wsRenderProjectPanelList() { + var list = document.getElementById('ws-project-list'); + if (!list || !webshellCurrentConn) return; + var conn = webshellCurrentConn; + var selected = wsResolveWebshellAiProjectSelection(conn); + var projects = []; + try { + if (typeof window.fetchAllProjects === 'function') { + projects = await window.fetchAllProjects(false); + } + } catch (e) { + list.innerHTML = '
' + escapeHtml(wsProjectT('projects.loadFailedRetry', '加载失败,请重试')) + '
'; + return; + } + if (typeof window.rebuildProjectNameMap === 'function') { + window.rebuildProjectNameMap(projects); + } + var activeProjects = projects.filter(function (p) { return p.status !== 'archived'; }); + var items = [{ id: '', name: wsProjectT('projects.noProject', '无项目'), description: wsProjectT('projects.noProjectDescription', '不绑定项目') }].concat(activeProjects); + list.innerHTML = ''; + items.forEach(function (p) { + var isNone = !p.id; + var isSelected = isNone ? !selected : selected === p.id; + var desc = isNone + ? (p.description || '') + : ((p.description || '').trim().slice(0, 80) || wsProjectT('projects.sharedFactBoard', '共享事实黑板')); + var btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'role-selection-item-main' + (isSelected ? ' selected' : ''); + btn.setAttribute('role', 'option'); + btn.onclick = function () { wsSelectProject(p.id || ''); }; + btn.innerHTML = '
' + (isNone ? '—' : '📁') + '
' + + '
' + + '
' + escapeHtml(p.name || '未命名') + '
' + + '
' + escapeHtml(desc) + '
' + + (isSelected ? '
' : ''); + list.appendChild(btn); + }); +} + +async function wsRenderProjectPanel() { + var list = document.getElementById('ws-project-list'); + if (!list) return; + list.innerHTML = '
' + escapeHtml(wsProjectT('common.loading', '加载中...')) + '
'; + await wsRenderProjectPanelList(); +} + +function wsCloseProjectPanel() { + var panel = document.getElementById('ws-project-panel'); + var btn = document.getElementById('ws-project-btn'); + if (panel) panel.style.display = 'none'; + if (btn) { + btn.classList.remove('active'); + btn.setAttribute('aria-expanded', 'false'); + } +} + +async function wsToggleProjectPanel() { + var panel = document.getElementById('ws-project-panel'); + var btn = document.getElementById('ws-project-btn'); + if (!panel) return; + var isHidden = panel.style.display === 'none' || !panel.style.display; + if (!isHidden) { + wsCloseProjectPanel(); + return; + } + wsCloseRolePanel(); + wsCloseAgentModePanel(); + panel.style.display = 'flex'; + if (btn) { + btn.classList.add('active'); + btn.setAttribute('aria-expanded', 'true'); + } + await wsRenderProjectPanel(); +} + +async function wsSelectProject(projectId) { + wsCloseProjectPanel(); + await applyWebshellAiProjectSelection(projectId || ''); +} + +async function applyWebshellAiProjectSelection(projectId) { + var conn = webshellCurrentConn; + if (!conn || !conn.id) return; + var prev = getWebshellAiProjectSelection(conn); + if (projectId === prev) { + wsUpdateProjectButtonLabel(); + return; + } + var convId = getWebshellAiConvId(conn); + if (convId) { + try { + var res = await apiFetch('/api/conversations/' + encodeURIComponent(convId) + '/project', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ projectId: projectId }), + }); + if (!res.ok) { + var err = await res.json().catch(function () { return {}; }); + throw new Error(err.error || res.statusText); + } + wsSetWebshellAiProject(conn, projectId); + if (typeof showNotification === 'function') { + showNotification( + projectId ? wsProjectT('projects.projectBound', '已绑定项目') : wsProjectT('projects.projectUnbound', '已解除项目绑定'), + 'success' + ); + } + } catch (e) { + console.error(e); + alert(wsProjectT('projects.updateProjectBindingFailed', '更新项目绑定失败') + ': ' + (e.message || e)); + wsUpdateProjectButtonLabel(); + return; + } + } else { + wsSetWebshellAiProject(conn, projectId); + } + wsUpdateProjectButtonLabel(); +} + +function showNewProjectModalFromWebshellAi() { + wsCloseProjectPanel(); + if (webshellCurrentConn && webshellCurrentConn.id) { + window._projectModalFromWebshellConnId = webshellCurrentConn.id; + } + window._projectModalFromChat = false; + if (typeof showNewProjectModal === 'function') showNewProjectModal(); +} + +window.applyWebshellAiProjectSelection = applyWebshellAiProjectSelection; +window.showNewProjectModalFromWebshellAi = showNewProjectModalFromWebshellAi; +window.wsToggleProjectPanel = wsToggleProjectPanel; +window.wsCloseProjectPanel = wsCloseProjectPanel; + +// ─── end WebShell AI 项目选择器 ─── + /** 当 WebShell AI Tab 可见时刷新选择器显示(同步主页可能的更改) */ function wsRefreshSelectors() { wsUpdateRoleSelectorDisplay(); wsRenderRoleList(); + wsUpdateProjectButtonLabel(); var stored = localStorage.getItem('cyberstrike-chat-agent-mode') || 'eino_single'; if (stored !== 'eino_single' && stored !== 'deep' && stored !== 'plan_execute' && stored !== 'supervisor') { stored = 'eino_single'; @@ -370,6 +569,11 @@ document.addEventListener('click', function (e) { if (modePanel && modePanel.style.display !== 'none' && modeBtn && !modePanel.contains(e.target) && !modeBtn.contains(e.target)) { wsCloseAgentModePanel(); } + var projectPanel = document.getElementById('ws-project-panel'); + var projectBtn = document.getElementById('ws-project-btn'); + if (projectPanel && projectPanel.style.display !== 'none' && projectBtn && !projectPanel.contains(e.target) && !projectBtn.contains(e.target)) { + wsCloseProjectPanel(); + } }); // ─── end WebShell AI 选择器 ─── @@ -1873,6 +2077,7 @@ function webshellAiConvListSelect(conn, convId, messagesContainer, listEl) { apiFetch('/api/conversations/' + encodeURIComponent(convId) + '?include_process_details=1', { method: 'GET' }) .then(function (r) { return r.json(); }) .then(function (data) { + wsSetWebshellAiProject(conn, data.projectId || data.project_id || ''); messagesContainer.innerHTML = ''; var list = data.messages || []; list.forEach(function (msg) { @@ -2003,6 +2208,25 @@ function selectWebshell(id, stateReady) { '
' + '
' + '
' + + '
' + + '' + + '
' + '
' + '