From 5d77a992364256266b28484dcf390cbdb138cefd 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, 23 Jun 2026 21:01:35 +0800 Subject: [PATCH] Add files via upload --- internal/database/conversation.go | 161 +++++++++++++++++++++++++++++- internal/handler/conversation.go | 58 ++++++++++- 2 files changed, 213 insertions(+), 6 deletions(-) diff --git a/internal/database/conversation.go b/internal/database/conversation.go index 94458f02..507a54ab 100644 --- a/internal/database/conversation.go +++ b/internal/database/conversation.go @@ -352,8 +352,8 @@ func (db *DB) GetConversationLite(id string) (*Conversation, error) { conv.Pinned = pinned != 0 - // 加载消息(不加载 process_details) - messages, err := db.GetMessages(id) + // 加载消息(不加载 process_details / reasoning_content,减少历史会话切换 payload) + messages, err := db.GetMessagesLite(id) if err != nil { return nil, fmt.Errorf("加载消息失败: %w", err) } @@ -835,6 +835,62 @@ func (db *DB) GetMessages(conversationID string) ([]Message, error) { return messages, nil } +// GetMessagesLite 获取对话消息(不含 reasoning_content),用于历史会话快速切换。 +func (db *DB) GetMessagesLite(conversationID string) ([]Message, error) { + rows, err := db.Query( + "SELECT id, conversation_id, role, content, mcp_execution_ids, created_at, updated_at FROM messages WHERE conversation_id = ? ORDER BY created_at ASC, rowid ASC", + conversationID, + ) + if err != nil { + return nil, fmt.Errorf("查询消息失败: %w", err) + } + defer rows.Close() + + var messages []Message + for rows.Next() { + var msg Message + var mcpIDsJSON sql.NullString + var createdAt string + var updatedAt sql.NullString + + if err := rows.Scan(&msg.ID, &msg.ConversationID, &msg.Role, &msg.Content, &mcpIDsJSON, &createdAt, &updatedAt); err != nil { + return nil, fmt.Errorf("扫描消息失败: %w", err) + } + + var err error + msg.CreatedAt, err = time.Parse("2006-01-02 15:04:05.999999999-07:00", createdAt) + if err != nil { + msg.CreatedAt, err = time.Parse("2006-01-02 15:04:05", createdAt) + } + if err != nil { + msg.CreatedAt, _ = time.Parse(time.RFC3339, createdAt) + } + + if updatedAt.Valid && strings.TrimSpace(updatedAt.String) != "" { + msg.UpdatedAt, err = time.Parse("2006-01-02 15:04:05.999999999-07:00", updatedAt.String) + if err != nil { + msg.UpdatedAt, err = time.Parse("2006-01-02 15:04:05", updatedAt.String) + } + if err != nil { + msg.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt.String) + } + } + if msg.UpdatedAt.IsZero() { + msg.UpdatedAt = msg.CreatedAt + } + + if mcpIDsJSON.Valid && mcpIDsJSON.String != "" { + if err := json.Unmarshal([]byte(mcpIDsJSON.String), &msg.MCPExecutionIDs); err != nil { + db.logger.Warn("解析MCP执行ID失败", zap.Error(err)) + } + } + + messages = append(messages, msg) + } + + return messages, nil +} + // turnSliceRange 根据任意一条消息 ID 定位「一轮对话」在 msgs 中的 [start, end) 下标区间(msgs 须已按时间升序,与 GetMessages 一致)。 // 一轮 = 从某条 user 消息起,至下一条 user 之前(含中间所有 assistant)。 func turnSliceRange(msgs []Message, anchorID string) (start, end int, err error) { @@ -1003,6 +1059,107 @@ func (db *DB) GetProcessDetails(messageID string) ([]ProcessDetail, error) { return details, nil } +// ProcessDetailsSummary 过程详情摘要(用于折叠态展示,避免全量加载)。 +type ProcessDetailsSummary struct { + Total int `json:"total"` + IterationCount int `json:"iterationCount"` + MaxIteration int `json:"maxIteration"` +} + +// GetProcessDetailsSummary 统计消息的过程详情数量与迭代轮次。 +func (db *DB) GetProcessDetailsSummary(messageID string) (*ProcessDetailsSummary, error) { + var total int + if err := db.QueryRow( + "SELECT COUNT(*) FROM process_details WHERE message_id = ?", + messageID, + ).Scan(&total); err != nil { + return nil, fmt.Errorf("统计过程详情失败: %w", err) + } + + summary := &ProcessDetailsSummary{Total: total} + if total == 0 { + return summary, nil + } + + rows, err := db.Query( + "SELECT data FROM process_details WHERE message_id = ? AND event_type = 'iteration' ORDER BY created_at ASC, rowid ASC", + messageID, + ) + if err != nil { + return nil, fmt.Errorf("查询迭代详情失败: %w", err) + } + defer rows.Close() + + maxIter := 0 + iterCount := 0 + for rows.Next() { + var dataJSON string + if err := rows.Scan(&dataJSON); err != nil { + return nil, fmt.Errorf("扫描迭代详情失败: %w", err) + } + iterCount++ + if dataJSON == "" { + continue + } + var payload map[string]interface{} + if err := json.Unmarshal([]byte(dataJSON), &payload); err != nil { + continue + } + if n, ok := payload["iteration"].(float64); ok && int(n) > maxIter { + maxIter = int(n) + } + } + summary.IterationCount = iterCount + summary.MaxIteration = maxIter + return summary, nil +} + +// GetProcessDetailsPage 分页获取消息的过程详情(按时间升序)。 +func (db *DB) GetProcessDetailsPage(messageID string, limit, offset int) ([]ProcessDetail, int, error) { + var total int + if err := db.QueryRow( + "SELECT COUNT(*) FROM process_details WHERE message_id = ?", + messageID, + ).Scan(&total); err != nil { + return nil, 0, fmt.Errorf("统计过程详情失败: %w", err) + } + if total == 0 || offset >= total { + return nil, total, nil + } + + 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, rowid ASC LIMIT ? OFFSET ?", + messageID, limit, offset, + ) + if err != nil { + return nil, 0, 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, 0, fmt.Errorf("扫描过程详情失败: %w", err) + } + + var parseErr error + detail.CreatedAt, parseErr = time.Parse("2006-01-02 15:04:05.999999999-07:00", createdAt) + if parseErr != nil { + detail.CreatedAt, parseErr = time.Parse("2006-01-02 15:04:05", createdAt) + } + if parseErr != nil { + detail.CreatedAt, _ = time.Parse(time.RFC3339, createdAt) + } + + details = append(details, detail) + } + + return details, total, nil +} + // GetProcessDetailsByConversation 获取对话的所有过程详情(按消息分组) func (db *DB) GetProcessDetailsByConversation(conversationID string) (map[string][]ProcessDetail, error) { rows, err := db.Query( diff --git a/internal/handler/conversation.go b/internal/handler/conversation.go index 4cc069db..06d04888 100644 --- a/internal/handler/conversation.go +++ b/internal/handler/conversation.go @@ -176,6 +176,9 @@ func (h *ConversationHandler) GetConversation(c *gin.Context) { } // GetMessageProcessDetails 获取指定消息的过程详情(按需加载) +// 查询参数: +// - summary=1:仅返回摘要(total / iterationCount / maxIteration) +// - limit + offset:分页返回 processDetails(未指定 limit 时保持全量兼容) func (h *ConversationHandler) GetMessageProcessDetails(c *gin.Context) { messageID := c.Param("id") if messageID == "" { @@ -183,6 +186,51 @@ func (h *ConversationHandler) GetMessageProcessDetails(c *gin.Context) { return } + summaryStr := strings.TrimSpace(c.Query("summary")) + if summaryStr == "1" || strings.EqualFold(summaryStr, "true") || strings.EqualFold(summaryStr, "yes") { + summary, err := h.db.GetProcessDetailsSummary(messageID) + if err != nil { + h.logger.Error("获取过程详情摘要失败", zap.Error(err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"summary": summary}) + return + } + + limitStr := strings.TrimSpace(c.Query("limit")) + if limitStr != "" { + limit, err := strconv.Atoi(limitStr) + if err != nil || limit <= 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid limit"}) + return + } + if limit > 500 { + limit = 500 + } + offset, _ := strconv.Atoi(strings.TrimSpace(c.Query("offset"))) + if offset < 0 { + offset = 0 + } + + details, total, err := h.db.GetProcessDetailsPage(messageID, limit, offset) + if err != nil { + h.logger.Error("分页获取过程详情失败", zap.Error(err)) + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + details = database.DedupeConsecutiveProcessDetails(details) + out := processDetailsToJSON(h.logger, details) + c.JSON(http.StatusOK, gin.H{ + "processDetails": out, + "total": total, + "offset": offset, + "limit": limit, + "hasMore": offset+len(out) < total, + }) + return + } + details, err := h.db.GetProcessDetails(messageID) if err != nil { h.logger.Error("获取过程详情失败", zap.Error(err)) @@ -191,14 +239,17 @@ func (h *ConversationHandler) GetMessageProcessDetails(c *gin.Context) { } details = database.DedupeConsecutiveProcessDetails(details) + out := processDetailsToJSON(h.logger, details) + c.JSON(http.StatusOK, gin.H{"processDetails": out, "total": len(out)}) +} - // 转换为前端期望的 JSON 结构(与 GetConversation 中 processDetails 结构一致) +func processDetailsToJSON(logger *zap.Logger, details []database.ProcessDetail) []map[string]interface{} { out := make([]map[string]interface{}, 0, len(details)) for _, d := range details { var data interface{} if d.Data != "" { if err := json.Unmarshal([]byte(d.Data), &data); err != nil { - h.logger.Warn("解析过程详情数据失败", zap.Error(err)) + logger.Warn("解析过程详情数据失败", zap.Error(err)) } } out = append(out, map[string]interface{}{ @@ -211,8 +262,7 @@ func (h *ConversationHandler) GetMessageProcessDetails(c *gin.Context) { "createdAt": d.CreatedAt, }) } - - c.JSON(http.StatusOK, gin.H{"processDetails": out}) + return out } // UpdateConversationRequest 更新对话请求