feat: 任务/模板备注展示与 Worker 运行数量配置

- 新建任务可填写备注;详情页头部展示备注(列表原有 RemarkEditor 保留)
- 任务模板支持备注与 Worker 数量,应用/另存为模板时同步带入
- 新建任务高级设置与模板均可单独设置 Worker 运行数量(0=回落全局默认)
- schema/API/引擎贯通:tasks.worker_count、task_templates.remark/worker_count
This commit is contained in:
RuoJi6 committed 2026-10-23 19:01:21 +08:00
1 parent 620ca5d96c
commit 45d4e4e67b
15 files changed
+250 -26

No files matched your search

@@ -404,6 +404,11 @@ function TaskDetailInner() {
</Button>
</div>
<p className="truncate text-xs text-muted-foreground">{task.goal}</p>
{task.remark ? (
<p className="truncate text-xs text-muted-foreground" title={task.remark}>
备注:{task.remark}
</p>
) : null}
{/* Tabs */}
<div className="no-scrollbar min-w-0 overflow-x-auto">
<TabsList variant="default" className="min-w-max">
+39 -1
View File
@@ -3104,6 +3104,7 @@ function CreateTaskSheet({
}) {
const [open, setOpen] = React.useState(false);
const [name, setName] = React.useState("");
const [remark, setRemark] = React.useState("");
const [categoryID, setCategoryID] = React.useState<number | undefined>(undefined);
const [description, setDescription] = React.useState("");
const [goal, setGoal] = React.useState("");
@@ -3118,6 +3119,7 @@ function CreateTaskSheet({
const [heartbeatMin, setHeartbeatMin] = React.useState("10"); // planner 心跳(分钟);默认10,下限10(与后端一致)
const [seedFirstIntent, setSeedFirstIntent] = React.useState(false); // 创建时下发种子意图,worker 免等首轮 planner 直接开跑;默认关闭,走标准先规划再执行
const [coverageEnabled, setCoverageEnabled] = React.useState(true); // 资产覆盖度功能;默认开。关闭=不计算/展示覆盖度、不累积范围、隐藏范围类工具(company 关联不受影响)
const [workerCount, setWorkerCount] = React.useState(""); // 本任务 Worker 数;空/0=用全局默认
const [interceptRules, setInterceptRules] = React.useState<AssetInterceptRuleInput[]>([]); // 任务级资产拦截规则(仅本任务生效,不进全局表)
const [scheduleEnabled, setScheduleEnabled] = React.useState(false);
const [schedule, setSchedule] = React.useState<ScheduleInput>({
@@ -3185,8 +3187,10 @@ function CreateTaskSheet({
try {
const timeoutSec = Math.max(0, Math.floor(Number(timeoutMin) || 0)) * 60;
const heartbeatSec = Math.max(10, Math.floor(Number(heartbeatMin) || 10)) * 60; // 下限 10min,与后端归一一致
const workerCountNum = Math.min(50, Math.max(0, Math.floor(Number(workerCount) || 0)));
await api.createTask({
name: name.trim(),
remark: remark.trim(),
categoryId: categoryID,
description: description.trim(),
goal: goal.trim(),
@@ -3197,6 +3201,7 @@ function CreateTaskSheet({
seedFirstIntent,
planHeartbeatSeconds: heartbeatSec,
coverageEnabled,
workerCount: workerCountNum,
schedule: scheduleEnabled ? { ...schedule, task_ids: [] } : undefined,
interceptRules: interceptRules
.map((r) => ({ ...r, pattern: r.pattern.trim() }))
@@ -3204,6 +3209,7 @@ function CreateTaskSheet({
});
toast.success("任务已创建");
setName("");
setRemark("");
setCategoryID(undefined);
setDescription("");
setGoal("");
@@ -3215,6 +3221,7 @@ function CreateTaskSheet({
setHeartbeatMin("10");
setSeedFirstIntent(false);
setCoverageEnabled(true);
setWorkerCount("");
setInterceptRules([]);
setScheduleEnabled(false);
setSchedule({
@@ -3263,15 +3270,19 @@ function CreateTaskSheet({
<TaskTemplateControls
description={description}
goal={goal}
remark={remark}
categoryID={categoryID}
interceptRules={interceptRules}
workerCount={workerCount}
selectedTemplateID={selectedTemplateID}
onSelectedTemplateIDChange={setSelectedTemplateID}
onApply={(template) => {
setDescription(template.description);
setGoal(template.goal);
setRemark(template.remark ?? "");
setCategoryID(template.category_id ?? undefined);
setInterceptRules(template.intercept_rules ?? []);
setWorkerCount(template.worker_count ? String(template.worker_count) : "");
setUploadCount(0);
}}
portalContainer={sheetContentRef}
@@ -3285,6 +3296,16 @@ function CreateTaskSheet({
onChange={(e) => setName(e.target.value)}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="remark">备注(可选)</Label>
<Textarea
id="remark"
className="min-h-20"
placeholder="补充说明、环境信息等,便于在任务列表识别"
value={remark}
onChange={(e) => setRemark(e.target.value)}
/>
</div>
<Field>
<FieldLabel htmlFor="task-category">任务分类</FieldLabel>
<CategoryPicker
@@ -3507,7 +3528,7 @@ function CreateTaskSheet({
<CollapsibleTrigger className="group flex w-full items-center gap-2 border-t pt-4 text-sm font-medium">
<ChevronRightIcon className="text-muted-foreground size-4 transition-transform group-data-[state=open]:rotate-90" />
高级设置
<span className="text-muted-foreground ml-auto text-xs font-normal">超时 · 心跳 · 首个意图</span>
<span className="text-muted-foreground ml-auto text-xs font-normal">超时 · 心跳 · Worker · 首个意图</span>
</CollapsibleTrigger>
<CollapsibleContent className="grid gap-5 pt-5">
<div className="grid gap-2">
@@ -3541,6 +3562,23 @@ function CreateTaskSheet({
worker)。下限 10 分钟。
</p>
</div>
<div className="grid gap-2">
<Label htmlFor="worker-count">Worker 运行数量(可选)</Label>
<Input
id="worker-count"
type="number"
min={0}
max={50}
className="w-40"
placeholder="留空 = 全局默认"
value={workerCount}
onChange={(e) => setWorkerCount(e.target.value)}
/>
<p className="text-muted-foreground text-xs">
单独设置本任务并发运行的工作 Agent 数量;留空或 0 = 用全局默认(系统设置里的工作并发,默认
3)。上限 50。
</p>
</div>
<div className="grid gap-2">
<label htmlFor="seed-first-intent" className="flex items-center gap-2 text-sm">
<Checkbox
+45 -2
View File
@@ -40,12 +40,14 @@ interface TemplateDraft {
name: string;
description: string;
goal: string;
remark: string;
categoryID: number | null;
interceptRules: AssetInterceptRuleInput[];
workerCount: string; // 输入框用字符串;空/0=用全局默认
}
// TemplateSeed 是「另存为模板」时从创建表单带入的初值。
type TemplateSeed = Pick<TemplateDraft, "description" | "goal" | "categoryID" | "interceptRules">;
type TemplateSeed = Pick<TemplateDraft, "description" | "goal" | "remark" | "categoryID" | "interceptRules" | "workerCount">;
interface TaskTemplateManagerProps {
open: boolean;
@@ -61,8 +63,10 @@ const emptyDraft = (): TemplateDraft => ({
name: "",
description: "",
goal: "",
remark: "",
categoryID: null,
interceptRules: [],
workerCount: "",
});
function templateDraft(template: TaskTemplate): TemplateDraft {
@@ -70,8 +74,10 @@ function templateDraft(template: TaskTemplate): TemplateDraft {
name: template.name,
description: template.description,
goal: template.goal,
remark: template.remark ?? "",
categoryID: template.category_id ?? null,
interceptRules: template.intercept_rules ?? [],
workerCount: template.worker_count ? String(template.worker_count) : "",
};
}
@@ -108,8 +114,10 @@ function TaskTemplateManager({
name: "",
description: seed.description,
goal: seed.goal,
remark: seed.remark,
categoryID: seed.categoryID,
interceptRules: seed.interceptRules,
workerCount: seed.workerCount,
});
} else if (templates[0]) {
setSelectedID(templates[0].id);
@@ -132,7 +140,7 @@ function TaskTemplateManager({
setDraft(emptyDraft());
};
const updateDraft = (field: "name" | "description" | "goal", value: string) => {
const updateDraft = (field: "name" | "description" | "goal" | "remark" | "workerCount", value: string) => {
setDraft((current) => ({ ...current, [field]: value }));
};
@@ -145,7 +153,9 @@ function TaskTemplateManager({
name: draft.name.trim(),
description: draft.description.trim(),
goal: draft.goal.trim(),
remark: draft.remark.trim(),
category_id: draft.categoryID,
worker_count: Math.min(50, Math.max(0, Math.floor(Number(draft.workerCount) || 0))),
intercept_rules: draft.interceptRules
.map((r) => ({ ...r, pattern: r.pattern.trim() }))
.filter((r) => r.pattern !== ""),
@@ -232,6 +242,9 @@ function TaskTemplateManager({
>
<span className="block truncate font-medium text-sm">{template.name}</span>
<span className="block truncate text-muted-foreground text-xs">{template.description}</span>
{template.remark ? (
<span className="block truncate text-muted-foreground/80 text-xs">备注:{template.remark}</span>
) : null}
</button>
))}
</div>
@@ -269,6 +282,30 @@ function TaskTemplateManager({
onChange={(event) => updateDraft("goal", event.target.value)}
/>
</Field>
<Field>
<FieldLabel htmlFor="task-template-remark">备注(可选)</FieldLabel>
<Textarea
id="task-template-remark"
className="min-h-20"
value={draft.remark}
placeholder="预设备注,应用模板时写入新任务"
onChange={(event) => updateDraft("remark", event.target.value)}
/>
</Field>
<Field>
<FieldLabel htmlFor="task-template-worker-count">Worker 运行数量(可选)</FieldLabel>
<Input
id="task-template-worker-count"
type="number"
min={0}
max={50}
className="w-40"
placeholder="留空 = 全局默认"
value={draft.workerCount}
onChange={(event) => updateDraft("workerCount", event.target.value)}
/>
<FieldDescription>应用模板时预填本任务 Worker 数;留空或 0 = 用全局默认。</FieldDescription>
</Field>
<Field>
<FieldLabel htmlFor="task-template-category">任务分类</FieldLabel>
<NativeSelect
@@ -347,8 +384,10 @@ function TaskTemplateManager({
interface TaskTemplateControlsProps {
description: string;
goal: string;
remark?: string;
categoryID?: number;
interceptRules?: AssetInterceptRuleInput[];
workerCount?: string;
selectedTemplateID: number | null;
onSelectedTemplateIDChange: (id: number | null) => void;
onApply: (template: TaskTemplate) => void;
@@ -358,8 +397,10 @@ interface TaskTemplateControlsProps {
export function TaskTemplateControls({
description,
goal,
remark = "",
categoryID,
interceptRules,
workerCount = "",
selectedTemplateID,
onSelectedTemplateIDChange,
onApply,
@@ -455,8 +496,10 @@ export function TaskTemplateControls({
openManager({
description,
goal,
remark,
categoryID: categoryID ?? null,
interceptRules: interceptRules ?? [],
workerCount,
})
}
>
+6 -2
View File
@@ -300,6 +300,7 @@ export const api = {
task: (id: string) => get<Task>(`/tasks/${encodeURIComponent(id)}`),
createTask: (input: {
name?: string;
remark?: string;
categoryId?: number;
description: string;
goal: string;
@@ -310,11 +311,13 @@ export const api = {
seedFirstIntent?: boolean;
planHeartbeatSeconds?: number;
coverageEnabled?: boolean;
workerCount?: number;
interceptRules?: AssetInterceptRuleInput[];
schedule?: ScheduleInput;
}) =>
post<Task>("/tasks", {
name: input.name ?? "",
remark: input.remark ?? "",
category_id: input.categoryId ?? null,
description: input.description,
goal: input.goal,
@@ -325,6 +328,7 @@ export const api = {
seed_first_intent: input.seedFirstIntent ?? false,
plan_heartbeat_seconds: input.planHeartbeatSeconds ?? 0, // 0 = 后端归一到默认 600(10min)
coverage_enabled: input.coverageEnabled ?? true, // 默认开;false=关闭资产覆盖度功能
worker_count: input.workerCount ?? 0, // 0 = 用全局默认 workers
intercept_rules: input.interceptRules ?? [], // 任务级资产拦截规则
schedule: input.schedule ? schedulePayload(input.schedule) : undefined,
}),
@@ -357,11 +361,11 @@ export const api = {
}),
taskTemplates: () => get<{ templates: TaskTemplate[] }>("/task-templates").then((r) => arr(r.templates)),
createTaskTemplate: (
input: Pick<TaskTemplate, "name" | "description" | "goal" | "category_id" | "intercept_rules">,
input: Pick<TaskTemplate, "name" | "description" | "goal" | "remark" | "category_id" | "intercept_rules" | "worker_count">,
) => post<TaskTemplate>("/task-templates", input),
updateTaskTemplate: (
id: number,
input: Partial<Pick<TaskTemplate, "name" | "description" | "goal" | "category_id" | "intercept_rules">>,
input: Partial<Pick<TaskTemplate, "name" | "description" | "goal" | "remark" | "category_id" | "intercept_rules" | "worker_count">>,
) => patch<TaskTemplate>(`/task-templates/${id}`, input),
deleteTaskTemplate: (id: number) => del<{ deleted: number }>(`/task-templates/${id}`),
updateTaskLLMProfiles: (id: string, llmProfileIds: number[], activeLLMProfileId?: number) =>
+10
View File
@@ -1182,10 +1182,12 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
const created: Task = {
id,
name: String(b.name ?? ""),
remark: typeof b.remark === "string" ? b.remark.trim() : "",
category_id: category?.id,
category_name: category?.name,
description: String(b.description ?? "新任务"),
goal: String(b.goal ?? ""),
worker_count: typeof b.worker_count === "number" ? Math.min(50, Math.max(0, Math.floor(b.worker_count))) : 0,
status: "created",
created_at: now.toISOString(),
created_unix: Math.floor(now.getTime() / 1000),
@@ -1315,6 +1317,10 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
name,
description,
goal,
remark: typeof b.remark === "string" ? b.remark.trim() : "",
category_id: typeof b.category_id === "number" ? b.category_id : null,
intercept_rules: Array.isArray(b.intercept_rules) ? b.intercept_rules : [],
worker_count: typeof b.worker_count === "number" ? Math.min(50, Math.max(0, Math.floor(b.worker_count))) : 0,
created_at: now,
updated_at: now,
};
@@ -1338,6 +1344,10 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
template.name = name;
template.description = description;
template.goal = goal;
if (typeof b.remark === "string") template.remark = b.remark.trim();
if ("category_id" in b) template.category_id = typeof b.category_id === "number" ? b.category_id : null;
if (Array.isArray(b.intercept_rules)) template.intercept_rules = b.intercept_rules;
if (typeof b.worker_count === "number") template.worker_count = Math.min(50, Math.max(0, Math.floor(b.worker_count)));
template.updated_at = new Date().toISOString();
mockTaskTemplates.sort((a, b) => b.updated_at.localeCompare(a.updated_at) || b.id - a.id);
return template;
+3
View File
@@ -8,6 +8,7 @@ export interface Task {
distinct_findings?: FindingDistinctStats;
id: string;
remark?: string;
worker_count?: number; // 本任务 Worker 数;0/缺省=用全局默认
name?: string; // 可选任务名称;空/缺省=未命名,展示时回退到描述
category_id?: number;
category_name?: string;
@@ -56,8 +57,10 @@ export interface TaskTemplate {
name: string;
description: string;
goal: string;
remark?: string; // 预设备注
category_id?: number | null; // 预设分类;null/缺省=无
intercept_rules?: AssetInterceptRuleInput[]; // 预设的任务级拦截/允许规则
worker_count?: number; // 预设 Worker 数;0/缺省=用全局默认
created_at: string;
updated_at: string;
}