From 60ea1063017df2075a294256872533161f92c82a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Fri, 5 Jun 2026 10:38:24 +0800 Subject: [PATCH] Add files via upload --- internal/app/app.go | 2 + internal/database/monitor.go | 57 ++++++++++++++ internal/database/vulnerability.go | 33 ++++++++ internal/handler/monitor.go | 118 +++++++++++++++++++++++++++++ internal/handler/vulnerability.go | 32 ++++++++ 5 files changed, 242 insertions(+) diff --git a/internal/app/app.go b/internal/app/app.go index 573ad64b..ffd6d119 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -880,6 +880,7 @@ func setupRoutes( protected.DELETE("/monitor/execution/:id", monitorHandler.DeleteExecution) protected.DELETE("/monitor/executions", monitorHandler.DeleteExecutions) protected.GET("/monitor/stats", monitorHandler.GetStats) + protected.GET("/monitor/calls-timeline", monitorHandler.GetCallsTimeline) protected.GET("/notifications/summary", notificationHandler.GetSummary) protected.POST("/notifications/read", notificationHandler.MarkRead) @@ -1065,6 +1066,7 @@ func setupRoutes( // 漏洞管理 protected.GET("/vulnerabilities", vulnerabilityHandler.ListVulnerabilities) protected.GET("/vulnerabilities/export", vulnerabilityHandler.ExportVulnerabilities) + protected.DELETE("/vulnerabilities/batch", vulnerabilityHandler.BatchDeleteVulnerabilities) protected.GET("/vulnerabilities/filter-options", vulnerabilityHandler.GetVulnerabilityFilterOptions) protected.GET("/vulnerabilities/stats", vulnerabilityHandler.GetVulnerabilityStats) protected.GET("/vulnerabilities/:id", vulnerabilityHandler.GetVulnerability) diff --git a/internal/database/monitor.go b/internal/database/monitor.go index bdfffb61..9ca136a9 100644 --- a/internal/database/monitor.go +++ b/internal/database/monitor.go @@ -493,6 +493,63 @@ func (db *DB) UpdateToolStats(toolName string, totalCalls, successCalls, failedC return nil } +// CallsTimelineBucket 调用趋势时间桶 +type CallsTimelineBucket struct { + BucketTime time.Time + Total int + Failed int +} + +// LoadCallsTimeline 按时间范围加载调用趋势(since 起至今,含边界) +func (db *DB) LoadCallsTimeline(since time.Time, dailyBuckets bool) ([]CallsTimelineBucket, error) { + var bucketExpr string + if dailyBuckets { + bucketExpr = `strftime('%Y-%m-%d 00:00:00', start_time)` + } else { + bucketExpr = `strftime('%Y-%m-%d %H:00:00', start_time)` + } + + query := ` + SELECT ` + bucketExpr + ` AS bucket, + COUNT(*) AS total, + SUM(CASE WHEN status IN ('failed', 'cancelled') THEN 1 ELSE 0 END) AS failed + FROM tool_executions + WHERE start_time >= ? + GROUP BY bucket + ORDER BY bucket ASC + ` + + rows, err := db.Query(query, since) + if err != nil { + return nil, err + } + defer rows.Close() + + var buckets []CallsTimelineBucket + for rows.Next() { + var bucketStr string + var total, failed int + if err := rows.Scan(&bucketStr, &total, &failed); err != nil { + db.logger.Warn("加载调用趋势失败", zap.Error(err)) + continue + } + t, parseErr := time.ParseInLocation("2006-01-02 15:04:05", bucketStr, time.Local) + if parseErr != nil { + t, parseErr = time.Parse("2006-01-02 15:04:05", bucketStr) + if parseErr != nil { + db.logger.Warn("解析趋势时间桶失败", zap.String("bucket", bucketStr), zap.Error(parseErr)) + continue + } + } + buckets = append(buckets, CallsTimelineBucket{ + BucketTime: t, + Total: total, + Failed: failed, + }) + } + return buckets, nil +} + // DecreaseToolStats 减少工具统计信息(用于删除执行记录时) // 如果统计信息变为0,则删除该统计记录 func (db *DB) DecreaseToolStats(toolName string, totalCalls, successCalls, failedCalls int) error { diff --git a/internal/database/vulnerability.go b/internal/database/vulnerability.go index 77766207..8ca3352c 100644 --- a/internal/database/vulnerability.go +++ b/internal/database/vulnerability.go @@ -263,6 +263,39 @@ func (db *DB) UpdateVulnerability(id string, vuln *Vulnerability) error { return nil } +// DeleteVulnerabilitiesByFilter 按筛选条件批量删除漏洞,返回实际删除条数 +func (db *DB) DeleteVulnerabilitiesByFilter(filter VulnerabilityListFilter) (int64, error) { + tx, err := db.Begin() + if err != nil { + return 0, fmt.Errorf("开启事务失败: %w", err) + } + defer func() { _ = tx.Rollback() }() + + where := "WHERE 1=1" + args := []interface{}{} + where, args = filter.appendWhere(where, args) + + clearQuery := `UPDATE project_facts SET related_vulnerability_id = NULL + WHERE related_vulnerability_id IN (SELECT id FROM vulnerabilities ` + where + `)` + if _, err := tx.Exec(clearQuery, args...); err != nil { + return 0, fmt.Errorf("清理事实漏洞关联失败: %w", err) + } + + deleteQuery := `DELETE FROM vulnerabilities ` + where + result, err := tx.Exec(deleteQuery, args...) + if err != nil { + return 0, fmt.Errorf("批量删除漏洞失败: %w", err) + } + deleted, err := result.RowsAffected() + if err != nil { + return 0, fmt.Errorf("获取删除条数失败: %w", err) + } + if err := tx.Commit(); err != nil { + return 0, fmt.Errorf("提交事务失败: %w", err) + } + return deleted, nil +} + // DeleteVulnerability 删除漏洞 func (db *DB) DeleteVulnerability(id string) error { tx, err := db.Begin() diff --git a/internal/handler/monitor.go b/internal/handler/monitor.go index 334494f9..b2d08aac 100644 --- a/internal/handler/monitor.go +++ b/internal/handler/monitor.go @@ -327,6 +327,124 @@ func (h *MonitorHandler) GetStats(c *gin.Context) { c.JSON(http.StatusOK, stats) } +// CallsTimelinePoint 调用趋势数据点 +type CallsTimelinePoint struct { + T time.Time `json:"t"` + Total int `json:"total"` + Failed int `json:"failed"` +} + +// CallsTimelineSummary 调用趋势汇总 +type CallsTimelineSummary struct { + TotalCalls int `json:"totalCalls"` + Peak int `json:"peak"` +} + +// CallsTimelineResponse 调用趋势响应 +type CallsTimelineResponse struct { + Range string `json:"range"` + Points []CallsTimelinePoint `json:"points"` + Summary CallsTimelineSummary `json:"summary"` +} + +type callsTimelineConfig struct { + rangeKey string + duration time.Duration + bucketSize time.Duration + dailyBuckets bool +} + +func parseCallsTimelineRange(raw string) (callsTimelineConfig, bool) { + switch strings.TrimSpace(raw) { + case "24h": + return callsTimelineConfig{rangeKey: "24h", duration: 24 * time.Hour, bucketSize: time.Hour, dailyBuckets: false}, true + case "30d": + return callsTimelineConfig{rangeKey: "30d", duration: 30 * 24 * time.Hour, bucketSize: 24 * time.Hour, dailyBuckets: true}, true + default: + return callsTimelineConfig{rangeKey: "7d", duration: 7 * 24 * time.Hour, bucketSize: time.Hour, dailyBuckets: false}, true + } +} + +func truncateToBucket(t time.Time, bucketSize time.Duration, dailyBuckets bool) time.Time { + if dailyBuckets { + y, m, d := t.Date() + return time.Date(y, m, d, 0, 0, 0, 0, t.Location()) + } + return t.Truncate(bucketSize) +} + +func buildCallsTimelinePoints(cfg callsTimelineConfig, buckets map[time.Time]struct{ total, failed int }) []CallsTimelinePoint { + now := time.Now() + start := truncateToBucket(now.Add(-cfg.duration), cfg.bucketSize, cfg.dailyBuckets) + end := truncateToBucket(now, cfg.bucketSize, cfg.dailyBuckets) + + points := make([]CallsTimelinePoint, 0) + for current := start; !current.After(end); current = current.Add(cfg.bucketSize) { + val := buckets[current] + points = append(points, CallsTimelinePoint{ + T: current, + Total: val.total, + Failed: val.failed, + }) + } + return points +} + +func (h *MonitorHandler) loadCallsTimeline(cfg callsTimelineConfig) []CallsTimelinePoint { + since := time.Now().Add(-cfg.duration) + bucketMap := make(map[time.Time]struct{ total, failed int }) + + if h.db != nil { + dbBuckets, err := h.db.LoadCallsTimeline(since, cfg.dailyBuckets) + if err != nil { + h.logger.Warn("从数据库加载调用趋势失败,回退到内存数据", zap.Error(err)) + } else { + for _, b := range dbBuckets { + key := truncateToBucket(b.BucketTime, cfg.bucketSize, cfg.dailyBuckets) + entry := bucketMap[key] + entry.total += b.Total + entry.failed += b.Failed + bucketMap[key] = entry + } + return buildCallsTimelinePoints(cfg, bucketMap) + } + } + + for _, exec := range h.mcpServer.GetAllExecutions() { + if exec == nil || exec.StartTime.Before(since) { + continue + } + key := truncateToBucket(exec.StartTime, cfg.bucketSize, cfg.dailyBuckets) + entry := bucketMap[key] + entry.total++ + if exec.Status == "failed" || exec.Status == "cancelled" { + entry.failed++ + } + bucketMap[key] = entry + } + return buildCallsTimelinePoints(cfg, bucketMap) +} + +// GetCallsTimeline 获取 MCP 工具调用趋势 +func (h *MonitorHandler) GetCallsTimeline(c *gin.Context) { + cfg, _ := parseCallsTimelineRange(c.Query("range")) + points := h.loadCallsTimeline(cfg) + + summary := CallsTimelineSummary{} + for _, p := range points { + summary.TotalCalls += p.Total + if p.Total > summary.Peak { + summary.Peak = p.Total + } + } + + c.JSON(http.StatusOK, CallsTimelineResponse{ + Range: cfg.rangeKey, + Points: points, + Summary: summary, + }) +} + // DeleteExecution 删除执行记录 func (h *MonitorHandler) DeleteExecution(c *gin.Context) { id := c.Param("id") diff --git a/internal/handler/vulnerability.go b/internal/handler/vulnerability.go index 237f91ad..57d84d0b 100644 --- a/internal/handler/vulnerability.go +++ b/internal/handler/vulnerability.go @@ -311,6 +311,38 @@ func (h *VulnerabilityHandler) DeleteVulnerability(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "删除成功"}) } +// BatchDeleteVulnerabilities 按当前筛选条件批量删除漏洞 +func (h *VulnerabilityHandler) BatchDeleteVulnerabilities(c *gin.Context) { + filter := parseVulnerabilityListFilter(c) + + total, err := h.db.CountVulnerabilities(filter) + if err != nil { + h.logger.Error("统计待删除漏洞失败", zap.Error(err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + if total == 0 { + c.JSON(http.StatusOK, gin.H{"message": "当前筛选条件下没有可删除的漏洞", "deleted": 0}) + return + } + + deleted, err := h.db.DeleteVulnerabilitiesByFilter(filter) + if err != nil { + h.logger.Error("批量删除漏洞失败", zap.Error(err), zap.Int("count", total)) + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + if h.audit != nil { + h.audit.RecordOK(c, "vulnerability", "delete_batch", "批量删除漏洞记录", "vulnerability", "", map[string]interface{}{ + "deleted": deleted, + "filter": filter, + }) + } + + c.JSON(http.StatusOK, gin.H{"message": "批量删除成功", "deleted": deleted}) +} + // GetVulnerabilityStats 获取漏洞统计 func (h *VulnerabilityHandler) GetVulnerabilityStats(c *gin.Context) { filter := parseVulnerabilityListFilter(c)