feat(findings): consolidate duplicate reports into expandable cases

This commit is contained in:
ji ruo committed 2026-10-09 10:43:53 +08:00
1 parent a91b27feb7
commit 24d4726aee
29 files changed
+3051 -63

No files matched your search

+42
View File
@@ -131,6 +131,8 @@ type provEntry struct {
// taskID + mergeable let the drainer coalesce several event triggers (finding/goal)
// from the SAME task into one conversation before it starts (interval fires don't merge).
type triggeredRun struct {
conversationID int64 // pre-created manual finding review; never coalesced
agentKey string
title string
message string // 事件正文(触发语 + 工具/入参/返回等);不含任务描述/目标头
@@ -755,6 +757,16 @@ func (s *Server) Handler() http.Handler {
mux.HandleFunc("GET /api/exploration/frontier", s.frontier)
mux.HandleFunc("GET /api/exploration/findings", s.findings)
mux.HandleFunc("GET /api/exploration/findings/groups", s.findingGroups)
mux.HandleFunc("GET /api/exploration/finding-cases", s.listFindingCases)
mux.HandleFunc("POST /api/exploration/finding-cases", s.mergeFindingCase)
mux.HandleFunc("GET /api/exploration/finding-cases/{id}", s.getFindingCase)
mux.HandleFunc("GET /api/exploration/finding-cases/{id}/members", s.getFindingCaseMembers)
mux.HandleFunc("DELETE /api/exploration/finding-cases/{id}/members/{fid}", s.removeFindingCaseMember)
mux.HandleFunc("PUT /api/exploration/finding-cases/{id}/report", s.updateCaseReport)
mux.HandleFunc("GET /api/exploration/finding-case-suggestions", s.caseSuggestions)
mux.HandleFunc("POST /api/exploration/finding-case-suggestions/{id}", s.resolveCaseSuggestion)
mux.HandleFunc("POST /api/exploration/finding-case-review", s.reviewFindingCases)
mux.HandleFunc("GET /api/exploration/finding-case-review", s.caseReviewRuns)
mux.HandleFunc("GET /api/exploration/findings/asset-tree", s.findingAssetTree)
mux.HandleFunc("GET /api/exploration/findings/stats", s.findingStats)
mux.HandleFunc("GET /api/exploration/findings/export", s.findingsExport)
@@ -1040,6 +1052,7 @@ func (s *Server) listTasks(w http.ResponseWriter, r *http.Request) {
}
list := s.m.List()
metrics, _ := s.m.PG().TaskListMetricsAll()
distinct, _ := s.m.pg.DistinctFindingStatsByTask()
archiveBlockers, _ := s.m.PG().TaskArchiveBlockers()
dtos := make([]TaskDTO, 0, len(list))
for _, t := range list {
@@ -1056,6 +1069,7 @@ func (s *Server) listTasks(w http.ResponseWriter, r *http.Request) {
dto.GoalsTotal = metric.Goals.Total
dto.GoalsMet = metric.Goals.Met
dto.InFlight = metric.RunningIntents
dto.DistinctFindings = distinct[t.ID]
dto.Findings = FindingSeverityDTO{
Critical: metric.Findings.Critical,
High: metric.Findings.High,
@@ -2052,6 +2066,11 @@ func (s *Server) findingStats(w http.ResponseWriter, r *http.Request) {
writeErr(w, 500, err.Error())
return
}
st.Distinct, err = s.m.pg.DistinctFindingStats("")
if err != nil {
writeErr(w, 500, err.Error())
return
}
writeJSON(w, 200, st)
}
@@ -2074,6 +2093,14 @@ func (s *Server) getFinding(w http.ResponseWriter, r *http.Request) {
}
assets := s.resolveAssetIDs(f.AssetIDs)
dto := findingFromDB(f, assets)
cid, err := s.m.pg.FindingCaseID(f.ID)
if err != nil {
writeErr(w, 500, err.Error())
return
}
if cid > 0 {
dto.CaseID = i64s(cid)
}
if contextTaskID := strings.TrimSpace(r.URL.Query().Get("context_task")); contextTaskID != "" {
contextTask := s.m.ResolveTask(contextTaskID)
if contextTask == nil {
@@ -2138,6 +2165,14 @@ func (s *Server) findingsExport(w http.ResponseWriter, r *http.Request) {
return
}
var casePlan *findingCaseExportPlan
if q.Get("mode") == "consolidated" {
fs, casePlan, err = s.prepareFindingCaseExport(fs)
if err != nil {
writeErr(w, 500, err.Error())
return
}
}
stage, err := os.MkdirTemp("", "artex-finding-export-")
if err != nil {
writeErr(w, 500, err.Error())
@@ -2148,6 +2183,13 @@ func (s *Server) findingsExport(w http.ResponseWriter, r *http.Request) {
evidenceError(w, err)
return
}
if casePlan != nil {
fs, err = s.consolidateFindingCaseExport(fs, casePlan, q.Get("include_originals") == "true")
if err != nil {
writeErr(w, 500, err.Error())
return
}
}
now := time.Now()
stamp := now.Format("20060102-150405")
setDownload := func(contentType, filename string) {