From f63a451b875e53f2fc206aef4a21c13fe0e4faa5 Mon Sep 17 00:00:00 2001 From: Autumn-27 <53844290+Autumn-27@users.noreply.github.com> Date: Sun, 23 Aug 2026 09:08:05 -0400 Subject: [PATCH] =?UTF-8?q?feat(tasks):=20=E4=BB=BB=E5=8A=A1=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E5=8F=AF=E9=80=89=E5=90=8D=E7=A7=B0=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- db/findings.go | 14 +++++---- db/schema.sql | 3 ++ db/tasks.go | 13 +++++--- server/dto.go | 2 ++ server/manager.go | 2 ++ server/server.go | 2 ++ web/src/app/(main)/function/findings/page.tsx | 8 +++-- .../app/(main)/function/tasks/detail/page.tsx | 4 +-- web/src/app/(main)/function/tasks/page.tsx | 31 +++++++++++++++++-- web/src/lib/api.ts | 2 ++ web/src/lib/mock/handler.ts | 9 ++++-- web/src/lib/types.ts | 3 ++ 12 files changed, 72 insertions(+), 21 deletions(-) diff --git a/db/findings.go b/db/findings.go index 485a016..b79f00c 100644 --- a/db/findings.go +++ b/db/findings.go @@ -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) diff --git a/db/schema.sql b/db/schema.sql index 968bd74..7f2ae59 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -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; diff --git a/db/tasks.go b/db/tasks.go index cddadd3..c7a94ac 100644 --- a/db/tasks.go +++ b/db/tasks.go @@ -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 diff --git a/server/dto.go b/server/dto.go index f818a9c..6a9f62f 100644 --- a/server/dto.go +++ b/server/dto.go @@ -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, diff --git a/server/manager.go b/server/manager.go index 4bac104..38ce41b 100644 --- a/server/manager.go +++ b/server/manager.go @@ -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, diff --git a/server/server.go b/server/server.go index f376df2..590eb9c 100644 --- a/server/server.go +++ b/server/server.go @@ -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, diff --git a/web/src/app/(main)/function/findings/page.tsx b/web/src/app/(main)/function/findings/page.tsx index 561592c..0ba7988 100644 --- a/web/src/app/(main)/function/findings/page.tsx +++ b/web/src/app/(main)/function/findings/page.tsx @@ -626,7 +626,7 @@ export default function FindingsPage() { 未关联 / 任务已删除 {(stats.tasks ?? []).map((t) => { const id = String(t.id); - const label = t.description || `任务 #${id}(已删除)`; + const label = t.name || t.description || `任务 #${id}(已删除)`; return ( @@ -704,7 +704,11 @@ export default function FindingsPage() { />
- {group.task_id === null ? "未关联 / 任务已删除" : `任务 #${group.task_id}`} + {group.task_id === null + ? "未关联 / 任务已删除" + : group.task_name + ? `${group.task_name}(任务 #${group.task_id})` + : `任务 #${group.task_id}`} {group.task_description || "来源任务不可用"} diff --git a/web/src/app/(main)/function/tasks/detail/page.tsx b/web/src/app/(main)/function/tasks/detail/page.tsx index b58f702..b07434a 100644 --- a/web/src/app/(main)/function/tasks/detail/page.tsx +++ b/web/src/app/(main)/function/tasks/detail/page.tsx @@ -314,8 +314,8 @@ function TaskDetailInner() { -

- {task.description} +

+ {task.name || task.description}

{task.id} diff --git a/web/src/app/(main)/function/tasks/page.tsx b/web/src/app/(main)/function/tasks/page.tsx index bb95cc2..d221d00 100644 --- a/web/src/app/(main)/function/tasks/page.tsx +++ b/web/src/app/(main)/function/tasks/page.tsx @@ -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() { /> ID + 名称 描述 目标 状态 @@ -741,13 +745,22 @@ const TaskRow = React.memo(function TaskRow({ - {task.description} + {task.name || 未命名} {task.active && }
+ + + {task.description} + + {task.goal} @@ -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(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} /> +
+ + setName(e.target.value)} + /> +