feat(web): 首页只回答三件事、计划页巡检置顶与分组折叠、任务列表去噪(DEAI-2)

首页:顶部一行状态(自动化开关 + 只在异常时出现的服务 / 下载盘 / 站点账号 / 下载器问题),
下面只有「在忙什么」「要我处理」「最近入库」三块;去掉运行状态、计划、缓存、来源健康、
满足率与 0 值铺陈;0 订阅时写「还没有订阅。去搜索添加」。只读对账移出首页,设置里放进
自动化 →「高级」(与存储预算、流水保留、缓存有效期一起默认收起)。

计划页:「智能体巡检」移到最上面——巡检数量少、由用户自己建、每次会花钱,是这页最需要
看的;自动任务任务多、多数不用管,放在下面。自动任务按分类分组、每组可折叠:有失败或
运行中的组默认展开,其余收起为「N 个任务 · 下次 xx」一行,折叠状态记在 localStorage;
维护类整组叫「数据清理」,未接入调度的备份不列。巡检卡只留名称、开关、间隔、上次结果与
费用,目标 / 能力 / 预算 / 模型 / 最近会话收进「详情」。去掉顶部 6 个计数卡。定时任务说明
去掉表名、字段名与实现备注。

任务页:默认隐藏系统任务(无业务对象的引擎步骤、定时任务的成功运行),可一键显示;
标题不再用 ID;耗时移入技术信息;< 1 秒写「< 1 秒」;费用两位小数,不足一分写「< $0.01」。
下载盘相关文案改用「暂停线 / 恢复线」。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-29 20:48:35 +08:00
1 parent f0d4935fa7
commit 260f3ab27f
23 files changed
+559 -816

No files matched your search

