mirror of
https://github.com/RuoJi6/ARTEX.git
synced 2026-11-13 06:03:49 +08:00
feat(tasks): stop tasks that miss a configurable findings checkpoint
This commit is contained in:
1 parent
39a8f2c31a
commit
997d2d98c0
24 files changed
+812
-64
No files matched your search
@@ -395,7 +395,12 @@ function TaskDetailInner() {
|
||||
</code>
|
||||
<Separator orientation="vertical" className="mx-1 hidden h-4 sm:block" />
|
||||
<TaskLLMControl task={task} profiles={profiles} onUpdated={load} />
|
||||
<StatusBadge domain={terminal ? "task" : "engine"} value={terminal ? task.status : engineMode} dot />
|
||||
<StatusBadge
|
||||
stopReason={task.stop_reason}
|
||||
domain={terminal ? "task" : "engine"}
|
||||
value={terminal ? task.status : engineMode}
|
||||
dot
|
||||
/>
|
||||
{canArchive ? (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>{archiveTrigger}</AlertDialogTrigger>
|
||||
|
||||
@@ -1489,7 +1489,7 @@ const TaskRow = React.memo(function TaskRow({
|
||||
{task.goal}
|
||||
</TableCell>
|
||||
<TableCell className={cn(!visibleColumns.status && "hidden")}>
|
||||
<StatusBadge domain="task" value={task.status} dot />
|
||||
<StatusBadge domain="task" value={task.status} stopReason={task.stop_reason} dot />
|
||||
</TableCell>
|
||||
<TableCell
|
||||
className={cn("text-muted-foreground text-center text-xs tabular-nums", !visibleColumns.progress && "hidden")}
|
||||
@@ -2885,7 +2885,7 @@ function SourceTaskPicker({
|
||||
</p>
|
||||
{task?.goal && <p className="text-muted-foreground truncate text-xs">{task.goal}</p>}
|
||||
</div>
|
||||
{task && <StatusBadge domain="task" value={task.status} />}
|
||||
{task && <StatusBadge domain="task" value={task.status} stopReason={task.stop_reason} />}
|
||||
</div>
|
||||
</ComboboxItem>
|
||||
);
|
||||
@@ -3199,7 +3199,7 @@ function DraggableCategoryTask({ task, disabled, moving }: { task: Task; disable
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
<ItemActions>
|
||||
<StatusBadge domain="task" value={task.status} />
|
||||
<StatusBadge domain="task" value={task.status} stopReason={task.stop_reason} />
|
||||
</ItemActions>
|
||||
</Item>
|
||||
);
|
||||
@@ -3597,6 +3597,9 @@ function CreateTaskSheet({
|
||||
const [llmProfileIDs, setLLMProfileIDs] = React.useState<string[]>([]);
|
||||
const [creating, setCreating] = React.useState(false);
|
||||
const [timeoutMin, setTimeoutMin] = React.useState(""); // 任务级超时(分钟);空/0 = 不限时
|
||||
const [noResultMin, setNoResultMin] = React.useState("");
|
||||
const [noResultCount, setNoResultCount] = React.useState("1");
|
||||
const [noResultSeverity, setNoResultSeverity] = React.useState<NonNullable<Task["no_result_min_severity"]>>("");
|
||||
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({
|
||||
/>
|
||||
<p className="text-muted-foreground text-xs">{translate("taskUI.timeoutHelp")}</p>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="no-result-min">{translate("taskUI.noResultOptional")}</Label>
|
||||
<Input
|
||||
id="no-result-min"
|
||||
type="number"
|
||||
min={0}
|
||||
className="w-40"
|
||||
placeholder="120"
|
||||
value={noResultMin}
|
||||
onChange={(e) => setNoResultMin(e.target.value)}
|
||||
/>
|
||||
<p className="text-muted-foreground text-xs">{translate("taskUI.noResultHelp")}</p>
|
||||
</div>
|
||||
{Number(noResultMin) > 0 && (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="no-result-count">{translate("taskUI.noResultCount")}</Label>
|
||||
<Input
|
||||
id="no-result-count"
|
||||
type="number"
|
||||
min={1}
|
||||
step={1}
|
||||
value={noResultCount}
|
||||
onChange={(e) => setNoResultCount(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="no-result-severity">{translate("taskUI.noResultSeverity")}</Label>
|
||||
<Select
|
||||
value={noResultSeverity || "any"}
|
||||
onValueChange={(v) =>
|
||||
setNoResultSeverity((v === "any" ? "" : v) as NonNullable<Task["no_result_min_severity"]>)
|
||||
}
|
||||
>
|
||||
<SelectTrigger id="no-result-severity">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="any">{translate("taskUI.noResultAnySeverity")}</SelectItem>
|
||||
<SelectItem value="low">{translate("taskUI.noResultLow")}</SelectItem>
|
||||
<SelectItem value="medium">{translate("taskUI.noResultMedium")}</SelectItem>
|
||||
<SelectItem value="high">{translate("taskUI.noResultHigh")}</SelectItem>
|
||||
<SelectItem value="critical">{translate("taskUI.noResultCritical")}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="heartbeat-min">{translate("taskUI.heartbeat")}</Label>
|
||||
<Input
|
||||
|
||||
@@ -9,11 +9,13 @@ export function StatusBadge({
|
||||
domain,
|
||||
value,
|
||||
dot = false,
|
||||
stopReason,
|
||||
className,
|
||||
}: {
|
||||
domain: StatusDomain;
|
||||
value: string;
|
||||
dot?: boolean;
|
||||
stopReason?: string;
|
||||
className?: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
@@ -27,7 +29,9 @@ export function StatusBadge({
|
||||
)}
|
||||
>
|
||||
{dot && <span className={cn("size-1.5 rounded-full", toneDot[meta.tone])} />}
|
||||
{meta.label}
|
||||
{domain === "task" && value === "timeout" && stopReason === "no_result"
|
||||
? t("taskUI.noResultStopped")
|
||||
: meta.label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user