feat(findings): reduce duplicate-confirmation noise with grouped suggestions and ranked candidates

- Prompt: evidenced endpoint/handler/param differences justify independent; suggest only with concrete shared points
- Review acceptance: merged no longer blocked by unified report
- Suggestions: 2..N member groups, one-click group confirm with per-member exclusion, related cards cleared
- Candidates: score/tier/hit_reasons ranking, low-score kept paginated
This commit is contained in:
RuoJi6 committed 2026-10-24 20:01:37 +08:00
1 parent 620ca5d96c
commit 00ec84d14e
11 files changed
+532 -105

No files matched your search

+44 -15
View File
@@ -192,15 +192,18 @@ function FindingMergeSuggestionCard({
onResolve,
}: {
suggestion: FindingCaseSuggestion;
onResolve: (s: FindingCaseSuggestion, accept: boolean) => Promise<void>;
onResolve: (s: FindingCaseSuggestion, accept: boolean, excludeIds?: string[]) => Promise<void>;
}) {
const [expanded, setExpanded] = React.useState(false);
const [busy, setBusy] = React.useState(false);
const ids = s.finding_ids?.length ? s.finding_ids : [s.left_id, s.right_id];
const [excluded, setExcluded] = React.useState<string[]>([]);
const included = ids.filter((id) => !excluded.includes(id));
async function resolve(accept: boolean) {
if (busy) return;
setBusy(true);
try {
await onResolve(s, accept);
await onResolve(s, accept, accept ? excluded : []);
} finally {
setBusy(false);
}
@@ -209,17 +212,37 @@ function FindingMergeSuggestionCard({
<Card size="sm" className="min-w-0">
<CardHeader>
<div className="flex flex-wrap items-center gap-2">
<Badge variant="outline">疑似重复 · 待确认</Badge>
<Badge variant="outline">疑似重复分组 · {ids.length} 条 · 待确认</Badge>
<CardDescription>任务 #{s.task_id}</CardDescription>
</div>
<CardTitle className="break-words">{s.title}</CardTitle>
<CardDescription className="flex flex-wrap gap-3">
<Link className="underline underline-offset-4" href={`/function/findings/detail/?id=${s.left_id}`}>
查看原报告 #{s.left_id}
</Link>
<Link className="underline underline-offset-4" href={`/function/findings/detail/?id=${s.right_id}`}>
查看原报告 #{s.right_id}
</Link>
<CardDescription className="flex flex-col gap-1">
{ids.map((id) => (
<span key={id} className="flex items-center gap-2">
<Checkbox
checked={!excluded.includes(id)}
disabled={busy}
onCheckedChange={(v) =>
setExcluded((old) =>
v === true ? old.filter((x) => x !== id) : old.includes(id) ? old : [...old, id],
)
}
aria-label={`归并时包含上报 #${id}`}
/>
<Link
className={cn(
"underline underline-offset-4",
excluded.includes(id) && "text-muted-foreground line-through",
)}
href={`/function/findings/detail/?id=${id}`}
>
查看原报告 #{id}
</Link>
</span>
))}
{excluded.length > 0 ? (
<span className="text-xs">已排除 {excluded.length} 条:确认后这些记录不会再与本组建议归并</span>
) : null}
</CardDescription>
</CardHeader>
<CardContent>
@@ -238,11 +261,12 @@ function FindingMergeSuggestionCard({
</Collapsible>
</CardContent>
<CardFooter className="flex flex-wrap gap-2">
<Button variant="outline" size="sm" disabled={busy} onClick={() => resolve(true)}>
{busy ? <Spinner /> : null}确认归并
<Button variant="outline" size="sm" disabled={busy || included.length < 2} onClick={() => resolve(true)}>
{busy ? <Spinner /> : null}
{included.length === ids.length ? "整组确认归并" : `确认归并所选(${included.length} 条)`}
</Button>
<Button variant="ghost" size="sm" disabled={busy} onClick={() => resolve(false)}>
不是同一漏洞
整组不是同一漏洞
</Button>
</CardFooter>
</Card>
@@ -294,10 +318,15 @@ export function FindingCaseReviewPanel({ taskId, onChange }: { taskId?: string;
clearTimeout(timer);
};
}, [taskId]);
async function resolve(s: FindingCaseSuggestion, accept: boolean) {
async function resolve(s: FindingCaseSuggestion, accept: boolean, excludeIds: string[] = []) {
try {
const result = await api.resolveFindingCaseSuggestion(s.id, accept);
const result = await api.resolveFindingCaseSuggestion(s.id, accept, excludeIds);
setSuggestions((v) => v.filter((x) => x.id !== s.id));
// Server-side resolution may also clear related groups; refresh instead of waiting for the poll.
api
.findingCaseSuggestions(taskId)
.then(setSuggestions)
.catch(() => undefined);
if (accept && result.case_id !== "0") {
toast.success("已归并,请进入文件夹生成统一报告");
} else {
+5 -2
View File
@@ -645,8 +645,11 @@ export const api = {
get<FindingCaseSuggestion[]>(
`/exploration/finding-case-suggestions${task ? `?task_id=${encodeURIComponent(task)}` : ""}`,
),
resolveFindingCaseSuggestion: (id: string, accept: boolean) =>
post<{ case_id: string }>(`/exploration/finding-case-suggestions/${id}`, { accept }),
resolveFindingCaseSuggestion: (id: string, accept: boolean, excludeIds?: string[]) =>
post<{ case_id: string }>(`/exploration/finding-case-suggestions/${id}`, {
accept,
...(excludeIds?.length ? { exclude_ids: excludeIds } : {}),
}),
reviewFindingCases: (ids: string[]) =>
post<{ runs: { conversation_id: number; task_id: string }[] }>("/exploration/finding-case-review", {
finding_ids: ids,
+1
View File
@@ -1737,6 +1737,7 @@ export interface FindingCaseSuggestion {
title: string;
reason: string;
state: string;
finding_ids: string[];
}
export interface FindingCaseReviewRun {
conversation_id: number;