import { AgentCheckConfig, type AgentCheckTrigger, CAPABILITY_CATALOG, type KnownCapabilityId, MIN_INTERVAL_MS, type ScheduledJob, } from "@umt/contracts"; import { LoaderCircle } from "lucide-react"; import { useState } from "react"; import { toast } from "sonner"; import { useCreateAgentCheck, useUpdateSchedule } from "~/api/schedules.ts"; import { FormField } from "~/components/console/form-field.tsx"; import { Button } from "~/components/ui/button.tsx"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "~/components/ui/dialog.tsx"; import { Input } from "~/components/ui/input.tsx"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select.tsx"; import { Switch } from "~/components/ui/switch.tsx"; import { Textarea } from "~/components/ui/textarea.tsx"; import { errorMessage } from "~/lib/api.ts"; import { CHECK_TRIGGER_LABEL, GOAL_PLACEHOLDER_LABEL, splitInterval, UNIT_MS } from "./labels.ts"; /** 巡检只开放只读 / 预览 / 提案类能力 */ const READONLY_CAPS = Object.values(CAPABILITY_CATALOG).filter((c) => (["read", "preview", "propose"] as string[]).includes(c.kind), ); const PLACEHOLDERS = Object.entries(GOAL_PLACEHOLDER_LABEL) .map(([k, v]) => `{{${k}}} ${v}`) .join("、"); type Unit = "m" | "h"; type Draft = { name: string; goal: string; caps: string[]; maxToolCalls: string; maxDurationSec: string; trigger: AgentCheckTrigger; every: string; unit: Unit; reportAsDecision: boolean; allowRecover: boolean; }; function initial(job: ScheduledJob | null): Draft { const c = (job?.config ?? {}) as Partial; let every = "6"; let unit: Unit = "h"; if (job?.schedule.type === "interval") { const s = splitInterval(job.schedule.everyMs); if (s.unit === "h") every = String(s.value); else { every = String(Math.max(1, Math.round(job.schedule.everyMs / UNIT_MS.m))); unit = "m"; } } return { name: job?.name ?? "", goal: c.goalTemplate ?? "", caps: c.capabilityAllowlist ?? [], maxToolCalls: c.budget?.maxToolCalls ? String(c.budget.maxToolCalls) : "20", maxDurationSec: c.budget?.maxDurationSec ? String(c.budget.maxDurationSec) : "300", trigger: c.triggerCondition ?? "always", every, unit, reportAsDecision: c.reportAsDecision ?? false, allowRecover: c.allowRecover ?? false, }; } /** 新建 / 编辑 Agent 巡检。内置巡检名称不可改,自定义巡检可改名 */ export function AgentCheckDialog({ open, onOpenChange, job, }: { open: boolean; onOpenChange: (o: boolean) => void; job: ScheduledJob | null; }) { const [d, setD] = useState(() => initial(job)); const [errors, setErrors] = useState>({}); const create = useCreateAgentCheck(); const update = useUpdateSchedule(); const pending = create.isPending || update.isPending; const set = (k: K, v: Draft[K]) => setD((x) => ({ ...x, [k]: v })); const cronJob = job?.schedule.type === "cron"; const submit = () => { const e: Record = {}; if (!job?.builtin && !d.name.trim()) e.name = "名称不能为空"; const everyMs = Math.round(Number(d.every) * UNIT_MS[d.unit]); if (!cronJob && (!Number.isFinite(everyMs) || everyMs < MIN_INTERVAL_MS)) e.every = "填正数"; const tools = Number(d.maxToolCalls); const secs = Number(d.maxDurationSec); if (!Number.isInteger(tools) || tools <= 0) e.maxToolCalls = "填正整数"; if (!Number.isInteger(secs) || secs <= 0) e.maxDurationSec = "填正整数"; const cfg = AgentCheckConfig.safeParse({ goalTemplate: d.goal, capabilityAllowlist: d.caps, budget: { maxToolCalls: tools, maxDurationSec: secs }, triggerCondition: d.trigger, allowRecover: d.allowRecover, reportAsDecision: d.reportAsDecision, }); if (!cfg.success && cfg.error.issues.some((i) => i.path[0] === "goalTemplate")) e.goal = "目标至少 4 个字"; if (Object.keys(e).length || !cfg.success) { setErrors(Object.keys(e).length ? e : { goal: cfg.error?.issues[0]?.message ?? "配置不合法" }); return; } setErrors({}); const schedule = { type: "interval" as const, everyMs }; const done = { onSuccess: () => { toast.success(job ? "已保存" : "巡检已创建"); onOpenChange(false); }, onError: (err: unknown) => toast.error(errorMessage(err)), }; if (job) update.mutate( { id: job.id, body: { config: cfg.data, ...(cronJob ? {} : { schedule }), ...(!job.builtin && d.name.trim() !== job.name ? { name: d.name.trim() } : {}), }, }, done, ); else create.mutate({ name: d.name.trim(), schedule, config: cfg.data, enabled: false }, done); }; return ( {job ? "编辑巡检" : "新建巡检"} 到点按目标发起一次智能体会话,只在白名单和预算内执行。
{ e.preventDefault(); submit(); }} > set("name", e.target.value)} />