mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-04-01 00:30:33 +02:00
Add files via upload
This commit is contained in:
BIN
data/conversations.db
Normal file
BIN
data/conversations.db
Normal file
Binary file not shown.
BIN
data/conversations.db-shm
Normal file
BIN
data/conversations.db-shm
Normal file
Binary file not shown.
BIN
data/conversations.db-wal
Normal file
BIN
data/conversations.db-wal
Normal file
Binary file not shown.
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 = `
|
||||
<div class="progress-header">
|
||||
<span class="progress-title">🔍 渗透测试进行中...</span>
|
||||
<button class="progress-toggle" onclick="toggleProgressDetails('${id}')">收起详情</button>
|
||||
</div>
|
||||
<div class="progress-timeline expanded" id="${id}-timeline"></div>
|
||||
`;
|
||||
|
||||
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 = `
|
||||
<div class="process-details-content">
|
||||
${hasContent ? `<div class="progress-timeline" id="${detailsId}-timeline">${timelineHTML}</div>` : '<div class="progress-timeline-empty">暂无过程详情</div>'}
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 移除原来的进度消息
|
||||
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 = '<span>📋 过程详情</span>';
|
||||
} else {
|
||||
timeline.classList.add('expanded');
|
||||
if (btn) btn.innerHTML = '<span>📋 收起详情</span>';
|
||||
}
|
||||
} else if (timeline) {
|
||||
// 如果只有timeline,直接切换
|
||||
if (timeline.classList.contains('expanded')) {
|
||||
timeline.classList.remove('expanded');
|
||||
if (btn) btn.innerHTML = '<span>📋 过程详情</span>';
|
||||
} else {
|
||||
timeline.classList.add('expanded');
|
||||
if (btn) btn.innerHTML = '<span>📋 收起详情</span>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 将进度消息转换为可折叠的详情组件
|
||||
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 = `
|
||||
<div class="progress-header">
|
||||
<span class="progress-title">📋 渗透测试详情</span>
|
||||
${hasContent ? `<button class="progress-toggle" onclick="toggleProgressDetails('${detailsId}')">收起详情</button>` : ''}
|
||||
</div>
|
||||
${hasContent ? `<div class="progress-timeline expanded" id="${detailsId}-timeline">${timelineHTML}</div>` : '<div class="progress-timeline-empty">暂无过程详情(可能执行过快或未触发详细事件)</div>'}
|
||||
`;
|
||||
|
||||
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 = `🔧 正在调用工具: <strong>${escapeHtml(toolName)}</strong> (${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} 工具 <strong>${escapeHtml(resultToolName)}</strong> 执行${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 = `
|
||||
<div class="timeline-item-header">
|
||||
<span class="timeline-item-time">${time}</span>
|
||||
<span class="timeline-item-title">${options.title}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 根据类型添加详细内容
|
||||
if (type === 'thinking' && options.message) {
|
||||
content += `<div class="timeline-item-content">${formatMarkdown(options.message)}</div>`;
|
||||
} else if (type === 'tool_call' && options.data) {
|
||||
const data = options.data;
|
||||
const args = data.argumentsObj || (data.arguments ? JSON.parse(data.arguments) : {});
|
||||
content += `
|
||||
<div class="timeline-item-content">
|
||||
<div class="tool-details">
|
||||
<div class="tool-arg-section">
|
||||
<strong>参数:</strong>
|
||||
<pre class="tool-args">${JSON.stringify(args, null, 2)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else if (type === 'tool_result' && options.data) {
|
||||
const data = options.data;
|
||||
const isError = data.isError || !data.success;
|
||||
const result = data.result || data.error || '无结果';
|
||||
content += `
|
||||
<div class="timeline-item-content">
|
||||
<div class="tool-result-section ${isError ? 'error' : 'success'}">
|
||||
<strong>执行结果:</strong>
|
||||
<pre class="tool-result">${escapeHtml(result)}</pre>
|
||||
${data.executionId ? `<div class="tool-execution-id">执行ID: <code>${data.executionId}</code></div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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 = `<span>📋 过程详情</span>`;
|
||||
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 = '<span>📋 过程详情</span>';
|
||||
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, '<br>');
|
||||
}
|
||||
} else {
|
||||
return escapeHtml(text).replace(/\n/g, '<br>');
|
||||
}
|
||||
}
|
||||
|
||||
// 开始新对话
|
||||
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 = `
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m3 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6h14zM10 11v6M14 11v6"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
`;
|
||||
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 => {
|
||||
|
||||
Reference in New Issue
Block a user