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:
Autumn-27 committed 2026-08-22 10:03:18 -04:00
1 parent 5126ac4592
commit eb3f278711
18 files changed
+179 -43

No files matched your search

@@ -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>
+3
View File
@@ -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;
+1
View File
@@ -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,
+1
View File
@@ -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 {