diff --git a/db/exploration.go b/db/exploration.go index e5b0fd5..4b6aef3 100644 --- a/db/exploration.go +++ b/db/exploration.go @@ -1293,13 +1293,17 @@ func (d *DB) LastActivityAll() (map[int64]int64, error) { // GoalCounts is the goal summary for one exploration. type GoalCounts struct{ Total, Met int } +// FindingSeverityCounts breaks a task's findings down by severity for the task +// list (severity 白名单外/为空的记录不计入任一档)。 +type FindingSeverityCounts struct{ Critical, High, Medium, Low int } + // TaskListMetrics contains the aggregates rendered in task lists. type TaskListMetrics struct { Tokens TokenUsage LastActivity int64 Goals GoalCounts - RunningIntents int // kind='intent' 且 state='running' 的条数,即运行中 Worker 数 - Findings int // findings 表里该任务的漏洞条数 + RunningIntents int // kind='intent' 且 state='running' 的条数,即运行中 Worker 数 + Findings FindingSeverityCounts // findings 表里该任务的漏洞数(按严重度分档) } // TaskListMetricsAll returns list aggregates for every live task in one query. @@ -1316,7 +1320,10 @@ func (d *DB) TaskListMetricsAll() (map[int64]TaskListMetrics, error) { COALESCE(goal_metrics.total,0), COALESCE(goal_metrics.met,0), COALESCE(intent_metrics.running,0), - COALESCE(finding_metrics.findings,0) + COALESCE(finding_metrics.critical,0), + COALESCE(finding_metrics.high,0), + COALESCE(finding_metrics.medium,0), + COALESCE(finding_metrics.low,0) FROM tasks task LEFT JOIN LATERAL ( SELECT SUM(input_tokens) AS input_tokens, @@ -1345,7 +1352,10 @@ func (d *DB) TaskListMetricsAll() (map[int64]TaskListMetrics, error) { WHERE exploration_id=task.exploration_id AND kind='intent' AND state='running' ) intent_metrics ON true LEFT JOIN LATERAL ( - SELECT COUNT(*) AS findings + SELECT COUNT(*) FILTER (WHERE severity='critical') AS critical, + COUNT(*) FILTER (WHERE severity='high') AS high, + COUNT(*) FILTER (WHERE severity='medium') AS medium, + COUNT(*) FILTER (WHERE severity='low') AS low FROM findings WHERE task_id=task.id ) finding_metrics ON true @@ -1368,7 +1378,10 @@ func (d *DB) TaskListMetricsAll() (map[int64]TaskListMetrics, error) { &metrics.Goals.Total, &metrics.Goals.Met, &metrics.RunningIntents, - &metrics.Findings, + &metrics.Findings.Critical, + &metrics.Findings.High, + &metrics.Findings.Medium, + &metrics.Findings.Low, ); err != nil { return nil, err } diff --git a/server/dto.go b/server/dto.go index de51334..38c5211 100644 --- a/server/dto.go +++ b/server/dto.go @@ -30,37 +30,45 @@ func rawString(raw json.RawMessage) string { // ---- Task (frontend "Task") ---- created_at as RFC3339, plus a derived status. type TaskDTO struct { - ID string `json:"id"` - ExplorationID int64 `json:"exploration_id"` - Name string `json:"name"` // 可选任务名称;空=未命名 - CategoryID *int64 `json:"category_id,omitempty"` - CategoryName string `json:"category_name,omitempty"` - Pinned bool `json:"pinned"` - PinnedAt string `json:"pinned_at,omitempty"` - Description string `json:"description"` - Goal string `json:"goal"` - Status string `json:"status"` // created | running | paused | done | failed - CreatedAt string `json:"created_at"` - CreatedUnix int64 `json:"created_unix"` // created_at as unix seconds (for run-duration calc) - CompletedAt string `json:"completed_at"` // RFC3339 finish time (done/failed); "" if unfinished - CompletedUnix int64 `json:"completed_unix"` // completed_at as unix seconds (0 if unfinished) - LastActivity int64 `json:"last_activity_unix"` // unix seconds of the last activity (0 if none) - Paused bool `json:"paused"` - Queued bool `json:"queued"` - Tokens TokenTotalDTO `json:"tokens"` // whole-task token consumption - GoalsTotal int `json:"goals_total"` - GoalsMet int `json:"goals_met"` - InFlight int `json:"in_flight"` // 运行中 Worker 数(state=running 的意图) - Findings int `json:"findings"` // 该任务已登记的漏洞数(findings 表) - LLMProfileID *int64 `json:"llm_profile_id,omitempty"` // LLM profile used for this task; nil = default - LLMProfileIDs []int64 `json:"llm_profile_ids"` - ActiveLLMProfileID *int64 `json:"active_llm_profile_id,omitempty"` - LLMFailoverState string `json:"llm_failover_state"` - LLMFailoverReason string `json:"llm_failover_reason,omitempty"` - SourceTaskIDs []string `json:"source_task_ids"` - ArchiveBlockedBy string `json:"archive_blocked_by_task_id,omitempty"` - CompanyIDs []int64 `json:"company_ids"` - CoverageEnabled bool `json:"coverage_enabled"` // 资产覆盖度功能开关(创建时定) + ID string `json:"id"` + ExplorationID int64 `json:"exploration_id"` + Name string `json:"name"` // 可选任务名称;空=未命名 + CategoryID *int64 `json:"category_id,omitempty"` + CategoryName string `json:"category_name,omitempty"` + Pinned bool `json:"pinned"` + PinnedAt string `json:"pinned_at,omitempty"` + Description string `json:"description"` + Goal string `json:"goal"` + Status string `json:"status"` // created | running | paused | done | failed + CreatedAt string `json:"created_at"` + CreatedUnix int64 `json:"created_unix"` // created_at as unix seconds (for run-duration calc) + CompletedAt string `json:"completed_at"` // RFC3339 finish time (done/failed); "" if unfinished + CompletedUnix int64 `json:"completed_unix"` // completed_at as unix seconds (0 if unfinished) + LastActivity int64 `json:"last_activity_unix"` // unix seconds of the last activity (0 if none) + Paused bool `json:"paused"` + Queued bool `json:"queued"` + Tokens TokenTotalDTO `json:"tokens"` // whole-task token consumption + GoalsTotal int `json:"goals_total"` + GoalsMet int `json:"goals_met"` + InFlight int `json:"in_flight"` // 运行中 Worker 数(state=running 的意图) + Findings FindingSeverityDTO `json:"findings"` // 该任务已登记的漏洞数(findings 表,按严重度分档) + LLMProfileID *int64 `json:"llm_profile_id,omitempty"` // LLM profile used for this task; nil = default + LLMProfileIDs []int64 `json:"llm_profile_ids"` + ActiveLLMProfileID *int64 `json:"active_llm_profile_id,omitempty"` + LLMFailoverState string `json:"llm_failover_state"` + LLMFailoverReason string `json:"llm_failover_reason,omitempty"` + SourceTaskIDs []string `json:"source_task_ids"` + ArchiveBlockedBy string `json:"archive_blocked_by_task_id,omitempty"` + CompanyIDs []int64 `json:"company_ids"` + CoverageEnabled bool `json:"coverage_enabled"` // 资产覆盖度功能开关(创建时定) +} + +// FindingSeverityDTO 是任务列表里按严重度分档的漏洞计数(严重/高/中/低)。 +type FindingSeverityDTO struct { + Critical int `json:"critical"` + High int `json:"high"` + Medium int `json:"medium"` + Low int `json:"low"` } func applyTaskArchiveBlocker(dto *TaskDTO, blockers map[int64]int64) { diff --git a/server/server.go b/server/server.go index 5e6d0d9..66a26ad 100644 --- a/server/server.go +++ b/server/server.go @@ -148,7 +148,7 @@ func New(ctx context.Context, m *Manager, skillDir string, dataDir string, keyDi chatCancel: map[string]context.CancelCauseFunc{}, triggerQ: map[string][]triggeredRun{}, triggerActive: map[string]int{}, triggerCfg: map[string]triggerBehavior{}, profChatAgents: map[int64]*agent.ChatAgent{}, - provByProfile: map[int64]*provEntry{}, llmHealth: newLLMHealthRegistry(m.pg), + provByProfile: map[int64]*provEntry{}, llmHealth: newLLMHealthRegistry(m.pg), taskAgents: map[string]*taskAgentBundle{}, archiveWake: make(chan struct{}, 1)} s.initSideQuestions() // 熔断阈值/冷却是失败路径上的热参数,启动时把全局重试策略推给 Registry 一次; @@ -1040,7 +1040,12 @@ 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.Findings = metric.Findings + dto.Findings = FindingSeverityDTO{ + Critical: metric.Findings.Critical, + High: metric.Findings.High, + Medium: metric.Findings.Medium, + Low: metric.Findings.Low, + } dtos = append(dtos, dto) } writeJSON(w, 200, map[string]any{"tasks": dtos, "active": active}) diff --git a/web/src/app/(main)/function/tasks/page.tsx b/web/src/app/(main)/function/tasks/page.tsx index 89df25d..95b07b3 100644 --- a/web/src/app/(main)/function/tasks/page.tsx +++ b/web/src/app/(main)/function/tasks/page.tsx @@ -950,7 +950,9 @@ export default function TasksPage() { onSort={sortTasksBy} /> 目标进度 - 漏洞 + + 漏洞 严/高/中/低 + 运行中 Worker - {task.findings && task.findings > 0 ? ( - {task.findings} - ) : ( - 0 - )} + {(() => { + const f = task.findings; + const total = f ? f.critical + f.high + f.medium + f.low : 0; + if (!f || total === 0) return 0; + const seg = (n: number, cls: string) => ( + 0 ? cls : "text-muted-foreground"}>{n} + ); + return ( + + {seg(f.critical, "text-rose-600 dark:text-rose-400")} + / + {seg(f.high, "text-red-600 dark:text-red-400")} + / + {seg(f.medium, "text-amber-600 dark:text-amber-400")} + / + {seg(f.low, "text-slate-600 dark:text-slate-400")} + + ); + })()} {task.in_flight && task.in_flight > 0 ? ( diff --git a/web/src/lib/mock/data.ts b/web/src/lib/mock/data.ts index 1ed2d77..f4d922f 100644 --- a/web/src/lib/mock/data.ts +++ b/web/src/lib/mock/data.ts @@ -65,7 +65,7 @@ export const tasks: Task[] = [ paused: false, active: true, in_flight: 3, - findings: 4, + findings: { critical: 1, high: 2, medium: 1, low: 0 }, stalled: false, goals_total: 5, goals_met: 2, @@ -91,7 +91,7 @@ export const tasks: Task[] = [ paused: false, active: false, in_flight: 2, - findings: 2, + findings: { critical: 0, high: 1, medium: 1, low: 0 }, stalled: false, goals_total: 4, goals_met: 1, diff --git a/web/src/lib/mock/handler.ts b/web/src/lib/mock/handler.ts index da22153..476ef0f 100644 --- a/web/src/lib/mock/handler.ts +++ b/web/src/lib/mock/handler.ts @@ -1192,7 +1192,7 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re paused: false, active: true, in_flight: 0, - findings: 0, + findings: { critical: 0, high: 0, medium: 0, low: 0 }, stalled: false, goals_total: 0, goals_met: 0, diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index 7dc8e3f..e57bde6 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -23,7 +23,7 @@ export interface Task { queued?: boolean; active?: boolean; in_flight?: number; - findings?: number; // 该任务已登记的漏洞数 + findings?: { critical: number; high: number; medium: number; low: number }; // 已登记漏洞数(按严重度分档) last_activity?: string; stalled?: boolean; goals_total?: number;