feat(settings): storage budgets, retention days and cache TTL settings (CACHE-2)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
c3aaf901ad
commit
58e358e4a9
2 files changed
+139
-8
No files matched your search
@@ -1,5 +1,11 @@
|
||||
import { createCipheriv, createDecipheriv, createHmac, randomBytes, randomUUID } from "node:crypto";
|
||||
import { DEFAULT_SETTINGS, GlobalSettings, type SecretPurpose, type SettingsKey } from "@umt/contracts";
|
||||
import {
|
||||
DEFAULT_SETTINGS,
|
||||
GlobalSettings,
|
||||
type SecretPurpose,
|
||||
type SettingsKey,
|
||||
withStorageDefaults,
|
||||
} from "@umt/contracts";
|
||||
import type { DB, Kysely } from "@umt/db";
|
||||
import type { Logger } from "pino";
|
||||
import type { EventBusPort } from "../../http/sse.ts";
|
||||
@@ -335,7 +341,13 @@ export function createSettingsReader(deps: {
|
||||
for (const key of Object.keys(DEFAULT_SETTINGS) as SettingsKey[]) {
|
||||
const def = DEFAULT_SETTINGS[key];
|
||||
const v = stored.get(key);
|
||||
const merged = v && typeof v === "object" && !Array.isArray(v) ? { ...def, ...(v as object) } : def;
|
||||
// storage 是两层结构:逐层补缺省,旧库里缺子字段也能整体通过校验
|
||||
const merged =
|
||||
key === "storage"
|
||||
? withStorageDefaults(v)
|
||||
: v && typeof v === "object" && !Array.isArray(v)
|
||||
? { ...def, ...(v as object) }
|
||||
: def;
|
||||
const parsed = GlobalSettings.shape[key].safeParse(merged);
|
||||
if (!parsed.success) deps.logger.warn({ key }, "settings 值不合法,回退缺省");
|
||||
out[key] = parsed.success ? parsed.data : def;
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user