- 左栏三组分区 + 当前分区的节;窄屏为搜索框、分组下拉和节的横排胶囊 - 搜索框按分区、节、节别名和字段名定位,选中字段时滚到该字段并聚焦 - 旧 hash(#automation/cache、#api-keys、#library/scrape 等)replace 到新位置 - 下载器分区放下载盘水位与保种缺省值,名字与实例抽屉统一(暂停线 / 恢复线 / 保种天数) - 新增获取、整理与刮削、系统、缓存分区;API Key 并入账户与安全,快捷键移到界面 - 补上缺的入口:最小视频体积、子会话并发与层数、搜索站点;日志保留天数(已废弃)不再显示 - 缓存分区按区列出条数、占用、最近写入,单区清空二次确认;TMDB 与图片缓存并入 - 保存提示统一为「已保存「节名」」;按入库口径把相关文案改成「整理」 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
267 lines
9.0 KiB
TypeScript
267 lines
9.0 KiB
TypeScript
import { cacheRegionLabel } from "@umt/contracts";
|
|
import { useState } from "react";
|
|
import { toast } from "sonner";
|
|
import {
|
|
useCacheRegions,
|
|
useClearCacheRegion,
|
|
useClearImageCache,
|
|
useClearTmdbCache,
|
|
useImageCache,
|
|
useTmdbCache,
|
|
} from "~/api/cache.ts";
|
|
import { ConfirmDialog } from "~/components/console/confirm-dialog.tsx";
|
|
import { type DataColumn, DataTable } from "~/components/console/data-table.tsx";
|
|
import { Button } from "~/components/ui/button.tsx";
|
|
import { errorMessage } from "~/lib/api.ts";
|
|
import { formatRelative } from "~/lib/labels.ts";
|
|
import { formatBytes } from "~/lib/utils.ts";
|
|
import { num } from "./resource-section.tsx";
|
|
import { SettingsBlock } from "./settings-block.tsx";
|
|
import { type CardSpec, n, SettingsCard, SettingsGate } from "./settings-card.tsx";
|
|
|
|
// 缓存分区:通用缓存(kv_cache)按区统计与清空 + 图片缓存(磁盘)同表展示;TMDB 缓存条数上限、图片缓存开关、各类有效期。
|
|
// 清空只删缓存,下次用到时重新请求;不会删作品、资源或文件。
|
|
|
|
/** 图片缓存在磁盘上,不在 kv_cache 里;表里用这个保留名占一行 */
|
|
const IMAGES_ROW = "images";
|
|
|
|
export type CacheRow = {
|
|
region: string;
|
|
label: string;
|
|
entries: number;
|
|
expired: number;
|
|
bytes: number;
|
|
lastWriteAt: string | null;
|
|
};
|
|
|
|
/** 区名 → 行:有中文名的用中文名,没有的原样显示 region */
|
|
export function cacheRow(r: Omit<CacheRow, "label">): CacheRow {
|
|
return { ...r, label: cacheRegionLabel(r.region) ?? r.region };
|
|
}
|
|
|
|
function CacheRegionsBlock() {
|
|
const regions = useCacheRegions();
|
|
const images = useImageCache();
|
|
const clearRegion = useClearCacheRegion();
|
|
const clearImages = useClearImageCache();
|
|
const [target, setTarget] = useState<CacheRow | null>(null);
|
|
|
|
const rows: CacheRow[] | undefined = regions.data
|
|
? [
|
|
...regions.data.items.map(cacheRow),
|
|
...(images.data
|
|
? [
|
|
cacheRow({
|
|
region: IMAGES_ROW,
|
|
entries: images.data.files,
|
|
expired: 0,
|
|
bytes: images.data.usedBytes,
|
|
lastWriteAt: null,
|
|
}),
|
|
]
|
|
: []),
|
|
]
|
|
: undefined;
|
|
|
|
const columns: DataColumn<CacheRow>[] = [
|
|
{
|
|
key: "label",
|
|
header: "缓存",
|
|
cell: (r) => (
|
|
<div className="min-w-0">
|
|
<p className="font-medium">{r.label}</p>
|
|
{r.label !== r.region ? <p className="font-mono text-meta text-muted-foreground">{r.region}</p> : null}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
key: "entries",
|
|
header: "条数",
|
|
align: "right",
|
|
cell: (r) => (
|
|
<div className="tabular-nums">
|
|
{r.entries}
|
|
{r.expired > 0 ? <p className="text-meta text-muted-foreground">另有 {r.expired} 条已过期</p> : null}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
key: "bytes",
|
|
header: "占用",
|
|
align: "right",
|
|
cell: (r) => <span className="tabular-nums">{formatBytes(r.bytes, 1)}</span>,
|
|
},
|
|
{
|
|
key: "last",
|
|
header: "最近写入",
|
|
hideBelow: "md",
|
|
cell: (r) => <span className="text-muted-foreground">{formatRelative(r.lastWriteAt)}</span>,
|
|
},
|
|
{
|
|
key: "actions",
|
|
header: <span className="sr-only">操作</span>,
|
|
align: "right",
|
|
cell: (r) => (
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
className="hover:text-destructive"
|
|
disabled={r.entries + r.expired === 0}
|
|
onClick={() => setTarget(r)}
|
|
>
|
|
清空
|
|
</Button>
|
|
),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<SettingsBlock
|
|
section="cache"
|
|
anchor="regions"
|
|
title="缓存区"
|
|
description="清空后下次用到时重新请求,不影响作品、资源和文件。已过期的条目由定时任务清理。"
|
|
plain
|
|
>
|
|
<DataTable<CacheRow>
|
|
rows={rows}
|
|
rowKey={(r) => r.region}
|
|
loading={regions.isPending}
|
|
error={regions.error}
|
|
onRetry={() => regions.refetch()}
|
|
empty={{ title: "还没有缓存", description: "用到 TMDB、豆瓣、站点详情等数据后会出现在这里。" }}
|
|
columns={columns}
|
|
/>
|
|
<ConfirmDialog
|
|
open={!!target}
|
|
onOpenChange={(o) => !o && setTarget(null)}
|
|
title={`清空「${target?.label ?? ""}」?`}
|
|
description={`删除 ${target ? target.entries + target.expired : 0} 条缓存,之后用到时重新请求。`}
|
|
confirmLabel="清空"
|
|
destructive
|
|
onConfirm={async () => {
|
|
if (!target) return;
|
|
try {
|
|
const r =
|
|
target.region === IMAGES_ROW
|
|
? await clearImages.mutateAsync()
|
|
: await clearRegion.mutateAsync(target.region);
|
|
toast.success(`已清空「${target.label}」,删除 ${r.deleted} 条`);
|
|
setTarget(null);
|
|
} catch (err) {
|
|
toast.error(errorMessage(err, "清空失败"));
|
|
throw err;
|
|
}
|
|
}}
|
|
/>
|
|
</SettingsBlock>
|
|
);
|
|
}
|
|
|
|
// TMDB 缓存(对照 MP:CONF.tmdb 条数;有效期在下面「缓存有效期」)
|
|
export const TMDB_CACHE_CARD: CardSpec = {
|
|
anchor: "tmdb-cache",
|
|
title: "TMDB 缓存",
|
|
description: "请求响应与识别结果存在数据库里,三个进程共用。超过条数上限时删最久未用的条目。",
|
|
fields: [{ name: "cache.tmdbMaxSize", label: "条数上限", kind: "number", hint: "请求缓存和识别缓存各自计数" }],
|
|
fromSettings: (s) => ({ "cache.tmdbMaxSize": n(s.cache.tmdbMaxSize) }),
|
|
toPatch: (v) => ({ cache: { tmdbMaxSize: num(v["cache.tmdbMaxSize"]) } }),
|
|
extra: () => <TmdbCacheStatus />,
|
|
};
|
|
|
|
function TmdbCacheStatus() {
|
|
const q = useTmdbCache();
|
|
const clear = useClearTmdbCache();
|
|
const c = q.data?.counts;
|
|
return (
|
|
<div className="mb-2 flex flex-wrap items-center gap-2 text-meta text-muted-foreground">
|
|
<span className="tabular-nums">
|
|
{c ? `识别 ${c.recognize} 条 · 请求 ${c.request} 条 · 上限各 ${q.data?.maxSize} 条` : "读取中…"}
|
|
</span>
|
|
<ConfirmDialog
|
|
trigger={
|
|
<Button type="button" variant="ghost" size="xs" className="hover:text-destructive">
|
|
清空
|
|
</Button>
|
|
}
|
|
title="清空 TMDB 缓存?"
|
|
description="请求响应和识别结果一起删除,之后用到时重新请求 TMDB。"
|
|
confirmLabel="清空"
|
|
destructive
|
|
onConfirm={async () => {
|
|
try {
|
|
const r = await clear.mutateAsync();
|
|
toast.success(`已清空 ${r.deleted} 条 TMDB 缓存`);
|
|
} catch (err) {
|
|
toast.error(errorMessage(err, "清空失败"));
|
|
throw err;
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 图片代理与缓存(对照 MP GLOBAL_IMAGE_CACHE / GLOBAL_IMAGE_CACHE_DAYS);用量与清空在上面的缓存区表里
|
|
export const IMAGES_CARD: CardSpec = {
|
|
anchor: "images",
|
|
title: "图片缓存",
|
|
description: "海报经服务端从 TMDB 取回。开启缓存后保存到数据目录,下次直接读取;容量上限在「系统 → 存储预算」。",
|
|
fields: [
|
|
{ name: "cache.globalImageCache", label: "缓存图片", kind: "switch" },
|
|
{ name: "cache.globalImageCacheDays", label: "保留天数", kind: "number", hint: "超过天数的图片每天清理一次" },
|
|
],
|
|
fromSettings: (s) => ({
|
|
"cache.globalImageCache": s.cache.globalImageCache,
|
|
"cache.globalImageCacheDays": n(s.cache.globalImageCacheDays),
|
|
}),
|
|
toPatch: (v) => ({
|
|
cache: {
|
|
globalImageCache: v["cache.globalImageCache"] === true,
|
|
globalImageCacheDays: num(v["cache.globalImageCacheDays"]),
|
|
},
|
|
}),
|
|
};
|
|
|
|
const TTL_KEYS = ["tmdbSearchDays", "tmdbDetailDays", "tmdbMissDays", "siteDetailDays", "webSearchDays"] as const;
|
|
const ttlField = (k: string, label: string, hint?: string) => ({
|
|
name: `storage.cacheTtl.${k}`,
|
|
label,
|
|
kind: "number" as const,
|
|
hint,
|
|
});
|
|
export const TTL_CARD: CardSpec = {
|
|
anchor: "ttl",
|
|
title: "缓存有效期",
|
|
description: "单位天。追踪中的作品命中 TMDB 详情缓存时自动续期。",
|
|
fields: [
|
|
ttlField("tmdbSearchDays", "TMDB 搜索与识别结果"),
|
|
ttlField("tmdbDetailDays", "TMDB 详情"),
|
|
ttlField("tmdbMissDays", "TMDB 空结果", "搜索无结果、未识别时的负缓存"),
|
|
ttlField("siteDetailDays", "站点详情页内容"),
|
|
ttlField("webSearchDays", "网页搜索结果"),
|
|
],
|
|
fromSettings: (s) => Object.fromEntries(TTL_KEYS.map((k) => [`storage.cacheTtl.${k}`, n(s.storage.cacheTtl[k])])),
|
|
toPatch: (v) => ({
|
|
storage: { cacheTtl: Object.fromEntries(TTL_KEYS.map((k) => [k, num(v[`storage.cacheTtl.${k}`])])) },
|
|
}),
|
|
};
|
|
|
|
export function CacheSection() {
|
|
return (
|
|
<div className="grid grid-cols-1 gap-8">
|
|
<CacheRegionsBlock />
|
|
<SettingsGate>
|
|
{(s) => (
|
|
<>
|
|
<SettingsCard section="cache" spec={TMDB_CACHE_CARD} settings={s} />
|
|
<SettingsCard section="cache" spec={IMAGES_CARD} settings={s} />
|
|
<SettingsCard section="cache" spec={TTL_CARD} settings={s} />
|
|
</>
|
|
)}
|
|
</SettingsGate>
|
|
</div>
|
|
);
|
|
}
|