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
134 lines
4.6 KiB
TypeScript
134 lines
4.6 KiB
TypeScript
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;
|
||
}
|