import { useQueryClient } from "@tanstack/react-query"; import { SCHEDULE_CHANGED_EVENT, type ScheduledJob } from "@umt/contracts"; import { AlertTriangle, Bot, CalendarClock, CircleCheck, Coins, LoaderCircle, Plus, XCircle } from "lucide-react"; import { type ReactNode, useEffect, useRef, useState } from "react"; import { useLocation } from "react-router"; import { groupRunsBySchedule, useScheduleAgentRuns } from "~/api/agent.ts"; import { usePiUpstreams } from "~/api/pi.ts"; import { useScheduleBoard } from "~/api/schedules.ts"; import { EmptyState, ErrorState, LoadingState } from "~/components/console/empty-state.tsx"; import { StatTile } from "~/components/console/stat-tile.tsx"; import { Button } from "~/components/ui/button.tsx"; import { qk } from "~/lib/query-keys.ts"; import { useSse } from "~/lib/sse.ts"; import { AgentCheckCard, type DefaultUpstream } from "./agent-check-card.tsx"; import { AgentCheckDialog } from "./agent-check-dialog.tsx"; import { AutoTaskList } from "./auto-task-list.tsx"; import { formatUsd } from "./labels.ts"; import { RunsSheet } from "./runs-sheet.tsx"; // 计划页(FE-6,/console/schedules):「自动任务」= 程序按固定周期执行的定时任务(紧凑列表); // 「Agent 巡检」= 到点发起 Agent 会话的定时任务(卡片)。自动任务的运行记录跳任务页按该定时任务筛选。 /** 每秒一跳的当前时间(倒计时用) */ export function useNow(ms = 1000) { const [now, setNow] = useState(() => Date.now()); useEffect(() => { const t = setInterval(() => setNow(Date.now()), ms); return () => clearInterval(t); }, [ms]); return now; } /** SSE:调度器每次开始/结束一次运行都会发 schedule.changed;1 秒节流 */ export function useLiveSchedules() { const qc = useQueryClient(); const timer = useRef | null>(null); useSse([SCHEDULE_CHANGED_EVENT], () => { if (timer.current) return; timer.current = setTimeout(() => { timer.current = null; qc.invalidateQueries({ queryKey: qk.all("schedules") }); }, 1000); }); useEffect( () => () => { if (timer.current) clearTimeout(timer.current); }, [], ); return useScheduleBoard(); } const WEEK_MS = 7 * 24 * 3600_000; /** 两节的计数:自动任务 启用/失败/运行中;巡检 启用/失败/近 7 天费用 */ export function planStats(items: ScheduledJob[], checkCosts: { at: string; cost: number | undefined }[], now: number) { const auto = items.filter((j) => j.kind === "task"); const checks = items.filter((j) => j.kind === "agent_check"); const count = (list: ScheduledJob[]) => ({ total: list.length, enabled: list.filter((j) => j.enabled && j.available).length, failed: list.filter((j) => j.lastStatus === "failed").length, running: list.filter((j) => !!j.runningSince).length, }); const recent = checkCosts.filter((c) => now - new Date(c.at).getTime() <= WEEK_MS); return { auto: count(auto), checks: { ...count(checks), weekRuns: recent.length, weekCost: recent.reduce((s, c) => s + (c.cost ?? 0), 0), }, }; } function SectionHead({ id, title, hint, actions }: { id: string; title: string; hint: string; actions?: ReactNode }) { return (

{title}

{hint}

{actions ?
{actions}
: null}
); } export function PlanBoard({ highlightJob }: { highlightJob?: string }) { const board = useLiveSchedules(); const agentRuns = useScheduleAgentRuns(); const upstreams = usePiUpstreams(); const now = useNow(); const { hash } = useLocation(); const [editing, setEditing] = useState<{ job: ScheduledJob | null; key: number } | null>(null); const [history, setHistory] = useState(null); const data = board.data; const items = data?.items ?? []; const autoJobs = items.filter((j) => j.kind === "task"); const checks = items.filter((j) => j.kind === "agent_check"); const runsByJob = groupRunsBySchedule(agentRuns.data ?? []); const def = upstreams.data?.items.find((u) => u.isDefault); const upstream: DefaultUpstream = def ? { name: def.name, model: def.defaultModel ?? null } : null; const s = planStats( items, (agentRuns.data ?? []).map((r) => ({ at: r.createdAt, cost: r.usage.costUsd })), now, ); const val = (n: number) => (board.isPending ? "…" : board.isError ? "—" : n); // 首页计数链到 #auto / #checks:数据到了再滚过去 const loaded = !!data; useEffect(() => { if (!loaded || highlightJob) return; const id = hash.replace(/^#/, ""); if (id === "auto" || id === "checks") document.getElementById(id)?.scrollIntoView({ block: "start" }); }, [loaded, hash, highlightJob]); const jump = (id: string) => document.getElementById(id)?.scrollIntoView({ block: "start", behavior: "smooth" }); return ( <>

自动任务

jump("auto")} /> 0 ? "destructive" : "neutral"} value={val(s.auto.failed)} onClick={() => jump("auto")} /> jump("auto")} />

Agent 巡检

jump("checks")} /> 0 ? "destructive" : "neutral"} value={val(s.checks.failed)} onClick={() => jump("checks")} /> jump("checks")} />
{data && !data.schedulerOnline ? (

调度器未在线,定时任务不会按时运行。

) : null} {board.isPending ? ( ) : board.isError ? ( board.refetch()} /> ) : (
{autoJobs.length === 0 ? ( ) : ( )}
setEditing({ job: null, key: Date.now() })}> 新建巡检 } /> {checks.length === 0 ? ( ) : (
{checks.map((j) => ( setEditing({ job, key: Date.now() })} onHistory={setHistory} /> ))}
)}
)} {editing ? ( !o && setEditing(null)} /> ) : null} setHistory(null)} /> ); }