From 53840faf7f62ee6c8bb0e6596003e277d15badff Mon Sep 17 00:00:00 2001 From: ji ruo <79234113+RuoJi6@users.noreply.github.com> Date: Fri, 30 Oct 2026 12:03:21 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=BD=92=E6=A1=A3=E6=8A=A5=E5=91=8A?= =?UTF-8?q?=E6=88=AA=E5=9B=BE=E5=B9=B6=E6=94=AF=E6=8C=81=E5=AF=B9=E8=AF=9D?= =?UTF-8?q?=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/orchestration.go | 5 ++ server/workspace.go | 62 +++++++++++++++++++ .../(main)/function/findings/detail/page.tsx | 16 +++++ web/src/components/finding-auto-chat.tsx | 22 ++++--- 4 files changed, 97 insertions(+), 8 deletions(-) diff --git a/server/orchestration.go b/server/orchestration.go index e7515c4..9f62f7e 100644 --- a/server/orchestration.go +++ b/server/orchestration.go @@ -474,6 +474,11 @@ func (s *Server) toolUpdateFindingReport() actool.CoreTool { if err := s.checkCaseReviewScope(ctx, "update_finding_report", findingCaseRequest{FindingID: json.RawMessage(i64s(fid))}); err != nil { return actool.Errorf(err.Error()), nil } + if f, err := s.m.pg.GetFinding(fid); err == nil && f != nil && f.TaskID != nil { + if err := s.archiveFindingScreenshots(*f.TaskID, a.Report); err != nil { + return actool.Errorf("归档报告截图失败: " + err.Error()), nil + } + } } n, err := s.m.pg.SetFindingReportVersionByNodeID(ctx, nodeID, a.Report, a.EvidenceVersion) if err != nil { diff --git a/server/workspace.go b/server/workspace.go index a0d1162..4e2b445 100644 --- a/server/workspace.go +++ b/server/workspace.go @@ -2,6 +2,7 @@ package server import ( "bytes" + "fmt" "io" "mime/multipart" "net/http" @@ -309,6 +310,67 @@ func (s *Server) findSessionPreview(rawPath string) (string, bool) { return newest, newest != "" } +// archiveFindingScreenshots materializes report-referenced screenshots in the +// task workspace. Browser runs use per-session working directories, while +// reports intentionally use the portable findings/screenshots/ path. +func (s *Server) archiveFindingScreenshots(taskID int64, markdown string) error { + if taskID <= 0 { + return nil + } + for _, match := range screenshotMarkdownLink.FindAllStringSubmatch(markdown, -1) { + raw := strings.TrimSpace(match[2]) + rel := filepath.Clean(strings.TrimPrefix(raw, "/")) + const prefix = "findings" + string(os.PathSeparator) + "screenshots" + string(os.PathSeparator) + if !strings.HasPrefix(rel, prefix) || strings.Contains(rel, ".."+string(os.PathSeparator)) { + continue + } + target := filepath.Join(s.m.dir, "tasks", fmt.Sprint(taskID), rel) + if info, err := os.Stat(target); err == nil && !info.IsDir() { + continue + } + source, ok := s.resolveSessionScreenshot(rel) + if !ok { + continue + } + if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { + return err + } + in, err := os.Open(source) + if err != nil { + return err + } + out, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) + if err == nil { + _, err = io.Copy(out, in) + } + _ = in.Close() + _ = out.Close() + if err != nil { + return err + } + } + return nil +} + +func (s *Server) resolveSessionScreenshot(rel string) (string, bool) { + matches, err := filepath.Glob(filepath.Join(filepath.Clean(s.m.dir), "sessions", "*", rel)) + if err != nil || len(matches) == 0 { + return "", false + } + var newest string + var newestTime int64 + for _, candidate := range matches { + info, statErr := os.Stat(candidate) + if statErr != nil || info.IsDir() { + continue + } + if newest == "" || info.ModTime().UnixNano() > newestTime { + newest, newestTime = candidate, info.ModTime().UnixNano() + } + } + return newest, newest != "" +} + // POST /api/workspace/upload?path= — multipart form field "file" (one or more). func (s *Server) wsUpload(w http.ResponseWriter, r *http.Request) { dirAbs, ok := s.wsResolve(r.URL.Query().Get("path")) diff --git a/web/src/app/(main)/function/findings/detail/page.tsx b/web/src/app/(main)/function/findings/detail/page.tsx index c74fc09..a9334e8 100644 --- a/web/src/app/(main)/function/findings/detail/page.tsx +++ b/web/src/app/(main)/function/findings/detail/page.tsx @@ -72,6 +72,7 @@ function FindingDetailInner() { const [tab, setTab] = React.useState("overview"); const [reportGenerating, setReportGenerating] = React.useState(false); const [autoChatOpen, setAutoChatOpen] = React.useState(false); + const [reportChatOpen, setReportChatOpen] = React.useState(false); const title = finding?.name || finding?.vulnclass || "未分类"; const load = React.useCallback(() => { @@ -190,6 +191,12 @@ function FindingDetailInner() { 询问 Auto Agent + {!finding.inherited ? ( + + ) : null} {finding.inherited && finding.source_task_id && ( 来源任务 #{finding.source_task_id} · 只读 )} @@ -410,6 +417,15 @@ function FindingDetailInner() { open={autoChatOpen} onOpenChange={setAutoChatOpen} /> + ); } diff --git a/web/src/components/finding-auto-chat.tsx b/web/src/components/finding-auto-chat.tsx index 1c01c81..6d70788 100644 --- a/web/src/components/finding-auto-chat.tsx +++ b/web/src/components/finding-auto-chat.tsx @@ -22,11 +22,17 @@ function mergeActivities(oldItems: Activity[], incoming: Activity[]) { export function FindingAutoChat({ findingId, findingTitle, + agentKey = "auto", + agentLabel = "Auto Agent", + promptPlaceholder = "询问这个漏洞…", open, onOpenChange, }: { findingId: string; findingTitle: string; + agentKey?: string; + agentLabel?: string; + promptPlaceholder?: string; open: boolean; onOpenChange: (open: boolean) => void; }) { @@ -42,7 +48,7 @@ export function FindingAutoChat({ const [history, setHistory] = React.useState([]); const cursor = React.useRef(0); const safeTitle = findingTitle.replace(/[\[\]\r\n]/g, " ").trim().slice(0, 100); - const storageKey = `artex.finding.auto-conversation.${findingId}`; + const storageKey = `artex.finding.${agentKey}-conversation.${findingId}`; const createConversation = React.useCallback(async () => { setManualEmpty(false); @@ -50,7 +56,7 @@ export function FindingAutoChat({ setRunning(false); setLoading(true); try { - const created = await api.createConversation("auto", `漏洞 #${findingId} · ${safeTitle}`); + const created = await api.createConversation(agentKey, `漏洞 #${findingId} · ${safeTitle}`); window.localStorage.setItem(storageKey, String(created.id)); setConversation(created); } catch (error) { @@ -58,7 +64,7 @@ export function FindingAutoChat({ } finally { setLoading(false); } - }, [findingId, safeTitle, storageKey]); + }, [agentKey, findingId, safeTitle, storageKey]); React.useEffect(() => { if (!open || conversation || manualEmpty) return; @@ -66,7 +72,7 @@ export function FindingAutoChat({ if (Number.isSafeInteger(storedID) && storedID > 0) { setConversation({ id: storedID, - agent_key: "auto", + agent_key: agentKey, title: `漏洞 #${findingId} · ${safeTitle}`, created_at: "", updated_at: "", @@ -85,7 +91,7 @@ export function FindingAutoChat({ const items = await api.conversations(); if (!live) return; const prefix = `漏洞 #${findingId} ·`; - setHistory(items.filter((item) => item.agent_key === "auto" && item.title.startsWith(prefix))); + setHistory(items.filter((item) => item.agent_key === agentKey && item.title.startsWith(prefix))); } catch { if (live && historyOpen) setHistory([]); } finally { @@ -97,7 +103,7 @@ export function FindingAutoChat({ live = false; clearTimeout(timer); }; - }, [findingId, historyOpen, running]); + }, [agentKey, findingId, historyOpen, running]); React.useEffect(() => { if (!open || !conversation) return; @@ -209,7 +215,7 @@ export function FindingAutoChat({

- Auto Agent + {agentLabel} {running && 运行中}

漏洞 #{findingId} · {safeTitle}

@@ -274,7 +280,7 @@ export function FindingAutoChat({ inputGroup onValueChange={setInput} disabled={!conversation || loading || sending || running} - placeholder="询问这个漏洞…" + placeholder={promptPlaceholder} onKeyDown={(event) => { if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) { event.preventDefault();