feat(task): 资产覆盖度功能可按任务开关
创建任务时新增「资产覆盖度功能」开关(默认开)。关闭后: - 不计算/展示测试覆盖度,总览卡片隐藏、覆盖度 API 返回 enabled:false - 态势图仅展示资产不显示测试进度(B1:抹平 tested、去已测统计与图例) - 不再自动累积 task_scope(source=auto) - planner/mainagent 的 add_task_scope、list_untested_assets 从工具列表剔除 - graph_overview 丢弃 coverage 度量块、保留 scope 字段(防上下文污染) company 关联(task_scope kind=company)与此开关无关,永不受影响: insertTaskCompanies 不 gate,TaskCompanyIDs 照常读回,scope 字段照常喂给 agent。 DB:tasks 加 coverage_enabled 列(schema.sql + ALTER ADD COLUMN IF NOT EXISTS 补旧库,默认 true 保持存量任务原行为)。 Claude-Session: https://claude.ai/code/session_01Jm87Gr5fq5aso6SfLyguhx
This commit is contained in:
1 parent
5126ac4592
commit
eb3f278711
18 files changed
+179
-43
No files matched your search
+3
-1
@@ -80,11 +80,13 @@ func (m *MainAgent) Chat(ctx context.Context, taskID int64, as *db.AssetStore, t
|
||||
tsx.SetAssetStore(as, as.Companies())
|
||||
}
|
||||
tsx.SetTaskID(taskID)
|
||||
tsx.SetCoverageEnabled(as == nil || as.CoverageEnabled(taskID))
|
||||
tsx.SetNotify(notify) // add_hint wakes this task's planner (debounced)
|
||||
tsx.SetResumeTask(resume) // set_goals 新增目标 → 把已完成/暂停的任务拉回 running
|
||||
tsx.SetNotifyGoal(notifyGoal) // set_goals 新增目标 → 给 planner 记一条「人新增了目标:…」触发
|
||||
// 领域工具 + 基础默认工具集(Read/Write/Edit/MultiEdit/LS/Glob/Grep/Bash)
|
||||
base := append(tsx.MainAgentTools(), actool.DefaultTools()...)
|
||||
// 资产覆盖度功能关闭时剔除 add_task_scope/list_untested_assets(不入 prompt)。
|
||||
base := append(tsx.DropCoverageTools(tsx.MainAgentTools()), actool.DefaultTools()...)
|
||||
ctx = WithRunInfo(ctx, RunInfo{TaskID: taskID, ExplorationID: explorationID(ts)})
|
||||
tools, def, cleanup := AugmentTools(ctx, "mainagent", base)
|
||||
defer cleanup()
|
||||
|
||||
+3
-1
@@ -266,13 +266,15 @@ func (p *Planner) Plan(ctx context.Context, taskID int64, as *db.AssetStore, ts
|
||||
tsx.SetAssetStore(as, as.Companies())
|
||||
}
|
||||
tsx.SetTaskID(taskID)
|
||||
tsx.SetCoverageEnabled(as == nil || as.CoverageEnabled(taskID))
|
||||
tsx.killWork = p.killWork // enable kill_work tool (nil = unavailable)
|
||||
tsx.steerWork = p.steerWork // enable steer_work tool (nil = unavailable)
|
||||
if origin, _ := ts.OriginFactID(); origin > 0 {
|
||||
tsx.SetOwnerNode(origin) // planner-side anchors default to the task root (origin fact)
|
||||
}
|
||||
// 领域工具 + 基础默认工具集(Read/Write/Edit/MultiEdit/LS/Glob/Grep/Bash)
|
||||
base := append(tsx.PlannerTools(), actool.DefaultTools()...)
|
||||
// 资产覆盖度功能关闭时剔除 add_task_scope/list_untested_assets(不入 prompt)。
|
||||
base := append(tsx.DropCoverageTools(tsx.PlannerTools()), actool.DefaultTools()...)
|
||||
ctx = WithRunInfo(ctx, RunInfo{TaskID: taskID, ExplorationID: explorationID(ts)})
|
||||
tools, def, cleanup := AugmentTools(ctx, "planner", base)
|
||||
defer cleanup()
|
||||
|
||||
+58
-14
@@ -56,6 +56,12 @@ type ToolSet struct {
|
||||
ts *db.ExplorationStore
|
||||
worker string
|
||||
taskID int64 // PG tasks.id; 0 when unknown (tests / orchestrator cross-task reads)
|
||||
// coverageDisabled mirrors tasks.coverage_enabled=false. Stored inverted so the
|
||||
// zero value (all existing ToolSet constructions) means ENABLED — matching the
|
||||
// DB default (true). When true: graphOverviewData drops the coverage block, the
|
||||
// auto-scope hook (insertAssets) is skipped, and add_task_scope/list_untested_assets
|
||||
// are filtered out of the agent's tool list. The scope field stays regardless.
|
||||
coverageDisabled bool
|
||||
// ownerNode is the exploration node that writes attach to: assets this run
|
||||
// touches get anchored to it as lineage/provenance (NOT visibility — the asset
|
||||
// graph is global and shared). Worker = its claimed intent; planner = begin root.
|
||||
@@ -150,6 +156,36 @@ func NewToolSet(ts *db.ExplorationStore, worker string) *ToolSet {
|
||||
// dual-write to the standalone findings table (which survives task deletion).
|
||||
func (t *ToolSet) SetTaskID(id int64) { t.taskID = id }
|
||||
|
||||
// SetCoverageEnabled records whether this task has the asset-coverage feature on
|
||||
// (default enabled). Passing false makes graphOverviewData omit the coverage block,
|
||||
// the insertAssets auto-scope hook a no-op, and CoverageTools reports the two
|
||||
// coverage-only tools so callers can drop them from the agent's tool list.
|
||||
func (t *ToolSet) SetCoverageEnabled(enabled bool) { t.coverageDisabled = !enabled }
|
||||
|
||||
// CoverageDisabled reports whether the coverage feature is off for this task.
|
||||
func (t *ToolSet) CoverageDisabled() bool { return t.coverageDisabled }
|
||||
|
||||
// coverageOnlyTools are the LLM tools that only make sense when asset coverage is
|
||||
// on. When the feature is off they are filtered out of the agent's tool list so
|
||||
// they neither pollute the prompt nor let the model build a disabled denominator.
|
||||
var coverageOnlyTools = map[string]bool{"add_task_scope": true, "list_untested_assets": true}
|
||||
|
||||
// DropCoverageTools returns tools with the coverage-only ones removed when this
|
||||
// task has the feature disabled; otherwise it returns tools unchanged.
|
||||
func (t *ToolSet) DropCoverageTools(tools []actool.CoreTool) []actool.CoreTool {
|
||||
if !t.coverageDisabled {
|
||||
return tools
|
||||
}
|
||||
out := tools[:0:0]
|
||||
for _, tool := range tools {
|
||||
if coverageOnlyTools[tool.Name()] {
|
||||
continue
|
||||
}
|
||||
out = append(out, tool)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Cross-task reuse: exported accessors returning the per-task tool logic bound to
|
||||
// THIS ToolSet's store. Host-side orchestration tools build a ToolSet for an
|
||||
// arbitrary task, then Call these — so cross-task reads/hint reuse the exact
|
||||
@@ -362,22 +398,28 @@ func (t *ToolSet) graphOverviewData() map[string]any {
|
||||
out["related_tasks"] = t.relatedTaskOverviews()
|
||||
// coverage:粗略的资产测试覆盖度参考——范围(task_scope)内的资产里,被 fact 碰过的
|
||||
// 占比 + by_type(按类型的 总数/已测)。要看未测的具体资产由 agent 按需调 list_untested_assets 自行判断。仅任务上下文有。
|
||||
// 资产覆盖度功能关闭时(coverageDisabled):只保留 scope/hosts(范围边界与目标主机的
|
||||
// 感知信息,company 关联经由 scope 在此浮现),丢弃 denominator/tested/pct/by_type/note
|
||||
// 等覆盖度度量,避免污染上下文、也不诱导已隐藏的 add_task_scope/list_untested_assets。
|
||||
if t.as != nil && t.ts != nil && t.taskID > 0 {
|
||||
if cov, err := t.as.TaskCoverageWithSources(t.taskID); err == nil {
|
||||
m := map[string]any{
|
||||
"denominator": cov.Denominator,
|
||||
"tested": cov.Tested,
|
||||
"by_type": cov.ByType,
|
||||
"note": "coverage资产测试覆盖度(包括接口等各种相关资产),粗略估计、仅供参考:包含当前任务与直接关联任务的 scope、事实锚点;关联 scope 只读。容器型资产/大量枚举会让它偏低,勿据此认为已测完;可用 add_task_scope 增补本任务范围、list_untested_assets 看未测资产【通常不调用list_untested_assets,按照任务推进即可】;",
|
||||
}
|
||||
if cov.Denominator == 0 {
|
||||
m["pct"] = nil
|
||||
m["status"] = "范围未锚定"
|
||||
} else {
|
||||
m["pct"] = cov.Pct
|
||||
{
|
||||
m := map[string]any{}
|
||||
if !t.coverageDisabled {
|
||||
if cov, err := t.as.TaskCoverageWithSources(t.taskID); err == nil {
|
||||
m["denominator"] = cov.Denominator
|
||||
m["tested"] = cov.Tested
|
||||
m["by_type"] = cov.ByType
|
||||
m["note"] = "coverage资产测试覆盖度(包括接口等各种相关资产),粗略估计、仅供参考:包含当前任务与直接关联任务的 scope、事实锚点;关联 scope 只读。容器型资产/大量枚举会让它偏低,勿据此认为已测完;可用 add_task_scope 增补本任务范围、list_untested_assets 看未测资产【通常不调用list_untested_assets,按照任务推进即可】;"
|
||||
if cov.Denominator == 0 {
|
||||
m["pct"] = nil
|
||||
m["status"] = "范围未锚定"
|
||||
} else {
|
||||
m["pct"] = cov.Pct
|
||||
}
|
||||
}
|
||||
}
|
||||
// scope:当前测试范围的根资产(task_scope 原始行),让 agent 知道这个任务到底
|
||||
// 圈定了哪些目标(不是全部测试资产,而是范围边界本身)。
|
||||
// 圈定了哪些目标(不是全部测试资产,而是范围边界本身)。覆盖度开关无关,始终提供。
|
||||
if rows, err := t.as.ListTaskScopeWithSources(t.taskID); err == nil && len(rows) > 0 {
|
||||
scope := make([]map[string]any, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
@@ -445,7 +487,9 @@ func (t *ToolSet) graphOverviewData() map[string]any {
|
||||
m["hosts_truncated"] = true
|
||||
}
|
||||
}
|
||||
out["coverage"] = m
|
||||
if len(m) > 0 {
|
||||
out["coverage"] = m
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
|
||||
@@ -274,11 +274,14 @@ func (t *ToolSet) insertAssets() actool.CoreTool {
|
||||
t.anchorOwner(id)
|
||||
// 自动入测试范围(source='auto'):只对 worker 顶层显式插入的这一项,按其
|
||||
// 类型加保守范围;side-effect 派生的资产不经此处,故范围不盲目扩大。taskID=0 时无操作。
|
||||
svcIP := item.ServiceIP
|
||||
if svcIP == "" {
|
||||
svcIP = item.IP
|
||||
// 资产覆盖度功能关闭时不再累积测试范围(分母)。
|
||||
if !t.coverageDisabled {
|
||||
svcIP := item.ServiceIP
|
||||
if svcIP == "" {
|
||||
svcIP = item.IP
|
||||
}
|
||||
_ = t.as.AddAutoScope(taskID, typ, item.Domain, item.URL, svcIP)
|
||||
}
|
||||
_ = t.as.AddAutoScope(taskID, typ, item.Domain, item.URL, svcIP)
|
||||
}
|
||||
|
||||
return jsonResult(map[string]any{
|
||||
|
||||
+7
-2
@@ -254,6 +254,8 @@ func renderWorkerGraphOverview(data map[string]any) string {
|
||||
func (w *Worker) Execute(ctx context.Context, name string, taskID int64, as *db.AssetStore, ts *db.ExplorationStore, intent *db.Node, hooks harness.HookRunner, emit func(db.Activity), enr EnrichTrigger, notifyFinding func(int64, string)) (harness.TerminalReason, WriteCounts, error) {
|
||||
tsx := NewToolSet(ts, name)
|
||||
tsx.SetTaskID(taskID)
|
||||
coverageEnabled := as == nil || as.CoverageEnabled(taskID)
|
||||
tsx.SetCoverageEnabled(coverageEnabled)
|
||||
if as != nil {
|
||||
tsx.SetAssetStore(as, as.Companies())
|
||||
}
|
||||
@@ -351,8 +353,11 @@ func (w *Worker) Execute(ctx context.Context, name string, taskID int64, as *db.
|
||||
// 意图明确针对的这些资产 → 自动纳入任务测试范围(与 insertAssets 同一套
|
||||
// 保守粒度)。upsertTaskScope 的 ON CONFLICT DO NOTHING + uq_task_scope
|
||||
// 唯一索引保证不会重复添加;重跑/重试同样是幂等 no-op。
|
||||
for _, a := range assets {
|
||||
_ = as.AddAutoScope(taskID, a.Type, a.Domain, a.URL, a.IP)
|
||||
// 资产覆盖度功能关闭时不再累积测试范围(分母)。
|
||||
if coverageEnabled {
|
||||
for _, a := range assets {
|
||||
_ = as.AddAutoScope(taskID, a.Type, a.Domain, a.URL, a.IP)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,6 +341,7 @@ CREATE TABLE IF NOT EXISTS tasks (
|
||||
parent_ref TEXT,
|
||||
timeout_seconds INTEGER NOT NULL DEFAULT 0,
|
||||
plan_heartbeat_seconds INTEGER NOT NULL DEFAULT 300,
|
||||
coverage_enabled BOOLEAN NOT NULL DEFAULT true,
|
||||
first_run_at TIMESTAMPTZ,
|
||||
deadline_at TIMESTAMPTZ,
|
||||
deleted_at TIMESTAMPTZ,
|
||||
@@ -357,6 +358,10 @@ CREATE TRIGGER trg_tasks_upd BEFORE UPDATE ON tasks
|
||||
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS plan_heartbeat_seconds INTEGER NOT NULL DEFAULT 300;
|
||||
-- 并发上限挂起态;补旧库。true=因并发上限排队、等待空位自动启动。
|
||||
ALTER TABLE tasks ADD COLUMN IF NOT EXISTS queued BOOLEAN NOT NULL DEFAULT false;
|
||||
-- 资产覆盖度功能开关;补旧库。true(默认)=计算/展示测试覆盖度、自动累积测试范围、
|
||||
-- 给 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;
|
||||
-- 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.
|
||||
|
||||
@@ -248,6 +248,7 @@ type CoverageByType struct {
|
||||
// NOT a precise metric. Denominator = assets matching any active task_scope row;
|
||||
// Tested = those anchored to at least one fact node in the exploration.
|
||||
type Coverage struct {
|
||||
Enabled bool `json:"enabled"` // 资产覆盖度功能是否开启;false 时其余字段为零值
|
||||
ScopeRows int `json:"scope_rows"` // 0 → 范围未锚定
|
||||
Denominator int `json:"denominator"` // 范围内资产数
|
||||
Tested int `json:"tested"` // 已测(约)
|
||||
@@ -255,6 +256,20 @@ type Coverage struct {
|
||||
ByType []CoverageByType `json:"by_type"` // 按资产类型的 总数/已测
|
||||
}
|
||||
|
||||
// CoverageEnabled reports whether a task has the asset-coverage feature turned on
|
||||
// (tasks.coverage_enabled). Missing row / error → true (fail open to the default),
|
||||
// so unknown/legacy tasks keep the historical behavior. taskID<=0 → true.
|
||||
func (s *AssetStore) CoverageEnabled(taskID int64) bool {
|
||||
if taskID <= 0 {
|
||||
return true
|
||||
}
|
||||
var enabled bool
|
||||
if err := s.db.QueryRow(`SELECT COALESCE(coverage_enabled,true) FROM tasks WHERE id=$1`, taskID).Scan(&enabled); err != nil {
|
||||
return true
|
||||
}
|
||||
return enabled
|
||||
}
|
||||
|
||||
// task_scope→assets match predicate, reused by the count / by-type / untested queries. $1=taskID.
|
||||
const covTargetCTE = `
|
||||
target AS (
|
||||
|
||||
+15
-5
@@ -40,6 +40,11 @@ type Task struct {
|
||||
// planner 心跳触发间隔(秒):距上轮 plan 结束/任务开始满该值且期间无触发 → 触发一轮。
|
||||
// 下限=默认=300(5min),低于一律抬到 300(在 CreateTask 归一)。见 docs/planner-trigger-impl-plan.md
|
||||
PlanHeartbeatSeconds int `json:"plan_heartbeat_seconds"`
|
||||
// CoverageEnabled 是「资产覆盖度功能」总开关(默认 true)。false 时:不计算/不展示测试
|
||||
// 覆盖度、不自动累积 task_scope(source=auto)、不给 agent 开放 add_task_scope/
|
||||
// list_untested_assets、态势里不注入 coverage 块(scope 字段仍保留)。company 关联
|
||||
// (task_scope kind=company)与此开关无关,永不受影响。见 db/task_scope.go。
|
||||
CoverageEnabled bool `json:"coverage_enabled"`
|
||||
}
|
||||
|
||||
// TaskDeleteResult reports optional related-data cleanup performed in the same
|
||||
@@ -137,6 +142,9 @@ type TaskCreateOptions struct {
|
||||
LLMProfileIDs []int64
|
||||
TimeoutSeconds int
|
||||
PlanHeartbeatSeconds int
|
||||
// CoverageEnabled 是「资产覆盖度功能」开关;nil=默认开(true),让不关心该开关的创建
|
||||
// 路径(编排 spawn、老 API)沿用原行为。仅 web 创建任务时可显式传 false 关闭。
|
||||
CoverageEnabled *bool
|
||||
}
|
||||
|
||||
// CreateTaskWithOptions creates an exploration, task, direct source relations,
|
||||
@@ -178,6 +186,7 @@ VALUES ($1, 'fact', $2, 0, 'origin', 'system')`, expID, string(originPayload));
|
||||
opts.TimeoutSeconds = 0
|
||||
}
|
||||
opts.PlanHeartbeatSeconds = normalizeHeartbeat(opts.PlanHeartbeatSeconds)
|
||||
coverageEnabled := opts.CoverageEnabled == nil || *opts.CoverageEnabled
|
||||
var active *int64
|
||||
if len(opts.LLMProfileIDs) > 0 {
|
||||
id := opts.LLMProfileIDs[0]
|
||||
@@ -190,11 +199,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,
|
||||
}
|
||||
if err := tx.QueryRow(`
|
||||
INSERT INTO tasks(description, goal, exploration_id, llm_profile_id, active_llm_profile_id, timeout_seconds, plan_heartbeat_seconds)
|
||||
VALUES ($1,$2,$3,$4,$4,$5,$6)
|
||||
RETURNING id, status, paused, created_at`, description, goal, expID, active, opts.TimeoutSeconds, opts.PlanHeartbeatSeconds).Scan(&t.ID, &t.Status, &t.Paused, &t.CreatedAt); err != nil {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
if err := insertTaskRelations(tx, t.ID, opts.SourceTaskIDs); err != nil {
|
||||
@@ -274,11 +284,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), first_run_at, deadline_at`
|
||||
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`
|
||||
|
||||
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.FirstRunAt, &t.DeadlineAt); err != nil {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
return &t, nil
|
||||
|
||||
@@ -52,6 +52,7 @@ type TaskDTO struct {
|
||||
LLMFailoverReason string `json:"llm_failover_reason,omitempty"`
|
||||
SourceTaskIDs []string `json:"source_task_ids"`
|
||||
CompanyIDs []int64 `json:"company_ids"`
|
||||
CoverageEnabled bool `json:"coverage_enabled"` // 资产覆盖度功能开关(创建时定)
|
||||
}
|
||||
|
||||
// TokenTotalDTO is a whole-task (all agents) token aggregate.
|
||||
@@ -98,6 +99,7 @@ func taskDTO(t *Task, status string) TaskDTO {
|
||||
LLMFailoverReason: llmState.FailoverReason,
|
||||
SourceTaskIDs: sourceIDs,
|
||||
CompanyIDs: lifecycle.CompanyIDs,
|
||||
CoverageEnabled: t.CoverageEnabled,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -53,6 +53,7 @@ type Task struct {
|
||||
// 任务级超时(见 docs/任务级超时与收尾设计.md)。DeadlineAt/FirstRunAt 为 unix 秒,0=未设/未运行。
|
||||
TimeoutSeconds int `json:"timeout_seconds"`
|
||||
PlanHeartbeatSeconds int `json:"plan_heartbeat_seconds"` // planner 心跳触发间隔(秒)
|
||||
CoverageEnabled bool `json:"coverage_enabled"` // 资产覆盖度功能开关(创建时定,默认开)
|
||||
FirstRunAt int64 `json:"first_run_at,omitempty"`
|
||||
DeadlineAt int64 `json:"deadline_at,omitempty"`
|
||||
Store *pgdb.ExplorationStore `json:"-"`
|
||||
@@ -696,7 +697,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,
|
||||
FirstRunAt: unixOrZero(pt.FirstRunAt), DeadlineAt: unixOrZero(pt.DeadlineAt),
|
||||
CoverageEnabled: pt.CoverageEnabled,
|
||||
FirstRunAt: unixOrZero(pt.FirstRunAt), DeadlineAt: unixOrZero(pt.DeadlineAt),
|
||||
Store: store, Guard: guard.NewWithInterceptor(ic), notify: make(chan struct{}, 1),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1307,6 +1307,7 @@ type createTaskReq struct {
|
||||
TimeoutSeconds int `json:"timeout_seconds"` // 任务级超时(秒);0/省略=不限时
|
||||
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 关联不受影响。
|
||||
}
|
||||
|
||||
func (s *Server) createTask(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -1356,6 +1357,7 @@ func (s *Server) createTask(w http.ResponseWriter, r *http.Request) {
|
||||
t, err := s.m.CreateTaskWithOptions(req.Description, req.Goal, db.TaskCreateOptions{
|
||||
SourceTaskIDs: sourceIDs, CompanyIDs: req.CompanyIDs, LLMProfileIDs: req.LLMProfileIDs,
|
||||
TimeoutSeconds: req.TimeoutSeconds, PlanHeartbeatSeconds: req.PlanHeartbeatSeconds,
|
||||
CoverageEnabled: req.CoverageEnabled,
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, db.ErrTaskCompanyIDsInvalid) || errors.Is(err, db.ErrTaskCompanyNotFound) {
|
||||
@@ -1587,12 +1589,18 @@ func (s *Server) taskCoverage(w http.ResponseWriter, r *http.Request) {
|
||||
writeErr(w, 503, "asset store 未启用")
|
||||
return
|
||||
}
|
||||
// 资产覆盖度功能关闭 → 短路返回 {enabled:false},前端据此隐藏覆盖度卡片/进度。
|
||||
if !t.CoverageEnabled {
|
||||
writeJSON(w, 200, &db.Coverage{Enabled: false, ByType: []db.CoverageByType{}})
|
||||
return
|
||||
}
|
||||
taskID, _ := strconv.ParseInt(t.ID, 10, 64)
|
||||
cov, err := as.TaskCoverageWithSources(taskID)
|
||||
if err != nil {
|
||||
writeErr(w, 500, err.Error())
|
||||
return
|
||||
}
|
||||
cov.Enabled = true
|
||||
writeJSON(w, 200, cov)
|
||||
}
|
||||
|
||||
|
||||
@@ -456,7 +456,7 @@ function FoldSheet({
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
function GraphInner({ taskId }: { taskId: string }) {
|
||||
function GraphInner({ taskId, coverageEnabled = true }: { taskId: string; coverageEnabled?: boolean }) {
|
||||
const [data, setData] = React.useState<{
|
||||
nodes: CoverageGraphNode[];
|
||||
edges: CoverageGraphEdge[];
|
||||
@@ -479,12 +479,17 @@ function GraphInner({ taskId }: { taskId: string }) {
|
||||
setLoading(true);
|
||||
api
|
||||
.taskCoverageGraph(taskId)
|
||||
.then((g) => setData({ nodes: g.nodes ?? [], edges: g.edges ?? [] }))
|
||||
.then((g) => {
|
||||
// 资产覆盖度功能关闭(B1):仍出图(范围内资产/company 关联依旧可见),但抹平
|
||||
// tested 状态——不显示测试进度、不做已测高亮。
|
||||
const nodes = coverageEnabled ? (g.nodes ?? []) : (g.nodes ?? []).map((n) => ({ ...n, tested: false }));
|
||||
setData({ nodes, edges: g.edges ?? [] });
|
||||
})
|
||||
.catch(() => {
|
||||
/* 保留上一次数据 */
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}, [taskId]);
|
||||
}, [taskId, coverageEnabled]);
|
||||
|
||||
React.useEffect(() => {
|
||||
fetchGraph();
|
||||
@@ -639,8 +644,14 @@ function GraphInner({ taskId }: { taskId: string }) {
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
{total > 0 ? (
|
||||
<span className="text-muted-foreground">
|
||||
范围内 <span className="text-foreground font-semibold tabular-nums">{inScope}</span> · 已测{" "}
|
||||
<span className="font-semibold tabular-nums text-emerald-600 dark:text-emerald-400">{tested}</span>
|
||||
范围内 <span className="text-foreground font-semibold tabular-nums">{inScope}</span>
|
||||
{coverageEnabled && (
|
||||
<>
|
||||
{" "}
|
||||
· 已测{" "}
|
||||
<span className="font-semibold tabular-nums text-emerald-600 dark:text-emerald-400">{tested}</span>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-muted-foreground">{loading ? "加载中…" : "暂无范围内资产(先锚定任务范围)"}</span>
|
||||
@@ -664,12 +675,16 @@ function GraphInner({ taskId }: { taskId: string }) {
|
||||
})}
|
||||
</div>
|
||||
<div className="border-border/60 text-muted-foreground flex flex-wrap gap-x-3 gap-y-1.5 border-t pt-2">
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<span className="size-3 rounded-full bg-emerald-500" /> 已测(高亮)
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<span className="size-3 rounded-full bg-neutral-400" /> 未测
|
||||
</span>
|
||||
{coverageEnabled && (
|
||||
<>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<span className="size-3 rounded-full bg-emerald-500" /> 已测(高亮)
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<span className="size-3 rounded-full bg-neutral-400" /> 未测
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<span className="size-3 rounded-full border border-dashed border-neutral-400 bg-neutral-200" /> 范围外
|
||||
</span>
|
||||
@@ -693,12 +708,12 @@ function GraphInner({ taskId }: { taskId: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function CoverageGraphTab({ taskId }: { taskId: string }) {
|
||||
export function CoverageGraphTab({ taskId, coverageEnabled = true }: { taskId: string; coverageEnabled?: boolean }) {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-0">
|
||||
<div className="h-[72vh] w-full overflow-hidden rounded-xl">
|
||||
<GraphInner taskId={taskId} />
|
||||
<GraphInner taskId={taskId} coverageEnabled={coverageEnabled} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -89,6 +89,7 @@ export function OverviewTab({ taskId }: { taskId: string }) {
|
||||
const [intents, setIntents] = React.useState<TaskNode[]>([]);
|
||||
const [findings, setFindings] = React.useState<Finding[]>([]);
|
||||
const [coverage, setCoverage] = React.useState<{
|
||||
enabled: boolean;
|
||||
scope_rows: number;
|
||||
denominator: number;
|
||||
tested: number;
|
||||
@@ -263,7 +264,7 @@ export function OverviewTab({ taskId }: { taskId: string }) {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{coverage && coverage.scope_rows > 0 && (
|
||||
{coverage && coverage.enabled && coverage.scope_rows > 0 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
|
||||
@@ -364,7 +364,7 @@ function TaskDetailInner() {
|
||||
<AssetsTab taskId={id} />
|
||||
</TabsContent>
|
||||
<TabsContent value="coverage" className="mt-0">
|
||||
<CoverageGraphTab taskId={id} />
|
||||
<CoverageGraphTab taskId={id} coverageEnabled={task?.coverage_enabled !== false} />
|
||||
</TabsContent>
|
||||
<TabsContent value="intercept" className="mt-0">
|
||||
<InterceptTab taskId={id} />
|
||||
|
||||
@@ -1294,6 +1294,7 @@ function CreateTaskSheet({ tasks, onCreated }: { tasks: Task[]; onCreated: () =>
|
||||
const [timeoutMin, setTimeoutMin] = React.useState(""); // 任务级超时(分钟);空/0 = 不限时
|
||||
const [heartbeatMin, setHeartbeatMin] = React.useState("10"); // planner 心跳(分钟);默认10,下限10(与后端一致)
|
||||
const [seedFirstIntent, setSeedFirstIntent] = React.useState(false); // 创建时下发种子意图,worker 免等首轮 planner 直接开跑;默认关闭,走标准先规划再执行
|
||||
const [coverageEnabled, setCoverageEnabled] = React.useState(true); // 资产覆盖度功能;默认开。关闭=不计算/展示覆盖度、不累积范围、隐藏范围类工具(company 关联不受影响)
|
||||
// 方式1 文件上传:建任务前把文件暂存到 drafts/<draftId>/uploads/,拿回绝对路径追加进描述。
|
||||
const [uploading, setUploading] = React.useState(false);
|
||||
const [uploadCount, setUploadCount] = React.useState(0);
|
||||
@@ -1357,6 +1358,7 @@ function CreateTaskSheet({ tasks, onCreated }: { tasks: Task[]; onCreated: () =>
|
||||
timeoutSeconds: timeoutSec,
|
||||
seedFirstIntent,
|
||||
planHeartbeatSeconds: heartbeatSec,
|
||||
coverageEnabled,
|
||||
});
|
||||
toast.success("任务已创建");
|
||||
setDescription("");
|
||||
@@ -1368,6 +1370,7 @@ function CreateTaskSheet({ tasks, onCreated }: { tasks: Task[]; onCreated: () =>
|
||||
setTimeoutMin("");
|
||||
setHeartbeatMin("10");
|
||||
setSeedFirstIntent(false);
|
||||
setCoverageEnabled(true);
|
||||
setUploadCount(0);
|
||||
draftIdRef.current = "";
|
||||
setOpen(false);
|
||||
@@ -1548,6 +1551,20 @@ function CreateTaskSheet({ tasks, onCreated }: { tasks: Task[]; onCreated: () =>
|
||||
接手判定/补充。CTF 等常一个 work 直接解决的场景推荐开启;关闭则走标准的先规划再执行。
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<label htmlFor="coverage-enabled" className="flex items-center gap-2 text-sm">
|
||||
<Checkbox
|
||||
id="coverage-enabled"
|
||||
checked={coverageEnabled}
|
||||
onCheckedChange={(v) => setCoverageEnabled(!!v)}
|
||||
/>
|
||||
资产覆盖度功能
|
||||
</label>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
默认开启:计算并展示测试覆盖度、态势图显示测试进度、自动累积测试范围。关闭后不再计算/展示覆盖度,
|
||||
态势图仅展示资产不显示进度,agent 也不再获得范围类工具。关闭不影响「关联企业资产范围」。
|
||||
</p>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
|
||||
@@ -188,6 +188,7 @@ export const api = {
|
||||
timeoutSeconds?: number;
|
||||
seedFirstIntent?: boolean;
|
||||
planHeartbeatSeconds?: number;
|
||||
coverageEnabled?: boolean;
|
||||
}) =>
|
||||
post<Task>("/tasks", {
|
||||
description: input.description,
|
||||
@@ -198,6 +199,7 @@ export const api = {
|
||||
timeout_seconds: input.timeoutSeconds ?? 0,
|
||||
seed_first_intent: input.seedFirstIntent ?? false,
|
||||
plan_heartbeat_seconds: input.planHeartbeatSeconds ?? 0, // 0 = 后端归一到默认 600(10min)
|
||||
coverage_enabled: input.coverageEnabled ?? true, // 默认开;false=关闭资产覆盖度功能
|
||||
}),
|
||||
taskTemplates: () => get<{ templates: TaskTemplate[] }>("/task-templates").then((r) => arr(r.templates)),
|
||||
createTaskTemplate: (input: Pick<TaskTemplate, "name" | "description" | "goal">) =>
|
||||
@@ -240,6 +242,7 @@ export const api = {
|
||||
// 资产测试覆盖度(粗估,供参考):范围内资产被 fact 碰过的占比 + 按类型的 总数/已测。
|
||||
taskCoverage: (id: string) =>
|
||||
get<{
|
||||
enabled: boolean; // 资产覆盖度功能是否开启;false 时其余字段为零值
|
||||
scope_rows: number;
|
||||
denominator: number;
|
||||
tested: number;
|
||||
|
||||
@@ -844,6 +844,7 @@ export const conversationMessages: Record<number, Activity[]> = {
|
||||
|
||||
// ── 资产测试覆盖度(/tasks/{id}/coverage)──
|
||||
export const coverage = {
|
||||
enabled: true,
|
||||
scope_rows: 4,
|
||||
denominator: 26,
|
||||
tested: 11,
|
||||
|
||||
@@ -31,6 +31,7 @@ export interface Task {
|
||||
llm_failover_reason?: string;
|
||||
source_task_ids?: string[]; // directly related tasks inherited as read-only context
|
||||
company_ids?: number[]; // associated company asset scopes available as task context
|
||||
coverage_enabled?: boolean; // 资产覆盖度功能开关(创建时定,默认开);false=不计算/不展示覆盖度
|
||||
}
|
||||
|
||||
export interface TaskTemplate {
|
||||
|
||||
Reference in new issue
Block a user