feat(download): 下载器可设「H&R 未知也可清理」(M2)
下载器策略加 hnrUnknownCleanable(缺省关,保持现状):保种天数到期、站点 H&R 要求未知 (M-Team、接管任务、站点没配 H&R)的任务视为可清理;H&R 明确未满足的不受影响。 空间不足事项里写明有多少任务因 H&R 未知没清理、可在哪里放行。 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
1 parent
3d3a4cefc6
commit
07d5f42143
3 files changed
+30
-8
No files matched your search
@@ -1272,8 +1272,9 @@ export async function cleanupCandidates(d: AoDeps, v: VolumeState): Promise<Clea
|
|||||||
const site = rel
|
const site = rel
|
||||||
? await db.selectFrom("site").select("config").where("id", "=", rel.site_id).executeTakeFirst()
|
? await db.selectFrom("site").select("config").where("id", "=", rel.site_id).executeTakeFirst()
|
||||||
: null;
|
: null;
|
||||||
const days = instancePolicy(topo.inst).seedRetentionDays ?? retention.seedRetentionDays;
|
const policy = instancePolicy(topo.inst);
|
||||||
const lock = await lockReason(d, row, rel ?? null, site ?? null, days, now);
|
const days = policy.seedRetentionDays ?? retention.seedRetentionDays;
|
||||||
|
const lock = await lockReason(d, row, rel ?? null, site ?? null, days, now, policy.hnrUnknownCleanable === true);
|
||||||
out.push({ row, topo, bytes: row.size_bytes ?? 0, cleanable: lock === null, lockReason: lock });
|
out.push({ row, topo, bytes: row.size_bytes ?? 0, cleanable: lock === null, lockReason: lock });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1287,10 +1288,13 @@ async function lockReason(
|
|||||||
site: { config: unknown } | null,
|
site: { config: unknown } | null,
|
||||||
days: number,
|
days: number,
|
||||||
now: Date,
|
now: Date,
|
||||||
|
/** 下载器设置「H&R 未知也可清理」:保种天数到期后,H&R 要求未知按已满足处理 */
|
||||||
|
hnrUnknownCleanable = false,
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
if (!retentionElapsed(row, days, now)) return "retention_days";
|
if (!retentionElapsed(row, days, now)) return "retention_days";
|
||||||
const hnr = hnrSatisfied(row, rel as never, site as never);
|
const hnr = hnrSatisfied(row, rel as never, site as never);
|
||||||
if (hnr !== true) return hnr === false ? "hnr_unsatisfied" : "hnr_unknown";
|
if (hnr === false) return "hnr_unsatisfied";
|
||||||
|
if (hnr === "unknown" && !hnrUnknownCleanable) return "hnr_unknown";
|
||||||
// 已验证入库:最近一次整理记录成功,且没有失败/排队的文件(部分入库不能整种清理)
|
// 已验证入库:最近一次整理记录成功,且没有失败/排队的文件(部分入库不能整种清理)
|
||||||
const rec = await d.ctx.db
|
const rec = await d.ctx.db
|
||||||
.selectFrom("organization_record")
|
.selectFrom("organization_record")
|
||||||
@@ -1385,11 +1389,16 @@ export async function cleanupDownloadCache(
|
|||||||
if (pct > th.cleanupTargetPercent && pct >= th.hardThresholdPercent) {
|
if (pct > th.cleanupTargetPercent && pct >= th.hardThresholdPercent) {
|
||||||
entry.action = entry.removed.length > 0 ? "cleaned" : "insufficient";
|
entry.action = entry.removed.length > 0 ? "cleaned" : "insufficient";
|
||||||
// 没有可清理对象:待处理 + 阈值继续阻止新下载(不突破保留要求)
|
// 没有可清理对象:待处理 + 阈值继续阻止新下载(不突破保留要求)
|
||||||
|
const hnrUnknown = entry.skipped.filter((x) => x.reason === "hnr_unknown").length;
|
||||||
await upsertDecision(d, db, {
|
await upsertDecision(d, db, {
|
||||||
kind: "space_insufficient",
|
kind: "space_insufficient",
|
||||||
severity: "critical",
|
severity: "critical",
|
||||||
title: `下载缓存卷 ${v.volumeKey} 空间不足`,
|
title: `下载缓存卷 ${v.volumeKey} 空间不足`,
|
||||||
summary: `使用率 ${pct.toFixed(1)}%,可清理任务已用尽;新下载已暂停`,
|
summary:
|
||||||
|
`使用率 ${pct.toFixed(1)}%,可清理任务已用尽;新下载已暂停` +
|
||||||
|
(hnrUnknown
|
||||||
|
? `。另有 ${hnrUnknown} 个任务因为 H&R 要求未知没有清理,确认没有 H&R 要求的话,可在下载器设置里打开「H&R 未知也可清理」`
|
||||||
|
: ""),
|
||||||
subjectType: "cache_volume",
|
subjectType: "cache_volume",
|
||||||
dedupeKey: `space_insufficient:${v.volumeKey}`,
|
dedupeKey: `space_insufficient:${v.volumeKey}`,
|
||||||
evidence: { usedPercent: pct, locked: entry.skipped },
|
evidence: { usedPercent: pct, locked: entry.skipped },
|
||||||
|
|||||||
@@ -97,16 +97,20 @@ const POLICY_FIELDS = [
|
|||||||
["targetPct", "cleanupTargetPercent"],
|
["targetPct", "cleanupTargetPercent"],
|
||||||
["seedDays", "seedRetentionDays"],
|
["seedDays", "seedRetentionDays"],
|
||||||
] as const;
|
] as const;
|
||||||
function policyForm(r: DownloaderInstance): Record<string, string> {
|
function policyForm(r: DownloaderInstance): Record<string, string | boolean> {
|
||||||
const p = ((r.settings as { policy?: Record<string, unknown> }).policy ?? {}) as Record<string, unknown>;
|
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]) : ""]));
|
return {
|
||||||
|
...Object.fromEntries(POLICY_FIELDS.map(([f, k]) => [f, typeof p[k] === "number" ? String(p[k]) : ""])),
|
||||||
|
hnrUnknownCleanable: p.hnrUnknownCleanable === true,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
function policyBody(v: FormValues): Record<string, number> {
|
function policyBody(v: FormValues): Record<string, number | boolean> {
|
||||||
const out: Record<string, number> = {};
|
const out: Record<string, number | boolean> = {};
|
||||||
for (const [f, k] of POLICY_FIELDS) {
|
for (const [f, k] of POLICY_FIELDS) {
|
||||||
const raw = String(v[f] ?? "").trim();
|
const raw = String(v[f] ?? "").trim();
|
||||||
if (raw !== "" && Number.isFinite(Number(raw))) out[k] = Number(raw);
|
if (raw !== "" && Number.isFinite(Number(raw))) out[k] = Number(raw);
|
||||||
}
|
}
|
||||||
|
if (v.hnrUnknownCleanable === true) out.hnrUnknownCleanable = true;
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,6 +206,12 @@ function DownloadersTable() {
|
|||||||
placeholder: "留空用全局",
|
placeholder: "留空用全局",
|
||||||
hint: "入库后至少保留几天再允许清理;公网 BT 可填 0(站点 H&R 要求仍然遵守)",
|
hint: "入库后至少保留几天再允许清理;公网 BT 可填 0(站点 H&R 要求仍然遵守)",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "hnrUnknownCleanable",
|
||||||
|
label: "H&R 未知也可清理",
|
||||||
|
kind: "switch",
|
||||||
|
hint: "保种天数到期后,没给出 H&R 要求的任务(如 M-Team、接管的任务、站点没配 H&R)也允许清理;默认不清理",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "isDefault",
|
name: "isDefault",
|
||||||
label: "设为默认下载器",
|
label: "设为默认下载器",
|
||||||
@@ -223,6 +233,7 @@ function DownloadersTable() {
|
|||||||
resumePct: "",
|
resumePct: "",
|
||||||
targetPct: "",
|
targetPct: "",
|
||||||
seedDays: "",
|
seedDays: "",
|
||||||
|
hnrUnknownCleanable: false,
|
||||||
isDefault: false,
|
isDefault: false,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ export const DownloaderPolicy = z
|
|||||||
resumePercent: z.number().min(1).max(100).optional(),
|
resumePercent: z.number().min(1).max(100).optional(),
|
||||||
cleanupTargetPercent: z.number().min(1).max(100).optional(),
|
cleanupTargetPercent: z.number().min(1).max(100).optional(),
|
||||||
seedRetentionDays: z.number().int().min(0).max(365).optional(),
|
seedRetentionDays: z.number().int().min(0).max(365).optional(),
|
||||||
|
/** 保种天数到期、但站点 H&R 要求未知(M-Team、接管任务、站点没配 H&R)时也允许清理;缺省不允许 */
|
||||||
|
hnrUnknownCleanable: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
export type DownloaderPolicy = z.infer<typeof DownloaderPolicy>;
|
export type DownloaderPolicy = z.infer<typeof DownloaderPolicy>;
|
||||||
|
|||||||
Reference in new issue
Block a user