From 6815e038422cbe6e1470a80ed2014750c5651ecc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Tue, 27 Jan 2026 20:56:20 +0800 Subject: [PATCH] Add files via upload --- README.md | 1 + README_CN.md | 1 + internal/app/app.go | 18 +- internal/database/conversation.go | 22 +- internal/handler/agent.go | 27 +- internal/handler/openapi.go | 647 ++++++++++++++++++++++++++ web/static/css/style.css | 81 +++- web/static/js/api-docs.js | 749 ++++++++++++++++++++++++++++++ web/static/js/chat.js | 30 ++ web/templates/api-docs.html | 578 +++++++++++++++++++++++ web/templates/index.html | 3 + 11 files changed, 2141 insertions(+), 16 deletions(-) create mode 100644 internal/handler/openapi.go create mode 100644 web/static/js/api-docs.js create mode 100644 web/templates/api-docs.html 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 = ` +
+ + + + + +

加载失败

+

${message}

+
+ 返回首页登录 +
+
+ `; +} + +// 渲染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

该分组下没有API端点

'; + return; + } + + endpoints.forEach(endpoint => { + main.appendChild(createEndpointCard(endpoint)); + }); +} + +// 创建API端点卡片 +function createEndpointCard(endpoint) { + const card = document.createElement('div'); + card.className = 'api-endpoint'; + + const methodClass = endpoint.method.toLowerCase(); + const tags = endpoint.tags || []; + const tagHtml = tags.map(tag => `${tag}`).join(''); + + card.innerHTML = ` +
+
+ ${endpoint.method.toUpperCase()} + ${endpoint.path} + ${tagHtml} +
+
+
+
+
描述
+
${endpoint.summary || endpoint.description || '无描述'}
+
+ + ${renderParameters(endpoint)} + ${renderRequestBody(endpoint)} + ${renderResponses(endpoint)} + ${renderTestSection(endpoint)} +
+ `; + + return card; +} + +// 渲染参数 +function renderParameters(endpoint) { + const params = endpoint.parameters || []; + if (params.length === 0) return ''; + + const rows = params.map(param => { + const required = param.required ? '必需' : '可选'; + return ` + + ${param.name} + ${param.schema?.type || 'string'} + ${param.description || '-'} + ${required} + + `; + }).join(''); + + return ` +
+
参数
+ + + + + + + + + + + ${rows} + +
参数名类型描述必需
+
+ `; +} + +// 渲染请求体 +function renderRequestBody(endpoint) { + if (!endpoint.requestBody) return ''; + + const content = endpoint.requestBody.content || {}; + let schema = content['application/json']?.schema || {}; + + // 处理 $ref 引用 + if (schema.$ref) { + const refPath = schema.$ref.split('/'); + const refName = refPath[refPath.length - 1]; + if (apiSpec.components && apiSpec.components.schemas && apiSpec.components.schemas[refName]) { + schema = apiSpec.components.schemas[refName]; + } + } + + // 渲染参数表格 + let paramsTable = ''; + if (schema.properties) { + const requiredFields = schema.required || []; + const rows = Object.keys(schema.properties).map(key => { + const prop = schema.properties[key]; + const required = requiredFields.includes(key) + ? '必需' + : '可选'; + + // 处理嵌套类型 + let typeDisplay = prop.type || 'object'; + if (prop.type === 'array' && prop.items) { + typeDisplay = `array[${prop.items.type || 'object'}]`; + } else if (prop.$ref) { + const refPath = prop.$ref.split('/'); + typeDisplay = refPath[refPath.length - 1]; + } + + // 处理枚举 + if (prop.enum) { + typeDisplay += ` (${prop.enum.join(', ')})`; + } + + return ` + + ${escapeHtml(key)} + ${escapeHtml(typeDisplay)} + ${prop.description ? escapeHtml(prop.description) : '-'} + ${required} + ${prop.example !== undefined ? `${escapeHtml(String(prop.example))}` : '-'} + + `; + }).join(''); + + if (rows) { + paramsTable = ` + + + + + + + + + + + + ${rows} + +
参数名类型描述必需示例
+ `; + } + } + + // 生成示例JSON + let example = ''; + if (schema.example) { + example = JSON.stringify(schema.example, null, 2); + } else if (schema.properties) { + const exampleObj = {}; + Object.keys(schema.properties).forEach(key => { + const prop = schema.properties[key]; + if (prop.example !== undefined) { + exampleObj[key] = prop.example; + } else { + // 根据类型生成默认示例 + if (prop.type === 'string') { + exampleObj[key] = prop.description || 'string'; + } else if (prop.type === 'number') { + exampleObj[key] = 0; + } else if (prop.type === 'boolean') { + exampleObj[key] = false; + } else if (prop.type === 'array') { + exampleObj[key] = []; + } else { + exampleObj[key] = null; + } + } + }); + example = JSON.stringify(exampleObj, null, 2); + } + + return ` +
+
请求体
+ ${endpoint.requestBody.description ? `
${endpoint.requestBody.description}
` : ''} + ${paramsTable} + ${example ? ` +
+
示例JSON:
+
+
${escapeHtml(example)}
+
+
+ ` : ''} +
+ `; +} + +// 渲染响应 +function renderResponses(endpoint) { + const responses = endpoint.responses || {}; + const responseItems = Object.keys(responses).map(status => { + const response = responses[status]; + const schema = response.content?.['application/json']?.schema || {}; + let example = ''; + if (schema.example) { + example = JSON.stringify(schema.example, null, 2); + } + + return ` +
+ ${status} + ${response.description ? `${response.description}` : ''} + ${example ? ` +
+
${escapeHtml(example)}
+
+ ` : ''} +
+ `; + }).join(''); + + if (!responseItems) return ''; + + return ` +
+
响应
+ ${responseItems} +
+ `; +} + +// 渲染测试区域 +function renderTestSection(endpoint) { + const method = endpoint.method.toUpperCase(); + const path = endpoint.path; + const hasBody = endpoint.requestBody && ['POST', 'PUT', 'PATCH'].includes(method); + + let bodyInput = ''; + if (hasBody) { + const schema = endpoint.requestBody.content?.['application/json']?.schema || {}; + let defaultBody = ''; + if (schema.example) { + defaultBody = JSON.stringify(schema.example, null, 2); + } else if (schema.properties) { + const exampleObj = {}; + Object.keys(schema.properties).forEach(key => { + const prop = schema.properties[key]; + exampleObj[key] = prop.example || (prop.type === 'string' ? '' : prop.type === 'number' ? 0 : prop.type === 'boolean' ? false : null); + }); + defaultBody = JSON.stringify(exampleObj, null, 2); + } + + const bodyInputId = `test-body-${escapeId(path)}-${method}`; + bodyInput = ` +
+ + +
+ `; + } + + // 处理路径参数 + const pathParams = (endpoint.parameters || []).filter(p => p.in === 'path'); + let pathParamsInput = ''; + if (pathParams.length > 0) { + pathParamsInput = pathParams.map(param => { + const inputId = `test-param-${param.name}-${escapeId(path)}-${method}`; + return ` +
+ + +
+ `; + }).join(''); + } + + // 处理查询参数 + const queryParams = (endpoint.parameters || []).filter(p => p.in === 'query'); + let queryParamsInput = ''; + if (queryParams.length > 0) { + queryParamsInput = queryParams.map(param => { + const inputId = `test-query-${param.name}-${escapeId(path)}-${method}`; + const defaultValue = param.schema?.default !== undefined ? param.schema.default : ''; + const placeholder = param.description || param.name; + const required = param.required ? '*' : '可选'; + return ` +
+ + +
+ `; + }).join(''); + } + + return ` +
+
测试接口
+
+ ${pathParamsInput} + ${queryParamsInput ? `
查询参数:
${queryParamsInput}
` : ''} + ${bodyInput} +
+ + + +
+ +
+
+ `; +} + +// 测试API +async function testAPI(method, path, operationId) { + const resultId = `test-result-${escapeId(path)}-${method}`; + const resultDiv = document.getElementById(resultId); + if (!resultDiv) return; + + resultDiv.style.display = 'block'; + resultDiv.className = 'api-test-result loading'; + resultDiv.textContent = '发送请求中...'; + + try { + // 替换路径参数 + let actualPath = path; + const pathParams = path.match(/\{([^}]+)\}/g) || []; + pathParams.forEach(param => { + const paramName = param.slice(1, -1); + const inputId = `test-param-${paramName}-${escapeId(path)}-${method}`; + const input = document.getElementById(inputId); + if (input && input.value) { + actualPath = actualPath.replace(param, encodeURIComponent(input.value)); + } else { + throw new Error(`路径参数 ${paramName} 不能为空`); + } + }); + + // 确保路径以/api开头(如果OpenAPI规范中的路径不包含/api) + if (!actualPath.startsWith('/api') && !actualPath.startsWith('http')) { + actualPath = '/api' + actualPath; + } + + // 构建查询参数 + const queryParams = []; + const endpointSpec = apiSpec.paths[path]?.[method.toLowerCase()]; + if (endpointSpec && endpointSpec.parameters) { + endpointSpec.parameters.filter(p => p.in === 'query').forEach(param => { + const inputId = `test-query-${param.name}-${escapeId(path)}-${method}`; + const input = document.getElementById(inputId); + if (input && input.value !== '' && input.value !== null && input.value !== undefined) { + queryParams.push(`${encodeURIComponent(param.name)}=${encodeURIComponent(input.value)}`); + } else if (param.required) { + throw new Error(`查询参数 ${param.name} 不能为空`); + } + }); + } + + // 添加查询字符串 + if (queryParams.length > 0) { + actualPath += (actualPath.includes('?') ? '&' : '?') + queryParams.join('&'); + } + + // 构建请求选项 + const options = { + method: method, + headers: { + 'Content-Type': 'application/json', + } + }; + + // 添加token + if (currentToken) { + options.headers['Authorization'] = 'Bearer ' + currentToken; + } else { + // 如果没有token,提示用户 + throw new Error('未检测到 Token。请先在前端页面登录,然后刷新此页面。或者手动在请求头中添加 Authorization: Bearer your_token'); + } + + // 添加请求体 + if (['POST', 'PUT', 'PATCH'].includes(method)) { + const bodyInputId = `test-body-${escapeId(path)}-${method}`; + const bodyInput = document.getElementById(bodyInputId); + if (bodyInput && bodyInput.value.trim()) { + try { + options.body = JSON.stringify(JSON.parse(bodyInput.value.trim())); + } catch (e) { + throw new Error('请求体JSON格式错误: ' + e.message); + } + } + } + + // 发送请求 + const response = await fetch(actualPath, options); + const responseText = await response.text(); + + let responseData; + try { + responseData = JSON.parse(responseText); + } catch { + responseData = responseText; + } + + // 显示结果 + resultDiv.className = response.ok ? 'api-test-result success' : 'api-test-result error'; + resultDiv.textContent = `状态码: ${response.status} ${response.statusText}\n\n${typeof responseData === 'string' ? responseData : JSON.stringify(responseData, null, 2)}`; + + } catch (error) { + resultDiv.className = 'api-test-result error'; + resultDiv.textContent = '请求失败: ' + error.message; + } +} + +// 清除测试结果 +function clearTestResult(id) { + const resultDiv = document.getElementById(`test-result-${id}`); + if (resultDiv) { + resultDiv.style.display = 'none'; + resultDiv.textContent = ''; + } +} + +// 复制curl命令 +function copyCurlCommand(event, method, path) { + try { + // 替换路径参数 + let actualPath = path; + const pathParams = path.match(/\{([^}]+)\}/g) || []; + pathParams.forEach(param => { + const paramName = param.slice(1, -1); + const inputId = `test-param-${paramName}-${escapeId(path)}-${method}`; + const input = document.getElementById(inputId); + if (input && input.value) { + actualPath = actualPath.replace(param, encodeURIComponent(input.value)); + } + }); + + // 确保路径以/api开头 + if (!actualPath.startsWith('/api') && !actualPath.startsWith('http')) { + actualPath = '/api' + actualPath; + } + + // 构建查询参数 + const queryParams = []; + const endpointSpec = apiSpec.paths[path]?.[method.toLowerCase()]; + if (endpointSpec && endpointSpec.parameters) { + endpointSpec.parameters.filter(p => p.in === 'query').forEach(param => { + const inputId = `test-query-${param.name}-${escapeId(path)}-${method}`; + const input = document.getElementById(inputId); + if (input && input.value !== '' && input.value !== null && input.value !== undefined) { + queryParams.push(`${encodeURIComponent(param.name)}=${encodeURIComponent(input.value)}`); + } + }); + } + + // 添加查询字符串 + if (queryParams.length > 0) { + actualPath += (actualPath.includes('?') ? '&' : '?') + queryParams.join('&'); + } + + // 构建完整的URL + const baseUrl = window.location.origin; + const fullUrl = baseUrl + actualPath; + + // 构建curl命令 + let curlCommand = `curl -X ${method.toUpperCase()} "${fullUrl}"`; + + // 添加请求头 + curlCommand += ` \\\n -H "Content-Type: application/json"`; + + // 添加Authorization头 + if (currentToken) { + curlCommand += ` \\\n -H "Authorization: Bearer ${currentToken}"`; + } else { + curlCommand += ` \\\n -H "Authorization: Bearer YOUR_TOKEN_HERE"`; + } + + // 添加请求体(如果有) + if (['POST', 'PUT', 'PATCH'].includes(method.toUpperCase())) { + const bodyInputId = `test-body-${escapeId(path)}-${method}`; + const bodyInput = document.getElementById(bodyInputId); + if (bodyInput && bodyInput.value.trim()) { + try { + // 验证JSON格式并格式化 + const jsonBody = JSON.parse(bodyInput.value.trim()); + const jsonString = JSON.stringify(jsonBody); + // 在单引号内,只需要转义单引号本身 + const escapedJson = jsonString.replace(/'/g, "'\\''"); + curlCommand += ` \\\n -d '${escapedJson}'`; + } catch (e) { + // 如果不是有效JSON,直接使用原始值 + const escapedBody = bodyInput.value.trim().replace(/'/g, "'\\''"); + curlCommand += ` \\\n -d '${escapedBody}'`; + } + } + } + + // 复制到剪贴板 + const button = event ? event.target.closest('button') : null; + navigator.clipboard.writeText(curlCommand).then(() => { + // 显示成功提示 + if (button) { + const originalText = button.innerHTML; + button.innerHTML = '已复制'; + button.style.color = 'var(--success-color)'; + setTimeout(() => { + button.innerHTML = originalText; + button.style.color = ''; + }, 2000); + } else { + alert('curl命令已复制到剪贴板!'); + } + }).catch(err => { + console.error('复制失败:', err); + // 如果clipboard API失败,使用fallback方法 + const textarea = document.createElement('textarea'); + textarea.value = curlCommand; + textarea.style.position = 'fixed'; + textarea.style.opacity = '0'; + document.body.appendChild(textarea); + textarea.select(); + try { + document.execCommand('copy'); + if (button) { + const originalText = button.innerHTML; + button.innerHTML = '已复制'; + button.style.color = 'var(--success-color)'; + setTimeout(() => { + button.innerHTML = originalText; + button.style.color = ''; + }, 2000); + } else { + alert('curl命令已复制到剪贴板!'); + } + } catch (e) { + alert('复制失败,请手动复制:\n\n' + curlCommand); + } + document.body.removeChild(textarea); + }); + + } catch (error) { + console.error('生成curl命令失败:', error); + alert('生成curl命令失败: ' + error.message); + } +} + +// HTML转义 +function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} + +// ID转义(用于HTML ID属性) +function escapeId(text) { + return text.replace(/[{}]/g, '').replace(/\//g, '-'); +} diff --git a/web/static/js/chat.js b/web/static/js/chat.js index 621f3888..349f9bb6 100644 --- a/web/static/js/chat.js +++ b/web/static/js/chat.js @@ -5761,4 +5761,34 @@ document.addEventListener('DOMContentLoaded', async () => { }; } await loadConversationsWithGroups(); + + // 添加页面焦点时自动刷新对话列表的功能 + // 这样当通过OpenAPI创建对话后,切换回页面时能自动看到新对话 + let lastFocusTime = Date.now(); + const CONVERSATION_REFRESH_INTERVAL = 30000; // 30秒内最多刷新一次,避免过于频繁 + + window.addEventListener('focus', () => { + const now = Date.now(); + // 如果距离上次刷新超过30秒,才刷新对话列表 + if (now - lastFocusTime > CONVERSATION_REFRESH_INTERVAL) { + lastFocusTime = now; + if (typeof loadConversationsWithGroups === 'function') { + loadConversationsWithGroups(); + } + } + }); + + // 监听页面可见性变化(当用户切换标签页回来时) + document.addEventListener('visibilitychange', () => { + if (!document.hidden) { + // 页面变为可见时,检查是否需要刷新 + const now = Date.now(); + if (now - lastFocusTime > CONVERSATION_REFRESH_INTERVAL) { + lastFocusTime = now; + if (typeof loadConversationsWithGroups === 'function') { + loadConversationsWithGroups(); + } + } + } + }); }); diff --git a/web/templates/api-docs.html b/web/templates/api-docs.html new file mode 100644 index 00000000..3df5a1ce --- /dev/null +++ b/web/templates/api-docs.html @@ -0,0 +1,578 @@ + + + + + + API 文档 - CyberStrikeAI + + + + + +
+
+

+ + + + + + + API 文档 +

+

CyberStrikeAI 平台 API 接口文档,支持在线测试

+
+ + + + + +
+
+

API 分组

+ +
+ +
+
+ + + + + +

加载中...

+

正在加载 API 文档

+
+
+
+
+ + + + diff --git a/web/templates/index.html b/web/templates/index.html index 3fbc2247..6a7e2696 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -36,6 +36,9 @@

AI 驱动的自动化安全测试平台

+