feat(tasks): 任务并发上限(可选,超限排队自动补位)
任务页新建任务左侧加「并发设置」齿轮:开关(默认关) + 并发数量(开启默认5)。 开启后新建任务若「运行中」数已达上限,进入「排队中」不启动;有空位时后台 每 5s 自动把最早排队的任务补位启动。 - db: tasks 加持久化 queued 列(schema + ALTER 补旧库);Task.Queued/SetQueued。 - manager: ConcurrencyLimit/SetConcurrency(settings KV);SetTaskQueued(落库+同步内存)。 - 准入与促进器(goals.go):launchTask 统一准入,满则挂起排队;抽出 startTaskEngine 复用;occupiesSlot/runningTaskCount/queueIfAtCapacity;reconcileConcurrency 每 scheduler tick 补位(关闭则放行全部排队),concMu 串行化防超发;启动重载/setActive/ reviveTask 三处'排队则不启动'门。 - settings: /api/settings 增 task_concurrency_enabled/limit;PUT 后立即协调一次。 - web: 齿轮 Dialog(Switch+数量);新增 queued/「排队中」状态(类型/状态色/筛选)。 - 超限只排队新任务,不打断在跑的;排队任务不消耗 LLM(目标拆解推迟到出队)。 go build/vet、tsc 通过;DB queued 往返测试通过。
This commit is contained in:
1 parent
a6bd18050c
commit
8047bb7aca
10 files changed
+360
-30
No files matched your search
@@ -13,6 +13,7 @@ import {
|
||||
ChevronRightIcon,
|
||||
PaperclipIcon,
|
||||
Loader2Icon,
|
||||
SlidersHorizontalIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
import { StatusBadge } from "@/components/status-badge";
|
||||
@@ -30,8 +31,19 @@ import {
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
Table,
|
||||
@@ -143,6 +155,7 @@ function fmtDateTime(unix?: number): string {
|
||||
|
||||
const STATUS_OPTIONS: { value: TaskStatus; label: string }[] = [
|
||||
{ value: "created", label: "已创建" },
|
||||
{ value: "queued", label: "排队中" },
|
||||
{ value: "running", label: "运行中" },
|
||||
{ value: "paused", label: "已暂停" },
|
||||
{ value: "done", label: "已完成" },
|
||||
@@ -263,6 +276,7 @@ export default function TasksPage() {
|
||||
<span className="text-muted-foreground text-xs tabular-nums">
|
||||
{filtered.length}/{tasks.length} 条
|
||||
</span>
|
||||
<ConcurrencySettingsDialog />
|
||||
<CreateTaskSheet onCreated={load} />
|
||||
</div>
|
||||
|
||||
@@ -423,6 +437,99 @@ const TaskRow = React.memo(function TaskRow({
|
||||
);
|
||||
});
|
||||
|
||||
// ConcurrencySettingsDialog is the 齿轮 button left of 新建任务: toggle the
|
||||
// simultaneous-running-task cap and pick the limit. Default off; default limit 5.
|
||||
// Persisted globally via /api/settings — applies to tasks created afterwards.
|
||||
function ConcurrencySettingsDialog() {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [enabled, setEnabled] = React.useState(false);
|
||||
const [limit, setLimit] = React.useState("5");
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
const [saving, setSaving] = React.useState(false);
|
||||
|
||||
// 每次打开都拉取当前值,避免显示陈旧状态。
|
||||
React.useEffect(() => {
|
||||
if (!open) return;
|
||||
setLoading(true);
|
||||
api
|
||||
.settings()
|
||||
.then((s) => {
|
||||
setEnabled(!!s.task_concurrency_enabled);
|
||||
setLimit(String(s.task_concurrency_limit ?? 5));
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => setLoading(false));
|
||||
}, [open]);
|
||||
|
||||
async function save() {
|
||||
const n = Math.max(1, Math.floor(Number(limit) || 5));
|
||||
setSaving(true);
|
||||
try {
|
||||
await api.setSettings({ task_concurrency_enabled: enabled, task_concurrency_limit: n });
|
||||
toast.success(enabled ? `已开启并发限制:最多同时运行 ${n} 个任务` : "已关闭任务并发限制");
|
||||
setOpen(false);
|
||||
} catch (e) {
|
||||
toast.error("保存失败:" + (e as Error).message);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button size="sm" variant="outline" className="ml-auto" aria-label="任务并发设置">
|
||||
<SlidersHorizontalIcon /> 并发设置
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>任务并发限制</DialogTitle>
|
||||
<DialogDescription>
|
||||
限制同时「运行中」的任务数量。关闭时不限制;开启后新建任务若已达上限,会进入「排队中」,有空位时自动开始。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid gap-5 py-2">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="grid gap-1">
|
||||
<Label htmlFor="conc-enabled">开启任务并发限制</Label>
|
||||
<span className="text-muted-foreground text-xs">默认关闭(不限制并发)</span>
|
||||
</div>
|
||||
<Switch id="conc-enabled" checked={enabled} onCheckedChange={setEnabled} disabled={loading} />
|
||||
</div>
|
||||
|
||||
{enabled && (
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="conc-limit">并发数量(同时运行的任务上限)</Label>
|
||||
<Input
|
||||
id="conc-limit"
|
||||
type="number"
|
||||
min={1}
|
||||
className="w-32"
|
||||
value={limit}
|
||||
onChange={(e) => setLimit(e.target.value)}
|
||||
disabled={loading}
|
||||
/>
|
||||
<span className="text-muted-foreground text-xs">开启后默认 5,最小 1。</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">取消</Button>
|
||||
</DialogClose>
|
||||
<Button onClick={save} disabled={loading || saving}>
|
||||
{saving ? <Loader2Icon className="animate-spin" /> : null}
|
||||
保存
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
// CreateTaskSheet is the 新建任务 drawer. Its form state lives HERE, not in TasksPage: with
|
||||
// 描述/目标 held by the page component every keystroke re-rendered the whole task table
|
||||
// behind the drawer (plus its sticky column and 20 AlertDialog trees), which showed up as
|
||||
@@ -499,7 +606,7 @@ function CreateTaskSheet({ onCreated }: { onCreated: () => void }) {
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button size="sm" className="ml-auto">
|
||||
<Button size="sm">
|
||||
<PlusIcon /> 新建任务
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
|
||||
@@ -54,6 +54,7 @@ const intent: Record<string, StatusMeta> = {
|
||||
|
||||
const task: Record<string, StatusMeta> = {
|
||||
created: { label: "已创建", tone: "slate" },
|
||||
queued: { label: "排队中", tone: "amber" },
|
||||
running: { label: "运行中", tone: "blue" },
|
||||
paused: { label: "已暂停", tone: "amber" },
|
||||
done: { label: "已完成", tone: "green" },
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// ARTEX domain model — types used across the UI.
|
||||
// Derived from the functional spec (section 7: 关键数据形状).
|
||||
|
||||
export type TaskStatus = "created" | "running" | "paused" | "done" | "failed" | "timeout";
|
||||
export type TaskStatus = "created" | "queued" | "running" | "paused" | "done" | "failed" | "timeout";
|
||||
export type EngineMode = "exploring" | "paused" | "stalled" | "idle";
|
||||
|
||||
export interface Task {
|
||||
@@ -498,6 +498,9 @@ export interface Settings {
|
||||
web_search_proxy?: string;
|
||||
python_interpreter?: string; // 自定义脚本工具的 python 解释器路径(空=运行时检测)
|
||||
workers?: number; // 并发工作 agent 数(默认3);对之后启动的任务生效
|
||||
// 任务并发上限:同时「运行中」的任务数上限。关闭=不限;开启后新建任务超限则排队,有空位自动启动。
|
||||
task_concurrency_enabled?: boolean; // 默认 false
|
||||
task_concurrency_limit?: number; // 开启后默认 5
|
||||
}
|
||||
|
||||
// ---- LLM config ----
|
||||
|
||||
Reference in new issue
Block a user