Merge branch 'cache2'
This commit is contained in:
commit
eb9cfaa63d
32 files changed
+1927
-87
No files matched your search
@@ -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) ──
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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