feat(tasks): 删除任务可选连带删除资产/漏洞/流量/文件(默认不选)

删除弹窗新增四个默认不勾的复选框:
- 删除关联的资产:仅删本任务独有资产,与其他任务共享的仅解除关联(保留)。
- 删除发现的漏洞:删本任务全部 findings。
- 删除测试的流量:按本任务资产的 host 清录制流量(host 无任务标签,会连带清
  其他任务命中同一 host 的流量)。
- 删除测试过程中写的文件:删 <workDir>/tasks/<id> 目录(worker 产物 + 上传)。

- db: AssetStore.DeleteByTaskID(独有删+共享解关联)/HostsByTask;DeleteFindingsByTask。
- server: pgDeleteTask 读可选 JSON body {assets,findings,traffic,files},先按 host
  清流量(需在删资产前取 host)→删漏洞→删资产→删任务→删文件目录;空 body=只删任务。
- web: api.deleteTask(id,opts);删除弹窗四复选框(默认不选)+说明。

go build/vet、tsc 通过;新增 TestDeleteByTaskID(独有删/共享解关联/host 反查)。
This commit is contained in:
Autumn-27 committed 2026-08-19 10:40:19 -04:00
1 parent 2aeb2e49da
commit 4f7c1a199d
6 files changed
+215 -14

No files matched your search

+53 -12
View File
@@ -227,15 +227,18 @@ export default function TasksPage() {
return () => clearInterval(i);
}, [hasRunning]);
const deleteTask = React.useCallback(async (id: string) => {
try {
await api.deleteTask(id);
toast.success("任务已删除(全局资产图保留)");
load();
} catch (e) {
toast.error("删除失败:" + (e as Error).message);
}
}, [load]);
const deleteTask = React.useCallback(
async (id: string, opts: DeleteOpts) => {
try {
await api.deleteTask(id, opts);
toast.success("任务已删除");
load();
} catch (e) {
toast.error("删除失败:" + (e as Error).message);
}
},
[load],
);
return (
<Card>
@@ -333,6 +336,15 @@ export default function TasksPage() {
// the POLL_MS list refresh only re-render the rows whose data actually moved — a table page
// is 20 rows × (StatusBadge + Link + a Radix AlertDialog tree), far too heavy to rebuild
// wholesale on every parent render.
type DeleteOpts = { assets: boolean; findings: boolean; traffic: boolean; files: boolean };
const DELETE_EXTRAS: { key: keyof DeleteOpts; label: string; hint: string }[] = [
{ key: "assets", label: "删除关联的资产", hint: "仅本任务独有的资产;与其他任务共享的仅解除关联" },
{ key: "findings", label: "删除发现的漏洞", hint: "本任务记录的全部漏洞" },
{ key: "traffic", label: "删除测试的流量", hint: "按本任务涉及的 host 清除录制流量" },
{ key: "files", label: "删除测试过程中写的文件", hint: "本任务工作目录 tasks/<id>(产物 + 上传)" },
];
const TaskRow = React.memo(function TaskRow({
task,
nowSec,
@@ -340,8 +352,14 @@ const TaskRow = React.memo(function TaskRow({
}: {
task: Task;
nowSec: number;
onDelete: (id: string) => void;
onDelete: (id: string, opts: DeleteOpts) => void;
}) {
const [delOpts, setDelOpts] = React.useState<DeleteOpts>({
assets: false,
findings: false,
traffic: false,
files: false,
});
return (
<TableRow className="group border-border/60">
<TableCell>
@@ -430,12 +448,35 @@ const TaskRow = React.memo(function TaskRow({
) : (
"该任务"
)}
的执行记录、会话与产物将被删除,此操作不可撤销(全局资产图保留)。
的执行记录、会话与产物将被删除,此操作不可撤销。
</AlertDialogDescription>
</AlertDialogHeader>
{/* 额外清理项:默认都不选,勾选后连带删除。全局资产图默认保留。 */}
<div className="grid gap-2 rounded-md border bg-muted/30 p-3">
<span className="text-xs font-medium text-muted-foreground">
同时删除以下关联数据(默认不删):
</span>
{DELETE_EXTRAS.map((opt) => (
<label key={opt.key} className="flex items-start gap-2 text-sm">
<Checkbox
className="mt-0.5"
checked={delOpts[opt.key]}
onCheckedChange={(v) =>
setDelOpts((s) => ({ ...s, [opt.key]: v === true }))
}
/>
<span className="grid gap-0.5">
<span>{opt.label}</span>
<span className="text-xs text-muted-foreground">{opt.hint}</span>
</span>
</label>
))}
</div>
<AlertDialogFooter>
<AlertDialogCancel>取消</AlertDialogCancel>
<AlertDialogAction onClick={() => onDelete(task.id)}>
<AlertDialogAction onClick={() => onDelete(task.id, delOpts)}>
删除
</AlertDialogAction>
</AlertDialogFooter>
+10 -1
View File
@@ -122,7 +122,16 @@ export const api = {
seed_first_intent: seedFirstIntent ?? false,
plan_heartbeat_seconds: planHeartbeatSeconds ?? 0, // 0 = 后端归一到默认 600(10min)
}),
deleteTask: (id: string) => del<{ deleted: number }>(`/tasks/${id}`),
// 删除任务;opts 里的四个可选项默认不删:assets 关联资产、findings 发现漏洞、
// traffic 测试流量(按任务 host 清)、files 测试过程写的文件(tasks/<id> 目录)。
deleteTask: (
id: string,
opts?: { assets?: boolean; findings?: boolean; traffic?: boolean; files?: boolean },
) =>
del<{ deleted: string; assets_deleted?: number; findings_deleted?: number; traffic_deleted?: number; files_deleted?: boolean }>(
`/tasks/${id}`,
opts,
),
controlTask: (id: string, action: "pause" | "resume") => post<{ id: string; paused: boolean }>(`/tasks/${id}/control`, { action }),
// 重跑一条没跑成功的意图(blocked/exhausted/stopped):置回 open,worker 会重新认领、从头再跑。
rerunIntent: (taskId: string, intentId: string) => post<{ id: string; reopened: number }>(`/tasks/${taskId}/intents/${intentId}/rerun`),