>("");
const [heartbeatMin, setHeartbeatMin] = React.useState("10"); // planner 心跳(分钟);默认10,下限10(与后端一致)
const [seedFirstIntent, setSeedFirstIntent] = React.useState(false); // 创建时下发种子意图,worker 免等首轮 planner 直接开跑;默认关闭,走标准先规划再执行
const [shareGlobalAssets, setShareGlobalAssets] = React.useState(false);
@@ -3684,6 +3687,9 @@ function CreateTaskSheet({
sourceTaskIds: sourceTaskIDs,
companyIds: companyIDs,
timeoutSeconds: timeoutSec,
+ noResultSeconds: Math.max(0, Math.floor((Number(noResultMin) || 0) * 60)),
+ noResultMinFindings: Math.max(1, Math.floor(Number(noResultCount) || 1)),
+ noResultMinSeverity: noResultSeverity,
seedFirstIntent,
planHeartbeatSeconds: heartbeatSec,
coverageEnabled,
@@ -3707,6 +3713,9 @@ function CreateTaskSheet({
setCompanyIDs([]);
setLLMProfileIDs([]);
setTimeoutMin("");
+ setNoResultMin("");
+ setNoResultCount("1");
+ setNoResultSeverity("");
setHeartbeatMin("10");
setSeedFirstIntent(false);
setCoverageEnabled(true);
@@ -4055,6 +4064,54 @@ function CreateTaskSheet({
/>
{translate("taskUI.timeoutHelp")}
+
+
+
setNoResultMin(e.target.value)}
+ />
+
{translate("taskUI.noResultHelp")}
+
+ {Number(noResultMin) > 0 && (
+
+
+
+ setNoResultCount(e.target.value)}
+ />
+
+
+
+
+
+
+ )}
{dot && }
- {meta.label}
+ {domain === "task" && value === "timeout" && stopReason === "no_result"
+ ? t("taskUI.noResultStopped")
+ : meta.label}
);
}
diff --git a/web/src/i18n/task-messages.ts b/web/src/i18n/task-messages.ts
index ee8c641..e910bb3 100644
--- a/web/src/i18n/task-messages.ts
+++ b/web/src/i18n/task-messages.ts
@@ -346,13 +346,29 @@ export const taskMessages = {
"Fail over in list order. The first configuration is current; switch to the next only when quota is confirmed insufficient.",
],
"taskUI.advanced": ["高级设置", "Advanced settings"],
- "taskUI.advancedSummary": ["超时 · 心跳 · Worker · 首个意图", "Timeout · Heartbeat · Workers · First intent"],
+ "taskUI.advancedSummary": [
+ "超时 · 无成果止损 · 心跳 · Worker · 首个意图",
+ "Timeout · No-result stop · Heartbeat · Workers · First intent",
+ ],
"taskUI.timeoutOptional": ["任务超时(分钟,可选)", "Task timeout (minutes, optional)"],
"taskUI.noTimeout": ["留空 = 不限时", "Leave empty for no timeout"],
"taskUI.timeoutHelp": [
"到点后触发优雅收尾(各 agent 写回 + planner 终局判定),任务进入 timeout 终态。",
"At the timeout, Agents write back and the Planner makes a final decision before the task ends with timeout status.",
],
+ "taskUI.noResultStopped": ["无成果停止", "Stopped: no results"],
+ "taskUI.noResultOptional": ["无成果止损(分钟,可选)", "No-result stop (minutes, optional)"],
+ "taskUI.noResultCount": ["最少漏洞数", "Minimum findings"],
+ "taskUI.noResultSeverity": ["最低等级", "Minimum severity"],
+ "taskUI.noResultAnySeverity": ["不限等级", "Any severity"],
+ "taskUI.noResultLow": ["低危及以上", "Low or above"],
+ "taskUI.noResultMedium": ["中危及以上", "Medium or above"],
+ "taskUI.noResultHigh": ["高危及以上", "High or above"],
+ "taskUI.noResultCritical": ["严重", "Critical"],
+ "taskUI.noResultHelp": [
+ "留空或 0 = 关闭。累计实际运行到此时长后检查一次;达标独立漏洞不足则收尾停止,达标后继续运行且不再检查。暂停、排队和计划窗口外不计时;误报不计,归并漏洞算一个。最短 1 分钟,可与任务超时同时设置。",
+ "Empty or 0 disables this option. Check once after this much active runtime: stop if too few independent findings qualify; otherwise continue without further checks. Pauses, queue time and closed schedule windows do not count. Exclude false positives and count merged findings once. Minimum: 1 minute. Can be combined with the task timeout.",
+ ],
"taskUI.heartbeat": ["planner 心跳(分钟)", "Planner heartbeat (minutes)"],
"taskUI.defaultTen": ["默认 10", "Default: 10"],
"taskUI.heartbeatHelp": [
diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts
index c47bde9..8e75e44 100644
--- a/web/src/lib/api.ts
+++ b/web/src/lib/api.ts
@@ -388,6 +388,9 @@ export const api = {
sourceTaskIds?: string[];
companyIds?: number[];
timeoutSeconds?: number;
+ noResultSeconds?: number;
+ noResultMinFindings?: number;
+ noResultMinSeverity?: "" | "low" | "medium" | "high" | "critical";
seedFirstIntent?: boolean;
planHeartbeatSeconds?: number;
shareGlobalAssets?: boolean;
@@ -406,6 +409,9 @@ export const api = {
source_task_ids: input.sourceTaskIds ?? [],
company_ids: input.companyIds ?? [],
timeout_seconds: input.timeoutSeconds ?? 0,
+ no_result_seconds: input.noResultSeconds ?? 0,
+ no_result_min_findings: input.noResultMinFindings ?? 1,
+ no_result_min_severity: input.noResultMinSeverity ?? "",
seed_first_intent: input.seedFirstIntent ?? false,
plan_heartbeat_seconds: input.planHeartbeatSeconds ?? 0, // 0 = 后端归一到默认 600(10min)
share_global_assets: input.shareGlobalAssets ?? false,
diff --git a/web/src/lib/mock/handler.ts b/web/src/lib/mock/handler.ts
index 4000d9a..76bb834 100644
--- a/web/src/lib/mock/handler.ts
+++ b/web/src/lib/mock/handler.ts
@@ -1274,6 +1274,11 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
const categoryID = mockIntegerField(b.category_id, "category_id") ?? undefined;
const category = categoryID === undefined ? undefined : mockTaskCategories.find((item) => item.id === categoryID);
if (categoryID !== undefined && !category) throw new Error("任务分类不存在");
+ const noResultSeconds = Math.max(0, mockIntegerField(b.no_result_seconds, "no_result_seconds") ?? 0);
+ const noResultSeverity = String(b.no_result_min_severity ?? "");
+ if (!["", "low", "medium", "high", "critical"].includes(noResultSeverity)) {
+ throw new Error("invalid no_result_min_severity");
+ }
const created: Task = {
id,
name,
@@ -1282,6 +1287,10 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
category_name: category?.name,
description: description || "新任务",
goal,
+ no_result_seconds: noResultSeconds > 0 ? Math.max(60, noResultSeconds) : 0,
+ no_result_min_findings: Math.max(1, mockIntegerField(b.no_result_min_findings, "no_result_min_findings") ?? 1),
+ no_result_min_severity: noResultSeverity as Task["no_result_min_severity"],
+ stop_reason: "",
worker_count: mockWorkerCount(b.worker_count),
status: "created",
created_at: now.toISOString(),
diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts
index 0599cc5..7d26ff9 100644
--- a/web/src/lib/types.ts
+++ b/web/src/lib/types.ts
@@ -34,6 +34,10 @@ export interface Task {
description: string;
goal: string;
status: TaskStatus;
+ no_result_seconds?: number;
+ no_result_min_findings?: number;
+ no_result_min_severity?: "" | "low" | "medium" | "high" | "critical";
+ stop_reason?: string;
created_at: string;
created_unix?: number;
completed_at?: string; // RFC3339 finish time (done/failed); "" if unfinished