mirror of
https://github.com/RuoJi6/ARTEX.git
synced 2026-11-13 14:03:49 +08:00
feat(findings): grouped duplicate suggestions, ranked candidates, relaxed merged acceptance (Codex rewrite)
Authored entirely by Codex CLI (codex exec, reasoning effort high), one session resumed across capacity fallbacks: - gpt-6.1-sol (schema + initial db work, stopped at capacity) - gpt-6-astra (db/server/agent/frontend, stopped at capacity) - gpt-6-sol (remaining items, tests, fixes; stopped at capacity after final checks) Schemes 1+2+3: prompt/acceptance (2..20, evidenced differences justify independent, merged no longer blocked by unified report), group suggestions with member table, superset reuse/subset superseded, veto-aware reject/exclusion, anchor re-anchoring trigger, sweep after merge/reject/remove, frontend group card with stale-response guard, candidate scoring with tier/hit_reasons/strong_total.
This commit is contained in:
1 parent
b0d6ad5d76
commit
09e90b4170
12 files changed
+740
-115
No files matched your search
@@ -192,15 +192,19 @@ 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 = Array.isArray(s.finding_ids) && s.finding_ids.length > 0 ? 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;
|
||||
if (accept && included.length < 2) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
await onResolve(s, accept);
|
||||
await onResolve(s, accept, accept ? excluded : []);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
@@ -209,17 +213,30 @@ 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={(value) =>
|
||||
setExcluded((current) =>
|
||||
value === true
|
||||
? current.filter((item) => item !== id)
|
||||
: [...current.filter((item) => item !== id), id],
|
||||
)
|
||||
}
|
||||
aria-label={`归并时包含上报 #${id}`}
|
||||
/>
|
||||
<Link className="underline underline-offset-4" href={`/function/findings/detail/?id=${id}`}>
|
||||
查看原报告 #{id}
|
||||
</Link>
|
||||
</span>
|
||||
))}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -238,11 +255,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>
|
||||
@@ -271,21 +289,32 @@ export function FindingCaseReviewPanel({ taskId, onChange }: { taskId?: string;
|
||||
const [suggestions, setSuggestions] = React.useState<FindingCaseSuggestion[]>([]);
|
||||
const [runs, setRuns] = React.useState<FindingCaseReviewRun[]>([]);
|
||||
const [error, setError] = React.useState("");
|
||||
const [reload, setReload] = React.useState(0);
|
||||
const requestSeq = React.useRef(0);
|
||||
const taskKey = taskId ?? "";
|
||||
const taskKeyRef = React.useRef(taskKey);
|
||||
taskKeyRef.current = taskKey;
|
||||
React.useEffect(() => {
|
||||
const seq = ++requestSeq.current;
|
||||
const requestedTask = taskKey;
|
||||
void reload;
|
||||
let active = true;
|
||||
let timer: ReturnType<typeof setTimeout>;
|
||||
setSuggestions([]);
|
||||
async function load() {
|
||||
try {
|
||||
const [s, r] = await Promise.all([api.findingCaseSuggestions(taskId), api.findingCaseReviewRuns()]);
|
||||
if (active) {
|
||||
if (active && seq === requestSeq.current && requestedTask === taskKeyRef.current) {
|
||||
setSuggestions(s);
|
||||
setRuns(r.filter((x) => !taskId || x.task_id === taskId));
|
||||
setError("");
|
||||
}
|
||||
} catch (e) {
|
||||
if (active) setError((e as Error).message);
|
||||
if (active && seq === requestSeq.current && requestedTask === taskKeyRef.current)
|
||||
setError((e as Error).message);
|
||||
} finally {
|
||||
if (active) timer = setTimeout(load, 5000);
|
||||
if (active && seq === requestSeq.current && requestedTask === taskKeyRef.current)
|
||||
timer = setTimeout(load, 5000);
|
||||
}
|
||||
}
|
||||
void load();
|
||||
@@ -293,11 +322,15 @@ export function FindingCaseReviewPanel({ taskId, onChange }: { taskId?: string;
|
||||
active = false;
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [taskId]);
|
||||
async function resolve(s: FindingCaseSuggestion, accept: boolean) {
|
||||
}, [reload, taskId, taskKey]);
|
||||
async function resolve(s: FindingCaseSuggestion, accept: boolean, excludeIds?: string[]) {
|
||||
const seq = ++requestSeq.current;
|
||||
const requestedTask = taskKey;
|
||||
try {
|
||||
const result = await api.resolveFindingCaseSuggestion(s.id, accept);
|
||||
const result = await api.resolveFindingCaseSuggestion(s.id, accept, excludeIds);
|
||||
if (seq !== requestSeq.current || requestedTask !== taskKeyRef.current) return;
|
||||
setSuggestions((v) => v.filter((x) => x.id !== s.id));
|
||||
setReload((v) => v + 1);
|
||||
if (accept && result.case_id !== "0") {
|
||||
toast.success("已归并,请进入文件夹生成统一报告");
|
||||
} else {
|
||||
@@ -305,6 +338,7 @@ export function FindingCaseReviewPanel({ taskId, onChange }: { taskId?: string;
|
||||
}
|
||||
onChange?.();
|
||||
} catch (e) {
|
||||
if (seq === requestSeq.current && requestedTask === taskKeyRef.current) setReload((v) => v + 1);
|
||||
toast.error((e as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user