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 = ` - +