feat(worker): IO Worker 分道(WK-1)

io worker 进程内按任务声明的 lane 分文件 / 网络 / 维护三条道,各自并发(缺省 2/4/1)、
各自排队;维护道在文件道或网络道有排队/运行时让路(连续 10 分钟放行一个),调度器内联的
清理/回填/保留期裁剪也走维护道。并发在 设置 → 自动化 → 高级 热生效,env 覆盖;旧
WORKER_CONCURRENCY 映射到网络道。运行状态页按道显示运行中/并发/排队。归属表写进
docs/runbook/dev.md,ci 与注册表逐项核对。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-30 00:24:21 +08:00
1 parent 68f53390f6
commit da33cad81d
38 files changed
+1240 -105

No files matched your search

@@ -10,7 +10,7 @@ import { type FormValues, num } from "./resource-section.tsx";
import { anchorId, SettingsBlock } from "./settings-block.tsx";
import { type CardSpec, n, SettingsCard, SettingsGate } from "./settings-card.tsx";
// 自动化分区:总开关、只读对账、缓存水位、每日下载预算、保留天数、存储预算与保留(CACHE-2)
// 自动化分区:总开关、只读对账、缓存水位、每日下载预算、保留天数、后台任务并发(WK-1)、存储预算与保留(CACHE-2)
const GiB = 1024 ** 3;
@@ -106,6 +106,33 @@ const dayField = (group: "retentionDays" | "cacheTtl", k: string, label: string,
const pick = (v: FormValues, prefix: string, keys: readonly string[]) =>
Object.fromEntries(keys.map((k) => [k, num(v[`${prefix}.${k}`])]));
// WK-1:io worker 三条道的并发(settings.automation.workerLanes,docs/runbook/dev.md「IO Worker 分道」)
const LANE_KEYS = ["file", "network", "maintenance"] as const;
const WORKER_LANES_CARD: CardSpec = {
anchor: "worker-lanes",
title: "后台任务并发",
description:
"后台任务分三条道各自排队,互不占用。保存后几秒内生效,不用重启;服务器上设置了对应的环境变量时以环境变量为准。",
fields: [
{ name: "automation.workerLanes.file", label: "文件道", kind: "number", hint: "整理、硬链接、移动、校验;1–16" },
{
name: "automation.workerLanes.network",
label: "网络道",
kind: "number",
hint: "站点、RSS、TMDB、下载器、搜索;单站限流另计;1–16",
},
{
name: "automation.workerLanes.maintenance",
label: "维护道",
kind: "number",
hint: "清理、回填、对账;前两道有活时让路;1–16",
},
],
fromSettings: (s) =>
Object.fromEntries(LANE_KEYS.map((k) => [`automation.workerLanes.${k}`, n(s.automation.workerLanes[k])])),
toPatch: (v) => ({ automation: { workerLanes: pick(v, "automation.workerLanes", LANE_KEYS) } }),
};
const BUDGET_KEYS = ["pgDataGb", "pgDataWarnGb", "imagesGb", "backupsGb", "sessionsEvidenceGb"] as const;
const STORAGE_BUDGET_CARD: CardSpec = {
anchor: "storage",
@@ -212,7 +239,7 @@ function ToggleRow({
);
}
/** 高级:只读对账、存储预算、流水保留、缓存有效期;默认收起,从 #automation/advanced 进来时展开 */
/** 高级:只读对账、后台任务并发、存储预算、流水保留、缓存有效期;默认收起,从 #automation/advanced 进来时展开 */
function Advanced({ children }: { children: ReactNode }) {
const location = useLocation();
const [open, setOpen] = useState(() => location.hash.endsWith("/advanced"));
@@ -232,7 +259,9 @@ function Advanced({ children }: { children: ReactNode }) {
className={cn("size-4 text-muted-foreground transition-transform duration-base", open && "rotate-180")}
/>
</button>
<p className="mt-1 text-caption text-muted-foreground">只读对账、存储预算、数据保留和缓存有效期。一般不用改。</p>
<p className="mt-1 text-caption text-muted-foreground">
只读对账、后台任务并发、存储预算、数据保留和缓存有效期。一般不用改。
</p>
{open ? <div className="mt-6 grid grid-cols-1 gap-8">{children}</div> : null}
</section>
);
@@ -309,6 +338,7 @@ export function AutomationSection() {
<SettingsCard section="automation" spec={RETENTION_CARD} settings={s} />
<Advanced>
<ReadOnlySwitch />
<SettingsCard section="automation" spec={WORKER_LANES_CARD} settings={s} />
<SettingsCard section="automation" spec={STORAGE_BUDGET_CARD} settings={s} />
<SettingsCard section="automation" spec={STORAGE_RETENTION_CARD} settings={s} />
<SettingsCard section="automation" spec={STORAGE_TTL_CARD} settings={s} />