feat(tasks): 任务增加可选名称字段

tasks 表新增 name 列(空=未命名,前端回退到描述),端到端打通:
- DB: schema.sql 加列并补旧库 ALTER;tasks.go 的 Task/CreateOptions/CRUD;
  findings.go 分组与任务下拉带出 t.name
- 后端: TaskDTO、内存 Task、createTaskReq 透传 name
- 前端: 类型/API/mock 加 name;任务列表新增名称列与搜索、创建表单加名称输入;
  详情页与漏洞页标题回退 name || description

Claude-Session: https://claude.ai/code/session_019QXSCDmzimwnjntBNCgUD7
This commit is contained in:
Autumn-27 committed 2026-08-23 09:08:05 -04:00
1 parent 070784d43c
commit f63a451b87
12 files changed
+72 -21

No files matched your search

@@ -626,7 +626,7 @@ export default function FindingsPage() {
<SelectItem value={UNASSIGNED_TASK}>未关联 / 任务已删除</SelectItem>
{(stats.tasks ?? []).map((t) => {
const id = String(t.id);
const label = t.description || `任务 #${id}(已删除)`;
const label = t.name || t.description || `任务 #${id}(已删除)`;
return (
<SelectItem key={id} value={id}>
<span className="flex w-full items-center gap-2">
@@ -704,7 +704,11 @@ export default function FindingsPage() {
/>
<div className="flex min-w-0 flex-col gap-1">
<CardTitle className="truncate text-sm">
{group.task_id === null ? "未关联 / 任务已删除" : `任务 #${group.task_id}`}
{group.task_id === null
? "未关联 / 任务已删除"
: group.task_name
? `${group.task_name}(任务 #${group.task_id})`
: `任务 #${group.task_id}`}
</CardTitle>
<CardDescription className="truncate" title={group.task_description}>
{group.task_description || "来源任务不可用"}
@@ -314,8 +314,8 @@ function TaskDetailInner() {
<ArrowLeftIcon />
</Link>
</Button>
<h1 className="min-w-0 flex-1 truncate text-sm font-semibold" title={task.description}>
{task.description}
<h1 className="min-w-0 flex-1 truncate text-sm font-semibold" title={task.name || task.description}>
{task.name || task.description}
</h1>
<code className="hidden rounded bg-muted px-1.5 py-0.5 font-mono text-xs text-muted-foreground sm:inline">
{task.id}
+28 -3
View File
@@ -225,7 +225,10 @@ export default function TasksPage() {
if (statusFilter !== "all" && t.status !== statusFilter) return false;
if (!q) return true;
return (
t.description.toLowerCase().includes(q) || t.goal.toLowerCase().includes(q) || t.id.toLowerCase().includes(q)
(t.name ?? "").toLowerCase().includes(q) ||
t.description.toLowerCase().includes(q) ||
t.goal.toLowerCase().includes(q) ||
t.id.toLowerCase().includes(q)
);
});
}, [tasks, query, statusFilter]);
@@ -575,6 +578,7 @@ export default function TasksPage() {
/>
</TableHead>
<TableHead className="font-mono">ID</TableHead>
<TableHead>名称</TableHead>
<TableHead>描述</TableHead>
<TableHead>目标</TableHead>
<TableHead>状态</TableHead>
@@ -741,13 +745,22 @@ const TaskRow = React.memo(function TaskRow({
<Link
href={`/function/tasks/detail?id=${encodeURIComponent(task.id)}`}
className="min-w-0 truncate rounded-sm hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
title={task.description}
title={task.name || task.description}
>
{task.description}
{task.name || <span className="text-muted-foreground">未命名</span>}
</Link>
{task.active && <StarIcon className="size-4 shrink-0 fill-amber-400 text-amber-400" />}
</div>
</TableCell>
<TableCell className="text-muted-foreground max-w-xs">
<Link
href={`/function/tasks/detail?id=${encodeURIComponent(task.id)}`}
className="block truncate rounded-sm hover:text-foreground hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
title={task.description}
>
{task.description}
</Link>
</TableCell>
<TableCell className="text-muted-foreground max-w-xs truncate">{task.goal}</TableCell>
<TableCell>
<StatusBadge domain="task" value={task.status} dot />
@@ -1282,6 +1295,7 @@ function CompanyPicker({
function CreateTaskSheet({ tasks, onCreated }: { tasks: Task[]; onCreated: () => void }) {
const [open, setOpen] = React.useState(false);
const [name, setName] = React.useState("");
const [description, setDescription] = React.useState("");
const [goal, setGoal] = React.useState("");
const [selectedTemplateID, setSelectedTemplateID] = React.useState<number | null>(null);
@@ -1350,6 +1364,7 @@ function CreateTaskSheet({ tasks, onCreated }: { tasks: Task[]; onCreated: () =>
const timeoutSec = Math.max(0, Math.floor(Number(timeoutMin) || 0)) * 60;
const heartbeatSec = Math.max(10, Math.floor(Number(heartbeatMin) || 10)) * 60; // 下限 10min,与后端归一一致
await api.createTask({
name: name.trim(),
description: description.trim(),
goal: goal.trim(),
llmProfileIds: llmProfileIDs.map(Number),
@@ -1361,6 +1376,7 @@ function CreateTaskSheet({ tasks, onCreated }: { tasks: Task[]; onCreated: () =>
coverageEnabled,
});
toast.success("任务已创建");
setName("");
setDescription("");
setGoal("");
setSelectedTemplateID(null);
@@ -1415,6 +1431,15 @@ function CreateTaskSheet({ tasks, onCreated }: { tasks: Task[]; onCreated: () =>
}}
portalContainer={sheetContentRef}
/>
<div className="grid gap-2">
<Label htmlFor="name">名称(可选)</Label>
<Input
id="name"
placeholder="给任务起个便于识别的名字,例如:Acme 官网渗透"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="description">描述</Label>
<Textarea
+2
View File
@@ -180,6 +180,7 @@ export const api = {
tasks: () =>
get<{ tasks: Task[]; active: string }>("/tasks").then((r) => ({ tasks: arr(r.tasks), active: r.active ?? "" })),
createTask: (input: {
name?: string;
description: string;
goal: string;
llmProfileIds?: number[];
@@ -191,6 +192,7 @@ export const api = {
coverageEnabled?: boolean;
}) =>
post<Task>("/tasks", {
name: input.name ?? "",
description: input.description,
goal: input.goal,
llm_profile_ids: input.llmProfileIds ?? [],
+6 -3
View File
@@ -243,6 +243,7 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
const companyIDs = [...((b.company_ids as number[] | undefined) ?? [])];
const created: Task = {
id,
name: String(b.name ?? ""),
description: String(b.description ?? "新任务"),
goal: String(b.goal ?? ""),
status: "created",
@@ -595,14 +596,15 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
if (path === "/exploration/findings/stats") {
const vulnclasses = Array.from(new Set(mockFindings.map((f) => f.vulnclass))).sort();
// 「按任务」下拉:有漏洞的任务 + 描述 + 条数(mock 任务 id 是字符串,直接当 id 用)。
const taskMap = new Map<string, { description: string; count: number }>();
const taskMap = new Map<string, { name: string; description: string; count: number }>();
for (const f of mockFindings) {
if (!f.task_id) continue;
const cur = taskMap.get(f.task_id) ?? { description: f.task_description ?? "", count: 0 };
const owner = mockTasks.find((candidate) => candidate.id === f.task_id);
const cur = taskMap.get(f.task_id) ?? { name: owner?.name ?? "", description: f.task_description ?? "", count: 0 };
cur.count++;
taskMap.set(f.task_id, cur);
}
const tasks = Array.from(taskMap, ([id, v]) => ({ id, description: v.description, count: v.count }));
const tasks = Array.from(taskMap, ([id, v]) => ({ id, name: v.name, description: v.description, count: v.count }));
return {
total: mockFindings.length,
pending: mockFindings.filter((f) => f.status === "pending").length,
@@ -636,6 +638,7 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
const owner = mockTasks.find((candidate) => candidate.id === key);
return {
task_id: key === "__unassigned__" ? null : key,
task_name: owner?.name ?? "",
task_description: owner?.description ?? items[0]?.task_description ?? "",
task_status: owner?.status ?? "",
count: items.length,
+3
View File
@@ -6,6 +6,7 @@ export type EngineMode = "exploring" | "paused" | "stalled" | "idle";
export interface Task {
id: string;
name?: string; // 可选任务名称;空/缺省=未命名,展示时回退到描述
description: string;
goal: string;
status: TaskStatus;
@@ -349,6 +350,7 @@ export interface FindingsPage {
export interface FindingGroup {
task_id: string | number | null;
task_name?: string; // 可选任务名称;空/缺省=未命名
task_description: string;
task_status: string;
count: number;
@@ -390,6 +392,7 @@ export interface FindingStats {
// 前端回退展示 id)及其漏洞条数。
export interface FindingTaskOption {
id: string | number;
name?: string; // 可选任务名称;空/缺省=未命名
description: string;
count: number;
}