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
+8
-6
@@ -228,6 +228,7 @@ func (d *DB) ListFindingsPage(f FindingFilter, page, pageSize int) ([]*DBFinding
|
||||
// deletion; those records intentionally share one "unassigned/deleted" bucket.
|
||||
type FindingGroup struct {
|
||||
TaskID *int64 `json:"task_id"`
|
||||
TaskName string `json:"task_name"` // 可选任务名称;空=未命名
|
||||
TaskDescription string `json:"task_description"`
|
||||
TaskStatus string `json:"task_status"`
|
||||
Count int `json:"count"`
|
||||
@@ -250,7 +251,7 @@ func (d *DB) ListFindingGroups(f FindingFilter, page, pageSize int) ([]FindingGr
|
||||
}
|
||||
where, args := f.where()
|
||||
grouped := ` FROM findings f LEFT JOIN tasks t ON f.task_id=t.id` + where +
|
||||
` GROUP BY t.id, t.description, t.status, t.paused, t.queued`
|
||||
` GROUP BY t.id, t.name, t.description, t.status, t.paused, t.queued`
|
||||
|
||||
var groupTotal, findingTotal int
|
||||
countQuery := `SELECT COUNT(*), COALESCE(SUM(finding_count),0) FROM (` +
|
||||
@@ -267,7 +268,7 @@ func (d *DB) ListFindingGroups(f FindingFilter, page, pageSize int) ([]FindingGr
|
||||
order = `MAX(CASE f.severity WHEN 'critical' THEN 4 WHEN 'high' THEN 3 WHEN 'medium' THEN 2 WHEN 'low' THEN 1 ELSE 0 END) DESC, MAX(f.created_at) DESC, t.id DESC NULLS LAST`
|
||||
}
|
||||
pageArgs := append(append([]any{}, args...), pageSize, (page-1)*pageSize)
|
||||
query := fmt.Sprintf(`SELECT t.id, COALESCE(t.description,''), COALESCE(
|
||||
query := fmt.Sprintf(`SELECT t.id, COALESCE(t.name,''), COALESCE(t.description,''), COALESCE(
|
||||
CASE
|
||||
WHEN t.status IN ('done','failed','timeout') THEN t.status
|
||||
WHEN t.queued THEN 'queued'
|
||||
@@ -290,7 +291,7 @@ func (d *DB) ListFindingGroups(f FindingFilter, page, pageSize int) ([]FindingGr
|
||||
for rows.Next() {
|
||||
var group FindingGroup
|
||||
var taskID sql.NullInt64
|
||||
if err := rows.Scan(&taskID, &group.TaskDescription, &group.TaskStatus, &group.Count,
|
||||
if err := rows.Scan(&taskID, &group.TaskName, &group.TaskDescription, &group.TaskStatus, &group.Count,
|
||||
&group.Critical, &group.High, &group.Medium, &group.Low, &group.LastFoundAt); err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
@@ -477,6 +478,7 @@ type FindingStats struct {
|
||||
// the id.
|
||||
type FindingTaskOption struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"` // 可选任务名称;空=未命名
|
||||
Description string `json:"description"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
@@ -513,11 +515,11 @@ func (d *DB) FindingStats() (*FindingStats, error) {
|
||||
}
|
||||
|
||||
// 任务下拉:有漏洞的任务,带描述(任务删除后为空,前端回退 id)和条数,最新有漏洞的排前。
|
||||
trows, err := d.Query(`SELECT f.task_id, COALESCE(t.description, ''), COUNT(*)
|
||||
trows, err := d.Query(`SELECT f.task_id, COALESCE(t.name, ''), COALESCE(t.description, ''), COUNT(*)
|
||||
FROM findings f
|
||||
LEFT JOIN tasks t ON f.task_id = t.id
|
||||
WHERE f.task_id IS NOT NULL
|
||||
GROUP BY f.task_id, t.description
|
||||
GROUP BY f.task_id, t.name, t.description
|
||||
ORDER BY MAX(f.created_at) DESC`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -525,7 +527,7 @@ func (d *DB) FindingStats() (*FindingStats, error) {
|
||||
defer trows.Close()
|
||||
for trows.Next() {
|
||||
var opt FindingTaskOption
|
||||
if err := trows.Scan(&opt.ID, &opt.Description, &opt.Count); err != nil {
|
||||
if err := trows.Scan(&opt.ID, &opt.Name, &opt.Description, &opt.Count); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
st.Tasks = append(st.Tasks, opt)
|
||||
|
||||
@@ -324,6 +324,7 @@ CREATE TABLE IF NOT EXISTS llm_profile_health (
|
||||
-- =====================================================================
|
||||
CREATE TABLE IF NOT EXISTS tasks (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL DEFAULT '',
|
||||
description TEXT NOT NULL,
|
||||
goal TEXT NOT NULL,
|
||||
exploration_id BIGINT NOT NULL UNIQUE
|
||||
@@ -367,6 +368,8 @@ ALTER TABLE tasks ADD COLUMN IF NOT EXISTS coverage_enabled BOOLEAN NOT NULL DEF
|
||||
-- exploration that already owns goals/history.
|
||||
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS queued_at TIMESTAMPTZ;
|
||||
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS queue_mode TEXT NOT NULL DEFAULT '';
|
||||
-- 可选的任务名称;补旧库。空串=未命名,前端展示时回退到描述。
|
||||
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS name TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS active_llm_profile_id BIGINT REFERENCES llm_profiles(id) ON DELETE SET NULL;
|
||||
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS llm_chain_revision BIGINT NOT NULL DEFAULT 0;
|
||||
|
||||
|
||||
+8
-5
@@ -12,6 +12,7 @@ import (
|
||||
// Task is a row in the task registry (1:1 with an exploration).
|
||||
type Task struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"` // 可选任务名称;空=未命名
|
||||
Description string `json:"description"`
|
||||
Goal string `json:"goal"`
|
||||
ExplorationID int64 `json:"exploration_id"`
|
||||
@@ -137,6 +138,7 @@ func (d *DB) CreateTask(description, goal string, llmProfileID *int64, timeoutSe
|
||||
// TaskCreateOptions contains the task data that must be committed atomically
|
||||
// with the task/exploration row.
|
||||
type TaskCreateOptions struct {
|
||||
Name string // 可选任务名称;空=未命名
|
||||
SourceTaskIDs []int64
|
||||
CompanyIDs []int64
|
||||
LLMProfileIDs []int64
|
||||
@@ -193,6 +195,7 @@ VALUES ($1, 'fact', $2, 0, 'origin', 'system')`, expID, string(originPayload));
|
||||
active = &id
|
||||
}
|
||||
t := &Task{
|
||||
Name: opts.Name,
|
||||
Description: description, Goal: goal, ExplorationID: expID,
|
||||
LLMProfileID: active, ActiveLLMProfileID: active,
|
||||
LLMProfileIDs: append([]int64(nil), opts.LLMProfileIDs...),
|
||||
@@ -202,9 +205,9 @@ VALUES ($1, 'fact', $2, 0, 'origin', 'system')`, expID, string(originPayload));
|
||||
CoverageEnabled: coverageEnabled,
|
||||
}
|
||||
if err := tx.QueryRow(`
|
||||
INSERT INTO tasks(description, goal, exploration_id, llm_profile_id, active_llm_profile_id, timeout_seconds, plan_heartbeat_seconds, coverage_enabled)
|
||||
VALUES ($1,$2,$3,$4,$4,$5,$6,$7)
|
||||
RETURNING id, status, paused, created_at`, description, goal, expID, active, opts.TimeoutSeconds, opts.PlanHeartbeatSeconds, coverageEnabled).Scan(&t.ID, &t.Status, &t.Paused, &t.CreatedAt); err != nil {
|
||||
INSERT INTO tasks(name, description, goal, exploration_id, llm_profile_id, active_llm_profile_id, timeout_seconds, plan_heartbeat_seconds, coverage_enabled)
|
||||
VALUES ($1,$2,$3,$4,$5,$5,$6,$7,$8)
|
||||
RETURNING id, status, paused, created_at`, opts.Name, description, goal, expID, active, opts.TimeoutSeconds, opts.PlanHeartbeatSeconds, coverageEnabled).Scan(&t.ID, &t.Status, &t.Paused, &t.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := insertTaskRelations(tx, t.ID, opts.SourceTaskIDs); err != nil {
|
||||
@@ -284,11 +287,11 @@ func insertTaskLLMProfiles(tx *sql.Tx, taskID int64, profileIDs []int64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
const taskCols = `id, description, goal, exploration_id, status, paused, queued, queued_at, COALESCE(queue_mode,''), llm_profile_id, active_llm_profile_id, COALESCE(parent_ref,''), created_at, completed_at, COALESCE(timeout_seconds,0), COALESCE(plan_heartbeat_seconds,300), COALESCE(coverage_enabled,true), first_run_at, deadline_at`
|
||||
const taskCols = `id, COALESCE(name,''), description, goal, exploration_id, status, paused, queued, queued_at, COALESCE(queue_mode,''), llm_profile_id, active_llm_profile_id, COALESCE(parent_ref,''), created_at, completed_at, COALESCE(timeout_seconds,0), COALESCE(plan_heartbeat_seconds,300), COALESCE(coverage_enabled,true), first_run_at, deadline_at`
|
||||
|
||||
func scanTask(sc interface{ Scan(...any) error }) (*Task, error) {
|
||||
var t Task
|
||||
if err := sc.Scan(&t.ID, &t.Description, &t.Goal, &t.ExplorationID, &t.Status, &t.Paused, &t.Queued, &t.QueuedAt, &t.QueueMode, &t.LLMProfileID, &t.ActiveLLMProfileID, &t.ParentRef, &t.CreatedAt, &t.CompletedAt, &t.TimeoutSeconds, &t.PlanHeartbeatSeconds, &t.CoverageEnabled, &t.FirstRunAt, &t.DeadlineAt); err != nil {
|
||||
if err := sc.Scan(&t.ID, &t.Name, &t.Description, &t.Goal, &t.ExplorationID, &t.Status, &t.Paused, &t.Queued, &t.QueuedAt, &t.QueueMode, &t.LLMProfileID, &t.ActiveLLMProfileID, &t.ParentRef, &t.CreatedAt, &t.CompletedAt, &t.TimeoutSeconds, &t.PlanHeartbeatSeconds, &t.CoverageEnabled, &t.FirstRunAt, &t.DeadlineAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &t, nil
|
||||
|
||||
@@ -32,6 +32,7 @@ func rawString(raw json.RawMessage) string {
|
||||
type TaskDTO struct {
|
||||
ID string `json:"id"`
|
||||
ExplorationID int64 `json:"exploration_id"`
|
||||
Name string `json:"name"` // 可选任务名称;空=未命名
|
||||
Description string `json:"description"`
|
||||
Goal string `json:"goal"`
|
||||
Status string `json:"status"` // created | running | paused | done | failed
|
||||
@@ -83,6 +84,7 @@ func taskDTO(t *Task, status string) TaskDTO {
|
||||
return TaskDTO{
|
||||
ID: t.ID,
|
||||
ExplorationID: t.ExpID,
|
||||
Name: t.Name,
|
||||
Description: t.Description,
|
||||
Goal: t.Goal,
|
||||
Status: status,
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
type Task struct {
|
||||
ID string `json:"id"`
|
||||
ExpID int64 `json:"exploration_id"`
|
||||
Name string `json:"name"` // 可选任务名称;空=未命名
|
||||
Description string `json:"description"`
|
||||
Goal string `json:"goal"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
@@ -687,6 +688,7 @@ func unixNanoOrZero(t *time.Time) int64 {
|
||||
func taskFromPG(pt *pgdb.Task, store *pgdb.ExplorationStore, ic *intercept.Interceptor) *Task {
|
||||
return &Task{
|
||||
ID: strconv.FormatInt(pt.ID, 10), ExpID: pt.ExplorationID,
|
||||
Name: pt.Name,
|
||||
Description: pt.Description, Goal: pt.Goal, CreatedAt: pt.CreatedAt.Unix(), Paused: pt.Paused, Queued: pt.Queued,
|
||||
QueuedAt: unixNanoOrZero(pt.QueuedAt), QueueMode: pt.QueueMode,
|
||||
CompletedAt: unixOrZero(pt.CompletedAt), Status: pt.Status, ParentRef: pt.ParentRef,
|
||||
|
||||
@@ -1299,6 +1299,7 @@ func (s *Server) testLLM(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
type createTaskReq struct {
|
||||
Name string `json:"name,omitempty"` // 可选任务名称;省略/空=未命名
|
||||
Description string `json:"description"`
|
||||
Goal string `json:"goal"`
|
||||
LLMProfileID *int64 `json:"llm_profile_id,omitempty"` // 指定运行本任务的 LLM 配置;省略/null=用激活配置
|
||||
@@ -1356,6 +1357,7 @@ func (s *Server) createTask(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
req.CompanyIDs = companyIDs
|
||||
t, err := s.m.CreateTaskWithOptions(req.Description, req.Goal, db.TaskCreateOptions{
|
||||
Name: strings.TrimSpace(req.Name),
|
||||
SourceTaskIDs: sourceIDs, CompanyIDs: req.CompanyIDs, LLMProfileIDs: req.LLMProfileIDs,
|
||||
TimeoutSeconds: req.TimeoutSeconds, PlanHeartbeatSeconds: req.PlanHeartbeatSeconds,
|
||||
CoverageEnabled: req.CoverageEnabled,
|
||||
|
||||
@@ -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