Files
UHD_Media_Tool/packages/contracts/src/cache.ts
T
rootandClaude Opus 5.5 b7460d5d0a feat(cache): 通用缓存按区统计与清空接口
GET /cache/regions 列出 kv_cache 各区的未过期条数、已过期条数、占用字节、最近写入;
POST /cache/regions/{region}/clear 清空一个区(admin)。contracts 加区名中文映射
cacheRegionLabel,web 加对应 hooks。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
2026-10-06 18:01:19 +08:00

134 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { z } from "zod";
import { NonNegInt, Timestamp } from "./common.ts";
// C2-A 缓存管理(docs/runbook/cache.md):TMDB 缓存(请求响应 + 识别结果,对照 MP TmdbCache)、图片代理缓存。
export const TmdbCacheKind = z.enum(["request", "recognize"]);
export type TmdbCacheKind = z.infer<typeof TmdbCacheKind>;
/** 一条 TMDB 缓存(对照 MP list_items:识别条目带 tmdbId / 标题 / 年份 / 类型 / 海报) */
export const TmdbCacheItem = z.object({
kind: TmdbCacheKind,
key: z.string(),
expiresAt: Timestamp,
updatedAt: Timestamp,
/** jsonb 存储字节数 */
bytes: NonNegInt,
/** 识别条目:TMDB id,0 = 负缓存(未识别) */
tmdbId: z.number().int().nullable(),
title: z.string().nullable(),
year: z.string().nullable(),
mediaType: z.enum(["movie", "tv", "unknown"]).nullable(),
posterPath: z.string().nullable(),
});
export type TmdbCacheItem = z.infer<typeof TmdbCacheItem>;
export const TmdbCacheQuery = z.object({
kind: TmdbCacheKind.optional(),
limit: z.coerce.number().int().min(1).max(5000).default(500),
});
export const TmdbCacheList = z.object({
items: z.array(TmdbCacheItem),
counts: z.object({ request: NonNegInt, recognize: NonNegInt }),
/** 每类条数上限(cache.tmdbMaxSize) */
maxSize: NonNegInt,
ttlHours: NonNegInt,
emptyResultTtlSec: NonNegInt,
});
export type TmdbCacheList = z.infer<typeof TmdbCacheList>;
export const CacheClearResult = z.object({ deleted: NonNegInt });
export type CacheClearResult = z.infer<typeof CacheClearResult>;
/** 图片代理缓存用量 */
export const ImageCacheStatus = z.object({
/** cache.globalImageCache */
enabled: z.boolean(),
/** cache.globalImageCacheDays */
days: NonNegInt,
usedBytes: NonNegInt,
files: NonNegInt,
dir: z.string(),
});
export type ImageCacheStatus = z.infer<typeof ImageCacheStatus>;
/** TMDB 图片尺寸白名单(与 TMDB configuration.images 一致) */
export const TMDB_IMAGE_SIZES = [
"w92",
"w154",
"w185",
"w300",
"w342",
"w500",
"w780",
"w1280",
"h632",
"original",
] as const;
export type TmdbImageSize = (typeof TMDB_IMAGE_SIZES)[number];
/** 海报 / 背景图路径 → 图片代理 URL(相对 API_PREFIX 之外的完整路径,前端直接用作 img src) */
export function tmdbImageUrl(path: string | null | undefined, size: TmdbImageSize = "w342"): string | null {
if (!path) return null;
const p = path.startsWith("/") ? path.slice(1) : path;
if (!/^[A-Za-z0-9_-]+\.(jpg|jpeg|png|webp)$/.test(p)) return null;
return `/api/v1/img/tmdb/${size}/${p}`;
}
// ── 通用缓存(kv_cache)按 region 管理(设置 → 缓存) ──
/** region 名:小写字母、数字、`.`、`_`、`-` */
export const CacheRegionName = z.string().regex(/^[a-z0-9][a-z0-9._-]{0,63}$/, "缓存区名不合法");
export const CacheRegionParam = z.object({ region: CacheRegionName });
export const CacheRegionStat = z.object({
region: z.string(),
/** 未过期条目数 */
entries: NonNegInt,
/** 已过期、等清理任务删除的条目数 */
expired: NonNegInt,
/** 键与值占用字节(pg_column_size 之和,含已过期) */
bytes: NonNegInt,
/** 最近一次写入 */
lastWriteAt: Timestamp.nullable(),
});
export type CacheRegionStat = z.infer<typeof CacheRegionStat>;
export const CacheRegionList = z.object({ items: z.array(CacheRegionStat) });
export type CacheRegionList = z.infer<typeof CacheRegionList>;
/** region → 界面名;精确匹配优先,其次按前缀(`bangumi.*`);都不中返回 null(界面原样显示 region) */
const CACHE_REGION_LABELS: Record<string, string> = {
"tmdb.request": "TMDB 请求",
"tmdb.recognize": "TMDB 识别",
"douban.request": "豆瓣请求",
"douban.subject": "豆瓣条目",
"bangumi.request": "Bangumi 请求",
"bangumi.subject": "Bangumi 条目",
"meta.subject_miss": "豆瓣 / Bangumi 查无结果",
"discover.list": "发现列表",
"site.detail": "站点详情页",
"identity.detail_ids": "识别抓详情页计数",
enrich: "元数据补全",
"enrich.robots": "元数据补全 robots.txt",
websearch: "网页搜索",
"websearch.allow": "可读取的网页",
images: "图片",
};
const CACHE_REGION_PREFIX_LABELS: [string, string][] = [
["tmdb.", "TMDB"],
["douban.", "豆瓣"],
["bangumi.", "Bangumi"],
["discover.", "发现"],
["site.", "站点"],
["enrich.", "元数据补全"],
["websearch.", "网页搜索"],
];
export function cacheRegionLabel(region: string): string | null {
const exact = CACHE_REGION_LABELS[region];
if (exact) return exact;
const hit = CACHE_REGION_PREFIX_LABELS.find(([p]) => region.startsWith(p));
return hit ? `${hit[1]}(${region.slice(hit[0].length)})` : null;
}