Add files via upload

This commit is contained in:
公明 authored and GitHub committed 2025-11-14 01:34:16 +08:00
1 parent d61c85e73e
commit 1b14070cee
9 files changed
+1276 -272

No files matched your search

+3 -4
View File
@@ -60,8 +60,8 @@ func New(cfg *config.Config, log *logger.Logger) (*App, error) {
return nil, fmt.Errorf("初始化数据库失败: %w", err) return nil, fmt.Errorf("初始化数据库失败: %w", err)
} }
// 创建MCP服务器 // 创建MCP服务器(带数据库持久化)
mcpServer := mcp.NewServer(log.Logger) mcpServer := mcp.NewServerWithStorage(log.Logger, db)
// 创建安全工具执行器 // 创建安全工具执行器
executor := security.NewExecutor(&cfg.Security, mcpServer, log.Logger) executor := security.NewExecutor(&cfg.Security, mcpServer, log.Logger)
@@ -91,7 +91,7 @@ func New(cfg *config.Config, log *logger.Logger) (*App, error) {
// 创建处理器 // 创建处理器
agentHandler := handler.NewAgentHandler(agent, db, log.Logger) agentHandler := handler.NewAgentHandler(agent, db, log.Logger)
monitorHandler := handler.NewMonitorHandler(mcpServer, executor, log.Logger) monitorHandler := handler.NewMonitorHandler(mcpServer, executor, db, log.Logger)
conversationHandler := handler.NewConversationHandler(db, log.Logger) conversationHandler := handler.NewConversationHandler(db, log.Logger)
authHandler := handler.NewAuthHandler(authManager, cfg, configPath, log.Logger) authHandler := handler.NewAuthHandler(authManager, cfg, configPath, log.Logger)
configHandler := handler.NewConfigHandler(configPath, cfg, mcpServer, executor, agent, log.Logger) configHandler := handler.NewConfigHandler(configPath, cfg, mcpServer, executor, agent, log.Logger)
@@ -188,7 +188,6 @@ func setupRoutes(
protected.GET("/monitor", monitorHandler.Monitor) protected.GET("/monitor", monitorHandler.Monitor)
protected.GET("/monitor/execution/:id", monitorHandler.GetExecution) protected.GET("/monitor/execution/:id", monitorHandler.GetExecution)
protected.GET("/monitor/stats", monitorHandler.GetStats) protected.GET("/monitor/stats", monitorHandler.GetStats)
protected.GET("/monitor/vulnerabilities", monitorHandler.GetVulnerabilities)
// 配置管理 // 配置管理
protected.GET("/config", configHandler.GetConfig) protected.GET("/config", configHandler.GetConfig)
+37
View File
@@ -75,12 +75,41 @@ func (db *DB) initTables() error {
FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE
);` );`
// 创建工具执行记录表
createToolExecutionsTable := `
CREATE TABLE IF NOT EXISTS tool_executions (
id TEXT PRIMARY KEY,
tool_name TEXT NOT NULL,
arguments TEXT NOT NULL,
status TEXT NOT NULL,
result TEXT,
error TEXT,
start_time DATETIME NOT NULL,
end_time DATETIME,
duration_ms INTEGER,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);`
// 创建工具统计表
createToolStatsTable := `
CREATE TABLE IF NOT EXISTS tool_stats (
tool_name TEXT PRIMARY KEY,
total_calls INTEGER NOT NULL DEFAULT 0,
success_calls INTEGER NOT NULL DEFAULT 0,
failed_calls INTEGER NOT NULL DEFAULT 0,
last_call_time DATETIME,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);`
// 创建索引 // 创建索引
createIndexes := ` createIndexes := `
CREATE INDEX IF NOT EXISTS idx_messages_conversation_id ON messages(conversation_id); CREATE INDEX IF NOT EXISTS idx_messages_conversation_id ON messages(conversation_id);
CREATE INDEX IF NOT EXISTS idx_conversations_updated_at ON conversations(updated_at); CREATE INDEX IF NOT EXISTS idx_conversations_updated_at ON conversations(updated_at);
CREATE INDEX IF NOT EXISTS idx_process_details_message_id ON process_details(message_id); CREATE INDEX IF NOT EXISTS idx_process_details_message_id ON process_details(message_id);
CREATE INDEX IF NOT EXISTS idx_process_details_conversation_id ON process_details(conversation_id); CREATE INDEX IF NOT EXISTS idx_process_details_conversation_id ON process_details(conversation_id);
CREATE INDEX IF NOT EXISTS idx_tool_executions_tool_name ON tool_executions(tool_name);
CREATE INDEX IF NOT EXISTS idx_tool_executions_start_time ON tool_executions(start_time);
CREATE INDEX IF NOT EXISTS idx_tool_executions_status ON tool_executions(status);
` `
if _, err := db.Exec(createConversationsTable); err != nil { if _, err := db.Exec(createConversationsTable); err != nil {
@@ -95,6 +124,14 @@ func (db *DB) initTables() error {
return fmt.Errorf("创建process_details表失败: %w", err) return fmt.Errorf("创建process_details表失败: %w", err)
} }
if _, err := db.Exec(createToolExecutionsTable); err != nil {
return fmt.Errorf("创建tool_executions表失败: %w", err)
}
if _, err := db.Exec(createToolStatsTable); err != nil {
return fmt.Errorf("创建tool_stats表失败: %w", err)
}
if _, err := db.Exec(createIndexes); err != nil { if _, err := db.Exec(createIndexes); err != nil {
return fmt.Errorf("创建索引失败: %w", err) return fmt.Errorf("创建索引失败: %w", err)
} }
+309
View File
@@ -0,0 +1,309 @@
package database
import (
"database/sql"
"encoding/json"
"time"
"cyberstrike-ai/internal/mcp"
"go.uber.org/zap"
)
// SaveToolExecution 保存工具执行记录
func (db *DB) SaveToolExecution(exec *mcp.ToolExecution) error {
argsJSON, err := json.Marshal(exec.Arguments)
if err != nil {
db.logger.Warn("序列化执行参数失败", zap.Error(err))
argsJSON = []byte("{}")
}
var resultJSON sql.NullString
if exec.Result != nil {
resultBytes, err := json.Marshal(exec.Result)
if err != nil {
db.logger.Warn("序列化执行结果失败", zap.Error(err))
} else {
resultJSON = sql.NullString{String: string(resultBytes), Valid: true}
}
}
var errorText sql.NullString
if exec.Error != "" {
errorText = sql.NullString{String: exec.Error, Valid: true}
}
var endTime sql.NullTime
if exec.EndTime != nil {
endTime = sql.NullTime{Time: *exec.EndTime, Valid: true}
}
var durationMs sql.NullInt64
if exec.Duration > 0 {
durationMs = sql.NullInt64{Int64: exec.Duration.Milliseconds(), Valid: true}
}
query := `
INSERT OR REPLACE INTO tool_executions
(id, tool_name, arguments, status, result, error, start_time, end_time, duration_ms, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`
_, err = db.Exec(query,
exec.ID,
exec.ToolName,
string(argsJSON),
exec.Status,
resultJSON,
errorText,
exec.StartTime,
endTime,
durationMs,
time.Now(),
)
if err != nil {
db.logger.Error("保存工具执行记录失败", zap.Error(err), zap.String("executionId", exec.ID))
return err
}
return nil
}
// LoadToolExecutions 加载所有工具执行记录
func (db *DB) LoadToolExecutions() ([]*mcp.ToolExecution, error) {
query := `
SELECT id, tool_name, arguments, status, result, error, start_time, end_time, duration_ms
FROM tool_executions
ORDER BY start_time DESC
LIMIT 1000
`
rows, err := db.Query(query)
if err != nil {
return nil, err
}
defer rows.Close()
var executions []*mcp.ToolExecution
for rows.Next() {
var exec mcp.ToolExecution
var argsJSON string
var resultJSON sql.NullString
var errorText sql.NullString
var endTime sql.NullTime
var durationMs sql.NullInt64
err := rows.Scan(
&exec.ID,
&exec.ToolName,
&argsJSON,
&exec.Status,
&resultJSON,
&errorText,
&exec.StartTime,
&endTime,
&durationMs,
)
if err != nil {
db.logger.Warn("加载执行记录失败", zap.Error(err))
continue
}
// 解析参数
if err := json.Unmarshal([]byte(argsJSON), &exec.Arguments); err != nil {
db.logger.Warn("解析执行参数失败", zap.Error(err))
exec.Arguments = make(map[string]interface{})
}
// 解析结果
if resultJSON.Valid && resultJSON.String != "" {
var result mcp.ToolResult
if err := json.Unmarshal([]byte(resultJSON.String), &result); err != nil {
db.logger.Warn("解析执行结果失败", zap.Error(err))
} else {
exec.Result = &result
}
}
// 设置错误
if errorText.Valid {
exec.Error = errorText.String
}
// 设置结束时间
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 := `
SELECT id, tool_name, arguments, status, result, error, start_time, end_time, duration_ms
FROM tool_executions
WHERE id = ?
`
row := db.QueryRow(query, id)
var exec mcp.ToolExecution
var argsJSON string
var resultJSON sql.NullString
var errorText sql.NullString
var endTime sql.NullTime
var durationMs sql.NullInt64
err := row.Scan(
&exec.ID,
&exec.ToolName,
&argsJSON,
&exec.Status,
&resultJSON,
&errorText,
&exec.StartTime,
&endTime,
&durationMs,
)
if err != nil {
return nil, err
}
if err := json.Unmarshal([]byte(argsJSON), &exec.Arguments); err != nil {
db.logger.Warn("解析执行参数失败", zap.Error(err))
exec.Arguments = make(map[string]interface{})
}
if resultJSON.Valid && resultJSON.String != "" {
var result mcp.ToolResult
if err := json.Unmarshal([]byte(resultJSON.String), &result); err != nil {
db.logger.Warn("解析执行结果失败", zap.Error(err))
} else {
exec.Result = &result
}
}
if errorText.Valid {
exec.Error = errorText.String
}
if endTime.Valid {
exec.EndTime = &endTime.Time
}
if durationMs.Valid {
exec.Duration = time.Duration(durationMs.Int64) * time.Millisecond
}
return &exec, nil
}
// SaveToolStats 保存工具统计信息
func (db *DB) SaveToolStats(toolName string, stats *mcp.ToolStats) error {
var lastCallTime sql.NullTime
if stats.LastCallTime != nil {
lastCallTime = sql.NullTime{Time: *stats.LastCallTime, Valid: true}
}
query := `
INSERT OR REPLACE INTO tool_stats
(tool_name, total_calls, success_calls, failed_calls, last_call_time, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
`
_, err := db.Exec(query,
toolName,
stats.TotalCalls,
stats.SuccessCalls,
stats.FailedCalls,
lastCallTime,
time.Now(),
)
if err != nil {
db.logger.Error("保存工具统计信息失败", zap.Error(err), zap.String("toolName", toolName))
return err
}
return nil
}
// LoadToolStats 加载所有工具统计信息
func (db *DB) LoadToolStats() (map[string]*mcp.ToolStats, error) {
query := `
SELECT tool_name, total_calls, success_calls, failed_calls, last_call_time
FROM tool_stats
`
rows, err := db.Query(query)
if err != nil {
return nil, err
}
defer rows.Close()
stats := make(map[string]*mcp.ToolStats)
for rows.Next() {
var stat mcp.ToolStats
var lastCallTime sql.NullTime
err := rows.Scan(
&stat.ToolName,
&stat.TotalCalls,
&stat.SuccessCalls,
&stat.FailedCalls,
&lastCallTime,
)
if err != nil {
db.logger.Warn("加载统计信息失败", zap.Error(err))
continue
}
if lastCallTime.Valid {
stat.LastCallTime = &lastCallTime.Time
}
stats[stat.ToolName] = &stat
}
return stats, nil
}
// UpdateToolStats 更新工具统计信息(累加模式)
func (db *DB) UpdateToolStats(toolName string, totalCalls, successCalls, failedCalls int, lastCallTime *time.Time) error {
var lastCallTimeSQL sql.NullTime
if lastCallTime != nil {
lastCallTimeSQL = sql.NullTime{Time: *lastCallTime, Valid: true}
}
query := `
INSERT INTO tool_stats (tool_name, total_calls, success_calls, failed_calls, last_call_time, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(tool_name) DO UPDATE SET
total_calls = total_calls + ?,
success_calls = success_calls + ?,
failed_calls = failed_calls + ?,
last_call_time = COALESCE(?, last_call_time),
updated_at = ?
`
_, err := db.Exec(query,
toolName, totalCalls, successCalls, failedCalls, lastCallTimeSQL, time.Now(),
totalCalls, successCalls, failedCalls, lastCallTimeSQL, time.Now(),
)
if err != nil {
db.logger.Error("更新工具统计信息失败", zap.Error(err), zap.String("toolName", toolName))
return err
}
return nil
}
+40 -36
View File
@@ -4,6 +4,7 @@ import (
"net/http" "net/http"
"time" "time"
"cyberstrike-ai/internal/database"
"cyberstrike-ai/internal/mcp" "cyberstrike-ai/internal/mcp"
"cyberstrike-ai/internal/security" "cyberstrike-ai/internal/security"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -14,57 +15,66 @@ import (
type MonitorHandler struct { type MonitorHandler struct {
mcpServer *mcp.Server mcpServer *mcp.Server
executor *security.Executor executor *security.Executor
db *database.DB
logger *zap.Logger logger *zap.Logger
vulns []security.Vulnerability
} }
// NewMonitorHandler 创建新的监控处理器 // NewMonitorHandler 创建新的监控处理器
func NewMonitorHandler(mcpServer *mcp.Server, executor *security.Executor, logger *zap.Logger) *MonitorHandler { func NewMonitorHandler(mcpServer *mcp.Server, executor *security.Executor, db *database.DB, logger *zap.Logger) *MonitorHandler {
return &MonitorHandler{ return &MonitorHandler{
mcpServer: mcpServer, mcpServer: mcpServer,
executor: executor, executor: executor,
db: db,
logger: logger, logger: logger,
vulns: []security.Vulnerability{},
} }
} }
// MonitorResponse 监控响应 // MonitorResponse 监控响应
type MonitorResponse struct { type MonitorResponse struct {
Executions []*mcp.ToolExecution `json:"executions"` Executions []*mcp.ToolExecution `json:"executions"`
Stats map[string]*mcp.ToolStats `json:"stats"` Stats map[string]*mcp.ToolStats `json:"stats"`
Vulnerabilities []security.Vulnerability `json:"vulnerabilities"` Timestamp time.Time `json:"timestamp"`
Report map[string]interface{} `json:"report"`
Timestamp time.Time `json:"timestamp"`
} }
// Monitor 获取监控信息 // Monitor 获取监控信息
func (h *MonitorHandler) Monitor(c *gin.Context) { func (h *MonitorHandler) Monitor(c *gin.Context) {
// 获取所有执行记录 executions := h.loadExecutions()
executions := h.mcpServer.GetAllExecutions() stats := h.loadStats()
// 分析执行结果,提取漏洞
for _, exec := range executions {
if exec.Status == "completed" && exec.Result != nil {
vulns := h.executor.AnalyzeResults(exec.ToolName, exec.Result)
h.vulns = append(h.vulns, vulns...)
}
}
// 获取统计信息
stats := h.mcpServer.GetStats()
// 生成报告
report := h.executor.GetVulnerabilityReport(h.vulns)
c.JSON(http.StatusOK, MonitorResponse{ c.JSON(http.StatusOK, MonitorResponse{
Executions: executions, Executions: executions,
Stats: stats, Stats: stats,
Vulnerabilities: h.vulns, Timestamp: time.Now(),
Report: report,
Timestamp: time.Now(),
}) })
} }
func (h *MonitorHandler) loadExecutions() []*mcp.ToolExecution {
if h.db == nil {
return h.mcpServer.GetAllExecutions()
}
executions, err := h.db.LoadToolExecutions()
if err != nil {
h.logger.Warn("从数据库加载执行记录失败,回退到内存数据", zap.Error(err))
return h.mcpServer.GetAllExecutions()
}
return executions
}
func (h *MonitorHandler) loadStats() map[string]*mcp.ToolStats {
if h.db == nil {
return h.mcpServer.GetStats()
}
stats, err := h.db.LoadToolStats()
if err != nil {
h.logger.Warn("从数据库加载统计信息失败,回退到内存数据", zap.Error(err))
return h.mcpServer.GetStats()
}
return stats
}
// GetExecution 获取特定执行记录 // GetExecution 获取特定执行记录
func (h *MonitorHandler) GetExecution(c *gin.Context) { func (h *MonitorHandler) GetExecution(c *gin.Context) {
id := c.Param("id") id := c.Param("id")
@@ -80,13 +90,7 @@ func (h *MonitorHandler) GetExecution(c *gin.Context) {
// GetStats 获取统计信息 // GetStats 获取统计信息
func (h *MonitorHandler) GetStats(c *gin.Context) { func (h *MonitorHandler) GetStats(c *gin.Context) {
stats := h.mcpServer.GetStats() stats := h.loadStats()
c.JSON(http.StatusOK, stats) c.JSON(http.StatusOK, stats)
} }
// GetVulnerabilities 获取漏洞列表
func (h *MonitorHandler) GetVulnerabilities(c *gin.Context) {
report := h.executor.GetVulnerabilityReport(h.vulns)
c.JSON(http.StatusOK, report)
}
+284 -145
View File
@@ -15,14 +15,25 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
) )
// MonitorStorage 监控数据存储接口
type MonitorStorage interface {
SaveToolExecution(exec *ToolExecution) error
LoadToolExecutions() ([]*ToolExecution, error)
GetToolExecution(id string) (*ToolExecution, error)
SaveToolStats(toolName string, stats *ToolStats) error
LoadToolStats() (map[string]*ToolStats, error)
UpdateToolStats(toolName string, totalCalls, successCalls, failedCalls int, lastCallTime *time.Time) error
}
// Server MCP服务器 // Server MCP服务器
type Server struct { type Server struct {
tools map[string]ToolHandler tools map[string]ToolHandler
toolDefs map[string]Tool // 工具定义 toolDefs map[string]Tool // 工具定义
executions map[string]*ToolExecution executions map[string]*ToolExecution
stats map[string]*ToolStats stats map[string]*ToolStats
prompts map[string]*Prompt // 提示词模板 prompts map[string]*Prompt // 提示词模板
resources map[string]*Resource // 资源 resources map[string]*Resource // 资源
storage MonitorStorage // 可选的持久化存储
mu sync.RWMutex mu sync.RWMutex
logger *zap.Logger logger *zap.Logger
} }
@@ -32,6 +43,11 @@ type ToolHandler func(ctx context.Context, args map[string]interface{}) (*ToolRe
// NewServer 创建新的MCP服务器 // NewServer 创建新的MCP服务器
func NewServer(logger *zap.Logger) *Server { func NewServer(logger *zap.Logger) *Server {
return NewServerWithStorage(logger, nil)
}
// NewServerWithStorage 创建新的MCP服务器(带持久化存储)
func NewServerWithStorage(logger *zap.Logger, storage MonitorStorage) *Server {
s := &Server{ s := &Server{
tools: make(map[string]ToolHandler), tools: make(map[string]ToolHandler),
toolDefs: make(map[string]Tool), toolDefs: make(map[string]Tool),
@@ -39,6 +55,7 @@ func NewServer(logger *zap.Logger) *Server {
stats: make(map[string]*ToolStats), stats: make(map[string]*ToolStats),
prompts: make(map[string]*Prompt), prompts: make(map[string]*Prompt),
resources: make(map[string]*Resource), resources: make(map[string]*Resource),
storage: storage,
logger: logger, logger: logger,
} }
@@ -239,7 +256,6 @@ func (s *Server) handleCallTool(msg *Message) *Message {
} }
} }
// 创建执行记录
executionID := uuid.New().String() executionID := uuid.New().String()
execution := &ToolExecution{ execution := &ToolExecution{
ID: executionID, ID: executionID,
@@ -253,10 +269,12 @@ func (s *Server) handleCallTool(msg *Message) *Message {
s.executions[executionID] = execution s.executions[executionID] = execution
s.mu.Unlock() s.mu.Unlock()
// 更新统计 if s.storage != nil {
s.updateStats(req.Name, false) if err := s.storage.SaveToolExecution(execution); err != nil {
s.logger.Warn("保存执行记录到数据库失败", zap.Error(err))
}
}
// 执行工具
s.mu.RLock() s.mu.RLock()
handler, exists := s.tools[req.Name] handler, exists := s.tools[req.Name]
s.mu.RUnlock() s.mu.RUnlock()
@@ -266,6 +284,19 @@ func (s *Server) handleCallTool(msg *Message) *Message {
execution.Error = "Tool not found" execution.Error = "Tool not found"
now := time.Now() now := time.Now()
execution.EndTime = &now execution.EndTime = &now
execution.Duration = now.Sub(execution.StartTime)
if s.storage != nil {
if err := s.storage.SaveToolExecution(execution); err != nil {
s.logger.Warn("保存执行记录到数据库失败", zap.Error(err))
}
s.mu.Lock()
delete(s.executions, executionID)
s.mu.Unlock()
}
s.updateStats(req.Name, true)
return &Message{ return &Message{
ID: msg.ID, ID: msg.ID,
Type: MessageTypeError, Type: MessageTypeError,
@@ -274,7 +305,6 @@ func (s *Server) handleCallTool(msg *Message) *Message {
} }
} }
// 同步执行所有工具,确保错误能正确返回
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel() defer cancel()
@@ -284,24 +314,63 @@ func (s *Server) handleCallTool(msg *Message) *Message {
) )
result, err := handler(ctx, req.Arguments) result, err := handler(ctx, req.Arguments)
now := time.Now()
var failed bool
var finalResult *ToolResult
s.mu.Lock() s.mu.Lock()
now := time.Now()
execution.EndTime = &now execution.EndTime = &now
execution.Duration = now.Sub(execution.StartTime) execution.Duration = now.Sub(execution.StartTime)
if err != nil { if err != nil {
execution.Status = "failed" execution.Status = "failed"
execution.Error = err.Error() execution.Error = err.Error()
s.updateStats(req.Name, true) failed = true
s.mu.Unlock() } else if result != nil && result.IsError {
execution.Status = "failed"
if len(result.Content) > 0 {
execution.Error = result.Content[0].Text
} else {
execution.Error = "工具执行返回错误结果"
}
execution.Result = result
failed = true
} else {
execution.Status = "completed"
if result == nil {
result = &ToolResult{
Content: []Content{
{Type: "text", Text: "工具执行完成,但未返回结果"},
},
}
}
execution.Result = result
failed = false
}
finalResult = execution.Result
s.mu.Unlock()
if s.storage != nil {
if err := s.storage.SaveToolExecution(execution); err != nil {
s.logger.Warn("保存执行记录到数据库失败", zap.Error(err))
}
}
s.updateStats(req.Name, failed)
if s.storage != nil {
s.mu.Lock()
delete(s.executions, executionID)
s.mu.Unlock()
}
if err != nil {
s.logger.Error("工具执行失败", s.logger.Error("工具执行失败",
zap.String("toolName", req.Name), zap.String("toolName", req.Name),
zap.Error(err), zap.Error(err),
) )
// 返回错误结果
errorResult, _ := json.Marshal(CallToolResponse{ errorResult, _ := json.Marshal(CallToolResponse{
Content: []Content{ Content: []Content{
{Type: "text", Text: fmt.Sprintf("工具执行失败: %v", err)}, {Type: "text", Text: fmt.Sprintf("工具执行失败: %v", err)},
@@ -316,23 +385,25 @@ func (s *Server) handleCallTool(msg *Message) *Message {
} }
} }
// 检查result是否为错误 if finalResult != nil && finalResult.IsError {
if result != nil && result.IsError { s.logger.Warn("工具执行返回错误结果",
execution.Status = "failed" zap.String("toolName", req.Name),
if len(result.Content) > 0 { )
execution.Error = result.Content[0].Text
}
s.updateStats(req.Name, true)
} else {
execution.Status = "completed"
execution.Result = result
s.updateStats(req.Name, false)
}
s.mu.Unlock()
// 返回执行结果 errorResult, _ := json.Marshal(CallToolResponse{
if result == nil { Content: finalResult.Content,
result = &ToolResult{ IsError: true,
})
return &Message{
ID: msg.ID,
Type: MessageTypeResponse,
Version: "2.0",
Result: errorResult,
}
}
if finalResult == nil {
finalResult = &ToolResult{
Content: []Content{ Content: []Content{
{Type: "text", Text: "工具执行完成,但未返回结果"}, {Type: "text", Text: "工具执行完成,但未返回结果"},
}, },
@@ -340,13 +411,13 @@ func (s *Server) handleCallTool(msg *Message) *Message {
} }
resultJSON, _ := json.Marshal(CallToolResponse{ resultJSON, _ := json.Marshal(CallToolResponse{
Content: result.Content, Content: finalResult.Content,
IsError: result.IsError, IsError: false,
}) })
s.logger.Info("工具执行完成", s.logger.Info("工具执行完成",
zap.String("toolName", req.Name), zap.String("toolName", req.Name),
zap.Bool("isError", result.IsError), zap.Bool("isError", finalResult.IsError),
) )
return &Message{ return &Message{
@@ -359,6 +430,25 @@ func (s *Server) handleCallTool(msg *Message) *Message {
// updateStats 更新统计信息 // updateStats 更新统计信息
func (s *Server) updateStats(toolName string, failed bool) { func (s *Server) updateStats(toolName string, failed bool) {
now := time.Now()
if s.storage != nil {
totalCalls := 1
successCalls := 0
failedCalls := 0
if failed {
failedCalls = 1
} else {
successCalls = 1
}
if err := s.storage.UpdateToolStats(toolName, totalCalls, successCalls, failedCalls, &now); err != nil {
s.logger.Warn("保存统计信息到数据库失败", zap.Error(err))
}
return
}
s.mu.Lock()
defer s.mu.Unlock()
if s.stats[toolName] == nil { if s.stats[toolName] == nil {
s.stats[toolName] = &ToolStats{ s.stats[toolName] = &ToolStats{
ToolName: toolName, ToolName: toolName,
@@ -367,7 +457,6 @@ func (s *Server) updateStats(toolName string, failed bool) {
stats := s.stats[toolName] stats := s.stats[toolName]
stats.TotalCalls++ stats.TotalCalls++
now := time.Now()
stats.LastCallTime = &now stats.LastCallTime = &now
if failed { if failed {
@@ -377,34 +466,122 @@ func (s *Server) updateStats(toolName string, failed bool) {
} }
} }
// GetExecution 获取执行记录 // GetExecution 获取执行记录(先从内存查找,再从数据库查找)
func (s *Server) GetExecution(id string) (*ToolExecution, bool) { func (s *Server) GetExecution(id string) (*ToolExecution, bool) {
s.mu.RLock() s.mu.RLock()
defer s.mu.RUnlock()
exec, exists := s.executions[id] exec, exists := s.executions[id]
return exec, exists s.mu.RUnlock()
if exists {
return exec, true
}
if s.storage != nil {
exec, err := s.storage.GetToolExecution(id)
if err == nil {
return exec, true
}
}
return nil, false
} }
// GetAllExecutions 获取所有执行记录 // loadHistoricalData 从数据库加载历史数据
func (s *Server) loadHistoricalData() {
if s.storage == nil {
return
}
// 加载历史执行记录(最近1000条)
executions, err := s.storage.LoadToolExecutions()
if err != nil {
s.logger.Warn("加载历史执行记录失败", zap.Error(err))
} else {
s.mu.Lock()
for _, exec := range executions {
// 只加载最近1000条,避免内存占用过大
if len(s.executions) < 1000 {
s.executions[exec.ID] = exec
}
}
s.mu.Unlock()
s.logger.Info("加载历史执行记录", zap.Int("count", len(executions)))
}
// 加载历史统计信息
stats, err := s.storage.LoadToolStats()
if err != nil {
s.logger.Warn("加载历史统计信息失败", zap.Error(err))
} else {
s.mu.Lock()
for k, v := range stats {
s.stats[k] = v
}
s.mu.Unlock()
s.logger.Info("加载历史统计信息", zap.Int("count", len(stats)))
}
}
// GetAllExecutions 获取所有执行记录(合并内存和数据库)
func (s *Server) GetAllExecutions() []*ToolExecution { func (s *Server) GetAllExecutions() []*ToolExecution {
if s.storage != nil {
dbExecutions, err := s.storage.LoadToolExecutions()
if err == nil {
execMap := make(map[string]*ToolExecution)
for _, exec := range dbExecutions {
if _, exists := execMap[exec.ID]; !exists {
execMap[exec.ID] = exec
}
}
s.mu.RLock()
for id, exec := range s.executions {
if _, exists := execMap[id]; !exists {
execMap[id] = exec
}
}
s.mu.RUnlock()
result := make([]*ToolExecution, 0, len(execMap))
for _, exec := range execMap {
result = append(result, exec)
}
return result
} else {
s.logger.Warn("从数据库加载执行记录失败", zap.Error(err))
}
}
s.mu.RLock() s.mu.RLock()
defer s.mu.RUnlock() defer s.mu.RUnlock()
executions := make([]*ToolExecution, 0, len(s.executions))
memExecutions := make([]*ToolExecution, 0, len(s.executions))
for _, exec := range s.executions { for _, exec := range s.executions {
executions = append(executions, exec) memExecutions = append(memExecutions, exec)
} }
return executions return memExecutions
} }
// GetStats 获取统计信息 // GetStats 获取统计信息(合并内存和数据库)
func (s *Server) GetStats() map[string]*ToolStats { func (s *Server) GetStats() map[string]*ToolStats {
if s.storage != nil {
dbStats, err := s.storage.LoadToolStats()
if err == nil {
return dbStats
}
s.logger.Warn("从数据库加载统计信息失败", zap.Error(err))
}
s.mu.RLock() s.mu.RLock()
defer s.mu.RUnlock() defer s.mu.RUnlock()
stats := make(map[string]*ToolStats)
memStats := make(map[string]*ToolStats)
for k, v := range s.stats { for k, v := range s.stats {
stats[k] = v statCopy := *v
memStats[k] = &statCopy
} }
return stats
return memStats
} }
// GetAllTools 获取所有已注册的工具(用于Agent动态获取工具列表) // GetAllTools 获取所有已注册的工具(用于Agent动态获取工具列表)
@@ -443,30 +620,73 @@ func (s *Server) CallTool(ctx context.Context, toolName string, args map[string]
s.executions[executionID] = execution s.executions[executionID] = execution
s.mu.Unlock() s.mu.Unlock()
// 更新统计 if s.storage != nil {
s.updateStats(toolName, false) if err := s.storage.SaveToolExecution(execution); err != nil {
s.logger.Warn("保存执行记录到数据库失败", zap.Error(err))
}
}
// 执行工具
result, err := handler(ctx, args) result, err := handler(ctx, args)
s.mu.Lock() s.mu.Lock()
now := time.Now() now := time.Now()
execution.EndTime = &now execution.EndTime = &now
execution.Duration = now.Sub(execution.StartTime) execution.Duration = now.Sub(execution.StartTime)
var failed bool
var finalResult *ToolResult
if err != nil { if err != nil {
execution.Status = "failed" execution.Status = "failed"
execution.Error = err.Error() execution.Error = err.Error()
s.updateStats(toolName, true) failed = true
s.mu.Unlock() } else if result != nil && result.IsError {
return nil, executionID, err execution.Status = "failed"
if len(result.Content) > 0 {
execution.Error = result.Content[0].Text
} else {
execution.Error = "工具执行返回错误结果"
}
execution.Result = result
failed = true
finalResult = result
} else { } else {
execution.Status = "completed" execution.Status = "completed"
if result == nil {
result = &ToolResult{
Content: []Content{
{Type: "text", Text: "工具执行完成,但未返回结果"},
},
}
}
execution.Result = result execution.Result = result
s.updateStats(toolName, false) finalResult = result
s.mu.Unlock() failed = false
return result, executionID, nil
} }
if finalResult == nil {
finalResult = execution.Result
}
s.mu.Unlock()
if s.storage != nil {
if err := s.storage.SaveToolExecution(execution); err != nil {
s.logger.Warn("保存执行记录到数据库失败", zap.Error(err))
}
}
s.updateStats(toolName, failed)
if s.storage != nil {
s.mu.Lock()
delete(s.executions, executionID)
s.mu.Unlock()
}
if err != nil {
return nil, executionID, err
}
return finalResult, executionID, nil
} }
// initDefaultPrompts 初始化默认提示词模板 // initDefaultPrompts 初始化默认提示词模板
@@ -691,111 +911,31 @@ func (s *Server) generateResourceContent(resource *Resource) ResourceContent {
} }
// generateToolDocumentation 生成工具文档 // generateToolDocumentation 生成工具文档
// 注意:硬编码的工具文档已移除,现在只使用工具定义中的信息
func (s *Server) generateToolDocumentation(toolName string, resource *Resource) string { func (s *Server) generateToolDocumentation(toolName string, resource *Resource) string {
// 获取工具定义以获取更详细的信息 // 获取工具定义以获取更详细的信息
s.mu.RLock() s.mu.RLock()
tool, hasTool := s.toolDefs[toolName] tool, hasTool := s.toolDefs[toolName]
s.mu.RUnlock() s.mu.RUnlock()
// 为常见工具生成详细文档 // 使用工具定义中的描述信息
switch toolName { if hasTool {
case "nmap": doc := fmt.Sprintf("%s\n\n", resource.Description)
return `Nmap (Network Mapper) 是一个强大的网络扫描工具。 if tool.InputSchema != nil {
if props, ok := tool.InputSchema["properties"].(map[string]interface{}); ok {
主要功能: doc += "参数说明:\n"
- 端口扫描:发现目标主机开放的端口 for paramName, paramInfo := range props {
- 服务识别:识别运行在端口上的服务 if paramMap, ok := paramInfo.(map[string]interface{}); ok {
- 版本检测:检测服务版本信息 if desc, ok := paramMap["description"].(string); ok {
- 操作系统检测:识别目标操作系统 doc += fmt.Sprintf("- %s: %s\n", paramName, desc)
常用命令:
- nmap -sT target # TCP连接扫描
- nmap -sV target # 版本检测
- nmap -sC target # 默认脚本扫描
- nmap -p 1-1000 target # 扫描指定端口范围
参数说明:
- target: 目标IP地址或域名(必需)
- ports: 端口范围,例如: 1-1000(可选)`
case "sqlmap":
return `SQLMap 是一个自动化的SQL注入检测和利用工具。
主要功能:
- 自动检测SQL注入漏洞
- 数据库指纹识别
- 数据提取
- 文件系统访问
常用命令:
- sqlmap -u "http://target.com/page?id=1" # 检测URL参数
- sqlmap -u "http://target.com" --forms # 检测表单
- sqlmap -u "http://target.com" --dbs # 列出数据库
参数说明:
- url: 目标URL(必需)`
case "nikto":
return `Nikto 是一个Web服务器扫描工具。
主要功能:
- Web服务器漏洞扫描
- 检测过时的服务器软件
- 检测危险文件和程序
- 检测服务器配置问题
常用命令:
- nikto -h target # 扫描目标主机
- nikto -h target -p 80,443 # 扫描指定端口
参数说明:
- target: 目标URL(必需)`
case "dirb":
return `Dirb 是一个Web内容扫描器。
主要功能:
- 扫描Web目录和文件
- 发现隐藏的目录和文件
- 支持自定义字典
常用命令:
- dirb url # 扫描目标URL
- dirb url -w wordlist.txt # 使用自定义字典
参数说明:
- target: 目标URL(必需)`
case "exec":
return `Exec 工具用于执行系统命令。
⚠️ 警告:此工具可以执行任意系统命令,请谨慎使用!
参数说明:
- command: 要执行的系统命令(必需)
- shell: 使用的shell,默认为sh(可选)
- workdir: 工作目录(可选)`
default:
// 对于其他工具,使用工具定义中的描述信息
if hasTool {
doc := fmt.Sprintf("%s\n\n", resource.Description)
if tool.InputSchema != nil {
if props, ok := tool.InputSchema["properties"].(map[string]interface{}); ok {
doc += "参数说明:\n"
for paramName, paramInfo := range props {
if paramMap, ok := paramInfo.(map[string]interface{}); ok {
if desc, ok := paramMap["description"].(string); ok {
doc += fmt.Sprintf("- %s: %s\n", paramName, desc)
}
} }
} }
} }
} }
return doc
} }
return resource.Description return doc
} }
return resource.Description
} }
// handleSamplingRequest 处理采样请求 // handleSamplingRequest 处理采样请求
@@ -908,4 +1048,3 @@ func (s *Server) sendError(w http.ResponseWriter, id interface{}, code int, mess
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response) json.NewEncoder(w).Encode(response)
} }
+5 -69
View File
@@ -722,76 +722,12 @@ type Vulnerability struct {
} }
// AnalyzeResults 分析工具执行结果,提取漏洞信息 // AnalyzeResults 分析工具执行结果,提取漏洞信息
// 注意:硬编码的漏洞解析逻辑已移除,此函数现在返回空数组
// 漏洞检测应该由工具本身或专门的漏洞扫描工具来完成
func (e *Executor) AnalyzeResults(toolName string, result *mcp.ToolResult) []Vulnerability { func (e *Executor) AnalyzeResults(toolName string, result *mcp.ToolResult) []Vulnerability {
vulnerabilities := []Vulnerability{} // 不再进行硬编码的漏洞解析
// 漏洞检测应该由工具本身(如sqlmap、nmap等)的输出结果来体现
if result.IsError { return []Vulnerability{}
return vulnerabilities
}
// 分析输出内容
for _, content := range result.Content {
if content.Type == "text" {
vulns := e.parseToolOutput(toolName, content.Text)
vulnerabilities = append(vulnerabilities, vulns...)
}
}
return vulnerabilities
}
// parseToolOutput 解析工具输出
func (e *Executor) parseToolOutput(toolName, output string) []Vulnerability {
vulnerabilities := []Vulnerability{}
// 简单的漏洞检测逻辑
outputLower := strings.ToLower(output)
// SQL注入检测
if strings.Contains(outputLower, "sql injection") || strings.Contains(outputLower, "sqli") {
vulnerabilities = append(vulnerabilities, Vulnerability{
ID: fmt.Sprintf("sql-%d", time.Now().Unix()),
Type: "SQL Injection",
Severity: "high",
Title: "SQL注入漏洞",
Description: "检测到潜在的SQL注入漏洞",
FoundAt: time.Now(),
Details: output,
})
}
// XSS检测
if strings.Contains(outputLower, "xss") || strings.Contains(outputLower, "cross-site scripting") {
vulnerabilities = append(vulnerabilities, Vulnerability{
ID: fmt.Sprintf("xss-%d", time.Now().Unix()),
Type: "XSS",
Severity: "medium",
Title: "跨站脚本攻击漏洞",
Description: "检测到潜在的XSS漏洞",
FoundAt: time.Now(),
Details: output,
})
}
// 开放端口检测
if toolName == "nmap" {
lines := strings.Split(output, "\n")
for _, line := range lines {
if strings.Contains(line, "open") && strings.Contains(line, "port") {
vulnerabilities = append(vulnerabilities, Vulnerability{
ID: fmt.Sprintf("port-%d", time.Now().Unix()),
Type: "Open Port",
Severity: "low",
Title: "开放端口",
Description: fmt.Sprintf("发现开放端口: %s", line),
FoundAt: time.Now(),
Details: line,
})
}
}
}
return vulnerabilities
} }
// GetVulnerabilityReport 生成漏洞报告 // GetVulnerabilityReport 生成漏洞报告
+306 -14
View File
@@ -97,28 +97,55 @@ header {
font-weight: 400; font-weight: 400;
} }
.settings-btn { .header-actions {
background: transparent;
border: 1px solid rgba(255, 255, 255, 0.2);
color: white;
padding: 8px;
border-radius: 6px;
cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px;
}
.header-actions button {
display: inline-flex;
align-items: center;
justify-content: center; justify-content: center;
transition: all 0.2s; gap: 6px;
padding: 8px 14px;
border-radius: 999px;
border: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.05);
color: white;
cursor: pointer;
font-size: 0.85rem;
font-weight: 500;
transition: all 0.2s ease;
} }
.settings-btn:hover { .header-actions button svg {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.3);
}
.settings-btn svg {
stroke: currentColor; stroke: currentColor;
} }
.header-actions button:hover {
background: rgba(255, 255, 255, 0.12);
border-color: rgba(255, 255, 255, 0.35);
transform: translateY(-1px);
}
.monitor-btn {
color: #8cc4ff;
border-color: rgba(0, 102, 255, 0.35);
background: rgba(0, 102, 255, 0.15);
}
.monitor-btn:hover {
background: rgba(0, 102, 255, 0.25);
border-color: rgba(0, 102, 255, 0.45);
color: #cfe4ff;
}
.settings-btn {
padding: 8px;
min-width: 44px;
}
/* 侧边栏样式 */ /* 侧边栏样式 */
.sidebar { .sidebar {
width: 280px; width: 280px;
@@ -1625,3 +1652,268 @@ header {
background: var(--bg-tertiary); background: var(--bg-tertiary);
border-color: var(--accent-color); border-color: var(--accent-color);
} }
.monitor-modal-content {
max-width: 1080px;
width: 95%;
max-height: 92vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
.monitor-modal-body {
padding: 24px;
background: var(--bg-secondary);
border-radius: 0 0 16px 16px;
overflow-y: auto;
}
.monitor-sections {
display: grid;
gap: 24px;
}
.monitor-section {
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 14px;
padding: 20px;
box-shadow: var(--shadow-sm);
display: flex;
flex-direction: column;
gap: 16px;
}
.monitor-section .section-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.monitor-section .section-header h3 {
margin: 0;
font-size: 1.1rem;
color: var(--text-primary);
}
.monitor-section .section-actions {
display: flex;
align-items: center;
gap: 12px;
font-size: 0.875rem;
color: var(--text-secondary);
}
.monitor-section .section-actions select {
margin-left: 6px;
padding: 6px 10px;
border-radius: 999px;
border: 1px solid var(--border-color);
background: var(--bg-secondary);
color: var(--text-primary);
font-size: 0.875rem;
}
.monitor-stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
}
.monitor-stat-card {
background: var(--bg-secondary);
border: 1px solid rgba(0, 102, 255, 0.12);
border-radius: 12px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 6px;
box-shadow: var(--shadow-xs);
}
.monitor-stat-card h4 {
margin: 0;
font-size: 0.95rem;
color: var(--text-secondary);
}
.monitor-stat-value {
font-size: 1.8rem;
font-weight: 600;
color: var(--text-primary);
}
.monitor-stat-meta {
font-size: 0.8rem;
color: var(--text-muted);
}
.monitor-table-container {
border: 1px solid var(--border-color);
border-radius: 12px;
overflow: hidden;
}
.monitor-table {
width: 100%;
border-collapse: collapse;
font-size: 0.875rem;
}
.monitor-table th,
.monitor-table td {
padding: 12px 14px;
text-align: left;
border-bottom: 1px solid var(--border-color);
vertical-align: middle;
}
.monitor-table thead {
background: var(--bg-secondary);
color: var(--text-secondary);
font-weight: 600;
}
.monitor-table tbody tr:hover {
background: rgba(0, 102, 255, 0.08);
}
.monitor-status-chip {
display: inline-flex;
align-items: center;
gap: 6px;
border-radius: 999px;
padding: 4px 10px;
font-size: 0.75rem;
font-weight: 600;
}
.monitor-status-chip.completed {
background: rgba(40, 167, 69, 0.12);
color: var(--success-color);
}
.monitor-status-chip.running {
background: rgba(0, 102, 255, 0.12);
color: var(--accent-color);
}
.monitor-status-chip.failed {
background: rgba(220, 53, 69, 0.12);
color: var(--error-color);
}
.monitor-execution-actions {
display: flex;
align-items: center;
gap: 8px;
}
.monitor-execution-actions button {
padding: 6px 12px;
font-size: 0.75rem;
}
.monitor-vuln-container {
display: grid;
gap: 16px;
}
.monitor-vuln-summary {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.vuln-counter {
padding: 10px 16px;
border-radius: 12px;
background: var(--bg-secondary);
border: 1px solid rgba(0, 102, 255, 0.12);
min-width: 140px;
display: flex;
flex-direction: column;
gap: 4px;
}
.vuln-counter strong {
font-size: 1.4rem;
color: var(--text-primary);
}
.vuln-counter span {
font-size: 0.8rem;
color: var(--text-muted);
}
.vuln-list {
border: 1px solid var(--border-color);
border-radius: 12px;
overflow: hidden;
}
.vuln-item {
padding: 14px 16px;
border-bottom: 1px solid var(--border-color);
display: flex;
flex-direction: column;
gap: 6px;
}
.vuln-item:last-child {
border-bottom: none;
}
.vuln-title {
font-weight: 600;
color: var(--text-primary);
}
.vuln-severity {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 2px 10px;
border-radius: 999px;
font-size: 0.75rem;
font-weight: 600;
color: var(--text-primary);
}
.vuln-severity.critical {
background: rgba(108, 0, 150, 0.15);
color: #bf00ff;
}
.vuln-severity.high {
background: rgba(220, 53, 69, 0.12);
color: var(--error-color);
}
.vuln-severity.medium {
background: rgba(255, 193, 7, 0.15);
color: #b8860b;
}
.vuln-severity.low {
background: rgba(40, 167, 69, 0.12);
color: var(--success-color);
}
.monitor-empty {
text-align: center;
padding: 32px 16px;
color: var(--text-muted);
font-size: 0.9rem;
}
.monitor-error {
text-align: center;
padding: 24px 16px;
color: var(--error-color);
font-size: 0.9rem;
background: rgba(220, 53, 69, 0.08);
border-radius: 12px;
}
+240 -2
View File
@@ -1654,13 +1654,17 @@ function closeSettings() {
window.onclick = function(event) { window.onclick = function(event) {
const settingsModal = document.getElementById('settings-modal'); const settingsModal = document.getElementById('settings-modal');
const mcpModal = document.getElementById('mcp-detail-modal'); const mcpModal = document.getElementById('mcp-detail-modal');
const monitorModal = document.getElementById('monitor-modal');
if (event.target == settingsModal) { if (event.target === settingsModal) {
closeSettings(); closeSettings();
} }
if (event.target == mcpModal) { if (event.target === mcpModal) {
closeMCPDetail(); closeMCPDetail();
} }
if (event.target === monitorModal) {
closeMonitorPanel();
}
} }
// 加载配置 // 加载配置
@@ -1913,3 +1917,237 @@ async function changePassword() {
} }
} }
// 监控面板状态
const monitorState = {
executions: [],
stats: {},
lastFetchedAt: null
};
function openMonitorPanel() {
const modal = document.getElementById('monitor-modal');
if (!modal) {
return;
}
modal.style.display = 'block';
// 重置显示状态
const statsContainer = document.getElementById('monitor-stats');
const execContainer = document.getElementById('monitor-executions');
if (statsContainer) {
statsContainer.innerHTML = '<div class="monitor-empty">加载中...</div>';
}
if (execContainer) {
execContainer.innerHTML = '<div class="monitor-empty">加载中...</div>';
}
const statusFilter = document.getElementById('monitor-status-filter');
if (statusFilter) {
statusFilter.value = 'all';
}
refreshMonitorPanel();
}
function closeMonitorPanel() {
const modal = document.getElementById('monitor-modal');
if (modal) {
modal.style.display = 'none';
}
}
async function refreshMonitorPanel() {
const statsContainer = document.getElementById('monitor-stats');
const execContainer = document.getElementById('monitor-executions');
try {
const response = await apiFetch('/api/monitor', { method: 'GET' });
const result = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(result.error || '获取监控数据失败');
}
monitorState.executions = Array.isArray(result.executions) ? result.executions : [];
monitorState.stats = result.stats || {};
monitorState.lastFetchedAt = new Date();
renderMonitorStats(monitorState.stats, monitorState.lastFetchedAt);
renderMonitorExecutions(monitorState.executions);
} catch (error) {
console.error('刷新监控面板失败:', error);
if (statsContainer) {
statsContainer.innerHTML = `<div class="monitor-error">无法加载统计信息:${escapeHtml(error.message)}</div>`;
}
if (execContainer) {
execContainer.innerHTML = `<div class="monitor-error">无法加载执行记录:${escapeHtml(error.message)}</div>`;
}
}
}
function applyMonitorFilters() {
const statusFilter = document.getElementById('monitor-status-filter');
const status = statusFilter ? statusFilter.value : 'all';
renderMonitorExecutions(monitorState.executions, status);
}
function renderMonitorStats(statsMap = {}, lastFetchedAt = null) {
const container = document.getElementById('monitor-stats');
if (!container) {
return;
}
const entries = Object.values(statsMap);
if (entries.length === 0) {
container.innerHTML = '<div class="monitor-empty">暂无统计数据</div>';
return;
}
// 计算总体汇总
const totals = entries.reduce(
(acc, item) => {
acc.total += item.totalCalls || 0;
acc.success += item.successCalls || 0;
acc.failed += item.failedCalls || 0;
const lastCall = item.lastCallTime ? new Date(item.lastCallTime) : null;
if (lastCall && (!acc.lastCallTime || lastCall > acc.lastCallTime)) {
acc.lastCallTime = lastCall;
}
return acc;
},
{ total: 0, success: 0, failed: 0, lastCallTime: null }
);
const successRate = totals.total > 0 ? ((totals.success / totals.total) * 100).toFixed(1) : '0.0';
const lastUpdatedText = lastFetchedAt ? lastFetchedAt.toLocaleString('zh-CN') : 'N/A';
const lastCallText = totals.lastCallTime ? totals.lastCallTime.toLocaleString('zh-CN') : '暂无调用';
let html = `
<div class="monitor-stat-card">
<h4>总调用次数</h4>
<div class="monitor-stat-value">${totals.total}</div>
<div class="monitor-stat-meta">成功 ${totals.success} / 失败 ${totals.failed}</div>
</div>
<div class="monitor-stat-card">
<h4>成功率</h4>
<div class="monitor-stat-value">${successRate}%</div>
<div class="monitor-stat-meta">统计自全部工具调用</div>
</div>
<div class="monitor-stat-card">
<h4>最近一次调用</h4>
<div class="monitor-stat-value" style="font-size:1rem;">${lastCallText}</div>
<div class="monitor-stat-meta">最后刷新时间:${lastUpdatedText}</div>
</div>
`;
// 显示最多前4个工具的统计
const topTools = entries
.slice()
.sort((a, b) => (b.totalCalls || 0) - (a.totalCalls || 0))
.slice(0, 4);
topTools.forEach(tool => {
const toolSuccessRate = tool.totalCalls > 0 ? ((tool.successCalls || 0) / tool.totalCalls * 100).toFixed(1) : '0.0';
html += `
<div class="monitor-stat-card">
<h4>${escapeHtml(tool.toolName || '未知工具')}</h4>
<div class="monitor-stat-value">${tool.totalCalls || 0}</div>
<div class="monitor-stat-meta">
成功 ${tool.successCalls || 0} / 失败 ${tool.failedCalls || 0} · 成功率 ${toolSuccessRate}%
</div>
</div>
`;
});
container.innerHTML = `<div class="monitor-stats-grid">${html}</div>`;
}
function renderMonitorExecutions(executions = [], statusFilter = 'all') {
const container = document.getElementById('monitor-executions');
if (!container) {
return;
}
if (!Array.isArray(executions) || executions.length === 0) {
container.innerHTML = '<div class="monitor-empty">暂无执行记录</div>';
return;
}
const normalizedStatus = statusFilter === 'all' ? null : statusFilter;
const filtered = normalizedStatus
? executions.filter(exec => (exec.status || '').toLowerCase() === normalizedStatus)
: executions;
if (filtered.length === 0) {
container.innerHTML = '<div class="monitor-empty">当前筛选条件下暂无记录</div>';
return;
}
const rows = filtered
.slice(0, 25)
.map(exec => {
const status = (exec.status || 'unknown').toLowerCase();
const statusClass = `monitor-status-chip ${status}`;
const statusLabel = getStatusText(status);
const startTime = exec.startTime ? new Date(exec.startTime).toLocaleString('zh-CN') : '未知';
const duration = formatExecutionDuration(exec.startTime, exec.endTime);
const toolName = escapeHtml(exec.toolName || '未知工具');
const executionId = escapeHtml(exec.id || '');
return `
<tr>
<td>${toolName}</td>
<td><span class="${statusClass}">${statusLabel}</span></td>
<td>${startTime}</td>
<td>${duration}</td>
<td>
<div class="monitor-execution-actions">
<button class="btn-secondary" onclick="showMCPDetail('${executionId}')">查看详情</button>
</div>
</td>
</tr>
`;
})
.join('');
container.innerHTML = `
<div class="monitor-table-container">
<table class="monitor-table">
<thead>
<tr>
<th>工具</th>
<th>状态</th>
<th>开始时间</th>
<th>耗时</th>
<th>操作</th>
</tr>
</thead>
<tbody>${rows}</tbody>
</table>
</div>
`;
}
function formatExecutionDuration(start, end) {
if (!start) {
return '未知';
}
const startTime = new Date(start);
const endTime = end ? new Date(end) : new Date();
if (Number.isNaN(startTime.getTime()) || Number.isNaN(endTime.getTime())) {
return '未知';
}
const diffMs = Math.max(0, endTime - startTime);
const seconds = Math.floor(diffMs / 1000);
if (seconds < 60) {
return `${seconds} 秒`;
}
const minutes = Math.floor(seconds / 60);
if (minutes < 60) {
const remain = seconds % 60;
return remain > 0 ? `${minutes} 分 ${remain} 秒` : `${minutes} 分`;
}
const hours = Math.floor(minutes / 60);
const remainMinutes = minutes % 60;
return remainMinutes > 0 ? `${hours} 小时 ${remainMinutes} 分` : `${hours} 小时`;
}
+52 -2
View File
@@ -37,12 +37,20 @@
</div> </div>
<div class="header-right"> <div class="header-right">
<p class="header-subtitle">安全测试平台</p> <p class="header-subtitle">安全测试平台</p>
<button class="settings-btn" onclick="openSettings()" title="设置"> <div class="header-actions">
<button class="monitor-btn" onclick="openMonitorPanel()" title="MCP 监控面板">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 12h4l3 8 4-16 3 8h4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span>监控</span>
</button>
<button class="settings-btn" onclick="openSettings()" title="设置">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg> </svg>
</button> </button>
</div>
</div> </div>
</div> </div>
</header> </header>
@@ -155,6 +163,48 @@
</div> </div>
</div> </div>
<!-- 监控面板模态框 -->
<div id="monitor-modal" class="modal">
<div class="modal-content monitor-modal-content">
<div class="modal-header">
<h2>MCP 监控面板</h2>
<span class="modal-close" onclick="closeMonitorPanel()">&times;</span>
</div>
<div class="monitor-modal-body">
<div class="monitor-sections">
<section class="monitor-section monitor-overview">
<div class="section-header">
<h3>执行统计</h3>
<button class="btn-secondary" onclick="refreshMonitorPanel()">刷新</button>
</div>
<div id="monitor-stats" class="monitor-stats-grid">
<div class="monitor-empty">加载中...</div>
</div>
</section>
<section class="monitor-section monitor-executions">
<div class="section-header">
<h3>最新执行记录</h3>
<div class="section-actions">
<label>
状态筛选
<select id="monitor-status-filter" onchange="applyMonitorFilters()">
<option value="all">全部</option>
<option value="completed">已完成</option>
<option value="running">执行中</option>
<option value="failed">失败</option>
</select>
</label>
</div>
</div>
<div id="monitor-executions" class="monitor-table-container">
<div class="monitor-empty">加载中...</div>
</div>
</section>
</div>
</div>
</div>
</div>
<!-- MCP调用详情模态框 --> <!-- MCP调用详情模态框 -->
<div id="mcp-detail-modal" class="modal"> <div id="mcp-detail-modal" class="modal">
<div class="modal-content"> <div class="modal-content">