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 ? ( -
-

最近会话

- -
- ) : 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 ? ( +
+

最近会话

+ +
+ ) : 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 ? ( + "自动化已暂停,手动操作仍可用" + ) : a.readOnlyReconcile ? ( + <> + 只读模式:不会自动下载和整理。 + + 去设置关闭 + + + ) : a.activeSubscriptions > 0 ? ( + `自动化已开启 · ${a.activeSubscriptions} 个订阅` ) : ( - - - 暂停自动化 - - } - title="暂停全部自动化?" - description="自动收录、下载、整理会停止,已提交的下载任务不受影响。可随时恢复。" - confirmLabel="暂停自动化" - onConfirm={() => setAutomation({ enabled: false }, "自动化已暂停")} - /> + <> + 自动化已开启 · 还没有订阅。 + + 去搜索添加 + + )} - + {problems.map((p) => ( + + {" · "} + + {p.text} + + + ))} +

+ {paused ? ( + + ) : ( + + + 暂停 + + } + title="暂停全部自动化?" + description="自动收录、下载、整理会停止,已提交的下载任务不受影响。可随时恢复。" + confirmLabel="暂停自动化" + onConfirm={() => setEnabled(false)} + /> + )} ); } -// ─── 正在进行:任务流 top N ─── -function InProgress({ w }: { w: Dashboard["currentWork"] }) { - const running = useEngineRunsPage({ status: "running" }, 6, 10_000); - const stats = useEngineStats(); - const rows = running.data?.items ?? []; +// ─── 在忙什么:下载中的任务 + 进行中的智能体会话 + 有业务对象的运行 ─── +function Busy({ w }: { w: Dashboard["currentWork"] }) { + const running = useEngineRunsPage({ status: "running" }, 10, 10_000); + const runs = (running.data?.items ?? []).filter((r) => r.business); + const downloads = w.items.slice(0, 4); + const agents = w.agentRuns.slice(0, 2); + const summary = nonZero([ + [w.downloading, "个下载中"], + [w.importing, "个入库中"], + [w.stalled, "个没有进展"], + [w.paused, "个已暂停"], + ]); + const empty = !downloads.length && !agents.length && !runs.length; return ( -
- {running.isPending ? ( +
+ {running.isPending && !downloads.length ? ( - ) : running.isError ? ( - running.refetch()} /> - ) : rows.length === 0 ? ( - + ) : empty ? ( + ) : (
    - {rows.slice(0, 5).map((r) => ( + {downloads.map((t) => ( + + + {t.name} + {t.status === "stalled" ? 没有进展 : null} + + + + {Math.round(t.progress * 100)}% + + + ))} + {agents.map((r) => ( + + + {r.goal} + + + + ))} + {runs.slice(0, Math.max(0, 5 - downloads.length - agents.length)).map((r) => ( {runTitle(r)} - {workflowLabel(r.workflowName)} - - {formatDuration(spanSec(r))} - ))}
)} -

- 排队 {stats.data?.byStatus.pending ?? "—"} · 下载中 {w.downloading} · 无进展 {w.stalled} · 入库中 {w.importing} -

+ {summary ?

{summary}

: null}
); } -// ─── 需要我处理:计数 + 前 3 条 ─── +// ─── 要我处理什么:计数 + 前 3 条 ─── function NeedsMe({ n }: { n: Dashboard["needsAttention"] }) { const list = useDecisions({}); const top = (list.data?.pages[0]?.items ?? []).slice(0, 3); return ( -
+
{n.total === 0 ? ( - + ) : ( <>

@@ -268,9 +278,7 @@ function NeedsMe({ n }: { n: Dashboard["needsAttention"] }) { {d.title} - - {decisionKindLabel(d.kind)} · {formatRelative(d.createdAt)} - + {formatRelative(d.createdAt)} ))} @@ -281,387 +289,39 @@ function NeedsMe({ n }: { n: Dashboard["needsAttention"] }) { ); } -// ─── 媒体概览:作品/季/资产计数 + 收录版本满足率 + 今日产出 ─── -function MediaOverview({ a }: { a: Dashboard["automation"] }) { - const c = a.slotCounts; - const total = Object.values(c).reduce((x, y) => x + y, 0); - const rate = a.slotSatisfiedRate === null ? null : Math.round(a.slotSatisfiedRate * 100); - const lib = a.library; +// ─── 最近入库了什么:成功 / 部分成功的整理 ─── +function RecentImports({ d }: { d: Dashboard }) { + const rows = d.recentOutput.filter((r) => r.status === "succeeded" || r.status === "partial").slice(0, 6); + const today = d.automation.today.filesImported; return ( -

-
- - - -
-
-
- 收录版本满足率 - {rate === null ? "—" : `${rate}%`} -
-
-
-
-

- 满足 {c.satisfied} / {total} · 需关注 {c.attention} · 下载中 {c.downloading} -

-
-

- 今日 新计划 {a.today.plansCreated} · 提交下载 {a.today.downloadsSubmitted} · 入库文件 {a.today.filesImported} · - 升级 {a.today.upgrades} -

-
- ); -} - -// ─── 计划带:自动任务与巡检分别计数,链到计划页对应节 ─── -function PlanCount({ - to, - label, - icon: Icon, - children, -}: { - to: string; - label: string; - icon: LucideIcon; - children: ReactNode; -}) { - return ( - - - - {label} - - - {children} - - - ); -} - -function Num({ children, tone }: { children: ReactNode; tone?: "destructive" }) { - return ( - - {children} - - ); -} - -function PlanStrip() { - const board = useLiveSchedules(); - const s = planStats(board.data?.items ?? []); - return ( -
调度器未在线 : null - } - > - {board.isPending ? ( - - ) : board.isError ? ( - board.refetch()} /> - ) : ( -
- - - 启用 {s.auto.enabled} / {s.auto.total} - - - 失败 {s.auto.failed} - - - - - 启用 {s.checks.enabled} / {s.checks.total} - - - 近 7 天 {formatUsd(s.checks.weekCost)} - - -
- )} -
- ); -} - -// ─── 最近完成 / 失败 ─── -function RecentRuns() { - const done = useEngineRunsPage({ status: "completed" }, 6); - const failed = useEngineRunsPage({ status: "failed" }, 6); - const rows = [...(done.data?.items ?? []), ...(failed.data?.items ?? [])] - .sort((x, y) => (y.finishedAt ?? y.createdAt).localeCompare(x.finishedAt ?? x.createdAt)) - .slice(0, 6); - const loading = done.isPending || failed.isPending; - const err = done.error ?? failed.error; - return ( -
- {loading ? ( - - ) : err ? ( - { - void done.refetch(); - void failed.refetch(); - }} - /> - ) : rows.length === 0 ? ( - +
+ {rows.length === 0 ? ( + ) : (
    - {rows.map((r) => { - const s = engineRunStatus(r); - return ( - - - {runTitle(r)} - - {workflowLabel(r.workflowName)} · {formatRelative(r.finishedAt)} + {rows.map((r) => ( + + + {r.title ?? r.targetPath?.split("/").pop() ?? "未关联作品"} + {r.status === "partial" ? ( + + {r.counts.succeeded}/{r.counts.total} 个文件入库 - - {s.label} - - ); - })} + ) : null} + + {formatRelative(r.finishedAt)} + + ))}
)} + {today ?

今天入库 {today} 个文件

: null}
); } -// ─── 来源健康:站点(含账号状态)+ Feed / 下载器 / 媒体服务器,有问题的排前 ─── -const ACCOUNT: Record = { - unconfigured: { label: "未配置账号", tone: "neutral" }, - unknown: { label: "账号未验证", tone: "neutral" }, - active: { label: "账号正常", tone: "success" }, - invalid: { label: "账号失效", tone: "destructive" }, - expired: { label: "登录过期", tone: "warning" }, - challenge: { label: "需人工验证", tone: "warning" }, -}; - -function SourcesSection({ s }: { s: Dashboard["sources"] }) { - const sites = useSitesWithAccounts(); - const siteRows = (sites.data?.items ?? s.sites).map((x) => { - const acc = "accountStatus" in x && x.accountStatus ? ACCOUNT[x.accountStatus] : undefined; - return { - id: x.id, - kind: "站点", - name: x.name, - enabled: x.enabled, - health: "healthStatus" in x ? x.healthStatus : x.health, - account: acc, - to: `/console/sites/${x.id}`, - }; - }); - const rows = [ - ...siteRows, - ...s.feeds.map((x) => ({ - id: x.id, - kind: "RSS", - name: x.name, - enabled: x.enabled, - health: x.health, - account: undefined, - to: consolePath("sources"), - })), - ...s.downloaders.map((x) => ({ - id: x.id, - kind: "下载器", - name: x.name, - enabled: x.enabled, - health: x.health, - account: undefined, - to: settingsPath("downloaders"), - })), - ]; - const bad = (r: (typeof rows)[number]) => - (r.enabled && healthLabel(r.health).tone !== "success" ? 2 : 0) + - (r.account && r.account.tone !== "success" ? 1 : 0); - rows.sort((x, y) => bad(y) - bad(x)); - const accountIssues = siteRows.filter( - (r) => r.account && (r.account.tone === "destructive" || r.account.tone === "warning"), - ).length; - return ( -
- {rows.length === 0 ? ( - - ) : ( - <> - {accountIssues ?

{accountIssues} 个站点的账号需要处理

: null} -
    - {rows.slice(0, 7).map((r) => { - const h = r.enabled ? healthLabel(r.health) : healthLabel("disabled"); - return ( - - - - {r.kind} - {r.name} - - {r.account ? ( - - {r.account.label} - - ) : null} - - {h.label} - - ); - })} -
- - )} -
- ); -} - -// ─── 运行状态摘要(/system/health) ─── -const LEVEL: Record = { - ok: { label: "正常", tone: "success" }, - warn: { label: "需要关注", tone: "warning" }, - critical: { label: "需要处理", tone: "destructive" }, - unknown: { label: "未知", tone: "neutral" }, -}; - -function StatusSummary() { - const h = useSystemHealth(30_000); - const d = h.data; - const stale = d?.processes.filter((p) => p.stale) ?? []; - const alive = (d?.processes.length ?? 0) - stale.length; - const mountsBad = d?.mounts.filter((m) => m.status !== "online").length ?? 0; - const lv = LEVEL[d?.level ?? "unknown"] ?? LEVEL.unknown; - return ( -
{lv.label} : null} - > - {h.isPending ? ( - - ) : h.isError ? ( - h.refetch()} /> - ) : d ? ( -
- - 进程 {alive} 在线 - {stale.length ? ( - - {" "} - · {stale.length} 个无心跳({stale.map((p) => `${p.processKind} ${p.pid}`).join("、")}) - - ) : null} - - - 待执行 {d.engine.pending} - {d.engine.oldestPendingAgeSec ? ` · 最久 ${formatDuration(d.engine.oldestPendingAgeSec)}` : ""} - - - 24 小时失败率{" "} - - {d.engine.failureRate24h === null ? "—" : `${(d.engine.failureRate24h * 100).toFixed(1)}%`} - - - - 数据库{" "} - {d.db.latencyMs === null ? "—" : `${d.db.latencyMs} 毫秒`} - - {mountsBad ? {mountsBad} 个挂载根不可用 : null} -
- ) : null} -
- ); -} - -function CacheSection({ c }: { c: Dashboard["cache"] }) { - const volumeLabel = useVolumeLabel(); - return ( -
- {c.volumes.length === 0 ? ( - - ) : ( -
    - {c.volumes.map((v) => { - const pct = v.usedPercent; - const over = pct !== null && pct >= v.hardThresholdPercent; - const near = pct !== null && pct >= v.resumePercent; - return ( -
  • -
    - {volumeLabel(v)} - - {pct === null ? "用量未知" : `${pct.toFixed(0)}%${over ? " 已达阈值" : ""}`} - -
    -
    -
    - -
    -

    - 阈值 {v.hardThresholdPercent}% / 恢复 {v.resumePercent}% · 在途 {formatBytes(v.pendingBytes)} · 可清理{" "} - {formatBytes(v.cleanableBytes)} · 保留期锁定 {formatBytes(v.retentionLockedBytes)} - {v.pausedByThreshold ? ` · 因阈值暂停 ${v.pausedByThreshold} 个` : ""} -

    - {v.notes.map((n) => ( -

    - {n} -

    - ))} -
  • - ); - })} -
- )} - - 调整阈值与保留天数 - -
- ); -} - -function LiveIndicator() { - const s = useSseStatus(); - const tone = s === "open" ? "success" : s === "reconnecting" ? "warning" : "neutral"; - const label = s === "open" ? "实时" : s === "reconnecting" ? "重连中" : "连接中"; - return {label}; -} - export default function Home() { const q = useLiveDashboard(); useLiveTasks(); @@ -669,21 +329,15 @@ export default function Home() { <> - - {q.data ? ( - - 更新于 {formatRelative(q.data.generatedAt)} - - ) : null} - + q.data ? ( + 更新于 {formatRelative(q.data.generatedAt)} + ) : null } /> {q.isPending ? (
- {["a", "b", "c", "d", "e", "f"].map((k) => ( + {["a", "b", "c"].map((k) => (
@@ -693,16 +347,11 @@ export default function Home() { q.refetch()} /> ) : ( <> - +
- + - - - - - - +
)} diff --git a/apps/web/app/routes/console/schedules.tsx b/apps/web/app/routes/console/schedules.tsx index ce6e0f0d..455260a8 100644 --- a/apps/web/app/routes/console/schedules.tsx +++ b/apps/web/app/routes/console/schedules.tsx @@ -18,7 +18,6 @@ export default function SchedulesPage() { <> qc.invalidateQueries({ queryKey: qk.all("engine") })}> diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index 9afeda4d..5ff2b00d 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -803,14 +803,14 @@ export const SETTINGS_SECTIONS: readonly SettingsSectionDef[] = [ { id: "automation", label: "自动化", - description: "总开关、缓存水位、每日预算、保留天数", + description: "总开关、下载盘、每日预算、保留天数", legacyTabs: [{ tab: "automation" }, { tab: "storage", anchor: "cache" }], anchors: [ { id: "switches", label: "开关" }, - { id: "cache", label: "缓存水位" }, + { id: "cache", label: "下载盘" }, { id: "budget", label: "每日预算" }, { id: "retention", label: "保留" }, - { id: "storage", label: "存储与保留" }, + { id: "advanced", label: "高级" }, ], fields: [ "automation.enabled",