import { type AgentCheckConfig, CAPABILITY_CATALOG, type KnownCapabilityId, type ScheduledJob } from "@umt/contracts"; import { ChevronRight, History, LoaderCircle, Pencil, Play, RotateCcw, SkipForward, Trash2 } from "lucide-react"; import { type ReactNode, useEffect, useRef, useState } 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 { DetailSheet } from "~/components/console/detail-sheet.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 { formatTokens } from "~/lib/tokens.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, goalTemplateParts, RUN_STATUS } from "./labels.ts"; // 计划页「智能体巡检」:到点按目标模板发起一次智能体会话(kind=agent_check),一个巡检一张卡片。 // 卡片统一尺寸(FIX-9):名称 / 描述 / 目标各一行省略,最近汇总(或失败原因)固定 2 行,模型与费用一行, // 操作按钮贴底,网格按行等高。完整内容(目标、能力、预算、模型、最近会话)在「详情」抽屉里看。 /** 会话摘要是 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; /** 上游的默认模型;上游没填时是模型目录的第一个(modelFromCatalog) */ model: string | null; modelFromCatalog?: boolean; } | null; const modelsTitle = (models: { provider: string | null; model: string; count: number }[]) => models.map((m) => `${m.provider ? `${m.provider} · ` : ""}${m.model}:${m.count} 次`).join("\n"); /** 目标模板的纯文字版(悬浮提示用),占位符写成名称 */ const goalPlain = (template: string) => goalTemplateParts(template) .map((p) => ("text" in p ? p.text : `[${p.label}]`)) .join(""); /** 目标模板:占位符显示成名称标签(执行时替换成实际值),不露 `{{now}}` */ function GoalTemplate({ template }: { template: string }) { return ( <> {goalTemplateParts(template).map((p, i) => "text" in p ? ( {p.text} ) : ( {p.label} ), )} ); } function Field({ label, children }: { label: string; children: ReactNode }) { return ( <>
{label}
{children}
); } export function AgentCheckCard({ job, now, upstream, highlight, onEdit, onHistory, }: { job: ScheduledJob; now: number; 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 [detailOpen, setDetailOpen] = useState(false); const c = job.config as Partial; const last = job.lastStatus ? RUN_STATUS[job.lastStatus] : null; // 费用 / 模型由后端按最近 100 次会话汇总(GET /schedules 的 agentRuns) const stats = job.agentRuns; const latest = stats?.recent[0]; const failed = job.lastStatus === "failed" && !!job.lastError; return (

{job.name}

{job.description}

下次

{last ? ( job.lastStatus === "ok" ? ( 上次成功 ) : ( {last.label} ) ) : ( 还没运行过 )} {job.lastRunAt ? {formatRelative(job.lastRunAt)} : null}

目标 {c.goalTemplate ? : "—"}

{failed ? `失败原因:${job.lastError}` : latest ? plainSummary(latest.summary ?? latest.goal) : null}

{stats?.count ? · 每次约 {formatUsd(stats.avgCostUsd)} : null}

{job.customized ? ( ) : null} {!job.builtin ? ( } title={`删除「${job.name}」?`} description="运行记录一并删除,已产生的智能体会话保留。" confirmLabel="删除巡检" destructive onConfirm={() => del.mutateAsync({ id: job.id }).then( () => toast.success("已删除"), (e) => { toast.error(errorMessage(e)); throw e; }, ) } /> ) : null}
); } function ModelText({ stats, upstream }: { stats: ScheduledJob["agentRuns"]; upstream: DefaultUpstream }) { if (stats?.lastModel) { return ( <> {stats.lastProvider ? `${stats.lastProvider} · ` : ""} {stats.lastModel} {stats.models.length > 1 ? ( 等 {stats.models.length} 个模型 ) : null} ); } if (upstream) { return ( <> {upstream.name} · {upstream.model ?? "模型未指定"} ); } return 未配置模型服务; } /** 「详情」抽屉:卡片上省略掉的内容全在这里(目标全文、能力、触发条件、预算、模型、费用、失败原因、最近会话) */ function AgentCheckDetail({ job, upstream, open, onOpenChange, }: { job: ScheduledJob; upstream: DefaultUpstream; open: boolean; onOpenChange: (open: boolean) => void; }) { const c = job.config as Partial; const budget = c.budget ?? {}; const allow = c.capabilityAllowlist ?? []; const stats = job.agentRuns; const runs = stats?.recent ?? []; const budgetParts = [ budget.maxToolCalls ? `${budget.maxToolCalls} 次调用` : null, budget.maxDurationSec ? `${budget.maxDurationSec} 秒` : null, budget.maxTokens ? `${formatTokens(budget.maxTokens)} token` : null, budget.maxCostUsd ? `${formatUsd(budget.maxCostUsd)} 上限` : null, ].filter(Boolean); return ( {job.lastStatus === "failed" && job.lastError ? (

失败原因:{job.lastError}

) : null}
{c.goalTemplate ? : "—"} {allow.length ? ( {allow.map((id) => ( {capTitle(id)} ))} ) : ( "全部只读能力" )} {c.allowRecover ? · 允许授权内恢复 : null} {CHECK_TRIGGER_LABEL[c.triggerCondition ?? "always"]} {c.reportAsDecision ? · 生成巡检报告 : null} {budgetParts.length ? budgetParts.join(" · ") : "默认"} {stats?.count ? ( 上次 {formatUsd(stats.lastCostUsd)} · 近 {stats.count} 次平均 {formatUsd(stats.avgCostUsd)} ) : null}
{runs.length ? (

最近会话

    {runs.slice(0, 3).map((r) => { const st = agentRunLabel(r.status); return (
  • {r.status === "succeeded" ? null : {st.label}} {formatRelative(r.finishedAt ?? r.createdAt)} {plainSummary(r.summary ?? r.goal)}
  • ); })}
) : null}
); }