feat: 任务/模板备注展示与 Worker 运行数量配置(Codex gpt-6-astra 重写)

This commit is contained in:
RuoJi6 committed 2026-10-24 21:46:03 +08:00
1 parent ba3025d04f
commit a1dc362bae
21 files changed
+332 -32

No files matched your search

+1 -1
View File
@@ -6,9 +6,9 @@ import (
"path/filepath"
"testing"
actool "github.com/Autumn-27/norma/tool"
"github.com/Autumn-27/artex/agent"
"github.com/Autumn-27/artex/db"
actool "github.com/Autumn-27/norma/tool"
)
// hasSkillTool reports whether the packed tool set contains the Skill meta-tool.
+2
View File
@@ -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,
}
}
+8 -1
View File
@@ -746,7 +746,7 @@ 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 := workerCountForTask(t, e.m.Workers())
e.deleteMu.RLock()
if e.IsDeleting(t.ID) {
e.deleteMu.RUnlock()
@@ -776,6 +776,13 @@ func (e *Engine) Run(ctx context.Context, t *Task) {
}
}
func workerCountForTask(t *Task, global int) int {
if t != nil && t.WorkerCount > 0 {
return t.WorkerCount
}
return global
}
// plannerHeartbeatInterval 解析任务的 planner 心跳间隔。db.CreateTask 已归一
// (低于 600 一律抬到 600);这里再兜一次底,防内存态异常值。
func plannerHeartbeatInterval(t *Task) time.Duration {
+15
View File
@@ -0,0 +1,15 @@
package server
import "testing"
func TestWorkerCountForTaskUsesOverrideOrGlobalDefault(t *testing.T) {
if got := workerCountForTask(&Task{WorkerCount: 5}, 3); got != 5 {
t.Fatalf("override = %d, want 5", got)
}
if got := workerCountForTask(&Task{WorkerCount: 0}, 3); got != 3 {
t.Fatalf("zero override = %d, want global 3", got)
}
if got := workerCountForTask(&Task{WorkerCount: -1}, 3); got != 3 {
t.Fatalf("negative override = %d, want global 3", got)
}
}
+3 -2
View File
@@ -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,8 +850,8 @@ 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,
FirstRunAt: unixOrZero(pt.FirstRunAt), DeadlineAt: unixOrZero(pt.DeadlineAt),
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),
}
}
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"github.com/Autumn-27/artex/db"
)
const maxRemarkRunes = 2000
const maxRemarkRunes = db.MaxTaskRemarkRunes
func normalizeRemark(value string) (string, error) {
value = strings.TrimSpace(value)
+17
View File
@@ -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 {
+36 -2
View File
@@ -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.MaxTaskRemarkRunes}, // 与任务备注 normalizeRemark 上限一致
}
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)
+1 -1
View File
@@ -5,9 +5,9 @@ import (
"encoding/json"
"testing"
actool "github.com/Autumn-27/norma/tool"
"github.com/Autumn-27/artex/agent"
"github.com/Autumn-27/artex/db"
actool "github.com/Autumn-27/norma/tool"
)
func names(tools []actool.CoreTool) map[string]actool.CoreTool {