diff --git a/README.md b/README.md index a5457053..52140747 100644 --- a/README.md +++ b/README.md @@ -495,6 +495,7 @@ See [CHANGELOG.md](CHANGELOG.md) for detailed version history and all changes. ### Recent Highlights +- **2026-01-27** – OpenAPI documentation with interactive testing interface, supporting conversation management, message interaction, and result querying - **2026-01-15** – Skills system with 20+ predefined security testing skills - **2026-01-11** – Role-based testing with predefined security testing roles - **2026-01-08** – SSE transport mode support for external MCP servers diff --git a/README_CN.md b/README_CN.md index fb192564..09fc2461 100644 --- a/README_CN.md +++ b/README_CN.md @@ -494,6 +494,7 @@ CyberStrikeAI/ ### 近期亮点 +- **2026-01-27** – 新增 OpenAPI 文档,提供交互式测试界面,支持对话管理、消息交互和结果查询 - **2026-01-15** – 新增 Skills 技能系统,内置 20+ 预设安全测试技能 - **2026-01-11** – 新增角色化测试功能,支持预设安全测试角色 - **2026-01-08** – 新增 SSE 传输模式支持,外部 MCP 联邦支持三种模式 diff --git a/internal/app/app.go b/internal/app/app.go index 0fcfd47b..cbabb619 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -309,7 +309,6 @@ func New(cfg *config.Config, log *logger.Logger) (*App, error) { } monitorHandler := handler.NewMonitorHandler(mcpServer, executor, db, log.Logger) monitorHandler.SetExternalMCPManager(externalMCPMgr) // 设置外部MCP管理器,以便获取外部MCP执行记录 - conversationHandler := handler.NewConversationHandler(db, log.Logger) groupHandler := handler.NewGroupHandler(db, log.Logger) authHandler := handler.NewAuthHandler(authManager, cfg, configPath, log.Logger) attackChainHandler := handler.NewAttackChainHandler(db, &cfg.OpenAI, log.Logger) @@ -323,6 +322,10 @@ func New(cfg *config.Config, log *logger.Logger) (*App, error) { skillsHandler.SetDB(db) // 设置数据库连接以便获取调用统计 } + // 创建OpenAPI处理器 + conversationHandler := handler.NewConversationHandler(db, log.Logger) + openAPIHandler := handler.NewOpenAPIHandler(db, log.Logger, resultStorage, conversationHandler, agentHandler) + // 创建 App 实例(部分字段稍后填充) app := &App{ config: cfg, @@ -414,6 +417,7 @@ func New(cfg *config.Config, log *logger.Logger) (*App, error) { skillsHandler, mcpServer, authManager, + openAPIHandler, ) return app, nil @@ -476,6 +480,7 @@ func setupRoutes( skillsHandler *handler.SkillsHandler, mcpServer *mcp.Server, authManager *security.AuthManager, + openAPIHandler *handler.OpenAPIHandler, ) { // API路由 api := router.Group("/api") @@ -722,8 +727,19 @@ func setupRoutes( protected.POST("/mcp", func(c *gin.Context) { mcpServer.HandleHTTP(c.Writer, c.Request) }) + + // OpenAPI结果聚合端点(可选,用于获取对话的完整结果) + protected.GET("/conversations/:id/results", openAPIHandler.GetConversationResults) } + // OpenAPI规范(需要认证,避免暴露API结构信息) + protected.GET("/openapi/spec", openAPIHandler.GetOpenAPISpec) + + // API文档页面(公开访问,但需要登录后才能使用API) + router.GET("/api-docs", func(c *gin.Context) { + c.HTML(http.StatusOK, "api-docs.html", nil) + }) + // 静态文件 router.Static("/static", "./web/static") router.LoadHTMLGlob("web/templates/*") diff --git a/internal/database/conversation.go b/internal/database/conversation.go index 1fc99652..f6a9e0fe 100644 --- a/internal/database/conversation.go +++ b/internal/database/conversation.go @@ -223,12 +223,30 @@ func (db *DB) UpdateConversationTime(id string) error { return nil } -// DeleteConversation 删除对话 +// DeleteConversation 删除对话及其所有相关数据 +// 由于数据库外键约束设置了 ON DELETE CASCADE,删除对话时会自动删除: +// - messages(消息) +// - process_details(过程详情) +// - attack_chain_nodes(攻击链节点) +// - attack_chain_edges(攻击链边) +// - vulnerabilities(漏洞) +// - conversation_group_mappings(分组映射) +// 注意:knowledge_retrieval_logs 使用 ON DELETE SET NULL,记录会保留但 conversation_id 会被设为 NULL func (db *DB) DeleteConversation(id string) error { - _, err := db.Exec("DELETE FROM conversations WHERE id = ?", id) + // 显式删除知识检索日志(虽然外键是SET NULL,但为了彻底清理,我们手动删除) + _, err := db.Exec("DELETE FROM knowledge_retrieval_logs WHERE conversation_id = ?", id) + if err != nil { + db.logger.Warn("删除知识检索日志失败", zap.String("conversationId", id), zap.Error(err)) + // 不返回错误,继续删除对话 + } + + // 删除对话(外键CASCADE会自动删除其他相关数据) + _, err = db.Exec("DELETE FROM conversations WHERE id = ?", id) if err != nil { return fmt.Errorf("删除对话失败: %w", err) } + + db.logger.Info("对话及其所有相关数据已删除", zap.String("conversationId", id)) return nil } diff --git a/internal/handler/agent.go b/internal/handler/agent.go index 785676e3..3a0fe79a 100644 --- a/internal/handler/agent.go +++ b/internal/handler/agent.go @@ -147,6 +147,14 @@ func (h *AgentHandler) AgentLoop(c *gin.Context) { return } conversationID = conv.ID + } else { + // 验证对话是否存在 + _, err := h.db.GetConversation(conversationID) + if err != nil { + h.logger.Error("对话不存在", zap.String("conversationId", conversationID), zap.Error(err)) + c.JSON(http.StatusNotFound, gin.H{"error": "对话不存在"}) + return + } } // 优先尝试从保存的ReAct数据恢复历史上下文 @@ -203,6 +211,8 @@ func (h *AgentHandler) AgentLoop(c *gin.Context) { _, err = h.db.AddMessage(conversationID, "user", req.Message, nil) if err != nil { h.logger.Error("保存用户消息失败", zap.Error(err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "保存用户消息失败: " + err.Error()}) + return } // 执行Agent Loop,传入历史消息和对话ID(使用包含角色提示词的finalMessage和角色工具列表) @@ -228,6 +238,8 @@ func (h *AgentHandler) AgentLoop(c *gin.Context) { _, err = h.db.AddMessage(conversationID, "assistant", result.Response, result.MCPExecutionIDs) if err != nil { h.logger.Error("保存助手消息失败", zap.Error(err)) + // 即使保存失败,也返回响应,但记录错误 + // 因为AI已经生成了回复,用户应该能看到 } // 保存最后一轮ReAct的输入和输出 @@ -479,12 +491,19 @@ func (h *AgentHandler) AgentLoopStream(c *gin.Context) { return } conversationID = conv.ID + sendEvent("conversation", "会话已创建", map[string]interface{}{ + "conversationId": conversationID, + }) + } else { + // 验证对话是否存在 + _, err := h.db.GetConversation(conversationID) + if err != nil { + h.logger.Error("对话不存在", zap.String("conversationId", conversationID), zap.Error(err)) + sendEvent("error", "对话不存在", nil) + return + } } - sendEvent("conversation", "会话已创建", map[string]interface{}{ - "conversationId": conversationID, - }) - // 优先尝试从保存的ReAct数据恢复历史上下文 agentHistoryMessages, err := h.loadHistoryFromReActData(conversationID) if err != nil { diff --git a/internal/handler/openapi.go b/internal/handler/openapi.go new file mode 100644 index 00000000..48ab70f9 --- /dev/null +++ b/internal/handler/openapi.go @@ -0,0 +1,647 @@ +package handler + +import ( + "net/http" + "time" + + "cyberstrike-ai/internal/database" + "cyberstrike-ai/internal/storage" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +// OpenAPIHandler OpenAPI处理器 +type OpenAPIHandler struct { + db *database.DB + logger *zap.Logger + resultStorage storage.ResultStorage + conversationHdlr *ConversationHandler + agentHdlr *AgentHandler +} + +// NewOpenAPIHandler 创建新的OpenAPI处理器 +func NewOpenAPIHandler(db *database.DB, logger *zap.Logger, resultStorage storage.ResultStorage, conversationHdlr *ConversationHandler, agentHdlr *AgentHandler) *OpenAPIHandler { + return &OpenAPIHandler{ + db: db, + logger: logger, + resultStorage: resultStorage, + conversationHdlr: conversationHdlr, + agentHdlr: agentHdlr, + } +} + +// GetOpenAPISpec 获取OpenAPI规范 +func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) { + host := c.Request.Host + scheme := "http" + if c.Request.TLS != nil { + scheme = "https" + } + + spec := map[string]interface{}{ + "openapi": "3.0.0", + "info": map[string]interface{}{ + "title": "CyberStrikeAI API", + "description": "AI驱动的自动化安全测试平台API文档", + "version": "1.0.0", + "contact": map[string]interface{}{ + "name": "CyberStrikeAI", + }, + }, + "servers": []map[string]interface{}{ + { + "url": scheme + "://" + host, + "description": "当前服务器", + }, + }, + "components": map[string]interface{}{ + "securitySchemes": map[string]interface{}{ + "bearerAuth": map[string]interface{}{ + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT", + "description": "使用Bearer Token进行认证。Token通过 /api/auth/login 接口获取。", + }, + }, + "schemas": map[string]interface{}{ + "CreateConversationRequest": map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "title": map[string]interface{}{ + "type": "string", + "description": "对话标题", + "example": "Web应用安全测试", + }, + }, + }, + "Conversation": map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "id": map[string]interface{}{ + "type": "string", + "description": "对话ID", + "example": "550e8400-e29b-41d4-a716-446655440000", + }, + "title": map[string]interface{}{ + "type": "string", + "description": "对话标题", + "example": "Web应用安全测试", + }, + "createdAt": map[string]interface{}{ + "type": "string", + "format": "date-time", + "description": "创建时间", + }, + "updatedAt": map[string]interface{}{ + "type": "string", + "format": "date-time", + "description": "更新时间", + }, + }, + }, + "ConversationDetail": map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "id": map[string]interface{}{ + "type": "string", + "description": "对话ID", + }, + "title": map[string]interface{}{ + "type": "string", + "description": "对话标题", + }, + "status": map[string]interface{}{ + "type": "string", + "description": "对话状态:active(进行中)、completed(已完成)、failed(失败)", + "enum": []string{"active", "completed", "failed"}, + }, + "createdAt": map[string]interface{}{ + "type": "string", + "format": "date-time", + "description": "创建时间", + }, + "updatedAt": map[string]interface{}{ + "type": "string", + "format": "date-time", + "description": "更新时间", + }, + "messages": map[string]interface{}{ + "type": "array", + "description": "消息列表", + "items": map[string]interface{}{ + "$ref": "#/components/schemas/Message", + }, + }, + "messageCount": map[string]interface{}{ + "type": "integer", + "description": "消息数量", + }, + }, + }, + "Message": map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "id": map[string]interface{}{ + "type": "string", + "description": "消息ID", + }, + "conversationId": map[string]interface{}{ + "type": "string", + "description": "对话ID", + }, + "role": map[string]interface{}{ + "type": "string", + "description": "消息角色:user(用户)、assistant(助手)", + "enum": []string{"user", "assistant"}, + }, + "content": map[string]interface{}{ + "type": "string", + "description": "消息内容", + }, + "createdAt": map[string]interface{}{ + "type": "string", + "format": "date-time", + "description": "创建时间", + }, + }, + }, + "ConversationResults": map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "conversationId": map[string]interface{}{ + "type": "string", + "description": "对话ID", + }, + "messages": map[string]interface{}{ + "type": "array", + "description": "消息列表", + "items": map[string]interface{}{ + "$ref": "#/components/schemas/Message", + }, + }, + "vulnerabilities": map[string]interface{}{ + "type": "array", + "description": "发现的漏洞列表", + "items": map[string]interface{}{ + "$ref": "#/components/schemas/Vulnerability", + }, + }, + "executionResults": map[string]interface{}{ + "type": "array", + "description": "执行结果列表", + "items": map[string]interface{}{ + "$ref": "#/components/schemas/ExecutionResult", + }, + }, + }, + }, + "Vulnerability": map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "id": map[string]interface{}{ + "type": "string", + "description": "漏洞ID", + }, + "title": map[string]interface{}{ + "type": "string", + "description": "漏洞标题", + }, + "description": map[string]interface{}{ + "type": "string", + "description": "漏洞描述", + }, + "severity": map[string]interface{}{ + "type": "string", + "description": "严重程度", + "enum": []string{"critical", "high", "medium", "low", "info"}, + }, + "status": map[string]interface{}{ + "type": "string", + "description": "状态", + "enum": []string{"open", "closed", "fixed"}, + }, + "target": map[string]interface{}{ + "type": "string", + "description": "受影响的目标", + }, + }, + }, + "ExecutionResult": map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "id": map[string]interface{}{ + "type": "string", + "description": "执行ID", + }, + "toolName": map[string]interface{}{ + "type": "string", + "description": "工具名称", + }, + "status": map[string]interface{}{ + "type": "string", + "description": "执行状态", + "enum": []string{"success", "failed", "running"}, + }, + "result": map[string]interface{}{ + "type": "string", + "description": "执行结果", + }, + "createdAt": map[string]interface{}{ + "type": "string", + "format": "date-time", + "description": "创建时间", + }, + }, + }, + "Error": map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "error": map[string]interface{}{ + "type": "string", + "description": "错误信息", + }, + }, + }, + }, + }, + "security": []map[string]interface{}{ + { + "bearerAuth": []string{}, + }, + }, + "paths": map[string]interface{}{ + "/api/conversations": map[string]interface{}{ + "post": map[string]interface{}{ + "tags": []string{"对话管理"}, + "summary": "创建对话", + "description": "创建一个新的安全测试对话。\n\n**重要说明**:\n- ✅ 创建的对话会**立即保存到数据库**\n- ✅ 前端页面会**自动刷新**显示新对话\n- ✅ 与前端创建的对话**完全一致**\n\n**创建对话的两种方式**:\n\n**方式1(推荐):** 直接使用 `/api/agent-loop` 发送消息,**不提供** `conversationId` 参数,系统会自动创建新对话并发送消息。这是最简单的方式,一步完成创建和发送。\n\n**方式2:** 先调用此端点创建空对话,然后使用返回的 `conversationId` 调用 `/api/agent-loop` 发送消息。适用于需要先创建对话,稍后再发送消息的场景。\n\n**示例**:\n```json\n{\n \"title\": \"Web应用安全测试\"\n}\n```", + "operationId": "createConversation", + "requestBody": map[string]interface{}{ + "required": true, + "content": map[string]interface{}{ + "application/json": map[string]interface{}{ + "schema": map[string]interface{}{ + "$ref": "#/components/schemas/CreateConversationRequest", + }, + }, + }, + }, + "responses": map[string]interface{}{ + "200": map[string]interface{}{ + "description": "对话创建成功", + "content": map[string]interface{}{ + "application/json": map[string]interface{}{ + "schema": map[string]interface{}{ + "$ref": "#/components/schemas/Conversation", + }, + }, + }, + }, + "400": map[string]interface{}{ + "description": "请求参数错误", + }, + "401": map[string]interface{}{ + "description": "未授权,需要有效的Token", + }, + "500": map[string]interface{}{ + "description": "服务器内部错误", + }, + }, + }, + "get": map[string]interface{}{ + "tags": []string{"对话管理"}, + "summary": "列出对话", + "description": "获取对话列表,支持分页和搜索", + "operationId": "listConversations", + "parameters": []map[string]interface{}{ + { + "name": "limit", + "in": "query", + "required": false, + "description": "返回数量限制", + "schema": map[string]interface{}{ + "type": "integer", + "default": 50, + "minimum": 1, + "maximum": 100, + }, + }, + { + "name": "offset", + "in": "query", + "required": false, + "description": "偏移量", + "schema": map[string]interface{}{ + "type": "integer", + "default": 0, + "minimum": 0, + }, + }, + { + "name": "search", + "in": "query", + "required": false, + "description": "搜索关键词", + "schema": map[string]interface{}{ + "type": "string", + }, + }, + }, + "responses": map[string]interface{}{ + "200": map[string]interface{}{ + "description": "获取成功", + "content": map[string]interface{}{ + "application/json": map[string]interface{}{ + "schema": map[string]interface{}{ + "type": "array", + "items": map[string]interface{}{ + "$ref": "#/components/schemas/Conversation", + }, + }, + }, + }, + }, + "401": map[string]interface{}{ + "description": "未授权,需要有效的Token", + }, + }, + }, + }, + "/api/conversations/{id}": map[string]interface{}{ + "get": map[string]interface{}{ + "tags": []string{"对话管理"}, + "summary": "查看对话详情", + "description": "获取指定对话的详细信息,包括对话信息和消息列表", + "operationId": "getConversation", + "parameters": []map[string]interface{}{ + { + "name": "id", + "in": "path", + "required": true, + "description": "对话ID", + "schema": map[string]interface{}{ + "type": "string", + }, + }, + }, + "responses": map[string]interface{}{ + "200": map[string]interface{}{ + "description": "获取成功", + "content": map[string]interface{}{ + "application/json": map[string]interface{}{ + "schema": map[string]interface{}{ + "$ref": "#/components/schemas/ConversationDetail", + }, + }, + }, + }, + "404": map[string]interface{}{ + "description": "对话不存在", + }, + "401": map[string]interface{}{ + "description": "未授权,需要有效的Token", + }, + }, + }, + "delete": map[string]interface{}{ + "tags": []string{"对话管理"}, + "summary": "删除对话", + "description": "删除指定的对话及其所有相关数据(消息、漏洞等)。**此操作不可恢复**。", + "operationId": "deleteConversation", + "parameters": []map[string]interface{}{ + { + "name": "id", + "in": "path", + "required": true, + "description": "对话ID", + "schema": map[string]interface{}{ + "type": "string", + }, + }, + }, + "responses": map[string]interface{}{ + "200": map[string]interface{}{ + "description": "删除成功", + "content": map[string]interface{}{ + "application/json": map[string]interface{}{ + "schema": map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "message": map[string]interface{}{ + "type": "string", + "description": "成功消息", + "example": "删除成功", + }, + }, + }, + }, + }, + }, + "404": map[string]interface{}{ + "description": "对话不存在", + }, + "401": map[string]interface{}{ + "description": "未授权,需要有效的Token", + }, + "500": map[string]interface{}{ + "description": "服务器内部错误", + }, + }, + }, + }, + "/api/conversations/{id}/results": map[string]interface{}{ + "get": map[string]interface{}{ + "tags": []string{"结果查询"}, + "summary": "获取对话结果", + "description": "获取指定对话的执行结果,包括消息、漏洞信息和执行结果", + "operationId": "getConversationResults", + "parameters": []map[string]interface{}{ + { + "name": "id", + "in": "path", + "required": true, + "description": "对话ID", + "schema": map[string]interface{}{ + "type": "string", + }, + }, + }, + "responses": map[string]interface{}{ + "200": map[string]interface{}{ + "description": "获取成功", + "content": map[string]interface{}{ + "application/json": map[string]interface{}{ + "schema": map[string]interface{}{ + "$ref": "#/components/schemas/ConversationResults", + }, + }, + }, + }, + "404": map[string]interface{}{ + "description": "对话不存在或结果不存在", + }, + "401": map[string]interface{}{ + "description": "未授权,需要有效的Token", + }, + }, + }, + }, + "/api/agent-loop": map[string]interface{}{ + "post": map[string]interface{}{ + "tags": []string{"对话交互"}, + "summary": "发送消息并获取AI回复(核心端点)", + "description": "向AI发送消息并获取回复。**这是与AI交互的核心端点**,与前端聊天功能完全一致。\n\n**重要说明**:\n- ✅ 通过此API创建/发送的消息会**立即保存到数据库**\n- ✅ 前端页面会**自动刷新**显示新创建的对话和消息\n- ✅ 所有操作都有**完整的交互痕迹**,就像在前端操作一样\n- ✅ 支持角色配置,可以指定使用哪个测试角色\n\n**推荐使用流程**:\n\n1. **先创建对话**:调用 `POST /api/conversations` 创建新对话,获取 `conversationId`\n2. **再发送消息**:使用返回的 `conversationId` 调用此端点发送消息\n\n**使用示例**:\n\n**步骤1 - 创建对话:**\n```json\nPOST /api/conversations\n{\n \"title\": \"Web应用安全测试\"\n}\n```\n\n**步骤2 - 发送消息:**\n```json\nPOST /api/agent-loop\n{\n \"conversationId\": \"返回的对话ID\",\n \"message\": \"扫描 http://example.com 的SQL注入漏洞\",\n \"role\": \"渗透测试\"\n}\n```\n\n**其他方式**:\n\n如果不提供 `conversationId`,系统会自动创建新对话并发送消息。但**推荐先创建对话**,这样可以更好地管理对话列表。\n\n**响应**:返回AI的回复、对话ID和MCP执行ID列表。前端会自动刷新显示新消息。", + "operationId": "sendMessage", + "requestBody": map[string]interface{}{ + "required": true, + "content": map[string]interface{}{ + "application/json": map[string]interface{}{ + "schema": map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "message": map[string]interface{}{ + "type": "string", + "description": "要发送的消息(必需)", + "example": "扫描 http://example.com 的SQL注入漏洞", + }, + "conversationId": map[string]interface{}{ + "type": "string", + "description": "对话ID(可选)。\n- **不提供**:自动创建新对话并发送消息(推荐)\n- **提供**:消息会添加到指定对话中(对话必须存在)", + "example": "550e8400-e29b-41d4-a716-446655440000", + }, + "role": map[string]interface{}{ + "type": "string", + "description": "角色名称(可选),如:默认、渗透测试、Web应用扫描等", + "example": "默认", + }, + }, + "required": []string{"message"}, + }, + }, + }, + }, + "responses": map[string]interface{}{ + "200": map[string]interface{}{ + "description": "消息发送成功,返回AI回复", + "content": map[string]interface{}{ + "application/json": map[string]interface{}{ + "schema": map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "response": map[string]interface{}{ + "type": "string", + "description": "AI的回复内容", + }, + "conversationId": map[string]interface{}{ + "type": "string", + "description": "对话ID", + }, + "mcpExecutionIds": map[string]interface{}{ + "type": "array", + "description": "MCP执行ID列表", + "items": map[string]interface{}{ + "type": "string", + }, + }, + "time": map[string]interface{}{ + "type": "string", + "format": "date-time", + "description": "响应时间", + }, + }, + }, + }, + }, + }, + "400": map[string]interface{}{ + "description": "请求参数错误", + }, + "401": map[string]interface{}{ + "description": "未授权,需要有效的Token", + }, + "500": map[string]interface{}{ + "description": "服务器内部错误", + }, + }, + }, + }, + }, + } + + c.JSON(http.StatusOK, spec) +} + + +// GetConversationResults 获取对话结果(OpenAPI端点) +// 注意:创建对话和获取对话详情直接使用标准的 /api/conversations 端点 +// 这个端点只是为了提供结果聚合功能 +func (h *OpenAPIHandler) GetConversationResults(c *gin.Context) { + conversationID := c.Param("id") + + // 验证对话是否存在 + conv, err := h.db.GetConversation(conversationID) + if err != nil { + h.logger.Error("获取对话失败", zap.Error(err)) + c.JSON(http.StatusNotFound, gin.H{"error": "对话不存在"}) + return + } + + // 获取消息列表 + messages, err := h.db.GetMessages(conversationID) + if err != nil { + h.logger.Error("获取消息失败", zap.Error(err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // 获取漏洞列表 + vulnList, err := h.db.ListVulnerabilities(1000, 0, "", conversationID, "", "") + if err != nil { + h.logger.Warn("获取漏洞列表失败", zap.Error(err)) + vulnList = []*database.Vulnerability{} + } + vulnerabilities := make([]database.Vulnerability, len(vulnList)) + for i, v := range vulnList { + vulnerabilities[i] = *v + } + + // 获取执行结果(从MCP执行记录中获取) + executionResults := []map[string]interface{}{} + for _, msg := range messages { + if len(msg.MCPExecutionIDs) > 0 { + for _, execID := range msg.MCPExecutionIDs { + // 尝试从结果存储中获取执行结果 + if h.resultStorage != nil { + result, err := h.resultStorage.GetResult(execID) + if err == nil && result != "" { + // 获取元数据以获取工具名称和创建时间 + metadata, err := h.resultStorage.GetResultMetadata(execID) + toolName := "unknown" + createdAt := time.Now() + if err == nil && metadata != nil { + toolName = metadata.ToolName + createdAt = metadata.CreatedAt + } + executionResults = append(executionResults, map[string]interface{}{ + "id": execID, + "toolName": toolName, + "status": "success", + "result": result, + "createdAt": createdAt.Format(time.RFC3339), + }) + } + } + } + } + } + + response := map[string]interface{}{ + "conversationId": conv.ID, + "messages": messages, + "vulnerabilities": vulnerabilities, + "executionResults": executionResults, + } + + c.JSON(http.StatusOK, response) +} diff --git a/web/static/css/style.css b/web/static/css/style.css index 032e4f2a..ca5efb6f 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -538,6 +538,28 @@ header { transform: translateY(-1px); } +.openapi-doc-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + border-radius: 6px; + border: 1px solid var(--border-color); + background: var(--bg-primary); + color: var(--text-primary); + cursor: pointer; + font-size: 0.8125rem; + font-weight: 400; + transition: all 0.2s ease; +} + +.openapi-doc-btn:hover { + background: var(--bg-tertiary); + border-color: var(--accent-color); + color: var(--accent-color); + transform: translateY(-1px); +} + .monitor-btn { color: var(--accent-color); border-color: var(--border-color); @@ -1619,35 +1641,76 @@ header { .chat-input-container .send-btn { display: flex; align-items: center; + justify-content: center; gap: 6px; - padding: 10px 18px; + padding: 10px 20px; height: 40px; - background: var(--accent-color); + background: linear-gradient(135deg, var(--accent-color) 0%, #0052cc 100%); color: white; border: none; border-radius: 12px; cursor: pointer; font-size: 0.9375rem; - font-weight: 500; - transition: all 0.2s; + font-weight: 600; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); white-space: nowrap; flex-shrink: 0; box-sizing: border-box; + position: relative; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 102, 255, 0.2), 0 1px 3px rgba(0, 0, 0, 0.1); +} + +/* 按钮光效动画 */ +.chat-input-container .send-btn::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); + transition: left 0.5s; +} + +.chat-input-container .send-btn:hover::before { + left: 100%; } .chat-input-container .send-btn:hover { - background: var(--accent-hover); - transform: translateY(-1px); - box-shadow: 0 2px 8px rgba(0, 102, 255, 0.25); + background: linear-gradient(135deg, #0052cc 0%, var(--accent-color) 100%); + transform: translateY(-2px) scale(1.02); + box-shadow: 0 4px 12px rgba(0, 102, 255, 0.35), 0 2px 6px rgba(0, 0, 0, 0.15); } .chat-input-container .send-btn:active { - transform: translateY(0); - box-shadow: 0 1px 4px rgba(0, 102, 255, 0.2); + transform: translateY(0) scale(0.98); + box-shadow: 0 2px 6px rgba(0, 102, 255, 0.25), 0 1px 3px rgba(0, 0, 0, 0.1); + transition: all 0.1s; +} + +.chat-input-container .send-btn:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; } .chat-input-container .send-btn svg { flex-shrink: 0; + transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.chat-input-container .send-btn:hover svg { + transform: translateX(2px); +} + +.chat-input-container .send-btn:active svg { + transform: translateX(4px); +} + +.chat-input-container .send-btn span { + position: relative; + z-index: 1; } .mention-suggestions { diff --git a/web/static/js/api-docs.js b/web/static/js/api-docs.js new file mode 100644 index 00000000..d7a5751b --- /dev/null +++ b/web/static/js/api-docs.js @@ -0,0 +1,749 @@ +// API文档页面JavaScript + +let apiSpec = null; +let currentToken = null; + +// 初始化 +document.addEventListener('DOMContentLoaded', async () => { + await loadToken(); + await loadAPISpec(); + if (apiSpec) { + renderAPIDocs(); + } +}); + +// 加载token +async function loadToken() { + try { + const authData = localStorage.getItem('cyberstrike-auth'); + if (authData) { + const parsed = JSON.parse(authData); + if (parsed && parsed.token) { + const expiry = parsed.expiresAt ? new Date(parsed.expiresAt) : null; + if (!expiry || expiry.getTime() > Date.now()) { + currentToken = parsed.token; + return; + } + } + } + currentToken = localStorage.getItem('swagger_auth_token'); + } catch (e) { + console.error('加载token失败:', e); + } +} + +// 加载OpenAPI规范 +async function loadAPISpec() { + try { + let url = '/api/openapi/spec'; + if (currentToken) { + url += '?token=' + encodeURIComponent(currentToken); + } + + const response = await fetch(url); + if (!response.ok) { + if (response.status === 401) { + showError('需要登录才能查看API文档。请先在前端页面登录,然后刷新此页面。'); + return; + } + throw new Error('加载API规范失败: ' + response.status); + } + + apiSpec = await response.json(); + } catch (error) { + console.error('加载API规范失败:', error); + showError('加载API文档失败: ' + error.message); + } +} + +// 显示错误 +function showError(message) { + const main = document.getElementById('api-docs-main'); + main.innerHTML = ` +
+ `; +} + +// 渲染API文档 +function renderAPIDocs() { + if (!apiSpec || !apiSpec.paths) { + showError('API规范格式错误'); + return; + } + + // 显示认证说明 + renderAuthInfo(); + + // 渲染侧边栏分组 + renderSidebar(); + + // 渲染API端点 + renderEndpoints(); +} + +// 渲染认证说明 +function renderAuthInfo() { + const authSection = document.getElementById('auth-info-section'); + if (!authSection) return; + + // 显示认证说明部分 + authSection.style.display = 'block'; + + // 检查是否有token + const tokenStatus = document.getElementById('token-status'); + if (currentToken && tokenStatus) { + tokenStatus.style.display = 'block'; + } else if (tokenStatus) { + // 如果没有token,显示提示 + tokenStatus.style.display = 'block'; + tokenStatus.style.background = 'rgba(255, 152, 0, 0.1)'; + tokenStatus.style.borderLeftColor = '#ff9800'; + tokenStatus.innerHTML = '⚠ 未检测到 Token - 请先在前端页面登录,然后刷新此页面。测试接口时需要在请求头中添加 Authorization: Bearer token
'; + } +} + +// 渲染侧边栏 +function renderSidebar() { + const groups = new Set(); + Object.keys(apiSpec.paths).forEach(path => { + Object.keys(apiSpec.paths[path]).forEach(method => { + const endpoint = apiSpec.paths[path][method]; + if (endpoint.tags && endpoint.tags.length > 0) { + endpoint.tags.forEach(tag => groups.add(tag)); + } + }); + }); + + const groupList = document.getElementById('api-group-list'); + const allGroups = Array.from(groups).sort(); + + allGroups.forEach(group => { + const li = document.createElement('li'); + li.className = 'api-group-item'; + li.innerHTML = `${group}`; + groupList.appendChild(li); + }); + + // 绑定点击事件 + groupList.querySelectorAll('.api-group-link').forEach(link => { + link.addEventListener('click', (e) => { + e.preventDefault(); + groupList.querySelectorAll('.api-group-link').forEach(l => l.classList.remove('active')); + link.classList.add('active'); + const group = link.dataset.group; + renderEndpoints(group === 'all' ? null : group); + }); + }); +} + +// 渲染API端点 +function renderEndpoints(filterGroup = null) { + const main = document.getElementById('api-docs-main'); + main.innerHTML = ''; + + const endpoints = []; + Object.keys(apiSpec.paths).forEach(path => { + Object.keys(apiSpec.paths[path]).forEach(method => { + const endpoint = apiSpec.paths[path][method]; + const tags = endpoint.tags || []; + if (!filterGroup || filterGroup === 'all' || tags.includes(filterGroup)) { + endpoints.push({ + path, + method, + ...endpoint + }); + } + }); + }); + + // 按分组排序 + endpoints.sort((a, b) => { + const tagA = a.tags && a.tags.length > 0 ? a.tags[0] : ''; + const tagB = b.tags && b.tags.length > 0 ? b.tags[0] : ''; + if (tagA !== tagB) return tagA.localeCompare(tagB); + return a.path.localeCompare(b.path); + }); + + if (endpoints.length === 0) { + main.innerHTML = '该分组下没有API端点
| 参数名 | +类型 | +描述 | +必需 | +
|---|
${escapeHtml(String(prop.example))}` : '-'}| 参数名 | +类型 | +描述 | +必需 | +示例 | +
|---|
${escapeHtml(example)}
+ ${escapeHtml(example)}
+ CyberStrikeAI 平台 API 接口文档,支持在线测试
+正在加载 API 文档
+AI 驱动的自动化安全测试平台