feat(settings): storage budgets and retention in settings UI, tmdbMaxSize default 5000 (INT-3)

- Automation section gets storage budget, retention and cache TTL cards bound to settings.storage
- Metadata section drops deprecated cache.tmdbTtlHours / cache.emptyResultTtlSec / identity.poolArchiveDays inputs
- cache.tmdbMaxSize default 256 -> 5000; migration 0051 bumps stored values still at the old default

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-29 17:46:36 +08:00
1 parent 0a8bfbb63e
commit fe05138c9c
7 files changed
+119 -31

No files matched your search

@@ -5,11 +5,11 @@ 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 { num } from "./resource-section.tsx";
import { type FormValues, num } from "./resource-section.tsx";
import { SettingsBlock } from "./settings-block.tsx";
import { type CardSpec, n, SettingsCard, SettingsGate } from "./settings-card.tsx";
// 自动化分区:总开关、只读对账、缓存水位、每日下载预算、保留天数
// 自动化分区:总开关、只读对账、缓存水位、每日下载预算、保留天数、存储预算与保留(CACHE-2)
const GiB = 1024 ** 3;
@@ -89,6 +89,88 @@ const RETENTION_CARD: CardSpec = {
}),
};
// CACHE-2:存储预算、流水保留期、缓存有效期(settings.storage,docs/runbook/storage.md)
const gbField = (k: string, label: string, hint?: string) => ({
name: `storage.budgets.${k}`,
label,
kind: "number" as const,
hint,
});
const dayField = (group: "retentionDays" | "cacheTtl", k: string, label: string, hint?: string) => ({
name: `storage.${group}.${k}`,
label,
kind: "number" as const,
hint,
});
const pick = (v: FormValues, prefix: string, keys: readonly string[]) =>
Object.fromEntries(keys.map((k) => [k, num(v[`${prefix}.${k}`])]));
const BUDGET_KEYS = ["pgDataGb", "pgDataWarnGb", "imagesGb", "backupsGb", "sessionsEvidenceGb"] as const;
const STORAGE_BUDGET_CARD: CardSpec = {
anchor: "storage",
title: "存储预算",
description: "超过预算时健康页告警;图片缓存超出容量按最久未访问删除。单位 GB。",
fields: [
gbField("pgDataGb", "数据库预算", "超过为严重"),
gbField("pgDataWarnGb", "数据库告警线", "不高于数据库预算"),
gbField("imagesGb", "图片缓存容量"),
gbField("backupsGb", "备份目录预算"),
gbField("sessionsEvidenceGb", "Agent 会话与截图证据预算"),
],
fromSettings: (s) => Object.fromEntries(BUDGET_KEYS.map((k) => [`storage.budgets.${k}`, n(s.storage.budgets[k])])),
toPatch: (v) => ({ storage: { budgets: pick(v, "storage.budgets", BUDGET_KEYS) } }),
};
const RETENTION_KEYS = [
"observation",
"sseEvent",
"heartbeatHours",
"taskRun",
"agentEvent",
"audit",
"capabilityStats",
"identityPoolArchive",
] as const;
const STORAGE_RETENTION_CARD: CardSpec = {
anchor: "storage-retention",
title: "流水数据保留",
description: "到期由定时清理任务分批删除。单位天,心跳为小时。",
fields: [
dayField("retentionDays", "observation", "Feed 观察"),
dayField("retentionDays", "sseEvent", "实时事件"),
dayField("retentionDays", "heartbeatHours", "进程心跳(小时)"),
dayField("retentionDays", "taskRun", "任务运行记录"),
dayField("retentionDays", "agentEvent", "Agent 会话事件", "只删已结束的会话"),
dayField("retentionDays", "audit", "审计日志", "能力调用审计除外"),
dayField("retentionDays", "capabilityStats", "能力调用统计"),
dayField(
"retentionDays",
"identityPoolArchive",
"待识别池归档",
"未命中的条目这么久没有新证据就归档,仍可手动指定",
),
],
fromSettings: (s) =>
Object.fromEntries(RETENTION_KEYS.map((k) => [`storage.retentionDays.${k}`, n(s.storage.retentionDays[k])])),
toPatch: (v) => ({ storage: { retentionDays: pick(v, "storage.retentionDays", RETENTION_KEYS) } }),
};
const TTL_KEYS = ["tmdbSearchDays", "tmdbDetailDays", "tmdbMissDays", "siteDetailDays", "webSearchDays"] as const;
const STORAGE_TTL_CARD: CardSpec = {
anchor: "storage-ttl",
title: "缓存有效期",
description: "单位天。追踪中的作品命中 TMDB 详情缓存时自动续期。",
fields: [
dayField("cacheTtl", "tmdbSearchDays", "TMDB 搜索与识别结果"),
dayField("cacheTtl", "tmdbDetailDays", "TMDB 详情"),
dayField("cacheTtl", "tmdbMissDays", "TMDB 空结果", "搜索无结果、未识别时的负缓存"),
dayField("cacheTtl", "siteDetailDays", "站点详情页内容"),
dayField("cacheTtl", "webSearchDays", "网页搜索结果"),
],
fromSettings: (s) => Object.fromEntries(TTL_KEYS.map((k) => [`storage.cacheTtl.${k}`, n(s.storage.cacheTtl[k])])),
toPatch: (v) => ({ storage: { cacheTtl: pick(v, "storage.cacheTtl", TTL_KEYS) } }),
};
function ToggleRow({
id,
icon,
@@ -181,6 +263,9 @@ export function AutomationSection() {
<SettingsCard section="automation" spec={CACHE_CARD} settings={s} />
<SettingsCard section="automation" spec={BUDGET_CARD} settings={s} />
<SettingsCard section="automation" spec={RETENTION_CARD} settings={s} />
<SettingsCard section="automation" spec={STORAGE_BUDGET_CARD} settings={s} />
<SettingsCard section="automation" spec={STORAGE_RETENTION_CARD} settings={s} />
<SettingsCard section="automation" spec={STORAGE_TTL_CARD} settings={s} />
</>
)}
</SettingsGate>