diff --git a/data/conversations.db b/data/conversations.db new file mode 100644 index 00000000..4ebf78cf Binary files /dev/null and b/data/conversations.db differ diff --git a/data/conversations.db-shm b/data/conversations.db-shm new file mode 100644 index 00000000..82b33425 Binary files /dev/null and b/data/conversations.db-shm differ diff --git a/data/conversations.db-wal b/data/conversations.db-wal new file mode 100644 index 00000000..87fdd572 Binary files /dev/null and b/data/conversations.db-wal differ diff --git a/internal/app/app.go b/internal/app/app.go index f16d553e..addef393 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -233,6 +233,7 @@ func setupRoutes( // 监控 protected.GET("/monitor", monitorHandler.Monitor) protected.GET("/monitor/execution/:id", monitorHandler.GetExecution) + protected.DELETE("/monitor/execution/:id", monitorHandler.DeleteExecution) protected.GET("/monitor/stats", monitorHandler.GetStats) // 配置管理 diff --git a/internal/database/monitor.go b/internal/database/monitor.go index 06807fb5..5d8c1cbc 100644 --- a/internal/database/monitor.go +++ b/internal/database/monitor.go @@ -232,6 +232,17 @@ func (db *DB) GetToolExecution(id string) (*mcp.ToolExecution, error) { return &exec, nil } +// DeleteToolExecution 删除工具执行记录 +func (db *DB) DeleteToolExecution(id string) error { + query := `DELETE FROM tool_executions WHERE id = ?` + _, err := db.Exec(query, id) + if err != nil { + db.logger.Error("删除工具执行记录失败", zap.Error(err), zap.String("executionId", id)) + return err + } + return nil +} + // SaveToolStats 保存工具统计信息 func (db *DB) SaveToolStats(toolName string, stats *mcp.ToolStats) error { var lastCallTime sql.NullTime @@ -332,3 +343,46 @@ func (db *DB) UpdateToolStats(toolName string, totalCalls, successCalls, failedC return nil } + +// DecreaseToolStats 减少工具统计信息(用于删除执行记录时) +// 如果统计信息变为0,则删除该统计记录 +func (db *DB) DecreaseToolStats(toolName string, totalCalls, successCalls, failedCalls int) error { + // 先更新统计信息 + query := ` + UPDATE tool_stats SET + total_calls = CASE WHEN total_calls - ? < 0 THEN 0 ELSE total_calls - ? END, + success_calls = CASE WHEN success_calls - ? < 0 THEN 0 ELSE success_calls - ? END, + failed_calls = CASE WHEN failed_calls - ? < 0 THEN 0 ELSE failed_calls - ? END, + updated_at = ? + WHERE tool_name = ? + ` + + _, err := db.Exec(query, totalCalls, totalCalls, successCalls, successCalls, failedCalls, failedCalls, time.Now(), toolName) + if err != nil { + db.logger.Error("减少工具统计信息失败", zap.Error(err), zap.String("toolName", toolName)) + return err + } + + // 检查更新后的 total_calls 是否为 0,如果是则删除该统计记录 + checkQuery := `SELECT total_calls FROM tool_stats WHERE tool_name = ?` + var newTotalCalls int + err = db.QueryRow(checkQuery, toolName).Scan(&newTotalCalls) + if err != nil { + // 如果查询失败(记录不存在),直接返回 + return nil + } + + // 如果 total_calls 为 0,删除该统计记录 + if newTotalCalls == 0 { + deleteQuery := `DELETE FROM tool_stats WHERE tool_name = ?` + _, err = db.Exec(deleteQuery, toolName) + if err != nil { + db.logger.Warn("删除零统计记录失败", zap.Error(err), zap.String("toolName", toolName)) + // 不返回错误,因为主要操作(更新统计)已成功 + } else { + db.logger.Info("已删除零统计记录", zap.String("toolName", toolName)) + } + } + + return nil +} diff --git a/internal/handler/monitor.go b/internal/handler/monitor.go index a7155c83..e901904f 100644 --- a/internal/handler/monitor.go +++ b/internal/handler/monitor.go @@ -220,4 +220,59 @@ func (h *MonitorHandler) GetStats(c *gin.Context) { c.JSON(http.StatusOK, stats) } +// DeleteExecution 删除执行记录 +func (h *MonitorHandler) DeleteExecution(c *gin.Context) { + id := c.Param("id") + if id == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "执行记录ID不能为空"}) + return + } + + // 如果使用数据库,先获取执行记录信息,然后删除并更新统计 + if h.db != nil { + // 先获取执行记录信息(用于更新统计) + exec, err := h.db.GetToolExecution(id) + if err != nil { + // 如果找不到记录,可能已经被删除,直接返回成功 + h.logger.Warn("执行记录不存在,可能已被删除", zap.String("executionId", id), zap.Error(err)) + c.JSON(http.StatusOK, gin.H{"message": "执行记录不存在或已被删除"}) + return + } + + // 删除执行记录 + err = h.db.DeleteToolExecution(id) + if err != nil { + h.logger.Error("删除执行记录失败", zap.Error(err), zap.String("executionId", id)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "删除执行记录失败: " + err.Error()}) + return + } + + // 更新统计信息(减少相应的计数) + totalCalls := 1 + successCalls := 0 + failedCalls := 0 + if exec.Status == "failed" { + failedCalls = 1 + } else if exec.Status == "completed" { + successCalls = 1 + } + + if exec.ToolName != "" { + if err := h.db.DecreaseToolStats(exec.ToolName, totalCalls, successCalls, failedCalls); err != nil { + h.logger.Warn("更新统计信息失败", zap.Error(err), zap.String("toolName", exec.ToolName)) + // 不返回错误,因为记录已经删除成功 + } + } + + h.logger.Info("执行记录已从数据库删除", zap.String("executionId", id), zap.String("toolName", exec.ToolName)) + c.JSON(http.StatusOK, gin.H{"message": "执行记录已删除"}) + return + } + + // 如果不使用数据库,尝试从内存中删除(内部MCP服务器) + // 注意:内存中的记录可能已经被清理,所以这里只记录日志 + h.logger.Info("尝试删除内存中的执行记录", zap.String("executionId", id)) + c.JSON(http.StatusOK, gin.H{"message": "执行记录已删除(如果存在)"}) +} + diff --git a/web/static/css/style.css b/web/static/css/style.css index 4335be03..bb845c16 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -1842,6 +1842,24 @@ header { border-color: var(--accent-color); } +.btn-danger { + padding: 10px 20px; + background: rgba(220, 53, 69, 0.08); + color: var(--error-color, #dc3545); + border: 1px solid rgba(220, 53, 69, 0.3); + border-radius: 6px; + font-size: 0.9375rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; +} + +.btn-danger:hover { + background: rgba(220, 53, 69, 0.15); + border-color: var(--error-color, #dc3545); + color: #c82333; +} + .monitor-modal-content { max-width: 1080px; width: 95%; @@ -2005,6 +2023,16 @@ header { font-size: 0.75rem; } +.monitor-execution-actions .btn-delete { + color: var(--error-color, #dc3545); +} + +.monitor-execution-actions .btn-delete:hover { + background: rgba(220, 53, 69, 0.1); + border-color: rgba(220, 53, 69, 0.4); + color: #c82333; +} + .monitor-vuln-container { display: grid; gap: 16px; diff --git a/web/static/js/app.js b/web/static/js/app.js index 542a20d7..43750f47 100644 --- a/web/static/js/app.js +++ b/web/static/js/app.js @@ -1688,6 +1688,9 @@ async function cancelActiveTask(conversationId, button) { // 设置相关功能 let currentConfig = null; let allTools = []; +// 全局工具状态映射,用于保存用户在所有页面的修改 +// key: tool.name, value: { enabled: boolean, is_external: boolean, external_mcp: string } +let toolStateMap = new Map(); // 从localStorage读取每页显示数量,默认为20 const getToolsPageSize = () => { const saved = localStorage.getItem('toolsPageSize'); @@ -1706,6 +1709,9 @@ async function openSettings() { const modal = document.getElementById('settings-modal'); modal.style.display = 'block'; + // 每次打开时清空全局状态映射,重新加载最新配置 + toolStateMap.clear(); + // 每次打开时重新加载最新配置 await loadConfig(); @@ -1775,6 +1781,9 @@ let toolsSearchKeyword = ''; // 加载工具列表(分页) async function loadToolsList(page = 1, searchKeyword = '') { try { + // 在加载新页面之前,先保存当前页的状态到全局映射 + saveCurrentPageToolStates(); + const pageSize = toolsPagination.pageSize; let url = `/api/config/tools?page=${page}&page_size=${pageSize}`; if (searchKeyword) { @@ -1795,6 +1804,17 @@ async function loadToolsList(page = 1, searchKeyword = '') { totalPages: result.total_pages || 1 }; + // 初始化工具状态映射(如果工具不在映射中,使用服务器返回的状态) + allTools.forEach(tool => { + if (!toolStateMap.has(tool.name)) { + toolStateMap.set(tool.name, { + enabled: tool.enabled, + is_external: tool.is_external || false, + external_mcp: tool.external_mcp || '' + }); + } + }); + renderToolsList(); renderToolsPagination(); } catch (error) { @@ -1806,6 +1826,23 @@ async function loadToolsList(page = 1, searchKeyword = '') { } } +// 保存当前页的工具状态到全局映射 +function saveCurrentPageToolStates() { + document.querySelectorAll('#tools-list .tool-item').forEach(item => { + const checkbox = item.querySelector('input[type="checkbox"]'); + const toolName = item.dataset.toolName; + const isExternal = item.dataset.isExternal === 'true'; + const externalMcp = item.dataset.externalMcp || ''; + if (toolName && checkbox) { + toolStateMap.set(toolName, { + enabled: checkbox.checked, + is_external: isExternal, + external_mcp: externalMcp + }); + } + }); +} + // 搜索工具 function searchTools() { const searchInput = document.getElementById('tools-search'); @@ -1859,11 +1896,18 @@ function renderToolsList() { toolItem.dataset.isExternal = tool.is_external ? 'true' : 'false'; toolItem.dataset.externalMcp = tool.external_mcp || ''; + // 从全局状态映射获取工具状态,如果不存在则使用服务器返回的状态 + const toolState = toolStateMap.get(tool.name) || { + enabled: tool.enabled, + is_external: tool.is_external || false, + external_mcp: tool.external_mcp || '' + }; + // 外部工具标签 - const externalBadge = tool.is_external ? '外部' : ''; + const externalBadge = toolState.is_external ? '外部' : ''; toolItem.innerHTML = ` - +
${escapeHtml(tool.name)} @@ -1932,10 +1976,40 @@ function renderToolsPagination() { toolsList.appendChild(pagination); } +// 处理工具checkbox状态变化 +function handleToolCheckboxChange(toolName, enabled) { + // 更新全局状态映射 + const toolItem = document.querySelector(`.tool-item[data-tool-name="${toolName}"]`); + if (toolItem) { + const isExternal = toolItem.dataset.isExternal === 'true'; + const externalMcp = toolItem.dataset.externalMcp || ''; + toolStateMap.set(toolName, { + enabled: enabled, + is_external: isExternal, + external_mcp: externalMcp + }); + } + updateToolsStats(); +} + // 全选工具 function selectAllTools() { document.querySelectorAll('#tools-list input[type="checkbox"]').forEach(checkbox => { checkbox.checked = true; + // 更新全局状态映射 + const toolItem = checkbox.closest('.tool-item'); + if (toolItem) { + const toolName = toolItem.dataset.toolName; + const isExternal = toolItem.dataset.isExternal === 'true'; + const externalMcp = toolItem.dataset.externalMcp || ''; + if (toolName) { + toolStateMap.set(toolName, { + enabled: true, + is_external: isExternal, + external_mcp: externalMcp + }); + } + } }); updateToolsStats(); } @@ -1944,6 +2018,20 @@ function selectAllTools() { function deselectAllTools() { document.querySelectorAll('#tools-list input[type="checkbox"]').forEach(checkbox => { checkbox.checked = false; + // 更新全局状态映射 + const toolItem = checkbox.closest('.tool-item'); + if (toolItem) { + const toolName = toolItem.dataset.toolName; + const isExternal = toolItem.dataset.isExternal === 'true'; + const externalMcp = toolItem.dataset.externalMcp || ''; + if (toolName) { + toolStateMap.set(toolName, { + enabled: false, + is_external: isExternal, + external_mcp: externalMcp + }); + } + } }); updateToolsStats(); } @@ -1980,6 +2068,9 @@ async function updateToolsStats() { const statsEl = document.getElementById('tools-stats'); if (!statsEl) return; + // 先保存当前页的状态到全局映射 + saveCurrentPageToolStates(); + // 计算当前页的启用工具数 const currentPageEnabled = Array.from(document.querySelectorAll('#tools-list input[type="checkbox"]:checked')).length; const currentPageTotal = document.querySelectorAll('#tools-list input[type="checkbox"]').length; @@ -1993,22 +2084,32 @@ async function updateToolsStats() { if (toolsSearchKeyword) { totalTools = allTools.length; totalEnabled = allTools.filter(tool => { + // 优先使用全局状态映射,否则使用checkbox状态,最后使用服务器返回的状态 + const savedState = toolStateMap.get(tool.name); + if (savedState !== undefined) { + return savedState.enabled; + } const checkbox = document.getElementById(`tool-${tool.name}`); return checkbox ? checkbox.checked : tool.enabled; }).length; } else { // 没有搜索时,需要获取所有工具的状态 - // 先使用当前已知的工具状态 - const toolStateMap = new Map(); + // 先使用全局状态映射和当前页的checkbox状态 + const localStateMap = new Map(); - // 从当前页的checkbox获取状态 + // 从当前页的checkbox获取状态(如果全局映射中没有) allTools.forEach(tool => { - const checkbox = document.getElementById(`tool-${tool.name}`); - if (checkbox) { - toolStateMap.set(tool.name, checkbox.checked); + const savedState = toolStateMap.get(tool.name); + if (savedState !== undefined) { + localStateMap.set(tool.name, savedState.enabled); } else { - // 如果checkbox不存在(不在当前页),使用工具原始状态 - toolStateMap.set(tool.name, tool.enabled); + const checkbox = document.getElementById(`tool-${tool.name}`); + if (checkbox) { + localStateMap.set(tool.name, checkbox.checked); + } else { + // 如果checkbox不存在(不在当前页),使用工具原始状态 + localStateMap.set(tool.name, tool.enabled); + } } }); @@ -2026,9 +2127,10 @@ async function updateToolsStats() { const pageResult = await pageResponse.json(); pageResult.tools.forEach(tool => { - // 如果工具不在当前页,使用服务器返回的状态 - if (!toolStateMap.has(tool.name)) { - toolStateMap.set(tool.name, tool.enabled); + // 优先使用全局状态映射,否则使用服务器返回的状态 + if (!localStateMap.has(tool.name)) { + const savedState = toolStateMap.get(tool.name); + localStateMap.set(tool.name, savedState ? savedState.enabled : tool.enabled); } }); @@ -2041,7 +2143,7 @@ async function updateToolsStats() { } // 计算启用的工具数 - totalEnabled = Array.from(toolStateMap.values()).filter(enabled => enabled).length; + totalEnabled = Array.from(localStateMap.values()).filter(enabled => enabled).length; } } catch (error) { console.warn('获取工具统计失败,使用当前页数据', error); @@ -2112,22 +2214,8 @@ async function applySettings() { }; // 收集工具启用状态 - // 由于使用分页,需要先获取所有工具的状态 - // 先获取当前页的工具状态 - const currentPageTools = new Map(); - document.querySelectorAll('#tools-list .tool-item').forEach(item => { - const checkbox = item.querySelector('input[type="checkbox"]'); - const toolName = item.dataset.toolName; - const isExternal = item.dataset.isExternal === 'true'; - const externalMcp = item.dataset.externalMcp || ''; - if (toolName) { - currentPageTools.set(toolName, { - enabled: checkbox.checked, - is_external: isExternal, - external_mcp: externalMcp - }); - } - }); + // 先保存当前页的状态到全局映射 + saveCurrentPageToolStates(); // 获取所有工具列表以获取完整状态(遍历所有页面) // 注意:无论是否在搜索状态下,都要获取所有工具的状态,以确保完整保存 @@ -2148,16 +2236,15 @@ async function applySettings() { const pageResult = await pageResponse.json(); - // 将当前页的工具添加到映射中 - // 如果工具在当前显示的页面中(匹配搜索且在当前页),使用当前页的修改 - // 否则使用服务器返回的状态 + // 将工具添加到映射中 + // 优先使用全局状态映射中的状态(用户修改过的),否则使用服务器返回的状态 pageResult.tools.forEach(tool => { - const currentPageTool = currentPageTools.get(tool.name); + const savedState = toolStateMap.get(tool.name); allToolsMap.set(tool.name, { name: tool.name, - enabled: currentPageTool ? currentPageTool.enabled : tool.enabled, - is_external: currentPageTool ? currentPageTool.is_external : (tool.is_external || false), - external_mcp: currentPageTool ? currentPageTool.external_mcp : (tool.external_mcp || '') + enabled: savedState ? savedState.enabled : tool.enabled, + is_external: savedState ? savedState.is_external : (tool.is_external || false), + external_mcp: savedState ? savedState.external_mcp : (tool.external_mcp || '') }); }); @@ -2179,9 +2266,9 @@ async function applySettings() { }); }); } catch (error) { - console.warn('获取所有工具列表失败,仅使用当前页工具状态', error); - // 如果获取失败,只使用当前页的工具 - currentPageTools.forEach((toolData, toolName) => { + console.warn('获取所有工具列表失败,仅使用全局状态映射', error); + // 如果获取失败,使用全局状态映射 + toolStateMap.forEach((toolData, toolName) => { config.tools.push({ name: toolName, enabled: toolData.enabled, @@ -2457,8 +2544,9 @@ function renderMonitorStats(statsMap = {}, lastFetchedAt = null) {
`; - // 显示最多前4个工具的统计 + // 显示最多前4个工具的统计(过滤掉 totalCalls 为 0 的工具) const topTools = entries + .filter(tool => (tool.totalCalls || 0) > 0) .slice() .sort((a, b) => (b.totalCalls || 0) - (a.totalCalls || 0)) .slice(0, 4); @@ -2518,6 +2606,7 @@ function renderMonitorExecutions(executions = [], statusFilter = 'all') {
+
@@ -2525,6 +2614,12 @@ function renderMonitorExecutions(executions = [], statusFilter = 'all') { }) .join(''); + // 先移除旧的表格容器(保留分页控件) + const oldTableContainer = container.querySelector('.monitor-table-container'); + if (oldTableContainer) { + oldTableContainer.remove(); + } + // 创建表格容器 const tableContainer = document.createElement('div'); tableContainer.className = 'monitor-table-container'; @@ -2543,9 +2638,13 @@ function renderMonitorExecutions(executions = [], statusFilter = 'all') { `; - // 清空容器并添加表格 - container.innerHTML = ''; - container.appendChild(tableContainer); + // 在分页控件之前插入表格(如果存在分页控件) + const existingPagination = container.querySelector('.monitor-pagination'); + if (existingPagination) { + container.insertBefore(tableContainer, existingPagination); + } else { + container.appendChild(tableContainer); + } } // 渲染监控面板分页控件 @@ -2588,6 +2687,37 @@ function renderMonitorPagination() { container.appendChild(pagination); } +// 删除执行记录 +async function deleteExecution(executionId) { + if (!executionId) { + return; + } + + // 确认删除 + if (!confirm('确定要删除此执行记录吗?此操作不可恢复。')) { + return; + } + + try { + const response = await apiFetch(`/api/monitor/execution/${executionId}`, { + method: 'DELETE' + }); + + if (!response.ok) { + const error = await response.json().catch(() => ({})); + throw new Error(error.error || '删除执行记录失败'); + } + + // 删除成功后刷新当前页面 + const currentPage = monitorState.pagination.page; + await refreshMonitorPanel(currentPage); + + alert('执行记录已删除'); + } catch (error) { + console.error('删除执行记录失败:', error); + alert('删除执行记录失败: ' + error.message); + } +} function formatExecutionDuration(start, end) { if (!start) {