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:
1 parent
64e1dfb1d2
commit
135c50048f
12 files changed
+196
-11
No files matched your search
@@ -102,16 +102,21 @@ function DownloadersTable() {
|
||||
config={{
|
||||
noun: "下载器实例",
|
||||
description:
|
||||
"只接管带管理标签的任务。多个实例共用一块缓存盘时填相同的缓存卷标识,水位合并计算。点行编辑实例和路径映射。",
|
||||
"只接管带管理标签的任务。可添加多个下载器:订阅或手动下载指定了下载器就用指定的,否则用站点指定的,再否则用默认下载器。多个实例共用一块缓存盘时填相同的缓存卷标识,水位合并计算。点行编辑实例和路径映射。",
|
||||
emptyHint: "添加 qBittorrent 后才能提交下载。",
|
||||
drawerExtra: (r) => <PathMappingsPanel viewKind="downloader" instanceId={r.id} />,
|
||||
columns: [
|
||||
{
|
||||
key: "name",
|
||||
header: "名称",
|
||||
header: "别名",
|
||||
cell: (r) => (
|
||||
<div className="min-w-0">
|
||||
<p className="font-medium">{r.name}</p>
|
||||
<p className="font-medium">
|
||||
{r.name}
|
||||
{r.isDefault ? (
|
||||
<span className="ml-1.5 font-normal text-meta text-muted-foreground">默认</span>
|
||||
) : null}
|
||||
</p>
|
||||
<p className="truncate text-meta text-muted-foreground">{r.baseUrl}</p>
|
||||
</div>
|
||||
),
|
||||
@@ -128,7 +133,7 @@ function DownloadersTable() {
|
||||
],
|
||||
rowActions: (r) => <TestButton pending={test.isPending} run={() => test.mutateAsync(r.id)} />,
|
||||
fields: [
|
||||
{ name: "name", label: "名称", kind: "text", required: true, placeholder: "例如:家里的 qB" },
|
||||
{ name: "name", label: "别名", kind: "text", required: true, placeholder: "例如:家里的 qB、番剧专用" },
|
||||
{
|
||||
name: "baseUrl",
|
||||
label: "WebUI 地址",
|
||||
@@ -161,6 +166,12 @@ function DownloadersTable() {
|
||||
},
|
||||
{ name: "managedTag", label: "管理标签", kind: "text", placeholder: "umt", hint: "只接管带此标签的任务" },
|
||||
{ name: "cacheVolumeKey", label: "缓存卷标识", kind: "text", placeholder: "cache-1" },
|
||||
{
|
||||
name: "isDefault",
|
||||
label: "设为默认下载器",
|
||||
kind: "switch",
|
||||
hint: "订阅、手动下载和站点都没指定下载器时用它;只能有一个默认",
|
||||
},
|
||||
{ name: "enabled", label: "启用", kind: "switch" },
|
||||
],
|
||||
initial: {
|
||||
@@ -172,6 +183,7 @@ function DownloadersTable() {
|
||||
apiKey: "",
|
||||
managedTag: "umt",
|
||||
cacheVolumeKey: "",
|
||||
isDefault: false,
|
||||
enabled: true,
|
||||
},
|
||||
fromRow: (r) => ({
|
||||
@@ -185,6 +197,7 @@ function DownloadersTable() {
|
||||
"secret:authKind": authKindOf(r),
|
||||
managedTag: r.managedTag,
|
||||
cacheVolumeKey: r.cacheVolumeKey ?? "",
|
||||
isDefault: r.isDefault,
|
||||
enabled: r.enabled,
|
||||
}),
|
||||
toBody: (v: FormValues, mode) => ({
|
||||
@@ -201,6 +214,7 @@ function DownloadersTable() {
|
||||
: undefined,
|
||||
managedTag: optStr(v.managedTag),
|
||||
cacheVolumeKey: nullStr(v.cacheVolumeKey),
|
||||
isDefault: v.isDefault === true,
|
||||
enabled: v.enabled === true,
|
||||
}),
|
||||
createSchema: DownloaderInstanceCreate,
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in new issue
Block a user