@@ -7,6 +7,7 @@ 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 { 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";
@@ -16,7 +17,8 @@ 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),一个巡检一张卡片。
// 计划页「智能体巡检」:到点按目标模板发起一次智能体会话(kind=agent_check),一个巡检一张卡片。
// 卡片默认只显示名称、开关、间隔、上次结果与费用;目标、能力、预算、模型、最近会话收进「详情」。
/** 卡片上能力白名单最多列几个,其余折成"+N" */
const MAX_CAPS = 6;
@@ -113,103 +115,31 @@ export function AgentCheckCard({
/>
</header>
<div className="rounded-tight border border-border/70 px-2.5 py-2">
<p className="line-clamp-3 text-caption text-foreground" title={c.goalTemplate}>
{c.goalTemplate ?? "—"}
</p>
<div className="flex flex-wrap items-center gap-x-4 gap-y-2 text-meta text-muted-foreground">
<IntervalSelect
label={`${job.name}间隔`}
schedule={job.schedule}
disabled={!job.available}
pending={a.update.isPending}
onSave={a.setSchedule}
/>
<span className="inline-flex items-center gap-1">
下次 <NextRun job={job} now={now} />
</span>
</div>
<dl className="grid grid-cols-[4.5rem_minmax(0,1fr)] items-baseline gap-x-3 gap-y-1.5">
<Field label="能力">
{allow.length ? (
<span className="flex flex-wrap gap-1">
{allow.slice(0, MAX_CAPS).map((id) => (
<span
key={id}
title={id}
className="rounded-full border border-border/70 px-1.5 py-px text-meta text-muted-foreground"
>
{capTitle(id)}
</span>
))}
{allow.length > MAX_CAPS ? (
<span
className="px-1 text-meta text-muted-foreground"
title={allow.slice(MAX_CAPS).map(capTitle).join("、")}
>
+{allow.length - MAX_CAPS}
</span>
) : null}
</span>
<p className="flex flex-wrap items-center gap-x-2 gap-y-1 text-meta text-muted-foreground">
{last ? (
job.lastStatus === "ok" ? (
<span>上次成功</span>
) : (
"全部只读能力"
)}
{c.allowRecover ? <span className="ml-1 text-meta text-muted-foreground">· 允许授权内恢复</span> : null}
</Field>
<Field label="预算">{budgetParts.length ? budgetParts.join(" · ") : "默认"}</Field>
<Field label="模型服务">
{stats?.lastModel ? (
<span className="break-words" title={modelsTitle(stats.models)}>
{stats.lastProvider ? `${stats.lastProvider} · ` : ""}
{stats.lastModel}
{stats.models.length > 1 ? (
<span className="text-muted-foreground"> 等 {stats.models.length} 个模型</span>
) : null}
</span>
) : upstream ? (
<span
className="break-words"
title={
upstream.modelFromCatalog
? "模型服务没有指定默认模型,运行时使用模型目录里的第一个"
: "还没有运行记录,按默认模型服务显示"
}
>
默认模型服务 {upstream.name} · {upstream.model ?? "模型未指定"}
{upstream.modelFromCatalog ? <span className="text-muted-foreground">(目录首个)</span> : null}
</span>
) : (
<span className="text-muted-foreground">未配置模型服务</span>
)}
</Field>
<Field label="触发条件">
{CHECK_TRIGGER_LABEL[c.triggerCondition ?? "always"]}
{c.reportAsDecision ? <span className="ml-1 text-meta text-muted-foreground">· 结果写入待处理</span> : null}
</Field>
<Field label="间隔">
<IntervalSelect
label={`${job.name}间隔`}
schedule={job.schedule}
disabled={!job.available}
pending={a.update.isPending}
onSave={a.setSchedule}
/>
</Field>
<Field label="下次">
<span className="text-meta">
<NextRun job={job} now={now} />
</span>
</Field>
<Field label="每次费用">
{stats?.count ? (
<span className="tabular-nums">
上次 {formatUsd(stats.lastCostUsd)}
<span className="text-muted-foreground">
{" "}
· 近 {stats.count} 次平均 {formatUsd(stats.avgCostUsd)}
</span>
</span>
) : (
<span className="text-muted-foreground">还没有会话</span>
)}
</Field>
<Field label="最近结果">
<span className="flex flex-wrap items-center gap-x-2 gap-y-1 text-meta text-muted-foreground">
{last ? <StatusBadge tone={last.tone}>{last.label}</StatusBadge> : <span>未运行</span>}
{job.lastRunAt ? <span title={job.lastRunAt}>{formatRelative(job.lastRunAt)}</span> : null}
</span>
</Field>
</dl>
<StatusBadge tone={last.tone}>{last.label}</StatusBadge>
)
) : (
<span>还没运行过</span>
)}
{job.lastRunAt ? <span title={job.lastRunAt}>{formatRelative(job.lastRunAt)}</span> : null}
{stats?.count ? <span className="tabular-nums">· 每次约 {formatUsd(stats.avgCostUsd)}</span> : null}
</p>
{job.lastStatus === "failed" && job.lastError ? (
<details className="group rounded-tight border border-destructive/30 bg-destructive/10 px-2.5 py-1.5 text-meta text-destructive">
@@ -218,33 +148,96 @@ export function AgentCheckCard({
</details>
) : null}
{runs.length ? (
<section aria-label="最近会话" className="grid min-w-0 gap-1 border-t border-border/70 pt-2.5">
<h4 className="text-meta text-muted-foreground">最近会话</h4>
<ul className="grid min-w-0 grid-cols-[minmax(0,1fr)] gap-1">
{runs.slice(0, 3).map((r) => {
const s = agentRunLabel(r.status);
return (
<li key={r.id} className="min-w-0">
<Link
to={consolePath("agent", r.id)}
className="flex min-w-0 items-center gap-2 rounded-tight px-1 py-0.5 text-meta text-muted-foreground hover:bg-foreground/[0.04] hover:text-foreground"
<TechInfo title="详情" columns={1}>
<dl className="grid grid-cols-[4.5rem_minmax(0,1fr)] items-baseline gap-x-3 gap-y-1.5">
<Field label="目标">
<span className="line-clamp-4 break-words" title={c.goalTemplate}>
{c.goalTemplate ?? "—"}
</span>
</Field>
<Field label="能力">
{allow.length ? (
<span className="flex flex-wrap gap-1">
{allow.slice(0, MAX_CAPS).map((id) => (
<span
key={id}
title={id}
className="rounded-full border border-border/70 px-1.5 py-px text-meta text-muted-foreground"
>
<StatusBadge tone={s.tone}>{s.label}</StatusBadge>
<span className="min-w-0 flex-1 truncate" title={r.summary ?? undefined}>
{plainSummary(r.summary ?? r.goal)}
</span>
<span className="shrink-0 tabular-nums">{formatUsd(r.costUsd)}</span>
<span className="shrink-0" title={r.createdAt}>
{formatRelative(r.finishedAt ?? r.createdAt)}
</span>
</Link>
</li>
);
})}
</ul>
</section>
) : null}
{capTitle(id)}
</span>
))}
{allow.length > MAX_CAPS ? (
<span
className="px-1 text-meta text-muted-foreground"
title={allow.slice(MAX_CAPS).map(capTitle).join("、")}
>
+{allow.length - MAX_CAPS}
</span>
) : null}
</span>
) : (
"全部只读能力"
)}
{c.allowRecover ? <span className="ml-1 text-meta text-muted-foreground">· 允许授权内恢复</span> : null}
</Field>
<Field label="触发条件">
{CHECK_TRIGGER_LABEL[c.triggerCondition ?? "always"]}
{c.reportAsDecision ? <span className="ml-1 text-meta text-muted-foreground">· 结果写入待处理</span> : null}
</Field>
<Field label="预算">{budgetParts.length ? budgetParts.join(" · ") : "默认"}</Field>
<Field label="模型">
{stats?.lastModel ? (
<span className="break-words" title={modelsTitle(stats.models)}>
{stats.lastProvider ? `${stats.lastProvider} · ` : ""}
{stats.lastModel}
{stats.models.length > 1 ? (
<span className="text-muted-foreground"> 等 {stats.models.length} 个模型</span>
) : null}
</span>
) : upstream ? (
<span className="break-words">
{upstream.name} · {upstream.model ?? "模型未指定"}
</span>
) : (
<span className="text-muted-foreground">未配置模型服务</span>
)}
</Field>
{stats?.count ? (
<Field label="费用">
<span className="tabular-nums">
上次 {formatUsd(stats.lastCostUsd)} · 近 {stats.count} 次平均 {formatUsd(stats.avgCostUsd)}
</span>
</Field>
) : null}
</dl>
{runs.length ? (
<section aria-label="最近会话" className="grid min-w-0 gap-1">
<h4 className="text-meta text-muted-foreground">最近会话</h4>
<ul className="grid min-w-0 grid-cols-[minmax(0,1fr)] gap-1">
{runs.slice(0, 3).map((r) => {
const st = agentRunLabel(r.status);
return (
<li key={r.id} className="min-w-0">
<Link
to={consolePath("agent", r.id)}
className="flex min-w-0 items-center gap-2 rounded-tight px-1 py-0.5 text-meta text-muted-foreground hover:bg-foreground/[0.04] hover:text-foreground"
>
{r.status === "succeeded" ? null : <StatusBadge tone={st.tone}>{st.label}</StatusBadge>}
<span className="min-w-0 flex-1 truncate" title={r.summary ?? undefined}>
{plainSummary(r.summary ?? r.goal)}
</span>
<span className="shrink-0" title={r.createdAt}>
{formatRelative(r.finishedAt ?? r.createdAt)}
</span>
</Link>
</li>
);
})}
</ul>
</section>
) : null}
</TechInfo>
<footer className="flex flex-wrap items-center gap-1.5">
<Button size="sm" disabled={!a.canRun} onClick={a.run}>