Add files via upload

This commit is contained in:
公明
2026-06-05 10:38:24 +08:00
committed by GitHub
parent 92c0ae19bb
commit 60ea106301
5 changed files with 242 additions and 0 deletions
+2
View File
@@ -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)
+57
View File
@@ -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 {
+33
View File
@@ -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()
+118
View File
@@ -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")
+32
View File
@@ -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)