diff --git a/internal/database/attackchain.go b/internal/database/attackchain.go index dc3b8362..964cbfe4 100644 --- a/internal/database/attackchain.go +++ b/internal/database/attackchain.go @@ -77,7 +77,7 @@ func (db *DB) LoadAttackChainNodes(conversationID string) ([]AttackChainNode, er SELECT id, node_type, node_name, tool_execution_id, metadata, risk_score FROM attack_chain_nodes WHERE conversation_id = ? - ORDER BY created_at ASC + ORDER BY created_at ASC, rowid ASC ` rows, err := db.Query(query, conversationID) @@ -123,7 +123,7 @@ func (db *DB) LoadAttackChainEdges(conversationID string) ([]AttackChainEdge, er SELECT id, source_node_id, target_node_id, edge_type, weight FROM attack_chain_edges WHERE conversation_id = ? - ORDER BY created_at ASC + ORDER BY created_at ASC, rowid ASC ` rows, err := db.Query(query, conversationID) diff --git a/internal/database/c2.go b/internal/database/c2.go index 0965ba3d..58d92efa 100644 --- a/internal/database/c2.go +++ b/internal/database/c2.go @@ -840,7 +840,7 @@ func (db *DB) PopQueuedC2Tasks(sessionID string, limit int) ([]*C2Task, error) { created_at FROM c2_tasks WHERE session_id = ? AND (status = 'queued' AND (approval_status = '' OR approval_status = 'approved')) - ORDER BY created_at ASC + ORDER BY created_at ASC, rowid ASC LIMIT ? ` rows, err := tx.Query(query, sessionID, limit) diff --git a/internal/database/conversation.go b/internal/database/conversation.go index c0f1c422..7e72a5a6 100644 --- a/internal/database/conversation.go +++ b/internal/database/conversation.go @@ -604,7 +604,7 @@ func (db *DB) UpdateAssistantMessageFinalize(messageID, content string, mcpExecu // GetMessages 获取对话的所有消息 func (db *DB) GetMessages(conversationID string) ([]Message, error) { rows, err := db.Query( - "SELECT id, conversation_id, role, content, reasoning_content, mcp_execution_ids, created_at, updated_at FROM messages WHERE conversation_id = ? ORDER BY created_at ASC", + "SELECT id, conversation_id, role, content, reasoning_content, mcp_execution_ids, created_at, updated_at FROM messages WHERE conversation_id = ? ORDER BY created_at ASC, rowid ASC", conversationID, ) if err != nil { @@ -799,7 +799,7 @@ func (db *DB) AddProcessDetail(messageID, conversationID, eventType, message str // 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", + "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", messageID, ) if err != nil { @@ -835,7 +835,7 @@ func (db *DB) GetProcessDetails(messageID string) ([]ProcessDetail, error) { // 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", + "SELECT id, message_id, conversation_id, event_type, message, data, created_at FROM process_details WHERE conversation_id = ? ORDER BY created_at ASC, rowid ASC", conversationID, ) if err != nil { diff --git a/internal/database/project_dashboard.go b/internal/database/project_dashboard.go new file mode 100644 index 00000000..e4408fdf --- /dev/null +++ b/internal/database/project_dashboard.go @@ -0,0 +1,91 @@ +package database + +import ( + "fmt" + "strings" + "time" +) + +// ProjectDashboardFact 仪表盘跨项目近期事实条目。 +type ProjectDashboardFact struct { + ID string `json:"id"` + ProjectID string `json:"project_id"` + ProjectName string `json:"project_name"` + FactKey string `json:"fact_key"` + Category string `json:"category"` + Summary string `json:"summary"` + Confidence string `json:"confidence"` + Pinned bool `json:"pinned"` + UpdatedAt time.Time `json:"updated_at"` +} + +// ProjectDashboardTotals 仪表盘项目事实汇总计数。 +type ProjectDashboardTotals struct { + ActiveProjects int `json:"active_projects"` + TotalFacts int `json:"total_facts"` +} + +// ProjectDashboardSummary 仪表盘项目情报摘要。 +type ProjectDashboardSummary struct { + RecentFacts []ProjectDashboardFact `json:"recent_facts"` + Totals ProjectDashboardTotals `json:"totals"` +} + +// GetProjectDashboardSummary 聚合跨项目近期事实(仅活跃项目、排除 deprecated)。 +func (db *DB) GetProjectDashboardSummary(factLimit int) (*ProjectDashboardSummary, error) { + if factLimit <= 0 { + factLimit = 5 + } + if factLimit > 50 { + factLimit = 50 + } + + out := &ProjectDashboardSummary{ + RecentFacts: []ProjectDashboardFact{}, + } + + if err := db.QueryRow(`SELECT COUNT(*) FROM projects WHERE status = 'active'`).Scan(&out.Totals.ActiveProjects); err != nil { + return nil, fmt.Errorf("统计活跃项目失败: %w", err) + } + if err := db.QueryRow( + `SELECT COUNT(*) FROM project_facts f + INNER JOIN projects p ON p.id = f.project_id + WHERE f.confidence != 'deprecated' AND p.status = 'active'`, + ).Scan(&out.Totals.TotalFacts); err != nil { + return nil, fmt.Errorf("统计事实失败: %w", err) + } + + rows, err := db.Query( + `SELECT f.id, f.project_id, p.name, f.fact_key, f.category, f.summary, f.confidence, f.pinned, f.updated_at + FROM project_facts f + INNER JOIN projects p ON p.id = f.project_id + WHERE f.confidence != 'deprecated' AND p.status = 'active' + ORDER BY f.pinned DESC, f.updated_at DESC + LIMIT ?`, + factLimit, + ) + if err != nil { + return nil, fmt.Errorf("查询近期事实失败: %w", err) + } + defer rows.Close() + + for rows.Next() { + var item ProjectDashboardFact + var pinned int + var updatedAt string + if err := rows.Scan( + &item.ID, &item.ProjectID, &item.ProjectName, &item.FactKey, + &item.Category, &item.Summary, &item.Confidence, &pinned, &updatedAt, + ); err != nil { + return nil, err + } + item.Pinned = pinned != 0 + item.ProjectName = strings.TrimSpace(item.ProjectName) + item.UpdatedAt = parseDBTime(updatedAt) + out.RecentFacts = append(out.RecentFacts, item) + } + if err := rows.Err(); err != nil { + return nil, err + } + return out, nil +}