From 07363476ab14c9478d2656c40f8e56d4831bf072 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Sat, 8 Nov 2025 21:25:09 +0800 Subject: [PATCH] Add files via upload --- data/conversations.db | Bin 0 -> 4096 bytes data/conversations.db-shm | Bin 0 -> 32768 bytes data/conversations.db-wal | Bin 0 -> 910552 bytes internal/agent/agent.go | 42 ++- internal/database/conversation.go | 154 +++++++- internal/database/database.go | 20 ++ internal/handler/agent.go | 54 ++- web/static/css/style.css | 322 +++++++++++++++++ web/static/js/app.js | 574 ++++++++++++++++++++++++++++-- 9 files changed, 1126 insertions(+), 40 deletions(-) create mode 100644 data/conversations.db create mode 100644 data/conversations.db-shm create mode 100644 data/conversations.db-wal diff --git a/data/conversations.db b/data/conversations.db new file mode 100644 index 0000000000000000000000000000000000000000..4ebf78cf96088c7148cb47caf804cc036de75a34 GIT binary patch literal 4096 zcmWFz^vNtqRY=P(%1ta$FlG>7U}9o$P*7lCU|@t|AVoG{WY8;GzzfnYK(-m98b?E5 nGz3ONU^E0qLtr!nMnhmU1V%$(Gz3ONU^E0qLtr!nC=3ArfDQ*E literal 0 HcmV?d00001 diff --git a/data/conversations.db-shm b/data/conversations.db-shm new file mode 100644 index 0000000000000000000000000000000000000000..f7cdced6bcad3e1c09413cc8300bd9389ecb770c GIT binary patch literal 32768 zcmeI*p-6Fecc-|!dkB98?_h@9CHtZz9J!@S z_)RjIx!Kw0dG@n+Z|B0kmc+K4rYC^3pUjxn^r+9h$Astn5>xDTY{aZ@(H)juj%pKG zCoEZ5fcs6TpUuYo?r6Scrurr(HwjHoWV)RvHa)~QPvRRx&FJ4_=l(tRx?Oi(cV2U@ z|LQ*Hem7(NN&fi0$N4_2byCxl#bIviwC~LBabAyW&Bjb_dJ5C+Jf&|;Wk%;aj_cmI z*8H(-92+Ch&;EMt_b-s=2sBTi`<+hyXG?8rOk=vW^RNBRXa9S&&uV|;Y7&ru1SB8< z2}nQ!5|DrdBp?9^NI(J-kbndvAOQ(TKmrnwfCMBU0SQPz0uqpb1SB8<2}nQ!5|Drd zBp?9^NI(J-kbndvAOQ(TKmrnwfCMBU0dEK-FlR`FnzIT962%N=afkQbC`?sIz;%Iy z<^+j!WFRZKDL^4gP>#w}p%(RNNCd5EOJ{n}i~bB}B;%RF92T*h9qi+<>-8yt1l$uy zWX?{>NHz*nlBzVK9g&P;E<4>zsmK!Wh(HLb$wUtFQiRe}pgMJELJK<3l|BsOgGcIA z84?J-KoWC)R%UbNT|SCZhKkgnE=_4kN4n9M!Hi)d^I5`f4g^2BB1#}$1(KT6-?EU4 z{1l@sm8eNQn$e0*bf+If7|SFUu#`O46z90aRc>;R z$2{k?cgj>H5^zCaCef^8BU?GnY0h(*Yuw^KPk6x_7xF2j1iT|Ko7KdyiEW(V3>Ub< zb#C*3r@Z8?cgj>H5^zD_p$jn;QUd>mz$2dVig*7;v8qx6K^3t7yQd}r2}nQ!5|Drd RBp?9^NI(J-kih>f@B`KNjspMy literal 0 HcmV?d00001 diff --git a/data/conversations.db-wal b/data/conversations.db-wal new file mode 100644 index 0000000000000000000000000000000000000000..7aec42588a854bd3e4735021d63574cfe717b70c GIT binary patch literal 910552 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 literal 0 HcmV?d00001 diff --git a/internal/agent/agent.go b/internal/agent/agent.go index a6451212..be3ff05a 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -276,11 +276,17 @@ func (a *Agent) AgentLoopWithProgress(ctx context.Context, userInput string, his // 获取可用工具 tools := a.getAvailableTools() - // 发送进度更新 + // 发送迭代开始事件 if i == 0 { - sendProgress("progress", "正在分析请求并制定测试策略...", nil) + sendProgress("iteration", "开始分析请求并制定测试策略", map[string]interface{}{ + "iteration": i + 1, + "total": maxIterations, + }) } else { - sendProgress("progress", fmt.Sprintf("正在继续分析(第 %d 轮迭代)...", i+1), nil) + sendProgress("iteration", fmt.Sprintf("第 %d 轮迭代", i+1), map[string]interface{}{ + "iteration": i + 1, + "total": maxIterations, + }) } // 记录每次调用OpenAI @@ -333,6 +339,13 @@ func (a *Agent) AgentLoopWithProgress(ctx context.Context, userInput string, his // 检查是否有工具调用 if len(choice.Message.ToolCalls) > 0 { + // 如果有思考内容,先发送思考事件 + if choice.Message.Content != "" { + sendProgress("thinking", choice.Message.Content, map[string]interface{}{ + "iteration": i + 1, + }) + } + // 添加assistant消息(包含工具调用) messages = append(messages, ChatMessage{ Role: "assistant", @@ -341,7 +354,10 @@ func (a *Agent) AgentLoopWithProgress(ctx context.Context, userInput string, his }) // 发送工具调用进度 - sendProgress("progress", fmt.Sprintf("检测到 %d 个工具调用,开始执行...", len(choice.Message.ToolCalls)), nil) + sendProgress("tool_calls_detected", fmt.Sprintf("检测到 %d 个工具调用", len(choice.Message.ToolCalls)), map[string]interface{}{ + "count": len(choice.Message.ToolCalls), + "iteration": i + 1, + }) // 执行所有工具调用 for idx, toolCall := range choice.Message.ToolCalls { @@ -350,8 +366,11 @@ func (a *Agent) AgentLoopWithProgress(ctx context.Context, userInput string, his sendProgress("tool_call", fmt.Sprintf("正在调用工具: %s", toolCall.Function.Name), map[string]interface{}{ "toolName": toolCall.Function.Name, "arguments": string(toolArgsJSON), + "argumentsObj": toolCall.Function.Arguments, + "toolCallId": toolCall.ID, "index": idx + 1, "total": len(choice.Message.ToolCalls), + "iteration": i + 1, }) // 执行工具 @@ -369,9 +388,12 @@ func (a *Agent) AgentLoopWithProgress(ctx context.Context, userInput string, his sendProgress("tool_result", fmt.Sprintf("工具 %s 执行失败", toolCall.Function.Name), map[string]interface{}{ "toolName": toolCall.Function.Name, "success": false, + "isError": true, "error": err.Error(), + "toolCallId": toolCall.ID, "index": idx + 1, "total": len(choice.Message.ToolCalls), + "iteration": i + 1, }) a.logger.Warn("工具执行失败,已返回详细错误信息", @@ -399,10 +421,13 @@ func (a *Agent) AgentLoopWithProgress(ctx context.Context, userInput string, his "toolName": toolCall.Function.Name, "success": !execResult.IsError, "isError": execResult.IsError, - "result": resultPreview, + "result": execResult.Result, // 完整结果 + "resultPreview": resultPreview, // 预览结果 "executionId": execResult.ExecutionID, + "toolCallId": toolCall.ID, "index": idx + 1, "total": len(choice.Message.ToolCalls), + "iteration": i + 1, }) // 如果工具返回了错误,记录日志但不中断流程 @@ -423,6 +448,13 @@ func (a *Agent) AgentLoopWithProgress(ctx context.Context, userInput string, his Content: choice.Message.Content, }) + // 发送AI思考内容(如果没有工具调用) + if choice.Message.Content != "" { + sendProgress("thinking", choice.Message.Content, map[string]interface{}{ + "iteration": i + 1, + }) + } + // 如果完成,返回结果 if choice.FinishReason == "stop" { sendProgress("progress", "正在生成最终回复...", nil) diff --git a/internal/database/conversation.go b/internal/database/conversation.go index 95905772..e21c6c27 100644 --- a/internal/database/conversation.go +++ b/internal/database/conversation.go @@ -21,12 +21,13 @@ type Conversation struct { // Message 消息 type Message struct { - ID string `json:"id"` - ConversationID string `json:"conversationId"` - Role string `json:"role"` - Content string `json:"content"` - MCPExecutionIDs []string `json:"mcpExecutionIds,omitempty"` - CreatedAt time.Time `json:"createdAt"` + ID string `json:"id"` + ConversationID string `json:"conversationId"` + Role string `json:"role"` + Content string `json:"content"` + MCPExecutionIDs []string `json:"mcpExecutionIds,omitempty"` + ProcessDetails []map[string]interface{} `json:"processDetails,omitempty"` + CreatedAt time.Time `json:"createdAt"` } // CreateConversation 创建新对话 @@ -91,6 +92,39 @@ func (db *DB) GetConversation(id string) (*Conversation, error) { } conv.Messages = messages + // 加载过程详情(按消息ID分组) + processDetailsMap, err := db.GetProcessDetailsByConversation(id) + if err != nil { + db.logger.Warn("加载过程详情失败", zap.Error(err)) + processDetailsMap = make(map[string][]ProcessDetail) + } + + // 将过程详情附加到对应的消息上 + for i := range conv.Messages { + if details, ok := processDetailsMap[conv.Messages[i].ID]; ok { + // 将ProcessDetail转换为JSON格式,以便前端使用 + detailsJSON := make([]map[string]interface{}, len(details)) + for j, detail := range details { + var data interface{} + if detail.Data != "" { + if err := json.Unmarshal([]byte(detail.Data), &data); err != nil { + db.logger.Warn("解析过程详情数据失败", zap.Error(err)) + } + } + detailsJSON[j] = map[string]interface{}{ + "id": detail.ID, + "messageId": detail.MessageID, + "conversationId": detail.ConversationID, + "eventType": detail.EventType, + "message": detail.Message, + "data": data, + "createdAt": detail.CreatedAt, + } + } + conv.Messages[i].ProcessDetails = detailsJSON + } + } + return &conv, nil } @@ -254,3 +288,111 @@ func (db *DB) GetMessages(conversationID string) ([]Message, error) { return messages, nil } +// ProcessDetail 过程详情事件 +type ProcessDetail struct { + ID string `json:"id"` + MessageID string `json:"messageId"` + ConversationID string `json:"conversationId"` + EventType string `json:"eventType"` // iteration, thinking, tool_calls_detected, tool_call, tool_result, progress, error + Message string `json:"message"` + Data string `json:"data"` // JSON格式的数据 + CreatedAt time.Time `json:"createdAt"` +} + +// AddProcessDetail 添加过程详情事件 +func (db *DB) AddProcessDetail(messageID, conversationID, eventType, message string, data interface{}) error { + id := uuid.New().String() + + var dataJSON string + if data != nil { + jsonData, err := json.Marshal(data) + if err != nil { + db.logger.Warn("序列化过程详情数据失败", zap.Error(err)) + } else { + dataJSON = string(jsonData) + } + } + + _, err := db.Exec( + "INSERT INTO process_details (id, message_id, conversation_id, event_type, message, data, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)", + id, messageID, conversationID, eventType, message, dataJSON, time.Now(), + ) + if err != nil { + return fmt.Errorf("添加过程详情失败: %w", err) + } + + return nil +} + +// GetProcessDetails 获取消息的过程详情 +func (db *DB) GetProcessDetails(messageID string) ([]ProcessDetail, error) { + rows, err := db.Query( + "SELECT id, message_id, conversation_id, event_type, message, data, created_at FROM process_details WHERE message_id = ? ORDER BY created_at ASC", + messageID, + ) + if err != nil { + return nil, fmt.Errorf("查询过程详情失败: %w", err) + } + defer rows.Close() + + var details []ProcessDetail + for rows.Next() { + var detail ProcessDetail + var createdAt string + + if err := rows.Scan(&detail.ID, &detail.MessageID, &detail.ConversationID, &detail.EventType, &detail.Message, &detail.Data, &createdAt); err != nil { + return nil, fmt.Errorf("扫描过程详情失败: %w", err) + } + + // 尝试多种时间格式解析 + var err error + detail.CreatedAt, err = time.Parse("2006-01-02 15:04:05.999999999-07:00", createdAt) + if err != nil { + detail.CreatedAt, err = time.Parse("2006-01-02 15:04:05", createdAt) + } + if err != nil { + detail.CreatedAt, err = time.Parse(time.RFC3339, createdAt) + } + + details = append(details, detail) + } + + return details, nil +} + +// GetProcessDetailsByConversation 获取对话的所有过程详情(按消息分组) +func (db *DB) GetProcessDetailsByConversation(conversationID string) (map[string][]ProcessDetail, error) { + rows, err := db.Query( + "SELECT id, message_id, conversation_id, event_type, message, data, created_at FROM process_details WHERE conversation_id = ? ORDER BY created_at ASC", + conversationID, + ) + if err != nil { + return nil, fmt.Errorf("查询过程详情失败: %w", err) + } + defer rows.Close() + + detailsMap := make(map[string][]ProcessDetail) + for rows.Next() { + var detail ProcessDetail + var createdAt string + + if err := rows.Scan(&detail.ID, &detail.MessageID, &detail.ConversationID, &detail.EventType, &detail.Message, &detail.Data, &createdAt); err != nil { + return nil, fmt.Errorf("扫描过程详情失败: %w", err) + } + + // 尝试多种时间格式解析 + var err error + detail.CreatedAt, err = time.Parse("2006-01-02 15:04:05.999999999-07:00", createdAt) + if err != nil { + detail.CreatedAt, err = time.Parse("2006-01-02 15:04:05", createdAt) + } + if err != nil { + detail.CreatedAt, err = time.Parse(time.RFC3339, createdAt) + } + + detailsMap[detail.MessageID] = append(detailsMap[detail.MessageID], detail) + } + + return detailsMap, nil +} + diff --git a/internal/database/database.go b/internal/database/database.go index 5091e5ef..0cf0e7e7 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -61,10 +61,26 @@ func (db *DB) initTables() error { FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE );` + // 创建过程详情表 + createProcessDetailsTable := ` + CREATE TABLE IF NOT EXISTS process_details ( + id TEXT PRIMARY KEY, + message_id TEXT NOT NULL, + conversation_id TEXT NOT NULL, + event_type TEXT NOT NULL, + message TEXT, + data TEXT, + created_at DATETIME NOT NULL, + FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE, + FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE + );` + // 创建索引 createIndexes := ` CREATE INDEX IF NOT EXISTS idx_messages_conversation_id ON messages(conversation_id); CREATE INDEX IF NOT EXISTS idx_conversations_updated_at ON conversations(updated_at); + CREATE INDEX IF NOT EXISTS idx_process_details_message_id ON process_details(message_id); + CREATE INDEX IF NOT EXISTS idx_process_details_conversation_id ON process_details(conversation_id); ` if _, err := db.Exec(createConversationsTable); err != nil { @@ -75,6 +91,10 @@ func (db *DB) initTables() error { return fmt.Errorf("创建messages表失败: %w", err) } + if _, err := db.Exec(createProcessDetailsTable); err != nil { + return fmt.Errorf("创建process_details表失败: %w", err) + } + if _, err := db.Exec(createIndexes); err != nil { return fmt.Errorf("创建索引失败: %w", err) } diff --git a/internal/handler/agent.go b/internal/handler/agent.go index 50974318..9d33d86d 100644 --- a/internal/handler/agent.go +++ b/internal/handler/agent.go @@ -220,9 +220,29 @@ func (h *AgentHandler) AgentLoopStream(c *gin.Context) { h.logger.Error("保存用户消息失败", zap.Error(err)) } - // 创建进度回调函数 + // 预先创建助手消息,以便关联过程详情 + assistantMsg, err := h.db.AddMessage(conversationID, "assistant", "处理中...", nil) + if err != nil { + h.logger.Error("创建助手消息失败", zap.Error(err)) + // 如果创建失败,继续执行但不保存过程详情 + assistantMsg = nil + } + + // 创建进度回调函数,同时保存到数据库 + var assistantMessageID string + if assistantMsg != nil { + assistantMessageID = assistantMsg.ID + } + progressCallback := func(eventType, message string, data interface{}) { sendEvent(eventType, message, data) + + // 保存过程详情到数据库(排除response和done事件,它们会在后面单独处理) + if assistantMessageID != "" && eventType != "response" && eventType != "done" { + if err := h.db.AddProcessDetail(assistantMessageID, conversationID, eventType, message, data); err != nil { + h.logger.Warn("保存过程详情失败", zap.Error(err), zap.String("eventType", eventType)) + } + } } // 执行Agent Loop,传入进度回调 @@ -231,20 +251,44 @@ func (h *AgentHandler) AgentLoopStream(c *gin.Context) { if err != nil { h.logger.Error("Agent Loop执行失败", zap.Error(err)) sendEvent("error", "执行失败: "+err.Error(), nil) + // 保存错误事件 + if assistantMessageID != "" { + h.db.AddProcessDetail(assistantMessageID, conversationID, "error", "执行失败: "+err.Error(), nil) + } sendEvent("done", "", nil) return } - // 保存助手回复 - _, err = h.db.AddMessage(conversationID, "assistant", result.Response, result.MCPExecutionIDs) - if err != nil { - h.logger.Error("保存助手消息失败", zap.Error(err)) + // 更新助手消息内容 + if assistantMsg != nil { + _, err = h.db.Exec( + "UPDATE messages SET content = ?, mcp_execution_ids = ? WHERE id = ?", + result.Response, + func() string { + if len(result.MCPExecutionIDs) > 0 { + jsonData, _ := json.Marshal(result.MCPExecutionIDs) + return string(jsonData) + } + return "" + }(), + assistantMessageID, + ) + if err != nil { + h.logger.Error("更新助手消息失败", zap.Error(err)) + } + } else { + // 如果之前创建失败,现在创建 + _, err = h.db.AddMessage(conversationID, "assistant", result.Response, result.MCPExecutionIDs) + if err != nil { + h.logger.Error("保存助手消息失败", zap.Error(err)) + } } // 发送最终响应 sendEvent("response", result.Response, map[string]interface{}{ "mcpExecutionIds": result.MCPExecutionIDs, "conversationId": conversationID, + "messageId": assistantMessageID, // 包含消息ID,以便前端关联过程详情 }) sendEvent("done", "", map[string]interface{}{ "conversationId": conversationID, diff --git a/web/static/css/style.css b/web/static/css/style.css index f462ea5e..aa543643 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -167,17 +167,32 @@ header { cursor: pointer; transition: all 0.2s; border: 1px solid transparent; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + position: relative; } .conversation-item:hover { background: var(--bg-tertiary); } +.conversation-item:hover .conversation-delete-btn { + opacity: 1; +} + .conversation-item.active { background: var(--bg-primary); border-color: var(--accent-color); } +.conversation-content { + flex: 1; + min-width: 0; + overflow: hidden; +} + .conversation-title { font-size: 0.875rem; font-weight: 500; @@ -193,6 +208,53 @@ header { color: var(--text-muted); } +.conversation-delete-btn { + width: 28px; + height: 28px; + border: none; + background: transparent; + color: var(--text-muted); + cursor: pointer; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + opacity: 0; + transition: all 0.2s ease; + flex-shrink: 0; + position: relative; +} + +.conversation-delete-btn svg { + width: 14px; + height: 14px; + transition: all 0.2s ease; +} + +.conversation-delete-btn:hover { + background: rgba(220, 53, 69, 0.1); + color: var(--error-color); + opacity: 1; +} + +.conversation-delete-btn:hover svg { + transform: scale(1.1); +} + +.conversation-delete-btn:active { + background: rgba(220, 53, 69, 0.2); + transform: scale(0.95); +} + +.conversation-item.active .conversation-delete-btn { + opacity: 0.6; +} + +.conversation-item.active:hover .conversation-delete-btn { + opacity: 1; +} + /* 对话界面样式 */ .chat-container { display: flex; @@ -503,6 +565,40 @@ header { gap: 6px; } +.process-detail-btn { + background: rgba(156, 39, 176, 0.1) !important; + border-color: rgba(156, 39, 176, 0.3) !important; + color: #9c27b0 !important; +} + +.process-detail-btn:hover { + background: rgba(156, 39, 176, 0.2) !important; + border-color: #9c27b0 !important; + color: #7b1fa2 !important; +} + +.process-details-container { + margin-top: 12px; + padding-top: 12px; + border-top: 1px solid var(--border-color); + width: 100%; +} + +.process-details-content { + width: 100%; +} + +.process-details-content .progress-timeline { + max-height: 0; + overflow: hidden; + transition: max-height 0.3s ease; +} + +.process-details-content .progress-timeline.expanded { + max-height: 2000px; + overflow-y: auto; +} + .chat-input-container { display: flex; gap: 12px; @@ -813,3 +909,229 @@ header { margin: 10% auto; } } + +/* 进度展示样式 */ +.progress-container { + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 16px; + max-width: 100%; +} + +.progress-container.completed { + background: var(--bg-secondary); + border-color: var(--border-color); + opacity: 0.95; +} + +.progress-details { + margin-top: 8px; + margin-bottom: 24px; +} + +.progress-timeline-empty { + padding: 12px; + text-align: center; + color: var(--text-muted); + font-size: 0.875rem; +} + +.progress-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; + padding-bottom: 12px; + border-bottom: 1px solid var(--border-color); +} + +.progress-title { + font-weight: 600; + color: var(--text-primary); + font-size: 0.9375rem; +} + +.progress-toggle { + padding: 4px 12px; + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 4px; + font-size: 0.8125rem; + color: var(--text-secondary); + cursor: pointer; + transition: all 0.2s; +} + +.progress-toggle:hover { + background: var(--bg-secondary); + color: var(--text-primary); +} + +.progress-timeline { + max-height: 0; + overflow: hidden; + transition: max-height 0.3s ease; +} + +.progress-timeline.expanded { + max-height: 2000px; + overflow-y: auto; +} + +.timeline-item { + padding: 12px; + margin-bottom: 8px; + border-left: 3px solid var(--border-color); + padding-left: 16px; + background: var(--bg-secondary); + border-radius: 4px; + transition: all 0.2s; +} + +.timeline-item:hover { + background: var(--bg-tertiary); +} + +.timeline-item-iteration { + border-left-color: var(--accent-color); + background: rgba(0, 102, 255, 0.05); +} + +.timeline-item-thinking { + border-left-color: #9c27b0; + background: rgba(156, 39, 176, 0.05); +} + +.timeline-item-tool_call { + border-left-color: #ff9800; + background: rgba(255, 152, 0, 0.05); +} + +.timeline-item-tool_result { + border-left-color: var(--success-color); + background: rgba(40, 167, 69, 0.05); +} + +.timeline-item-tool_result.error { + border-left-color: var(--error-color); + background: rgba(220, 53, 69, 0.05); +} + +.timeline-item-error { + border-left-color: var(--error-color); + background: rgba(220, 53, 69, 0.1); +} + +.timeline-item-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 8px; +} + +.timeline-item-time { + font-size: 0.75rem; + color: var(--text-muted); + font-family: monospace; + min-width: 70px; +} + +.timeline-item-title { + font-weight: 500; + color: var(--text-primary); + font-size: 0.875rem; + flex: 1; +} + +.timeline-item-content { + margin-top: 8px; + padding-top: 8px; + border-top: 1px solid var(--border-color); + font-size: 0.875rem; + color: var(--text-secondary); + line-height: 1.6; +} + +.tool-details { + display: flex; + flex-direction: column; + gap: 12px; +} + +.tool-arg-section { + display: flex; + flex-direction: column; + gap: 8px; +} + +.tool-arg-section strong { + color: var(--text-primary); + font-size: 0.8125rem; +} + +.tool-args { + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 4px; + padding: 12px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 0.8125rem; + line-height: 1.5; + overflow-x: auto; + margin: 0; + color: var(--text-primary); +} + +.tool-result-section { + display: flex; + flex-direction: column; + gap: 8px; +} + +.tool-result-section strong { + color: var(--text-primary); + font-size: 0.8125rem; +} + +.tool-result { + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 4px; + padding: 12px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 0.8125rem; + line-height: 1.5; + overflow-x: auto; + max-height: 400px; + overflow-y: auto; + margin: 0; + white-space: pre-wrap; + word-wrap: break-word; + color: var(--text-primary); +} + +.tool-result-section.error .tool-result { + background: rgba(220, 53, 69, 0.1); + border-color: var(--error-color); + color: var(--error-color); +} + +.tool-result-section.success .tool-result { + background: rgba(40, 167, 69, 0.1); + border-color: var(--success-color); +} + +.tool-execution-id { + margin-top: 8px; + font-size: 0.75rem; + color: var(--text-muted); +} + +.tool-execution-id code { + background: var(--bg-tertiary); + padding: 2px 6px; + border-radius: 3px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + color: var(--text-secondary); +} diff --git a/web/static/js/app.js b/web/static/js/app.js index 50bbc9ba..6b0b61fb 100644 --- a/web/static/js/app.js +++ b/web/static/js/app.js @@ -15,10 +15,9 @@ async function sendMessage() { addMessage('user', message); input.value = ''; - // 创建进度消息容器 - const progressId = addMessage('system', '正在处理中...'); + // 创建进度消息容器(使用详细的进度展示) + const progressId = addProgressMessage(); const progressElement = document.getElementById(progressId); - const progressBubble = progressElement.querySelector('.message-bubble'); let assistantMessageId = null; let mcpExecutionIds = []; @@ -54,7 +53,7 @@ async function sendMessage() { if (line.startsWith('data: ')) { try { const eventData = JSON.parse(line.slice(6)); - handleStreamEvent(eventData, progressElement, progressBubble, progressId, + handleStreamEvent(eventData, progressElement, progressId, () => assistantMessageId, (id) => { assistantMessageId = id; }, () => mcpExecutionIds, (ids) => { mcpExecutionIds = ids; }); } catch (e) { @@ -71,7 +70,7 @@ async function sendMessage() { if (line.startsWith('data: ')) { try { const eventData = JSON.parse(line.slice(6)); - handleStreamEvent(eventData, progressElement, progressBubble, progressId, + handleStreamEvent(eventData, progressElement, progressId, () => assistantMessageId, (id) => { assistantMessageId = id; }, () => mcpExecutionIds, (ids) => { mcpExecutionIds = ids; }); } catch (e) { @@ -87,13 +86,243 @@ async function sendMessage() { } } +// 创建进度消息容器 +function addProgressMessage() { + const messagesDiv = document.getElementById('chat-messages'); + const messageDiv = document.createElement('div'); + messageCounter++; + const id = 'progress-' + Date.now() + '-' + messageCounter; + messageDiv.id = id; + messageDiv.className = 'message system progress-message'; + + const contentWrapper = document.createElement('div'); + contentWrapper.className = 'message-content'; + + const bubble = document.createElement('div'); + bubble.className = 'message-bubble progress-container'; + bubble.innerHTML = ` +
+ 🔍 渗透测试进行中... + +
+
+ `; + + contentWrapper.appendChild(bubble); + messageDiv.appendChild(contentWrapper); + messagesDiv.appendChild(messageDiv); + messagesDiv.scrollTop = messagesDiv.scrollHeight; + + return id; +} + +// 切换进度详情显示 +function toggleProgressDetails(progressId) { + const timeline = document.getElementById(progressId + '-timeline'); + const toggleBtn = document.querySelector(`#${progressId} .progress-toggle`); + + if (!timeline || !toggleBtn) return; + + if (timeline.classList.contains('expanded')) { + timeline.classList.remove('expanded'); + toggleBtn.textContent = '展开详情'; + } else { + timeline.classList.add('expanded'); + toggleBtn.textContent = '收起详情'; + } +} + +// 将进度详情集成到工具调用区域 +function integrateProgressToMCPSection(progressId, assistantMessageId) { + const progressElement = document.getElementById(progressId); + if (!progressElement) return; + + // 获取时间线内容 + const timeline = document.getElementById(progressId + '-timeline'); + let timelineHTML = ''; + if (timeline) { + timelineHTML = timeline.innerHTML; + } + + // 获取助手消息元素 + const assistantElement = document.getElementById(assistantMessageId); + if (!assistantElement) { + removeMessage(progressId); + return; + } + + // 查找MCP调用区域 + const mcpSection = assistantElement.querySelector('.mcp-call-section'); + if (!mcpSection) { + // 如果没有MCP区域,创建详情组件放在消息下方 + convertProgressToDetails(progressId, assistantMessageId); + return; + } + + // 获取时间线内容 + const hasContent = timelineHTML.trim().length > 0; + + // 确保按钮容器存在 + let buttonsContainer = mcpSection.querySelector('.mcp-call-buttons'); + if (!buttonsContainer) { + buttonsContainer = document.createElement('div'); + buttonsContainer.className = 'mcp-call-buttons'; + mcpSection.appendChild(buttonsContainer); + } + + // 创建详情容器,放在MCP按钮区域下方(统一结构) + const detailsId = 'process-details-' + assistantMessageId; + let detailsContainer = document.getElementById(detailsId); + + if (!detailsContainer) { + detailsContainer = document.createElement('div'); + detailsContainer.id = detailsId; + detailsContainer.className = 'process-details-container'; + // 确保容器在按钮容器之后 + if (buttonsContainer.nextSibling) { + mcpSection.insertBefore(detailsContainer, buttonsContainer.nextSibling); + } else { + mcpSection.appendChild(detailsContainer); + } + } + + // 设置详情内容 + detailsContainer.innerHTML = ` +
+ ${hasContent ? `
${timelineHTML}
` : '
暂无过程详情
'} +
+ `; + + // 移除原来的进度消息 + removeMessage(progressId); +} + +// 切换过程详情显示 +function toggleProcessDetails(progressId, assistantMessageId) { + const detailsId = 'process-details-' + assistantMessageId; + const detailsContainer = document.getElementById(detailsId); + if (!detailsContainer) return; + + const content = detailsContainer.querySelector('.process-details-content'); + const timeline = detailsContainer.querySelector('.progress-timeline'); + const btn = document.querySelector(`#${assistantMessageId} .process-detail-btn`); + + if (content && timeline) { + if (timeline.classList.contains('expanded')) { + timeline.classList.remove('expanded'); + if (btn) btn.innerHTML = '📋 过程详情'; + } else { + timeline.classList.add('expanded'); + if (btn) btn.innerHTML = '📋 收起详情'; + } + } else if (timeline) { + // 如果只有timeline,直接切换 + if (timeline.classList.contains('expanded')) { + timeline.classList.remove('expanded'); + if (btn) btn.innerHTML = '📋 过程详情'; + } else { + timeline.classList.add('expanded'); + if (btn) btn.innerHTML = '📋 收起详情'; + } + } +} + +// 将进度消息转换为可折叠的详情组件 +function convertProgressToDetails(progressId, assistantMessageId) { + const progressElement = document.getElementById(progressId); + if (!progressElement) return; + + // 获取时间线内容 + const timeline = document.getElementById(progressId + '-timeline'); + // 即使时间线不存在,也创建详情组件(显示空状态) + let timelineHTML = ''; + if (timeline) { + timelineHTML = timeline.innerHTML; + } + + // 获取助手消息元素 + const assistantElement = document.getElementById(assistantMessageId); + if (!assistantElement) { + removeMessage(progressId); + return; + } + + // 创建详情组件 + const detailsId = 'details-' + Date.now() + '-' + messageCounter++; + const detailsDiv = document.createElement('div'); + detailsDiv.id = detailsId; + detailsDiv.className = 'message system progress-details'; + + const contentWrapper = document.createElement('div'); + contentWrapper.className = 'message-content'; + + const bubble = document.createElement('div'); + bubble.className = 'message-bubble progress-container completed'; + + // 获取时间线HTML内容 + const hasContent = timelineHTML.trim().length > 0; + + // 总是显示详情组件,即使没有内容也显示 + bubble.innerHTML = ` +
+ 📋 渗透测试详情 + ${hasContent ? `` : ''} +
+ ${hasContent ? `
${timelineHTML}
` : '
暂无过程详情(可能执行过快或未触发详细事件)
'} + `; + + contentWrapper.appendChild(bubble); + detailsDiv.appendChild(contentWrapper); + + // 将详情组件插入到助手消息之后 + const messagesDiv = document.getElementById('chat-messages'); + // assistantElement 是消息div,需要插入到它的下一个兄弟节点之前 + if (assistantElement.nextSibling) { + messagesDiv.insertBefore(detailsDiv, assistantElement.nextSibling); + } else { + // 如果没有下一个兄弟节点,直接追加 + messagesDiv.appendChild(detailsDiv); + } + + // 移除原来的进度消息 + removeMessage(progressId); + + // 滚动到底部 + messagesDiv.scrollTop = messagesDiv.scrollHeight; +} + // 处理流式事件 -function handleStreamEvent(event, progressElement, progressBubble, progressId, +function handleStreamEvent(event, progressElement, progressId, getAssistantId, setAssistantId, getMcpIds, setMcpIds) { + const timeline = document.getElementById(progressId + '-timeline'); + if (!timeline) return; + switch (event.type) { - case 'progress': - // 更新进度消息 - progressBubble.textContent = event.message; + case 'iteration': + // 添加迭代标记 + addTimelineItem(timeline, 'iteration', { + title: `第 ${event.data?.iteration || 1} 轮迭代`, + message: event.message, + data: event.data + }); + break; + + case 'thinking': + // 显示AI思考内容 + addTimelineItem(timeline, 'thinking', { + title: '🤔 AI思考', + message: event.message, + data: event.data + }); + break; + + case 'tool_calls_detected': + // 工具调用检测 + addTimelineItem(timeline, 'tool_calls_detected', { + title: `🔧 检测到 ${event.data?.count || 0} 个工具调用`, + message: event.message, + data: event.data + }); break; case 'tool_call': @@ -102,7 +331,12 @@ function handleStreamEvent(event, progressElement, progressBubble, progressId, const toolName = toolInfo.toolName || '未知工具'; const index = toolInfo.index || 0; const total = toolInfo.total || 0; - progressBubble.innerHTML = `🔧 正在调用工具: ${escapeHtml(toolName)} (${index}/${total})`; + addTimelineItem(timeline, 'tool_call', { + title: `🔧 调用工具: ${escapeHtml(toolName)} (${index}/${total})`, + message: event.message, + data: toolInfo, + expanded: false + }); break; case 'tool_result': @@ -111,12 +345,24 @@ function handleStreamEvent(event, progressElement, progressBubble, progressId, const resultToolName = resultInfo.toolName || '未知工具'; const success = resultInfo.success !== false; const statusIcon = success ? '✅' : '❌'; - progressBubble.innerHTML = `${statusIcon} 工具 ${escapeHtml(resultToolName)} 执行${success ? '完成' : '失败'}`; + addTimelineItem(timeline, 'tool_result', { + title: `${statusIcon} 工具 ${escapeHtml(resultToolName)} 执行${success ? '完成' : '失败'}`, + message: event.message, + data: resultInfo, + expanded: false + }); + break; + + case 'progress': + // 更新进度状态 + const progressTitle = document.querySelector(`#${progressId} .progress-title`); + if (progressTitle) { + progressTitle.textContent = '🔍 ' + event.message; + } break; case 'response': - // 移除进度消息,显示最终回复 - removeMessage(progressId); + // 先添加助手回复 const responseData = event.data || {}; const mcpIds = responseData.mcpExecutionIds || []; setMcpIds(mcpIds); @@ -127,24 +373,31 @@ function handleStreamEvent(event, progressElement, progressBubble, progressId, updateActiveConversation(); } - // 添加助手回复 - const assistantId = addMessage('assistant', event.message, mcpIds); + // 添加助手回复,并传入进度ID以便集成详情 + const assistantId = addMessage('assistant', event.message, mcpIds, progressId); setAssistantId(assistantId); + // 将进度详情集成到工具调用区域 + integrateProgressToMCPSection(progressId, assistantId); + // 刷新对话列表 loadConversations(); break; case 'error': // 显示错误 - removeMessage(progressId); - addMessage('system', '错误: ' + event.message); + addTimelineItem(timeline, 'error', { + title: '❌ 错误', + message: event.message, + data: event.data + }); break; case 'done': - // 完成,确保进度消息已移除 - if (progressElement && progressElement.parentNode) { - removeMessage(progressId); + // 完成,更新进度标题(如果进度消息还存在) + const doneTitle = document.querySelector(`#${progressId} .progress-title`); + if (doneTitle) { + doneTitle.textContent = '✅ 渗透测试完成'; } // 更新对话ID if (event.data && event.data.conversationId) { @@ -153,13 +406,72 @@ function handleStreamEvent(event, progressElement, progressBubble, progressId, } break; } + + // 自动滚动到底部 + const messagesDiv = document.getElementById('chat-messages'); + messagesDiv.scrollTop = messagesDiv.scrollHeight; +} + +// 添加时间线项目 +function addTimelineItem(timeline, type, options) { + const item = document.createElement('div'); + item.className = `timeline-item timeline-item-${type}`; + + const time = new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); + + let content = ` +
+ ${time} + ${options.title} +
+ `; + + // 根据类型添加详细内容 + if (type === 'thinking' && options.message) { + content += `
${formatMarkdown(options.message)}
`; + } else if (type === 'tool_call' && options.data) { + const data = options.data; + const args = data.argumentsObj || (data.arguments ? JSON.parse(data.arguments) : {}); + content += ` +
+
+
+ 参数: +
${JSON.stringify(args, null, 2)}
+
+
+
+ `; + } else if (type === 'tool_result' && options.data) { + const data = options.data; + const isError = data.isError || !data.success; + const result = data.result || data.error || '无结果'; + content += ` +
+
+ 执行结果: +
${escapeHtml(result)}
+ ${data.executionId ? `
执行ID: ${data.executionId}
` : ''} +
+
+ `; + } + + item.innerHTML = content; + timeline.appendChild(item); + + // 自动展开详情 + const expanded = timeline.classList.contains('expanded'); + if (!expanded && (type === 'tool_call' || type === 'tool_result')) { + // 对于工具调用和结果,默认显示摘要 + } } // 消息计数器,确保ID唯一 let messageCounter = 0; // 添加消息 -function addMessage(role, content, mcpExecutionIds = null) { +function addMessage(role, content, mcpExecutionIds = null, progressId = null) { const messagesDiv = document.getElementById('chat-messages'); const messageDiv = document.createElement('div'); messageCounter++; @@ -238,6 +550,17 @@ function addMessage(role, content, mcpExecutionIds = null) { buttonsContainer.appendChild(detailBtn); }); + // 如果有进度ID,添加过程详情按钮 + if (progressId) { + const progressDetailBtn = document.createElement('button'); + progressDetailBtn.className = 'mcp-detail-btn process-detail-btn'; + progressDetailBtn.innerHTML = `📋 过程详情`; + progressDetailBtn.onclick = () => toggleProcessDetails(progressId, messageDiv.id); + buttonsContainer.appendChild(progressDetailBtn); + // 存储进度ID到消息元素 + messageDiv.dataset.progressId = progressId; + } + mcpSection.appendChild(buttonsContainer); contentWrapper.appendChild(mcpSection); } @@ -248,6 +571,131 @@ function addMessage(role, content, mcpExecutionIds = null) { return id; } +// 渲染过程详情 +function renderProcessDetails(messageId, processDetails) { + if (!processDetails || processDetails.length === 0) { + return; + } + + const messageElement = document.getElementById(messageId); + if (!messageElement) { + return; + } + + // 查找或创建MCP调用区域 + let mcpSection = messageElement.querySelector('.mcp-call-section'); + if (!mcpSection) { + mcpSection = document.createElement('div'); + mcpSection.className = 'mcp-call-section'; + + const contentWrapper = messageElement.querySelector('.message-content'); + if (contentWrapper) { + contentWrapper.appendChild(mcpSection); + } else { + return; + } + } + + // 确保有标签和按钮容器(统一结构) + let mcpLabel = mcpSection.querySelector('.mcp-call-label'); + let buttonsContainer = mcpSection.querySelector('.mcp-call-buttons'); + + // 如果没有标签,创建一个(当没有工具调用时) + if (!mcpLabel && !buttonsContainer) { + mcpLabel = document.createElement('div'); + mcpLabel.className = 'mcp-call-label'; + mcpLabel.textContent = '过程详情'; + mcpSection.appendChild(mcpLabel); + } + + // 如果没有按钮容器,创建一个 + if (!buttonsContainer) { + buttonsContainer = document.createElement('div'); + buttonsContainer.className = 'mcp-call-buttons'; + mcpSection.appendChild(buttonsContainer); + } + + // 添加过程详情按钮(如果还没有) + let processDetailBtn = buttonsContainer.querySelector('.process-detail-btn'); + if (!processDetailBtn) { + processDetailBtn = document.createElement('button'); + processDetailBtn.className = 'mcp-detail-btn process-detail-btn'; + processDetailBtn.innerHTML = '📋 过程详情'; + processDetailBtn.onclick = () => toggleProcessDetails(null, messageId); + buttonsContainer.appendChild(processDetailBtn); + } + + // 创建过程详情容器(放在按钮容器之后) + const detailsId = 'process-details-' + messageId; + let detailsContainer = document.getElementById(detailsId); + + if (!detailsContainer) { + detailsContainer = document.createElement('div'); + detailsContainer.id = detailsId; + detailsContainer.className = 'process-details-container'; + // 确保容器在按钮容器之后 + if (buttonsContainer.nextSibling) { + mcpSection.insertBefore(detailsContainer, buttonsContainer.nextSibling); + } else { + mcpSection.appendChild(detailsContainer); + } + } + + // 创建时间线 + const timelineId = detailsId + '-timeline'; + let timeline = document.getElementById(timelineId); + + if (!timeline) { + const contentDiv = document.createElement('div'); + contentDiv.className = 'process-details-content'; + + timeline = document.createElement('div'); + timeline.id = timelineId; + timeline.className = 'progress-timeline'; + + contentDiv.appendChild(timeline); + detailsContainer.appendChild(contentDiv); + } + + // 清空时间线并重新渲染 + timeline.innerHTML = ''; + + // 渲染每个过程详情事件 + processDetails.forEach(detail => { + const eventType = detail.eventType || ''; + const title = detail.message || ''; + const data = detail.data || {}; + + // 根据事件类型渲染不同的内容 + let itemTitle = title; + if (eventType === 'iteration') { + itemTitle = `第 ${data.iteration || 1} 轮迭代`; + } else if (eventType === 'thinking') { + itemTitle = '🤔 AI思考'; + } else if (eventType === 'tool_calls_detected') { + itemTitle = `🔧 检测到 ${data.count || 0} 个工具调用`; + } else if (eventType === 'tool_call') { + const toolName = data.toolName || '未知工具'; + const index = data.index || 0; + const total = data.total || 0; + itemTitle = `🔧 调用工具: ${escapeHtml(toolName)} (${index}/${total})`; + } else if (eventType === 'tool_result') { + const toolName = data.toolName || '未知工具'; + const success = data.success !== false; + const statusIcon = success ? '✅' : '❌'; + itemTitle = `${statusIcon} 工具 ${escapeHtml(toolName)} 执行${success ? '完成' : '失败'}`; + } else if (eventType === 'error') { + itemTitle = '❌ 错误'; + } + + addTimelineItem(timeline, eventType, { + title: itemTitle, + message: detail.message || '', + data: data + }); + }); +} + // 移除消息 function removeMessage(id) { const messageDiv = document.getElementById(id); @@ -357,6 +805,23 @@ function escapeHtml(text) { return div.innerHTML; } +function formatMarkdown(text) { + if (typeof marked !== 'undefined') { + try { + marked.setOptions({ + breaks: true, + gfm: true, + }); + return marked.parse(text); + } catch (e) { + console.error('Markdown 解析失败:', e); + return escapeHtml(text).replace(/\n/g, '
'); + } + } else { + return escapeHtml(text).replace(/\n/g, '
'); + } +} + // 开始新对话 function startNewConversation() { currentConversationId = null; @@ -387,10 +852,14 @@ async function loadConversations() { item.classList.add('active'); } + // 创建内容容器 + const contentWrapper = document.createElement('div'); + contentWrapper.className = 'conversation-content'; + const title = document.createElement('div'); title.className = 'conversation-title'; title.textContent = conv.title || '未命名对话'; - item.appendChild(title); + contentWrapper.appendChild(title); const time = document.createElement('div'); time.className = 'conversation-time'; @@ -448,7 +917,25 @@ async function loadConversations() { } time.textContent = timeText; - item.appendChild(time); + contentWrapper.appendChild(time); + + item.appendChild(contentWrapper); + + // 创建删除按钮 + const deleteBtn = document.createElement('button'); + deleteBtn.className = 'conversation-delete-btn'; + deleteBtn.innerHTML = ` + + + + `; + deleteBtn.title = '删除对话'; + deleteBtn.onclick = (e) => { + e.stopPropagation(); // 阻止触发对话加载 + deleteConversation(conv.id); + }; + item.appendChild(deleteBtn); item.onclick = () => loadConversation(conv.id); listContainer.appendChild(item); @@ -480,7 +967,14 @@ async function loadConversation(conversationId) { // 加载消息 if (conversation.messages && conversation.messages.length > 0) { conversation.messages.forEach(msg => { - addMessage(msg.role, msg.content, msg.mcpExecutionIds || []); + const messageId = addMessage(msg.role, msg.content, msg.mcpExecutionIds || []); + // 如果有过程详情,显示它们 + if (msg.processDetails && msg.processDetails.length > 0 && msg.role === 'assistant') { + // 延迟一下,确保消息已经渲染 + setTimeout(() => { + renderProcessDetails(messageId, msg.processDetails); + }, 100); + } }); } else { addMessage('assistant', '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。'); @@ -497,6 +991,38 @@ async function loadConversation(conversationId) { } } +// 删除对话 +async function deleteConversation(conversationId) { + // 确认删除 + if (!confirm('确定要删除这个对话吗?此操作不可恢复。')) { + return; + } + + try { + const response = await fetch(`/api/conversations/${conversationId}`, { + method: 'DELETE' + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || '删除失败'); + } + + // 如果删除的是当前对话,清空对话界面 + if (conversationId === currentConversationId) { + currentConversationId = null; + document.getElementById('chat-messages').innerHTML = ''; + addMessage('assistant', '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。'); + } + + // 刷新对话列表 + loadConversations(); + } catch (error) { + console.error('删除对话失败:', error); + alert('删除对话失败: ' + error.message); + } +} + // 更新活动对话样式 function updateActiveConversation() { document.querySelectorAll('.conversation-item').forEach(item => {