diff --git a/apps/web/app/components/schedules/agent-check-card.tsx b/apps/web/app/components/schedules/agent-check-card.tsx index ed147829..bd07be18 100644 --- a/apps/web/app/components/schedules/agent-check-card.tsx +++ b/apps/web/app/components/schedules/agent-check-card.tsx @@ -1,13 +1,13 @@ import { type AgentCheckConfig, 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 { 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 { TechInfo } from "~/components/console/tech-info.tsx"; import { Button } from "~/components/ui/button.tsx"; import { Switch } from "~/components/ui/switch.tsx"; import { errorMessage } from "~/lib/api.ts"; @@ -19,10 +19,8 @@ import { IntervalSelect } from "./interval-select.tsx"; import { CHECK_TRIGGER_LABEL, formatUsd, goalTemplateParts, RUN_STATUS } from "./labels.ts"; // 计划页「智能体巡检」:到点按目标模板发起一次智能体会话(kind=agent_check),一个巡检一张卡片。 -// 卡片默认只显示名称、开关、间隔、上次结果与费用;目标、能力、预算、模型、最近会话收进「详情」。 - -/** 卡片上能力白名单最多列几个,其余折成"+N" */ -const MAX_CAPS = 6; +// 卡片统一尺寸(FIX-9):名称 / 描述 / 目标各一行省略,最近汇总(或失败原因)固定 2 行,模型与费用一行, +// 操作按钮贴底,网格按行等高。完整内容(目标、能力、预算、模型、最近会话)在「详情」抽屉里看。 /** 会话摘要是 Markdown:列表里只取纯文字 */ const plainSummary = (t: string) => @@ -44,6 +42,12 @@ export type DefaultUpstream = { 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 ( @@ -96,27 +100,22 @@ export function AgentCheckCard({ if (highlight) ref.current?.scrollIntoView({ block: "center" }); }, [highlight]); + const [detailOpen, setDetailOpen] = useState(false); const c = job.config as Partial; - const budget = c.budget ?? {}; - const allow = c.capabilityAllowlist ?? []; const last = job.lastStatus ? RUN_STATUS[job.lastStatus] : null; // 费用 / 模型由后端按最近 100 次会话汇总(GET /schedules 的 agentRuns) 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); + const latest = stats?.recent[0]; + const failed = job.lastStatus === "failed" && !!job.lastError; return (
@@ -125,9 +124,9 @@ export function AgentCheckCard({

{job.name}

- {job.description ? ( -

{job.description}

- ) : null} +

+ {job.description} +

-
+
- + 下次
-

- {last ? ( - job.lastStatus === "ok" ? ( - 上次成功 + +

+

+ {last ? ( + job.lastStatus === "ok" ? ( + 上次成功 + ) : ( + {last.label} + ) ) : ( - {last.label} - ) - ) : ( - 还没运行过 - )} - {job.lastRunAt ? {formatRelative(job.lastRunAt)} : null} - {stats?.count ? · 每次约 {formatUsd(stats.avgCostUsd)} : null} -

+ 还没运行过 + )} + {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.lastStatus === "failed" && job.lastError ? ( -
- 失败原因:{job.lastError} -

{job.lastError}

-
- ) : null} - - -
- - - {c.goalTemplate ? : "—"} - - - - {allow.length ? ( - - {allow.slice(0, MAX_CAPS).map((id) => ( - - {capTitle(id)} - - ))} - {allow.length > MAX_CAPS ? ( - - +{allow.length - MAX_CAPS} - - ) : null} - - ) : ( - "全部只读能力" - )} - {c.allowRecover ? · 允许授权内恢复 : null} - - - {CHECK_TRIGGER_LABEL[c.triggerCondition ?? "always"]} - {c.reportAsDecision ? · 结果写入待处理 : null} - - {budgetParts.length ? budgetParts.join(" · ") : "默认"} - - {stats?.lastModel ? ( - - {stats.lastProvider ? `${stats.lastProvider} · ` : ""} - {stats.lastModel} - {stats.models.length > 1 ? ( - 等 {stats.models.length} 个模型 - ) : null} - - ) : upstream ? ( - - {upstream.name} · {upstream.model ?? "模型未指定"} - - ) : ( - 未配置模型服务 - )} - - {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}} - - {plainSummary(r.summary ?? r.goal)} - - - {formatRelative(r.finishedAt ?? r.createdAt)} - - -
  • - ); - })} -
-
- ) : 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} +
+ ); +} diff --git a/apps/web/app/components/schedules/schedule-board.tsx b/apps/web/app/components/schedules/schedule-board.tsx index 1330c7bd..73073481 100644 --- a/apps/web/app/components/schedules/schedule-board.tsx +++ b/apps/web/app/components/schedules/schedule-board.tsx @@ -169,7 +169,7 @@ export function PlanBoard({ highlightJob }: { highlightJob?: string }) { {checks.length === 0 ? ( ) : ( -
+
{checks.map((j) => (