feat(downloads): 多下载器路由:默认下载器、站点指定下载器、别名

照 MP(chain/download/submission.py:明确指定 or 站点下载器 or 默认):
- 迁移 0063:downloader_instance.is_default(至多一个,唯一索引)、site.downloader_instance_id
- pickInstance:明确指定 > 站点指定 > 默认 > 第一个可用;自动下载(gates)与手动下载共用
- 设置:下载器名称改称别名,新增「设为默认下载器」;站点编辑新增「下载器」选择
- 测试 downloader-routing.test.ts

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-02 20:32:29 +08:00
1 parent 64e1dfb1d2
commit 135c50048f
12 files changed
+196 -11

No files matched your search

@@ -3,6 +3,7 @@ import { CircleCheck, CircleX, KeyRound, LoaderCircle, TriangleAlert } from "luc
import { useState } from "react";
import { Link } from "react-router";
import { toast } from "sonner";
import { downloaders } from "~/api/instances.ts";
import {
useDeleteSite,
useHealthCheck,
@@ -25,6 +26,7 @@ import {
DialogTitle,
} from "~/components/ui/dialog.tsx";
import { Input } from "~/components/ui/input.tsx";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select.tsx";
import { Switch } from "~/components/ui/switch.tsx";
import { errorMessage } from "~/lib/api.ts";
import { cn } from "~/lib/utils.ts";
@@ -323,8 +325,11 @@ export function CredentialDialog({
}
/** 编辑抽屉:名称、域名、限流(空 = 站点默认)、定时快照、代理 */
const DEFAULT_DOWNLOADER = "__default__";
export function SiteEditSheet({ site, onClose }: { site: SiteListItem; onClose: () => void }) {
const update = useUpdateSite();
const dl = downloaders.useList();
const [v, setV] = useState({
name: site.name,
baseUrl: site.baseUrl,
@@ -334,6 +339,7 @@ export function SiteEditSheet({ site, onClose }: { site: SiteListItem; onClose:
intervalMin: site.snapshotIntervalSec ? String(Math.round(site.snapshotIntervalSec / 60)) : "",
proxy: site.proxy,
searchDefault: site.searchDefault !== false,
downloader: site.downloaderInstanceId ?? DEFAULT_DOWNLOADER,
});
const [errors, setErrors] = useState<Record<string, string>>({});
const set = <K extends keyof typeof v>(k: K, val: (typeof v)[K]) => setV((s) => ({ ...s, [k]: val }));
@@ -376,6 +382,7 @@ export function SiteEditSheet({ site, onClose }: { site: SiteListItem; onClose:
snapshotIntervalSec: interval ? interval * 60 : null,
proxy: v.proxy,
searchDefault: v.searchDefault,
downloaderInstanceId: v.downloader === DEFAULT_DOWNLOADER ? null : v.downloader,
},
});
toast.success(`${v.name.trim()} 已保存`);
@@ -458,6 +465,27 @@ export function SiteEditSheet({ site, onClose }: { site: SiteListItem; onClose:
/>
</FormField>
) : null}
<FormField
label="下载器"
htmlFor="se-downloader"
hint="这个站的资源用哪个下载器;订阅或手动下载时明确指定的下载器优先"
>
<Select value={v.downloader} onValueChange={(x) => set("downloader", x)} disabled={busy}>
<SelectTrigger id="se-downloader" className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent position="popper">
<SelectItem value={DEFAULT_DOWNLOADER}>默认下载器</SelectItem>
{(dl.data?.items ?? []).map((d) => (
<SelectItem key={d.id} value={d.id}>
{d.name}
{d.isDefault ? "(默认)" : ""}
{d.enabled ? "" : "(已停用)"}
</SelectItem>
))}
</SelectContent>
</Select>
</FormField>
<div className="mb-2 flex items-start gap-3">
<Switch id="se-proxy" checked={v.proxy} onCheckedChange={(x) => set("proxy", x)} disabled={busy} />
<label htmlFor="se-proxy" className="text-caption">