feat(cache): TMDB / 豆瓣 / Bangumi 接入统一元数据缓存与策略表

- 策略表「来源 × 资源类型」(metaCachePolicy):搜索 / 识别 7 天(空 1 天)、详情 / 季 / 剧集组 30 天(空 1 天)、
  榜单 / 热门 / 每日放送 6 小时(空 1 小时)、人物 / 图片 90 天;取值来自 storage.cacheTtl,
  追加 tmdbListHours / tmdbListEmptyHours / imageDays / metaDetailDays / metaMissDays,metaSearchDays 缺省改 7 天
- TMDB 客户端共享缓存端口加 fetch(带请求类别与空结果判定),服务端整条读取交给 MetadataCache;
  识别缓存、douban.request / bangumi.request、douban.subject / bangumi.subject、meta.subject_miss、discover.match 都走这一层
- 豆瓣 / bgm 条目只在「不存在」时记查无,网络失败 / 被拦不缓存(本进程冷却 30 分钟)
- 关注中作品(isEntityWatched 口径,含选中的剧集组)的 TMDB 条目写入 pinned、命中续期
- 各进程启动 LISTEN 失效通知并每分钟把命中计数写 cache_stat;GET /cache/regions 给元数据 region 带近 7 天命中统计

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-06 19:37:34 +08:00
1 parent ad472a1a19
commit 1dca95f883
16 files changed
+687 -137

No files matched your search

+2 -2
View File
@@ -218,13 +218,13 @@ describe.skipIf(!up)("发现页接口", () => {
expect(meta.filter((m) => m.includes("movie_showing"))).toHaveLength(3);
expect(meta.at(-1)).toContain("start=20&count=20");
// 空结果:写入但用空结果 TTL(缺省 1 小时),正常 6 小时
// 空结果:写入但用空结果 TTL(缺省 1 小时),正常 6 小时(看新鲜期;过期后还有 50% 宽限期先用旧值)
doubanReply = () => json({ subject_collection_items: [], total: 0 });
await req("/discover/douban?list=tv_korean");
const rows = await cache.items(META_REQUEST_REGIONS.douban);
const ttlOf = (part: string) => {
const row = rows.find((x) => x.key.includes(part))!;
return Math.round((new Date(row.expiresAt!).getTime() - Date.now()) / 60_000);
return Math.round((new Date(row.freshUntil).getTime() - Date.now()) / 60_000);
};
expect(ttlOf("tv_korean")).toBeLessThanOrEqual(60);
expect(ttlOf("tv_korean")).toBeGreaterThan(55);