feat(cache): PG kv_cache ported from MP TTLCache, TMDB request and recognize caches

Migration 0042_cache adds UNLOGGED kv_cache. PgCache mirrors MP
runtime/cache.py (region, per-key TTL, TLRU maxsize eviction, @cached as
cachedCall); only the storage moves from memory to PG. TMDB request
responses (MP TMDb.request @cached) and recognize results (MP TmdbCache:
same key, {id:0} negative cache with EMPTY_RESULT_CACHE_TTL, type-conflict
checks, backfill after manual assignment) live in kv_cache, shared by all
processes. Settings cache.tmdbTtlHours / tmdbMaxSize / emptyResultTtlSec
map to META_CACHE_EXPIRE / CONF.tmdb / EMPTY_RESULT_CACHE_TTL. TMDB
details now also store posterPath / backdropPath.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-28 22:26:29 +08:00
1 parent 61f585a707
commit 946fca5e55
22 files changed
+743 -14

No files matched your search

+1
View File
@@ -189,6 +189,7 @@ export const EntityMetadataField = z.enum([
"originalLanguage",
"originCountry",
"posterPath",
"backdropPath",
]);
export type EntityMetadataField = z.infer<typeof EntityMetadataField>;
+2
View File
@@ -52,6 +52,8 @@ export const TmdbCandidate = z.object({
year: z.number().int().nullable(),
overview: z.string().nullable(),
popularity: z.number().nullable(),
/** TMDB poster_path;前端经图片代理显示海报 */
posterPath: z.string().nullable().optional(),
/** 已有本地作品接受了这个 TMDB 身份时给出(跟随合并跳转),前端显示"查看"而不是"订阅" */
localEntityId: Id.nullable(),
});
+36 -4
View File
@@ -24,6 +24,16 @@ const CacheShape = z.object({
checkIntervalSec: z.number().int().min(10).max(3600),
/** 缓存清理的目标使用率(低水位) */
cleanupTargetPercent: Percent,
/** TMDB 缓存(请求响应 + 识别结果)保留小时数(对照 MP META_CACHE_EXPIRE,缺省 24) */
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) */
emptyResultTtlSec: z.number().int().min(0).max(86_400),
/** 图片代理落盘缓存(对照 MP GLOBAL_IMAGE_CACHE,缺省关闭;关闭时仍代理,只是不落盘) */
globalImageCache: z.boolean(),
/** 图片缓存保留天数,超过即清理(对照 MP GLOBAL_IMAGE_CACHE_DAYS,缺省 7) */
globalImageCacheDays: z.number().int().min(1).max(3650),
});
/** 下载缓存卷硬阈值与恢复水位(REQ §8.4.1) */
export const CacheSettings = CacheShape.refine((v) => v.resumePercent < v.hardThresholdPercent, {
@@ -330,7 +340,17 @@ export type SettingsKey = z.infer<typeof SettingsKey>;
/** 缺省设置(settings 表缺 key 或缺字段时使用) */
export const DEFAULT_SETTINGS: GlobalSettings = {
automation: { enabled: true, readOnlyReconcile: false },
cache: { hardThresholdPercent: 90, resumePercent: 85, checkIntervalSec: 60, cleanupTargetPercent: 80 },
cache: {
hardThresholdPercent: 90,
resumePercent: 85,
checkIntervalSec: 60,
cleanupTargetPercent: 80,
tmdbTtlHours: 24,
tmdbMaxSize: 256,
emptyResultTtlSec: 1800,
globalImageCache: false,
globalImageCacheDays: 7,
},
organize: { defaultMode: "copy", allowCrossVolumeMove: true, minSizeBytes: 50 * 1024 * 1024 },
retention: { seedRetentionDays: 7, logRetentionDays: 180 },
pi: {
@@ -587,10 +607,22 @@ export const SETTINGS_SECTIONS: readonly SettingsSectionDef[] = [
{
id: "metadata",
label: "元数据",
description: "TMDB API Key 与地址",
description: "TMDB API Key 与地址、TMDB 缓存、图片代理",
legacyTabs: [],
anchors: [{ id: "tmdb", label: "TMDB" }],
fields: ["metadata.tmdbApiKeySecretRef", "metadata.tmdbBaseUrl"],
anchors: [
{ id: "tmdb", label: "TMDB" },
{ id: "tmdb-cache", label: "TMDB 缓存" },
{ id: "images", label: "图片代理与缓存" },
],
fields: [
"metadata.tmdbApiKeySecretRef",
"metadata.tmdbBaseUrl",
"cache.tmdbTtlHours",
"cache.tmdbMaxSize",
"cache.emptyResultTtlSec",
"cache.globalImageCache",
"cache.globalImageCacheDays",
],
resources: [],
},
{
+3
View File
@@ -130,6 +130,9 @@ const INTERNAL_TABLES = [
"SseEvent",
// W9-C 命名模板版本登记(0032):服务端内部,经 /naming/* 暴露
"NamingTemplateVersion",
// C2-A 跨进程限流桶(0041)与 PG 缓存(0042):服务端内部,缓存经 /cache/* 的专用 DTO 暴露
"RateLimitBucket",
"KvCache",
// W9-I 进程心跳(0034):服务端内部,经 /system/health 的专用 DTO 暴露
"ProcessHeartbeat",
"ProcessHeartbeatSample",