Merge branch 'cache2'

This commit is contained in:
root committed 2026-09-29 17:42:24 +08:00
commit eb9cfaa63d
32 files changed
+1927 -87

No files matched your search

+15
View File
@@ -2017,6 +2017,21 @@ export const Api = {
query: ops.SystemHealthHistoryQuery,
response: ops.SystemHealthHistory,
}),
storage: ep({
method: "get",
path: "/system/storage",
scope: "admin",
summary: "存储:表体积前 15、库总体积、各缓存用量与预算、备份占用、DATA_DIR 分目录用量、清理任务状态",
response: ops.SystemStorage,
}),
storageCleanup: ep({
method: "post",
path: "/system/storage/cleanup/{target}",
scope: "admin",
summary: "立即执行某个存储清理任务(排入对应定时任务,已在排队或运行中则合并)",
params: ops.StorageCleanupParam,
response: ops.StorageCleanupResult,
}),
},
// ── 引擎面板(仅 admin) ──
+101
View File
@@ -481,9 +481,110 @@ export const SystemHealth = z.object({
),
failedRuns24h: NonNegInt,
}),
/** 存储预算(CACHE-2):PG 库体积、图片缓存、备份、会话与证据对照 settings.storage.budgets;明细见 GET /system/storage */
storage: z.object({
level: HealthLevel,
/** 超出告警线 / 预算的项(人读文案) */
alerts: z.array(z.string()),
}),
});
export type SystemHealth = z.infer<typeof SystemHealth>;
// ── 存储预算与保留(CACHE-2;GET /system/storage、POST /system/storage/cleanup/{target},仅 admin) ──
/** 手动触发的清理目标;每个对应一个维护组定时任务 */
export const StorageCleanupTarget = z.enum([
"observation",
"sse_event",
"heartbeat",
"task_run",
"agent_event",
"audit",
"capability_stats",
"identity_pool",
"analyze",
"images",
"kv_cache",
]);
export type StorageCleanupTarget = z.infer<typeof StorageCleanupTarget>;
export const StorageCleanupParam = z.object({ target: StorageCleanupTarget });
export const StorageCleanupResult = z.object({
target: StorageCleanupTarget,
/** 对应的定时任务 key */
jobId: z.string(),
/** false = 已在排队或运行中(请求被合并) */
queued: z.boolean(),
});
export type StorageCleanupResult = z.infer<typeof StorageCleanupResult>;
/** 一项占用对照预算 */
export const StorageBudgetUsage = z.object({
usedBytes: NonNegInt.nullable(),
/** 预算(critical 线);无预算为 null */
budgetBytes: NonNegInt.nullable(),
/** 告警线;无单独告警线时为预算的 80% */
warnBytes: NonNegInt.nullable(),
level: HealthLevel,
error: z.string().nullable(),
});
export type StorageBudgetUsage = z.infer<typeof StorageBudgetUsage>;
export const SystemStorage = z.object({
/** 各项最差档(unknown 不参与) */
level: HealthLevel,
checkedAt: Timestamp,
/** 目录用量的统计时间(目录遍历结果缓存 5 分钟) */
dirsScannedAt: Timestamp.nullable(),
db: StorageBudgetUsage.extend({
/** 体积前 15 的表(含索引与 TOAST) */
tables: z.array(
z.object({
schema: z.string(),
name: z.string(),
totalBytes: NonNegInt,
tableBytes: NonNegInt,
indexBytes: NonNegInt,
/** 估算行数(pg_class.reltuples);未 ANALYZE 为 null */
rowsEstimate: NonNegInt.nullable(),
deadRows: NonNegInt.nullable(),
lastAutovacuumAt: Timestamp.nullable(),
lastAnalyzeAt: Timestamp.nullable(),
}),
),
}),
/** PG kv_cache 各 region(条数含未清理的过期行) */
kvCache: z.array(z.object({ region: z.string(), entries: NonNegInt, expired: NonNegInt, bytes: NonNegInt })),
images: StorageBudgetUsage.extend({ files: NonNegInt.nullable() }),
backups: StorageBudgetUsage.extend({ count: NonNegInt.nullable() }),
sessionsEvidence: StorageBudgetUsage,
/** DATA_DIR 下各一级目录(cache 再细分一层)的用量 */
dataDir: z.array(
z.object({
name: z.string(),
path: z.string(),
usedBytes: NonNegInt,
files: NonNegInt,
error: z.string().nullable(),
}),
),
/** 各清理任务的当前设置与最近运行 */
cleanup: z.array(
z.object({
target: StorageCleanupTarget,
jobId: z.string(),
/** 保留期说明(如 "90 天"、"24 小时"、"容量 20 GB") */
retention: z.string().nullable(),
/** 定时任务不存在(未登记 / 调度器未同步)时为 null */
enabled: z.boolean().nullable(),
lastRunAt: Timestamp.nullable(),
lastStatus: z.string().nullable(),
lastSummary: z.string().nullable(),
nextRunAt: Timestamp.nullable(),
}),
),
});
export type SystemStorage = z.infer<typeof SystemStorage>;
export const SystemHealthHistoryQuery = z.object({
/** 回看小时数,1–168,缺省 6 */
hours: z.coerce.number().int().min(1).max(168).default(6),
+125 -6
View File
@@ -24,11 +24,14 @@ const CacheShape = z.object({
checkIntervalSec: z.number().int().min(10).max(3600),
/** 缓存清理的目标使用率(低水位) */
cleanupTargetPercent: Percent,
/** TMDB 缓存(请求响应 + 识别结果)保留小时数(对照 MP META_CACHE_EXPIRE,缺省 24) */
/**
* @deprecated CACHE-2 起由 storage.cacheTtl(tmdbSearchDays / tmdbDetailDays)取代,服务端不再读取;
* 保留在 schema 里只为兼容已存的 JSON 与旧设置页。
*/
tmdbTtlHours: z.number().int().min(1).max(720),
/** TMDB 缓存每类条数上限,超出删最旧(对照 MP CONF.tmdb,缺省 256;大内存模式 1024) */
tmdbMaxSize: z.number().int().min(16).max(100_000),
/** 空结果 / 未识别的短缓存秒数(对照 MP EMPTY_RESULT_CACHE_TTL,缺省 1800) */
/** @deprecated CACHE-2 起由 storage.cacheTtl.tmdbMissDays 取代,服务端不再读取 */
emptyResultTtlSec: z.number().int().min(0).max(86_400),
/** 图片代理落盘缓存(对照 MP GLOBAL_IMAGE_CACHE,缺省关闭;关闭时仍代理,只是不落盘) */
globalImageCache: z.boolean(),
@@ -410,7 +413,7 @@ export const DEFAULT_ENRICH_DOMAINS: string[] = [
export const IdentitySettings = z.object({
eagerTmdbLookup: z.boolean(),
/** 待识别池里 unmatched 条目多少天没有新证据就归档(仍可按状态筛出、可手动指定) */
/** @deprecated CACHE-2 起由 storage.retentionDays.identityPoolArchive 取代,服务端不再读取 */
poolArchiveDays: z.number().int().min(1).max(3650),
/**
* META-1:订阅别名 / 新作品收录规则命中、TMDB 却查不到时自动尝试一次元数据补全(只出建议,不建作品)。
@@ -432,6 +435,98 @@ export const IdentitySettings = z.object({
});
export type IdentitySettings = z.infer<typeof IdentitySettings>;
// ── 存储预算与保留(CACHE-2,docs/runbook/storage.md)──
const Gb = z.number().min(0.01).max(10_000);
const Days = z.number().int().min(1).max(3650);
export const StorageBudgets = z
.object({
/** PG 数据目录预算(GB):超过 pgDataWarnGb 告警,超过本值 critical */
pgDataGb: Gb,
pgDataWarnGb: Gb,
/** 图片代理缓存容量(GB):超出按最久未访问删除(LRU) */
imagesGb: Gb,
/** DATA_DIR/backups 占用预算(GB) */
backupsGb: Gb,
/** Agent 会话 + 视觉证据(DATA_DIR/agent-sessions、tmp/evidence)预算(GB) */
sessionsEvidenceGb: Gb,
})
.refine((v) => v.pgDataWarnGb <= v.pgDataGb, { message: "PG 告警线不得高于预算" });
export type StorageBudgets = z.infer<typeof StorageBudgets>;
/** 各类流水数据保留期(到期由 storage.retention.* 定时任务分批删除) */
export const StorageRetentionDays = z.object({
/** observation(Feed 观察) */
observation: Days,
/** sse_event(SSE 补读) */
sseEvent: Days,
/** process_heartbeat_sample(小时) */
heartbeatHours: z
.number()
.int()
.min(1)
.max(24 * 90),
/** 已结束的业务任务 task 与引擎 run */
taskRun: Days,
/** 已结束 Agent 会话的 agent_run_event / agent_run_message */
agentEvent: Days,
/** audit_event(能力调用审计除外) */
audit: Days,
/** 能力调用统计:audit_event(target_type=capability)与已结束的 capability_invocation */
capabilityStats: Days,
/** 待识别池 unmatched 条目无新证据多少天后归档 */
identityPoolArchive: Days,
});
export type StorageRetentionDays = z.infer<typeof StorageRetentionDays>;
/** 缓存有效期(天):只改 TTL 的取值来源,缓存语义仍对照 MP(docs/porting/moviepilot-tmdb-cache.md) */
export const StorageCacheTtl = z.object({
/** TMDB 搜索 / find 响应与识别结果 */
tmdbSearchDays: Days,
/** TMDB 详情(/movie/{id}、/tv/{id}、季);追踪中的作品命中时续期 */
tmdbDetailDays: Days,
/** TMDB 空结果与未识别负缓存(MP EMPTY_RESULT_CACHE_TTL) */
tmdbMissDays: Days,
/** 站点详情页扩展内容(kv_cache site.detail) */
siteDetailDays: Days,
/** 网页搜索结果 */
webSearchDays: Days,
});
export type StorageCacheTtl = z.infer<typeof StorageCacheTtl>;
export const StorageSettings = z.object({
budgets: StorageBudgets,
retentionDays: StorageRetentionDays,
cacheTtl: StorageCacheTtl,
});
export type StorageSettings = z.infer<typeof StorageSettings>;
export const DEFAULT_STORAGE_SETTINGS: StorageSettings = {
budgets: { pgDataGb: 25, pgDataWarnGb: 20, imagesGb: 20, backupsGb: 20, sessionsEvidenceGb: 10 },
retentionDays: {
observation: 90,
sseEvent: 7,
heartbeatHours: 24,
taskRun: 60,
agentEvent: 90,
audit: 180,
capabilityStats: 90,
identityPoolArchive: 30,
},
cacheTtl: { tmdbSearchDays: 7, tmdbDetailDays: 30, tmdbMissDays: 1, siteDetailDays: 7, webSearchDays: 1 },
};
/** 存储设置与缺省逐层合并(旧库里缺子字段时补齐) */
export function withStorageDefaults(v: unknown): StorageSettings {
const o = (v && typeof v === "object" ? v : {}) as Partial<Record<keyof StorageSettings, object>>;
return {
budgets: { ...DEFAULT_STORAGE_SETTINGS.budgets, ...(o.budgets ?? {}) },
retentionDays: { ...DEFAULT_STORAGE_SETTINGS.retentionDays, ...(o.retentionDays ?? {}) },
cacheTtl: { ...DEFAULT_STORAGE_SETTINGS.cacheTtl, ...(o.cacheTtl ?? {}) },
} as StorageSettings;
}
export const GlobalSettings = z.object({
automation: AutomationSettings,
cache: CacheSettings,
@@ -445,6 +540,7 @@ export const GlobalSettings = z.object({
naming: NamingSettings,
identity: IdentitySettings,
search: SearchSettings,
storage: StorageSettings,
});
export type GlobalSettings = z.infer<typeof GlobalSettings>;
@@ -526,6 +622,7 @@ export const DEFAULT_SETTINGS: GlobalSettings = {
enrichDomains: DEFAULT_ENRICH_DOMAINS,
},
search: { defaultSiteMode: "default", siteIds: [] },
storage: DEFAULT_STORAGE_SETTINGS,
};
/** PATCH /settings:按分组部分更新;服务端与现值合并后用 GlobalSettings 整体再校验 */
@@ -576,6 +673,13 @@ export const GlobalSettingsUpdate = z.object({
naming: NamingSettings.partial().optional(),
identity: IdentitySettings.partial().optional(),
search: SearchSettings.partial().optional(),
storage: z
.object({
budgets: z.object(StorageBudgets.shape).partial().optional(),
retentionDays: StorageRetentionDays.partial().optional(),
cacheTtl: StorageCacheTtl.partial().optional(),
})
.optional(),
});
export type GlobalSettingsUpdate = z.infer<typeof GlobalSettingsUpdate>;
@@ -602,6 +706,16 @@ export function mergeSettings(current: GlobalSettings, patch: GlobalSettingsUpda
naming: { ...(current.naming ?? DEFAULT_SETTINGS.naming), ...patch.naming },
identity: { ...(current.identity ?? DEFAULT_SETTINGS.identity), ...patch.identity },
search: { ...(current.search ?? DEFAULT_SETTINGS.search), ...patch.search },
storage: mergeStorage(current.storage, patch.storage),
};
}
function mergeStorage(current: StorageSettings | undefined, patch: GlobalSettingsUpdate["storage"]): StorageSettings {
const base = withStorageDefaults(current);
return {
budgets: { ...base.budgets, ...patch?.budgets },
retentionDays: { ...base.retentionDays, ...patch?.retentionDays },
cacheTtl: { ...base.cacheTtl, ...patch?.cacheTtl },
};
}
@@ -638,6 +752,10 @@ export const DEPRECATED_SETTINGS_FIELDS = [
"pi.model",
"pi.thinkingLevel",
"pi.apiKeySecretRef",
// CACHE-2:由 storage.cacheTtl / storage.retentionDays 取代
"cache.tmdbTtlHours",
"cache.emptyResultTtlSec",
"identity.poolArchiveDays",
] as const;
// ── 设置页分区(docs/design/console-ia.md §3.4) ──
@@ -682,6 +800,7 @@ export const SETTINGS_SECTIONS: readonly SettingsSectionDef[] = [
{ id: "cache", label: "缓存水位" },
{ id: "budget", label: "每日预算" },
{ id: "retention", label: "保留" },
{ id: "storage", label: "存储与保留" },
],
fields: [
"automation.enabled",
@@ -695,6 +814,9 @@ export const SETTINGS_SECTIONS: readonly SettingsSectionDef[] = [
"budget.maxConcurrentDownloads",
"retention.seedRetentionDays",
"retention.logRetentionDays",
"storage.budgets",
"storage.retentionDays",
"storage.cacheTtl",
],
resources: [],
},
@@ -774,11 +896,8 @@ export const SETTINGS_SECTIONS: readonly SettingsSectionDef[] = [
fields: [
"metadata.tmdbApiKeySecretRef",
"metadata.tmdbBaseUrl",
"cache.tmdbTtlHours",
"cache.tmdbMaxSize",
"cache.emptyResultTtlSec",
"identity.eagerTmdbLookup",
"identity.poolArchiveDays",
"identity.autoEnrich",
"identity.enrichDailyBudget",
"identity.enrichDomains",