import {
type AgentCheckConfig,
type AgentRun,
CAPABILITY_CATALOG,
type KnownCapabilityId,
type ScheduledJob,
} from "@umt/contracts";
import { History, LoaderCircle, Pencil, Play, RotateCcw, SkipForward, Trash2 } from "lucide-react";
import { type ReactNode, useEffect, useRef } from "react";
import { Link } from "react-router";
import { toast } from "sonner";
import { useDeleteSchedule } from "~/api/schedules.ts";
import { ConfirmDialog } from "~/components/console/confirm-dialog.tsx";
import { consolePath } from "~/components/console/nav.ts";
import { StatusBadge } from "~/components/console/status-badge.tsx";
import { Button } from "~/components/ui/button.tsx";
import { Switch } from "~/components/ui/switch.tsx";
import { errorMessage } from "~/lib/api.ts";
import { agentRunLabel, formatRelative } from "~/lib/labels.ts";
import { cn } from "~/lib/utils.ts";
import { NextRun, useJobActions } from "./auto-task-list.tsx";
import { IntervalSelect } from "./interval-select.tsx";
import { CHECK_TRIGGER_LABEL, formatUsd, RUN_STATUS } from "./labels.ts";
// 计划页「Agent 巡检」:到点按目标模板发起一次 Agent 会话(kind=agent_check),一个巡检一张卡片。
/** 卡片上能力白名单最多列几个,其余折成"+N" */
const MAX_CAPS = 6;
/** 会话摘要是 Markdown:列表里只取纯文字 */
const plainSummary = (t: string) =>
t
.replace(/[#*`>_]+/g, " ")
.replace(/\s+/g, " ")
.trim();
const capTitle = (id: string) => CAPABILITY_CATALOG[id as KnownCapabilityId]?.title ?? id;
/** 默认上游(巡检没有单独的上游设置,跟随 Agent 设置里的默认上游) */
export type DefaultUpstream = { name: string; model: string | null } | null;
function Field({ label, children }: { label: string; children: ReactNode }) {
return (
<>
{label}
{children}
>
);
}
export function AgentCheckCard({
job,
now,
runs,
upstream,
highlight,
onEdit,
onHistory,
}: {
job: ScheduledJob;
now: number;
/** 该巡检最近的会话(新 → 旧) */
runs: AgentRun[];
upstream: DefaultUpstream;
highlight: boolean;
onEdit: (job: ScheduledJob) => void;
onHistory: (job: ScheduledJob) => void;
}) {
const a = useJobActions(job);
const del = useDeleteSchedule();
const ref = useRef(null);
useEffect(() => {
if (highlight) ref.current?.scrollIntoView({ block: "center" });
}, [highlight]);
const c = job.config as Partial;
const budget = c.budget ?? {};
const allow = c.capabilityAllowlist ?? [];
const last = job.lastStatus ? RUN_STATUS[job.lastStatus] : null;
const latest = runs[0] ?? null;
const costs = runs.map((r) => r.usage.costUsd).filter((v): v is number => typeof v === "number");
const avg = costs.length ? costs.reduce((s, v) => s + v, 0) / costs.length : null;
const budgetParts = [
budget.maxToolCalls ? `${budget.maxToolCalls} 次调用` : null,
budget.maxDurationSec ? `${budget.maxDurationSec} 秒` : null,
budget.maxTokens ? `${budget.maxTokens.toLocaleString("zh-CN")} tokens` : null,
budget.maxCostUsd ? `${formatUsd(budget.maxCostUsd)} 上限` : null,
].filter(Boolean);
return (
{job.name}
{job.description ? (
{job.description}
) : null}
{allow.length ? (
{allow.slice(0, MAX_CAPS).map((id) => (
{capTitle(id)}
))}
{allow.length > MAX_CAPS ? (
+{allow.length - MAX_CAPS}
) : null}
) : (
"全部只读能力"
)}
{c.allowRecover ? · 允许授权内恢复 : null}
{budgetParts.length ? budgetParts.join(" · ") : "默认"}
{latest?.model ? (
{latest.provider ? `${latest.provider} · ` : ""}
{latest.model}
) : upstream ? (
默认上游 {upstream.name}
{upstream.model ? ` · ${upstream.model}` : ""}
) : (
未配置 Agent 上游
)}
{CHECK_TRIGGER_LABEL[c.triggerCondition ?? "always"]}
{c.reportAsDecision ? · 结果写入待处理 : null}
{costs.length ? (
上次 {formatUsd(costs[0])}
{" "}
· 近 {costs.length} 次平均 {formatUsd(avg)}
) : (
还没有会话
)}
{last ? {last.label} : 未运行}
{job.lastRunAt ? {formatRelative(job.lastRunAt)} : null}
{job.lastStatus === "failed" && job.lastError ? (
失败原因:{job.lastError}
{job.lastError}
) : null}
{runs.length ? (
最近会话
{runs.slice(0, 3).map((r) => {
const s = agentRunLabel(r.status);
return (
-
{s.label}
{plainSummary(r.summary ?? r.goal)}
{formatUsd(r.usage.costUsd)}
{formatRelative(r.finishedAt ?? r.createdAt)}
);
})}
) : null}
);
}