diff --git a/db/findings.go b/db/findings.go index ca7ff81..efc1931 100644 --- a/db/findings.go +++ b/db/findings.go @@ -705,7 +705,11 @@ func (d *DB) ResolveFindingReportConversation(findingID int64) (*int64, error) { a.detail ILIKE '%finding_id=' || $1::text || '%' OR a.detail ILIKE '%"finding_id":' || $1::text || '%' ) - ORDER BY c.updated_at DESC, c.id DESC LIMIT 1`, findingID).Scan(&id) + ORDER BY CASE WHEN EXISTS ( + SELECT 1 FROM conversation_activities r + WHERE r.conversation_id=c.id AND (r.tool='update_finding_report' OR r.summary ILIKE 'finding % report updated%') + ) THEN 0 ELSE 1 END, + c.created_at ASC, c.id ASC LIMIT 1`, findingID).Scan(&id) if err == sql.ErrNoRows { return nil, nil } diff --git a/server/dto.go b/server/dto.go index fabfd65..f1c058e 100644 --- a/server/dto.go +++ b/server/dto.go @@ -325,15 +325,16 @@ type FindingDTO struct { ReportStale bool `json:"report_stale"` TrafficBindings []db.FindingTrafficBinding `json:"traffic_bindings,omitempty"` - ID string `json:"id"` - FindingID string `json:"finding_id,omitempty"` // standalone findings-table id — the handle for status updates - VulnClass string `json:"vulnclass"` - Name string `json:"name,omitempty"` // 漏洞名称;为空时前端回退展示 vulnclass - Severity string `json:"severity"` // critical | high | medium | low - Status string `json:"status"` // pending | in_progress | confirmed | resolved | fixed | false_positive | ignored | duplicate | risk_accepted - Summary string `json:"summary"` - Evidence string `json:"evidence"` - Report string `json:"report,omitempty"` // 详细报告(Markdown);仅详情接口返回,列表为空 + ID string `json:"id"` + FindingID string `json:"finding_id,omitempty"` // standalone findings-table id — the handle for status updates + VulnClass string `json:"vulnclass"` + Name string `json:"name,omitempty"` // 漏洞名称;为空时前端回退展示 vulnclass + Severity string `json:"severity"` // critical | high | medium | low + Status string `json:"status"` // pending | in_progress | confirmed | resolved | fixed | false_positive | ignored | duplicate | risk_accepted + Summary string `json:"summary"` + Evidence string `json:"evidence"` + Report string `json:"report,omitempty"` // 详细报告(Markdown);仅详情接口返回,列表为空 + ReportConversationID *int64 `json:"report_conversation_id,omitempty"` IntentID string `json:"intent_id,omitempty"` ParamID string `json:"param_id,omitempty"` @@ -464,14 +465,15 @@ func findingFromDB(f *db.DBFinding, assets map[int64]*db.Asset) FindingDTO { FindingID: i64s(f.ID), TrafficCount: f.TrafficCount, EvidenceVersion: f.EvidenceVersion, ReportEvidenceVersion: f.ReportEvidenceVersion, ReportStale: f.Report != "" && f.EvidenceVersion != f.ReportEvidenceVersion, TrafficBindings: f.TrafficBindings, - VulnClass: f.VulnClass, - Name: f.Name, - Severity: f.Severity, - Status: status, - Summary: f.Summary, - Evidence: f.Evidence, - Report: f.Report, - TS: rfc3339(f.CreatedAt), + VulnClass: f.VulnClass, + Name: f.Name, + Severity: f.Severity, + Status: status, + Summary: f.Summary, + Evidence: f.Evidence, + Report: f.Report, + ReportConversationID: f.ReportConversationID, + TS: rfc3339(f.CreatedAt), } if f.ExportCaseID > 0 { d.ID = "case:" + i64s(f.ExportCaseID) diff --git a/web/src/components/finding-auto-chat.tsx b/web/src/components/finding-auto-chat.tsx index 6e864e4..784aa7f 100644 --- a/web/src/components/finding-auto-chat.tsx +++ b/web/src/components/finding-auto-chat.tsx @@ -51,6 +51,7 @@ export function FindingAutoChat({ const [historyOpen, setHistoryOpen] = React.useState(false); const [history, setHistory] = React.useState([]); const cursor = React.useRef(0); + const chatViewportRef = React.useRef(null); const safeTitle = findingTitle.replace(/[\[\]\r\n]/g, " ").trim().slice(0, 100); const storageKey = `artex.finding.${agentKey}-conversation.${findingId}`; @@ -136,6 +137,18 @@ export function FindingAutoChat({ }; }, [conversation, open, storageKey]); + React.useEffect(() => { + if (!open || !conversation || loading || !messages.length) return; + const viewport = chatViewportRef.current; + if (!viewport) return; + const key = `artex.conversation-scroll.${conversation.id}`; + const saved = window.sessionStorage.getItem(key); + requestAnimationFrame(() => { + if (saved != null) viewport.scrollTop = Number(saved) || 0; + else viewport.scrollTop = viewport.scrollHeight; + }); + }, [conversation, loading, messages.length, open]); + React.useEffect(() => { if (!open || !conversation || !running) return; let live = true; @@ -271,7 +284,13 @@ export function FindingAutoChat({ -
+
{ + if (conversation) window.sessionStorage.setItem(`artex.conversation-scroll.${conversation.id}`, String(event.currentTarget.scrollTop)); + }} + className="min-h-0 min-w-0 flex-1 overflow-x-hidden overflow-y-auto p-3" + >
{loading ? (