diff --git a/apps/server/src/modules/acquisition-organize/index.ts b/apps/server/src/modules/acquisition-organize/index.ts
index 3812616b..4488decd 100644
--- a/apps/server/src/modules/acquisition-organize/index.ts
+++ b/apps/server/src/modules/acquisition-organize/index.ts
@@ -106,7 +106,7 @@ export function createAcquisitionOrganizeModule(overrides: AoOverrides = {}): Se
key: "downloads.sync",
defaults: {
name: "qB 同步",
- description: "同步下载器任务状态,按缓存阈值暂停或恢复",
+ description: "同步下载器任务状态;下载盘到暂停线时暂停,降到恢复线后恢复",
group: "download",
schedule: every(syncMs),
},
@@ -125,8 +125,8 @@ export function createAcquisitionOrganizeModule(overrides: AoOverrides = {}): Se
registerScheduledJob(ctx, {
key: "cache.threshold",
defaults: {
- name: "缓存阈值检查",
- description: "下载缓存超过硬阈值时入队清理",
+ name: "下载盘检查",
+ description: "下载盘超过暂停线时清理已入库的下载",
group: "maintain",
schedule: every(planMs),
},
@@ -141,7 +141,7 @@ export function createAcquisitionOrganizeModule(overrides: AoOverrides = {}): Se
key: "downloads.orchestrate",
defaults: {
name: "补提交与补入库",
- description: "补投已预留未提交的下载、已完成未入库的任务",
+ description: "补交没提交成功的下载,补整理下载完但没入库的任务",
group: "download",
schedule: every(planMs),
},
@@ -155,7 +155,7 @@ export function createAcquisitionOrganizeModule(overrides: AoOverrides = {}): Se
key: "slots.reevaluate",
defaults: {
name: "到期重评",
- description: "到了重评时间的收录位重新评估(忙时延后的发布、定时)",
+ description: "重新检查等待中的资源",
group: "download",
schedule: every(planMs),
},
@@ -165,7 +165,7 @@ export function createAcquisitionOrganizeModule(overrides: AoOverrides = {}): Se
key: "subscriptions.search",
defaults: {
name: "订阅周期搜索",
- description: "到期的订阅主动搜索站点",
+ description: "到期的订阅去站点搜索",
group: "collect",
schedule: every(planMs),
},
diff --git a/apps/server/src/modules/auth-settings/index.ts b/apps/server/src/modules/auth-settings/index.ts
index 6240c0a7..4995f91f 100644
--- a/apps/server/src/modules/auth-settings/index.ts
+++ b/apps/server/src/modules/auth-settings/index.ts
@@ -52,7 +52,7 @@ export function createAuthSettingsModule(opts: AuthSettingsModuleOptions = {}):
key: "auth.cleanup",
defaults: {
name: "登录会话清理",
- description: "删除 7 天前的登录尝试和 30 天前过期或吊销的会话",
+ description: "删除 7 天前的登录尝试和 30 天前失效的会话",
group: "maintain",
schedule: { type: "interval", everyMs },
},
diff --git a/apps/server/src/modules/cache/index.ts b/apps/server/src/modules/cache/index.ts
index dd0b53d8..7e2ae53f 100644
--- a/apps/server/src/modules/cache/index.ts
+++ b/apps/server/src/modules/cache/index.ts
@@ -183,7 +183,7 @@ export function createCacheModule(opts: CacheModuleOptions = {}): ServerModule &
key: "cache.sweep",
defaults: {
name: "缓存清理",
- description: "删除 PG 缓存里已过期的条目(TMDB 请求与识别缓存)",
+ description: "删除过期的 TMDB 与识别缓存",
group: "maintain",
schedule: { type: "interval", everyMs: 10 * 60_000 },
firstRun: "after_interval",
@@ -194,7 +194,7 @@ export function createCacheModule(opts: CacheModuleOptions = {}): ServerModule &
key: "cache.images_clean",
defaults: {
name: "图片缓存清理",
- description: "删除超过保留天数的图片代理缓存文件(对照 MP 启动时的 clear_temp)",
+ description: "删除过期的海报缓存",
group: "maintain",
schedule: { type: "interval", everyMs: 24 * 3_600_000 },
},
@@ -208,7 +208,7 @@ export function createCacheModule(opts: CacheModuleOptions = {}): ServerModule &
key: "cache.images_lru",
defaults: {
name: "图片缓存容量控制",
- description: "图片代理缓存超过容量预算(存储设置,缺省 20 GB)时,按最久未访问删除到预算的 90%",
+ description: "海报缓存超过容量上限时,删掉最久没用的",
group: "maintain",
schedule: { type: "interval", everyMs: 30 * 60_000 },
firstRun: "after_interval",
diff --git a/apps/server/src/modules/pi/index.ts b/apps/server/src/modules/pi/index.ts
index 3225108f..401dadd9 100644
--- a/apps/server/src/modules/pi/index.ts
+++ b/apps/server/src/modules/pi/index.ts
@@ -74,7 +74,7 @@ export function createPiModule(
key: "pi.token_refresh",
defaults: {
name: "模型服务登录刷新",
- description: "订阅登录即将过期的模型服务,提前刷新 token",
+ description: "订阅登录快过期时提前续期",
group: "maintain",
schedule: { type: "interval", everyMs: interval },
},
diff --git a/apps/server/src/modules/schedules/index.ts b/apps/server/src/modules/schedules/index.ts
index bd4d8a54..3e0521d5 100644
--- a/apps/server/src/modules/schedules/index.ts
+++ b/apps/server/src/modules/schedules/index.ts
@@ -37,7 +37,7 @@ export function createSchedulesModule(opts: SchedulesModuleOptions = {}): Server
key: "maintain.backup",
defaults: {
name: "备份",
- description: "数据库与数据目录备份;目前用 umt backup(宿主机 cron 调用),未接入调度",
+ description: "数据库与数据目录备份,目前在服务器上用命令执行",
group: "maintain",
schedule: { type: "cron", cron: "30 3 * * *" },
enabled: false,
diff --git a/apps/server/src/modules/sources-identity/index.ts b/apps/server/src/modules/sources-identity/index.ts
index 770f0562..6fd1bd19 100644
--- a/apps/server/src/modules/sources-identity/index.ts
+++ b/apps/server/src/modules/sources-identity/index.ts
@@ -61,7 +61,7 @@ export function createSourcesIdentityModule(overrides: RuntimeOverrides = {}): S
return { summary: `${await fn(rt)} 项` };
},
});
- job("feeds.poll", "Feed 轮询", "扫描到期的 Feed 并入队抓取(每个 Feed 仍按自己的轮询间隔)", tickFeeds);
+ job("feeds.poll", "RSS 轮询", "按各 RSS 自己的间隔抓取新资源", tickFeeds);
job("sites.snapshot", "站点快照", "扫描到周期的站点并入队快照", tickSnapshots);
job("identity.review", "未识别复查", "到期的未识别条目重新识别", tickReviews);
// W9-A 站点中心:用户数据每 6 小时、健康检测每小时(可在定时任务页关闭或改周期)
@@ -69,7 +69,7 @@ export function createSourcesIdentityModule(overrides: RuntimeOverrides = {}): S
key: "sites.user_data",
defaults: {
name: "站点数据刷新",
- description: "登录状态正常的站点账号刷新上传 / 下载 / 分享率等用户数据;会话失效的账密账号自动重登一次",
+ description: "刷新站点账号的上传、下载和分享率;登录失效的账密账号自动重登一次",
group: "collect",
schedule: { type: "interval", everyMs: 6 * 3_600_000 },
firstRun: "after_interval",
@@ -83,7 +83,7 @@ export function createSourcesIdentityModule(overrides: RuntimeOverrides = {}): S
key: "sites.health_check",
defaults: {
name: "站点健康检测",
- description: "已启用站点逐个检测首页连通与登录态,结果显示在站点页",
+ description: "检查到期的站点能否打开、是否还在登录(每个站点按自己的检测间隔),结果在站点页",
group: "collect",
schedule: { type: "interval", everyMs: 3_600_000 },
firstRun: "after_interval",
@@ -95,8 +95,7 @@ export function createSourcesIdentityModule(overrides: RuntimeOverrides = {}): S
key: BACKFILL_LANGUAGES_JOB,
defaults: {
name: "元数据语言回刷",
- description:
- "对已确认 TMDB 身份的作品按语言回退链完整刷新一次(标题、简介、每集信息),TMDB 每秒最多 2 次请求;跑完自动停用,可手动再执行",
+ description: "按语言设置重新拉一遍作品的标题、简介和每集信息;跑完自动停用,可手动再执行",
group: "maintain",
schedule: { type: "interval", everyMs: 24 * 3_600_000 },
firstRun: "now",
diff --git a/apps/server/src/modules/system/index.ts b/apps/server/src/modules/system/index.ts
index ea77bb97..8af99ac2 100644
--- a/apps/server/src/modules/system/index.ts
+++ b/apps/server/src/modules/system/index.ts
@@ -210,7 +210,7 @@ export function createSystemModule(opts: SystemModuleOptions = {}): ServerModule
key: "system.heartbeat_cleanup",
defaults: {
name: "进程心跳清理",
- description: "删除 24 小时没有心跳的进程行和超过保留期(存储设置,缺省 24 小时)的心跳采样",
+ description: "删除过期的进程状态记录",
group: "maintain",
schedule: { type: "interval", everyMs },
},
diff --git a/apps/server/src/modules/system/retention.ts b/apps/server/src/modules/system/retention.ts
index 1c0bf0a6..9fce60a3 100644
--- a/apps/server/src/modules/system/retention.ts
+++ b/apps/server/src/modules/system/retention.ts
@@ -105,8 +105,8 @@ export const RETENTION_JOBS: RetentionJob[] = [
{
target: "observation",
jobId: "storage.retention.observation",
- name: "观察记录清理",
- description: "删除超过保留期的 Feed 观察记录(observation,按 created_at)",
+ name: "RSS 抓取记录清理",
+ description: "删除过期的 RSS 抓取记录",
everyMs: 6 * HOUR,
retention: (s) => `${s.retentionDays.observation} 天`,
async run(c) {
@@ -118,8 +118,8 @@ export const RETENTION_JOBS: RetentionJob[] = [
{
target: "sse_event",
jobId: "storage.retention.sse_event",
- name: "SSE 事件清理",
- description: "删除超过保留期的 SSE 补读事件(sse_event,按 at;发布方另按条数裁剪)",
+ name: "实时事件清理",
+ description: "删除过期的页面实时推送记录",
everyMs: HOUR,
retention: (s) => `${s.retentionDays.sseEvent} 天`,
async run(c) {
@@ -132,7 +132,7 @@ export const RETENTION_JOBS: RetentionJob[] = [
target: "task_run",
jobId: "storage.retention.task_run",
name: "任务运行清理",
- description: "删除超过保留期、已结束的业务任务(task)与引擎 run(含步骤、运行元数据)",
+ description: "删除过期的已结束任务",
everyMs: 6 * HOUR,
retention: (s) => `${s.retentionDays.taskRun} 天`,
async run(c) {
@@ -168,7 +168,7 @@ export const RETENTION_JOBS: RetentionJob[] = [
target: "agent_event",
jobId: "storage.retention.agent_event",
name: "智能体事件清理",
- description: "删除超过保留期、会话已结束的智能体事件流与消息(agent_run_event / agent_run_message;会话本身保留)",
+ description: "删除已结束会话的过期消息,会话本身保留",
everyMs: 6 * HOUR,
retention: (s) => `${s.retentionDays.agentEvent} 天`,
async run(c) {
@@ -189,7 +189,7 @@ export const RETENTION_JOBS: RetentionJob[] = [
target: "audit",
jobId: "storage.retention.audit",
name: "审计清理",
- description: "删除超过保留期的审计事件(audit_event,能力调用审计按能力统计的保留期另算)",
+ description: "删除过期的审计日志",
everyMs: 24 * HOUR,
retention: (s) => `${s.retentionDays.audit} 天`,
async run(c) {
@@ -207,7 +207,7 @@ export const RETENTION_JOBS: RetentionJob[] = [
target: "capability_stats",
jobId: "storage.retention.capability_stats",
name: "能力统计清理",
- description: "删除超过保留期的能力调用审计(audit_event target_type=capability)与已结束的能力调用记录",
+ description: "删除过期的能力调用记录",
everyMs: 24 * HOUR,
retention: (s) => `${s.retentionDays.capabilityStats} 天`,
async run(c) {
@@ -231,7 +231,7 @@ export const RETENTION_JOBS: RetentionJob[] = [
target: "identity_pool",
jobId: "storage.retention.identity_pool",
name: "未识别资源归档",
- description: "未识别资源里 unmatched 条目超过保留期没有新证据就归档(不删除,仍可按状态筛出)",
+ description: "长期没有新线索的未识别资源归档,仍可手动指定",
everyMs: 6 * HOUR,
retention: (s) => `${s.retentionDays.identityPoolArchive} 天`,
async run(c) {
@@ -252,8 +252,8 @@ export const RETENTION_JOBS: RetentionJob[] = [
{
target: "analyze",
jobId: "storage.analyze",
- name: "统计信息更新",
- description: "对全库执行 ANALYZE,刷新查询规划用的统计信息(每周)",
+ name: "数据库维护",
+ description: "每周刷新数据库统计信息",
everyMs: 7 * 24 * HOUR,
retention: () => null,
async run(c) {
diff --git a/apps/web/app/components/schedules/agent-check-card.tsx b/apps/web/app/components/schedules/agent-check-card.tsx
index 4259991b..f605743c 100644
--- a/apps/web/app/components/schedules/agent-check-card.tsx
+++ b/apps/web/app/components/schedules/agent-check-card.tsx
@@ -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({
/>
-
-
- {c.goalTemplate ?? "—"}
-
+
+
+
+ 下次
+
-
-
-
- {allow.length ? (
-
- {allow.slice(0, MAX_CAPS).map((id) => (
-
- {capTitle(id)}
-
- ))}
- {allow.length > MAX_CAPS ? (
-
- +{allow.length - MAX_CAPS}
-
- ) : null}
-
+
+ {last ? (
+ job.lastStatus === "ok" ? (
+ 上次成功
) : (
- "全部只读能力"
- )}
- {c.allowRecover ? · 允许授权内恢复 : 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 ?? "模型未指定"}
- {upstream.modelFromCatalog ? (目录首个) : null}
-
- ) : (
- 未配置模型服务
- )}
-
-
- {CHECK_TRIGGER_LABEL[c.triggerCondition ?? "always"]}
- {c.reportAsDecision ? · 结果写入待处理 : null}
-
-
-
-
-
-
-
-
-
-
- {stats?.count ? (
-
- 上次 {formatUsd(stats.lastCostUsd)}
-
- {" "}
- · 近 {stats.count} 次平均 {formatUsd(stats.avgCostUsd)}
-
-
- ) : (
- 还没有会话
- )}
-
-
-
- {last ? {last.label} : 未运行}
- {job.lastRunAt ? {formatRelative(job.lastRunAt)} : null}
-
-
-
+
{last.label}
+ )
+ ) : (
+
还没运行过
+ )}
+ {job.lastRunAt ?
{formatRelative(job.lastRunAt)} : null}
+ {stats?.count ?
· 每次约 {formatUsd(stats.avgCostUsd)} : null}
+
{job.lastStatus === "failed" && job.lastError ? (
@@ -218,33 +148,96 @@ export function AgentCheckCard({
) : null}
- {runs.length ? (
-
- 最近会话
-
- {runs.slice(0, 3).map((r) => {
- const s = agentRunLabel(r.status);
- return (
- -
-
+
+
+
+ {c.goalTemplate ?? "—"}
+
+
+
+ {allow.length ? (
+
+ {allow.slice(0, MAX_CAPS).map((id) => (
+
- {s.label}
-
- {plainSummary(r.summary ?? r.goal)}
-
- {formatUsd(r.costUsd)}
-
- {formatRelative(r.finishedAt ?? r.createdAt)}
-
-
-
- );
- })}
-
-
- ) : null}
+ {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}
+
- {last ? {last.label} : 未运行}
- {job.lastRunAt ? {formatRelative(job.lastRunAt)} : null}
- {job.lastDurationMs !== null ? {formatMs(job.lastDurationMs)} : null}
+ {last ? (
+ job.lastStatus === "ok" ? (
+ 成功
+ ) : (
+ {last.label}
+ )
+ ) : (
+ 未运行
+ )}
+ {job.lastRunAt ? (
+
+ {formatRelative(job.lastRunAt)}
+
+ ) : null}
下次
@@ -178,6 +192,25 @@ function AutoTaskRow({ job, now, highlight }: { job: ScheduledJob; now: number;
);
}
+const COLLAPSE_KEY = "umt.schedules.groups";
+
+/** 组摘要:"6 个任务 · 下次 3 分后 · 1 个失败" */
+function groupSummary(list: ScheduledJob[], now: number): string {
+ const next = list
+ .filter((j) => j.enabled && j.available && j.nextRunAt)
+ .map((j) => j.nextRunAt as string)
+ .sort()[0];
+ const failed = list.filter((j) => j.lastStatus === "failed").length;
+ const running = list.filter((j) => j.runningSince).length;
+ return [
+ `${list.length} 个任务`,
+ running ? `${running} 个运行中` : next ? `下次 ${countdown(next, now)}` : null,
+ failed ? `${failed} 个失败` : null,
+ ]
+ .filter(Boolean)
+ .join(" · ");
+}
+
export function AutoTaskList({
jobs,
now,
@@ -189,37 +222,73 @@ export function AutoTaskList({
}) {
const byGroup = new Map
();
for (const j of jobs) {
+ // 未接入调度的占位任务(如备份)不列
+ if (!j.available && !j.enabled && j.group === "maintain") continue;
const list = byGroup.get(j.group);
if (list) list.push(j);
else byGroup.set(j.group, [j]);
}
const groups = TASK_GROUPS.filter((g) => byGroup.get(g)?.length);
+ // 折叠:有失败 / 运行中 / 被高亮的组默认展开,其余收起;点过的按用户的来并记在本机
+ const [saved, setSaved] = useState>(() => readCollapseState(COLLAPSE_KEY) ?? {});
+ const expanded = (g: ScheduleGroup, list: ScheduledJob[]) =>
+ list.some((j) => j.id === highlightJob) ||
+ (saved[g] ?? list.some((j) => j.lastStatus === "failed" || !!j.runningSince));
+ const toggle = (g: ScheduleGroup, v: boolean) =>
+ setSaved((cur) => {
+ const next = { ...cur, [g]: v };
+ writeCollapseState(COLLAPSE_KEY, next);
+ return next;
+ });
+ const anyOpen = groups.some((g) => expanded(g, byGroup.get(g) ?? []));
return (
-
- 名称
- 开关
- 间隔
- 上次结果
- 下次运行
- 操作
-
+ {anyOpen ? (
+
+ 名称
+ 开关
+ 间隔
+ 上次结果
+ 下次运行
+ 操作
+
+ ) : null}
{groups.map((g) => {
const list = byGroup.get(g) ?? [];
+ const open = expanded(g, list);
+ const failed = list.some((j) => j.lastStatus === "failed");
return (
-
-
- {GROUP_LABEL[g]}
- {list.length}
+
+
+
-
- {list.map((j) => (
-
- ))}
-
+ {open ? (
+
+ {list.map((j) => (
+
+ ))}
+
+ ) : null}
);
})}
diff --git a/apps/web/app/components/schedules/labels.test.ts b/apps/web/app/components/schedules/labels.test.ts
index ca147590..e4f24173 100644
--- a/apps/web/app/components/schedules/labels.test.ts
+++ b/apps/web/app/components/schedules/labels.test.ts
@@ -12,10 +12,11 @@ describe("计划页标签", () => {
expect(scheduleStreamPath("trash.purge")).toBe("/console/tasks?tab=all&kind=schedule&ref=trash.purge");
});
- it("formatUsd:小额保留 4 位", () => {
+ it("formatUsd:两位小数,不足一分写 < $0.01", () => {
expect(formatUsd(undefined)).toBe("—");
expect(formatUsd(0)).toBe("$0");
- expect(formatUsd(0.0031258)).toBe("$0.0031");
+ expect(formatUsd(0.0031258)).toBe("< $0.01");
+ expect(formatUsd(0.0191)).toBe("$0.02");
expect(formatUsd(1.234)).toBe("$1.23");
});
diff --git a/apps/web/app/components/schedules/labels.ts b/apps/web/app/components/schedules/labels.ts
index b24e119c..1b675515 100644
--- a/apps/web/app/components/schedules/labels.ts
+++ b/apps/web/app/components/schedules/labels.ts
@@ -10,7 +10,7 @@ import type { StatusTone } from "~/components/console/status-badge.tsx";
// 定时任务看板的标签映射(只在本页用,不放 lib/labels.ts)
export const GROUP_ORDER: ScheduleGroup[] = ["collect", "download", "organize", "maintain", "agent"];
-/** 计划页「自动任务」一节的分组(Agent 巡检单独一节) */
+/** 计划页「自动任务」一节的分组(智能体巡检单独一节,放在最上面);维护类合成一组「数据清理」 */
export const TASK_GROUPS: ScheduleGroup[] = ["collect", "download", "organize", "maintain"];
/** 计划页两节的锚点:/console/schedules#auto、#checks */
@@ -28,7 +28,7 @@ export const GROUP_LABEL: Record = {
collect: "采集",
download: "下载",
organize: "整理",
- maintain: "维护",
+ maintain: "数据清理",
agent: "智能体巡检",
};
@@ -70,11 +70,12 @@ export const INTERVAL_PRESETS: number[] = [
7 * 24 * 3600_000,
];
-/** 美元费用:"$0.0031" / "$0.12";空值 "—" */
+/** 美元费用:两位小数,不足一分写 "< $0.01";空值 "—" */
export function formatUsd(v: number | null | undefined): string {
if (v === null || v === undefined) return "—";
if (v === 0) return "$0";
- return `$${v < 0.1 ? v.toFixed(4) : v.toFixed(2)}`;
+ if (v < 0.01) return "< $0.01";
+ return `$${v.toFixed(2)}`;
}
/** 毫秒 → 最大整除单位 */
diff --git a/apps/web/app/components/schedules/schedule-board.tsx b/apps/web/app/components/schedules/schedule-board.tsx
index d494d174..1330c7bd 100644
--- a/apps/web/app/components/schedules/schedule-board.tsx
+++ b/apps/web/app/components/schedules/schedule-board.tsx
@@ -1,12 +1,11 @@
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 { AlertTriangle, Bot, CalendarClock, Plus } from "lucide-react";
import { type ReactNode, useEffect, useRef, useState } from "react";
import { useLocation } from "react-router";
import { usePiUpstreamModels, 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";
@@ -16,8 +15,9 @@ import { AutoTaskList } from "./auto-task-list.tsx";
import { formatUsd } from "./labels.ts";
import { RunsSheet } from "./runs-sheet.tsx";
-// 计划页(FE-6,/console/schedules):「自动任务」= 程序按固定周期执行的定时任务(紧凑列表);
-// 「Agent 巡检」= 到点发起 Agent 会话的定时任务(卡片)。自动任务的运行记录跳任务页按该定时任务筛选。
+// 计划页(FE-6,/console/schedules):「智能体巡检」= 到点发起智能体会话的定时任务(卡片,放最上面:
+// 数量少、由用户自己建、会花钱,是这页最需要看的);「自动任务」= 程序按固定周期执行的定时任务
+// (按分类折叠的紧凑列表,维护类合成「数据清理」)。自动任务的运行记录跳任务页按该定时任务筛选。
/** 每秒一跳的当前时间(倒计时用) */
export function useNow(ms = 1000) {
@@ -69,7 +69,17 @@ export function planStats(items: ScheduledJob[]) {
};
}
-function SectionHead({ id, title, hint, actions }: { id: string; title: string; hint: string; actions?: ReactNode }) {
+function SectionHead({
+ id,
+ title,
+ hint,
+ actions,
+}: {
+ id: string;
+ title: string;
+ hint: ReactNode;
+ actions?: ReactNode;
+}) {
return (
@@ -107,7 +117,6 @@ export function PlanBoard({ highlightJob }: { highlightJob?: string }) {
}
: null;
const s = planStats(items);
- const val = (n: number) => (board.isPending ? "…" : board.isError ? "—" : n);
// 首页计数链到 #auto / #checks:数据到了再滚过去
const loaded = !!data;
@@ -117,59 +126,8 @@ export function PlanBoard({ highlightJob }: { highlightJob?: string }) {
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")} />
-
-
-
-
智能体巡检
-
- jump("checks")}
- />
- 0 ? "destructive" : "neutral"}
- value={val(s.checks.failed)}
- onClick={() => jump("checks")}
- />
- jump("checks")}
- />
-
-
-
-
{data && !data.schedulerOnline ? (
board.refetch()} />
) : (
-
-
- {autoJobs.length === 0 ? (
-
- ) : (
-
- )}
-
-
setEditing({ job: null, key: Date.now() })}>
@@ -229,6 +184,24 @@ export function PlanBoard({ highlightJob }: { highlightJob?: string }) {
)}
+
+
+ {autoJobs.length === 0 ? (
+
+ ) : (
+
+ )}
+
)}
diff --git a/apps/web/app/components/settings/automation.tsx b/apps/web/app/components/settings/automation.tsx
index 7e7bb75c..bb26eb02 100644
--- a/apps/web/app/components/settings/automation.tsx
+++ b/apps/web/app/components/settings/automation.tsx
@@ -1,12 +1,13 @@
-import { Pause, Play, ShieldAlert } from "lucide-react";
-import type { ReactNode } from "react";
+import { ChevronDown, Pause, Play, ShieldAlert } from "lucide-react";
+import { type ReactNode, useEffect, useState } from "react";
+import { useLocation } from "react-router";
import { toast } from "sonner";
import { useUpdateSettings } from "~/api/settings.ts";
import { Switch } from "~/components/ui/switch.tsx";
import { errorMessage } from "~/lib/api.ts";
import { cn } from "~/lib/utils.ts";
import { type FormValues, num } from "./resource-section.tsx";
-import { SettingsBlock } from "./settings-block.tsx";
+import { anchorId, SettingsBlock } from "./settings-block.tsx";
import { type CardSpec, n, SettingsCard, SettingsGate } from "./settings-card.tsx";
// 自动化分区:总开关、只读对账、缓存水位、每日下载预算、保留天数、存储预算与保留(CACHE-2)
@@ -15,12 +16,12 @@ const GiB = 1024 ** 3;
const CACHE_CARD: CardSpec = {
anchor: "cache",
- title: "缓存水位",
- description: "缓存卷使用率到硬阈值时停止新增下载并暂停本工具的下载任务;降到恢复水位以下只恢复因阈值暂停的任务。",
+ title: "下载盘",
+ description: "下载盘用量到暂停线时不再新增下载,并暂停本工具的下载任务;降到恢复线以下只恢复因此暂停的任务。",
fields: [
- { name: "cache.hardThresholdPercent", label: "硬阈值(%)", kind: "number" },
- { name: "cache.resumePercent", label: "恢复水位(%)", kind: "number", hint: "低于硬阈值" },
- { name: "cache.cleanupTargetPercent", label: "清理目标(%)", kind: "number", hint: "不高于恢复水位" },
+ { name: "cache.hardThresholdPercent", label: "暂停线(%)", kind: "number" },
+ { name: "cache.resumePercent", label: "恢复线(%)", kind: "number", hint: "低于暂停线" },
+ { name: "cache.cleanupTargetPercent", label: "清理目标(%)", kind: "number", hint: "不高于恢复线" },
{ name: "cache.checkIntervalSec", label: "检查间隔(秒)", kind: "number", hint: "10–3600" },
],
fromSettings: (s) => ({
@@ -75,7 +76,7 @@ const RETENTION_CARD: CardSpec = {
kind: "number",
hint: "复制入库后下载源在缓存里至少留这么久;0–365",
},
- { name: "retention.logRetentionDays", label: "日志保留天数", kind: "number", hint: "观察与日志类数据" },
+ { name: "retention.logRetentionDays", label: "日志保留天数", kind: "number" },
],
fromSettings: (s) => ({
"retention.seedRetentionDays": n(s.retention.seedRetentionDays),
@@ -211,6 +212,59 @@ function ToggleRow({
);
}
+/** 高级:只读对账、存储预算、流水保留、缓存有效期;默认收起,从 #automation/advanced 进来时展开 */
+function Advanced({ children }: { children: ReactNode }) {
+ const location = useLocation();
+ const [open, setOpen] = useState(() => location.hash.endsWith("/advanced"));
+ useEffect(() => {
+ if (location.hash.endsWith("/advanced")) setOpen(true);
+ }, [location.hash]);
+ return (
+
+
+ 只读对账、存储预算、数据保留和缓存有效期。一般不用改。
+ {open ? {children}
: null}
+
+ );
+}
+
+function ReadOnlySwitch() {
+ const update = useUpdateSettings();
+ return (
+
+ {(s) => (
+
+ }
+ tone={s.automation.readOnlyReconcile ? "warning" : "success"}
+ title={`只读对账 · ${s.automation.readOnlyReconcile ? "开启" : "关闭"}`}
+ description="从备份恢复后先开启:只核对库存与路径,不做自动写入。核对完再关闭。"
+ checked={s.automation.readOnlyReconcile}
+ disabled={update.isPending}
+ onChange={(v) =>
+ update.mutateAsync({ automation: { readOnlyReconcile: v } }).then(
+ () => toast.success(v ? "已开启只读对账" : "已关闭只读对账"),
+ (err) => toast.error(errorMessage(err, "切换失败")),
+ )
+ }
+ />
+
+ )}
+
+ );
+}
+
function Switches() {
const update = useUpdateSettings();
const set = async (patch: { enabled?: boolean; readOnlyReconcile?: boolean }, msg: string) => {
@@ -236,16 +290,6 @@ function Switches() {
disabled={update.isPending}
onChange={(v) => set({ enabled: v }, v ? "自动化已恢复" : "自动化已暂停")}
/>
-
}
- tone={s.automation.readOnlyReconcile ? "warning" : "success"}
- title={`只读对账 · ${s.automation.readOnlyReconcile ? "开启" : "关闭"}`}
- description="从备份恢复后先开启:只核对库存与路径,不做自动写入。核对完再关闭。"
- checked={s.automation.readOnlyReconcile}
- disabled={update.isPending}
- onChange={(v) => set({ readOnlyReconcile: v }, v ? "已开启只读对账" : "已关闭只读对账")}
- />
)}
@@ -263,9 +307,12 @@ export function AutomationSection() {
-
-
-
+
+
+
+
+
+
>
)}
diff --git a/apps/web/app/components/status/storage-section.tsx b/apps/web/app/components/status/storage-section.tsx
index cb8e79f4..431de792 100644
--- a/apps/web/app/components/status/storage-section.tsx
+++ b/apps/web/app/components/status/storage-section.tsx
@@ -38,7 +38,7 @@ export const CLEANUP_LABEL: Record = {
audit: "审计事件",
capability_stats: "能力调用统计",
identity_pool: "未识别资源归档",
- analyze: "统计信息更新",
+ analyze: "数据库维护",
images: "图片缓存容量",
kv_cache: "过期缓存",
};
diff --git a/apps/web/app/components/tasks/labels.ts b/apps/web/app/components/tasks/labels.ts
index b271149e..2e2cbcb9 100644
--- a/apps/web/app/components/tasks/labels.ts
+++ b/apps/web/app/components/tasks/labels.ts
@@ -94,7 +94,7 @@ export const RUN_STATUS_OPTIONS = ["pending", "running", "completed", "failed",
export function runTitle(r: Pick): string {
const b = r.business;
if (b?.title) return b.title;
- if (b) return `${BUSINESS_KIND_LABEL[b.kind]} ${b.refId.length > 12 ? b.refId.slice(0, 8) : b.refId}`;
+ if (b) return BUSINESS_KIND_LABEL[b.kind];
return workflowLabel(r.workflowName);
}
diff --git a/apps/web/app/components/tasks/task-detail.tsx b/apps/web/app/components/tasks/task-detail.tsx
index 355e0758..986fb0fc 100644
--- a/apps/web/app/components/tasks/task-detail.tsx
+++ b/apps/web/app/components/tasks/task-detail.tsx
@@ -254,7 +254,7 @@ export function TaskDetailBody({ r, onOpenRun }: { r: EngineRunDetail; onOpenRun
return (
<>
-
+
- {BUSINESS_KIND_LABEL[b.kind]} · {b.title ?? b.refId}
+ {b.title ? `${BUSINESS_KIND_LABEL[b.kind]} · ${b.title}` : BUSINESS_KIND_LABEL[b.kind]}
) : (
- `${BUSINESS_KIND_LABEL[b.kind]} · ${b.title ?? b.refId}`
+ `${BUSINESS_KIND_LABEL[b.kind]}${b.title ? ` · ${b.title}` : ""}`
)
) : (
"无"
@@ -279,7 +279,6 @@ export function TaskDetailBody({ r, onOpenRun }: { r: EngineRunDetail; onOpenRun
label: "开始",
value: r.startedAt ? `${formatDateTime(r.startedAt)}(${formatRelative(r.startedAt)})` : "未开始",
},
- { label: "耗时", value: formatDuration(spanSec(r)) },
]}
/>
@@ -304,9 +303,11 @@ export function TaskDetailBody({ r, onOpenRun }: { r: EngineRunDetail; onOpenRun
-
+
p.items);
- const filtered = Object.values(filters).some(Boolean);
+ const all = list.data?.pages.flatMap((p) => p.items);
+ // 系统任务(没有业务对象的引擎步骤、定时任务的成功运行)默认不列;按对象 / 定时任务筛选或勾选时显示
+ const showSystem = filters.system === "1" || !!filters.ref || filters.kind === "schedule" || !!filters.workflow;
+ const isSystem = (r: EngineRun) => !r.business || (r.business.kind === "schedule" && r.status === "completed");
+ const rows = showSystem ? all : all?.filter((r) => !isSystem(r));
+ const hidden = (all?.length ?? 0) - (rows?.length ?? 0);
+ const filtered = Object.entries(filters).some(([k, v]) => k !== "system" && !!v);
return (
<>
@@ -196,6 +203,14 @@ export function TaskStream({
options={SINCE}
allLabel="不限"
/>
+
{filters.workflow ? (