const AUTH_STORAGE_KEY = 'cyberstrike-auth'; let authToken = null; let authTokenExpiry = null; let authPromise = null; let authPromiseResolvers = []; let isAppInitialized = false; // 当前对话ID let currentConversationId = null; // 进度ID与任务信息映射 const progressTaskState = new Map(); // 活跃任务刷新定时器 let activeTaskInterval = null; const ACTIVE_TASK_REFRESH_INTERVAL = 10000; // 10秒检查一次,提供更实时的任务状态反馈 function isTokenValid() { return !!authToken && authTokenExpiry instanceof Date && authTokenExpiry.getTime() > Date.now(); } function saveAuth(token, expiresAt) { const expiry = expiresAt instanceof Date ? expiresAt : new Date(expiresAt); authToken = token; authTokenExpiry = expiry; try { localStorage.setItem(AUTH_STORAGE_KEY, JSON.stringify({ token, expiresAt: expiry.toISOString(), })); } catch (error) { console.warn('无法持久化认证信息:', error); } } function clearAuthStorage() { authToken = null; authTokenExpiry = null; try { localStorage.removeItem(AUTH_STORAGE_KEY); } catch (error) { console.warn('无法清除认证信息:', error); } } function loadAuthFromStorage() { try { const raw = localStorage.getItem(AUTH_STORAGE_KEY); if (!raw) { return false; } const stored = JSON.parse(raw); if (!stored.token || !stored.expiresAt) { clearAuthStorage(); return false; } const expiry = new Date(stored.expiresAt); if (Number.isNaN(expiry.getTime())) { clearAuthStorage(); return false; } authToken = stored.token; authTokenExpiry = expiry; return isTokenValid(); } catch (error) { console.error('读取认证信息失败:', error); clearAuthStorage(); return false; } } function resolveAuthPromises(success) { authPromiseResolvers.forEach(resolve => resolve(success)); authPromiseResolvers = []; authPromise = null; } function showLoginOverlay(message = '') { const overlay = document.getElementById('login-overlay'); const errorBox = document.getElementById('login-error'); const passwordInput = document.getElementById('login-password'); if (!overlay) { return; } overlay.style.display = 'flex'; if (errorBox) { if (message) { errorBox.textContent = message; errorBox.style.display = 'block'; } else { errorBox.textContent = ''; errorBox.style.display = 'none'; } } setTimeout(() => { if (passwordInput) { passwordInput.focus(); } }, 100); } function hideLoginOverlay() { const overlay = document.getElementById('login-overlay'); const errorBox = document.getElementById('login-error'); const passwordInput = document.getElementById('login-password'); if (overlay) { overlay.style.display = 'none'; } if (errorBox) { errorBox.textContent = ''; errorBox.style.display = 'none'; } if (passwordInput) { passwordInput.value = ''; } } function ensureAuthPromise() { if (!authPromise) { authPromise = new Promise(resolve => { authPromiseResolvers.push(resolve); }); } return authPromise; } async function ensureAuthenticated() { if (isTokenValid()) { return true; } showLoginOverlay(); await ensureAuthPromise(); return true; } function handleUnauthorized({ message = '认证已过期,请重新登录', silent = false } = {}) { clearAuthStorage(); authPromise = null; authPromiseResolvers = []; if (!silent) { showLoginOverlay(message); } else { showLoginOverlay(); } return false; } async function apiFetch(url, options = {}) { await ensureAuthenticated(); const opts = { ...options }; const headers = new Headers(options && options.headers ? options.headers : undefined); if (authToken && !headers.has('Authorization')) { headers.set('Authorization', `Bearer ${authToken}`); } opts.headers = headers; const response = await fetch(url, opts); if (response.status === 401) { handleUnauthorized(); throw new Error('未授权访问'); } return response; } async function submitLogin(event) { event.preventDefault(); const passwordInput = document.getElementById('login-password'); const errorBox = document.getElementById('login-error'); const submitBtn = document.querySelector('.login-submit'); if (!passwordInput) { return; } const password = passwordInput.value.trim(); if (!password) { if (errorBox) { errorBox.textContent = '请输入密码'; errorBox.style.display = 'block'; } return; } if (submitBtn) { submitBtn.disabled = true; } try { const response = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ password }), }); const result = await response.json().catch(() => ({})); if (!response.ok || !result.token) { if (errorBox) { errorBox.textContent = result.error || '登录失败,请检查密码'; errorBox.style.display = 'block'; } return; } saveAuth(result.token, result.expires_at); hideLoginOverlay(); resolveAuthPromises(true); if (!isAppInitialized) { await bootstrapApp(); } else { await refreshAppData(); } } catch (error) { console.error('登录失败:', error); if (errorBox) { errorBox.textContent = '登录失败,请稍后重试'; errorBox.style.display = 'block'; } } finally { if (submitBtn) { submitBtn.disabled = false; } } } async function refreshAppData(showTaskErrors = false) { await Promise.allSettled([ loadConversations(), loadActiveTasks(showTaskErrors), ]); } async function bootstrapApp() { if (!isAppInitialized) { initializeChatUI(); isAppInitialized = true; } await refreshAppData(); } function initializeChatUI() { const chatInputEl = document.getElementById('chat-input'); if (chatInputEl) { chatInputEl.style.height = '44px'; } const messagesDiv = document.getElementById('chat-messages'); if (messagesDiv && messagesDiv.childElementCount === 0) { addMessage('assistant', '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。'); } loadActiveTasks(true); if (activeTaskInterval) { clearInterval(activeTaskInterval); } activeTaskInterval = setInterval(() => loadActiveTasks(), ACTIVE_TASK_REFRESH_INTERVAL); } function setupLoginUI() { const loginForm = document.getElementById('login-form'); if (loginForm) { loginForm.addEventListener('submit', submitLogin); } } async function initializeApp() { setupLoginUI(); const hasStoredAuth = loadAuthFromStorage(); if (hasStoredAuth && isTokenValid()) { try { const response = await apiFetch('/api/auth/validate', { method: 'GET', }); if (response.ok) { hideLoginOverlay(); resolveAuthPromises(true); await bootstrapApp(); return; } } catch (error) { console.warn('本地会话已失效,需重新登录'); } } clearAuthStorage(); showLoginOverlay(); } document.addEventListener('DOMContentLoaded', initializeApp); function registerProgressTask(progressId, conversationId = null) { const state = progressTaskState.get(progressId) || {}; state.conversationId = conversationId !== undefined && conversationId !== null ? conversationId : (state.conversationId ?? currentConversationId); state.cancelling = false; progressTaskState.set(progressId, state); const progressElement = document.getElementById(progressId); if (progressElement) { progressElement.dataset.conversationId = state.conversationId || ''; } } function updateProgressConversation(progressId, conversationId) { if (!conversationId) { return; } registerProgressTask(progressId, conversationId); } function markProgressCancelling(progressId) { const state = progressTaskState.get(progressId); if (state) { state.cancelling = true; } } function finalizeProgressTask(progressId, finalLabel = '已完成') { const stopBtn = document.getElementById(`${progressId}-stop-btn`); if (stopBtn) { stopBtn.disabled = true; stopBtn.textContent = finalLabel; } progressTaskState.delete(progressId); } async function requestCancel(conversationId) { const response = await apiFetch('/api/agent-loop/cancel', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ conversationId }), }); const result = await response.json().catch(() => ({})); if (!response.ok) { throw new Error(result.error || '取消失败'); } return result; } // 发送消息 async function sendMessage() { const input = document.getElementById('chat-input'); const message = input.value.trim(); if (!message) { return; } // 显示用户消息 addMessage('user', message); input.value = ''; // 创建进度消息容器(使用详细的进度展示) const progressId = addProgressMessage(); const progressElement = document.getElementById(progressId); registerProgressTask(progressId, currentConversationId); loadActiveTasks(); let assistantMessageId = null; let mcpExecutionIds = []; try { const response = await apiFetch('/api/agent-loop/stream', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ message: message, conversationId: currentConversationId }), }); if (!response.ok) { throw new Error('请求失败: ' + response.status); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop(); // 保留最后一个不完整的行 for (const line of lines) { if (line.startsWith('data: ')) { try { const eventData = JSON.parse(line.slice(6)); handleStreamEvent(eventData, progressElement, progressId, () => assistantMessageId, (id) => { assistantMessageId = id; }, () => mcpExecutionIds, (ids) => { mcpExecutionIds = ids; }); } catch (e) { console.error('解析事件数据失败:', e, line); } } } } // 处理剩余的buffer if (buffer.trim()) { const lines = buffer.split('\n'); for (const line of lines) { if (line.startsWith('data: ')) { try { const eventData = JSON.parse(line.slice(6)); handleStreamEvent(eventData, progressElement, progressId, () => assistantMessageId, (id) => { assistantMessageId = id; }, () => mcpExecutionIds, (ids) => { mcpExecutionIds = ids; }); } catch (e) { console.error('解析事件数据失败:', e, line); } } } } } catch (error) { removeMessage(progressId); addMessage('system', '错误: ' + error.message); } } // 创建进度消息容器 function addProgressMessage() { const messagesDiv = document.getElementById('chat-messages'); const messageDiv = document.createElement('div'); messageCounter++; const id = 'progress-' + Date.now() + '-' + messageCounter; messageDiv.id = id; messageDiv.className = 'message system progress-message'; const contentWrapper = document.createElement('div'); contentWrapper.className = 'message-content'; const bubble = document.createElement('div'); bubble.className = 'message-bubble progress-container'; bubble.innerHTML = `
🔍 渗透测试进行中...
`; contentWrapper.appendChild(bubble); messageDiv.appendChild(contentWrapper); messageDiv.dataset.conversationId = currentConversationId || ''; messagesDiv.appendChild(messageDiv); messagesDiv.scrollTop = messagesDiv.scrollHeight; return id; } // 切换进度详情显示 function toggleProgressDetails(progressId) { const timeline = document.getElementById(progressId + '-timeline'); const toggleBtn = document.querySelector(`#${progressId} .progress-toggle`); if (!timeline || !toggleBtn) return; if (timeline.classList.contains('expanded')) { timeline.classList.remove('expanded'); toggleBtn.textContent = '展开详情'; } else { timeline.classList.add('expanded'); toggleBtn.textContent = '收起详情'; } } // 折叠所有进度详情 function collapseAllProgressDetails(assistantMessageId, progressId) { // 折叠集成到MCP区域的详情 const detailsId = 'process-details-' + assistantMessageId; const detailsContainer = document.getElementById(detailsId); if (detailsContainer) { const timeline = detailsContainer.querySelector('.progress-timeline'); if (timeline && timeline.classList.contains('expanded')) { timeline.classList.remove('expanded'); const btn = document.querySelector(`#${assistantMessageId} .process-detail-btn`); if (btn) { btn.innerHTML = '展开详情'; } } } // 折叠独立的详情组件(通过convertProgressToDetails创建的) // 查找所有以details-开头的详情组件 const allDetails = document.querySelectorAll('[id^="details-"]'); allDetails.forEach(detail => { const timeline = detail.querySelector('.progress-timeline'); const toggleBtn = detail.querySelector('.progress-toggle'); if (timeline && timeline.classList.contains('expanded')) { timeline.classList.remove('expanded'); if (toggleBtn) { toggleBtn.textContent = '展开详情'; } } }); // 折叠原始的进度消息(如果还存在) if (progressId) { const progressTimeline = document.getElementById(progressId + '-timeline'); const progressToggleBtn = document.querySelector(`#${progressId} .progress-toggle`); if (progressTimeline && progressTimeline.classList.contains('expanded')) { progressTimeline.classList.remove('expanded'); if (progressToggleBtn) { progressToggleBtn.textContent = '展开详情'; } } } } // 获取当前助手消息ID(用于done事件) function getAssistantId() { // 从最近的助手消息中获取ID const messages = document.querySelectorAll('.message.assistant'); if (messages.length > 0) { return messages[messages.length - 1].id; } return null; } // 将进度详情集成到工具调用区域 function integrateProgressToMCPSection(progressId, assistantMessageId) { const progressElement = document.getElementById(progressId); if (!progressElement) return; // 获取时间线内容 const timeline = document.getElementById(progressId + '-timeline'); let timelineHTML = ''; if (timeline) { timelineHTML = timeline.innerHTML; } // 获取助手消息元素 const assistantElement = document.getElementById(assistantMessageId); if (!assistantElement) { removeMessage(progressId); return; } // 查找MCP调用区域 const mcpSection = assistantElement.querySelector('.mcp-call-section'); if (!mcpSection) { // 如果没有MCP区域,创建详情组件放在消息下方 convertProgressToDetails(progressId, assistantMessageId); return; } // 获取时间线内容 const hasContent = timelineHTML.trim().length > 0; // 确保按钮容器存在 let buttonsContainer = mcpSection.querySelector('.mcp-call-buttons'); if (!buttonsContainer) { buttonsContainer = document.createElement('div'); buttonsContainer.className = 'mcp-call-buttons'; mcpSection.appendChild(buttonsContainer); } // 创建详情容器,放在MCP按钮区域下方(统一结构) const detailsId = 'process-details-' + assistantMessageId; let detailsContainer = document.getElementById(detailsId); if (!detailsContainer) { detailsContainer = document.createElement('div'); detailsContainer.id = detailsId; detailsContainer.className = 'process-details-container'; // 确保容器在按钮容器之后 if (buttonsContainer.nextSibling) { mcpSection.insertBefore(detailsContainer, buttonsContainer.nextSibling); } else { mcpSection.appendChild(detailsContainer); } } // 设置详情内容(默认折叠状态) detailsContainer.innerHTML = `
${hasContent ? `
${timelineHTML}
` : '
暂无过程详情
'}
`; // 确保初始状态是折叠的 if (hasContent) { const timeline = document.getElementById(detailsId + '-timeline'); if (timeline) { timeline.classList.remove('expanded'); } } // 移除原来的进度消息 removeMessage(progressId); } // 切换过程详情显示 function toggleProcessDetails(progressId, assistantMessageId) { const detailsId = 'process-details-' + assistantMessageId; const detailsContainer = document.getElementById(detailsId); if (!detailsContainer) return; const content = detailsContainer.querySelector('.process-details-content'); const timeline = detailsContainer.querySelector('.progress-timeline'); const btn = document.querySelector(`#${assistantMessageId} .process-detail-btn`); if (content && timeline) { if (timeline.classList.contains('expanded')) { timeline.classList.remove('expanded'); if (btn) btn.innerHTML = '展开详情'; } else { timeline.classList.add('expanded'); if (btn) btn.innerHTML = '收起详情'; } } else if (timeline) { // 如果只有timeline,直接切换 if (timeline.classList.contains('expanded')) { timeline.classList.remove('expanded'); if (btn) btn.innerHTML = '展开详情'; } else { timeline.classList.add('expanded'); if (btn) btn.innerHTML = '收起详情'; } } // 滚动到底部以便查看展开的内容 if (timeline && timeline.classList.contains('expanded')) { setTimeout(() => { const messagesDiv = document.getElementById('chat-messages'); messagesDiv.scrollTop = messagesDiv.scrollHeight; }, 100); } } // 停止当前进度对应的任务 async function cancelProgressTask(progressId) { const state = progressTaskState.get(progressId); const stopBtn = document.getElementById(`${progressId}-stop-btn`); if (!state || !state.conversationId) { if (stopBtn) { stopBtn.disabled = true; setTimeout(() => { stopBtn.disabled = false; }, 1500); } alert('任务信息尚未同步,请稍后再试。'); return; } if (state.cancelling) { return; } markProgressCancelling(progressId); if (stopBtn) { stopBtn.disabled = true; stopBtn.textContent = '取消中...'; } try { await requestCancel(state.conversationId); loadActiveTasks(); } catch (error) { console.error('取消任务失败:', error); alert('取消任务失败: ' + error.message); if (stopBtn) { stopBtn.disabled = false; stopBtn.textContent = '停止任务'; } const currentState = progressTaskState.get(progressId); if (currentState) { currentState.cancelling = false; } } } // 将进度消息转换为可折叠的详情组件 function convertProgressToDetails(progressId, assistantMessageId) { const progressElement = document.getElementById(progressId); if (!progressElement) return; // 获取时间线内容 const timeline = document.getElementById(progressId + '-timeline'); // 即使时间线不存在,也创建详情组件(显示空状态) let timelineHTML = ''; if (timeline) { timelineHTML = timeline.innerHTML; } // 获取助手消息元素 const assistantElement = document.getElementById(assistantMessageId); if (!assistantElement) { removeMessage(progressId); return; } // 创建详情组件 const detailsId = 'details-' + Date.now() + '-' + messageCounter++; const detailsDiv = document.createElement('div'); detailsDiv.id = detailsId; detailsDiv.className = 'message system progress-details'; const contentWrapper = document.createElement('div'); contentWrapper.className = 'message-content'; const bubble = document.createElement('div'); bubble.className = 'message-bubble progress-container completed'; // 获取时间线HTML内容 const hasContent = timelineHTML.trim().length > 0; // 总是显示详情组件,即使没有内容也显示 bubble.innerHTML = `
📋 渗透测试详情 ${hasContent ? `` : ''}
${hasContent ? `
${timelineHTML}
` : '
暂无过程详情(可能执行过快或未触发详细事件)
'} `; contentWrapper.appendChild(bubble); detailsDiv.appendChild(contentWrapper); // 将详情组件插入到助手消息之后 const messagesDiv = document.getElementById('chat-messages'); // assistantElement 是消息div,需要插入到它的下一个兄弟节点之前 if (assistantElement.nextSibling) { messagesDiv.insertBefore(detailsDiv, assistantElement.nextSibling); } else { // 如果没有下一个兄弟节点,直接追加 messagesDiv.appendChild(detailsDiv); } // 移除原来的进度消息 removeMessage(progressId); // 滚动到底部 messagesDiv.scrollTop = messagesDiv.scrollHeight; } // 处理流式事件 function handleStreamEvent(event, progressElement, progressId, getAssistantId, setAssistantId, getMcpIds, setMcpIds) { const timeline = document.getElementById(progressId + '-timeline'); if (!timeline) return; switch (event.type) { case 'conversation': if (event.data && event.data.conversationId) { updateProgressConversation(progressId, event.data.conversationId); currentConversationId = event.data.conversationId; updateActiveConversation(); loadActiveTasks(); // 立即刷新对话列表,让新对话显示在历史记录中 loadConversations(); } break; case 'iteration': // 添加迭代标记 addTimelineItem(timeline, 'iteration', { title: `第 ${event.data?.iteration || 1} 轮迭代`, message: event.message, data: event.data }); break; case 'thinking': // 显示AI思考内容 addTimelineItem(timeline, 'thinking', { title: '🤔 AI思考', message: event.message, data: event.data }); break; case 'tool_calls_detected': // 工具调用检测 addTimelineItem(timeline, 'tool_calls_detected', { title: `🔧 检测到 ${event.data?.count || 0} 个工具调用`, message: event.message, data: event.data }); break; case 'tool_call': // 显示工具调用信息 const toolInfo = event.data || {}; const toolName = toolInfo.toolName || '未知工具'; const index = toolInfo.index || 0; const total = toolInfo.total || 0; addTimelineItem(timeline, 'tool_call', { title: `🔧 调用工具: ${escapeHtml(toolName)} (${index}/${total})`, message: event.message, data: toolInfo, expanded: false }); break; case 'tool_result': // 显示工具执行结果 const resultInfo = event.data || {}; const resultToolName = resultInfo.toolName || '未知工具'; const success = resultInfo.success !== false; const statusIcon = success ? '✅' : '❌'; addTimelineItem(timeline, 'tool_result', { title: `${statusIcon} 工具 ${escapeHtml(resultToolName)} 执行${success ? '完成' : '失败'}`, message: event.message, data: resultInfo, expanded: false }); break; case 'progress': // 更新进度状态 const progressTitle = document.querySelector(`#${progressId} .progress-title`); if (progressTitle) { progressTitle.textContent = '🔍 ' + event.message; } break; case 'cancelled': addTimelineItem(timeline, 'cancelled', { title: '⛔ 任务已取消', message: event.message, data: event.data }); const cancelTitle = document.querySelector(`#${progressId} .progress-title`); if (cancelTitle) { cancelTitle.textContent = '⛔ 任务已取消'; } finalizeProgressTask(progressId, '已取消'); loadActiveTasks(); break; case 'response': // 先添加助手回复 const responseData = event.data || {}; const mcpIds = responseData.mcpExecutionIds || []; setMcpIds(mcpIds); // 更新对话ID if (responseData.conversationId) { currentConversationId = responseData.conversationId; updateActiveConversation(); updateProgressConversation(progressId, responseData.conversationId); loadActiveTasks(); } // 添加助手回复,并传入进度ID以便集成详情 const assistantId = addMessage('assistant', event.message, mcpIds, progressId); setAssistantId(assistantId); // 将进度详情集成到工具调用区域 integrateProgressToMCPSection(progressId, assistantId); // 延迟自动折叠详情(3秒后) setTimeout(() => { collapseAllProgressDetails(assistantId, progressId); }, 3000); // 刷新对话列表 loadConversations(); break; case 'error': // 显示错误 addTimelineItem(timeline, 'error', { title: '❌ 错误', message: event.message, data: event.data }); break; case 'done': // 完成,更新进度标题(如果进度消息还存在) const doneTitle = document.querySelector(`#${progressId} .progress-title`); if (doneTitle) { doneTitle.textContent = '✅ 渗透测试完成'; } // 更新对话ID if (event.data && event.data.conversationId) { currentConversationId = event.data.conversationId; updateActiveConversation(); updateProgressConversation(progressId, event.data.conversationId); } if (progressTaskState.has(progressId)) { finalizeProgressTask(progressId, '已完成'); } loadActiveTasks(); // 完成时自动折叠所有详情(延迟一下确保response事件已处理) setTimeout(() => { const assistantIdFromDone = getAssistantId(); if (assistantIdFromDone) { collapseAllProgressDetails(assistantIdFromDone, progressId); } else { // 如果无法获取助手ID,尝试折叠所有详情 collapseAllProgressDetails(null, progressId); } }, 500); break; } // 自动滚动到底部 const messagesDiv = document.getElementById('chat-messages'); messagesDiv.scrollTop = messagesDiv.scrollHeight; } // 添加时间线项目 function addTimelineItem(timeline, type, options) { const item = document.createElement('div'); item.className = `timeline-item timeline-item-${type}`; const time = new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); let content = `
${time} ${options.title}
`; // 根据类型添加详细内容 if (type === 'thinking' && options.message) { content += `
${formatMarkdown(options.message)}
`; } else if (type === 'tool_call' && options.data) { const data = options.data; const args = data.argumentsObj || (data.arguments ? JSON.parse(data.arguments) : {}); content += `
参数:
${JSON.stringify(args, null, 2)}
`; } else if (type === 'tool_result' && options.data) { const data = options.data; const isError = data.isError || !data.success; const result = data.result || data.error || '无结果'; content += `
执行结果:
${escapeHtml(result)}
${data.executionId ? `
执行ID: ${data.executionId}
` : ''}
`; } else if (type === 'cancelled') { content += `
${escapeHtml(options.message || '任务已取消')}
`; } item.innerHTML = content; timeline.appendChild(item); // 自动展开详情 const expanded = timeline.classList.contains('expanded'); if (!expanded && (type === 'tool_call' || type === 'tool_result')) { // 对于工具调用和结果,默认显示摘要 } } // 消息计数器,确保ID唯一 let messageCounter = 0; // 添加消息 function addMessage(role, content, mcpExecutionIds = null, progressId = null) { const messagesDiv = document.getElementById('chat-messages'); const messageDiv = document.createElement('div'); messageCounter++; const id = 'msg-' + Date.now() + '-' + messageCounter + '-' + Math.random().toString(36).substr(2, 9); messageDiv.id = id; messageDiv.className = 'message ' + role; // 创建头像 const avatar = document.createElement('div'); avatar.className = 'message-avatar'; if (role === 'user') { avatar.textContent = 'U'; } else if (role === 'assistant') { avatar.textContent = 'A'; } else { avatar.textContent = 'S'; } messageDiv.appendChild(avatar); // 创建消息内容容器 const contentWrapper = document.createElement('div'); contentWrapper.className = 'message-content'; // 创建消息气泡 const bubble = document.createElement('div'); bubble.className = 'message-bubble'; // 解析 Markdown 格式 let formattedContent; if (typeof marked !== 'undefined') { // 使用 marked.js 解析 Markdown try { // 配置 marked 选项 marked.setOptions({ breaks: true, // 支持换行 gfm: true, // 支持 GitHub Flavored Markdown }); formattedContent = marked.parse(content); } catch (e) { console.error('Markdown 解析失败:', e); // 降级处理:转义 HTML 并保留换行 formattedContent = escapeHtml(content).replace(/\n/g, '
'); } } else { // 如果没有 marked.js,使用简单处理 formattedContent = escapeHtml(content).replace(/\n/g, '
'); } bubble.innerHTML = formattedContent; contentWrapper.appendChild(bubble); // 添加时间戳 const timeDiv = document.createElement('div'); timeDiv.className = 'message-time'; timeDiv.textContent = new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }); contentWrapper.appendChild(timeDiv); // 如果有MCP执行ID或进度ID,添加查看详情区域(统一使用"渗透测试详情"样式) if (role === 'assistant' && ((mcpExecutionIds && Array.isArray(mcpExecutionIds) && mcpExecutionIds.length > 0) || progressId)) { const mcpSection = document.createElement('div'); mcpSection.className = 'mcp-call-section'; const mcpLabel = document.createElement('div'); mcpLabel.className = 'mcp-call-label'; mcpLabel.textContent = '📋 渗透测试详情'; mcpSection.appendChild(mcpLabel); const buttonsContainer = document.createElement('div'); buttonsContainer.className = 'mcp-call-buttons'; // 如果有MCP执行ID,添加MCP调用详情按钮 if (mcpExecutionIds && Array.isArray(mcpExecutionIds) && mcpExecutionIds.length > 0) { mcpExecutionIds.forEach((execId, index) => { const detailBtn = document.createElement('button'); detailBtn.className = 'mcp-detail-btn'; detailBtn.innerHTML = `调用 #${index + 1}`; detailBtn.onclick = () => showMCPDetail(execId); buttonsContainer.appendChild(detailBtn); }); } // 如果有进度ID,添加展开详情按钮(统一使用"展开详情"文本) if (progressId) { const progressDetailBtn = document.createElement('button'); progressDetailBtn.className = 'mcp-detail-btn process-detail-btn'; progressDetailBtn.innerHTML = '展开详情'; progressDetailBtn.onclick = () => toggleProcessDetails(progressId, messageDiv.id); buttonsContainer.appendChild(progressDetailBtn); // 存储进度ID到消息元素 messageDiv.dataset.progressId = progressId; } mcpSection.appendChild(buttonsContainer); contentWrapper.appendChild(mcpSection); } messageDiv.appendChild(contentWrapper); messagesDiv.appendChild(messageDiv); messagesDiv.scrollTop = messagesDiv.scrollHeight; return id; } // 渲染过程详情 function renderProcessDetails(messageId, processDetails) { if (!processDetails || processDetails.length === 0) { return; } const messageElement = document.getElementById(messageId); if (!messageElement) { return; } // 查找或创建MCP调用区域 let mcpSection = messageElement.querySelector('.mcp-call-section'); if (!mcpSection) { mcpSection = document.createElement('div'); mcpSection.className = 'mcp-call-section'; const contentWrapper = messageElement.querySelector('.message-content'); if (contentWrapper) { contentWrapper.appendChild(mcpSection); } else { return; } } // 确保有标签和按钮容器(统一结构) let mcpLabel = mcpSection.querySelector('.mcp-call-label'); let buttonsContainer = mcpSection.querySelector('.mcp-call-buttons'); // 如果没有标签,创建一个(当没有工具调用时) if (!mcpLabel && !buttonsContainer) { mcpLabel = document.createElement('div'); mcpLabel.className = 'mcp-call-label'; mcpLabel.textContent = '📋 渗透测试详情'; mcpSection.appendChild(mcpLabel); } else if (mcpLabel && mcpLabel.textContent !== '📋 渗透测试详情') { // 如果标签存在但不是统一格式,更新它 mcpLabel.textContent = '📋 渗透测试详情'; } // 如果没有按钮容器,创建一个 if (!buttonsContainer) { buttonsContainer = document.createElement('div'); buttonsContainer.className = 'mcp-call-buttons'; mcpSection.appendChild(buttonsContainer); } // 添加过程详情按钮(如果还没有) let processDetailBtn = buttonsContainer.querySelector('.process-detail-btn'); if (!processDetailBtn) { processDetailBtn = document.createElement('button'); processDetailBtn.className = 'mcp-detail-btn process-detail-btn'; processDetailBtn.innerHTML = '展开详情'; processDetailBtn.onclick = () => toggleProcessDetails(null, messageId); buttonsContainer.appendChild(processDetailBtn); } // 创建过程详情容器(放在按钮容器之后) const detailsId = 'process-details-' + messageId; let detailsContainer = document.getElementById(detailsId); if (!detailsContainer) { detailsContainer = document.createElement('div'); detailsContainer.id = detailsId; detailsContainer.className = 'process-details-container'; // 确保容器在按钮容器之后 if (buttonsContainer.nextSibling) { mcpSection.insertBefore(detailsContainer, buttonsContainer.nextSibling); } else { mcpSection.appendChild(detailsContainer); } } // 创建时间线 const timelineId = detailsId + '-timeline'; let timeline = document.getElementById(timelineId); if (!timeline) { const contentDiv = document.createElement('div'); contentDiv.className = 'process-details-content'; timeline = document.createElement('div'); timeline.id = timelineId; timeline.className = 'progress-timeline'; contentDiv.appendChild(timeline); detailsContainer.appendChild(contentDiv); } // 清空时间线并重新渲染 timeline.innerHTML = ''; // 渲染每个过程详情事件 processDetails.forEach(detail => { const eventType = detail.eventType || ''; const title = detail.message || ''; const data = detail.data || {}; // 根据事件类型渲染不同的内容 let itemTitle = title; if (eventType === 'iteration') { itemTitle = `第 ${data.iteration || 1} 轮迭代`; } else if (eventType === 'thinking') { itemTitle = '🤔 AI思考'; } else if (eventType === 'tool_calls_detected') { itemTitle = `🔧 检测到 ${data.count || 0} 个工具调用`; } else if (eventType === 'tool_call') { const toolName = data.toolName || '未知工具'; const index = data.index || 0; const total = data.total || 0; itemTitle = `🔧 调用工具: ${escapeHtml(toolName)} (${index}/${total})`; } else if (eventType === 'tool_result') { const toolName = data.toolName || '未知工具'; const success = data.success !== false; const statusIcon = success ? '✅' : '❌'; itemTitle = `${statusIcon} 工具 ${escapeHtml(toolName)} 执行${success ? '完成' : '失败'}`; } else if (eventType === 'error') { itemTitle = '❌ 错误'; } addTimelineItem(timeline, eventType, { title: itemTitle, message: detail.message || '', data: data }); }); } // 移除消息 function removeMessage(id) { const messageDiv = document.getElementById(id); if (messageDiv) { messageDiv.remove(); } } // 回车发送消息,Shift+Enter 换行 const chatInput = document.getElementById('chat-input'); chatInput.addEventListener('keydown', function(e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } // Shift+Enter 允许默认行为(换行) }); // 显示MCP调用详情 async function showMCPDetail(executionId) { try { const response = await apiFetch(`/api/monitor/execution/${executionId}`); const exec = await response.json(); if (response.ok) { // 填充模态框内容 document.getElementById('detail-tool-name').textContent = exec.toolName || 'Unknown'; document.getElementById('detail-execution-id').textContent = exec.id || 'N/A'; document.getElementById('detail-status').textContent = getStatusText(exec.status); document.getElementById('detail-time').textContent = new Date(exec.startTime).toLocaleString('zh-CN'); // 请求参数 const requestData = { tool: exec.toolName, arguments: exec.arguments }; document.getElementById('detail-request').textContent = JSON.stringify(requestData, null, 2); // 响应结果 if (exec.result) { const responseData = { content: exec.result.content, isError: exec.result.isError }; document.getElementById('detail-response').textContent = JSON.stringify(responseData, null, 2); document.getElementById('detail-response').className = exec.result.isError ? 'code-block error' : 'code-block'; } else { document.getElementById('detail-response').textContent = '暂无响应数据'; } // 错误信息 if (exec.error) { document.getElementById('detail-error-section').style.display = 'block'; document.getElementById('detail-error').textContent = exec.error; } else { document.getElementById('detail-error-section').style.display = 'none'; } // 显示模态框 document.getElementById('mcp-detail-modal').style.display = 'block'; } else { alert('获取详情失败: ' + (exec.error || '未知错误')); } } catch (error) { alert('获取详情失败: ' + error.message); } } // 关闭MCP详情模态框 function closeMCPDetail() { document.getElementById('mcp-detail-modal').style.display = 'none'; } // 工具函数 function getStatusText(status) { const statusMap = { 'pending': '等待中', 'running': '执行中', 'completed': '已完成', 'failed': '失败' }; return statusMap[status] || status; } function formatDuration(ms) { const seconds = Math.floor(ms / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); if (hours > 0) { return `${hours}小时${minutes % 60}分钟`; } else if (minutes > 0) { return `${minutes}分钟${seconds % 60}秒`; } else { return `${seconds}秒`; } } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function formatMarkdown(text) { if (typeof marked !== 'undefined') { try { marked.setOptions({ breaks: true, gfm: true, }); return marked.parse(text); } catch (e) { console.error('Markdown 解析失败:', e); return escapeHtml(text).replace(/\n/g, '
'); } } else { return escapeHtml(text).replace(/\n/g, '
'); } } // 开始新对话 function startNewConversation() { currentConversationId = null; document.getElementById('chat-messages').innerHTML = ''; addMessage('assistant', '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。'); updateActiveConversation(); // 刷新对话列表,确保显示最新的历史对话 loadConversations(); } // 加载对话列表 async function loadConversations() { try { const response = await apiFetch('/api/conversations?limit=50'); const conversations = await response.json(); const listContainer = document.getElementById('conversations-list'); listContainer.innerHTML = ''; if (conversations.length === 0) { listContainer.innerHTML = '
暂无历史对话
'; return; } conversations.forEach(conv => { const item = document.createElement('div'); item.className = 'conversation-item'; item.dataset.conversationId = conv.id; if (conv.id === currentConversationId) { item.classList.add('active'); } // 创建内容容器 const contentWrapper = document.createElement('div'); contentWrapper.className = 'conversation-content'; const title = document.createElement('div'); title.className = 'conversation-title'; title.textContent = conv.title || '未命名对话'; contentWrapper.appendChild(title); const time = document.createElement('div'); time.className = 'conversation-time'; // 解析时间,支持多种格式 let dateObj; if (conv.updatedAt) { dateObj = new Date(conv.updatedAt); // 检查日期是否有效 if (isNaN(dateObj.getTime())) { // 如果解析失败,尝试其他格式 console.warn('时间解析失败:', conv.updatedAt); dateObj = new Date(); } } else { dateObj = new Date(); } // 格式化时间显示 const now = new Date(); const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); const messageDate = new Date(dateObj.getFullYear(), dateObj.getMonth(), dateObj.getDate()); let timeText; if (messageDate.getTime() === today.getTime()) { // 今天:只显示时间 timeText = dateObj.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }); } else if (messageDate.getTime() === yesterday.getTime()) { // 昨天 timeText = '昨天 ' + dateObj.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }); } else if (now.getFullYear() === dateObj.getFullYear()) { // 今年:显示月日和时间 timeText = dateObj.toLocaleString('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); } else { // 去年或更早:显示完整日期和时间 timeText = dateObj.toLocaleString('zh-CN', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); } time.textContent = timeText; contentWrapper.appendChild(time); item.appendChild(contentWrapper); // 创建删除按钮 const deleteBtn = document.createElement('button'); deleteBtn.className = 'conversation-delete-btn'; deleteBtn.innerHTML = ` `; deleteBtn.title = '删除对话'; deleteBtn.onclick = (e) => { e.stopPropagation(); // 阻止触发对话加载 deleteConversation(conv.id); }; item.appendChild(deleteBtn); item.onclick = () => loadConversation(conv.id); listContainer.appendChild(item); }); } catch (error) { console.error('加载对话列表失败:', error); } } // 加载对话 async function loadConversation(conversationId) { try { const response = await apiFetch(`/api/conversations/${conversationId}`); const conversation = await response.json(); if (!response.ok) { alert('加载对话失败: ' + (conversation.error || '未知错误')); return; } // 更新当前对话ID currentConversationId = conversationId; updateActiveConversation(); // 清空消息区域 const messagesDiv = document.getElementById('chat-messages'); messagesDiv.innerHTML = ''; // 加载消息 if (conversation.messages && conversation.messages.length > 0) { conversation.messages.forEach(msg => { const messageId = addMessage(msg.role, msg.content, msg.mcpExecutionIds || []); // 如果有过程详情,显示它们 if (msg.processDetails && msg.processDetails.length > 0 && msg.role === 'assistant') { // 延迟一下,确保消息已经渲染 setTimeout(() => { renderProcessDetails(messageId, msg.processDetails); }, 100); } }); } else { addMessage('assistant', '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。'); } // 滚动到底部 messagesDiv.scrollTop = messagesDiv.scrollHeight; // 刷新对话列表 loadConversations(); } catch (error) { console.error('加载对话失败:', error); alert('加载对话失败: ' + error.message); } } // 删除对话 async function deleteConversation(conversationId) { // 确认删除 if (!confirm('确定要删除这个对话吗?此操作不可恢复。')) { return; } try { const response = await apiFetch(`/api/conversations/${conversationId}`, { method: 'DELETE' }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || '删除失败'); } // 如果删除的是当前对话,清空对话界面 if (conversationId === currentConversationId) { currentConversationId = null; document.getElementById('chat-messages').innerHTML = ''; addMessage('assistant', '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。'); } // 刷新对话列表 loadConversations(); } catch (error) { console.error('删除对话失败:', error); alert('删除对话失败: ' + error.message); } } // 更新活动对话样式 function updateActiveConversation() { document.querySelectorAll('.conversation-item').forEach(item => { item.classList.remove('active'); if (currentConversationId && item.dataset.conversationId === currentConversationId) { item.classList.add('active'); } }); } // 加载活跃任务列表 async function loadActiveTasks(showErrors = false) { const bar = document.getElementById('active-tasks-bar'); try { const response = await apiFetch('/api/agent-loop/tasks'); const result = await response.json().catch(() => ({})); if (!response.ok) { throw new Error(result.error || '获取活跃任务失败'); } renderActiveTasks(result.tasks || []); } catch (error) { console.error('获取活跃任务失败:', error); if (showErrors && bar) { bar.style.display = 'block'; bar.innerHTML = `
无法获取任务状态:${escapeHtml(error.message)}
`; } } } function renderActiveTasks(tasks) { const bar = document.getElementById('active-tasks-bar'); if (!bar) return; if (!tasks || tasks.length === 0) { bar.style.display = 'none'; bar.innerHTML = ''; return; } bar.style.display = 'flex'; bar.innerHTML = ''; tasks.forEach(task => { const item = document.createElement('div'); item.className = 'active-task-item'; const startedTime = task.startedAt ? new Date(task.startedAt) : null; const timeText = startedTime && !isNaN(startedTime.getTime()) ? startedTime.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' }) : ''; item.innerHTML = `
${task.status === 'cancelling' ? '取消中' : '执行中'} ${escapeHtml(task.message || '未命名任务')}
${timeText ? `${timeText}` : ''}
`; const cancelBtn = item.querySelector('.active-task-cancel'); cancelBtn.onclick = () => cancelActiveTask(task.conversationId, cancelBtn); if (task.status === 'cancelling') { cancelBtn.disabled = true; cancelBtn.textContent = '取消中...'; } bar.appendChild(item); }); } async function cancelActiveTask(conversationId, button) { if (!conversationId) return; const originalText = button.textContent; button.disabled = true; button.textContent = '取消中...'; try { await requestCancel(conversationId); loadActiveTasks(); } catch (error) { console.error('取消任务失败:', error); alert('取消任务失败: ' + error.message); button.disabled = false; button.textContent = originalText; } } // 设置相关功能 let currentConfig = null; let allTools = []; // 打开设置 async function openSettings() { const modal = document.getElementById('settings-modal'); modal.style.display = 'block'; // 每次打开时重新加载最新配置 await loadConfig(); // 清除之前的验证错误状态 document.querySelectorAll('.form-group input').forEach(input => { input.classList.remove('error'); }); } // 关闭设置 function closeSettings() { const modal = document.getElementById('settings-modal'); modal.style.display = 'none'; } // 点击模态框外部关闭 window.onclick = function(event) { const settingsModal = document.getElementById('settings-modal'); const mcpModal = document.getElementById('mcp-detail-modal'); if (event.target == settingsModal) { closeSettings(); } if (event.target == mcpModal) { closeMCPDetail(); } } // 加载配置 async function loadConfig() { try { const response = await apiFetch('/api/config'); if (!response.ok) { throw new Error('获取配置失败'); } currentConfig = await response.json(); // 填充OpenAI配置 document.getElementById('openai-api-key').value = currentConfig.openai.api_key || ''; document.getElementById('openai-base-url').value = currentConfig.openai.base_url || ''; document.getElementById('openai-model').value = currentConfig.openai.model || ''; // 填充Agent配置 document.getElementById('agent-max-iterations').value = currentConfig.agent.max_iterations || 30; // 填充工具列表 allTools = currentConfig.tools || []; renderToolsList(); } catch (error) { console.error('加载配置失败:', error); alert('加载配置失败: ' + error.message); } } // 渲染工具列表 function renderToolsList() { const toolsList = document.getElementById('tools-list'); toolsList.innerHTML = ''; allTools.forEach(tool => { const toolItem = document.createElement('div'); toolItem.className = 'tool-item'; toolItem.dataset.toolName = tool.name; // 保存原始工具名称 toolItem.innerHTML = `
${escapeHtml(tool.name)}
${escapeHtml(tool.description || '无描述')}
`; toolsList.appendChild(toolItem); }); } // 全选工具 function selectAllTools() { document.querySelectorAll('#tools-list input[type="checkbox"]').forEach(checkbox => { checkbox.checked = true; }); } // 全不选工具 function deselectAllTools() { document.querySelectorAll('#tools-list input[type="checkbox"]').forEach(checkbox => { checkbox.checked = false; }); } // 过滤工具 function filterTools() { const searchTerm = document.getElementById('tools-search').value.toLowerCase(); document.querySelectorAll('.tool-item').forEach(item => { const toolName = (item.dataset.toolName || '').toLowerCase(); const toolDesc = item.querySelector('.tool-item-desc').textContent.toLowerCase(); if (toolName.includes(searchTerm) || toolDesc.includes(searchTerm)) { item.classList.remove('hidden'); } else { item.classList.add('hidden'); } }); } // 应用设置 async function applySettings() { try { // 清除之前的验证错误状态 document.querySelectorAll('.form-group input').forEach(input => { input.classList.remove('error'); }); // 验证必填字段 const apiKey = document.getElementById('openai-api-key').value.trim(); const baseUrl = document.getElementById('openai-base-url').value.trim(); const model = document.getElementById('openai-model').value.trim(); let hasError = false; if (!apiKey) { document.getElementById('openai-api-key').classList.add('error'); hasError = true; } if (!baseUrl) { document.getElementById('openai-base-url').classList.add('error'); hasError = true; } if (!model) { document.getElementById('openai-model').classList.add('error'); hasError = true; } if (hasError) { alert('请填写所有必填字段(标记为 * 的字段)'); return; } // 收集配置 const config = { openai: { api_key: apiKey, base_url: baseUrl, model: model }, agent: { max_iterations: parseInt(document.getElementById('agent-max-iterations').value) || 30 }, tools: [] }; // 收集工具启用状态 document.querySelectorAll('#tools-list .tool-item').forEach(item => { const checkbox = item.querySelector('input[type="checkbox"]'); const toolName = item.dataset.toolName; if (toolName) { // 直接使用工具名称 config.tools.push({ name: toolName, enabled: checkbox.checked }); } }); // 更新配置 const updateResponse = await apiFetch('/api/config', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(config) }); if (!updateResponse.ok) { const error = await updateResponse.json(); throw new Error(error.error || '更新配置失败'); } // 应用配置 const applyResponse = await apiFetch('/api/config/apply', { method: 'POST' }); if (!applyResponse.ok) { const error = await applyResponse.json(); throw new Error(error.error || '应用配置失败'); } alert('配置已成功应用!'); closeSettings(); } catch (error) { console.error('应用配置失败:', error); alert('应用配置失败: ' + error.message); } } function resetPasswordForm() { const currentInput = document.getElementById('auth-current-password'); const newInput = document.getElementById('auth-new-password'); const confirmInput = document.getElementById('auth-confirm-password'); [currentInput, newInput, confirmInput].forEach(input => { if (input) { input.value = ''; input.classList.remove('error'); } }); } async function changePassword() { const currentInput = document.getElementById('auth-current-password'); const newInput = document.getElementById('auth-new-password'); const confirmInput = document.getElementById('auth-confirm-password'); const submitBtn = document.querySelector('.change-password-submit'); [currentInput, newInput, confirmInput].forEach(input => input && input.classList.remove('error')); const currentPassword = currentInput?.value.trim() || ''; const newPassword = newInput?.value.trim() || ''; const confirmPassword = confirmInput?.value.trim() || ''; let hasError = false; if (!currentPassword) { currentInput?.classList.add('error'); hasError = true; } if (!newPassword || newPassword.length < 8) { newInput?.classList.add('error'); hasError = true; } if (newPassword !== confirmPassword) { confirmInput?.classList.add('error'); hasError = true; } if (hasError) { alert('请正确填写当前密码和新密码,新密码至少 8 位且需要两次输入一致。'); return; } if (submitBtn) { submitBtn.disabled = true; } try { const response = await apiFetch('/api/auth/change-password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ oldPassword: currentPassword, newPassword: newPassword }) }); const result = await response.json().catch(() => ({})); if (!response.ok) { throw new Error(result.error || '修改密码失败'); } alert('密码已更新,请使用新密码重新登录。'); resetPasswordForm(); handleUnauthorized({ message: '密码已更新,请使用新密码重新登录。', silent: false }); closeSettings(); } catch (error) { console.error('修改密码失败:', error); alert('修改密码失败: ' + error.message); } finally { if (submitBtn) { submitBtn.disabled = false; } } }