feat(downloads): 下载器单独的水位与保种天数

- contracts DownloaderPolicy(存 downloader_instance.settings.policy):硬阈值 / 恢复水位 / 清理目标 / 保种天数,
  不填用全局 cache / retention
- volumeThresholds:卷的生效水位 = 各实例「自己的值 ?? 全局」取最小;decideVolume、阈值清理、手动恢复、
  首页卷信息、定时清理检查都改用它;cleanupCandidates 保种天数按实例
- 保存时校验合并全局后 恢复 < 硬阈值、清理目标 ≤ 恢复
- 下载器设置表单新增四项(留空用全局)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-02 20:51:04 +08:00
1 parent ff79554a09
commit ee128e0b3f
9 files changed
+158 -14

No files matched your search

@@ -90,6 +90,26 @@ export function DownloadersSection() {
);
}
/** 下载器自己的水位 / 保种(settings.policy):表单里是字符串,空 = 用全局 */
const POLICY_FIELDS = [
["hardPct", "hardThresholdPercent"],
["resumePct", "resumePercent"],
["targetPct", "cleanupTargetPercent"],
["seedDays", "seedRetentionDays"],
] as const;
function policyForm(r: DownloaderInstance): Record<string, string> {
const p = ((r.settings as { policy?: Record<string, unknown> }).policy ?? {}) as Record<string, unknown>;
return Object.fromEntries(POLICY_FIELDS.map(([f, k]) => [f, typeof p[k] === "number" ? String(p[k]) : ""]));
}
function policyBody(v: FormValues): Record<string, number> {
const out: Record<string, number> = {};
for (const [f, k] of POLICY_FIELDS) {
const raw = String(v[f] ?? "").trim();
if (raw !== "" && Number.isFinite(Number(raw))) out[k] = Number(raw);
}
return out;
}
function DownloadersTable() {
const list = downloaders.useList();
const create = downloaders.useCreate();
@@ -166,6 +186,22 @@ function DownloadersTable() {
},
{ name: "managedTag", label: "管理标签", kind: "text", placeholder: "umt", hint: "只接管带此标签的任务" },
{ name: "cacheVolumeKey", label: "缓存卷标识", kind: "text", placeholder: "cache-1" },
{
name: "hardPct",
label: "硬阈值(%)",
kind: "number",
placeholder: "留空用全局",
hint: "这个下载器的缓存盘用到多少就停止新增下载;多个下载器共用一块盘时取最严格的",
},
{ name: "resumePct", label: "恢复水位(%)", kind: "number", placeholder: "留空用全局" },
{ name: "targetPct", label: "清理目标(%)", kind: "number", placeholder: "留空用全局" },
{
name: "seedDays",
label: "保种天数",
kind: "number",
placeholder: "留空用全局",
hint: "入库后至少保留几天再允许清理;公网 BT 可填 0(站点 H&R 要求仍然遵守)",
},
{
name: "isDefault",
label: "设为默认下载器",
@@ -183,6 +219,10 @@ function DownloadersTable() {
apiKey: "",
managedTag: "umt",
cacheVolumeKey: "",
hardPct: "",
resumePct: "",
targetPct: "",
seedDays: "",
isDefault: false,
enabled: true,
},
@@ -197,6 +237,7 @@ function DownloadersTable() {
"secret:authKind": authKindOf(r),
managedTag: r.managedTag,
cacheVolumeKey: r.cacheVolumeKey ?? "",
...policyForm(r),
isDefault: r.isDefault,
enabled: r.enabled,
}),
@@ -214,6 +255,7 @@ function DownloadersTable() {
: undefined,
managedTag: optStr(v.managedTag),
cacheVolumeKey: nullStr(v.cacheVolumeKey),
settings: { policy: policyBody(v) },
isDefault: v.isDefault === true,
enabled: v.enabled === true,
}),