From 989766a73bbddf55eb7728a07f615e6ad1bf6a1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Thu, 13 Nov 2025 01:26:40 +0800 Subject: [PATCH] Add files via upload --- README.md | 50 +++++- config.yaml.backup | 20 +++ internal/agent/agent.go | 23 +++ internal/app/app.go | 16 +- internal/config/config.go | 8 +- internal/handler/config.go | 327 +++++++++++++++++++++++++++++++++++++ internal/mcp/server.go | 20 +++ web/static/css/style.css | 244 +++++++++++++++++++++++++++ web/static/js/app.js | 212 +++++++++++++++++++++++- web/templates/index.html | 68 +++++++- 10 files changed, 971 insertions(+), 17 deletions(-) create mode 100644 config.yaml.backup create mode 100644 internal/handler/config.go diff --git a/README.md b/README.md index 83e94f27..b1427017 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,16 @@ # CyberStrikeAI -🚀 **AI自主渗透测试平台** - 基于Golang构建,内置上百个安全工具,支持灵活扩展自定义工具,通过MCP协议实现AI智能决策与自动化执行,让安全测试像对话一样简单。(目前该工具也已支持 MCP Stdio 模式) +🚀 **AI自主渗透测试平台** - 基于Golang构建,内置上百个安全工具,支持灵活扩展自定义工具,通过MCP协议实现AI智能决策与自动化执行,让安全测试像对话一样简单。  ## 更新日志 - - 2025.11.13 ✨ 新增 MCP Stdio 模式支持,现可在代码编辑器、CLI 及自动化脚本等多种场景下,无缝集成并使用全套安全工具; - - 2025.11.12 ✨ 新增 任务停止功能,优化前端; + - 2025.11.13 新增 MCP stdio 模式支持,可在 Cursor IDE 中直接使用所有安全工具; + - 2025.11.12 增加了任务停止功能,优化前端; + - 2025.01.XX 优化系统设置功能: + - ✅ 打开设置时自动加载当前 `config.yaml` 中的配置 + - ✅ 关键配置项(API Key、Base URL、模型)设置为必填,保存时进行验证 + - ✅ 优化模态框圆角显示,修复视觉问题 + - ✅ 改进配置表单验证,提供清晰的错误提示 ## ✨ 功能特性 @@ -17,6 +22,7 @@ - 📝 **智能总结** - 达到最大迭代次数时,AI自动总结测试结果并提供下一步执行计划 - 💬 **对话式交互** - 自然语言对话界面,支持流式输出(SSE),实时查看执行过程 - 📊 **对话历史管理** - 完整的对话历史记录,支持查看、删除和管理 +- ⚙️ **可视化配置管理** - Web界面配置系统设置,支持实时加载和保存配置,必填项验证 ### 工具集成 - 🔌 **MCP协议支持** - 完整实现MCP协议,支持工具注册、调用、监控 @@ -109,6 +115,18 @@ go mod download ``` 3. **配置** + +#### 方式一:通过Web界面配置(推荐) + +启动服务器后,在Web界面点击右上角的"设置"按钮,可以可视化配置: +- **OpenAI配置**:API Key、Base URL、模型(必填项,标记为 *) +- **MCP工具配置**:启用/禁用工具 +- **Agent配置**:最大迭代次数等 + +配置会自动保存到 `config.yaml` 文件中。打开设置时会自动加载当前配置文件中的值。 + +#### 方式二:直接编辑配置文件 + 编辑 `config.yaml` 文件,设置您的API配置: ```yaml @@ -137,6 +155,8 @@ security: - DeepSeek: `https://api.deepseek.com/v1` - 其他兼容OpenAI协议的API服务 +**注意**:API Key、Base URL 和模型是必填项,必须配置才能正常运行系统。在Web界面配置时,这些字段会进行验证,未填写时会显示错误提示。 + 4. **安装安全工具(可选)** 根据您的需求安装相应的安全工具。系统支持上百个工具,您可以根据实际需要选择性安装: @@ -187,9 +207,33 @@ go run cmd/server/main.go -config /path/to/config.yaml - **对话测试** - 与AI对话进行渗透测试 - **工具监控** - 查看工具执行状态和结果 - **对话历史** - 管理历史对话记录 +- **系统设置** - 配置API密钥、工具启用状态等(点击右上角设置按钮) + +**首次使用提示**: +- 在开始使用前,请先点击右上角的"设置"按钮配置您的API Key +- API Key、Base URL 和模型是必填项(标记为 *),必须填写才能正常使用 +- 配置会自动保存到 `config.yaml` 文件中 +- 打开设置时会自动加载当前配置文件中的最新配置 ## ⚙️ 配置说明 +### Web界面配置管理 + +系统提供了可视化的配置管理界面,您可以通过以下方式访问: + +1. **打开设置**:点击Web界面右上角的"设置"按钮 +2. **加载配置**:打开设置时会自动从 `config.yaml` 加载当前配置 +3. **修改配置**: + - **OpenAI配置**:修改API Key、Base URL、模型(必填项标记为 *) + - **MCP工具配置**:启用或禁用工具,支持搜索和批量操作 + - **Agent配置**:设置最大迭代次数等参数 +4. **保存配置**:点击"应用配置"按钮,配置会保存到 `config.yaml` 并立即生效 +5. **验证提示**:必填项未填写时会显示错误提示,并高亮显示错误字段 + +**配置验证规则**: +- API Key、Base URL、模型为必填项 +- 保存时会自动验证,未填写必填项会阻止保存并提示错误 + ### 完整配置示例 ```yaml diff --git a/config.yaml.backup b/config.yaml.backup new file mode 100644 index 00000000..75aad81f --- /dev/null +++ b/config.yaml.backup @@ -0,0 +1,20 @@ +agent: + max_iterations: 30 +database: + path: data/conversations.db +log: + level: info + output: stdout +mcp: + enabled: true + host: 0.0.0.0 + port: 8081 +openai: + api_key: sk-f02ac0f7cd114ff3996e1466455ebba9 + base_url: https://api.deepseek.com/v1 + model: deepseek-chat +security: + tools_dir: tools +server: + host: 0.0.0.0 + port: 8080 diff --git a/internal/agent/agent.go b/internal/agent/agent.go index 2204dc6f..bb7a7820 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -9,6 +9,7 @@ import ( "net" "net/http" "strings" + "sync" "time" "cyberstrike-ai/internal/config" @@ -23,6 +24,7 @@ type Agent struct { mcpServer *mcp.Server logger *zap.Logger maxIterations int + mu sync.RWMutex // 添加互斥锁以支持并发更新 } // NewAgent 创建新的Agent @@ -938,6 +940,27 @@ func (a *Agent) executeToolViaMCP(ctx context.Context, toolName string, args map }, nil } +// UpdateConfig 更新OpenAI配置 +func (a *Agent) UpdateConfig(cfg *config.OpenAIConfig) { + a.mu.Lock() + defer a.mu.Unlock() + a.config = cfg + a.logger.Info("Agent配置已更新", + zap.String("base_url", cfg.BaseURL), + zap.String("model", cfg.Model), + ) +} + +// UpdateMaxIterations 更新最大迭代次数 +func (a *Agent) UpdateMaxIterations(maxIterations int) { + a.mu.Lock() + defer a.mu.Unlock() + if maxIterations > 0 { + a.maxIterations = maxIterations + a.logger.Info("Agent最大迭代次数已更新", zap.Int("max_iterations", maxIterations)) + } +} + // formatToolError 格式化工具错误信息,提供更友好的错误描述 func (a *Agent) formatToolError(toolName string, args map[string]interface{}, err error) string { errorMsg := fmt.Sprintf(`工具执行失败 diff --git a/internal/app/app.go b/internal/app/app.go index 265e66c8..8be46ea2 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -73,9 +73,16 @@ func New(cfg *config.Config, log *logger.Logger) (*App, error) { agentHandler := handler.NewAgentHandler(agent, db, log.Logger) monitorHandler := handler.NewMonitorHandler(mcpServer, executor, log.Logger) conversationHandler := handler.NewConversationHandler(db, log.Logger) + + // 获取配置文件路径 + configPath := "config.yaml" + if len(os.Args) > 1 { + configPath = os.Args[1] + } + configHandler := handler.NewConfigHandler(configPath, cfg, mcpServer, executor, agent, log.Logger) // 设置路由 - setupRoutes(router, agentHandler, monitorHandler, conversationHandler, mcpServer) + setupRoutes(router, agentHandler, monitorHandler, conversationHandler, configHandler, mcpServer) return &App{ config: cfg, @@ -113,7 +120,7 @@ func (a *App) Run() error { } // setupRoutes 设置路由 -func setupRoutes(router *gin.Engine, agentHandler *handler.AgentHandler, monitorHandler *handler.MonitorHandler, conversationHandler *handler.ConversationHandler, mcpServer *mcp.Server) { +func setupRoutes(router *gin.Engine, agentHandler *handler.AgentHandler, monitorHandler *handler.MonitorHandler, conversationHandler *handler.ConversationHandler, configHandler *handler.ConfigHandler, mcpServer *mcp.Server) { // API路由 api := router.Group("/api") { @@ -137,6 +144,11 @@ func setupRoutes(router *gin.Engine, agentHandler *handler.AgentHandler, monitor api.GET("/monitor/stats", monitorHandler.GetStats) api.GET("/monitor/vulnerabilities", monitorHandler.GetVulnerabilities) + // 配置管理 + api.GET("/config", configHandler.GetConfig) + api.PUT("/config", configHandler.UpdateConfig) + api.POST("/config/apply", configHandler.ApplyConfig) + // MCP端点 api.POST("/mcp", func(c *gin.Context) { mcpServer.HandleHTTP(c.Writer, c.Request) diff --git a/internal/config/config.go b/internal/config/config.go index 943a27f9..424eb4eb 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -36,9 +36,9 @@ type MCPConfig struct { } type OpenAIConfig struct { - APIKey string `yaml:"api_key"` - BaseURL string `yaml:"base_url"` - Model string `yaml:"model"` + APIKey string `yaml:"api_key" json:"api_key"` + BaseURL string `yaml:"base_url" json:"base_url"` + Model string `yaml:"model" json:"model"` } type SecurityConfig struct { @@ -51,7 +51,7 @@ type DatabaseConfig struct { } type AgentConfig struct { - MaxIterations int `yaml:"max_iterations"` + MaxIterations int `yaml:"max_iterations" json:"max_iterations"` } type ToolConfig struct { diff --git a/internal/handler/config.go b/internal/handler/config.go new file mode 100644 index 00000000..e24a4e61 --- /dev/null +++ b/internal/handler/config.go @@ -0,0 +1,327 @@ +package handler + +import ( + "fmt" + "net/http" + "os" + "path/filepath" + "sync" + + "cyberstrike-ai/internal/config" + "cyberstrike-ai/internal/mcp" + "cyberstrike-ai/internal/security" + "github.com/gin-gonic/gin" + "go.uber.org/zap" + "gopkg.in/yaml.v3" +) + +// ConfigHandler 配置处理器 +type ConfigHandler struct { + configPath string + config *config.Config + mcpServer *mcp.Server + executor *security.Executor + agent AgentUpdater // Agent接口,用于更新Agent配置 + logger *zap.Logger + mu sync.RWMutex +} + +// AgentUpdater Agent更新接口 +type AgentUpdater interface { + UpdateConfig(cfg *config.OpenAIConfig) + UpdateMaxIterations(maxIterations int) +} + +// NewConfigHandler 创建新的配置处理器 +func NewConfigHandler(configPath string, cfg *config.Config, mcpServer *mcp.Server, executor *security.Executor, agent AgentUpdater, logger *zap.Logger) *ConfigHandler { + return &ConfigHandler{ + configPath: configPath, + config: cfg, + mcpServer: mcpServer, + executor: executor, + agent: agent, + logger: logger, + } +} + +// GetConfigResponse 获取配置响应 +type GetConfigResponse struct { + OpenAI config.OpenAIConfig `json:"openai"` + MCP config.MCPConfig `json:"mcp"` + Tools []ToolConfigInfo `json:"tools"` + Agent config.AgentConfig `json:"agent"` +} + +// ToolConfigInfo 工具配置信息 +type ToolConfigInfo struct { + Name string `json:"name"` + Description string `json:"description"` + Enabled bool `json:"enabled"` +} + +// GetConfig 获取当前配置 +func (h *ConfigHandler) GetConfig(c *gin.Context) { + h.mu.RLock() + defer h.mu.RUnlock() + + // 获取工具列表 + tools := make([]ToolConfigInfo, 0, len(h.config.Security.Tools)) + for _, tool := range h.config.Security.Tools { + tools = append(tools, ToolConfigInfo{ + Name: tool.Name, + Description: tool.ShortDescription, + Enabled: tool.Enabled, + }) + // 如果没有简短描述,使用详细描述的前100个字符 + if tools[len(tools)-1].Description == "" { + desc := tool.Description + if len(desc) > 100 { + desc = desc[:100] + "..." + } + tools[len(tools)-1].Description = desc + } + } + + c.JSON(http.StatusOK, GetConfigResponse{ + OpenAI: h.config.OpenAI, + MCP: h.config.MCP, + Tools: tools, + Agent: h.config.Agent, + }) +} + +// UpdateConfigRequest 更新配置请求 +type UpdateConfigRequest struct { + OpenAI *config.OpenAIConfig `json:"openai,omitempty"` + MCP *config.MCPConfig `json:"mcp,omitempty"` + Tools []ToolEnableStatus `json:"tools,omitempty"` + Agent *config.AgentConfig `json:"agent,omitempty"` +} + +// ToolEnableStatus 工具启用状态 +type ToolEnableStatus struct { + Name string `json:"name"` + Enabled bool `json:"enabled"` +} + +// UpdateConfig 更新配置 +func (h *ConfigHandler) UpdateConfig(c *gin.Context) { + var req UpdateConfigRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求参数: " + err.Error()}) + return + } + + h.mu.Lock() + defer h.mu.Unlock() + + // 更新OpenAI配置 + if req.OpenAI != nil { + h.config.OpenAI = *req.OpenAI + h.logger.Info("更新OpenAI配置", + zap.String("base_url", h.config.OpenAI.BaseURL), + zap.String("model", h.config.OpenAI.Model), + ) + } + + // 更新MCP配置 + if req.MCP != nil { + h.config.MCP = *req.MCP + h.logger.Info("更新MCP配置", + zap.Bool("enabled", h.config.MCP.Enabled), + zap.String("host", h.config.MCP.Host), + zap.Int("port", h.config.MCP.Port), + ) + } + + // 更新Agent配置 + if req.Agent != nil { + h.config.Agent = *req.Agent + h.logger.Info("更新Agent配置", + zap.Int("max_iterations", h.config.Agent.MaxIterations), + ) + } + + // 更新工具启用状态 + if req.Tools != nil { + toolMap := make(map[string]bool) + for _, toolStatus := range req.Tools { + toolMap[toolStatus.Name] = toolStatus.Enabled + } + + // 更新配置中的工具状态 + for i := range h.config.Security.Tools { + if enabled, ok := toolMap[h.config.Security.Tools[i].Name]; ok { + h.config.Security.Tools[i].Enabled = enabled + h.logger.Info("更新工具启用状态", + zap.String("tool", h.config.Security.Tools[i].Name), + zap.Bool("enabled", enabled), + ) + } + } + } + + // 保存配置到文件 + if err := h.saveConfig(); err != nil { + h.logger.Error("保存配置失败", zap.Error(err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "保存配置失败: " + err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "配置已更新"}) +} + +// ApplyConfig 应用配置(重新加载并重启相关服务) +func (h *ConfigHandler) ApplyConfig(c *gin.Context) { + h.mu.Lock() + defer h.mu.Unlock() + + // 重新注册工具(根据新的启用状态) + h.logger.Info("重新注册工具") + + // 清空MCP服务器中的工具 + h.mcpServer.ClearTools() + + // 重新注册工具 + h.executor.RegisterTools(h.mcpServer) + + // 更新Agent的OpenAI配置 + if h.agent != nil { + h.agent.UpdateConfig(&h.config.OpenAI) + h.agent.UpdateMaxIterations(h.config.Agent.MaxIterations) + h.logger.Info("Agent配置已更新") + } + + h.logger.Info("配置已应用", + zap.Int("tools_count", len(h.config.Security.Tools)), + ) + + c.JSON(http.StatusOK, gin.H{ + "message": "配置已应用", + "tools_count": len(h.config.Security.Tools), + }) +} + +// saveConfig 保存配置到文件 +func (h *ConfigHandler) saveConfig() error { + // 读取现有配置文件 + data, err := os.ReadFile(h.configPath) + if err != nil { + return fmt.Errorf("读取配置文件失败: %w", err) + } + + // 解析现有配置 + var existingConfig map[string]interface{} + if err := yaml.Unmarshal(data, &existingConfig); err != nil { + return fmt.Errorf("解析配置文件失败: %w", err) + } + + // 更新配置值 + if existingConfig["openai"] == nil { + existingConfig["openai"] = make(map[string]interface{}) + } + openaiMap := existingConfig["openai"].(map[string]interface{}) + if h.config.OpenAI.APIKey != "" { + openaiMap["api_key"] = h.config.OpenAI.APIKey + } + if h.config.OpenAI.BaseURL != "" { + openaiMap["base_url"] = h.config.OpenAI.BaseURL + } + if h.config.OpenAI.Model != "" { + openaiMap["model"] = h.config.OpenAI.Model + } + + if existingConfig["mcp"] == nil { + existingConfig["mcp"] = make(map[string]interface{}) + } + mcpMap := existingConfig["mcp"].(map[string]interface{}) + mcpMap["enabled"] = h.config.MCP.Enabled + if h.config.MCP.Host != "" { + mcpMap["host"] = h.config.MCP.Host + } + if h.config.MCP.Port > 0 { + mcpMap["port"] = h.config.MCP.Port + } + + if h.config.Agent.MaxIterations > 0 { + if existingConfig["agent"] == nil { + existingConfig["agent"] = make(map[string]interface{}) + } + agentMap := existingConfig["agent"].(map[string]interface{}) + agentMap["max_iterations"] = h.config.Agent.MaxIterations + } + + // 更新工具配置文件中的enabled状态 + if h.config.Security.ToolsDir != "" { + configDir := filepath.Dir(h.configPath) + toolsDir := h.config.Security.ToolsDir + if !filepath.IsAbs(toolsDir) { + toolsDir = filepath.Join(configDir, toolsDir) + } + + for _, tool := range h.config.Security.Tools { + toolFile := filepath.Join(toolsDir, tool.Name+".yaml") + // 检查文件是否存在 + if _, err := os.Stat(toolFile); os.IsNotExist(err) { + // 尝试.yml扩展名 + toolFile = filepath.Join(toolsDir, tool.Name+".yml") + if _, err := os.Stat(toolFile); os.IsNotExist(err) { + h.logger.Warn("工具配置文件不存在", zap.String("tool", tool.Name)) + continue + } + } + + // 读取工具配置文件 + toolData, err := os.ReadFile(toolFile) + if err != nil { + h.logger.Warn("读取工具配置文件失败", zap.String("tool", tool.Name), zap.Error(err)) + continue + } + + // 解析工具配置 + var toolConfig map[string]interface{} + if err := yaml.Unmarshal(toolData, &toolConfig); err != nil { + h.logger.Warn("解析工具配置文件失败", zap.String("tool", tool.Name), zap.Error(err)) + continue + } + + // 更新enabled状态 + toolConfig["enabled"] = tool.Enabled + + // 保存工具配置文件 + updatedData, err := yaml.Marshal(toolConfig) + if err != nil { + h.logger.Warn("序列化工具配置失败", zap.String("tool", tool.Name), zap.Error(err)) + continue + } + + if err := os.WriteFile(toolFile, updatedData, 0644); err != nil { + h.logger.Warn("保存工具配置文件失败", zap.String("tool", tool.Name), zap.Error(err)) + continue + } + + h.logger.Info("更新工具配置", zap.String("tool", tool.Name), zap.Bool("enabled", tool.Enabled)) + } + } + + // 保存主配置文件 + updatedData, err := yaml.Marshal(existingConfig) + if err != nil { + return fmt.Errorf("序列化配置失败: %w", err) + } + + // 创建备份 + backupPath := h.configPath + ".backup" + if err := os.WriteFile(backupPath, data, 0644); err != nil { + h.logger.Warn("创建配置备份失败", zap.Error(err)) + } + + // 保存新配置 + if err := os.WriteFile(h.configPath, updatedData, 0644); err != nil { + return fmt.Errorf("保存配置文件失败: %w", err) + } + + h.logger.Info("配置已保存", zap.String("path", h.configPath)) + return nil +} + diff --git a/internal/mcp/server.go b/internal/mcp/server.go index ac668a0c..ec06934b 100644 --- a/internal/mcp/server.go +++ b/internal/mcp/server.go @@ -66,6 +66,26 @@ func (s *Server) RegisterTool(tool Tool, handler ToolHandler) { } } +// ClearTools 清空所有工具(用于重新加载配置) +func (s *Server) ClearTools() { + s.mu.Lock() + defer s.mu.Unlock() + + // 清空工具和工具定义 + s.tools = make(map[string]ToolHandler) + s.toolDefs = make(map[string]Tool) + + // 清空工具相关的资源(保留其他资源) + newResources := make(map[string]*Resource) + for uri, resource := range s.resources { + // 保留非工具资源 + if !strings.HasPrefix(uri, "tool://") { + newResources[uri] = resource + } + } + s.resources = newResources +} + // HandleHTTP 处理HTTP请求 func (s *Server) HandleHTTP(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { diff --git a/web/static/css/style.css b/web/static/css/style.css index 00aceef4..7d6500a4 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -84,6 +84,12 @@ header { margin: 0; } +.header-right { + display: flex; + align-items: center; + gap: 16px; +} + .header-subtitle { font-size: 0.875rem; color: rgba(255, 255, 255, 0.7); @@ -91,6 +97,28 @@ header { font-weight: 400; } +.settings-btn { + background: transparent; + border: 1px solid rgba(255, 255, 255, 0.2); + color: white; + padding: 8px; + border-radius: 6px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s; +} + +.settings-btn:hover { + background: rgba(255, 255, 255, 0.1); + border-color: rgba(255, 255, 255, 0.3); +} + +.settings-btn svg { + stroke: currentColor; +} + /* 侧边栏样式 */ .sidebar { width: 280px; @@ -755,6 +783,7 @@ header { box-shadow: var(--shadow-lg); border: 1px solid var(--border-color); animation: slideDown 0.3s ease-out; + overflow: hidden; } @keyframes slideDown { @@ -775,6 +804,8 @@ header { padding: 20px 24px; border-bottom: 1px solid var(--border-color); background: var(--bg-primary); + border-top-left-radius: 12px; + border-top-right-radius: 12px; } .modal-header h2 { @@ -1302,3 +1333,216 @@ header { font-size: 0.875rem; color: var(--error-color); } + +/* 设置模态框样式 */ +.settings-modal-content { + max-width: 800px; + max-height: 90vh; + display: flex; + flex-direction: column; +} + +.settings-body { + overflow-y: auto; + flex: 1; + padding: 24px; +} + +.settings-section { + margin-bottom: 32px; +} + +.settings-section:last-child { + margin-bottom: 0; +} + +.settings-section h3 { + font-size: 1.125rem; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 16px; + padding-bottom: 8px; + border-bottom: 2px solid var(--border-color); +} + +.settings-form { + display: flex; + flex-direction: column; + gap: 16px; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 6px; +} + +.form-group label { + font-size: 0.875rem; + font-weight: 500; + color: var(--text-primary); +} + +.form-group input { + padding: 10px 12px; + border: 1px solid var(--border-color); + border-radius: 6px; + font-size: 0.9375rem; + background: var(--bg-primary); + color: var(--text-primary); + transition: border-color 0.2s; +} + +.form-group input:focus { + outline: none; + border-color: var(--accent-color); + box-shadow: 0 0 0 3px rgba(0, 102, 255, 0.1); +} + +.form-group input.error { + border-color: var(--error-color); + box-shadow: 0 0 0 3px rgba(220, 53, 69, 0.1); +} + +.form-group input.error:focus { + border-color: var(--error-color); + box-shadow: 0 0 0 3px rgba(220, 53, 69, 0.2); +} + +.tools-controls { + display: flex; + flex-direction: column; + gap: 12px; +} + +.tools-actions { + display: flex; + gap: 8px; + align-items: center; +} + +.tools-actions button { + padding: 8px 16px; + border: 1px solid var(--border-color); + border-radius: 6px; + background: var(--bg-primary); + color: var(--text-primary); + font-size: 0.875rem; + cursor: pointer; + transition: all 0.2s; +} + +.tools-actions button:hover { + background: var(--bg-tertiary); + border-color: var(--accent-color); +} + +.tools-actions input { + flex: 1; + padding: 8px 12px; + border: 1px solid var(--border-color); + border-radius: 6px; + font-size: 0.875rem; + background: var(--bg-primary); + color: var(--text-primary); +} + +.tools-list { + max-height: 400px; + overflow-y: auto; + border: 1px solid var(--border-color); + border-radius: 6px; + background: var(--bg-primary); + padding: 8px; +} + +.tool-item { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 12px; + border-radius: 6px; + transition: background 0.2s; + cursor: pointer; +} + +.tool-item:hover { + background: var(--bg-tertiary); +} + +.tool-item input[type="checkbox"] { + width: 18px; + height: 18px; + cursor: pointer; + accent-color: var(--accent-color); +} + +.tool-item-info { + flex: 1; + min-width: 0; +} + +.tool-item-name { + font-weight: 500; + color: var(--text-primary); + font-size: 0.9375rem; + margin-bottom: 4px; +} + +.tool-item-desc { + font-size: 0.8125rem; + color: var(--text-secondary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.tool-item.hidden { + display: none; +} + +.modal-footer { + display: flex; + justify-content: flex-end; + gap: 12px; + padding: 16px 24px; + border-top: 1px solid var(--border-color); + flex-shrink: 0; + border-bottom-left-radius: 12px; + border-bottom-right-radius: 12px; +} + +.btn-primary { + padding: 10px 20px; + background: var(--accent-color); + color: white; + border: none; + border-radius: 6px; + font-size: 0.9375rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; +} + +.btn-primary:hover { + background: var(--accent-hover); + transform: translateY(-1px); + box-shadow: var(--shadow-sm); +} + +.btn-secondary { + padding: 10px 20px; + background: var(--bg-secondary); + color: var(--text-primary); + border: 1px solid var(--border-color); + border-radius: 6px; + font-size: 0.9375rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; +} + +.btn-secondary:hover { + background: var(--bg-tertiary); + border-color: var(--accent-color); +} diff --git a/web/static/js/app.js b/web/static/js/app.js index 7cbb5fa0..712f237a 100644 --- a/web/static/js/app.js +++ b/web/static/js/app.js @@ -1002,13 +1002,6 @@ function closeMCPDetail() { document.getElementById('mcp-detail-modal').style.display = 'none'; } -// 点击模态框外部关闭 -window.onclick = function(event) { - const modal = document.getElementById('mcp-detail-modal'); - if (event.target == modal) { - closeMCPDetail(); - } -} // 工具函数 function getStatusText(status) { @@ -1353,6 +1346,211 @@ async function cancelActiveTask(conversationId, button) { } } +// 设置相关功能 +let currentConfig = null; +let allTools = []; + +// 打开设置 +async function openSettings() { + const modal = document.getElementById('settings-modal'); + modal.style.display = 'block'; + + // 每次打开时重新加载最新配置 + await loadConfig(); + + // 清除之前的验证错误状态 + document.querySelectorAll('.form-group input').forEach(input => { + input.classList.remove('error'); + }); +} + +// 关闭设置 +function closeSettings() { + const modal = document.getElementById('settings-modal'); + modal.style.display = 'none'; +} + +// 点击模态框外部关闭 +window.onclick = function(event) { + const settingsModal = document.getElementById('settings-modal'); + const mcpModal = document.getElementById('mcp-detail-modal'); + + if (event.target == settingsModal) { + closeSettings(); + } + if (event.target == mcpModal) { + closeMCPDetail(); + } +} + +// 加载配置 +async function loadConfig() { + try { + const response = await fetch('/api/config'); + if (!response.ok) { + throw new Error('获取配置失败'); + } + + currentConfig = await response.json(); + + // 填充OpenAI配置 + document.getElementById('openai-api-key').value = currentConfig.openai.api_key || ''; + document.getElementById('openai-base-url').value = currentConfig.openai.base_url || ''; + document.getElementById('openai-model').value = currentConfig.openai.model || ''; + + // 填充Agent配置 + document.getElementById('agent-max-iterations').value = currentConfig.agent.max_iterations || 30; + + // 填充工具列表 + allTools = currentConfig.tools || []; + renderToolsList(); + } catch (error) { + console.error('加载配置失败:', error); + alert('加载配置失败: ' + error.message); + } +} + +// 渲染工具列表 +function renderToolsList() { + const toolsList = document.getElementById('tools-list'); + toolsList.innerHTML = ''; + + allTools.forEach(tool => { + const toolItem = document.createElement('div'); + toolItem.className = 'tool-item'; + toolItem.dataset.toolName = tool.name; // 保存原始工具名称 + toolItem.innerHTML = ` + +
安全测试平台
+安全测试平台
+ +