mirror of
https://github.com/RuoJi6/ARTEX.git
synced 2026-10-28 01:03:50 +08:00
feat: 任务/模板备注展示与 Worker 运行数量配置
- 新建任务可填写备注;详情页头部展示备注(列表原有 RemarkEditor 保留) - 任务模板支持备注与 Worker 数量,应用/另存为模板时同步带入 - 新建任务高级设置与模板均可单独设置 Worker 运行数量(0=回落全局默认) - schema/API/引擎贯通:tasks.worker_count、task_templates.remark/worker_count
This commit is contained in:
1 parent
620ca5d96c
commit
45d4e4e67b
15 files changed
+250
-26
No files matched your search
@@ -524,6 +524,7 @@ CREATE TABLE IF NOT EXISTS tasks (
|
||||
timeout_seconds INTEGER NOT NULL DEFAULT 0,
|
||||
plan_heartbeat_seconds INTEGER NOT NULL DEFAULT 300,
|
||||
coverage_enabled BOOLEAN NOT NULL DEFAULT true,
|
||||
worker_count INTEGER NOT NULL DEFAULT 0, -- 0=用全局默认 workers
|
||||
pinned_at TIMESTAMPTZ,
|
||||
first_run_at TIMESTAMPTZ,
|
||||
deadline_at TIMESTAMPTZ,
|
||||
@@ -546,6 +547,8 @@ ALTER TABLE tasks ADD COLUMN IF NOT EXISTS queued BOOLEAN NOT NULL DEFAULT false
|
||||
-- 给 agent 开放 add_task_scope/list_untested_assets;false=全部关闭(见 task_scope.go)。
|
||||
-- 存量任务默认 true 保持原行为;company 关联(task_scope kind=company)不受此开关影响。
|
||||
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS coverage_enabled BOOLEAN NOT NULL DEFAULT true;
|
||||
-- 任务级 Worker 运行数量;0=用全局默认(settings.workers)。补旧库。
|
||||
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS worker_count INTEGER NOT NULL DEFAULT 0;
|
||||
-- queued_at makes admission FIFO reflect the actual enqueue order rather than the
|
||||
-- task creation order. queue_mode distinguishes first bootstrap from resuming an
|
||||
-- exploration that already owns goals/history.
|
||||
@@ -617,16 +620,21 @@ CREATE TABLE IF NOT EXISTS task_templates (
|
||||
nkey TEXT NOT NULL UNIQUE,
|
||||
description TEXT NOT NULL,
|
||||
goal TEXT NOT NULL,
|
||||
remark TEXT NOT NULL DEFAULT '',
|
||||
-- 预设的任务分类;分类删除时置空(与 tasks.category_id 一致,不阻断)。
|
||||
category_id BIGINT REFERENCES task_categories(id) ON DELETE SET NULL,
|
||||
-- 预设的任务级拦截/允许规则快照(AssetInterceptRuleInput 数组);应用模板时灌进新任务。
|
||||
intercept_rules JSONB NOT NULL DEFAULT '[]',
|
||||
-- 预设的任务级 Worker 运行数量;0=用全局默认。
|
||||
worker_count INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
-- 补旧库(已发版,加列带 IF NOT EXISTS)。
|
||||
ALTER TABLE task_templates ADD COLUMN IF NOT EXISTS category_id BIGINT REFERENCES task_categories(id) ON DELETE SET NULL;
|
||||
ALTER TABLE task_templates ADD COLUMN IF NOT EXISTS intercept_rules JSONB NOT NULL DEFAULT '[]';
|
||||
ALTER TABLE task_templates ADD COLUMN IF NOT EXISTS remark TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE task_templates ADD COLUMN IF NOT EXISTS worker_count INTEGER NOT NULL DEFAULT 0;
|
||||
CREATE INDEX IF NOT EXISTS idx_task_templates_updated ON task_templates(updated_at DESC, id DESC);
|
||||
DROP TRIGGER IF EXISTS trg_task_templates_upd ON task_templates;
|
||||
CREATE TRIGGER trg_task_templates_upd BEFORE UPDATE ON task_templates
|
||||
|
||||
@@ -122,7 +122,7 @@ AND NOT EXISTS (
|
||||
if _, err := tx.Exec(`UPDATE tasks SET
|
||||
name='',remark='',category_id=NULL,description='',goal='',paused=true,queued=false,queued_at=NULL,queue_mode='',
|
||||
llm_profile_id=NULL,active_llm_profile_id=NULL,llm_chain_revision=llm_chain_revision+1,
|
||||
company_id=NULL,parent_ref=NULL,timeout_seconds=0,coverage_enabled=true,pinned_at=NULL,
|
||||
company_id=NULL,parent_ref=NULL,timeout_seconds=0,coverage_enabled=true,worker_count=0,pinned_at=NULL,
|
||||
first_run_at=NULL,deadline_at=NULL,archived_at=now(),deleted_at=now()
|
||||
WHERE id=$1`, taskID); err != nil {
|
||||
return err
|
||||
@@ -322,7 +322,7 @@ func restoreTaskStub(tx *sql.Tx, row map[string]any, taskID, remaining int64) er
|
||||
llm_profile_id=archived.llm_profile_id,active_llm_profile_id=archived.active_llm_profile_id,
|
||||
llm_chain_revision=archived.llm_chain_revision,company_id=archived.company_id,parent_ref=archived.parent_ref,
|
||||
timeout_seconds=archived.timeout_seconds,plan_heartbeat_seconds=archived.plan_heartbeat_seconds,
|
||||
coverage_enabled=archived.coverage_enabled,pinned_at=archived.pinned_at,first_run_at=archived.first_run_at,
|
||||
coverage_enabled=archived.coverage_enabled,worker_count=COALESCE(archived.worker_count,0),pinned_at=archived.pinned_at,first_run_at=archived.first_run_at,
|
||||
deadline_at=CASE WHEN archived.paused AND $3>0 THEN now()+make_interval(secs=>$3::double precision)
|
||||
ELSE archived.deadline_at END,
|
||||
deleted_at=NULL,archived_at=NULL,completed_at=archived.completed_at,
|
||||
|
||||
+36
-9
@@ -31,8 +31,10 @@ type TaskTemplate struct {
|
||||
NKey string `json:"-"`
|
||||
Description string `json:"description"`
|
||||
Goal string `json:"goal"`
|
||||
Remark string `json:"remark"`
|
||||
CategoryID *int64 `json:"category_id"`
|
||||
InterceptRules []TaskInterceptRuleInput `json:"intercept_rules"`
|
||||
WorkerCount int `json:"worker_count"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
@@ -42,8 +44,10 @@ type TaskTemplateInput struct {
|
||||
Name string
|
||||
Description string
|
||||
Goal string
|
||||
Remark string
|
||||
CategoryID *int64
|
||||
InterceptRules []TaskInterceptRuleInput
|
||||
WorkerCount int
|
||||
}
|
||||
|
||||
// TaskTemplatePatch changes only fields flagged as set. Name/Description/Goal use
|
||||
@@ -53,18 +57,20 @@ type TaskTemplatePatch struct {
|
||||
Name *string
|
||||
Description *string
|
||||
Goal *string
|
||||
Remark *string
|
||||
CategoryID *int64
|
||||
SetCategoryID bool
|
||||
InterceptRules []TaskInterceptRuleInput
|
||||
SetInterceptRules bool
|
||||
WorkerCount *int
|
||||
}
|
||||
|
||||
const taskTemplateCols = `id, name, nkey, description, goal, category_id, intercept_rules, created_at, updated_at`
|
||||
const taskTemplateCols = `id, name, nkey, description, goal, COALESCE(remark,''), category_id, intercept_rules, COALESCE(worker_count,0), created_at, updated_at`
|
||||
|
||||
func scanTaskTemplate(row interface{ Scan(...any) error }) (TaskTemplate, error) {
|
||||
var t TaskTemplate
|
||||
var rulesRaw []byte
|
||||
if err := row.Scan(&t.ID, &t.Name, &t.NKey, &t.Description, &t.Goal, &t.CategoryID, &rulesRaw, &t.CreatedAt, &t.UpdatedAt); err != nil {
|
||||
if err := row.Scan(&t.ID, &t.Name, &t.NKey, &t.Description, &t.Goal, &t.Remark, &t.CategoryID, &rulesRaw, &t.WorkerCount, &t.CreatedAt, &t.UpdatedAt); err != nil {
|
||||
return t, err
|
||||
}
|
||||
t.InterceptRules = []TaskInterceptRuleInput{}
|
||||
@@ -112,11 +118,16 @@ func normalizeTaskTemplateInput(in TaskTemplateInput) (TaskTemplateInput, string
|
||||
case utf8.RuneCountInString(in.Goal) > MaxTaskTemplateTextRunes:
|
||||
return in, "", fmt.Errorf("%w: goal exceeds %d characters", ErrTaskTemplateInvalid, MaxTaskTemplateTextRunes)
|
||||
}
|
||||
in.Remark = strings.TrimSpace(in.Remark)
|
||||
if utf8.RuneCountInString(in.Remark) > MaxTaskTemplateTextRunes {
|
||||
return in, "", fmt.Errorf("%w: remark exceeds %d characters", ErrTaskTemplateInvalid, MaxTaskTemplateTextRunes)
|
||||
}
|
||||
in.WorkerCount = normalizeWorkerCount(in.WorkerCount)
|
||||
return in, taskTemplateNKey(in.Name), nil
|
||||
}
|
||||
|
||||
func normalizeTaskTemplatePatch(patch TaskTemplatePatch) (TaskTemplatePatch, *string, error) {
|
||||
if patch.Name == nil && patch.Description == nil && patch.Goal == nil && !patch.SetCategoryID && !patch.SetInterceptRules {
|
||||
if patch.Name == nil && patch.Description == nil && patch.Goal == nil && patch.Remark == nil && !patch.SetCategoryID && !patch.SetInterceptRules && patch.WorkerCount == nil {
|
||||
return patch, nil, fmt.Errorf("%w: no fields supplied", ErrTaskTemplateInvalid)
|
||||
}
|
||||
var nkey *string
|
||||
@@ -152,6 +163,17 @@ func normalizeTaskTemplatePatch(patch TaskTemplatePatch) (TaskTemplatePatch, *st
|
||||
}
|
||||
patch.Goal = &goal
|
||||
}
|
||||
if patch.Remark != nil {
|
||||
remark := strings.TrimSpace(*patch.Remark)
|
||||
if utf8.RuneCountInString(remark) > MaxTaskTemplateTextRunes {
|
||||
return patch, nil, fmt.Errorf("%w: remark exceeds %d characters", ErrTaskTemplateInvalid, MaxTaskTemplateTextRunes)
|
||||
}
|
||||
patch.Remark = &remark
|
||||
}
|
||||
if patch.WorkerCount != nil {
|
||||
n := normalizeWorkerCount(*patch.WorkerCount)
|
||||
patch.WorkerCount = &n
|
||||
}
|
||||
return patch, nkey, nil
|
||||
}
|
||||
|
||||
@@ -171,10 +193,10 @@ func (d *DB) CreateTaskTemplate(in TaskTemplateInput) (*TaskTemplate, error) {
|
||||
return nil, err
|
||||
}
|
||||
t, err := scanTaskTemplate(d.QueryRow(`
|
||||
INSERT INTO task_templates(name, nkey, description, goal, category_id, intercept_rules)
|
||||
VALUES ($1,$2,$3,$4,$5,$6)
|
||||
INSERT INTO task_templates(name, nkey, description, goal, remark, category_id, intercept_rules, worker_count)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8)
|
||||
ON CONFLICT (nkey) DO NOTHING
|
||||
RETURNING `+taskTemplateCols, in.Name, nkey, in.Description, in.Goal, in.CategoryID, rulesJSON))
|
||||
RETURNING `+taskTemplateCols, in.Name, nkey, in.Description, in.Goal, in.Remark, in.CategoryID, rulesJSON, in.WorkerCount))
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, ErrTaskTemplateNameConflict
|
||||
}
|
||||
@@ -221,7 +243,8 @@ func (d *DB) UpdateTaskTemplate(id int64, in TaskTemplateInput) (*TaskTemplate,
|
||||
return nil, err
|
||||
}
|
||||
return d.PatchTaskTemplate(id, TaskTemplatePatch{
|
||||
Name: &in.Name, Description: &in.Description, Goal: &in.Goal,
|
||||
Name: &in.Name, Description: &in.Description, Goal: &in.Goal, Remark: &in.Remark,
|
||||
WorkerCount: &in.WorkerCount,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -242,16 +265,20 @@ SET name=CASE WHEN $2 THEN $3::text ELSE name END,
|
||||
nkey=CASE WHEN $2 THEN $4::text ELSE nkey END,
|
||||
description=CASE WHEN $5 THEN $6::text ELSE description END,
|
||||
goal=CASE WHEN $7 THEN $8::text ELSE goal END,
|
||||
category_id=CASE WHEN $9 THEN $10::bigint ELSE category_id END,
|
||||
intercept_rules=CASE WHEN $11 THEN $12::jsonb ELSE intercept_rules END
|
||||
remark=CASE WHEN $9 THEN $10::text ELSE remark END,
|
||||
category_id=CASE WHEN $11 THEN $12::bigint ELSE category_id END,
|
||||
intercept_rules=CASE WHEN $13 THEN $14::jsonb ELSE intercept_rules END,
|
||||
worker_count=CASE WHEN $15 THEN $16::integer ELSE worker_count END
|
||||
WHERE id=$1
|
||||
RETURNING `+taskTemplateCols,
|
||||
id,
|
||||
patch.Name != nil, patch.Name, nkey,
|
||||
patch.Description != nil, patch.Description,
|
||||
patch.Goal != nil, patch.Goal,
|
||||
patch.Remark != nil, patch.Remark,
|
||||
patch.SetCategoryID, patch.CategoryID,
|
||||
patch.SetInterceptRules, rulesJSON,
|
||||
patch.WorkerCount != nil, patch.WorkerCount,
|
||||
))
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, ErrTaskTemplateNotFound
|
||||
|
||||
+35
-6
@@ -6,7 +6,9 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Task is a row in the task registry (1:1 with an exploration).
|
||||
@@ -51,6 +53,9 @@ type Task struct {
|
||||
// list_untested_assets、态势里不注入 coverage 块(scope 字段仍保留)。company 关联
|
||||
// (task_scope kind=company)与此开关无关,永不受影响。见 db/task_scope.go。
|
||||
CoverageEnabled bool `json:"coverage_enabled"`
|
||||
// WorkerCount is the per-task concurrent work-agent count. 0 means use the
|
||||
// global settings.workers default when the engine starts this task.
|
||||
WorkerCount int `json:"worker_count"`
|
||||
}
|
||||
|
||||
// TaskDeleteResult reports optional related-data cleanup performed in the same
|
||||
@@ -132,6 +137,21 @@ func normalizeHeartbeat(sec int) int {
|
||||
return sec
|
||||
}
|
||||
|
||||
// MaxTaskWorkerCount is the hard upper bound for a per-task worker override.
|
||||
const MaxTaskWorkerCount = 50
|
||||
|
||||
// normalizeWorkerCount clamps a per-task worker override into [0, MaxTaskWorkerCount].
|
||||
// 0 means "use the global settings.workers default".
|
||||
func normalizeWorkerCount(n int) int {
|
||||
if n < 0 {
|
||||
return 0
|
||||
}
|
||||
if n > MaxTaskWorkerCount {
|
||||
return MaxTaskWorkerCount
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (d *DB) CreateTask(description, goal string, llmProfileID *int64, timeoutSeconds, planHeartbeatSeconds int) (*Task, error) {
|
||||
var ids []int64
|
||||
if llmProfileID != nil {
|
||||
@@ -156,6 +176,10 @@ type TaskCreateOptions struct {
|
||||
// CoverageEnabled 是「资产覆盖度功能」开关;nil=默认开(true),让不关心该开关的创建
|
||||
// 路径(编排 spawn、老 API)沿用原行为。仅 web 创建任务时可显式传 false 关闭。
|
||||
CoverageEnabled *bool
|
||||
// WorkerCount 是本任务并发运行的工作 Agent 数;0/省略=用全局默认。上限 50。
|
||||
WorkerCount int
|
||||
// Remark 是创建时可带的任务备注(最多 2000 字);空=无备注。
|
||||
Remark string
|
||||
// InterceptRules 是任务级资产拦截规则,创建时随任务在同一事务内写入 task_intercept_rules。
|
||||
InterceptRules []TaskInterceptRuleInput
|
||||
}
|
||||
@@ -200,6 +224,11 @@ VALUES ($1, 'fact', $2, 0, 'origin', 'system')`, expID, string(originPayload));
|
||||
}
|
||||
opts.PlanHeartbeatSeconds = normalizeHeartbeat(opts.PlanHeartbeatSeconds)
|
||||
coverageEnabled := opts.CoverageEnabled == nil || *opts.CoverageEnabled
|
||||
opts.WorkerCount = normalizeWorkerCount(opts.WorkerCount)
|
||||
opts.Remark = strings.TrimSpace(opts.Remark)
|
||||
if utf8.RuneCountInString(opts.Remark) > 2000 {
|
||||
return nil, fmt.Errorf("备注最多 2000 个字符")
|
||||
}
|
||||
var categoryName string
|
||||
if opts.CategoryID != nil {
|
||||
if *opts.CategoryID <= 0 {
|
||||
@@ -225,12 +254,12 @@ VALUES ($1, 'fact', $2, 0, 'origin', 'system')`, expID, string(originPayload));
|
||||
SourceTaskIDs: append([]int64(nil), opts.SourceTaskIDs...),
|
||||
CompanyIDs: append([]int64(nil), opts.CompanyIDs...),
|
||||
TimeoutSeconds: opts.TimeoutSeconds, PlanHeartbeatSeconds: opts.PlanHeartbeatSeconds,
|
||||
CoverageEnabled: coverageEnabled,
|
||||
CoverageEnabled: coverageEnabled, WorkerCount: opts.WorkerCount, Remark: opts.Remark,
|
||||
}
|
||||
if err := tx.QueryRow(`
|
||||
INSERT INTO tasks(name, category_id, description, goal, exploration_id, llm_profile_id, active_llm_profile_id, timeout_seconds, plan_heartbeat_seconds, coverage_enabled)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$6,$7,$8,$9)
|
||||
RETURNING id, status, paused, created_at`, opts.Name, opts.CategoryID, description, goal, expID, active, opts.TimeoutSeconds, opts.PlanHeartbeatSeconds, coverageEnabled).Scan(&t.ID, &t.Status, &t.Paused, &t.CreatedAt); err != nil {
|
||||
INSERT INTO tasks(name, remark, category_id, description, goal, exploration_id, llm_profile_id, active_llm_profile_id, timeout_seconds, plan_heartbeat_seconds, coverage_enabled, worker_count)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$7,$8,$9,$10,$11)
|
||||
RETURNING id, status, paused, created_at`, opts.Name, opts.Remark, opts.CategoryID, description, goal, expID, active, opts.TimeoutSeconds, opts.PlanHeartbeatSeconds, coverageEnabled, opts.WorkerCount).Scan(&t.ID, &t.Status, &t.Paused, &t.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := insertTaskRelations(tx, t.ID, opts.SourceTaskIDs); err != nil {
|
||||
@@ -363,11 +392,11 @@ func insertTaskLLMProfiles(tx *sql.Tx, taskID int64, profileIDs []int64) error {
|
||||
|
||||
const taskCols = `id, COALESCE(name,''), remark, category_id,
|
||||
COALESCE((SELECT category.name FROM task_categories category WHERE category.id=tasks.category_id),''),
|
||||
description, goal, exploration_id, status, paused, queued, queued_at, COALESCE(queue_mode,''), llm_profile_id, active_llm_profile_id, COALESCE(parent_ref,''), pinned_at, created_at, completed_at, COALESCE(timeout_seconds,0), COALESCE(plan_heartbeat_seconds,300), COALESCE(coverage_enabled,true), first_run_at, deadline_at`
|
||||
description, goal, exploration_id, status, paused, queued, queued_at, COALESCE(queue_mode,''), llm_profile_id, active_llm_profile_id, COALESCE(parent_ref,''), pinned_at, created_at, completed_at, COALESCE(timeout_seconds,0), COALESCE(plan_heartbeat_seconds,300), COALESCE(coverage_enabled,true), COALESCE(worker_count,0), first_run_at, deadline_at`
|
||||
|
||||
func scanTask(sc interface{ Scan(...any) error }) (*Task, error) {
|
||||
var t Task
|
||||
if err := sc.Scan(&t.ID, &t.Name, &t.Remark, &t.CategoryID, &t.CategoryName, &t.Description, &t.Goal, &t.ExplorationID, &t.Status, &t.Paused, &t.Queued, &t.QueuedAt, &t.QueueMode, &t.LLMProfileID, &t.ActiveLLMProfileID, &t.ParentRef, &t.PinnedAt, &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.Remark, &t.CategoryID, &t.CategoryName, &t.Description, &t.Goal, &t.ExplorationID, &t.Status, &t.Paused, &t.Queued, &t.QueuedAt, &t.QueueMode, &t.LLMProfileID, &t.ActiveLLMProfileID, &t.ParentRef, &t.PinnedAt, &t.CreatedAt, &t.CompletedAt, &t.TimeoutSeconds, &t.PlanHeartbeatSeconds, &t.CoverageEnabled, &t.WorkerCount, &t.FirstRunAt, &t.DeadlineAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.Pinned = t.PinnedAt != nil
|
||||
|
||||
@@ -64,6 +64,7 @@ type TaskDTO struct {
|
||||
ArchiveBlockedBy string `json:"archive_blocked_by_task_id,omitempty"`
|
||||
CompanyIDs []int64 `json:"company_ids"`
|
||||
CoverageEnabled bool `json:"coverage_enabled"` // 资产覆盖度功能开关(创建时定)
|
||||
WorkerCount int `json:"worker_count"` // 本任务 Worker 数;0=用全局默认
|
||||
}
|
||||
|
||||
// FindingSeverityDTO 是任务列表里按严重度分档的漏洞计数(严重/高/中/低)。
|
||||
@@ -138,6 +139,7 @@ func taskDTO(t *Task, status string) TaskDTO {
|
||||
SourceTaskIDs: sourceIDs,
|
||||
CompanyIDs: lifecycle.CompanyIDs,
|
||||
CoverageEnabled: t.CoverageEnabled,
|
||||
WorkerCount: t.WorkerCount,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-1
@@ -746,7 +746,10 @@ func (e *Engine) ReadyFor(t *Task) bool {
|
||||
// but no-op until an LLM is configured (so a task created while idle picks up
|
||||
// automatically once LLM is set from the UI).
|
||||
func (e *Engine) Run(ctx context.Context, t *Task) {
|
||||
workers := e.m.Workers()
|
||||
workers := t.WorkerCount
|
||||
if workers <= 0 {
|
||||
workers = e.m.Workers()
|
||||
}
|
||||
e.deleteMu.RLock()
|
||||
if e.IsDeleting(t.ID) {
|
||||
e.deleteMu.RUnlock()
|
||||
|
||||
+2
-1
@@ -59,6 +59,7 @@ type Task struct {
|
||||
TimeoutSeconds int `json:"timeout_seconds"`
|
||||
PlanHeartbeatSeconds int `json:"plan_heartbeat_seconds"` // planner 心跳触发间隔(秒)
|
||||
CoverageEnabled bool `json:"coverage_enabled"` // 资产覆盖度功能开关(创建时定,默认开)
|
||||
WorkerCount int `json:"worker_count"` // 本任务 Worker 数;0=用全局默认
|
||||
FirstRunAt int64 `json:"first_run_at,omitempty"`
|
||||
DeadlineAt int64 `json:"deadline_at,omitempty"`
|
||||
Store *pgdb.ExplorationStore `json:"-"`
|
||||
@@ -849,7 +850,7 @@ func taskFromPG(pt *pgdb.Task, store *pgdb.ExplorationStore, ic *intercept.Inter
|
||||
SourceTaskIDs: append([]int64(nil), pt.SourceTaskIDs...),
|
||||
CompanyIDs: append([]int64(nil), pt.CompanyIDs...),
|
||||
TimeoutSeconds: pt.TimeoutSeconds, PlanHeartbeatSeconds: pt.PlanHeartbeatSeconds,
|
||||
CoverageEnabled: pt.CoverageEnabled,
|
||||
CoverageEnabled: pt.CoverageEnabled, WorkerCount: pt.WorkerCount,
|
||||
FirstRunAt: unixOrZero(pt.FirstRunAt), DeadlineAt: unixOrZero(pt.DeadlineAt),
|
||||
Store: store, Guard: guard.NewWithInterceptor(ic), notify: make(chan struct{}, 1),
|
||||
}
|
||||
|
||||
@@ -1540,6 +1540,8 @@ type createTaskReq struct {
|
||||
PlanHeartbeatSeconds int `json:"plan_heartbeat_seconds"` // planner 心跳触发间隔(秒);0/省略=默认600(10min);下限=默认=600,低于自动抬到600
|
||||
SeedFirstIntent *bool `json:"seed_first_intent,omitempty"` // 创建时直接下发一条种子意图(内容=描述+目标),让 worker 免等首轮 planner 直接开跑;省略/null=默认关闭,走标准先规划再执行。显式传 true 才开(CTF 常一 work 解决时可省掉开跑前的 planner 轮)。
|
||||
CoverageEnabled *bool `json:"coverage_enabled,omitempty"` // 资产覆盖度功能;省略/null=默认开(true)。false=关闭覆盖度计算/展示/自动累积范围+隐藏 add_task_scope/list_untested_assets。company 关联不受影响。
|
||||
Remark string `json:"remark,omitempty"` // 任务备注(最多2000字);空=无
|
||||
WorkerCount int `json:"worker_count,omitempty"` // 本任务 Worker 数;0/省略=用全局默认;上限50
|
||||
// InterceptRules 任务级资产拦截/允许规则(创建时录入,存 task_intercept_rules,不进全局表)。
|
||||
InterceptRules []taskInterceptRuleReq `json:"intercept_rules,omitempty"`
|
||||
}
|
||||
@@ -1603,12 +1605,27 @@ func (s *Server) createTask(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
schedule = &input
|
||||
}
|
||||
remark, err := normalizeRemark(req.Remark)
|
||||
if err != nil {
|
||||
writeErr(w, 400, err.Error())
|
||||
return
|
||||
}
|
||||
if req.WorkerCount < 0 {
|
||||
writeErr(w, 400, "Worker 运行数量不能为负数")
|
||||
return
|
||||
}
|
||||
if req.WorkerCount > db.MaxTaskWorkerCount {
|
||||
writeErr(w, 400, fmt.Sprintf("Worker 运行数量最多 %d", db.MaxTaskWorkerCount))
|
||||
return
|
||||
}
|
||||
t, err := s.m.CreateTaskWithOptions(req.Description, req.Goal, db.TaskCreateOptions{
|
||||
Schedule: schedule,
|
||||
Name: strings.TrimSpace(req.Name), CategoryID: req.CategoryID,
|
||||
SourceTaskIDs: sourceIDs, CompanyIDs: req.CompanyIDs, LLMProfileIDs: req.LLMProfileIDs,
|
||||
TimeoutSeconds: req.TimeoutSeconds, PlanHeartbeatSeconds: req.PlanHeartbeatSeconds,
|
||||
CoverageEnabled: req.CoverageEnabled,
|
||||
Remark: remark,
|
||||
WorkerCount: req.WorkerCount,
|
||||
InterceptRules: interceptRules,
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -22,8 +22,10 @@ type taskTemplateRequest struct {
|
||||
Name *string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
Goal *string `json:"goal"`
|
||||
Remark *string `json:"remark"`
|
||||
CategoryID *int64 `json:"category_id"`
|
||||
InterceptRules []taskInterceptRuleReq `json:"intercept_rules"`
|
||||
WorkerCount *int `json:"worker_count"`
|
||||
}
|
||||
|
||||
// decodeTaskTemplateRequest decodes the body into req and returns the set of
|
||||
@@ -65,6 +67,7 @@ func validateTaskTemplateRequest(req taskTemplateRequest) error {
|
||||
{name: "name", value: req.Name, limit: db.MaxTaskTemplateNameRunes},
|
||||
{name: "description", value: req.Description, limit: db.MaxTaskTemplateTextRunes},
|
||||
{name: "goal", value: req.Goal, limit: db.MaxTaskTemplateTextRunes},
|
||||
{name: "remark", value: req.Remark, limit: db.MaxTaskTemplateTextRunes},
|
||||
}
|
||||
for _, check := range checks {
|
||||
if check.value == nil {
|
||||
@@ -78,6 +81,14 @@ func validateTaskTemplateRequest(req taskTemplateRequest) error {
|
||||
return fmt.Errorf("%s 最多 %d 个字符", check.name, check.limit)
|
||||
}
|
||||
}
|
||||
if req.WorkerCount != nil {
|
||||
if *req.WorkerCount < 0 {
|
||||
return fmt.Errorf("Worker 运行数量不能为负数")
|
||||
}
|
||||
if *req.WorkerCount > db.MaxTaskWorkerCount {
|
||||
return fmt.Errorf("Worker 运行数量最多 %d", db.MaxTaskWorkerCount)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -125,12 +136,18 @@ func (s *Server) pgCreateTaskTemplate(w http.ResponseWriter, r *http.Request) {
|
||||
writeErr(w, http.StatusBadRequest, "拦截/允许规则无效:"+err.Error())
|
||||
return
|
||||
}
|
||||
workerCount := 0
|
||||
if req.WorkerCount != nil {
|
||||
workerCount = *req.WorkerCount
|
||||
}
|
||||
template, err := pg.CreateTaskTemplate(db.TaskTemplateInput{
|
||||
Name: stringValue(req.Name),
|
||||
Description: stringValue(req.Description),
|
||||
Goal: stringValue(req.Goal),
|
||||
Remark: stringValue(req.Remark),
|
||||
CategoryID: req.CategoryID,
|
||||
InterceptRules: rules,
|
||||
WorkerCount: workerCount,
|
||||
})
|
||||
if err != nil {
|
||||
writeTaskTemplateErr(w, err)
|
||||
@@ -160,11 +177,21 @@ func (s *Server) pgUpdateTaskTemplate(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
_, catPresent := present["category_id"]
|
||||
_, rulesPresent := present["intercept_rules"]
|
||||
if req.Name == nil && req.Description == nil && req.Goal == nil && !catPresent && !rulesPresent {
|
||||
writeErr(w, http.StatusBadRequest, "至少需要提供 name、description、goal、category_id 或 intercept_rules")
|
||||
_, remarkPresent := present["remark"]
|
||||
_, workerPresent := present["worker_count"]
|
||||
if req.Name == nil && req.Description == nil && req.Goal == nil && !catPresent && !rulesPresent && !remarkPresent && !workerPresent {
|
||||
writeErr(w, http.StatusBadRequest, "至少需要提供 name、description、goal、remark、category_id、intercept_rules 或 worker_count")
|
||||
return
|
||||
}
|
||||
patch := db.TaskTemplatePatch{Name: req.Name, Description: req.Description, Goal: req.Goal}
|
||||
if remarkPresent {
|
||||
if req.Remark == nil {
|
||||
empty := ""
|
||||
patch.Remark = &empty
|
||||
} else {
|
||||
patch.Remark = req.Remark
|
||||
}
|
||||
}
|
||||
if catPresent {
|
||||
patch.SetCategoryID = true
|
||||
patch.CategoryID = req.CategoryID
|
||||
@@ -178,6 +205,13 @@ func (s *Server) pgUpdateTaskTemplate(w http.ResponseWriter, r *http.Request) {
|
||||
patch.SetInterceptRules = true
|
||||
patch.InterceptRules = rules
|
||||
}
|
||||
if workerPresent {
|
||||
n := 0
|
||||
if req.WorkerCount != nil {
|
||||
n = *req.WorkerCount
|
||||
}
|
||||
patch.WorkerCount = &n
|
||||
}
|
||||
template, err := pg.PatchTaskTemplate(id, patch)
|
||||
if err != nil {
|
||||
writeTaskTemplateErr(w, err)
|
||||
|
||||
@@ -404,6 +404,11 @@ function TaskDetailInner() {
|
||||
</Button>
|
||||
</div>
|
||||
<p className="truncate text-xs text-muted-foreground">{task.goal}</p>
|
||||
{task.remark ? (
|
||||
<p className="truncate text-xs text-muted-foreground" title={task.remark}>
|
||||
备注:{task.remark}
|
||||
</p>
|
||||
) : null}
|
||||
{/* Tabs */}
|
||||
<div className="no-scrollbar min-w-0 overflow-x-auto">
|
||||
<TabsList variant="default" className="min-w-max">
|
||||
|
||||
@@ -3104,6 +3104,7 @@ function CreateTaskSheet({
|
||||
}) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [name, setName] = React.useState("");
|
||||
const [remark, setRemark] = React.useState("");
|
||||
const [categoryID, setCategoryID] = React.useState<number | undefined>(undefined);
|
||||
const [description, setDescription] = React.useState("");
|
||||
const [goal, setGoal] = React.useState("");
|
||||
@@ -3118,6 +3119,7 @@ function CreateTaskSheet({
|
||||
const [heartbeatMin, setHeartbeatMin] = React.useState("10"); // planner 心跳(分钟);默认10,下限10(与后端一致)
|
||||
const [seedFirstIntent, setSeedFirstIntent] = React.useState(false); // 创建时下发种子意图,worker 免等首轮 planner 直接开跑;默认关闭,走标准先规划再执行
|
||||
const [coverageEnabled, setCoverageEnabled] = React.useState(true); // 资产覆盖度功能;默认开。关闭=不计算/展示覆盖度、不累积范围、隐藏范围类工具(company 关联不受影响)
|
||||
const [workerCount, setWorkerCount] = React.useState(""); // 本任务 Worker 数;空/0=用全局默认
|
||||
const [interceptRules, setInterceptRules] = React.useState<AssetInterceptRuleInput[]>([]); // 任务级资产拦截规则(仅本任务生效,不进全局表)
|
||||
const [scheduleEnabled, setScheduleEnabled] = React.useState(false);
|
||||
const [schedule, setSchedule] = React.useState<ScheduleInput>({
|
||||
@@ -3185,8 +3187,10 @@ function CreateTaskSheet({
|
||||
try {
|
||||
const timeoutSec = Math.max(0, Math.floor(Number(timeoutMin) || 0)) * 60;
|
||||
const heartbeatSec = Math.max(10, Math.floor(Number(heartbeatMin) || 10)) * 60; // 下限 10min,与后端归一一致
|
||||
const workerCountNum = Math.min(50, Math.max(0, Math.floor(Number(workerCount) || 0)));
|
||||
await api.createTask({
|
||||
name: name.trim(),
|
||||
remark: remark.trim(),
|
||||
categoryId: categoryID,
|
||||
description: description.trim(),
|
||||
goal: goal.trim(),
|
||||
@@ -3197,6 +3201,7 @@ function CreateTaskSheet({
|
||||
seedFirstIntent,
|
||||
planHeartbeatSeconds: heartbeatSec,
|
||||
coverageEnabled,
|
||||
workerCount: workerCountNum,
|
||||
schedule: scheduleEnabled ? { ...schedule, task_ids: [] } : undefined,
|
||||
interceptRules: interceptRules
|
||||
.map((r) => ({ ...r, pattern: r.pattern.trim() }))
|
||||
@@ -3204,6 +3209,7 @@ function CreateTaskSheet({
|
||||
});
|
||||
toast.success("任务已创建");
|
||||
setName("");
|
||||
setRemark("");
|
||||
setCategoryID(undefined);
|
||||
setDescription("");
|
||||
setGoal("");
|
||||
@@ -3215,6 +3221,7 @@ function CreateTaskSheet({
|
||||
setHeartbeatMin("10");
|
||||
setSeedFirstIntent(false);
|
||||
setCoverageEnabled(true);
|
||||
setWorkerCount("");
|
||||
setInterceptRules([]);
|
||||
setScheduleEnabled(false);
|
||||
setSchedule({
|
||||
@@ -3263,15 +3270,19 @@ function CreateTaskSheet({
|
||||
<TaskTemplateControls
|
||||
description={description}
|
||||
goal={goal}
|
||||
remark={remark}
|
||||
categoryID={categoryID}
|
||||
interceptRules={interceptRules}
|
||||
workerCount={workerCount}
|
||||
selectedTemplateID={selectedTemplateID}
|
||||
onSelectedTemplateIDChange={setSelectedTemplateID}
|
||||
onApply={(template) => {
|
||||
setDescription(template.description);
|
||||
setGoal(template.goal);
|
||||
setRemark(template.remark ?? "");
|
||||
setCategoryID(template.category_id ?? undefined);
|
||||
setInterceptRules(template.intercept_rules ?? []);
|
||||
setWorkerCount(template.worker_count ? String(template.worker_count) : "");
|
||||
setUploadCount(0);
|
||||
}}
|
||||
portalContainer={sheetContentRef}
|
||||
@@ -3285,6 +3296,16 @@ function CreateTaskSheet({
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="remark">备注(可选)</Label>
|
||||
<Textarea
|
||||
id="remark"
|
||||
className="min-h-20"
|
||||
placeholder="补充说明、环境信息等,便于在任务列表识别"
|
||||
value={remark}
|
||||
onChange={(e) => setRemark(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="task-category">任务分类</FieldLabel>
|
||||
<CategoryPicker
|
||||
@@ -3507,7 +3528,7 @@ function CreateTaskSheet({
|
||||
<CollapsibleTrigger className="group flex w-full items-center gap-2 border-t pt-4 text-sm font-medium">
|
||||
<ChevronRightIcon className="text-muted-foreground size-4 transition-transform group-data-[state=open]:rotate-90" />
|
||||
高级设置
|
||||
<span className="text-muted-foreground ml-auto text-xs font-normal">超时 · 心跳 · 首个意图</span>
|
||||
<span className="text-muted-foreground ml-auto text-xs font-normal">超时 · 心跳 · Worker · 首个意图</span>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="grid gap-5 pt-5">
|
||||
<div className="grid gap-2">
|
||||
@@ -3541,6 +3562,23 @@ function CreateTaskSheet({
|
||||
worker)。下限 10 分钟。
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="worker-count">Worker 运行数量(可选)</Label>
|
||||
<Input
|
||||
id="worker-count"
|
||||
type="number"
|
||||
min={0}
|
||||
max={50}
|
||||
className="w-40"
|
||||
placeholder="留空 = 全局默认"
|
||||
value={workerCount}
|
||||
onChange={(e) => setWorkerCount(e.target.value)}
|
||||
/>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
单独设置本任务并发运行的工作 Agent 数量;留空或 0 = 用全局默认(系统设置里的工作并发,默认
|
||||
3)。上限 50。
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<label htmlFor="seed-first-intent" className="flex items-center gap-2 text-sm">
|
||||
<Checkbox
|
||||
|
||||
@@ -40,12 +40,14 @@ interface TemplateDraft {
|
||||
name: string;
|
||||
description: string;
|
||||
goal: string;
|
||||
remark: string;
|
||||
categoryID: number | null;
|
||||
interceptRules: AssetInterceptRuleInput[];
|
||||
workerCount: string; // 输入框用字符串;空/0=用全局默认
|
||||
}
|
||||
|
||||
// TemplateSeed 是「另存为模板」时从创建表单带入的初值。
|
||||
type TemplateSeed = Pick<TemplateDraft, "description" | "goal" | "categoryID" | "interceptRules">;
|
||||
type TemplateSeed = Pick<TemplateDraft, "description" | "goal" | "remark" | "categoryID" | "interceptRules" | "workerCount">;
|
||||
|
||||
interface TaskTemplateManagerProps {
|
||||
open: boolean;
|
||||
@@ -61,8 +63,10 @@ const emptyDraft = (): TemplateDraft => ({
|
||||
name: "",
|
||||
description: "",
|
||||
goal: "",
|
||||
remark: "",
|
||||
categoryID: null,
|
||||
interceptRules: [],
|
||||
workerCount: "",
|
||||
});
|
||||
|
||||
function templateDraft(template: TaskTemplate): TemplateDraft {
|
||||
@@ -70,8 +74,10 @@ function templateDraft(template: TaskTemplate): TemplateDraft {
|
||||
name: template.name,
|
||||
description: template.description,
|
||||
goal: template.goal,
|
||||
remark: template.remark ?? "",
|
||||
categoryID: template.category_id ?? null,
|
||||
interceptRules: template.intercept_rules ?? [],
|
||||
workerCount: template.worker_count ? String(template.worker_count) : "",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -108,8 +114,10 @@ function TaskTemplateManager({
|
||||
name: "",
|
||||
description: seed.description,
|
||||
goal: seed.goal,
|
||||
remark: seed.remark,
|
||||
categoryID: seed.categoryID,
|
||||
interceptRules: seed.interceptRules,
|
||||
workerCount: seed.workerCount,
|
||||
});
|
||||
} else if (templates[0]) {
|
||||
setSelectedID(templates[0].id);
|
||||
@@ -132,7 +140,7 @@ function TaskTemplateManager({
|
||||
setDraft(emptyDraft());
|
||||
};
|
||||
|
||||
const updateDraft = (field: "name" | "description" | "goal", value: string) => {
|
||||
const updateDraft = (field: "name" | "description" | "goal" | "remark" | "workerCount", value: string) => {
|
||||
setDraft((current) => ({ ...current, [field]: value }));
|
||||
};
|
||||
|
||||
@@ -145,7 +153,9 @@ function TaskTemplateManager({
|
||||
name: draft.name.trim(),
|
||||
description: draft.description.trim(),
|
||||
goal: draft.goal.trim(),
|
||||
remark: draft.remark.trim(),
|
||||
category_id: draft.categoryID,
|
||||
worker_count: Math.min(50, Math.max(0, Math.floor(Number(draft.workerCount) || 0))),
|
||||
intercept_rules: draft.interceptRules
|
||||
.map((r) => ({ ...r, pattern: r.pattern.trim() }))
|
||||
.filter((r) => r.pattern !== ""),
|
||||
@@ -232,6 +242,9 @@ function TaskTemplateManager({
|
||||
>
|
||||
<span className="block truncate font-medium text-sm">{template.name}</span>
|
||||
<span className="block truncate text-muted-foreground text-xs">{template.description}</span>
|
||||
{template.remark ? (
|
||||
<span className="block truncate text-muted-foreground/80 text-xs">备注:{template.remark}</span>
|
||||
) : null}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -269,6 +282,30 @@ function TaskTemplateManager({
|
||||
onChange={(event) => updateDraft("goal", event.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="task-template-remark">备注(可选)</FieldLabel>
|
||||
<Textarea
|
||||
id="task-template-remark"
|
||||
className="min-h-20"
|
||||
value={draft.remark}
|
||||
placeholder="预设备注,应用模板时写入新任务"
|
||||
onChange={(event) => updateDraft("remark", event.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="task-template-worker-count">Worker 运行数量(可选)</FieldLabel>
|
||||
<Input
|
||||
id="task-template-worker-count"
|
||||
type="number"
|
||||
min={0}
|
||||
max={50}
|
||||
className="w-40"
|
||||
placeholder="留空 = 全局默认"
|
||||
value={draft.workerCount}
|
||||
onChange={(event) => updateDraft("workerCount", event.target.value)}
|
||||
/>
|
||||
<FieldDescription>应用模板时预填本任务 Worker 数;留空或 0 = 用全局默认。</FieldDescription>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="task-template-category">任务分类</FieldLabel>
|
||||
<NativeSelect
|
||||
@@ -347,8 +384,10 @@ function TaskTemplateManager({
|
||||
interface TaskTemplateControlsProps {
|
||||
description: string;
|
||||
goal: string;
|
||||
remark?: string;
|
||||
categoryID?: number;
|
||||
interceptRules?: AssetInterceptRuleInput[];
|
||||
workerCount?: string;
|
||||
selectedTemplateID: number | null;
|
||||
onSelectedTemplateIDChange: (id: number | null) => void;
|
||||
onApply: (template: TaskTemplate) => void;
|
||||
@@ -358,8 +397,10 @@ interface TaskTemplateControlsProps {
|
||||
export function TaskTemplateControls({
|
||||
description,
|
||||
goal,
|
||||
remark = "",
|
||||
categoryID,
|
||||
interceptRules,
|
||||
workerCount = "",
|
||||
selectedTemplateID,
|
||||
onSelectedTemplateIDChange,
|
||||
onApply,
|
||||
@@ -455,8 +496,10 @@ export function TaskTemplateControls({
|
||||
openManager({
|
||||
description,
|
||||
goal,
|
||||
remark,
|
||||
categoryID: categoryID ?? null,
|
||||
interceptRules: interceptRules ?? [],
|
||||
workerCount,
|
||||
})
|
||||
}
|
||||
>
|
||||
|
||||
+6
-2
@@ -300,6 +300,7 @@ export const api = {
|
||||
task: (id: string) => get<Task>(`/tasks/${encodeURIComponent(id)}`),
|
||||
createTask: (input: {
|
||||
name?: string;
|
||||
remark?: string;
|
||||
categoryId?: number;
|
||||
description: string;
|
||||
goal: string;
|
||||
@@ -310,11 +311,13 @@ export const api = {
|
||||
seedFirstIntent?: boolean;
|
||||
planHeartbeatSeconds?: number;
|
||||
coverageEnabled?: boolean;
|
||||
workerCount?: number;
|
||||
interceptRules?: AssetInterceptRuleInput[];
|
||||
schedule?: ScheduleInput;
|
||||
}) =>
|
||||
post<Task>("/tasks", {
|
||||
name: input.name ?? "",
|
||||
remark: input.remark ?? "",
|
||||
category_id: input.categoryId ?? null,
|
||||
description: input.description,
|
||||
goal: input.goal,
|
||||
@@ -325,6 +328,7 @@ export const api = {
|
||||
seed_first_intent: input.seedFirstIntent ?? false,
|
||||
plan_heartbeat_seconds: input.planHeartbeatSeconds ?? 0, // 0 = 后端归一到默认 600(10min)
|
||||
coverage_enabled: input.coverageEnabled ?? true, // 默认开;false=关闭资产覆盖度功能
|
||||
worker_count: input.workerCount ?? 0, // 0 = 用全局默认 workers
|
||||
intercept_rules: input.interceptRules ?? [], // 任务级资产拦截规则
|
||||
schedule: input.schedule ? schedulePayload(input.schedule) : undefined,
|
||||
}),
|
||||
@@ -357,11 +361,11 @@ export const api = {
|
||||
}),
|
||||
taskTemplates: () => get<{ templates: TaskTemplate[] }>("/task-templates").then((r) => arr(r.templates)),
|
||||
createTaskTemplate: (
|
||||
input: Pick<TaskTemplate, "name" | "description" | "goal" | "category_id" | "intercept_rules">,
|
||||
input: Pick<TaskTemplate, "name" | "description" | "goal" | "remark" | "category_id" | "intercept_rules" | "worker_count">,
|
||||
) => post<TaskTemplate>("/task-templates", input),
|
||||
updateTaskTemplate: (
|
||||
id: number,
|
||||
input: Partial<Pick<TaskTemplate, "name" | "description" | "goal" | "category_id" | "intercept_rules">>,
|
||||
input: Partial<Pick<TaskTemplate, "name" | "description" | "goal" | "remark" | "category_id" | "intercept_rules" | "worker_count">>,
|
||||
) => patch<TaskTemplate>(`/task-templates/${id}`, input),
|
||||
deleteTaskTemplate: (id: number) => del<{ deleted: number }>(`/task-templates/${id}`),
|
||||
updateTaskLLMProfiles: (id: string, llmProfileIds: number[], activeLLMProfileId?: number) =>
|
||||
|
||||
@@ -1182,10 +1182,12 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
|
||||
const created: Task = {
|
||||
id,
|
||||
name: String(b.name ?? ""),
|
||||
remark: typeof b.remark === "string" ? b.remark.trim() : "",
|
||||
category_id: category?.id,
|
||||
category_name: category?.name,
|
||||
description: String(b.description ?? "新任务"),
|
||||
goal: String(b.goal ?? ""),
|
||||
worker_count: typeof b.worker_count === "number" ? Math.min(50, Math.max(0, Math.floor(b.worker_count))) : 0,
|
||||
status: "created",
|
||||
created_at: now.toISOString(),
|
||||
created_unix: Math.floor(now.getTime() / 1000),
|
||||
@@ -1315,6 +1317,10 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
|
||||
name,
|
||||
description,
|
||||
goal,
|
||||
remark: typeof b.remark === "string" ? b.remark.trim() : "",
|
||||
category_id: typeof b.category_id === "number" ? b.category_id : null,
|
||||
intercept_rules: Array.isArray(b.intercept_rules) ? b.intercept_rules : [],
|
||||
worker_count: typeof b.worker_count === "number" ? Math.min(50, Math.max(0, Math.floor(b.worker_count))) : 0,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
@@ -1338,6 +1344,10 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
|
||||
template.name = name;
|
||||
template.description = description;
|
||||
template.goal = goal;
|
||||
if (typeof b.remark === "string") template.remark = b.remark.trim();
|
||||
if ("category_id" in b) template.category_id = typeof b.category_id === "number" ? b.category_id : null;
|
||||
if (Array.isArray(b.intercept_rules)) template.intercept_rules = b.intercept_rules;
|
||||
if (typeof b.worker_count === "number") template.worker_count = Math.min(50, Math.max(0, Math.floor(b.worker_count)));
|
||||
template.updated_at = new Date().toISOString();
|
||||
mockTaskTemplates.sort((a, b) => b.updated_at.localeCompare(a.updated_at) || b.id - a.id);
|
||||
return template;
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface Task {
|
||||
distinct_findings?: FindingDistinctStats;
|
||||
id: string;
|
||||
remark?: string;
|
||||
worker_count?: number; // 本任务 Worker 数;0/缺省=用全局默认
|
||||
name?: string; // 可选任务名称;空/缺省=未命名,展示时回退到描述
|
||||
category_id?: number;
|
||||
category_name?: string;
|
||||
@@ -56,8 +57,10 @@ export interface TaskTemplate {
|
||||
name: string;
|
||||
description: string;
|
||||
goal: string;
|
||||
remark?: string; // 预设备注
|
||||
category_id?: number | null; // 预设分类;null/缺省=无
|
||||
intercept_rules?: AssetInterceptRuleInput[]; // 预设的任务级拦截/允许规则
|
||||
worker_count?: number; // 预设 Worker 数;0/缺省=用全局默认
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user