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:
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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ?? [],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user