diff --git a/db/findings.go b/db/findings.go index 35ccb93..d4b3986 100644 --- a/db/findings.go +++ b/db/findings.go @@ -23,20 +23,21 @@ type DBFinding struct { ReportEvidenceVersion int64 TrafficBindings []FindingTrafficBinding // populated only for export - ID int64 - TaskID *int64 - NodeID *int64 - VulnClass string - Name string // 漏洞名称(可读标题);为空时前端回退展示 VulnClass - Severity string - Summary string - Evidence string - Worker string - AssetIDs []int64 - Status string - Report string // 详细报告(Markdown);仅 GetFinding 填充,列表查询不带 - CreatedAt time.Time - TaskDescription string // populated via LEFT JOIN on tasks + ID int64 + TaskID *int64 + NodeID *int64 + VulnClass string + Name string // 漏洞名称(可读标题);为空时前端回退展示 VulnClass + Severity string + Summary string + Evidence string + Worker string + AssetIDs []int64 + Status string + Report string // 详细报告(Markdown);仅 GetFinding 填充,列表查询不带 + ReportConversationID *int64 `json:"report_conversation_id,omitempty"` + CreatedAt time.Time + TaskDescription string // populated via LEFT JOIN on tasks } // Finding triage states (findings.status). @@ -106,9 +107,9 @@ func (d *DB) AddFinding(taskID, nodeID int64, vulnclass, name, severity, summary // findingSelectCols is the column list (with task_description join) every finding // list query selects, so scanFinding stays in sync across callers. const findingSelectCols = `f.id, f.task_id, f.node_id, f.vulnclass, COALESCE(f.name, ''), f.severity, f.summary, - f.evidence, f.worker, f.asset_ids, COALESCE(f.status, 'pending'), f.created_at, + f.evidence, f.worker, f.asset_ids, COALESCE(f.status, 'pending'), f.created_at, COALESCE(t.description, '') AS task_description, f.evidence_version, f.report_evidence_version, - (SELECT count(*) FROM finding_traffic_bindings b WHERE b.finding_id=f.id)` + (SELECT count(*) FROM finding_traffic_bindings b WHERE b.finding_id=f.id)` // scanFindings materializes rows selected via findingSelectCols. func scanFindings(rows interface { @@ -661,13 +662,14 @@ func (d *DB) FindingStats() (*FindingStats, error) { func (d *DB) GetFinding(id int64) (*DBFinding, error) { f := &DBFinding{} var aidsJSON string - err := d.QueryRow(`SELECT `+findingSelectCols+`, COALESCE(f.report, '') + var reportConv sql.NullInt64 + err := d.QueryRow(`SELECT `+findingSelectCols+`, COALESCE(f.report, ''), f.report_conversation_id FROM findings f LEFT JOIN tasks t ON f.task_id = t.id WHERE f.id = $1`, id).Scan( &f.ID, &f.TaskID, &f.NodeID, &f.VulnClass, &f.Name, &f.Severity, &f.Summary, &f.Evidence, &f.Worker, &aidsJSON, &f.Status, &f.CreatedAt, - &f.TaskDescription, &f.EvidenceVersion, &f.ReportEvidenceVersion, &f.TrafficCount, &f.Report) + &f.TaskDescription, &f.EvidenceVersion, &f.ReportEvidenceVersion, &f.TrafficCount, &f.Report, &reportConv) if err == sql.ErrNoRows { return nil, nil } @@ -675,9 +677,17 @@ func (d *DB) GetFinding(id int64) (*DBFinding, error) { return nil, err } _ = json.Unmarshal([]byte(aidsJSON), &f.AssetIDs) + if reportConv.Valid { + f.ReportConversationID = &reportConv.Int64 + } return f, nil } +func (d *DB) SetFindingReportConversationID(findingID, conversationID int64) error { + _, err := d.Exec(`UPDATE findings SET report_conversation_id=$2 WHERE id=$1`, findingID, conversationID) + return err +} + // DeleteFinding removes a finding entirely: the standalone findings row and its // originating exploration node (kind='finding'), so it disappears from the findings // list, the per-task 发现 Tab, and the exploration graph alike. Deleting the node diff --git a/db/schema.sql b/db/schema.sql index 7599e7c..593ef41 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -1162,11 +1162,13 @@ CREATE TABLE IF NOT EXISTS findings ( status TEXT NOT NULL DEFAULT 'pending', -- 漏洞详细报告(Markdown);默认空,仅详情页读取/展示,不进列表接口以免 payload 膨胀。 report TEXT NOT NULL DEFAULT '', + report_conversation_id BIGINT REFERENCES conversations(id) ON DELETE SET NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); ALTER TABLE findings ADD COLUMN IF NOT EXISTS status TEXT NOT NULL DEFAULT 'pending'; ALTER TABLE findings ADD COLUMN IF NOT EXISTS name TEXT NOT NULL DEFAULT ''; ALTER TABLE findings ADD COLUMN IF NOT EXISTS report TEXT NOT NULL DEFAULT ''; +ALTER TABLE findings ADD COLUMN IF NOT EXISTS report_conversation_id BIGINT REFERENCES conversations(id) ON DELETE SET NULL; CREATE INDEX IF NOT EXISTS idx_findings_task ON findings(task_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_findings_time ON findings(created_at DESC); CREATE INDEX IF NOT EXISTS idx_findings_status ON findings(status, created_at DESC); diff --git a/server/conversations.go b/server/conversations.go index 69e3839..54113cf 100644 --- a/server/conversations.go +++ b/server/conversations.go @@ -701,6 +701,14 @@ func (s *Server) readTriggerBehavior(agentKey string) triggerBehavior { // merging by task / all / none); parallel → run each fire in its own concurrent // conversation up to trigger_max_parallel. Distinct agents always run concurrently. func (s *Server) StartTriggeredRun(agentKey, title, message string, taskID int64, mergeable bool, taskDesc, taskGoal string) { + s.startTriggeredRun(agentKey, title, message, taskID, mergeable, taskDesc, taskGoal, 0) +} + +func (s *Server) StartTriggeredRunWithConversation(agentKey, title, message string, taskID int64, mergeable bool, taskDesc, taskGoal string, conversationID int64) { + s.startTriggeredRun(agentKey, title, message, taskID, mergeable, taskDesc, taskGoal, conversationID) +} + +func (s *Server) startTriggeredRun(agentKey, title, message string, taskID int64, mergeable bool, taskDesc, taskGoal string, conversationID int64) { if s.m.pg == nil || s.chatAgentRef() == nil { return } @@ -714,7 +722,7 @@ func (s *Server) StartTriggeredRun(agentKey, title, message string, taskID int64 cfg := s.readTriggerBehavior(agentKey) // DB read BEFORE the lock (never under queueMu) s.queueMu.Lock() s.triggerCfg[agentKey] = cfg - s.triggerQ[agentKey] = append(s.triggerQ[agentKey], triggeredRun{agentKey: agentKey, title: title, message: message, taskID: taskID, taskDesc: taskDesc, taskGoal: taskGoal, mergeable: mergeable}) + s.triggerQ[agentKey] = append(s.triggerQ[agentKey], triggeredRun{conversationID: conversationID, agentKey: agentKey, title: title, message: message, taskID: taskID, taskDesc: taskDesc, taskGoal: taskGoal, mergeable: mergeable}) s.pumpLocked(agentKey) s.queueMu.Unlock() } diff --git a/server/finding_report.go b/server/finding_report.go index ca7ef81..e63d5ee 100644 --- a/server/finding_report.go +++ b/server/finding_report.go @@ -58,20 +58,30 @@ func (s *Server) generateFindingReport(w http.ResponseWriter, r *http.Request) { } } message := manualFindingReportMessage(f, taskID) - s.StartTriggeredRun( + conversation, err := s.m.pg.CreateConversation("reporter", fmt.Sprintf("漏洞报告 · finding#%d", f.ID), nil) + if err != nil { + writeErr(w, http.StatusInternalServerError, "创建报告对话失败: "+err.Error()) + return + } + if err := s.m.pg.SetFindingReportConversationID(f.ID, conversation.ID); err != nil { + writeErr(w, http.StatusInternalServerError, "关联报告对话失败: "+err.Error()) + return + } + s.StartTriggeredRunWithConversation( "reporter", fmt.Sprintf("人工生成步骤与报告 · finding#%d", f.ID), message, taskID, false, taskDesc, - taskGoal, + taskGoal, conversation.ID, ) writeJSON(w, http.StatusAccepted, map[string]any{ "queued": true, "agent_key": "reporter", "finding_id": f.ID, "finding_node_id": *f.NodeID, + "conversation_id": conversation.ID, }) } diff --git a/web/src/app/(main)/function/findings/detail/page.tsx b/web/src/app/(main)/function/findings/detail/page.tsx index bc9455c..f8db407 100644 --- a/web/src/app/(main)/function/findings/detail/page.tsx +++ b/web/src/app/(main)/function/findings/detail/page.tsx @@ -424,6 +424,7 @@ function FindingDetailInner() { agentLabel="报告撰写 Agent" promptPlaceholder="描述要如何调整漏洞报告…" modal + initialConversationId={finding.report_conversation_id} open={reportChatOpen} onOpenChange={setReportChatOpen} /> diff --git a/web/src/components/finding-auto-chat.tsx b/web/src/components/finding-auto-chat.tsx index a3ff0f7..c92c016 100644 --- a/web/src/components/finding-auto-chat.tsx +++ b/web/src/components/finding-auto-chat.tsx @@ -26,6 +26,7 @@ export function FindingAutoChat({ agentLabel = "Auto Agent", promptPlaceholder = "询问这个漏洞…", modal = false, + initialConversationId, open, onOpenChange, }: { @@ -35,6 +36,7 @@ export function FindingAutoChat({ agentLabel?: string; promptPlaceholder?: string; modal?: boolean; + initialConversationId?: number; open: boolean; onOpenChange: (open: boolean) => void; }) { @@ -70,6 +72,10 @@ export function FindingAutoChat({ React.useEffect(() => { if (!open || conversation || manualEmpty) return; + if (initialConversationId && initialConversationId > 0) { + setConversation({ id: initialConversationId, agent_key: agentKey, title: `漏洞 #${findingId} · ${safeTitle}`, created_at: "", updated_at: "" }); + return; + } const storedID = Number(window.localStorage.getItem(storageKey)); if (Number.isSafeInteger(storedID) && storedID > 0) { setConversation({ @@ -82,7 +88,7 @@ export function FindingAutoChat({ return; } void createConversation(); - }, [conversation, createConversation, manualEmpty, open, safeTitle, storageKey]); + }, [conversation, createConversation, initialConversationId, manualEmpty, open, safeTitle, storageKey]); React.useEffect(() => { if (!historyOpen && !running) return; diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index 33de71f..e3757c1 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -529,6 +529,7 @@ export interface Finding { summary: string; evidence: string; report?: string; // 详细报告(Markdown);仅详情接口返回,列表为空 + report_conversation_id?: number; intent_id?: string; param_id?: string; task_id?: string;