mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-06-26 07:49:58 +02:00
Add files via upload
This commit is contained in:
+201
-26
@@ -3,7 +3,6 @@ package database
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -227,6 +226,167 @@ func (db *DB) LoadToolExecutionsWithPagination(offset, limit int, status, toolNa
|
||||
return executions, nil
|
||||
}
|
||||
|
||||
func toolExecutionsFilterSQL(status, toolName string) (string, []interface{}) {
|
||||
args := []interface{}{}
|
||||
conditions := []string{}
|
||||
if status != "" {
|
||||
conditions = append(conditions, "status = ?")
|
||||
args = append(args, status)
|
||||
}
|
||||
if toolName != "" {
|
||||
conditions = append(conditions, "LOWER(tool_name) LIKE ?")
|
||||
args = append(args, "%"+strings.ToLower(toolName)+"%")
|
||||
}
|
||||
if len(conditions) == 0 {
|
||||
return "", args
|
||||
}
|
||||
return ` WHERE ` + strings.Join(conditions, ` AND `), args
|
||||
}
|
||||
|
||||
// ToolStatsSummary 工具调用汇总(全量聚合,不含逐工具明细)
|
||||
type ToolStatsSummary struct {
|
||||
TotalCalls int
|
||||
SuccessCalls int
|
||||
FailedCalls int
|
||||
LastCallTime *time.Time
|
||||
ToolCount int
|
||||
}
|
||||
|
||||
// ToolStatsSummaryResult 汇总 + Top N 工具排行
|
||||
type ToolStatsSummaryResult struct {
|
||||
Summary ToolStatsSummary
|
||||
TopTools []*mcp.ToolStats
|
||||
}
|
||||
|
||||
// LoadToolStatsSummary 聚合统计信息,仅返回汇总与 Top N 工具(避免全量 map 传输)
|
||||
func (db *DB) LoadToolStatsSummary(topN int) (*ToolStatsSummaryResult, error) {
|
||||
if topN <= 0 {
|
||||
topN = 6
|
||||
}
|
||||
if topN > 100 {
|
||||
topN = 100
|
||||
}
|
||||
|
||||
result := &ToolStatsSummaryResult{
|
||||
TopTools: make([]*mcp.ToolStats, 0, topN),
|
||||
}
|
||||
|
||||
summaryQuery := `
|
||||
SELECT COUNT(*),
|
||||
COALESCE(SUM(total_calls), 0),
|
||||
COALESCE(SUM(success_calls), 0),
|
||||
COALESCE(SUM(failed_calls), 0),
|
||||
MAX(last_call_time)
|
||||
FROM tool_stats
|
||||
`
|
||||
var lastCallRaw sql.NullString
|
||||
err := db.QueryRow(summaryQuery).Scan(
|
||||
&result.Summary.ToolCount,
|
||||
&result.Summary.TotalCalls,
|
||||
&result.Summary.SuccessCalls,
|
||||
&result.Summary.FailedCalls,
|
||||
&lastCallRaw,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if lastCallRaw.Valid && strings.TrimSpace(lastCallRaw.String) != "" {
|
||||
if t, parseErr := time.Parse(time.RFC3339Nano, lastCallRaw.String); parseErr == nil {
|
||||
result.Summary.LastCallTime = &t
|
||||
} else if t, parseErr := time.Parse("2006-01-02 15:04:05.999999999-07:00", lastCallRaw.String); parseErr == nil {
|
||||
result.Summary.LastCallTime = &t
|
||||
} else if t, parseErr := time.Parse("2006-01-02 15:04:05", lastCallRaw.String); parseErr == nil {
|
||||
result.Summary.LastCallTime = &t
|
||||
}
|
||||
}
|
||||
|
||||
topQuery := `
|
||||
SELECT tool_name, total_calls, success_calls, failed_calls, last_call_time
|
||||
FROM tool_stats
|
||||
WHERE total_calls > 0
|
||||
ORDER BY total_calls DESC, tool_name ASC
|
||||
LIMIT ?
|
||||
`
|
||||
rows, err := db.Query(topQuery, topN)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var stat mcp.ToolStats
|
||||
var lastCallTime sql.NullTime
|
||||
if err := rows.Scan(
|
||||
&stat.ToolName,
|
||||
&stat.TotalCalls,
|
||||
&stat.SuccessCalls,
|
||||
&stat.FailedCalls,
|
||||
&lastCallTime,
|
||||
); err != nil {
|
||||
db.logger.Warn("加载 Top 工具统计失败", zap.Error(err))
|
||||
continue
|
||||
}
|
||||
if lastCallTime.Valid {
|
||||
stat.LastCallTime = &lastCallTime.Time
|
||||
}
|
||||
result.TopTools = append(result.TopTools, &stat)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// LoadToolExecutionListPage 分页加载执行记录列表(不含 arguments/result,供监控列表使用)
|
||||
func (db *DB) LoadToolExecutionListPage(offset, limit int, status, toolName string) ([]*mcp.ToolExecution, error) {
|
||||
if limit <= 0 {
|
||||
limit = 20
|
||||
}
|
||||
if limit > 100 {
|
||||
limit = 100
|
||||
}
|
||||
|
||||
query := `
|
||||
SELECT id, tool_name, status, start_time, end_time, duration_ms
|
||||
FROM tool_executions
|
||||
`
|
||||
whereSQL, args := toolExecutionsFilterSQL(status, toolName)
|
||||
query += whereSQL + ` ORDER BY start_time DESC LIMIT ? OFFSET ?`
|
||||
args = append(args, limit, offset)
|
||||
|
||||
rows, err := db.Query(query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
executions := make([]*mcp.ToolExecution, 0, limit)
|
||||
for rows.Next() {
|
||||
var exec mcp.ToolExecution
|
||||
var endTime sql.NullTime
|
||||
var durationMs sql.NullInt64
|
||||
|
||||
if err := rows.Scan(
|
||||
&exec.ID,
|
||||
&exec.ToolName,
|
||||
&exec.Status,
|
||||
&exec.StartTime,
|
||||
&endTime,
|
||||
&durationMs,
|
||||
); err != nil {
|
||||
db.logger.Warn("加载执行记录列表失败", zap.Error(err))
|
||||
continue
|
||||
}
|
||||
if endTime.Valid {
|
||||
exec.EndTime = &endTime.Time
|
||||
}
|
||||
if durationMs.Valid {
|
||||
exec.Duration = time.Duration(durationMs.Int64) * time.Millisecond
|
||||
}
|
||||
executions = append(executions, &exec)
|
||||
}
|
||||
|
||||
return executions, nil
|
||||
}
|
||||
|
||||
// GetToolExecution 根据ID获取单条工具执行记录
|
||||
func (db *DB) GetToolExecution(id string) (*mcp.ToolExecution, error) {
|
||||
query := `
|
||||
@@ -687,13 +847,28 @@ func truncateCallsTimelineBucket(t time.Time, dailyBuckets bool) time.Time {
|
||||
|
||||
// LoadCallsTimeline 按时间范围加载调用趋势(since 起至今,含边界)
|
||||
func (db *DB) LoadCallsTimeline(since time.Time, dailyBuckets bool) ([]CallsTimelineBucket, error) {
|
||||
// 在 Go 侧按本地时区分桶,避免 SQLite strftime 对 UTC 存储时间分桶后再误当本地时间解析(差 8h 等问题)
|
||||
query := `
|
||||
SELECT start_time,
|
||||
CASE WHEN status IN ('failed', 'cancelled') THEN 1 ELSE 0 END AS failed
|
||||
FROM tool_executions
|
||||
WHERE start_time >= ?
|
||||
`
|
||||
var query string
|
||||
if dailyBuckets {
|
||||
query = `
|
||||
SELECT date(start_time, 'localtime') 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
|
||||
`
|
||||
} else {
|
||||
query = `
|
||||
SELECT strftime('%Y-%m-%d %H:00:00', start_time, 'localtime') 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
|
||||
`
|
||||
}
|
||||
|
||||
rows, err := db.Query(query, since)
|
||||
if err != nil {
|
||||
@@ -701,35 +876,35 @@ func (db *DB) LoadCallsTimeline(since time.Time, dailyBuckets bool) ([]CallsTime
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
bucketMap := make(map[time.Time]struct{ total, failed int })
|
||||
buckets := make([]CallsTimelineBucket, 0)
|
||||
for rows.Next() {
|
||||
var startTime time.Time
|
||||
var failed int
|
||||
if err := rows.Scan(&startTime, &failed); err != nil {
|
||||
var bucketStr string
|
||||
var total, failed int
|
||||
if err := rows.Scan(&bucketStr, &total, &failed); err != nil {
|
||||
db.logger.Warn("加载调用趋势失败", zap.Error(err))
|
||||
continue
|
||||
}
|
||||
key := truncateCallsTimelineBucket(startTime, dailyBuckets)
|
||||
entry := bucketMap[key]
|
||||
entry.total++
|
||||
entry.failed += failed
|
||||
bucketMap[key] = entry
|
||||
}
|
||||
|
||||
buckets := make([]CallsTimelineBucket, 0, len(bucketMap))
|
||||
for bucketTime, counts := range bucketMap {
|
||||
bucketTime, err := parseCallsTimelineBucket(bucketStr, dailyBuckets)
|
||||
if err != nil {
|
||||
db.logger.Warn("解析调用趋势时间桶失败", zap.Error(err), zap.String("bucket", bucketStr))
|
||||
continue
|
||||
}
|
||||
buckets = append(buckets, CallsTimelineBucket{
|
||||
BucketTime: bucketTime,
|
||||
Total: counts.total,
|
||||
Failed: counts.failed,
|
||||
Total: total,
|
||||
Failed: failed,
|
||||
})
|
||||
}
|
||||
sort.Slice(buckets, func(i, j int) bool {
|
||||
return buckets[i].BucketTime.Before(buckets[j].BucketTime)
|
||||
})
|
||||
return buckets, nil
|
||||
}
|
||||
|
||||
func parseCallsTimelineBucket(bucketStr string, dailyBuckets bool) (time.Time, error) {
|
||||
if dailyBuckets {
|
||||
return time.ParseInLocation("2006-01-02", bucketStr, time.Local)
|
||||
}
|
||||
return time.ParseInLocation("2006-01-02 15:04:05", bucketStr, time.Local)
|
||||
}
|
||||
|
||||
// DecreaseToolStats 减少工具统计信息(用于删除执行记录时)
|
||||
// 如果统计信息变为0,则删除该统计记录
|
||||
func (db *DB) DecreaseToolStats(toolName string, totalCalls, successCalls, failedCalls int) error {
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"cyberstrike-ai/internal/mcp"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestLoadToolStatsSummaryAndListPage(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "monitor-summary.db")
|
||||
db, err := NewDB(dbPath, zap.NewNop())
|
||||
if err != nil {
|
||||
t.Fatalf("NewDB: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
now := time.Now()
|
||||
tools := []struct {
|
||||
name string
|
||||
calls int
|
||||
ok int
|
||||
fail int
|
||||
result string
|
||||
}{
|
||||
{"alpha::run", 10, 9, 1, `{"content":[{"type":"text","text":"` + string(make([]byte, 64*1024)) + `"}]}`},
|
||||
{"beta::scan", 5, 5, 0, `{"content":[{"type":"text","text":"ok"}]}`},
|
||||
{"gamma::ping", 1, 1, 0, `{"content":[{"type":"text","text":"pong"}]}`},
|
||||
}
|
||||
|
||||
for _, tool := range tools {
|
||||
if err := db.UpdateToolStats(tool.name, tool.calls, tool.ok, tool.fail, &now); err != nil {
|
||||
t.Fatalf("UpdateToolStats(%s): %v", tool.name, err)
|
||||
}
|
||||
for j := 0; j < tool.calls; j++ {
|
||||
exec := &mcp.ToolExecution{
|
||||
ID: fmt.Sprintf("%s-exec-%d", tool.name, j),
|
||||
ToolName: tool.name,
|
||||
Arguments: map[string]interface{}{"n": j},
|
||||
Status: "completed",
|
||||
StartTime: now.Add(-time.Duration(j) * time.Minute),
|
||||
Result: &mcp.ToolResult{Content: []mcp.Content{{Type: "text", Text: tool.result}}},
|
||||
}
|
||||
end := exec.StartTime.Add(time.Second)
|
||||
exec.EndTime = &end
|
||||
exec.Duration = time.Second
|
||||
if err := db.SaveToolExecution(exec); err != nil {
|
||||
t.Fatalf("SaveToolExecution: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
summary, err := db.LoadToolStatsSummary(2)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadToolStatsSummary: %v", err)
|
||||
}
|
||||
if summary.Summary.ToolCount != 3 {
|
||||
t.Fatalf("toolCount = %d, want 3", summary.Summary.ToolCount)
|
||||
}
|
||||
if summary.Summary.TotalCalls != 16 {
|
||||
t.Fatalf("totalCalls = %d, want 16", summary.Summary.TotalCalls)
|
||||
}
|
||||
if len(summary.TopTools) != 2 {
|
||||
t.Fatalf("top tools = %d, want 2", len(summary.TopTools))
|
||||
}
|
||||
if summary.TopTools[0].ToolName != "alpha::run" {
|
||||
t.Fatalf("top tool = %q, want alpha::run", summary.TopTools[0].ToolName)
|
||||
}
|
||||
|
||||
list, err := db.LoadToolExecutionListPage(0, 5, "", "")
|
||||
if err != nil {
|
||||
t.Fatalf("LoadToolExecutionListPage: %v", err)
|
||||
}
|
||||
if len(list) != 5 {
|
||||
t.Fatalf("list len = %d, want 5", len(list))
|
||||
}
|
||||
for _, exec := range list {
|
||||
if exec.Arguments != nil || exec.Result != nil || exec.Error != "" {
|
||||
t.Fatalf("expected lite execution row, got args/result/error on %s", exec.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user